From ff7bb5f77a059ec58a2afcc472cc51818d81a5ba Mon Sep 17 00:00:00 2001 From: armando-migliaccio Date: Tue, 18 Jun 2013 18:24:03 -0700 Subject: [PATCH] Add support for the extra route extension in the NVP plugin. The underlying feature is available in NVP 3.2, which introduces a new type of router. Therefore, create_lrouter needs to be made version 'aware'. This led to a number of fixes in the nvplib, especially around how version is retrieved and how version-dependent methods are called. Implements blueprint nvp-extra-route-extension Change-Id: Ie4e2d93f70e1948a62563c8523aea61bb2194c84 --- neutron/plugins/nicira/NeutronPlugin.py | 42 ++- neutron/plugins/nicira/NvpApiClient.py | 25 +- neutron/plugins/nicira/api_client/request.py | 5 +- neutron/plugins/nicira/common/exceptions.py | 4 + neutron/plugins/nicira/nvplib.py | 259 +++++++++++++++--- neutron/tests/unit/nicira/test_maclearning.py | 3 +- .../tests/unit/nicira/test_nicira_plugin.py | 3 +- neutron/tests/unit/nicira/test_nvplib.py | 251 ++++++++++++++--- 8 files changed, 511 insertions(+), 81 deletions(-) diff --git a/neutron/plugins/nicira/NeutronPlugin.py b/neutron/plugins/nicira/NeutronPlugin.py index c9d38b12916..557521e0257 100644 --- a/neutron/plugins/nicira/NeutronPlugin.py +++ b/neutron/plugins/nicira/NeutronPlugin.py @@ -42,11 +42,13 @@ from neutron.db import api as db from neutron.db import db_base_plugin_v2 from neutron.db import dhcp_rpc_base +from neutron.db import extraroute_db from neutron.db import l3_db from neutron.db import models_v2 from neutron.db import portsecurity_db from neutron.db import quota_db # noqa from neutron.db import securitygroups_db +from neutron.extensions import extraroute from neutron.extensions import l3 from neutron.extensions import portsecurity as psec from neutron.extensions import providernet as pnet @@ -124,7 +126,7 @@ def create_rpc_dispatcher(self): class NvpPluginV2(db_base_plugin_v2.NeutronDbPluginV2, - l3_db.L3_NAT_db_mixin, + extraroute_db.ExtraRoute_db_mixin, portsecurity_db.PortSecurityDbMixin, securitygroups_db.SecurityGroupDbMixin, mac_db.MacLearningDbMixin, @@ -139,7 +141,8 @@ class NvpPluginV2(db_base_plugin_v2.NeutronDbPluginV2, functionality using NVP. """ - supported_extension_aliases = ["mac-learning", + supported_extension_aliases = ["extraroute", + "mac-learning", "network-gateway", "nvp-qos", "port-security", @@ -1458,7 +1461,7 @@ def create_router(self, context, router): self._update_router_gw_info(context, router_db['id'], gw_info) return self._make_router_dict(router_db) - def update_router(self, context, id, router): + def update_router(self, context, router_id, router): # Either nexthop is updated or should be kept as it was before r = router['router'] nexthop = None @@ -1479,22 +1482,45 @@ def update_router(self, context, id, router): ext_subnet = ext_net.subnets[0] nexthop = ext_subnet.gateway_ip try: - nvplib.update_lrouter(self.cluster, id, - router['router'].get('name'), nexthop) + for route in r.get('routes', []): + if route['destination'] == '0.0.0.0/0': + msg = _("'routes' cannot contain route '0.0.0.0/0', " + "this must be updated through the default " + "gateway attribute") + raise q_exc.BadRequest(resource='router', msg=msg) + previous_routes = nvplib.update_lrouter( + self.cluster, router_id, r.get('name'), + nexthop, routes=r.get('routes')) # NOTE(salv-orlando): The exception handling below is not correct, but # unfortunately nvplib raises a neutron notfound exception when an # object is not found in the underlying backend except q_exc.NotFound: # Put the router in ERROR status with context.session.begin(subtransactions=True): - router_db = self._get_router(context, id) + router_db = self._get_router(context, router_id) router_db['status'] = constants.NET_STATUS_ERROR raise nvp_exc.NvpPluginException( - err_msg=_("Logical router %s not found on NVP Platform") % id) + err_msg=_("Logical router %s not found " + "on NVP Platform") % router_id) except NvpApiClient.NvpApiException: raise nvp_exc.NvpPluginException( err_msg=_("Unable to update logical router on NVP Platform")) - return super(NvpPluginV2, self).update_router(context, id, router) + except nvp_exc.NvpInvalidVersion: + msg = _("Request cannot contain 'routes' with the NVP " + "platform currently in execution. Please, try " + "without specifying the static routes.") + LOG.exception(msg) + raise q_exc.BadRequest(resource='router', msg=msg) + try: + return super(NvpPluginV2, self).update_router(context, + router_id, router) + except (extraroute.InvalidRoutes, + extraroute.RouterInterfaceInUseByRoute, + extraroute.RoutesExhausted): + with excutils.save_and_reraise_exception(): + # revert changes made to NVP + nvplib.update_explicit_routes_lrouter( + self.cluster, router_id, previous_routes) def delete_router(self, context, id): with context.session.begin(subtransactions=True): diff --git a/neutron/plugins/nicira/NvpApiClient.py b/neutron/plugins/nicira/NvpApiClient.py index 08cad9e920e..d64c3721651 100644 --- a/neutron/plugins/nicira/NvpApiClient.py +++ b/neutron/plugins/nicira/NvpApiClient.py @@ -31,12 +31,24 @@ def _find_nvp_version_in_headers(headers): for (header_name, header_value) in (headers or ()): try: if header_name == 'server': - return header_value.split('/')[1] + return NVPVersion(header_value.split('/')[1]) except IndexError: LOG.warning(_("Unable to fetch NVP version from response " "headers:%s"), headers) +class NVPVersion(object): + """Abstracts NVP version by exposing major and minor.""" + + def __init__(self, nvp_version): + self.full_version = nvp_version.split('.') + self.major = int(self.full_version[0]) + self.minor = int(self.full_version[1]) + + def __str__(self): + return '.'.join(self.full_version) + + class NVPApiHelper(client_eventlet.NvpApiClientEventlet): '''API helper class. @@ -153,10 +165,13 @@ def request(self, method, url, body="", content_type="application/json"): def get_nvp_version(self): if not self._nvp_version: - # generate a simple request (/ws.v1/log) - # this will cause nvp_version to be fetched - # don't bother about response - self.request('GET', '/ws.v1/log') + # Determine the NVP version by querying the control + # cluster nodes. Currently, the version will be the + # one of the server that responds. + self.request('GET', '/ws.v1/control-cluster/node') + if not self._nvp_version: + LOG.error(_('Unable to determine NVP version. ' + 'Plugin might not work as expected.')) return self._nvp_version def fourZeroFour(self): diff --git a/neutron/plugins/nicira/api_client/request.py b/neutron/plugins/nicira/api_client/request.py index 01d08bfda8e..056e55cd8bb 100644 --- a/neutron/plugins/nicira/api_client/request.py +++ b/neutron/plugins/nicira/api_client/request.py @@ -190,8 +190,9 @@ def _issue_request(self): # the conn to be released with is_conn_error == True # which puts the conn on the back of the client's priority # queue. - if response.status >= 500: - LOG.warn(_("[%(rid)d] Request '%(method) %(url)s' " + if (response.status == httplib.INTERNAL_SERVER_ERROR and + response.status > httplib.NOT_IMPLEMENTED): + LOG.warn(_("[%(rid)d] Request '%(method)s %(url)s' " "received: %(status)s"), {'rid': self._rid(), 'method': self._method, 'url': self._url, 'status': response.status}) diff --git a/neutron/plugins/nicira/common/exceptions.py b/neutron/plugins/nicira/common/exceptions.py index 62acbf83866..c18102440fa 100644 --- a/neutron/plugins/nicira/common/exceptions.py +++ b/neutron/plugins/nicira/common/exceptions.py @@ -24,6 +24,10 @@ class NvpPluginException(q_exc.NeutronException): message = _("An unexpected error occurred in the NVP Plugin:%(err_msg)s") +class NvpInvalidVersion(NvpPluginException): + message = _("Unable to fulfill request with version %(version)s.") + + class NvpInvalidConnection(NvpPluginException): message = _("Invalid NVP connection parameters: %(conn_params)s") diff --git a/neutron/plugins/nicira/nvplib.py b/neutron/plugins/nicira/nvplib.py index 6f3780383fe..88b2b848c10 100644 --- a/neutron/plugins/nicira/nvplib.py +++ b/neutron/plugins/nicira/nvplib.py @@ -30,6 +30,7 @@ # no neutron-specific logic in it from neutron.common import constants from neutron.common import exceptions as exception +from neutron.openstack.common import excutils from neutron.openstack.common import log from neutron.plugins.nicira.common import ( exceptions as nvp_exc) @@ -48,11 +49,12 @@ LSWITCH_RESOURCE = "lswitch" LSWITCHPORT_RESOURCE = "lport/%s" % LSWITCH_RESOURCE LROUTER_RESOURCE = "lrouter" -# Current neutron version LROUTERPORT_RESOURCE = "lport/%s" % LROUTER_RESOURCE +LROUTERRIB_RESOURCE = "rib/%s" % LROUTER_RESOURCE LROUTERNAT_RESOURCE = "nat/lrouter" LQUEUE_RESOURCE = "lqueue" GWSERVICE_RESOURCE = "gateway-service" +# Current neutron version NEUTRON_VERSION = "2013.1" # Other constants for NVP resource MAX_DISPLAY_NAME_LEN = 40 @@ -74,16 +76,29 @@ _lqueue_cache = {} -def version_dependent(func): - func_name = func.__name__ +def version_dependent(wrapped_func): + func_name = wrapped_func.__name__ def dispatch_version_dependent_function(cluster, *args, **kwargs): - nvp_ver = cluster.api_client.get_nvp_version() - if nvp_ver: - ver_major = int(nvp_ver.split('.')[0]) - real_func = NVPLIB_FUNC_DICT[func_name][ver_major] + # Call the wrapper function, in case we need to + # run validation checks regarding versions. It + # should return the NVP version + v = (wrapped_func(cluster, *args, **kwargs) or + cluster.api_client.get_nvp_version()) + if v: + func = (NVPLIB_FUNC_DICT[func_name][v.major].get(v.minor) or + NVPLIB_FUNC_DICT[func_name][v.major]['default']) + if func is None: + LOG.error(_('NVP version %(ver)s does not support method ' + '%(fun)s.') % {'ver': v, 'fun': func_name}) + raise NotImplementedError() + else: + raise NvpApiClient.ServiceUnavailable('NVP version is not set. ' + 'Unable to complete request' + 'correctly. Check log for ' + 'NVP communication errors.') func_kwargs = kwargs - arg_spec = inspect.getargspec(real_func) + arg_spec = inspect.getargspec(func) if not arg_spec.keywords and not arg_spec.varargs: # drop args unknown to function from func_args arg_set = set(func_kwargs.keys()) @@ -91,7 +106,7 @@ def dispatch_version_dependent_function(cluster, *args, **kwargs): del func_kwargs[arg] # NOTE(salvatore-orlando): shall we fail here if a required # argument is not passed, or let the called function raise? - real_func(cluster, *args, **func_kwargs) + return func(cluster, *args, **func_kwargs) return dispatch_version_dependent_function @@ -284,7 +299,22 @@ def create_l2_gw_service(cluster, tenant_id, display_name, devices): json.dumps(gwservice_obj), cluster=cluster) -def create_lrouter(cluster, tenant_id, display_name, nexthop): +def _prepare_lrouter_body(name, tenant_id, router_type, **kwargs): + body = { + "display_name": _check_and_truncate_name(name), + "tags": [{"tag": tenant_id, "scope": "os_tid"}, + {"tag": NEUTRON_VERSION, "scope": "quantum"}], + "routing_config": { + "type": router_type + }, + "type": "LogicalRouterConfig" + } + if kwargs: + body["routing_config"].update(kwargs) + return body + + +def create_implicit_routing_lrouter(cluster, tenant_id, display_name, nexthop): """Create a NVP logical router on the specified cluster. :param cluster: The target NVP cluster @@ -295,25 +325,36 @@ def create_lrouter(cluster, tenant_id, display_name, nexthop): :raise NvpApiException: if there is a problem while communicating with the NVP controller """ - display_name = _check_and_truncate_name(display_name) - tags = [{"tag": tenant_id, "scope": "os_tid"}, - {"tag": NEUTRON_VERSION, "scope": "quantum"}] - lrouter_obj = { - "display_name": display_name, - "tags": tags, - "routing_config": { - "default_route_next_hop": { - "gateway_ip_address": nexthop, - "type": "RouterNextHop" - }, - "type": "SingleDefaultRouteImplicitRoutingConfig" + implicit_routing_config = { + "default_route_next_hop": { + "gateway_ip_address": nexthop, + "type": "RouterNextHop" }, - "type": "LogicalRouterConfig" } + lrouter_obj = _prepare_lrouter_body( + display_name, tenant_id, + "SingleDefaultRouteImplicitRoutingConfig", + **implicit_routing_config) return do_request(HTTP_POST, _build_uri_path(LROUTER_RESOURCE), json.dumps(lrouter_obj), cluster=cluster) +def create_explicit_routing_lrouter(cluster, tenant_id, + display_name, nexthop): + lrouter_obj = _prepare_lrouter_body( + display_name, tenant_id, "RoutingTableRoutingConfig") + router = do_request(HTTP_POST, _build_uri_path(LROUTER_RESOURCE), + json.dumps(lrouter_obj), cluster=cluster) + default_gw = {'prefix': '0.0.0.0/0', 'next_hop_ip': nexthop} + create_explicit_route_lrouter(cluster, router['uuid'], default_gw) + return router + + +@version_dependent +def create_lrouter(cluster, *args, **kwargs): + pass + + def delete_lrouter(cluster, lrouter_id): do_request(HTTP_DELETE, _build_uri_path(LROUTER_RESOURCE, resource_id=lrouter_id), @@ -381,8 +422,8 @@ def update_l2_gw_service(cluster, gateway_id, display_name): json.dumps(gwservice_obj), cluster=cluster) -def update_lrouter(cluster, lrouter_id, display_name, nexthop): - lrouter_obj = get_lrouter(cluster, lrouter_id) +def update_implicit_routing_lrouter(cluster, r_id, display_name, nexthop): + lrouter_obj = get_lrouter(cluster, r_id) if not display_name and not nexthop: # Nothing to update return lrouter_obj @@ -395,11 +436,150 @@ def update_lrouter(cluster, lrouter_id, display_name, nexthop): if nh_element: nh_element["gateway_ip_address"] = nexthop return do_request(HTTP_PUT, _build_uri_path(LROUTER_RESOURCE, - resource_id=lrouter_id), + resource_id=r_id), json.dumps(lrouter_obj), cluster=cluster) +def get_explicit_routes_lrouter(cluster, router_id, protocol_type='static'): + static_filter = {'protocol': protocol_type} + existing_routes = do_request( + HTTP_GET, + _build_uri_path(LROUTERRIB_RESOURCE, + filters=static_filter, + fields="*", + parent_resource_id=router_id), + cluster=cluster)['results'] + return existing_routes + + +def delete_explicit_route_lrouter(cluster, router_id, route_id): + do_request(HTTP_DELETE, + _build_uri_path(LROUTERRIB_RESOURCE, + resource_id=route_id, + parent_resource_id=router_id), + cluster=cluster) + + +def create_explicit_route_lrouter(cluster, router_id, route): + next_hop_ip = route.get("nexthop") or route.get("next_hop_ip") + prefix = route.get("destination") or route.get("prefix") + uuid = do_request( + HTTP_POST, + _build_uri_path(LROUTERRIB_RESOURCE, + parent_resource_id=router_id), + json.dumps({ + "action": "accept", + "next_hop_ip": next_hop_ip, + "prefix": prefix, + "protocol": "static" + }), + cluster=cluster)['uuid'] + return uuid + + +def update_explicit_routes_lrouter(cluster, router_id, routes): + # Update in bulk: delete them all, and add the ones specified + # but keep track of what is been modified to allow roll-backs + # in case of failures + nvp_routes = get_explicit_routes_lrouter(cluster, router_id) + try: + deleted_routes = [] + added_routes = [] + # omit the default route (0.0.0.0/0) from the processing; + # this must be handled through the nexthop for the router + for route in nvp_routes: + prefix = route.get("destination") or route.get("prefix") + if prefix != '0.0.0.0/0': + delete_explicit_route_lrouter(cluster, + router_id, + route['uuid']) + deleted_routes.append(route) + for route in routes: + prefix = route.get("destination") or route.get("prefix") + if prefix != '0.0.0.0/0': + uuid = create_explicit_route_lrouter(cluster, + router_id, route) + added_routes.append(uuid) + except NvpApiClient.NvpApiException: + LOG.exception(_('Cannot update NVP routes %(routes)s for' + 'router %(router_id)s') % {'routes': routes, + 'router_id': router_id}) + # Roll back to keep NVP in consistent state + with excutils.save_and_reraise_exception(): + if nvp_routes: + if deleted_routes: + for route in deleted_routes: + create_explicit_route_lrouter(cluster, + router_id, route) + if added_routes: + for route_id in added_routes: + delete_explicit_route_lrouter(cluster, + router_id, route_id) + return nvp_routes + + +@version_dependent +def get_default_route_explicit_routing_lrouter(cluster, *args, **kwargs): + pass + + +def get_default_route_explicit_routing_lrouter_v33(cluster, router_id): + static_filter = {"protocol": "static", + "prefix": "0.0.0.0/0"} + default_route = do_request( + HTTP_GET, + _build_uri_path(LROUTERRIB_RESOURCE, + filters=static_filter, + fields="*", + parent_resource_id=router_id), + cluster=cluster)["results"][0] + return default_route + + +def get_default_route_explicit_routing_lrouter_v32(cluster, router_id): + # Scan all routes because 3.2 does not support query by prefix + all_routes = get_explicit_routes_lrouter(cluster, router_id) + for route in all_routes: + if route['prefix'] == '0.0.0.0/0': + return route + + +def update_default_gw_explicit_routing_lrouter(cluster, router_id, next_hop): + default_route = get_default_route_explicit_routing_lrouter(cluster, + router_id) + if next_hop != default_route["next_hop_ip"]: + new_default_route = {"action": "accept", + "next_hop_ip": next_hop, + "prefix": "0.0.0.0/0", + "protocol": "static"} + do_request(HTTP_PUT, + _build_uri_path(LROUTERRIB_RESOURCE, + resource_id=default_route['uuid'], + parent_resource_id=router_id), + json.dumps(new_default_route), + cluster=cluster) + + +def update_explicit_routing_lrouter(cluster, router_id, + display_name, next_hop, routes=None): + update_implicit_routing_lrouter(cluster, router_id, display_name, next_hop) + if next_hop: + update_default_gw_explicit_routing_lrouter(cluster, + router_id, next_hop) + if routes: + return update_explicit_routes_lrouter(cluster, router_id, routes) + + +@version_dependent +def update_lrouter(cluster, *args, **kwargs): + if kwargs.get('routes', None): + v = cluster.api_client.get_nvp_version() + if (v.major < 3) or (v.major >= 3 and v.minor < 2): + raise nvp_exc.NvpInvalidVersion(version=v) + return v + + def delete_network(cluster, net_id, lswitch_id): delete_networks(cluster, net_id, [lswitch_id]) @@ -1027,14 +1207,27 @@ def update_lrouter_port_ips(cluster, lrouter_id, lport_id, raise nvp_exc.NvpPluginException(err_msg=msg) -# TODO(salvatore-orlando): Also handle changes in minor versions NVPLIB_FUNC_DICT = { - 'create_lrouter_dnat_rule': {2: create_lrouter_dnat_rule_v2, - 3: create_lrouter_dnat_rule_v3}, - 'create_lrouter_snat_rule': {2: create_lrouter_snat_rule_v2, - 3: create_lrouter_snat_rule_v3}, - 'create_lrouter_nosnat_rule': {2: create_lrouter_nosnat_rule_v2, - 3: create_lrouter_nosnat_rule_v3} + 'create_lrouter': { + 2: {'default': create_implicit_routing_lrouter, }, + 3: {'default': create_implicit_routing_lrouter, + 2: create_explicit_routing_lrouter, }, }, + 'update_lrouter': { + 2: {'default': update_implicit_routing_lrouter, }, + 3: {'default': update_implicit_routing_lrouter, + 2: update_explicit_routing_lrouter, }, }, + 'create_lrouter_dnat_rule': { + 2: {'default': create_lrouter_dnat_rule_v2, }, + 3: {'default': create_lrouter_dnat_rule_v3, }, }, + 'create_lrouter_snat_rule': { + 2: {'default': create_lrouter_snat_rule_v2, }, + 3: {'default': create_lrouter_snat_rule_v3, }, }, + 'create_lrouter_nosnat_rule': { + 2: {'default': create_lrouter_nosnat_rule_v2, }, + 3: {'default': create_lrouter_nosnat_rule_v3, }, }, + 'get_default_route_explicit_routing_lrouter': { + 3: {2: get_default_route_explicit_routing_lrouter_v32, + 3: get_default_route_explicit_routing_lrouter_v33, }, }, } diff --git a/neutron/tests/unit/nicira/test_maclearning.py b/neutron/tests/unit/nicira/test_maclearning.py index db05e45f5fd..2c46ac5179a 100644 --- a/neutron/tests/unit/nicira/test_maclearning.py +++ b/neutron/tests/unit/nicira/test_maclearning.py @@ -27,6 +27,7 @@ from neutron.extensions import agent from neutron.openstack.common import log as logging import neutron.plugins.nicira as nvp_plugin +from neutron.plugins.nicira.NvpApiClient import NVPVersion from neutron.tests.unit.nicira import fake_nvpapiclient from neutron.tests.unit import test_db_plugin @@ -84,7 +85,7 @@ def _fake_request(*args, **kwargs): return self.fc.fake_request(*args, **kwargs) # Emulate tests against NVP 2.x - instance.return_value.get_nvp_version.return_value = "2.999" + instance.return_value.get_nvp_version.return_value = NVPVersion("3.0") instance.return_value.request.side_effect = _fake_request cfg.CONF.set_override('metadata_mode', None, 'NVP') self.addCleanup(self.fc.reset_all) diff --git a/neutron/tests/unit/nicira/test_nicira_plugin.py b/neutron/tests/unit/nicira/test_nicira_plugin.py index 3be010b028d..f95da9e153a 100644 --- a/neutron/tests/unit/nicira/test_nicira_plugin.py +++ b/neutron/tests/unit/nicira/test_nicira_plugin.py @@ -34,6 +34,7 @@ from neutron.plugins.nicira.extensions import nvp_qos as ext_qos from neutron.plugins.nicira import NeutronPlugin from neutron.plugins.nicira import NvpApiClient +from neutron.plugins.nicira.NvpApiClient import NVPVersion from neutron.plugins.nicira import nvplib from neutron.tests.unit.nicira import fake_nvpapiclient import neutron.tests.unit.nicira.test_networkgw as test_l2_gw @@ -86,7 +87,7 @@ def _fake_request(*args, **kwargs): return self.fc.fake_request(*args, **kwargs) # Emulate tests against NVP 2.x - instance.return_value.get_nvp_version.return_value = "2.999" + instance.return_value.get_nvp_version.return_value = NVPVersion("2.9") instance.return_value.request.side_effect = _fake_request super(NiciraPluginV2TestCase, self).setUp(self._plugin_name) cfg.CONF.set_override('metadata_mode', None, 'NVP') diff --git a/neutron/tests/unit/nicira/test_nvplib.py b/neutron/tests/unit/nicira/test_nvplib.py index 0a98f77c5d6..bc860f9f4f3 100644 --- a/neutron/tests/unit/nicira/test_nvplib.py +++ b/neutron/tests/unit/nicira/test_nvplib.py @@ -44,6 +44,9 @@ def setUp(self): % NICIRA_PKG_PATH, autospec=True) instance = self.mock_nvpapi.start() instance.return_value.login.return_value = "the_cookie" + fake_version = getattr(self, 'fake_version', "2.9") + instance.return_value.get_nvp_version.return_value = ( + NvpApiClient.NVPVersion(fake_version)) def _fake_request(*args, **kwargs): return self.fc.fake_request(*args, **kwargs) @@ -69,35 +72,32 @@ def _build_tag_dict(self, tags): class TestNvplibNatRules(NvplibTestCase): - def _test_create_lrouter_dnat_rule(self, func): - tenant_id = 'pippo' - lrouter = nvplib.create_lrouter(self.fake_cluster, - tenant_id, - 'fake_router', - '192.168.0.1') - nat_rule = func(self.fake_cluster, lrouter['uuid'], '10.0.0.99', - match_criteria={'destination_ip_addresses': - '192.168.0.5'}) - uri = nvplib._build_uri_path(nvplib.LROUTERNAT_RESOURCE, - nat_rule['uuid'], - lrouter['uuid']) - return nvplib.do_request("GET", uri, cluster=self.fake_cluster) + def _test_create_lrouter_dnat_rule(self, version): + with mock.patch.object(self.fake_cluster.api_client, + 'get_nvp_version', + new=lambda: NvpApiClient.NVPVersion(version)): + tenant_id = 'pippo' + lrouter = nvplib.create_lrouter(self.fake_cluster, + tenant_id, + 'fake_router', + '192.168.0.1') + nat_rule = nvplib.create_lrouter_dnat_rule( + self.fake_cluster, lrouter['uuid'], '10.0.0.99', + match_criteria={'destination_ip_addresses': + '192.168.0.5'}) + uri = nvplib._build_uri_path(nvplib.LROUTERNAT_RESOURCE, + nat_rule['uuid'], + lrouter['uuid']) + resp_obj = nvplib.do_request("GET", uri, cluster=self.fake_cluster) + self.assertEqual('DestinationNatRule', resp_obj['type']) + self.assertEqual('192.168.0.5', + resp_obj['match']['destination_ip_addresses']) def test_create_lrouter_dnat_rule_v2(self): - resp_obj = self._test_create_lrouter_dnat_rule( - nvplib.create_lrouter_dnat_rule_v2) - self.assertEqual('DestinationNatRule', resp_obj['type']) - self.assertEqual('192.168.0.5', - resp_obj['match']['destination_ip_addresses']) - - def test_create_lrouter_dnat_rule_v3(self): - resp_obj = self._test_create_lrouter_dnat_rule( - nvplib.create_lrouter_dnat_rule_v2) - # TODO(salvatore-orlando): Extend FakeNVPApiClient to deal with - # different versions of NVP API - self.assertEqual('DestinationNatRule', resp_obj['type']) - self.assertEqual('192.168.0.5', - resp_obj['match']['destination_ip_addresses']) + self._test_create_lrouter_dnat_rule('2.9') + + def test_create_lrouter_dnat_rule_v31(self): + self._test_create_lrouter_dnat_rule('3.1') class NvplibNegativeTests(base.BaseTestCase): @@ -110,6 +110,10 @@ def setUp(self): % NICIRA_PKG_PATH, autospec=True) instance = self.mock_nvpapi.start() instance.return_value.login.return_value = "the_cookie" + # Choose 2.9, but the version is irrelevant for the aim of + # these tests as calls are throwing up errors anyway + self.fake_version = NvpApiClient.NVPVersion('2.9') + instance.return_value.get_nvp_version.return_value = self.fake_version def _faulty_request(*args, **kwargs): raise nvplib.NvpApiClient.NvpApiException @@ -365,6 +369,187 @@ def test_delete_non_existing_lswitch_raises(self): self.fake_cluster, 'whatever', ['whatever']) +class TestNvplibExplicitLRouters(NvplibTestCase): + + def setUp(self): + self.fake_version = '3.2' + super(TestNvplibExplicitLRouters, self).setUp() + + def _get_lrouter(self, tenant_id, router_name, router_id, relations=None): + schema = '/ws.v1/schema/RoutingTableRoutingConfig' + + router = {'display_name': router_name, + 'uuid': router_id, + 'tags': [{'scope': 'quantum', 'tag': '2013.1'}, + {'scope': 'os_tid', 'tag': '%s' % tenant_id}], + 'distributed': False, + 'routing_config': {'type': 'RoutingTableRoutingConfig', + '_schema': schema}, + '_schema': schema, + 'nat_synchronization_enabled': True, + 'replication_mode': 'service', + 'type': 'LogicalRouterConfig', + '_href': '/ws.v1/lrouter/%s' % router_id, } + if relations: + router['_relations'] = relations + return router + + def _get_single_route(self, router_id, route_id='fake_route_id_0', + prefix='0.0.0.0/0', next_hop_ip='1.1.1.1'): + return {'protocol': 'static', + '_href': '/ws.v1/lrouter/%s/rib/%s' % (router_id, route_id), + 'prefix': prefix, + '_schema': '/ws.v1/schema/RoutingTableEntry', + 'next_hop_ip': next_hop_ip, + 'action': 'accept', + 'uuid': route_id} + + def test_prepare_body_with_implicit_routing_config(self): + router_name = 'fake_router_name' + tenant_id = 'fake_tenant_id' + router_type = 'SingleDefaultRouteImplicitRoutingConfig' + route_config = { + 'default_route_next_hop': {'gateway_ip_address': 'fake_address', + 'type': 'RouterNextHop'}, } + body = nvplib._prepare_lrouter_body(router_name, tenant_id, + router_type, **route_config) + expected = {'display_name': 'fake_router_name', + 'routing_config': { + 'default_route_next_hop': + {'gateway_ip_address': 'fake_address', + 'type': 'RouterNextHop'}, + 'type': 'SingleDefaultRouteImplicitRoutingConfig'}, + 'tags': [{'scope': 'os_tid', 'tag': 'fake_tenant_id'}, + {'scope': 'quantum', 'tag': '2013.1'}], + 'type': 'LogicalRouterConfig'} + self.assertEqual(expected, body) + + def test_prepare_body_without_routing_config(self): + router_name = 'fake_router_name' + tenant_id = 'fake_tenant_id' + router_type = 'RoutingTableRoutingConfig' + body = nvplib._prepare_lrouter_body(router_name, tenant_id, + router_type) + expected = {'display_name': 'fake_router_name', + 'routing_config': {'type': 'RoutingTableRoutingConfig'}, + 'tags': [{'scope': 'os_tid', 'tag': 'fake_tenant_id'}, + {'scope': 'quantum', 'tag': '2013.1'}], + 'type': 'LogicalRouterConfig'} + self.assertEqual(expected, body) + + def test_get_lrouter(self): + tenant_id = 'fake_tenant_id' + router_name = 'fake_router_name' + router_id = 'fake_router_id' + relations = { + 'LogicalRouterStatus': + {'_href': '/ws.v1/lrouter/%s/status' % router_id, + 'lport_admin_up_count': 1, + '_schema': '/ws.v1/schema/LogicalRouterStatus', + 'lport_count': 1, + 'fabric_status': True, + 'type': 'LogicalRouterStatus', + 'lport_link_up_count': 0, }, } + + with mock.patch(_nicira_method('do_request'), + return_value=self._get_lrouter(tenant_id, + router_name, + router_id, + relations)): + lrouter = nvplib.get_lrouter(self.fake_cluster, router_id) + self.assertTrue( + lrouter['_relations']['LogicalRouterStatus']['fabric_status']) + + def test_create_lrouter(self): + tenant_id = 'fake_tenant_id' + router_name = 'fake_router_name' + router_id = 'fake_router_id' + nexthop_ip = '10.0.0.1' + with mock.patch(_nicira_method('do_request'), + return_value=self._get_lrouter(tenant_id, + router_name, + router_id)): + lrouter = nvplib.create_lrouter(self.fake_cluster, tenant_id, + router_name, nexthop_ip) + self.assertEqual(lrouter['routing_config']['type'], + 'RoutingTableRoutingConfig') + self.assertNotIn('default_route_next_hop', + lrouter['routing_config']) + + def test_update_lrouter_nvp_with_no_routes(self): + router_id = 'fake_router_id' + new_routes = [{"nexthop": "10.0.0.2", + "destination": "169.254.169.0/30"}, ] + + nvp_routes = [self._get_single_route(router_id)] + with mock.patch(_nicira_method('get_explicit_routes_lrouter'), + return_value=nvp_routes): + with mock.patch(_nicira_method('create_explicit_route_lrouter'), + return_value='fake_uuid'): + old_routes = nvplib.update_explicit_routes_lrouter( + self.fake_cluster, router_id, new_routes) + self.assertEqual(old_routes, nvp_routes) + + def test_update_lrouter_nvp_with_no_routes_raise_nvp_exception(self): + router_id = 'fake_router_id' + new_routes = [{"nexthop": "10.0.0.2", + "destination": "169.254.169.0/30"}, ] + + nvp_routes = [self._get_single_route(router_id)] + with mock.patch(_nicira_method('get_explicit_routes_lrouter'), + return_value=nvp_routes): + with mock.patch(_nicira_method('create_explicit_route_lrouter'), + side_effect=NvpApiClient.NvpApiException): + self.assertRaises(NvpApiClient.NvpApiException, + nvplib.update_explicit_routes_lrouter, + self.fake_cluster, router_id, new_routes) + + def test_update_lrouter_with_routes(self): + router_id = 'fake_router_id' + new_routes = [{"next_hop_ip": "10.0.0.2", + "prefix": "169.254.169.0/30"}, ] + + nvp_routes = [self._get_single_route(router_id), + self._get_single_route(router_id, 'fake_route_id_1', + '0.0.0.1/24', '10.0.0.3'), + self._get_single_route(router_id, 'fake_route_id_2', + '0.0.0.2/24', '10.0.0.4'), ] + + with mock.patch(_nicira_method('get_explicit_routes_lrouter'), + return_value=nvp_routes): + with mock.patch(_nicira_method('delete_explicit_route_lrouter'), + return_value=None): + with mock.patch(_nicira_method( + 'create_explicit_route_lrouter'), + return_value='fake_uuid'): + old_routes = nvplib.update_explicit_routes_lrouter( + self.fake_cluster, router_id, new_routes) + self.assertEqual(old_routes, nvp_routes) + + def test_update_lrouter_with_routes_raises_nvp_expception(self): + router_id = 'fake_router_id' + new_routes = [{"nexthop": "10.0.0.2", + "destination": "169.254.169.0/30"}, ] + + nvp_routes = [self._get_single_route(router_id), + self._get_single_route(router_id, 'fake_route_id_1', + '0.0.0.1/24', '10.0.0.3'), + self._get_single_route(router_id, 'fake_route_id_2', + '0.0.0.2/24', '10.0.0.4'), ] + + with mock.patch(_nicira_method('get_explicit_routes_lrouter'), + return_value=nvp_routes): + with mock.patch(_nicira_method('delete_explicit_route_lrouter'), + side_effect=NvpApiClient.NvpApiException): + with mock.patch( + _nicira_method('create_explicit_route_lrouter'), + return_value='fake_uuid'): + self.assertRaises( + NvpApiClient.NvpApiException, + nvplib.update_explicit_routes_lrouter, + self.fake_cluster, router_id, new_routes) + + class TestNvplibLogicalRouters(NvplibTestCase): def _verify_lrouter(self, res_lrouter, @@ -733,7 +918,7 @@ def _test_create_router_snat_rule(self, version): '10.0.0.1') with mock.patch.object(self.fake_cluster.api_client, 'get_nvp_version', - new=lambda: version): + new=lambda: NvpApiClient.NVPVersion(version)): nvplib.create_lrouter_snat_rule( self.fake_cluster, lrouter['uuid'], '10.0.0.2', '10.0.0.2', order=200, @@ -754,7 +939,7 @@ def _test_create_router_dnat_rule(self, version, dest_port=None): '10.0.0.1') with mock.patch.object(self.fake_cluster.api_client, 'get_nvp_version', - return_value=version): + return_value=NvpApiClient.NVPVersion(version)): nvplib.create_lrouter_dnat_rule( self.fake_cluster, lrouter['uuid'], '192.168.0.2', order=200, dest_port=dest_port, @@ -797,7 +982,7 @@ def _test_create_router_nosnat_rule(self, version, expected=1): '10.0.0.1') with mock.patch.object(self.fake_cluster.api_client, 'get_nvp_version', - new=lambda: version): + new=lambda: NvpApiClient.NVPVersion(version)): nvplib.create_lrouter_nosnat_rule( self.fake_cluster, lrouter['uuid'], order=100, @@ -820,7 +1005,7 @@ def _prepare_nat_rules_for_delete_tests(self): # v2 or v3 makes no difference for this test with mock.patch.object(self.fake_cluster.api_client, 'get_nvp_version', - new=lambda: '2.0'): + new=lambda: NvpApiClient.NVPVersion('2.0')): nvplib.create_lrouter_snat_rule( self.fake_cluster, lrouter['uuid'], '10.0.0.2', '10.0.0.2', order=220, @@ -1164,3 +1349,7 @@ def fakedorequest(*args, **kwargs): with mock.patch.object(nvplib, 'do_request', new=fakedorequest): version = nvplib.get_cluster_version('whatever') self.assertIsNone(version) + + +def _nicira_method(method_name, module_name='nvplib'): + return '%s.%s.%s' % ('neutron.plugins.nicira', module_name, method_name)