Merge "Make copy_to_volume a bit more useful."
This commit is contained in:
commit
29b9290cb9
|
@ -0,0 +1,241 @@
|
|||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# All Rights Reserved.
|
||||
# Copyright (c) 2010 Citrix Systems, Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Helper methods to deal with images.
|
||||
|
||||
This is essentially a copy from nova.virt.images.py
|
||||
Some slight modifications, but at some point
|
||||
we should look at maybe pushign this up to OSLO
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
from cinder import exception
|
||||
from cinder import flags
|
||||
from cinder.openstack.common import cfg
|
||||
from cinder.openstack.common import log as logging
|
||||
from cinder import utils
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
image_helper_opt = [cfg.StrOpt('image_conversion_dir',
|
||||
default='/tmp',
|
||||
help='parent dir for tempdir used for image conversion'), ]
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
FLAGS.register_opts(image_helper_opt)
|
||||
|
||||
|
||||
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):
|
||||
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 = utils.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 occuring 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 qemu_img_info(path):
|
||||
"""Return a object containing the parsed output from qemu-img info."""
|
||||
out, err = utils.execute('env', 'LC_ALL=C', 'LANG=C',
|
||||
'qemu-img', 'info', path,
|
||||
run_as_root=True)
|
||||
return QemuImgInfo(out)
|
||||
|
||||
|
||||
def convert_image(source, dest, out_format):
|
||||
"""Convert image to other format"""
|
||||
cmd = ('qemu-img', 'convert', '-O', out_format, source, dest)
|
||||
utils.execute(*cmd, run_as_root=True)
|
||||
|
||||
|
||||
def fetch(context, image_service, image_id, path, _user_id, _project_id):
|
||||
# TODO(vish): Improve context handling and add owner and auth data
|
||||
# when it is added to glance. Right now there is no
|
||||
# auth checking in glance, so we assume that access was
|
||||
# checked before we got here.
|
||||
with utils.remove_path_on_error(path):
|
||||
with open(path, "wb") as image_file:
|
||||
image_service.download(context, image_id, image_file)
|
||||
|
||||
|
||||
def fetch_to_raw(context, image_service,
|
||||
image_id, dest,
|
||||
user_id=None, project_id=None):
|
||||
if (FLAGS.image_conversion_dir and not
|
||||
os.path.exists(FLAGS.image_conversion_dir)):
|
||||
os.makedirs(FLAGS.image_conversion_dir)
|
||||
|
||||
with tempfile.NamedTemporaryFile(dir=FLAGS.image_conversion_dir) as tmp:
|
||||
fetch(context, image_service, image_id, tmp.name, user_id, project_id)
|
||||
tmp.flush()
|
||||
|
||||
data = qemu_img_info(tmp.name)
|
||||
with utils.remove_path_on_error(tmp.name):
|
||||
fmt = data.file_format
|
||||
if fmt is None:
|
||||
raise exception.ImageUnacceptable(
|
||||
reason=_("'qemu-img info' parsing failed."),
|
||||
image_id=image_id)
|
||||
|
||||
backing_file = data.backing_file
|
||||
if backing_file is not None:
|
||||
raise exception.ImageUnacceptable(
|
||||
image_id=image_id,
|
||||
reason=_("fmt=%(fmt)s backed by:"
|
||||
"%(backing_file)s") % locals())
|
||||
|
||||
# NOTE(jdg): I'm using qemu-img convert to write
|
||||
# to the volume regardless if it *needs* conversion or not
|
||||
LOG.debug("%s was %s, converting to raw" % (image_id, fmt))
|
||||
convert_image(tmp.name, dest, 'raw')
|
||||
tmp.close()
|
||||
|
||||
data = qemu_img_info(dest)
|
||||
if data.file_format != "raw":
|
||||
raise exception.ImageUnacceptable(
|
||||
image_id=image_id,
|
||||
reason=_("Converted to raw, but format is now %s") %
|
||||
data.file_format)
|
|
@ -31,6 +31,7 @@ from cinder import context
|
|||
from cinder import db
|
||||
from cinder import exception
|
||||
from cinder import flags
|
||||
from cinder.image import image_utils
|
||||
from cinder.openstack.common import importutils
|
||||
from cinder.openstack.common.notifier import api as notifier_api
|
||||
from cinder.openstack.common.notifier import test_notifier
|
||||
|
@ -493,9 +494,13 @@ class VolumeTestCase(test.TestCase):
|
|||
def fake_copy_image_to_volume(context, volume, image_id):
|
||||
pass
|
||||
|
||||
def fake_fetch_to_raw(context, image_service, image_id, vol_path):
|
||||
pass
|
||||
|
||||
dst_fd, dst_path = tempfile.mkstemp()
|
||||
os.close(dst_fd)
|
||||
self.stubs.Set(self.volume.driver, 'local_path', fake_local_path)
|
||||
self.stubs.Set(image_utils, 'fetch_to_raw', fake_fetch_to_raw)
|
||||
if fakeout_copy_image_to_volume:
|
||||
self.stubs.Set(self.volume, '_copy_image_to_volume',
|
||||
fake_copy_image_to_volume)
|
||||
|
|
|
@ -1126,3 +1126,39 @@ def ensure_tree(path):
|
|||
raise
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
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(text) * multiplier
|
||||
except KeyError:
|
||||
msg = _('Unknown byte multiplier: %s') % mult_key_org
|
||||
raise TypeError(msg)
|
||||
except ValueError:
|
||||
return default
|
||||
|
|
|
@ -26,6 +26,7 @@ import time
|
|||
|
||||
from cinder import exception
|
||||
from cinder import flags
|
||||
from cinder.image import image_utils
|
||||
from cinder.openstack.common import cfg
|
||||
from cinder.openstack.common import log as logging
|
||||
from cinder import utils
|
||||
|
@ -586,10 +587,10 @@ class ISCSIDriver(VolumeDriver):
|
|||
|
||||
def copy_image_to_volume(self, context, volume, image_service, image_id):
|
||||
"""Fetch the image from image_service and write it to the volume."""
|
||||
volume_path = self.local_path(volume)
|
||||
with utils.temporary_chown(volume_path):
|
||||
with utils.file_open(volume_path, "wb") as image_file:
|
||||
image_service.download(context, image_id, image_file)
|
||||
image_utils.fetch_to_raw(context,
|
||||
image_service,
|
||||
image_id,
|
||||
self.local_path(volume))
|
||||
|
||||
def copy_volume_to_image(self, context, volume, image_service, image_id):
|
||||
"""Copy the volume to the specified image."""
|
||||
|
|
|
@ -35,3 +35,5 @@ chown: CommandFilter, /bin/chown, root
|
|||
dmsetup: CommandFilter, /sbin/dmsetup, root
|
||||
dmsetup_usr: CommandFilter, /usr/sbin/dmsetup, root
|
||||
ln: CommandFilter, /bin/ln, root
|
||||
qemu-img: CommandFilter, /usr/bin/qemu-img, root
|
||||
env: CommandFilter, /usr/bin/env, root
|
||||
|
|
Loading…
Reference in New Issue