diff --git a/nova/privsep/qemu.py b/nova/privsep/qemu.py index 5880fb1775..07db81a381 100644 --- a/nova/privsep/qemu.py +++ b/nova/privsep/qemu.py @@ -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 diff --git a/nova/tests/unit/virt/libvirt/test_utils.py b/nova/tests/unit/virt/libvirt/test_utils.py index 2888078cde..1c3b3df0ac 100644 --- a/nova/tests/unit/virt/libvirt/test_utils.py +++ b/nova/tests/unit/virt/libvirt/test_utils.py @@ -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): diff --git a/nova/virt/images.py b/nova/virt/images.py index 442c878e2a..7d13dc46d2 100644 --- a/nova/virt/images.py +++ b/nova/virt/images.py @@ -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,