# Copyright 2017 VMware, Inc.
# All Rights Reserved
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

from oslo_config import cfg
from oslo_log import log

from vmware_nsx.common import availability_zones as common_az
from vmware_nsx.common import config
from vmware_nsx.common import exceptions as nsx_exc
from vmware_nsx.plugins.common_v3 import availability_zones as v3_az
from vmware_nsx.plugins.nsx_p import utils
from vmware_nsxlib.v3 import exceptions as nsx_lib_exc
from vmware_nsxlib.v3 import nsx_constants
from vmware_nsxlib.v3.policy import utils as p_utils

LOG = log.getLogger(__name__)

DEFAULT_NAME = common_az.DEFAULT_NAME + 'p'


class NsxPAvailabilityZone(v3_az.NsxV3AvailabilityZone):

    def get_az_opts(self):
        return config.get_nsxp_az_opts(self.name)

    def init_defaults(self):
        # use the default configuration
        self.metadata_proxy = cfg.CONF.nsx_p.metadata_proxy
        self.dhcp_profile = cfg.CONF.nsx_p.dhcp_profile
        self.native_metadata_route = cfg.CONF.nsx_p.native_metadata_route
        self.windows_metadata_route = cfg.CONF.nsx_p.windows_metadata_route
        self.default_overlay_tz = cfg.CONF.nsx_p.default_overlay_tz
        self.default_vlan_tz = cfg.CONF.nsx_p.default_vlan_tz
        self.default_tier0_router = cfg.CONF.nsx_p.default_tier0_router
        self.dns_domain = cfg.CONF.nsx_p.dns_domain
        self.nameservers = cfg.CONF.nsx_p.nameservers
        self.edge_cluster = cfg.CONF.nsx_p.edge_cluster

    def _init_default_resource(self, nsxpolicy, resource_api, config_name,
                               filter_list_results=None,
                               auto_config=False,
                               is_mandatory=True,
                               search_scope=None):
        # NOTE(annak): we may need to generalize this for API calls
        # requiring path ids
        name_or_id = getattr(self, config_name)
        err_msg = (_("Could not find %(res)s %(id)s for availability "
                     "zone %(az)s") % {
                'res': config_name,
                'id': name_or_id,
                'az': self.name})

        if not name_or_id:
            if auto_config:
                # If the field not specified, the system will auto-configure
                # in case only single resource is present
                resources = resource_api.list()
                if filter_list_results:
                    resources = filter_list_results(resources)
                if len(resources) == 1:
                    return resources[0]['id']

            if is_mandatory:
                if self.is_default():
                    raise cfg.RequiredOptError(config_name,
                                               group=cfg.OptGroup('nsx_p'))
                msg = (_("No %(res)s provided for availability "
                         "zone %(az)s") % {
                    'res': config_name,
                    'az': self.name})
                raise nsx_exc.NsxPluginException(err_msg=msg)
            return None

        # If filtering was specified, we need to ensure the configured
        # resource matches the filter
        def verify_resource_matches_filter(result):
            if filter_list_results:
                exists = filter_list_results([result])
                if not exists:
                    LOG.error("Resource %s doesn't match config "
                              "requirement for %s" % (name_or_id, config_name))
                    if self.is_default():
                        raise cfg.RequiredOptError(config_name,
                                                   group=cfg.OptGroup('nsx_p'))
                    raise nsx_exc.NsxPluginException(err_msg=err_msg)
        try:
            # Check if the configured value is the ID
            resource = resource_api.get(name_or_id, silent=True)
            verify_resource_matches_filter(resource)

            return name_or_id
        except nsx_lib_exc.ResourceNotFound:
            # Search by tags
            if search_scope:
                resource_type = resource_api.entry_def.resource_type()
                resource_id = nsxpolicy.get_id_by_resource_and_tag(
                    resource_type,
                    search_scope,
                    name_or_id)
                if resource_id:
                    return resource_id

            # Check if the configured value is the name
            resource = resource_api.get_by_name(name_or_id)
            if resource:
                verify_resource_matches_filter(resource)
                return resource['id']

            # Resource not found
            if self.is_default():
                raise cfg.RequiredOptError(config_name,
                                           group=cfg.OptGroup('nsx_p'))
            raise nsx_exc.NsxPluginException(err_msg=err_msg)

    def translate_configured_names_to_uuids(self, nsxpolicy, nsxlib=None,
                                            search_scope=None):
        super(NsxPAvailabilityZone, self).translate_configured_names_to_uuids(
            nsxpolicy)

        self._default_overlay_tz_uuid = self._init_default_resource(
            nsxpolicy, nsxpolicy.transport_zone, 'default_overlay_tz',
            auto_config=True, is_mandatory=True,
            filter_list_results=lambda tzs: [
                tz for tz in tzs if tz['tz_type'].startswith('OVERLAY')],
            search_scope=search_scope)

        self._default_vlan_tz_uuid = self._init_default_resource(
            nsxpolicy, nsxpolicy.transport_zone, 'default_vlan_tz',
            auto_config=True, is_mandatory=False,
            filter_list_results=lambda tzs: [
                tz for tz in tzs if tz['tz_type'].startswith('VLAN')],
            search_scope=search_scope)

        self._default_tier0_router = self._init_default_resource(
            nsxpolicy, nsxpolicy.tier0, 'default_tier0_router',
            auto_config=True, is_mandatory=True,
            search_scope=search_scope)

        self._edge_cluster_uuid = self._init_default_resource(
            nsxpolicy, nsxpolicy.edge_cluster, 'edge_cluster',
            auto_config=False, is_mandatory=False,
            search_scope=search_scope)

        # Init dhcp config from policy or MP
        self.use_policy_dhcp = False
        if (nsxpolicy.feature_supported(
                nsx_constants.FEATURE_NSX_POLICY_DHCP)):
            try:
                self._policy_dhcp_server_config = self._init_default_resource(
                    nsxpolicy, nsxpolicy.dhcp_server_config, 'dhcp_profile',
                    auto_config=False, is_mandatory=False,
                    search_scope=search_scope)
                if self._policy_dhcp_server_config:
                    self.use_policy_dhcp = True
            except Exception:
                # Not found. try as MP profile
                pass
        self._native_dhcp_profile_uuid = None
        if not self.use_policy_dhcp and nsxlib:
            self._translate_dhcp_profile(nsxlib, search_scope=search_scope)

        self.use_policy_md = False
        if (nsxpolicy.feature_supported(
                nsx_constants.FEATURE_NSX_POLICY_MDPROXY)):
            # Try to initialize md-proxy from the policy
            try:
                self._native_md_proxy_uuid = self._init_default_resource(
                    nsxpolicy, nsxpolicy.md_proxy, 'metadata_proxy',
                    auto_config=True, is_mandatory=True,
                    search_scope=search_scope)
                LOG.info("NSX-P az using policy MD proxy: %s",
                    self._native_md_proxy_uuid)
                self.use_policy_md = True
            except Exception:
                LOG.info("NSX-P az could not use policy MD proxy. Using MP "
                         "one instead")

        if not self.use_policy_md:
            # Try to initialize md-proxy from the MP
            if nsxlib:
                self._translate_metadata_proxy(
                    nsxlib, search_scope=search_scope)
                LOG.info("NSX-P az using MP MD proxy: %s",
                    self._native_md_proxy_uuid)
            else:
                self._native_md_proxy_uuid = None

    def _validate_tz(self, nsxpolicy, nsxlib, obj_type, obj_id, ec_uuid):
        try:
            obj_tzs = utils.get_edge_cluster_tzs(nsxpolicy, nsxlib, ec_uuid)
        except nsx_lib_exc.ResourceNotFound as e:
            # Do not fail plugin init if this code fails
            LOG.warning("Failed to get edge cluster %s transport zones: %s",
                        ec_uuid, e)
            return

        if self._default_overlay_tz_uuid not in obj_tzs:
            msg = (_("%(type)s %(id)s of availability zone %(az)s with edge "
                     "cluster %(ec)s does not match the default overlay tz "
                     "%(tz)s") % {
                'type': obj_type,
                'id': obj_id,
                'ec': ec_uuid,
                'tz': self._default_overlay_tz_uuid,
                'az': self.name})
            raise nsx_exc.NsxPluginException(err_msg=msg)

        if (self._default_vlan_tz_uuid and
            self._default_vlan_tz_uuid not in obj_tzs):
            msg = (_("%(type)s %(id)s of availability zone %(az)s with edge "
                     "cluster %(ec)s does not match the default vlan tz "
                     "%(tz)s") % {
                'type': obj_type,
                'id': obj_id,
                'ec': ec_uuid,
                'tz': self._default_vlan_tz_uuid,
                'az': self.name})
            raise nsx_exc.NsxPluginException(err_msg=msg)

    def validate_availability_zone(self, nsxpolicy, nsxlib=None):
        """Validate that all the components of this AZ are connected"""

        if not nsxlib:
            LOG.warning("Cannot validate availability zone %s without "
                        "passthrough api", self.name)
            return

        # Validate tier0 TZ match the default ones
        tier0_ec_path = nsxpolicy.tier0.get_edge_cluster_path(
            self._default_tier0_router)
        if not tier0_ec_path:
            msg = (_("Tier0 %(id)s of availability zone %(az)s does not have "
                     "an edge cluster") % {
                'id': self._default_tier0_router,
                'az': self.name})
            raise nsx_exc.NsxPluginException(err_msg=msg)
        tier0_ec_uuid = p_utils.path_to_id(tier0_ec_path)
        self._validate_tz(nsxpolicy, nsxlib, 'Tier0',
                          self._default_tier0_router,
                          tier0_ec_uuid)

        if self.use_policy_dhcp:
            dhcp_ec_path = nsxpolicy.dhcp_server_config.get(
                self._policy_dhcp_server_config).get('edge_cluster_path')
            if dhcp_ec_path:
                dhcp_ec = p_utils.path_to_id(dhcp_ec_path)
                if dhcp_ec != tier0_ec_uuid:
                    self._validate_tz(nsxpolicy, nsxlib, 'DHCP server config',
                                      self._policy_dhcp_server_config,
                                      dhcp_ec)
        elif self._native_dhcp_profile_uuid:
            dhcp_ec = nsxlib.native_dhcp_profile.get(
                self._native_dhcp_profile_uuid).get('edge_cluster_id')
            if dhcp_ec != tier0_ec_uuid:
                self._validate_tz(nsxpolicy, nsxlib, 'DHCP profile',
                                  self._native_dhcp_profile_uuid,
                                  dhcp_ec)

        if self._native_md_proxy_uuid:
            # Validate that the edge cluster of the MD proxy (MP or policy one)
            # match the configured TZs
            if self.use_policy_md:
                md_ec_path = nsxpolicy.md_proxy.get(
                    self._native_md_proxy_uuid).get('edge_cluster_path')
                md_ec = p_utils.path_to_id(md_ec_path)
            else:
                md_ec = nsxlib.native_md_proxy.get(
                    self._native_md_proxy_uuid).get('edge_cluster_id')
            if md_ec != tier0_ec_uuid:
                self._validate_tz(nsxpolicy, nsxlib, 'MD Proxy',
                                  self._native_md_proxy_uuid,
                                  md_ec)


class NsxPAvailabilityZones(common_az.ConfiguredAvailabilityZones):

    default_name = DEFAULT_NAME

    def __init__(self):
        default_azs = cfg.CONF.default_availability_zones
        super(NsxPAvailabilityZones, self).__init__(
            cfg.CONF.nsx_p.availability_zones,
            NsxPAvailabilityZone,
            default_availability_zones=default_azs)
        self.non_default_dns_domain = self.dns_domain_configured_non_default()

    def dns_domain_configured_non_default(self):
        for az in self.availability_zones.values():
            if az.dns_domain and az.dns_domain != cfg.CONF.nsx_p.dns_domain:
                return True
        return False
