Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
Size: Mime:
# vim: tabstop=4 shiftwidth=4 softtabstop=4

# Copyright (c) 2013 TrilioData, Inc.
# All Rights Reserved.

"""
Handling of VM disk images.
"""
import re
import time
import os

try:
    from oslo_log import log as logging
except ImportError:
    from nova.openstack.common import log as logging

try:
    from oslo_config import cfg
except ImportError:
    from oslo.config import cfg

from nova import utils

LOG = logging.getLogger(__name__)

contego_qemuimage_opts = [
    cfg.BoolOpt('force_raw_images',
                default=True,
                help='Force backing images to raw format'),
]

CONF = cfg.CONF
if 'force_raw_images' not in list(CONF.keys()):
    CONF.register_opts(contego_qemuimage_opts)


def to_bytes(text, default=0):
    """Try to turn a string into a number of bytes. Looks at the last
    characters of the text to determine what conversion is needed to
    turn the input text into a byte number.

    Supports: B/b, K/k, M/m, G/g, T/t (or the same with b/B on the end)

    """

    BYTE_MULTIPLIERS = {
        '': 1,
        't': 1024 ** 4,
        'g': 1024 ** 3,
        'm': 1024 ** 2,
        'k': 1024,
    }

    # Take off everything not number 'like' (which should leave
    # only the byte 'identifier' left)
    mult_key_org = text.lstrip('-1234567890.')
    mult_key = mult_key_org.lower()
    mult_key_len = len(mult_key)
    if mult_key.endswith("b"):
        mult_key = mult_key[0:-1]
    try:
        multiplier = BYTE_MULTIPLIERS[mult_key]
        if mult_key_len:
            # Empty cases shouldn't cause text[0:-0]
            text = text[0:-mult_key_len]
        return int(float(text) * multiplier)
    except KeyError:
        msg = ('Unknown byte multiplier: %s') % mult_key_org
        raise TypeError(msg)
    except ValueError:
        return default


class QemuImgInfo(object):
    BACKING_FILE_RE = re.compile((r"^(.*?)\s*\(actual\s+path\s*:"
                                  r"\s+(.*?)\)\s*$"), re.I)
    TOP_LEVEL_RE = re.compile(r"^([\w\d\s\_\-]+):(.*)$")
    SIZE_RE = re.compile(r"\(\s*(\d+)\s+bytes\s*\)", re.I)

    def __init__(self, cmd_output=None):
        details = self._parse(cmd_output)
        self.image = details.get('image')
        self.backing_file = details.get('backing_file')
        self.file_format = details.get('file_format')
        self.virtual_size = details.get('virtual_size')
        self.cluster_size = details.get('cluster_size')
        self.disk_size = details.get('disk_size')
        self.snapshots = details.get('snapshot_list', [])
        self.encryption = details.get('encryption')

    def __str__(self):
        lines = [
            'image: %s' % self.image,
            'file_format: %s' % self.file_format,
            'virtual_size: %s' % self.virtual_size,
            'disk_size: %s' % self.disk_size,
            'cluster_size: %s' % self.cluster_size,
            'backing_file: %s' % self.backing_file,
        ]
        if self.snapshots:
            lines.append("snapshots: %s" % self.snapshots)
        return "\n".join(lines)

    def _canonicalize(self, field):
        # Standardize on underscores/lc/no dash and no spaces
        # since qemu seems to have mixed outputs here... and
        # this format allows for better integration with python
        # - ie for usage in kwargs and such...
        field = field.lower().strip()
        for c in (" ", "-"):
            field = field.replace(c, '_')
        return field

    def _extract_bytes(self, details):
        # Replace it with the byte amount
        real_size = self.SIZE_RE.search(details)
        if real_size:
            details = real_size.group(1)
        try:
            details = to_bytes(details)
        except (TypeError, ValueError):
            pass
        return details

    def _extract_details(self, root_cmd, root_details, lines_after):
        consumed_lines = 0
        real_details = root_details
        if root_cmd == 'backing_file':
            # Replace it with the real backing file
            backing_match = self.BACKING_FILE_RE.match(root_details)
            if backing_match:
                real_details = backing_match.group(2).strip()
        elif root_cmd in ['virtual_size', 'cluster_size', 'disk_size']:
            # Replace it with the byte amount (if we can convert it)
            real_details = self._extract_bytes(root_details)
        elif root_cmd == 'file_format':
            real_details = real_details.strip().lower()
        elif root_cmd == 'snapshot_list':
            # Next line should be a header, starting with 'ID'
            if not lines_after or not lines_after[0].startswith("ID"):
                msg = ("Snapshot list encountered but no header found!")
                raise ValueError(msg)
            consumed_lines += 1
            possible_contents = lines_after[1:]
            real_details = []
            # This is the sprintf pattern we will try to match
            # "%-10s%-20s%7s%20s%15s"
            # ID TAG VM SIZE DATE VM CLOCK (current header)
            for line in possible_contents:
                line_pieces = line.split(None)
                if len(line_pieces) != 6:
                    break
                else:
                    # Check against this pattern in the final position
                    # "%02d:%02d:%02d.%03d"
                    date_pieces = line_pieces[5].split(":")
                    if len(date_pieces) != 3:
                        break
                    real_details.append({
                        'id': line_pieces[0],
                        'tag': line_pieces[1],
                        'vm_size': line_pieces[2],
                        'date': line_pieces[3],
                        'vm_clock': line_pieces[4] + " " + line_pieces[5],
                    })
                    consumed_lines += 1
        return (real_details, consumed_lines)

    def _parse(self, cmd_output):
        # Analysis done of qemu-img.c to figure out what is going on here
        # Find all points start with some chars and then a ':' then a newline
        # and then handle the results of those 'top level' items in a separate
        # function.
        #
        # TODO(harlowja): newer versions might have a json output format
        #                 we should switch to that whenever possible.
        #                 see: http://bit.ly/XLJXDX
        if not cmd_output:
            cmd_output = ''
        contents = {}
        lines = cmd_output.splitlines()
        i = 0
        line_am = len(lines)
        while i < line_am:
            line = lines[i]
            if not line.strip():
                i += 1
                continue
            consumed_lines = 0
            top_level = self.TOP_LEVEL_RE.match(line)
            if top_level:
                root = self._canonicalize(top_level.group(1))
                if not root:
                    i += 1
                    continue
                root_details = top_level.group(2).strip()
                details, consumed_lines = self._extract_details(root,
                                                                root_details,
                                                                lines[i + 1:])
                contents[root] = details
            i += consumed_lines + 1
        return contents


def copy_to_temp(src, dst):
    out, err = utils.execute('env', 'LC_ALL=C', 'LANG=C',
                             'cp', src, dst, run_as_root=False)
    if err == '':
        return True
    return False


def qemu_img_info(path):
    """Return an object containing the parsed output from qemu-img info."""
    try:
        out, err = utils.execute('env', 'LC_ALL=C', 'LANG=C',
                         'qemu-img', 'info', '--image-opts',
                         'file.locking=off,file.filename=%s'%path, run_as_root=False)
        if "--force-share" in err:
            out, err = utils.execute('env', 'LC_ALL=C', 'LANG=C',
                         'qemu-img', 'info', path, run_as_root=False)
        return QemuImgInfo(out)
    except Exception as ex:
        LOG.exception(ex)
        # Sometimes it fails to get "consistent read" lock.
        time.sleep(30)
        out, err = utils.execute('env', 'LC_ALL=C', 'LANG=C',
                         'qemu-img', 'info', '--image-opts',
                         'file.locking=off,file.filename=%s'%path, run_as_root=False)
        if "--force-share" in err:
            out, err = utils.execute('env', 'LC_ALL=C', 'LANG=C',
                         'qemu-img', 'info', path, run_as_root=False)
        return QemuImgInfo(out)


def qemu_check_n_resize(path, vsize):
    """Return an object containing the parsed output from qemu-img info."""
    try:
        out, err = utils.execute('env', 'LC_ALL=C', 'LANG=C',
                         'qemu-img', 'check', '-r', 'all', path,
                         run_as_root=False)
    except Exception as ex:
        LOG.exception(ex)
        # Sometimes it fails to get "consistent read" lock.
        time.sleep(30)
        out, err = utils.execute('env', 'LC_ALL=C', 'LANG=C',
                         'qemu-img', 'check', '-r', 'all', path,
                         run_as_root=False)

    resize_image(path, vsize)


def convert_image(source, dest, out_format, run_as_root=False):
    """Convert image to other format."""
    cmd = ('env', 'LC_ALL=C', 'LANG=C', 'qemu-img', 'convert') + \
          ('-O', out_format, source, dest)
    utils.execute(*cmd, run_as_root=run_as_root)


def rebase_qcow2(backing_file_base, backing_file_top, run_as_root=False):
    """rebase the backing_file_top to backing_file_base using unsafe mode
    :param backing_file_base: backing file to rebase to
    :param backing_file_top: top file to rebase
    """
    try:
        utils.execute('qemu-img', 'rebase', '-u', '-b',
                      backing_file_base, backing_file_top,
                      run_as_root=run_as_root)
    except Exception as ex:
        LOG.exception("qemu-img Errored. Retrying: %s", ex)
        time.sleep(30)
        utils.execute('qemu-img', 'rebase', '-u', '-b',
                      backing_file_base, backing_file_top,
                      run_as_root=run_as_root)


def commit_qcow2(backing_file_top, run_as_root=False):
    """rebase the backing_file_top to backing_file_base
     :param backing_file_top: top file to commit from to its base
    """
    try:
        utils.execute('qemu-img', 'commit', backing_file_top,
                      run_as_root=run_as_root)
    except Exception as ex:
        if int(ex.exit_code) in [1, 13]:
            try:
                user_id = str(os.getuid())
                utils.execute('chown', user_id + ':' + user_id,
                              backing_file_top, run_as_root=False)
                qemuinfo = qemu_img_info(backing_file_top)
                utils.execute('chown', user_id + ':' + user_id,
                              qemuinfo.backing_file, run_as_root=False)
            except Exception as ex:
                LOG.exception(ex)
                try:
                    user_id = str(os.getuid())
                    utils.execute('chown', user_id + ':' + user_id,
                                  backing_file_top, run_as_root=True)
                    qemuinfo = qemu_img_info(backing_file_top)
                    utils.execute('chown', user_id + ':' + user_id,
                                  qemuinfo.backing_file, run_as_root=True)
                except Exception as ex:
                    LOG.exception(ex)

            utils.execute('qemu-img', 'commit', backing_file_top,
                          run_as_root=run_as_root)
        else:
            raise


def resize_image(path, new_size, run_as_root=False):
    """rebase the backing_file_top to backing_file_base
     :param backing_file_top: top file to commit from to its base
    """
    utils.execute(
        'qemu-img',
        'resize',
        path,
        new_size,
        run_as_root=run_as_root)

def create_cow_image(backing_file, path, size=None):
    """Create COW image

    Creates a COW image with the given backing file

    :param backing_file: Existing image on which to base the COW image
    :param path: Desired location of the COW image
    """
    base_cmd = ['qemu-img', 'create', '-f', 'qcow2']
    cow_opts = []
    if backing_file:
        cow_opts += ['backing_file=%s' % backing_file]
        base_details = qemu_img_info(backing_file)
    else:
        base_details = None
    # Explicitly inherit the value of 'cluster_size' property of a qcow2
    # overlay image from its backing file. This can be useful in cases
    # when people create a base image with a non-default 'cluster_size'
    # value or cases when images were created with very old QEMU
    # versions which had a different default 'cluster_size'.
    if base_details and base_details.cluster_size is not None:
        cow_opts += ['cluster_size=%s' % base_details.cluster_size]
    if size is not None:
        cow_opts += ['size=%s' % size]
    if cow_opts:
        # Format as a comma separated list
        csv_opts = ",".join(cow_opts)
        cow_opts = ['-o', csv_opts]
    cmd = base_cmd + cow_opts + [path]
    utils.execute(*cmd)

def get_disk_backing_file(path, basename=True, format=None):
    """Get the backing file of a disk image

    :param path: Path to the disk image
    :returns: a path to the image's backing store
    """
    backing_file = qemu_img_info(path).backing_file
    if backing_file and basename:
        backing_file = os.path.basename(backing_file)

    return backing_file