# Copyright (c) 2014 Scality # # 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. """ Volume driver for the Scality REST Block storage system This driver provisions Linux SRB volumes leveraging RESTful storage platforms (e.g. Scality CDMI). """ import contextlib import functools import re import sys import time from oslo_concurrency import lockutils from oslo_concurrency import processutils as putils from oslo_config import cfg from oslo_log import log as logging from oslo_utils import excutils from oslo_utils import units import six from six.moves import range from cinder.brick.local_dev import lvm from cinder import exception from cinder.i18n import _, _LI, _LE, _LW from cinder.image import image_utils from cinder import utils from cinder.volume import driver from cinder.volume import utils as volutils LOG = logging.getLogger(__name__) srb_opts = [ cfg.StrOpt('srb_base_urls', default=None, help='Comma-separated list of REST servers IP to connect to. ' '(eg http://IP1/,http://IP2:81/path'), ] CONF = cfg.CONF CONF.register_opts(srb_opts) ACCEPTED_REST_SERVER = re.compile(r'^http://' '(\d{1,3}\.){3}\d{1,3}' '(:\d+)?/[a-zA-Z0-9\-_\/]*$') class retry(object): SLEEP_NONE = 'none' SLEEP_DOUBLE = 'double' SLEEP_INCREMENT = 'increment' def __init__(self, exceptions, count, sleep_mechanism=SLEEP_INCREMENT, sleep_factor=1): if sleep_mechanism not in [self.SLEEP_NONE, self.SLEEP_DOUBLE, self.SLEEP_INCREMENT]: raise ValueError('Invalid value for `sleep_mechanism` argument') self._exceptions = exceptions self._count = count self._sleep_mechanism = sleep_mechanism self._sleep_factor = sleep_factor def __call__(self, fun): func_name = fun.func_name @functools.wraps(fun) def wrapped(*args, **kwargs): sleep_time = self._sleep_factor exc_info = None for attempt in range(self._count): if attempt != 0: LOG.warning(_LW('Retrying failed call to %(func)s, ' 'attempt %(attempt)i.'), {'func': func_name, 'attempt': attempt}) try: return fun(*args, **kwargs) except self._exceptions: exc_info = sys.exc_info() if attempt != self._count - 1: if self._sleep_mechanism == self.SLEEP_NONE: continue elif self._sleep_mechanism == self.SLEEP_INCREMENT: time.sleep(sleep_time) sleep_time += self._sleep_factor elif self._sleep_mechanism == self.SLEEP_DOUBLE: time.sleep(sleep_time) sleep_time *= 2 else: raise ValueError('Unknown sleep mechanism: %r' % self._sleep_mechanism) six.reraise(exc_info[0], exc_info[1], exc_info[2]) return wrapped class LVM(lvm.LVM): def activate_vg(self): """Activate the Volume Group associated with this instantiation. :raises: putils.ProcessExecutionError """ cmd = ['vgchange', '-ay', self.vg_name] try: self._execute(*cmd, root_helper=self._root_helper, run_as_root=True) except putils.ProcessExecutionError as err: LOG.exception(_LE('Error activating Volume Group')) LOG.error(_LE('Cmd :%s'), err.cmd) LOG.error(_LE('StdOut :%s'), err.stdout) LOG.error(_LE('StdErr :%s'), err.stderr) raise def deactivate_vg(self): """Deactivate the Volume Group associated with this instantiation. This forces LVM to release any reference to the device. :raises: putils.ProcessExecutionError """ cmd = ['vgchange', '-an', self.vg_name] try: self._execute(*cmd, root_helper=self._root_helper, run_as_root=True) except putils.ProcessExecutionError as err: LOG.exception(_LE('Error deactivating Volume Group')) LOG.error(_LE('Cmd :%s'), err.cmd) LOG.error(_LE('StdOut :%s'), err.stdout) LOG.error(_LE('StdErr :%s'), err.stderr) raise def destroy_vg(self): """Destroy the Volume Group associated with this instantiation. :raises: putils.ProcessExecutionError """ cmd = ['vgremove', '-f', self.vg_name] try: self._execute(*cmd, root_helper=self._root_helper, run_as_root=True) except putils.ProcessExecutionError as err: LOG.exception(_LE('Error destroying Volume Group')) LOG.error(_LE('Cmd :%s'), err.cmd) LOG.error(_LE('StdOut :%s'), err.stdout) LOG.error(_LE('StdErr :%s'), err.stderr) raise def pv_resize(self, pv_name, new_size_str): """Extend the size of an existing PV (for virtual PVs). :raises: putils.ProcessExecutionError """ try: self._execute('pvresize', '--setphysicalvolumesize', new_size_str, pv_name, root_helper=self._root_helper, run_as_root=True) except putils.ProcessExecutionError as err: LOG.exception(_LE('Error resizing Physical Volume')) LOG.error(_LE('Cmd :%s'), err.cmd) LOG.error(_LE('StdOut :%s'), err.stdout) LOG.error(_LE('StdErr :%s'), err.stderr) raise def extend_thin_pool(self): """Extend the size of the thin provisioning pool. This method extends the size of a thin provisioning pool to 95% of the size of the VG, if the VG is configured as thin and owns a thin provisioning pool. :raises: putils.ProcessExecutionError """ if self.vg_thin_pool is None: return new_size_str = self._calculate_thin_pool_size() try: self._execute('lvextend', '-L', new_size_str, "%s/%s-pool" % (self.vg_name, self.vg_name), root_helper=self._root_helper, run_as_root=True) except putils.ProcessExecutionError as err: LOG.exception(_LE('Error extending thin provisioning pool')) LOG.error(_LE('Cmd :%s'), err.cmd) LOG.error(_LE('StdOut :%s'), err.stdout) LOG.error(_LE('StdErr :%s'), err.stderr) raise @contextlib.contextmanager def patched(obj, attr, fun): """Context manager to locally patch a method. Within the managed context, the `attr` method of `obj` will be replaced by a method which calls `fun` passing in the original `attr` attribute of `obj` as well as any positional and keyword arguments. At the end of the context, the original method is restored. """ orig = getattr(obj, attr) def patch(*args, **kwargs): return fun(orig, *args, **kwargs) setattr(obj, attr, patch) try: yield finally: setattr(obj, attr, orig) @contextlib.contextmanager def handle_process_execution_error(message, info_message, reraise=True): """Consistently handle `putils.ProcessExecutionError` exceptions This context-manager will catch any `putils.ProcessExecutionError` exceptions raised in the managed block, and generate logging output accordingly. The value of the `message` argument will be logged at `logging.ERROR` level, and the `info_message` argument at `logging.INFO` level. Finally the command string, exit code, standard output and error output of the process will be logged at `logging.DEBUG` level. The `reraise` argument specifies what should happen when a `putils.ProcessExecutionError` is caught. If it's equal to `True`, the exception will be re-raised. If it's some other non-`False` object, this object will be raised instead (so you most likely want it to be some `Exception`). Any `False` value will result in the exception to be swallowed. """ try: yield except putils.ProcessExecutionError as exc: LOG.error(message) LOG.info(info_message) LOG.debug('Command : %s', exc.cmd) LOG.debug('Exit Code : %r', exc.exit_code) LOG.debug('StdOut : %s', exc.stdout) LOG.debug('StdErr : %s', exc.stderr) if reraise is True: raise elif reraise: raise reraise # pylint: disable=E0702 @contextlib.contextmanager def temp_snapshot(driver, volume, src_vref): snapshot = {'volume_name': src_vref['name'], 'volume_id': src_vref['id'], 'volume_size': src_vref['size'], 'name': 'snapshot-clone-%s' % volume['id'], 'id': 'tmp-snap-%s' % volume['id'], 'size': src_vref['size']} driver.create_snapshot(snapshot) try: yield snapshot finally: driver.delete_snapshot(snapshot) @contextlib.contextmanager def temp_raw_device(driver, volume): driver._attach_file(volume) try: yield finally: driver._detach_file(volume) @contextlib.contextmanager def temp_lvm_device(driver, volume): with temp_raw_device(driver, volume): vg = driver._get_lvm_vg(volume) vg.activate_vg() yield vg class SRBDriver(driver.VolumeDriver): """Scality SRB volume driver This driver manages volumes provisioned by the Scality REST Block driver Linux kernel module, backed by RESTful storage providers (e.g. Scality CDMI). """ VERSION = '1.1.0' # Over-allocation ratio (multiplied with requested size) for thin # provisioning OVER_ALLOC_RATIO = 2 SNAPSHOT_PREFIX = 'snapshot' def __init__(self, *args, **kwargs): super(SRBDriver, self).__init__(*args, **kwargs) self.configuration.append_config_values(srb_opts) self.urls_setup = False self.backend_name = None self.base_urls = None self.root_helper = utils.get_root_helper() self._attached_devices = {} def _setup_urls(self): if not self.base_urls: message = _("No url configured") raise exception.VolumeBackendAPIException(data=message) with handle_process_execution_error( message=_LE('Cound not setup urls on the Block Driver.'), info_message=_LI('Error creating Volume'), reraise=False): cmd = self.base_urls path = '/sys/class/srb/add_urls' putils.execute('tee', path, process_input=cmd, root_helper=self.root_helper, run_as_root=True) self.urls_setup = True def do_setup(self, context): """Any initialization the volume driver does while starting.""" self.backend_name = self.configuration.safe_get('volume_backend_name') base_urls = self.configuration.safe_get('srb_base_urls') sane_urls = [] if base_urls: for url in base_urls.split(','): stripped_url = url.strip() if ACCEPTED_REST_SERVER.match(stripped_url): sane_urls.append(stripped_url) else: LOG.warning(_LW("%s is not an accepted REST server " "IP address"), stripped_url) self.base_urls = ','.join(sane_urls) self._setup_urls() def check_for_setup_error(self): """Returns an error if prerequisites aren't met.""" if not self.base_urls: LOG.warning(_LW("Configuration variable srb_base_urls" " not set or empty.")) if self.urls_setup is False: message = _("Could not setup urls properly") raise exception.VolumeBackendAPIException(data=message) @classmethod def _is_snapshot(cls, volume): return volume['name'].startswith(cls.SNAPSHOT_PREFIX) @classmethod def _get_volname(cls, volume): """Returns the name of the actual volume If the volume is a snapshot, it returns the name of the parent volume. otherwise, returns the volume's name. """ name = volume['name'] if cls._is_snapshot(volume): name = "volume-%s" % (volume['volume_id']) return name @classmethod def _get_volid(cls, volume): """Returns the ID of the actual volume If the volume is a snapshot, it returns the ID of the parent volume. otherwise, returns the volume's id. """ volid = volume['id'] if cls._is_snapshot(volume): volid = volume['volume_id'] return volid @classmethod def _device_name(cls, volume): volume_id = cls._get_volid(volume) name = 'cinder-%s' % volume_id # Device names can't be longer than 32 bytes (incl. \0) return name[:31] @classmethod def _device_path(cls, volume): return "/dev/" + cls._device_name(volume) @classmethod def _escape_snapshot(cls, snapshot_name): # Linux LVM reserves name that starts with snapshot, so that # such volume name can't be created. Mangle it. if not snapshot_name.startswith(cls.SNAPSHOT_PREFIX): return snapshot_name return '_' + snapshot_name @classmethod def _mapper_path(cls, volume): groupname = cls._get_volname(volume) name = volume['name'] if cls._is_snapshot(volume): name = cls._escape_snapshot(name) # NOTE(vish): stops deprecation warning groupname = groupname.replace('-', '--') name = name.replace('-', '--') return "/dev/mapper/%s-%s" % (groupname, name) @staticmethod def _size_int(size_in_g): try: return max(int(size_in_g), 1) except ValueError: message = (_("Invalid size parameter '%s': Cannot be interpreted" " as an integer value.") % size_in_g) LOG.error(message) raise exception.VolumeBackendAPIException(data=message) @classmethod def _set_device_path(cls, volume): volume['provider_location'] = cls._get_volname(volume) return { 'provider_location': volume['provider_location'], } @staticmethod def _activate_lv(orig, *args, **kwargs): """Activate lv. Use with `patched` to patch `lvm.LVM.activate_lv` to ignore `EEXIST` """ try: orig(*args, **kwargs) except putils.ProcessExecutionError as exc: if exc.exit_code != 5: raise else: LOG.debug('`activate_lv` returned 5, ignored') def _get_lvm_vg(self, volume, create_vg=False): # NOTE(joachim): One-device volume group to manage thin snapshots # Get origin volume name even for snapshots volume_name = self._get_volname(volume) physical_volumes = [self._device_path(volume)] with patched(lvm.LVM, 'activate_lv', self._activate_lv): return LVM(volume_name, utils.get_root_helper(), create_vg=create_vg, physical_volumes=physical_volumes, lvm_type='thin', executor=self._execute) @staticmethod def _volume_not_present(vg, volume_name): # Used to avoid failing to delete a volume for which # the create operation partly failed return vg.get_volume(volume_name) is None def _create_file(self, volume): message = _('Could not create volume on any configured REST server.') with handle_process_execution_error( message=message, info_message=_LI('Error creating Volume %s.') % volume['name'], reraise=exception.VolumeBackendAPIException(data=message)): size = self._size_int(volume['size']) * self.OVER_ALLOC_RATIO cmd = volume['name'] cmd += ' %dG' % size path = '/sys/class/srb/create' putils.execute('tee', path, process_input=cmd, root_helper=self.root_helper, run_as_root=True) return self._set_device_path(volume) def _extend_file(self, volume, new_size): message = _('Could not extend volume on any configured REST server.') with handle_process_execution_error( message=message, info_message=(_LI('Error extending Volume %s.') % volume['name']), reraise=exception.VolumeBackendAPIException(data=message)): size = self._size_int(new_size) * self.OVER_ALLOC_RATIO cmd = volume['name'] cmd += ' %dG' % size path = '/sys/class/srb/extend' putils.execute('tee', path, process_input=cmd, root_helper=self.root_helper, run_as_root=True) @staticmethod def _destroy_file(volume): message = _('Could not destroy volume on any configured REST server.') volname = volume['name'] with handle_process_execution_error( message=message, info_message=_LI('Error destroying Volume %s.') % volname, reraise=exception.VolumeBackendAPIException(data=message)): cmd = volume['name'] path = '/sys/class/srb/destroy' putils.execute('tee', path, process_input=cmd, root_helper=utils.get_root_helper(), run_as_root=True) # NOTE(joachim): Must only be called within a function decorated by: # @lockutils.synchronized('devices', 'cinder-srb-') def _increment_attached_count(self, volume): """Increments the attach count of the device""" volid = self._get_volid(volume) if volid not in self._attached_devices: self._attached_devices[volid] = 1 else: self._attached_devices[volid] += 1 # NOTE(joachim): Must only be called within a function decorated by: # @lockutils.synchronized('devices', 'cinder-srb-') def _decrement_attached_count(self, volume): """Decrements the attach count of the device""" volid = self._get_volid(volume) if volid not in self._attached_devices: raise exception.VolumeBackendAPIException( (_("Internal error in srb driver: " "Trying to detach detached volume %s.")) % (self._get_volname(volume)) ) self._attached_devices[volid] -= 1 if self._attached_devices[volid] == 0: del self._attached_devices[volid] # NOTE(joachim): Must only be called within a function decorated by: # @lockutils.synchronized('devices', 'cinder-srb-') def _get_attached_count(self, volume): volid = self._get_volid(volume) return self._attached_devices.get(volid, 0) @lockutils.synchronized('devices', 'cinder-srb-') def _is_attached(self, volume): return self._get_attached_count(volume) > 0 @lockutils.synchronized('devices', 'cinder-srb-') def _attach_file(self, volume): name = self._get_volname(volume) devname = self._device_name(volume) LOG.debug('Attaching volume %(name)s as %(devname)s', {'name': name, 'devname': devname}) count = self._get_attached_count(volume) if count == 0: message = (_('Could not attach volume %(vol)s as %(dev)s ' 'on system.') % {'vol': name, 'dev': devname}) with handle_process_execution_error( message=message, info_message=_LI('Error attaching Volume'), reraise=exception.VolumeBackendAPIException(data=message)): cmd = name + ' ' + devname path = '/sys/class/srb/attach' putils.execute('tee', path, process_input=cmd, root_helper=self.root_helper, run_as_root=True) else: LOG.debug('Volume %s already attached', name) self._increment_attached_count(volume) @retry(exceptions=(putils.ProcessExecutionError, ), count=3, sleep_mechanism=retry.SLEEP_INCREMENT, sleep_factor=5) def _do_deactivate(self, volume, vg): vg.deactivate_vg() @retry(exceptions=(putils.ProcessExecutionError, ), count=5, sleep_mechanism=retry.SLEEP_DOUBLE, sleep_factor=1) def _do_detach(self, volume, vg): devname = self._device_name(volume) volname = self._get_volname(volume) cmd = devname path = '/sys/class/srb/detach' try: putils.execute('tee', path, process_input=cmd, root_helper=self.root_helper, run_as_root=True) except putils.ProcessExecutionError: with excutils.save_and_reraise_exception(reraise=True): try: with patched(lvm.LVM, 'activate_lv', self._activate_lv): vg.activate_lv(volname) self._do_deactivate(volume, vg) except putils.ProcessExecutionError: LOG.warning(_LW('All attempts to recover failed detach ' 'of %(volume)s failed.'), {'volume': volname}) @lockutils.synchronized('devices', 'cinder-srb-') def _detach_file(self, volume): name = self._get_volname(volume) devname = self._device_name(volume) vg = self._get_lvm_vg(volume) LOG.debug('Detaching device %s', devname) count = self._get_attached_count(volume) if count > 1: LOG.info(_LI('Reference count of %(volume)s is %(count)d, ' 'not detaching.'), {'volume': volume['name'], 'count': count}) return message = (_('Could not detach volume %(vol)s from device %(dev)s.') % {'vol': name, 'dev': devname}) with handle_process_execution_error( message=message, info_message=_LI('Error detaching Volume'), reraise=exception.VolumeBackendAPIException(data=message)): try: if vg is not None: self._do_deactivate(volume, vg) except putils.ProcessExecutionError: LOG.error(_LE('Could not deactivate volume group %s'), self._get_volname(volume)) raise try: self._do_detach(volume, vg=vg) except putils.ProcessExecutionError: LOG.error(_LE('Could not detach volume %(vol)s from device ' '%(dev)s.'), {'vol': name, 'dev': devname}) raise self._decrement_attached_count(volume) def _setup_lvm(self, volume): # NOTE(joachim): One-device volume group to manage thin snapshots size = self._size_int(volume['size']) * self.OVER_ALLOC_RATIO size_str = '%dg' % size vg = self._get_lvm_vg(volume, create_vg=True) vg.create_volume(volume['name'], size_str, lv_type='thin') def _destroy_lvm(self, volume): vg = self._get_lvm_vg(volume) if vg.lv_has_snapshot(volume['name']): LOG.error(_LE('Unable to delete due to existing snapshot ' 'for volume: %s.'), volume['name']) raise exception.VolumeIsBusy(volume_name=volume['name']) vg.destroy_vg() # NOTE(joachim) Force lvm vg flush through a vgs command vgs = vg.get_all_volume_groups(root_helper=self.root_helper, vg_name=vg.vg_name) if len(vgs) != 0: LOG.warning(_LW('Removed volume group %s still appears in vgs.'), vg.vg_name) def _create_and_copy_volume(self, dstvol, srcvol): """Creates a volume from a volume or a snapshot.""" updates = self._create_file(dstvol) # We need devices attached for IO operations. with temp_lvm_device(self, srcvol) as vg, \ temp_raw_device(self, dstvol): self._setup_lvm(dstvol) # Some configurations of LVM do not automatically activate # ThinLVM snapshot LVs. with patched(lvm.LVM, 'activate_lv', self._activate_lv): vg.activate_lv(srcvol['name'], True) # copy_volume expects sizes in MiB, we store integer GiB # be sure to convert before passing in volutils.copy_volume(self._mapper_path(srcvol), self._mapper_path(dstvol), srcvol['volume_size'] * units.Ki, self.configuration.volume_dd_blocksize, execute=self._execute) return updates def create_volume(self, volume): """Creates a volume. Can optionally return a Dictionary of changes to the volume object to be persisted. """ updates = self._create_file(volume) # We need devices attached for LVM operations. with temp_raw_device(self, volume): self._setup_lvm(volume) return updates def create_volume_from_snapshot(self, volume, snapshot): """Creates a volume from a snapshot.""" return self._create_and_copy_volume(volume, snapshot) def create_cloned_volume(self, volume, src_vref): """Creates a clone of the specified volume.""" LOG.info(_LI('Creating clone of volume: %s'), src_vref['id']) updates = None with temp_lvm_device(self, src_vref): with temp_snapshot(self, volume, src_vref) as snapshot: updates = self._create_and_copy_volume(volume, snapshot) return updates def delete_volume(self, volume): """Deletes a volume.""" attached = False if self._is_attached(volume): attached = True with temp_lvm_device(self, volume): self._destroy_lvm(volume) self._detach_file(volume) LOG.debug('Deleting volume %(volume_name)s, attached=%(attached)s', {'volume_name': volume['name'], 'attached': attached}) self._destroy_file(volume) def create_snapshot(self, snapshot): """Creates a snapshot.""" with temp_lvm_device(self, snapshot) as vg: # NOTE(joachim) we only want to support thin lvm_types vg.create_lv_snapshot(self._escape_snapshot(snapshot['name']), snapshot['volume_name'], lv_type='thin') def delete_snapshot(self, snapshot): """Deletes a snapshot.""" with temp_lvm_device(self, snapshot) as vg: if self._volume_not_present( vg, self._escape_snapshot(snapshot['name'])): # If the snapshot isn't present, then don't attempt to delete LOG.warning(_LW("snapshot: %s not found, " "skipping delete operations"), snapshot['name']) return vg.delete(self._escape_snapshot(snapshot['name'])) def get_volume_stats(self, refresh=False): """Return the current state of the volume service.""" stats = { 'vendor_name': 'Scality', 'driver_version': self.VERSION, 'storage_protocol': 'Scality Rest Block Device', 'total_capacity_gb': 'infinite', 'free_capacity_gb': 'infinite', 'reserved_percentage': 0, 'volume_backend_name': self.backend_name, } return stats def copy_image_to_volume(self, context, volume, image_service, image_id): """Fetch the image from image_service and write it to the volume.""" with temp_lvm_device(self, volume): image_utils.fetch_to_volume_format(context, image_service, image_id, self._mapper_path(volume), 'qcow2', self.configuration. volume_dd_blocksize, size=volume['size']) def copy_volume_to_image(self, context, volume, image_service, image_meta): """Copy the volume to the specified image.""" with temp_lvm_device(self, volume): image_utils.upload_volume(context, image_service, image_meta, self._mapper_path(volume)) def extend_volume(self, volume, new_size): new_alloc_size = self._size_int(new_size) * self.OVER_ALLOC_RATIO new_size_str = '%dg' % new_alloc_size self._extend_file(volume, new_size) with temp_lvm_device(self, volume) as vg: vg.pv_resize(self._device_path(volume), new_size_str) vg.extend_thin_pool() vg.extend_volume(volume['name'], new_size_str) class SRBISCSIDriver(SRBDriver, driver.ISCSIDriver): """Scality SRB volume driver with ISCSI support This driver manages volumes provisioned by the Scality REST Block driver Linux kernel module, backed by RESTful storage providers (e.g. Scality CDMI), and exports them through ISCSI to Nova. """ VERSION = '1.0.0' def __init__(self, *args, **kwargs): self.db = kwargs.get('db') self.target_driver = \ self.target_mapping[self.configuration.safe_get('iscsi_helper')] super(SRBISCSIDriver, self).__init__(*args, **kwargs) self.backend_name =\ self.configuration.safe_get('volume_backend_name') or 'SRB_iSCSI' self.protocol = 'iSCSI' def set_execute(self, execute): super(SRBISCSIDriver, self).set_execute(execute) if self.target_driver is not None: self.target_driver.set_execute(execute) def ensure_export(self, context, volume): device_path = self._mapper_path(volume) model_update = self.target_driver.ensure_export(context, volume, device_path) if model_update: self.db.volume_update(context, volume['id'], model_update) def create_export(self, context, volume): """Creates an export for a logical volume.""" self._attach_file(volume) vg = self._get_lvm_vg(volume) vg.activate_vg() # SRB uses the same name as the volume for the VG volume_path = self._mapper_path(volume) data = self.target_driver.create_export(context, volume, volume_path) return { 'provider_location': data['location'], 'provider_auth': data['auth'], } def remove_export(self, context, volume): # NOTE(joachim) Taken from iscsi._ExportMixin.remove_export # This allows us to avoid "detaching" a device not attached by # an export, and avoid screwing up the device attach refcount. try: # Raises exception.NotFound if export not provisioned iscsi_target = self.target_driver._get_iscsi_target(context, volume['id']) # Raises an Exception if currently not exported location = volume['provider_location'].split(' ') iqn = location[1] self.target_driver.show_target(iscsi_target, iqn=iqn) self.target_driver.remove_export(context, volume) self._detach_file(volume) except exception.NotFound: LOG.warning(_LW('Volume %r not found while trying to remove.'), volume['id']) except Exception as exc: LOG.warning(_LW('Error while removing export: %r'), exc)