Introduce uniqueness constraint on resource names

Resource names are handled to be unique per tenant for all 3
resources vnfd, vnf and vim.

Also existing duplicate names are updated to name-<12char. uudid>
based on resource id's last 12 char. to handle the uniqueness
constraint on 'name' column for all 3 resources.

Change-Id: I689d7219db67892b855e1dc5c3698f9e1a67b408
Closes-Bug: #1475047
Closes-Bug: #1474993
This commit is contained in:
Sripriya 2016-06-14 21:36:54 -07:00
parent 0988954e15
commit f9729fb388
15 changed files with 154 additions and 41 deletions

View File

@ -0,0 +1,4 @@
---
features:
- Introduce uniqueness constraint on the name of Tacker resources such as
VNFD, VNF, VIM, etc.

View File

@ -257,3 +257,7 @@ class VnfPolicyActionInvalid(BadRequest):
class VnfPolicyTypeInvalid(BadRequest): class VnfPolicyTypeInvalid(BadRequest):
message = _("Invalid type %(type)s for policy %(policy)s, " message = _("Invalid type %(type)s for policy %(policy)s, "
"should be one of %(valid_types)s") "should be one of %(valid_types)s")
class DuplicateResourceName(TackerException):
message = _("%(resource)s with name %(name)s already exists")

View File

@ -15,12 +15,16 @@
import weakref import weakref
from oslo_log import log as logging
from six import iteritems from six import iteritems
from sqlalchemy.orm import exc as orm_exc
from sqlalchemy import sql from sqlalchemy import sql
from tacker.common import exceptions as n_exc from tacker.common import exceptions as n_exc
from tacker.db import sqlalchemyutils from tacker.db import sqlalchemyutils
LOG = logging.getLogger(__name__)
class CommonDbMixin(object): class CommonDbMixin(object):
"""Common methods used in core and service plugins.""" """Common methods used in core and service plugins."""
@ -201,3 +205,11 @@ class CommonDbMixin(object):
columns = [c.name for c in model.__table__.columns] columns = [c.name for c in model.__table__.columns]
return dict((k, v) for (k, v) in return dict((k, v) for (k, v) in
iteritems(data) if k in columns) iteritems(data) if k in columns)
def _get_by_name(self, context, model, name):
try:
query = self._model_query(context, model)
return query.filter(model.name == name).one()
except orm_exc.NoResultFound:
LOG.info(_("No result found for %(name)s in %(model)s table"),
{'name': name, 'model': model})

View File

@ -0,0 +1,58 @@
# Copyright 2016 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.
#
"""unique_constraint_name
Revision ID: 0ae5b1ce3024
Revises: 507122918800
Create Date: 2016-09-15 16:27:08.736673
"""
# revision identifiers, used by Alembic.
revision = '0ae5b1ce3024'
down_revision = '507122918800'
from alembic import op
import sqlalchemy as sa
def _migrate_duplicate_names(table):
meta = sa.MetaData(bind=op.get_bind())
t = sa.Table(table, meta, autoload=True)
session = sa.orm.Session(bind=op.get_bind())
with session.begin(subtransactions=True):
dup_names = session.query(t.c.name).group_by(
t.c.name).having(sa.func.count() > 1).all()
if dup_names:
for name in dup_names:
duplicate_obj_query = session.query(t).filter(t.c.name == name[
0]).all()
for dup_obj in duplicate_obj_query:
name = dup_obj.name[:242] if dup_obj.name > 242 else \
dup_obj.name
new_name = '{0}-{1}'.format(name, dup_obj.id[-12:])
session.execute(t.update().where(
t.c.id == dup_obj.id).values(name=new_name))
session.commit()
def upgrade(active_plugins=None, options=None):
_migrate_duplicate_names('vnf')
_migrate_duplicate_names('vnfd')
_migrate_duplicate_names('vims')

View File

@ -1 +1 @@
507122918800 0ae5b1ce3024

View File

@ -223,16 +223,8 @@ class NfvoPluginDb(nfvo.NFVOPluginBase, db_base.CommonDbMixin):
def get_vim_by_name(self, context, vim_name, fields=None, def get_vim_by_name(self, context, vim_name, fields=None,
mask_password=True): mask_password=True):
vim_db = self._get_by_name(context, Vim, vim_name) vim_db = self._get_by_name(context, Vim, vim_name)
return self._make_vim_dict(vim_db, mask_password=mask_password) return self._make_vim_dict(vim_db, mask_password=mask_password
)if vim_db else None
# Deprecated. Will be removed in Ocata release
def _get_by_name(self, context, model, name):
try:
query = self._model_query(context, model)
return query.filter(model.name == name).one()
except orm_exc.NoResultFound:
if issubclass(model, Vim):
raise
def _validate_default_vim(self, context, vim, vim_id=None): def _validate_default_vim(self, context, vim, vim_id=None):
if not vim.get('is_default'): if not vim.get('is_default'):

View File

@ -45,11 +45,9 @@ class VimDefaultNameNotDefined(exceptions.TackerException):
" in tacker.conf") " in tacker.conf")
# Deprecated. Will be removed in Ocata release class VimDefaultNameNotFound(exceptions.TackerException):
class VimDefaultIdException(exceptions.TackerException): message = _("Default VIM name %(vim_name)s is invalid. Please specify a "
message = _("Default VIM name %(vim_name)s is invalid or there are " "valid default VIM name in tacker.conf")
"multiple VIM matches found. Please specify a valid default "
"VIM in tacker.conf")
class VimDefaultDuplicateException(exceptions.TackerException): class VimDefaultDuplicateException(exceptions.TackerException):

View File

@ -25,6 +25,7 @@ from oslo_utils import strutils
from tacker._i18n import _ from tacker._i18n import _
from tacker.common import driver_manager from tacker.common import driver_manager
from tacker.common import exceptions
from tacker.common import log from tacker.common import log
from tacker.common import utils from tacker.common import utils
from tacker import context as t_context from tacker import context as t_context
@ -36,6 +37,7 @@ from tacker.plugins.common import constants
from tacker.vnfm.tosca import utils as toscautils from tacker.vnfm.tosca import utils as toscautils
from toscaparser import tosca_template from toscaparser import tosca_template
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -88,6 +90,9 @@ class NfvoPlugin(nfvo_db.NfvoPluginDb, vnffg_db.VnffgPluginDbMixin):
LOG.debug(_('Create vim called with parameters %s'), LOG.debug(_('Create vim called with parameters %s'),
strutils.mask_password(vim)) strutils.mask_password(vim))
vim_obj = vim['vim'] vim_obj = vim['vim']
name = vim_obj['name']
if self._get_by_name(context, nfvo_db.Vim, name):
raise exceptions.DuplicateResourceName(resource='VIM', name=name)
vim_type = vim_obj['type'] vim_type = vim_obj['type']
vim_obj['id'] = str(uuid.uuid4()) vim_obj['id'] = str(uuid.uuid4())
vim_obj['status'] = 'PENDING' vim_obj['status'] = 'PENDING'

View File

@ -24,10 +24,9 @@ VNF_CIRROS_CREATE_TIMEOUT = 120
class VnfTestCreate(base.BaseTackerTest): class VnfTestCreate(base.BaseTackerTest):
def _test_create_delete_vnf(self, vnf_name, vim_id=None): def _test_create_delete_vnf(self, vnf_name, vnfd_name, vim_id=None):
data = dict() data = dict()
data['tosca'] = read_file('sample_cirros_vnf_no_monitoring.yaml') data['tosca'] = read_file('sample_cirros_vnf_no_monitoring.yaml')
vnfd_name = 'sample_cirros_vnf_no_monitoring'
toscal = data['tosca'] toscal = data['tosca']
tosca_arg = {'vnfd': {'name': vnfd_name, tosca_arg = {'vnfd': {'name': vnfd_name,
'attributes': {'vnfd': toscal}}} 'attributes': {'vnfd': toscal}}}
@ -81,10 +80,13 @@ class VnfTestCreate(base.BaseTackerTest):
def test_create_delete_vnf_with_default_vim(self): def test_create_delete_vnf_with_default_vim(self):
self._test_create_delete_vnf( self._test_create_delete_vnf(
vnf_name='test_vnf_with_cirros_no_monitoring') vnf_name='test_vnf_with_cirros_no_monitoring_default_vim',
vnfd_name='sample_cirros_vnf_no_monitoring_default_vim')
def test_create_delete_vnf_with_vim_id(self): def test_create_delete_vnf_with_vim_id(self):
vim_list = self.client.list_vims() vim_list = self.client.list_vims()
vim0_id = self.get_vim(vim_list, 'VIM0')['id'] vim0_id = self.get_vim(vim_list, 'VIM0')['id']
self._test_create_delete_vnf(vim_id=vim0_id, self._test_create_delete_vnf(
vnf_name='test_vnf_with_cirros_with_default_vim_id') vim_id=vim0_id,
vnf_name='test_vnf_with_cirros_vim_id',
vnfd_name='sample_cirros_vnf_no_monitoring_vim_id')

View File

@ -21,9 +21,8 @@ from tacker.tests.utils import read_file
class VnfmTestParam(base.BaseTackerTest): class VnfmTestParam(base.BaseTackerTest):
def _test_vnfd_create(self, vnfd_file): def _test_vnfd_create(self, vnfd_file, vnfd_name):
yaml_input = read_file(vnfd_file) yaml_input = read_file(vnfd_file)
vnfd_name = 'sample_cirros_vnf'
# TODO(anyone) remove this condition check once old templates # TODO(anyone) remove this condition check once old templates
# are deprecated # are deprecated
if "tosca_definitions_version" in yaml_input: if "tosca_definitions_version" in yaml_input:
@ -104,8 +103,9 @@ class VnfmTestParam(base.BaseTackerTest):
assert True, "Vnf Delete success" + str(vfn_d) + str(Exception) assert True, "Vnf Delete success" + str(vfn_d) + str(Exception)
def test_vnf_param(self): def test_vnf_param(self):
vnfd_name = 'sample_cirros_vnfd_old_template'
vnfd_instance = self._test_vnfd_create( vnfd_instance = self._test_vnfd_create(
'sample_cirros_vnf_param.yaml') 'sample_cirros_vnf_param.yaml', vnfd_name)
values_str = read_file('sample_cirros_vnf_values.yaml') values_str = read_file('sample_cirros_vnf_values.yaml')
vnf_instance, param_values_dict = self._test_vnf_create(vnfd_instance, vnf_instance, param_values_dict = self._test_vnf_create(vnfd_instance,
'test_vnf_with_parameters', 'test_vnf_with_parameters',
@ -127,13 +127,15 @@ class VnfmTestParam(base.BaseTackerTest):
self.addCleanup(self.client.delete_vnfd, vnfd_instance['vnfd']['id']) self.addCleanup(self.client.delete_vnfd, vnfd_instance['vnfd']['id'])
def test_vnfd_param_tosca_template(self): def test_vnfd_param_tosca_template(self):
vnfd_name = 'sample_cirros_vnfd_tosca'
vnfd_instance = self._test_vnfd_create( vnfd_instance = self._test_vnfd_create(
'sample-tosca-vnfd-param.yaml') 'sample-tosca-vnfd-param.yaml', vnfd_name)
self._test_vnfd_delete(vnfd_instance) self._test_vnfd_delete(vnfd_instance)
def test_vnf_param_tosca_template(self): def test_vnf_param_tosca_template(self):
vnfd_name = 'cirros_vnfd_tosca_param'
vnfd_instance = self._test_vnfd_create( vnfd_instance = self._test_vnfd_create(
'sample-tosca-vnfd-param.yaml') 'sample-tosca-vnfd-param.yaml', vnfd_name)
values_str = read_file('sample-tosca-vnf-values.yaml') values_str = read_file('sample-tosca-vnf-values.yaml')
values_dict = yaml.safe_load(values_str) values_dict = yaml.safe_load(values_str)
vnf_instance, param_values_dict = self._test_vnf_create(vnfd_instance, vnf_instance, param_values_dict = self._test_vnf_create(vnfd_instance,

View File

@ -156,6 +156,16 @@ def get_dummy_device_obj_userdata_attr():
'description': u"Parameterized VNF descriptor"} 'description': u"Parameterized VNF descriptor"}
def get_vim_obj():
return {'vim': {'type': 'openstack', 'auth_url':
'http://localhost:5000', 'vim_project': {'name':
'test_project'}, 'auth_cred': {'username': 'test_user',
'password':
'test_password'},
'name': 'VIM0',
'tenant_id': 'test-project'}}
def get_vim_auth_obj(): def get_vim_auth_obj():
return {'username': 'test_user', return {'username': 'test_user',
'password': 'test_password', 'password': 'test_password',

View File

@ -18,6 +18,8 @@ import uuid
from mock import patch from mock import patch
from tacker.common import exceptions
from tacker import context from tacker import context
from tacker.db.common_services import common_services_db from tacker.db.common_services import common_services_db
from tacker.db.nfvo import nfvo_db from tacker.db.nfvo import nfvo_db
@ -173,13 +175,7 @@ class TestNfvoPlugin(db_base.SqlTestCase):
session.flush() session.flush()
def test_create_vim(self): def test_create_vim(self):
vim_dict = {'vim': {'type': 'openstack', 'auth_url': vim_dict = utils.get_vim_obj()
'http://localhost:5000', 'vim_project': {'name':
'test_project'}, 'auth_cred': {'username': 'test_user',
'password':
'test_password'},
'name': 'VIM0',
'tenant_id': 'test-project'}}
vim_type = 'openstack' vim_type = 'openstack'
res = self.nfvo_plugin.create_vim(self.context, vim_dict) res = self.nfvo_plugin.create_vim(self.context, vim_dict)
self._cos_db_plugin.create_event.assert_any_call( self._cos_db_plugin.create_event.assert_any_call(
@ -201,6 +197,14 @@ class TestNfvoPlugin(db_base.SqlTestCase):
self.assertIn('created_at', res) self.assertIn('created_at', res)
self.assertIn('updated_at', res) self.assertIn('updated_at', res)
def test_create_vim_duplicate_name(self):
self._insert_dummy_vim()
vim_dict = utils.get_vim_obj()
vim_dict['vim']['name'] = 'fake_vim'
self.assertRaises(exceptions.DuplicateResourceName,
self.nfvo_plugin.create_vim,
self.context, vim_dict)
def test_delete_vim(self): def test_delete_vim(self):
self._insert_dummy_vim() self._insert_dummy_vim()
vim_type = 'openstack' vim_type = 'openstack'

View File

@ -18,6 +18,7 @@ import uuid
import mock import mock
import yaml import yaml
from tacker.common import exceptions
from tacker import context from tacker import context
from tacker.db.common_services import common_services_db from tacker.db.common_services import common_services_db
from tacker.db.nfvo import nfvo_db from tacker.db.nfvo import nfvo_db
@ -192,6 +193,14 @@ class TestVNFMPlugin(db_base.SqlTestCase):
self.vnfm_plugin.create_vnfd, self.vnfm_plugin.create_vnfd,
self.context, vnfd_obj) self.context, vnfd_obj)
def test_create_vnfd_duplicate_name(self):
self._insert_dummy_device_template()
vnfd_obj = utils.get_dummy_vnfd_obj()
vnfd_obj['vnfd']['name'] = 'fake_template'
self.assertRaises(exceptions.DuplicateResourceName,
self.vnfm_plugin.create_vnfd,
self.context, vnfd_obj)
def test_create_vnf(self): def test_create_vnf(self):
self._insert_dummy_device_template() self._insert_dummy_device_template()
vnf_obj = utils.get_dummy_vnf_obj() vnf_obj = utils.get_dummy_vnf_obj()
@ -232,6 +241,15 @@ class TestVNFMPlugin(db_base.SqlTestCase):
self.assertIn('type', resources) self.assertIn('type', resources)
self.assertIn('id', resources) self.assertIn('id', resources)
def test_create_vnf_duplicate_name(self):
self._insert_dummy_device_template()
self._insert_dummy_device()
vnf_obj = utils.get_dummy_vnf_obj()
vnf_obj['vnf']['name'] = 'fake_device'
self.assertRaises(exceptions.DuplicateResourceName,
self.vnfm_plugin.create_vnf,
self.context, vnf_obj)
def test_delete_vnf(self): def test_delete_vnf(self):
self._insert_dummy_device_template() self._insert_dummy_device_template()
dummy_device_obj = self._insert_dummy_device() dummy_device_obj = self._insert_dummy_device()

View File

@ -157,6 +157,9 @@ class VNFMPlugin(vnfm_db.VNFMPluginDb, VNFMMgmtMixin):
" will be removed in Ocata. infra_driver will be automatically" " will be removed in Ocata. infra_driver will be automatically"
" derived from target vim type. mgmt_driver will be derived " " derived from target vim type. mgmt_driver will be derived "
"from TOSCA template values.") "from TOSCA template values.")
name = vnfd_data['name']
if self._get_by_name(context, vnfm_db.VNFD, name):
raise exceptions.DuplicateResourceName(resource='VNFD', name=name)
service_types = vnfd_data.get('service_types') service_types = vnfd_data.get('service_types')
if not attributes.is_attr_set(service_types): if not attributes.is_attr_set(service_types):
@ -344,6 +347,9 @@ class VNFMPlugin(vnfm_db.VNFMPluginDb, VNFMMgmtMixin):
def create_vnf(self, context, vnf): def create_vnf(self, context, vnf):
vnf_info = vnf['vnf'] vnf_info = vnf['vnf']
name = vnf_info['name']
if self._get_by_name(context, vnfm_db.VNF, name):
raise exceptions.DuplicateResourceName(resource='VNF', name=name)
vnf_attributes = vnf_info['attributes'] vnf_attributes = vnf_info['attributes']
if vnf_attributes.get('param_values'): if vnf_attributes.get('param_values'):
param = vnf_attributes['param_values'] param = vnf_attributes['param_values']

View File

@ -53,7 +53,7 @@ class VimClient(object):
if not vim_id: if not vim_id:
LOG.debug(_('VIM id not provided. Attempting to find default ' LOG.debug(_('VIM id not provided. Attempting to find default '
'VIM id')) 'VIM information'))
try: try:
vim_info = nfvo_plugin.get_default_vim(context) vim_info = nfvo_plugin.get_default_vim(context)
except Exception: except Exception:
@ -66,8 +66,7 @@ class VimClient(object):
'default-vim in tacker.conf is deprecated and will be ' 'default-vim in tacker.conf is deprecated and will be '
'removed in Newton cycle') 'removed in Newton cycle')
vim_info = self._get_default_vim_by_name(context, vim_info = self._get_default_vim_by_name(context,
nfvo_plugin, nfvo_plugin, vim_name)
vim_name)
else: else:
try: try:
vim_info = nfvo_plugin.get_vim(context, vim_id, vim_info = nfvo_plugin.get_vim(context, vim_id,
@ -91,11 +90,10 @@ class VimClient(object):
# Deprecated. Will be removed in Ocata release # Deprecated. Will be removed in Ocata release
def _get_default_vim_by_name(self, context, plugin, vim_name): def _get_default_vim_by_name(self, context, plugin, vim_name):
try: vim_info = plugin.get_vim_by_name(context, vim_name,
vim_info = plugin.get_vim_by_name(context, vim_name, mask_password=False)
mask_password=False) if not vim_info:
except Exception: raise nfvo.VimDefaultNameNotFound(vim_name=vim_name)
raise nfvo.VimDefaultIdException(vim_name=vim_name)
return vim_info return vim_info
def _build_vim_auth(self, vim_info): def _build_vim_auth(self, vim_info):