Merge "Refactor API create_volume flow"

This commit is contained in:
Jenkins 2015-06-17 17:29:13 +00:00 committed by Gerrit Code Review
commit 06e5b315ef
2 changed files with 93 additions and 181 deletions

View File

@ -277,18 +277,14 @@ class API(base.Base):
'multiattach': multiattach, 'multiattach': multiattach,
} }
try: try:
if cgsnapshot: sched_rpcapi = self.scheduler_rpcapi if not cgsnapshot else None
flow_engine = create_volume.get_flow_no_rpc(self.db, volume_rpcapi = self.volume_rpcapi if not cgsnapshot else None
self.image_service, flow_engine = create_volume.get_flow(self.db,
availability_zones, self.image_service,
create_what) availability_zones,
else: create_what,
flow_engine = create_volume.get_flow(self.scheduler_rpcapi, sched_rpcapi,
self.volume_rpcapi, volume_rpcapi)
self.db,
self.image_service,
availability_zones,
create_what)
except Exception: except Exception:
msg = _('Failed to create api volume flow.') msg = _('Failed to create api volume flow.')
LOG.exception(msg) LOG.exception(msg)

View File

@ -72,117 +72,64 @@ class ExtractVolumeRequestTask(flow_utils.CinderTask):
self.availability_zones = availability_zones self.availability_zones = availability_zones
@staticmethod @staticmethod
def _extract_consistencygroup(consistencygroup): def _extract_resource(resource, allowed_vals, exc, resource_name,
"""Extracts the consistencygroup id from the provided consistencygroup. props=('status',)):
"""Extracts the resource id from the provided resource.
This function validates the input consistencygroup dict and checks that This method validates the input resource dict and checks that the
the status of that consistencygroup is valid for creating a volume in. properties which names are passed in `props` argument match
corresponding lists in `allowed` argument. In case of mismatch
exception of type exc is raised.
:param resource: Resource dict.
:param allowed_vals: Tuple of allowed values lists.
:param exc: Exception type to raise.
:param resource_name: Name of resource - used to construct log message.
:param props: Tuple of resource properties names to validate.
:return: Id of a resource.
""" """
consistencygroup_id = None resource_id = None
if consistencygroup is not None: if resource:
if consistencygroup['status'] not in CG_PROCEED_STATUS: for prop, allowed_states in zip(props, allowed_vals):
msg = _("Originating consistencygroup status must be one" if resource[prop] not in allowed_states:
" of '%s' values") msg = _("Originating %(res)s %(prop)s must be one of"
msg = msg % (", ".join(CG_PROCEED_STATUS)) "'%(vals)s' values")
raise exception.InvalidConsistencyGroup(reason=msg) msg = msg % {'res': resource_name,
consistencygroup_id = consistencygroup['id'] 'prop': prop,
return consistencygroup_id 'vals': ', '.join(allowed_states)}
# TODO(harlowja): what happens if the status changes after
# this initial resource status check occurs??? Seems like
# someone could delete the resource after this check passes
# but before the volume is officially created?
raise exc(reason=msg)
resource_id = resource['id']
return resource_id
@staticmethod def _extract_consistencygroup(self, consistencygroup):
def _extract_cgsnapshot(cgsnapshot): return self._extract_resource(consistencygroup, (CG_PROCEED_STATUS,),
"""Extracts the cgsnapshot id from the provided cgsnapshot. exception.InvalidConsistencyGroup,
'consistencygroup')
This function validates the input cgsnapshot dict and checks that def _extract_cgsnapshot(self, cgsnapshot):
the status of that cgsnapshot is valid for creating a cg from. return self._extract_resource(cgsnapshot, (CGSNAPSHOT_PROCEED_STATUS,),
""" exception.InvalidCgSnapshot,
'CGSNAPSHOT')
cgsnapshot_id = None def _extract_snapshot(self, snapshot):
if cgsnapshot: return self._extract_resource(snapshot, (SNAPSHOT_PROCEED_STATUS,),
if cgsnapshot['status'] not in CGSNAPSHOT_PROCEED_STATUS: exception.InvalidSnapshot, 'snapshot')
msg = _("Originating CGSNAPSHOT status must be one"
" of '%s' values")
msg = msg % (", ".join(CGSNAPSHOT_PROCEED_STATUS))
raise exception.InvalidCgSnapshot(reason=msg)
cgsnapshot_id = cgsnapshot['id']
return cgsnapshot_id
@staticmethod def _extract_source_volume(self, source_volume):
def _extract_snapshot(snapshot): return self._extract_resource(source_volume, (SRC_VOL_PROCEED_STATUS,),
"""Extracts the snapshot id from the provided snapshot (if provided). exception.InvalidVolume, 'source volume')
This function validates the input snapshot dict and checks that the def _extract_source_replica(self, source_replica):
status of that snapshot is valid for creating a volume from. return self._extract_resource(source_replica, (SRC_VOL_PROCEED_STATUS,
""" REPLICA_PROCEED_STATUS),
exception.InvalidVolume,
snapshot_id = None 'replica', ('status',
if snapshot is not None: 'replication_status'))
if snapshot['status'] not in SNAPSHOT_PROCEED_STATUS:
msg = _("Originating snapshot status must be one"
" of %s values")
msg = msg % (", ".join(SNAPSHOT_PROCEED_STATUS))
# TODO(harlowja): what happens if the status changes after this
# initial snapshot status check occurs??? Seems like someone
# could delete the snapshot after this check passes but before
# the volume is officially created?
raise exception.InvalidSnapshot(reason=msg)
snapshot_id = snapshot['id']
return snapshot_id
@staticmethod
def _extract_source_volume(source_volume):
"""Extracts the volume id from the provided volume (if provided).
This function validates the input source_volume dict and checks that
the status of that source_volume is valid for creating a volume from.
"""
source_volid = None
if source_volume is not None:
if source_volume['status'] not in SRC_VOL_PROCEED_STATUS:
msg = _("Unable to create a volume from an originating source"
" volume when its status is not one of %s"
" values")
msg = msg % (", ".join(SRC_VOL_PROCEED_STATUS))
# TODO(harlowja): what happens if the status changes after this
# initial volume status check occurs??? Seems like someone
# could delete the volume after this check passes but before
# the volume is officially created?
raise exception.InvalidVolume(reason=msg)
source_volid = source_volume['id']
return source_volid
@staticmethod
def _extract_source_replica(source_replica):
"""Extracts the volume id from the provided replica (if provided).
This function validates the input replica_volume dict and checks that
the status of that replica_volume is valid for creating a volume from.
"""
source_replicaid = None
if source_replica is not None:
if source_replica['status'] not in SRC_VOL_PROCEED_STATUS:
msg = _("Unable to create a volume from an originating source"
" volume when its status is not one of %s"
" values")
msg = msg % (", ".join(SRC_VOL_PROCEED_STATUS))
# TODO(harlowja): what happens if the status changes after this
# initial volume status check occurs??? Seems like someone
# could delete the volume after this check passes but before
# the volume is officially created?
raise exception.InvalidVolume(reason=msg)
replication_status = source_replica['replication_status']
if replication_status not in REPLICA_PROCEED_STATUS:
msg = _("Unable to create a volume from a replica"
" when replication status is not one of %s"
" values")
msg = msg % (", ".join(REPLICA_PROCEED_STATUS))
# TODO(ronenkat): what happens if the replication status
# changes after this initial volume status check occurs???
raise exception.InvalidVolume(reason=msg)
source_replicaid = source_replica['id']
return source_replicaid
@staticmethod @staticmethod
def _extract_size(size, source_volume, snapshot): def _extract_size(size, source_volume, snapshot):
@ -328,6 +275,7 @@ class ExtractVolumeRequestTask(flow_utils.CinderTask):
else: else:
# For backwards compatibility use the storage_availability_zone # For backwards compatibility use the storage_availability_zone
availability_zone = CONF.storage_availability_zone availability_zone = CONF.storage_availability_zone
if availability_zone not in self.availability_zones: if availability_zone not in self.availability_zones:
msg = _("Availability zone '%s' is invalid") % (availability_zone) msg = _("Availability zone '%s' is invalid") % (availability_zone)
LOG.warning(msg) LOG.warning(msg)
@ -379,27 +327,22 @@ class ExtractVolumeRequestTask(flow_utils.CinderTask):
return encryption_key_id return encryption_key_id
def _get_volume_type_id(self, volume_type, source_volume, snapshot): def _get_volume_type_id(self, volume_type, source_volume, snapshot):
volume_type_id = None
if not volume_type and source_volume: if not volume_type and source_volume:
volume_type_id = source_volume['volume_type_id'] return source_volume['volume_type_id']
elif snapshot is not None: elif snapshot is not None:
if volume_type: if volume_type:
current_volume_type_id = volume_type.get('id') current_volume_type_id = volume_type.get('id')
if (current_volume_type_id != if current_volume_type_id != snapshot['volume_type_id']:
snapshot['volume_type_id']):
msg = _LW("Volume type will be changed to " msg = _LW("Volume type will be changed to "
"be the same as the source volume.") "be the same as the source volume.")
LOG.warning(msg) LOG.warning(msg)
volume_type_id = snapshot['volume_type_id'] return snapshot['volume_type_id']
else: else:
volume_type_id = volume_type.get('id') return volume_type.get('id')
return volume_type_id
def execute(self, context, size, snapshot, image_id, source_volume, def execute(self, context, size, snapshot, image_id, source_volume,
availability_zone, volume_type, metadata, availability_zone, volume_type, metadata, key_manager,
key_manager, source_replica, source_replica, consistencygroup, cgsnapshot):
consistencygroup, cgsnapshot):
utils.check_exclusive_options(snapshot=snapshot, utils.check_exclusive_options(snapshot=snapshot,
imageRef=image_id, imageRef=image_id,
@ -425,7 +368,7 @@ class ExtractVolumeRequestTask(flow_utils.CinderTask):
# their volume type matches the source volume is too convoluted. We # their volume type matches the source volume is too convoluted. We
# should copy encryption metadata from the encrypted volume type to the # should copy encryption metadata from the encrypted volume type to the
# volume upon creation and propagate that information to each snapshot. # volume upon creation and propagate that information to each snapshot.
# This strategy avoid any dependency upon the encrypted volume type. # This strategy avoids any dependency upon the encrypted volume type.
def_vol_type = volume_types.get_default_volume_type() def_vol_type = volume_types.get_default_volume_type()
if not volume_type and not source_volume and not snapshot: if not volume_type and not source_volume and not snapshot:
volume_type = def_vol_type volume_type = def_vol_type
@ -532,14 +475,15 @@ class EntryCreateTask(flow_utils.CinderTask):
} }
def revert(self, context, result, optional_args, **kwargs): def revert(self, context, result, optional_args, **kwargs):
# We never produced a result and therefore can't destroy anything.
if isinstance(result, ft.Failure): if isinstance(result, ft.Failure):
# We never produced a result and therefore can't destroy anything.
return return
if optional_args['is_quota_committed']: if optional_args['is_quota_committed']:
# Committed quota doesn't rollback as the volume has already been # If quota got commited we shouldn't rollback as the volume has
# created at this point, and the quota has already been absorbed. # already been created and the quota has already been absorbed.
return return
vol_id = result['volume_id'] vol_id = result['volume_id']
try: try:
self.db.volume_destroy(context.elevated(), vol_id) self.db.volume_destroy(context.elevated(), vol_id)
@ -594,7 +538,7 @@ class QuotaReserveTask(flow_utils.CinderTask):
usages = e.kwargs['usages'] usages = e.kwargs['usages']
def _consumed(name): def _consumed(name):
return (usages[name]['reserved'] + usages[name]['in_use']) return usages[name]['reserved'] + usages[name]['in_use']
def _is_over(name): def _is_over(name):
for over in overs: for over in overs:
@ -675,6 +619,7 @@ class QuotaCommitTask(flow_utils.CinderTask):
# We never produced a result and therefore can't destroy anything. # We never produced a result and therefore can't destroy anything.
if isinstance(result, ft.Failure): if isinstance(result, ft.Failure):
return return
volume = result['volume_properties'] volume = result['volume_properties']
try: try:
reserve_opts = {'volumes': -1, 'gigabytes': -volume['size']} reserve_opts = {'volumes': -1, 'gigabytes': -volume['size']}
@ -695,10 +640,11 @@ class QuotaCommitTask(flow_utils.CinderTask):
class VolumeCastTask(flow_utils.CinderTask): class VolumeCastTask(flow_utils.CinderTask):
"""Performs a volume create cast to the scheduler or to the volume manager. """Performs a volume create cast to the scheduler or to the volume manager.
This which will signal a transition of the api workflow to another child This will signal a transition of the api workflow to another child and/or
and/or related workflow on another component. related workflow on another component.
Reversion strategy: N/A Reversion strategy: rollback source volume status and error out newly
created volume.
""" """
def __init__(self, scheduler_rpcapi, volume_rpcapi, db): def __init__(self, scheduler_rpcapi, volume_rpcapi, db):
@ -718,23 +664,23 @@ class VolumeCastTask(flow_utils.CinderTask):
volume_id = request_spec['volume_id'] volume_id = request_spec['volume_id']
snapshot_id = request_spec['snapshot_id'] snapshot_id = request_spec['snapshot_id']
image_id = request_spec['image_id'] image_id = request_spec['image_id']
group_id = request_spec['consistencygroup_id'] cgroup_id = request_spec['consistencygroup_id']
host = None host = None
cgsnapshot_id = request_spec['cgsnapshot_id'] cgsnapshot_id = request_spec['cgsnapshot_id']
if group_id: if cgroup_id:
group = self.db.consistencygroup_get(context, group_id) cgroup = self.db.consistencygroup_get(context, cgroup_id)
if group: if cgroup:
host = group.get('host', None) host = cgroup.get('host', None)
elif snapshot_id and CONF.snapshot_same_host: elif snapshot_id and CONF.snapshot_same_host:
# NOTE(Rongze Zhu): A simple solution for bug 1008866. # NOTE(Rongze Zhu): A simple solution for bug 1008866.
# #
# If snapshot_id is set, make the call create volume directly to # If snapshot_id is set and CONF.snapshot_same_host is True, make
# the volume host where the snapshot resides instead of passing it # the call create volume directly to the volume host where the
# through the scheduler. So snapshot can be copy to new volume. # snapshot resides instead of passing it through the scheduler, so
# snapshot can be copied to the new volume.
snapshot = objects.Snapshot.get_by_id(context, snapshot_id) snapshot = objects.Snapshot.get_by_id(context, snapshot_id)
source_volume_ref = self.db.volume_get(context, source_volume_ref = self.db.volume_get(context, snapshot.volume_id)
snapshot.volume_id)
host = source_volume_ref['host'] host = source_volume_ref['host']
elif source_volid: elif source_volid:
source_volume_ref = self.db.volume_get(context, source_volid) source_volume_ref = self.db.volume_get(context, source_volid)
@ -772,7 +718,7 @@ class VolumeCastTask(flow_utils.CinderTask):
image_id=image_id, image_id=image_id,
source_volid=source_volid, source_volid=source_volid,
source_replicaid=source_replicaid, source_replicaid=source_replicaid,
consistencygroup_id=group_id) consistencygroup_id=cgroup_id)
def execute(self, context, **kwargs): def execute(self, context, **kwargs):
scheduler_hints = kwargs.pop('scheduler_hints', None) scheduler_hints = kwargs.pop('scheduler_hints', None)
@ -797,9 +743,8 @@ class VolumeCastTask(flow_utils.CinderTask):
LOG.error(_LE('Unexpected build error:'), exc_info=exc_info) LOG.error(_LE('Unexpected build error:'), exc_info=exc_info)
def get_flow(scheduler_rpcapi, volume_rpcapi, db_api, def get_flow(db_api, image_service_api, availability_zones, create_what,
image_service_api, availability_zones, scheduler_rpcapi=None, volume_rpcapi=None):
create_what):
"""Constructs and returns the api entrypoint flow. """Constructs and returns the api entrypoint flow.
This flow will do the following: This flow will do the following:
@ -825,39 +770,10 @@ def get_flow(scheduler_rpcapi, volume_rpcapi, db_api,
EntryCreateTask(db_api), EntryCreateTask(db_api),
QuotaCommitTask()) QuotaCommitTask())
# This will cast it out to either the scheduler or volume manager via if scheduler_rpcapi and volume_rpcapi:
# the rpc apis provided. # This will cast it out to either the scheduler or volume manager via
api_flow.add(VolumeCastTask(scheduler_rpcapi, volume_rpcapi, db_api)) # the rpc apis provided.
api_flow.add(VolumeCastTask(scheduler_rpcapi, volume_rpcapi, db_api))
# Now load (but do not run) the flow using the provided initial data.
return taskflow.engines.load(api_flow, store=create_what)
def get_flow_no_rpc(db_api, image_service_api, availability_zones,
create_what):
"""Constructs and returns the api entrypoint flow.
This flow will do the following:
1. Inject keys & values for dependent tasks.
2. Extracts and validates the input keys & values.
3. Reserves the quota (reverts quota on any failures).
4. Creates the database entry.
5. Commits the quota.
"""
flow_name = ACTION.replace(":", "_") + "_api"
api_flow = linear_flow.Flow(flow_name)
api_flow.add(ExtractVolumeRequestTask(
image_service_api,
availability_zones,
rebind={'size': 'raw_size',
'availability_zone': 'raw_availability_zone',
'volume_type': 'raw_volume_type'}))
api_flow.add(QuotaReserveTask(),
EntryCreateTask(db_api),
QuotaCommitTask())
# Now load (but do not run) the flow using the provided initial data. # Now load (but do not run) the flow using the provided initial data.
return taskflow.engines.load(api_flow, store=create_what) return taskflow.engines.load(api_flow, store=create_what)