Split common qemu-img functions from disk_utils
Adds a new module for two qemu-img wrappers that are used both by Ironic and IPA. The remaining functions in disk_utils are only used by IPA and, since most of them require root, can no longer be used in the post-rootwrap Ironic any more. They should be moved to IPA eventually. Change-Id: I3364a58b52a3e57c2f52356a6496865b749e74a8
This commit is contained in:
parent
4ac6cf0857
commit
7644196e7d
@ -12,7 +12,6 @@ blkid: CommandFilter, blkid, root
|
|||||||
blockdev: CommandFilter, blockdev, root
|
blockdev: CommandFilter, blockdev, root
|
||||||
hexdump: CommandFilter, hexdump, root
|
hexdump: CommandFilter, hexdump, root
|
||||||
lsblk: CommandFilter, lsblk, root
|
lsblk: CommandFilter, lsblk, root
|
||||||
qemu-img: CommandFilter, qemu-img, root
|
|
||||||
wipefs: CommandFilter, wipefs, root
|
wipefs: CommandFilter, wipefs, root
|
||||||
sgdisk: CommandFilter, sgdisk, root
|
sgdisk: CommandFilter, sgdisk, root
|
||||||
partprobe: CommandFilter, partprobe, root
|
partprobe: CommandFilter, partprobe, root
|
||||||
@ -26,3 +25,6 @@ mount: CommandFilter, mount, root
|
|||||||
# ironic_lib/disk_partitioner.py
|
# ironic_lib/disk_partitioner.py
|
||||||
fuser: CommandFilter, fuser, root
|
fuser: CommandFilter, fuser, root
|
||||||
parted: CommandFilter, parted, root
|
parted: CommandFilter, parted, root
|
||||||
|
|
||||||
|
# ironic_lib/qemu_img.py
|
||||||
|
qemu-img: CommandFilter, qemu-img, root
|
||||||
|
@ -23,13 +23,11 @@ import warnings
|
|||||||
from oslo_concurrency import processutils
|
from oslo_concurrency import processutils
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_utils import excutils
|
from oslo_utils import excutils
|
||||||
from oslo_utils import imageutils
|
|
||||||
from oslo_utils import units
|
|
||||||
import tenacity
|
|
||||||
|
|
||||||
from ironic_lib.common.i18n import _
|
from ironic_lib.common.i18n import _
|
||||||
from ironic_lib import disk_partitioner
|
from ironic_lib import disk_partitioner
|
||||||
from ironic_lib import exception
|
from ironic_lib import exception
|
||||||
|
from ironic_lib import qemu_img
|
||||||
from ironic_lib import utils
|
from ironic_lib import utils
|
||||||
|
|
||||||
|
|
||||||
@ -56,13 +54,6 @@ opts = [
|
|||||||
default=10,
|
default=10,
|
||||||
help='Maximum number of attempts to try to read the '
|
help='Maximum number of attempts to try to read the '
|
||||||
'partition.'),
|
'partition.'),
|
||||||
cfg.IntOpt('image_convert_memory_limit',
|
|
||||||
default=2048,
|
|
||||||
help='Memory limit for "qemu-img convert" in MiB. Implemented '
|
|
||||||
'via the address space resource limit.'),
|
|
||||||
cfg.IntOpt('image_convert_attempts',
|
|
||||||
default=3,
|
|
||||||
help='Number of attempts to convert an image.'),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
@ -83,17 +74,9 @@ GPT_SIZE_SECTORS = 33
|
|||||||
# Maximum disk size supported by MBR is 2TB (2 * 1024 * 1024 MB)
|
# Maximum disk size supported by MBR is 2TB (2 * 1024 * 1024 MB)
|
||||||
MAX_DISK_SIZE_MB_SUPPORTED_BY_MBR = 2097152
|
MAX_DISK_SIZE_MB_SUPPORTED_BY_MBR = 2097152
|
||||||
|
|
||||||
# Limit the memory address space to 1 GiB when running qemu-img
|
# Backward compatibility, do not use
|
||||||
QEMU_IMG_LIMITS = None
|
qemu_img_info = qemu_img.image_info
|
||||||
|
convert_image = qemu_img.convert_image
|
||||||
|
|
||||||
def _qemu_img_limits():
|
|
||||||
global QEMU_IMG_LIMITS
|
|
||||||
if QEMU_IMG_LIMITS is None:
|
|
||||||
QEMU_IMG_LIMITS = processutils.ProcessLimits(
|
|
||||||
address_space=CONF.disk_utils.image_convert_memory_limit
|
|
||||||
* units.Mi)
|
|
||||||
return QEMU_IMG_LIMITS
|
|
||||||
|
|
||||||
|
|
||||||
def list_partitions(device):
|
def list_partitions(device):
|
||||||
@ -485,73 +468,12 @@ def dd(src, dst, conv_flags=None):
|
|||||||
*extra_args)
|
*extra_args)
|
||||||
|
|
||||||
|
|
||||||
def qemu_img_info(path):
|
|
||||||
"""Return an object containing the parsed output from qemu-img info."""
|
|
||||||
if not os.path.exists(path):
|
|
||||||
raise FileNotFoundError(_("File %s does not exist") % path)
|
|
||||||
|
|
||||||
out, err = utils.execute('env', 'LC_ALL=C', 'LANG=C',
|
|
||||||
'qemu-img', 'info', path,
|
|
||||||
'--output=json',
|
|
||||||
prlimit=_qemu_img_limits())
|
|
||||||
return imageutils.QemuImgInfo(out, format='json')
|
|
||||||
|
|
||||||
|
|
||||||
def _retry_on_res_temp_unavailable(exc):
|
|
||||||
if (isinstance(exc, processutils.ProcessExecutionError)
|
|
||||||
and ('Resource temporarily unavailable' in exc.stderr
|
|
||||||
or 'Cannot allocate memory' in exc.stderr)):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
@tenacity.retry(
|
|
||||||
retry=tenacity.retry_if_exception(_retry_on_res_temp_unavailable),
|
|
||||||
stop=tenacity.stop_after_attempt(CONF.disk_utils.image_convert_attempts),
|
|
||||||
reraise=True)
|
|
||||||
def convert_image(source, dest, out_format, run_as_root=False, cache=None,
|
|
||||||
out_of_order=False, sparse_size=None):
|
|
||||||
"""Convert image to other format."""
|
|
||||||
cmd = ['qemu-img', 'convert', '-O', out_format]
|
|
||||||
if cache is not None:
|
|
||||||
cmd += ['-t', cache]
|
|
||||||
if sparse_size is not None:
|
|
||||||
cmd += ['-S', sparse_size]
|
|
||||||
if out_of_order:
|
|
||||||
cmd.append('-W')
|
|
||||||
cmd += [source, dest]
|
|
||||||
# NOTE(TheJulia): Statically set the MALLOC_ARENA_MAX to prevent leaking
|
|
||||||
# and the creation of new malloc arenas which will consume the system
|
|
||||||
# memory. If limited to 1, qemu-img consumes ~250 MB of RAM, but when
|
|
||||||
# another thread tries to access a locked section of memory in use with
|
|
||||||
# another thread, then by default a new malloc arena is created,
|
|
||||||
# which essentially balloons the memory requirement of the machine.
|
|
||||||
# Default for qemu-img is 8 * nCPU * ~250MB (based on defaults +
|
|
||||||
# thread/code/process/library overhead. In other words, 64 GB. Limiting
|
|
||||||
# this to 3 keeps the memory utilization in happy cases below the overall
|
|
||||||
# threshold which is in place in case a malicious image is attempted to
|
|
||||||
# be passed through qemu-img.
|
|
||||||
env_vars = {'MALLOC_ARENA_MAX': '3'}
|
|
||||||
try:
|
|
||||||
utils.execute(*cmd, run_as_root=run_as_root,
|
|
||||||
prlimit=_qemu_img_limits(),
|
|
||||||
use_standard_locale=True,
|
|
||||||
env_variables=env_vars)
|
|
||||||
except processutils.ProcessExecutionError as e:
|
|
||||||
if ('Resource temporarily unavailable' in e.stderr
|
|
||||||
or 'Cannot allocate memory' in e.stderr):
|
|
||||||
LOG.debug('Failed to convert image, retrying. Error: %s', e)
|
|
||||||
# Sync disk caches before the next attempt
|
|
||||||
utils.execute('sync')
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
def populate_image(src, dst, conv_flags=None):
|
def populate_image(src, dst, conv_flags=None):
|
||||||
data = qemu_img_info(src)
|
data = qemu_img.image_info(src)
|
||||||
if data.file_format == 'raw':
|
if data.file_format == 'raw':
|
||||||
dd(src, dst, conv_flags=conv_flags)
|
dd(src, dst, conv_flags=conv_flags)
|
||||||
else:
|
else:
|
||||||
convert_image(src, dst, 'raw', True, sparse_size='0')
|
qemu_img.convert_image(src, dst, 'raw', True, sparse_size='0')
|
||||||
|
|
||||||
|
|
||||||
def block_uuid(dev):
|
def block_uuid(dev):
|
||||||
@ -575,7 +497,7 @@ def get_image_mb(image_path, virtual_size=True):
|
|||||||
if not virtual_size:
|
if not virtual_size:
|
||||||
image_byte = os.path.getsize(image_path)
|
image_byte = os.path.getsize(image_path)
|
||||||
else:
|
else:
|
||||||
data = qemu_img_info(image_path)
|
data = qemu_img.image_info(image_path)
|
||||||
image_byte = data.virtual_size
|
image_byte = data.virtual_size
|
||||||
|
|
||||||
# round up size to MB
|
# round up size to MB
|
||||||
|
117
ironic_lib/qemu_img.py
Normal file
117
ironic_lib/qemu_img.py
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
from oslo_concurrency import processutils
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_utils import imageutils
|
||||||
|
from oslo_utils import units
|
||||||
|
import tenacity
|
||||||
|
|
||||||
|
from ironic_lib.common.i18n import _
|
||||||
|
from ironic_lib import utils
|
||||||
|
|
||||||
|
|
||||||
|
opts = [
|
||||||
|
cfg.IntOpt('image_convert_memory_limit',
|
||||||
|
default=2048,
|
||||||
|
help='Memory limit for "qemu-img convert" in MiB. Implemented '
|
||||||
|
'via the address space resource limit.'),
|
||||||
|
cfg.IntOpt('image_convert_attempts',
|
||||||
|
default=3,
|
||||||
|
help='Number of attempts to convert an image.'),
|
||||||
|
]
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
CONF.register_opts(opts, group='disk_utils')
|
||||||
|
|
||||||
|
# Limit the memory address space to 1 GiB when running qemu-img
|
||||||
|
QEMU_IMG_LIMITS = None
|
||||||
|
|
||||||
|
|
||||||
|
def _qemu_img_limits():
|
||||||
|
global QEMU_IMG_LIMITS
|
||||||
|
if QEMU_IMG_LIMITS is None:
|
||||||
|
QEMU_IMG_LIMITS = processutils.ProcessLimits(
|
||||||
|
address_space=CONF.disk_utils.image_convert_memory_limit
|
||||||
|
* units.Mi)
|
||||||
|
return QEMU_IMG_LIMITS
|
||||||
|
|
||||||
|
|
||||||
|
def _retry_on_res_temp_unavailable(exc):
|
||||||
|
if (isinstance(exc, processutils.ProcessExecutionError)
|
||||||
|
and ('Resource temporarily unavailable' in exc.stderr
|
||||||
|
or 'Cannot allocate memory' in exc.stderr)):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def image_info(path):
|
||||||
|
"""Return an object containing the parsed output from qemu-img info."""
|
||||||
|
if not os.path.exists(path):
|
||||||
|
raise FileNotFoundError(_("File %s does not exist") % path)
|
||||||
|
|
||||||
|
out, err = utils.execute('env', 'LC_ALL=C', 'LANG=C',
|
||||||
|
'qemu-img', 'info', path,
|
||||||
|
'--output=json',
|
||||||
|
prlimit=_qemu_img_limits())
|
||||||
|
return imageutils.QemuImgInfo(out, format='json')
|
||||||
|
|
||||||
|
|
||||||
|
@tenacity.retry(
|
||||||
|
retry=tenacity.retry_if_exception(_retry_on_res_temp_unavailable),
|
||||||
|
stop=tenacity.stop_after_attempt(CONF.disk_utils.image_convert_attempts),
|
||||||
|
reraise=True)
|
||||||
|
def convert_image(source, dest, out_format, run_as_root=False, cache=None,
|
||||||
|
out_of_order=False, sparse_size=None):
|
||||||
|
"""Convert image to other format."""
|
||||||
|
cmd = ['qemu-img', 'convert', '-O', out_format]
|
||||||
|
if cache is not None:
|
||||||
|
cmd += ['-t', cache]
|
||||||
|
if sparse_size is not None:
|
||||||
|
cmd += ['-S', sparse_size]
|
||||||
|
if out_of_order:
|
||||||
|
cmd.append('-W')
|
||||||
|
cmd += [source, dest]
|
||||||
|
# NOTE(TheJulia): Statically set the MALLOC_ARENA_MAX to prevent leaking
|
||||||
|
# and the creation of new malloc arenas which will consume the system
|
||||||
|
# memory. If limited to 1, qemu-img consumes ~250 MB of RAM, but when
|
||||||
|
# another thread tries to access a locked section of memory in use with
|
||||||
|
# another thread, then by default a new malloc arena is created,
|
||||||
|
# which essentially balloons the memory requirement of the machine.
|
||||||
|
# Default for qemu-img is 8 * nCPU * ~250MB (based on defaults +
|
||||||
|
# thread/code/process/library overhead. In other words, 64 GB. Limiting
|
||||||
|
# this to 3 keeps the memory utilization in happy cases below the overall
|
||||||
|
# threshold which is in place in case a malicious image is attempted to
|
||||||
|
# be passed through qemu-img.
|
||||||
|
env_vars = {'MALLOC_ARENA_MAX': '3'}
|
||||||
|
try:
|
||||||
|
utils.execute(*cmd, run_as_root=run_as_root,
|
||||||
|
prlimit=_qemu_img_limits(),
|
||||||
|
use_standard_locale=True,
|
||||||
|
env_variables=env_vars)
|
||||||
|
except processutils.ProcessExecutionError as e:
|
||||||
|
if ('Resource temporarily unavailable' in e.stderr
|
||||||
|
or 'Cannot allocate memory' in e.stderr):
|
||||||
|
LOG.debug('Failed to convert image, retrying. Error: %s', e)
|
||||||
|
# Sync disk caches before the next attempt
|
||||||
|
utils.execute('sync')
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def list_opts():
|
||||||
|
"""Entry point for oslo-config-generator."""
|
||||||
|
return [('disk_utils', opts)]
|
@ -19,10 +19,10 @@ from unittest import mock
|
|||||||
|
|
||||||
from oslo_concurrency import processutils
|
from oslo_concurrency import processutils
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_utils import imageutils
|
|
||||||
|
|
||||||
from ironic_lib import disk_utils
|
from ironic_lib import disk_utils
|
||||||
from ironic_lib import exception
|
from ironic_lib import exception
|
||||||
|
from ironic_lib import qemu_img
|
||||||
from ironic_lib.tests import base
|
from ironic_lib.tests import base
|
||||||
from ironic_lib import utils
|
from ironic_lib import utils
|
||||||
|
|
||||||
@ -545,8 +545,8 @@ class GetDeviceBlockSizeTestCase(base.IronicLibTestCase):
|
|||||||
|
|
||||||
|
|
||||||
@mock.patch.object(disk_utils, 'dd', autospec=True)
|
@mock.patch.object(disk_utils, 'dd', autospec=True)
|
||||||
@mock.patch.object(disk_utils, 'qemu_img_info', autospec=True)
|
@mock.patch.object(qemu_img, 'image_info', autospec=True)
|
||||||
@mock.patch.object(disk_utils, 'convert_image', autospec=True)
|
@mock.patch.object(qemu_img, 'convert_image', autospec=True)
|
||||||
class PopulateImageTestCase(base.IronicLibTestCase):
|
class PopulateImageTestCase(base.IronicLibTestCase):
|
||||||
|
|
||||||
def test_populate_raw_image(self, mock_cg, mock_qinfo, mock_dd):
|
def test_populate_raw_image(self, mock_cg, mock_qinfo, mock_dd):
|
||||||
@ -603,147 +603,8 @@ class OtherFunctionTestCase(base.IronicLibTestCase):
|
|||||||
disk_utils.is_block_device, device)
|
disk_utils.is_block_device, device)
|
||||||
mock_os.assert_has_calls([mock.call(device)] * 2)
|
mock_os.assert_has_calls([mock.call(device)] * 2)
|
||||||
|
|
||||||
@mock.patch.object(os.path, 'exists', return_value=False, autospec=True)
|
|
||||||
def test_qemu_img_info_path_doesnt_exist(self, path_exists_mock):
|
|
||||||
self.assertRaises(FileNotFoundError, disk_utils.qemu_img_info, 'noimg')
|
|
||||||
path_exists_mock.assert_called_once_with('noimg')
|
|
||||||
|
|
||||||
@mock.patch.object(utils, 'execute', return_value=('out', 'err'),
|
|
||||||
autospec=True)
|
|
||||||
@mock.patch.object(imageutils, 'QemuImgInfo', autospec=True)
|
|
||||||
@mock.patch.object(os.path, 'exists', return_value=True, autospec=True)
|
|
||||||
def test_qemu_img_info_path_exists(self, path_exists_mock,
|
|
||||||
qemu_img_info_mock, execute_mock):
|
|
||||||
disk_utils.qemu_img_info('img')
|
|
||||||
path_exists_mock.assert_called_once_with('img')
|
|
||||||
execute_mock.assert_called_once_with('env', 'LC_ALL=C', 'LANG=C',
|
|
||||||
'qemu-img', 'info', 'img',
|
|
||||||
'--output=json',
|
|
||||||
prlimit=mock.ANY)
|
|
||||||
qemu_img_info_mock.assert_called_once_with('out', format='json')
|
|
||||||
|
|
||||||
@mock.patch.object(utils, 'execute', autospec=True)
|
|
||||||
def test_convert_image(self, execute_mock):
|
|
||||||
disk_utils.convert_image('source', 'dest', 'out_format')
|
|
||||||
execute_mock.assert_called_once_with(
|
|
||||||
'qemu-img', 'convert', '-O',
|
|
||||||
'out_format', 'source', 'dest',
|
|
||||||
run_as_root=False,
|
|
||||||
prlimit=mock.ANY,
|
|
||||||
use_standard_locale=True,
|
|
||||||
env_variables={'MALLOC_ARENA_MAX': '3'})
|
|
||||||
|
|
||||||
@mock.patch.object(utils, 'execute', autospec=True)
|
|
||||||
def test_convert_image_flags(self, execute_mock):
|
|
||||||
disk_utils.convert_image('source', 'dest', 'out_format',
|
|
||||||
cache='directsync', out_of_order=True,
|
|
||||||
sparse_size='0')
|
|
||||||
execute_mock.assert_called_once_with(
|
|
||||||
'qemu-img', 'convert', '-O',
|
|
||||||
'out_format', '-t', 'directsync',
|
|
||||||
'-S', '0', '-W', 'source', 'dest',
|
|
||||||
run_as_root=False,
|
|
||||||
prlimit=mock.ANY,
|
|
||||||
use_standard_locale=True,
|
|
||||||
env_variables={'MALLOC_ARENA_MAX': '3'})
|
|
||||||
|
|
||||||
@mock.patch.object(utils, 'execute', autospec=True)
|
|
||||||
def test_convert_image_retries(self, execute_mock):
|
|
||||||
ret_err = 'qemu: qemu_thread_create: Resource temporarily unavailable'
|
|
||||||
execute_mock.side_effect = [
|
|
||||||
processutils.ProcessExecutionError(stderr=ret_err), ('', ''),
|
|
||||||
processutils.ProcessExecutionError(stderr=ret_err), ('', ''),
|
|
||||||
('', ''),
|
|
||||||
]
|
|
||||||
|
|
||||||
disk_utils.convert_image('source', 'dest', 'out_format')
|
|
||||||
convert_call = mock.call('qemu-img', 'convert', '-O',
|
|
||||||
'out_format', 'source', 'dest',
|
|
||||||
run_as_root=False,
|
|
||||||
prlimit=mock.ANY,
|
|
||||||
use_standard_locale=True,
|
|
||||||
env_variables={'MALLOC_ARENA_MAX': '3'})
|
|
||||||
execute_mock.assert_has_calls([
|
|
||||||
convert_call,
|
|
||||||
mock.call('sync'),
|
|
||||||
convert_call,
|
|
||||||
mock.call('sync'),
|
|
||||||
convert_call,
|
|
||||||
])
|
|
||||||
|
|
||||||
@mock.patch.object(utils, 'execute', autospec=True)
|
|
||||||
def test_convert_image_retries_alternate_error(self, execute_mock):
|
|
||||||
ret_err = 'Failed to allocate memory: Cannot allocate memory\n'
|
|
||||||
execute_mock.side_effect = [
|
|
||||||
processutils.ProcessExecutionError(stderr=ret_err), ('', ''),
|
|
||||||
processutils.ProcessExecutionError(stderr=ret_err), ('', ''),
|
|
||||||
('', ''),
|
|
||||||
]
|
|
||||||
|
|
||||||
disk_utils.convert_image('source', 'dest', 'out_format')
|
|
||||||
convert_call = mock.call('qemu-img', 'convert', '-O',
|
|
||||||
'out_format', 'source', 'dest',
|
|
||||||
run_as_root=False,
|
|
||||||
prlimit=mock.ANY,
|
|
||||||
use_standard_locale=True,
|
|
||||||
env_variables={'MALLOC_ARENA_MAX': '3'})
|
|
||||||
execute_mock.assert_has_calls([
|
|
||||||
convert_call,
|
|
||||||
mock.call('sync'),
|
|
||||||
convert_call,
|
|
||||||
mock.call('sync'),
|
|
||||||
convert_call,
|
|
||||||
])
|
|
||||||
|
|
||||||
@mock.patch.object(utils, 'execute', autospec=True)
|
|
||||||
def test_convert_image_retries_and_fails(self, execute_mock):
|
|
||||||
ret_err = 'qemu: qemu_thread_create: Resource temporarily unavailable'
|
|
||||||
execute_mock.side_effect = [
|
|
||||||
processutils.ProcessExecutionError(stderr=ret_err), ('', ''),
|
|
||||||
processutils.ProcessExecutionError(stderr=ret_err), ('', ''),
|
|
||||||
processutils.ProcessExecutionError(stderr=ret_err), ('', ''),
|
|
||||||
processutils.ProcessExecutionError(stderr=ret_err),
|
|
||||||
]
|
|
||||||
|
|
||||||
self.assertRaises(processutils.ProcessExecutionError,
|
|
||||||
disk_utils.convert_image,
|
|
||||||
'source', 'dest', 'out_format')
|
|
||||||
convert_call = mock.call('qemu-img', 'convert', '-O',
|
|
||||||
'out_format', 'source', 'dest',
|
|
||||||
run_as_root=False,
|
|
||||||
prlimit=mock.ANY,
|
|
||||||
use_standard_locale=True,
|
|
||||||
env_variables={'MALLOC_ARENA_MAX': '3'})
|
|
||||||
execute_mock.assert_has_calls([
|
|
||||||
convert_call,
|
|
||||||
mock.call('sync'),
|
|
||||||
convert_call,
|
|
||||||
mock.call('sync'),
|
|
||||||
convert_call,
|
|
||||||
])
|
|
||||||
|
|
||||||
@mock.patch.object(utils, 'execute', autospec=True)
|
|
||||||
def test_convert_image_just_fails(self, execute_mock):
|
|
||||||
ret_err = 'Aliens'
|
|
||||||
execute_mock.side_effect = [
|
|
||||||
processutils.ProcessExecutionError(stderr=ret_err),
|
|
||||||
]
|
|
||||||
|
|
||||||
self.assertRaises(processutils.ProcessExecutionError,
|
|
||||||
disk_utils.convert_image,
|
|
||||||
'source', 'dest', 'out_format')
|
|
||||||
convert_call = mock.call('qemu-img', 'convert', '-O',
|
|
||||||
'out_format', 'source', 'dest',
|
|
||||||
run_as_root=False,
|
|
||||||
prlimit=mock.ANY,
|
|
||||||
use_standard_locale=True,
|
|
||||||
env_variables={'MALLOC_ARENA_MAX': '3'})
|
|
||||||
execute_mock.assert_has_calls([
|
|
||||||
convert_call,
|
|
||||||
])
|
|
||||||
|
|
||||||
@mock.patch.object(os.path, 'getsize', autospec=True)
|
@mock.patch.object(os.path, 'getsize', autospec=True)
|
||||||
@mock.patch.object(disk_utils, 'qemu_img_info', autospec=True)
|
@mock.patch.object(qemu_img, 'image_info', autospec=True)
|
||||||
def test_get_image_mb(self, mock_qinfo, mock_getsize):
|
def test_get_image_mb(self, mock_qinfo, mock_getsize):
|
||||||
mb = 1024 * 1024
|
mb = 1024 * 1024
|
||||||
|
|
||||||
|
169
ironic_lib/tests/test_qemu_img.py
Normal file
169
ironic_lib/tests/test_qemu_img.py
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
import os
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from oslo_concurrency import processutils
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_utils import imageutils
|
||||||
|
|
||||||
|
from ironic_lib import qemu_img
|
||||||
|
from ironic_lib.tests import base
|
||||||
|
from ironic_lib import utils
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
class ImageInfoTestCase(base.IronicLibTestCase):
|
||||||
|
|
||||||
|
@mock.patch.object(os.path, 'exists', return_value=False, autospec=True)
|
||||||
|
def test_image_info_path_doesnt_exist(self, path_exists_mock):
|
||||||
|
self.assertRaises(FileNotFoundError, qemu_img.image_info, 'noimg')
|
||||||
|
path_exists_mock.assert_called_once_with('noimg')
|
||||||
|
|
||||||
|
@mock.patch.object(utils, 'execute', return_value=('out', 'err'),
|
||||||
|
autospec=True)
|
||||||
|
@mock.patch.object(imageutils, 'QemuImgInfo', autospec=True)
|
||||||
|
@mock.patch.object(os.path, 'exists', return_value=True, autospec=True)
|
||||||
|
def test_image_info_path_exists(self, path_exists_mock,
|
||||||
|
image_info_mock, execute_mock):
|
||||||
|
qemu_img.image_info('img')
|
||||||
|
path_exists_mock.assert_called_once_with('img')
|
||||||
|
execute_mock.assert_called_once_with('env', 'LC_ALL=C', 'LANG=C',
|
||||||
|
'qemu-img', 'info', 'img',
|
||||||
|
'--output=json',
|
||||||
|
prlimit=mock.ANY)
|
||||||
|
image_info_mock.assert_called_once_with('out', format='json')
|
||||||
|
|
||||||
|
|
||||||
|
class ConvertImageTestCase(base.IronicLibTestCase):
|
||||||
|
|
||||||
|
@mock.patch.object(utils, 'execute', autospec=True)
|
||||||
|
def test_convert_image(self, execute_mock):
|
||||||
|
qemu_img.convert_image('source', 'dest', 'out_format')
|
||||||
|
execute_mock.assert_called_once_with(
|
||||||
|
'qemu-img', 'convert', '-O',
|
||||||
|
'out_format', 'source', 'dest',
|
||||||
|
run_as_root=False,
|
||||||
|
prlimit=mock.ANY,
|
||||||
|
use_standard_locale=True,
|
||||||
|
env_variables={'MALLOC_ARENA_MAX': '3'})
|
||||||
|
|
||||||
|
@mock.patch.object(utils, 'execute', autospec=True)
|
||||||
|
def test_convert_image_flags(self, execute_mock):
|
||||||
|
qemu_img.convert_image('source', 'dest', 'out_format',
|
||||||
|
cache='directsync', out_of_order=True,
|
||||||
|
sparse_size='0')
|
||||||
|
execute_mock.assert_called_once_with(
|
||||||
|
'qemu-img', 'convert', '-O',
|
||||||
|
'out_format', '-t', 'directsync',
|
||||||
|
'-S', '0', '-W', 'source', 'dest',
|
||||||
|
run_as_root=False,
|
||||||
|
prlimit=mock.ANY,
|
||||||
|
use_standard_locale=True,
|
||||||
|
env_variables={'MALLOC_ARENA_MAX': '3'})
|
||||||
|
|
||||||
|
@mock.patch.object(utils, 'execute', autospec=True)
|
||||||
|
def test_convert_image_retries(self, execute_mock):
|
||||||
|
ret_err = 'qemu: qemu_thread_create: Resource temporarily unavailable'
|
||||||
|
execute_mock.side_effect = [
|
||||||
|
processutils.ProcessExecutionError(stderr=ret_err), ('', ''),
|
||||||
|
processutils.ProcessExecutionError(stderr=ret_err), ('', ''),
|
||||||
|
('', ''),
|
||||||
|
]
|
||||||
|
|
||||||
|
qemu_img.convert_image('source', 'dest', 'out_format')
|
||||||
|
convert_call = mock.call('qemu-img', 'convert', '-O',
|
||||||
|
'out_format', 'source', 'dest',
|
||||||
|
run_as_root=False,
|
||||||
|
prlimit=mock.ANY,
|
||||||
|
use_standard_locale=True,
|
||||||
|
env_variables={'MALLOC_ARENA_MAX': '3'})
|
||||||
|
execute_mock.assert_has_calls([
|
||||||
|
convert_call,
|
||||||
|
mock.call('sync'),
|
||||||
|
convert_call,
|
||||||
|
mock.call('sync'),
|
||||||
|
convert_call,
|
||||||
|
])
|
||||||
|
|
||||||
|
@mock.patch.object(utils, 'execute', autospec=True)
|
||||||
|
def test_convert_image_retries_alternate_error(self, execute_mock):
|
||||||
|
ret_err = 'Failed to allocate memory: Cannot allocate memory\n'
|
||||||
|
execute_mock.side_effect = [
|
||||||
|
processutils.ProcessExecutionError(stderr=ret_err), ('', ''),
|
||||||
|
processutils.ProcessExecutionError(stderr=ret_err), ('', ''),
|
||||||
|
('', ''),
|
||||||
|
]
|
||||||
|
|
||||||
|
qemu_img.convert_image('source', 'dest', 'out_format')
|
||||||
|
convert_call = mock.call('qemu-img', 'convert', '-O',
|
||||||
|
'out_format', 'source', 'dest',
|
||||||
|
run_as_root=False,
|
||||||
|
prlimit=mock.ANY,
|
||||||
|
use_standard_locale=True,
|
||||||
|
env_variables={'MALLOC_ARENA_MAX': '3'})
|
||||||
|
execute_mock.assert_has_calls([
|
||||||
|
convert_call,
|
||||||
|
mock.call('sync'),
|
||||||
|
convert_call,
|
||||||
|
mock.call('sync'),
|
||||||
|
convert_call,
|
||||||
|
])
|
||||||
|
|
||||||
|
@mock.patch.object(utils, 'execute', autospec=True)
|
||||||
|
def test_convert_image_retries_and_fails(self, execute_mock):
|
||||||
|
ret_err = 'qemu: qemu_thread_create: Resource temporarily unavailable'
|
||||||
|
execute_mock.side_effect = [
|
||||||
|
processutils.ProcessExecutionError(stderr=ret_err), ('', ''),
|
||||||
|
processutils.ProcessExecutionError(stderr=ret_err), ('', ''),
|
||||||
|
processutils.ProcessExecutionError(stderr=ret_err), ('', ''),
|
||||||
|
processutils.ProcessExecutionError(stderr=ret_err),
|
||||||
|
]
|
||||||
|
|
||||||
|
self.assertRaises(processutils.ProcessExecutionError,
|
||||||
|
qemu_img.convert_image,
|
||||||
|
'source', 'dest', 'out_format')
|
||||||
|
convert_call = mock.call('qemu-img', 'convert', '-O',
|
||||||
|
'out_format', 'source', 'dest',
|
||||||
|
run_as_root=False,
|
||||||
|
prlimit=mock.ANY,
|
||||||
|
use_standard_locale=True,
|
||||||
|
env_variables={'MALLOC_ARENA_MAX': '3'})
|
||||||
|
execute_mock.assert_has_calls([
|
||||||
|
convert_call,
|
||||||
|
mock.call('sync'),
|
||||||
|
convert_call,
|
||||||
|
mock.call('sync'),
|
||||||
|
convert_call,
|
||||||
|
])
|
||||||
|
|
||||||
|
@mock.patch.object(utils, 'execute', autospec=True)
|
||||||
|
def test_convert_image_just_fails(self, execute_mock):
|
||||||
|
ret_err = 'Aliens'
|
||||||
|
execute_mock.side_effect = [
|
||||||
|
processutils.ProcessExecutionError(stderr=ret_err),
|
||||||
|
]
|
||||||
|
|
||||||
|
self.assertRaises(processutils.ProcessExecutionError,
|
||||||
|
qemu_img.convert_image,
|
||||||
|
'source', 'dest', 'out_format')
|
||||||
|
convert_call = mock.call('qemu-img', 'convert', '-O',
|
||||||
|
'out_format', 'source', 'dest',
|
||||||
|
run_as_root=False,
|
||||||
|
prlimit=mock.ANY,
|
||||||
|
use_standard_locale=True,
|
||||||
|
env_variables={'MALLOC_ARENA_MAX': '3'})
|
||||||
|
execute_mock.assert_has_calls([
|
||||||
|
convert_call,
|
||||||
|
])
|
@ -37,6 +37,7 @@ oslo.config.opts =
|
|||||||
ironic_lib.mdns = ironic_lib.mdns:list_opts
|
ironic_lib.mdns = ironic_lib.mdns:list_opts
|
||||||
ironic_lib.metrics = ironic_lib.metrics_utils:list_opts
|
ironic_lib.metrics = ironic_lib.metrics_utils:list_opts
|
||||||
ironic_lib.metrics_statsd = ironic_lib.metrics_statsd:list_opts
|
ironic_lib.metrics_statsd = ironic_lib.metrics_statsd:list_opts
|
||||||
|
ironic_lib.qemu_img = ironic_lib.qemu_img:list_opts
|
||||||
ironic_lib.utils = ironic_lib.utils:list_opts
|
ironic_lib.utils = ironic_lib.utils:list_opts
|
||||||
|
|
||||||
[extra]
|
[extra]
|
||||||
|
Loading…
Reference in New Issue
Block a user