From 28c1c21b7b1fe0710df95a36063ca5c6cbace529 Mon Sep 17 00:00:00 2001 From: Adit Sarfaty Date: Sun, 9 Dec 2018 15:18:09 +0200 Subject: [PATCH] NSX|P: Add router advertisement & static routes Support router advertisment and setting static routes for the policy plugin. Change-Id: Ifb68b62ef3088ce602735cbc93200bbf8906a77b --- vmware_nsx/plugins/common_v3/plugin.py | 65 +++++++++++++++ vmware_nsx/plugins/nsx_p/plugin.py | 110 ++++++++++++++++++++++--- vmware_nsx/plugins/nsx_v3/plugin.py | 41 +-------- 3 files changed, 168 insertions(+), 48 deletions(-) diff --git a/vmware_nsx/plugins/common_v3/plugin.py b/vmware_nsx/plugins/common_v3/plugin.py index df0a9df124..2ac9bb0c31 100644 --- a/vmware_nsx/plugins/common_v3/plugin.py +++ b/vmware_nsx/plugins/common_v3/plugin.py @@ -35,6 +35,7 @@ from neutron_lib.exceptions import allowedaddresspairs as addr_exc from neutron_lib.exceptions import port_security as psec_exc from neutron_lib.plugins import utils as plugin_utils 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.common import exceptions as nsx_exc @@ -905,3 +906,67 @@ class NsxPluginV3Base(plugin.NsxPluginBase, orgaddr and not newaddr)) and not (fw_exist or lb_exist) return actions + + def _is_overlay_network(self): + """Should be implemented by each plugin""" + pass + + 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 _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) diff --git a/vmware_nsx/plugins/nsx_p/plugin.py b/vmware_nsx/plugins/nsx_p/plugin.py index d1c045de43..1b80ddcf0a 100644 --- a/vmware_nsx/plugins/nsx_p/plugin.py +++ b/vmware_nsx/plugins/nsx_p/plugin.py @@ -994,12 +994,10 @@ class NsxPolicyPlugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, for subnet in router_subnets: self._add_subnet_no_dnat_rule(context, router_id, subnet) - #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. + self.nsxpolicy.tier1.update_route_advertisement( + router_id, + nat=actions['advertise_route_nat_flag'], + subnets=actions['advertise_route_connected_flag']) if actions['remove_service_router']: # disable edge firewall before removing the service router @@ -1064,16 +1062,82 @@ class NsxPolicyPlugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, return ret_val + def _get_static_route_id(self, route): + return "%s-%s" % (route['destination'].replace('/', '_'), + route['nexthop']) + + def _add_static_routes(self, router_id, routes): + for route in routes: + dest = route['destination'] + self.nsxpolicy.tier1_static_route.create_or_overwrite( + 'Static route for %s' % dest, + router_id, + static_route_id=self._get_static_route_id(route), + network=dest, + next_hop=route['nexthop']) + + def _delete_static_routes(self, router_id, routes): + for route in routes: + self.nsxpolicy.tier1_static_route.delete( + router_id, + static_route_id=self._get_static_route_id(route)) + def update_router(self, context, router_id, router): gw_info = self._extract_external_gw(context, router, is_extract=False) router_data = router['router'] - LOG.debug("Updating router %s: %s. GW info %s", - router_id, router_data, gw_info) - #TODO(asarfaty) update the NSX logical router & interfaces + self._assert_on_router_admin_state(router_data) - return super(NsxPolicyPlugin, self).update_router( + if validators.is_attr_set(gw_info): + self._validate_update_router_gw(context, router_id, gw_info) + + routes_added = [] + routes_removed = [] + if 'routes' in router_data: + routes_added, routes_removed = self._get_static_routes_diff( + context, router_id, gw_info, router_data) + + # Update the neutron router + updated_router = super(NsxPolicyPlugin, self).update_router( context, router_id, router) + # Update the policy backend + try: + added_routes = removed_routes = False + # Updating name & description + if 'name' in router_data or 'description' in router_data: + router_name = utils.get_name_and_uuid( + updated_router.get('name') or 'router', + router_id) + self.nsxpolicy.tier1.update( + router_id, name=router_name, + description=updated_router.get('description')) + # Updating static routes + self._delete_static_routes(router_id, routes_removed) + removed_routes = True + self._add_static_routes(router_id, routes_added) + added_routes = True + + except (nsx_lib_exc.ResourceNotFound, nsx_lib_exc.ManagerError): + with excutils.save_and_reraise_exception(): + with db_api.CONTEXT_WRITER.using(context): + router_db = self._get_router(context, router_id) + router_db['status'] = const.NET_STATUS_ERROR + # return the static routes to the old state + if added_routes: + try: + self._delete_static_routes(router_id, routes_added) + except Exception as e: + LOG.error("Rollback router %s changes failed to " + "delete static routes: %s", router_id, e) + if removed_routes: + try: + self._add_static_routes(router_id, routes_removed) + except Exception as e: + LOG.error("Rollback router %s changes failed to add " + "static routes: %s", router_id, e) + + return updated_router + def add_router_interface(self, context, router_id, interface_info): LOG.info("Adding router %s interface %s", router_id, interface_info) network_id = self._get_interface_network(context, interface_info) @@ -1739,3 +1803,29 @@ class NsxPolicyPlugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, super(NsxPolicyPlugin, self).delete_security_group_rule( context, rule_id) + + def _is_overlay_network(self, context, network_id): + """Return True if this is an overlay network + + 1. No binding ("normal" overlay networks will have no binding) + 2. Geneve network + 3. nsx network where the backend network is connected to an overlay TZ + """ + bindings = nsx_db.get_network_bindings(context.session, network_id) + # With NSX plugin, "normal" overlay networks will have no binding + if not bindings: + # using the default /AZ overlay_tz + return True + + binding = bindings[0] + if binding.binding_type == utils.NsxV3NetworkTypes.GENEVE: + return True + if binding.binding_type == utils.NsxV3NetworkTypes.NSX_NETWORK: + # check the backend network + segment = self.nsxpolicy.segments.get(binding.phy_uuid) + tz = self._get_nsx_net_tz_id(segment) + if tz: + type = self.nsxpolicy.transport_zone.get_transport_type( + tz) + return type == nsxlib_consts.TRANSPORT_TYPE_OVERLAY + return False diff --git a/vmware_nsx/plugins/nsx_v3/plugin.py b/vmware_nsx/plugins/nsx_v3/plugin.py index dbacd0f3cb..58fc606440 100644 --- a/vmware_nsx/plugins/nsx_v3/plugin.py +++ b/vmware_nsx/plugins/nsx_v3/plugin.py @@ -71,7 +71,6 @@ from neutron_lib.callbacks import resources from neutron_lib import constants as const from neutron_lib import context as q_context from neutron_lib import exceptions as n_exc -from neutron_lib.utils import helpers from oslo_config import cfg from oslo_db import exception as db_exc from oslo_log import log @@ -3369,12 +3368,6 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, action="NO_DNAT", match_destination_network=subnet['cidr']) - 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 validate_router_dhcp_relay(self, context): """Fail router creation dhcp relay is configured without IPAM""" if (self._availability_zones_data.dhcp_relay_configured() and @@ -3495,29 +3488,6 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, # get the availability zones from the hints return [self.get_router_az(router).name] - 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 _update_router_wrapper(self, context, router_id, router): if cfg.CONF.api_replay_mode: # NOTE(arosen): the mock.patch here is needed for api_replay_mode @@ -3535,6 +3505,7 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, self._assert_on_router_admin_state(router_data) if validators.is_attr_set(gw_info): + self._validate_update_router_gw(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 @@ -3565,14 +3536,8 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin, routes_removed = [] try: if 'routes' in 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) + routes_added, routes_removed = self._get_static_routes_diff( + context, router_id, gw_info, router_data) nsx_router_id = nsx_db.get_nsx_router_id(context.session, router_id) for route in routes_removed: