Merge "Associate flavor types with datastore versions"

This commit is contained in:
Jenkins 2015-08-22 17:37:52 +00:00 committed by Gerrit Code Review
commit 7cf297cd22
11 changed files with 465 additions and 10 deletions

@ -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."))

@ -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")

@ -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'.")

@ -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)

@ -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)

@ -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'],

@ -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])

@ -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:

@ -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)

@ -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):

@ -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)