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:
Lee Yarwood
2020-02-05 12:36:40 +00:00
parent e483ca1cd9
commit 03d6eb500f
3 changed files with 102 additions and 81 deletions
+62 -1
View File
@@ -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
+33 -33
View File
@@ -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
View File
@@ -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,