Associate datastore, version with volume-type
Cinder supports multiple volume types and volume types can be explicitly requested in create requests. This change allows users to restrict the allowed volume types for a given datastore/version in a manner similar to flavors. Co-Authored-By: amrith <amrith@tesora.com> Change-Id: I790751ade042e271ba1cc902a8ef4d3c3a8dc557 Implements: blueprint associate-volume-type-datastore
This commit is contained in:
parent
8adfb7e4f2
commit
610354413e
@ -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:
|
except exception.DatastoreVersionNotFound as e:
|
||||||
print(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):
|
def params_of(self, command_name):
|
||||||
if Commands.has(command_name):
|
if Commands.has(command_name):
|
||||||
return utils.MethodInspector(getattr(self, command_name))
|
return utils.MethodInspector(getattr(self, command_name))
|
||||||
@ -205,6 +251,33 @@ def main():
|
|||||||
'datastore version.')
|
'datastore version.')
|
||||||
parser.add_argument('flavor_id', help='The flavor to be deleted for '
|
parser.add_argument('flavor_id', help='The flavor to be deleted for '
|
||||||
'a given datastore and datastore version.')
|
'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.custom_parser('action', actions)
|
||||||
cfg.parse_args(sys.argv)
|
cfg.parse_args(sys.argv)
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ from trove.instance.service import InstanceController
|
|||||||
from trove.limits.service import LimitsController
|
from trove.limits.service import LimitsController
|
||||||
from trove.module.service import ModuleController
|
from trove.module.service import ModuleController
|
||||||
from trove.versions import VersionsController
|
from trove.versions import VersionsController
|
||||||
|
from trove.volume_type.service import VolumeTypesController
|
||||||
|
|
||||||
|
|
||||||
class API(wsgi.Router):
|
class API(wsgi.Router):
|
||||||
@ -36,6 +37,7 @@ class API(wsgi.Router):
|
|||||||
self._cluster_router(mapper)
|
self._cluster_router(mapper)
|
||||||
self._datastore_router(mapper)
|
self._datastore_router(mapper)
|
||||||
self._flavor_router(mapper)
|
self._flavor_router(mapper)
|
||||||
|
self._volume_type_router(mapper)
|
||||||
self._versions_router(mapper)
|
self._versions_router(mapper)
|
||||||
self._limits_router(mapper)
|
self._limits_router(mapper)
|
||||||
self._backups_router(mapper)
|
self._backups_router(mapper)
|
||||||
@ -66,6 +68,13 @@ class API(wsgi.Router):
|
|||||||
action="list_associated_flavors",
|
action="list_associated_flavors",
|
||||||
conditions={'method': ['GET']}
|
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}",
|
mapper.connect("/{tenant_id}/datastores/versions/{uuid}",
|
||||||
controller=datastore_resource,
|
controller=datastore_resource,
|
||||||
action="version_show_by_uuid")
|
action="version_show_by_uuid")
|
||||||
@ -168,6 +177,17 @@ class API(wsgi.Router):
|
|||||||
action="show",
|
action="show",
|
||||||
conditions={'method': ['GET']})
|
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):
|
def _limits_router(self, mapper):
|
||||||
limits_resource = LimitsController().create_resource()
|
limits_resource = LimitsController().create_resource()
|
||||||
mapper.connect("/{tenant_id}/limits",
|
mapper.connect("/{tenant_id}/limits",
|
||||||
@ -227,6 +247,10 @@ class API(wsgi.Router):
|
|||||||
controller=modules_resource,
|
controller=modules_resource,
|
||||||
action="instances",
|
action="instances",
|
||||||
conditions={'method': ['GET']})
|
conditions={'method': ['GET']})
|
||||||
|
mapper.connect("/{tenant_id}/modules/{id}/instances",
|
||||||
|
controller=modules_resource,
|
||||||
|
action="reapply",
|
||||||
|
conditions={'method': ['PUT']})
|
||||||
|
|
||||||
def _configurations_router(self, mapper):
|
def _configurations_router(self, mapper):
|
||||||
parameters_resource = ParametersController().create_resource()
|
parameters_resource = ParametersController().create_resource()
|
||||||
|
@ -121,16 +121,41 @@ class DatastoresNotFound(NotFound):
|
|||||||
|
|
||||||
class DatastoreFlavorAssociationNotFound(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")
|
"%(datastore)s version %(datastore_version)s")
|
||||||
|
|
||||||
|
|
||||||
class DatastoreFlavorAssociationAlreadyExists(TroveError):
|
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")
|
"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):
|
class DatastoreNoVersion(TroveError):
|
||||||
|
|
||||||
message = _("Datastore '%(datastore)s' has no version '%(version)s'.")
|
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 get_db_api
|
||||||
from trove.db import models as dbmodels
|
from trove.db import models as dbmodels
|
||||||
from trove.flavor.models import Flavor as flavor_model
|
from trove.flavor.models import Flavor as flavor_model
|
||||||
|
from trove.volume_type import models as volume_type_models
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
@ -519,6 +519,38 @@ def get_datastore_version(type=None, version=None, return_inactive=False):
|
|||||||
return (datastore, datastore_version)
|
return (datastore, datastore_version)
|
||||||
|
|
||||||
|
|
||||||
|
def get_datastore_or_version(datastore=None, datastore_version=None):
|
||||||
|
"""
|
||||||
|
Validate that the specified datastore/version exists, and return the
|
||||||
|
corresponding ids. This differs from 'get_datastore_version' in that
|
||||||
|
you don't need to specify both - specifying only a datastore will
|
||||||
|
return 'None' in the ds_ver field. Raises DatastoreNoVersion if
|
||||||
|
you pass in a ds_ver without a ds. Originally designed for module
|
||||||
|
management.
|
||||||
|
|
||||||
|
:param datastore: Datastore name or id
|
||||||
|
:param datastore_version: Version name or id
|
||||||
|
:return: Tuple of ds_id, ds_ver_id if found
|
||||||
|
"""
|
||||||
|
|
||||||
|
datastore_id = None
|
||||||
|
datastore_version_id = None
|
||||||
|
if datastore:
|
||||||
|
if datastore_version:
|
||||||
|
ds, ds_ver = get_datastore_version(
|
||||||
|
type=datastore, version=datastore_version)
|
||||||
|
datastore_id = ds.id
|
||||||
|
datastore_version_id = ds_ver.id
|
||||||
|
else:
|
||||||
|
ds = Datastore.load(datastore)
|
||||||
|
datastore_id = ds.id
|
||||||
|
elif datastore_version:
|
||||||
|
# Cannot specify version without datastore.
|
||||||
|
raise exception.DatastoreNoVersion(
|
||||||
|
datastore=datastore, version=datastore_version)
|
||||||
|
return datastore_id, datastore_version_id
|
||||||
|
|
||||||
|
|
||||||
def update_datastore(name, default_version):
|
def update_datastore(name, default_version):
|
||||||
db_api.configure_db(CONF)
|
db_api.configure_db(CONF)
|
||||||
try:
|
try:
|
||||||
@ -562,16 +594,37 @@ def update_datastore_version(datastore, name, manager, image_id, packages,
|
|||||||
|
|
||||||
|
|
||||||
class DatastoreVersionMetadata(object):
|
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
|
@classmethod
|
||||||
def _datastore_version_metadata_add(cls, datastore_name,
|
def _datastore_version_metadata_add(cls, datastore_name,
|
||||||
datastore_version_name,
|
datastore_version_name,
|
||||||
datastore_version_id,
|
datastore_version_id,
|
||||||
key, value, exception_class):
|
key, value, exception_class):
|
||||||
"""Create an entry in the Datastore Version Metadata table."""
|
"""
|
||||||
# Do we have a mapping in the db?
|
Create a record of the specified key and value in the
|
||||||
# yes: and its deleted then modify the association
|
metadata table.
|
||||||
# yes: and its not deleted then error on create
|
"""
|
||||||
# no: then just create the new association
|
# 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:
|
try:
|
||||||
db_record = DBDatastoreVersionMetadata.find_by(
|
db_record = DBDatastoreVersionMetadata.find_by(
|
||||||
datastore_version_id=datastore_version_id,
|
datastore_version_id=datastore_version_id,
|
||||||
@ -585,9 +638,11 @@ class DatastoreVersionMetadata(object):
|
|||||||
raise exception_class(
|
raise exception_class(
|
||||||
datastore=datastore_name,
|
datastore=datastore_name,
|
||||||
datastore_version=datastore_version_name,
|
datastore_version=datastore_version_name,
|
||||||
flavor_id=value)
|
id=value)
|
||||||
except exception.NotFound:
|
except exception.NotFound:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# the record in the database only contains the datastore_verion_id
|
||||||
DBDatastoreVersionMetadata.create(
|
DBDatastoreVersionMetadata.create(
|
||||||
datastore_version_id=datastore_version_id,
|
datastore_version_id=datastore_version_id,
|
||||||
key=key, value=value)
|
key=key, value=value)
|
||||||
@ -595,8 +650,19 @@ class DatastoreVersionMetadata(object):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def _datastore_version_metadata_delete(cls, datastore_name,
|
def _datastore_version_metadata_delete(cls, datastore_name,
|
||||||
datastore_version_name,
|
datastore_version_name,
|
||||||
datastore_version_id,
|
|
||||||
key, value, exception_class):
|
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:
|
try:
|
||||||
db_record = DBDatastoreVersionMetadata.find_by(
|
db_record = DBDatastoreVersionMetadata.find_by(
|
||||||
datastore_version_id=datastore_version_id,
|
datastore_version_id=datastore_version_id,
|
||||||
@ -608,26 +674,20 @@ class DatastoreVersionMetadata(object):
|
|||||||
raise exception_class(
|
raise exception_class(
|
||||||
datastore=datastore_name,
|
datastore=datastore_name,
|
||||||
datastore_version=datastore_version_name,
|
datastore_version=datastore_version_name,
|
||||||
flavor_id=value)
|
id=value)
|
||||||
except exception.ModelNotFoundError:
|
except exception.ModelNotFoundError:
|
||||||
raise exception_class(datastore=datastore_name,
|
raise exception_class(datastore=datastore_name,
|
||||||
datastore_version=datastore_version_name,
|
datastore_version=datastore_version_name,
|
||||||
flavor_id=value)
|
id=value)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add_datastore_version_flavor_association(cls, datastore_name,
|
def add_datastore_version_flavor_association(cls, datastore_name,
|
||||||
datastore_version_name,
|
datastore_version_name,
|
||||||
flavor_ids):
|
flavor_ids):
|
||||||
db_api.configure_db(CONF)
|
datastore_version_id = cls._datastore_version_find(
|
||||||
db_ds_record = DBDatastore.find_by(
|
datastore_name,
|
||||||
name=datastore_name
|
datastore_version_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:
|
for flavor_id in flavor_ids:
|
||||||
cls._datastore_version_metadata_add(
|
cls._datastore_version_metadata_add(
|
||||||
datastore_name, datastore_version_name,
|
datastore_name, datastore_version_name,
|
||||||
@ -638,19 +698,8 @@ class DatastoreVersionMetadata(object):
|
|||||||
def delete_datastore_version_flavor_association(cls, datastore_name,
|
def delete_datastore_version_flavor_association(cls, datastore_name,
|
||||||
datastore_version_name,
|
datastore_version_name,
|
||||||
flavor_id):
|
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(
|
cls._datastore_version_metadata_delete(
|
||||||
datastore_name, datastore_version_name,
|
datastore_name, datastore_version_name, 'flavor', flavor_id,
|
||||||
datastore_version_id, 'flavor', flavor_id,
|
|
||||||
exception.DatastoreFlavorAssociationNotFound)
|
exception.DatastoreFlavorAssociationNotFound)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -689,3 +738,138 @@ class DatastoreVersionMetadata(object):
|
|||||||
else:
|
else:
|
||||||
msg = _("Specify both the datastore and datastore_version_id.")
|
msg = _("Specify both the datastore and datastore_version_id.")
|
||||||
raise exception.BadRequest(msg)
|
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.common import wsgi
|
||||||
from trove.datastore import models, views
|
from trove.datastore import models, views
|
||||||
from trove.flavor import views as flavor_views
|
from trove.flavor import views as flavor_views
|
||||||
|
from trove.volume_type import views as volume_type_view
|
||||||
|
|
||||||
|
|
||||||
class DatastoreController(wsgi.Controller):
|
class DatastoreController(wsgi.Controller):
|
||||||
@ -90,3 +91,17 @@ class DatastoreController(wsgi.Controller):
|
|||||||
list_datastore_version_flavor_associations(
|
list_datastore_version_flavor_associations(
|
||||||
context, datastore, version_id))
|
context, datastore, version_id))
|
||||||
return wsgi.Result(flavor_views.FlavorsView(flavors, req).data(), 200)
|
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)
|
||||||
|
@ -41,6 +41,7 @@ from trove.common.trove_remote import create_trove_client
|
|||||||
from trove.common import utils
|
from trove.common import utils
|
||||||
from trove.configuration.models import Configuration
|
from trove.configuration.models import Configuration
|
||||||
from trove.datastore import models as datastore_models
|
from trove.datastore import models as datastore_models
|
||||||
|
from trove.datastore.models import DatastoreVersionMetadata as dvm
|
||||||
from trove.datastore.models import DBDatastoreVersionMetadata
|
from trove.datastore.models import DBDatastoreVersionMetadata
|
||||||
from trove.db import get_db_api
|
from trove.db import get_db_api
|
||||||
from trove.db import models as dbmodels
|
from trove.db import models as dbmodels
|
||||||
@ -879,6 +880,9 @@ class Instance(BuiltInstance):
|
|||||||
deltas = {'instances': 1}
|
deltas = {'instances': 1}
|
||||||
volume_support = datastore_cfg.volume_support
|
volume_support = datastore_cfg.volume_support
|
||||||
if 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
|
call_args['volume_size'] = volume_size
|
||||||
validate_volume_size(volume_size)
|
validate_volume_size(volume_size)
|
||||||
deltas['volumes'] = volume_size
|
deltas['volumes'] = volume_size
|
||||||
|
@ -322,7 +322,7 @@ class FakeGuest(object):
|
|||||||
backup.checksum = 'fake-md5-sum'
|
backup.checksum = 'fake-md5-sum'
|
||||||
backup.size = BACKUP_SIZE
|
backup.size = BACKUP_SIZE
|
||||||
backup.save()
|
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):
|
def mount_volume(self, device_path=None, mount_point=None):
|
||||||
pass
|
pass
|
||||||
|
@ -37,6 +37,7 @@ class TestDatastoreBase(trove_testtools.TestCase):
|
|||||||
self.capability_enabled = True
|
self.capability_enabled = True
|
||||||
self.datastore_version_id = str(uuid.uuid4())
|
self.datastore_version_id = str(uuid.uuid4())
|
||||||
self.flavor_id = 1
|
self.flavor_id = 1
|
||||||
|
self.volume_type = 'some-valid-volume-type'
|
||||||
|
|
||||||
datastore_models.update_datastore(self.ds_name, False)
|
datastore_models.update_datastore(self.ds_name, False)
|
||||||
self.datastore = Datastore.load(self.ds_name)
|
self.datastore = Datastore.load(self.ds_name)
|
||||||
@ -45,6 +46,8 @@ class TestDatastoreBase(trove_testtools.TestCase):
|
|||||||
self.ds_name, self.ds_version, "mysql", "", "", True)
|
self.ds_name, self.ds_version, "mysql", "", "", True)
|
||||||
DatastoreVersionMetadata.add_datastore_version_flavor_association(
|
DatastoreVersionMetadata.add_datastore_version_flavor_association(
|
||||||
self.ds_name, self.ds_version, [self.flavor_id])
|
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.datastore_version = DatastoreVersion.load(self.datastore,
|
||||||
self.ds_version)
|
self.ds_version)
|
||||||
|
@ -12,7 +12,10 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import mock
|
||||||
|
|
||||||
from trove.common import exception
|
from trove.common import exception
|
||||||
|
from trove.common import remote
|
||||||
from trove.datastore import models as datastore_models
|
from trove.datastore import models as datastore_models
|
||||||
from trove.tests.unittests.datastore.base import TestDatastoreBase
|
from trove.tests.unittests.datastore.base import TestDatastoreBase
|
||||||
|
|
||||||
@ -20,6 +23,12 @@ from trove.tests.unittests.datastore.base import TestDatastoreBase
|
|||||||
class TestDatastoreVersionMetadata(TestDatastoreBase):
|
class TestDatastoreVersionMetadata(TestDatastoreBase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestDatastoreVersionMetadata, self).setUp()
|
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):
|
def tearDown(self):
|
||||||
super(TestDatastoreVersionMetadata, self).tearDown()
|
super(TestDatastoreVersionMetadata, self).tearDown()
|
||||||
@ -35,7 +44,18 @@ class TestDatastoreVersionMetadata(TestDatastoreBase):
|
|||||||
self.assertEqual(ds_version.id, mapping.datastore_version_id)
|
self.assertEqual(ds_version.id, mapping.datastore_version_id)
|
||||||
self.assertEqual('flavor', str(mapping.key))
|
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
|
dsmetadata = datastore_models.DatastoreVersionMetadata
|
||||||
self.assertRaisesRegexp(
|
self.assertRaisesRegexp(
|
||||||
exception.DatastoreFlavorAssociationAlreadyExists,
|
exception.DatastoreFlavorAssociationAlreadyExists,
|
||||||
@ -44,7 +64,14 @@ class TestDatastoreVersionMetadata(TestDatastoreBase):
|
|||||||
dsmetadata.add_datastore_version_flavor_association,
|
dsmetadata.add_datastore_version_flavor_association,
|
||||||
self.ds_name, self.ds_version, [self.flavor_id])
|
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
|
dsmeta = datastore_models.DatastoreVersionMetadata
|
||||||
self.assertRaisesRegexp(
|
self.assertRaisesRegexp(
|
||||||
exception.DatastoreFlavorAssociationNotFound,
|
exception.DatastoreFlavorAssociationNotFound,
|
||||||
@ -53,7 +80,15 @@ class TestDatastoreVersionMetadata(TestDatastoreBase):
|
|||||||
dsmeta.delete_datastore_version_flavor_association,
|
dsmeta.delete_datastore_version_flavor_association,
|
||||||
self.ds_name, self.ds_version, flavor_id=2)
|
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
|
flavor_id = 2
|
||||||
dsmetadata = datastore_models. DatastoreVersionMetadata
|
dsmetadata = datastore_models. DatastoreVersionMetadata
|
||||||
dsmetadata.add_datastore_version_flavor_association(self.ds_name,
|
dsmetadata.add_datastore_version_flavor_association(self.ds_name,
|
||||||
@ -79,3 +114,90 @@ class TestDatastoreVersionMetadata(TestDatastoreBase):
|
|||||||
delete_datastore_version_flavor_association(self.ds_name,
|
delete_datastore_version_flavor_association(self.ds_name,
|
||||||
self.ds_version,
|
self.ds_version,
|
||||||
flavor_id)
|
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])
|
||||||
|
@ -374,7 +374,7 @@ class TestReplication(trove_testtools.TestCase):
|
|||||||
self.master_status.save()
|
self.master_status.save()
|
||||||
self.assertRaises(exception.UnprocessableEntity,
|
self.assertRaises(exception.UnprocessableEntity,
|
||||||
Instance.create,
|
Instance.create,
|
||||||
None, 'name', 1, "UUID", [], [], None,
|
None, 'name', 1, "UUID", [], [], self.datastore,
|
||||||
self.datastore_version, 1,
|
self.datastore_version, 1,
|
||||||
None, slave_of_id=self.master.id)
|
None, slave_of_id=self.master.id)
|
||||||
|
|
||||||
@ -382,7 +382,7 @@ class TestReplication(trove_testtools.TestCase):
|
|||||||
def test_replica_with_invalid_slave_of_id(self, mock_logging):
|
def test_replica_with_invalid_slave_of_id(self, mock_logging):
|
||||||
self.assertRaises(exception.NotFound,
|
self.assertRaises(exception.NotFound,
|
||||||
Instance.create,
|
Instance.create,
|
||||||
None, 'name', 1, "UUID", [], [], None,
|
None, 'name', 1, "UUID", [], [], self.datastore,
|
||||||
self.datastore_version, 1,
|
self.datastore_version, 1,
|
||||||
None, slave_of_id=str(uuid.uuid4()))
|
None, slave_of_id=str(uuid.uuid4()))
|
||||||
|
|
||||||
@ -399,6 +399,6 @@ class TestReplication(trove_testtools.TestCase):
|
|||||||
slave_of_id=self.master.id)
|
slave_of_id=self.master.id)
|
||||||
self.replica_info.save()
|
self.replica_info.save()
|
||||||
self.assertRaises(exception.Forbidden, Instance.create,
|
self.assertRaises(exception.Forbidden, Instance.create,
|
||||||
None, 'name', 2, "UUID", [], [], None,
|
None, 'name', 2, "UUID", [], [], self.datastore,
|
||||||
self.datastore_version, 1,
|
self.datastore_version, 1,
|
||||||
None, slave_of_id=self.replica_info.id)
|
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…
x
Reference in New Issue
Block a user