1223 lines
54 KiB
Python
1223 lines
54 KiB
Python
# Copyright 2015 Dell Inc.
|
|
#
|
|
# 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 oslo_config import cfg
|
|
from oslo_config import types
|
|
from oslo_log import log as logging
|
|
from oslo_utils import excutils
|
|
|
|
from cinder import exception
|
|
from cinder.i18n import _, _LE, _LI, _LW
|
|
from cinder.volume import driver
|
|
from cinder.volume.drivers.dell import dell_storagecenter_api
|
|
from cinder.volume.drivers.san.san import san_opts
|
|
from cinder.volume import volume_types
|
|
|
|
|
|
common_opts = [
|
|
cfg.IntOpt('dell_sc_ssn',
|
|
default=64702,
|
|
help='Storage Center System Serial Number'),
|
|
cfg.PortOpt('dell_sc_api_port',
|
|
default=3033,
|
|
help='Dell API port'),
|
|
cfg.StrOpt('dell_sc_server_folder',
|
|
default='openstack',
|
|
help='Name of the server folder to use on the Storage Center'),
|
|
cfg.StrOpt('dell_sc_volume_folder',
|
|
default='openstack',
|
|
help='Name of the volume folder to use on the Storage Center'),
|
|
cfg.BoolOpt('dell_sc_verify_cert',
|
|
default=False,
|
|
help='Enable HTTPS SC certificate verification'),
|
|
cfg.MultiOpt('excluded_domain_ip',
|
|
item_type=types.IPAddress(),
|
|
default=None,
|
|
help='Domain IP to be excluded from iSCSI returns.')
|
|
]
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
CONF = cfg.CONF
|
|
CONF.register_opts(common_opts)
|
|
|
|
|
|
class DellCommonDriver(driver.ConsistencyGroupVD, driver.ManageableVD,
|
|
driver.ExtendVD, driver.ManageableSnapshotsVD,
|
|
driver.SnapshotVD, driver.BaseVD):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(DellCommonDriver, self).__init__(*args, **kwargs)
|
|
self.configuration.append_config_values(common_opts)
|
|
self.configuration.append_config_values(san_opts)
|
|
self.backend_name =\
|
|
self.configuration.safe_get('volume_backend_name') or 'Dell'
|
|
self.backends = self.configuration.safe_get('replication_device')
|
|
self.replication_enabled = True if self.backends else False
|
|
self.is_direct_connect = False
|
|
self.active_backend_id = kwargs.get('active_backend_id', None)
|
|
self.failed_over = (self.active_backend_id is not None)
|
|
|
|
def _bytes_to_gb(self, spacestring):
|
|
"""Space is returned in a string like ...
|
|
|
|
7.38197504E8 Bytes
|
|
Need to split that apart and convert to GB.
|
|
|
|
:returns: gbs in int form
|
|
"""
|
|
try:
|
|
n = spacestring.split(' ', 1)
|
|
fgbs = float(n[0]) / 1073741824.0
|
|
igbs = int(fgbs)
|
|
return igbs
|
|
except Exception:
|
|
# If any of that blew up it isn't in the format we
|
|
# thought so eat our error and return None
|
|
return None
|
|
|
|
def do_setup(self, context):
|
|
"""One time driver setup.
|
|
|
|
Called once by the manager after the driver is loaded.
|
|
Sets up clients, check licenses, sets up protocol
|
|
specific helpers.
|
|
"""
|
|
self._client = dell_storagecenter_api.StorageCenterApiHelper(
|
|
self.configuration, self.active_backend_id)
|
|
|
|
def check_for_setup_error(self):
|
|
"""Validates the configuration information."""
|
|
with self._client.open_connection() as api:
|
|
api.find_sc()
|
|
self.is_direct_connect = api.is_direct_connect
|
|
if self.is_direct_connect and self.replication_enabled:
|
|
msg = _('Dell Cinder driver configuration error replication '
|
|
'not supported with direct connect.')
|
|
raise exception.InvalidHost(reason=msg)
|
|
|
|
# If we are a healthy replicated system make sure our backend
|
|
# is alive.
|
|
if self.replication_enabled and not self.failed_over:
|
|
# Check that our replication destinations are available.
|
|
for backend in self.backends:
|
|
replssn = backend['target_device_id']
|
|
try:
|
|
# Just do a find_sc on it. If it raises we catch
|
|
# that and raise with a correct exception.
|
|
api.find_sc(int(replssn))
|
|
except exception.VolumeBackendAPIException:
|
|
msg = _('Dell Cinder driver configuration error '
|
|
'replication_device %s not found') % replssn
|
|
raise exception.InvalidHost(reason=msg)
|
|
|
|
def _get_volume_extra_specs(self, volume):
|
|
"""Gets extra specs for the given volume."""
|
|
type_id = volume.get('volume_type_id')
|
|
if type_id:
|
|
return volume_types.get_volume_type_extra_specs(type_id)
|
|
|
|
return {}
|
|
|
|
def _add_volume_to_consistency_group(self, api, scvolume, volume):
|
|
"""Just a helper to add a volume to a consistency group.
|
|
|
|
:param api: Dell SC API opbject.
|
|
:param scvolume: Dell SC Volume object.
|
|
:param volume: Cinder Volume object.
|
|
:returns: Nothing.
|
|
"""
|
|
if scvolume and volume.get('consistencygroup_id'):
|
|
profile = api.find_replay_profile(
|
|
volume.get('consistencygroup_id'))
|
|
if profile:
|
|
api.update_cg_volumes(profile, [volume])
|
|
|
|
def _do_repl(self, api, volume):
|
|
"""Checks if we can do replication.
|
|
|
|
Need the extra spec set and we have to be talking to EM.
|
|
|
|
:param api: Dell REST API object.
|
|
:param volume: Cinder Volume object.
|
|
:return: Boolean (True if replication enabled), Boolean (True if
|
|
replication type is sync.
|
|
"""
|
|
do_repl = False
|
|
sync = False
|
|
# Repl does not work with direct connect.
|
|
if not self.failed_over and not self.is_direct_connect:
|
|
specs = self._get_volume_extra_specs(volume)
|
|
do_repl = specs.get('replication_enabled') == '<is> True'
|
|
sync = specs.get('replication_type') == '<in> sync'
|
|
return do_repl, sync
|
|
|
|
def _create_replications(self, api, volume, scvolume):
|
|
"""Creates any appropriate replications for a given volume.
|
|
|
|
:param api: Dell REST API object.
|
|
:param volume: Cinder volume object.
|
|
:param scvolume: Dell Storage Center Volume object.
|
|
:return: model_update
|
|
"""
|
|
# Replication V2
|
|
# for now we assume we have an array named backends.
|
|
replication_driver_data = None
|
|
# Replicate if we are supposed to.
|
|
do_repl, sync = self._do_repl(api, volume)
|
|
if do_repl:
|
|
for backend in self.backends:
|
|
# Check if we are to replicate the active replay or not.
|
|
specs = self._get_volume_extra_specs(volume)
|
|
replact = specs.get('replication:activereplay') == '<is> True'
|
|
if not api.create_replication(scvolume,
|
|
backend['target_device_id'],
|
|
backend.get('qosnode',
|
|
'cinderqos'),
|
|
sync,
|
|
backend.get('diskfolder', None),
|
|
replact):
|
|
# Create replication will have printed a better error.
|
|
msg = _('Replication %(name)s to %(ssn)s failed.') % {
|
|
'name': volume['id'],
|
|
'ssn': backend['target_device_id']}
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
if not replication_driver_data:
|
|
replication_driver_data = backend['target_device_id']
|
|
else:
|
|
replication_driver_data += ','
|
|
replication_driver_data += backend['target_device_id']
|
|
# If we did something return model update.
|
|
model_update = {}
|
|
if replication_driver_data:
|
|
model_update = {'replication_status': 'enabled',
|
|
'replication_driver_data': replication_driver_data}
|
|
return model_update
|
|
|
|
@staticmethod
|
|
def _cleanup_failed_create_volume(api, volumename):
|
|
try:
|
|
api.delete_volume(volumename)
|
|
except exception.VolumeBackendAPIException as ex:
|
|
LOG.info(_LI('Non fatal cleanup error: %s.'), ex.msg)
|
|
|
|
def create_volume(self, volume):
|
|
"""Create a volume."""
|
|
model_update = {}
|
|
|
|
# We use id as our name as it is unique.
|
|
volume_name = volume.get('id')
|
|
# Look for our volume
|
|
volume_size = volume.get('size')
|
|
|
|
# See if we have any extra specs.
|
|
specs = self._get_volume_extra_specs(volume)
|
|
storage_profile = specs.get('storagetype:storageprofile')
|
|
replay_profile_string = specs.get('storagetype:replayprofiles')
|
|
|
|
LOG.debug('Creating volume %(name)s of size %(size)s',
|
|
{'name': volume_name,
|
|
'size': volume_size})
|
|
scvolume = None
|
|
with self._client.open_connection() as api:
|
|
try:
|
|
if api.find_sc():
|
|
scvolume = api.create_volume(volume_name,
|
|
volume_size,
|
|
storage_profile,
|
|
replay_profile_string)
|
|
if scvolume is None:
|
|
raise exception.VolumeBackendAPIException(
|
|
message=_('Unable to create volume %s') %
|
|
volume_name)
|
|
|
|
# Update Consistency Group
|
|
self._add_volume_to_consistency_group(api, scvolume, volume)
|
|
|
|
# Create replications. (Or not. It checks.)
|
|
model_update = self._create_replications(api, volume, scvolume)
|
|
|
|
except Exception:
|
|
# if we actually created a volume but failed elsewhere
|
|
# clean up the volume now.
|
|
self._cleanup_failed_create_volume(api, volume_name)
|
|
with excutils.save_and_reraise_exception():
|
|
LOG.error(_LE('Failed to create volume %s'),
|
|
volume_name)
|
|
if scvolume is None:
|
|
raise exception.VolumeBackendAPIException(
|
|
data=_('Unable to create volume. Backend down.'))
|
|
|
|
return model_update
|
|
|
|
def _split_driver_data(self, replication_driver_data):
|
|
"""Splits the replication_driver_data into an array of ssn strings.
|
|
|
|
:param replication_driver_data: A string of comma separated SSNs.
|
|
:returns: SSNs in an array of strings.
|
|
"""
|
|
ssnstrings = []
|
|
# We have any replication_driver_data.
|
|
if replication_driver_data:
|
|
# Split the array and wiffle through the entries.
|
|
for str in replication_driver_data.split(','):
|
|
# Strip any junk from the string.
|
|
ssnstring = str.strip()
|
|
# Anything left?
|
|
if ssnstring:
|
|
# Add it to our array.
|
|
ssnstrings.append(ssnstring)
|
|
return ssnstrings
|
|
|
|
def _delete_replications(self, api, volume):
|
|
"""Delete replications associated with a given volume.
|
|
|
|
We should be able to roll through the replication_driver_data list
|
|
of SSNs and delete replication objects between them and the source
|
|
volume.
|
|
|
|
:param api: Dell REST API object.
|
|
:param volume: Cinder Volume object
|
|
:return:
|
|
"""
|
|
do_repl, sync = self._do_repl(api, volume)
|
|
if do_repl:
|
|
volume_name = volume.get('id')
|
|
scvol = api.find_volume(volume_name)
|
|
replication_driver_data = volume.get('replication_driver_data')
|
|
# This is just a string of ssns separated by commas.
|
|
ssnstrings = self._split_driver_data(replication_driver_data)
|
|
# Trundle through these and delete them all.
|
|
for ssnstring in ssnstrings:
|
|
ssn = int(ssnstring)
|
|
if not api.delete_replication(scvol, ssn):
|
|
LOG.warning(_LW('Unable to delete replication of '
|
|
'Volume %(vname)s to Storage Center '
|
|
'%(sc)s.'),
|
|
{'vname': volume_name,
|
|
'sc': ssnstring})
|
|
# If none of that worked or there was nothing to do doesn't matter.
|
|
# Just move on.
|
|
|
|
def delete_volume(self, volume):
|
|
deleted = False
|
|
# We use id as our name as it is unique.
|
|
volume_name = volume.get('id')
|
|
LOG.debug('Deleting volume %s', volume_name)
|
|
with self._client.open_connection() as api:
|
|
try:
|
|
if api.find_sc():
|
|
self._delete_replications(api, volume)
|
|
deleted = api.delete_volume(volume_name)
|
|
except Exception:
|
|
with excutils.save_and_reraise_exception():
|
|
LOG.error(_LE('Failed to delete volume %s'),
|
|
volume_name)
|
|
|
|
# if there was an error we will have raised an
|
|
# exception. If it failed to delete it is because
|
|
# the conditions to delete a volume were not met.
|
|
if deleted is False:
|
|
raise exception.VolumeIsBusy(volume_name=volume_name)
|
|
|
|
def create_snapshot(self, snapshot):
|
|
"""Create snapshot"""
|
|
# our volume name is the volume id
|
|
volume_name = snapshot.get('volume_id')
|
|
snapshot_id = snapshot.get('id')
|
|
LOG.debug('Creating snapshot %(snap)s on volume %(vol)s',
|
|
{'snap': snapshot_id,
|
|
'vol': volume_name})
|
|
with self._client.open_connection() as api:
|
|
if api.find_sc():
|
|
scvolume = api.find_volume(volume_name)
|
|
if scvolume is not None:
|
|
if api.create_replay(scvolume,
|
|
snapshot_id,
|
|
0) is not None:
|
|
snapshot['status'] = 'available'
|
|
return
|
|
else:
|
|
LOG.warning(_LW('Unable to locate volume:%s'),
|
|
volume_name)
|
|
|
|
snapshot['status'] = 'error_creating'
|
|
msg = _('Failed to create snapshot %s') % snapshot_id
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
|
|
def create_volume_from_snapshot(self, volume, snapshot):
|
|
"""Create new volume from other volume's snapshot on appliance."""
|
|
model_update = {}
|
|
scvolume = None
|
|
src_volume_name = snapshot.get('volume_id')
|
|
# This snapshot could have been created on its own or as part of a
|
|
# cgsnapshot. If it was a cgsnapshot it will be identified on the Dell
|
|
# backend under cgsnapshot_id. Given the volume ID and the
|
|
# cgsnapshot_id we can find the appropriate snapshot.
|
|
# So first we look for cgsnapshot_id. If that is blank then it must
|
|
# have been a normal snapshot which will be found under snapshot_id.
|
|
snapshot_id = snapshot.get('cgsnapshot_id')
|
|
if not snapshot_id:
|
|
snapshot_id = snapshot.get('id')
|
|
volume_name = volume.get('id')
|
|
LOG.debug(
|
|
'Creating new volume %(vol)s from snapshot %(snap)s '
|
|
'from vol %(src)s',
|
|
{'vol': volume_name,
|
|
'snap': snapshot_id,
|
|
'src': src_volume_name})
|
|
with self._client.open_connection() as api:
|
|
try:
|
|
if api.find_sc():
|
|
srcvol = api.find_volume(src_volume_name)
|
|
if srcvol is not None:
|
|
replay = api.find_replay(srcvol,
|
|
snapshot_id)
|
|
if replay is not None:
|
|
volume_name = volume.get('id')
|
|
# See if we have any extra specs.
|
|
specs = self._get_volume_extra_specs(volume)
|
|
replay_profile_string = specs.get(
|
|
'storagetype:replayprofiles')
|
|
scvolume = api.create_view_volume(
|
|
volume_name, replay, replay_profile_string)
|
|
if scvolume is None:
|
|
raise exception.VolumeBackendAPIException(
|
|
message=_('Unable to create volume '
|
|
'%(name)s from %(snap)s.') %
|
|
{'name': volume_name,
|
|
'snap': snapshot_id})
|
|
|
|
# Update Consistency Group
|
|
self._add_volume_to_consistency_group(api,
|
|
scvolume,
|
|
volume)
|
|
# Replicate if we are supposed to.
|
|
model_update = self._create_replications(api,
|
|
volume,
|
|
scvolume)
|
|
|
|
except Exception:
|
|
# Clean up after ourselves.
|
|
self._cleanup_failed_create_volume(api, volume_name)
|
|
with excutils.save_and_reraise_exception():
|
|
LOG.error(_LE('Failed to create volume %s'),
|
|
volume_name)
|
|
if scvolume is not None:
|
|
LOG.debug('Volume %(vol)s created from %(snap)s',
|
|
{'vol': volume_name,
|
|
'snap': snapshot_id})
|
|
else:
|
|
msg = _('Failed to create volume %s') % volume_name
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
|
|
return model_update
|
|
|
|
def create_cloned_volume(self, volume, src_vref):
|
|
"""Creates a clone of the specified volume."""
|
|
model_update = {}
|
|
scvolume = None
|
|
src_volume_name = src_vref.get('id')
|
|
volume_name = volume.get('id')
|
|
LOG.debug('Creating cloned volume %(clone)s from volume %(vol)s',
|
|
{'clone': volume_name,
|
|
'vol': src_volume_name})
|
|
with self._client.open_connection() as api:
|
|
try:
|
|
if api.find_sc():
|
|
srcvol = api.find_volume(src_volume_name)
|
|
if srcvol is not None:
|
|
# See if we have any extra specs.
|
|
specs = self._get_volume_extra_specs(volume)
|
|
replay_profile_string = specs.get(
|
|
'storagetype:replayprofiles')
|
|
# Create our volume
|
|
scvolume = api.create_cloned_volume(
|
|
volume_name, srcvol, replay_profile_string)
|
|
|
|
# Extend Volume
|
|
if scvolume and volume['size'] > src_vref['size']:
|
|
LOG.debug("Resize the new volume to %s.",
|
|
volume['size'])
|
|
scvolume = api.expand_volume(scvolume,
|
|
volume['size'])
|
|
|
|
# If either of those didn't work we bail.
|
|
if scvolume is None:
|
|
raise exception.VolumeBackendAPIException(
|
|
message=_('Unable to create volume '
|
|
'%(name)s from %(vol)s.') %
|
|
{'name': volume_name,
|
|
'vol': src_volume_name})
|
|
|
|
# Update Consistency Group
|
|
self._add_volume_to_consistency_group(api,
|
|
scvolume,
|
|
volume)
|
|
# Replicate if we are supposed to.
|
|
model_update = self._create_replications(api,
|
|
volume,
|
|
scvolume)
|
|
except Exception:
|
|
# Clean up after ourselves.
|
|
self._cleanup_failed_create_volume(api, volume_name)
|
|
with excutils.save_and_reraise_exception():
|
|
LOG.error(_LE('Failed to create volume %s'),
|
|
volume_name)
|
|
if scvolume is not None:
|
|
LOG.debug('Volume %(vol)s cloned from %(src)s',
|
|
{'vol': volume_name,
|
|
'src': src_volume_name})
|
|
else:
|
|
msg = _('Failed to create volume %s') % volume_name
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
return model_update
|
|
|
|
def delete_snapshot(self, snapshot):
|
|
"""delete_snapshot"""
|
|
volume_name = snapshot.get('volume_id')
|
|
snapshot_id = snapshot.get('id')
|
|
LOG.debug('Deleting snapshot %(snap)s from volume %(vol)s',
|
|
{'snap': snapshot_id,
|
|
'vol': volume_name})
|
|
with self._client.open_connection() as api:
|
|
if api.find_sc():
|
|
scvolume = api.find_volume(volume_name)
|
|
if scvolume is not None:
|
|
if api.delete_replay(scvolume,
|
|
snapshot_id):
|
|
return
|
|
# if we are here things went poorly.
|
|
snapshot['status'] = 'error_deleting'
|
|
msg = _('Failed to delete snapshot %s') % snapshot_id
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
|
|
def create_export(self, context, volume, connector):
|
|
"""Create an export of a volume.
|
|
|
|
The volume exists on creation and will be visible on
|
|
initialize connection. So nothing to do here.
|
|
"""
|
|
# TODO(tswanson): Move mapping code here.
|
|
pass
|
|
|
|
def ensure_export(self, context, volume):
|
|
"""Ensure an export of a volume.
|
|
|
|
Per the eqlx driver we just make sure that the volume actually
|
|
exists where we think it does.
|
|
"""
|
|
scvolume = None
|
|
volume_name = volume.get('id')
|
|
LOG.debug('Checking existence of volume %s', volume_name)
|
|
with self._client.open_connection() as api:
|
|
try:
|
|
if api.find_sc():
|
|
scvolume = api.find_volume(volume_name)
|
|
except Exception:
|
|
with excutils.save_and_reraise_exception():
|
|
LOG.error(_LE('Failed to ensure export of volume %s'),
|
|
volume_name)
|
|
if scvolume is None:
|
|
msg = _('Unable to find volume %s') % volume_name
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
|
|
def remove_export(self, context, volume):
|
|
"""Remove an export of a volume.
|
|
|
|
We do nothing here to match the nothing we do in create export. Again
|
|
we do everything in initialize and terminate connection.
|
|
"""
|
|
pass
|
|
|
|
def extend_volume(self, volume, new_size):
|
|
"""Extend the size of the volume."""
|
|
volume_name = volume.get('id')
|
|
LOG.debug('Extending volume %(vol)s to %(size)s',
|
|
{'vol': volume_name,
|
|
'size': new_size})
|
|
if volume is not None:
|
|
with self._client.open_connection() as api:
|
|
if api.find_sc():
|
|
scvolume = api.find_volume(volume_name)
|
|
if api.expand_volume(scvolume, new_size) is not None:
|
|
return
|
|
# If we are here nothing good happened.
|
|
msg = _('Unable to extend volume %s') % volume_name
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
|
|
def get_volume_stats(self, refresh=False):
|
|
"""Get volume status.
|
|
|
|
If 'refresh' is True, run update the stats first.
|
|
"""
|
|
if refresh:
|
|
self._update_volume_stats()
|
|
|
|
return self._stats
|
|
|
|
def _update_volume_stats(self):
|
|
"""Retrieve stats info from volume group."""
|
|
with self._client.open_connection() as api:
|
|
storageusage = api.get_storage_usage() if api.find_sc() else None
|
|
|
|
# all of this is basically static for now
|
|
data = {}
|
|
data['volume_backend_name'] = self.backend_name
|
|
data['vendor_name'] = 'Dell'
|
|
data['driver_version'] = self.VERSION
|
|
data['storage_protocol'] = 'iSCSI'
|
|
data['reserved_percentage'] = 0
|
|
data['free_capacity_gb'] = 'unavailable'
|
|
data['total_capacity_gb'] = 'unavailable'
|
|
data['consistencygroup_support'] = True
|
|
# In theory if storageusage is None then we should have
|
|
# blown up getting it. If not just report unavailable.
|
|
if storageusage is not None:
|
|
totalcapacity = storageusage.get('availableSpace')
|
|
totalcapacitygb = self._bytes_to_gb(totalcapacity)
|
|
data['total_capacity_gb'] = totalcapacitygb
|
|
freespace = storageusage.get('freeSpace')
|
|
freespacegb = self._bytes_to_gb(freespace)
|
|
data['free_capacity_gb'] = freespacegb
|
|
data['QoS_support'] = False
|
|
data['replication_enabled'] = self.replication_enabled
|
|
if self.replication_enabled:
|
|
data['replication_type'] = ['async', 'sync']
|
|
data['replication_count'] = len(self.backends)
|
|
replication_targets = []
|
|
# Trundle through our backends.
|
|
for backend in self.backends:
|
|
target_device_id = backend.get('target_device_id')
|
|
if target_device_id:
|
|
replication_targets.append(target_device_id)
|
|
data['replication_targets'] = replication_targets
|
|
|
|
self._stats = data
|
|
LOG.debug('Total cap %(total)s Free cap %(free)s',
|
|
{'total': data['total_capacity_gb'],
|
|
'free': data['free_capacity_gb']})
|
|
|
|
def update_migrated_volume(self, ctxt, volume, new_volume,
|
|
original_volume_status):
|
|
"""Return model update for migrated volume.
|
|
|
|
:param volume: The original volume that was migrated to this backend
|
|
:param new_volume: The migration volume object that was created on
|
|
this backend as part of the migration process
|
|
:param original_volume_status: The status of the original volume
|
|
:returns: model_update to update DB with any needed changes
|
|
"""
|
|
# We use id as our volume name so we need to rename the backend
|
|
# volume to the original volume name.
|
|
original_volume_name = volume.get('id')
|
|
current_name = new_volume.get('id')
|
|
LOG.debug('update_migrated_volume: %(current)s to %(original)s',
|
|
{'current': current_name,
|
|
'original': original_volume_name})
|
|
if original_volume_name:
|
|
with self._client.open_connection() as api:
|
|
if api.find_sc():
|
|
scvolume = api.find_volume(current_name)
|
|
if (scvolume and
|
|
api.rename_volume(scvolume, original_volume_name)):
|
|
# Replicate if we are supposed to.
|
|
model_update = self._create_replications(api,
|
|
new_volume,
|
|
scvolume)
|
|
model_update['_name_id'] = None
|
|
|
|
return model_update
|
|
# The world was horrible to us so we should error and leave.
|
|
LOG.error(_LE('Unable to rename the logical volume for volume: %s'),
|
|
original_volume_name)
|
|
|
|
return {'_name_id': new_volume['_name_id'] or new_volume['id']}
|
|
|
|
def create_consistencygroup(self, context, group):
|
|
"""This creates a replay profile on the storage backend.
|
|
|
|
:param context: the context of the caller.
|
|
:param group: the dictionary of the consistency group to be created.
|
|
:returns: Nothing on success.
|
|
:raises: VolumeBackendAPIException
|
|
"""
|
|
gid = group['id']
|
|
with self._client.open_connection() as api:
|
|
cgroup = api.create_replay_profile(gid)
|
|
if cgroup:
|
|
LOG.info(_LI('Created Consistency Group %s'), gid)
|
|
return
|
|
msg = _('Unable to create consistency group %s') % gid
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
|
|
def delete_consistencygroup(self, context, group, volumes):
|
|
"""Delete the Dell SC profile associated with this consistency group.
|
|
|
|
:param context: the context of the caller.
|
|
:param group: the dictionary of the consistency group to be created.
|
|
:returns: Updated model_update, volumes.
|
|
"""
|
|
gid = group['id']
|
|
with self._client.open_connection() as api:
|
|
profile = api.find_replay_profile(gid)
|
|
if profile:
|
|
api.delete_replay_profile(profile)
|
|
# If we are here because we found no profile that should be fine
|
|
# as we are trying to delete it anyway.
|
|
|
|
# Trundle through the list deleting the volumes.
|
|
for volume in volumes:
|
|
self.delete_volume(volume)
|
|
volume['status'] = 'deleted'
|
|
|
|
model_update = {'status': group['status']}
|
|
|
|
return model_update, volumes
|
|
|
|
def update_consistencygroup(self, context, group,
|
|
add_volumes=None, remove_volumes=None):
|
|
"""Updates a consistency group.
|
|
|
|
:param context: the context of the caller.
|
|
:param group: the dictionary of the consistency group to be updated.
|
|
:param add_volumes: a list of volume dictionaries to be added.
|
|
:param remove_volumes: a list of volume dictionaries to be removed.
|
|
:returns: model_update, add_volumes_update, remove_volumes_update
|
|
|
|
model_update is a dictionary that the driver wants the manager
|
|
to update upon a successful return. If None is returned, the manager
|
|
will set the status to 'available'.
|
|
|
|
add_volumes_update and remove_volumes_update are lists of dictionaries
|
|
that the driver wants the manager to update upon a successful return.
|
|
Note that each entry requires a {'id': xxx} so that the correct
|
|
volume entry can be updated. If None is returned, the volume will
|
|
remain its original status. Also note that you cannot directly
|
|
assign add_volumes to add_volumes_update as add_volumes is a list of
|
|
cinder.db.sqlalchemy.models.Volume objects and cannot be used for
|
|
db update directly. Same with remove_volumes.
|
|
|
|
If the driver throws an exception, the status of the group as well as
|
|
those of the volumes to be added/removed will be set to 'error'.
|
|
"""
|
|
gid = group['id']
|
|
with self._client.open_connection() as api:
|
|
profile = api.find_replay_profile(gid)
|
|
if not profile:
|
|
LOG.error(_LE('Cannot find Consistency Group %s'), gid)
|
|
elif api.update_cg_volumes(profile,
|
|
add_volumes,
|
|
remove_volumes):
|
|
LOG.info(_LI('Updated Consistency Group %s'), gid)
|
|
# we need nothing updated above us so just return None.
|
|
return None, None, None
|
|
# Things did not go well so throw.
|
|
msg = _('Unable to update consistency group %s') % gid
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
|
|
def create_cgsnapshot(self, context, cgsnapshot, snapshots):
|
|
"""Takes a snapshot of the consistency group.
|
|
|
|
:param context: the context of the caller.
|
|
:param cgsnapshot: Information about the snapshot to take.
|
|
:param snapshots: List of snapshots for this cgsnapshot.
|
|
:returns: Updated model_update, snapshots.
|
|
:raises: VolumeBackendAPIException.
|
|
"""
|
|
cgid = cgsnapshot['consistencygroup_id']
|
|
snapshotid = cgsnapshot['id']
|
|
|
|
with self._client.open_connection() as api:
|
|
profile = api.find_replay_profile(cgid)
|
|
if profile:
|
|
LOG.debug('profile %s replayid %s', profile, snapshotid)
|
|
if api.snap_cg_replay(profile, snapshotid, 0):
|
|
snapshot_updates = []
|
|
for snapshot in snapshots:
|
|
snapshot_updates.append({
|
|
'id': snapshot.id,
|
|
'status': 'available'
|
|
})
|
|
|
|
model_update = {'status': 'available'}
|
|
|
|
return model_update, snapshot_updates
|
|
|
|
# That didn't go well. Tell them why. Then bomb out.
|
|
LOG.error(_LE('Failed to snap Consistency Group %s'), cgid)
|
|
else:
|
|
LOG.error(_LE('Cannot find Consistency Group %s'), cgid)
|
|
|
|
msg = _('Unable to snap Consistency Group %s') % cgid
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
|
|
def delete_cgsnapshot(self, context, cgsnapshot, snapshots):
|
|
"""Deletes a cgsnapshot.
|
|
|
|
If profile isn't found return success. If failed to delete the
|
|
replay (the snapshot) then raise an exception.
|
|
|
|
:param context: the context of the caller.
|
|
:param cgsnapshot: Information about the snapshot to delete.
|
|
:returns: Updated model_update, snapshots.
|
|
:raises: VolumeBackendAPIException.
|
|
"""
|
|
cgid = cgsnapshot['consistencygroup_id']
|
|
snapshotid = cgsnapshot['id']
|
|
|
|
with self._client.open_connection() as api:
|
|
profile = api.find_replay_profile(cgid)
|
|
if profile:
|
|
LOG.info(_LI('Deleting snapshot %(ss)s from %(pro)s'),
|
|
{'ss': snapshotid,
|
|
'pro': profile})
|
|
if not api.delete_cg_replay(profile, snapshotid):
|
|
msg = (_('Unable to delete Consistency Group snapshot %s')
|
|
% snapshotid)
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
|
|
for snapshot in snapshots:
|
|
snapshot.status = 'deleted'
|
|
|
|
model_update = {'status': 'deleted'}
|
|
|
|
return model_update, snapshots
|
|
|
|
def manage_existing(self, volume, existing_ref):
|
|
"""Brings an existing backend storage object under Cinder management.
|
|
|
|
existing_ref is passed straight through from the API request's
|
|
manage_existing_ref value, and it is up to the driver how this should
|
|
be interpreted. It should be sufficient to identify a storage object
|
|
that the driver should somehow associate with the newly-created cinder
|
|
volume structure.
|
|
|
|
There are two ways to do this:
|
|
|
|
1. Rename the backend storage object so that it matches the,
|
|
volume['name'] which is how drivers traditionally map between a
|
|
cinder volume and the associated backend storage object.
|
|
|
|
2. Place some metadata on the volume, or somewhere in the backend, that
|
|
allows other driver requests (e.g. delete, clone, attach, detach...)
|
|
to locate the backend storage object when required.
|
|
|
|
If the existing_ref doesn't make sense, or doesn't refer to an existing
|
|
backend storage object, raise a ManageExistingInvalidReference
|
|
exception.
|
|
|
|
The volume may have a volume_type, and the driver can inspect that and
|
|
compare against the properties of the referenced backend storage
|
|
object. If they are incompatible, raise a
|
|
ManageExistingVolumeTypeMismatch, specifying a reason for the failure.
|
|
|
|
:param volume: Cinder volume to manage
|
|
:param existing_ref: Driver-specific information used to identify a
|
|
volume
|
|
"""
|
|
if existing_ref.get('source-name') or existing_ref.get('source-id'):
|
|
with self._client.open_connection() as api:
|
|
api.manage_existing(volume['id'], existing_ref)
|
|
# Replicate if we are supposed to.
|
|
scvolume = api.find_volume(volume['id'])
|
|
model_update = self._create_replications(api, volume, scvolume)
|
|
if model_update:
|
|
return model_update
|
|
else:
|
|
msg = _('Must specify source-name or source-id.')
|
|
raise exception.ManageExistingInvalidReference(
|
|
existing_ref=existing_ref, reason=msg)
|
|
# Only return a model_update if we have replication info to add.
|
|
return None
|
|
|
|
def manage_existing_get_size(self, volume, existing_ref):
|
|
"""Return size of volume to be managed by manage_existing.
|
|
|
|
When calculating the size, round up to the next GB.
|
|
|
|
:param volume: Cinder volume to manage
|
|
:param existing_ref: Driver-specific information used to identify a
|
|
volume
|
|
"""
|
|
if existing_ref.get('source-name') or existing_ref.get('source-id'):
|
|
with self._client.open_connection() as api:
|
|
return api.get_unmanaged_volume_size(existing_ref)
|
|
else:
|
|
msg = _('Must specify source-name or source-id.')
|
|
raise exception.ManageExistingInvalidReference(
|
|
existing_ref=existing_ref, reason=msg)
|
|
|
|
def unmanage(self, volume):
|
|
"""Removes the specified volume from Cinder management.
|
|
|
|
Does not delete the underlying backend storage object.
|
|
|
|
For most drivers, this will not need to do anything. However, some
|
|
drivers might use this call as an opportunity to clean up any
|
|
Cinder-specific configuration that they have associated with the
|
|
backend storage object.
|
|
|
|
:param volume: Cinder volume to unmanage
|
|
"""
|
|
with self._client.open_connection() as api:
|
|
scvolume = api.find_volume(volume['id'])
|
|
if scvolume:
|
|
api.unmanage(scvolume)
|
|
|
|
def _get_retype_spec(self, diff, volume_name, specname, spectype):
|
|
"""Helper function to get current and requested spec.
|
|
|
|
:param diff: A difference dictionary.
|
|
:param volume_name: The volume name we are working with.
|
|
:param specname: The pretty name of the parameter.
|
|
:param spectype: The actual spec string.
|
|
:return: current, requested spec.
|
|
:raises: VolumeBackendAPIException
|
|
"""
|
|
spec = (diff['extra_specs'].get(spectype))
|
|
if spec:
|
|
if len(spec) != 2:
|
|
msg = _('Unable to retype %(specname)s, expected to receive '
|
|
'current and requested %(spectype)s values. Value '
|
|
'received: %(spec)s') % {'specname': specname,
|
|
'spectype': spectype,
|
|
'spec': spec}
|
|
LOG.error(msg)
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
|
|
current = spec[0]
|
|
requested = spec[1]
|
|
|
|
if current != requested:
|
|
LOG.debug('Retyping volume %(vol)s to use %(specname)s '
|
|
'%(spec)s.',
|
|
{'vol': volume_name,
|
|
'specname': specname,
|
|
'spec': requested})
|
|
return current, requested
|
|
else:
|
|
LOG.info(_LI('Retype was to same Storage Profile.'))
|
|
return None, None
|
|
|
|
def retype(self, ctxt, volume, new_type, diff, host):
|
|
"""Convert the volume to be of the new type.
|
|
|
|
Returns a boolean indicating whether the retype occurred.
|
|
|
|
:param ctxt: Context
|
|
:param volume: A dictionary describing the volume to migrate
|
|
:param new_type: A dictionary describing the volume type to convert to
|
|
:param diff: A dictionary with the difference between the two types
|
|
:param host: A dictionary describing the host to migrate to, where
|
|
host['host'] is its name, and host['capabilities'] is a
|
|
dictionary of its reported capabilities (Not Used).
|
|
"""
|
|
model_update = None
|
|
# Any spec changes?
|
|
if diff['extra_specs']:
|
|
volume_name = volume.get('id')
|
|
with self._client.open_connection() as api:
|
|
try:
|
|
# Get our volume
|
|
scvolume = api.find_volume(volume_name)
|
|
if scvolume is None:
|
|
LOG.error(_LE('Retype unable to find volume %s.'),
|
|
volume_name)
|
|
return False
|
|
# Check our specs.
|
|
# Storage profiles.
|
|
current, requested = (
|
|
self._get_retype_spec(diff, volume_name,
|
|
'Storage Profile',
|
|
'storagetype:storageprofile'))
|
|
# if there is a change and it didn't work fast fail.
|
|
if (current != requested and not
|
|
api.update_storage_profile(scvolume, requested)):
|
|
LOG.error(_LE('Failed to update storage profile'))
|
|
return False
|
|
|
|
# Replay profiles.
|
|
current, requested = (
|
|
self._get_retype_spec(diff, volume_name,
|
|
'Replay Profiles',
|
|
'storagetype:replayprofiles'))
|
|
# if there is a change and it didn't work fast fail.
|
|
if requested and not api.update_replay_profiles(scvolume,
|
|
requested):
|
|
LOG.error(_LE('Failed to update replay profiles'))
|
|
return False
|
|
|
|
# Replication_enabled.
|
|
current, requested = (
|
|
self._get_retype_spec(diff,
|
|
volume_name,
|
|
'replication_enabled',
|
|
'replication_enabled'))
|
|
# if there is a change and it didn't work fast fail.
|
|
if current != requested:
|
|
if requested:
|
|
model_update = self._create_replications(api,
|
|
volume,
|
|
scvolume)
|
|
else:
|
|
self._delete_replications(api, volume)
|
|
model_update = {'replication_status': 'disabled',
|
|
'replication_driver_data': ''}
|
|
|
|
# Active Replay
|
|
current, requested = (
|
|
self._get_retype_spec(diff, volume_name,
|
|
'Replicate Active Replay',
|
|
'replication:activereplay'))
|
|
if current != requested and not (
|
|
api.update_replicate_active_replay(
|
|
scvolume, requested == '<is> True')):
|
|
LOG.error(_LE('Failed to apply '
|
|
'replication:activereplay setting'))
|
|
return False
|
|
|
|
# TODO(tswanson): replaytype once it actually works.
|
|
|
|
except exception.VolumeBackendAPIException:
|
|
# We do nothing with this. We simply return failure.
|
|
return False
|
|
# If we have something to send down...
|
|
if model_update:
|
|
return model_update
|
|
return True
|
|
|
|
def _parse_secondary(self, api, secondary):
|
|
"""Find the replication destination associated with secondary.
|
|
|
|
:param api: Dell StorageCenterApi
|
|
:param secondary: String indicating the secondary to failover to.
|
|
:return: Destination SSN for the given secondary.
|
|
"""
|
|
LOG.debug('_parse_secondary. Looking for %s.', secondary)
|
|
destssn = None
|
|
# Trundle through these looking for our secondary.
|
|
for backend in self.backends:
|
|
ssnstring = backend['target_device_id']
|
|
# If they list a secondary it has to match.
|
|
# If they do not list a secondary we return the first
|
|
# replication on a working system.
|
|
if not secondary or secondary == ssnstring:
|
|
# Is a string. Need an int.
|
|
ssn = int(ssnstring)
|
|
# Without the source being up we have no good
|
|
# way to pick a destination to failover to. So just
|
|
# look for one that is just up.
|
|
try:
|
|
# If the SC ssn exists use it.
|
|
if api.find_sc(ssn):
|
|
destssn = ssn
|
|
break
|
|
except exception.VolumeBackendAPIException:
|
|
LOG.warning(_LW('SSN %s appears to be down.'), ssn)
|
|
LOG.info(_LI('replication failover secondary is %(ssn)s'),
|
|
{'ssn': destssn})
|
|
return destssn
|
|
|
|
def _update_backend(self, active_backend_id):
|
|
# Update our backend id. On the next open_connection it will use this.
|
|
self.active_backend_id = str(active_backend_id)
|
|
self._client.active_backend_id = self.active_backend_id
|
|
|
|
def failover_host(self, context, volumes, secondary_id=None):
|
|
"""Failover to secondary.
|
|
|
|
:param context: security context
|
|
:param secondary_id: Specifies rep target to fail over to
|
|
:param volumes: List of volumes serviced by this backend.
|
|
:returns : destssn, volume_updates data structure
|
|
|
|
Example volume_updates data structure:
|
|
[{'volume_id': <cinder-uuid>,
|
|
'updates': {'provider_id': 8,
|
|
'replication_status': 'failed-over',
|
|
'replication_extended_status': 'whatever',...}},]
|
|
"""
|
|
|
|
# We do not allow failback. Dragons be there.
|
|
if self.failed_over:
|
|
raise exception.VolumeBackendAPIException(message=_(
|
|
'Backend has already been failed over. Unable to fail back.'))
|
|
|
|
LOG.info(_LI('Failing backend to %s'), secondary_id)
|
|
# basic check
|
|
if self.replication_enabled:
|
|
with self._client.open_connection() as api:
|
|
# Look for the specified secondary.
|
|
destssn = self._parse_secondary(api, secondary_id)
|
|
if destssn:
|
|
# We roll through trying to break replications.
|
|
# Is failing here a complete failure of failover?
|
|
volume_updates = []
|
|
for volume in volumes:
|
|
model_update = {}
|
|
if volume.get('replication_driver_data'):
|
|
ret = api.break_replication(volume['id'], destssn)
|
|
LOG.info(_LI('Failing over volume %(id)s '
|
|
'replication: %(res)s.'),
|
|
{'id': volume['id'],
|
|
'res': ('FAILED', 'SUCCESS')[ret]})
|
|
# We should note that we are now failed over.
|
|
model_update = {
|
|
'replication_status': 'failed-over'}
|
|
else:
|
|
# Not a replicated volume. Try to unmap it.
|
|
scvolume = api.find_volume(volume['id'])
|
|
api.remove_mappings(scvolume)
|
|
model_update = {'status': 'error'}
|
|
# Either we are failed over or our status is now error.
|
|
volume_updates.append({'volume_id': volume['id'],
|
|
'updates': model_update})
|
|
|
|
# this is it.
|
|
self._update_backend(destssn)
|
|
return destssn, volume_updates
|
|
else:
|
|
raise exception.InvalidInput(message=(
|
|
_('replication_failover failed. %s not found.') %
|
|
secondary_id))
|
|
# I don't think we should ever get here.
|
|
raise exception.VolumeBackendAPIException(message=(
|
|
_('replication_failover failed. '
|
|
'Backend not configured for failover')))
|
|
|
|
def _get_unmanaged_replay(self, api, volume_name, existing_ref):
|
|
replay_name = None
|
|
if existing_ref:
|
|
replay_name = existing_ref.get('source-name')
|
|
if not replay_name:
|
|
msg = _('_get_unmanaged_replay: Must specify source-name.')
|
|
LOG.error(msg)
|
|
raise exception.ManageExistingInvalidReference(
|
|
existing_ref=existing_ref, reason=msg)
|
|
# Find our volume.
|
|
scvolume = api.find_volume(volume_name)
|
|
if not scvolume:
|
|
# Didn't find it.
|
|
msg = (_('_get_unmanaged_replay: Cannot find volume id %s')
|
|
% volume_name)
|
|
LOG.error(msg)
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
# Find our replay.
|
|
screplay = api.find_replay(scvolume, replay_name)
|
|
if not screplay:
|
|
# Didn't find it. Reference must be invalid.
|
|
msg = (_('_get_unmanaged_replay: Cannot '
|
|
'find snapshot named %s') % replay_name)
|
|
LOG.error(msg)
|
|
raise exception.ManageExistingInvalidReference(
|
|
existing_ref=existing_ref, reason=msg)
|
|
return screplay
|
|
|
|
def manage_existing_snapshot(self, snapshot, existing_ref):
|
|
"""Brings an existing backend storage object under Cinder management.
|
|
|
|
existing_ref is passed straight through from the API request's
|
|
manage_existing_ref value, and it is up to the driver how this should
|
|
be interpreted. It should be sufficient to identify a storage object
|
|
that the driver should somehow associate with the newly-created cinder
|
|
snapshot structure.
|
|
|
|
There are two ways to do this:
|
|
|
|
1. Rename the backend storage object so that it matches the
|
|
snapshot['name'] which is how drivers traditionally map between a
|
|
cinder snapshot and the associated backend storage object.
|
|
|
|
2. Place some metadata on the snapshot, or somewhere in the backend,
|
|
that allows other driver requests (e.g. delete) to locate the
|
|
backend storage object when required.
|
|
|
|
If the existing_ref doesn't make sense, or doesn't refer to an existing
|
|
backend storage object, raise a ManageExistingInvalidReference
|
|
exception.
|
|
"""
|
|
volume_name = snapshot.get('volume_id')
|
|
snapshot_id = snapshot.get('id')
|
|
with self._client.open_connection() as api:
|
|
# Find our unmanaged snapshot. This will raise on error.
|
|
screplay = self._get_unmanaged_replay(api, volume_name,
|
|
existing_ref)
|
|
# Manage means update description and update expiration.
|
|
if not api.manage_replay(screplay, snapshot_id):
|
|
# That didn't work. Error.
|
|
msg = (_('manage_existing_snapshot: Error managing '
|
|
'existing replay %(ss)s on volume %(vol)s') %
|
|
{'ss': screplay.get('description'),
|
|
'vol': volume_name})
|
|
LOG.error(msg)
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
|
|
# Life is good. Let the world know what we've done.
|
|
LOG.info(_LI('manage_existing_snapshot: snapshot %(exist)s on '
|
|
'volume %(volume)s has been renamed to %(id)s and is '
|
|
'now managed by Cinder.'),
|
|
{'exist': screplay.get('description'),
|
|
'volume': volume_name,
|
|
'id': snapshot_id})
|
|
|
|
# NOTE: Can't use abstractmethod before all drivers implement it
|
|
def manage_existing_snapshot_get_size(self, snapshot, existing_ref):
|
|
"""Return size of snapshot to be managed by manage_existing.
|
|
|
|
When calculating the size, round up to the next GB.
|
|
"""
|
|
volume_name = snapshot.get('volume_id')
|
|
with self._client.open_connection() as api:
|
|
screplay = self._get_unmanaged_replay(api, volume_name,
|
|
existing_ref)
|
|
sz, rem = dell_storagecenter_api.StorageCenterApi.size_to_gb(
|
|
screplay['size'])
|
|
if rem > 0:
|
|
raise exception.VolumeBackendAPIException(
|
|
data=_('Volume size must be a multiple of 1 GB.'))
|
|
return sz
|
|
|
|
# NOTE: Can't use abstractmethod before all drivers implement it
|
|
def unmanage_snapshot(self, snapshot):
|
|
"""Removes the specified snapshot from Cinder management.
|
|
|
|
Does not delete the underlying backend storage object.
|
|
|
|
NOTE: We do set the expire countdown to 1 day. Once a snapshot is
|
|
unmanaged it will expire 24 hours later.
|
|
"""
|
|
volume_name = snapshot.get('volume_id')
|
|
snapshot_id = snapshot.get('id')
|
|
with self._client.open_connection() as api:
|
|
# Find our volume.
|
|
scvolume = api.find_volume(volume_name)
|
|
if not scvolume:
|
|
# Didn't find it.
|
|
msg = (_('unmanage_snapshot: Cannot find volume id %s')
|
|
% volume_name)
|
|
LOG.error(msg)
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
# Find our replay.
|
|
screplay = api.find_replay(scvolume, snapshot_id)
|
|
if not screplay:
|
|
# Didn't find it. Reference must be invalid.
|
|
msg = (_('unmanage_snapshot: Cannot find snapshot named %s')
|
|
% snapshot_id)
|
|
LOG.error(msg)
|
|
raise exception.VolumeBackendAPIException(data=msg)
|
|
# Free our snapshot.
|
|
api.unmanage_replay(screplay)
|
|
# Do not check our result.
|