NSXv3: static route support

Change-Id: I7022e6eab687deea609c5e3ce17995cde3818017
This commit is contained in:
Shih-Hao Li 2015-09-29 14:20:04 -07:00
parent 12abbae367
commit 346a0aa5df
6 changed files with 239 additions and 24 deletions

View File

@ -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

View File

@ -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)

View File

@ -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'])

View File

@ -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):

View File

@ -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.

View File

@ -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):