From: Akihiro MOTOKI Date: Fri, 23 Aug 2013 06:22:04 +0000 (+0900) Subject: OpenFlow distributed router support in NEC plugin X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=d2a5c0f982e8e4f9fbdce17b90925b8d8df56c75;p=openstack-build%2Fneutron-build.git OpenFlow distributed router support in NEC plugin Implements blueprint nec-distribute-router Two types of neutron router will be supported: l3-agent and distributed. A type can be specified through "provider" attribute of a router. The naming of the attribute "provider" is intentional since I plan to support the service provider framework for router in the future and would like to make it easy to migrate. distributed router in NEC OpenFLow controller now does not support NAT, so l3-agent and distributed router coexists. To achieve it, l3-agent scheudler logic is modified in NEC plugin to exclude distributed routers from candidates of floating IP hosting routers. To support the above feature, the following related changes are done: - Adds a new driver to PFC driver which supports OpenFlow based router support in NEC OpenFlow products in PFlow v5. - Update ofc_client to extract detail error message from OpenFlow controller This commit also changes the following outside of NEC plugin: - Makes L3 agent notifiers configurable. l3-agent router and OpenFlow distributed router can coexist. Notication to l3-agent should be done only when routers are hosted by l3-agent, so we need custom L3 agent notifiers to filter non l3-agent routers. - Split test_agent_scheduler base class (in OVS plugin) into the base setup and testcases. By doing so we can implement custom testcases related to agent scheduler. Change-Id: I538201742950a61b92fb05c49a9256bc96ae9014 --- diff --git a/etc/neutron/plugins/nec/nec.ini b/etc/neutron/plugins/nec/nec.ini index cbcc0ec5c..cc9d812bf 100644 --- a/etc/neutron/plugins/nec/nec.ini +++ b/etc/neutron/plugins/nec/nec.ini @@ -41,3 +41,9 @@ firewall_driver = neutron.agent.linux.iptables_firewall.OVSHybridIptablesFirewal # Certificate file # cert_file = + +[provider] +# Default router provider to use. +# default_router_provider = l3-agent +# List of enabled router providers. +# router_providers = l3-agent,openflow diff --git a/neutron/db/l3_db.py b/neutron/db/l3_db.py index 7059c00ac..87cf9dc9c 100644 --- a/neutron/db/l3_db.py +++ b/neutron/db/l3_db.py @@ -91,6 +91,8 @@ class FloatingIP(model_base.BASEV2, models_v2.HasId, models_v2.HasTenant): class L3_NAT_db_mixin(l3.RouterPluginBase): """Mixin class to add L3/NAT router methods to db_plugin_base_v2.""" + l3_rpc_notifier = l3_rpc_agent_api.L3AgentNotify + def _network_model_hook(self, context, original_model, query): query = query.outerjoin(ExternalNetwork, (original_model.id == @@ -186,7 +188,7 @@ class L3_NAT_db_mixin(l3.RouterPluginBase): # Ensure we actually have something to update if r.keys(): router_db.update(r) - l3_rpc_agent_api.L3AgentNotify.routers_updated( + self.l3_rpc_notifier.routers_updated( context, [router_db['id']]) return self._make_router_dict(router_db) @@ -278,7 +280,7 @@ class L3_NAT_db_mixin(l3.RouterPluginBase): self._delete_port(context.elevated(), ports[0]['id']) context.session.delete(router) - l3_rpc_agent_api.L3AgentNotify.router_deleted(context, id) + self.l3_rpc_notifier.router_deleted(context, id) def get_router(self, context, id, fields=None): router = self._get_router(context, id) @@ -385,7 +387,7 @@ class L3_NAT_db_mixin(l3.RouterPluginBase): 'device_owner': DEVICE_OWNER_ROUTER_INTF, 'name': ''}}) - l3_rpc_agent_api.L3AgentNotify.routers_updated( + self.l3_rpc_notifier.routers_updated( context, [router_id], 'add_router_interface') info = {'id': router_id, 'tenant_id': subnet['tenant_id'], @@ -457,7 +459,7 @@ class L3_NAT_db_mixin(l3.RouterPluginBase): if not found: raise l3.RouterInterfaceNotFoundForSubnet(router_id=router_id, subnet_id=subnet_id) - l3_rpc_agent_api.L3AgentNotify.routers_updated( + self.l3_rpc_notifier.routers_updated( context, [router_id], 'remove_router_interface') info = {'id': router_id, 'tenant_id': subnet['tenant_id'], @@ -670,7 +672,7 @@ class L3_NAT_db_mixin(l3.RouterPluginBase): router_id = floatingip_db['router_id'] if router_id: - l3_rpc_agent_api.L3AgentNotify.routers_updated( + self.l3_rpc_notifier.routers_updated( context, [router_id], 'create_floatingip') return self._make_floatingip_dict(floatingip_db) @@ -693,8 +695,8 @@ class L3_NAT_db_mixin(l3.RouterPluginBase): if router_id and router_id != before_router_id: router_ids.append(router_id) if router_ids: - l3_rpc_agent_api.L3AgentNotify.routers_updated(context, router_ids, - 'update_floatingip') + self.l3_rpc_notifier.routers_updated( + context, router_ids, 'update_floatingip') return self._make_floatingip_dict(floatingip_db) def delete_floatingip(self, context, id): @@ -706,7 +708,7 @@ class L3_NAT_db_mixin(l3.RouterPluginBase): floatingip['floating_port_id'], l3_port_check=False) if router_id: - l3_rpc_agent_api.L3AgentNotify.routers_updated( + self.l3_rpc_notifier.routers_updated( context, [router_id], 'delete_floatingip') @@ -777,7 +779,7 @@ class L3_NAT_db_mixin(l3.RouterPluginBase): raise Exception(_('Multiple floating IPs found for port %s') % port_id) if router_id: - l3_rpc_agent_api.L3AgentNotify.routers_updated( + self.l3_rpc_notifier.routers_updated( context, [router_id]) def _network_is_external(self, context, net_id): diff --git a/neutron/db/migration/alembic_migrations/versions/66a59a7f516_nec_openflow_router.py b/neutron/db/migration/alembic_migrations/versions/66a59a7f516_nec_openflow_router.py new file mode 100644 index 000000000..ffead1147 --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/66a59a7f516_nec_openflow_router.py @@ -0,0 +1,68 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2013 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +"""NEC OpenFlow Router + +Revision ID: 66a59a7f516 +Revises: 32a65f71af51 +Create Date: 2013-09-03 22:16:31.446031 + +""" + +# revision identifiers, used by Alembic. +revision = '66a59a7f516' +down_revision = '32a65f71af51' + +# Change to ['*'] if this migration applies to all plugins + +migration_for_plugins = [ + 'neutron.plugins.nec.nec_plugin.NECPluginV2' +] + +from alembic import op +import sqlalchemy as sa + +from neutron.db import migration + + +def upgrade(active_plugins=None, options=None): + if not migration.should_run(active_plugins, migration_for_plugins): + return + + op.create_table( + 'ofcroutermappings', + sa.Column('ofc_id', sa.String(length=255), nullable=False), + sa.Column('quantum_id', sa.String(length=36), nullable=False), + sa.PrimaryKeyConstraint('quantum_id'), + sa.UniqueConstraint('ofc_id'), + ) + op.create_table( + 'routerproviders', + sa.Column('provider', sa.String(length=255), nullable=True), + sa.Column('router_id', sa.String(length=36), nullable=False), + sa.ForeignKeyConstraint(['router_id'], ['routers.id'], + ondelete='CASCADE'), + sa.PrimaryKeyConstraint('router_id'), + ) + + +def downgrade(active_plugins=None, options=None): + if not migration.should_run(active_plugins, migration_for_plugins): + return + + op.drop_table('routerproviders') + op.drop_table('ofcroutermappings') diff --git a/neutron/plugins/nec/common/config.py b/neutron/plugins/nec/common/config.py index 669887f59..bcd4cc8d2 100644 --- a/neutron/plugins/nec/common/config.py +++ b/neutron/plugins/nec/common/config.py @@ -19,6 +19,7 @@ from oslo.config import cfg from neutron.agent.common import config from neutron.openstack.common import rpc # noqa +from neutron.plugins.nec.common import constants as nconst ovs_opts = [ @@ -49,10 +50,20 @@ ofc_opts = [ help=_("Certificate file")), ] +provider_opts = [ + cfg.StrOpt('default_router_provider', + default=nconst.DEFAULT_ROUTER_PROVIDER, + help=_('Default router provider to use.')), + cfg.ListOpt('router_providers', + default=nconst.DEFAULT_ROUTER_PROVIDERS, + help=_('List of enabled router providers.')) +] + cfg.CONF.register_opts(ovs_opts, "OVS") cfg.CONF.register_opts(agent_opts, "AGENT") cfg.CONF.register_opts(ofc_opts, "OFC") +cfg.CONF.register_opts(provider_opts, "PROVIDER") config.register_agent_state_opts_helper(cfg.CONF) config.register_root_helper(cfg.CONF) @@ -61,3 +72,4 @@ CONF = cfg.CONF OVS = cfg.CONF.OVS AGENT = cfg.CONF.AGENT OFC = cfg.CONF.OFC +PROVIDER = cfg.CONF.PROVIDER diff --git a/neutron/plugins/nec/common/constants.py b/neutron/plugins/nec/common/constants.py new file mode 100644 index 000000000..b1bc7e5b3 --- /dev/null +++ b/neutron/plugins/nec/common/constants.py @@ -0,0 +1,24 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2013 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +ROUTER_PROVIDER_L3AGENT = 'l3-agent' +ROUTER_PROVIDER_OPENFLOW = 'openflow' + +DEFAULT_ROUTER_PROVIDERS = [ROUTER_PROVIDER_L3AGENT, ROUTER_PROVIDER_OPENFLOW] +DEFAULT_ROUTER_PROVIDER = ROUTER_PROVIDER_L3AGENT + +ROUTER_STATUS_ACTIVE = 'ACTIVE' +ROUTER_STATUS_ERROR = 'ERROR' diff --git a/neutron/plugins/nec/common/exceptions.py b/neutron/plugins/nec/common/exceptions.py index 74e000b5c..d6b2b3b50 100644 --- a/neutron/plugins/nec/common/exceptions.py +++ b/neutron/plugins/nec/common/exceptions.py @@ -21,6 +21,12 @@ from neutron.common import exceptions as qexc class OFCException(qexc.NeutronException): message = _("An OFC exception has occurred: %(reason)s") + def __init__(self, **kwargs): + super(OFCException, self).__init__(**kwargs) + self.status = kwargs.get('status') + self.err_msg = kwargs.get('err_msg') + self.err_code = kwargs.get('err_code') + class NECDBException(qexc.NeutronException): message = _("An exception occurred in NECPluginV2 DB: %(reason)s") @@ -44,3 +50,22 @@ class ProfilePortInfoInvalidDataPathId(qexc.InvalidInput): class ProfilePortInfoInvalidPortNo(qexc.InvalidInput): message = _('Invalid input for operation: ' 'portinfo:port_no should be [0:65535]') + + +class RouterExternalGatewayNotSupported(qexc.BadRequest): + message = _("Router (provider=%(provider)s) does not support " + "an external network") + + +class ProviderNotFound(qexc.NotFound): + message = _("Provider %(provider)s could not be found") + + +class RouterOverLimit(qexc.Conflict): + message = _("Cannot create more routers with provider=%(provider)s") + + +class RouterProviderMismatch(qexc.Conflict): + message = _("Provider of Router %(router_id)s is %(provider)s. " + "This operation is supported only for router provider " + "%(expected_provider)s.") diff --git a/neutron/plugins/nec/common/ofc_client.py b/neutron/plugins/nec/common/ofc_client.py index 44ba4dfd9..6df8b4f03 100644 --- a/neutron/plugins/nec/common/ofc_client.py +++ b/neutron/plugins/nec/common/ofc_client.py @@ -46,12 +46,26 @@ class OFCClient(object): self.cert_file = cert_file self.connection = None - def get_connection_type(self): - """Returns the proper connection type.""" + def get_connection(self): + """Returns the proper connection.""" if self.use_ssl: - return httplib.HTTPSConnection + connection_type = httplib.HTTPSConnection else: - return httplib.HTTPConnection + connection_type = httplib.HTTPConnection + + # Open connection and send request, handling SSL certs + certs = {'key_file': self.key_file, 'cert_file': self.cert_file} + certs = dict((x, certs[x]) for x in certs if certs[x] is not None) + if self.use_ssl and len(certs): + conn = connection_type(self.host, self.port, **certs) + else: + conn = connection_type(self.host, self.port) + return conn + + def _format_error_message(self, status, detail): + detail = ' ' + detail if detail else '' + return (_("Operation on OFC failed: %(status)s%(msg)s") % + {'status': status, 'msg': detail}) def do_request(self, method, action, body=None): LOG.debug(_("Client request: %(host)s:%(port)s " @@ -61,32 +75,40 @@ class OFCClient(object): if type(body) is dict: body = json.dumps(body) try: - connection_type = self.get_connection_type() + conn = self.get_connection() headers = {"Content-Type": "application/json"} - # Open connection and send request, handling SSL certs - certs = {'key_file': self.key_file, 'cert_file': self.cert_file} - certs = dict((x, certs[x]) for x in certs if certs[x] is not None) - if self.use_ssl and len(certs): - conn = connection_type(self.host, self.port, **certs) - else: - conn = connection_type(self.host, self.port) conn.request(method, action, body, headers) res = conn.getresponse() data = res.read() LOG.debug(_("OFC returns [%(status)s:%(data)s]"), {'status': res.status, 'data': data}) + + # Try to decode JSON data if possible. + try: + data = json.loads(data) + except (ValueError, TypeError): + pass + if res.status in (httplib.OK, httplib.CREATED, httplib.ACCEPTED, httplib.NO_CONTENT): - if data and len(data) > 1: - return json.loads(data) + return data else: - reason = _("An operation on OFC is failed.") - raise nexc.OFCException(reason=reason) + LOG.warning(_("Operation on OFC failed: " + "status=%(status), detail=%(detail)"), + {'status': res.status, 'detail': data}) + params = {'reason': _("Operation on OFC failed"), + 'status': res.status} + if isinstance(data, dict): + params['err_code'] = data.get('err_code') + params['err_msg'] = data.get('err_msg') + else: + params['err_msg'] = data + raise nexc.OFCException(**params) except (socket.error, IOError) as e: - reason = _("Failed to connect OFC : %s") % str(e) + reason = _("Failed to connect OFC : %s") % e LOG.error(reason) raise nexc.OFCException(reason=reason) diff --git a/neutron/plugins/nec/db/api.py b/neutron/plugins/nec/db/api.py index 333065122..e606861ff 100644 --- a/neutron/plugins/nec/db/api.py +++ b/neutron/plugins/nec/db/api.py @@ -36,6 +36,7 @@ OFP_VLAN_NONE = 0xffff resource_map = {'ofc_tenant': nmodels.OFCTenantMapping, 'ofc_network': nmodels.OFCNetworkMapping, 'ofc_port': nmodels.OFCPortMapping, + 'ofc_router': nmodels.OFCRouterMapping, 'ofc_packet_filter': nmodels.OFCFilterMapping} old_resource_map = {'ofc_tenant': nmodels.OFCTenant, @@ -48,7 +49,9 @@ old_resource_map = {'ofc_tenant': nmodels.OFCTenant, def _get_resource_model(resource, old_style): if old_style: - return old_resource_map[resource] + # NOTE: Some new resources are not defined in old_resource_map. + # In such case None is returned. + return old_resource_map.get(resource) else: return resource_map[resource] @@ -62,8 +65,10 @@ def clear_db(base=model_base.BASEV2): def get_ofc_item(session, resource, neutron_id, old_style=False): + model = _get_resource_model(resource, old_style) + if not model: + return None try: - model = _get_resource_model(resource, old_style) return session.query(model).filter_by(quantum_id=neutron_id).one() except sa.orm.exc.NoResultFound: return None diff --git a/neutron/plugins/nec/db/models.py b/neutron/plugins/nec/db/models.py index 7d9cf1069..bf2dfea6d 100644 --- a/neutron/plugins/nec/db/models.py +++ b/neutron/plugins/nec/db/models.py @@ -47,6 +47,10 @@ class OFCPortMapping(model_base.BASEV2, NeutronId, OFCId): """Represents a Port on OpenFlow Network/Controller.""" +class OFCRouterMapping(model_base.BASEV2, NeutronId, OFCId): + """Represents a router on OpenFlow Network/Controller.""" + + class OFCFilterMapping(model_base.BASEV2, NeutronId, OFCId): """Represents a Filter on OpenFlow Network/Controller.""" diff --git a/neutron/plugins/nec/db/router.py b/neutron/plugins/nec/db/router.py new file mode 100644 index 000000000..9659cd7fd --- /dev/null +++ b/neutron/plugins/nec/db/router.py @@ -0,0 +1,92 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import sqlalchemy as sa +from sqlalchemy import orm +from sqlalchemy.orm import exc as sa_exc + +from neutron.db import l3_db +from neutron.db import models_v2 +from neutron.openstack.common import log as logging + +LOG = logging.getLogger(__name__) + + +class RouterProvider(models_v2.model_base.BASEV2): + """Represents a binding of router_id to provider.""" + provider = sa.Column(sa.String(255)) + router_id = sa.Column(sa.String(36), + sa.ForeignKey('routers.id', ondelete="CASCADE"), + primary_key=True) + + router = orm.relationship(l3_db.Router, uselist=False, + backref=orm.backref('provider', uselist=False, + lazy='joined', + cascade='delete')) + + +def _get_router_providers_query(query, provider=None, router_ids=None): + if provider: + query = query.filter_by(provider=provider) + if router_ids: + column = RouterProvider.router_id + query = query.filter(column.in_(router_ids)) + return query + + +def get_router_providers(session, provider=None, router_ids=None): + """Retrieve a list of a pair of router ID and its provider.""" + query = session.query(RouterProvider) + query = _get_router_providers_query(query, provider, router_ids) + return [{'provider': router.provider, 'router_id': router.router_id} + for router in query] + + +def get_routers_by_provider(session, provider, router_ids=None): + """Retrieve a list of router IDs with the given provider.""" + query = session.query(RouterProvider.router_id) + query = _get_router_providers_query(query, provider, router_ids) + return [router[0] for router in query] + + +def get_router_count_by_provider(session, provider, tenant_id=None): + """Return the number of routers with the given provider.""" + query = session.query(RouterProvider).filter_by(provider=provider) + if tenant_id: + query = (query.join('router'). + filter(l3_db.Router.tenant_id == tenant_id)) + return query.count() + + +def get_provider_by_router(session, router_id): + """Retrieve a provider of the given router.""" + try: + binding = (session.query(RouterProvider). + filter_by(router_id=router_id). + one()) + except sa_exc.NoResultFound: + return None + return binding.provider + + +def add_router_provider_binding(session, provider, router_id): + """Add a router provider association.""" + LOG.debug(_("Add provider binding " + "(router=%(router_id)s, provider=%(provider)s)"), + {'router_id': router_id, 'provider': provider}) + binding = RouterProvider(provider=provider, router_id=router_id) + session.add(binding) + return binding diff --git a/neutron/plugins/nec/drivers/__init__.py b/neutron/plugins/nec/drivers/__init__.py index 178b34d34..2bb051685 100644 --- a/neutron/plugins/nec/drivers/__init__.py +++ b/neutron/plugins/nec/drivers/__init__.py @@ -28,7 +28,9 @@ DRIVER_LIST = { 'trema_mac': DRIVER_PATH % "trema.TremaMACBaseDriver", 'pfc': DRIVER_PATH % "pfc.PFCV4Driver", 'pfc_v3': DRIVER_PATH % "pfc.PFCV3Driver", - 'pfc_v4': DRIVER_PATH % "pfc.PFCV4Driver"} + 'pfc_v4': DRIVER_PATH % "pfc.PFCV4Driver", + 'pfc_v5': DRIVER_PATH % "pfc.PFCV5Driver", +} def get_driver(driver_name): diff --git a/neutron/plugins/nec/drivers/pfc.py b/neutron/plugins/nec/drivers/pfc.py index e60bb8907..e8d23677f 100644 --- a/neutron/plugins/nec/drivers/pfc.py +++ b/neutron/plugins/nec/drivers/pfc.py @@ -33,6 +33,8 @@ class PFCDriverBase(ofc_driver_base.OFCDriverBase): The class implements the API for PFC V4.0 or later. """ + router_supported = False + def __init__(self, conf_ofc): self.client = ofc_client.OFCClient(host=conf_ofc.host, port=conf_ofc.port, @@ -73,6 +75,10 @@ class PFCDriverBase(ofc_driver_base.OFCDriverBase): """ return self._generate_pfc_str(desc)[:127] + def _extract_ofc_network_id(self, ofc_network_id): + # ofc_network_id : /tenants//networks/ + return ofc_network_id.split('/')[4] + def create_tenant(self, description, tenant_id=None): ofc_tenant_id = self._generate_pfc_id(tenant_id) body = {'id': ofc_tenant_id} @@ -136,6 +142,66 @@ class PFCDriverBase(ofc_driver_base.OFCDriverBase): return '%(network)s/ports/%(port)s' % params +class PFCRouterDriverMixin(object): + + router_supported = True + router_nat_supported = False + + def create_router(self, ofc_tenant_id, router_id, description): + path = '%s/routers' % ofc_tenant_id + res = self.client.post(path, body=None) + ofc_router_id = res['id'] + return path + '/' + ofc_router_id + + def delete_router(self, ofc_router_id): + return self.client.delete(ofc_router_id) + + def add_router_interface(self, ofc_router_id, ofc_net_id, + ip_address=None, mac_address=None): + # ip_address : / (e.g., 10.0.0.0/24) + path = '%s/interfaces' % ofc_router_id + body = {'net_id': self._extract_ofc_network_id(ofc_net_id)} + if ip_address: + body['ip_address'] = ip_address + if mac_address: + body['mac_address'] = mac_address + res = self.client.post(path, body=body) + return path + '/' + res['id'] + + def update_router_interface(self, ofc_router_inf_id, + ip_address=None, mac_address=None): + # ip_address : / (e.g., 10.0.0.0/24) + if not ip_address and not mac_address: + return + body = {} + if ip_address: + body['ip_address'] = ip_address + if mac_address: + body['mac_address'] = mac_address + return self.client.put(ofc_router_inf_id, body=body) + + def delete_router_interface(self, ofc_router_inf_id): + return self.client.delete(ofc_router_inf_id) + + def list_router_routes(self, ofc_router_id): + path = '%s/routes' % ofc_router_id + ret = self.client.get(path) + # Prepend ofc_router_id to route_id + for r in ret['routes']: + r['id'] = ofc_router_id + '/routes/' + r['id'] + return ret['routes'] + + def add_router_route(self, ofc_router_id, destination, nexthop): + path = '%s/routes' % ofc_router_id + body = {'destination': destination, + 'nexthop': nexthop} + ret = self.client.post(path, body=body) + return path + '/' + ret['id'] + + def delete_router_route(self, ofc_router_route_id): + return self.client.delete(ofc_router_route_id) + + class PFCV3Driver(PFCDriverBase): def create_tenant(self, description, tenant_id): @@ -148,3 +214,7 @@ class PFCV3Driver(PFCDriverBase): class PFCV4Driver(PFCDriverBase): pass + + +class PFCV5Driver(PFCRouterDriverMixin, PFCDriverBase): + pass diff --git a/neutron/plugins/nec/drivers/trema.py b/neutron/plugins/nec/drivers/trema.py index e4081e1d2..5a2d29524 100644 --- a/neutron/plugins/nec/drivers/trema.py +++ b/neutron/plugins/nec/drivers/trema.py @@ -27,6 +27,8 @@ class TremaDriverBase(ofc_driver_base.OFCDriverBase): networks_path = "/networks" network_path = "/networks/%s" + router_supported = False + def __init__(self, conf_ofc): # Trema sliceable REST API does not support HTTPS self.client = ofc_client.OFCClient(host=conf_ofc.host, @@ -74,7 +76,7 @@ class TremaDriverBase(ofc_driver_base.OFCDriverBase): return self.network_path % ofc_network_id -class TremaFilterDriver(object): +class TremaFilterDriverMixin(object): """Trema (Sliceable Switch) PacketFilter Driver Mixin.""" filters_path = "/filters" filter_path = "/filters/%s" @@ -173,7 +175,7 @@ class TremaFilterDriver(object): return self.filter_path % ofc_filter_id -class TremaPortBaseDriver(TremaDriverBase, TremaFilterDriver): +class TremaPortBaseDriver(TremaDriverBase, TremaFilterDriverMixin): """Trema (Sliceable Switch) Driver for port base binding. TremaPortBaseDriver uses port base binding. @@ -211,7 +213,7 @@ class TremaPortBaseDriver(TremaDriverBase, TremaFilterDriver): 'port': ofc_port_id} -class TremaPortMACBaseDriver(TremaDriverBase, TremaFilterDriver): +class TremaPortMACBaseDriver(TremaDriverBase, TremaFilterDriverMixin): """Trema (Sliceable Switch) Driver for port-mac base binding. TremaPortBaseDriver uses port-mac base binding. diff --git a/neutron/plugins/nec/extensions/router_provider.py b/neutron/plugins/nec/extensions/router_provider.py new file mode 100644 index 000000000..d893a4c18 --- /dev/null +++ b/neutron/plugins/nec/extensions/router_provider.py @@ -0,0 +1,61 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2013 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from neutron.api import extensions +from neutron.api.v2 import attributes +from neutron.openstack.common import log as logging + + +LOG = logging.getLogger(__name__) + +ROUTER_PROVIDER = 'provider' + +ROUTER_PROVIDER_ATTRIBUTE = { + 'routers': {ROUTER_PROVIDER: + {'allow_post': True, + 'allow_put': False, + 'is_visible': True, + 'default': attributes.ATTR_NOT_SPECIFIED} + } +} + + +class Router_provider(extensions.ExtensionDescriptor): + @classmethod + def get_name(cls): + return "Router Provider" + + @classmethod + def get_alias(cls): + return "router_provider" + + @classmethod + def get_description(cls): + return "Router Provider Support" + + @classmethod + def get_namespace(cls): + return "http://docs.openstack.org/ext/router_provider/api/v1.0" + + @classmethod + def get_updated(cls): + return "2013-08-20T10:00:00-00:00" + + def get_extended_resources(self, version): + if version == "2.0": + return ROUTER_PROVIDER_ATTRIBUTE + else: + return {} diff --git a/neutron/plugins/nec/nec_plugin.py b/neutron/plugins/nec/nec_plugin.py index df3ee1ef6..26088148d 100644 --- a/neutron/plugins/nec/nec_plugin.py +++ b/neutron/plugins/nec/nec_plugin.py @@ -18,7 +18,6 @@ from neutron.agent import securitygroups_rpc as sg_rpc from neutron.api.rpc.agentnotifiers import dhcp_rpc_agent_api -from neutron.api.rpc.agentnotifiers import l3_rpc_agent_api from neutron.api.v2 import attributes as attrs from neutron.common import constants as const from neutron.common import exceptions as q_exc @@ -28,8 +27,6 @@ from neutron.db import agents_db from neutron.db import agentschedulers_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_gwmode_db from neutron.db import l3_rpc_base from neutron.db import portbindings_base from neutron.db import portbindings_db @@ -44,6 +41,8 @@ from neutron.openstack.common import uuidutils from neutron.plugins.nec.common import config from neutron.plugins.nec.common import exceptions as nexc from neutron.plugins.nec.db import api as ndb +from neutron.plugins.nec.db import router as rdb +from neutron.plugins.nec import nec_router from neutron.plugins.nec import ofc_manager from neutron.plugins.nec import packet_filter @@ -51,11 +50,10 @@ LOG = logging.getLogger(__name__) class NECPluginV2(db_base_plugin_v2.NeutronDbPluginV2, - extraroute_db.ExtraRoute_db_mixin, - l3_gwmode_db.L3_NAT_db_mixin, + nec_router.RouterMixin, sg_db_rpc.SecurityGroupServerRpcMixin, - agentschedulers_db.L3AgentSchedulerDbMixin, agentschedulers_db.DhcpAgentSchedulerDbMixin, + nec_router.L3AgentSchedulerDbMixin, packet_filter.PacketFilterMixin, portbindings_db.PortBindingMixin): """NECPluginV2 controls an OpenFlow Controller. @@ -70,12 +68,18 @@ class NECPluginV2(db_base_plugin_v2.NeutronDbPluginV2, The port binding extension enables an external application relay information to and from the plugin. """ - _supported_extension_aliases = ["router", "ext-gw-mode", "quotas", - "binding", "security-group", - "extraroute", "agent", - "l3_agent_scheduler", + _supported_extension_aliases = ["agent", + "binding", "dhcp_agent_scheduler", - "packet-filter"] + "ext-gw-mode", + "extraroute", + "l3_agent_scheduler", + "packet-filter", + "quotas", + "router", + "router_provider", + "security-group", + ] @property def supported_extension_aliases(self): @@ -99,6 +103,7 @@ class NECPluginV2(db_base_plugin_v2.NeutronDbPluginV2, 'neutron/plugins/nec/extensions') self.setup_rpc() + self.l3_rpc_notifier = nec_router.L3AgentNotifyAPI() self.network_scheduler = importutils.import_object( config.CONF.network_scheduler_driver @@ -107,6 +112,20 @@ class NECPluginV2(db_base_plugin_v2.NeutronDbPluginV2, config.CONF.router_scheduler_driver ) + nec_router.load_driver(self, self.ofc) + self.port_handlers = { + 'create': { + const.DEVICE_OWNER_ROUTER_GW: self.create_router_port, + const.DEVICE_OWNER_ROUTER_INTF: self.create_router_port, + 'default': self.activate_port_if_ready, + }, + 'delete': { + const.DEVICE_OWNER_ROUTER_GW: self.delete_router_port, + const.DEVICE_OWNER_ROUTER_INTF: self.delete_router_port, + 'default': self.deactivate_port, + } + } + def setup_rpc(self): self.topic = topics.PLUGIN self.conn = rpc.create_connection(new=True) @@ -115,13 +134,14 @@ class NECPluginV2(db_base_plugin_v2.NeutronDbPluginV2, dhcp_rpc_agent_api.DhcpAgentNotifyAPI() ) self.agent_notifiers[const.AGENT_TYPE_L3] = ( - l3_rpc_agent_api.L3AgentNotify + nec_router.L3AgentNotifyAPI() ) # NOTE: callback_sg is referred to from the sg unit test. self.callback_sg = SecurityGroupServerRpcCallback() callbacks = [NECPluginV2RPCCallbacks(self), - DhcpRpcCallback(), L3RpcCallback(), + DhcpRpcCallback(), + L3RpcCallback(), self.callback_sg, agents_db.AgentExtRpcCallback()] self.dispatcher = q_rpc.PluginRpcDispatcher(callbacks) @@ -131,10 +151,35 @@ class NECPluginV2(db_base_plugin_v2.NeutronDbPluginV2, def _update_resource_status(self, context, resource, id, status): """Update status of specified resource.""" - request = {} - request[resource] = dict(status=status) - obj_updater = getattr(super(NECPluginV2, self), "update_%s" % resource) - obj_updater(context, id, request) + request = {'status': status} + obj_getter = getattr(self, '_get_%s' % resource) + with context.session.begin(subtransactions=True): + obj_db = obj_getter(context, id) + obj_db.update(request) + + def _check_ofc_tenant_in_use(self, context, tenant_id): + """Check if the specified tenant is used.""" + # All networks are created on OFC + filters = {'tenant_id': [tenant_id]} + if self.get_networks_count(context, filters=filters): + return True + if rdb.get_router_count_by_provider(context.session, + nec_router.PROVIDER_OPENFLOW, + tenant_id): + return True + return False + + def _cleanup_ofc_tenant(self, context, tenant_id): + if not self._check_ofc_tenant_in_use(context, tenant_id): + try: + if self.ofc.exists_ofc_tenant(context, tenant_id): + self.ofc.delete_ofc_tenant(context, tenant_id) + else: + LOG.debug(_('_cleanup_ofc_tenant: No OFC tenant for %s'), + tenant_id) + except (nexc.OFCException, nexc.OFCConsistencyBroken) as exc: + reason = _("delete_ofc_tenant() failed due to %s") % exc + LOG.warn(reason) def activate_port_if_ready(self, context, port, network=None): """Activate port by creating port on OFC if ready. @@ -315,7 +360,6 @@ class NECPluginV2(db_base_plugin_v2.NeutronDbPluginV2, self.delete_packet_filter(context, pf['id']) try: - # 'net' parameter is required to lookup old OFC mapping self.ofc.delete_ofc_network(context, id, net) except (nexc.OFCException, nexc.OFCConsistencyBroken) as exc: reason = _("delete_network() failed due to %s") % exc @@ -326,15 +370,7 @@ class NECPluginV2(db_base_plugin_v2.NeutronDbPluginV2, super(NECPluginV2, self).delete_network(context, id) - # delete unnessary ofc_tenant - filters = dict(tenant_id=[tenant_id]) - nets = super(NECPluginV2, self).get_networks(context, filters=filters) - if not nets: - try: - self.ofc.delete_ofc_tenant(context, tenant_id) - except (nexc.OFCException, nexc.OFCConsistencyBroken) as exc: - reason = _("delete_ofc_tenant() failed due to %s") % exc - LOG.warn(reason) + self._cleanup_ofc_tenant(context, tenant_id) def _get_base_binding_dict(self): binding = { @@ -449,6 +485,14 @@ class NECPluginV2(db_base_plugin_v2.NeutronDbPluginV2, context, port_data, port) return portinfo_changed + def _get_port_handler(self, operation, device_owner): + handlers = self.port_handlers[operation] + handler = handlers.get(device_owner) + if handler: + return handler + else: + return handlers['default'] + def create_port(self, context, port): """Create a new port entry on DB, then try to activate it.""" LOG.debug(_("NECPluginV2.create_port() called, port=%s ."), port) @@ -465,7 +509,8 @@ class NECPluginV2(db_base_plugin_v2.NeutronDbPluginV2, context, port, sgids) self.notify_security_groups_member_updated(context, port) - return self.activate_port_if_ready(context, port) + handler = self._get_port_handler('create', port['device_owner']) + return handler(context, port) def _update_ofc_port_if_required(self, context, old_port, new_port, portinfo_changed): @@ -539,7 +584,9 @@ class NECPluginV2(db_base_plugin_v2.NeutronDbPluginV2, # Thus we need to call self.get_port() instead of super().get_port() port = self.get_port(context, id) - port = self.deactivate_port(context, port) + handler = self._get_port_handler('delete', port['device_owner']) + port = handler(context, port) + # port = self.deactivate_port(context, port) if port['status'] == const.PORT_STATUS_ERROR: reason = _("Failed to delete port=%s from OFC.") % id raise nexc.OFCException(reason=reason) diff --git a/neutron/plugins/nec/nec_router.py b/neutron/plugins/nec/nec_router.py new file mode 100644 index 000000000..be1f0e37c --- /dev/null +++ b/neutron/plugins/nec/nec_router.py @@ -0,0 +1,356 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2013 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# @author: Akihiro Motoki + +from neutron.api.rpc.agentnotifiers import l3_rpc_agent_api +from neutron.api.v2 import attributes as attr +from neutron.common import exceptions as q_exc +from neutron.db import agentschedulers_db +from neutron.db import db_base_plugin_v2 +from neutron.db import extraroute_db +from neutron.db import l3_db +from neutron.db import l3_gwmode_db +from neutron.db import models_v2 +from neutron.extensions import l3 +from neutron.openstack.common import importutils +from neutron.openstack.common import log as logging +from neutron.plugins.nec.common import config +from neutron.plugins.nec.common import constants as nconst +from neutron.plugins.nec.common import exceptions as nexc +from neutron.plugins.nec.db import router as rdb +from neutron.plugins.nec.extensions import router_provider as ext_provider + +LOG = logging.getLogger(__name__) + +PROVIDER_L3AGENT = nconst.ROUTER_PROVIDER_L3AGENT +PROVIDER_OPENFLOW = nconst.ROUTER_PROVIDER_OPENFLOW + +ROUTER_DRIVER_PATH = 'neutron.plugins.nec.router_drivers.' +ROUTER_DRIVER_MAP = { + PROVIDER_L3AGENT: ROUTER_DRIVER_PATH + 'RouterL3AgentDriver', + PROVIDER_OPENFLOW: ROUTER_DRIVER_PATH + 'RouterOpenFlowDriver' +} + +ROUTER_DRIVERS = {} + +STATUS_ACTIVE = nconst.ROUTER_STATUS_ACTIVE +STATUS_ERROR = nconst.ROUTER_STATUS_ERROR + + +class RouterMixin(extraroute_db.ExtraRoute_db_mixin, + l3_gwmode_db.L3_NAT_db_mixin): + + def create_router(self, context, router): + """Create a new router entry on DB, and create it on OFC.""" + LOG.debug(_("RouterMixin.create_router() called, " + "router=%s ."), router) + tenant_id = self._get_tenant_id_for_create(context, router['router']) + + provider = get_provider_with_default( + router['router'].get(ext_provider.ROUTER_PROVIDER)) + driver = get_driver_by_provider(provider) + + with context.session.begin(subtransactions=True): + new_router = super(RouterMixin, self).create_router(context, + router) + new_router['gw_port'] = self._get_gw_port_detail( + context, driver, new_router['gw_port_id']) + rdb.add_router_provider_binding(context.session, + provider, str(new_router['id'])) + self._extend_router_dict_provider(new_router, provider) + + # create router on the network controller + try: + return driver.create_router(context, tenant_id, new_router) + except nexc.RouterOverLimit: + super(RouterMixin, self).delete_router(context, new_router['id']) + raise + + def update_router(self, context, router_id, router): + LOG.debug(_("RouterMixin.update_router() called, " + "id=%(id)s, router=%(router)s ."), + {'id': router_id, 'router': router}) + + with context.session.begin(subtransactions=True): + old_rtr = super(RouterMixin, self).get_router(context, router_id) + provider = old_rtr[ext_provider.ROUTER_PROVIDER] + driver = get_driver_by_provider(provider) + old_rtr['gw_port'] = self._get_gw_port_detail( + context, driver, old_rtr['gw_port_id']) + new_rtr = super(RouterMixin, self).update_router( + context, router_id, router) + new_rtr['gw_port'] = self._get_gw_port_detail( + context, driver, new_rtr['gw_port_id']) + driver.update_router(context, router_id, old_rtr, new_rtr) + return new_rtr + + def delete_router(self, context, router_id): + LOG.debug(_("RouterMixin.delete_router() called, id=%s."), router_id) + + router = super(RouterMixin, self).get_router(context, router_id) + tenant_id = router['tenant_id'] + # Since l3_db.delete_router() has no interaction with the plugin layer, + # we need to check if the router can be deleted first. + self._check_router_in_use(context, router_id) + driver = self._get_router_driver_by_id(context, router_id) + # If gw_port exists, remove it. + gw_port = self._get_gw_port(context, router_id) + if gw_port: + driver.delete_interface(context, router_id, gw_port) + driver.delete_router(context, router_id, router) + + super(RouterMixin, self).delete_router(context, router_id) + + self._cleanup_ofc_tenant(context, tenant_id) + + def add_router_interface(self, context, router_id, interface_info): + LOG.debug(_("RouterMixin.add_router_interface() called, " + "id=%(id)s, interface=%(interface)s."), + {'id': router_id, 'interface': interface_info}) + return super(RouterMixin, self).add_router_interface( + context, router_id, interface_info) + + def remove_router_interface(self, context, router_id, interface_info): + LOG.debug(_("RouterMixin.remove_router_interface() called, " + "id=%(id)s, interface=%(interface)s."), + {'id': router_id, 'interface': interface_info}) + return super(RouterMixin, self).remove_router_interface( + context, router_id, interface_info) + + def create_router_port(self, context, port): + # This method is called from plugin.create_port() + router_id = port['device_id'] + driver = self._get_router_driver_by_id(context, router_id) + port = driver.add_interface(context, router_id, port) + return port + + def delete_router_port(self, context, port): + # This method is called from plugin.delete_port() + router_id = port['device_id'] + driver = self._get_router_driver_by_id(context, router_id) + return driver.delete_interface(context, router_id, port) + + def _get_gw_port_detail(self, context, driver, gw_port_id): + if not gw_port_id or not driver.need_gw_info: + return + ctx_elevated = context.elevated() + gw_port = self._get_port(ctx_elevated, gw_port_id) + # At this moment gw_port has been created, so it is guaranteed + # that fixed_ip is assigned for the gw_port. + ext_subnet_id = gw_port['fixed_ips'][0]['subnet_id'] + ext_subnet = self._get_subnet(ctx_elevated, ext_subnet_id) + gw_info = {'network_id': gw_port['network_id'], + 'ip_address': gw_port['fixed_ips'][0]['ip_address'], + 'mac_address': gw_port['mac_address'], + 'cidr': ext_subnet['cidr'], + 'gateway_ip': ext_subnet['gateway_ip']} + return gw_info + + def _get_gw_port(self, context, router_id): + device_filter = {'device_id': [router_id], + 'device_owner': [l3_db.DEVICE_OWNER_ROUTER_GW]} + ports = self.get_ports(context.elevated(), filters=device_filter) + if ports: + return ports[0] + + def _check_router_in_use(self, context, router_id): + with context.session.begin(subtransactions=True): + # Ensure that the router is not used + router_filter = {'router_id': [router_id]} + fips = self.get_floatingips_count(context.elevated(), + filters=router_filter) + if fips: + raise l3.RouterInUse(router_id=router_id) + + device_filter = {'device_id': [router_id], + 'device_owner': [l3_db.DEVICE_OWNER_ROUTER_INTF]} + ports = self.get_ports_count(context.elevated(), + filters=device_filter) + if ports: + raise l3.RouterInUse(router_id=router_id) + + def _get_router_for_floatingip(self, context, internal_port, + internal_subnet_id, + external_network_id): + """Get a router for a requested floating IP. + + OpenFlow vrouter does not support NAT, so we need to exclude them + from candidate routers for floating IP association. + This method is called in l3_db.get_assoc_data(). + """ + subnet_db = self._get_subnet(context, internal_subnet_id) + if not subnet_db['gateway_ip']: + msg = (_('Cannot add floating IP to port on subnet %s ' + 'which has no gateway_ip') % internal_subnet_id) + raise q_exc.BadRequest(resource='floatingip', msg=msg) + + # find router interface ports on this network + router_intf_qry = context.session.query(models_v2.Port) + router_intf_ports = router_intf_qry.filter_by( + network_id=internal_port['network_id'], + device_owner=l3_db.DEVICE_OWNER_ROUTER_INTF) + + for intf_p in router_intf_ports: + if intf_p['fixed_ips'][0]['subnet_id'] == internal_subnet_id: + router_id = intf_p['device_id'] + router_gw_qry = context.session.query(models_v2.Port) + has_gw_port = router_gw_qry.filter_by( + network_id=external_network_id, + device_id=router_id, + device_owner=l3_db.DEVICE_OWNER_ROUTER_GW).count() + driver = self._get_router_driver_by_id(context, router_id) + if (has_gw_port and driver.floating_ip_support()): + return router_id + + raise l3.ExternalGatewayForFloatingIPNotFound( + subnet_id=internal_subnet_id, + external_network_id=external_network_id, + port_id=internal_port['id']) + + def _get_sync_routers(self, context, router_ids=None, active=None): + """Query routers and their gw ports for l3 agent. + + The difference from the superclass in l3_db is that this method + only lists routers hosted on l3-agents. + """ + router_list = super(RouterMixin, self)._get_sync_routers( + context, router_ids, active) + if router_list: + _router_ids = [r['id'] for r in router_list] + agent_routers = rdb.get_routers_by_provider( + context.session, 'l3-agent', + router_ids=_router_ids) + router_list = [r for r in router_list + if r['id'] in agent_routers] + return router_list + + def _get_router_driver_by_id(self, context, router_id): + provider = self._get_provider_by_router_id(context, router_id) + return get_driver_by_provider(provider) + + def _get_provider_by_router_id(self, context, router_id): + return rdb.get_provider_by_router(context.session, router_id) + + def _extend_router_dict_provider(self, router_res, provider): + router_res[ext_provider.ROUTER_PROVIDER] = provider + + def extend_router_dict_provider(self, router_res, router_db): + # NOTE: router_db.provider is None just after creating a router, + # so we need to skip setting router_provider here. + if not router_db.provider: + return + self._extend_router_dict_provider(router_res, + router_db.provider['provider']) + + db_base_plugin_v2.NeutronDbPluginV2.register_dict_extend_funcs( + l3.ROUTERS, [extend_router_dict_provider]) + + +class L3AgentSchedulerDbMixin(agentschedulers_db.L3AgentSchedulerDbMixin): + + def auto_schedule_routers(self, context, host, router_ids): + router_ids = rdb.get_routers_by_provider( + context.session, nconst.ROUTER_PROVIDER_L3AGENT, router_ids) + # If no l3-agent hosted router, there is no need to schedule. + if not router_ids: + return + return super(L3AgentSchedulerDbMixin, self).auto_schedule_routers( + context, host, router_ids) + + def schedule_router(self, context, router): + if (self._get_provider_by_router_id(context, router) == + nconst.ROUTER_PROVIDER_L3AGENT): + return super(L3AgentSchedulerDbMixin, self).schedule_router( + context, router) + + def add_router_to_l3_agent(self, context, id, router_id): + provider = self._get_provider_by_router_id(context, router_id) + if provider != nconst.ROUTER_PROVIDER_L3AGENT: + raise nexc.RouterProviderMismatch( + router_id=router_id, provider=provider, + expected_provider=nconst.ROUTER_PROVIDER_L3AGENT) + return super(L3AgentSchedulerDbMixin, self).add_router_to_l3_agent( + context, id, router_id) + + +class L3AgentNotifyAPI(l3_rpc_agent_api.L3AgentNotifyAPI): + + def _notification(self, context, method, router_ids, operation, data): + """Notify all the agents that are hosting the routers. + + _notification() is called in L3 db plugin for all routers regardless + the routers are hosted on l3 agents or not. When the routers are + not hosted on l3 agents, there is no need to notify. + This method filters routers not hosted by l3 agents. + """ + router_ids = rdb.get_routers_by_provider( + context.session, nconst.ROUTER_PROVIDER_L3AGENT, router_ids) + super(L3AgentNotifyAPI, self)._notification( + context, method, router_ids, operation, data) + + +def load_driver(plugin, ofc_manager): + + if (PROVIDER_OPENFLOW in ROUTER_DRIVER_MAP and + not ofc_manager.driver.router_supported): + LOG.warning( + _('OFC does not support router with provider=%(provider)s, ' + 'so removed it from supported provider ' + '(new router driver map=%(driver_map)s)'), + {'provider': PROVIDER_OPENFLOW, + 'driver_map': ROUTER_DRIVER_MAP}) + del ROUTER_DRIVER_MAP[PROVIDER_OPENFLOW] + + if config.PROVIDER.default_router_provider not in ROUTER_DRIVER_MAP: + LOG.error(_('default_router_provider %(default)s is supported! ' + 'Please specify one of %(supported)s'), + {'default': config.PROVIDER.default_router_provider, + 'supported': ROUTER_DRIVER_MAP.keys()}) + raise SystemExit(1) + + enabled_providers = (set(config.PROVIDER.router_providers + + [config.PROVIDER.default_router_provider]) & + set(ROUTER_DRIVER_MAP.keys())) + + for driver in enabled_providers: + driver_klass = importutils.import_class(ROUTER_DRIVER_MAP[driver]) + ROUTER_DRIVERS[driver] = driver_klass(plugin, ofc_manager) + + LOG.info(_('Enabled router drivers: %s'), ROUTER_DRIVERS.keys()) + + if not ROUTER_DRIVERS: + LOG.error(_('No router provider is enabled. neutron-server terminated!' + ' (supported=%(supported)s, configured=%(config)s)'), + {'supported': ROUTER_DRIVER_MAP.keys(), + 'config': config.PROVIDER.router_providers}) + raise SystemExit(1) + + +def get_provider_with_default(provider): + if not attr.is_attr_set(provider): + provider = config.PROVIDER.default_router_provider + elif provider not in ROUTER_DRIVERS: + raise nexc.ProviderNotFound(provider=provider) + return provider + + +def get_driver_by_provider(provider): + if provider is None: + provider = config.PROVIDER.default_router_provider + elif provider not in ROUTER_DRIVERS: + raise nexc.ProviderNotFound(provider=provider) + return ROUTER_DRIVERS[provider] diff --git a/neutron/plugins/nec/ofc_manager.py b/neutron/plugins/nec/ofc_manager.py index fd80a00f7..ff12e7256 100644 --- a/neutron/plugins/nec/ofc_manager.py +++ b/neutron/plugins/nec/ofc_manager.py @@ -16,12 +16,19 @@ # @author: Ryota MIBU # @author: Akihiro MOTOKI +import netaddr + +from neutron.common import utils +from neutron.openstack.common import log as logging from neutron.plugins.nec.common import config from neutron.plugins.nec.common import exceptions as nexc from neutron.plugins.nec.db import api as ndb from neutron.plugins.nec import drivers +LOG = logging.getLogger(__name__) + + class OFCManager(object): """This class manages an OpenFlow Controller and map resources. @@ -50,6 +57,10 @@ class OFCManager(object): def _del_ofc_item(self, context, resource, neutron_id): ndb.del_ofc_item_lookup_both(context.session, resource, neutron_id) + def ensure_ofc_tenant(self, context, tenant_id): + if not self.exists_ofc_tenant(context, tenant_id): + self.create_ofc_tenant(context, tenant_id) + def create_ofc_tenant(self, context, tenant_id): desc = "ID=%s at OpenStack." % tenant_id ofc_tenant_id = self.driver.create_tenant(desc, tenant_id) @@ -134,3 +145,60 @@ class OFCManager(object): self.driver.delete_filter(ofc_pf_id) self._del_ofc_item(context, "ofc_packet_filter", filter_id) + + def create_ofc_router(self, context, tenant_id, router_id, name=None): + ofc_tenant_id = self._get_ofc_id(context, "ofc_tenant", tenant_id) + ofc_tenant_id = self.driver.convert_ofc_tenant_id( + context, ofc_tenant_id) + + desc = "ID=%s Name=%s at Neutron." % (router_id, name) + ofc_router_id = self.driver.create_router(ofc_tenant_id, router_id, + desc) + self._add_ofc_item(context, "ofc_router", router_id, ofc_router_id) + + def exists_ofc_router(self, context, router_id): + return self._exists_ofc_item(context, "ofc_router", router_id) + + def delete_ofc_router(self, context, router_id, router): + ofc_router_id = self._get_ofc_id(context, "ofc_router", router_id) + self.driver.delete_router(ofc_router_id) + self._del_ofc_item(context, "ofc_router", router_id) + + def add_ofc_router_interface(self, context, router_id, port_id, port): + # port must have the following fields: + # network_id, cidr, ip_address, mac_address + ofc_router_id = self._get_ofc_id(context, "ofc_router", router_id) + ofc_net_id = self._get_ofc_id(context, "ofc_network", + port['network_id']) + ip_address = '%s/%s' % (port['ip_address'], + netaddr.IPNetwork(port['cidr']).prefixlen) + mac_address = port['mac_address'] + ofc_inf_id = self.driver.add_router_interface( + ofc_router_id, ofc_net_id, ip_address, mac_address) + # Use port mapping table to maintain an interface of OFC router + self._add_ofc_item(context, "ofc_port", port_id, ofc_inf_id) + + def delete_ofc_router_interface(self, context, router_id, port_id): + # Use port mapping table to maintain an interface of OFC router + ofc_inf_id = self._get_ofc_id(context, "ofc_port", port_id) + self.driver.delete_router_interface(ofc_inf_id) + self._del_ofc_item(context, "ofc_port", port_id) + + def update_ofc_router_route(self, context, router_id, new_routes): + ofc_router_id = self._get_ofc_id(context, "ofc_router", router_id) + ofc_routes = self.driver.list_router_routes(ofc_router_id) + route_dict = {} + cur_routes = [] + for r in ofc_routes: + key = ','.join((r['destination'], r['nexthop'])) + route_dict[key] = r['id'] + del r['id'] + cur_routes.append(r) + added, removed = utils.diff_list_of_dict(cur_routes, new_routes) + for r in removed: + key = ','.join((r['destination'], r['nexthop'])) + route_id = route_dict[key] + self.driver.delete_router_route(route_id) + for r in added: + self.driver.add_router_route(ofc_router_id, r['destination'], + r['nexthop']) diff --git a/neutron/plugins/nec/router_drivers.py b/neutron/plugins/nec/router_drivers.py new file mode 100644 index 000000000..b16c4d8a0 --- /dev/null +++ b/neutron/plugins/nec/router_drivers.py @@ -0,0 +1,221 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2013 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# @author: Akihiro Motoki + +import abc +import httplib + +from neutron.common import log as call_log +from neutron.common import utils +from neutron.openstack.common import excutils +from neutron.openstack.common import log as logging +from neutron.plugins.nec.common import constants as nconst +from neutron.plugins.nec.common import exceptions as nexc + +LOG = logging.getLogger(__name__) + +PROVIDER_OPENFLOW = nconst.ROUTER_PROVIDER_OPENFLOW + + +class RouterDriverBase(object): + + __metaclass__ = abc.ABCMeta + + def __init__(self, plugin, ofc_manager): + self.plugin = plugin + self.ofc = ofc_manager + + def floating_ip_support(self): + return True + + @abc.abstractmethod + def create_router(self, context, tenant_id, router): + pass + + @abc.abstractmethod + def update_router(self, context, router_id, old_router, new_router): + pass + + @abc.abstractmethod + def delete_router(self, context, router_id, router): + pass + + @abc.abstractmethod + def add_interface(self, context, router_id, port): + pass + + @abc.abstractmethod + def delete_interface(self, context, router_id, port): + pass + + +class RouterL3AgentDriver(RouterDriverBase): + + need_gw_info = False + + @call_log.log + def create_router(self, context, tenant_id, router): + return router + + @call_log.log + def update_router(self, context, router_id, old_router, new_router): + return new_router + + @call_log.log + def delete_router(self, context, router_id, router): + pass + + @call_log.log + def add_interface(self, context, router_id, port): + return self.plugin.activate_port_if_ready(context, port) + + @call_log.log + def delete_interface(self, context, router_id, port): + return self.plugin.deactivate_port(context, port) + + +class RouterOpenFlowDriver(RouterDriverBase): + + need_gw_info = True + + def floating_ip_support(self): + return self.ofc.driver.router_nat_supported + + def _process_gw_port(self, gw_info, routes): + if gw_info and gw_info['gateway_ip']: + routes.append({'destination': '0.0.0.0/0', + 'nexthop': gw_info['gateway_ip']}) + + @call_log.log + def create_router(self, context, tenant_id, router): + try: + router_id = router['id'] + added_routes = [] + self.ofc.ensure_ofc_tenant(context, tenant_id) + self.ofc.create_ofc_router(context, tenant_id, router_id, + router['name']) + self._process_gw_port(router['gw_port'], added_routes) + if added_routes: + self.ofc.update_ofc_router_route(context, router_id, + added_routes, []) + new_status = nconst.ROUTER_STATUS_ACTIVE + self.plugin._update_resource_status(context, "router", + router['id'], + new_status) + router['status'] = new_status + return router + except (nexc.OFCException, nexc.OFCConsistencyBroken) as exc: + with excutils.save_and_reraise_exception(): + if (isinstance(exc, nexc.OFCException) and + exc.status == httplib.CONFLICT): + raise nexc.RouterOverLimit(provider=PROVIDER_OPENFLOW) + reason = _("create_router() failed due to %s") % exc + LOG.error(reason) + new_status = nconst.ROUTER_STATUS_ERROR + self._update_resource_status(context, "router", + router['id'], + new_status) + + @call_log.log + def update_router(self, context, router_id, old_router, new_router): + old_routes = old_router['routes'][:] + new_routes = new_router['routes'][:] + self._process_gw_port(old_router['gw_port'], old_routes) + self._process_gw_port(new_router['gw_port'], new_routes) + added, removed = utils.diff_list_of_dict(old_routes, new_routes) + if added or removed: + try: + # NOTE(amotoki): PFC supports one-by-one route update at now. + # It means there may be a case where some route is updated but + # some not. To allow the next call of failures to sync routes + # with Neutron side, we pass the whole new routes here. + # PFC should support atomic route update in the future. + self.ofc.update_ofc_router_route(context, router_id, + new_routes) + new_status = nconst.ROUTER_STATUS_ACTIVE + self.plugin._update_resource_status( + context, "router", router_id, new_status) + new_router['status'] = new_status + except (nexc.OFCException, nexc.OFCConsistencyBroken) as exc: + with excutils.save_and_reraise_exception(): + reason = _("_update_ofc_routes() failed due to %s") % exc + LOG.error(reason) + new_status = nconst.ROUTER_STATUS_ERROR + self.plugin._update_resource_status( + context, "router", router_id, new_status) + return new_router + + @call_log.log + def delete_router(self, context, router_id, router): + try: + self.ofc.delete_ofc_router(context, router_id, router) + except (nexc.OFCException, nexc.OFCConsistencyBroken) as exc: + with excutils.save_and_reraise_exception(): + LOG.error(_("delete_router() failed due to %s"), exc) + self.plugin._update_resource_status( + context, "router", router_id, nconst.ROUTER_STATUS_ERROR) + + @call_log.log + def add_interface(self, context, router_id, port): + port_id = port['id'] + # port['fixed_ips'] may be empty if ext_net has no subnet. + # Such port is invalid for a router port and we don't create a port + # on OFC. The port is removed in l3_db._create_router_gw_port. + if not port['fixed_ips']: + msg = _('RouterOpenFlowDriver.add_interface(): the requested port ' + 'has no subnet. add_interface() is skipped. ' + 'router_id=%(id)s, port=%(port)s)') + LOG.warning(msg, {'id': router_id, 'port': port}) + return port + fixed_ip = port['fixed_ips'][0] + subnet = self.plugin._get_subnet(context, fixed_ip['subnet_id']) + port_info = {'network_id': port['network_id'], + 'ip_address': fixed_ip['ip_address'], + 'cidr': subnet['cidr'], + 'mac_address': port['mac_address']} + try: + self.ofc.add_ofc_router_interface(context, router_id, + port_id, port_info) + new_status = nconst.ROUTER_STATUS_ACTIVE + self.plugin._update_resource_status( + context, "port", port_id, new_status) + return port + except (nexc.OFCException, nexc.OFCConsistencyBroken) as exc: + with excutils.save_and_reraise_exception(): + reason = _("add_router_interface() failed due to %s") % exc + LOG.error(reason) + new_status = nconst.ROUTER_STATUS_ERROR + self.plugin._update_resource_status( + context, "port", port_id, new_status) + + @call_log.log + def delete_interface(self, context, router_id, port): + port_id = port['id'] + try: + self.ofc.delete_ofc_router_interface(context, router_id, port_id) + new_status = nconst.ROUTER_STATUS_ACTIVE + self.plugin._update_resource_status(context, "port", port_id, + new_status) + port['status'] = new_status + return port + except (nexc.OFCException, nexc.OFCConsistencyBroken) as exc: + with excutils.save_and_reraise_exception(): + reason = _("delete_router_interface() failed due to %s") % exc + LOG.error(reason) + new_status = nconst.ROUTER_STATUS_ERROR + self.plugin._update_resource_status(context, "port", port_id, + new_status) diff --git a/neutron/tests/unit/nec/stub_ofc_driver.py b/neutron/tests/unit/nec/stub_ofc_driver.py index 88ee53554..378c80614 100644 --- a/neutron/tests/unit/nec/stub_ofc_driver.py +++ b/neutron/tests/unit/nec/stub_ofc_driver.py @@ -15,34 +15,129 @@ # under the License. # @author: Ryota MIBU +import netaddr + +from neutron.common import log as call_log +from neutron.openstack.common import log as logging +from neutron.openstack.common import uuidutils +from neutron.plugins.nec.common import exceptions as nexc from neutron.plugins.nec import ofc_driver_base +LOG = logging.getLogger(__name__) + +MAX_NUM_OPENFLOW_ROUTER = 2 + + class StubOFCDriver(ofc_driver_base.OFCDriverBase): + """Stub OFC driver for testing. + + This driver can be used not only for unit tests but also for real testing + as a logging driver. It stores the created resources on OFC and returns + them in get methods(). + + If autocheck is enabled, it checks whether the specified resource exists + in OFC and raises an exception if it is different from expected status. + """ def __init__(self, conf): - pass + self.autocheck = False + self.reset_all() + + def reset_all(self): + self.ofc_tenant_dict = {} + self.ofc_network_dict = {} + self.ofc_port_dict = {} + self.ofc_filter_dict = {} + self.ofc_router_dict = {} + self.ofc_router_inf_dict = {} + self.ofc_router_route_dict = {} + + def enable_autocheck(self): + self.autocheck = True + def disable_autocheck(self): + self.autocheck = False + + @call_log.log def create_tenant(self, description, tenant_id=None): - return "ofc-" + tenant_id[:-4] + ofc_id = "ofc-" + tenant_id[:-4] + if self.autocheck: + if ofc_id in self.ofc_tenant_dict: + raise Exception(_('(create_tenant) OFC tenant %s ' + 'already exists') % ofc_id) + self.ofc_tenant_dict[ofc_id] = {'tenant_id': tenant_id, + 'description': description} + return ofc_id + @call_log.log def delete_tenant(self, ofc_tenant_id): - pass + if ofc_tenant_id in self.ofc_tenant_dict: + del self.ofc_tenant_dict[ofc_tenant_id] + else: + if self.autocheck: + raise Exception(_('(delete_tenant) OFC tenant %s not found') + % ofc_tenant_id) + LOG.debug(_('delete_tenant: SUCCEED')) + @call_log.log def create_network(self, ofc_tenant_id, description, network_id=None): - return "ofc-" + network_id[:-4] + ofc_id = "ofc-" + network_id[:-4] + if self.autocheck: + if ofc_tenant_id not in self.ofc_tenant_dict: + raise Exception(_('(create_network) OFC tenant %s not found') + % ofc_tenant_id) + if ofc_id in self.ofc_network_dict: + raise Exception(_('(create_network) OFC network %s ' + 'already exists') % ofc_id) + self.ofc_network_dict[ofc_id] = {'tenant_id': ofc_tenant_id, + 'network_id': network_id, + 'description': description} + return ofc_id + @call_log.log def update_network(self, ofc_network_id, description): - pass + if self.autocheck: + if ofc_network_id not in self.ofc_network_dict: + raise Exception(_('(update_network) OFC network %s not found') + % ofc_network_id) + data = {'description': description} + self.ofc_network_dict[ofc_network_id].update(data) + LOG.debug(_('update_network: SUCCEED')) + @call_log.log def delete_network(self, ofc_network_id): - pass + if ofc_network_id in self.ofc_network_dict: + del self.ofc_network_dict[ofc_network_id] + else: + if self.autocheck: + raise Exception(_('(delete_network) OFC network %s not found') + % ofc_network_id) + LOG.debug(_('delete_network: SUCCEED')) + @call_log.log def create_port(self, ofc_network_id, info, port_id=None): - return "ofc-" + port_id[:-4] + ofc_id = "ofc-" + port_id[:-4] + if self.autocheck: + if ofc_network_id not in self.ofc_network_dict: + raise Exception(_('(create_port) OFC network %s not found') + % ofc_network_id) + if ofc_id in self.ofc_port_dict: + raise Exception(_('(create_port) OFC port %s already exists') + % ofc_id) + self.ofc_port_dict[ofc_id] = {'network_id': ofc_network_id, + 'port_id': port_id} + return ofc_id + @call_log.log def delete_port(self, ofc_port_id): - pass + if ofc_port_id in self.ofc_port_dict: + del self.ofc_port_dict[ofc_port_id] + else: + if self.autocheck: + raise Exception(_('(delete_port) OFC port %s not found') + % ofc_port_id) + LOG.debug(_('delete_port: SUCCEED')) @classmethod def filter_supported(cls): @@ -66,3 +161,131 @@ class StubOFCDriver(ofc_driver_base.OFCDriverBase): def convert_ofc_filter_id(self, context, ofc_filter_id): return ofc_filter_id + + router_supported = True + router_nat_supported = True + + @call_log.log + def create_router(self, ofc_tenant_id, router_id, description): + ofc_id = "ofc-" + router_id[:-4] + if self.autocheck: + if ofc_tenant_id not in self.ofc_tenant_dict: + raise Exception(_('(create_router) OFC tenant %s not found') + % ofc_tenant_id) + if ofc_id in self.ofc_router_dict: + raise Exception(_('(create_router) OFC router %s ' + 'already exists') % ofc_id) + if len(self.ofc_router_dict) >= MAX_NUM_OPENFLOW_ROUTER: + params = {'reason': _("Operation on OFC is failed"), + 'status': 409} + raise nexc.OFCException(**params) + self.ofc_router_dict[ofc_id] = {'tenant_id': ofc_tenant_id, + 'router_id': router_id, + 'description': description} + return ofc_id + + @call_log.log + def delete_router(self, ofc_router_id): + if ofc_router_id in self.ofc_router_dict: + del self.ofc_router_dict[ofc_router_id] + else: + if self.autocheck: + raise Exception(_('(delete_router) OFC router %s not found') + % ofc_router_id) + LOG.debug(_('delete_router: SUCCEED')) + + @call_log.log + def add_router_interface(self, ofc_router_id, ofc_net_id, + ip_address=None, mac_address=None): + if_id = "ofc-" + uuidutils.generate_uuid()[:-4] + # IP address should have a format of a.b.c.d/N + if ip_address != str(netaddr.IPNetwork(ip_address)): + raise Exception(_('(add_router_interface) ' + 'ip_address %s is not a valid format (a.b.c.d/N).') + % ip_address) + if self.autocheck: + if ofc_router_id not in self.ofc_router_dict: + raise Exception(_('(add_router_interface) ' + 'OFC router %s not found') % ofc_router_id) + if ofc_net_id not in self.ofc_network_dict: + raise Exception(_('(add_router_interface) ' + 'OFC network %s not found') % ofc_net_id) + # Check duplicate destination + self.ofc_router_inf_dict[if_id] = {'router_id': ofc_router_id, + 'network_id': ofc_net_id, + 'ip_address': ip_address, + 'mac_address': mac_address} + LOG.debug(_('add_router_interface: SUCCEED (if_id=%s)'), if_id) + return if_id + + @call_log.log + def update_router_interface(self, ofc_router_inf_id, + ip_address=None, mac_address=None): + if ofc_router_inf_id not in self.ofc_router_inf_dict: + if self.autocheck: + raise Exception(_('(delete_router_interface) ' + 'OFC router interface %s not found') + % ofc_router_inf_id) + self.ofc_router_inf_dict[ofc_router_inf_id] = {} + inf = self.ofc_router_inf_dict[ofc_router_inf_id] + if ip_address: + inf.update({'ip_address': ip_address}) + if mac_address: + inf.update({'mac_address': mac_address}) + LOG.debug(_('update_router_route: SUCCEED')) + + @call_log.log + def delete_router_interface(self, ofc_router_inf_id): + if ofc_router_inf_id in self.ofc_router_inf_dict: + del self.ofc_router_inf_dict[ofc_router_inf_id] + else: + if self.autocheck: + raise Exception(_('(delete_router_interface) ' + 'OFC router interface %s not found') + % ofc_router_inf_id) + LOG.debug(_('delete_router_interface: SUCCEED')) + + @call_log.log + def add_router_route(self, ofc_router_id, destination, nexthop): + route_id = "ofc-" + uuidutils.generate_uuid()[:-4] + # IP address format check + netaddr.IPNetwork(destination) + netaddr.IPAddress(nexthop) + if self.autocheck: + if ofc_router_id not in self.ofc_router_dict: + raise Exception(_('(add_router_route) OFC router %s not found') + % ofc_router_id) + # Check duplicate destination + if destination in [route['destination'] for route in + self.ofc_router_route_dict.values()]: + raise Exception(_('(add_router_route) ' + 'route to "%s" already exists') % destination) + self.ofc_router_route_dict[route_id] = {'router_id': ofc_router_id, + 'destination': destination, + 'nexthop': nexthop} + LOG.debug(_('add_router_route: SUCCEED (route_id=%s)'), route_id) + return route_id + + @call_log.log + def delete_router_route(self, ofc_router_route_id): + if ofc_router_route_id in self.ofc_router_route_dict: + del self.ofc_router_route_dict[ofc_router_route_id] + else: + if self.autocheck: + raise Exception(_('(delete_router_route) OFC router route %s ' + 'not found') % ofc_router_route_id) + LOG.debug(_('delete_router_route: SUCCEED')) + + @call_log.log + def list_router_routes(self, ofc_router_id): + if self.autocheck: + if ofc_router_id not in self.ofc_router_dict: + raise Exception(_('(delete_router) OFC router %s not found') + % ofc_router_id) + routes = [{'id': k, + 'destination': v['destination'], + 'nexthop': v['nexthop']} + for k, v in self.ofc_router_route_dict.items() + if v['router_id'] == ofc_router_id] + LOG.debug(_('list_router_routes: routes=%s'), routes) + return routes diff --git a/neutron/tests/unit/nec/test_agent_scheduler.py b/neutron/tests/unit/nec/test_agent_scheduler.py index 68dd70e18..ddd6ab13e 100644 --- a/neutron/tests/unit/nec/test_agent_scheduler.py +++ b/neutron/tests/unit/nec/test_agent_scheduler.py @@ -15,41 +15,104 @@ # See the License for the specific language governing permissions and # limitations under the License. -import mox +import contextlib -from neutron.plugins.nec.common import ofc_client +from neutron.common import constants +from neutron.db import l3_rpc_base from neutron.tests.unit.nec import test_nec_plugin from neutron.tests.unit.openvswitch import test_agent_scheduler +L3_HOSTA = test_agent_scheduler.L3_HOSTA +L3_HOSTB = test_agent_scheduler.L3_HOSTB + class NecAgentSchedulerTestCase( - test_agent_scheduler.OvsAgentSchedulerTestCase): + test_agent_scheduler.OvsAgentSchedulerTestCase, + test_nec_plugin.NecPluginV2TestCaseBase): + plugin_str = test_nec_plugin.PLUGIN_NAME def setUp(self): + self.setup_nec_plugin_base() super(NecAgentSchedulerTestCase, self).setUp() - self.mox = mox.Mox() - self.mox.StubOutWithMock(ofc_client.OFCClient, 'do_request') - self.addCleanup(self.mox.UnsetStubs) class NecDhcpAgentNotifierTestCase( - test_agent_scheduler.OvsDhcpAgentNotifierTestCase): + test_agent_scheduler.OvsDhcpAgentNotifierTestCase, + test_nec_plugin.NecPluginV2TestCaseBase): + plugin_str = test_nec_plugin.PLUGIN_NAME def setUp(self): + # OvsDhcpAgentNotifierTestCase uses stop() for each mock. + self.setup_nec_plugin_base(use_stop_each=True) super(NecDhcpAgentNotifierTestCase, self).setUp() - self.mox = mox.Mox() - self.mox.StubOutWithMock(ofc_client.OFCClient, 'do_request') - self.addCleanup(self.mox.UnsetStubs) class NecL3AgentNotifierTestCase( - test_agent_scheduler.OvsL3AgentNotifierTestCase): + test_agent_scheduler.OvsL3AgentNotifierTestCase, + test_nec_plugin.NecPluginV2TestCaseBase): + plugin_str = test_nec_plugin.PLUGIN_NAME def setUp(self): + # OvsDhcpAgentNotifierTestCase uses stop() for each mock. + self.setup_nec_plugin_base(use_stop_each=True) super(NecL3AgentNotifierTestCase, self).setUp() - self.mox = mox.Mox() - self.mox.StubOutWithMock(ofc_client.OFCClient, 'do_request') - self.addCleanup(self.mox.UnsetStubs) + + +class NecL3AgentSchedulerWithOpenFlowRouter( + test_agent_scheduler.OvsAgentSchedulerTestCaseBase, + test_nec_plugin.NecPluginV2TestCaseBase): + + plugin_str = test_nec_plugin.PLUGIN_NAME + + def setUp(self): + self.setup_nec_plugin_base() + super(NecL3AgentSchedulerWithOpenFlowRouter, self).setUp() + + def test_router_auto_schedule_with_l3agent_and_openflow(self): + with contextlib.nested( + self.router(), + self.router(arg_list=('provider',), + provider='openflow' + )) as (r1, r2): + l3_rpc = l3_rpc_base.L3RpcCallbackMixin() + self._register_agent_states() + ret_a = l3_rpc.sync_routers(self.adminContext, host=L3_HOSTA) + ret_b = l3_rpc.sync_routers(self.adminContext, host=L3_HOSTB) + l3_agents = self._list_l3_agents_hosting_router( + r1['router']['id']) + self.assertEqual(1, len(ret_a)) + self.assertFalse(len(ret_b)) + self.assertIn(r1['router']['id'], [r['id'] for r in ret_a]) + self.assertNotIn(r2['router']['id'], [r['id'] for r in ret_a]) + self.assertEqual(1, len(l3_agents['agents'])) + self.assertEqual(L3_HOSTA, l3_agents['agents'][0]['host']) + + def test_router_auto_schedule_only_with_openflow_router(self): + with contextlib.nested( + self.router(arg_list=('provider',), provider='openflow'), + self.router(arg_list=('provider',), provider='openflow') + ) as (r1, r2): + l3_rpc = l3_rpc_base.L3RpcCallbackMixin() + self._register_agent_states() + ret_a = l3_rpc.sync_routers(self.adminContext, host=L3_HOSTA) + l3_agents_1 = self._list_l3_agents_hosting_router( + r1['router']['id']) + l3_agents_2 = self._list_l3_agents_hosting_router( + r2['router']['id']) + self.assertFalse(len(ret_a)) + self.assertNotIn(r1['router']['id'], [r['id'] for r in ret_a]) + self.assertNotIn(r2['router']['id'], [r['id'] for r in ret_a]) + self.assertFalse(len(l3_agents_1['agents'])) + self.assertFalse(len(l3_agents_2['agents'])) + + def test_add_router_to_l3_agent_for_openflow_router(self): + with self.router(arg_list=('provider',), provider='openflow') as r1: + self._register_agent_states() + hosta_id = self._get_agent_id(constants.AGENT_TYPE_L3, + L3_HOSTA) + self._add_router_to_l3_agent(hosta_id, + r1['router']['id'], + expected_code=409) diff --git a/neutron/tests/unit/nec/test_nec_plugin.py b/neutron/tests/unit/nec/test_nec_plugin.py index a4e81bd7a..2f913caf5 100644 --- a/neutron/tests/unit/nec/test_nec_plugin.py +++ b/neutron/tests/unit/nec/test_nec_plugin.py @@ -33,6 +33,8 @@ from neutron.tests.unit import test_db_plugin as test_plugin PLUGIN_NAME = 'neutron.plugins.nec.nec_plugin.NECPluginV2' +OFC_MANAGER = 'neutron.plugins.nec.nec_plugin.ofc_manager.OFCManager' +NOTIFIER = 'neutron.plugins.nec.nec_plugin.NECPluginV2AgentNotifierApi' NEC_PLUGIN_INI = """ [DEFAULT] api_extensions_path = neutron/plugins/nec/extensions @@ -42,9 +44,7 @@ enable_packet_filter = False """ -class NecPluginV2TestCase(test_plugin.NeutronDbPluginV2TestCase): - - _plugin_name = PLUGIN_NAME +class NecPluginV2TestCaseBase(object): _nec_ini = NEC_PLUGIN_INI def _set_nec_ini(self): @@ -64,6 +64,34 @@ class NecPluginV2TestCase(test_plugin.NeutronDbPluginV2TestCase): os.remove(self.nec_ini_file) self.nec_ini_file = None + def patch_remote_calls(self, use_stop=False): + self.plugin_notifier_p = mock.patch(NOTIFIER) + self.ofc_manager_p = mock.patch(OFC_MANAGER) + self.plugin_notifier_p.start() + self.ofc_manager_p.start() + # When using mock.patch.stopall, we need to ensure + # stop is not used anywhere in a single test. + # In Neutron several tests use stop for each patched object, + # so we need to take care of both cases. + if use_stop: + self.addCleanup(self.plugin_notifier_p.stop) + self.addCleanup(self.ofc_manager_p.stop) + + def setup_nec_plugin_base(self, use_stop_all=True, + use_stop_each=False): + # If use_stop_each is set, use_stop_all cannot be set. + if use_stop_all and not use_stop_each: + self.addCleanup(mock.patch.stopall) + self._set_nec_ini() + self.addCleanup(self._clean_nec_ini) + self.patch_remote_calls(use_stop_each) + + +class NecPluginV2TestCase(NecPluginV2TestCaseBase, + test_plugin.NeutronDbPluginV2TestCase): + + _plugin_name = PLUGIN_NAME + def rpcapi_update_ports(self, agent_id='nec-q-agent.fake', datapath_id="0xabc", added=[], removed=[]): kwargs = {'topic': topics.AGENT, @@ -348,6 +376,7 @@ class TestNecPluginOfcManager(NecPluginV2TestCase): mock.call.create_ofc_network(ctx, self._tenant_id, net['id'], net['name']), mock.call.delete_ofc_network(ctx, net['id'], mock.ANY), + mock.call.exists_ofc_tenant(ctx, self._tenant_id), mock.call.delete_ofc_tenant(ctx, self._tenant_id) ] self.ofc.assert_has_calls(expected) @@ -365,6 +394,7 @@ class TestNecPluginOfcManager(NecPluginV2TestCase): mock.call.create_ofc_network(ctx, self._tenant_id, net['id'], net['name']), mock.call.delete_ofc_network(ctx, net['id'], mock.ANY), + mock.call.exists_ofc_tenant(ctx, self._tenant_id), mock.call.delete_ofc_tenant(ctx, self._tenant_id) ] self.ofc.assert_has_calls(expected) @@ -389,6 +419,7 @@ class TestNecPluginOfcManager(NecPluginV2TestCase): nets[1]['name']), mock.call.delete_ofc_network(ctx, nets[1]['id'], mock.ANY), mock.call.delete_ofc_network(ctx, nets[0]['id'], mock.ANY), + mock.call.exists_ofc_tenant(ctx, self._tenant_id), mock.call.delete_ofc_tenant(ctx, self._tenant_id) ] self.ofc.assert_has_calls(expected) @@ -451,6 +482,7 @@ class TestNecPluginOfcManager(NecPluginV2TestCase): mock.call.create_ofc_network(ctx, self._tenant_id, net['id'], net['name']), mock.call.delete_ofc_network(ctx, net['id'], mock.ANY), + mock.call.exists_ofc_tenant(ctx, self._tenant_id), mock.call.delete_ofc_tenant(ctx, self._tenant_id) ] self.ofc.assert_has_calls(expected) @@ -478,6 +510,7 @@ class TestNecPluginOfcManager(NecPluginV2TestCase): mock.call.exists_ofc_port(ctx, p1['id']), mock.call.delete_ofc_network(ctx, net['id'], mock.ANY), + mock.call.exists_ofc_tenant(ctx, self._tenant_id), mock.call.delete_ofc_tenant(ctx, self._tenant_id) ] self.ofc.assert_has_calls(expected) @@ -520,6 +553,7 @@ class TestNecPluginOfcManager(NecPluginV2TestCase): mock.call.exists_ofc_port(ctx, p1['id']), mock.call.delete_ofc_port(ctx, p1['id'], mock.ANY), mock.call.delete_ofc_network(ctx, net['id'], mock.ANY), + mock.call.exists_ofc_tenant(ctx, self._tenant_id), mock.call.delete_ofc_tenant(ctx, self._tenant_id) ] self.ofc.assert_has_calls(expected) @@ -550,6 +584,7 @@ class TestNecPluginOfcManager(NecPluginV2TestCase): mock.call.exists_ofc_port(ctx, p['id']), mock.call.delete_ofc_port(ctx, p['id'], mock.ANY), mock.call.delete_ofc_network(ctx, net['id'], mock.ANY), + mock.call.exists_ofc_tenant(ctx, self._tenant_id), mock.call.delete_ofc_tenant(ctx, self._tenant_id) ] self.ofc.assert_has_calls(expected) @@ -686,6 +721,7 @@ class TestNecPluginOfcManager(NecPluginV2TestCase): mock.call.exists_ofc_port(ctx, p1['id']), mock.call.delete_ofc_network(ctx, net['id'], mock.ANY), + mock.call.exists_ofc_tenant(ctx, self._tenant_id), mock.call.delete_ofc_tenant(ctx, self._tenant_id) ] self.ofc.assert_has_calls(expected) diff --git a/neutron/tests/unit/nec/test_ofc_client.py b/neutron/tests/unit/nec/test_ofc_client.py new file mode 100644 index 000000000..1567ae9e0 --- /dev/null +++ b/neutron/tests/unit/nec/test_ofc_client.py @@ -0,0 +1,117 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2013 NEC Corporation. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# @author: Akihiro Motoki + +import json +import socket + +import mock + +from neutron.plugins.nec.common import exceptions as nexc +from neutron.plugins.nec.common import ofc_client +from neutron.tests import base + + +class OFCClientTest(base.BaseTestCase): + + def _test_do_request(self, status, resbody, data, exctype=None, + exc_checks=None): + res = mock.Mock() + res.status = status + res.read.return_value = resbody + + conn = mock.Mock() + conn.getresponse.return_value = res + + with mock.patch.object(ofc_client.OFCClient, 'get_connection', + return_value=conn): + client = ofc_client.OFCClient() + + if exctype: + e = self.assertRaises(exctype, client.do_request, + 'GET', '/somewhere', body={}) + self.assertEqual(data, str(e)) + if exc_checks: + for k, v in exc_checks.items(): + self.assertEqual(v, getattr(e, k)) + else: + response = client.do_request('GET', '/somewhere', body={}) + self.assertEqual(response, data) + + headers = {"Content-Type": "application/json"} + expected = [ + mock.call.request('GET', '/somewhere', '{}', headers), + mock.call.getresponse(), + ] + conn.assert_has_calls(expected) + + def test_do_request_200_json_value(self): + self._test_do_request(200, json.dumps([1, 2, 3]), [1, 2, 3]) + + def test_do_request_200_string(self): + self._test_do_request(200, 'abcdef', 'abcdef') + + def test_do_request_200_no_body(self): + self._test_do_request(200, None, None) + + def test_do_request_other_success_codes(self): + for status in [201, 202, 204]: + self._test_do_request(status, None, None) + + def test_do_request_error_no_body(self): + errmsg = _("An OFC exception has occurred: Operation on OFC failed") + exc_checks = {'status': 400, 'err_code': None, 'err_msg': None} + self._test_do_request(400, None, errmsg, nexc.OFCException, exc_checks) + + def test_do_request_error_string_body(self): + resbody = 'This is an error.' + errmsg = _("An OFC exception has occurred: Operation on OFC failed") + exc_checks = {'status': 400, 'err_code': None, + 'err_msg': 'This is an error.'} + self._test_do_request(400, resbody, errmsg, nexc.OFCException, + exc_checks) + + def test_do_request_error_json_body(self): + resbody = json.dumps({'err_code': 40022, + 'err_msg': 'This is an error.'}) + errmsg = _("An OFC exception has occurred: Operation on OFC failed") + exc_checks = {'status': 400, 'err_code': 40022, + 'err_msg': 'This is an error.'} + self._test_do_request(400, resbody, errmsg, nexc.OFCException, + exc_checks) + + def test_do_request_socket_error(self): + conn = mock.Mock() + conn.request.side_effect = socket.error + + data = _("An OFC exception has occurred: Failed to connect OFC : ") + + with mock.patch.object(ofc_client.OFCClient, 'get_connection', + return_value=conn): + client = ofc_client.OFCClient() + + e = self.assertRaises(nexc.OFCException, client.do_request, + 'GET', '/somewhere', body={}) + self.assertEqual(data, str(e)) + for k in ['status', 'err_code', 'err_msg']: + self.assertIsNone(getattr(e, k)) + + headers = {"Content-Type": "application/json"} + expected = [ + mock.call.request('GET', '/somewhere', '{}', headers), + ] + conn.assert_has_calls(expected) diff --git a/neutron/tests/unit/nec/test_ofc_manager.py b/neutron/tests/unit/nec/test_ofc_manager.py index 569f5db5e..309a4ac30 100644 --- a/neutron/tests/unit/nec/test_ofc_manager.py +++ b/neutron/tests/unit/nec/test_ofc_manager.py @@ -49,6 +49,8 @@ class OFCManagerTestBase(base.BaseTestCase): ndb.initialize() self.addCleanup(ndb.clear_db) self.ofc = ofc_manager.OFCManager() + # NOTE: enable_autocheck() is a feature of StubOFCDriver + self.ofc.driver.enable_autocheck() self.ctx = context.get_admin_context() self.addCleanup(mock.patch.stopall) @@ -160,6 +162,8 @@ class OFCManagerTest(OFCManagerTestBase): self.assertFalse(ndb.get_ofc_item(self.ctx.session, 'ofc_port', p)) get_portinfo.assert_called_once_with(mock.ANY, p) + +class OFCManagerFilterTest(OFCManagerTestBase): def testj_create_ofc_packet_filter(self): """test create ofc_filter.""" t, n, p, f, none = self.get_random_params() @@ -198,8 +202,92 @@ class OFCManagerTest(OFCManagerTestBase): 'ofc_packet_filter', f)) +class OFCManagerRouterTest(OFCManagerTestBase): + def get_random_params(self): + tenant = uuidutils.generate_uuid() + router = uuidutils.generate_uuid() + network = uuidutils.generate_uuid() + return (tenant, router, network) + + def test_create_ofc_router(self): + """test create ofc_router""" + t, r, _n = self.get_random_params() + self.ofc.create_ofc_tenant(self.ctx, t) + self.assertFalse(ndb.get_ofc_item(self.ctx.session, 'ofc_router', r)) + self.ofc.create_ofc_router(self.ctx, t, r, 'test router') + self.assertTrue(ndb.get_ofc_item(self.ctx.session, 'ofc_router', r)) + router = ndb.get_ofc_item(self.ctx.session, 'ofc_router', r) + self.assertEqual(router.ofc_id, "ofc-" + r[:-4]) + + def test_exists_ofc_router(self): + """test exists_ofc_router""" + t, r, _n = self.get_random_params() + self.ofc.create_ofc_tenant(self.ctx, t) + self.assertFalse(self.ofc.exists_ofc_router(self.ctx, r)) + self.ofc.create_ofc_router(self.ctx, t, r) + self.assertTrue(self.ofc.exists_ofc_router(self.ctx, r)) + + def test_delete_ofc_router(self): + """test delete ofc_router""" + t, r, _n = self.get_random_params() + self.ofc.create_ofc_tenant(self.ctx, t) + self.ofc.create_ofc_router(self.ctx, t, r) + self.assertTrue(ndb.get_ofc_item(self.ctx.session, 'ofc_router', r)) + self.ofc.delete_ofc_router(self.ctx, r, {'tenant_id': t}) + self.assertFalse(ndb.get_ofc_item(self.ctx.session, 'ofc_network', r)) + + def test_router_interface(self): + t, r, n = self.get_random_params() + self.ofc.create_ofc_tenant(self.ctx, t) + self.ofc.create_ofc_router(self.ctx, t, r) + self.ofc.create_ofc_network(self.ctx, t, n) + self.assertTrue(ndb.get_ofc_item(self.ctx.session, 'ofc_router', r)) + self.assertTrue(ndb.get_ofc_item(self.ctx.session, 'ofc_network', n)) + + p = {'id': uuidutils.generate_uuid(), + 'network_id': n, 'ip_address': '10.1.1.1', 'cidr': '10.1.0.0/20', + 'mac_address': '11:22:33:44:55:66'} + self.ofc.add_ofc_router_interface(self.ctx, r, p['id'], p) + self.assertTrue(ndb.get_ofc_item(self.ctx.session, + 'ofc_port', p['id'])) + self.ofc.delete_ofc_router_interface(self.ctx, r, p['id']) + self.assertFalse(ndb.get_ofc_item(self.ctx.session, + 'ofc_port', p['id'])) + self.ofc.delete_ofc_router(self.ctx, r, {'tenant_id': t}) + self.assertFalse(ndb.get_ofc_item(self.ctx.session, 'ofc_network', r)) + + def test_router_route(self): + t, r, _n = self.get_random_params() + self.ofc.create_ofc_tenant(self.ctx, t) + self.ofc.create_ofc_router(self.ctx, t, r) + self.assertTrue(ndb.get_ofc_item(self.ctx.session, 'ofc_router', r)) + + routes = [{'destination': '2.2.2.0/24', 'nexthop': '1.1.1.10'}] + self.ofc.update_ofc_router_route(self.ctx, r, routes) + self.assertEqual(len(self.ofc.driver.ofc_router_route_dict), 1) + + routes = [{'destination': '3.3.3.0/24', 'nexthop': '1.1.1.11'}, + {'destination': '4.4.4.0/24', 'nexthop': '1.1.1.11'}] + self.ofc.update_ofc_router_route(self.ctx, r, routes) + self.assertEqual(len(self.ofc.driver.ofc_router_route_dict), 2) + + routes = [{'destination': '2.2.2.0/24', 'nexthop': '1.1.1.10'}] + self.ofc.update_ofc_router_route(self.ctx, r, routes) + self.assertEqual(len(self.ofc.driver.ofc_router_route_dict), 1) + + routes = [] + self.ofc.update_ofc_router_route(self.ctx, r, routes) + self.assertEqual(len(self.ofc.driver.ofc_router_route_dict), 0) + + class OFCManagerTestWithOldMapping(OFCManagerTestBase): + def setUp(self): + super(OFCManagerTestWithOldMapping, self).setUp() + # NOTE(amotoki): In OldMapping tests, DB entries are directly modified + # to create a case where the old mapping tables are used intentionally. + self.ofc.driver.disable_autocheck() + def test_exists_ofc_tenant(self): t, n, p, f, none = self.get_random_params() ofc_t, ofc_n, ofc_p, ofc_f, ofc_none = self.get_random_params() diff --git a/neutron/tests/unit/nec/test_pfc_driver.py b/neutron/tests/unit/nec/test_pfc_driver.py index 229002f59..7f5883247 100644 --- a/neutron/tests/unit/nec/test_pfc_driver.py +++ b/neutron/tests/unit/nec/test_pfc_driver.py @@ -19,6 +19,7 @@ import random import string import mox +import netaddr from neutron import context from neutron.openstack.common import uuidutils @@ -202,6 +203,167 @@ class PFCV4DriverTest(PFCDriverTestBase): driver = 'pfc_v4' +class PFCV5DriverTest(PFCDriverTestBase): + driver = 'pfc_v5' + + def test_create_router(self): + t = uuidutils.generate_uuid() + r = uuidutils.generate_uuid() + description = 'dummy_router_desc' + + tenant_path = "/tenants/%s" % _ofc(t) + post_path = "%s/routers" % tenant_path + router = {'id': _ofc(r)} + ofc.OFCClient.do_request("POST", post_path, + body=None).AndReturn(router) + self.mox.ReplayAll() + + ret = self.driver.create_router(tenant_path, description, r) + self.mox.VerifyAll() + router_path = "/tenants/%s/routers/%s" % (_ofc(t), _ofc(r)) + self.assertEqual(ret, router_path) + + def test_delete_router(self): + t = uuidutils.generate_uuid() + r = uuidutils.generate_uuid() + + router_path = "/tenants/%s/routers/%s" % (_ofc(t), _ofc(r)) + ofc.OFCClient.do_request("DELETE", router_path) + self.mox.ReplayAll() + + self.driver.delete_router(router_path) + self.mox.VerifyAll() + + def test_add_router_interface(self): + t = uuidutils.generate_uuid() + r = uuidutils.generate_uuid() + n = uuidutils.generate_uuid() + p = uuidutils.generate_uuid() + + router_path = "/tenants/%s/routers/%s" % (_ofc(t), _ofc(r)) + infs_path = router_path + "/interfaces" + net_path = "/tenants/%s/networks/%s" % (_ofc(t), _ofc(n)) + ip_address = '10.1.1.1/24' + mac_address = '11:22:33:44:55:66' + body = {'net_id': _ofc(n), + 'ip_address': ip_address, + 'mac_address': mac_address} + inf = {'id': _ofc(p)} + ofc.OFCClient.do_request("POST", infs_path, + body=body).AndReturn(inf) + self.mox.ReplayAll() + + ret = self.driver.add_router_interface(router_path, net_path, + ip_address, mac_address) + self.mox.VerifyAll() + inf_path = "%s/interfaces/%s" % (router_path, _ofc(p)) + self.assertEqual(ret, inf_path) + + def test_update_router_interface(self): + t = uuidutils.generate_uuid() + r = uuidutils.generate_uuid() + p = uuidutils.generate_uuid() + + router_path = "/tenants/%s/routers/%s" % (_ofc(t), _ofc(r)) + inf_path = "%s/interfaces/%s" % (router_path, _ofc(p)) + ip_address = '10.1.1.1/24' + mac_address = '11:22:33:44:55:66' + + body = {'ip_address': ip_address, + 'mac_address': mac_address} + ofc.OFCClient.do_request("PUT", inf_path, body=body) + + body = {'ip_address': ip_address} + ofc.OFCClient.do_request("PUT", inf_path, body=body) + + body = {'mac_address': mac_address} + ofc.OFCClient.do_request("PUT", inf_path, body=body) + + self.mox.ReplayAll() + + self.driver.update_router_interface(inf_path, ip_address, mac_address) + self.driver.update_router_interface(inf_path, ip_address=ip_address) + self.driver.update_router_interface(inf_path, mac_address=mac_address) + self.mox.VerifyAll() + + def test_delete_router_interface(self): + t = uuidutils.generate_uuid() + r = uuidutils.generate_uuid() + p = uuidutils.generate_uuid() + + router_path = "/tenants/%s/routers/%s" % (_ofc(t), _ofc(r)) + inf_path = "%s/interfaces/%s" % (router_path, _ofc(p)) + ofc.OFCClient.do_request("DELETE", inf_path) + self.mox.ReplayAll() + + self.driver.delete_router_interface(inf_path) + self.mox.VerifyAll() + + def _get_route_id(self, dest, nexthop): + dest = netaddr.IPNetwork(dest) + return '-'.join([str(dest.network), nexthop, str(dest.netmask)]) + + def test_add_router_route(self): + t = uuidutils.generate_uuid() + r = uuidutils.generate_uuid() + + router_path = "/tenants/%s/routers/%s" % (_ofc(t), _ofc(r)) + routes_path = router_path + "/routes" + dest = '10.1.1.0/24' + nexthop = '192.168.100.10' + body = {'destination': dest, 'nexthop': nexthop} + route_id = self._get_route_id(dest, nexthop) + ofc.OFCClient.do_request("POST", routes_path, + body=body).AndReturn({'id': route_id}) + self.mox.ReplayAll() + + ret = self.driver.add_router_route(router_path, '10.1.1.0/24', + '192.168.100.10') + self.mox.VerifyAll() + route_path = routes_path + '/' + route_id + self.assertEqual(ret, route_path) + + def test_delete_router_route(self): + t = uuidutils.generate_uuid() + r = uuidutils.generate_uuid() + + router_path = "/tenants/%s/routers/%s" % (_ofc(t), _ofc(r)) + routes_path = router_path + "/routes" + + route_id = self._get_route_id('10.1.1.0/24', '192.168.100.10') + route_path = routes_path + '/' + route_id + ofc.OFCClient.do_request("DELETE", route_path) + self.mox.ReplayAll() + + self.driver.delete_router_route(route_path) + self.mox.VerifyAll() + + def test_list_router_routes(self): + t = uuidutils.generate_uuid() + r = uuidutils.generate_uuid() + + router_path = "/tenants/%s/routers/%s" % (_ofc(t), _ofc(r)) + routes_path = router_path + "/routes" + + routes = [('10.1.1.0/24', '192.168.100.10'), + ('10.2.2.0/20', '192.168.100.20')] + data = {'routes': [{'id': self._get_route_id(route[0], route[1]), + 'destination': route[0], 'nexthop': route[1]} + for route in routes]} + ofc.OFCClient.do_request("GET", routes_path).AndReturn(data) + self.mox.ReplayAll() + + ret = self.driver.list_router_routes(router_path) + self.mox.VerifyAll() + + expected = [{'id': (routes_path + "/" + + self._get_route_id(route[0], route[1])), + 'destination': route[0], 'nexthop': route[1]} + for route in routes] + self.assertEqual(len(routes), len(ret)) + self.assertEqual(data['routes'], expected) + + class PFCDriverStringTest(base.BaseTestCase): driver = 'neutron.plugins.nec.drivers.pfc.PFCDriverBase' diff --git a/neutron/tests/unit/nec/test_router.py b/neutron/tests/unit/nec/test_router.py new file mode 100644 index 000000000..4e7505ef0 --- /dev/null +++ b/neutron/tests/unit/nec/test_router.py @@ -0,0 +1,46 @@ +# Copyright (c) 2013 OpenStack Foundation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import mock + +from neutron import manager +from neutron.plugins.nec.common import config +from neutron.tests.unit.nec import test_nec_plugin +from neutron.tests.unit import test_extension_extraroute as test_ext_route + + +class NecRouterL3AgentTestCase(test_ext_route.ExtraRouteDBTestCase): + + _plugin_name = test_nec_plugin.PLUGIN_NAME + + def setUp(self): + self.addCleanup(mock.patch.stopall) + mock.patch(test_nec_plugin.OFC_MANAGER).start() + super(NecRouterL3AgentTestCase, self).setUp(self._plugin_name) + + plugin = manager.NeutronManager.get_plugin() + plugin.network_scheduler = None + plugin.router_scheduler = None + + def test_floatingip_with_invalid_create_port(self): + self._test_floatingip_with_invalid_create_port(self._plugin_name) + + +class NecRouterOpenFlowTestCase(NecRouterL3AgentTestCase): + + def setUp(self): + config.CONF.set_override('default_router_provider', + 'openflow', 'PROVIDER') + super(NecRouterOpenFlowTestCase, self).setUp() diff --git a/neutron/tests/unit/openvswitch/test_agent_scheduler.py b/neutron/tests/unit/openvswitch/test_agent_scheduler.py index c843a2b10..726a7f277 100644 --- a/neutron/tests/unit/openvswitch/test_agent_scheduler.py +++ b/neutron/tests/unit/openvswitch/test_agent_scheduler.py @@ -189,10 +189,10 @@ class AgentSchedulerTestMixIn(object): return agent_data['id'] -class OvsAgentSchedulerTestCase(test_l3_plugin.L3NatTestCaseMixin, - test_agent_ext_plugin.AgentDBTestMixIn, - AgentSchedulerTestMixIn, - test_plugin.NeutronDbPluginV2TestCase): +class OvsAgentSchedulerTestCaseBase(test_l3_plugin.L3NatTestCaseMixin, + test_agent_ext_plugin.AgentDBTestMixIn, + AgentSchedulerTestMixIn, + test_plugin.NeutronDbPluginV2TestCase): fmt = 'json' plugin_str = ('neutron.plugins.openvswitch.' 'ovs_neutron_plugin.OVSNeutronPluginV2') @@ -202,7 +202,7 @@ class OvsAgentSchedulerTestCase(test_l3_plugin.L3NatTestCaseMixin, self.saved_attr_map = {} for resource, attrs in attributes.RESOURCE_ATTRIBUTE_MAP.iteritems(): self.saved_attr_map[resource] = attrs.copy() - super(OvsAgentSchedulerTestCase, self).setUp(self.plugin_str) + super(OvsAgentSchedulerTestCaseBase, self).setUp(self.plugin_str) ext_mgr = extensions.PluginAwareExtensionManager.get_instance() self.ext_api = test_extensions.setup_extensions_middleware(ext_mgr) self.adminContext = context.get_admin_context() @@ -219,6 +219,9 @@ class OvsAgentSchedulerTestCase(test_l3_plugin.L3NatTestCaseMixin, # Restore the original RESOURCE_ATTRIBUTE_MAP attributes.RESOURCE_ATTRIBUTE_MAP = self.saved_attr_map + +class OvsAgentSchedulerTestCase(OvsAgentSchedulerTestCaseBase): + def test_report_states(self): self._register_agent_states() agents = self._list_agents() @@ -957,12 +960,7 @@ class OvsAgentSchedulerTestCase(test_l3_plugin.L3NatTestCaseMixin, admin_context=False) -class OvsDhcpAgentNotifierTestCase(test_l3_plugin.L3NatTestCaseMixin, - test_agent_ext_plugin.AgentDBTestMixIn, - AgentSchedulerTestMixIn, - test_plugin.NeutronDbPluginV2TestCase): - plugin_str = ('neutron.plugins.openvswitch.' - 'ovs_neutron_plugin.OVSNeutronPluginV2') +class OvsDhcpAgentNotifierTestCase(OvsAgentSchedulerTestCaseBase): def setUp(self): self.dhcp_notifier = dhcp_rpc_agent_api.DhcpAgentNotifyAPI() @@ -971,27 +969,8 @@ class OvsDhcpAgentNotifierTestCase(test_l3_plugin.L3NatTestCaseMixin, 'DhcpAgentNotifyAPI') self.dhcp_notifier_cls = self.dhcp_notifier_cls_p.start() self.dhcp_notifier_cls.return_value = self.dhcp_notifier - # Save the global RESOURCE_ATTRIBUTE_MAP - self.saved_attr_map = {} - for resource, attrs in attributes.RESOURCE_ATTRIBUTE_MAP.iteritems(): - self.saved_attr_map[resource] = attrs.copy() - super(OvsDhcpAgentNotifierTestCase, self).setUp(self.plugin_str) - ext_mgr = extensions.PluginAwareExtensionManager.get_instance() - self.ext_api = test_extensions.setup_extensions_middleware(ext_mgr) - self.adminContext = context.get_admin_context() - # Add the resources to the global attribute map - # This is done here as the setup process won't - # initialize the main API router which extends - # the global attribute map - attributes.RESOURCE_ATTRIBUTE_MAP.update( - agent.RESOURCE_ATTRIBUTE_MAP) - self.agentscheduler_dbMinxin = manager.NeutronManager.get_plugin() + super(OvsDhcpAgentNotifierTestCase, self).setUp() self.addCleanup(self.dhcp_notifier_cls_p.stop) - self.addCleanup(self.restore_attribute_map) - - def restore_attribute_map(self): - # Restore the original RESOURCE_ATTRIBUTE_MAP - attributes.RESOURCE_ATTRIBUTE_MAP = self.saved_attr_map def test_network_add_to_dhcp_agent_notification(self): with mock.patch.object(self.dhcp_notifier, 'cast') as mock_dhcp: @@ -1093,12 +1072,7 @@ class OvsDhcpAgentNotifierTestCase(test_l3_plugin.L3NatTestCaseMixin, self.assertIn(expected, mock_dhcp.call_args_list) -class OvsL3AgentNotifierTestCase(test_l3_plugin.L3NatTestCaseMixin, - test_agent_ext_plugin.AgentDBTestMixIn, - AgentSchedulerTestMixIn, - test_plugin.NeutronDbPluginV2TestCase): - plugin_str = ('neutron.plugins.openvswitch.' - 'ovs_neutron_plugin.OVSNeutronPluginV2') +class OvsL3AgentNotifierTestCase(OvsAgentSchedulerTestCaseBase): def setUp(self): self.dhcp_notifier_cls_p = mock.patch( @@ -1107,27 +1081,8 @@ class OvsL3AgentNotifierTestCase(test_l3_plugin.L3NatTestCaseMixin, self.dhcp_notifier = mock.Mock(name='dhcp_notifier') self.dhcp_notifier_cls = self.dhcp_notifier_cls_p.start() self.dhcp_notifier_cls.return_value = self.dhcp_notifier - # Save the global RESOURCE_ATTRIBUTE_MAP - self.saved_attr_map = {} - for resource, attrs in attributes.RESOURCE_ATTRIBUTE_MAP.iteritems(): - self.saved_attr_map[resource] = attrs.copy() - super(OvsL3AgentNotifierTestCase, self).setUp(self.plugin_str) - ext_mgr = extensions.PluginAwareExtensionManager.get_instance() - self.ext_api = test_extensions.setup_extensions_middleware(ext_mgr) - self.adminContext = context.get_admin_context() - # Add the resources to the global attribute map - # This is done here as the setup process won't - # initialize the main API router which extends - # the global attribute map - attributes.RESOURCE_ATTRIBUTE_MAP.update( - agent.RESOURCE_ATTRIBUTE_MAP) - self.agentscheduler_dbMinxin = manager.NeutronManager.get_plugin() + super(OvsL3AgentNotifierTestCase, self).setUp() self.addCleanup(self.dhcp_notifier_cls_p.stop) - self.addCleanup(self.restore_attribute_map) - - def restore_attribute_map(self): - # Restore the original RESOURCE_ATTRIBUTE_MAP - attributes.RESOURCE_ATTRIBUTE_MAP = self.saved_attr_map def test_router_add_to_l3_agent_notification(self): plugin = manager.NeutronManager.get_plugin() diff --git a/neutron/tests/unit/test_extension_extraroute.py b/neutron/tests/unit/test_extension_extraroute.py index 4ea4c83cd..50ab18c97 100644 --- a/neutron/tests/unit/test_extension_extraroute.py +++ b/neutron/tests/unit/test_extension_extraroute.py @@ -59,10 +59,11 @@ class TestExtraRoutePlugin(test_l3.TestL3NatPlugin, class ExtraRouteDBTestCase(test_l3.L3NatDBTestCase): - def setUp(self): - test_config['plugin_name_v2'] = ( - 'neutron.tests.unit.' - 'test_extension_extraroute.TestExtraRoutePlugin') + def setUp(self, plugin=None): + if not plugin: + plugin = ('neutron.tests.unit.test_extension_extraroute.' + 'TestExtraRoutePlugin') + test_config['plugin_name_v2'] = plugin # for these tests we need to enable overlapping ips cfg.CONF.set_default('allow_overlapping_ips', True) cfg.CONF.set_default('max_routes', 3) diff --git a/neutron/tests/unit/test_l3_plugin.py b/neutron/tests/unit/test_l3_plugin.py index c2412c879..ce68bd2f4 100644 --- a/neutron/tests/unit/test_l3_plugin.py +++ b/neutron/tests/unit/test_l3_plugin.py @@ -28,7 +28,6 @@ from webob import exc import webtest from neutron.api import extensions -from neutron.api.rpc.agentnotifiers import l3_rpc_agent_api from neutron.api.v2 import attributes from neutron.common import config from neutron.common import constants as l3_constants @@ -321,13 +320,15 @@ class L3NatTestCaseMixin(object): return router_req.get_response(self.ext_api) def _make_router(self, fmt, tenant_id, name=None, admin_state_up=None, - external_gateway_info=None, set_context=False): - arg_list = (external_gateway_info and - ('external_gateway_info', ) or None) + external_gateway_info=None, set_context=False, + arg_list=None, **kwargs): + if external_gateway_info: + arg_list = ('external_gateway_info', ) + (arg_list or ()) res = self._create_router(fmt, tenant_id, name, admin_state_up, set_context, arg_list=arg_list, - external_gateway_info=external_gateway_info) + external_gateway_info=external_gateway_info, + **kwargs) return self.deserialize(fmt, res) def _add_external_gateway_to_router(self, router_id, network_id, @@ -367,10 +368,11 @@ class L3NatTestCaseMixin(object): @contextlib.contextmanager def router(self, name='router1', admin_state_up=True, fmt=None, tenant_id=_uuid(), - external_gateway_info=None, set_context=False): + external_gateway_info=None, set_context=False, + **kwargs): router = self._make_router(fmt or self.fmt, tenant_id, name, admin_state_up, external_gateway_info, - set_context) + set_context, **kwargs) try: yield router finally: @@ -1673,18 +1675,19 @@ class L3AgentDbTestCase(L3NatTestCaseBase): def _test_notify_op_agent(self, target_func, *args): l3_rpc_agent_api_str = ( 'neutron.api.rpc.agentnotifiers.l3_rpc_agent_api.L3AgentNotifyAPI') - oldNotify = l3_rpc_agent_api.L3AgentNotify + plugin = NeutronManager.get_plugin() + oldNotify = plugin.l3_rpc_notifier try: with mock.patch(l3_rpc_agent_api_str) as notifyApi: - l3_rpc_agent_api.L3AgentNotify = notifyApi + plugin.l3_rpc_notifier = notifyApi kargs = [item for item in args] kargs.append(notifyApi) target_func(*kargs) except Exception: - l3_rpc_agent_api.L3AgentNotify = oldNotify + plugin.l3_rpc_notifier = oldNotify raise else: - l3_rpc_agent_api.L3AgentNotify = oldNotify + plugin.l3_rpc_notifier = oldNotify def _test_router_gateway_op_agent(self, notifyApi): with self.router() as r: