From 8f59f8ef41e97f9b27db2690ccb78353040b4c9b Mon Sep 17 00:00:00 2001 From: Riddhi Shah Date: Sat, 26 Jul 2014 14:23:27 -0500 Subject: [PATCH] Associate flavor types with datastore versions This feature provides the ability to associate flavor types with datastore versions.The trove-manage util will provide the ability to add or delete this information. All nova flavors are permitted for a datastore-version unless one or more entries are found in datastore_version_metadata, in which case only those are permitted. partially implements blueprint associate-flavors-datastores Change-Id: Ib012401f89d07c502f93d5ee1cd4abb6b738953a --- trove/cmd/manage.py | 43 +++++- trove/common/api.py | 7 + trove/common/exception.py | 12 ++ trove/datastore/models.py | 133 +++++++++++++++++- trove/datastore/service.py | 14 ++ trove/db/sqlalchemy/mappers.py | 2 + .../036_add_datastore_version_metadata.py | 61 ++++++++ trove/instance/models.py | 14 ++ trove/tests/api/flavors.py | 103 ++++++++++++-- trove/tests/unittests/datastore/base.py | 10 ++ .../test_datastore_version_metadata.py | 76 ++++++++++ 11 files changed, 465 insertions(+), 10 deletions(-) create mode 100644 trove/db/sqlalchemy/migrate_repo/versions/036_add_datastore_version_metadata.py create mode 100644 trove/tests/unittests/datastore/test_datastore_version_metadata.py diff --git a/trove/cmd/manage.py b/trove/cmd/manage.py index 87a3479747..18eec66ecc 100755 --- a/trove/cmd/manage.py +++ b/trove/cmd/manage.py @@ -92,6 +92,30 @@ class Commands(object): config_models.load_datastore_configuration_parameters( datastore, datastore_version, config_file_location) + def datastore_version_flavor_add(self, datastore_name, + datastore_version_name, flavor_ids): + """Adds flavors for a given datastore version id.""" + try: + dsmetadata = datastore_models.DatastoreVersionMetadata + dsmetadata.add_datastore_version_flavor_association( + datastore_name, datastore_version_name, flavor_ids.split(",")) + print("Added flavors '%s' to the '%s' '%s'." + % (flavor_ids, datastore_name, datastore_version_name)) + except exception.DatastoreVersionNotFound as e: + print(e) + + def datastore_version_flavor_delete(self, datastore_name, + datastore_version_name, flavor_id): + """Deletes a flavor's association with a given datastore.""" + try: + dsmetadata = datastore_models.DatastoreVersionMetadata + dsmetadata.delete_datastore_version_flavor_association( + datastore_name, datastore_version_name, flavor_id) + print("Deleted flavor '%s' from '%s' '%s'." + % (flavor_id, datastore_name, datastore_version_name)) + except exception.DatastoreVersionNotFound as e: + print(e) + def params_of(self, command_name): if Commands.has(command_name): return utils.MethodInspector(getattr(self, command_name)) @@ -170,6 +194,23 @@ def main(): help='Fully qualified file path to the configuration group ' 'parameter validation rules.') + parser = subparser.add_parser( + 'datastore_version_flavor_add', help='Adds flavor association to ' + 'a given datastore and datastore version.') + parser.add_argument('datastore_name', help='Name of the datastore.') + parser.add_argument('datastore_version_name', help='Name of the ' + 'datastore version.') + parser.add_argument('flavor_ids', help='Comma separated list of ' + 'flavor ids.') + + parser = subparser.add_parser( + 'datastore_version_flavor_delete', help='Deletes a flavor ' + 'associated with a given datastore and datastore version.') + parser.add_argument('datastore_name', help='Name of the datastore.') + parser.add_argument('datastore_version_name', help='Name of the ' + 'datastore version.') + parser.add_argument('flavor_id', help='The flavor to be deleted for ' + 'a given datastore and datastore version.') cfg.custom_parser('action', actions) cfg.parse_args(sys.argv) @@ -179,7 +220,7 @@ def main(): Commands().execute() sys.exit(0) except TypeError as e: - print(_("Possible wrong number of arguments supplied %s") % e) + print(_("Possible wrong number of arguments supplied %s.") % e) sys.exit(2) except Exception: print(_("Command failed, please check log for more info.")) diff --git a/trove/common/api.py b/trove/common/api.py index 853272de0b..883df0fefc 100644 --- a/trove/common/api.py +++ b/trove/common/api.py @@ -57,6 +57,13 @@ class API(wsgi.Router): mapper.connect("/{tenant_id}/datastores/{datastore}/versions/{id}", controller=datastore_resource, action="version_show") + mapper.connect( + "/{tenant_id}/datastores/{datastore}/versions/" + "{version_id}/flavors", + controller=datastore_resource, + action="list_associated_flavors", + conditions={'method': ['GET']} + ) mapper.connect("/{tenant_id}/datastores/versions/{uuid}", controller=datastore_resource, action="version_show_by_uuid") diff --git a/trove/common/exception.py b/trove/common/exception.py index 133fa08b04..5766bc89cf 100644 --- a/trove/common/exception.py +++ b/trove/common/exception.py @@ -119,6 +119,18 @@ class DatastoresNotFound(NotFound): message = _("Datastores cannot be found.") +class DatastoreFlavorAssociationNotFound(NotFound): + + message = _("Datastore '%(datastore)s' version id %(version_id)s " + "and flavor %(flavor_id)s mapping not found.") + + +class DatastoreFlavorAssociationAlreadyExists(TroveError): + + message = _("Datastore '%(datastore)s' version %(datastore_version)s " + "and flavor %(flavor_id)s mapping already exists.") + + class DatastoreNoVersion(TroveError): message = _("Datastore '%(datastore)s' has no version '%(version)s'.") diff --git a/trove/datastore/models.py b/trove/datastore/models.py index 81d8ee1055..04da5f57df 100644 --- a/trove/datastore/models.py +++ b/trove/datastore/models.py @@ -14,15 +14,16 @@ # 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 trove.common import cfg from trove.common import exception +from trove.common.remote import create_nova_client from trove.common import utils from trove.db import get_db_api from trove.db import models as dbmodels +from trove.flavor.models import Flavor as flavor_model LOG = logging.getLogger(__name__) @@ -36,6 +37,7 @@ def persisted_models(): 'capabilities': DBCapabilities, 'datastore_version': DBDatastoreVersion, 'capability_overrides': DBCapabilityOverrides, + 'datastore_version_metadata': DBDatastoreVersionMetadata } @@ -60,6 +62,13 @@ class DBDatastoreVersion(dbmodels.DatabaseModelBase): 'packages', 'active'] +class DBDatastoreVersionMetadata(dbmodels.DatabaseModelBase): + + _data_fields = ['id', 'datastore_version_id', 'key', 'value', + 'created', 'deleted', 'deleted_at', 'updated_at'] + preserve_on_delete = True + + class Capabilities(object): def __init__(self, datastore_version_id=None): @@ -526,4 +535,126 @@ def update_datastore_version(datastore, name, manager, image_id, packages, version.image_id = image_id version.packages = packages version.active = active + db_api.save(version) + + +class DatastoreVersionMetadata(object): + @classmethod + def _datastore_version_metadata_add(cls, datastore_version_id, + key, value, exception_class): + """Create an entry in the Datastore Version Metadata table.""" + # Do we have a mapping in the db? + # yes: and its deleted then modify the association + # yes: and its not deleted then error on create + # no: then just create the new association + try: + db_record = DBDatastoreVersionMetadata.find_by( + datastore_version_id=datastore_version_id, + key=key, value=value) + if db_record.deleted == 1: + db_record.deleted = 0 + db_record.updated_at = utils.utcnow() + db_record.save() + return + else: + raise exception_class( + datastore_version_id=datastore_version_id, + flavor_id=value) + except exception.NotFound: + pass + DBDatastoreVersionMetadata.create( + datastore_version_id=datastore_version_id, + key=key, value=value) + + @classmethod + def _datastore_version_metadata_delete(cls, datastore_version_id, + key, value, exception_class): + try: + db_record = DBDatastoreVersionMetadata.find_by( + datastore_version_id=datastore_version_id, + key=key, value=value) + if db_record.deleted == 0: + db_record.delete() + return + else: + raise exception_class( + datastore_version_id=datastore_version_id, + flavor_id=value) + except exception.ModelNotFoundError: + raise exception_class(datastore_version_id=datastore_version_id, + flavor_id=value) + + @classmethod + def add_datastore_version_flavor_association(cls, datastore_name, + datastore_version_name, + flavor_ids): + db_api.configure_db(CONF) + db_ds_record = DBDatastore.find_by( + name=datastore_name + ) + db_datastore_id = db_ds_record.id + db_dsv_record = DBDatastoreVersion.find_by( + datastore_id=db_datastore_id, + name=datastore_version_name + ) + datastore_version_id = db_dsv_record.id + for flavor_id in flavor_ids: + cls._datastore_version_metadata_add( + datastore_version_id, 'flavor', flavor_id, + exception.DatastoreFlavorAssociationAlreadyExists) + + @classmethod + def delete_datastore_version_flavor_association(cls, datastore_name, + datastore_version_name, + flavor_id): + db_api.configure_db(CONF) + db_ds_record = DBDatastore.find_by( + name=datastore_name + ) + db_datastore_id = db_ds_record.id + db_dsv_record = DBDatastoreVersion.find_by( + datastore_id=db_datastore_id, + name=datastore_version_name + ) + datastore_version_id = db_dsv_record.id + cls._datastore_version_metadata_delete( + datastore_version_id, 'flavor', flavor_id, + exception.DatastoreFlavorAssociationNotFound) + + @classmethod + def list_datastore_version_flavor_associations(cls, context, + datastore_type, + datastore_version_id): + if datastore_type and datastore_version_id: + """ + All nova flavors are permitted for a datastore_version unless + one or more entries are found in datastore_version_metadata, + in which case only those are permitted. + """ + (datastore, datastore_version) = get_datastore_version( + type=datastore_type, version=datastore_version_id) + # If datastore_version_id and flavor key exists in the + # metadata table return all the associated flavors for + # that datastore version. + nova_flavors = create_nova_client(context).flavors.list() + bound_flavors = DBDatastoreVersionMetadata.find_all( + datastore_version_id=datastore_version.id, + key='flavor', deleted=False + ) + if (bound_flavors.count() != 0): + bound_flavors = tuple(f.value for f in bound_flavors) + # Generate a filtered list of nova flavors + ds_nova_flavors = (f for f in nova_flavors + if f.id in bound_flavors) + associated_flavors = tuple(flavor_model(flavor=item) + for item in ds_nova_flavors) + else: + # Return all nova flavors if no flavor metadata found + # for datastore_version. + associated_flavors = tuple(flavor_model(flavor=item) + for item in nova_flavors) + return associated_flavors + else: + msg = _("Specify both the datastore and datastore_version_id.") + raise exception.BadRequest(msg) diff --git a/trove/datastore/service.py b/trove/datastore/service.py index 376bdb5661..6a04a1ce49 100644 --- a/trove/datastore/service.py +++ b/trove/datastore/service.py @@ -18,6 +18,7 @@ from trove.common import wsgi from trove.datastore import models, views +from trove.flavor import views as flavor_views class DatastoreController(wsgi.Controller): @@ -61,3 +62,16 @@ class DatastoreController(wsgi.Controller): return wsgi.Result(views. DatastoreVersionsView(datastore_versions, req).data(), 200) + + def list_associated_flavors(self, req, tenant_id, datastore, + version_id): + """ + All nova flavors are returned for a datastore-version unless + one or more entries are found in datastore_version_metadata, + in which case only those are returned. + """ + context = req.environ[wsgi.CONTEXT_KEY] + flavors = (models.DatastoreVersionMetadata. + list_datastore_version_flavor_associations( + context, datastore, version_id)) + return wsgi.Result(flavor_views.FlavorsView(flavors, req).data(), 200) diff --git a/trove/db/sqlalchemy/mappers.py b/trove/db/sqlalchemy/mappers.py index 7965253262..9d379bc0e7 100644 --- a/trove/db/sqlalchemy/mappers.py +++ b/trove/db/sqlalchemy/mappers.py @@ -32,6 +32,8 @@ def map(engine, models): Table('datastores', meta, autoload=True)) orm.mapper(models['datastore_version'], Table('datastore_versions', meta, autoload=True)) + orm.mapper(models['datastore_version_metadata'], + Table('datastore_version_metadata', meta, autoload=True)) orm.mapper(models['capabilities'], Table('capabilities', meta, autoload=True)) orm.mapper(models['capability_overrides'], diff --git a/trove/db/sqlalchemy/migrate_repo/versions/036_add_datastore_version_metadata.py b/trove/db/sqlalchemy/migrate_repo/versions/036_add_datastore_version_metadata.py new file mode 100644 index 0000000000..cad5148677 --- /dev/null +++ b/trove/db/sqlalchemy/migrate_repo/versions/036_add_datastore_version_metadata.py @@ -0,0 +1,61 @@ +# Copyright 2015 Rackspace +# 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 sqlalchemy import ForeignKey +from sqlalchemy.schema import Column +from sqlalchemy.schema import MetaData +from sqlalchemy.schema import UniqueConstraint + +from trove.db.sqlalchemy.migrate_repo.schema import Boolean +from trove.db.sqlalchemy.migrate_repo.schema import create_tables +from trove.db.sqlalchemy.migrate_repo.schema import DateTime +from trove.db.sqlalchemy.migrate_repo.schema import drop_tables +from trove.db.sqlalchemy.migrate_repo.schema import String +from trove.db.sqlalchemy.migrate_repo.schema import Table + +meta = MetaData() + +datastore_version_metadata = Table( + 'datastore_version_metadata', + meta, + Column('id', String(36), primary_key=True, nullable=False), + Column( + 'datastore_version_id', + String(36), + ForeignKey('datastore_versions.id', ondelete='CASCADE'), + ), + Column('key', String(128), nullable=False), + Column('value', String(128)), + Column('created', DateTime(), nullable=False), + Column('deleted', Boolean(), nullable=False, default=False), + Column('deleted_at', DateTime()), + Column('updated_at', DateTime()), + UniqueConstraint( + 'datastore_version_id', 'key', 'value', + name='UQ_datastore_version_metadata_datastore_version_id_key_value') +) + + +def upgrade(migrate_engine): + meta.bind = migrate_engine + # Load the datastore_versions table into the session. + # creates datastore_version_metadata table + Table('datastore_versions', meta, autoload=True) + create_tables([datastore_version_metadata]) + + +def downgrade(migrate_engine): + meta.bind = migrate_engine + drop_tables([datastore_version_metadata]) diff --git a/trove/instance/models.py b/trove/instance/models.py index 8efc2b8b70..cc9c5886eb 100644 --- a/trove/instance/models.py +++ b/trove/instance/models.py @@ -37,6 +37,7 @@ from trove.common import template from trove.common import utils from trove.configuration.models import Configuration from trove.datastore import models as datastore_models +from trove.datastore.models import DBDatastoreVersionMetadata from trove.db import get_db_api from trove.db import models as dbmodels from trove.extensions.security_group.models import SecurityGroup @@ -670,6 +671,19 @@ class Instance(BuiltInstance): availability_zone=None, nics=None, configuration_id=None, slave_of_id=None, cluster_config=None, replica_count=None): + # All nova flavors are permitted for a datastore-version unless one + # or more entries are found in datastore_version_metadata, + # in which case only those are permitted. + bound_flavors = DBDatastoreVersionMetadata.find_all( + datastore_version_id=datastore_version.id, + key='flavor', deleted=False + ) + if bound_flavors.count() > 0: + valid_flavors = tuple(f.value for f in bound_flavors) + if flavor_id not in valid_flavors: + raise exception.DatastoreFlavorAssociationNotFound( + version_id=datastore_version.id, flavor_id=flavor_id) + datastore_cfg = CONF.get(datastore_version.manager) client = create_nova_client(context) try: diff --git a/trove/tests/api/flavors.py b/trove/tests/api/flavors.py index 407b193ee9..bda90e968c 100644 --- a/trove/tests/api/flavors.py +++ b/trove/tests/api/flavors.py @@ -1,8 +1,8 @@ # Copyright (c) 2011 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 +# 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 @@ -20,19 +20,22 @@ from nose.tools import assert_false from nose.tools import assert_true from proboscis.asserts import assert_raises from proboscis import before_class +from proboscis.decorators import time_out from proboscis import test -from troveclient.compat import exceptions -from troveclient.v1.flavors import Flavor - +from trove.common.utils import poll_until from trove import tests +from trove.tests.api.instances import TIMEOUT_INSTANCE_CREATE from trove.tests.util.check import AttrCheck from trove.tests.util import create_dbaas_client from trove.tests.util import create_nova_client from trove.tests.util import test_config from trove.tests.util.users import Requirements +from troveclient.compat import exceptions +from troveclient.v1.flavors import Flavor GROUP = "dbaas.api.flavors" - +GROUP_DS = "dbaas.api.datastores" +FAKE_MODE = test_config.values['fake_mode'] servers_flavors = None dbaas_flavors = None @@ -91,10 +94,9 @@ def assert_link_list_is_equal(flavor): assert_false(True, "Unexpected rel - %s" % link['rel']) -@test(groups=[tests.DBAAS_API, GROUP, tests.PRE_INSTANCES], +@test(groups=[tests.DBAAS_API, GROUP, GROUP_DS, tests.PRE_INSTANCES], depends_on_groups=["services.initialize"]) class Flavors(object): - @before_class def setUp(self): rd_user = test_config.users.find_user( @@ -171,3 +173,88 @@ class Flavors(object): def test_flavor_not_found(self): assert_raises(exceptions.NotFound, self.rd_client.flavors.get, "foo") + + @test + def test_flavor_list_datastore_version_associated_flavors(self): + datastore = self.rd_client.datastores.get( + test_config.dbaas_datastore) + dbaas_flavors = (self.rd_client.flavors. + list_datastore_version_associated_flavors( + datastore=test_config.dbaas_datastore, + version_id=datastore.default_version)) + os_flavors = self.get_expected_flavors() + assert_equal(len(dbaas_flavors), len(os_flavors)) + # verify flavor lists are identical + for os_flavor in os_flavors: + found_index = None + for index, dbaas_flavor in enumerate(dbaas_flavors): + if os_flavor.name == dbaas_flavor.name: + msg = ("Flavor ID '%s' appears in elements #%s and #%d." % + (dbaas_flavor.id, str(found_index), index)) + assert_true(found_index is None, msg) + assert_flavors_roughly_equivalent(os_flavor, dbaas_flavor) + found_index = index + msg = "Some flavors from OS list were missing in DBAAS list." + assert_false(found_index is None, msg) + for flavor in dbaas_flavors: + assert_link_list_is_equal(flavor) + + +@test(runs_after=[Flavors], + groups=[tests.DBAAS_API, GROUP, GROUP_DS], + depends_on_groups=["services.initialize"], + enabled=FAKE_MODE) +class DatastoreFlavorAssociation(object): + @before_class + def setUp(self): + rd_user = test_config.users.find_user( + Requirements(is_admin=False, services=["trove"])) + self.rd_client = create_dbaas_client(rd_user) + + self.datastore = self.rd_client.datastores.get( + test_config.dbaas_datastore) + self.name1 = "test_instance1" + self.name2 = "test_instance2" + self.volume = {'size': 2} + self.instance_id = None + + @test + @time_out(TIMEOUT_INSTANCE_CREATE) + def test_create_instance_with_valid_flavor_association(self): + # all the nova flavors are associated with the default datastore + result = self.rd_client.instances.create( + name=self.name1, flavor_id='1', volume=self.volume, + datastore=self.datastore.id) + self.instance_id = result.id + assert_equal(200, self.rd_client.last_http_code) + + def result_is_active(): + instance = self.rd_client.instances.get(self.instance_id) + if instance.status == "ACTIVE": + return True + else: + # If its not ACTIVE, anything but BUILD must be + # an error. + assert_equal("BUILD", instance.status) + return False + + poll_until(result_is_active) + self.rd_client.instances.delete(self.instance_id) + + @test(runs_after=[test_create_instance_with_valid_flavor_association]) + def test_create_instance_with_invalid_flavor_association(self): + dbaas_flavors = (self.rd_client.flavors. + list_datastore_version_associated_flavors( + datastore=test_config.dbaas_datastore, + version_id=self.datastore.default_version)) + self.flavor_not_associated = None + os_flavors = Flavors().get_expected_flavors() + for os_flavor in os_flavors: + if os_flavor not in dbaas_flavors: + self.flavor_not_associated = os_flavor.id + break + if self.flavor_not_associated is not None: + assert_raises(exceptions.BadRequest, + self.rd_client.instances.create, self.name2, + flavor_not_associated, self.volume, + datastore=self.datastore.id) diff --git a/trove/tests/unittests/datastore/base.py b/trove/tests/unittests/datastore/base.py index 45b87a1abc..a64b6718e9 100644 --- a/trove/tests/unittests/datastore/base.py +++ b/trove/tests/unittests/datastore/base.py @@ -17,6 +17,7 @@ from trove.datastore import models as datastore_models from trove.datastore.models import Capability from trove.datastore.models import Datastore from trove.datastore.models import DatastoreVersion +from trove.datastore.models import DatastoreVersionMetadata from trove.datastore.models import DBCapabilityOverrides from trove.tests.unittests import trove_testtools from trove.tests.unittests.util import util @@ -34,12 +35,16 @@ class TestDatastoreBase(trove_testtools.TestCase): self.capability_name = "root_on_create" + self.rand_id self.capability_desc = "Enables root on create" self.capability_enabled = True + self.datastore_version_id = str(uuid.uuid4()) + self.flavor_id = 1 datastore_models.update_datastore(self.ds_name, False) self.datastore = Datastore.load(self.ds_name) datastore_models.update_datastore_version( self.ds_name, self.ds_version, "mysql", "", "", True) + DatastoreVersionMetadata.add_datastore_version_flavor_association( + self.ds_name, self.ds_version, [self.flavor_id]) self.datastore_version = DatastoreVersion.load(self.datastore, self.ds_version) @@ -63,6 +68,11 @@ class TestDatastoreBase(trove_testtools.TestCase): self.cap1.delete() self.cap2.delete() self.cap3.delete() + datastore = datastore_models.Datastore.load(self.ds_name) + ds_version = datastore_models.DatastoreVersion.load(datastore, + self.ds_version) + datastore_models.DBDatastoreVersionMetadata.find_by( + datastore_version_id=ds_version.id).delete() Datastore.load(self.ds_name).delete() def capability_name_filter(self, capabilities): diff --git a/trove/tests/unittests/datastore/test_datastore_version_metadata.py b/trove/tests/unittests/datastore/test_datastore_version_metadata.py new file mode 100644 index 0000000000..899b605d9a --- /dev/null +++ b/trove/tests/unittests/datastore/test_datastore_version_metadata.py @@ -0,0 +1,76 @@ +# Copyright (c) 2015 Rackspace Hosting +# +# 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 trove.common import exception +from trove.datastore import models as datastore_models +from trove.tests.unittests.datastore.base import TestDatastoreBase + + +class TestDatastoreVersionMetadata(TestDatastoreBase): + def setUp(self): + super(TestDatastoreVersionMetadata, self).setUp() + + def tearDown(self): + super(TestDatastoreVersionMetadata, self).tearDown() + + def test_map_flavors_to_datastore(self): + datastore = datastore_models.Datastore.load(self.ds_name) + ds_version = datastore_models.DatastoreVersion.load(datastore, + self.ds_version) + mapping = datastore_models.DBDatastoreVersionMetadata.find_by( + datastore_version_id=ds_version.id, + value=self.flavor_id, deleted=False, key='flavor') + self.assertEqual(str(self.flavor_id), mapping.value) + self.assertEqual(ds_version.id, mapping.datastore_version_id) + self.assertEqual('flavor', str(mapping.key)) + + def test_add_existing_associations(self): + dsmetadata = datastore_models.DatastoreVersionMetadata + self.assertRaises(exception.DatastoreFlavorAssociationAlreadyExists, + dsmetadata.add_datastore_version_flavor_association, + self.ds_name, self.ds_version, [self.flavor_id]) + + def test_delete_nonexistent_mapping(self): + dsmeta = datastore_models.DatastoreVersionMetadata + self.assertRaises(exception.DatastoreFlavorAssociationNotFound, + dsmeta.delete_datastore_version_flavor_association, + self.ds_name, self.ds_version, + flavor_id=2) + + def test_delete_mapping(self): + flavor_id = 2 + dsmetadata = datastore_models. DatastoreVersionMetadata + dsmetadata.add_datastore_version_flavor_association(self.ds_name, + self.ds_version, + [flavor_id]) + dsmetadata.delete_datastore_version_flavor_association(self.ds_name, + self.ds_version, + flavor_id) + datastore = datastore_models.Datastore.load(self.ds_name) + ds_version = datastore_models.DatastoreVersion.load(datastore, + self.ds_version) + mapping = datastore_models.DBDatastoreVersionMetadata.find_by( + datastore_version_id=ds_version.id, value=flavor_id, key='flavor') + self.assertTrue(mapping.deleted) + # check update + dsmetadata.add_datastore_version_flavor_association( + self.ds_name, self.ds_version, [flavor_id]) + mapping = datastore_models.DBDatastoreVersionMetadata.find_by( + datastore_version_id=ds_version.id, value=flavor_id, key='flavor') + self.assertFalse(mapping.deleted) + # clear the mapping + datastore_models.DatastoreVersionMetadata.\ + delete_datastore_version_flavor_association(self.ds_name, + self.ds_version, + flavor_id)