Merge "Associate datastore, version with volume-type"
This commit is contained in:
commit
0cff23d8d1
@ -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.
|
@ -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)
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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'.")
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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])
|
||||
|
@ -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)
|
||||
|
||||
|
0
trove/tests/unittests/volume_type/__init__.py
Normal file
0
trove/tests/unittests/volume_type/__init__.py
Normal file
52
trove/tests/unittests/volume_type/test_volume_type.py
Normal file
52
trove/tests/unittests/volume_type/test_volume_type.py
Normal 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))
|
60
trove/tests/unittests/volume_type/test_volume_type_views.py
Normal file
60
trove/tests/unittests/volume_type/test_volume_type_views.py
Normal 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())
|
0
trove/volume_type/__init__.py
Normal file
0
trove/volume_type/__init__.py
Normal file
74
trove/volume_type/models.py
Normal file
74
trove/volume_type/models.py
Normal 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
|
36
trove/volume_type/service.py
Normal file
36
trove/volume_type/service.py
Normal 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)
|
46
trove/volume_type/views.py
Normal file
46
trove/volume_type/views.py
Normal 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}
|
Loading…
Reference in New Issue
Block a user