# Copyright 2018 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. import ipaddress import decorator import mock import netaddr from oslo_config import cfg from oslo_db import exception as db_exc from oslo_log import log as logging import oslo_messaging from oslo_utils import excutils from sqlalchemy import exc as sql_exc import webob.exc from six import moves from neutron.db import agents_db from neutron.db import agentschedulers_db from neutron.db import allowedaddresspairs_db as addr_pair_db from neutron.db.availability_zone import router as router_az_db from neutron.db import dns_db from neutron.db import external_net_db from neutron.db import extradhcpopt_db from neutron.db import extraroute_db from neutron.db import l3_attrs_db from neutron.db import l3_db from neutron.db import l3_gwmode_db from neutron.db.models import securitygroup as securitygroup_model from neutron.db import models_v2 from neutron.db import portbindings_db from neutron.db import portsecurity_db from neutron.db.quota import driver # noqa from neutron.db import securitygroups_db from neutron.db import vlantransparent_db from neutron.extensions import securitygroup as ext_sg from neutron.extensions import tagging from neutron_lib.agent import topics from neutron_lib.api.definitions import allowedaddresspairs as addr_apidef from neutron_lib.api.definitions import availability_zone as az_def from neutron_lib.api.definitions import external_net as extnet_apidef from neutron_lib.api.definitions import extra_dhcp_opt as ext_edo from neutron_lib.api.definitions import l3 as l3_apidef from neutron_lib.api.definitions import port_security as psec from neutron_lib.api.definitions import portbindings as pbin from neutron_lib.api.definitions import provider_net as pnet from neutron_lib.api import faults from neutron_lib.api import validators from neutron_lib.api.validators import availability_zone as az_validator from neutron_lib import constants from neutron_lib.db import api as db_api from neutron_lib.db import utils as db_utils from neutron_lib import exceptions as n_exc from neutron_lib.exceptions import allowedaddresspairs as addr_exc from neutron_lib.exceptions import l3 as l3_exc from neutron_lib.exceptions import port_security as psec_exc from neutron_lib.plugins import directory from neutron_lib.plugins import utils as plugin_utils from neutron_lib import rpc as n_rpc from neutron_lib.services.qos import constants as qos_consts from neutron_lib.utils import helpers from neutron_lib.utils import net as nl_net_utils from vmware_nsx.api_replay import utils as api_replay_utils from vmware_nsx.common import availability_zones as nsx_com_az from vmware_nsx.common import exceptions as nsx_exc from vmware_nsx.common import locking from vmware_nsx.common import nsx_constants from vmware_nsx.common import utils from vmware_nsx.db import db as nsx_db from vmware_nsx.db import extended_security_group as extended_sec from vmware_nsx.db import extended_security_group_rule as extend_sg_rule from vmware_nsx.db import maclearning as mac_db from vmware_nsx.db import nsx_portbindings_db as pbin_db from vmware_nsx.extensions import advancedserviceproviders as as_providers from vmware_nsx.extensions import maclearning as mac_ext from vmware_nsx.extensions import providersecuritygroup as provider_sg from vmware_nsx.plugins.common import plugin from vmware_nsx.plugins.common_v3 import utils as common_utils from vmware_nsx.services.qos.common import utils as qos_com_utils from vmware_nsx.services.vpnaas.common_v3 import ipsec_utils from vmware_nsxlib.v3 import exceptions as nsx_lib_exc from vmware_nsxlib.v3 import nsx_constants as nsxlib_consts from vmware_nsxlib.v3 import utils as nsxlib_utils LOG = logging.getLogger(__name__) @decorator.decorator def api_replay_mode_wrapper(f, *args, **kwargs): if cfg.CONF.api_replay_mode: # NOTE(arosen): the mock.patch here is needed for api_replay_mode with mock.patch("neutron_lib.plugins.utils._fixup_res_dict", side_effect=api_replay_utils._fixup_res_dict): return f(*args, **kwargs) else: return f(*args, **kwargs) # NOTE(asarfaty): the order of inheritance here is important. in order for the # QoS notification to work, the AgentScheduler init must be called first # NOTE(arosen): same is true with the ExtendedSecurityGroupPropertiesMixin # this needs to be above securitygroups_db.SecurityGroupDbMixin. # FIXME(arosen): we can solve this inheritance order issue by just mixining in # the classes into a new class to handle the order correctly. class NsxPluginV3Base(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, addr_pair_db.AllowedAddressPairsMixin, plugin.NsxPluginBase, extended_sec.ExtendedSecurityGroupPropertiesMixin, pbin_db.NsxPortBindingMixin, extend_sg_rule.ExtendedSecurityGroupRuleMixin, securitygroups_db.SecurityGroupDbMixin, external_net_db.External_net_db_mixin, extraroute_db.ExtraRoute_db_mixin, router_az_db.RouterAvailabilityZoneMixin, l3_gwmode_db.L3_NAT_db_mixin, portbindings_db.PortBindingMixin, portsecurity_db.PortSecurityDbMixin, extradhcpopt_db.ExtraDhcpOptMixin, dns_db.DNSDbMixin, vlantransparent_db.Vlantransparent_db_mixin, mac_db.MacLearningDbMixin, l3_attrs_db.ExtraAttributesMixin, nsx_com_az.NSXAvailabilityZonesPluginCommon): """Common methods for NSX-V3 plugins (NSX-V3 & Policy)""" def __init__(self): super(NsxPluginV3Base, self).__init__() self._network_vlans = plugin_utils.parse_network_vlan_ranges( self._get_conf_attr('network_vlan_ranges')) self._native_dhcp_enabled = False self.start_rpc_listeners_called = False def _init_native_dhcp(self): if not self.nsxlib: self._native_dhcp_enabled = False return self._native_dhcp_enabled = True for az in self.get_azs_list(): if not az._native_dhcp_profile_uuid: LOG.error("Unable to retrieve DHCP Profile %s for " "availability zone %s, " "native DHCP service is not supported", az.name, az.dhcp_profile) self._native_dhcp_enabled = False def _init_native_metadata(self): for az in self.get_azs_list(): if not az._native_md_proxy_uuid: LOG.error("Unable to retrieve Metadata Proxy %s for " "availability zone %s, " "native metadata service is not supported", az.name, az.metadata_proxy) def _extend_fault_map(self): """Extends the Neutron Fault Map. Exceptions specific to the NSX Plugin are mapped to standard HTTP Exceptions. """ faults.FAULT_MAP.update({nsx_lib_exc.InvalidInput: webob.exc.HTTPBadRequest, nsx_lib_exc.ServiceClusterUnavailable: webob.exc.HTTPServiceUnavailable, nsx_lib_exc.ClientCertificateNotTrusted: webob.exc.HTTPBadRequest, nsx_exc.SecurityGroupMaximumCapacityReached: webob.exc.HTTPBadRequest, nsx_lib_exc.NsxLibInvalidInput: webob.exc.HTTPBadRequest, nsx_exc.NsxENSPortSecurity: webob.exc.HTTPBadRequest, nsx_exc.NsxPluginTemporaryError: webob.exc.HTTPServiceUnavailable, nsx_lib_exc.ServiceUnavailable: webob.exc.HTTPServiceUnavailable, nsx_lib_exc.TooManyRequests: webob.exc.HTTPServiceUnavailable }) def _get_conf_attr(self, attr): plugin_cfg = getattr(cfg.CONF, self.cfg_group) return getattr(plugin_cfg, attr) def _setup_rpc(self): """Should be implemented by each plugin""" pass @property def support_external_port_tagging(self): # oslo_messaging_notifications must be defined for this to work if (cfg.CONF.oslo_messaging_notifications.driver and self._get_conf_attr('support_nsx_port_tagging')): return True return False def update_port_nsx_tags(self, context, port_id, tags, is_delete=False): """Can be implemented by each plugin to update the backend port tags""" pass def start_rpc_listeners(self): if self.start_rpc_listeners_called: # If called more than once - we should not create it again return self.conn.consume_in_threads() self._setup_rpc() self.topic = topics.PLUGIN self.conn = n_rpc.Connection() self.conn.create_consumer(self.topic, self.endpoints, fanout=False) self.conn.create_consumer(topics.REPORTS, [agents_db.AgentExtRpcCallback()], fanout=False) self.start_rpc_listeners_called = True if self.support_external_port_tagging: self.start_tagging_rpc_listener() return self.conn.consume_in_threads() def start_tagging_rpc_listener(self): # Add listener for tags plugin notifications transport = oslo_messaging.get_notification_transport(cfg.CONF) targets = [oslo_messaging.Target( topic=cfg.CONF.oslo_messaging_notifications.topics[0])] endpoints = [TagsCallbacks()] pool = "tags-listeners" server = oslo_messaging.get_notification_listener(transport, targets, endpoints, pool=pool) server.start() server.wait() def _translate_external_tag(self, external_tag, port_id): tag_parts = external_tag.split(':') if len(tag_parts) != 2: LOG.warning("Skipping tag %s for port %s: wrong format", external_tag, port_id) else: return {'scope': tag_parts[0][:nsxlib_utils.MAX_RESOURCE_TYPE_LEN], 'tag': tag_parts[1][:nsxlib_utils.MAX_TAG_LEN]} def _translate_external_tags(self, external_tags, port_id): new_tags = [] for tag in external_tags: new_tag = self._translate_external_tag(tag, port_id) if new_tag: new_tags.append(new_tag) return new_tags def get_external_tags_for_port(self, context, port_id): tags_plugin = directory.get_plugin(tagging.TAG_PLUGIN_TYPE) if tags_plugin: extra_tags = tags_plugin.get_tags(context, 'ports', port_id) return self._translate_external_tags(extra_tags['tags'], port_id) def _get_interface_subnet(self, context, interface_info): is_port, is_sub = self._validate_interface_info(interface_info) subnet_id = None if is_sub: subnet_id = interface_info.get('subnet_id') if not subnet_id: port_id = interface_info['port_id'] port = self.get_port(context, port_id) if 'fixed_ips' in port and port['fixed_ips']: if len(port['fixed_ips']) > 1: # This should never happen since router interface is per # IP version, and we allow single fixed ip per ip version return subnet_id = port['fixed_ips'][0]['subnet_id'] if subnet_id: return self.get_subnet(context, subnet_id) def _get_interface_network_id(self, context, interface_info, subnet=None): if subnet: return subnet['network_id'] is_port, is_sub = self._validate_interface_info(interface_info) if is_port: net_id = self.get_port(context, interface_info['port_id'])['network_id'] elif is_sub: net_id = self.get_subnet(context, interface_info['subnet_id'])['network_id'] return net_id def _validate_interface_address_scope(self, context, router_db, interface_subnet): gw_network_id = (router_db.gw_port.network_id if router_db.gw_port else None) if not router_db.enable_snat and gw_network_id: self._validate_address_scope_for_router_interface( context.elevated(), router_db.id, gw_network_id, interface_subnet['id'], subnet=interface_subnet) def _format_mac_address(self, mac): # The NSX backend does not support mac format MMM.MMM.SSS.SSS # Translate it to MM:MM:MM:SS:SS:SS instead if '.' in mac: fixed = mac.replace('.', '') if len(fixed) == 12: fixed = "%s:%s:%s:%s:%s:%s" % (fixed[0:2], fixed[2:4], fixed[4:6], fixed[6:8], fixed[8:10], fixed[10:12]) return fixed return mac def _support_address_pairs_ipv4_cidr(self): """Can be implemented by each plugin""" return False def _validate_address_pairs(self, address_pairs): for pair in address_pairs: ip = pair.get('ip_address') if ':' in ip: # IPv6 address pair ip_split = ip.split('/') if len(ip_split) > 1 and ip_split[1] != '128': # Validate ipv6 CIDR try: ipaddress.ip_network(ip) except ValueError: # This means the host bits are set err_msg = (_("Allowed address pairs Cidr %s cannot " "have host bits set") % ip) raise n_exc.InvalidInput(error_message=err_msg) else: # IPv4 address pair if len(ip.split('/')) > 1 and ip.split('/')[1] != '32': if self._support_address_pairs_ipv4_cidr(): # validate host bits try: ipaddress.ip_network(ip) except ValueError: # This means the host bits are set err_msg = (_("Allowed address pairs Cidr %s " "cannot have host bits set") % ip) raise n_exc.InvalidInput(error_message=err_msg) else: # IPv4 CIDR is not allowed LOG.error("Cidr %s is not supported in allowed " "address pairs", ip) raise nsx_exc.InvalidIPAddress(ip_address=ip) if ip in ['127.0.0.0', '0.0.0.0', '::']: LOG.error("IP %s is not supported in allowed address " "pairs", ip) raise nsx_exc.InvalidIPAddress(ip_address=ip) def _validate_number_of_address_pairs(self, port): address_pairs = port.get(addr_apidef.ADDRESS_PAIRS) if not address_pairs: return num_allowed_on_backend_v4 = nsxlib_consts.NUM_ALLOWED_IP_ADDRESSES_v4 num_allowed_on_backend_v6 = nsxlib_consts.NUM_ALLOWED_IP_ADDRESSES_v6 # Reserving 1 additional ipv4 fixed ip, and 2 for ipv6 # (including link local) max_addr_pairs_v4 = num_allowed_on_backend_v4 - 1 max_addr_pairs_v6 = num_allowed_on_backend_v6 - 2 count_v4 = count_v6 = 0 for pair in address_pairs: ip = pair.get('ip_address') if ':' in ip: count_v6 = count_v6 + 1 else: count_v4 = count_v4 + 1 if count_v4 > max_addr_pairs_v4: err_msg = (_("Maximum of %(max)s IPv4 address pairs can be " "defined for this port on the NSX backend") % {'max': max_addr_pairs_v4}) raise n_exc.InvalidInput(error_message=err_msg) if count_v6 > max_addr_pairs_v6: err_msg = (_("Maximum of %(max)s IPv6 address pairs can be " "defined for this port on the NSX backend") % {'max': max_addr_pairs_v6}) raise n_exc.InvalidInput(error_message=err_msg) def _create_port_address_pairs(self, context, port_data): (port_security, has_ip) = self._determine_port_security_and_has_ip( context, port_data) address_pairs = port_data.get(addr_apidef.ADDRESS_PAIRS) if validators.is_attr_set(address_pairs): if not port_security: raise addr_exc.AddressPairAndPortSecurityRequired() else: self._validate_address_pairs(address_pairs) self._validate_number_of_address_pairs(port_data) self._process_create_allowed_address_pairs(context, port_data, address_pairs) else: port_data[addr_apidef.ADDRESS_PAIRS] = [] def _provider_sgs_specified(self, port_data): # checks if security groups were updated adding/modifying # security groups, port security is set and port has ip provider_sgs_specified = (validators.is_attr_set( port_data.get(provider_sg.PROVIDER_SECURITYGROUPS)) and port_data.get(provider_sg.PROVIDER_SECURITYGROUPS) != []) return provider_sgs_specified def _create_port_preprocess_security( self, context, port, port_data, neutron_db, is_ens_tz_port): (port_security, has_ip) = self._determine_port_security_and_has_ip( context, port_data) port_data[psec.PORTSECURITY] = port_security self._process_port_port_security_create( context, port_data, neutron_db) # allowed address pair checks self._create_port_address_pairs(context, port_data) if port_security and has_ip: self._ensure_default_security_group_on_port(context, port) (sgids, psgids) = self._get_port_security_groups_lists( context, port) elif (self._check_update_has_security_groups({'port': port_data}) or self._provider_sgs_specified(port_data) or self._get_provider_security_groups_on_port(context, port)): LOG.error("Port has conflicting port security status and " "security groups") raise psec_exc.PortSecurityAndIPRequiredForSecurityGroups() else: sgids = psgids = [] port_data[ext_sg.SECURITYGROUPS] = ( self._get_security_groups_on_port(context, port)) return port_security, has_ip, sgids, psgids def _should_validate_port_sec_on_update_port(self, port_data): # Need to determine if we skip validations for port security. # This is the edge case when the subnet is deleted. # This should be called prior to deleting the fixed ip from the # port data for fixed_ip in port_data.get('fixed_ips', []): if 'delete_subnet' in fixed_ip: return False return True def _update_port_preprocess_security( self, context, port, id, updated_port, is_ens_tz_port, validate_port_sec=True, direct_vnic_type=False): delete_addr_pairs = self._check_update_deletes_allowed_address_pairs( port) has_addr_pairs = self._check_update_has_allowed_address_pairs(port) has_security_groups = self._check_update_has_security_groups(port) delete_security_groups = self._check_update_deletes_security_groups( port) # populate port_security setting port_data = port['port'] if psec.PORTSECURITY not in port_data: updated_port[psec.PORTSECURITY] = \ self._get_port_security_binding(context, id) has_ip = self._ip_on_port(updated_port) # validate port security and allowed address pairs if not updated_port[psec.PORTSECURITY]: # has address pairs in request if has_addr_pairs: raise addr_exc.AddressPairAndPortSecurityRequired() elif not delete_addr_pairs: # check if address pairs are in db updated_port[addr_apidef.ADDRESS_PAIRS] = ( self.get_allowed_address_pairs(context, id)) if updated_port[addr_apidef.ADDRESS_PAIRS]: raise addr_exc.AddressPairAndPortSecurityRequired() if delete_addr_pairs or has_addr_pairs: self._validate_address_pairs( updated_port[addr_apidef.ADDRESS_PAIRS]) # delete address pairs and read them in self._delete_allowed_address_pairs(context, id) self._process_create_allowed_address_pairs( context, updated_port, updated_port[addr_apidef.ADDRESS_PAIRS]) if updated_port[psec.PORTSECURITY] and psec.PORTSECURITY in port_data: # No port security is allowed if the port has a direct vnic type if direct_vnic_type: err_msg = _("Security features are not supported for " "ports with direct/direct-physical VNIC type") raise n_exc.InvalidInput(error_message=err_msg) # checks if security groups were updated adding/modifying # security groups, port security is set and port has ip provider_sgs_specified = self._provider_sgs_specified(updated_port) if (validate_port_sec and not (has_ip and updated_port[psec.PORTSECURITY])): if has_security_groups or provider_sgs_specified: LOG.error("Port has conflicting port security status and " "security groups") raise psec_exc.PortSecurityAndIPRequiredForSecurityGroups() # Update did not have security groups passed in. Check # that port does not have any security groups already on it. filters = {'port_id': [id]} security_groups = ( super(NsxPluginV3Base, self)._get_port_security_group_bindings( context, filters) ) if security_groups and not delete_security_groups: raise psec_exc.PortSecurityPortHasSecurityGroup() if delete_security_groups or has_security_groups: # delete the port binding and read it with the new rules. self._delete_port_security_group_bindings(context, id) sgids = self._get_security_groups_on_port(context, port) self._process_port_create_security_group(context, updated_port, sgids) if psec.PORTSECURITY in port['port']: self._process_port_port_security_update( context, port['port'], updated_port) return updated_port def _validate_create_network(self, context, net_data): """Validate the parameters of the new network to be created This method includes general validations that does not depend on provider attributes, or plugin specific configurations """ external = net_data.get(extnet_apidef.EXTERNAL) is_external_net = validators.is_attr_set(external) and external with_qos = validators.is_attr_set( net_data.get(qos_consts.QOS_POLICY_ID)) if with_qos: self._validate_qos_policy_id( context, net_data.get(qos_consts.QOS_POLICY_ID)) if is_external_net: raise nsx_exc.QoSOnExternalNet() def _validate_update_network(self, context, net_id, original_net, net_data): """Validate the updated parameters of a network This method includes general validations that does not depend on provider attributes, or plugin specific configurations """ extern_net = self._network_is_external(context, net_id) with_qos = validators.is_attr_set( net_data.get(qos_consts.QOS_POLICY_ID)) # Do not allow QoS on external networks if with_qos: if extern_net: raise nsx_exc.QoSOnExternalNet() self._validate_qos_policy_id( context, net_data.get(qos_consts.QOS_POLICY_ID)) # Do not support changing external/non-external networks if (extnet_apidef.EXTERNAL in net_data and net_data[extnet_apidef.EXTERNAL] != extern_net): err_msg = _("Cannot change the router:external flag of a network") raise n_exc.InvalidInput(error_message=err_msg) is_ens_net = self._is_ens_tz_net(context, net_id) if is_ens_net: self._assert_on_ens_with_qos(net_data) def _assert_on_illegal_port_with_qos(self, device_owner): # Prevent creating/update port with QoS policy # on router-interface/network-dhcp ports. if ((device_owner == l3_db.DEVICE_OWNER_ROUTER_INTF or device_owner == constants.DEVICE_OWNER_DHCP)): err_msg = _("Unable to create or update %s port with a QoS " "policy") % device_owner LOG.warning(err_msg) raise n_exc.InvalidInput(error_message=err_msg) def _assert_on_external_net_with_compute(self, port_data): # Prevent creating port with device owner prefix 'compute' # on external networks. device_owner = port_data.get('device_owner') if (device_owner is not None and device_owner.startswith(constants.DEVICE_OWNER_COMPUTE_PREFIX)): err_msg = _("Unable to update/create a port with an external " "network") LOG.warning(err_msg) raise n_exc.InvalidInput(error_message=err_msg) def _validate_ens_create_port(self, context, port_data): if self._ens_qos_supported(): return qos_selected = validators.is_attr_set(port_data.get( qos_consts.QOS_POLICY_ID)) if qos_selected: err_msg = _("Cannot configure QOS on ENS networks") raise n_exc.InvalidInput(error_message=err_msg) def _assert_on_port_admin_state(self, port_data, device_owner): """Do not allow changing the admin state of some ports""" if (device_owner == l3_db.DEVICE_OWNER_ROUTER_INTF or device_owner == l3_db.DEVICE_OWNER_ROUTER_GW): if port_data.get("admin_state_up") is False: err_msg = _("admin_state_up=False router ports are not " "supported") LOG.warning(err_msg) raise n_exc.InvalidInput(error_message=err_msg) def _validate_max_ips_per_port(self, context, fixed_ip_list, device_owner, relax_ip_validation=False): """Validate the number of fixed ips on a port Do not allow multiple ip addresses on a port since the nsx backend cannot add multiple static dhcp bindings with the same port, unless relax_ip_validation is set to True. In any case allow at most 1 IPv4 subnet and 1 IPv6 subnet """ if (device_owner and nl_net_utils.is_port_trusted({'device_owner': device_owner})): return if not validators.is_attr_set(fixed_ip_list): return # Do not perform additional checks in the trivial case where there is a # single fixed IP if len(fixed_ip_list) <= 1: return # Validation needed err_msg = _("Exceeded maximum amount of fixed IPs " "per port and IP version") ip1_err_msg = ("Subnet %s is IPv6 or has DHCP enabled. IP %s " "already allocated for this port. Cannot allocate %s") ip2_err_msg = ("Subnet %s is IPv6 or has DHCP enabled. Can only " "allocate a single IP per subnet. Requested %d") # Build a map of fixed IPs per subnet id subnet_fixed_ip_dict = {} fixed_ips_no_subnet = [] for item in fixed_ip_list: if item.get('subnet_id'): subnet_id = item['subnet_id'] subnet_fixed_ips = subnet_fixed_ip_dict.setdefault( subnet_id, []) subnet_fixed_ips.append(item.get('ip_address', 'UNSPECIFIED')) else: # Log something so we know we know this entry will be # processed differently LOG.debug("Fixed IP entry %s has no subnet ID", item) if item.get('ip_address'): fixed_ips_no_subnet.append(item['ip_address']) if len(subnet_fixed_ip_dict) > 2: raise n_exc.InvalidInput(error_message=err_msg) # This works as a cache, fetch from DB only the subnets we need # and reuse them -> optimize DB access subnet_map = {} def infer_fixed_ip_version(subnet_id, fixed_ips): # Infer from IPs for ip_address in fixed_ips: if ip_address != 'UNSPECIFIED': return (subnet_id, netaddr.IPAddress(ip_address).version) # Infer from subnet subnet = subnet_map.setdefault( subnet_id, self.get_subnet(context.elevated(), subnet_id)) return (subnet_id, subnet['ip_version']) # If we are here we have at most 2 subnets, and must have different IP # version ip_versions = [infer_fixed_ip_version(subnet_id, fixed_ips) for (subnet_id, fixed_ips) in subnet_fixed_ip_dict.items()] for item in zip(ip_versions, ip_versions): if item[0][0] != item[1][0] and item[0][1] == item[1][1]: # Not good! Different subnet but same IP version # One fixed IP is allowed for each IP version raise n_exc.InvalidInput(error_message=err_msg) def validate_subnet_dhcp_and_ipv6_state(subnet_id, fixed_ips): subnet = subnet_map.setdefault( subnet_id, self.get_subnet(context.elevated(), subnet_id)) if ((subnet['enable_dhcp'] or subnet.get('ip_version') == 6) and len(fixed_ips) > 1): raise n_exc.InvalidInput( error_message=ip2_err_msg % (subnet_id, len(fixed_ips))) if ((subnet['enable_dhcp'] or subnet.get('ip_version') == 6) and fixed_ips): cidr = netaddr.IPNetwork(subnet['cidr']) for ip in fixed_ips_no_subnet: # netaddr converts string IP into IPAddress if ip in cidr: raise n_exc.InvalidInput( error_message=ip1_err_msg % (subnet_id, ",".join(fixed_ips), ip)) # Validate if any "unbound" IP would fall on a subnet which has DHCP # enabled and other IPs. Also ensure no more than one IP for any # DHCP subnet is requested. fixed_ip attribute does not have patch # behaviour, so all requested IP allocations must be included in it if not relax_ip_validation: for subnet_id, fixed_ips in subnet_fixed_ip_dict.items(): validate_subnet_dhcp_and_ipv6_state(subnet_id, fixed_ips) def _get_subnets_for_fixed_ips_on_port(self, context, port_data): # get the subnet id from the fixed ips of the port if 'fixed_ips' in port_data and port_data['fixed_ips']: subnet_ids = (fixed_ip['subnet_id'] for fixed_ip in port_data['fixed_ips']) return (self._get_subnet(context.elevated(), subnet_id) for subnet_id in subnet_ids) return [] def _validate_create_port(self, context, port_data, relax_ip_validation=False): # Validate IP address settings. Relax IP validation parameter should # be true to allow multiple IPs from the same subnet self._validate_max_ips_per_port(context, port_data.get('fixed_ips', []), port_data.get('device_owner'), relax_ip_validation) is_external_net = self._network_is_external( context, port_data['network_id']) qos_selected = validators.is_attr_set(port_data.get( qos_consts.QOS_POLICY_ID)) device_owner = port_data.get('device_owner') # QoS validations if qos_selected: self._validate_qos_policy_id( context, port_data.get(qos_consts.QOS_POLICY_ID)) self._assert_on_illegal_port_with_qos(device_owner) if is_external_net: raise nsx_exc.QoSOnExternalNet() is_ens_tz_port = self._is_ens_tz_port(context, port_data) if is_ens_tz_port: self._validate_ens_create_port(context, port_data) # External network validations: if is_external_net: self._assert_on_external_net_with_compute(port_data) self._assert_on_port_admin_state(port_data, device_owner) self._validate_extra_dhcp_options(port_data.get(ext_edo.EXTRADHCPOPTS)) def _assert_on_vpn_port_change(self, port_data): if port_data['device_owner'] == ipsec_utils.VPN_PORT_OWNER: msg = _('Can not update/delete VPNaaS port %s') % port_data['id'] raise n_exc.InvalidInput(error_message=msg) def _assert_on_lb_port_fixed_ip_change(self, port_data, orig_dev_own): if orig_dev_own == constants.DEVICE_OWNER_LOADBALANCERV2: if "fixed_ips" in port_data and port_data["fixed_ips"]: msg = _('Can not update Loadbalancer port with fixed IP') raise n_exc.InvalidInput(error_message=msg) def _assert_on_device_owner_change(self, port_data, orig_dev_own): """Prevent illegal device owner modifications """ if orig_dev_own == constants.DEVICE_OWNER_LOADBALANCERV2: if ("allowed_address_pairs" in port_data and port_data["allowed_address_pairs"]): msg = _('Loadbalancer port can not be updated ' 'with address pairs') raise n_exc.InvalidInput(error_message=msg) if 'device_owner' not in port_data: return new_dev_own = port_data['device_owner'] if new_dev_own == orig_dev_own: return err_msg = (_("Changing port device owner '%(orig)s' to '%(new)s' is " "not allowed") % {'orig': orig_dev_own, 'new': new_dev_own}) # Do not allow changing nova <-> neutron device owners if ((orig_dev_own.startswith(constants.DEVICE_OWNER_COMPUTE_PREFIX) and new_dev_own.startswith(constants.DEVICE_OWNER_NETWORK_PREFIX)) or (orig_dev_own.startswith(constants.DEVICE_OWNER_NETWORK_PREFIX) and new_dev_own.startswith(constants.DEVICE_OWNER_COMPUTE_PREFIX))): raise n_exc.InvalidInput(error_message=err_msg) # Do not allow removing the device owner in some cases if orig_dev_own == constants.DEVICE_OWNER_DHCP: raise n_exc.InvalidInput(error_message=err_msg) def _assert_on_port_sec_change(self, port_data, device_owner): """Do not allow enabling port security/mac learning of some ports Trusted ports are created with port security and mac learning disabled in neutron, and it should not change. """ if nl_net_utils.is_port_trusted({'device_owner': device_owner}): if port_data.get(psec.PORTSECURITY) is True: err_msg = _("port_security_enabled=True is not supported for " "trusted ports") LOG.warning(err_msg) raise n_exc.InvalidInput(error_message=err_msg) mac_learning = port_data.get(mac_ext.MAC_LEARNING) if (validators.is_attr_set(mac_learning) and mac_learning is True): err_msg = _("mac_learning_enabled=True is not supported for " "trusted ports") LOG.warning(err_msg) raise n_exc.InvalidInput(error_message=err_msg) def _validate_update_port(self, context, id, original_port, port_data, relax_ip_validation=False): qos_selected = validators.is_attr_set(port_data.get (qos_consts.QOS_POLICY_ID)) is_external_net = self._network_is_external( context, original_port['network_id']) device_owner = (port_data['device_owner'] if 'device_owner' in port_data else original_port.get('device_owner')) # QoS validations if qos_selected: self._validate_qos_policy_id( context, port_data.get(qos_consts.QOS_POLICY_ID)) if is_external_net: raise nsx_exc.QoSOnExternalNet() self._assert_on_illegal_port_with_qos(device_owner) is_ens_tz_port = self._is_ens_tz_port(context, original_port) if is_ens_tz_port and not self._ens_qos_supported(): err_msg = _("Cannot configure QOS on ENS networks") raise n_exc.InvalidInput(error_message=err_msg) # External networks validations: if is_external_net: self._assert_on_external_net_with_compute(port_data) # Device owner validations: orig_dev_owner = original_port.get('device_owner') self._assert_on_device_owner_change(port_data, orig_dev_owner) self._assert_on_port_admin_state(port_data, device_owner) self._assert_on_port_sec_change(port_data, device_owner) # Validate IP address settings. Relax IP validation parameter should # be true to allow multiple IPs from the same subnet self._validate_max_ips_per_port(context, port_data.get('fixed_ips', []), device_owner, relax_ip_validation) self._validate_number_of_address_pairs(port_data) self._assert_on_vpn_port_change(original_port) self._assert_on_lb_port_fixed_ip_change(port_data, orig_dev_owner) self._validate_extra_dhcp_options(port_data.get(ext_edo.EXTRADHCPOPTS)) def _get_dhcp_port_name(self, net_name, net_id): return utils.get_name_and_uuid('%s-%s' % ('dhcp', net_name or 'network'), net_id) def _build_port_name(self, context, port_data): device_owner = port_data.get('device_owner') device_id = port_data.get('device_id') if device_owner == l3_db.DEVICE_OWNER_ROUTER_INTF and device_id: router = self._get_router(context, device_id) name = utils.get_name_and_uuid( router['name'] or 'router', port_data['id'], tag='port') elif device_owner == constants.DEVICE_OWNER_DHCP: network = self.get_network(context, port_data['network_id']) name = self._get_dhcp_port_name(network['name'], network['id']) elif device_owner.startswith(constants.DEVICE_OWNER_COMPUTE_PREFIX): name = utils.get_name_and_uuid( port_data['name'] or 'instance-port', port_data['id']) else: name = port_data['name'] return name def _validate_external_net_create(self, net_data, default_tier0_router, tier0_validator=None): """Validate external network configuration Returns a tuple of: - Boolean is provider network (always True) - Network type (always L3_EXT) - tier 0 router id - vlan id """ if not validators.is_attr_set(net_data.get(pnet.PHYSICAL_NETWORK)): tier0_uuid = default_tier0_router else: tier0_uuid = net_data[pnet.PHYSICAL_NETWORK] if ((validators.is_attr_set(net_data.get(pnet.NETWORK_TYPE)) and net_data.get(pnet.NETWORK_TYPE) != utils.NetworkTypes.L3_EXT and net_data.get(pnet.NETWORK_TYPE) != utils.NetworkTypes.LOCAL) or validators.is_attr_set(net_data.get(pnet.SEGMENTATION_ID))): msg = (_("External network cannot be created with %s provider " "network or segmentation id") % net_data.get(pnet.NETWORK_TYPE)) raise n_exc.InvalidInput(error_message=msg) if tier0_validator: tier0_validator(tier0_uuid) return (True, utils.NetworkTypes.L3_EXT, tier0_uuid, 0) def _extend_network_dict_provider(self, context, network, bindings=None): """Add network provider fields to the network dict from the DB""" if 'id' not in network: return if not bindings: bindings = nsx_db.get_network_bindings(context.session, network['id']) # With NSX plugin, "normal" overlay networks will have no binding if bindings: # Network came in through provider networks API network[pnet.NETWORK_TYPE] = bindings[0].binding_type network[pnet.PHYSICAL_NETWORK] = bindings[0].phy_uuid network[pnet.SEGMENTATION_ID] = bindings[0].vlan_id def _extend_get_network_dict_provider(self, context, network): self._extend_network_dict_provider(context, network) network[qos_consts.QOS_POLICY_ID] = (qos_com_utils. get_network_policy_id(context, network['id'])) def _translate_net_db_2_dict(self, context, net_db): net_dict = self._make_network_dict(net_db, context=context) self._extend_get_network_dict_provider(context, net_dict) return net_dict def get_network(self, context, id, fields=None): with db_api.CONTEXT_READER.using(context): # Get network from Neutron database network = self._get_network(context, id) # Don't do field selection here otherwise we won't be able to add # provider networks fields net = self._translate_net_db_2_dict(context, network) return db_utils.resource_fields(net, fields) def get_networks(self, context, filters=None, fields=None, sorts=None, limit=None, marker=None, page_reverse=False): # Get networks from Neutron database filters = filters or {} with db_api.CONTEXT_READER.using(context): networks = super(NsxPluginV3Base, self).get_networks( context, filters, fields, sorts, limit, marker, page_reverse) # Add provider network fields for net in networks: self._extend_get_network_dict_provider(context, net) return (networks if not fields else [db_utils.resource_fields(network, fields) for network in networks]) def _assert_on_ens_with_qos(self, net_data): if self._ens_qos_supported(): return qos_id = net_data.get(qos_consts.QOS_POLICY_ID) if validators.is_attr_set(qos_id): err_msg = _("Cannot configure QOS on ENS networks") raise n_exc.InvalidInput(error_message=err_msg) def _get_port_qos_policy_id(self, context, original_port, updated_port): """Return the QoS policy Id of a port that is being created/updated Return the QoS policy assigned directly to the port (after update or originally), or the policy of the network, if it is a compute port that should inherit it. original_port: the neutron port data before this update (or None in a case of a new port creation) updated_ports: the modified fields of this port (or all th attributes of the new port) """ orig_compute = False if original_port: orig_compute = original_port.get('device_owner', '').startswith( constants.DEVICE_OWNER_COMPUTE_PREFIX) updated_compute = updated_port.get('device_owner', '').startswith( constants.DEVICE_OWNER_COMPUTE_PREFIX) is_new_compute = updated_compute and not orig_compute qos_policy_id = None if qos_consts.QOS_POLICY_ID in updated_port: qos_policy_id = updated_port[qos_consts.QOS_POLICY_ID] elif original_port: # Look for the original QoS policy of this port qos_policy_id = qos_com_utils.get_port_policy_id( context, original_port['id']) # If the port is now a 'compute' port (attached to a vm) and # Qos policy was not configured on the port directly, # try to take it from the ports network if qos_policy_id is None and is_new_compute: # check if the network of this port has a policy net_id = (original_port.get('network_id') if original_port else updated_port.get('network_id')) qos_policy_id = qos_com_utils.get_network_policy_id( context, net_id) return qos_policy_id def _ens_qos_supported(self): """Should be implemented by each plugin""" pass def _has_native_dhcp_metadata(self): """Should be implemented by each plugin""" pass def _get_nsx_net_tz_id(self, nsx_net): """Should be implemented by each plugin""" pass def _get_network_nsx_id(self, context, neutron_id): """Should be implemented by each plugin""" pass def _get_tier0_uplink_cidrs(self, tier0_id): """Should be implemented by each plugin""" pass def _is_ens_tz_net(self, context, net_id): """Return True if the network is based on an END transport zone""" tz_id = self._get_net_tz(context, net_id) if tz_id: # Check the mode of this TZ return self._is_ens_tz(tz_id) return False def _is_ens_tz_port(self, context, port_data): # Check the host-switch-mode of the TZ connected to the ports network return self._is_ens_tz_net(context, port_data['network_id']) def _is_overlay_network(self, context, network_id): """Should be implemented by each plugin""" pass def _generate_segment_id(self, context, physical_network, net_data, restricted_vlans): bindings = nsx_db.get_network_bindings_by_phy_uuid( context.session, physical_network) vlan_ranges = self._network_vlans.get(physical_network, []) if vlan_ranges: vlan_ids = set() for vlan_min, vlan_max in vlan_ranges: vlan_ids |= set(moves.range(vlan_min, vlan_max + 1)) else: vlan_min = constants.MIN_VLAN_TAG vlan_max = constants.MAX_VLAN_TAG vlan_ids = set(moves.range(vlan_min, vlan_max + 1)) used_ids_in_range = [binding.vlan_id for binding in bindings if binding.vlan_id in vlan_ids] not_allowed_in_range = set(used_ids_in_range + restricted_vlans) free_ids = list(vlan_ids ^ not_allowed_in_range) if len(free_ids) == 0: raise n_exc.NoNetworkAvailable() net_data[pnet.SEGMENTATION_ID] = free_ids[0] return net_data[pnet.SEGMENTATION_ID] def _default_physical_net(self, physical_net): return physical_net is None or physical_net == 'default' def _validate_provider_create(self, context, network_data, az, nsxlib_tz, nsxlib_network, transparent_vlan=False): """Validate the parameters of a new provider network raises an error if illegal returns a dictionary with the relevant processed data: - is_provider_net: boolean - net_type: provider network type or None - physical_net: the uuid of the relevant transport zone or None - vlan_id: vlan tag, 0 or None - switch_mode: standard or ENS """ # initialize the relevant parameters from the AZ default_vlan_tz_uuid = az._default_vlan_tz_uuid default_overlay_tz_uuid = az._default_overlay_tz_uuid mdproxy_uuid = az._native_md_proxy_uuid is_provider_net = any( validators.is_attr_set(network_data.get(f)) for f in (pnet.NETWORK_TYPE, pnet.PHYSICAL_NETWORK, pnet.SEGMENTATION_ID)) physical_net = network_data.get(pnet.PHYSICAL_NETWORK) if not validators.is_attr_set(physical_net): physical_net = None vlan_id = network_data.get(pnet.SEGMENTATION_ID) if not validators.is_attr_set(vlan_id): vlan_id = None if vlan_id and transparent_vlan: err_msg = (_("Segmentation ID cannot be set with transparent " "vlan!")) raise n_exc.InvalidInput(error_message=err_msg) err_msg = None net_type = network_data.get(pnet.NETWORK_TYPE) tz_type = nsxlib_consts.TRANSPORT_TYPE_VLAN switch_mode = nsxlib_consts.HOST_SWITCH_MODE_STANDARD if validators.is_attr_set(net_type): if net_type == utils.NsxV3NetworkTypes.FLAT: if vlan_id is not None: err_msg = (_("Segmentation ID cannot be specified with " "%s network type") % utils.NsxV3NetworkTypes.FLAT) else: if not transparent_vlan: # Set VLAN id to 0 for flat networks vlan_id = '0' if self._default_physical_net(physical_net): physical_net = default_vlan_tz_uuid elif net_type == utils.NsxV3NetworkTypes.VLAN: # Use default VLAN transport zone if physical network not given if self._default_physical_net(physical_net): physical_net = default_vlan_tz_uuid restricted_vlans = self._get_tz_restricted_vlans(physical_net) if not transparent_vlan: # Validate VLAN id if not vlan_id: vlan_id = self._generate_segment_id(context, physical_net, network_data, restricted_vlans) elif not plugin_utils.is_valid_vlan_tag(vlan_id): err_msg = (_('Segmentation ID %(seg_id)s out of ' 'range (%(min_id)s through %(max_id)s)') % {'seg_id': vlan_id, 'min_id': constants.MIN_VLAN_TAG, 'max_id': constants.MAX_VLAN_TAG}) elif vlan_id in restricted_vlans: err_msg = (_('Segmentation ID %(seg_id)s cannot be ' 'used as it is used by the transport ' 'node') % {'seg_id': vlan_id}) else: # Verify VLAN id is not already allocated bindings = nsx_db.\ get_network_bindings_by_vlanid_and_physical_net( context.session, vlan_id, physical_net) if bindings: raise n_exc.VlanIdInUse( vlan_id=vlan_id, physical_network=physical_net) elif net_type == utils.NsxV3NetworkTypes.GENEVE: if vlan_id: err_msg = (_("Segmentation ID cannot be specified with " "%s network type") % utils.NsxV3NetworkTypes.GENEVE) tz_type = nsxlib_consts.TRANSPORT_TYPE_OVERLAY elif net_type == utils.NsxV3NetworkTypes.NSX_NETWORK: # Linking neutron networks to an existing NSX logical switch if not physical_net: err_msg = (_("Physical network must be specified with " "%s network type") % net_type) # Validate the logical switch existence else: try: nsx_net = nsxlib_network.get(physical_net) tz_id = self._get_nsx_net_tz_id(nsx_net) switch_mode = nsxlib_tz.get_host_switch_mode(tz_id) except nsx_lib_exc.ResourceNotFound: err_msg = (_('Logical switch %s does not exist') % physical_net) # make sure no other neutron network is using it bindings = ( nsx_db.get_network_bindings_by_vlanid_and_physical_net( context.elevated().session, 0, physical_net)) if bindings: err_msg = (_('Logical switch %s is already used by ' 'another network') % physical_net) else: err_msg = (_('%(net_type_param)s %(net_type_value)s not ' 'supported') % {'net_type_param': pnet.NETWORK_TYPE, 'net_type_value': net_type}) elif is_provider_net: # FIXME: Ideally provider-network attributes should be checked # at the NSX backend. For now, the network_type is required, # so the plugin can do a quick check locally. err_msg = (_('%s is required for creating a provider network') % pnet.NETWORK_TYPE) else: net_type = None if physical_net is None: # Default to transport type overlay physical_net = default_overlay_tz_uuid # validate the transport zone existence and type if (not err_msg and physical_net and net_type != utils.NsxV3NetworkTypes.NSX_NETWORK): if is_provider_net: try: backend_type = nsxlib_tz.get_transport_type( physical_net) except nsx_lib_exc.ResourceNotFound: err_msg = (_('Transport zone %s does not exist') % physical_net) else: if backend_type != tz_type: err_msg = (_('%(tz)s transport zone is required for ' 'creating a %(net)s provider network') % {'tz': tz_type, 'net': net_type}) if not err_msg: switch_mode = nsxlib_tz.get_host_switch_mode(physical_net) # validate the mdproxy TZ matches this one. if (not err_msg and physical_net and self._has_native_dhcp_metadata() and net_type != utils.NsxV3NetworkTypes.NSX_NETWORK): if not self._validate_net_mdproxy_tz( az, physical_net, mdproxy_uuid): err_msg = (_('Network TZ %(tz)s does not match MD proxy ' '%(md)s edge cluster') % {'tz': physical_net, 'md': mdproxy_uuid}) if err_msg: raise n_exc.InvalidInput(error_message=err_msg) if (switch_mode == nsxlib_consts.HOST_SWITCH_MODE_ENS): if not self._allow_ens_networks(): raise NotImplementedError(_("ENS support is disabled")) self._assert_on_ens_with_qos(network_data) return {'is_provider_net': is_provider_net, 'net_type': net_type, 'physical_net': physical_net, 'vlan_id': vlan_id, 'switch_mode': switch_mode} def _validate_net_mdproxy_tz(self, az, tz_uuid, mdproxy_uuid): """Validate that the network TZ matches the mdproxy edge cluster Should be implemented by each plugin. """ pass def _network_is_nsx_net(self, context, network_id): bindings = nsx_db.get_network_bindings(context.session, network_id) if not bindings: return False return (bindings[0].binding_type == utils.NsxV3NetworkTypes.NSX_NETWORK) def _vif_type_by_vnic_type(self, direct_vnic_type): return (nsx_constants.VIF_TYPE_DVS if direct_vnic_type else pbin.VIF_TYPE_OVS) def _get_network_segmentation_id(self, context, neutron_id): bindings = nsx_db.get_network_bindings(context.session, neutron_id) if bindings: return bindings[0].vlan_id def _get_network_vlan_transparent(self, context, network_id): if not cfg.CONF.vlan_transparent: return False # Get this flag directly from DB to improve performance db_entry = context.session.query(models_v2.Network).filter_by( id=network_id).first() if db_entry: return True if db_entry.vlan_transparent else False def _is_backend_port(self, context, port_data, delete=False): # Can be implemented by each plugin return True def _extend_nsx_port_dict_binding(self, context, port_data): # Not using the register api for this because we need the context # Some attributes were already initialized by _extend_port_portbinding if pbin.VIF_TYPE not in port_data: port_data[pbin.VIF_TYPE] = pbin.VIF_TYPE_OVS if pbin.VNIC_TYPE not in port_data: port_data[pbin.VNIC_TYPE] = pbin.VNIC_NORMAL if 'network_id' in port_data: net_id = port_data['network_id'] if pbin.VIF_DETAILS not in port_data: port_data[pbin.VIF_DETAILS] = {} port_data[pbin.VIF_DETAILS][pbin.OVS_HYBRID_PLUG] = False if (port_data.get('device_owner') == constants.DEVICE_OWNER_FLOATINGIP): # floatingip belongs to an external net without nsx-id port_data[pbin.VIF_DETAILS]['nsx-logical-switch-id'] = None elif not self._is_backend_port(context, port_data): # this port is not relevant for Nova port_data[pbin.VIF_DETAILS]['nsx-logical-switch-id'] = None else: port_data[pbin.VIF_DETAILS]['nsx-logical-switch-id'] = ( self._get_network_nsx_id(context, net_id)) if port_data[pbin.VNIC_TYPE] != pbin.VNIC_NORMAL: port_data[pbin.VIF_DETAILS]['segmentation-id'] = ( self._get_network_segmentation_id(context, net_id)) port_data[pbin.VIF_DETAILS]['vlan-transparent'] = ( self._get_network_vlan_transparent(context, net_id)) def _extend_qos_port_dict_binding(self, context, port): # add the qos policy id from the DB if 'id' in port: port[qos_consts.QOS_POLICY_ID] = qos_com_utils.get_port_policy_id( context, port['id']) def fix_direct_vnic_port_sec(self, direct_vnic_type, port_data): if direct_vnic_type: if validators.is_attr_set(port_data.get(psec.PORTSECURITY)): # 'direct' and 'direct-physical' vnic types ports requires # port-security to be disabled. if port_data[psec.PORTSECURITY]: err_msg = _("Security features are not supported for " "ports with direct/direct-physical VNIC " "type") raise n_exc.InvalidInput(error_message=err_msg) else: # Implicitly disable port-security for direct vnic types. port_data[psec.PORTSECURITY] = False def _validate_network_type(self, context, network_id, net_types): net = self.get_network(context, network_id) if net.get(pnet.NETWORK_TYPE) in net_types: return True return False def _revert_neutron_port_update(self, context, port_id, original_port, updated_port, port_security, sec_grp_updated): # revert the neutron port update super(NsxPluginV3Base, self).update_port(context, port_id, {'port': original_port}) # revert allowed address pairs if port_security: orig_pair = original_port.get(addr_apidef.ADDRESS_PAIRS) updated_pair = updated_port.get(addr_apidef.ADDRESS_PAIRS) if orig_pair != updated_pair: self._delete_allowed_address_pairs(context, port_id) if orig_pair: self._process_create_allowed_address_pairs( context, original_port, orig_pair) # revert the security groups modifications if sec_grp_updated: self.update_security_group_on_port( context, port_id, {'port': original_port}, updated_port, original_port) def _get_external_attachment_info(self, context, router): gw_port = router.gw_port ipaddress = None netmask = None nexthop = None if gw_port: # gw_port may have multiple IPs, only configure the first one if gw_port.get('fixed_ips'): ipaddress = gw_port['fixed_ips'][0]['ip_address'] network_id = gw_port.get('network_id') if network_id: ext_net = self._get_network(context, network_id) if not ext_net.external: msg = (_("Network '%s' is not a valid external " "network") % network_id) raise n_exc.BadRequest(resource='router', msg=msg) if ext_net.subnets: ext_subnet = ext_net.subnets[0] netmask = str(netaddr.IPNetwork(ext_subnet.cidr).netmask) nexthop = ext_subnet.gateway_ip return (ipaddress, netmask, nexthop) def _get_tier0_uuid_by_net_id(self, context, network_id): if not network_id: return network = self.get_network(context, network_id) if not network.get(pnet.PHYSICAL_NETWORK): az = self.get_network_az(network) return az._default_tier0_router else: return network.get(pnet.PHYSICAL_NETWORK) def _validate_router_tz(self, context, tier0_uuid, subnets): """Ensure the related GW (Tier0 router) belongs to the same TZ as the subnets attached to the Tier1 router Should be implemented by each plugin. """ pass def _get_router_gw_info(self, context, router_id): router = self.get_router(context, router_id) return router.get(l3_apidef.EXTERNAL_GW_INFO, {}) def _validate_router_gw_and_tz(self, context, router_id, info, org_enable_snat, router_subnets): # Ensure that a router cannot have SNAT disabled if there are # floating IP's assigned if (info and 'enable_snat' in info and org_enable_snat != info.get('enable_snat') and info.get('enable_snat') is False and self.router_gw_port_has_floating_ips(context, router_id)): msg = _("Unable to set SNAT disabled. Floating IPs assigned") raise n_exc.InvalidInput(error_message=msg) # Ensure that the router GW tier0 belongs to the same TZ as the # subnets of its interfaces if info and info.get('network_id'): new_tier0_uuid = self._get_tier0_uuid_by_net_id(context.elevated(), info['network_id']) if new_tier0_uuid: self._validate_router_tz(context, new_tier0_uuid, router_subnets) def _get_tier0_uuid_by_router(self, context, router): network_id = router.gw_port_id and router.gw_port.network_id return self._get_tier0_uuid_by_net_id(context, network_id) def _validate_gw_overlap_interfaces(self, context, gateway_net, interfaces_networks): # Ensure that interface subnets cannot overlap with the GW subnet gw_subnets = self._get_subnets_by_network( context.elevated(), gateway_net) gw_cidrs = [subnet['cidr'] for subnet in gw_subnets] gw_ip_set = netaddr.IPSet(gw_cidrs) if_subnets = [] for net in interfaces_networks: if_subnets.extend(self._get_subnets_by_network( context.elevated(), net)) if_cidrs = [subnet['cidr'] for subnet in if_subnets] if_ip_set = netaddr.IPSet(if_cidrs) if gw_ip_set & if_ip_set: msg = _("Interface network cannot overlap with router GW network") LOG.error(msg) raise n_exc.InvalidInput(error_message=msg) def _get_update_router_gw_actions( self, org_tier0_uuid, orgaddr, org_enable_snat, new_tier0_uuid, newaddr, new_enable_snat, tier1_services_exist, sr_currently_exists): """Return a dictionary of flags indicating which actions should be performed on this router GW update. """ actions = {} # Remove router link port between tier1 and tier0 if tier0 router link # is removed or changed actions['remove_router_link_port'] = ( org_tier0_uuid and (not new_tier0_uuid or org_tier0_uuid != new_tier0_uuid)) # Remove SNAT rules for gw ip if gw ip is deleted/changed or # enable_snat is updated from True to False actions['remove_snat_rules'] = ( org_enable_snat and orgaddr and (newaddr != orgaddr or not new_enable_snat)) # Remove No-DNAT rules if GW was removed or snat was disabled actions['remove_no_dnat_rules'] = ( orgaddr and org_enable_snat and (not newaddr or not new_enable_snat)) # Revocate bgp announce for nonat subnets if tier0 router link is # changed or enable_snat is updated from False to True actions['revocate_bgp_announce'] = ( not org_enable_snat and org_tier0_uuid and (new_tier0_uuid != org_tier0_uuid or new_enable_snat)) # Add router link port between tier1 and tier0 if tier0 router link is # added or changed to a new one actions['add_router_link_port'] = ( new_tier0_uuid and (not org_tier0_uuid or org_tier0_uuid != new_tier0_uuid)) # Add SNAT rules for gw ip if gw ip is add/changed or # enable_snat is updated from False to True actions['add_snat_rules'] = ( new_enable_snat and newaddr and (newaddr != orgaddr or not org_enable_snat)) # Add No-DNAT rules if GW was added, and the router has SNAT enabled, # or if SNAT was enabled actions['add_no_dnat_rules'] = ( new_enable_snat and newaddr and (not orgaddr or not org_enable_snat)) # Bgp announce for nonat subnets if tier0 router link is changed or # enable_snat is updated from True to False actions['bgp_announce'] = ( not new_enable_snat and new_tier0_uuid and (new_tier0_uuid != org_tier0_uuid or not org_enable_snat)) # Advertise NAT routes if enable SNAT to support FIP. In the NoNAT # use case, only NSX connected routes need to be advertised. actions['advertise_route_nat_flag'] = ( True if new_enable_snat else False) actions['advertise_route_connected_flag'] = ( True if not new_enable_snat else False) # the purpose of this var is to be able to differ between # adding a gateway w/o snat and adding snat (when adding/removing gw # the snat option is on by default). new_with_snat = True if (new_enable_snat and newaddr) else False has_gw = True if newaddr else False if sr_currently_exists: # currently there is a service router on the backend actions['add_service_router'] = False # Should remove the service router if the GW was removed, # or no service needs it: SNAT, LBaaS or FWaaS actions['remove_service_router'] = ( not has_gw or not (tier1_services_exist or new_with_snat)) if actions['remove_service_router']: LOG.info("Removing service router [has GW: %s, services %s, " "SNAT %s]", has_gw, tier1_services_exist, new_with_snat) else: # currently there is no service router on the backend actions['remove_service_router'] = False # Should add service router if there is a GW # and there is a service that needs it: SNAT, LB or FWaaS actions['add_service_router'] = ( has_gw is not None and (new_with_snat or tier1_services_exist)) if actions['add_service_router']: LOG.info("Adding service router [has GW: %s, services %s, " "SNAT %s]", has_gw, tier1_services_exist, new_with_snat) return actions def _validate_update_router_gw(self, context, router_id, gw_info): router_ports = self._get_router_interfaces(context, router_id) for port in router_ports: # if setting this router as no-snat, make sure gw address scope # match those of the subnets if not gw_info.get('enable_snat', cfg.CONF.enable_snat_by_default): for fip in port['fixed_ips']: self._validate_address_scope_for_router_interface( context.elevated(), router_id, gw_info['network_id'], fip['subnet_id']) # If the network attached to a router is a VLAN backed network # then it must be attached to an edge cluster if (not gw_info and not self._is_overlay_network(context, port['network_id'])): msg = _("A router attached to a VLAN backed network " "must have an external network assigned") raise n_exc.InvalidInput(error_message=msg) def _validate_ext_routes(self, context, router_id, gw_info, new_routes): ext_net_id = (gw_info['network_id'] if validators.is_attr_set(gw_info) and gw_info else None) if not ext_net_id: port_filters = {'device_id': [router_id], 'device_owner': [l3_db.DEVICE_OWNER_ROUTER_GW]} gw_ports = self.get_ports(context, filters=port_filters) if gw_ports: ext_net_id = gw_ports[0]['network_id'] if ext_net_id: subnets = self._get_subnets_by_network(context, ext_net_id) ext_cidrs = [subnet['cidr'] for subnet in subnets] for route in new_routes: if netaddr.all_matching_cidrs( route['nexthop'], ext_cidrs): error_message = (_("route with destination %(dest)s have " "an external nexthop %(nexthop)s which " "can't be supported") % {'dest': route['destination'], 'nexthop': route['nexthop']}) LOG.error(error_message) raise n_exc.InvalidInput(error_message=error_message) def _validate_routes(self, context, router_id, routes): super(NsxPluginV3Base, self)._validate_routes( context, router_id, routes) # routes with mixed ip versions are not allowed for route in routes: if route.get('destination') and route.get('nexthop'): dest_ver = netaddr.IPNetwork(route['destination']).version nexthop_ver = netaddr.IPAddress(route['nexthop']).version if dest_ver != nexthop_ver: msg = _("Static route network CIDR and next hop IP " "addresses must be same address family.") raise n_exc.BadRequest(resource='router', msg=msg) def _get_static_routes_diff(self, context, router_id, gw_info, router_data): new_routes = router_data['routes'] self._validate_ext_routes(context, router_id, gw_info, new_routes) self._validate_routes(context, router_id, new_routes) old_routes = self._get_extra_routes_by_router_id( context, router_id) routes_added, routes_removed = helpers.diff_list_of_dict( old_routes, new_routes) return routes_added, routes_removed def _assert_on_router_admin_state(self, router_data): if router_data.get("admin_state_up") is False: err_msg = _("admin_state_up=False routers are not supported") LOG.warning(err_msg) raise n_exc.InvalidInput(error_message=err_msg) def _enable_native_dhcp(self, context, network, subnet, az=None): # Enable native DHCP service on the backend for this network. # First create a Neutron DHCP port and use its assigned IP # address as the DHCP server address in an API call to create a # LogicalDhcpServer on the backend. Then create the corresponding # logical port for the Neutron port with DHCP attachment as the # LogicalDhcpServer UUID. # TODO(annak): # This function temporarily serves both nsx_v3 and nsx_p plugins. # In future, when platform supports native dhcp in policy for infra # segments, this function should move back to nsx_v3 plugin # Delete obsolete settings if exist. This could happen when a # previous failed transaction was rolled back. But the backend # entries are still there. self._disable_native_dhcp(context, network['id']) # Get existing ports on subnet. existing_ports = super(NsxPluginV3Base, self).get_ports( context, filters={'network_id': [network['id']], 'fixed_ips': {'subnet_id': [subnet['id']]}}) nsx_net_id = self._get_network_nsx_id(context, network['id']) if not nsx_net_id: msg = ("Unable to obtain backend network id for logical DHCP " "server for network %s" % network['id']) LOG.error(msg) raise nsx_exc.NsxPluginException(err_msg=msg) if not az: az = self.get_network_az_by_net_id(context, network['id']) port_data = { "name": "", "admin_state_up": True, "device_id": az._native_dhcp_profile_uuid, "device_owner": constants.DEVICE_OWNER_DHCP, "network_id": network['id'], "tenant_id": network["tenant_id"], "mac_address": constants.ATTR_NOT_SPECIFIED, "fixed_ips": [{"subnet_id": subnet['id']}], psec.PORTSECURITY: False } # Create the DHCP port (on neutron only) and update its port security port = {'port': port_data} neutron_port = super(NsxPluginV3Base, self).create_port(context, port) is_ens_tz_port = self._is_ens_tz_port(context, port_data) self._create_port_preprocess_security(context, port, port_data, neutron_port, is_ens_tz_port) self._process_portbindings_create_and_update( context, port_data, neutron_port) server_data = common_utils.build_dhcp_server_config( self.nsxlib, context.tenant_name, network, subnet, neutron_port, az) port_tags = self.nsxlib.build_v3_tags_payload( neutron_port, resource_type='os-neutron-dport-id', project_name=context.tenant_name) dhcp_server = None dhcp_port_profiles = [] if (not self._has_native_dhcp_metadata() and not self._is_ens_tz_net(context, network['id'])): dhcp_port_profiles.append(self._dhcp_profile) try: dhcp_server = self.nsxlib.dhcp_server.create(**server_data) LOG.debug("Created logical DHCP server %(server)s for network " "%(network)s", {'server': dhcp_server['id'], 'network': network['id']}) name = self._build_port_name(context, port_data) nsx_port = self.nsxlib.logical_port.create( nsx_net_id, dhcp_server['id'], tags=port_tags, name=name, attachment_type=nsxlib_consts.ATTACHMENT_DHCP, switch_profile_ids=dhcp_port_profiles) LOG.debug("Created DHCP logical port %(port)s for " "network %(network)s", {'port': nsx_port['id'], 'network': network['id']}) except nsx_lib_exc.ServiceClusterUnavailable: raise webob.exc.HTTPServiceUnavailable() except nsx_lib_exc.ManagerError: err_msg = ("Unable to create logical DHCP server for " "network %s" % network['id']) LOG.error(err_msg) if dhcp_server: self.nsxlib.dhcp_server.delete(dhcp_server['id']) super(NsxPluginV3Base, self).delete_port( context, neutron_port['id']) raise nsx_exc.NsxPluginException(err_msg=err_msg) try: # Add neutron_port_id -> nsx_port_id mapping to the DB. nsx_db.add_neutron_nsx_port_mapping( context.session, neutron_port['id'], nsx_net_id, nsx_port['id']) # Add neutron_net_id -> dhcp_service_id mapping to the DB. nsx_db.add_neutron_nsx_service_binding( context.session, network['id'], neutron_port['id'], nsxlib_consts.SERVICE_DHCP, dhcp_server['id']) except (db_exc.DBError, sql_exc.TimeoutError): with excutils.save_and_reraise_exception(): LOG.error("Failed to create mapping for DHCP port %s," "deleting port and logical DHCP server", neutron_port['id']) self.nsxlib.dhcp_server.delete(dhcp_server['id']) self._cleanup_port(context, neutron_port['id'], nsx_port['id']) # Configure existing ports to work with the new DHCP server try: for port_data in existing_ports: self._add_port_mp_dhcp_binding(context, port_data) except Exception: LOG.error('Unable to create DHCP bindings for existing ports ' 'on subnet %s', subnet['id']) @nsxlib_utils.retry_upon_exception( Exception, max_attempts=cfg.CONF.nsx_v3.retries) def _safe_delete_dhcp_server(self, nsx_service_id): self.nsxlib.dhcp_server.delete(nsx_service_id) def _disable_native_dhcp(self, context, network_id): # Disable native DHCP service on the backend for this network. # First delete the DHCP port in this network. Then delete the # corresponding LogicalDhcpServer for this network. self._ensure_native_dhcp() dhcp_service = nsx_db.get_nsx_service_binding( context.session, network_id, nsxlib_consts.SERVICE_DHCP) if not dhcp_service: return if dhcp_service['port_id']: try: _net_id, nsx_port_id = nsx_db.get_nsx_switch_and_port_id( context.session, dhcp_service['port_id']) self._cleanup_port(context, dhcp_service['port_id'], nsx_port_id) except (nsx_lib_exc.ResourceNotFound, n_exc.NotFound): # This could happen when the port has been manually deleted # from the NSX, or when the neutron port deletion previously # failed LOG.error("Failed to delete DHCP port %(port)s for " "network %(network)s", {'port': dhcp_service['port_id'], 'network': network_id}) else: LOG.error("DHCP port is not configured for network %s", network_id) try: self._safe_delete_dhcp_server(dhcp_service['nsx_service_id']) LOG.debug("Deleted logical DHCP server %(server)s for network " "%(network)s", {'server': dhcp_service['nsx_service_id'], 'network': network_id}) except nsx_lib_exc.ManagerError: with excutils.save_and_reraise_exception(): LOG.error("Unable to delete logical DHCP server %(server)s " "for network %(network)s", {'server': dhcp_service['nsx_service_id'], 'network': network_id}) try: # Delete neutron_id -> dhcp_service_id mapping from the DB. nsx_db.delete_neutron_nsx_service_binding( context.session, network_id, nsxlib_consts.SERVICE_DHCP) # Delete all DHCP bindings under this DHCP server from the DB. nsx_db.delete_neutron_nsx_dhcp_bindings_by_service_id( context.session, dhcp_service['nsx_service_id']) except db_exc.DBError: with excutils.save_and_reraise_exception(): LOG.error("Unable to delete DHCP server mapping for " "network %s", network_id) def _filter_dhcp_fixed_ips(self, context, fixed_ips, version=None): ips = [] for fixed_ip in fixed_ips: if (version and netaddr.IPNetwork(fixed_ip['ip_address']).version != version): continue with db_api.CONTEXT_READER.using(context): subnet = self.get_subnet(context, fixed_ip['subnet_id']) if subnet['enable_dhcp']: ips.append(fixed_ip) return ips def _filter_ipv4_dhcp_fixed_ips(self, context, fixed_ips): return self._filter_dhcp_fixed_ips(context, fixed_ips, 4) def _add_port_mp_dhcp_binding(self, context, port): if not utils.is_port_dhcp_configurable(port): return dhcp_service = nsx_db.get_nsx_service_binding( context.session, port['network_id'], nsxlib_consts.SERVICE_DHCP) if not dhcp_service: return for fixed_ip in self._filter_ipv4_dhcp_fixed_ips( context, port['fixed_ips']): binding = self._add_dhcp_binding_on_server( context, dhcp_service['nsx_service_id'], fixed_ip['subnet_id'], fixed_ip['ip_address'], port) try: nsx_db.add_neutron_nsx_dhcp_binding( context.session, port['id'], fixed_ip['subnet_id'], fixed_ip['ip_address'], dhcp_service['nsx_service_id'], binding['id']) except (db_exc.DBError, sql_exc.TimeoutError): LOG.error("Failed to add mapping of DHCP binding " "%(binding)s for port %(port)s, deleting " "DHCP binding on server", {'binding': binding['id'], 'port': port['id']}) fake_db_binding = { 'port_id': port['id'], 'nsx_service_id': dhcp_service['nsx_service_id'], 'nsx_binding_id': binding['id']} self._delete_dhcp_binding_on_server(context, fake_db_binding) def _add_dhcp_binding_on_server(self, context, dhcp_service_id, subnet_id, ip, port): try: hostname = 'host-%s' % ip.replace('.', '-') subnet = self.get_subnet(context, subnet_id) gateway_ip = subnet.get('gateway_ip') options = self._get_dhcp_options( context, ip, port.get(ext_edo.EXTRADHCPOPTS), port['network_id'], subnet) binding = self.nsxlib.dhcp_server.create_binding( dhcp_service_id, port['mac_address'], ip, hostname, self._get_conf_attr('dhcp_lease_time'), options, gateway_ip) LOG.debug("Created static binding (mac: %(mac)s, ip: %(ip)s, " "gateway: %(gateway)s, options: %(options)s) for port " "%(port)s on logical DHCP server %(server)s", {'mac': port['mac_address'], 'ip': ip, 'gateway': gateway_ip, 'options': options, 'port': port['id'], 'server': dhcp_service_id}) return binding except nsx_lib_exc.ManagerError: with excutils.save_and_reraise_exception(): LOG.error("Unable to create static binding (mac: %(mac)s, " "ip: %(ip)s, gateway: %(gateway)s, options: " "%(options)s) for port %(port)s on logical DHCP " "server %(server)s", {'mac': port['mac_address'], 'ip': ip, 'gateway': gateway_ip, 'options': options, 'port': port['id'], 'server': dhcp_service_id}) def _delete_port_mp_dhcp_binding(self, context, port): # Do not check device_owner here because Nova may have already # deleted that before Neutron's port deletion. bindings = nsx_db.get_nsx_dhcp_bindings(context.session, port['id']) for binding in bindings: self._delete_dhcp_binding_on_server(context, binding) try: nsx_db.delete_neutron_nsx_dhcp_binding( context.session, binding['port_id'], binding['nsx_binding_id']) except db_exc.DBError: LOG.error("Unable to delete mapping of DHCP binding " "%(binding)s for port %(port)s", {'binding': binding['nsx_binding_id'], 'port': binding['port_id']}) def _delete_dhcp_binding_on_server(self, context, binding): try: self.nsxlib.dhcp_server.delete_binding( binding['nsx_service_id'], binding['nsx_binding_id']) LOG.debug("Deleted static binding for port %(port)s) on " "logical DHCP server %(server)s", {'port': binding['port_id'], 'server': binding['nsx_service_id']}) except nsx_lib_exc.ManagerError: with excutils.save_and_reraise_exception(): LOG.error("Unable to delete static binding for port " "%(port)s) on logical DHCP server %(server)s", {'port': binding['port_id'], 'server': binding['nsx_service_id']}) def _find_dhcp_binding(self, subnet_id, ip_address, bindings): for binding in bindings: if (subnet_id == binding['subnet_id'] and ip_address == binding['ip_address']): return binding def _update_port_mp_dhcp_binding(self, context, old_port, new_port): # First check if any IPv4 address in fixed_ips is changed. # Then update DHCP server setting or DHCP static binding # depending on the port type. # Note that Neutron allows a port with multiple IPs in the # same subnet. But backend DHCP server may not support that. if (utils.is_port_dhcp_configurable(old_port) != utils.is_port_dhcp_configurable(new_port)): # Note that the device_owner could be changed, # but still needs DHCP binding. if utils.is_port_dhcp_configurable(old_port): self._delete_port_mp_dhcp_binding(context, old_port) else: self._add_port_mp_dhcp_binding(context, new_port) return # Collect IPv4 DHCP addresses from original and updated fixed_ips # in the form of [(subnet_id, ip_address)]. old_fixed_ips = set([(fixed_ip['subnet_id'], fixed_ip['ip_address']) for fixed_ip in self._filter_ipv4_dhcp_fixed_ips( context, old_port['fixed_ips'])]) new_fixed_ips = set([(fixed_ip['subnet_id'], fixed_ip['ip_address']) for fixed_ip in self._filter_ipv4_dhcp_fixed_ips( context, new_port['fixed_ips'])]) # Find out the subnet/IP differences before and after the update. ips_to_add = list(new_fixed_ips - old_fixed_ips) ips_to_delete = list(old_fixed_ips - new_fixed_ips) ip_change = (ips_to_add or ips_to_delete) if (old_port["device_owner"] == constants.DEVICE_OWNER_DHCP and ip_change): # Update backend DHCP server address if the IP address of a DHCP # port is changed. if len(new_fixed_ips) != 1: msg = _("Can only configure one IP address on a DHCP server") LOG.error(msg) raise n_exc.InvalidInput(error_message=msg) # Locate the backend DHCP server for this DHCP port. dhcp_service = nsx_db.get_nsx_service_binding( context.session, old_port['network_id'], nsxlib_consts.SERVICE_DHCP) if dhcp_service: new_ip = ips_to_add[0][1] try: self.nsxlib.dhcp_server.update( dhcp_service['nsx_service_id'], server_ip=new_ip) LOG.debug("Updated IP %(ip)s for logical DHCP server " "%(server)s", {'ip': new_ip, 'server': dhcp_service['nsx_service_id']}) except nsx_lib_exc.ManagerError: with excutils.save_and_reraise_exception(): LOG.error("Unable to update IP %(ip)s for logical " "DHCP server %(server)s", {'ip': new_ip, 'server': dhcp_service['nsx_service_id']}) elif utils.is_port_dhcp_configurable(old_port): # Update static DHCP bindings for a compute port. bindings = nsx_db.get_nsx_dhcp_bindings(context.session, old_port['id']) dhcp_opts = new_port.get(ext_edo.EXTRADHCPOPTS) dhcp_opts_changed = (old_port[ext_edo.EXTRADHCPOPTS] != new_port[ext_edo.EXTRADHCPOPTS]) if ip_change: # If IP address is changed, update associated DHCP bindings, # metadata route, and default hostname. # Mac address (if changed) will be updated at the same time. if ([subnet_id for (subnet_id, ip) in ips_to_add] == [subnet_id for (subnet_id, ip) in ips_to_delete]): # No change on subnet_id, just update corresponding IPs. for i, (subnet_id, ip) in enumerate(ips_to_delete): binding = self._find_dhcp_binding(subnet_id, ip, bindings) if binding: subnet = self.get_subnet(context, binding['subnet_id']) self._update_dhcp_binding_on_server( context, binding, new_port['mac_address'], ips_to_add[i][1], old_port['network_id'], dhcp_opts=dhcp_opts, subnet=subnet) # Update DB IP nsx_db.update_nsx_dhcp_bindings(context.session, old_port['id'], ip, ips_to_add[i][1]) else: for (subnet_id, ip) in ips_to_delete: binding = self._find_dhcp_binding(subnet_id, ip, bindings) if binding: self._delete_dhcp_binding_on_server(context, binding) if ips_to_add: dhcp_service = nsx_db.get_nsx_service_binding( context.session, new_port['network_id'], nsxlib_consts.SERVICE_DHCP) if dhcp_service: for (subnet_id, ip) in ips_to_add: self._add_dhcp_binding_on_server( context, dhcp_service['nsx_service_id'], subnet_id, ip, new_port) elif (old_port['mac_address'] != new_port['mac_address'] or dhcp_opts_changed): # If only Mac address/dhcp opts is changed, # update it in all associated DHCP bindings. for binding in bindings: subnet = self.get_subnet(context, binding['subnet_id']) self._update_dhcp_binding_on_server( context, binding, new_port['mac_address'], binding['ip_address'], old_port['network_id'], dhcp_opts=dhcp_opts, subnet=subnet) def _cleanup_port(self, context, port_id, nsx_port_id=None): # Clean up neutro nsx manager port if provided and then # neutron port. # Does not handle cleanup of policy port try: if nsx_port_id and self.nsxlib: self.nsxlib.logical_port.delete(nsx_port_id) except nsx_lib_exc.ResourceNotFound: LOG.info("NSX Port %s for neutron port %s was not found during " "cleanuo. Probably already deleted", port_id, nsx_port_id) super(NsxPluginV3Base, self).delete_port(context, port_id) def _is_excluded_port(self, device_owner, port_security): if device_owner == l3_db.DEVICE_OWNER_ROUTER_INTF: return False if device_owner == constants.DEVICE_OWNER_DHCP: if not self._has_native_dhcp_metadata(): return True elif not port_security: return True return False def _validate_obj_az_on_creation(self, context, obj_data, obj_type): # validate the availability zone, and get the AZ object if az_def.AZ_HINTS in obj_data: self._validate_availability_zones_forced( context, obj_type, obj_data[az_def.AZ_HINTS]) return self.get_obj_az_by_hints(obj_data) def _add_az_to_net(self, context, net_id, net_data): if az_def.AZ_HINTS in net_data: # Update the AZ hints in the neutron object az_hints = az_validator.convert_az_list_to_string( net_data[az_def.AZ_HINTS]) super(NsxPluginV3Base, self).update_network( context, net_id, {'network': {az_def.AZ_HINTS: az_hints}}) def _add_az_to_router(self, context, router_id, router_data): if az_def.AZ_HINTS in router_data: # Update the AZ hints in the neutron object az_hints = az_validator.convert_az_list_to_string( router_data[az_def.AZ_HINTS]) super(NsxPluginV3Base, self).update_router( context, router_id, {'router': {az_def.AZ_HINTS: az_hints}}) def get_network_availability_zones(self, net_db): hints = az_validator.convert_az_string_to_list( net_db[az_def.AZ_HINTS]) # When using the configured AZs, the az will always be the same # as the hint (or default if none) if hints: az_name = hints[0] else: az_name = self.get_default_az().name return [az_name] def _get_router_az_obj(self, router): l3_attrs_db.ExtraAttributesMixin._extend_extra_router_dict( router, router) return self.get_router_az(router) def get_router_availability_zones(self, router): """Return availability zones which a router belongs to.""" return [self._get_router_az_obj(router).name] def _validate_availability_zones_forced(self, context, resource_type, availability_zones): return self.validate_availability_zones(context, resource_type, availability_zones, force=True) def _list_availability_zones(self, context, filters=None): result = {} for az in self._availability_zones_data.list_availability_zones(): # Add this availability zone as a network & router resource if filters: if 'name' in filters and az not in filters['name']: continue for res in ['network', 'router']: if 'resource' not in filters or res in filters['resource']: result[(az, res)] = True return result def validate_availability_zones(self, context, resource_type, availability_zones, force=False): # This method is called directly from this plugin but also from # registered callbacks if self._is_sub_plugin and not force: # validation should be done together for both plugins return # Validate against the configured AZs return self.validate_obj_azs(availability_zones) def _ensure_nsxlib(self, feature): if not self.nsxlib: msg = (_("%s is not supported since passthough API is disabled") % feature) LOG.error(msg) raise n_exc.InvalidInput(error_message=msg) def _ensure_native_dhcp(self): self._ensure_nsxlib("Native DHCP") if not self._native_dhcp_enabled: msg = (_("Native DHCP is not supported since dhcp_profile is not" " provided in plugin configuration")) LOG.error(msg) raise n_exc.InvalidInput(error_message=msg) def _get_net_dhcp_relay(self, context, net_id): """Should be implemented by each plugin""" pass def _get_ipv6_subnet(self, context, network): for subnet in network.subnets: if subnet.ip_version == 6: return subnet def _validate_single_ipv6_subnet(self, context, network, subnet): if subnet.get('ip_version') == 6: if self._get_ipv6_subnet(context, network): msg = (_("Only one ipv6 subnet per network is supported")) LOG.error(msg) raise n_exc.InvalidInput(error_message=msg) def _subnet_with_native_dhcp(self, subnet, orig_subnet=None): native_metadata = self._has_native_dhcp_metadata() default_enable_dhcp = (orig_subnet.get('enable_dhcp', False) if orig_subnet else False) # DHCPv6 is not yet supported, but slaac is. # When configuring slaac, neutron requires the user # to enable dhcp, however plugin code does not consider # slaac as dhcp. return (native_metadata and subnet.get('enable_dhcp', default_enable_dhcp) and subnet.get('ipv6_address_mode') != constants.IPV6_SLAAC) def _validate_mp_subnet_ip_version(self, subnet): # This validation only needs to be called at create, # since ip version and ipv6 mode attributes are read only if subnet.get('ip_version') == 4: # No dhcp restrictions for V4 return enable_dhcp = subnet.get('enable_dhcp', False) is_slaac = (subnet.get('ipv6_address_mode') == constants.IPV6_SLAAC) if enable_dhcp and not is_slaac: # No DHCPv6 support with the MP DHCP msg = _("DHCPv6 is not supported") LOG.error(msg) raise n_exc.InvalidInput(error_message=msg) def _validate_net_dhcp_profile(self, context, network, az): """Validate that the dhcp profile edge cluster match the one of the network TZ """ if not self.nsxlib: msg = (_("Native DHCP is not supported since " "passthough API is disabled")) LOG.error(msg) raise n_exc.InvalidInput(error_message=msg) net_tz = self._get_net_tz(context, network['id']) dhcp_profile = az._native_dhcp_profile_uuid dhcp_obj = self.nsxlib.native_dhcp_profile.get(dhcp_profile) ec_id = dhcp_obj['edge_cluster_id'] if not ec_id: LOG.warning("DHCP profile %s is missing an edge cluster", dhcp_profile) return ec_nodes = self.nsxlib.edge_cluster.get_transport_nodes(ec_id) ec_tzs = [] for tn_uuid in ec_nodes: ec_tzs.extend(self.nsxlib.transport_node.get_transport_zones( tn_uuid)) if net_tz not in ec_tzs: msg = (_('Network TZ %(tz)s does not match DHCP profile ' '%(dhcp)s edge cluster') % {'tz': net_tz, 'dhcp': dhcp_profile}) LOG.error(msg) raise n_exc.InvalidInput(error_message=msg) def _create_subnet_with_mp_dhcp(self, context, subnet): self._validate_number_of_subnet_static_routes(subnet) self._validate_host_routes_input(subnet) self._validate_mp_subnet_ip_version(subnet['subnet']) net_id = subnet['subnet']['network_id'] network = self._get_network(context, net_id) self._validate_single_ipv6_subnet(context, network, subnet['subnet']) # TODO(berlin): public external subnet announcement if self._subnet_with_native_dhcp(subnet['subnet']): self._validate_external_subnet(context, net_id) self._ensure_native_dhcp() net_az = self.get_network_az_by_net_id(context, net_id) self._validate_net_dhcp_profile(context, network, net_az) lock = 'nsxv3_network_' + net_id ddi_support, ddi_type = self._is_ddi_supported_on_net_with_type( context, net_id, network=network) dhcp_relay = self._get_net_dhcp_relay(context, net_id) with locking.LockManager.get_lock(lock): msg = None # Check if it is on an overlay network and is the first # DHCP-enabled subnet to create. if ddi_support: if self._has_no_dhcp_enabled_subnet(context, network): if not dhcp_relay and not self.nsxlib: # cannot create DHCP for this subnet msg = (_("Native DHCP is not supported since " "passthough API is disabled")) LOG.error(msg) raise n_exc.InvalidInput(error_message=msg) # Create the neutron subnet. # Any failure from here and on will require rollback. created_subnet = super( NsxPluginV3Base, self).create_subnet(context, subnet) try: # This can be called only after the super create # since we need the subnet pool to be translated # to allocation pools self._validate_address_space( context, created_subnet) except n_exc.InvalidInput: # revert the subnet creation with excutils.save_and_reraise_exception(): super(NsxPluginV3Base, self).delete_subnet( context, created_subnet['id']) self._extension_manager.process_create_subnet(context, subnet['subnet'], created_subnet) if not dhcp_relay: try: self._enable_native_dhcp( context, network, created_subnet, az=net_az) except (nsx_lib_exc.ManagerError, nsx_exc.NsxPluginException): with excutils.save_and_reraise_exception(): super(NsxPluginV3Base, self).delete_subnet( context, created_subnet['id']) else: msg = (_("Can not create more than one DHCP-enabled " "subnet in network %s") % net_id) else: msg = _("Native DHCP is not supported for %(type)s " "network %(id)s") % {'id': net_id, 'type': ddi_type} if msg: LOG.error(msg) raise n_exc.InvalidInput(error_message=msg) else: # Subnet without DHCP created_subnet = super(NsxPluginV3Base, self).create_subnet( context, subnet) try: # This can be called only after the super create # since we need the subnet pool to be translated # to allocation pools self._validate_address_space(context, created_subnet) except n_exc.InvalidInput: # revert the subnet creation with excutils.save_and_reraise_exception(): super(NsxPluginV3Base, self).delete_subnet( context, created_subnet['id']) return created_subnet def _create_bulk_with_callback(self, resource, context, request_items, post_create_func=None, rollback_func=None): # This is a copy of the _create_bulk() in db_base_plugin_v2.py, # but extended with user-provided callback functions. objects = [] collection = "%ss" % resource items = request_items[collection] try: with db_api.CONTEXT_WRITER.using(context): for item in items: obj_creator = getattr(self, 'create_%s' % resource) obj = obj_creator(context, item) objects.append(obj) if post_create_func: # The user-provided post_create function is called # after a new object is created. post_create_func(obj) except Exception: if rollback_func: # The user-provided rollback function is called when an # exception occurred. for obj in objects: rollback_func(obj) # Note that the session.rollback() function is called here. # session.rollback() will invoke transaction.rollback() on # the transaction this session maintains. The latter will # deactivate the transaction and clear the session's cache. # # But depending on where the exception occurred, # transaction.rollback() may have already been called # internally before reaching here. # # For example, if the exception happened under a # "with session.begin(subtransactions=True):" statement # anywhere in the middle of processing obj_creator(), # transaction.__exit__() will invoke transaction.rollback(). # Thus when the exception reaches here, the session's cache # is already empty. context.session.rollback() with excutils.save_and_reraise_exception(): LOG.error("An exception occurred while creating " "the %(resource)s:%(item)s", {'resource': resource, 'item': item}) return objects def _post_create_subnet(self, context, subnet): LOG.debug("Collect native DHCP entries for network %s", subnet['network_id']) dhcp_service = nsx_db.get_nsx_service_binding( context.session, subnet['network_id'], nsxlib_consts.SERVICE_DHCP) if dhcp_service: _net_id, nsx_port_id = nsx_db.get_nsx_switch_and_port_id( context.session, dhcp_service['port_id']) return {'nsx_port_id': nsx_port_id, 'nsx_service_id': dhcp_service['nsx_service_id']} def _rollback_subnet(self, subnet, dhcp_info): LOG.debug("Rollback native DHCP entries for network %s", subnet['network_id']) if dhcp_info and self.nsxlib: try: self.nsxlib.logical_port.delete(dhcp_info['nsx_port_id']) except Exception as e: LOG.error("Failed to delete logical port %(id)s " "during rollback. Exception: %(e)s", {'id': dhcp_info['nsx_port_id'], 'e': e}) try: self.nsxlib.dhcp_server.delete(dhcp_info['nsx_service_id']) except Exception as e: LOG.error("Failed to delete logical DHCP server %(id)s " "during rollback. Exception: %(e)s", {'id': dhcp_info['nsx_service_id'], 'e': e}) def create_subnet_bulk(self, context, subnets): # Maintain a local cache here because when the rollback function # is called, the cache in the session may have already been cleared. _subnet_dhcp_info = {} def _post_create(subnet): if subnet['enable_dhcp']: _subnet_dhcp_info[subnet['id']] = self._post_create_subnet( context, subnet) def _rollback(subnet): if (subnet and subnet['enable_dhcp'] and subnet['id'] in _subnet_dhcp_info): self._rollback_subnet(subnet, _subnet_dhcp_info[subnet['id']]) del _subnet_dhcp_info[subnet['id']] # callback should be called only with MP DHCP if (self._has_native_dhcp_metadata() and (not hasattr(self, 'use_policy_dhcp') or not self.use_policy_dhcp)): return self._create_bulk_with_callback('subnet', context, subnets, _post_create, _rollback) else: return self._create_bulk('subnet', context, subnets) def _get_neutron_net_ids_by_nsx_id(self, context, nsx_id): """Should be implemented by each plugin""" pass def _validate_number_of_subnet_static_routes(self, subnet_input): s = subnet_input['subnet'] request_host_routes = (validators.is_attr_set(s.get('host_routes')) and s['host_routes']) num_allowed_on_backend = nsxlib_consts.MAX_STATIC_ROUTES if request_host_routes: if len(request_host_routes) > num_allowed_on_backend: err_msg = (_( "Number of static routes is limited at the backend to %(" "backend)s. Requested %(requested)s") % {'backend': nsxlib_consts.MAX_STATIC_ROUTES, 'requested': len(request_host_routes)}) raise n_exc.InvalidInput(error_message=err_msg) def get_subnets(self, context, filters=None, fields=None, sorts=None, limit=None, marker=None, page_reverse=False): filters = filters or {} lswitch_ids = filters.pop(as_providers.ADV_SERVICE_PROVIDERS, []) if lswitch_ids: # This is a request from Nova for metadata processing. # Find the corresponding neutron network for each logical switch. network_ids = filters.pop('network_id', []) context = context.elevated() for lswitch_id in lswitch_ids: network_ids += self._get_neutron_net_ids_by_nsx_id( context, lswitch_id) filters['network_id'] = network_ids return super(NsxPluginV3Base, self).get_subnets( context, filters, fields, sorts, limit, marker, page_reverse) def delete_subnet_with_mp_dhcp(self, context, subnet_id): # TODO(berlin): cancel public external subnet announcement if self._has_native_dhcp_metadata(): # Ensure that subnet is not deleted if attached to router. self._subnet_check_ip_allocations_internal_router_ports( context, subnet_id) subnet = self.get_subnet(context, subnet_id) if self._subnet_with_native_dhcp(subnet): lock = 'nsxv3_network_' + subnet['network_id'] with locking.LockManager.get_lock(lock): # Check if it is the last DHCP-enabled subnet to delete. network = self._get_network(context, subnet['network_id']) if self._has_single_dhcp_enabled_subnet(context, network): try: self._disable_native_dhcp(context, network['id']) except Exception as e: LOG.error("Failed to disable native DHCP for " "network %(id)s. Exception: %(e)s", {'id': network['id'], 'e': e}) super(NsxPluginV3Base, self).delete_subnet( context, subnet_id) return super(NsxPluginV3Base, self).delete_subnet(context, subnet_id) def update_subnet_with_mp_dhcp(self, context, subnet_id, subnet): updated_subnet = None orig_subnet = self.get_subnet(context, subnet_id) self._validate_number_of_subnet_static_routes(subnet) self._validate_host_routes_input( subnet, orig_enable_dhcp=orig_subnet['enable_dhcp'], orig_host_routes=orig_subnet['host_routes']) network = self._get_network(context, orig_subnet['network_id']) if self._has_native_dhcp_metadata(): enable_dhcp = self._subnet_with_native_dhcp( subnet['subnet'], orig_subnet=orig_subnet) orig_enable_dhcp = self._subnet_with_native_dhcp(orig_subnet) if enable_dhcp != orig_enable_dhcp: self._ensure_native_dhcp() self._validate_external_subnet( context, orig_subnet['network_id']) lock = 'nsxv3_network_' + orig_subnet['network_id'] with locking.LockManager.get_lock(lock): if enable_dhcp: (ddi_support, ddi_type) = self._is_ddi_supported_on_net_with_type( context, orig_subnet['network_id'], network=network) if ddi_support: if self._has_no_dhcp_enabled_subnet( context, network): net_az = self.get_network_az_by_net_id( context, orig_subnet['network_id']) self._validate_net_dhcp_profile( context, network, net_az) updated_subnet = super( NsxPluginV3Base, self).update_subnet( context, subnet_id, subnet) self._extension_manager.process_update_subnet( context, subnet['subnet'], updated_subnet) self._enable_native_dhcp( context, network, updated_subnet, az=net_az) msg = None else: msg = (_("Multiple DHCP-enabled subnets is " "not allowed in network %s") % orig_subnet['network_id']) else: msg = (_("Native DHCP is not supported for " "%(type)s network %(id)s") % {'id': orig_subnet['network_id'], 'type': ddi_type}) if msg: LOG.error(msg) raise n_exc.InvalidInput(error_message=msg) elif self._has_single_dhcp_enabled_subnet(context, network): self._disable_native_dhcp(context, network['id']) updated_subnet = super( NsxPluginV3Base, self).update_subnet( context, subnet_id, subnet) self._extension_manager.process_update_subnet( context, subnet['subnet'], updated_subnet) if not updated_subnet: updated_subnet = super(NsxPluginV3Base, self).update_subnet( context, subnet_id, subnet) self._extension_manager.process_update_subnet( context, subnet['subnet'], updated_subnet) # Check if needs to update logical DHCP server for native DHCP. if self._subnet_with_native_dhcp(updated_subnet): self._ensure_native_dhcp() kwargs = {} for key in ('dns_nameservers', 'gateway_ip', 'host_routes'): if key in subnet['subnet']: value = subnet['subnet'][key] if value != orig_subnet[key]: kwargs[key] = value if key != 'dns_nameservers': kwargs['options'] = None if 'options' in kwargs: sr, gw_ip = self._build_static_routes( updated_subnet.get('gateway_ip'), updated_subnet.get('cidr'), updated_subnet.get('host_routes', [])) kwargs['options'] = {'option121': {'static_routes': sr}} kwargs.pop('host_routes', None) if (gw_ip is not None and 'gateway_ip' not in kwargs and gw_ip != updated_subnet['gateway_ip']): kwargs['gateway_ip'] = gw_ip if kwargs: dhcp_service = nsx_db.get_nsx_service_binding( context.session, orig_subnet['network_id'], nsxlib_consts.SERVICE_DHCP) if dhcp_service: try: self.nsxlib.dhcp_server.update( dhcp_service['nsx_service_id'], **kwargs) except nsx_lib_exc.ManagerError: with excutils.save_and_reraise_exception(): LOG.error( "Unable to update logical DHCP server " "%(server)s for network %(network)s", {'server': dhcp_service['nsx_service_id'], 'network': orig_subnet['network_id']}) if 'options' in kwargs: # Need to update the static binding of every VM in # this logical DHCP server. bindings = nsx_db.get_nsx_dhcp_bindings_by_service( context.session, dhcp_service['nsx_service_id']) for binding in bindings: port = self._get_port(context, binding['port_id']) dhcp_opts = port.get(ext_edo.EXTRADHCPOPTS) self._update_dhcp_binding_on_server( context, binding, port['mac_address'], binding['ip_address'], port['network_id'], gateway_ip=kwargs.get('gateway_ip', False), dhcp_opts=dhcp_opts, options=kwargs.get('options'), subnet=updated_subnet) return updated_subnet def _has_active_port(self, context, network_id): ports_in_use = context.session.query(models_v2.Port).filter_by( network_id=network_id).all() return not all([p.device_owner == constants.DEVICE_OWNER_DHCP for p in ports_in_use]) if ports_in_use else False def _delete_network_disable_dhcp(self, context, network_id): # Disable native DHCP and delete DHCP ports before network deletion lock = 'nsxv3_network_' + network_id with locking.LockManager.get_lock(lock): # Disable native DHCP if there is no other existing port # besides DHCP port. if not self._has_active_port(context, network_id): self._disable_native_dhcp(context, network_id) def _retry_delete_network(self, context, network_id): """This method attempts to retry the delete on a network if there are AUTO_DELETE_PORT_OWNERS left. This is to avoid a race condition between delete_network and the dhcp creating a port on the network. """ first_try = True while True: try: with db_api.CONTEXT_WRITER.using(context): self._process_l3_delete(context, network_id) return super(NsxPluginV3Base, self).delete_network( context, network_id) except n_exc.NetworkInUse: # There is a race condition in delete_network() that we need # to work around here. delete_network() issues a query to # automatically delete DHCP ports and then checks to see if any # ports exist on the network. If a network is created and # deleted quickly, such as when running tempest, the DHCP agent # may be creating its port for the network around the same time # that the network is deleted. This can result in the DHCP # port getting created in between these two queries in # delete_network(). To work around that, we'll call # delete_network() a second time if we get a NetworkInUse # exception but the only port(s) that exist are ones that # delete_network() is supposed to automatically delete. if not first_try: # We tried once to work around the known race condition, # but we still got the exception, so something else is # wrong that we can't recover from. raise first_try = False if self._has_active_port(context, network_id): # There is a port on the network that is not going to be # automatically deleted (such as a tenant created port), so # we have nothing else to do but raise the exception. raise def _build_static_routes(self, gateway_ip, cidr, host_routes): # The following code is based on _generate_opts_per_subnet() in # neutron/agent/linux/dhcp.py. It prepares DHCP options for a subnet. # This code is for IPv4 only (IPv6 dhcp does not support options) # Add route for directly connected network. static_routes = [{'network': cidr, 'next_hop': '0.0.0.0'}] # Copy routes from subnet host_routes attribute. if host_routes: for hr in host_routes: if hr['destination'] == constants.IPv4_ANY: if not gateway_ip: gateway_ip = hr['nexthop'] else: static_routes.append({'network': hr['destination'], 'next_hop': hr['nexthop']}) # If gateway_ip is defined, add default route via this gateway. if gateway_ip: static_routes.append({'network': constants.IPv4_ANY, 'next_hop': gateway_ip}) return static_routes, gateway_ip def _get_dhcp_options(self, context, ip, extra_dhcp_opts, net_id, subnet): # Always add option121. net_az = self.get_network_az_by_net_id(context, net_id) static_routes = [] options = {'option121': {'static_routes': static_routes}} if net_az.windows_metadata_route: static_routes.append( {'network': '%s' % net_az.native_metadata_route, 'next_hop': '0.0.0.0'}) static_routes.append( {'network': '%s' % net_az.native_metadata_route, 'next_hop': ip}) if subnet: sr, gateway_ip = self._build_static_routes( subnet.get('gateway_ip'), subnet.get('cidr'), subnet.get('host_routes', [])) options['option121']['static_routes'].extend(sr) # Adding extra options only if configured on port if extra_dhcp_opts: other_opts = [] for opt in extra_dhcp_opts: opt_name = opt['opt_name'] if opt['opt_value'] is not None: # None value means - delete this option. Since we rebuild # the options from scratch, it can be ignored. opt_val = opt['opt_value'] if opt_name == 'classless-static-route': # Add to the option121 static routes net, ip = opt_val.split(',') options['option121']['static_routes'].append({ 'network': net, 'next_hop': ip}) else: other_opts.append({ 'code': nsxlib_utils.get_dhcp_opt_code(opt_name), 'values': [opt_val]}) if other_opts: options['others'] = other_opts return options def _update_dhcp_binding_on_server(self, context, binding, mac, ip, net_id, gateway_ip=False, dhcp_opts=None, options=None, subnet=None): try: data = {'mac_address': mac, 'ip_address': ip} if ip != binding['ip_address']: data['host_name'] = 'host-%s' % ip.replace('.', '-') data['options'] = self._get_dhcp_options( context, ip, dhcp_opts, net_id, subnet) elif (dhcp_opts is not None or options is not None): data['options'] = self._get_dhcp_options( context, ip, dhcp_opts, net_id, subnet) if gateway_ip is not False: # Note that None is valid for gateway_ip, means deleting it. data['gateway_ip'] = gateway_ip self.nsxlib.dhcp_server.update_binding( binding['nsx_service_id'], binding['nsx_binding_id'], **data) LOG.debug("Updated static binding (mac: %(mac)s, ip: %(ip)s, " "gateway: %(gateway)s) for port %(port)s on " "logical DHCP server %(server)s", {'mac': mac, 'ip': ip, 'gateway': gateway_ip, 'port': binding['port_id'], 'server': binding['nsx_service_id']}) except nsx_lib_exc.ManagerError: with excutils.save_and_reraise_exception(): LOG.error("Unable to update static binding (mac: %(mac)s, " "ip: %(ip)s, gateway: %(gateway)s) for port " "%(port)s on logical DHCP server %(server)s", {'mac': mac, 'ip': ip, 'gateway': gateway_ip, 'port': binding['port_id'], 'server': binding['nsx_service_id']}) def _validate_extra_dhcp_options(self, opts): if not opts or not self._has_native_dhcp_metadata(): return for opt in opts: opt_name = opt['opt_name'] opt_val = opt['opt_value'] if opt_name == 'classless-static-route': # separate validation for option121 if opt_val is not None: try: net, ip = opt_val.split(',') except Exception: msg = (_("Bad value %(val)s for DHCP option " "%(name)s") % {'name': opt_name, 'val': opt_val}) raise n_exc.InvalidInput(error_message=msg) elif not nsxlib_utils.get_dhcp_opt_code(opt_name): msg = (_("DHCP option %s is not supported") % opt_name) raise n_exc.InvalidInput(error_message=msg) def _is_ddi_supported_on_network(self, context, network_id, network=None): result, _ = self._is_ddi_supported_on_net_with_type( context, network_id, network=network) return result def _is_ddi_supported_on_net_with_type(self, context, network_id, network=None): # Get the network dictionary from the inputs if network: net = (network if isinstance(network, dict) else self._translate_net_db_2_dict(context, network)) else: net = self.get_network(context, network_id) # NSX current does not support transparent VLAN ports for # DHCP and metadata if cfg.CONF.vlan_transparent: if net.get('vlan_transparent') is True: return False, "VLAN transparent" # NSX current does not support flat network ports for # DHCP and metadata if net.get(pnet.NETWORK_TYPE) == utils.NsxV3NetworkTypes.FLAT: return False, "flat" # supported for overlay networks, and for vlan networks depending on # NSX version is_overlay = self._is_overlay_network(context, network_id) net_type = "overlay" if is_overlay else "non-overlay" return True, net_type def _has_no_dhcp_enabled_subnet(self, context, network): # Check if there is no DHCP-enabled subnet in the network. for subnet in network.subnets: if (subnet.enable_dhcp and subnet.ipv6_address_mode != constants.IPV6_SLAAC): return False return True def _has_single_dhcp_enabled_subnet(self, context, network): # Check if there is only one DHCP-enabled subnet in the network. count = 0 for subnet in network.subnets: if subnet.enable_dhcp and subnet.ip_version == 4: count += 1 if count > 1: return False return True if count == 1 else False def _cidrs_overlap(self, cidr0, cidr1): return cidr0.first <= cidr1.last and cidr1.first <= cidr0.last def _validate_address_space(self, context, subnet): # get the subnet IPs if ('allocation_pools' in subnet and validators.is_attr_set(subnet['allocation_pools'])): # use the pools instead of the cidr subnet_networks = [ netaddr.IPRange(pool.get('start'), pool.get('end')) for pool in subnet.get('allocation_pools')] else: cidr = subnet.get('cidr') if not validators.is_attr_set(cidr): return subnet_networks = [netaddr.IPNetwork(subnet['cidr'])] # Check if subnet overlaps with shared address space. # This is checked on the backend when attaching subnet to a router. shared_ips_cidrs = self._get_conf_attr('transit_networks') for subnet_net in subnet_networks: for shared_ips in shared_ips_cidrs: if netaddr.IPSet(subnet_net) & netaddr.IPSet([shared_ips]): msg = _("Subnet overlaps with shared address space " "%s") % shared_ips LOG.error(msg) raise n_exc.InvalidInput(error_message=msg) def _need_router_no_dnat_rules(self, subnet): # NAT is not supported for IPv6 return (subnet['ip_version'] == 4) def _need_router_snat_rules(self, context, router_id, subnet, gw_address_scope): # NAT is not supported for IPv6 if subnet['ip_version'] != 4: return False # if the subnets address scope is the same as the gateways: # no need for SNAT if gw_address_scope: subnet_address_scope = self._get_subnetpool_address_scope( context, subnet['subnetpool_id']) if (gw_address_scope == subnet_address_scope): LOG.info("No need for SNAT rule for router %(router)s " "and subnet %(subnet)s because they use the " "same address scope %(addr_scope)s.", {'router': router_id, 'subnet': subnet['id'], 'addr_scope': gw_address_scope}) return False return True def _get_mdproxy_port_name(self, net_name, net_id): return utils.get_name_and_uuid('%s-%s' % ('mdproxy', net_name or 'network'), net_id) def _create_net_mp_mdproxy_port(self, context, network, az, nsx_net_id): """Add MD proxy on the MP logical-switch by creating a logical port""" if (not self.nsxlib or not self._has_native_dhcp_metadata()): return is_ddi_network = self._is_ddi_supported_on_network( context, network['id'], network=network) if is_ddi_network: # Enable native metadata proxy for this network. tags = self.nsxlib.build_v3_tags_payload( network, resource_type='os-neutron-net-id', project_name=context.tenant_name) name = self._get_mdproxy_port_name(network['name'], network['id']) try: md_port = self.nsxlib.logical_port.create( nsx_net_id, az._native_md_proxy_uuid, tags=tags, name=name, attachment_type=nsxlib_consts.ATTACHMENT_MDPROXY) except nsx_lib_exc.ResourceNotFound: err_msg = (_('Logical switch %s or MD proxy %s do ' 'not exist') % (nsx_net_id, az._native_md_proxy_uuid)) LOG.error(err_msg) raise nsx_exc.NsxPluginException(err_msg=err_msg) LOG.debug("Created MD-Proxy logical port %(port)s " "for network %(network)s", {'port': md_port['id'], 'network': network['id']}) def _delete_nsx_port_by_network(self, network_id): if not self.nsxlib: return port_id = self.nsxlib.get_id_by_resource_and_tag( self.nsxlib.logical_port.resource_type, 'os-neutron-net-id', network_id) if port_id: self.nsxlib.logical_port.delete(port_id) def _validate_multiple_subnets_routers(self, context, router_id, net_id, subnet): network = self.get_network(context, net_id) # Unable to attach a trunked network to a router interface if cfg.CONF.vlan_transparent: if network.get('vlan_transparent') is True: err_msg = (_("Transparent VLAN networks cannot be attached to " "a logical router.")) LOG.error(err_msg) raise n_exc.InvalidInput(error_message=err_msg) intf_ports = self._get_network_interface_ports( context.elevated(), net_id) router_ids = [port['device_id'] for port in intf_ports if port['device_id']] if len(router_ids) > 0: err_msg = _("Only one subnet of each IP version in a network " "%(net_id)s can be attached to router, one subnet " "is already attached to router %(router_id)s") % { 'net_id': net_id, 'router_id': router_ids[0]} if router_id in router_ids: # We support 2 subnets from same net only for dual stack case if not subnet: # No IP provided on connected port LOG.error(err_msg) raise n_exc.InvalidInput(error_message=err_msg) for port in intf_ports: if port['device_id'] != router_id: continue if 'fixed_ips' in port and port['fixed_ips']: ex_subnet = self.get_subnet( context.elevated(), port['fixed_ips'][0]['subnet_id']) if ex_subnet['ip_version'] == subnet['ip_version']: # attach to the same router with same IP version LOG.error(err_msg) raise n_exc.InvalidInput(error_message=err_msg) else: # attach to multiple routers LOG.error(err_msg) raise l3_exc.RouterInterfaceAttachmentConflict(reason=err_msg) def _router_has_edge_fw_rules(self, context, router): if not router.gw_port_id: # No GW -> No rule on the edge firewall return False if self.fwaas_callbacks and self.fwaas_callbacks.fwaas_enabled: ports = self._get_router_interfaces(context, router.id) return self.fwaas_callbacks.router_with_fwg(context, ports) def _get_tz_restricted_vlans(self, tz_uuid): if not self.nsxlib: return [] restricted_vlans = [] known_profiles = [] # Get all transport nodes of this transport zone tns = self.nsxlib.transport_node.list()['results'] for tn in tns: # Find desired transport zone in host switches if ('host_switch_spec' in tn and 'host_switches' in tn['host_switch_spec']): for hs in tn['host_switch_spec']['host_switches']: # Check if it belongs to the current TZ tzs = [ep.get('transport_zone_id') for ep in hs.get('transport_zone_endpoints', [])] if tz_uuid not in tzs: # Move to next host switch for transport node continue profile_attrs = hs.get('host_switch_profile_ids', []) for profile_attr in profile_attrs: if profile_attr['key'] == 'UplinkHostSwitchProfile': profile_id = profile_attr['value'] if profile_id in known_profiles: LOG.debug("Profile %s has already been " "loaded, skipping", profile_id) continue profile = self.nsxlib.host_switch_profiles.get( profile_id) vlan_id = profile.get('transport_vlan') known_profiles.append(profile_id) if vlan_id: restricted_vlans.append(vlan_id) return restricted_vlans @api_replay_mode_wrapper def _create_floating_ip_wrapper(self, context, floatingip): initial_status = (constants.FLOATINGIP_STATUS_ACTIVE if floatingip['floatingip']['port_id'] else constants.FLOATINGIP_STATUS_DOWN) return super(NsxPluginV3Base, self).create_floatingip( context, floatingip, initial_status=initial_status) def _ensure_default_security_group(self, context, tenant_id): # NOTE(arosen): if in replay mode we'll create all the default # security groups for the user with their data so we don't # want this to be called. if not cfg.CONF.api_replay_mode: return super(NsxPluginV3Base, self)._ensure_default_security_group( context, tenant_id) def _handle_api_replay_default_sg(self, context, secgroup_db): """Set default api-replay migrated SG as default manually""" if (secgroup_db['name'] == 'default'): # this is a default security group copied from another cloud # Ugly patch! mark it as default manually with context.session.begin(subtransactions=True): try: default_entry = securitygroup_model.DefaultSecurityGroup( security_group_id=secgroup_db['id'], project_id=secgroup_db['project_id']) context.session.add(default_entry) except Exception as e: LOG.error("Failed to mark migrated security group %(id)s " "as default %(e)s", {'id': secgroup_db['id'], 'e': e}) def verify_sr_at_backend(self, context, router_id): """Should be implemented by each plugin""" pass class TagsCallbacks(object): target = oslo_messaging.Target( namespace=None, version='1.0') def __init__(self, **kwargs): super(TagsCallbacks, self).__init__() def info(self, ctxt, publisher_id, event_type, payload, metadata): # Make sure we catch only tags operations, and each one only once # tagging events look like 'tag.create/delete.start/end' if (event_type.startswith('tag.') and event_type.endswith('.end')): action = event_type.split('.')[1] is_delete = (action == 'delete') # Currently support only ports tags if payload.get('parent_resource') == 'ports': core_plugin = directory.get_plugin() port_id = payload.get('parent_resource_id') core_plugin.update_port_nsx_tags(ctxt, port_id, payload.get('tags'), is_delete=is_delete)