diff --git a/vmware_nsx/common/utils.py b/vmware_nsx/common/utils.py index 982149045c..a9872fdcbd 100644 --- a/vmware_nsx/common/utils.py +++ b/vmware_nsx/common/utils.py @@ -106,3 +106,44 @@ def retry_upon_exception_nsxv3(exc, delay=500, max_delay=2000, wait_exponential_multiplier=delay, wait_exponential_max=max_delay, stop_max_attempt_number=max_attempts) + + +def list_match(list1, list2): + # Check if list1 and list2 have identical elements, but relaxed on + # dict elements where list1's dict element can be a subset of list2's + # corresponding element. + if (not isinstance(list1, list) or + not isinstance(list2, list) or + len(list1) != len(list2)): + return False + list1 = sorted(list1) + list2 = sorted(list2) + for (v1, v2) in zip(list1, list2): + if isinstance(v1, dict): + if not dict_match(v1, v2): + return False + elif isinstance(v1, list): + if not list_match(v1, v2): + return False + elif v1 != v2: + return False + return True + + +def dict_match(dict1, dict2): + # Check if dict1 is a subset of dict2. + if not isinstance(dict1, dict) or not isinstance(dict2, dict): + return False + for k1, v1 in dict1.items(): + if k1 not in dict2: + return False + v2 = dict2[k1] + if isinstance(v1, dict): + if not dict_match(v1, v2): + return False + elif isinstance(v1, list): + if not list_match(v1, v2): + return False + elif v1 != v2: + return False + return True diff --git a/vmware_nsx/nsxlib/v3/__init__.py b/vmware_nsx/nsxlib/v3/__init__.py index 6d00cb17c8..b791b3b979 100644 --- a/vmware_nsx/nsxlib/v3/__init__.py +++ b/vmware_nsx/nsxlib/v3/__init__.py @@ -49,30 +49,33 @@ def update_resource_with_retry(resource, payload): return client.update_resource(resource, revised_payload) -def delete_resource_by_values(resource, res_id='id', skip_not_found=True, +def delete_resource_by_values(resource, res_id='id', results_key='results', + skip_not_found=True, **kwargs): resources_get = client.get_resource(resource) - for res in resources_get['results']: - is_matched = True - for (key, value) in kwargs.items(): - if res.get(key) != value: - is_matched = False - break - if is_matched: + matched_num = 0 + for res in resources_get[results_key]: + if utils.dict_match(kwargs, res): LOG.debug("Deleting %s from resource %s", res, resource) delete_resource = resource + "/" + str(res[res_id]) client.delete_resource(delete_resource) - return - if skip_not_found: - LOG.warning(_LW("No resource in %(res)s matched for values: " - "%(values)s"), {'res': resource, + matched_num = matched_num + 1 + if matched_num == 0: + if skip_not_found: + LOG.warning(_LW("No resource in %(res)s matched for values: " + "%(values)s"), {'res': resource, + 'values': kwargs}) + else: + err_msg = (_("No resource in %(res)s matched for values: " + "%(values)s") % {'res': resource, + 'values': kwargs}) + raise nsx_exc.ResourceNotFound(manager=client._get_manager_ip(), + operation=err_msg) + elif matched_num > 1: + LOG.warning(_LW("%(num)s resources in %(res)s matched for values: " + "%(values)s"), {'num': matched_num, + 'res': resource, 'values': kwargs}) - else: - err_msg = (_("No resource in %(res)s matched for values: %(values)s") % - {'res': resource, - 'values': kwargs}) - raise nsx_exc.ResourceNotFound(manager=client._get_manager_ip(), - operation=err_msg) def create_logical_switch(display_name, transport_zone_id, tags, @@ -252,6 +255,33 @@ def add_nat_rule(logical_router_id, action, translated_network, return client.create_resource(resource, body) +def add_static_route(logical_router_id, dest_cidr, nexthop): + resource = 'logical-routers/%s/routing/static-routes' % logical_router_id + body = {} + if dest_cidr: + body['network'] = dest_cidr + if nexthop: + body['next_hops'] = [{"ip_address": nexthop}] + return client.create_resource(resource, body) + + +def delete_static_route(logical_router_id, static_route_id): + resource = 'logical-routers/%s/routing/static-routes/%s' % ( + logical_router_id, static_route_id) + client.delete_resource(resource) + + +def delete_static_route_by_values(logical_router_id, + dest_cidr=None, nexthop=None): + resource = 'logical-routers/%s/routing/static-routes' % logical_router_id + kwargs = {} + if dest_cidr: + kwargs['network'] = dest_cidr + if nexthop: + kwargs['next_hops'] = [{"ip_address": nexthop}] + return delete_resource_by_values(resource, results_key='routes', **kwargs) + + def delete_nat_rule(logical_router_id, nat_rule_id): resource = 'logical-routers/%s/nat/rules/%s' % (logical_router_id, nat_rule_id) diff --git a/vmware_nsx/nsxlib/v3/router.py b/vmware_nsx/nsxlib/v3/router.py index 1324d658d2..8eb7c57842 100644 --- a/vmware_nsx/nsxlib/v3/router.py +++ b/vmware_nsx/nsxlib/v3/router.py @@ -169,3 +169,14 @@ def delete_fip_nat_rules(logical_router_id, ext_ip, int_ip): action="DNAT", translated_network=int_ip, match_destination_network=ext_ip) + + +def add_static_routes(nsx_router_id, route): + return nsxlib.add_static_route(nsx_router_id, route['destination'], + route['nexthop']) + + +def delete_static_routes(nsx_router_id, route): + return nsxlib.delete_static_route_by_values( + nsx_router_id, dest_cidr=route['destination'], + nexthop=route['nexthop']) diff --git a/vmware_nsx/plugins/nsx_v3/plugin.py b/vmware_nsx/plugins/nsx_v3/plugin.py index 603dd37fb3..7a43f20a9a 100644 --- a/vmware_nsx/plugins/nsx_v3/plugin.py +++ b/vmware_nsx/plugins/nsx_v3/plugin.py @@ -35,6 +35,7 @@ from neutron.common import constants as const from neutron.common import exceptions as n_exc from neutron.common import rpc as n_rpc from neutron.common import topics +from neutron.common import utils as neutron_utils from neutron.db import agents_db from neutron.db import agentschedulers_db from neutron.db import db_base_plugin_v2 @@ -763,11 +764,52 @@ class NsxV3Plugin(db_base_plugin_v2.NeutronDbPluginV2, return ret_val + def _validate_ext_routes(self, context, router_id, gw_info, new_routes): + ext_net_id = (gw_info['network_id'] + if attributes.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']}) + raise n_exc.InvalidInput(error_message=error_message) + def update_router(self, context, router_id, router): # TODO(berlin): admin_state_up support + gw_info = self._extract_external_gw(context, router, is_extract=False) + router_data = router['router'] + nsx_router_id = None try: - return super(NsxV3Plugin, self).update_router(context, router_id, - router) + 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, routes_dict = ( + self._get_extra_routes_dict_by_router_id( + context, router_id)) + routes_added, routes_removed = neutron_utils.diff_list_of_dict( + old_routes, new_routes) + nsx_router_id = nsx_db.get_nsx_router_id(context.session, + router_id) + for route in routes_removed: + routerlib.delete_static_routes(nsx_router_id, route) + for route in routes_added: + routerlib.add_static_routes(nsx_router_id, route) + return super(NsxV3Plugin, self).update_router( + context, router_id, router) except nsx_exc.ResourceNotFound: with context.session.begin(subtransactions=True): router_db = self._get_router(context, router_id) @@ -776,9 +818,16 @@ class NsxV3Plugin(db_base_plugin_v2.NeutronDbPluginV2, err_msg=(_("logical router %s not found at the backend") % router_id)) except nsx_exc.ManagerError: - raise nsx_exc.NsxPluginException( - err_msg=(_("Unable to update router %s at the backend") - % router_id)) + with excutils.save_and_reraise_exception(): + router_db = self._get_router(context, router_id) + curr_status = router_db['status'] + router_db['status'] = const.NET_STATUS_ERROR + if nsx_router_id: + for route in routes_added: + routerlib.delete_static_routes(nsx_router_id, route) + for route in routes_removed: + routerlib.add_static_routes(nsx_router_id, route) + router_db['status'] = curr_status def _get_router_interface_ports_by_network( self, context, router_id, network_id): diff --git a/vmware_nsx/tests/unit/nsx_v3/mocks.py b/vmware_nsx/tests/unit/nsx_v3/mocks.py index f68ba95b0d..312d2afa42 100644 --- a/vmware_nsx/tests/unit/nsx_v3/mocks.py +++ b/vmware_nsx/tests/unit/nsx_v3/mocks.py @@ -221,6 +221,7 @@ class NsxV3Mock(object): self.logical_router_ports = {} self.logical_ports = {} self.logical_router_nat_rules = {} + self.static_routes = {} if default_tier0_router_uuid: self.create_logical_router( DEFAULT_TIER0_ROUTER_UUID, None, @@ -411,9 +412,52 @@ class NsxV3Mock(object): remove_nat_rule_ids.append(nat_id) for nat_id in remove_nat_rule_ids: del nat_rules[nat_id] + + def add_static_route(self, logical_router_id, dest_cidr, nexthop): + fake_rule_id = uuidutils.generate_uuid() + if logical_router_id not in self.logical_routers.keys(): + raise nsx_exc.ResourceNotFound( + manager=FAKE_MANAGER, operation="get_logical_router") + body = {} + if dest_cidr: + body['network'] = dest_cidr + if nexthop: + body['next_hops'] = [{"ip_address": nexthop}] + body['id'] = fake_rule_id + if self.static_routes.get(logical_router_id): + self.static_routes[logical_router_id][fake_rule_id] = body + else: + self.static_routes[logical_router_id] = {fake_rule_id: body} + return body + + def delete_static_route(self, logical_router_id, static_route_id): + if (self.static_routes.get(logical_router_id) and + self.static_routes[logical_router_id].get(static_route_id)): + del self.static_routes[logical_router_id][static_route_id] else: raise nsx_exc.ResourceNotFound( - manager=FAKE_MANAGER, operation="delete_nat_rule_by_values") + manager=FAKE_MANAGER, operation="delete_static_route") + + def delete_static_route_by_values(self, logical_router_id, + dest_cidr=None, nexthop=None): + kwargs = {} + if dest_cidr: + kwargs['network'] = dest_cidr + if nexthop: + kwargs['next_hops'] = [{"ip_address": nexthop}] + if self.static_routes.get(logical_router_id): + static_rules = self.static_routes[logical_router_id] + remove_static_rule_ids = [] + for rule_id, rule_body in static_rules.items(): + remove_flag = True + for k, v in kwargs.items(): + if rule_body[k] != v: + remove_flag = False + break + if remove_flag: + remove_static_rule_ids.append(rule_id) + for rule_id in remove_static_rule_ids: + del static_rules[rule_id] def update_logical_router_advertisement(self, logical_router_id, **kwargs): # TODO(berlin): implement this latter. diff --git a/vmware_nsx/tests/unit/nsx_v3/test_plugin.py b/vmware_nsx/tests/unit/nsx_v3/test_plugin.py index 53bcea77e4..e6870393e1 100644 --- a/vmware_nsx/tests/unit/nsx_v3/test_plugin.py +++ b/vmware_nsx/tests/unit/nsx_v3/test_plugin.py @@ -18,6 +18,7 @@ from oslo_config import cfg import six from neutron.api.v2 import attributes +from neutron.common import constants from neutron.common import exceptions as n_exc from neutron import context from neutron.extensions import external_net @@ -213,6 +214,10 @@ class L3NatTest(test_l3_plugin.L3BaseForIntTests, NsxPluginV3TestCase): self.v3_mock.get_logical_router_ports_by_router_id) nsxlib.update_logical_router_advertisement = ( self.v3_mock.update_logical_router_advertisement) + nsxlib.add_static_route = self.v3_mock.add_static_route + nsxlib.delete_static_route = self.v3_mock.delete_static_route + nsxlib.delete_static_route_by_values = ( + self.v3_mock.delete_static_route_by_values) def _create_l3_ext_network( self, physical_network=nsx_v3_mocks.DEFAULT_TIER0_ROUTER_UUID): @@ -278,6 +283,41 @@ class TestL3NatTestCase(L3NatTest, self._router_interface_action('remove', r2['router']['id'], s2['subnet']['id'], None) + def test_router_update_on_external_port(self): + with self.router() as r: + with self.subnet(cidr='10.0.1.0/24') as s: + self._set_net_external(s['subnet']['network_id']) + self._add_external_gateway_to_router( + r['router']['id'], + s['subnet']['network_id']) + body = self._show('routers', r['router']['id']) + net_id = body['router']['external_gateway_info']['network_id'] + self.assertEqual(net_id, s['subnet']['network_id']) + port_res = self._list_ports( + 'json', + 200, + s['subnet']['network_id'], + tenant_id=r['router']['tenant_id'], + device_owner=constants.DEVICE_OWNER_ROUTER_GW) + port_list = self.deserialize('json', port_res) + self.assertEqual(len(port_list['ports']), 1) + + routes = [{'destination': '135.207.0.0/16', + 'nexthop': '10.0.1.3'}] + + self.assertRaises(n_exc.InvalidInput, + self.plugin_instance.update_router, + context.get_admin_context(), + r['router']['id'], + {'router': {'routes': + routes}}) + self._remove_external_gateway_from_router( + r['router']['id'], + s['subnet']['network_id']) + body = self._show('routers', r['router']['id']) + gw_info = body['router']['external_gateway_info'] + self.assertIsNone(gw_info) + class ExtGwModeTestCase(L3NatTest, test_ext_gw_mode.ExtGwModeIntTestCase):