Esempio n. 1
0
    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)
Esempio n. 2
0
    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
Esempio n. 3
0
    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)
Esempio n. 4
0
    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)
Esempio n. 5
0
    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)