Service Type Framework refactoring

implements blueprint service-type-framework-cleanup

* Defines logic and API for ServiceProvider - read-only entity
that admins provide in configuration and which is stored in memory
* ServiceType entity which maps to ServiceOfferings in new terms
is removed for now.
* Routed service insertion fixed to not to refer to service providers.
* In case configuration changes and some service providers are removed
then the resources must be cleanup in a special way (undeploy logical
resources). This is a matter of future work
* Add migration.

Change-Id: I400ad8f544ec8bdc7d2efb597c995f284ff05829
This commit is contained in:
Eugene Nikanorov 2013-07-07 10:50:56 +04:00
parent 450174b103
commit 1b36e20771
13 changed files with 619 additions and 819 deletions

View File

@ -290,14 +290,6 @@ notification_topics = notifications
# default driver to use for quota checks
# quota_driver = neutron.quota.ConfDriver
[default_servicetype]
# Description of the default service type (optional)
# description = "default service type"
# Enter a service definition line for each advanced service provided
# by the default service type.
# Each service definition should be in the following format:
# <service>:<plugin>[:driver]
[agent]
# Use "sudo neutron-rootwrap /etc/neutron/rootwrap.conf" to use the real
# root filter facility.
@ -365,3 +357,14 @@ signing_dir = $state_path/keystone-signing
# If set, use this value for pool_timeout with sqlalchemy
# pool_timeout = 10
[service_providers]
# Specify service providers (drivers) for advanced services like loadbalancer, VPN, Firewall.
# Must be in form:
# service_provider=<service_type>:<name>:<driver>[:default]
# List of allowed service type include LOADBALANCER, FIREWALL, VPN
# Combination of <service type> and <name> must be unique; <driver> must also be unique
# this is multiline option, example for default provider:
#service_provider=LOADBALANCER:name:lbaas_plugin_driver_path:default
# example of non-default provider:
#service_provider=FIREWALL:name2:firewall_driver_path

View File

@ -58,11 +58,6 @@
"create_router:external_gateway_info:enable_snat": "rule:admin_only",
"update_router:external_gateway_info:enable_snat": "rule:admin_only",
"create_service_type": "rule:admin_only",
"update_service_type": "rule:admin_only",
"delete_service_type": "rule:admin_only",
"get_service_type": "rule:regular_user",
"create_qos_queue": "rule:admin_only",
"get_qos_queue": "rule:admin_only",

View File

@ -0,0 +1,80 @@
# 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.
#
"""New service types framework (service providers)
Revision ID: 557edfc53098
Revises: 52c5e4a18807
Create Date: 2013-06-29 21:10:41.283358
"""
# revision identifiers, used by Alembic.
revision = '557edfc53098'
down_revision = '52c5e4a18807'
# Change to ['*'] if this migration applies to all plugins
migration_for_plugins = ['*']
from alembic import op
import sqlalchemy as sa
from neutron.db import migration
def upgrade(active_plugin=None, options=None):
if not migration.should_run(active_plugin, migration_for_plugins):
return
op.create_table(
'providerresourceassociations',
sa.Column('provider_name', sa.String(length=255), nullable=False),
sa.Column('resource_id', sa.String(length=36),
nullable=False, unique=True),
)
# dropping unused tables
op.drop_table('servicedefinitions')
op.drop_table('servicetypes')
def downgrade(active_plugin=None, options=None):
if not migration.should_run(active_plugin, migration_for_plugins):
return
op.create_table(
'servicetypes',
sa.Column('id', sa.String(length=36), nullable=False),
sa.Column('tenant_id', sa.String(length=255)),
sa.Column('name', sa.String(255)),
sa.Column('description', sa.String(255)),
sa.Column('default', sa.Boolean(), nullable=False, default=False),
sa.Column('num_instances', sa.Column(sa.Integer(), default=0)),
sa.PrimaryKeyConstraint('id')
)
op.create_table(
'servicedefinitions',
sa.Column('id', sa.String(length=36), nullable=False),
sa.Column('service_class', sa.String(255)),
sa.Column('plugin', sa.String(255)),
sa.Column('driver', sa.String(255)),
sa.Column('service_type_id', sa.String(36),
sa.ForeignKey('servicetypes.id',
ondelete='CASCADE')),
sa.PrimaryKeyConstraint('id', 'service_class')
)
op.drop_table('providerresourceassociations')

View File

@ -27,7 +27,6 @@ class RouterServiceTypeBinding(model_base.BASEV2):
sa.ForeignKey('routers.id', ondelete="CASCADE"),
primary_key=True)
service_type_id = sa.Column(sa.String(36),
sa.ForeignKey('servicetypes.id'),
nullable=False)

View File

@ -17,105 +17,27 @@
# @author: Salvatore Orlando, VMware
#
from oslo.config import cfg
import sqlalchemy as sa
from sqlalchemy import orm
from sqlalchemy.orm import exc as orm_exc
from sqlalchemy.sql import expression as expr
from neutron.common import exceptions as q_exc
from neutron import context
from neutron.db import api as db
from neutron.db import model_base
from neutron.db import models_v2
from neutron.openstack.common import log as logging
from neutron.services import provider_configuration as pconf
LOG = logging.getLogger(__name__)
DEFAULT_SVCTYPE_NAME = 'default'
default_servicetype_opts = [
cfg.StrOpt('description',
default='',
help=_('Textual description for the default service type')),
cfg.MultiStrOpt('service_definition',
help=_('Defines a provider for an advanced service '
'using the format: <service>:<plugin>[:<driver>]'))
]
cfg.CONF.register_opts(default_servicetype_opts, 'default_servicetype')
def parse_service_definition_opt():
"""Parse service definition opts and returns result."""
results = []
svc_def_opt = cfg.CONF.default_servicetype.service_definition
try:
for svc_def_str in svc_def_opt:
split = svc_def_str.split(':')
svc_def = {'service_class': split[0],
'plugin': split[1]}
try:
svc_def['driver'] = split[2]
except IndexError:
# Never mind, driver is optional
LOG.debug(_("Default service type - no driver for service "
"%(service_class)s and plugin %(plugin)s"),
svc_def)
results.append(svc_def)
return results
except (TypeError, IndexError):
raise q_exc.InvalidConfigurationOption(opt_name='service_definition',
opt_value=svc_def_opt)
class NoDefaultServiceDefinition(q_exc.NeutronException):
message = _("No default service definition in configuration file. "
"Please add service definitions using the service_definition "
"variable in the [default_servicetype] section")
class ServiceTypeNotFound(q_exc.NotFound):
message = _("Service type %(service_type_id)s could not be found ")
class ServiceTypeInUse(q_exc.InUse):
message = _("There are still active instances of service type "
"'%(service_type_id)s'. Therefore it cannot be removed.")
class ServiceDefinition(model_base.BASEV2, models_v2.HasId):
service_class = sa.Column(sa.String(255), primary_key=True)
plugin = sa.Column(sa.String(255))
driver = sa.Column(sa.String(255))
service_type_id = sa.Column(sa.String(36),
sa.ForeignKey('servicetypes.id',
ondelete='CASCADE'),
primary_key=True)
class ServiceType(model_base.BASEV2, models_v2.HasId, models_v2.HasTenant):
"""Service Type Object Model."""
name = sa.Column(sa.String(255))
description = sa.Column(sa.String(255))
default = sa.Column(sa.Boolean(), nullable=False, default=False)
service_definitions = orm.relationship(ServiceDefinition,
backref='servicetypes',
lazy='joined',
cascade='all')
# Keep track of number of instances for this service type
num_instances = sa.Column(sa.Integer(), default=0)
def as_dict(self):
"""Convert a row into a dict."""
ret_dict = {}
for c in self.__table__.columns:
ret_dict[c.name] = getattr(self, c.name)
return ret_dict
class ProviderResourceAssociation(model_base.BASEV2):
provider_name = sa.Column(sa.String(255),
nullable=False, primary_key=True)
# should be manualy deleted on resource deletion
resource_id = sa.Column(sa.String(36), nullable=False, primary_key=True,
unique=True)
class ServiceTypeManager(object):
"""Manage service type objects in Neutron database."""
"""Manage service type objects in Neutron."""
_instance = None
@ -127,189 +49,42 @@ class ServiceTypeManager(object):
def __init__(self):
self._initialize_db()
ctx = context.get_admin_context()
# Init default service type from configuration file
svc_defs = cfg.CONF.default_servicetype.service_definition
if not svc_defs:
raise NoDefaultServiceDefinition()
def_service_type = {'name': DEFAULT_SVCTYPE_NAME,
'description':
cfg.CONF.default_servicetype.description,
'service_definitions':
parse_service_definition_opt(),
'default': True}
# Create or update record in database
def_svc_type_db = self._get_default_service_type(ctx)
if not def_svc_type_db:
def_svc_type_db = self._create_service_type(ctx, def_service_type)
else:
self._update_service_type(ctx,
def_svc_type_db['id'],
def_service_type,
svc_type_db=def_svc_type_db)
LOG.debug(_("Default service type record updated in Neutron database. "
"identifier is '%s'"), def_svc_type_db['id'])
self._load_conf()
def _initialize_db(self):
db.configure_db()
# Register models for service type management
# Note this might have been already done if configure_db also
# created the engine
db.register_models(models_v2.model_base.BASEV2)
def _create_service_type(self, context, service_type):
svc_defs = service_type.pop('service_definitions')
def _load_conf(self):
self.conf = pconf.ProviderConfiguration(
pconf.parse_service_provider_opt())
def get_service_providers(self, context, filters=None, fields=None):
return self.conf.get_service_providers(filters, fields)
def get_default_service_provider(self, context, service_type):
"""Return the default provider for a given service type."""
filters = {'service_type': [service_type],
'default': [True]}
providers = self.get_service_providers(context, filters=filters)
# By construction we expect at most a single item in provider
if not providers:
raise pconf.DefaultServiceProviderNotFound(
service_type=service_type
)
return providers[0]
def add_resource_association(self, context, service_type, provider_name,
resource_id):
r = self.conf.get_service_providers(
filters={'service_type': service_type, 'name': provider_name})
if not r:
raise pconf.ServiceProviderNotFound(service_type=service_type)
with context.session.begin(subtransactions=True):
svc_type_db = ServiceType(**service_type)
# and now insert provided service type definitions
for svc_def in svc_defs:
svc_type_db.service_definitions.append(
ServiceDefinition(**svc_def))
# sqlalchemy save-update on relationship is on by
# default, the following will save both the service
# type and its service definitions
context.session.add(svc_type_db)
return svc_type_db
def _update_service_type(self, context, id, service_type,
svc_type_db=None):
with context.session.begin(subtransactions=True):
if not svc_type_db:
svc_type_db = self._get_service_type(context, id)
try:
svc_defs_map = dict([(svc_def['service'], svc_def)
for svc_def in
service_type.pop('service_definitions')])
except KeyError:
# No service defs in request
svc_defs_map = {}
svc_type_db.update(service_type)
for svc_def_db in svc_type_db.service_definitions:
try:
svc_def_db.update(svc_defs_map.pop(
svc_def_db['service_class']))
except KeyError:
# too bad, the service def was not there
# then we should delete it.
context.session.delete(svc_def_db)
# Add remaining service definitions
for svc_def in svc_defs_map:
context.session.add(ServiceDefinition(**svc_def))
return svc_type_db
def _get_service_type(self, context, svc_type_id):
try:
query = context.session.query(ServiceType)
return query.filter(ServiceType.id == svc_type_id).one()
# filter is on primary key, do not catch MultipleResultsFound
except orm_exc.NoResultFound:
raise ServiceTypeNotFound(service_type_id=svc_type_id)
def _get_default_service_type(self, context):
try:
query = context.session.query(ServiceType)
return query.filter(ServiceType.default == expr.true()).one()
except orm_exc.NoResultFound:
return
except orm_exc.MultipleResultsFound:
# This should never happen. If it does, take the first instance
query2 = context.session.query(ServiceType)
results = query2.filter(ServiceType.default == expr.true()).all()
LOG.warning(_("Multiple default service type instances found."
"Will use instance '%s'"), results[0]['id'])
return results[0]
def _make_svc_type_dict(self, context, svc_type, fields=None):
def _make_svc_def_dict(svc_def_db):
svc_def = {'service_class': svc_def_db['service_class']}
svc_def.update({'plugin': svc_def_db['plugin'],
'driver': svc_def_db['driver']})
return svc_def
res = {'id': svc_type['id'],
'name': svc_type['name'],
'default': svc_type['default'],
'num_instances': svc_type['num_instances'],
'service_definitions':
[_make_svc_def_dict(svc_def) for svc_def
in svc_type['service_definitions']]}
# Field selection
if fields:
return dict(((k, v) for k, v in res.iteritems()
if k in fields))
return res
def get_service_type(self, context, id, fields=None):
"""Retrieve a service type record."""
return self._make_svc_type_dict(context,
self._get_service_type(context, id),
fields)
def get_service_types(self, context, fields=None, filters=None):
"""Retrieve a possibly filtered list of service types."""
query = context.session.query(ServiceType)
if filters:
for key, value in filters.iteritems():
column = getattr(ServiceType, key, None)
if column:
query = query.filter(column.in_(value))
return [self._make_svc_type_dict(context, svc_type, fields)
for svc_type in query]
def create_service_type(self, context, service_type):
"""Create a new service type."""
svc_type_data = service_type['service_type']
svc_type_db = self._create_service_type(context, svc_type_data)
LOG.debug(_("Created service type object:%s"), svc_type_db['id'])
return self._make_svc_type_dict(context, svc_type_db)
def update_service_type(self, context, id, service_type):
"""Update a service type."""
svc_type_data = service_type['service_type']
svc_type_db = self._update_service_type(context, id,
svc_type_data)
return self._make_svc_type_dict(context, svc_type_db)
def delete_service_type(self, context, id):
"""Delete a service type."""
# Verify that the service type is not in use.
svc_type_db = self._get_service_type(context, id)
if svc_type_db['num_instances'] > 0:
raise ServiceTypeInUse(service_type_id=svc_type_db['id'])
with context.session.begin(subtransactions=True):
context.session.delete(svc_type_db)
def increase_service_type_refcount(self, context, id):
"""Increase references count for a service type object
This method should be invoked by plugins using the service
type concept everytime an instance of an object associated
with a given service type is created.
"""
#TODO(salvatore-orlando): Devise a better solution than this
#refcount mechanisms. Perhaps adding hooks into models which
#use service types in order to enforce ref. integrity and cascade
with context.session.begin(subtransactions=True):
svc_type_db = self._get_service_type(context, id)
svc_type_db['num_instances'] = svc_type_db['num_instances'] + 1
return svc_type_db['num_instances']
def decrease_service_type_refcount(self, context, id):
"""Decrease references count for a service type object
This method should be invoked by plugins using the service
type concept everytime an instance of an object associated
with a given service type is removed
"""
#TODO(salvatore-orlando): Devise a better solution than this
#refcount mechanisms. Perhaps adding hooks into models which
#use service types in order to enforce ref. integrity and cascade
with context.session.begin(subtransactions=True):
svc_type_db = self._get_service_type(context, id)
if svc_type_db['num_instances'] == 0:
LOG.warning(_("Number of instances for service type "
"'%s' is already 0."), svc_type_db['name'])
return
svc_type_db['num_instances'] = svc_type_db['num_instances'] - 1
return svc_type_db['num_instances']
# we don't actually need service type for association.
# resource_id is unique and belongs to specific service
# which knows its type
assoc = ProviderResourceAssociation(provider_name=provider_name,
resource_id=resource_id)
context.session.add(assoc)

View File

@ -21,149 +21,32 @@
from neutron.api import extensions
from neutron.api.v2 import attributes
from neutron.api.v2 import base
from neutron import context
from neutron.db import servicetype_db
from neutron import manager
from neutron.openstack.common import log as logging
from neutron.plugins.common import constants
LOG = logging.getLogger(__name__)
RESOURCE_NAME = "service_type"
RESOURCE_NAME = "service_provider"
COLLECTION_NAME = "%ss" % RESOURCE_NAME
SERVICE_ATTR = 'service_class'
SERVICE_ATTR = 'service_type'
PLUGIN_ATTR = 'plugin'
DRIVER_ATTR = 'driver'
EXT_ALIAS = 'service-type'
# Attribute Map for Service Type Resource
# Attribute Map for Service Provider Resource
# Allow read-only access
RESOURCE_ATTRIBUTE_MAP = {
COLLECTION_NAME: {
'id': {'allow_post': False, 'allow_put': False,
'is_visible': True},
'name': {'allow_post': True, 'allow_put': True,
'validate': {'type:string': None},
'is_visible': True, 'default': ''},
'service_type': {'allow_post': False, 'allow_put': False,
'is_visible': True},
'name': {'allow_post': False, 'allow_put': False,
'is_visible': True},
'default': {'allow_post': False, 'allow_put': False,
'is_visible': True},
#TODO(salvatore-orlando): Service types should not have ownership
'tenant_id': {'allow_post': True, 'allow_put': False,
'required_by_policy': True,
'is_visible': True},
'num_instances': {'allow_post': False, 'allow_put': False,
'is_visible': True},
'service_definitions': {'allow_post': True, 'allow_put': True,
'is_visible': True, 'default': None,
'validate': {'type:service_definitions':
None}}
}
}
def set_default_svctype_id(original_id):
if not original_id:
svctype_mgr = servicetype_db.ServiceTypeManager.get_instance()
# Fetch default service type - it must exist
res = svctype_mgr.get_service_types(context.get_admin_context(),
filters={'default': [True]})
return res[0]['id']
return original_id
def _validate_servicetype_ref(data, valid_values=None):
"""Verify the service type id exists."""
svc_type_id = data
svctype_mgr = servicetype_db.ServiceTypeManager.get_instance()
try:
svctype_mgr.get_service_type(context.get_admin_context(),
svc_type_id)
except servicetype_db.ServiceTypeNotFound:
return _("The service type '%s' does not exist") % svc_type_id
def _validate_service_defs(data, valid_values=None):
"""Validate the list of service definitions."""
try:
if not data:
return _("No service type definition was provided. At least a "
"service type definition must be provided")
f_name = _validate_service_defs.__name__
for svc_def in data:
try:
# Do a copy of the original object so we can easily
# pop out stuff from it
svc_def_copy = svc_def.copy()
try:
svc_name = svc_def_copy.pop(SERVICE_ATTR)
plugin_name = svc_def_copy.pop(PLUGIN_ATTR)
except KeyError:
msg = (_("Required attributes missing in service "
"definition: %s") % svc_def)
LOG.error(_("%(f_name)s: %(msg)s"),
{'f_name': f_name, 'msg': msg})
return msg
# Validate 'service' attribute
if svc_name not in constants.ALLOWED_SERVICES:
msg = (_("Service name '%s' unspecified "
"or invalid") % svc_name)
LOG.error(_("%(f_name)s: %(msg)s"),
{'f_name': f_name, 'msg': msg})
return msg
# Validate 'plugin' attribute
if not plugin_name:
msg = (_("Plugin name not specified in "
"service definition %s") % svc_def)
LOG.error(_("%(f_name)s: %(msg)s"),
{'f_name': f_name, 'msg': msg})
return msg
# TODO(salvatore-orlando): This code will need to change when
# multiple plugins for each adv service will be supported
svc_plugin = manager.NeutronManager.get_service_plugins().get(
svc_name)
if not svc_plugin:
msg = _("No plugin for service '%s'") % svc_name
LOG.error(_("%(f_name)s: %(msg)s"),
{'f_name': f_name, 'msg': msg})
return msg
if svc_plugin.get_plugin_name() != plugin_name:
msg = _("Plugin name '%s' is not correct ") % plugin_name
LOG.error(_("%(f_name)s: %(msg)s"),
{'f_name': f_name, 'msg': msg})
return msg
# Validate 'driver' attribute (just check it's a string)
# FIXME(salvatore-orlando): This should be a list
# Note: using get() instead of pop() as pop raises if the
# key is not found, which might happen for the driver
driver = svc_def_copy.get(DRIVER_ATTR)
if driver:
msg = attributes._validate_string(driver,)
if msg:
return msg
del svc_def_copy[DRIVER_ATTR]
# Anything left - it should be an error
if svc_def_copy:
msg = (_("Unparseable attributes found in "
"service definition %s") % svc_def)
LOG.error(_("%(f_name)s: %(msg)s"),
{'f_name': f_name, 'msg': msg})
return msg
except TypeError:
LOG.exception(_("Exception while parsing service "
"definition:%s"), svc_def)
msg = (_("Was expecting a dict for service definition, found "
"the following: %s") % svc_def)
LOG.error(_("%(f_name)s: %(msg)s"),
{'f_name': f_name, 'msg': msg})
return msg
except TypeError:
return (_("%s: provided data are not iterable") %
_validate_service_defs.__name__)
attributes.validators['type:service_definitions'] = _validate_service_defs
attributes.validators['type:servicetype_ref'] = _validate_servicetype_ref
class Servicetype(extensions.ExtensionDescriptor):
@classmethod
@ -176,7 +59,7 @@ class Servicetype(extensions.ExtensionDescriptor):
@classmethod
def get_description(cls):
return _("API for retrieving and managing service types for "
return _("API for retrieving service providers for "
"Neutron advanced services")
@classmethod
@ -191,7 +74,6 @@ class Servicetype(extensions.ExtensionDescriptor):
def get_resources(cls):
"""Returns Extended Resource for service type management."""
my_plurals = [(key, key[:-1]) for key in RESOURCE_ATTRIBUTE_MAP.keys()]
my_plurals.append(('service_definitions', 'service_definition'))
attributes.PLURALS.update(dict(my_plurals))
attr_map = RESOURCE_ATTRIBUTE_MAP[COLLECTION_NAME]
collection_name = COLLECTION_NAME.replace('_', '-')
@ -206,6 +88,6 @@ class Servicetype(extensions.ExtensionDescriptor):
def get_extended_resources(self, version):
if version == "2.0":
return dict(RESOURCE_ATTRIBUTE_MAP.items())
return RESOURCE_ATTRIBUTE_MAP
else:
return {}

View File

@ -0,0 +1,157 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 OpenStack Foundation.
# 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 oslo.config import cfg
from neutron.common import exceptions as n_exc
from neutron.openstack.common import log as logging
from neutron.plugins.common import constants
LOG = logging.getLogger(__name__)
serviceprovider_opts = [
cfg.MultiStrOpt('service_provider', default=[],
help=_('Defines providers for advanced services '
'using the format: '
'<service_type>:<name>:<driver>[:default]'))
]
cfg.CONF.register_opts(serviceprovider_opts, 'service_providers')
#global scope function that should be used in service APIs
def normalize_provider_name(name):
return name.lower()
def parse_service_provider_opt():
"""Parse service definition opts and returns result."""
def validate_name(name):
if len(name) > 255:
raise n_exc.Invalid("Provider name is limited by 255 characters:"
" %s" % name)
svc_providers_opt = cfg.CONF.service_providers.service_provider
res = []
for prov_def in svc_providers_opt:
split = prov_def.split(':')
try:
svc_type, name, driver = split[:3]
except ValueError:
raise n_exc.Invalid(_("Invalid service provider format"))
validate_name(name)
name = normalize_provider_name(name)
default = False
if len(split) == 4 and split[3]:
if split[3] == 'default':
default = True
else:
msg = (_("Invalid provider format. "
"Last part should be 'default' or empty: %s") %
prov_def)
LOG.error(msg)
raise n_exc.Invalid(msg)
if svc_type not in constants.ALLOWED_SERVICES:
msg = (_("Service type '%(svc_type)s' is not allowed, "
"allowed types: %(allowed)s") %
{'svc_type': svc_type,
'allowed': constants.ALLOWED_SERVICES})
LOG.error(msg)
raise n_exc.Invalid(msg)
res.append({'service_type': svc_type,
'name': name,
'driver': driver,
'default': default})
return res
class ServiceProviderNotFound(n_exc.NotFound):
message = _("Service provider could not be found "
"for service type %(service_type)s")
class DefaultServiceProviderNotFound(ServiceProviderNotFound):
message = _("Service type %(service_type)s does not have a default "
"service provider")
class ProviderConfiguration(object):
def __init__(self, prov_data):
self.providers = {}
for prov in prov_data:
self.add_provider(prov)
def _ensure_driver_unique(self, driver):
for k, v in self.providers.items():
if v['driver'] == driver:
msg = (_("Driver %s is not unique across providers") %
driver)
LOG.exception(msg)
raise n_exc.Invalid(msg)
def _ensure_default_unique(self, type, default):
if not default:
return
for k, v in self.providers.items():
if k[0] == type and v['default']:
msg = _("Multiple default providers "
"for service %s") % type
LOG.exception(msg)
raise n_exc.Invalid(msg)
def add_provider(self, provider):
self._ensure_driver_unique(provider['driver'])
self._ensure_default_unique(provider['service_type'],
provider['default'])
provider_type = (provider['service_type'], provider['name'])
if provider_type in self.providers:
msg = (_("Multiple providers specified for service "
"%s") % provider['service_type'])
LOG.exception(msg)
raise n_exc.Invalid(msg)
self.providers[provider_type] = {'driver': provider['driver'],
'default': provider['default']}
def _check_entry(self, k, v, filters):
# small helper to deal with query filters
if not filters:
return True
for index, key in enumerate(['service_type', 'name']):
if key in filters:
if k[index] not in filters[key]:
return False
for key in ['driver', 'default']:
if key in filters:
if v[key] not in filters[key]:
return False
return True
def _fields(self, resource, fields):
if fields:
return dict(((key, item) for key, item in resource.items()
if key in fields))
return resource
def get_service_providers(self, filters=None, fields=None):
res = [{'service_type': k[0],
'name': k[1],
'driver': v['driver'],
'default': v['default']}
for k, v in self.providers.items()
if self._check_entry(k, v, filters)]
return self._fields(res, fields)

View File

@ -25,7 +25,3 @@ lock_path = $state_path/lock
[database]
connection = 'sqlite://'
[default_servicetype]
description = "default service type"
service_definition=dummy:neutron.tests.unit.dummy_plugin.NeutronDummyPlugin

View File

@ -45,7 +45,6 @@ RESOURCE_ATTRIBUTE_MAP = {
'service_type': {'allow_post': True,
'allow_put': False,
'validate': {'type:servicetype_ref': None},
'convert_to': servicetype.set_default_svctype_id,
'is_visible': True,
'default': None}
}

View File

@ -24,8 +24,3 @@ lock_path = $state_path/lock
[database]
connection = 'sqlite://'
[default_servicetype]
description = "default service type"
service_definition=dummy:neutron.tests.unit.dummy_plugin.NeutronDummyPlugin

View File

@ -0,0 +1,183 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 VMware, Inc. 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 oslo.config import cfg
from neutron.common import exceptions as q_exc
from neutron.plugins.common import constants
from neutron.services import provider_configuration as provconf
from neutron.tests import base
class ParseServiceProviderConfigurationTestCase(base.BaseTestCase):
def test_default_service_provider_configuration(self):
providers = cfg.CONF.service_providers.service_provider
self.assertEqual(providers, [])
def test_parse_single_service_provider_opt(self):
cfg.CONF.set_override('service_provider',
[constants.LOADBALANCER +
':lbaas:driver_path'],
'service_providers')
expected = {'service_type': constants.LOADBALANCER,
'name': 'lbaas',
'driver': 'driver_path',
'default': False}
res = provconf.parse_service_provider_opt()
self.assertEqual(len(res), 1)
self.assertEqual(res, [expected])
def test_parse_single_default_service_provider_opt(self):
cfg.CONF.set_override('service_provider',
[constants.LOADBALANCER +
':lbaas:driver_path:default'],
'service_providers')
expected = {'service_type': constants.LOADBALANCER,
'name': 'lbaas',
'driver': 'driver_path',
'default': True}
res = provconf.parse_service_provider_opt()
self.assertEqual(len(res), 1)
self.assertEqual(res, [expected])
def test_parse_multi_service_provider_opt(self):
cfg.CONF.set_override('service_provider',
[constants.LOADBALANCER +
':lbaas:driver_path',
constants.LOADBALANCER + ':name1:path1',
constants.LOADBALANCER +
':name2:path2:default'],
'service_providers')
expected = {'service_type': constants.LOADBALANCER,
'name': 'lbaas',
'driver': 'driver_path',
'default': False}
res = provconf.parse_service_provider_opt()
self.assertEqual(len(res), 3)
self.assertEqual(res, [expected,
{'service_type': constants.LOADBALANCER,
'name': 'name1',
'driver': 'path1',
'default': False},
{'service_type': constants.LOADBALANCER,
'name': 'name2',
'driver': 'path2',
'default': True}])
def test_parse_service_provider_opt_not_allowed_raises(self):
cfg.CONF.set_override('service_provider',
[constants.LOADBALANCER +
':lbaas:driver_path',
'svc_type:name1:path1'],
'service_providers')
self.assertRaises(q_exc.Invalid, provconf.parse_service_provider_opt)
def test_parse_service_provider_invalid_format(self):
cfg.CONF.set_override('service_provider',
[constants.LOADBALANCER +
':lbaas:driver_path',
'svc_type:name1:path1:def'],
'service_providers')
self.assertRaises(q_exc.Invalid, provconf.parse_service_provider_opt)
cfg.CONF.set_override('service_provider',
[constants.LOADBALANCER +
':',
'svc_type:name1:path1:def'],
'service_providers')
self.assertRaises(q_exc.Invalid, provconf.parse_service_provider_opt)
def test_parse_service_provider_name_too_long(self):
name = 'a' * 256
cfg.CONF.set_override('service_provider',
[constants.LOADBALANCER +
':' + name + ':driver_path',
'svc_type:name1:path1:def'],
'service_providers')
self.assertRaises(q_exc.Invalid, provconf.parse_service_provider_opt)
class ProviderConfigurationTestCase(base.BaseTestCase):
def setUp(self):
super(ProviderConfigurationTestCase, self).setUp()
def test_ensure_driver_unique(self):
pconf = provconf.ProviderConfiguration([])
pconf.providers[('svctype', 'name')] = {'driver': 'driver',
'default': True}
self.assertRaises(q_exc.Invalid,
pconf._ensure_driver_unique, 'driver')
self.assertIsNone(pconf._ensure_driver_unique('another_driver1'))
def test_ensure_default_unique(self):
pconf = provconf.ProviderConfiguration([])
pconf.providers[('svctype', 'name')] = {'driver': 'driver',
'default': True}
self.assertRaises(q_exc.Invalid,
pconf._ensure_default_unique,
'svctype', True)
self.assertIsNone(pconf._ensure_default_unique('svctype', False))
self.assertIsNone(pconf._ensure_default_unique('svctype1', True))
self.assertIsNone(pconf._ensure_default_unique('svctype1', False))
def test_add_provider(self):
pconf = provconf.ProviderConfiguration([])
prov = {'service_type': constants.LOADBALANCER,
'name': 'name',
'driver': 'path',
'default': False}
pconf.add_provider(prov)
self.assertEqual(len(pconf.providers), 1)
self.assertEqual(pconf.providers.keys(),
[(constants.LOADBALANCER, 'name')])
self.assertEqual(pconf.providers.values(),
[{'driver': 'path', 'default': False}])
def test_add_duplicate_provider(self):
pconf = provconf.ProviderConfiguration([])
prov = {'service_type': constants.LOADBALANCER,
'name': 'name',
'driver': 'path',
'default': False}
pconf.add_provider(prov)
self.assertRaises(q_exc.Invalid, pconf.add_provider, prov)
self.assertEqual(len(pconf.providers), 1)
def test_get_service_providers(self):
provs = [{'service_type': constants.LOADBALANCER,
'name': 'name',
'driver': 'path',
'default': False},
{'service_type': constants.LOADBALANCER,
'name': 'name2',
'driver': 'path2',
'default': False},
{'service_type': 'st2',
'name': 'name',
'driver': 'driver',
'default': True
},
{'service_type': 'st3',
'name': 'name2',
'driver': 'driver2',
'default': True}]
pconf = provconf.ProviderConfiguration(provs)
for prov in provs:
p = pconf.get_service_providers(
filters={'name': [prov['name']],
'service_type': prov['service_type']}
)
self.assertEqual(p, [prov])

View File

@ -189,8 +189,7 @@ class RouterServiceInsertionTestCase(base.BaseTestCase):
self._tenant_id = "8c70909f-b081-452d-872b-df48e6c355d1"
res = self._do_request('GET', _get_path('service-types'))
self._service_type_id = res['service_types'][0]['id']
self._service_type_id = _uuid()
self._setup_core_resources()

View File

@ -17,25 +17,22 @@
# @author: Salvatore Orlando, VMware
#
import contextlib
import logging
import os
import tempfile
import fixtures
import mock
from oslo.config import cfg
import webob.exc as webexc
import webtest
from neutron.api import extensions
from neutron.api.v2 import attributes
from neutron.common import exceptions as q_exc
from neutron import context
from neutron.db import api as db_api
from neutron.db import servicetype_db
from neutron.db import servicetype_db as st_db
from neutron.extensions import servicetype
from neutron import manager
from neutron.plugins.common import constants
from neutron.services import provider_configuration as provconf
from neutron.tests import base
from neutron.tests.unit import dummy_plugin as dp
from neutron.tests.unit import test_api_v2
@ -52,17 +49,116 @@ _uuid = test_api_v2._uuid
_get_path = test_api_v2._get_path
class ServiceTypeManagerTestCase(base.BaseTestCase):
def setUp(self):
super(ServiceTypeManagerTestCase, self).setUp()
st_db.ServiceTypeManager._instance = None
self.manager = st_db.ServiceTypeManager.get_instance()
self.ctx = context.get_admin_context()
def test_service_provider_driver_not_unique(self):
cfg.CONF.set_override('service_provider',
[constants.LOADBALANCER +
':lbaas:driver'],
'service_providers')
prov = {'service_type': constants.LOADBALANCER,
'name': 'name2',
'driver': 'driver',
'default': False}
self.manager._load_conf()
self.assertRaises(
q_exc.Invalid, self.manager.conf.add_provider, prov)
def test_get_service_providers(self):
cfg.CONF.set_override('service_provider',
[constants.LOADBALANCER +
':lbaas:driver_path',
constants.DUMMY + ':dummy:dummy_dr'],
'service_providers')
ctx = context.get_admin_context()
provconf.parse_service_provider_opt()
self.manager._load_conf()
res = self.manager.get_service_providers(ctx)
self.assertEqual(len(res), 2)
res = self.manager.get_service_providers(
ctx,
filters=dict(service_type=[constants.DUMMY])
)
self.assertEqual(len(res), 1)
res = self.manager.get_service_providers(
ctx,
filters=dict(service_type=[constants.LOADBALANCER])
)
self.assertEqual(len(res), 1)
def test_multiple_default_providers_specified_for_service(self):
cfg.CONF.set_override('service_provider',
[constants.LOADBALANCER +
':lbaas1:driver_path:default',
constants.LOADBALANCER +
':lbaas2:driver_path:default'],
'service_providers')
self.assertRaises(q_exc.Invalid, self.manager._load_conf)
def test_get_default_provider(self):
cfg.CONF.set_override('service_provider',
[constants.LOADBALANCER +
':lbaas1:driver_path:default',
constants.DUMMY +
':lbaas2:driver_path2'],
'service_providers')
self.manager._load_conf()
# can pass None as a context
p = self.manager.get_default_service_provider(None,
constants.LOADBALANCER)
self.assertEqual(p, {'service_type': constants.LOADBALANCER,
'name': 'lbaas1',
'driver': 'driver_path',
'default': True})
self.assertRaises(
provconf.DefaultServiceProviderNotFound,
self.manager.get_default_service_provider,
None, constants.DUMMY
)
def test_add_resource_association(self):
cfg.CONF.set_override('service_provider',
[constants.LOADBALANCER +
':lbaas1:driver_path:default',
constants.DUMMY +
':lbaas2:driver_path2'],
'service_providers')
self.manager._load_conf()
ctx = context.get_admin_context()
self.manager.add_resource_association(ctx,
constants.LOADBALANCER,
'lbaas1', '123-123')
self.assertEqual(ctx.session.
query(st_db.ProviderResourceAssociation).count(),
1)
assoc = ctx.session.query(st_db.ProviderResourceAssociation).one()
ctx.session.delete(assoc)
def test_invalid_resource_association(self):
cfg.CONF.set_override('service_provider',
[constants.LOADBALANCER +
':lbaas1:driver_path:default',
constants.DUMMY +
':lbaas2:driver_path2'],
'service_providers')
self.manager._load_conf()
ctx = context.get_admin_context()
self.assertRaises(provconf.ServiceProviderNotFound,
self.manager.add_resource_association,
ctx, 'BLABLA_svc', 'name', '123-123')
class TestServiceTypeExtensionManager(object):
"""Mock extensions manager."""
def get_resources(self):
# 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(
servicetype.RESOURCE_ATTRIBUTE_MAP)
attributes.RESOURCE_ATTRIBUTE_MAP.update(dp.RESOURCE_ATTRIBUTE_MAP)
return (servicetype.Servicetype.get_resources() +
dp.Dummy.get_resources())
@ -73,13 +169,14 @@ class TestServiceTypeExtensionManager(object):
return []
class ServiceTypeTestCaseBase(testlib_api.WebTestCase):
class ServiceTypeExtensionTestCaseBase(testlib_api.WebTestCase):
fmt = 'json'
def setUp(self):
# This is needed because otherwise a failure will occur due to
# nonexisting core_plugin
cfg.CONF.set_override('core_plugin', test_db_plugin.DB_PLUGIN_KLASS)
cfg.CONF.set_override('service_plugins',
["%s.%s" % (dp.__name__,
dp.DummyServicePlugin.__name__)])
@ -92,418 +189,58 @@ class ServiceTypeTestCaseBase(testlib_api.WebTestCase):
self.ext_mdw = test_extensions.setup_extensions_middleware(ext_mgr)
self.api = webtest.TestApp(self.ext_mdw)
self.resource_name = servicetype.RESOURCE_NAME.replace('-', '_')
super(ServiceTypeTestCaseBase, self).setUp()
super(ServiceTypeExtensionTestCaseBase, self).setUp()
class ServiceTypeExtensionTestCase(ServiceTypeTestCaseBase):
class ServiceTypeExtensionTestCase(ServiceTypeExtensionTestCaseBase):
def setUp(self):
self._patcher = mock.patch(
"%s.%s" % (servicetype_db.__name__,
servicetype_db.ServiceTypeManager.__name__),
"neutron.db.servicetype_db.ServiceTypeManager",
autospec=True)
self.addCleanup(self._patcher.stop)
self.mock_mgr = self._patcher.start()
self.mock_mgr.get_instance.return_value = self.mock_mgr.return_value
super(ServiceTypeExtensionTestCase, self).setUp()
def _test_service_type_create(self, env=None,
expected_status=webexc.HTTPCreated.code):
tenant_id = 'fake'
if env and 'neutron.context' in env:
tenant_id = env['neutron.context'].tenant_id
data = {self.resource_name:
{'name': 'test',
'tenant_id': tenant_id,
'service_definitions':
[{'service_class': constants.DUMMY,
'plugin': dp.DUMMY_PLUGIN_NAME}]}}
return_value = data[self.resource_name].copy()
svc_type_id = _uuid()
return_value['id'] = svc_type_id
def test_service_provider_list(self):
instance = self.mock_mgr.return_value
instance.create_service_type.return_value = return_value
expect_errors = expected_status >= webexc.HTTPBadRequest.code
res = self.api.post(_get_path('service-types', fmt=self.fmt),
self.serialize(data),
extra_environ=env,
expect_errors=expect_errors,
content_type='application/%s' % self.fmt)
self.assertEqual(res.status_int, expected_status)
if not expect_errors:
instance.create_service_type.assert_called_with(mock.ANY,
service_type=data)
res = self.deserialize(res)
self.assertTrue(self.resource_name in res)
svc_type = res[self.resource_name]
self.assertEqual(svc_type['id'], svc_type_id)
# NOTE(salvatore-orlando): The following two checks are
# probably not essential
self.assertEqual(svc_type['service_definitions'],
data[self.resource_name]['service_definitions'])
def _test_service_type_update(self, env=None,
expected_status=webexc.HTTPOk.code):
svc_type_name = 'updated'
data = {self.resource_name: {'name': svc_type_name}}
res = self.api.get(_get_path('service-providers', fmt=self.fmt))
svc_type_id = _uuid()
return_value = {'id': svc_type_id,
'name': svc_type_name}
instance = self.mock_mgr.return_value
expect_errors = expected_status >= webexc.HTTPBadRequest.code
instance.update_service_type.return_value = return_value
res = self.api.put(_get_path('service-types/%s' % svc_type_id,
fmt=self.fmt),
self.serialize(data))
if not expect_errors:
instance.update_service_type.assert_called_with(mock.ANY,
svc_type_id,
service_type=data)
self.assertEqual(res.status_int, webexc.HTTPOk.code)
res = self.deserialize(res)
self.assertTrue(self.resource_name in res)
svc_type = res[self.resource_name]
self.assertEqual(svc_type['id'], svc_type_id)
self.assertEqual(svc_type['name'],
data[self.resource_name]['name'])
def test_service_type_create(self):
self._test_service_type_create()
def test_service_type_update(self):
self._test_service_type_update()
def test_service_type_delete(self):
svctype_id = _uuid()
instance = self.mock_mgr.return_value
res = self.api.delete(_get_path('service-types/%s' % svctype_id,
fmt=self.fmt))
instance.delete_service_type.assert_called_with(mock.ANY,
svctype_id)
self.assertEqual(res.status_int, webexc.HTTPNoContent.code)
def test_service_type_get(self):
svctype_id = _uuid()
return_value = {self.resource_name: {'name': 'test',
'service_definitions': [],
'id': svctype_id}}
instance = self.mock_mgr.return_value
instance.get_service_type.return_value = return_value
res = self.api.get(_get_path('service-types/%s' % svctype_id,
fmt=self.fmt))
instance.get_service_type.assert_called_with(mock.ANY,
svctype_id,
fields=mock.ANY)
instance.get_service_providers.assert_called_with(mock.ANY,
filters={},
fields=[])
self.assertEqual(res.status_int, webexc.HTTPOk.code)
def test_service_type_list(self):
svctype_id = _uuid()
return_value = [{self.resource_name: {'name': 'test',
'service_definitions': [],
'id': svctype_id}}]
instance = self.mock_mgr.return_value
instance.get_service_types.return_value = return_value
res = self.api.get(_get_path('service-types',
fmt=self.fmt))
instance.get_service_types.assert_called_with(mock.ANY,
fields=mock.ANY,
filters=mock.ANY)
self.assertEqual(res.status_int, webexc.HTTPOk.code)
def test_create_service_type_nonadminctx_returns_403(self):
tenant_id = _uuid()
env = {'neutron.context': context.Context('', tenant_id,
is_admin=False)}
self._test_service_type_create(
env=env, expected_status=webexc.HTTPForbidden.code)
def test_create_service_type_adminctx_returns_200(self):
env = {'neutron.context': context.Context('', '', is_admin=True)}
self._test_service_type_create(env=env)
</