def update_migrated_volume(self, ctxt, volume, new_volume, original_volume_status): """Return model update from RBD for migrated volume. This method should rename the back-end volume name(id) on the destination host back to its original name(id) on the source host. :param ctxt: The context used to run the method update_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 """ name_id = None provider_location = None existing_name = CONF.volume_name_template % new_volume.id wanted_name = CONF.volume_name_template % volume.id with RADOSClient(self) as client: try: self.RBDProxy().rename(client.ioctx, utils.convert_str(existing_name), utils.convert_str(wanted_name)) except self.rbd.ImageNotFound: LOG.error(_LE('Unable to rename the logical volume ' 'for volume %s.'), volume.id) # If the rename fails, _name_id should be set to the new # volume id and provider_location should be set to the # one from the new volume as well. name_id = new_volume._name_id or new_volume.id provider_location = new_volume['provider_location'] return {'_name_id': name_id, 'provider_location': provider_location}
def _check_restore_vol_size(self, backup_base, restore_vol, restore_length, src_pool): """Ensure that the restore volume is the correct size. If the restore volume was bigger than the backup, the diff restore will shrink it to the size of the original backup so we need to post-process and resize it back to its expected size. """ with rbd_driver.RADOSClient(self, self._ceph_backup_pool) as client: adjust_size = 0 base_image = self.rbd.Image(client.ioctx, utils.convert_str(backup_base), read_only=True) try: if restore_length != base_image.size(): adjust_size = restore_length finally: base_image.close() if adjust_size: with rbd_driver.RADOSClient(self, src_pool) as client: restore_vol_encode = utils.convert_str(restore_vol) dest_image = self.rbd.Image(client.ioctx, restore_vol_encode) try: LOG.debug("Adjusting restore vol size") dest_image.resize(adjust_size) finally: dest_image.close()
def delete_snapshot(self, snapshot): """Deletes an rbd snapshot.""" # NOTE(dosaboy): this was broken by commit cbe1d5f. Ensure names are # utf-8 otherwise librbd will barf. volume_name = utils.convert_str(snapshot.volume_name) snap_name = utils.convert_str(snapshot.name) with RBDVolumeProxy(self, volume_name) as volume: try: volume.unprotect_snap(snap_name) except self.rbd.InvalidArgument: LOG.info( _LI("InvalidArgument: Unable to unprotect snapshot %s."), snap_name) except self.rbd.ImageNotFound: LOG.info( _LI("ImageNotFound: Unable to unprotect snapshot %s."), snap_name) except self.rbd.ImageBusy: children_list = self._get_children_info(volume, snap_name) if children_list: for (pool, image) in children_list: LOG.info(_LI('Image %(pool)s/%(image)s is dependent ' 'on the snapshot %(snap)s.'), {'pool': pool, 'image': image, 'snap': snap_name}) raise exception.SnapshotIsBusy(snapshot_name=snap_name) try: volume.remove_snap(snap_name) except self.rbd.ImageNotFound: LOG.info(_LI("Snapshot %s does not exist in backend."), snap_name)
def delete_snapshot(self, snapshot): """Deletes an rbd snapshot.""" # NOTE(dosaboy): this was broken by commit cbe1d5f. Ensure names are # utf-8 otherwise librbd will barf. volume_name = utils.convert_str(snapshot['volume_name']) snap_name = utils.convert_str(snapshot['name']) with RBDVolumeProxy(self, volume_name) as volume: try: volume.unprotect_snap(snap_name) except self.rbd.ImageBusy: raise exception.SnapshotIsBusy(snapshot_name=snap_name) volume.remove_snap(snap_name)
def _clone(self, volume, src_pool, src_image, src_snap): LOG.debug('cloning %(pool)s/%(img)s@%(snap)s to %(dst)s', dict(pool=src_pool, img=src_image, snap=src_snap, dst=volume['name'])) with RADOSClient(self, src_pool) as src_client: with RADOSClient(self) as dest_client: self.RBDProxy().clone(src_client.ioctx, utils.convert_str(src_image), utils.convert_str(src_snap), dest_client.ioctx, utils.convert_str(volume['name']), features=src_client.features)
def _get_backup_base_name(self, volume_id, backup_id=None, diff_format=False): """Return name of base image used for backup. Incremental backups use a new base name so we support old and new style format. """ # Ensure no unicode if diff_format: return utils.convert_str("volume-%s.backup.base" % volume_id) else: if backup_id is None: msg = _("Backup id required") raise exception.InvalidParameterValue(msg) return utils.convert_str("volume-%s.backup.%s" % (volume_id, backup_id))
def __init__(self, driver, name, pool=None, snapshot=None, read_only=False): client, ioctx = driver._connect_to_rados(pool) if snapshot is not None: snapshot = utils.convert_str(snapshot) try: self.volume = driver.rbd.Image(ioctx, utils.convert_str(name), snapshot=snapshot, read_only=read_only) except driver.rbd.Error: LOG.exception(_LE("error opening rbd image %s"), name) driver._disconnect_from_rados(client, ioctx) raise self.driver = driver self.client = client self.ioctx = ioctx
def _rbd_diff_transfer( self, src_name, src_pool, dest_name, dest_pool, src_user, src_conf, dest_user, dest_conf, src_snap=None, from_snap=None, ): """Copy only extents changed between two points. If no snapshot is provided, the diff extents will be all those changed since the rbd volume/base was created, otherwise it will be those changed since the snapshot was created. """ LOG.debug( "Performing differential transfer from '%(src)s' to " "'%(dest)s'", {"src": src_name, "dest": dest_name} ) # NOTE(dosaboy): Need to be tolerant of clusters/clients that do # not support these operations since at the time of writing they # were very new. src_ceph_args = self._ceph_args(src_user, src_conf, pool=src_pool) dest_ceph_args = self._ceph_args(dest_user, dest_conf, pool=dest_pool) cmd1 = ["rbd", "export-diff"] + src_ceph_args if from_snap is not None: cmd1.extend(["--from-snap", from_snap]) if src_snap: path = utils.convert_str("%s/%s@%s" % (src_pool, src_name, src_snap)) else: path = utils.convert_str("%s/%s" % (src_pool, src_name)) cmd1.extend([path, "-"]) cmd2 = ["rbd", "import-diff"] + dest_ceph_args rbd_path = utils.convert_str("%s/%s" % (dest_pool, dest_name)) cmd2.extend(["-", rbd_path]) ret, stderr = self._piped_execute(cmd1, cmd2) if ret: msg = _("RBD diff op failed - (ret=%(ret)s stderr=%(stderr)s)") % {"ret": ret, "stderr": stderr} LOG.info(msg) raise exception.BackupRBDOperationFailed(msg)
def manage_existing(self, volume, existing_ref): """Manages an existing image. Renames the image name to match the expected name for the volume. Error checking done by manage_existing_get_size is not repeated. :param volume: volume ref info to be set :param existing_ref: existing_ref is a dictionary of the form: {'source-name': <name of rbd image>} """ # Raise an exception if we didn't find a suitable rbd image. with RADOSClient(self) as client: rbd_name = existing_ref["source-name"] self.RBDProxy().rename(client.ioctx, utils.convert_str(rbd_name), utils.convert_str(volume["name"]))
def _connect_to_rados(self, pool=None): LOG.debug("opening connection to ceph cluster (timeout=%s).", self.configuration.rados_connect_timeout) client = self.rados.Rados( rados_id=self.configuration.rbd_user, clustername=self.configuration.rbd_cluster_name, conffile=self.configuration.rbd_ceph_conf) if pool is not None: pool = utils.convert_str(pool) else: pool = self.configuration.rbd_pool try: if self.configuration.rados_connect_timeout >= 0: client.connect(timeout= self.configuration.rados_connect_timeout) else: client.connect() ioctx = client.open_ioctx(pool) return client, ioctx except self.rados.Error: msg = _("Error connecting to ceph cluster.") LOG.exception(msg) client.shutdown() raise exception.VolumeBackendAPIException(data=msg)
def _connect_to_rados(self, pool=None): LOG.debug("opening connection to ceph cluster (timeout=%s).", self.configuration.rados_connect_timeout) # NOTE (e0ne): rados is binding to C lbirary librados. # It blocks eventlet loop so we need to run it in a native # python thread. client = tpool.Proxy( self.rados.Rados( rados_id=self.configuration.rbd_user, clustername=self.configuration.rbd_cluster_name, conffile=self.configuration.rbd_ceph_conf)) if pool is not None: pool = utils.convert_str(pool) else: pool = self.configuration.rbd_pool try: if self.configuration.rados_connect_timeout >= 0: client.connect(timeout= self.configuration.rados_connect_timeout) else: client.connect() ioctx = client.open_ioctx(pool) return client, ioctx except self.rados.Error: msg = _("Error connecting to ceph cluster.") LOG.exception(msg) client.shutdown() raise exception.VolumeBackendAPIException(data=msg)
def _clone(self, volume, src_pool, src_image, src_snap): LOG.debug('cloning %(pool)s/%(img)s@%(snap)s to %(dst)s', dict(pool=src_pool, img=src_image, snap=src_snap, dst=volume.name)) chunk_size = self.configuration.rbd_store_chunk_size * units.Mi order = int(math.log(chunk_size, 2)) with RADOSClient(self, src_pool) as src_client: with RADOSClient(self) as dest_client: self.RBDProxy().clone(src_client.ioctx, utils.convert_str(src_image), utils.convert_str(src_snap), dest_client.ioctx, utils.convert_str(volume.name), features=src_client.features, order=order)
def __init__(self, context, db_driver=None, execute=None): super(CephBackupDriver, self).__init__(context, db_driver) self.rbd = rbd self.rados = rados self.chunk_size = CONF.backup_ceph_chunk_size self._execute = execute or utils.execute if self._supports_stripingv2: self.rbd_stripe_unit = CONF.backup_ceph_stripe_unit self.rbd_stripe_count = CONF.backup_ceph_stripe_count else: LOG.info(_LI("RBD striping not supported - ignoring configuration " "settings for rbd striping")) self.rbd_stripe_count = 0 self.rbd_stripe_unit = 0 self._ceph_backup_user = utils.convert_str(CONF.backup_ceph_user) self._ceph_backup_pool = utils.convert_str(CONF.backup_ceph_pool) self._ceph_backup_conf = utils.convert_str(CONF.backup_ceph_conf)
def _connect_to_rados(self, pool=None): """Establish connection to the backup Ceph cluster.""" client = self.rados.Rados(rados_id=self._ceph_backup_user, conffile=self._ceph_backup_conf) try: client.connect() pool_to_open = utils.convert_str(pool or self._ceph_backup_pool) ioctx = client.open_ioctx(pool_to_open) return client, ioctx except self.rados.Error: # shutdown cannot raise an exception client.shutdown() raise
def copy_volume_to_image(self, context, volume, image_service, image_meta): tmp_dir = volume_utils.image_conversion_dir() tmp_file = os.path.join(tmp_dir, volume.name + '-' + image_meta['id']) with fileutils.remove_path_on_error(tmp_file): vol_name = utils.convert_str(volume.name) self._try_execute( 'qemu-img', 'convert', '-f', 'raw', 'vitastor:image=' + vol_name.replace(':', '\\:') + self._qemu_args(), '-O', 'raw', tmp_file) # FIXME: Copy directly if the destination image is also in Vitastor volume_utils.upload_volume(context, image_service, image_meta, tmp_file, volume) os.unlink(tmp_file)
def delete_snapshot(self, snapshot): """Deletes an rbd snapshot.""" # NOTE(dosaboy): this was broken by commit cbe1d5f. Ensure names are # utf-8 otherwise librbd will barf. volume_name = utils.convert_str(snapshot["volume_name"]) snap_name = utils.convert_str(snapshot["name"]) with RBDVolumeProxy(self, volume_name) as volume: try: volume.unprotect_snap(snap_name) except self.rbd.ImageBusy: children_list = self._get_children_info(volume, snap_name) if children_list: for (pool, image) in children_list: LOG.info( _LI("Image %(pool)s/%(image)s is dependent " "on the snapshot %(snap)s."), {"pool": pool, "image": image, "snap": snap_name}, ) raise exception.SnapshotIsBusy(snapshot_name=snap_name) volume.remove_snap(snap_name)
def _get_existing_name(existing_ref): if not isinstance(existing_ref, dict): existing_ref = {"source-name": existing_ref} if 'source-name' not in existing_ref: reason = _('Reference must contain source-name element.') raise exception.ManageExistingInvalidReference( existing_ref=existing_ref, reason=reason) src_name = utils.convert_str(existing_ref['source-name']) if not src_name: reason = _('Reference must contain source-name element.') raise exception.ManageExistingInvalidReference( existing_ref=existing_ref, reason=reason) return src_name
def create_volume(self, volume): """Creates a logical volume.""" size = int(volume["size"]) * units.Gi LOG.debug("creating volume '%s'", volume["name"]) chunk_size = self.configuration.rbd_store_chunk_size * units.Mi order = int(math.log(chunk_size, 2)) with RADOSClient(self) as client: self.RBDProxy().create( client.ioctx, utils.convert_str(volume["name"]), size, order, old_format=False, features=client.features )
def manage_existing_snapshot_get_size(self, snapshot, existing_ref): """Return size of an existing image for manage_existing. :param snapshot: snapshot ref info to be set :param existing_ref: {'source-name': <name of snapshot>} """ vol_name = utils.convert_str(snapshot.volume_name) snap_name = self._get_existing_name(existing_ref) vol = self._get_image(vol_name + '@' + snap_name) if not vol: raise exception.ManageExistingInvalidReference( existing_ref=snapshot_name, reason='Specified snapshot does not exist.') return int(math.ceil(float(vol['cfg']['size']) / units.Gi))
def revert_to_snapshot(self, context, volume, snapshot): """Revert a volume to a given snapshot.""" vol_name = utils.convert_str(snapshot.volume_name) snap_name = utils.convert_str(snapshot.name) # Delete the image and recreate it from the snapshot args = ['vitastor-cli', 'rm', vol_name, *(self._vitastor_args())] try: self._execute(*args) except processutils.ProcessExecutionError as exc: LOG.error("Failed to delete image " + vol_name + ": " + exc) raise exception.VolumeBackendAPIException(data=exc.stderr) args = [ 'vitastor-cli', 'create', '--parent', vol_name + '@' + snap_name, vol_name, *(self._vitastor_args()) ] try: self._execute(*args) except processutils.ProcessExecutionError as exc: LOG.error("Failed to recreate image " + vol_name + " from " + vol_name + "@" + snap_name + ": " + exc) raise exception.VolumeBackendAPIException(data=exc.stderr)
def create_volume_from_snapshot(self, volume, snapshot): """Creates a cloned volume from an existing snapshot.""" vol_name = utils.convert_str(volume.name) snap_name = utils.convert_str(snapshot.name) snap = self._get_image(vol_name + '@' + snap_name) if not snap: raise exception.SnapshotNotFound(snapshot_id=snap_name) snap_inode_id = int(resp['responses'][0]['kvs'][0]['value']['id']) snap_pool_id = int(resp['responses'][0]['kvs'][0]['value']['pool_id']) size = snap['cfg']['size'] if int(volume.size): size = int(volume.size) * units.Gi new_cfg = self._create_image( vol_name, { 'size': size, 'parent_id': snap['idx']['id'], 'parent_pool_id': snap['idx']['pool_id'], }) return {}
def __init__(self, *args, **kwargs): super(RBDDriver, self).__init__(*args, **kwargs) self.configuration.append_config_values(rbd_opts) self._stats = {} # allow overrides for testing self.rados = kwargs.get("rados", rados) self.rbd = kwargs.get("rbd", rbd) # All string args used with librbd must be None or utf-8 otherwise # librbd will break. for attr in ["rbd_cluster_name", "rbd_user", "rbd_ceph_conf", "rbd_pool"]: val = getattr(self.configuration, attr) if val is not None: setattr(self.configuration, attr, utils.convert_str(val))
def delete_snapshot(self, snapshot): """Deletes an rbd snapshot.""" # NOTE(dosaboy): this was broken by commit cbe1d5f. Ensure names are # utf-8 otherwise librbd will barf. volume_name = utils.convert_str(snapshot.volume_name) snap_name = utils.convert_str(snapshot.name) with RBDVolumeProxy(self, volume_name) as volume: try: volume.unprotect_snap(snap_name) except self.rbd.InvalidArgument: LOG.info( _LI("InvalidArgument: Unable to unprotect snapshot %s."), snap_name) except self.rbd.ImageNotFound: LOG.info( _LI("ImageNotFound: Unable to unprotect snapshot %s."), snap_name) except self.rbd.ImageBusy: children_list = self._get_children_info(volume, snap_name) if children_list: for (pool, image) in children_list: LOG.info( _LI('Image %(pool)s/%(image)s is dependent ' 'on the snapshot %(snap)s.'), { 'pool': pool, 'image': image, 'snap': snap_name }) raise exception.SnapshotIsBusy(snapshot_name=snap_name) try: volume.remove_snap(snap_name) except self.rbd.ImageNotFound: LOG.info(_LI("Snapshot %s does not exist in backend."), snap_name)
def __init__(self, *args, **kwargs): super(RBDDriver, self).__init__(*args, **kwargs) self.configuration.append_config_values(rbd_opts) self._stats = {} # allow overrides for testing self.rados = kwargs.get('rados', rados) self.rbd = kwargs.get('rbd', rbd) # All string args used with librbd must be None or utf-8 otherwise # librbd will break. for attr in ['rbd_cluster_name', 'rbd_user', 'rbd_ceph_conf', 'rbd_pool']: val = getattr(self.configuration, attr) if val is not None: setattr(self.configuration, attr, utils.convert_str(val))
def create_volume(self, volume): """Creates a logical volume.""" size = int(volume['size']) * units.Gi LOG.debug("creating volume '%s'", volume['name']) chunk_size = self.configuration.rbd_store_chunk_size * units.Mi order = int(math.log(chunk_size, 2)) with RADOSClient(self) as client: self.RBDProxy().create(client.ioctx, utils.convert_str(volume['name']), size, order, old_format=False, features=client.features)
def manage_existing_get_size(self, volume, existing_ref): """Return size of an existing image for manage_existing. :param volume: volume ref info to be set :param existing_ref: existing_ref is a dictionary of the form: {'source-name': <name of rbd image>} """ # Check that the reference is valid if 'source-name' not in existing_ref: reason = _('Reference must contain source-name element.') raise exception.ManageExistingInvalidReference( existing_ref=existing_ref, reason=reason) rbd_name = utils.convert_str(existing_ref['source-name']) with RADOSClient(self) as client: # Raise an exception if we didn't find a suitable rbd image. try: rbd_image = self.rbd.Image(client.ioctx, rbd_name) image_size = rbd_image.size() except self.rbd.ImageNotFound: kwargs = { 'existing_ref': rbd_name, 'reason': 'Specified rbd image does not exist.' } raise exception.ManageExistingInvalidReference(**kwargs) finally: rbd_image.close() # RBD image size is returned in bytes. Attempt to parse # size as a float and round up to the next integer. try: convert_size = int(math.ceil(int(image_size))) / units.Gi return convert_size except ValueError: exception_message = (_("Failed to manage existing volume " "%(name)s, because reported size " "%(size)s was not a floating-point" " number.") % { 'name': rbd_name, 'size': image_size }) raise exception.VolumeBackendAPIException( data=exception_message)
def manage_existing_get_size(self, volume, existing_ref): """Return size of an existing image for manage_existing. :param volume: volume ref info to be set :param existing_ref: existing_ref is a dictionary of the form: {'source-name': <name of rbd image>} """ # Check that the reference is valid if 'source-name' not in existing_ref: reason = _('Reference must contain source-name element.') raise exception.ManageExistingInvalidReference( existing_ref=existing_ref, reason=reason) rbd_name = utils.convert_str(existing_ref['source-name']) with RADOSClient(self) as client: # Raise an exception if we didn't find a suitable rbd image. try: rbd_image = self.rbd.Image(client.ioctx, rbd_name) image_size = rbd_image.size() except self.rbd.ImageNotFound: kwargs = {'existing_ref': rbd_name, 'reason': 'Specified rbd image does not exist.'} raise exception.ManageExistingInvalidReference(**kwargs) finally: rbd_image.close() # RBD image size is returned in bytes. Attempt to parse # size as a float and round up to the next integer. try: convert_size = int(math.ceil(int(image_size))) / units.Gi return convert_size except ValueError: exception_message = (_("Failed to manage existing volume " "%(name)s, because reported size " "%(size)s was not a floating-point" " number.") % {'name': rbd_name, 'size': image_size}) raise exception.VolumeBackendAPIException( data=exception_message)
def create_volume(self, volume): """Creates a logical volume.""" size = int(volume.size) * units.Gi # FIXME: Check if convert_str is really required vol_name = utils.convert_str(volume.name) if vol_name.find('@') >= 0 or vol_name.find('/') >= 0: raise exception.VolumeBackendAPIException( data='@ and / are forbidden in volume and snapshot names') LOG.debug("creating volume '%s'", vol_name) self._create_image(vol_name, {'size': size}) if volume.encryption_key_id: self._create_encrypted_volume(volume, volume.obj_context) volume_update = {} return volume_update
def extend_volume(self, volume, new_size): """Extend an existing volume.""" vol_name = utils.convert_str(volume.name) while True: vol = self._get_image(vol_name) if not vol: raise exception.VolumeBackendAPIException( data='Volume ' + vol_name + ' does not exist') # change size size = int(new_size) * units.Gi if size == vol['cfg']['size']: break resp = self._etcd_txn({ 'compare': [{ 'target': 'MOD', 'mod_revision': vol['cfg_mod'], 'key': 'config/inode/' + str(vol['idx']['pool_id']) + '/' + str(vol['idx']['id']), }], 'success': [ { 'request_put': { 'key': 'config/inode/' + str(vol['idx']['pool_id']) + '/' + str(vol['idx']['id']), 'value': json.dumps({ **vol['cfg'], 'size': size }), } }, ] }) if resp.get('succeeded'): break LOG.debug("Extend volume from %(old_size)s GB to %(new_size)s GB.", { 'old_size': volume.size, 'new_size': new_size })
def create_volume(self, volume): """Creates a logical volume.""" # if volume.encryption_key_id: # message = _("Encryption is not yet supported.") # raise exception.VolumeDriverException(message=message) size = int(volume.size) * units.Gi LOG.debug("creating volume '%s'", volume.name) chunk_size = self.configuration.rbd_store_chunk_size * units.Mi order = int(math.log(chunk_size, 2)) with RADOSClient(self) as client: self.RBDProxy().create(client.ioctx, utils.convert_str(volume.name), size, order, old_format=False, features=client.features)
def _synchronized(f, *a, **k) -> Callable: call_args = inspect.getcallargs(f, *a, **k) call_args['f_name'] = f.__name__ lock = coordinator.get_lock(lock_name.format(**call_args)) name = utils.convert_str(lock.name) f_name = f.__name__ t1 = timeutils.now() t2 = None try: LOG.debug(f'Acquiring lock "{name}" by "{f_name}"') with lock(blocking): t2 = timeutils.now() LOG.debug(f'Lock "{name}" acquired by "{f_name}" :: ' f'waited {t2 - t1:0.3f}s') return f(*a, **k) finally: t3 = timeutils.now() if t2 is None: held_secs = "N/A" else: held_secs = "%0.3fs" % (t3 - t2) LOG.debug( f'Lock "{name}" released by "{f_name}" :: held {held_secs}')
def create_cloned_volume(self, volume, src_vref): """Create a cloned volume from another volume. Since we are cloning from a volume and not a snapshot, we must first create a snapshot of the source volume. The user has the option to limit how long a volume's clone chain can be by setting rbd_max_clone_depth. If a clone is made of another clone and that clone has rbd_max_clone_depth clones behind it, the source volume will be flattened. """ src_name = utils.convert_str(src_vref['name']) dest_name = utils.convert_str(volume['name']) flatten_parent = False # Do full copy if requested if self.configuration.rbd_max_clone_depth <= 0: with RBDVolumeProxy(self, src_name, read_only=True) as vol: vol.copy(vol.ioctx, dest_name) return # Otherwise do COW clone. with RADOSClient(self) as client: depth = self._get_clone_depth(client, src_name) # If source volume is a clone and rbd_max_clone_depth reached, # flatten the source before cloning. Zero rbd_max_clone_depth means # infinite is allowed. if depth == self.configuration.rbd_max_clone_depth: LOG.debug("maximum clone depth (%d) has been reached - " "flattening source volume", self.configuration.rbd_max_clone_depth) flatten_parent = True src_volume = self.rbd.Image(client.ioctx, src_name) try: # First flatten source volume if required. if flatten_parent: _pool, parent, snap = self._get_clone_info(src_volume, src_name) # Flatten source volume LOG.debug("flattening source volume %s", src_name) src_volume.flatten() # Delete parent clone snap parent_volume = self.rbd.Image(client.ioctx, parent) try: parent_volume.unprotect_snap(snap) parent_volume.remove_snap(snap) finally: parent_volume.close() # Create new snapshot of source volume clone_snap = "%s.clone_snap" % dest_name LOG.debug("creating snapshot='%s'", clone_snap) src_volume.create_snap(clone_snap) src_volume.protect_snap(clone_snap) except Exception: # Only close if exception since we still need it. src_volume.close() raise # Now clone source volume snapshot try: LOG.debug("cloning '%(src_vol)s@%(src_snap)s' to " "'%(dest)s'", {'src_vol': src_name, 'src_snap': clone_snap, 'dest': dest_name}) self.RBDProxy().clone(client.ioctx, src_name, clone_snap, client.ioctx, dest_name, features=client.features) except Exception: src_volume.unprotect_snap(clone_snap) src_volume.remove_snap(clone_snap) raise finally: src_volume.close() if volume['size'] != src_vref['size']: LOG.debug("resize volume '%(dst_vol)s' from %(src_size)d to " "%(dst_size)d", {'dst_vol': volume['name'], 'src_size': src_vref['size'], 'dst_size': volume['size']}) self._resize(volume) LOG.debug("clone created successfully")
def delete_volume(self, volume): """Deletes a logical volume.""" vol_name = utils.convert_str(volume.name) # Find the volume and all its snapshots range_end = b'index/image/' + vol_name.encode('utf-8') range_end = range_end[0:len(range_end) - 1] + six.int2byte(range_end[len(range_end) - 1] + 1) resp = self._etcd_txn({ 'success': [ { 'request_range': { 'key': 'index/image/' + vol_name, 'range_end': range_end } }, ] }) if len(resp['responses'][0]['kvs']) == 0: # already deleted LOG.info("volume %s no longer exists in backend", vol_name) return layers = resp['responses'][0]['kvs'] layer_ids = {} for kv in layers: inode_id = int(kv['value']['id']) pool_id = int(kv['value']['pool_id']) inode_pool_id = (pool_id << 48) | (inode_id & 0xffffffffffff) layer_ids[inode_pool_id] = True # Check if the volume has clones and raise 'busy' if so children = self._child_count(layer_ids) if children > 0: raise exception.VolumeIsBusy(volume_name=vol_name) # Clear data for kv in layers: args = [ 'vitastor-cli', 'rm-data', '--pool', str(kv['value']['pool_id']), '--inode', str(kv['value']['id']), '--progress', '0', *(self._vitastor_args()) ] try: self._execute(*args) except processutils.ProcessExecutionError as exc: LOG.error("Failed to remove layer " + kv['key'] + ": " + exc) raise exception.VolumeBackendAPIException(data=exc.stderr) # Delete all layers from etcd requests = [] for kv in layers: requests.append({'request_delete_range': {'key': kv['key']}}) requests.append({ 'request_delete_range': { 'key': 'config/inode/' + str(kv['value']['pool_id']) + '/' + str(kv['value']['id']) } }) self._etcd_txn({'success': requests})
def create_snapshot(self, snapshot): """Creates an rbd snapshot.""" with RBDVolumeProxy(self, snapshot['volume_name']) as volume: snap = utils.convert_str(snapshot['name']) volume.create_snap(snap) volume.protect_snap(snap)
def delete_volume(self, volume): """Deletes a logical volume.""" # NOTE(dosaboy): this was broken by commit cbe1d5f. Ensure names are # utf-8 otherwise librbd will barf. volume_name = utils.convert_str(volume['name']) with RADOSClient(self) as client: try: rbd_image = self.rbd.Image(client.ioctx, volume_name) except self.rbd.ImageNotFound: LOG.info(_LI("volume %s no longer exists in backend"), volume_name) return clone_snap = None parent = None # Ensure any backup snapshots are deleted self._delete_backup_snaps(rbd_image) # If the volume has non-clone snapshots this delete is expected to # raise VolumeIsBusy so do so straight away. try: snaps = rbd_image.list_snaps() for snap in snaps: if snap['name'].endswith('.clone_snap'): LOG.debug("volume has clone snapshot(s)") # We grab one of these and use it when fetching parent # info in case the volume has been flattened. clone_snap = snap['name'] break raise exception.VolumeIsBusy(volume_name=volume_name) # Determine if this volume is itself a clone _pool, parent, parent_snap = self._get_clone_info( rbd_image, volume_name, clone_snap) finally: rbd_image.close() @utils.retry(self.rbd.ImageBusy, retries=3) def _try_remove_volume(client, volume_name): self.RBDProxy().remove(client.ioctx, volume_name) if clone_snap is None: LOG.debug("deleting rbd volume %s", volume_name) try: _try_remove_volume(client, volume_name) except self.rbd.ImageBusy: msg = (_("ImageBusy error raised while deleting rbd " "volume. This may have been caused by a " "connection from a client that has crashed and, " "if so, may be resolved by retrying the delete " "after 30 seconds has elapsed.")) LOG.warning(msg) # Now raise this so that volume stays available so that we # delete can be retried. raise exception.VolumeIsBusy(msg, volume_name=volume_name) except self.rbd.ImageNotFound: LOG.info( _LI("RBD volume %s not found, allowing delete " "operation to proceed."), volume_name) return # If it is a clone, walk back up the parent chain deleting # references. if parent: LOG.debug("volume is a clone so cleaning references") self._delete_clone_parent_refs(client, parent, parent_snap) else: # If the volume has copy-on-write clones we will not be able to # delete it. Instead we will keep it as a silent volume which # will be deleted when it's snapshot and clones are deleted. new_name = "%s.deleted" % (volume_name) self.RBDProxy().rename(client.ioctx, volume_name, new_name)
def create_cloned_volume(self, volume, src_vref): """Create a cloned volume from another volume. Since we are cloning from a volume and not a snapshot, we must first create a snapshot of the source volume. The user has the option to limit how long a volume's clone chain can be by setting rbd_max_clone_depth. If a clone is made of another clone and that clone has rbd_max_clone_depth clones behind it, the source volume will be flattened. """ src_name = utils.convert_str(src_vref['name']) dest_name = utils.convert_str(volume['name']) flatten_parent = False # Do full copy if requested if self.configuration.rbd_max_clone_depth <= 0: with RBDVolumeProxy(self, src_name, read_only=True) as vol: vol.copy(vol.ioctx, dest_name) return # Otherwise do COW clone. with RADOSClient(self) as client: depth = self._get_clone_depth(client, src_name) # If source volume is a clone and rbd_max_clone_depth reached, # flatten the source before cloning. Zero rbd_max_clone_depth means # infinite is allowed. if depth == self.configuration.rbd_max_clone_depth: LOG.debug( "maximum clone depth (%d) has been reached - " "flattening source volume", self.configuration.rbd_max_clone_depth) flatten_parent = True src_volume = self.rbd.Image(client.ioctx, src_name) try: # First flatten source volume if required. if flatten_parent: _pool, parent, snap = self._get_clone_info( src_volume, src_name) # Flatten source volume LOG.debug("flattening source volume %s", src_name) src_volume.flatten() # Delete parent clone snap parent_volume = self.rbd.Image(client.ioctx, parent) try: parent_volume.unprotect_snap(snap) parent_volume.remove_snap(snap) finally: parent_volume.close() # Create new snapshot of source volume clone_snap = "%s.clone_snap" % dest_name LOG.debug("creating snapshot='%s'", clone_snap) src_volume.create_snap(clone_snap) src_volume.protect_snap(clone_snap) except Exception: # Only close if exception since we still need it. src_volume.close() raise # Now clone source volume snapshot try: LOG.debug( "cloning '%(src_vol)s@%(src_snap)s' to " "'%(dest)s'", { 'src_vol': src_name, 'src_snap': clone_snap, 'dest': dest_name }) self.RBDProxy().clone(client.ioctx, src_name, clone_snap, client.ioctx, dest_name, features=client.features) except Exception: src_volume.unprotect_snap(clone_snap) src_volume.remove_snap(clone_snap) raise finally: src_volume.close() if volume['size'] != src_vref['size']: LOG.debug( "resize volume '%(dst_vol)s' from %(src_size)d to " "%(dst_size)d", { 'dst_vol': volume['name'], 'src_size': src_vref['size'], 'dst_size': volume['size'] }) self._resize(volume) LOG.debug("clone created successfully")
def _get_new_snap_name(self, backup_id): return utils.convert_str("backup.%s.snap.%s" % (backup_id, time.time()))
def __init__(self, image, pool, user, conf): self.image = image self.pool = utils.convert_str(pool) self.user = utils.convert_str(user) self.conf = utils.convert_str(conf)
def _process_stack(self, request, action, action_args, content_type, body, accept): """Implement the processing stack.""" # Get the implementing method try: meth, extensions = self.get_method(request, action, content_type, body) except (AttributeError, TypeError): return Fault(webob.exc.HTTPNotFound()) except KeyError as ex: msg = _("There is no such action: %s") % ex.args[0] return Fault(webob.exc.HTTPBadRequest(explanation=msg)) except exception.MalformedRequestBody: msg = _("Malformed request body") return Fault(webob.exc.HTTPBadRequest(explanation=msg)) if body: decoded_body = encodeutils.safe_decode(body, errors='ignore') msg = ("Action: '%(action)s', calling method: %(meth)s, body: " "%(body)s") % {'action': action, 'body': six.text_type(decoded_body), 'meth': six.text_type(meth)} LOG.debug(strutils.mask_password(msg)) else: LOG.debug("Calling method '%(meth)s'", {'meth': six.text_type(meth)}) # Now, deserialize the request body... try: if content_type: contents = self.deserialize(meth, content_type, body) else: contents = {} except exception.InvalidContentType: msg = _("Unsupported Content-Type") return Fault(webob.exc.HTTPBadRequest(explanation=msg)) except exception.MalformedRequestBody: msg = _("Malformed request body") return Fault(webob.exc.HTTPBadRequest(explanation=msg)) # Update the action args action_args.update(contents) project_id = action_args.pop("project_id", None) context = request.environ.get('cinder.context') if (context and project_id and (project_id != context.project_id)): msg = _("Malformed request url") return Fault(webob.exc.HTTPBadRequest(explanation=msg)) # Run pre-processing extensions response, post = self.pre_process_extensions(extensions, request, action_args) if not response: try: with ResourceExceptionHandler(): action_result = self.dispatch(meth, request, action_args) except Fault as ex: response = ex if not response: # No exceptions; convert action_result into a # ResponseObject resp_obj = None if isinstance(action_result, dict) or action_result is None: resp_obj = ResponseObject(action_result) elif isinstance(action_result, ResponseObject): resp_obj = action_result else: response = action_result # Run post-processing extensions if resp_obj: _set_request_id_header(request, resp_obj) # Do a preserialize to set up the response object serializers = getattr(meth, 'wsgi_serializers', {}) resp_obj._bind_method_serializers(serializers) if hasattr(meth, 'wsgi_code'): resp_obj._default_code = meth.wsgi_code resp_obj.preserialize(accept, self.default_serializers) # Process post-processing extensions response = self.post_process_extensions(post, resp_obj, request, action_args) if resp_obj and not response: response = resp_obj.serialize(request, accept, self.default_serializers) try: msg_dict = dict(url=request.url, status=response.status_int) msg = _LI("%(url)s returned with HTTP %(status)d") except AttributeError as e: msg_dict = dict(url=request.url, e=e) msg = _LI("%(url)s returned a fault: %(e)s") LOG.info(msg, msg_dict) if hasattr(response, 'headers'): for hdr, val in response.headers.items(): # Headers must be utf-8 strings val = utils.convert_str(val) response.headers[hdr] = val if (not request.api_version_request.is_null() and not _is_legacy_endpoint(request)): response.headers[API_VERSION_REQUEST_HEADER] = ( VOLUME_SERVICE + ' ' + request.api_version_request.get_string()) response.headers['Vary'] = API_VERSION_REQUEST_HEADER return response
def delete_volume(self, volume): """Deletes a logical volume.""" # NOTE(dosaboy): this was broken by commit cbe1d5f. Ensure names are # utf-8 otherwise librbd will barf. volume_name = utils.convert_str(volume['name']) with RADOSClient(self) as client: try: rbd_image = self.rbd.Image(client.ioctx, volume_name) except self.rbd.ImageNotFound: LOG.info(_LI("volume %s no longer exists in backend"), volume_name) return clone_snap = None parent = None # Ensure any backup snapshots are deleted self._delete_backup_snaps(rbd_image) # If the volume has non-clone snapshots this delete is expected to # raise VolumeIsBusy so do so straight away. try: snaps = rbd_image.list_snaps() for snap in snaps: if snap['name'].endswith('.clone_snap'): LOG.debug("volume has clone snapshot(s)") # We grab one of these and use it when fetching parent # info in case the volume has been flattened. clone_snap = snap['name'] break raise exception.VolumeIsBusy(volume_name=volume_name) # Determine if this volume is itself a clone _pool, parent, parent_snap = self._get_clone_info(rbd_image, volume_name, clone_snap) finally: rbd_image.close() if clone_snap is None: LOG.debug("deleting rbd volume %s", volume_name) try: self.RBDProxy().remove(client.ioctx, volume_name) except self.rbd.ImageBusy: msg = (_("ImageBusy error raised while deleting rbd " "volume. This may have been caused by a " "connection from a client that has crashed and, " "if so, may be resolved by retrying the delete " "after 30 seconds has elapsed.")) LOG.warning(msg) # Now raise this so that volume stays available so that we # delete can be retried. raise exception.VolumeIsBusy(msg, volume_name=volume_name) except self.rbd.ImageNotFound: LOG.info(_LI("RBD volume %s not found, allowing delete " "operation to proceed."), volume_name) return # If it is a clone, walk back up the parent chain deleting # references. if parent: LOG.debug("volume is a clone so cleaning references") self._delete_clone_parent_refs(client, parent, parent_snap) else: # If the volume has copy-on-write clones we will not be able to # delete it. Instead we will keep it as a silent volume which # will be deleted when it's snapshot and clones are deleted. new_name = "%s.deleted" % (volume_name) self.RBDProxy().rename(client.ioctx, volume_name, new_name)
def name(self): return utils.convert_str("backup.%s.meta" % self._backup_id)
def _try_delete_base_image(self, backup_id, volume_id, base_name=None): """Try to delete backup RBD image. If the rbd image is a base image for incremental backups, it may have snapshots. Delete the snapshot associated with backup_id and if the image has no more snapshots, delete it. Otherwise return. If no base name is provided try normal (full) format then diff format image name. If a base name is provided but does not exist, ImageNotFound will be raised. If the image is busy, a number of retries will be performed if ImageBusy is received, after which the exception will be propagated to the caller. """ retries = 3 delay = 5 try_diff_format = False if base_name is None: try_diff_format = True base_name = self._get_backup_base_name(volume_id, backup_id) LOG.debug("Trying diff format basename='%(basename)s' for " "backup base image of volume %(volume)s.", {'basename': base_name, 'volume': volume_id}) with rbd_driver.RADOSClient(self) as client: rbd_exists, base_name = \ self._rbd_image_exists(base_name, volume_id, client, try_diff_format=try_diff_format) if not rbd_exists: raise self.rbd.ImageNotFound(_("image %s not found") % base_name) while retries >= 0: # First delete associated snapshot from base image (if exists) snap, rem = self._delete_backup_snapshot(client, base_name, backup_id) if rem: LOG.info( _LI("Backup base image of volume %(volume)s still " "has %(snapshots)s snapshots so skipping base " "image delete."), {'snapshots': rem, 'volume': volume_id}) return LOG.info(_LI("Deleting backup base image='%(basename)s' of " "volume %(volume)s."), {'basename': base_name, 'volume': volume_id}) # Delete base if no more snapshots try: self.rbd.RBD().remove(client.ioctx, base_name) except self.rbd.ImageBusy: # Allow a retry if the image is busy if retries > 0: LOG.info(_LI("Backup image of volume %(volume)s is " "busy, retrying %(retries)s more time(s) " "in %(delay)ss."), {'retries': retries, 'delay': delay, 'volume': volume_id}) eventlet.sleep(delay) else: LOG.error(_LE("Max retries reached deleting backup " "%(basename)s image of volume " "%(volume)s."), {'volume': volume_id, 'basename': base_name}) raise else: LOG.debug("Base backup image='%(basename)s' of volume " "%(volume)s deleted.", {'basename': base_name, 'volume': volume_id}) retries = 0 finally: retries -= 1 # Since we have deleted the base image we can delete the source # volume backup snapshot. src_name = utils.convert_str(volume_id) if src_name in self.rbd.RBD().list(client.ioctx): LOG.debug("Deleting source volume snapshot '%(snapshot)s' " "for backup %(basename)s.", {'snapshot': snap, 'basename': base_name}) src_rbd = self.rbd.Image(client.ioctx, src_name) try: src_rbd.remove_snap(snap) finally: src_rbd.close()