From 183b86b1f027b88b235e0718c22bd0ae21cede78 Mon Sep 17 00:00:00 2001 From: Adit Sarfaty Date: Wed, 7 Nov 2018 10:39:39 +0200 Subject: [PATCH] NSX|P: Basic router interface & GW support Additional actions will be added with nsxlib support Depends-on: I10a3f691b33e37e1cd8ec8094f4bfa89d7a96f35 Change-Id: I92fff433646202a0245c1cef9630173fe245a296 --- devstack/tools/nsxp_cleanup.py | 3 + vmware_nsx/plugins/common_v3/plugin.py | 108 +++++++++++++++++ vmware_nsx/plugins/nsx_p/plugin.py | 160 ++++++++++++++++++++++--- vmware_nsx/plugins/nsx_v3/plugin.py | 110 +++-------------- 4 files changed, 269 insertions(+), 112 deletions(-) diff --git a/devstack/tools/nsxp_cleanup.py b/devstack/tools/nsxp_cleanup.py index 30e83f13eb..489e5c86f8 100755 --- a/devstack/tools/nsxp_cleanup.py +++ b/devstack/tools/nsxp_cleanup.py @@ -150,7 +150,10 @@ class NSXClient(object): segments = self.get_os_nsx_segments() print("Number of OS segments to be deleted: %s" % len(segments)) for s in segments: + # Delete all the ports self.cleanup_segment_ports(s['id']) + # Disassociate from a tier1 router + self.nsxlib.segment.update(s['id'], tier1_id=None) self.nsxlib.segment.delete(s['id']) def get_os_nsx_segment_ports(self, segment_id): diff --git a/vmware_nsx/plugins/common_v3/plugin.py b/vmware_nsx/plugins/common_v3/plugin.py index 3e30f1cdbf..9c72d0d772 100644 --- a/vmware_nsx/plugins/common_v3/plugin.py +++ b/vmware_nsx/plugins/common_v3/plugin.py @@ -14,6 +14,7 @@ # under the License. +import netaddr from oslo_config import cfg from oslo_log import log as logging from six import moves @@ -789,3 +790,110 @@ class NsxPluginV3Base(plugin.NsxPluginBase, 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 _validate_router_gw(self, context, router_id, info, org_enable_snat): + # 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) + + def _get_update_router_gw_actions( + self, + org_tier0_uuid, orgaddr, org_enable_snat, + new_tier0_uuid, newaddr, new_enable_snat): + """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) + + # TODO(asarfaty): calculate flags for add/remove service router + actions['remove_service_router'] = ( + actions['remove_router_link_port'] and + not actions['add_router_link_port']) + actions['add_service_router'] = ( + actions['add_router_link_port'] and + not actions['remove_router_link_port']) + + return actions diff --git a/vmware_nsx/plugins/nsx_p/plugin.py b/vmware_nsx/plugins/nsx_p/plugin.py index 4c9e61adbb..b0143074d2 100644 --- a/vmware_nsx/plugins/nsx_p/plugin.py +++ b/vmware_nsx/plugins/nsx_p/plugin.py @@ -16,6 +16,7 @@ import netaddr from oslo_config import cfg +from oslo_db import exception as db_exc from oslo_log import log from oslo_utils import excutils from oslo_utils import uuidutils @@ -45,6 +46,7 @@ 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 +from neutron_lib.api.definitions import provider_net as pnet from neutron_lib.api.definitions import vlantransparent as vlan_apidef from neutron_lib.api import faults from neutron_lib.api import validators @@ -76,6 +78,7 @@ from vmware_nsx.plugins.nsx_v3 import utils as v3_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 policy_constants +from vmware_nsxlib.v3 import policy_defs from vmware_nsxlib.v3 import utils as nsxlib_utils LOG = log.getLogger(__name__) @@ -793,11 +796,100 @@ class NsxPolicyPlugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, return (ports if not fields else [db_utils.resource_fields(port, fields) for port in ports]) + 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): + return self.default_tier0_router + else: + return network.get(pnet.PHYSICAL_NETWORK) + + 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 _update_router_gw_info(self, context, router_id, info): + # Get the original data of the router GW router = self._get_router(context, router_id) + org_tier0_uuid = self._get_tier0_uuid_by_router(context, router) + org_enable_snat = router.enable_snat + orgaddr, orgmask, _orgnexthop = ( + self._get_external_attachment_info( + context, router)) + self._validate_router_gw(context, router_id, info, org_enable_snat) + + # First update the neutron DB super(NsxPolicyPlugin, self)._update_router_gw_info( context, router_id, info, router=router) - #TODO(asarfaty): Update the NSX + + # Get the new tier0 of the updated router (or None if GW was removed) + new_tier0_uuid = self._get_tier0_uuid_by_router(context, router) + new_enable_snat = router.enable_snat + newaddr, newmask, _newnexthop = self._get_external_attachment_info( + context, router) + router_name = utils.get_name_and_uuid(router['name'] or 'router', + router['id']) + router_subnets = self._find_router_subnets( + context.elevated(), router_id) + actions = self._get_update_router_gw_actions( + org_tier0_uuid, orgaddr, org_enable_snat, + new_tier0_uuid, newaddr, new_enable_snat) + + if actions['add_service_router']: + edge_cluster = self.nsxpolicy.tier0.get_edge_cluster_path( + new_tier0_uuid) + if edge_cluster: + self.nsxpolicy.tier1.set_edge_cluster_path( + router_id, edge_cluster) + + if actions['remove_snat_rules']: + #self.nsxpolicy.tier1.delete_gw_snat_rules(nsx_router_id, orgaddr) + pass + if actions['remove_no_dnat_rules']: + for subnet in router_subnets: + #self._del_subnet_no_dnat_rule(context, nsx_router_id, subnet) + pass + + if (actions['remove_router_link_port'] or + actions['add_router_link_port']): + # GW was changed + #TODO(asarfaty): adding the router name even though it was not + # changed because otherwise the NSX will set it to default. + # This code should be removed once NSX supports it. + self.nsxpolicy.tier1.update(router_id, name=router_name, + tier0=new_tier0_uuid) + + # Set/Unset the router TZ to allow vlan switches traffic + #TODO(asarfaty) no api for this yet + + if actions['add_snat_rules']: + # Add SNAT rules for all the subnets which are in different scope + # than the GW + #gw_address_scope = self._get_network_address_scope( + # context, router.gw_port.network_id) + for subnet in router_subnets: + #self._add_subnet_snat_rule(context, router_id, nsx_router_id, + # subnet, gw_address_scope, newaddr) + pass + if actions['add_no_dnat_rules']: + for subnet in router_subnets: + #self._add_subnet_no_dnat_rule(context, nsx_router_id, subnet) + pass + + #self.nsxpolicy.tier1.update_route_advertisement( + # router_id, + # actions['advertise_route_nat_flag'], + # actions['advertise_route_connected_flag']) + + # TODO(asarfaty): handle enable/disable snat, router adv flags, etc. + + if actions['remove_service_router']: + # disable edge firewall before removing the service router + #TODO(asarfaty) no api for this yet + + # remove the edge cluster + self.nsxpolicy.tier1.remove_edge_cluster(router_id) def create_router(self, context, router): r = router['router'] @@ -812,11 +904,10 @@ class NsxPolicyPlugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, router['id']) tags = self.nsxpolicy.build_v3_api_version_project_tag( context.tenant_name) - #TODO(annak): handle GW try: self.nsxpolicy.tier1.create_or_overwrite( router_name, router['id'], - tier0=self.default_tier0_router, + tier0=None, tags=tags) #TODO(annak): narrow down the exception except Exception as ex: @@ -826,8 +917,19 @@ class NsxPolicyPlugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, {'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) + if gw_info and gw_info != const.ATTR_NOT_SPECIFIED: + try: + self._update_router_gw_info(context, router['id'], gw_info) + except (db_exc.DBError, nsx_lib_exc.ManagerError): + with excutils.save_and_reraise_exception(): + LOG.error("Failed to set gateway info for router " + "being created: %s - removing router", + router['id']) + self.delete_router(context, router['id']) + LOG.info("Create router failed while setting external " + "gateway. Router:%s has been removed from " + "DB and backend", + router['id']) return self.get_router(context, router['id']) def delete_router(self, context, router_id): @@ -859,10 +961,7 @@ class NsxPolicyPlugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, network_id = self._get_interface_network(context, interface_info) extern_net = self._network_is_external(context, network_id) router_db = self._get_router(context, router_id) - gw_network_id = (router_db.gw_port.network_id if router_db.gw_port - else None) - LOG.debug("Adding router %s interface %s with GW %s", - router_id, network_id, gw_network_id) + # A router interface cannot be an external network if extern_net: msg = _("An external network cannot be attached as " @@ -874,29 +973,54 @@ class NsxPolicyPlugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, context, router_id, interface_info) self._validate_interface_address_scope(context, router_db, info) - subnet = self.get_subnet(context, info['subnet_ids'][0]) - segment_id = self._get_network_nsx_segment_id(context, network_id) # 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(segment_id, - tier1_id=router_id) + #TODO(asarfaty): adding the segment name even though it was not + # changed because otherwise the NSX will set it to default. + # This code should be removed once NSX supports it. + net = self._get_network(context, network_id) + net_name = utils.get_name_and_uuid( + net['name'] or 'network', network_id) + segment_id = self._get_network_nsx_id(context, network_id) + subnet = self.get_subnet(context, info['subnet_ids'][0]) + pol_subnet = policy_defs.Subnet( + gateway_address=("%s/32" % subnet.get('gateway_ip'))) + self.nsxpolicy.segment.update(segment_id, + name=net_name, + tier1_id=router_id, + subnets=[pol_subnet]) except Exception as ex: with excutils.save_and_reraise_exception(): - LOG.error('Failed to create router interface for subnet ' + LOG.error('Failed to create router interface for network ' '%(id)s on NSX backend. Exception: %(e)s', - {'id': subnet['id'], 'e': ex}) + {'id': network_id, 'e': ex}) self.remove_router_interface( context, router_id, interface_info) return info def remove_router_interface(self, context, router_id, interface_info): - #TODO(asarfaty) Update the NSX logical router ports + # Update the neutron router first info = super(NsxPolicyPlugin, self).remove_router_interface( context, router_id, interface_info) + network_id = info['network_id'] + # Remove the tier1 router from this segment on the nSX + try: + #TODO(asarfaty): adding the segment name even though it was not + # changed because otherwise the NSX will set it to default. + # This code should be removed once NSX supports it. + net = self._get_network(context, network_id) + net_name = utils.get_name_and_uuid( + net['name'] or 'network', network_id) + segment_id = self._get_network_nsx_id(context, network_id) + self.nsxpolicy.segment.update(segment_id, name=net_name, + tier1_id=None) + except Exception as ex: + # do not fail the neutron action + LOG.error('Failed to remove router interface for network ' + '%(id)s on NSX backend. Exception: %(e)s', + {'id': network_id, 'e': ex}) return info def create_floatingip(self, context, floatingip): diff --git a/vmware_nsx/plugins/nsx_v3/plugin.py b/vmware_nsx/plugins/nsx_v3/plugin.py index 7bdef10eb6..795db1feb8 100644 --- a/vmware_nsx/plugins/nsx_v3/plugin.py +++ b/vmware_nsx/plugins/nsx_v3/plugin.py @@ -3157,31 +3157,6 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, return (ports if not fields else [db_utils.resource_fields(port, fields) for port in ports]) - 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 @@ -3221,15 +3196,7 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, orgaddr, orgmask, _orgnexthop = ( self._get_external_attachment_info( context, router)) - - # 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) + self._validate_router_gw(context, router_id, info, org_enable_snat) router_subnets = self._find_router_subnets( context.elevated(), router_id) @@ -3254,72 +3221,26 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, context, router)) nsx_router_id = nsx_db.get_nsx_router_id(context.session, router_id) - # Remove router link port between tier1 and tier0 if tier0 router link - # is removed or changed - remove_router_link_port = (org_tier0_uuid and - (not new_tier0_uuid or - org_tier0_uuid != new_tier0_uuid)) + actions = self._get_update_router_gw_actions( + org_tier0_uuid, orgaddr, org_enable_snat, + new_tier0_uuid, newaddr, new_enable_snat) - # Remove SNAT rules for gw ip if gw ip is deleted/changed or - # enable_snat is updated from True to False - 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 - 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 - 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 - 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 - 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 - 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 - 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. - advertise_route_nat_flag = True if new_enable_snat else False - advertise_route_connected_flag = True if not new_enable_snat else False - - if revocate_bgp_announce: + if actions['revocate_bgp_announce']: # TODO(berlin): revocate bgp announce on org tier0 router pass - if remove_snat_rules: + if actions['remove_snat_rules']: self.nsxlib.router.delete_gw_snat_rules(nsx_router_id, orgaddr) - if remove_no_dnat_rules: + if actions['remove_no_dnat_rules']: for subnet in router_subnets: self._del_subnet_no_dnat_rule(context, nsx_router_id, subnet) - if remove_router_link_port: + if actions['remove_router_link_port']: # remove the link port and reset the router transport zone self.nsxlib.router.remove_router_link_port(nsx_router_id) if self.nsxlib.feature_supported( nsxlib_consts.FEATURE_ROUTER_TRANSPORT_ZONE): self.nsxlib.router.update_router_transport_zone( nsx_router_id, None) - if add_router_link_port: + if actions['add_router_link_port']: # First update edge cluster info for router edge_cluster_uuid = self._get_edge_cluster(new_tier0_uuid) self.nsxlib.router.update_router_edge_cluster( @@ -3338,7 +3259,7 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, self.nsxlib.router.add_router_link_port(nsx_router_id, new_tier0_uuid, tags=tags) - if add_snat_rules: + if actions['add_snat_rules']: # Add SNAT rules for all the subnets which are in different scope # than the gw gw_address_scope = self._get_network_address_scope( @@ -3346,17 +3267,18 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, for subnet in router_subnets: self._add_subnet_snat_rule(context, router_id, nsx_router_id, subnet, gw_address_scope, newaddr) - if add_no_dnat_rules: + if actions['add_no_dnat_rules']: for subnet in router_subnets: self._add_subnet_no_dnat_rule(context, nsx_router_id, subnet) - if bgp_announce: + if actions['bgp_announce']: # TODO(berlin): bgp announce on new tier0 router pass - self.nsxlib.router.update_advertisement(nsx_router_id, - advertise_route_nat_flag, - advertise_route_connected_flag) + self.nsxlib.router.update_advertisement( + nsx_router_id, + actions['advertise_route_nat_flag'], + actions['advertise_route_connected_flag']) def _add_subnet_snat_rule(self, context, router_id, nsx_router_id, subnet, gw_address_scope, gw_ip):