Merge "Associate datastore, version with volume-type"

This commit is contained in:
Jenkins 2017-04-11 07:12:37 +00:00 committed by Gerrit Code Review
commit 0cff23d8d1
18 changed files with 732 additions and 41 deletions

View File

@ -0,0 +1,9 @@
---
features:
- Added the ability to associate datastore versions with volume types. This
enables operators to limit the volume types available when launching
datastores. The associations are set via the trove-manage tool commands
datastore_version_volume_type_add, datastore_version_volume_type_delete,
and datastore_version_volume_type_list. If a user attempts to create an
instance with a volume type that is not on the approved list for the
specified datastore version they will receive an error.

View File

@ -116,6 +116,52 @@ class Commands(object):
except exception.DatastoreVersionNotFound as e:
print(e)
def datastore_version_volume_type_add(self, datastore_name,
datastore_version_name,
volume_type_ids):
"""Adds volume type assiciation for a given datastore version id."""
try:
dsmetadata = datastore_models.DatastoreVersionMetadata
dsmetadata.add_datastore_version_volume_type_association(
datastore_name, datastore_version_name,
volume_type_ids.split(","))
print("Added volume type '%s' to the '%s' '%s'."
% (volume_type_ids, datastore_name, datastore_version_name))
except exception.DatastoreVersionNotFound as e:
print(e)
def datastore_version_volume_type_delete(self, datastore_name,
datastore_version_name,
volume_type_id):
"""Deletes a volume type association with a given datastore."""
try:
dsmetadata = datastore_models.DatastoreVersionMetadata
dsmetadata.delete_datastore_version_volume_type_association(
datastore_name, datastore_version_name, volume_type_id)
print("Deleted volume type '%s' from '%s' '%s'."
% (volume_type_id, datastore_name, datastore_version_name))
except exception.DatastoreVersionNotFound as e:
print(e)
def datastore_version_volume_type_list(self, datastore_name,
datastore_version_name):
"""Lists volume type association with a given datastore."""
try:
dsmetadata = datastore_models.DatastoreVersionMetadata
vtlist = dsmetadata.list_datastore_volume_type_associations(
datastore_name, datastore_version_name)
if vtlist.count() > 0:
for volume_type in vtlist:
print ("Datastore: %s, Version: %s, Volume Type: %s" %
(datastore_name, datastore_version_name,
volume_type.value))
else:
print("No Volume Type Associations found for Datastore: %s, "
"Version: %s." %
(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))
@ -205,6 +251,33 @@ def main():
'datastore version.')
parser.add_argument('flavor_id', help='The flavor to be deleted for '
'a given datastore and datastore version.')
parser = subparser.add_parser(
'datastore_version_volume_type_add', help='Adds volume_type '
'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('volume_type_ids', help='Comma separated list of '
'volume_type ids.')
parser = subparser.add_parser(
'datastore_version_volume_type_delete',
help='Deletes a volume_type '
'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('volume_type_id', help='The volume_type to be '
'deleted for a given datastore and datastore '
'version.')
parser = subparser.add_parser(
'datastore_version_volume_type_list',
help='Lists the volume_types '
'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.')
cfg.custom_parser('action', actions)
cfg.parse_args(sys.argv)

View File

@ -25,6 +25,7 @@ from trove.instance.service import InstanceController
from trove.limits.service import LimitsController
from trove.module.service import ModuleController
from trove.versions import VersionsController
from trove.volume_type.service import VolumeTypesController
class API(wsgi.Router):
@ -36,6 +37,7 @@ class API(wsgi.Router):
self._cluster_router(mapper)
self._datastore_router(mapper)
self._flavor_router(mapper)
self._volume_type_router(mapper)
self._versions_router(mapper)
self._limits_router(mapper)
self._backups_router(mapper)
@ -66,6 +68,13 @@ class API(wsgi.Router):
action="list_associated_flavors",
conditions={'method': ['GET']}
)
mapper.connect(
"/{tenant_id}/datastores/{datastore}/versions/"
"{version_id}/volume-types",
controller=datastore_resource,
action="list_associated_volume_types",
conditions={'method': ['GET']}
)
mapper.connect("/{tenant_id}/datastores/versions/{uuid}",
controller=datastore_resource,
action="version_show_by_uuid")
@ -168,6 +177,17 @@ class API(wsgi.Router):
action="show",
conditions={'method': ['GET']})
def _volume_type_router(self, mapper):
volume_type_resource = VolumeTypesController().create_resource()
mapper.connect("/{tenant_id}/volume-types",
controller=volume_type_resource,
action="index",
conditions={'method': ['GET']})
mapper.connect("/{tenant_id}/volume-types/{id}",
controller=volume_type_resource,
action="show",
conditions={'method': ['GET']})
def _limits_router(self, mapper):
limits_resource = LimitsController().create_resource()
mapper.connect("/{tenant_id}/limits",

View File

@ -121,16 +121,41 @@ class DatastoresNotFound(NotFound):
class DatastoreFlavorAssociationNotFound(NotFound):
message = _("Flavor %(flavor_id)s is not supported for datastore "
message = _("Flavor %(id)s is not supported for datastore "
"%(datastore)s version %(datastore_version)s")
class DatastoreFlavorAssociationAlreadyExists(TroveError):
message = _("Flavor %(flavor_id)s is already associated with "
message = _("Flavor %(id)s is already associated with "
"datastore %(datastore)s version %(datastore_version)s")
class DatastoreVolumeTypeAssociationNotFound(NotFound):
message = _("The volume type %(id)s is not valid for datastore "
"%(datastore)s and version %(version_id)s.")
class DatastoreVolumeTypeAssociationAlreadyExists(TroveError):
message = _("Datastore '%(datastore)s' version %(datastore_version)s "
"and volume-type %(id)s mapping already exists.")
class DataStoreVersionVolumeTypeRequired(TroveError):
message = _("Only specific volume types are allowed for a "
"datastore %(datastore)s version %(datastore_version)s. "
"You must specify a valid volume type.")
class DatastoreVersionNoVolumeTypes(TroveError):
message = _("No valid volume types could be found for datastore "
"%(datastore)s and version %(datastore_version)s.")
class DatastoreNoVersion(TroveError):
message = _("Datastore '%(datastore)s' has no version '%(version)s'.")

View File

@ -25,7 +25,7 @@ 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
from trove.volume_type import models as volume_type_models
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
@ -594,16 +594,37 @@ def update_datastore_version(datastore, name, manager, image_id, packages,
class DatastoreVersionMetadata(object):
@classmethod
def _datastore_version_find(cls, datastore_name,
datastore_version_name):
"""
Helper to find a datastore version id for a given
datastore and datastore version name.
"""
db_api.configure_db(CONF)
db_ds_record = DBDatastore.find_by(
name=datastore_name
)
db_dsv_record = DBDatastoreVersion.find_by(
datastore_id=db_ds_record.id,
name=datastore_version_name
)
return db_dsv_record.id
@classmethod
def _datastore_version_metadata_add(cls, datastore_name,
datastore_version_name,
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
"""
Create a record of the specified key and value in the
metadata table.
"""
# if an association does not exist, create a new one.
# if a deleted association exists, undelete it.
# if an un-deleted association exists, raise an exception.
try:
db_record = DBDatastoreVersionMetadata.find_by(
datastore_version_id=datastore_version_id,
@ -617,9 +638,11 @@ class DatastoreVersionMetadata(object):
raise exception_class(
datastore=datastore_name,
datastore_version=datastore_version_name,
flavor_id=value)
id=value)
except exception.NotFound:
pass
# the record in the database only contains the datastore_verion_id
DBDatastoreVersionMetadata.create(
datastore_version_id=datastore_version_id,
key=key, value=value)
@ -627,8 +650,19 @@ class DatastoreVersionMetadata(object):
@classmethod
def _datastore_version_metadata_delete(cls, datastore_name,
datastore_version_name,
datastore_version_id,
key, value, exception_class):
"""
Delete a record of the specified key and value in the
metadata table.
"""
# if an association does not exist, raise an exception
# if a deleted association exists, raise an exception
# if an un-deleted association exists, delete it
datastore_version_id = cls._datastore_version_find(
datastore_name,
datastore_version_name)
try:
db_record = DBDatastoreVersionMetadata.find_by(
datastore_version_id=datastore_version_id,
@ -640,26 +674,20 @@ class DatastoreVersionMetadata(object):
raise exception_class(
datastore=datastore_name,
datastore_version=datastore_version_name,
flavor_id=value)
id=value)
except exception.ModelNotFoundError:
raise exception_class(datastore=datastore_name,
datastore_version=datastore_version_name,
flavor_id=value)
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
datastore_version_id = cls._datastore_version_find(
datastore_name,
datastore_version_name)
for flavor_id in flavor_ids:
cls._datastore_version_metadata_add(
datastore_name, datastore_version_name,
@ -670,19 +698,8 @@ class DatastoreVersionMetadata(object):
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_name, datastore_version_name,
datastore_version_id, 'flavor', flavor_id,
datastore_name, datastore_version_name, 'flavor', flavor_id,
exception.DatastoreFlavorAssociationNotFound)
@classmethod
@ -721,3 +738,138 @@ class DatastoreVersionMetadata(object):
else:
msg = _("Specify both the datastore and datastore_version_id.")
raise exception.BadRequest(msg)
@classmethod
def add_datastore_version_volume_type_association(cls, datastore_name,
datastore_version_name,
volume_type_names):
datastore_version_id = cls._datastore_version_find(
datastore_name,
datastore_version_name)
# the database record will contain
# datastore_version_id, 'volume_type', volume_type_name
for volume_type_name in volume_type_names:
cls._datastore_version_metadata_add(
datastore_name, datastore_version_name,
datastore_version_id, 'volume_type', volume_type_name,
exception.DatastoreVolumeTypeAssociationAlreadyExists)
@classmethod
def delete_datastore_version_volume_type_association(
cls, datastore_name,
datastore_version_name,
volume_type_name):
cls._datastore_version_metadata_delete(
datastore_name, datastore_version_name, 'volume_type',
volume_type_name,
exception.DatastoreVolumeTypeAssociationNotFound)
@classmethod
def list_datastore_version_volume_type_associations(cls,
datastore_version_id):
"""
List the datastore associations for a given datastore version id as
found in datastore version metadata. Note that this may return an
empty set (if no associations are provided)
"""
if datastore_version_id:
return DBDatastoreVersionMetadata.find_all(
datastore_version_id=datastore_version_id,
key='volume_type', deleted=False
)
else:
msg = _("Specify the datastore_version_id.")
raise exception.BadRequest(msg)
@classmethod
def list_datastore_volume_type_associations(cls,
datastore_name,
datastore_version_name):
"""
List the datastore associations for a given datastore and version.
"""
if datastore_name and datastore_version_name:
datastore_version_id = cls._datastore_version_find(
datastore_name, datastore_version_name)
return cls.list_datastore_version_volume_type_associations(
datastore_version_id)
else:
msg = _("Specify the datastore_name and datastore_version_name.")
raise exception.BadRequest(msg)
@classmethod
def datastore_volume_type_associations_exist(cls,
datastore_name,
datastore_version_name):
return cls.list_datastore_volume_type_associations(
datastore_name,
datastore_version_name).count() > 0
@classmethod
def allowed_datastore_version_volume_types(cls, context,
datastore_name,
datastore_version_name):
"""
List all allowed volume types for a given datastore and
datastore version. If datastore version metadata is
provided, then the valid volume types in that list are
allowed. If datastore version metadata is not provided
then all volume types known to cinder are allowed.
"""
if datastore_name and datastore_version_name:
# first obtain the list in the dsvmetadata
datastore_version_id = cls._datastore_version_find(
datastore_name, datastore_version_name)
metadata = cls.list_datastore_version_volume_type_associations(
datastore_version_id)
# then get the list of all volume types
all_volume_types = volume_type_models.VolumeTypes(context)
# if there's metadata: intersect,
# else, whatever cinder has.
if (metadata.count() != 0):
# the volume types from metadata first
ds_volume_types = tuple(f.value for f in metadata)
# Cinder volume type names are unique, intersect
allowed_volume_types = tuple(
f for f in all_volume_types
if ((f.name in ds_volume_types) or
(f.id in ds_volume_types)))
else:
allowed_volume_types = tuple(all_volume_types)
return allowed_volume_types
else:
msg = _("Specify the datastore_name and datastore_version_name.")
raise exception.BadRequest(msg)
@classmethod
def validate_volume_type(cls, context, volume_type,
datastore_name, datastore_version_name):
if cls.datastore_volume_type_associations_exist(
datastore_name, datastore_version_name):
allowed = cls.allowed_datastore_version_volume_types(
context, datastore_name, datastore_version_name)
if len(allowed) == 0:
raise exception.DatastoreVersionNoVolumeTypes(
datastore=datastore_name,
datastore_version=datastore_version_name)
if volume_type is None:
raise exception.DataStoreVersionVolumeTypeRequired(
datastore=datastore_name,
datastore_version=datastore_version_name)
allowed_names = tuple(f.name for f in allowed)
for n in allowed_names:
LOG.debug("Volume Type: %s is allowed for datastore "
"%s, version %s." %
(n, datastore_name, datastore_version_name))
if volume_type not in allowed_names:
raise exception.DatastoreVolumeTypeAssociationNotFound(
datastore=datastore_name,
version_id=datastore_version_name,
id=volume_type)

View File

@ -20,6 +20,7 @@ from trove.common import policy
from trove.common import wsgi
from trove.datastore import models, views
from trove.flavor import views as flavor_views
from trove.volume_type import views as volume_type_view
class DatastoreController(wsgi.Controller):
@ -90,3 +91,17 @@ class DatastoreController(wsgi.Controller):
list_datastore_version_flavor_associations(
context, datastore, version_id))
return wsgi.Result(flavor_views.FlavorsView(flavors, req).data(), 200)
def list_associated_volume_types(self, req, tenant_id, datastore,
version_id):
"""
Return all known volume types if no restrictions have been
established in datastore_version_metadata, otherwise return
that restricted set.
"""
context = req.environ[wsgi.CONTEXT_KEY]
volume_types = (models.DatastoreVersionMetadata.
allowed_datastore_version_volume_types(
context, datastore, version_id))
return wsgi.Result(volume_type_view.VolumeTypesView(
volume_types, req).data(), 200)

View File

@ -43,6 +43,7 @@ from trove.common.trove_remote import create_trove_client
from trove.common import utils
from trove.configuration.models import Configuration
from trove.datastore import models as datastore_models
from trove.datastore.models import DatastoreVersionMetadata as dvm
from trove.datastore.models import DBDatastoreVersionMetadata
from trove.db import get_db_api
from trove.db import models as dbmodels
@ -893,6 +894,9 @@ class Instance(BuiltInstance):
deltas = {'instances': 1}
volume_support = datastore_cfg.volume_support
if volume_support:
call_args['volume_type'] = volume_type
dvm.validate_volume_type(context, volume_type,
datastore.name, datastore_version.name)
call_args['volume_size'] = volume_size
validate_volume_size(volume_size)
deltas['volumes'] = volume_size

View File

@ -322,7 +322,7 @@ class FakeGuest(object):
backup.checksum = 'fake-md5-sum'
backup.size = BACKUP_SIZE
backup.save()
eventlet.spawn_after(8, finish_create_backup)
eventlet.spawn_after(10, finish_create_backup)
def mount_volume(self, device_path=None, mount_point=None):
pass

View File

@ -37,6 +37,7 @@ class TestDatastoreBase(trove_testtools.TestCase):
self.capability_enabled = True
self.datastore_version_id = str(uuid.uuid4())
self.flavor_id = 1
self.volume_type = 'some-valid-volume-type'
datastore_models.update_datastore(self.ds_name, False)
self.datastore = Datastore.load(self.ds_name)
@ -45,6 +46,8 @@ class TestDatastoreBase(trove_testtools.TestCase):
self.ds_name, self.ds_version, "mysql", "", "", True)
DatastoreVersionMetadata.add_datastore_version_flavor_association(
self.ds_name, self.ds_version, [self.flavor_id])
DatastoreVersionMetadata.add_datastore_version_volume_type_association(
self.ds_name, self.ds_version, [self.volume_type])
self.datastore_version = DatastoreVersion.load(self.datastore,
self.ds_version)

View File

@ -12,7 +12,10 @@
# License for the specific language governing permissions and limitations
# under the License.
import mock
from trove.common import exception
from trove.common import remote
from trove.datastore import models as datastore_models
from trove.tests.unittests.datastore.base import TestDatastoreBase
@ -20,6 +23,12 @@ from trove.tests.unittests.datastore.base import TestDatastoreBase
class TestDatastoreVersionMetadata(TestDatastoreBase):
def setUp(self):
super(TestDatastoreVersionMetadata, self).setUp()
self.dsmetadata = datastore_models.DatastoreVersionMetadata
self.volume_types = [
{'id': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'name': 'type_1'},
{'id': 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', 'name': 'type_2'},
{'id': 'cccccccc-cccc-cccc-cccc-cccccccccccc', 'name': 'type_3'},
]
def tearDown(self):
super(TestDatastoreVersionMetadata, self).tearDown()
@ -35,7 +44,18 @@ class TestDatastoreVersionMetadata(TestDatastoreBase):
self.assertEqual(ds_version.id, mapping.datastore_version_id)
self.assertEqual('flavor', str(mapping.key))
def test_add_existing_associations(self):
def test_map_volume_types_to_datastores(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.volume_type, deleted=False, key='volume_type')
self.assertEqual(str(self.volume_type), mapping.value)
self.assertEqual(ds_version.id, mapping.datastore_version_id)
self.assertEqual('volume_type', str(mapping.key))
def test_add_existing_flavor_associations(self):
dsmetadata = datastore_models.DatastoreVersionMetadata
self.assertRaisesRegexp(
exception.DatastoreFlavorAssociationAlreadyExists,
@ -44,7 +64,14 @@ class TestDatastoreVersionMetadata(TestDatastoreBase):
dsmetadata.add_datastore_version_flavor_association,
self.ds_name, self.ds_version, [self.flavor_id])
def test_delete_nonexistent_mapping(self):
def test_add_existing_volume_type_associations(self):
dsmetadata = datastore_models.DatastoreVersionMetadata
self.assertRaises(
exception.DatastoreVolumeTypeAssociationAlreadyExists,
dsmetadata.add_datastore_version_volume_type_association,
self.ds_name, self.ds_version, [self.volume_type])
def test_delete_nonexistent_flavor_mapping(self):
dsmeta = datastore_models.DatastoreVersionMetadata
self.assertRaisesRegexp(
exception.DatastoreFlavorAssociationNotFound,
@ -53,7 +80,15 @@ class TestDatastoreVersionMetadata(TestDatastoreBase):
dsmeta.delete_datastore_version_flavor_association,
self.ds_name, self.ds_version, flavor_id=2)
def test_delete_mapping(self):
def test_delete_nonexistent_volume_type_mapping(self):
dsmeta = datastore_models.DatastoreVersionMetadata
self.assertRaises(
exception.DatastoreVolumeTypeAssociationNotFound,
dsmeta.delete_datastore_version_volume_type_association,
self.ds_name, self.ds_version,
volume_type_name='some random thing')
def test_delete_flavor_mapping(self):
flavor_id = 2
dsmetadata = datastore_models. DatastoreVersionMetadata
dsmetadata.add_datastore_version_flavor_association(self.ds_name,
@ -79,3 +114,90 @@ class TestDatastoreVersionMetadata(TestDatastoreBase):
delete_datastore_version_flavor_association(self.ds_name,
self.ds_version,
flavor_id)
def test_delete_volume_type_mapping(self):
volume_type = 'this is bogus'
dsmetadata = datastore_models. DatastoreVersionMetadata
dsmetadata.add_datastore_version_volume_type_association(
self.ds_name,
self.ds_version,
[volume_type])
dsmetadata.delete_datastore_version_volume_type_association(
self.ds_name,
self.ds_version,
volume_type)
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=volume_type,
key='volume_type')
self.assertTrue(mapping.deleted)
# check update
dsmetadata.add_datastore_version_volume_type_association(
self.ds_name, self.ds_version, [volume_type])
mapping = datastore_models.DBDatastoreVersionMetadata.find_by(
datastore_version_id=ds_version.id, value=volume_type,
key='volume_type')
self.assertFalse(mapping.deleted)
# clear the mapping
dsmetadata.delete_datastore_version_volume_type_association(
self.ds_name,
self.ds_version,
volume_type)
@mock.patch.object(datastore_models.DatastoreVersionMetadata,
'_datastore_version_find')
@mock.patch.object(datastore_models.DatastoreVersionMetadata,
'list_datastore_version_volume_type_associations')
@mock.patch.object(remote, 'create_cinder_client')
def _mocked_allowed_datastore_version_volume_types(self,
trove_volume_types,
mock_cinder_client,
mock_list, *args):
"""Call this with a list of strings specifying volume types."""
cinder_vts = []
for vt in self.volume_types:
cinder_type = mock.Mock()
cinder_type.id = vt.get('id')
cinder_type.name = vt.get('name')
cinder_vts.append(cinder_type)
mock_cinder_client.return_value.volume_types.list.return_value = (
cinder_vts)
mock_trove_list_result = mock.MagicMock()
mock_trove_list_result.count.return_value = len(trove_volume_types)
mock_trove_list_result.__iter__.return_value = []
for trove_vt in trove_volume_types:
trove_type = mock.Mock()
trove_type.value = trove_vt
mock_trove_list_result.__iter__.return_value.append(trove_type)
mock_list.return_value = mock_trove_list_result
return self.dsmetadata.allowed_datastore_version_volume_types(
None, 'ds', 'dsv')
def _assert_equal_types(self, test_dict, output_obj):
self.assertEqual(test_dict.get('id'), output_obj.id)
self.assertEqual(test_dict.get('name'), output_obj.name)
def test_allowed_volume_types_from_ids(self):
id1 = self.volume_types[0].get('id')
id2 = self.volume_types[1].get('id')
res = self._mocked_allowed_datastore_version_volume_types([id1, id2])
self._assert_equal_types(self.volume_types[0], res[0])
self._assert_equal_types(self.volume_types[1], res[1])
def test_allowed_volume_types_from_names(self):
name1 = self.volume_types[0].get('name')
name2 = self.volume_types[1].get('name')
res = self._mocked_allowed_datastore_version_volume_types([name1,
name2])
self._assert_equal_types(self.volume_types[0], res[0])
self._assert_equal_types(self.volume_types[1], res[1])
def test_allowed_volume_types_no_restrictions(self):
res = self._mocked_allowed_datastore_version_volume_types([])
self._assert_equal_types(self.volume_types[0], res[0])
self._assert_equal_types(self.volume_types[1], res[1])
self._assert_equal_types(self.volume_types[2], res[2])

View File

@ -376,7 +376,7 @@ class TestReplication(trove_testtools.TestCase):
self.master_status.save()
self.assertRaises(exception.UnprocessableEntity,
Instance.create,
None, 'name', 1, "UUID", [], [], None,
None, 'name', 1, "UUID", [], [], self.datastore,
self.datastore_version, 1,
None, slave_of_id=self.master.id)
@ -384,7 +384,7 @@ class TestReplication(trove_testtools.TestCase):
def test_replica_with_invalid_slave_of_id(self, mock_logging):
self.assertRaises(exception.NotFound,
Instance.create,
None, 'name', 1, "UUID", [], [], None,
None, 'name', 1, "UUID", [], [], self.datastore,
self.datastore_version, 1,
None, slave_of_id=str(uuid.uuid4()))
@ -401,7 +401,7 @@ class TestReplication(trove_testtools.TestCase):
slave_of_id=self.master.id)
self.replica_info.save()
self.assertRaises(exception.Forbidden, Instance.create,
None, 'name', 2, "UUID", [], [], None,
None, 'name', 2, "UUID", [], [], self.datastore,
self.datastore_version, 1,
None, slave_of_id=self.replica_info.id)

View File

@ -0,0 +1,52 @@
# Copyright 2016 Tesora, 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.
#
import mock
from trove.common import remote
from trove.tests.unittests import trove_testtools
from trove.volume_type import models
class TestVolumeType(trove_testtools.TestCase):
def test_volume_type(self):
cinder_volume_type = mock.MagicMock()
cinder_volume_type.id = 123
cinder_volume_type.name = 'test_type'
cinder_volume_type.is_public = True
cinder_volume_type.description = 'Test volume type'
volume_type = models.VolumeType(cinder_volume_type)
self.assertEqual(cinder_volume_type.id, volume_type.id)
self.assertEqual(cinder_volume_type.name, volume_type.name)
self.assertEqual(cinder_volume_type.is_public, volume_type.is_public)
self.assertEqual(cinder_volume_type.description,
volume_type.description)
@mock.patch.object(remote, 'create_cinder_client')
def test_volume_types(self, mock_client):
mock_context = mock.MagicMock()
mock_types = [mock.MagicMock(), mock.MagicMock()]
mock_client(mock_context).volume_types.list.return_value = mock_types
volume_types = models.VolumeTypes(mock_context)
for i, volume_type in enumerate(volume_types):
self.assertEqual(mock_types[i], volume_type.volume_type,
"Volume type {} does not match.".format(i))

View File

@ -0,0 +1,60 @@
# Copyright 2016 Tesora, 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.
#
import mock
from trove.tests.unittests import trove_testtools
from trove.volume_type import views
class TestVolumeTypeViews(trove_testtools.TestCase):
def test_volume_type_view(self):
test_id = 'test_id'
test_name = 'test_name'
test_is_public = True
test_description = 'Test description'
test_req = mock.MagicMock()
volume_type = mock.MagicMock()
volume_type.id = test_id
volume_type.name = test_name
volume_type.is_public = test_is_public
volume_type.description = test_description
volume_type_view = views.VolumeTypeView(volume_type, req=test_req)
data = volume_type_view.data()
self.assertEqual(volume_type, volume_type_view.volume_type)
self.assertEqual(test_req, volume_type_view.req)
self.assertEqual(test_id, data['volume_type']['id'])
self.assertEqual(test_name, data['volume_type']['name'])
self.assertEqual(test_is_public, data['volume_type']['is_public'])
self.assertEqual(test_description, data['volume_type']['description'])
self.assertEqual(test_req, volume_type_view.req)
@mock.patch.object(views, 'VolumeTypeView')
def test_volume_types_view(self, mock_single_view):
test_type_1 = mock.MagicMock()
test_type_2 = mock.MagicMock()
volume_types_view = views.VolumeTypesView([test_type_1, test_type_2])
self.assertEqual(
{'volume_types': [
mock_single_view(test_type_1, None).data()['volume_type'],
mock_single_view(test_type_2, None).data()['volume_type']]},
volume_types_view.data())

View File

View File

@ -0,0 +1,74 @@
# Copyright 2016 Tesora, 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.
"""Model classes that form the core of volume-support functionality"""
from cinderclient import exceptions as cinder_exception
from trove.common import exception as trove_exception
from trove.common import models
from trove.common import remote
class VolumeType(object):
_data_fields = ['id', 'name', 'is_public', 'description']
def __init__(self, volume_type=None):
"""Initialize a cinder client volume_type object"""
self.volume_type = volume_type
@classmethod
def load(cls, volume_type_id, context=None, client=None):
if not(client or context):
raise trove_exception.InvalidModelError(
"client or context must be provided to load a volume_type")
if not client:
client = remote.create_cinder_client(context)
try:
volume_type = client.volume_types.get(volume_type_id)
except cinder_exception.NotFound:
raise trove_exception.NotFound(uuid=volume_type_id)
except cinder_exception.ClientException as ce:
raise trove_exception.TroveError(str(ce))
return cls(volume_type)
@property
def id(self):
return self.volume_type.id
@property
def name(self):
return self.volume_type.name
@property
def is_public(self):
return self.volume_type.is_public
@property
def description(self):
return self.volume_type.description
class VolumeTypes(models.CinderRemoteModelBase):
def __init__(self, context):
volume_types = remote.create_cinder_client(context).volume_types.list()
self.volume_types = [VolumeType(volume_type=item)
for item in volume_types]
def __iter__(self):
for item in self.volume_types:
yield item

View File

@ -0,0 +1,36 @@
# Copyright 2016 Tesora, 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 trove.common import wsgi
from trove.volume_type import models
from trove.volume_type import views
class VolumeTypesController(wsgi.Controller):
"""A controller for the Cinder Volume Types functionality."""
def show(self, req, tenant_id, id):
"""Return a single volume type."""
context = req.environ[wsgi.CONTEXT_KEY]
volume_type = models.VolumeType.load(id, context=context)
return wsgi.Result(views.VolumeTypeView(volume_type, req).data(), 200)
def index(self, req, tenant_id):
"""Return all volume types."""
context = req.environ[wsgi.CONTEXT_KEY]
volume_types = models.VolumeTypes(context=context)
return wsgi.Result(views.VolumeTypesView(volume_types,
req).data(), 200)

View File

@ -0,0 +1,46 @@
# Copyright 2016 Tesora, 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.
class VolumeTypeView(object):
def __init__(self, volume_type, req=None):
self.volume_type = volume_type
self.req = req
def data(self):
volume_type = {
'id': self.volume_type.id,
'name': self.volume_type.name,
'is_public': self.volume_type.is_public,
'description': self.volume_type.description
}
return {"volume_type": volume_type}
class VolumeTypesView(object):
def __init__(self, volume_types, req=None):
self.volume_types = volume_types
self.req = req
def data(self):
data = []
for volume_type in self.volume_types:
data.append(VolumeTypeView(volume_type,
req=self.req).data()['volume_type'])
return {"volume_types": data}