def _write_info_file(self, info_path, snap_info): if 'active' not in snap_info.keys(): msg = _("'active' must be present when writing snap_info.") raise exception.RemoteFSException(msg) with open(info_path, 'w') as f: json.dump(snap_info, f, indent=1, sort_keys=True)
def _qemu_img_info_base(self, path, volume_name, basedir): """Sanitize image_utils' qemu_img_info. This code expects to deal only with relative filenames. """ info = image_utils.qemu_img_info(path) if info.image: info.image = os.path.basename(info.image) if info.backing_file: backing_file_template = \ "(%(basedir)s/[0-9a-f]+/)?%" \ "(volname)s(.(tmp-snap-)?[0-9a-f-]+)?$" % { 'basedir': basedir, 'volname': volume_name } if not re.match(backing_file_template, info.backing_file): msg = _("File %(path)s has invalid backing file " "%(bfile)s, aborting.") % { 'path': path, 'bfile': info.backing_file } raise exception.RemoteFSException(msg) info.backing_file = os.path.basename(info.backing_file) return info
def _ensure_share_writable(self, path): """Ensure that the Cinder user can write to the share. If not, raise an exception. :param path: path to test :raises: RemoteFSException :returns: None """ prefix = '.cinder-write-test-' + str(os.getpid()) + '-' try: tempfile.NamedTemporaryFile(prefix=prefix, dir=path) except OSError: msg = _('Share at %(dir)s is not writable by the ' 'Cinder volume service. Snapshot operations will not be ' 'supported.') % { 'dir': path } raise exception.RemoteFSException(msg)
def _create_snapshot_online(self, snapshot, backing_filename, new_snap_path): # Perform online snapshot via Nova context = snapshot['context'] self._do_create_snapshot(snapshot, backing_filename, new_snap_path) connection_info = { 'type': 'qcow2', 'new_file': os.path.basename(new_snap_path), 'snapshot_id': snapshot['id'] } try: result = self._nova.create_volume_snapshot(context, snapshot['volume_id'], connection_info) LOG.debug('nova call result: %s' % result) except Exception as e: LOG.error(_('Call to Nova to create snapshot failed')) LOG.exception(e) raise e # Loop and wait for result # Nova will call Cinderclient to update the status in the database # An update of progress = '90%' means that Nova is done seconds_elapsed = 0 increment = 1 timeout = 600 while True: s = db.snapshot_get(context, snapshot['id']) if s['status'] == 'creating': if s['progress'] == '90%': # Nova tasks completed successfully break time.sleep(increment) seconds_elapsed += increment elif s['status'] == 'error': msg = _('Nova returned "error" status ' 'while creating snapshot.') raise exception.RemoteFSException(msg) LOG.debug('Status of snapshot %(id)s is now %(status)s' % { 'id': snapshot['id'], 'status': s['status'] }) if 10 < seconds_elapsed <= 20: increment = 2 elif 20 < seconds_elapsed <= 60: increment = 5 elif 60 < seconds_elapsed: increment = 10 if seconds_elapsed > timeout: msg = _('Timed out while waiting for Nova update ' 'for creation of snapshot %s.') % snapshot['id'] raise exception.RemoteFSException(msg)
def _delete_snapshot(self, snapshot): """Delete a snapshot. If volume status is 'available', delete snapshot here in Cinder using qemu-img. If volume status is 'in-use', calculate what qcow2 files need to merge, and call to Nova to perform this operation. :raises: InvalidVolume if status not acceptable :raises: RemotefsException(msg) if operation fails :returns: None """ LOG.debug('Deleting snapshot %s:' % snapshot['id']) volume_status = snapshot['volume']['status'] if volume_status not in ['available', 'in-use']: msg = _('Volume status must be "available" or "in-use".') raise exception.InvalidVolume(msg) self._ensure_share_writable(self._local_volume_dir(snapshot['volume'])) # Determine the true snapshot file for this snapshot # based on the .info file info_path = self._local_path_volume_info(snapshot['volume']) snap_info = self._read_info_file(info_path, empty_if_missing=True) if snapshot['id'] not in snap_info: # If snapshot info file is present, but snapshot record does not # exist, do not attempt to delete. # (This happens, for example, if snapshot_create failed due to lack # of permission to write to the share.) LOG.info( _('Snapshot record for %s is not present, allowing ' 'snapshot_delete to proceed.') % snapshot['id']) return snapshot_file = snap_info[snapshot['id']] LOG.debug('snapshot_file for this snap is: %s' % snapshot_file) snapshot_path = os.path.join( self._local_volume_dir(snapshot['volume']), snapshot_file) snapshot_path_img_info = self._qemu_img_info(snapshot_path) vol_path = self._local_volume_dir(snapshot['volume']) # Find what file has this as its backing file active_file = self.get_active_image_from_info(snapshot['volume']) active_file_path = os.path.join(vol_path, active_file) if volume_status == 'in-use': # Online delete context = snapshot['context'] base_file = snapshot_path_img_info.backing_file if base_file is None: # There should always be at least the original volume # file as base. msg = _('No backing file found for %s, allowing snapshot ' 'to be deleted.') % snapshot_path LOG.warn(msg) # Snapshot may be stale, so just delete it and update ther # info file instead of blocking return self._delete_stale_snapshot(snapshot) base_path = os.path.join( self._local_volume_dir(snapshot['volume']), base_file) base_file_img_info = self._qemu_img_info(base_path) new_base_file = base_file_img_info.backing_file base_id = None for key, value in snap_info.iteritems(): if value == base_file and key != 'active': base_id = key break if base_id is None: # This means we are deleting the oldest snapshot msg = 'No %(base_id)s found for %(file)s' % { 'base_id': 'base_id', 'file': snapshot_file } LOG.debug(msg) online_delete_info = { 'active_file': active_file, 'snapshot_file': snapshot_file, 'base_file': base_file, 'base_id': base_id, 'new_base_file': new_base_file } return self._delete_snapshot_online(context, snapshot, online_delete_info) if snapshot_file == active_file: # Need to merge snapshot_file into its backing file # There is no top file # T0 | T1 | # base | snapshot_file | None # (guaranteed to| (being deleted) | # exist) | | base_file = snapshot_path_img_info.backing_file self._img_commit(snapshot_path) # Remove snapshot_file from info del (snap_info[snapshot['id']]) # Active file has changed snap_info['active'] = base_file self._write_info_file(info_path, snap_info) else: # T0 | T1 | T2 | T3 # base | snapshot_file | higher_file | highest_file # (guaranteed to | (being deleted)|(guaranteed to | (may exist, # exist, not | | exist, being | needs ptr # used here) | | committed down)| update if so) backing_chain = self._get_backing_chain_for_path( snapshot['volume'], active_file_path) # This file is guaranteed to exist since we aren't operating on # the active file. higher_file = next( (os.path.basename(f['filename']) for f in backing_chain if f.get('backing-filename', '') == snapshot_file), None) if higher_file is None: msg = _('No file found with %s as backing file.') %\ snapshot_file raise exception.RemoteFSException(msg) higher_id = next( (i for i in snap_info if snap_info[i] == higher_file and i != 'active'), None) if higher_id is None: msg = _('No snap found with %s as backing file.') %\ higher_file raise exception.RemoteFSException(msg) # Is there a file depending on higher_file? highest_file = next( (os.path.basename(f['filename']) for f in backing_chain if f.get('backing-filename', '') == higher_file), None) if highest_file is None: msg = 'No file depends on %s.' % higher_file LOG.debug(msg) # Committing higher_file into snapshot_file # And update pointer in highest_file higher_file_path = os.path.join(vol_path, higher_file) self._img_commit(higher_file_path) if highest_file is not None: highest_file_path = os.path.join(vol_path, highest_file) snapshot_file_fmt = snapshot_path_img_info.file_format self._rebase_img(highest_file_path, snapshot_file, snapshot_file_fmt) # Remove snapshot_file from info del (snap_info[snapshot['id']]) snap_info[higher_id] = snapshot_file if higher_file == active_file: if highest_file is not None: msg = _('Check condition failed: ' '%s expected to be None.') % 'highest_file' raise exception.RemoteFSException(msg) # Active file has changed snap_info['active'] = snapshot_file self._write_info_file(info_path, snap_info)