From 4f6730911c437e597b0522baa620aecd12a8cf23 Mon Sep 17 00:00:00 2001 From: Anna Khmelnitsky Date: Fri, 21 Sep 2018 18:30:02 -0700 Subject: [PATCH] NSX|P: Initial connectivity support Not covered yet: DHCP, Switching Profiles, TZs, provider networks, GW, NAT Change-Id: I9f4163d8a233b0d3914363556ddf6ec0f375f99a --- vmware_nsx/common/config.py | 10 +- vmware_nsx/plugins/common/plugin.py | 53 +++++++ vmware_nsx/plugins/nsx_p/plugin.py | 170 ++++++++++++++++++--- vmware_nsx/plugins/nsx_v3/plugin.py | 49 +----- vmware_nsx/tests/unit/nsx_p/test_plugin.py | 5 + 5 files changed, 223 insertions(+), 64 deletions(-) diff --git a/vmware_nsx/common/config.py b/vmware_nsx/common/config.py index 2f08d6412e..fc91ff256a 100644 --- a/vmware_nsx/common/config.py +++ b/vmware_nsx/common/config.py @@ -482,7 +482,15 @@ nsx_v3_opts = nsx_v3_and_p + [ ] -nsx_p_opts = nsx_v3_and_p + [] +nsx_p_opts = nsx_v3_and_p + [ + cfg.StrOpt('default_tier0_router', + help=_("Name or UUID of the default tier0 router that will be " + "used for connecting to tier1 logical routers and " + "configuring external networks. If only one tier0 " + " router is present on backend, it will be assumed " + "as default unless this value is provided")), + ] + DEFAULT_STATUS_CHECK_INTERVAL = 2000 DEFAULT_MINIMUM_POOLED_EDGES = 1 diff --git a/vmware_nsx/plugins/common/plugin.py b/vmware_nsx/plugins/common/plugin.py index 0c526053ac..59e2ec329f 100644 --- a/vmware_nsx/plugins/common/plugin.py +++ b/vmware_nsx/plugins/common/plugin.py @@ -24,6 +24,7 @@ from neutron.db import l3_attrs_db from neutron.db import l3_db from neutron.db import models_v2 from neutron_lib.api.definitions import address_scope as ext_address_scope +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 network as net_def @@ -38,12 +39,14 @@ from neutron_lib.callbacks import resources from neutron_lib import constants from neutron_lib import context as n_context from neutron_lib import exceptions as n_exc +from neutron_lib.exceptions import allowedaddresspairs as addr_exc from neutron_lib.plugins import directory from neutron_lib.services.qos import constants as qos_consts from neutron_lib.utils import net from vmware_nsx._i18n import _ from vmware_nsx.common import exceptions as nsx_exc +from vmware_nsx.common import utils from vmware_nsx.extensions import maclearning as mac_ext from vmware_nsx.extensions import secgroup_rule_local_ip_prefix as sg_prefix from vmware_nsx.services.qos.common import utils as qos_com_utils @@ -567,6 +570,24 @@ class NsxPluginBase(db_base_plugin_v2.NeutronDbPluginV2, self._assert_on_vpn_port_change(original_port) self._assert_on_lb_port_fixed_ip_change(port_data, orig_dev_owner) + 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 _process_extra_attr_router_create(self, context, router_db, r): for extra_attr in l3_attrs_db.get_attr_info().keys(): if (extra_attr in r and @@ -595,6 +616,38 @@ class NsxPluginBase(db_base_plugin_v2.NeutronDbPluginV2, sg_rule[sg_prefix.LOCAL_IP_PREFIX].startswith('0.0.0.0/')): sg_rule[sg_prefix.LOCAL_IP_PREFIX] = None + def _validate_interface_address_scope(self, context, + router_db, interface_info): + gw_network_id = (router_db.gw_port.network_id if router_db.gw_port + else None) + + subnet = self.get_subnet(context, interface_info['subnet_ids'][0]) + 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, subnet['id']) + + def _validate_ipv4_address_pairs(self, address_pairs): + for pair in address_pairs: + ip = pair.get('ip_address') + if not utils.is_ipv4_ip_address(ip): + raise nsx_exc.InvalidIPAddress(ip_address=ip) + + # NSXv3 and Policy only + 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_ipv4_address_pairs(address_pairs) + self._process_create_allowed_address_pairs(context, port_data, + address_pairs) + else: + port_data[addr_apidef.ADDRESS_PAIRS] = [] + def get_housekeeper(self, context, name, fields=None): # run the job in readonly mode and get the results self.housekeeper.run(context, name, readonly=True) diff --git a/vmware_nsx/plugins/nsx_p/plugin.py b/vmware_nsx/plugins/nsx_p/plugin.py index 40c01a5b05..a98889d0d6 100644 --- a/vmware_nsx/plugins/nsx_p/plugin.py +++ b/vmware_nsx/plugins/nsx_p/plugin.py @@ -15,6 +15,8 @@ import sys +import netaddr + from oslo_config import cfg from oslo_log import log from oslo_utils import excutils @@ -30,6 +32,7 @@ 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 l3 as l3_db_models from neutron.db.models import securitygroup as securitygroup_model # noqa @@ -40,6 +43,7 @@ from neutron.db import securitygroups_db from neutron.db import vlantransparent_db from neutron.extensions import providernet from neutron.quota import resource_registry +from neutron_lib.api.definitions import allowedaddresspairs as addr_apidef from neutron_lib.api.definitions import external_net from neutron_lib.api.definitions import l3 as l3_apidef from neutron_lib.api.definitions import port_security as psec @@ -161,6 +165,7 @@ class NsxPolicyPlugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, self.cfg_group = 'nsx_p' # group name for nsx_p section in nsx.ini + self._init_default_config() self._prepare_default_rules() # subscribe the init complete method last, so it will be called only @@ -169,6 +174,38 @@ class NsxPolicyPlugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, resources.PROCESS, events.AFTER_INIT) + # NOTE(annak): we may need to generalize this for API calls + # requiring path ids + def _init_default_resource(self, resource_api, name_or_id): + if not name_or_id: + # If not specified, the system will auto-configure + # in case only single resource is present + resources = resource_api.list() + if len(resources) == 1: + return resources[0]['id'] + else: + return None + + try: + resource_api.get(name_or_id) + return name_or_id + except nsx_lib_exc.ResourceNotFound: + try: + resource = resource_api.get_by_name(name_or_id) + if resource: + return resource['id'] + except nsx_lib_exc.ResourceNotFound: + return None + + def _init_default_config(self): + self.default_tier0_router = self._init_default_resource( + self.nsxpolicy.tier0, + cfg.CONF.nsx_p.default_tier0_router) + + if not self.default_tier0_router: + raise cfg.RequiredOptError("default_tier0_router", + group=cfg.OptGroup('nsx_p')) + def _validate_nsx_policy_version(self): self._nsx_version = self.nsxpolicy.get_version() LOG.info("NSX Version: %s", self._nsx_version) @@ -219,8 +256,25 @@ class NsxPolicyPlugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, }) def _create_network_at_the_backend(self, context, net_data): - #TODO(asarfaty): implement, using nsx-id the same as the neutron id - pass + # TODO(annak): provider network + net_data['id'] = net_data.get('id') or uuidutils.generate_uuid() + + net_name = utils.get_name_and_uuid(net_data['name'] or 'network', + net_data['id']) + tags = self.nsxpolicy.build_v3_tags_payload( + net_data, resource_type='os-neutron-net-id', + project_name=context.tenant_name) + + # TODO(annak): admin state config is missing on policy + # should we not create networks that are down? + # alternative - configure status on manager for time being + # admin_state = net_data.get('admin_state_up', True) + + self.nsxpolicy.segment.create_or_overwrite( + net_name, + segment_id=net_data['id'], + description=net_data.get('description'), + tags=tags) def _validate_external_net_create(self, net_data): #TODO(asarfaty): implement @@ -255,7 +309,7 @@ class NsxPolicyPlugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, # Create the backend NSX network if not is_external_net: try: - self._create_network_at_the_backend(context, net_data) + self._create_network_at_the_backend(context, created_net) except Exception as e: LOG.exception("Failed to create NSX network network: %s", e) with excutils.save_and_reraise_exception(): @@ -272,11 +326,10 @@ class NsxPolicyPlugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, def delete_network(self, context, network_id): with db_api.context_manager.writer.using(context): self._process_l3_delete(context, network_id) - return super(NsxPolicyPlugin, self).delete_network( + super(NsxPolicyPlugin, self).delete_network( context, network_id) if not self._network_is_external(context, network_id): - # TODO(asarfaty) delete the NSX logical network - pass + self.nsxpolicy.segment.delete(network_id) def update_network(self, context, id, network): original_net = super(NsxPolicyPlugin, self).get_network(context, id) @@ -352,13 +405,51 @@ class NsxPolicyPlugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, return updated_subnet + def _build_address_bindings(self, port): + address_bindings = [] + for fixed_ip in port['fixed_ips']: + if netaddr.IPNetwork(fixed_ip['ip_address']).version != 4: + #TODO(annak): enable when IPv6 is supported + continue + binding = self.nsxpolicy.segment_port.build_address_binding( + fixed_ip['ip_address'], port['mac_address']) + address_bindings.append(binding) + + for pair in port.get(addr_apidef.ADDRESS_PAIRS): + binding = self.nsxpolicy.segment_port.build_address_binding( + pair['ip_address'], pair['mac_address']) + address_bindings.append(binding) + + return address_bindings + def _create_port_at_the_backend(self, context, port_data): - #TODO(asarfaty): implement - pass + # TODO(annak): admin_state not supported by policy + # TODO(annak): handle exclude list + # TODO(annak): switching profiles when supported + name = self._build_port_name(context, port_data) + psec, has_ip = self._determine_port_security_and_has_ip(context, + port_data) + address_bindings = (self._build_address_bindings(port_data) + if psec else None) + device_owner = port_data.get('device_owner') + vif_id = None + if device_owner and device_owner != l3_db.DEVICE_OWNER_ROUTER_INTF: + vif_id = port_data['id'] + + self.nsxpolicy.segment_port.create_or_overwrite( + port_data['network_id'], + port_data['id'], + name, + description=port_data.get('description'), + address_bindings=address_bindings, + vif_id=vif_id) def _cleanup_port(self, context, port_id, lport_id): super(NsxPolicyPlugin, self).delete_port(context, port_id) - #TODO(asarfaty): Delete the NSX logical port + + port_data = self.get_port(context, port_id) + self.nsxpolicy.segment_port.delete( + port_data['network_id'], port_data['id']) def base_create_port(self, context, port): neutron_db = super(NsxPolicyPlugin, self).create_port(context, port) @@ -380,6 +471,8 @@ class NsxPolicyPlugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, neutron_db = self.base_create_port(context, port) port["port"].update(neutron_db) + self._create_port_address_pairs(context, port_data) + if not is_external_net: try: self._create_port_at_the_backend(context, port_data) @@ -403,10 +496,15 @@ class NsxPolicyPlugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, l3_port_check=True, l2gw_port_check=True, force_delete_dhcp=False, force_delete_vpn=False): - port = self.get_port(context, port_id) - if not self._network_is_external(context, port['network_id']): - #TODO(asarfaty): Delete the NSX logical port - pass + port_data = self.get_port(context, port_id) + if not self._network_is_external(context, port_data['network_id']): + try: + self.nsxpolicy.segment_port.delete( + port_data['network_id'], port_data['id']) + except Exception as ex: + LOG.error("Failed to delete port %(id)s on NSX backend " + "due to %(e)s", + {'id': port_data['id'], 'e': ex}) self.disassociate_floatingips(context, port_id) @@ -510,7 +608,22 @@ class NsxPolicyPlugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, context, router) router_db = self._get_router(context, router['id']) self._process_extra_attr_router_create(context, router_db, r) - #TODO(asarfaty): Create the NSX logical router and add DB mapping + + router_name = utils.get_name_and_uuid(router['name'] or 'router', + router['id']) + #TODO(annak): handle GW + try: + self.nsxpolicy.tier1.create_or_overwrite( + router_name, router['id'], + tier0=self.default_tier0_router) + #TODO(annak): narrow down the exception + except Exception as ex: + with excutils.save_and_reraise_exception(): + LOG.error('Failed to create router %(id)s ' + 'on NSX backend. Exception: %(e)s', + {'id': router['id'], 'e': ex}) + self.delete_router(context, router['id']) + LOG.debug("Created router %s: %s. GW info %s", router['id'], r, gw_info) return self.get_router(context, router['id']) @@ -519,13 +632,14 @@ class NsxPolicyPlugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, router = self.get_router(context, router_id) if router.get(l3_apidef.EXTERNAL_GW_INFO): self._update_router_gw_info(context, router_id, {}) - nsx_router_id = nsx_db.get_nsx_router_id( - context.session, router_id) ret_val = super(NsxPolicyPlugin, self).delete_router( context, router_id) - if nsx_router_id: - #TODO(asarfaty): delete the NSX logical router - pass + + try: + self.nsxpolicy.tier1.delete(router_id) + except Exception as ex: + LOG.error("Failed to delete NSX T1 router %(id)s: %(e)s", { + 'e': ex, 'id': router_id}) return ret_val @@ -557,7 +671,23 @@ class NsxPolicyPlugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, info = super(NsxPolicyPlugin, self).add_router_interface( context, router_id, interface_info) - #TODO(asarfaty) Update the NSX logical router ports + self._validate_interface_address_scope(context, router_db, info) + subnet = self.get_subnet(context, info['subnet_ids'][0]) + + # TODO(annak): Validate TZ + try: + # This is always an overwrite call + # NOTE: Connecting network to multiple routers is not supported + self.nsxpolicy.segment.create_or_overwrite(network_id, + tier1_id=router_id) + except Exception as ex: + with excutils.save_and_reraise_exception(): + LOG.error('Failed to create router interface for subnet ' + '%(id)s on NSX backend. Exception: %(e)s', + {'id': subnet['id'], 'e': ex}) + self.remove_router_interface( + context, router_id, interface_info) + return info def remove_router_interface(self, context, router_id, interface_info): diff --git a/vmware_nsx/plugins/nsx_v3/plugin.py b/vmware_nsx/plugins/nsx_v3/plugin.py index 2c9184eadc..eb61331562 100644 --- a/vmware_nsx/plugins/nsx_v3/plugin.py +++ b/vmware_nsx/plugins/nsx_v3/plugin.py @@ -1727,7 +1727,7 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, LOG.debug("Created logical DHCP server %(server)s for network " "%(network)s", {'server': dhcp_server['id'], 'network': network['id']}) - name = self._get_port_name(context, port_data) + 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, @@ -2226,24 +2226,6 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, net_name or 'network'), net_id) - def _get_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 == const.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(const.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 _get_qos_profile_id(self, context, policy_id): switch_profile_id = nsx_db.get_switch_profile_by_qos_policy( context.session, policy_id) @@ -2378,7 +2360,7 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, profiles.append(self._mac_learning_profile) profiles.append(self._no_switch_security) - name = self._get_port_name(context, port_data) + name = self._build_port_name(context, port_data) nsx_net_id = self._get_network_nsx_id(context, port_data['network_id']) try: result = self.nsxlib.logical_port.create( @@ -2412,12 +2394,6 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, return result - def _validate_address_pairs(self, address_pairs): - for pair in address_pairs: - ip = pair.get('ip_address') - if not utils.is_ipv4_ip_address(ip): - raise nsx_exc.InvalidIPAddress(ip_address=ip) - 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 @@ -2462,18 +2438,7 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, context, port_data, neutron_db) # allowed address pair checks - 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._process_create_allowed_address_pairs( - context, neutron_db, - address_pairs) - else: - # remove ATTR_NOT_SPECIFIED - port_data[addr_apidef.ADDRESS_PAIRS] = [] + self._create_port_address_pairs(context, port_data) if port_security and has_ip: self._ensure_default_security_group_on_port(context, port) @@ -3147,7 +3112,7 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, raise addr_exc.AddressPairAndPortSecurityRequired() if delete_addr_pairs or has_addr_pairs: - self._validate_address_pairs( + self._validate_ipv4_address_pairs( updated_port[addr_apidef.ADDRESS_PAIRS]) # delete address pairs and read them in self._delete_allowed_address_pairs(context, id) @@ -3245,7 +3210,7 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, attachment_type = None vif_uuid = None - name = self._get_port_name(context, updated_port) + name = self._build_port_name(context, updated_port) # Update exclude list if necessary updated_ps = updated_port.get('port_security_enabled') @@ -4333,9 +4298,7 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, # If it is a no-snat router, interface address scope must be the # same as the gateways - if not router_db.enable_snat and gw_network_id: - self._validate_address_scope_for_router_interface( - context.elevated(), router_id, gw_network_id, subnet['id']) + self._validate_interface_address_scope(context, router_db, info) nsx_router_id = nsx_db.get_nsx_router_id(context.session, router_id) diff --git a/vmware_nsx/tests/unit/nsx_p/test_plugin.py b/vmware_nsx/tests/unit/nsx_p/test_plugin.py index b5d9d5b655..a31befcf03 100644 --- a/vmware_nsx/tests/unit/nsx_p/test_plugin.py +++ b/vmware_nsx/tests/unit/nsx_p/test_plugin.py @@ -37,11 +37,16 @@ class NsxPPluginTestCaseMixin( ext_mgr=ext_mgr) def _mock_nsx_policy_backend_calls(self): + resource_list_result = {'results': [{'id': 'test', + 'display_name': 'test'}]} mock.patch( "vmware_nsxlib.v3.NsxPolicyLib.get_version", return_value=nsx_constants.NSX_VERSION_2_4_0).start() mock.patch( "vmware_nsxlib.v3.client.RESTClient.get").start() + mock.patch( + "vmware_nsxlib.v3.client.RESTClient.list", + return_value=resource_list_result).start() mock.patch( "vmware_nsxlib.v3.client.RESTClient.patch").start() mock.patch(