Add encryption support to convert_image
This change enables ephemeral encryption support to convert: * encrypted source image to unencrypted destination image * unencrypted source image to encrypted destination image * encrypted source image to encrypted destination image This also makes necessary changes for mypy checks to pass. Related to blueprint ephemeral-storage-encryption Change-Id: I9edc87006b1f7de69bc52f916f45c2cbb66abe23
This commit is contained in:
parent
3391ac2656
commit
9f7a6732f9
@ -16,7 +16,10 @@
|
|||||||
Helpers for qemu tasks.
|
Helpers for qemu tasks.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import contextlib
|
||||||
import os
|
import os
|
||||||
|
import tempfile
|
||||||
|
import typing as ty
|
||||||
|
|
||||||
from oslo_concurrency import processutils
|
from oslo_concurrency import processutils
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
@ -33,16 +36,53 @@ QEMU_IMG_LIMITS = processutils.ProcessLimits(
|
|||||||
address_space=1 * units.Gi)
|
address_space=1 * units.Gi)
|
||||||
|
|
||||||
|
|
||||||
|
class EncryptionOptions(ty.TypedDict):
|
||||||
|
secret: str
|
||||||
|
format: str
|
||||||
|
|
||||||
|
|
||||||
@nova.privsep.sys_admin_pctxt.entrypoint
|
@nova.privsep.sys_admin_pctxt.entrypoint
|
||||||
def convert_image(source, dest, in_format, out_format, instances_path,
|
def convert_image(source, dest, in_format, out_format, instances_path,
|
||||||
compress):
|
compress, src_encryption=None, dest_encryption=None):
|
||||||
unprivileged_convert_image(source, dest, in_format, out_format,
|
unprivileged_convert_image(source, dest, in_format, out_format,
|
||||||
instances_path, compress)
|
instances_path, compress,
|
||||||
|
src_encryption=src_encryption,
|
||||||
|
dest_encryption=dest_encryption)
|
||||||
|
|
||||||
|
|
||||||
# NOTE(mikal): this method is deliberately not wrapped in a privsep entrypoint
|
# NOTE(mikal): this method is deliberately not wrapped in a privsep entrypoint
|
||||||
def unprivileged_convert_image(source, dest, in_format, out_format,
|
def unprivileged_convert_image(
|
||||||
instances_path, compress):
|
source: str,
|
||||||
|
dest: str,
|
||||||
|
in_format: ty.Optional[str],
|
||||||
|
out_format: str,
|
||||||
|
instances_path: str,
|
||||||
|
compress: bool,
|
||||||
|
src_encryption: ty.Optional[EncryptionOptions] = None,
|
||||||
|
dest_encryption: ty.Optional[EncryptionOptions] = None,
|
||||||
|
) -> None:
|
||||||
|
"""Disk image conversion with qemu-img
|
||||||
|
|
||||||
|
:param source: Location of the disk image to convert
|
||||||
|
:param dest: Desired location of the converted disk image
|
||||||
|
:param in_format: Disk image format of the source image
|
||||||
|
:param out_format: Desired disk image format of the converted disk image
|
||||||
|
:param instances_path: Location where instances are stored on disk
|
||||||
|
:param compress: Whether to compress the converted disk image
|
||||||
|
:param src_encryption: (Optional) Dict detailing various encryption
|
||||||
|
attributes for the source image such as the format and passphrase.
|
||||||
|
:param dest_encryption: (Optional) Dict detailing various encryption
|
||||||
|
attributes for the destination image such as the format and passphrase.
|
||||||
|
|
||||||
|
The in_format and out_format represent disk image file formats in QEMU,
|
||||||
|
which are:
|
||||||
|
|
||||||
|
* qcow2, which can be encrypted or not encrypted depending on options
|
||||||
|
* raw, which is unencrypted
|
||||||
|
* luks, which is encrypted raw
|
||||||
|
|
||||||
|
See https://www.qemu.org/docs/master/system/qemu-block-drivers.html
|
||||||
|
"""
|
||||||
# NOTE(mdbooth, kchamart): `qemu-img convert` defaults to
|
# NOTE(mdbooth, kchamart): `qemu-img convert` defaults to
|
||||||
# 'cache=writeback' for the source image, and 'cache=unsafe' for the
|
# 'cache=writeback' for the source image, and 'cache=unsafe' for the
|
||||||
# target, which means that data is not synced to disk at completion.
|
# target, which means that data is not synced to disk at completion.
|
||||||
@ -69,16 +109,114 @@ def unprivileged_convert_image(source, dest, in_format, out_format,
|
|||||||
cache_mode = 'none'
|
cache_mode = 'none'
|
||||||
else:
|
else:
|
||||||
cache_mode = 'writeback'
|
cache_mode = 'writeback'
|
||||||
cmd = ('qemu-img', 'convert', '-t', cache_mode, '-O', out_format)
|
cmd = ['qemu-img', 'convert', '-t', cache_mode, '-O', out_format]
|
||||||
|
|
||||||
|
# qemu-img: --image-opts and --format are mutually exclusive
|
||||||
|
# If the source is encrypted, we will need to pass encryption related
|
||||||
|
# options using --image-opts.
|
||||||
|
driver_str = ''
|
||||||
if in_format is not None:
|
if in_format is not None:
|
||||||
cmd = cmd + ('-f', in_format)
|
if not src_encryption:
|
||||||
|
cmd += ['-f', in_format]
|
||||||
|
else:
|
||||||
|
driver_str = f'driver={in_format},'
|
||||||
|
|
||||||
if compress:
|
if compress:
|
||||||
cmd += ('-c',)
|
cmd += ['-c']
|
||||||
|
|
||||||
cmd = cmd + (source, dest)
|
src_secret_file = None
|
||||||
processutils.execute(*cmd)
|
dest_secret_file = None
|
||||||
|
encryption_opts: ty.List[str] = []
|
||||||
|
with contextlib.ExitStack() as stack:
|
||||||
|
if src_encryption:
|
||||||
|
src_secret_file = stack.enter_context(
|
||||||
|
tempfile.NamedTemporaryFile(mode='tr+', encoding='utf-8'))
|
||||||
|
|
||||||
|
# Write out the passphrase secret to a temp file
|
||||||
|
src_secret_file.write(src_encryption['secret'])
|
||||||
|
|
||||||
|
# Ensure the secret is written to disk, we can't .close() here as
|
||||||
|
# that removes the file when using NamedTemporaryFile
|
||||||
|
src_secret_file.flush()
|
||||||
|
|
||||||
|
# When --image-opts is used, the source filename must be passed as
|
||||||
|
# part of the option string instead of as a positional arg.
|
||||||
|
#
|
||||||
|
# The basic options include the secret and encryption format
|
||||||
|
# Option names depend on the QEMU disk image file format:
|
||||||
|
# https://www.qemu.org/docs/master/system/qemu-block-drivers.html#disk-image-file-formats # noqa
|
||||||
|
# For 'luks' it is 'key-secret' and format is implied
|
||||||
|
# For 'qcow2' it is 'encrypt.key-secret' and 'encrypt.format'
|
||||||
|
prefix = 'encrypt.' if in_format == 'qcow2' else ''
|
||||||
|
encryption_opts = [
|
||||||
|
'--object', f"secret,id=sec0,file={src_secret_file.name}",
|
||||||
|
'--image-opts',
|
||||||
|
f"{driver_str}file.driver=file,file.filename={source},"
|
||||||
|
f"{prefix}key-secret=sec0",
|
||||||
|
]
|
||||||
|
|
||||||
|
if dest_encryption:
|
||||||
|
dest_secret_file = stack.enter_context(
|
||||||
|
tempfile.NamedTemporaryFile(mode='tr+', encoding='utf-8'))
|
||||||
|
|
||||||
|
# Write out the passphrase secret to a temp file
|
||||||
|
dest_secret_file.write(dest_encryption['secret'])
|
||||||
|
|
||||||
|
# Ensure the secret is written to disk, we can't .close()
|
||||||
|
# here as that removes the file when using
|
||||||
|
# NamedTemporaryFile
|
||||||
|
dest_secret_file.flush()
|
||||||
|
|
||||||
|
prefix = 'encrypt.' if out_format == 'qcow2' else ''
|
||||||
|
encryption_opts += [
|
||||||
|
'--object', f"secret,id=sec1,file={dest_secret_file.name}",
|
||||||
|
'-o', f'{prefix}key-secret=sec1',
|
||||||
|
]
|
||||||
|
if prefix:
|
||||||
|
# The encryption format is only relevant for the 'qcow2' disk
|
||||||
|
# format. Otherwise, the disk format is 'luks' and the
|
||||||
|
# encryption format is implied and not accepted as an option in
|
||||||
|
# that case.
|
||||||
|
encryption_opts += [
|
||||||
|
'-o', f"{prefix}format={dest_encryption['format']}"
|
||||||
|
]
|
||||||
|
# Supported luks options:
|
||||||
|
# cipher-alg=<str> - Name of cipher algorithm and
|
||||||
|
# key length
|
||||||
|
# cipher-mode=<str> - Name of encryption cipher mode
|
||||||
|
# hash-alg=<str> - Name of hash algorithm to use
|
||||||
|
# for PBKDF
|
||||||
|
# iter-time=<num> - Time to spend in PBKDF in
|
||||||
|
# milliseconds
|
||||||
|
# ivgen-alg=<str> - Name of IV generator algorithm
|
||||||
|
# ivgen-hash-alg=<str> - Name of IV generator hash
|
||||||
|
# algorithm
|
||||||
|
#
|
||||||
|
# NOTE(melwitt): Sensible defaults (that match the qemu
|
||||||
|
# defaults) are hardcoded at this time for simplicity and
|
||||||
|
# consistency when instances are migrated. Configuration of
|
||||||
|
# luks options could be added in a future release.
|
||||||
|
encryption_options = {
|
||||||
|
'cipher-alg': 'aes-256',
|
||||||
|
'cipher-mode': 'xts',
|
||||||
|
'hash-alg': 'sha256',
|
||||||
|
'iter-time': 2000,
|
||||||
|
'ivgen-alg': 'plain64',
|
||||||
|
'ivgen-hash-alg': 'sha256',
|
||||||
|
}
|
||||||
|
for option, value in encryption_options.items():
|
||||||
|
encryption_opts += [
|
||||||
|
'-o', f'{prefix}{option}={value}',
|
||||||
|
]
|
||||||
|
|
||||||
|
if src_encryption or dest_encryption:
|
||||||
|
cmd += encryption_opts
|
||||||
|
|
||||||
|
# If the source is not encrypted, it's passed as a positional argument.
|
||||||
|
if not src_encryption:
|
||||||
|
cmd += [source]
|
||||||
|
|
||||||
|
processutils.execute(*cmd + [dest])
|
||||||
|
|
||||||
|
|
||||||
@nova.privsep.sys_admin_pctxt.entrypoint
|
@nova.privsep.sys_admin_pctxt.entrypoint
|
||||||
@ -100,12 +238,12 @@ def unprivileged_qemu_img_info(path, format=None):
|
|||||||
os.path.exists(os.path.join(path, "DiskDescriptor.xml"))):
|
os.path.exists(os.path.join(path, "DiskDescriptor.xml"))):
|
||||||
path = os.path.join(path, "root.hds")
|
path = os.path.join(path, "root.hds")
|
||||||
|
|
||||||
cmd = (
|
cmd = [
|
||||||
'env', 'LC_ALL=C', 'LANG=C', 'qemu-img', 'info', path,
|
'env', 'LC_ALL=C', 'LANG=C', 'qemu-img', 'info', path,
|
||||||
'--force-share', '--output=json',
|
'--force-share', '--output=json',
|
||||||
)
|
]
|
||||||
if format is not None:
|
if format is not None:
|
||||||
cmd = cmd + ('-f', format)
|
cmd = cmd + ['-f', format]
|
||||||
out, err = processutils.execute(*cmd, prlimit=QEMU_IMG_LIMITS)
|
out, err = processutils.execute(*cmd, prlimit=QEMU_IMG_LIMITS)
|
||||||
except processutils.ProcessExecutionError as exp:
|
except processutils.ProcessExecutionError as exp:
|
||||||
if exp.exit_code == -9:
|
if exp.exit_code == -9:
|
||||||
|
@ -15,11 +15,14 @@
|
|||||||
|
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
|
import ddt
|
||||||
|
|
||||||
import nova.privsep.qemu
|
import nova.privsep.qemu
|
||||||
from nova import test
|
from nova import test
|
||||||
from nova.tests import fixtures
|
from nova.tests import fixtures
|
||||||
|
|
||||||
|
|
||||||
|
@ddt.ddt
|
||||||
class QemuTestCase(test.NoDBTestCase):
|
class QemuTestCase(test.NoDBTestCase):
|
||||||
"""Test qemu related utility methods."""
|
"""Test qemu related utility methods."""
|
||||||
|
|
||||||
@ -53,6 +56,136 @@ class QemuTestCase(test.NoDBTestCase):
|
|||||||
def test_convert_image_unprivileged(self):
|
def test_convert_image_unprivileged(self):
|
||||||
self._test_convert_image(nova.privsep.qemu.unprivileged_convert_image)
|
self._test_convert_image(nova.privsep.qemu.unprivileged_convert_image)
|
||||||
|
|
||||||
|
@mock.patch('oslo_concurrency.processutils.execute')
|
||||||
|
@mock.patch('tempfile.NamedTemporaryFile')
|
||||||
|
@mock.patch('nova.privsep.utils.supports_direct_io',
|
||||||
|
new=mock.Mock(return_value=True))
|
||||||
|
@ddt.data(
|
||||||
|
('qcow2', 'qcow2'), ('qcow2', 'raw'),
|
||||||
|
('luks', 'raw'), ('luks', 'qcow2'))
|
||||||
|
@ddt.unpack
|
||||||
|
def test_convert_image_encrypted_source_to_unencrypted_dest(
|
||||||
|
self, in_format, out_format, mock_tempfile, mock_execute):
|
||||||
|
# Simulate an encrypted source image conversion to an unencrypted
|
||||||
|
# destination image.
|
||||||
|
mock_file = mock.Mock()
|
||||||
|
mock_file.name = '/tmp/filename'
|
||||||
|
mock_tempfile.return_value.__enter__.return_value = mock_file
|
||||||
|
src_encryption = {'format': 'luks', 'secret': '12345'}
|
||||||
|
|
||||||
|
nova.privsep.qemu.convert_image(
|
||||||
|
'/fake/source', '/fake/dest', in_format, out_format,
|
||||||
|
'/fake/instances/path', compress=True,
|
||||||
|
src_encryption=src_encryption)
|
||||||
|
|
||||||
|
mock_file.write.assert_called_once_with('12345')
|
||||||
|
mock_file.flush.assert_called_once()
|
||||||
|
prefix = 'encrypt.' if in_format == 'qcow2' else ''
|
||||||
|
mock_execute.assert_called_once_with(
|
||||||
|
'qemu-img', 'convert', '-t', 'none', '-O', out_format, '-c',
|
||||||
|
'--object', 'secret,id=sec0,file=/tmp/filename', '--image-opts',
|
||||||
|
f'driver={in_format},file.driver=file,file.filename=/fake/source,'
|
||||||
|
f'{prefix}key-secret=sec0', '/fake/dest')
|
||||||
|
|
||||||
|
@mock.patch('oslo_concurrency.processutils.execute')
|
||||||
|
@mock.patch('tempfile.NamedTemporaryFile')
|
||||||
|
@mock.patch('nova.privsep.utils.supports_direct_io',
|
||||||
|
new=mock.Mock(return_value=True))
|
||||||
|
@ddt.data(
|
||||||
|
('qcow2', 'qcow2'), ('qcow2', 'raw'),
|
||||||
|
('raw', 'luks'), ('raw', 'qcow2'))
|
||||||
|
@ddt.unpack
|
||||||
|
def test_convert_image_unencrypted_source_to_encrypted_dest(
|
||||||
|
self, in_format, out_format, mock_tempfile, mock_execute):
|
||||||
|
# Simulate an unencrypted source image conversion to an encrypted
|
||||||
|
# destination image.
|
||||||
|
mock_file = mock.Mock()
|
||||||
|
mock_file.name = '/tmp/filename'
|
||||||
|
mock_tempfile.return_value.__enter__.return_value = mock_file
|
||||||
|
encryption = {'format': 'luks', 'secret': '12345'}
|
||||||
|
|
||||||
|
nova.privsep.qemu.convert_image(
|
||||||
|
'/fake/source', '/fake/dest', in_format, out_format,
|
||||||
|
'/fake/instances/path', compress=True, dest_encryption=encryption)
|
||||||
|
|
||||||
|
mock_file.write.assert_called_once_with('12345')
|
||||||
|
mock_file.flush.assert_called_once()
|
||||||
|
|
||||||
|
prefix = 'encrypt.' if out_format == 'qcow2' else ''
|
||||||
|
expected_args = [
|
||||||
|
'qemu-img', 'convert', '-t', 'none', '-O', out_format,
|
||||||
|
'-f', in_format, '-c',
|
||||||
|
'--object', 'secret,id=sec1,file=/tmp/filename',
|
||||||
|
'-o', f'{prefix}key-secret=sec1',
|
||||||
|
]
|
||||||
|
if prefix:
|
||||||
|
expected_args += ['-o', f'{prefix}format=luks']
|
||||||
|
|
||||||
|
expected_args += [
|
||||||
|
'-o', f'{prefix}cipher-alg=aes-256',
|
||||||
|
'-o', f'{prefix}cipher-mode=xts',
|
||||||
|
'-o', f'{prefix}hash-alg=sha256',
|
||||||
|
'-o', f'{prefix}iter-time=2000',
|
||||||
|
'-o', f'{prefix}ivgen-alg=plain64',
|
||||||
|
'-o', f'{prefix}ivgen-hash-alg=sha256',
|
||||||
|
'/fake/source', '/fake/dest',
|
||||||
|
]
|
||||||
|
mock_execute.assert_called_once_with(*expected_args)
|
||||||
|
|
||||||
|
@mock.patch('oslo_concurrency.processutils.execute')
|
||||||
|
@mock.patch('tempfile.NamedTemporaryFile')
|
||||||
|
@mock.patch('nova.privsep.utils.supports_direct_io',
|
||||||
|
new=mock.Mock(return_value=True))
|
||||||
|
@ddt.data(
|
||||||
|
('qcow2', 'qcow2'), ('qcow2', 'luks'),
|
||||||
|
('luks', 'luks'), ('luks', 'qcow2'))
|
||||||
|
@ddt.unpack
|
||||||
|
def test_convert_image_encrypted_source_and_dest(
|
||||||
|
self, in_format, out_format, mock_tempfile, mock_execute):
|
||||||
|
# Simulate an encrypted source image conversion to an encrypted
|
||||||
|
# destination image.
|
||||||
|
mock_file1 = mock.Mock()
|
||||||
|
mock_file1.name = '/tmp/filename1'
|
||||||
|
src_encryption = {'format': 'luks', 'secret': '12345'}
|
||||||
|
mock_file2 = mock.Mock()
|
||||||
|
mock_file2.name = '/tmp/filename2'
|
||||||
|
mock_tempfile.return_value.__enter__.side_effect = [
|
||||||
|
mock_file1, mock_file2]
|
||||||
|
dest_encryption = {'format': 'luks', 'secret': '67890'}
|
||||||
|
|
||||||
|
nova.privsep.qemu.convert_image(
|
||||||
|
'/fake/source', '/fake/dest', in_format, out_format,
|
||||||
|
'/fake/instances/path', compress=True,
|
||||||
|
src_encryption=src_encryption,
|
||||||
|
dest_encryption=dest_encryption)
|
||||||
|
|
||||||
|
mock_file1.write.assert_called_once_with('12345')
|
||||||
|
mock_file1.flush.assert_called_once()
|
||||||
|
mock_file2.write.assert_called_once_with('67890')
|
||||||
|
mock_file2.flush.assert_called_once()
|
||||||
|
|
||||||
|
in_prefix = 'encrypt.' if in_format == 'qcow2' else ''
|
||||||
|
out_prefix = 'encrypt.' if out_format == 'qcow2' else ''
|
||||||
|
expected_args = [
|
||||||
|
'qemu-img', 'convert', '-t', 'none', '-O', out_format, '-c',
|
||||||
|
'--object', 'secret,id=sec0,file=/tmp/filename1', '--image-opts',
|
||||||
|
f'driver={in_format},file.driver=file,file.filename=/fake/source,'
|
||||||
|
f'{in_prefix}key-secret=sec0',
|
||||||
|
'--object', 'secret,id=sec1,file=/tmp/filename2',
|
||||||
|
'-o', f'{out_prefix}key-secret=sec1',
|
||||||
|
]
|
||||||
|
if out_prefix:
|
||||||
|
expected_args += ['-o', f'{out_prefix}format=luks']
|
||||||
|
expected_args += [
|
||||||
|
'-o', f'{out_prefix}cipher-alg=aes-256',
|
||||||
|
'-o', f'{out_prefix}cipher-mode=xts',
|
||||||
|
'-o', f'{out_prefix}hash-alg=sha256',
|
||||||
|
'-o', f'{out_prefix}iter-time=2000',
|
||||||
|
'-o', f'{out_prefix}ivgen-alg=plain64',
|
||||||
|
'-o', f'{out_prefix}ivgen-hash-alg=sha256', '/fake/dest',
|
||||||
|
]
|
||||||
|
mock_execute.assert_called_once_with(*expected_args)
|
||||||
|
|
||||||
@mock.patch('oslo_concurrency.processutils.execute')
|
@mock.patch('oslo_concurrency.processutils.execute')
|
||||||
@mock.patch('os.path.isdir')
|
@mock.patch('os.path.isdir')
|
||||||
def _test_qemu_img_info(self, method, mock_isdir, mock_execute):
|
def _test_qemu_img_info(self, method, mock_isdir, mock_execute):
|
||||||
|
@ -734,7 +734,8 @@ class LvmTestCase(_ImageTestCase, test.NoDBTestCase):
|
|||||||
mock_get.assert_called_once_with(self.TEMPLATE_PATH)
|
mock_get.assert_called_once_with(self.TEMPLATE_PATH)
|
||||||
path = '/dev/%s/%s_%s' % (self.VG, self.INSTANCE.uuid, self.NAME)
|
path = '/dev/%s/%s_%s' % (self.VG, self.INSTANCE.uuid, self.NAME)
|
||||||
mock_convert_image.assert_called_once_with(
|
mock_convert_image.assert_called_once_with(
|
||||||
self.TEMPLATE_PATH, path, None, 'raw', CONF.instances_path, False)
|
self.TEMPLATE_PATH, path, None, 'raw', CONF.instances_path, False,
|
||||||
|
src_encryption=None, dest_encryption=None)
|
||||||
mock_disk_op_sema.__enter__.assert_called_once()
|
mock_disk_op_sema.__enter__.assert_called_once()
|
||||||
|
|
||||||
@mock.patch.object(imagebackend.lvm, 'create_volume')
|
@mock.patch.object(imagebackend.lvm, 'create_volume')
|
||||||
@ -769,7 +770,8 @@ class LvmTestCase(_ImageTestCase, test.NoDBTestCase):
|
|||||||
mock_get.assert_called_once_with(self.TEMPLATE_PATH)
|
mock_get.assert_called_once_with(self.TEMPLATE_PATH)
|
||||||
mock_convert_image.assert_called_once_with(
|
mock_convert_image.assert_called_once_with(
|
||||||
self.TEMPLATE_PATH, self.PATH, None, 'raw',
|
self.TEMPLATE_PATH, self.PATH, None, 'raw',
|
||||||
CONF.instances_path, False)
|
CONF.instances_path, False, src_encryption=None,
|
||||||
|
dest_encryption=None)
|
||||||
mock_disk_op_sema.__enter__.assert_called_once()
|
mock_disk_op_sema.__enter__.assert_called_once()
|
||||||
mock_resize.assert_called_once_with(self.PATH, run_as_root=True)
|
mock_resize.assert_called_once_with(self.PATH, run_as_root=True)
|
||||||
|
|
||||||
@ -1007,7 +1009,8 @@ class EncryptedLvmTestCase(_ImageTestCase, test.NoDBTestCase):
|
|||||||
self.KEY)
|
self.KEY)
|
||||||
nova.privsep.qemu.convert_image.assert_called_with(
|
nova.privsep.qemu.convert_image.assert_called_with(
|
||||||
self.TEMPLATE_PATH, self.PATH, None, 'raw',
|
self.TEMPLATE_PATH, self.PATH, None, 'raw',
|
||||||
CONF.instances_path, False)
|
CONF.instances_path, False, src_encryption=None,
|
||||||
|
dest_encryption=None)
|
||||||
|
|
||||||
def _create_image_generated(self, sparse):
|
def _create_image_generated(self, sparse):
|
||||||
with test.nested(
|
with test.nested(
|
||||||
@ -1078,7 +1081,8 @@ class EncryptedLvmTestCase(_ImageTestCase, test.NoDBTestCase):
|
|||||||
self.KEY)
|
self.KEY)
|
||||||
nova.privsep.qemu.convert_image.assert_called_with(
|
nova.privsep.qemu.convert_image.assert_called_with(
|
||||||
self.TEMPLATE_PATH, self.PATH, None, 'raw',
|
self.TEMPLATE_PATH, self.PATH, None, 'raw',
|
||||||
CONF.instances_path, False)
|
CONF.instances_path, False, src_encryption=None,
|
||||||
|
dest_encryption=None)
|
||||||
self.disk.resize2fs.assert_called_with(self.PATH, run_as_root=True)
|
self.disk.resize2fs.assert_called_with(self.PATH, run_as_root=True)
|
||||||
|
|
||||||
def test_create_image(self):
|
def test_create_image(self):
|
||||||
|
@ -448,7 +448,8 @@ class LibvirtUtilsTestCase(test.NoDBTestCase):
|
|||||||
mock_disk_op_sema.__enter__.assert_called_once()
|
mock_disk_op_sema.__enter__.assert_called_once()
|
||||||
mock_convert_image.assert_called_with(
|
mock_convert_image.assert_called_with(
|
||||||
't.qcow2.part', 't.qcow2.converted', 'qcow2', 'raw',
|
't.qcow2.part', 't.qcow2.converted', 'qcow2', 'raw',
|
||||||
CONF.instances_path, False)
|
CONF.instances_path, False, src_encryption=None,
|
||||||
|
dest_encryption=None)
|
||||||
mock_convert_image.reset_mock()
|
mock_convert_image.reset_mock()
|
||||||
|
|
||||||
target = 't.raw'
|
target = 't.raw'
|
||||||
|
@ -58,13 +58,14 @@ def privileged_qemu_img_info(path, format=None, output_format='json'):
|
|||||||
|
|
||||||
|
|
||||||
def convert_image(source, dest, in_format, out_format, run_as_root=False,
|
def convert_image(source, dest, in_format, out_format, run_as_root=False,
|
||||||
compress=False):
|
compress=False, src_encryption=None, dest_encryption=None):
|
||||||
"""Convert image to other format."""
|
"""Convert image to other format."""
|
||||||
if in_format is None:
|
if in_format is None:
|
||||||
raise RuntimeError("convert_image without input format is a security"
|
raise RuntimeError("convert_image without input format is a security"
|
||||||
" risk")
|
" risk")
|
||||||
_convert_image(source, dest, in_format, out_format, run_as_root,
|
_convert_image(source, dest, in_format, out_format, run_as_root,
|
||||||
compress=compress)
|
compress=compress, src_encryption=src_encryption,
|
||||||
|
dest_encryption=dest_encryption)
|
||||||
|
|
||||||
|
|
||||||
def convert_image_unsafe(source, dest, out_format, run_as_root=False):
|
def convert_image_unsafe(source, dest, out_format, run_as_root=False):
|
||||||
@ -81,17 +82,19 @@ def convert_image_unsafe(source, dest, out_format, run_as_root=False):
|
|||||||
|
|
||||||
|
|
||||||
def _convert_image(source, dest, in_format, out_format, run_as_root,
|
def _convert_image(source, dest, in_format, out_format, run_as_root,
|
||||||
compress=False):
|
compress=False, src_encryption=None, dest_encryption=None):
|
||||||
try:
|
try:
|
||||||
with compute_utils.disk_ops_semaphore:
|
with compute_utils.disk_ops_semaphore:
|
||||||
if not run_as_root:
|
if not run_as_root:
|
||||||
nova.privsep.qemu.unprivileged_convert_image(
|
nova.privsep.qemu.unprivileged_convert_image(
|
||||||
source, dest, in_format, out_format, CONF.instances_path,
|
source, dest, in_format, out_format, CONF.instances_path,
|
||||||
compress)
|
compress, src_encryption=src_encryption,
|
||||||
|
dest_encryption=dest_encryption)
|
||||||
else:
|
else:
|
||||||
nova.privsep.qemu.convert_image(
|
nova.privsep.qemu.convert_image(
|
||||||
source, dest, in_format, out_format, CONF.instances_path,
|
source, dest, in_format, out_format, CONF.instances_path,
|
||||||
compress)
|
compress, src_encryption=src_encryption,
|
||||||
|
dest_encryption=dest_encryption)
|
||||||
|
|
||||||
except processutils.ProcessExecutionError as exp:
|
except processutils.ProcessExecutionError as exp:
|
||||||
msg = (_("Unable to convert image to %(format)s: %(exp)s") %
|
msg = (_("Unable to convert image to %(format)s: %(exp)s") %
|
||||||
|
Loading…
Reference in New Issue
Block a user