From 7f90fa8c9005e268f914bd9ed437ca580b5e024a Mon Sep 17 00:00:00 2001 From: Ivar Lazzaro Date: Wed, 6 May 2015 20:02:43 -0700 Subject: [PATCH] node composition plugin implementation Partially implements blueprint node-centric-chain-plugin Change-Id: I16d56ebb7bff0c621c68106039b4217cfac77755 --- gbpservice/common/__init__.py | 0 gbpservice/common/utils.py | 33 ++ .../alembic_migrations/versions/HEAD | 2 +- .../d08627f64e37_ncp_implementation.py | 58 ++++ gbpservice/neutron/db/servicechain_db.py | 49 +-- .../servicechain/plugins/ncp/config.py | 8 +- .../servicechain/plugins/ncp/context.py | 64 ++-- .../servicechain/plugins/ncp/driver_base.py | 9 +- .../servicechain/plugins/ncp/exceptions.py | 48 +++ .../servicechain/plugins/ncp/model.py | 121 ++++++++ .../plugins/ncp/node_driver_manager.py | 45 ++- .../plugins/ncp/node_drivers/dummy_driver.py | 9 +- .../plugins/ncp/node_plumbers/__init__.py | 0 .../ncp/node_plumbers/dummy_plumber.py | 32 ++ .../servicechain/plugins/ncp/plugin.py | 276 ++++++++++++++++- .../servicechain/plugins/ncp/plumber_base.py | 78 +++++ .../db/grouppolicy/test_servicechain_db.py | 6 +- .../servicechain/ncp/test_ncp_plugin.py | 285 ++++++++++++++++-- .../servicechain/test_servicechain_plugin.py | 48 +++ setup.cfg | 2 + 20 files changed, 1099 insertions(+), 74 deletions(-) create mode 100644 gbpservice/common/__init__.py create mode 100644 gbpservice/common/utils.py create mode 100644 gbpservice/neutron/db/migration/alembic_migrations/versions/d08627f64e37_ncp_implementation.py create mode 100644 gbpservice/neutron/services/servicechain/plugins/ncp/exceptions.py create mode 100644 gbpservice/neutron/services/servicechain/plugins/ncp/model.py create mode 100644 gbpservice/neutron/services/servicechain/plugins/ncp/node_plumbers/__init__.py create mode 100644 gbpservice/neutron/services/servicechain/plugins/ncp/node_plumbers/dummy_plumber.py create mode 100644 gbpservice/neutron/services/servicechain/plugins/ncp/plumber_base.py diff --git a/gbpservice/common/__init__.py b/gbpservice/common/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/gbpservice/common/utils.py b/gbpservice/common/utils.py new file mode 100644 index 000000000..45c5227ed --- /dev/null +++ b/gbpservice/common/utils.py @@ -0,0 +1,33 @@ +# 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 oslo_log import log as logging +from oslo_utils import importutils +from stevedore import driver + +LOG = logging.getLogger(__name__) + + +def load_plugin(namespace, plugin): + try: + # Try to resolve plugin by name + mgr = driver.DriverManager(namespace, plugin) + plugin_class = mgr.driver + except RuntimeError as e1: + # fallback to class name + try: + plugin_class = importutils.import_class(plugin) + except ImportError as e2: + LOG.exception(_("Error loading plugin by name, %s"), e1) + LOG.exception(_("Error loading plugin by class, %s"), e2) + raise ImportError(_("Plugin not found.")) + return plugin_class() \ No newline at end of file diff --git a/gbpservice/neutron/db/migration/alembic_migrations/versions/HEAD b/gbpservice/neutron/db/migration/alembic_migrations/versions/HEAD index 633a2879d..9342e47d0 100644 --- a/gbpservice/neutron/db/migration/alembic_migrations/versions/HEAD +++ b/gbpservice/neutron/db/migration/alembic_migrations/versions/HEAD @@ -1 +1 @@ -9744740aa75c \ No newline at end of file +d08627f64e37 diff --git a/gbpservice/neutron/db/migration/alembic_migrations/versions/d08627f64e37_ncp_implementation.py b/gbpservice/neutron/db/migration/alembic_migrations/versions/d08627f64e37_ncp_implementation.py new file mode 100644 index 000000000..5812e1ffd --- /dev/null +++ b/gbpservice/neutron/db/migration/alembic_migrations/versions/d08627f64e37_ncp_implementation.py @@ -0,0 +1,58 @@ +# 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. +# + +"""ncp_implementation +""" + +# revision identifiers, used by Alembic. +revision = 'd08627f64e37' +down_revision = '9744740aa75c' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.create_table( + 'ncp_node_to_driver_mapping', + sa.Column('servicechain_node_id', sa.String(length=36), + nullable=False), + sa.Column('driver_name', sa.String(length=36)), + sa.Column('servicechain_instance_id', sa.String(length=36)), + sa.PrimaryKeyConstraint( + 'servicechain_node_id', 'servicechain_instance_id'), + sa.ForeignKeyConstraint(['servicechain_node_id'], + ['sc_nodes.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['servicechain_instance_id'], + ['sc_instances.id'], ondelete='CASCADE') + ) + + op.create_table( + 'ncp_service_targets', + sa.Column('servicechain_node_id', sa.String(length=36), + nullable=False), + sa.Column('servicechain_instance_id', sa.String(length=36)), + sa.Column('policy_target_id', sa.String(length=36)), + sa.Column('relationship', sa.String(length=25)), + sa.Column('position', sa.Integer), + + sa.PrimaryKeyConstraint( + 'servicechain_node_id', 'servicechain_instance_id', + 'policy_target_id'), + sa.ForeignKeyConstraint(['policy_target_id'], + ['gp_policy_targets.id'], ondelete='CASCADE') + ) + + +def downgrade(): + pass diff --git a/gbpservice/neutron/db/servicechain_db.py b/gbpservice/neutron/db/servicechain_db.py index 8245489d8..5460f9c8d 100644 --- a/gbpservice/neutron/db/servicechain_db.py +++ b/gbpservice/neutron/db/servicechain_db.py @@ -205,6 +205,8 @@ class ServiceChainDbPlugin(schain.ServiceChainPluginBase, 'config_param_names': spec.get('config_param_names'), 'shared': spec['shared']} res['nodes'] = [sc_node['node_id'] for sc_node in spec['nodes']] + res['instances'] = [x['servicechain_instance_id'] for x in + spec['instances']] return self._fields(res, fields) def _make_sc_instance_dict(self, instance, fields=None): @@ -296,13 +298,16 @@ class ServiceChainDbPlugin(schain.ServiceChainPluginBase, return self._get_collection_count(context, ServiceChainNode, filters=filters) - def _process_nodes_for_spec(self, context, spec_db, spec): + def _process_nodes_for_spec(self, context, spec_db, spec, + set_params=True): if 'nodes' in spec: - self._set_nodes_for_spec(context, spec_db, spec['nodes']) + self._set_nodes_for_spec(context, spec_db, spec['nodes'], + set_params=set_params) del spec['nodes'] return spec - def _set_nodes_for_spec(self, context, spec_db, nodes_id_list): + def _set_nodes_for_spec(self, context, spec_db, nodes_id_list, + set_params=True): if not nodes_id_list: spec_db.nodes = [] @@ -324,18 +329,21 @@ class ServiceChainDbPlugin(schain.ServiceChainPluginBase, # it as clearing existing nodes. spec_db.nodes = [] for node_id in nodes_id_list: - sc_node = self.get_servicechain_node(context, node_id) - node_dict = jsonutils.loads(sc_node['config']) - config_params = (node_dict.get('parameters') or - node_dict.get('Parameters')) - if config_params: - if not spec_db.config_param_names: - spec_db.config_param_names = str(config_params.keys()) - else: - config_param_names = ast.literal_eval( - spec_db.config_param_names) - config_param_names.extend(config_params.keys()) - spec_db.config_param_names = str(config_param_names) + if set_params: + sc_node = self.get_servicechain_node(context, node_id) + node_dict = jsonutils.loads(sc_node['config']) + config_params = (node_dict.get('parameters') or + node_dict.get('Parameters')) + if config_params: + if not spec_db.config_param_names: + spec_db.config_param_names = str( + config_params.keys()) + else: + config_param_names = ast.literal_eval( + spec_db.config_param_names) + config_param_names.extend(config_params.keys()) + spec_db.config_param_names = str( + config_param_names) assoc = SpecNodeAssociation(servicechain_spec_id=spec_db.id, node_id=node_id) @@ -371,7 +379,8 @@ class ServiceChainDbPlugin(schain.ServiceChainPluginBase, instance_db.specs.append(assoc) @log.log - def create_servicechain_spec(self, context, servicechain_spec): + def create_servicechain_spec(self, context, servicechain_spec, + set_params=True): spec = servicechain_spec['servicechain_spec'] tenant_id = self._get_tenant_id_for_create(context, spec) with context.session.begin(subtransactions=True): @@ -380,18 +389,20 @@ class ServiceChainDbPlugin(schain.ServiceChainPluginBase, name=spec['name'], description=spec['description'], shared=spec['shared']) - self._process_nodes_for_spec(context, spec_db, spec) + self._process_nodes_for_spec(context, spec_db, spec, + set_params=set_params) context.session.add(spec_db) return self._make_sc_spec_dict(spec_db) @log.log def update_servicechain_spec(self, context, spec_id, - servicechain_spec): + servicechain_spec, set_params=True): spec = servicechain_spec['servicechain_spec'] with context.session.begin(subtransactions=True): spec_db = self._get_servicechain_spec(context, spec_id) - spec = self._process_nodes_for_spec(context, spec_db, spec) + spec = self._process_nodes_for_spec(context, spec_db, spec, + set_params=set_params) spec_db.update(spec) return self._make_sc_spec_dict(spec_db) diff --git a/gbpservice/neutron/services/servicechain/plugins/ncp/config.py b/gbpservice/neutron/services/servicechain/plugins/ncp/config.py index 016f13dc4..771bde5c4 100644 --- a/gbpservice/neutron/services/servicechain/plugins/ncp/config.py +++ b/gbpservice/neutron/services/servicechain/plugins/ncp/config.py @@ -19,7 +19,13 @@ service_chain_opts = [ help=_("An ordered list of service chain node drivers " "entrypoints to be loaded from the " "gbpservice.neutron.servicechain.ncp_drivers " - "namespace.")) + "namespace.")), + cfg.StrOpt('node_plumber', + default='dummy_plumber', + help=_("The plumber used by the Node Composition Plugin " + "for service plumbing. Entrypoint loaded from the " + "gbpservice.neutron.servicechain.ncp_plumbers " + "namespace.")) ] diff --git a/gbpservice/neutron/services/servicechain/plugins/ncp/context.py b/gbpservice/neutron/services/servicechain/plugins/ncp/context.py index 8564c9543..f75c1c6b5 100644 --- a/gbpservice/neutron/services/servicechain/plugins/ncp/context.py +++ b/gbpservice/neutron/services/servicechain/plugins/ncp/context.py @@ -15,6 +15,7 @@ from neutron import manager from neutron.plugins.common import constants as pconst from gbpservice.neutron.extensions import group_policy +from gbpservice.neutron.services.servicechain.plugins.ncp import model def get_gbp_plugin(): @@ -26,13 +27,19 @@ def get_node_driver_context(sc_plugin, context, sc_instance, management_group=None, service_targets=None): specs = sc_plugin.get_servicechain_specs( context, filters={'id': sc_instance['servicechain_specs']}) - provider = _ptg_or_ep(context, sc_instance['provider_ptg_id']) - consumer = _ptg_or_ep(context, sc_instance['consumer_ptg_id']) + position = _calculate_node_position(specs, current_node['id']) + provider = _get_ptg_or_ep(context, sc_instance['provider_ptg_id']) + consumer = _get_ptg_or_ep(context, sc_instance['consumer_ptg_id']) current_profile = sc_plugin.get_service_profile( context, current_node['service_profile_id']) original_profile = sc_plugin.get_service_profile( context, original_node['service_profile_id']) if original_node else None + if not service_targets: + service_targets = model.get_service_targets( + context.session, servicechain_instance_id=sc_instance['id'], + position=position, servicechain_node_id=current_node['id']) + return NodeDriverContext(sc_plugin=sc_plugin, context=context, service_chain_instance=sc_instance, @@ -44,10 +51,11 @@ def get_node_driver_context(sc_plugin, context, sc_instance, management_group=management_group, original_service_chain_node=original_node, original_service_profile=original_profile, - service_targets=service_targets) + service_targets=service_targets, + position=position) -def _ptg_or_ep(context, group_id): +def _get_ptg_or_ep(context, group_id): group = None if group_id: try: @@ -59,11 +67,20 @@ def _ptg_or_ep(context, group_id): return group +def _calculate_node_position(specs, node_id): + for spec in specs: + pos = 0 + for node in spec['nodes']: + pos += 1 + if node_id == node: + return pos + + class NodeDriverContext(object): - """ Context passed down to NCC Node Drivers.""" + """Context passed down to NCP Node Drivers.""" def __init__(self, sc_plugin, context, service_chain_instance, - service_chain_specs, current_service_chain_node, + service_chain_specs, current_service_chain_node, position, current_service_profile, provider_group, consumer_group=None, management_group=None, original_service_chain_node=None, original_service_profile=None, service_targets=None): @@ -85,6 +102,7 @@ class NodeDriverContext(object): self._core_plugin = manager.NeutronManager.get_plugin() self._l3_plugin = manager.NeutronManager.get_service_plugins().get( pconst.L3_ROUTER_NAT) + self._position = position @property def gbp_plugin(self): @@ -136,6 +154,10 @@ class NodeDriverContext(object): def current_profile(self): return self._current_service_profile + @property + def current_position(self): + return self._position + @property def original_node(self): return self._original_service_chain_node @@ -152,18 +174,6 @@ class NodeDriverContext(object): self.current_node['id'] in x['nodes']] return self._relevant_specs - @property - def service_targets(self): - """ Returns the service targets assigned for this service if any. - The result looks like the following: - { - "provider": [pt_uuids], - "consumer": [pt_uuids], - "management": [pt_uuids], - } - """ - return self._service_targets - @property def provider(self): return self._provider_group @@ -174,4 +184,20 @@ class NodeDriverContext(object): @property def management(self): - return self._management_group \ No newline at end of file + return self._management_group + + def get_service_targets(self, update=False): + """ Returns the service targets assigned for this service if any. + The result looks like the following: + { + "provider": [pt_uuids], + "consumer": [pt_uuids], + "management": [pt_uuids], + } + """ + if update: + self._service_targets = model.get_service_targets( + self.session, servicechain_instance_id=self.instance['id'], + position=self.current_position, + servicechain_node_id=self.current_node['id']) + return self._service_targets diff --git a/gbpservice/neutron/services/servicechain/plugins/ncp/driver_base.py b/gbpservice/neutron/services/servicechain/plugins/ncp/driver_base.py index 3f2b3a615..5ca02e249 100644 --- a/gbpservice/neutron/services/servicechain/plugins/ncp/driver_base.py +++ b/gbpservice/neutron/services/servicechain/plugins/ncp/driver_base.py @@ -28,12 +28,13 @@ class NodeDriverBase(object): """ @abc.abstractmethod - def initialize(self): + def initialize(self, name): """Perform driver initialization. Called after all drivers have been loaded and the database has been initialized. No abstract methods defined below will be - called prior to this method being called. + called prior to this method being called. Name is a unique attribute + that identifies the driver. """ pass @@ -142,4 +143,8 @@ class NodeDriverBase(object): :param context: NodeDriverContext instance describing the service chain and the specific node to be processed by this driver. """ + pass + + @abc.abstractproperty + def name(self): pass \ No newline at end of file diff --git a/gbpservice/neutron/services/servicechain/plugins/ncp/exceptions.py b/gbpservice/neutron/services/servicechain/plugins/ncp/exceptions.py new file mode 100644 index 000000000..aa879ac3d --- /dev/null +++ b/gbpservice/neutron/services/servicechain/plugins/ncp/exceptions.py @@ -0,0 +1,48 @@ +# 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. + +"""Exceptions used by NodeCompositionPlugin and drivers.""" + +from neutron.common import exceptions + + +class NodeDriverError(exceptions.NeutronException): + """Node driver call failed.""" + message = _("%(method)s failed.") + + +class NodeCompositionPluginException(exceptions.NeutronException): + """Base for node driver exceptions returned to user.""" + pass + + +class NodeCompositionPluginBadRequest(exceptions.BadRequest, + NodeCompositionPluginException): + """Base for node driver bad request exceptions returned to user.""" + pass + + +class OneSpecPerInstanceAllowed(NodeCompositionPluginBadRequest): + message = _("The Node Composition Plugin only supports one Servicechain" + "Spec per Servicechain Instance.") + + +class NoDriverAvailableForAction(NodeCompositionPluginBadRequest): + message = _("The Node Composition Plugin can't find any Node Driver " + "available for executing %(action)s on node %(node_id)s. " + "This may be caused by a Servicechain Node misconfiguration " + "or an unsupported Service Profile.") + + +class ServiceProfileInUseByAnInstance(NodeCompositionPluginBadRequest): + message = _("Cannot update Service Profile %(profile_id)s because it's " + "used by servicechain instance %(instance_id)s.") \ No newline at end of file diff --git a/gbpservice/neutron/services/servicechain/plugins/ncp/model.py b/gbpservice/neutron/services/servicechain/plugins/ncp/model.py new file mode 100644 index 000000000..4ff21aafe --- /dev/null +++ b/gbpservice/neutron/services/servicechain/plugins/ncp/model.py @@ -0,0 +1,121 @@ +# 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.db import model_base +from oslo_log import log as logging +import sqlalchemy as sa + +from gbpservice.neutron.db.grouppolicy import group_policy_db as gp_db + +LOG = logging.getLogger(__name__) +PROVIDER = 'provider' +CONSUMER = 'consumer' +MANAGEMENT = 'management' +RELATIONSHIPS = [PROVIDER, CONSUMER, MANAGEMENT] + + +class NodeToDriverMapping(model_base.BASEV2): + """Node to Driver mapping DB. + + This table keeps track of the driver owning a specific SC Node based on + the SC instance + """ + + __tablename__ = 'ncp_node_to_driver_mapping' + servicechain_node_id = sa.Column(sa.String(36), + sa.ForeignKey('sc_nodes.id', + ondelete='CASCADE'), + nullable=False, primary_key=True) + # Based on the extension name + driver_name = sa.Column(sa.String(36), nullable=False) + servicechain_instance_id = sa.Column(sa.String(36), + sa.ForeignKey('sc_instances.id', + ondelete='CASCADE'), + primary_key=True) + + +class ServiceTarget(model_base.BASEV2): + """Service related policy targets. + + Internal information regarding the policy targets owned by services. + """ + + __tablename__ = 'ncp_service_targets' + policy_target_id = sa.Column(sa.String(36), + sa.ForeignKey(gp_db.PolicyTarget.id, + ondelete='CASCADE'), + nullable=False, primary_key=True) + # Not a FK to avoid constraint error on SCI delete + # keeping the DB entry is useful to identify uncleaned PTs + servicechain_instance_id = sa.Column(sa.String(36), + nullable=False, primary_key=True) + # Not a FK to avoid constraint error on SCN delete. + # keeping the DB entry is useful to identify uncleaned PTs + servicechain_node_id = sa.Column(sa.String(36), + nullable=False, primary_key=True) + # Defines on which "side" of the chain the PT is placed. typically + # its values can be "provider", "consumer" or "management" + relationship = sa.Column(sa.String(25), nullable=False) + position = sa.Column(sa.Integer) + + +def set_node_ownership(context, driver_name): + session = context.session + with session.begin(subtransactions=True): + owner = NodeToDriverMapping( + servicechain_instance_id=context.instance['id'], + servicechain_node_id=context.current_node['id'], + driver_name=driver_name) + session.add(owner) + + +def get_node_owner(context): + session = context.session + with session.begin(subtransactions=True): + query = session.query(NodeToDriverMapping) + query = query.filter_by( + servicechain_instance_id=context.instance['id']) + query = query.filter_by( + servicechain_node_id=context.current_node['id']) + return query.all() + + +def set_service_target(context, policy_target_id, relationship): + session = context.session + with session.begin(subtransactions=True): + owner = ServiceTarget( + policy_target_id=policy_target_id, + servicechain_instance_id=context.instance['id'], + servicechain_node_id=context.current_node['id'], + position=context.current_position, + relationship=relationship) + session.add(owner) + + +def get_service_targets(session, policy_target_id=None, relationship=None, + servicechain_instance_id=None, position=None, + servicechain_node_id=None): + with session.begin(subtransactions=True): + query = session.query(ServiceTarget) + if servicechain_instance_id: + query = query.filter_by( + servicechain_instance_id=servicechain_instance_id) + if servicechain_node_id: + query = query.filter_by( + servicechain_node_id=servicechain_node_id) + if policy_target_id: + query = query.filter_by(policy_target_id=policy_target_id) + if position: + query = query.filter_by(position=position) + if relationship: + query = query.filter_by(relationship=relationship) + return query.all() diff --git a/gbpservice/neutron/services/servicechain/plugins/ncp/node_driver_manager.py b/gbpservice/neutron/services/servicechain/plugins/ncp/node_driver_manager.py index ff154748d..d50dd3f35 100644 --- a/gbpservice/neutron/services/servicechain/plugins/ncp/node_driver_manager.py +++ b/gbpservice/neutron/services/servicechain/plugins/ncp/node_driver_manager.py @@ -10,11 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. +from neutron.common import exceptions as n_exc from oslo_config import cfg from oslo_log import log as logging import stevedore from gbpservice.neutron.services.servicechain.plugins.ncp import config # noqa +from gbpservice.neutron.services.servicechain.plugins.ncp import model LOG = logging.getLogger(__name__) @@ -52,6 +54,45 @@ class NodeDriverManager(stevedore.named.NamedExtensionManager): for driver in self.ordered_drivers: LOG.info(_("Initializing service chain node drivers '%s'"), driver.name) - driver.obj.initialize() + driver.obj.initialize(driver.name) self.native_bulk_support &= getattr(driver.obj, - 'native_bulk_support', True) \ No newline at end of file + 'native_bulk_support', True) + + def schedule_deploy(self, context): + """Schedule Node Driver for Node creation. + + Given a NodeContext, this method returns the driver capable of creating + the specific node. + """ + for driver in self.ordered_drivers: + try: + driver.obj.validate_create(context) + model.set_node_ownership(context, driver.obj.name) + return driver.obj + except n_exc.NeutronException as e: + LOG.warn(e.message) + + def schedule_destroy(self, context): + """Schedule Node Driver for Node disruption. + + Given a NodeContext, this method returns the driver capable of + destroying the specific node. + """ + return self._get_owning_driver(context) + + def schedule_update(self, context): + """Schedule Node Driver for Node Update. + + Given a NodeContext, this method returns the driver capable of updating + the specific node. + """ + driver = self._get_owning_driver(context) + if driver: + driver.validate_update(context) + return driver + + def _get_owning_driver(self, context): + owner = model.get_node_owner(context) + if owner: + driver = self.drivers.get(owner[0].driver_name) + return driver.obj if driver else None diff --git a/gbpservice/neutron/services/servicechain/plugins/ncp/node_drivers/dummy_driver.py b/gbpservice/neutron/services/servicechain/plugins/ncp/node_drivers/dummy_driver.py index 269ca4215..28afd33ec 100644 --- a/gbpservice/neutron/services/servicechain/plugins/ncp/node_drivers/dummy_driver.py +++ b/gbpservice/neutron/services/servicechain/plugins/ncp/node_drivers/dummy_driver.py @@ -20,8 +20,9 @@ class NoopNodeDriver(driver_base.NodeDriverBase): initialized = False @log.log - def initialize(self): + def initialize(self, name): self.initialized = True + self._name = name @log.log def get_plumbing_info(self, context): @@ -45,4 +46,8 @@ class NoopNodeDriver(driver_base.NodeDriverBase): @log.log def update(self, context): - pass \ No newline at end of file + pass + + @property + def name(self): + return self._name diff --git a/gbpservice/neutron/services/servicechain/plugins/ncp/node_plumbers/__init__.py b/gbpservice/neutron/services/servicechain/plugins/ncp/node_plumbers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/gbpservice/neutron/services/servicechain/plugins/ncp/node_plumbers/dummy_plumber.py b/gbpservice/neutron/services/servicechain/plugins/ncp/node_plumbers/dummy_plumber.py new file mode 100644 index 000000000..fd80477f0 --- /dev/null +++ b/gbpservice/neutron/services/servicechain/plugins/ncp/node_plumbers/dummy_plumber.py @@ -0,0 +1,32 @@ +# 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.common import log + +from gbpservice.neutron.services.servicechain.plugins.ncp import plumber_base + + +class NoopPlumber(plumber_base.NodePlumberBase): + + initialized = False + + @log.log + def initialize(self): + self.initialized = True + + @log.log + def plug_services(self, context, deployment): + self._sort_deployment(deployment) + + @log.log + def unplug_services(self, context, deployment): + self._sort_deployment(deployment) diff --git a/gbpservice/neutron/services/servicechain/plugins/ncp/plugin.py b/gbpservice/neutron/services/servicechain/plugins/ncp/plugin.py index a90dab8bb..b3a626e90 100644 --- a/gbpservice/neutron/services/servicechain/plugins/ncp/plugin.py +++ b/gbpservice/neutron/services/servicechain/plugins/ncp/plugin.py @@ -10,16 +10,28 @@ # License for the specific language governing permissions and limitations # under the License. +from neutron.common import log +from oslo_config import cfg from oslo_log import log as logging +from oslo_utils import excutils +from gbpservice.common import utils from gbpservice.neutron.db import servicechain_db +from gbpservice.neutron.services.servicechain.plugins.ncp import ( + context as ctx) +from gbpservice.neutron.services.servicechain.plugins.ncp import ( + exceptions as exc) from gbpservice.neutron.services.servicechain.plugins.ncp import ( node_driver_manager as manager) +from gbpservice.neutron.services.servicechain.plugins import sharing LOG = logging.getLogger(__name__) +PLUMBER_NAMESPACE = 'gbpservice.neutron.servicechain.ncp_plumbers' -class NodeCompositionPlugin(servicechain_db.ServiceChainDbPlugin): + +class NodeCompositionPlugin(servicechain_db.ServiceChainDbPlugin, + sharing.SharingMixin): """Implementation of the Service Chain Plugin. @@ -30,3 +42,265 @@ class NodeCompositionPlugin(servicechain_db.ServiceChainDbPlugin): self.driver_manager = manager.NodeDriverManager() super(NodeCompositionPlugin, self).__init__() self.driver_manager.initialize() + self.plumber = utils.load_plugin( + PLUMBER_NAMESPACE, cfg.CONF.node_composition_plugin.node_plumber) + self.plumber.initialize() + + @log.log + def create_servicechain_instance(self, context, servicechain_instance): + """Instance created. + + When a Servicechain Instance is created, all its nodes need to be + instantiated. + """ + session = context.session + deployers = {} + with session.begin(subtransactions=True): + instance = super(NodeCompositionPlugin, + self).create_servicechain_instance( + context, servicechain_instance) + if len(instance['servicechain_specs']) > 1: + raise exc.OneSpecPerInstanceAllowed() + deployers = self._get_scheduled_drivers(context, instance, + 'deploy') + + # Actual node deploy + try: + self._deploy_servicechain_nodes(context, deployers) + except Exception: + # Some node could not be deployed + with excutils.save_and_reraise_exception(): + LOG.error(_("Node deployment failed, " + "deleting servicechain_instance %s"), + instance['id']) + self.delete_servicechain_instance(context, instance['id']) + + return instance + + @log.log + def update_servicechain_instance(self, context, servicechain_instance_id, + servicechain_instance): + """Instance updated. + + When a Servicechain Instance is updated and the spec changed, all the + nodes of the previous spec should be destroyed and the newer ones + created. + """ + session = context.session + deployers = {} + destroyers = {} + with session.begin(subtransactions=True): + original_instance = self.get_servicechain_instance( + context, servicechain_instance_id) + updated_instance = super( + NodeCompositionPlugin, self).update_servicechain_instance( + context, servicechain_instance_id, servicechain_instance) + + if (original_instance['servicechain_specs'] != + updated_instance['servicechain_specs']): + if len(updated_instance['servicechain_specs']) > 1: + raise exc.OneSpecPerInstanceAllowed() + destroyers = self._get_scheduled_drivers( + context, original_instance, 'destroy') + deployers = self._get_scheduled_drivers( + context, updated_instance, 'deploy') + self._destroy_servicechain_nodes(context, destroyers) + self._deploy_servicechain_nodes(context, deployers) + return updated_instance + + @log.log + def delete_servicechain_instance(self, context, servicechain_instance_id): + """Instance deleted. + + When a Servicechain Instance is deleted, all its nodes need to be + destroyed. + """ + session = context.session + with session.begin(subtransactions=True): + instance = self.get_servicechain_instance(context, + servicechain_instance_id) + destroyers = self._get_scheduled_drivers(context, instance, + 'destroy') + self._destroy_servicechain_nodes(context, destroyers) + + with session.begin(subtransactions=True): + super(NodeCompositionPlugin, self).delete_servicechain_instance( + context, servicechain_instance_id) + + @log.log + def create_servicechain_node(self, context, servicechain_node): + session = context.session + with session.begin(subtransactions=True): + result = super(NodeCompositionPlugin, + self).create_servicechain_node(context, + servicechain_node) + self._validate_shared_create(context, result, 'servicechain_node') + return result + + @log.log + def update_servicechain_node(self, context, servicechain_node_id, + servicechain_node): + """Node Update. + + When a Servicechain Node is updated, all the corresponding instances + need to be updated as well. This usually results in a node + reconfiguration. + """ + session = context.session + updaters = {} + with session.begin(subtransactions=True): + original_sc_node = self.get_servicechain_node( + context, servicechain_node_id) + updated_sc_node = super(NodeCompositionPlugin, + self).update_servicechain_node( + context, servicechain_node_id, + servicechain_node) + self._validate_shared_update(context, original_sc_node, + updated_sc_node, 'servicechain_node') + instances = self._get_node_instances(context, updated_sc_node) + for instance in instances: + node_context = ctx.get_node_driver_context( + self, context, instance, updated_sc_node, original_sc_node) + # TODO(ivar): Validate that the node driver understands the + # update. + driver = self.driver_manager.schedule_update(node_context) + if not driver: + raise exc.NoDriverAvailableForAction( + action='update', node_id=original_sc_node['id']) + updaters[instance['id']] = {} + updaters[instance['id']]['context'] = node_context + updaters[instance['id']]['driver'] = driver + updaters[instance['id']]['plumbing_info'] = ( + driver.get_plumbing_info(node_context)) + # Update the nodes + for update in updaters.values(): + try: + update['driver'].update(update['context']) + except exc.NodeDriverError as ex: + LOG.error(_("Node Update failed, %s"), + ex.message) + + return updated_sc_node + + @log.log + def create_servicechain_spec(self, context, servicechain_spec): + session = context.session + with session.begin(subtransactions=True): + result = super( + NodeCompositionPlugin, self).create_servicechain_spec( + context, servicechain_spec, set_params=False) + self._validate_shared_create(context, result, 'servicechain_spec') + return result + + @log.log + def update_servicechain_spec(self, context, servicechain_spec_id, + servicechain_spec): + session = context.session + with session.begin(subtransactions=True): + original_sc_spec = self.get_servicechain_spec( + context, servicechain_spec_id) + updated_sc_spec = super(NodeCompositionPlugin, + self).update_servicechain_spec( + context, servicechain_spec_id, + servicechain_spec, set_params=False) + self._validate_shared_update(context, original_sc_spec, + updated_sc_spec, 'servicechain_spec') + return updated_sc_spec + + @log.log + def create_service_profile(self, context, service_profile): + session = context.session + with session.begin(subtransactions=True): + result = super( + NodeCompositionPlugin, self).create_service_profile( + context, service_profile) + self._validate_shared_create(context, result, 'service_profile') + return result + + @log.log + def update_service_profile(self, context, service_profile_id, + service_profile): + session = context.session + with session.begin(subtransactions=True): + original_profile = self.get_service_profile( + context, service_profile_id) + updated_profile = super(NodeCompositionPlugin, + self).update_service_profile( + context, service_profile_id, + service_profile) + self._validate_profile_update(context, original_profile, + updated_profile) + return updated_profile + + def _get_instance_nodes(self, context, instance): + if not instance['servicechain_specs']: + return [] + specs = self.get_servicechain_spec( + context, instance['servicechain_specs'][0]) + return self.get_servicechain_nodes(context, {'id': specs['nodes']}) + + def _get_node_instances(self, context, node): + specs = self.get_servicechain_specs( + context, {'id': node['servicechain_specs']}) + result = [] + for spec in specs: + result.extend(self.get_servicechain_instances( + context, {'id': spec['instances']})) + return result + + def _get_scheduled_drivers(self, context, instance, action): + nodes = self._get_instance_nodes(context, instance) + result = {} + func = getattr(self.driver_manager, 'schedule_' + action) + for node in nodes: + node_context = ctx.get_node_driver_context( + self, context, instance, node) + driver = func(node_context) + if not driver: + raise exc.NoDriverAvailableForAction(action=action, + node_id=node['id']) + result[node['id']] = {} + result[node['id']]['driver'] = driver + result[node['id']]['context'] = node_context + result[node['id']]['plumbing_info'] = driver.get_plumbing_info( + node_context) + return result + + def _deploy_servicechain_nodes(self, context, deployers): + self.plumber.plug_services(context, deployers.values()) + for deploy in deployers.values(): + driver = deploy['driver'] + driver.create(deploy['context']) + + def _destroy_servicechain_nodes(self, context, destroyers): + # Actual node disruption + try: + for destroy in destroyers.values(): + driver = destroy['driver'] + try: + driver.delete(destroy['context']) + except exc.NodeDriverError: + LOG.error(_("Node destroy failed, for node %s "), + driver['context'].current_node['id']) + except Exception as e: + LOG.exception(e) + finally: + self.plumber.unplug_services(context, destroyers.values()) + pass + + def _validate_profile_update(self, context, original, updated): + # Raise if the profile is in use by any instance + # Ugly one shot query to verify whether the profile is in use + db = servicechain_db + query = context.session.query(db.ServiceChainInstance) + query = query.join(db.InstanceSpecAssociation) + query = query.join(db.ServiceChainSpec) + query = query.join(db.SpecNodeAssociation) + query = query.join(db.ServiceChainNode) + instance = query.filter( + db.ServiceChainNode.service_profile_id == original['id']).first() + if instance: + raise exc.ServiceProfileInUseByAnInstance( + profile_id=original['id'], instance_id=instance.id) + self._validate_shared_update(context, original, updated, + 'service_profile') \ No newline at end of file diff --git a/gbpservice/neutron/services/servicechain/plugins/ncp/plumber_base.py b/gbpservice/neutron/services/servicechain/plugins/ncp/plumber_base.py new file mode 100644 index 000000000..b6357b178 --- /dev/null +++ b/gbpservice/neutron/services/servicechain/plugins/ncp/plumber_base.py @@ -0,0 +1,78 @@ +# 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 abc +import six + + +@six.add_metaclass(abc.ABCMeta) +class NodePlumberBase(object): + """Node Plumber base Class + + The node plumber is an entity which takes care of plumbing nodes in a + chain. By node plumbing is intended the creation/disruption of the + appropriate Neutron and GBP constructs (typically Ports and Policy Targets) + based on the specific Node needs, taking into account the whole service + chain in the process. Ideally, this module will ensure that the traffic + flows as expected according to the user intent. + """ + + @abc.abstractmethod + def initialize(self): + """Perform plumber initialization. + + No abstract methods defined below will be called prior to this method + being called. + """ + + @abc.abstractmethod + def plug_services(self, context, deployment): + """Plug services + + Given a deployment, this method is expected to create all the needed + policy targets / neutron ports placed where needed based on the whole + chain configuration. + The expectation is that this method ultimately creates ServiceTarget + object that will be retrieved by the node drivers at the right time. + + A deployment is a list composed as follows: + [{'context': node_context, + 'driver': deploying_driver, + 'plumbing_info': node_plumbing_needs}, + ...] + No assumptions should be made on the order of the nodes as received in + the deployment, but it can be retrieved by calling + node_context.current_position + """ + + @abc.abstractmethod + def unplug_services(self, context, deployment): + """Plug services + + Given a deployment, this method is expected to destroy all the + policy targets / neutron ports previously created for this chain + configuration. + The expectation is that this method ultimately removes all the + ServiceTarget related to this particular chain. + + A deployment is a list composed as follows: + [{'context': node_context, + 'driver': deploying_driver, + 'plumbing_info': node_plumbing_needs}, + ...] + No assumptions should be made on the order of the nodes as received in + the deployment, but it can be retrieved by calling + node_context.current_position + """ + + def _sort_deployment(self, deployment): + deployment.sort(key=lambda x: x['context'].current_position) \ No newline at end of file diff --git a/gbpservice/neutron/tests/unit/db/grouppolicy/test_servicechain_db.py b/gbpservice/neutron/tests/unit/db/grouppolicy/test_servicechain_db.py index 1db353b65..a55fbe996 100644 --- a/gbpservice/neutron/tests/unit/db/grouppolicy/test_servicechain_db.py +++ b/gbpservice/neutron/tests/unit/db/grouppolicy/test_servicechain_db.py @@ -140,8 +140,10 @@ class ServiceChainDbTestCase(ServiceChainDBTestBase, if not sc_plugin: sc_plugin = DB_GP_PLUGIN_KLASS if not service_plugins: - service_plugins = {'gp_plugin_name': gp_plugin or GP_PLUGIN_KLASS, - 'sc_plugin_name': sc_plugin} + service_plugins = { + 'l3_plugin_name': 'router', + 'gp_plugin_name': gp_plugin or GP_PLUGIN_KLASS, + 'sc_plugin_name': sc_plugin} super(ServiceChainDbTestCase, self).setUp( plugin=core_plugin, ext_mgr=ext_mgr, diff --git a/gbpservice/neutron/tests/unit/services/servicechain/ncp/test_ncp_plugin.py b/gbpservice/neutron/tests/unit/services/servicechain/ncp/test_ncp_plugin.py index 004545467..934ab3431 100644 --- a/gbpservice/neutron/tests/unit/services/servicechain/ncp/test_ncp_plugin.py +++ b/gbpservice/neutron/tests/unit/services/servicechain/ncp/test_ncp_plugin.py @@ -11,13 +11,23 @@ # See the License for the specific language governing permissions and # limitations under the License. +import mock +from neutron.common import config # noqa +from neutron.common import exceptions as n_exc from neutron import context as n_context from neutron.db import api as db_api from neutron.db import model_base +from neutron import manager +from neutron.plugins.common import constants as pconst from oslo_config import cfg +from oslo_serialization import jsonutils +from gbpservice.neutron.db.grouppolicy import group_policy_mapping_db # noqa +from gbpservice.neutron.services.grouppolicy import config as gpconfig # noqa from gbpservice.neutron.services.servicechain.plugins.ncp import ( context as ncp_context) +from gbpservice.neutron.services.servicechain.plugins.ncp import ( + exceptions as exc) import gbpservice.neutron.services.servicechain.plugins.ncp.config # noqa from gbpservice.neutron.services.servicechain.plugins.ncp.node_drivers import ( dummy_driver as dummy_driver) @@ -40,21 +50,72 @@ class NodeCompositionPluginTestCase( def setUp(self, core_plugin=None, gp_plugin=None, node_drivers=None): if node_drivers: cfg.CONF.set_override('node_drivers', node_drivers, - group='node_composition_chain') + group='node_composition_plugin') + config.cfg.CONF.set_override('policy_drivers', + ['implicit_policy', 'resource_mapping'], + group='group_policy') super(NodeCompositionPluginTestCase, self).setUp( core_plugin=core_plugin or CORE_PLUGIN, gp_plugin=gp_plugin or GP_PLUGIN_KLASS, sc_plugin=SC_PLUGIN_KLASS) engine = db_api.get_engine() model_base.BASEV2.metadata.create_all(engine) + self.driver = self.sc_plugin.driver_manager.ordered_drivers[0].obj - def test_node_shared(self): - pass + @property + def sc_plugin(self): + plugins = manager.NeutronManager.get_service_plugins() + servicechain_plugin = plugins.get(pconst.SERVICECHAIN) + return servicechain_plugin - def test_profile_shared(self): - pass + def _create_redirect_rule(self, spec_id): + action = self.create_policy_action(action_type='REDIRECT', + action_value=spec_id) + classifier = self.create_policy_classifier( + port_range=80, protocol='tcp', direction='bi') + rule = self.create_policy_rule( + policy_actions=[action['policy_action']['id']], + policy_classifier_id=classifier['policy_classifier']['id']) + return rule - def test_spec_shared(self): + def _create_redirect_prs(self, spec_id): + rule = self._create_redirect_rule(spec_id)['policy_rule'] + prs = self.create_policy_rule_set(policy_rules=[rule['id']]) + return prs + + def _create_simple_service_chain(self, number_of_nodes=1): + prof = self.create_service_profile( + service_type='LOADBALANCER')['service_profile'] + + node_ids = [] + for x in xrange(number_of_nodes): + node_ids.append(self.create_servicechain_node( + service_profile_id=prof['id'], + expected_res_status=201)['servicechain_node']['id']) + + return self._create_chain_with_nodes(node_ids) + + def _create_chain_with_nodes(self, node_ids=None): + node_ids = node_ids or [] + spec = self.create_servicechain_spec( + nodes=node_ids, + expected_res_status=201)['servicechain_spec'] + prs = self._create_redirect_prs(spec['id'])['policy_rule_set'] + provider = self.create_policy_target_group( + provided_policy_rule_sets={prs['id']: ''})['policy_target_group'] + consumer = self.create_policy_target_group( + consumed_policy_rule_sets={prs['id']: ''})['policy_target_group'] + return provider, consumer, prs + + def _add_node_driver(self, name): + inst = dummy_driver.NoopNodeDriver() + inst.initialize(name) + ext = mock.Mock() + ext.obj = inst + self.sc_plugin.driver_manager.ordered_drivers.append(ext) + self.sc_plugin.driver_manager.drivers[name] = ext + + def test_spec_ordering_list_servicechain_instances(self): pass def test_context_attributes(self): @@ -86,16 +147,15 @@ class NodeCompositionPluginTestCase( self.assertIsNotNone(ctx.session) self.assertIsNotNone(ctx.admin_context) self.assertIsNotNone(ctx.admin_session) - self.assertEqual(ctx.instance, instance) - self.assertEqual(ctx.provider, provider) - self.assertEqual(ctx.consumer, consumer) - self.assertEqual(ctx.management, management) - self.assertEqual(ctx.management, management) - self.assertEqual(ctx.relevant_specs, [spec]) del ctx.current_profile['nodes'] self.assertEqual(ctx.current_profile, profile) + self.assertEqual(instance['id'], ctx.instance['id']) + self.assertEqual(provider['id'], ctx.provider['id']) + self.assertEqual(consumer['id'], ctx.consumer['id']) + self.assertEqual(management['id'], ctx.management['id']) + self.assertEqual([spec['id']], [x['id'] for x in ctx.relevant_specs]) self.assertIsNone(ctx.original_node) - self.assertIsNone(ctx.service_targets) + self.assertEqual(0, len(ctx.get_service_targets())) def test_context_relevant_specs(self): plugin_context = n_context.get_admin_context() @@ -104,24 +164,15 @@ class NodeCompositionPluginTestCase( spec_used = self.create_servicechain_spec( nodes=[node_used['id']])['servicechain_spec'] - node_unused = self._create_profiled_servicechain_node( - service_type="TYPE", config='{}')['servicechain_node'] - spec_unused = self.create_servicechain_spec( - nodes=[node_unused['id']])['servicechain_spec'] - provider = self.create_policy_target_group()['policy_target_group'] instance = self.create_servicechain_instance( provider_ptg_id=provider['id'], - servicechain_specs=[spec_used['id'], - spec_unused['id']])['servicechain_instance'] - self.assertEqual(len(instance['servicechain_specs']), 2) + servicechain_specs=[spec_used['id']])['servicechain_instance'] ctx = ncp_context.get_node_driver_context( self.plugin, plugin_context, instance, node_used) - self.assertEqual(ctx.relevant_specs, [spec_used]) - - -class TestNcpNodeDriverManager(NodeCompositionPluginTestCase): + self.assertEqual([spec_used['id']], + [x['id'] for x in ctx.relevant_specs]) def test_manager_initialized(self): mgr = self.plugin.driver_manager @@ -129,3 +180,187 @@ class TestNcpNodeDriverManager(NodeCompositionPluginTestCase): dummy_driver.NoopNodeDriver) for driver in mgr.ordered_drivers: self.assertTrue(driver.obj.initialized) + + def test_spec_parameters(self): + """Test that config_param_names is empty when using NCP. + In NCP the config attribute of a node may be something different than + a HEAT template, therefore config_param_names is not used. + """ + + params_node_1 = ['p1', 'p2', 'p3'] + params_node_2 = ['p4', 'p5', 'p6'] + + def params_dict(params): + return jsonutils.dumps({'Parameters': + dict((x, {}) for x in params)}) + + prof = self.create_service_profile( + service_type='LOADBALANCER', shared=True, + tenant_id='admin')['service_profile'] + + # Create 2 nodes with different parameters + node1 = self.create_servicechain_node( + service_profile_id=prof['id'], shared=True, + config=params_dict(params_node_1), + expected_res_status=201)['servicechain_node'] + node2 = self.create_servicechain_node( + service_profile_id=prof['id'], shared=True, + config=params_dict(params_node_2), + expected_res_status=201)['servicechain_node'] + + # Create SC spec with the nodes assigned + spec = self.create_servicechain_spec( + nodes=[node1['id'], node2['id']], shared=True, + expected_res_status=201)['servicechain_spec'] + + # Verify param names is empty + self.assertIsNone(spec['config_param_names']) + + # Update the spec removing one node + self.update_servicechain_spec(spec['id'], nodes=[node1['id']], + expected_res_status=200) + + spec = self.show_servicechain_spec(spec['id'])['servicechain_spec'] + # Verify param names is empty + self.assertIsNone(spec['config_param_names']) + + def test_create_service_chain(self): + deploy = self.driver.create = mock.Mock() + destroy = self.driver.delete = mock.Mock() + + self._create_simple_service_chain(1) + self.assertEqual(1, deploy.call_count) + self.assertEqual(0, destroy.call_count) + + deploy.reset_mock() + + provider, _, _ = self._create_simple_service_chain(3) + self.assertEqual(3, deploy.call_count) + self.assertEqual(0, destroy.call_count) + + self.update_policy_target_group(provider['id'], + provided_policy_rule_sets={}) + self.assertEqual(3, deploy.call_count) + self.assertEqual(3, destroy.call_count) + + def test_create_service_chain_fails(self): + deploy = self.driver.create = mock.Mock() + destroy = self.driver.delete = mock.Mock() + + deploy.side_effect = Exception + + try: + self._create_simple_service_chain(3) + except Exception: + pass + + self.assertEqual(1, deploy.call_count) + self.assertEqual(3, destroy.call_count) + + def test_update_node_fails(self): + validate_update = self.driver.validate_update = mock.Mock() + validate_update.side_effect = exc.NodeCompositionPluginBadRequest( + resource='node', msg='reason') + + prof = self.create_service_profile( + service_type='LOADBALANCER')['service_profile'] + + node_id = self.create_servicechain_node( + service_profile_id=prof['id'], + expected_res_status=201)['servicechain_node']['id'] + + spec = self.create_servicechain_spec( + nodes=[node_id], + expected_res_status=201)['servicechain_spec'] + prs = self._create_redirect_prs(spec['id'])['policy_rule_set'] + self.create_policy_target_group( + provided_policy_rule_sets={prs['id']: ''}) + self.create_policy_target_group( + consumed_policy_rule_sets={prs['id']: ''}) + + res = self.update_servicechain_node(node_id, + description='somethingelse', + expected_res_status=400) + self.assertEqual('NodeCompositionPluginBadRequest', + res['NeutronError']['type']) + + def test_update_instantiated_profile_fails(self): + prof = self.create_service_profile( + service_type='LOADBALANCER')['service_profile'] + + node_id = self.create_servicechain_node( + service_profile_id=prof['id'], + expected_res_status=201)['servicechain_node']['id'] + + spec = self.create_servicechain_spec( + nodes=[node_id], + expected_res_status=201)['servicechain_spec'] + prs = self._create_redirect_prs(spec['id'])['policy_rule_set'] + self.create_policy_target_group( + provided_policy_rule_sets={prs['id']: ''}) + self.create_policy_target_group( + consumed_policy_rule_sets={prs['id']: ''}) + + res = self.update_service_profile(prof['id'], + vendor='somethingelse', + expected_res_status=400) + self.assertEqual('ServiceProfileInUseByAnInstance', + res['NeutronError']['type']) + + def test_second_driver_scheduled_if_first_fails(self): + self._add_node_driver('test') + drivers = [x.obj for x in + self.sc_plugin.driver_manager.ordered_drivers] + create_1 = drivers[0].validate_create = mock.Mock() + create_1.side_effect = n_exc.NeutronException() + + # This happens without error + profile = self.create_service_profile( + service_type="TYPE")['service_profile'] + node = self.create_servicechain_node( + service_profile_id=profile['id'], config='{}')['servicechain_node'] + spec = self.create_servicechain_spec( + nodes=[node['id']])['servicechain_spec'] + provider = self.create_policy_target_group()['policy_target_group'] + consumer = self.create_policy_target_group()['policy_target_group'] + self.create_servicechain_instance( + provider_ptg_id=provider['id'], consumer_ptg_id=consumer['id'], + servicechain_specs=[spec['id']], expected_res_status=201) + + def test_chain_fails_if_no_drivers_available(self): + self._add_node_driver('test') + drivers = [x.obj for x in + self.sc_plugin.driver_manager.ordered_drivers] + create_1 = drivers[0].validate_create = mock.Mock() + create_1.side_effect = n_exc.NeutronException() + create_2 = drivers[1].validate_create = mock.Mock() + create_2.side_effect = n_exc.NeutronException() + + profile = self.create_service_profile( + service_type="TYPE")['service_profile'] + node = self.create_servicechain_node( + service_profile_id=profile['id'], config='{}')['servicechain_node'] + spec = self.create_servicechain_spec( + nodes=[node['id']])['servicechain_spec'] + provider = self.create_policy_target_group()['policy_target_group'] + consumer = self.create_policy_target_group()['policy_target_group'] + self.create_servicechain_instance( + provider_ptg_id=provider['id'], consumer_ptg_id=consumer['id'], + servicechain_specs=[spec['id']], expected_res_status=400) + + def test_multiple_nodes_update(self): + update = self.driver.update = mock.Mock() + prof = self.create_service_profile( + service_type='LOADBALANCER')['service_profile'] + node = self.create_servicechain_node( + service_profile_id=prof['id'], config='{}')['servicechain_node'] + + self._create_chain_with_nodes([node['id']]) + self.update_servicechain_node(node['id'], name='somethingelse') + self.assertEqual(1, update.call_count) + + update.reset_mock() + self._create_chain_with_nodes([node['id']]) + self._create_chain_with_nodes([node['id']]) + self.update_servicechain_node(node['id'], name='somethingelse') + self.assertEqual(3, update.call_count) \ No newline at end of file diff --git a/gbpservice/neutron/tests/unit/services/servicechain/test_servicechain_plugin.py b/gbpservice/neutron/tests/unit/services/servicechain/test_servicechain_plugin.py index aa75d4b50..5e8093ffe 100644 --- a/gbpservice/neutron/tests/unit/services/servicechain/test_servicechain_plugin.py +++ b/gbpservice/neutron/tests/unit/services/servicechain/test_servicechain_plugin.py @@ -11,8 +11,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +import ast +import collections + from neutron import context as n_ctx from oslo_config import cfg +from oslo_serialization import jsonutils from gbpservice.neutron.services.servicechain.plugins.msc import context from gbpservice.neutron.tests.unit.db.grouppolicy import ( @@ -195,3 +199,47 @@ class TestGroupPolicyPluginGroupResources( self.assertIsNone(ctx.original_profile) self.assertEqual(ctx.current['id'], current['id']) self.assertIsNone(ctx.current_profile) + + def test_spec_parameters(self): + params_node_1 = ['p1', 'p2', 'p3'] + params_node_2 = ['p4', 'p5', 'p6'] + + def params_dict(params): + return jsonutils.dumps({'Parameters': + dict((x, {}) for x in params)}) + + prof = self.create_service_profile( + service_type='LOADBALANCER', shared=True, + tenant_id='admin')['service_profile'] + + # Create 2 nodes with different parameters + node1 = self.create_servicechain_node( + service_profile_id=prof['id'], shared=True, + config=params_dict(params_node_1), + expected_res_status=201)['servicechain_node'] + node2 = self.create_servicechain_node( + service_profile_id=prof['id'], shared=True, + config=params_dict(params_node_2), + expected_res_status=201)['servicechain_node'] + + # Create SC spec with the nodes assigned + spec = self.create_servicechain_spec( + nodes=[node1['id'], node2['id']], shared=True, + expected_res_status=201)['servicechain_spec'] + + # Verify param names correspondence + self.assertEqual( + collections.Counter(params_node_1 + params_node_2), + collections.Counter(ast.literal_eval(spec['config_param_names']))) + + # REVISIT(ivar): update verification fails because of bug/1460186 + + # Update the spec removing one node + #self.update_servicechain_spec(spec['id'], nodes=[node1['id']], + # expected_res_status=200) + + #spec = self.show_servicechain_spec(spec['id'])['servicechain_spec'] + # Verify param names correspondence + #self.assertEqual( + # collections.Counter(params_node_1), + # collections.Counter(ast.literal_eval(spec['config_param_names']))) diff --git a/setup.cfg b/setup.cfg index fbc14f133..998674698 100644 --- a/setup.cfg +++ b/setup.cfg @@ -59,6 +59,8 @@ gbpservice.neutron.servicechain.servicechain_drivers = oneconvergence_servicechain_driver = gbpservice.neutron.services.servicechain.plugins.msc.drivers.oneconvergence_servicechain_driver:OneconvergenceServiceChainDriver gbpservice.neutron.servicechain.ncp_drivers = node_dummy = gbpservice.neutron.services.servicechain.plugins.ncp.node_drivers.dummy_driver:NoopNodeDriver +gbpservice.neutron.servicechain.ncp_plumbers = + dummy_plumber = gbpservice.neutron.services.servicechain.plugins.ncp.node_plumbers.dummy_plumber:NoopPlumber [build_sphinx] source-dir = doc/source