From 4dc77de1883e89c706b197f520c6949bd0e87c4e Mon Sep 17 00:00:00 2001 From: Adit Sarfaty Date: Mon, 1 Aug 2016 16:37:30 +0300 Subject: [PATCH] NSX|V router flavor support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adding support for flavors in routers creation. The flavor profile metadata will include the relevant information for the router creation in a dictionary format. 4 parameters are currently supported: router_type, router_size, distributed & availability_zone_hints example for the metainfo: “{'router_type':'exclusive','router_size':'large','distributed':false, 'availability_zone_hints':'az_name’]}” Change-Id: Ia9009377f5d5994b6dbcd04365d9d94b01ac4c35 --- vmware_nsx/plugins/nsx_v/plugin.py | 110 +++++++++++- vmware_nsx/tests/unit/nsx_v/test_plugin.py | 190 ++++++++++++++++++++- 2 files changed, 297 insertions(+), 3 deletions(-) diff --git a/vmware_nsx/plugins/nsx_v/plugin.py b/vmware_nsx/plugins/nsx_v/plugin.py index 2846db93cb..56c4f3a767 100644 --- a/vmware_nsx/plugins/nsx_v/plugin.py +++ b/vmware_nsx/plugins/nsx_v/plugin.py @@ -23,6 +23,7 @@ from neutron_lib import constants from neutron_lib import exceptions as n_exc from oslo_config import cfg from oslo_log import log as logging +from oslo_serialization import jsonutils from oslo_utils import excutils from oslo_utils import uuidutils from sqlalchemy.orm import exc as sa_exc @@ -56,15 +57,18 @@ from neutron.db import securitygroups_db from neutron.extensions import allowedaddresspairs as addr_pair from neutron.extensions import availability_zone as az_ext from neutron.extensions import external_net as ext_net_extn +from neutron.extensions import flavors from neutron.extensions import l3 from neutron.extensions import multiprovidernet as mpnet from neutron.extensions import portbindings as pbin from neutron.extensions import portsecurity as psec from neutron.extensions import providernet as pnet from neutron.extensions import securitygroup as ext_sg +from neutron import manager from neutron.plugins.common import constants as plugin_const from neutron.plugins.common import utils from neutron.quota import resource_registry +from neutron.services.flavors import flavors_plugin from neutron.services.qos import qos_consts from vmware_nsx.dvs import dvs from vmware_nsx.services.qos.common import utils as qos_com_utils @@ -154,7 +158,8 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, "subnet_allocation", "availability_zone", "network_availability_zone", - "router_availability_zone"] + "router_availability_zone", + "l3-flavors", "flavors"] supported_qos_rule_types = [qos_consts.RULE_TYPE_BANDWIDTH_LIMIT, qos_consts.RULE_TYPE_DSCP_MARKING] @@ -2180,8 +2185,98 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, if r.get('router_type') == nsxv_constants.EXCLUSIVE: r[ROUTER_SIZE] = cfg.CONF.nsxv.exclusive_router_appliance_size + def _get_router_flavor_profile(self, context, flavor_id): + flv_plugin = manager.NeutronManager.get_service_plugins().get( + plugin_const.FLAVORS) + if not flv_plugin: + msg = _("Flavors plugin not found") + raise n_exc.BadRequest(resource="router", msg=msg) + + # Will raise FlavorNotFound if doesn't exist + fl_db = flavors_plugin.FlavorsPlugin.get_flavor( + flv_plugin, context, flavor_id) + + if fl_db['service_type'] != plugin_const.L3_ROUTER_NAT: + raise flavors.InvalidFlavorServiceType( + service_type=fl_db['service_type']) + + if not fl_db['enabled']: + raise flavors.FlavorDisabled() + + # get the profile (Currently only 1 is supported, so take the first) + if not fl_db['service_profiles']: + return + profile_id = fl_db['service_profiles'][0] + + return flavors_plugin.FlavorsPlugin.get_service_profile( + flv_plugin, + context, + profile_id) + + def _get_flavor_metainfo_from_profile(self, flavor_id, flavor_profile): + if not flavor_profile: + return {} + metainfo_string = flavor_profile.get('metainfo').replace("'", "\"") + try: + metainfo = jsonutils.loads(metainfo_string) + if not isinstance(metainfo, dict): + LOG.warning(_LW("Skipping router flavor %(flavor)s metainfo " + "[%(metainfo)s]: expected a dictionary"), + {'flavor': flavor_id, + 'metainfo': metainfo_string}) + metainfo = {} + except ValueError as e: + LOG.warning(_LW("Error reading router flavor %(flavor)s metainfo " + "[%(metainfo)s]: %(error)s"), + {'flavor': flavor_id, + 'metainfo': metainfo_string, + 'error': e}) + metainfo = {} + return metainfo + + def _get_router_config_from_flavor(self, context, router): + """Validate the router flavor and initialize router data + + Validate that the flavor is legit, and that contradicting configuration + does not exist. + Also update the router data to reflect the selected flavor. + """ + if not validators.is_attr_set(router.get('flavor_id')): + return + flavor_id = router['flavor_id'] + flavor_profile = self._get_router_flavor_profile(context, flavor_id) + metainfo = self._get_flavor_metainfo_from_profile(flavor_id, + flavor_profile) + + # Go over the attributes of the metainfo + allowed_keys = [ROUTER_SIZE, 'router_type', 'distributed', + az_ext.AZ_HINTS] + for k, v in metainfo.items(): + if k in allowed_keys: + #special case for availability zones hints which are an array + if k == az_ext.AZ_HINTS: + if not isinstance(v, list): + v = [v] + # The default az hists is an empty array + if (validators.is_attr_set(router.get(k)) and + len(router[k]) > 0): + msg = (_("Cannot specify %s if the flavor profile " + "defines it") % k) + raise n_exc.BadRequest(resource="router", msg=msg) + + elif validators.is_attr_set(router.get(k)) and router[k] != v: + msg = _("Cannot specify %s if the flavor defines it") % k + raise n_exc.BadRequest(resource="router", msg=msg) + # Legal value + router[k] = v + else: + LOG.warning(_LW("Skipping router flavor metainfo [%(k)s:%(v)s]" + ":unsupported field"), + {'k': k, 'v': v}) + def create_router(self, context, router, allow_metadata=True): r = router['router'] + self._get_router_config_from_flavor(context, r) self._decide_router_type(context, r) self._validate_router_size(router) self._validate_availability_zones_in_obj(context, 'router', r) @@ -2196,6 +2291,7 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, router_db = self._get_router(context, lrouter['id']) self._process_extra_attr_router_create(context, router_db, r) self._process_nsx_router_create(context, router_db, r) + self._process_router_flavor_create(context, router_db, r) lrouter = super(NsxVPluginV2, self).get_router(context, lrouter['id']) @@ -2350,6 +2446,18 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, context, edge_id)] return [] + def _process_router_flavor_create(self, context, router_db, r): + """Update the router DB structure with the flavor ID upon creation + """ + if validators.is_attr_set(r.get('flavor_id')): + router_db.flavor_id = r['flavor_id'] + + def add_flavor_id(plugin, router_res, router_db): + router_res['flavor_id'] = router_db['flavor_id'] + + db_base_plugin_v2.NeutronDbPluginV2.register_dict_extend_funcs( + l3.ROUTERS, [add_flavor_id]) + def get_router(self, context, id, fields=None): router = super(NsxVPluginV2, self).get_router(context, id, fields) if router.get("distributed") and 'router_type' in router: diff --git a/vmware_nsx/tests/unit/nsx_v/test_plugin.py b/vmware_nsx/tests/unit/nsx_v/test_plugin.py index c7ac2a5be3..8a1cb66bd4 100644 --- a/vmware_nsx/tests/unit/nsx_v/test_plugin.py +++ b/vmware_nsx/tests/unit/nsx_v/test_plugin.py @@ -26,6 +26,7 @@ from neutron.extensions import dvr as dist_router from neutron.extensions import external_net from neutron.extensions import l3 from neutron.extensions import l3_ext_gw_mode +from neutron.extensions import l3_flavors from neutron.extensions import portbindings from neutron.extensions import providernet as pnet from neutron.extensions import router_availability_zone @@ -37,6 +38,7 @@ from neutron.services.qos import qos_consts from neutron.tests.unit import _test_extension_portbindings as test_bindings import neutron.tests.unit.db.test_allowedaddresspairs_db as test_addr_pair import neutron.tests.unit.db.test_db_base_plugin_v2 as test_plugin +from neutron.tests.unit.extensions import base as extension import neutron.tests.unit.extensions.test_l3 as test_l3_plugin import neutron.tests.unit.extensions.test_l3_ext_gw_mode as test_ext_gw_mode import neutron.tests.unit.extensions.test_portsecurity as test_psec @@ -138,8 +140,16 @@ class NsxVPluginV2TestCase(test_plugin.NeutronDbPluginV2TestCase): self.default_res_pool = 'respool-28' cfg.CONF.set_override("resource_pool_id", self.default_res_pool, group="nsxv") - super(NsxVPluginV2TestCase, self).setUp(plugin=plugin, - ext_mgr=ext_mgr) + if service_plugins is not None: + # override the service plugins only if specified directly + super(NsxVPluginV2TestCase, self).setUp( + plugin=plugin, + service_plugins=service_plugins, + ext_mgr=ext_mgr) + else: + super(NsxVPluginV2TestCase, self).setUp( + plugin=plugin, + ext_mgr=ext_mgr) self.addCleanup(self.fc2.reset_all) plugin_instance = manager.NeutronManager.get_plugin() plugin_instance.real_get_edge = plugin_instance._get_edge_id_by_rtr_id @@ -1770,6 +1780,8 @@ class TestL3ExtensionManager(object): router_size.EXTENDED_ATTRIBUTES_2_0.get(key, {})) l3.RESOURCE_ATTRIBUTE_MAP[key].update( router_availability_zone.EXTENDED_ATTRIBUTES_2_0.get(key, {})) + l3.RESOURCE_ATTRIBUTE_MAP[key].update( + l3_flavors.EXTENDED_ATTRIBUTES_2_0.get(key, {})) # Finally add l3 resources to the global attribute map attributes.RESOURCE_ATTRIBUTE_MAP.update( l3.RESOURCE_ATTRIBUTE_MAP) @@ -4736,3 +4748,177 @@ class TestSharedRouterTestCase(L3NatTest, L3NatTestCaseBase, def test_create_rotuer_without_az_hint(self): self._test_create_rotuer_with_az_hint(False) + + +class TestRouterFlavorTestCase(extension.ExtensionTestCase, + test_l3_plugin.L3NatTestCaseMixin, + L3NatTest + ): + + FLAVOR_PLUGIN = 'neutron.services.flavors.flavors_plugin.FlavorsPlugin' + + def setUp(self, plugin=PLUGIN_NAME): + # init the core plugin and flavors plugin + service_plugins = {plugin_const.FLAVORS: self.FLAVOR_PLUGIN} + super(TestRouterFlavorTestCase, self).setUp( + plugin=plugin, service_plugins=service_plugins) + self.plugin = manager.NeutronManager.get_plugin() + self.plugin._flv_plugin = ( + manager.NeutronManager.get_service_plugins(). + get(plugin_const.FLAVORS)) + self.plugin._process_router_flavor_create = mock.Mock() + + # init the availability zones + self.az_name = 'az7' + az_config = self.az_name + ':respool-7:datastore-7:True' + cfg.CONF.set_override('availability_zones', [az_config], group="nsxv") + self.plugin._availability_zones_data = ( + nsx_az.ConfiguredAvailabilityZones()) + + def _test_router_create_with_flavor( + self, metainfo, expected_data, + create_type=None, + create_size=None, + create_az=None): + + router_data = {'flavor_id': 'dummy', + 'tenant_id': 'whatever', + 'name': 'test_router', + 'admin_state_up': True} + + if create_type is not None: + router_data['router_type'] = create_type + if create_size is not None: + router_data['router_size'] = create_size + if create_az is not None: + router_data['availability_zone_hints'] = [create_az] + + flavor_data = {'service_type': plugin_const.L3_ROUTER_NAT, + 'enabled': True, + 'service_profiles': ['profile_id']} + + # Mock the flavors plugin + with mock.patch(self.FLAVOR_PLUGIN + '.get_flavor', + return_value=flavor_data): + with mock.patch(self.FLAVOR_PLUGIN + '.get_service_profile', + return_value={'metainfo': metainfo}): + router = self.plugin.create_router( + context.get_admin_context(), + {'router': router_data}) + for key, expected_val in expected_data.items(): + self.assertEqual(expected_val, router[key]) + + def test_router_create_with_flavor_different_sizes(self): + """Create exclusive router with size in flavor + """ + for size in ['compact', 'large', 'xlarge', 'quadlarge']: + metainfo = "{'router_size':'%s'}" % size + expected_router = {'router_type': 'exclusive', + 'router_size': size} + self._test_router_create_with_flavor( + metainfo, expected_router, + create_type='exclusive') + + def test_router_create_with_flavor_ex_different_sizes(self): + """Create exclusive router with size and type in flavor + """ + for size in ['compact', 'large', 'xlarge', 'quadlarge']: + metainfo = "{'router_size':'%s','router_type':'exclusive'}" % size + expected_router = {'router_type': 'exclusive', + 'router_size': size} + self._test_router_create_with_flavor( + metainfo, expected_router) + + def test_router_create_with_flavor_az(self): + """Create exclusive router with availability zone in flavor + """ + metainfo = "{'availability_zone_hints':'%s'}" % self.az_name + expected_router = {'router_type': 'exclusive', + 'availability_zone_hints': [self.az_name], + 'distributed': False} + self._test_router_create_with_flavor( + metainfo, expected_router, + create_type='exclusive') + + def test_router_create_with_flavor_shared(self): + """Create shared router with availability zone and type in flavor + """ + metainfo = ("{'availability_zone_hints':'%s'," + "'router_type':'shared'}" % self.az_name) + expected_router = {'router_type': 'shared', + 'availability_zone_hints': [self.az_name], + 'distributed': False} + self._test_router_create_with_flavor( + metainfo, expected_router) + + def test_router_create_with_flavor_distributed(self): + """Create distributed router with availability zone and type in flavor + """ + metainfo = ("{'availability_zone_hints':'%s'," + "'distributed':true}" % self.az_name) + expected_router = {'distributed': True, + 'availability_zone_hints': [self.az_name]} + self._test_router_create_with_flavor( + metainfo, expected_router) + + def test_router_flavor_error_parsing(self): + """Use the wrong format for the flavor metainfo + + It should be ignored, and default values are used + """ + metainfo = "xxx" + expected_router = {'distributed': False, + 'router_type': 'shared'} + self._test_router_create_with_flavor( + metainfo, expected_router) + + def _test_router_create_with_flavor_error( + self, metainfo, error_code, + create_type=None, + create_size=None, + create_az=None): + + router_data = {'flavor_id': 'dummy', + 'tenant_id': 'whatever', + 'name': 'test_router', + 'admin_state_up': True} + + if create_type is not None: + router_data['router_type'] = create_type + if create_size is not None: + router_data['router_size'] = create_size + if create_az is not None: + router_data['availability_zone_hints'] = [create_az] + + flavor_data = {'service_type': plugin_const.L3_ROUTER_NAT, + 'enabled': True, + 'service_profiles': ['profile_id']} + + # Mock the flavors plugin + with mock.patch(self.FLAVOR_PLUGIN + '.get_flavor', + return_value=flavor_data): + with mock.patch(self.FLAVOR_PLUGIN + '.get_service_profile', + return_value={'metainfo': metainfo}): + self.assertRaises(error_code, + self.plugin.create_router, + context.get_admin_context(), + {'router': router_data}) + + def test_router_flavor_size_conflict(self): + metainfo = "{'router_size':'large','router_type':'exclusive'}" + self._test_router_create_with_flavor_error( + metainfo, n_exc.BadRequest, + create_size='compact') + + def test_router_flavor_type_conflict(self): + metainfo = "{'router_size':'large','router_type':'exclusive'}" + self._test_router_create_with_flavor_error( + metainfo, n_exc.BadRequest, + create_type='shared') + + def test_router_flavor_az_conflict(self): + metainfo = ("{'availability_zone_hints':'%s'," + "'distributed':true}" % self.az_name) + self._test_router_create_with_flavor_error( + metainfo, n_exc.BadRequest, + create_az=['az2'])