images: Move qemu-img info calls into privsep
This is mostly code motion from the nova.virt.images module into privsep to allow for both privileged and unprivileged calls to be made. A privileged_qemu_img_info function is introduced allowing QEMU to access devices requiring root privileges, such as host block devices. Change-Id: I5ac03f923d9d181d22d44d8ec8fbc31eb0c3999e
This commit is contained in:
+62
-1
@@ -16,14 +16,25 @@
|
||||
Helpers for qemu tasks.
|
||||
"""
|
||||
|
||||
import operator
|
||||
import os
|
||||
|
||||
from oslo_concurrency import processutils
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import units
|
||||
|
||||
from nova import exception
|
||||
from nova.i18n import _
|
||||
import nova.privsep.utils
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
QEMU_IMG_LIMITS = processutils.ProcessLimits(
|
||||
cpu_time=30,
|
||||
address_space=1 * units.Gi)
|
||||
|
||||
QEMU_VERSION_REQ_SHARED = 2010000
|
||||
|
||||
|
||||
@nova.privsep.sys_admin_pctxt.entrypoint
|
||||
def convert_image(source, dest, in_format, out_format, instances_path,
|
||||
@@ -71,3 +82,53 @@ def unprivileged_convert_image(source, dest, in_format, out_format,
|
||||
|
||||
cmd = cmd + (source, dest)
|
||||
processutils.execute(*cmd)
|
||||
|
||||
|
||||
@nova.privsep.sys_admin_pctxt.entrypoint
|
||||
def privileged_qemu_img_info(path, format=None, qemu_version=None):
|
||||
"""Return an oject containing the parsed output from qemu-img info
|
||||
|
||||
This is a privileged call to qemu-img info using the sys_admin_pctxt
|
||||
entrypoint allowing host block devices etc to be accessed.
|
||||
"""
|
||||
return unprivileged_qemu_img_info(
|
||||
path, format=format, qemu_version=qemu_version)
|
||||
|
||||
|
||||
def unprivileged_qemu_img_info(path, format=None, qemu_version=None):
|
||||
"""Return an object containing the parsed output from qemu-img info."""
|
||||
try:
|
||||
# The following check is about ploop images that reside within
|
||||
# directories and always have DiskDescriptor.xml file beside them
|
||||
if (os.path.isdir(path) and
|
||||
os.path.exists(os.path.join(path, "DiskDescriptor.xml"))):
|
||||
path = os.path.join(path, "root.hds")
|
||||
|
||||
cmd = ('env', 'LC_ALL=C', 'LANG=C', 'qemu-img', 'info', path)
|
||||
if format is not None:
|
||||
cmd = cmd + ('-f', format)
|
||||
# Check to see if the qemu version is >= 2.10 because if so, we need
|
||||
# to add the --force-share flag.
|
||||
if qemu_version and operator.ge(qemu_version, QEMU_VERSION_REQ_SHARED):
|
||||
cmd = cmd + ('--force-share',)
|
||||
out, err = processutils.execute(*cmd, prlimit=QEMU_IMG_LIMITS)
|
||||
except processutils.ProcessExecutionError as exp:
|
||||
if exp.exit_code == -9:
|
||||
# this means we hit prlimits, make the exception more specific
|
||||
msg = (_("qemu-img aborted by prlimits when inspecting "
|
||||
"%(path)s : %(exp)s") % {'path': path, 'exp': exp})
|
||||
elif exp.exit_code == 1 and 'No such file or directory' in exp.stderr:
|
||||
# The os.path.exists check above can race so this is a simple
|
||||
# best effort at catching that type of failure and raising a more
|
||||
# specific error.
|
||||
raise exception.DiskNotFound(location=path)
|
||||
else:
|
||||
msg = (_("qemu-img failed to execute on %(path)s : %(exp)s") %
|
||||
{'path': path, 'exp': exp})
|
||||
raise exception.InvalidDiskInfo(reason=msg)
|
||||
|
||||
if not out:
|
||||
msg = (_("Failed to run qemu-img info on %(path)s : %(error)s") %
|
||||
{'path': path, 'error': err})
|
||||
raise exception.InvalidDiskInfo(reason=msg)
|
||||
return out
|
||||
|
||||
@@ -32,6 +32,7 @@ from nova import exception
|
||||
from nova import objects
|
||||
from nova.objects import fields as obj_fields
|
||||
import nova.privsep.fs
|
||||
import nova.privsep.qemu
|
||||
from nova import test
|
||||
from nova.tests import fixtures as nova_fixtures
|
||||
from nova.tests.unit import fake_instance
|
||||
@@ -107,18 +108,18 @@ disk size: 96K
|
||||
})
|
||||
mock_execute.return_value = (output, '')
|
||||
d_backing = libvirt_utils.get_disk_backing_file(path)
|
||||
mock_execute.assert_called_once_with('env', 'LC_ALL=C', 'LANG=C',
|
||||
'qemu-img', 'info', path,
|
||||
prlimit=images.QEMU_IMG_LIMITS)
|
||||
mock_execute.assert_called_once_with(
|
||||
'env', 'LC_ALL=C', 'LANG=C', 'qemu-img', 'info', path,
|
||||
prlimit=nova.privsep.qemu.QEMU_IMG_LIMITS)
|
||||
mock_exists.assert_called_once_with(path)
|
||||
self.assertIsNone(d_backing)
|
||||
|
||||
def _test_disk_size(self, mock_execute, path, expected_size):
|
||||
d_size = libvirt_utils.get_disk_size(path)
|
||||
self.assertEqual(expected_size, d_size)
|
||||
mock_execute.assert_called_once_with('env', 'LC_ALL=C', 'LANG=C',
|
||||
'qemu-img', 'info', path,
|
||||
prlimit=images.QEMU_IMG_LIMITS)
|
||||
mock_execute.assert_called_once_with(
|
||||
'env', 'LC_ALL=C', 'LANG=C', 'qemu-img', 'info', path,
|
||||
prlimit=nova.privsep.qemu.QEMU_IMG_LIMITS)
|
||||
|
||||
@mock.patch('os.path.exists', return_value=True)
|
||||
def test_disk_size(self, mock_exists):
|
||||
@@ -163,9 +164,9 @@ blah BLAH: bb
|
||||
"""
|
||||
mock_execute.return_value = (example_output, '')
|
||||
image_info = images.qemu_img_info(path)
|
||||
mock_execute.assert_called_once_with('env', 'LC_ALL=C', 'LANG=C',
|
||||
'qemu-img', 'info', path,
|
||||
prlimit=images.QEMU_IMG_LIMITS)
|
||||
mock_execute.assert_called_once_with(
|
||||
'env', 'LC_ALL=C', 'LANG=C', 'qemu-img', 'info', path,
|
||||
prlimit=nova.privsep.qemu.QEMU_IMG_LIMITS)
|
||||
mock_exists.assert_called_once_with(path)
|
||||
self.assertEqual('disk.config', image_info.image)
|
||||
self.assertEqual('raw', image_info.file_format)
|
||||
@@ -176,7 +177,7 @@ blah BLAH: bb
|
||||
@mock.patch('os.path.exists', return_value=True)
|
||||
@mock.patch('oslo_concurrency.processutils.execute')
|
||||
def test_qemu_info_canon_qemu_2_10(self, mock_execute, mock_exists):
|
||||
images.QEMU_VERSION = images.QEMU_VERSION_REQ_SHARED
|
||||
images.QEMU_VERSION = nova.privsep.qemu.QEMU_VERSION_REQ_SHARED
|
||||
path = "disk.config"
|
||||
example_output = """image: disk.config
|
||||
file format: raw
|
||||
@@ -187,10 +188,9 @@ blah BLAH: bb
|
||||
"""
|
||||
mock_execute.return_value = (example_output, '')
|
||||
image_info = images.qemu_img_info(path)
|
||||
mock_execute.assert_called_once_with('env', 'LC_ALL=C', 'LANG=C',
|
||||
'qemu-img', 'info', path,
|
||||
'--force-share',
|
||||
prlimit=images.QEMU_IMG_LIMITS)
|
||||
mock_execute.assert_called_once_with(
|
||||
'env', 'LC_ALL=C', 'LANG=C', 'qemu-img', 'info', path,
|
||||
'--force-share', prlimit=nova.privsep.qemu.QEMU_IMG_LIMITS)
|
||||
mock_exists.assert_called_once_with(path)
|
||||
self.assertEqual('disk.config', image_info.image)
|
||||
self.assertEqual('raw', image_info.file_format)
|
||||
@@ -211,9 +211,9 @@ backing file: /var/lib/nova/a328c7998805951a_2
|
||||
"""
|
||||
mock_execute.return_value = (example_output, '')
|
||||
image_info = images.qemu_img_info(path)
|
||||
mock_execute.assert_called_once_with('env', 'LC_ALL=C', 'LANG=C',
|
||||
'qemu-img', 'info', path,
|
||||
prlimit=images.QEMU_IMG_LIMITS)
|
||||
mock_execute.assert_called_once_with(
|
||||
'env', 'LC_ALL=C', 'LANG=C', 'qemu-img', 'info', path,
|
||||
prlimit=nova.privsep.qemu.QEMU_IMG_LIMITS)
|
||||
mock_exists.assert_called_once_with(path)
|
||||
self.assertEqual('disk.config', image_info.image)
|
||||
self.assertEqual('qcow2', image_info.file_format)
|
||||
@@ -235,10 +235,10 @@ disk size: 706M
|
||||
"""
|
||||
mock_execute.return_value = (example_output, '')
|
||||
image_info = images.qemu_img_info(path)
|
||||
mock_execute.assert_called_once_with('env', 'LC_ALL=C', 'LANG=C',
|
||||
'qemu-img', 'info',
|
||||
os.path.join(path, 'root.hds'),
|
||||
prlimit=images.QEMU_IMG_LIMITS)
|
||||
mock_execute.assert_called_once_with(
|
||||
'env', 'LC_ALL=C', 'LANG=C', 'qemu-img', 'info',
|
||||
os.path.join(path, 'root.hds'),
|
||||
prlimit=nova.privsep.qemu.QEMU_IMG_LIMITS)
|
||||
mock_isdir.assert_called_once_with(path)
|
||||
self.assertEqual(2, mock_exists.call_count)
|
||||
self.assertEqual(path, mock_exists.call_args_list[0][0][0])
|
||||
@@ -266,9 +266,9 @@ backing file: /var/lib/nova/a328c7998805951a_2 (actual path: /b/3a988059e51a_2)
|
||||
"""
|
||||
mock_execute.return_value = (example_output, '')
|
||||
image_info = images.qemu_img_info(path)
|
||||
mock_execute.assert_called_once_with('env', 'LC_ALL=C', 'LANG=C',
|
||||
'qemu-img', 'info', path,
|
||||
prlimit=images.QEMU_IMG_LIMITS)
|
||||
mock_execute.assert_called_once_with(
|
||||
'env', 'LC_ALL=C', 'LANG=C', 'qemu-img', 'info', path,
|
||||
prlimit=nova.privsep.qemu.QEMU_IMG_LIMITS)
|
||||
mock_exists.assert_called_once_with(path)
|
||||
self.assertEqual('disk.config', image_info.image)
|
||||
self.assertEqual('raw', image_info.file_format)
|
||||
@@ -295,9 +295,9 @@ junk stuff: bbb
|
||||
"""
|
||||
mock_execute.return_value = (example_output, '')
|
||||
image_info = images.qemu_img_info(path)
|
||||
mock_execute.assert_called_once_with('env', 'LC_ALL=C', 'LANG=C',
|
||||
'qemu-img', 'info', path,
|
||||
prlimit=images.QEMU_IMG_LIMITS)
|
||||
mock_execute.assert_called_once_with(
|
||||
'env', 'LC_ALL=C', 'LANG=C', 'qemu-img', 'info', path,
|
||||
prlimit=nova.privsep.qemu.QEMU_IMG_LIMITS)
|
||||
mock_exists.assert_called_once_with(path)
|
||||
self.assertEqual('disk.config', image_info.image)
|
||||
self.assertEqual('raw', image_info.file_format)
|
||||
@@ -320,9 +320,9 @@ ID TAG VM SIZE DATE VM CLOCK
|
||||
"""
|
||||
mock_execute.return_value = (example_output, '')
|
||||
image_info = images.qemu_img_info(path)
|
||||
mock_execute.assert_called_once_with('env', 'LC_ALL=C', 'LANG=C',
|
||||
'qemu-img', 'info', path,
|
||||
prlimit=images.QEMU_IMG_LIMITS)
|
||||
mock_execute.assert_called_once_with(
|
||||
'env', 'LC_ALL=C', 'LANG=C', 'qemu-img', 'info', path,
|
||||
prlimit=nova.privsep.qemu.QEMU_IMG_LIMITS)
|
||||
mock_exists.assert_called_once_with(path)
|
||||
self.assertEqual('disk.config', image_info.image)
|
||||
self.assertEqual('raw', image_info.file_format)
|
||||
@@ -477,9 +477,9 @@ disk size: 4.4M
|
||||
"""
|
||||
mock_execute.return_value = (example_output, '')
|
||||
self.assertEqual(4592640, disk.get_disk_size('/some/path'))
|
||||
mock_execute.assert_called_once_with('env', 'LC_ALL=C', 'LANG=C',
|
||||
'qemu-img', 'info', path,
|
||||
prlimit=images.QEMU_IMG_LIMITS)
|
||||
mock_execute.assert_called_once_with(
|
||||
'env', 'LC_ALL=C', 'LANG=C', 'qemu-img', 'info', path,
|
||||
prlimit=nova.privsep.qemu.QEMU_IMG_LIMITS)
|
||||
mock_exists.assert_called_once_with(path)
|
||||
|
||||
def test_copy_image(self):
|
||||
|
||||
+7
-47
@@ -19,14 +19,12 @@
|
||||
Handling of VM disk images.
|
||||
"""
|
||||
|
||||
import operator
|
||||
import os
|
||||
|
||||
from oslo_concurrency import processutils
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import fileutils
|
||||
from oslo_utils import imageutils
|
||||
from oslo_utils import units
|
||||
|
||||
from nova.compute import utils as compute_utils
|
||||
import nova.conf
|
||||
@@ -35,20 +33,15 @@ from nova.i18n import _
|
||||
from nova.image import glance
|
||||
import nova.privsep.qemu
|
||||
|
||||
# This is set by the libvirt driver on startup. The version is used to
|
||||
# determine what flags need to be set on the command line.
|
||||
QEMU_VERSION = None
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
CONF = nova.conf.CONF
|
||||
IMAGE_API = glance.API()
|
||||
|
||||
QEMU_IMG_LIMITS = processutils.ProcessLimits(
|
||||
cpu_time=30,
|
||||
address_space=1 * units.Gi)
|
||||
|
||||
# This is set by the libvirt driver on startup. The version is used to
|
||||
# determine what flags need to be set on the command line.
|
||||
QEMU_VERSION = None
|
||||
QEMU_VERSION_REQ_SHARED = 2010000
|
||||
|
||||
|
||||
def qemu_img_info(path, format=None):
|
||||
"""Return an object containing the parsed output from qemu-img info."""
|
||||
@@ -57,42 +50,9 @@ def qemu_img_info(path, format=None):
|
||||
if not os.path.exists(path) and CONF.libvirt.images_type != 'rbd':
|
||||
raise exception.DiskNotFound(location=path)
|
||||
|
||||
try:
|
||||
# The following check is about ploop images that reside within
|
||||
# directories and always have DiskDescriptor.xml file beside them
|
||||
if (os.path.isdir(path) and
|
||||
os.path.exists(os.path.join(path, "DiskDescriptor.xml"))):
|
||||
path = os.path.join(path, "root.hds")
|
||||
|
||||
cmd = ('env', 'LC_ALL=C', 'LANG=C', 'qemu-img', 'info', path)
|
||||
if format is not None:
|
||||
cmd = cmd + ('-f', format)
|
||||
# Check to see if the qemu version is >= 2.10 because if so, we need
|
||||
# to add the --force-share flag.
|
||||
if QEMU_VERSION and operator.ge(QEMU_VERSION, QEMU_VERSION_REQ_SHARED):
|
||||
cmd = cmd + ('--force-share',)
|
||||
out, err = processutils.execute(*cmd, prlimit=QEMU_IMG_LIMITS)
|
||||
except processutils.ProcessExecutionError as exp:
|
||||
if exp.exit_code == -9:
|
||||
# this means we hit prlimits, make the exception more specific
|
||||
msg = (_("qemu-img aborted by prlimits when inspecting "
|
||||
"%(path)s : %(exp)s") % {'path': path, 'exp': exp})
|
||||
elif exp.exit_code == 1 and 'No such file or directory' in exp.stderr:
|
||||
# The os.path.exists check above can race so this is a simple
|
||||
# best effort at catching that type of failure and raising a more
|
||||
# specific error.
|
||||
raise exception.DiskNotFound(location=path)
|
||||
else:
|
||||
msg = (_("qemu-img failed to execute on %(path)s : %(exp)s") %
|
||||
{'path': path, 'exp': exp})
|
||||
raise exception.InvalidDiskInfo(reason=msg)
|
||||
|
||||
if not out:
|
||||
msg = (_("Failed to run qemu-img info on %(path)s : %(error)s") %
|
||||
{'path': path, 'error': err})
|
||||
raise exception.InvalidDiskInfo(reason=msg)
|
||||
|
||||
return imageutils.QemuImgInfo(out)
|
||||
info = nova.privsep.qemu.unprivileged_qemu_img_info(
|
||||
path, format=format, qemu_version=QEMU_VERSION)
|
||||
return imageutils.QemuImgInfo(info)
|
||||
|
||||
|
||||
def convert_image(source, dest, in_format, out_format, run_as_root=False,
|
||||
|
||||
Reference in New Issue
Block a user