NSXv3: static route support
Change-Id: I7022e6eab687deea609c5e3ce17995cde3818017
This commit is contained in:
parent
12abbae367
commit
346a0aa5df
@ -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
|
||||
|
@ -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
|
||||
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,
|
||||
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})
|
||||
|
||||
|
||||
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)
|
||||
|
@ -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'])
|
||||
|
@ -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):
|
||||
|
@ -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.
|
||||
|
@ -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):
|
||||
|
Loading…
Reference in New Issue
Block a user