Simplify injecting network data into an ISO image

Currently we're building a VFAT image with the network data just
to unpack it back on the next step. Just pass the file directly.
This fixes a permission denied problem on Bifrost on Fedora
(at least).

As a nice side effect, the change reduces the amount of IO done
for virtual media quite substantially.

Change-Id: I5499fa42c1d82a1a29099fbbba6f45d440448b72
This commit is contained in:
Dmitry Tantsur 2020-10-12 16:50:29 +02:00
parent 3fd513ee1c
commit d48479b52d
5 changed files with 132 additions and 244 deletions

View File

@ -19,7 +19,6 @@
Handling of VM disk images. Handling of VM disk images.
""" """
import contextlib
import os import os
import shutil import shutil
@ -48,9 +47,11 @@ def _create_root_fs(root_directory, files_info):
:param root_directory: the filesystem root directory. :param root_directory: the filesystem root directory.
:param files_info: A dict containing absolute path of file to be copied :param files_info: A dict containing absolute path of file to be copied
-> relative path within the vfat image. For example:: or its content as bytes -> relative path within
the vfat image. For example::
{ {
'/absolute/path/to/file' -> 'relative/path/within/root' '/absolute/path/to/file': 'relative/path/within/root',
b'{"some": "json"}': 'another/relative/path'
... ...
} }
:raises: OSError, if creation of any directory failed. :raises: OSError, if creation of any directory failed.
@ -59,10 +60,14 @@ def _create_root_fs(root_directory, files_info):
for src_file, path in files_info.items(): for src_file, path in files_info.items():
target_file = os.path.join(root_directory, path) target_file = os.path.join(root_directory, path)
dirname = os.path.dirname(target_file) dirname = os.path.dirname(target_file)
if not os.path.exists(dirname): if dirname:
os.makedirs(dirname) os.makedirs(dirname, exist_ok=True)
shutil.copyfile(src_file, target_file) if isinstance(src_file, bytes):
with open(target_file, 'wb') as fp:
fp.write(src_file)
else:
shutil.copyfile(src_file, target_file)
def _umount_without_raise(mount_dir): def _umount_without_raise(mount_dir):
@ -155,67 +160,19 @@ def _generate_cfg(kernel_params, template, options):
return utils.render_template(template, options) return utils.render_template(template, options)
def _read_dir(root_dir, prefix_dir=None): def _label(files_info):
"""Gather files under given directory. """Get a suitable label for the files.
:param root_dir: a directory to traverse. Returns "config-2" if the openstack metadata is present.
:returns: a dict mapping absolute paths to relative to the `root_dir`.
""" """
files_info = {} if any(x.startswith('openstack/') for x in files_info.values()):
return 'config-2'
if not prefix_dir: else:
prefix_dir = root_dir return 'VMEDIA_BOOT_ISO'
for entry in os.listdir(root_dir):
path = os.path.join(root_dir, entry)
if os.path.isdir(path):
files_info.update(_read_dir(path, prefix_dir))
else:
files_info[path] = path[len(prefix_dir) + 1:]
return files_info
@contextlib.contextmanager
def _collect_files(image_path):
"""Mount image and return a dictionary of paths found there.
Mounts given image under a temporary directory, walk its contents
and produce a dictionary of absolute->relative paths found on the
image.
:param image_path: ISO9660 or FAT-formatted image to mount.
:raises: ImageCreationFailed, if image inspection failed.
:returns: a dict mapping absolute paths to relative to the mount point.
"""
if not image_path:
yield {}
return
with utils.tempdir() as mount_dir:
try:
utils.mount(image_path, mount_dir, '-o', 'loop')
except processutils.ProcessExecutionError as e:
LOG.exception("Mounting filesystem image %(image)s "
"failed", {'image': image_path})
raise exception.ImageCreationFailed(image_type='iso', error=e)
try:
yield _read_dir(mount_dir)
except EnvironmentError as e:
LOG.exception(
"Examining image %(images)s failed: ", {'image': image_path})
_umount_without_raise(mount_dir)
raise exception.ImageCreationFailed(image_type='iso', error=e)
_umount_without_raise(mount_dir)
def create_isolinux_image_for_bios( def create_isolinux_image_for_bios(
output_file, kernel, ramdisk, kernel_params=None, configdrive=None): output_file, kernel, ramdisk, kernel_params=None, inject_files=None):
"""Creates an isolinux image on the specified file. """Creates an isolinux image on the specified file.
Copies the provided kernel, ramdisk to a directory, generates the isolinux Copies the provided kernel, ramdisk to a directory, generates the isolinux
@ -229,8 +186,8 @@ def create_isolinux_image_for_bios(
:param kernel_params: a list of strings(each element being a string like :param kernel_params: a list of strings(each element being a string like
'K=V' or 'K' or combination of them like 'K1=V1,K2,...') to be added 'K=V' or 'K' or combination of them like 'K1=V1,K2,...') to be added
as the kernel cmdline. as the kernel cmdline.
:param configdrive: ISO9660 or FAT-formatted OpenStack config drive :param inject_files: Mapping of local source file paths to their location
image. This image will be written onto the built ISO image. Optional. on the final ISO image.
:raises: ImageCreationFailed, if image creation failed while copying files :raises: ImageCreationFailed, if image creation failed while copying files
or while running command to generate iso. or while running command to generate iso.
""" """
@ -248,6 +205,8 @@ def create_isolinux_image_for_bios(
ramdisk: 'initrd', ramdisk: 'initrd',
CONF.isolinux_bin: ISOLINUX_BIN, CONF.isolinux_bin: ISOLINUX_BIN,
} }
if inject_files:
files_info.update(inject_files)
# ldlinux.c32 is required for syslinux 5.0 or later. # ldlinux.c32 is required for syslinux 5.0 or later.
if CONF.ldlinux_c32: if CONF.ldlinux_c32:
@ -262,15 +221,12 @@ def create_isolinux_image_for_bios(
if ldlinux_src: if ldlinux_src:
files_info[ldlinux_src] = LDLINUX_BIN files_info[ldlinux_src] = LDLINUX_BIN
with _collect_files(configdrive) as cfgdrv_files: try:
files_info.update(cfgdrv_files) _create_root_fs(tmpdir, files_info)
try: except EnvironmentError as e:
_create_root_fs(tmpdir, files_info) LOG.exception("Creating the filesystem root failed.")
raise exception.ImageCreationFailed(image_type='iso', error=e)
except EnvironmentError as e:
LOG.exception("Creating the filesystem root failed.")
raise exception.ImageCreationFailed(image_type='iso', error=e)
cfg = _generate_cfg(kernel_params, cfg = _generate_cfg(kernel_params,
CONF.isolinux_config_template, options) CONF.isolinux_config_template, options)
@ -279,8 +235,7 @@ def create_isolinux_image_for_bios(
utils.write_to_file(isolinux_cfg, cfg) utils.write_to_file(isolinux_cfg, cfg)
try: try:
utils.execute('mkisofs', '-r', '-V', utils.execute('mkisofs', '-r', '-V', _label(files_info),
'config-2' if configdrive else 'VMEDIA_BOOT_ISO',
'-cache-inodes', '-J', '-l', '-no-emul-boot', '-cache-inodes', '-J', '-l', '-no-emul-boot',
'-boot-load-size', '4', '-boot-info-table', '-boot-load-size', '4', '-boot-info-table',
'-b', ISOLINUX_BIN, '-o', output_file, tmpdir) '-b', ISOLINUX_BIN, '-o', output_file, tmpdir)
@ -291,7 +246,7 @@ def create_isolinux_image_for_bios(
def create_esp_image_for_uefi( def create_esp_image_for_uefi(
output_file, kernel, ramdisk, deploy_iso=None, esp_image=None, output_file, kernel, ramdisk, deploy_iso=None, esp_image=None,
kernel_params=None, configdrive=None): kernel_params=None, inject_files=None):
"""Creates an ESP image on the specified file. """Creates an ESP image on the specified file.
Copies the provided kernel, ramdisk and EFI system partition image (ESP) to Copies the provided kernel, ramdisk and EFI system partition image (ESP) to
@ -311,8 +266,8 @@ def create_esp_image_for_uefi(
:param kernel_params: a list of strings(each element being a string like :param kernel_params: a list of strings(each element being a string like
'K=V' or 'K' or combination of them like 'K1=V1,K2,...') to be added 'K=V' or 'K' or combination of them like 'K1=V1,K2,...') to be added
as the kernel cmdline. as the kernel cmdline.
:param configdrive: ISO9660 or FAT-formatted OpenStack config drive :param inject_files: Mapping of local source file paths to their location
image. This image will be written onto the built ISO image. Optional. on the final ISO image.
:raises: ImageCreationFailed, if image creation failed while copying files :raises: ImageCreationFailed, if image creation failed while copying files
or while running command to generate iso. or while running command to generate iso.
""" """
@ -325,6 +280,8 @@ def create_esp_image_for_uefi(
kernel: 'vmlinuz', kernel: 'vmlinuz',
ramdisk: 'initrd', ramdisk: 'initrd',
} }
if inject_files:
files_info.update(inject_files)
with utils.tempdir() as mountdir: with utils.tempdir() as mountdir:
# Open the deploy iso used to initiate deploy and copy the # Open the deploy iso used to initiate deploy and copy the
@ -359,20 +316,17 @@ def create_esp_image_for_uefi(
files_info.update(uefi_path_info) files_info.update(uefi_path_info)
with _collect_files(configdrive) as cfgdrv_files: try:
files_info.update(cfgdrv_files) _create_root_fs(tmpdir, files_info)
try: except EnvironmentError as e:
_create_root_fs(tmpdir, files_info) LOG.exception("Creating the filesystem root failed.")
raise exception.ImageCreationFailed(
image_type='iso', error=e)
except EnvironmentError as e: finally:
LOG.exception("Creating the filesystem root failed.") if deploy_iso:
raise exception.ImageCreationFailed( _umount_without_raise(mountdir)
image_type='iso', error=e)
finally:
if deploy_iso:
_umount_without_raise(mountdir)
# Generate and copy grub config file. # Generate and copy grub config file.
grub_conf = _generate_cfg(kernel_params, grub_conf = _generate_cfg(kernel_params,
@ -381,8 +335,7 @@ def create_esp_image_for_uefi(
# Create the boot_iso. # Create the boot_iso.
try: try:
utils.execute('mkisofs', '-r', '-V', utils.execute('mkisofs', '-r', '-V', _label(files_info),
'config-2' if configdrive else 'VMEDIA_BOOT_ISO',
'-l', '-e', e_img_rel_path, '-no-emul-boot', '-l', '-e', e_img_rel_path, '-no-emul-boot',
'-o', output_file, tmpdir) '-o', output_file, tmpdir)
@ -391,10 +344,6 @@ def create_esp_image_for_uefi(
raise exception.ImageCreationFailed(image_type='iso', error=e) raise exception.ImageCreationFailed(image_type='iso', error=e)
# NOTE(etingof): backward compatibility
create_isolinux_image_for_uefi = create_esp_image_for_uefi
def fetch(context, image_href, path, force_raw=False): def fetch(context, image_href, path, force_raw=False):
# TODO(vish): Improve context handling and add owner and auth data # TODO(vish): Improve context handling and add owner and auth data
# when it is added to glance. Right now there is no # when it is added to glance. Right now there is no
@ -526,7 +475,7 @@ def get_temp_url_for_glance_image(context, image_uuid):
def create_boot_iso(context, output_filename, kernel_href, def create_boot_iso(context, output_filename, kernel_href,
ramdisk_href, deploy_iso_href=None, esp_image_href=None, ramdisk_href, deploy_iso_href=None, esp_image_href=None,
root_uuid=None, kernel_params=None, boot_mode=None, root_uuid=None, kernel_params=None, boot_mode=None,
configdrive_href=None, base_iso=None): base_iso=None, inject_files=None):
"""Creates a bootable ISO image for a node. """Creates a bootable ISO image for a node.
Given the hrefs for kernel, ramdisk, root partition's UUID and Given the hrefs for kernel, ramdisk, root partition's UUID and
@ -550,12 +499,11 @@ def create_boot_iso(context, output_filename, kernel_href,
:param kernel_params: a string containing whitespace separated values :param kernel_params: a string containing whitespace separated values
kernel cmdline arguments of the form K=V or K (optional). kernel cmdline arguments of the form K=V or K (optional).
:boot_mode: the boot mode in which the deploy is to happen. :boot_mode: the boot mode in which the deploy is to happen.
:param configdrive_href: URL to ISO9660 or FAT-formatted OpenStack config
drive image. This image will be embedded into the built ISO image.
Optional.
:param base_iso: URL or glance UUID of a to be used as an override of :param base_iso: URL or glance UUID of a to be used as an override of
what should be retrieved for to use, instead of building an ISO what should be retrieved for to use, instead of building an ISO
bootable ramdisk. bootable ramdisk.
:param inject_files: Mapping of local source file paths to their location
on the final ISO image.
:raises: ImageCreationFailed, if creating boot ISO failed. :raises: ImageCreationFailed, if creating boot ISO failed.
""" """
with utils.tempdir() as tmpdir: with utils.tempdir() as tmpdir:
@ -574,14 +522,6 @@ def create_boot_iso(context, output_filename, kernel_href,
fetch(context, kernel_href, kernel_path) fetch(context, kernel_href, kernel_path)
fetch(context, ramdisk_href, ramdisk_path) fetch(context, ramdisk_href, ramdisk_path)
if configdrive_href:
configdrive_path = os.path.join(
tmpdir, configdrive_href.split('/')[-1])
fetch(context, configdrive_href, configdrive_path)
else:
configdrive_path = None
params = [] params = []
if root_uuid: if root_uuid:
params.append('root=UUID=%s' % root_uuid) params.append('root=UUID=%s' % root_uuid)
@ -612,12 +552,12 @@ def create_boot_iso(context, output_filename, kernel_href,
create_esp_image_for_uefi( create_esp_image_for_uefi(
output_filename, kernel_path, ramdisk_path, output_filename, kernel_path, ramdisk_path,
deploy_iso=deploy_iso_path, esp_image=esp_image_path, deploy_iso=deploy_iso_path, esp_image=esp_image_path,
kernel_params=params, configdrive=configdrive_path) kernel_params=params, inject_files=inject_files)
else: else:
create_isolinux_image_for_bios( create_isolinux_image_for_bios(
output_filename, kernel_path, ramdisk_path, output_filename, kernel_path, ramdisk_path,
kernel_params=params, configdrive=configdrive_path) kernel_params=params, inject_files=inject_files)
def is_whole_disk_image(ctx, instance_info): def is_whole_disk_image(ctx, instance_info):

View File

@ -22,7 +22,6 @@ from urllib import parse as urlparse
from ironic_lib import utils as ironic_utils from ironic_lib import utils as ironic_utils
from oslo_log import log from oslo_log import log
from oslo_serialization import base64
from ironic.common import exception from ironic.common import exception
from ironic.common.i18n import _ from ironic.common.i18n import _
@ -288,28 +287,24 @@ def cleanup_floppy_image(task):
def _prepare_iso_image(task, kernel_href, ramdisk_href, def _prepare_iso_image(task, kernel_href, ramdisk_href,
bootloader_href=None, configdrive=None, bootloader_href=None, root_uuid=None, params=None,
root_uuid=None, params=None, base_iso=None): base_iso=None, inject_files=None):
"""Prepare an ISO to boot the node. """Prepare an ISO to boot the node.
Build bootable ISO out of `kernel_href` and `ramdisk_href` (and Build bootable ISO out of `kernel_href` and `ramdisk_href` (and
`bootloader` if it's UEFI boot), then push built image up to Swift and `bootloader` if it's UEFI boot), then push built image up to Swift and
return a temporary URL. return a temporary URL.
If `configdrive` is specified it will be eventually written onto
the boot ISO image.
:param task: a TaskManager instance containing the node to act on. :param task: a TaskManager instance containing the node to act on.
:param kernel_href: URL or Glance UUID of the kernel to use :param kernel_href: URL or Glance UUID of the kernel to use
:param ramdisk_href: URL or Glance UUID of the ramdisk to use :param ramdisk_href: URL or Glance UUID of the ramdisk to use
:param bootloader_href: URL or Glance UUID of the EFI bootloader :param bootloader_href: URL or Glance UUID of the EFI bootloader
image to use when creating UEFI bootbable ISO image to use when creating UEFI bootbable ISO
:param configdrive: URL to or a compressed blob of a ISO9660 or
FAT-formatted OpenStack config drive image. This image will be
written onto the built ISO image. Optional.
:param root_uuid: optional uuid of the root partition. :param root_uuid: optional uuid of the root partition.
:param params: a dictionary containing 'parameter name'->'value' :param params: a dictionary containing 'parameter name'->'value'
mapping to be passed to kernel command line. mapping to be passed to kernel command line.
:param inject_files: Mapping of local source file paths to their location
on the final ISO image.
:returns: bootable ISO HTTP URL. :returns: bootable ISO HTTP URL.
:raises: MissingParameterValue, if any of the required parameters are :raises: MissingParameterValue, if any of the required parameters are
missing. missing.
@ -360,44 +355,21 @@ def _prepare_iso_image(task, kernel_href, ramdisk_href,
with tempfile.NamedTemporaryFile( with tempfile.NamedTemporaryFile(
dir=CONF.tempdir, suffix='.iso') as boot_fileobj: dir=CONF.tempdir, suffix='.iso') as boot_fileobj:
with tempfile.NamedTemporaryFile( boot_iso_tmp_file = boot_fileobj.name
dir=CONF.tempdir, suffix='.img') as cfgdrv_fileobj: images.create_boot_iso(
task.context, boot_iso_tmp_file,
kernel_href, ramdisk_href,
esp_image_href=bootloader_href,
root_uuid=root_uuid,
kernel_params=kernel_params,
boot_mode=boot_mode,
base_iso=base_iso,
inject_files=inject_files)
configdrive_href = configdrive iso_object_name = _get_iso_image_name(task.node)
# FIXME(TheJulia): This is treated as conditional with image_url = img_handler.publish_image(
# a base_iso as the intent, eventually, is to support boot_iso_tmp_file, iso_object_name)
# injection into the supplied image.
if configdrive and not base_iso:
parsed_url = urlparse.urlparse(configdrive)
if not parsed_url.scheme:
cfgdrv_blob = base64.decode_as_bytes(configdrive)
with open(cfgdrv_fileobj.name, 'wb') as f:
f.write(cfgdrv_blob)
configdrive_href = urlparse.urlunparse(
('file', '', cfgdrv_fileobj.name, '', '', ''))
LOG.debug("Built configdrive out of configdrive blob "
"for node %(node)s", {'node': task.node.uuid})
boot_iso_tmp_file = boot_fileobj.name
images.create_boot_iso(
task.context, boot_iso_tmp_file,
kernel_href, ramdisk_href,
esp_image_href=bootloader_href,
configdrive_href=configdrive_href,
root_uuid=root_uuid,
kernel_params=kernel_params,
boot_mode=boot_mode,
base_iso=base_iso)
iso_object_name = _get_iso_image_name(task.node)
image_url = img_handler.publish_image(
boot_iso_tmp_file, iso_object_name)
LOG.debug("Created ISO %(name)s in object store for node %(node)s, " LOG.debug("Created ISO %(name)s in object store for node %(node)s, "
"exposed as temporary URL " "exposed as temporary URL "
@ -426,8 +398,8 @@ def prepare_deploy_iso(task, params, mode, d_info):
and return temporary Swift URL to the image. and return temporary Swift URL to the image.
If network interface supplies network configuration (`network_data`), If network interface supplies network configuration (`network_data`),
a new `configdrive` will be created with `network_data.json` inside, a `network_data.json` will be written into an appropriate location on
and eventually written down onto the boot ISO. the final ISO.
:param task: a TaskManager instance containing the node to act on. :param task: a TaskManager instance containing the node to act on.
:param params: a dictionary containing 'parameter name'->'value' :param params: a dictionary containing 'parameter name'->'value'
@ -457,35 +429,17 @@ def prepare_deploy_iso(task, params, mode, d_info):
_prepare_iso_image, task, kernel_href, ramdisk_href, _prepare_iso_image, task, kernel_href, ramdisk_href,
bootloader_href=bootloader_href, params=params) bootloader_href=bootloader_href, params=params)
inject_files = {}
network_data = task.driver.network.get_node_network_data(task) network_data = task.driver.network.get_node_network_data(task)
if network_data: if network_data:
LOG.debug('Injecting custom network data for node %s', LOG.debug('Injecting custom network data for node %s',
task.node.uuid) task.node.uuid)
with tempfile.NamedTemporaryFile(dir=CONF.tempdir, network_data = json.dumps(network_data, indent=2).encode('utf-8')
suffix='.iso') as metadata_fileobj: inject_files[network_data] = (
'openstack/latest/network_data.json'
)
with open(metadata_fileobj.name, 'w') as f: return prepare_iso_image(inject_files=inject_files)
json.dump(network_data, f, indent=2)
files_info = {
metadata_fileobj.name: 'openstack/latest/network_data.json'
}
with tempfile.NamedTemporaryFile(
dir=CONF.tempdir, suffix='.img') as cfgdrv_fileobj:
images.create_vfat_image(cfgdrv_fileobj.name, files_info)
configdrive_href = urlparse.urlunparse(
('file', '', cfgdrv_fileobj.name, '', '', ''))
LOG.debug("Built configdrive %(name)s out of network data "
"for node %(node)s", {'name': configdrive_href,
'node': task.node.uuid})
return prepare_iso_image(configdrive=configdrive_href)
return prepare_iso_image()
def prepare_boot_iso(task, d_info, root_uuid=None): def prepare_boot_iso(task, d_info, root_uuid=None):

View File

@ -250,34 +250,28 @@ class IronicImagesTestCase(base.TestCase):
class FsImageTestCase(base.TestCase): class FsImageTestCase(base.TestCase):
@mock.patch.object(builtins, 'open', autospec=True)
@mock.patch.object(shutil, 'copyfile', autospec=True) @mock.patch.object(shutil, 'copyfile', autospec=True)
@mock.patch.object(os, 'makedirs', autospec=True) @mock.patch.object(os, 'makedirs', autospec=True)
@mock.patch.object(os.path, 'dirname', autospec=True) def test__create_root_fs(self, mkdir_mock, cp_mock, open_mock):
@mock.patch.object(os.path, 'exists', autospec=True)
def test__create_root_fs(self, path_exists_mock,
dirname_mock, mkdir_mock, cp_mock):
def path_exists_mock_func(path):
return path == 'root_dir'
files_info = { files_info = {
'a1': 'b1', 'a1': 'b1',
'a2': 'b2', 'a2': 'b2',
'a3': 'sub_dir/b3'} 'a3': 'sub_dir/b3',
b'a4': 'b4'}
path_exists_mock.side_effect = path_exists_mock_func
dirname_mock.side_effect = ['root_dir', 'root_dir', 'root_dir/sub_dir',
'root_dir/sub_dir']
images._create_root_fs('root_dir', files_info) images._create_root_fs('root_dir', files_info)
cp_mock.assert_any_call('a1', 'root_dir/b1') cp_mock.assert_any_call('a1', 'root_dir/b1')
cp_mock.assert_any_call('a2', 'root_dir/b2') cp_mock.assert_any_call('a2', 'root_dir/b2')
cp_mock.assert_any_call('a3', 'root_dir/sub_dir/b3') cp_mock.assert_any_call('a3', 'root_dir/sub_dir/b3')
path_exists_mock.assert_any_call('root_dir/sub_dir') open_mock.assert_called_once_with('root_dir/b4', 'wb')
dirname_mock.assert_any_call('root_dir/b1') fp = open_mock.return_value.__enter__.return_value
dirname_mock.assert_any_call('root_dir/b2') fp.write.assert_called_once_with(b'a4')
dirname_mock.assert_any_call('root_dir/sub_dir/b3')
mkdir_mock.assert_called_once_with('root_dir/sub_dir') mkdir_mock.assert_any_call('root_dir', exist_ok=True)
mkdir_mock.assert_any_call('root_dir/sub_dir', exist_ok=True)
@mock.patch.object(images, '_create_root_fs', autospec=True) @mock.patch.object(images, '_create_root_fs', autospec=True)
@mock.patch.object(utils, 'tempdir', autospec=True) @mock.patch.object(utils, 'tempdir', autospec=True)
@ -413,21 +407,6 @@ class FsImageTestCase(base.TestCase):
options) options)
self.assertEqual(expected_cfg, cfg) self.assertEqual(expected_cfg, cfg)
@mock.patch.object(images, 'os', autospec=True)
def test__read_dir(self, mock_os):
mock_os.path.join = os.path.join
mock_os.path.isdir.side_effect = (False, True, False)
mock_os.listdir.side_effect = [['a', 'b'], ['c']]
file_info = images._read_dir('/mnt')
expected = {
'/mnt/a': 'a',
'/mnt/b/c': 'b/c'
}
self.assertEqual(expected, file_info)
@mock.patch.object(os.path, 'relpath', autospec=True) @mock.patch.object(os.path, 'relpath', autospec=True)
@mock.patch.object(os, 'walk', autospec=True) @mock.patch.object(os, 'walk', autospec=True)
@mock.patch.object(utils, 'mount', autospec=True) @mock.patch.object(utils, 'mount', autospec=True)
@ -599,7 +578,8 @@ class FsImageTestCase(base.TestCase):
@mock.patch.object(images, '_generate_cfg', autospec=True) @mock.patch.object(images, '_generate_cfg', autospec=True)
def _test_create_isolinux_image_for_bios( def _test_create_isolinux_image_for_bios(
self, gen_cfg_mock, execute_mock, tempdir_mock, self, gen_cfg_mock, execute_mock, tempdir_mock,
write_to_file_mock, create_root_fs_mock, ldlinux_path=None): write_to_file_mock, create_root_fs_mock, ldlinux_path=None,
inject_files=None):
mock_file_handle = mock.MagicMock(spec=io.BytesIO) mock_file_handle = mock.MagicMock(spec=io.BytesIO)
mock_file_handle.__enter__.return_value = 'tmpdir' mock_file_handle.__enter__.return_value = 'tmpdir'
@ -616,13 +596,16 @@ class FsImageTestCase(base.TestCase):
images.create_isolinux_image_for_bios('tgt_file', images.create_isolinux_image_for_bios('tgt_file',
'path/to/kernel', 'path/to/kernel',
'path/to/ramdisk', 'path/to/ramdisk',
kernel_params=params) kernel_params=params,
inject_files=inject_files)
files_info = { files_info = {
'path/to/kernel': 'vmlinuz', 'path/to/kernel': 'vmlinuz',
'path/to/ramdisk': 'initrd', 'path/to/ramdisk': 'initrd',
CONF.isolinux_bin: 'isolinux/isolinux.bin' CONF.isolinux_bin: 'isolinux/isolinux.bin'
} }
if inject_files:
files_info.update(inject_files)
if ldlinux_path: if ldlinux_path:
files_info[ldlinux_path] = 'isolinux/ldlinux.c32' files_info[ldlinux_path] = 'isolinux/ldlinux.c32'
create_root_fs_mock.assert_called_once_with('tmpdir', files_info) create_root_fs_mock.assert_called_once_with('tmpdir', files_info)
@ -653,6 +636,12 @@ class FsImageTestCase(base.TestCase):
self._test_create_isolinux_image_for_bios( self._test_create_isolinux_image_for_bios(
ldlinux_path='/usr/share/syslinux/ldlinux.c32') ldlinux_path='/usr/share/syslinux/ldlinux.c32')
@mock.patch.object(os.path, 'isfile', autospec=True)
def test_create_isolinux_image_for_bios_inject_files(self, mock_isfile):
mock_isfile.return_value = False
self._test_create_isolinux_image_for_bios(
inject_files={'/source': 'target'})
@mock.patch.object(images, '_umount_without_raise', autospec=True) @mock.patch.object(images, '_umount_without_raise', autospec=True)
@mock.patch.object(images, '_create_root_fs', autospec=True) @mock.patch.object(images, '_create_root_fs', autospec=True)
@mock.patch.object(utils, 'tempdir', autospec=True) @mock.patch.object(utils, 'tempdir', autospec=True)
@ -765,7 +754,7 @@ class FsImageTestCase(base.TestCase):
create_isolinux_mock.assert_called_once_with( create_isolinux_mock.assert_called_once_with(
'output_file', 'tmpdir/kernel-uuid', 'tmpdir/ramdisk-uuid', 'output_file', 'tmpdir/kernel-uuid', 'tmpdir/ramdisk-uuid',
deploy_iso='tmpdir/deploy_iso-uuid', deploy_iso='tmpdir/deploy_iso-uuid',
esp_image=None, kernel_params=params, configdrive=None) esp_image=None, kernel_params=params, inject_files=None)
@mock.patch.object(images, 'create_esp_image_for_uefi', autospec=True) @mock.patch.object(images, 'create_esp_image_for_uefi', autospec=True)
@mock.patch.object(images, 'fetch', autospec=True) @mock.patch.object(images, 'fetch', autospec=True)
@ -793,7 +782,7 @@ class FsImageTestCase(base.TestCase):
create_isolinux_mock.assert_called_once_with( create_isolinux_mock.assert_called_once_with(
'output_file', 'tmpdir/kernel-uuid', 'tmpdir/ramdisk-uuid', 'output_file', 'tmpdir/kernel-uuid', 'tmpdir/ramdisk-uuid',
deploy_iso=None, esp_image='tmpdir/efiboot-uuid', deploy_iso=None, esp_image='tmpdir/efiboot-uuid',
kernel_params=params, configdrive=None) kernel_params=params, inject_files=None)
@mock.patch.object(images, 'create_esp_image_for_uefi', autospec=True) @mock.patch.object(images, 'create_esp_image_for_uefi', autospec=True)
@mock.patch.object(images, 'fetch', autospec=True) @mock.patch.object(images, 'fetch', autospec=True)
@ -821,7 +810,7 @@ class FsImageTestCase(base.TestCase):
create_isolinux_mock.assert_called_once_with( create_isolinux_mock.assert_called_once_with(
'output_file', 'tmpdir/kernel-href', 'tmpdir/ramdisk-href', 'output_file', 'tmpdir/kernel-href', 'tmpdir/ramdisk-href',
deploy_iso='tmpdir/deploy_iso-href', deploy_iso='tmpdir/deploy_iso-href',
esp_image=None, kernel_params=params, configdrive=None) esp_image=None, kernel_params=params, inject_files=None)
@mock.patch.object(images, 'create_esp_image_for_uefi', autospec=True) @mock.patch.object(images, 'create_esp_image_for_uefi', autospec=True)
@mock.patch.object(images, 'fetch', autospec=True) @mock.patch.object(images, 'fetch', autospec=True)
@ -849,7 +838,7 @@ class FsImageTestCase(base.TestCase):
create_isolinux_mock.assert_called_once_with( create_isolinux_mock.assert_called_once_with(
'output_file', 'tmpdir/kernel-href', 'tmpdir/ramdisk-href', 'output_file', 'tmpdir/kernel-href', 'tmpdir/ramdisk-href',
deploy_iso=None, esp_image='tmpdir/efiboot-href', deploy_iso=None, esp_image='tmpdir/efiboot-href',
kernel_params=params, configdrive=None) kernel_params=params, inject_files=None)
@mock.patch.object(images, 'create_isolinux_image_for_bios', autospec=True) @mock.patch.object(images, 'create_isolinux_image_for_bios', autospec=True)
@mock.patch.object(images, 'fetch', autospec=True) @mock.patch.object(images, 'fetch', autospec=True)
@ -863,26 +852,24 @@ class FsImageTestCase(base.TestCase):
images.create_boot_iso('ctx', 'output_file', 'kernel-uuid', images.create_boot_iso('ctx', 'output_file', 'kernel-uuid',
'ramdisk-uuid', 'deploy_iso-uuid', 'ramdisk-uuid', 'deploy_iso-uuid',
'efiboot-uuid', 'root-uuid', 'efiboot-uuid', 'root-uuid',
'kernel-params', 'bios', 'configdrive') 'kernel-params', 'bios')
fetch_images_mock.assert_any_call( fetch_images_mock.assert_any_call(
'ctx', 'kernel-uuid', 'tmpdir/kernel-uuid') 'ctx', 'kernel-uuid', 'tmpdir/kernel-uuid')
fetch_images_mock.assert_any_call( fetch_images_mock.assert_any_call(
'ctx', 'ramdisk-uuid', 'tmpdir/ramdisk-uuid') 'ctx', 'ramdisk-uuid', 'tmpdir/ramdisk-uuid')
fetch_images_mock.assert_any_call(
'ctx', 'configdrive', 'tmpdir/configdrive')
# Note (NobodyCam): the original assert asserted that fetch_images # Note (NobodyCam): the original assert asserted that fetch_images
# was not called with parameters, this did not # was not called with parameters, this did not
# work, So I instead assert that there were only # work, So I instead assert that there were only
# Two calls to the mock validating the above # Two calls to the mock validating the above
# asserts. # asserts.
self.assertEqual(3, fetch_images_mock.call_count) self.assertEqual(2, fetch_images_mock.call_count)
params = ['root=UUID=root-uuid', 'kernel-params'] params = ['root=UUID=root-uuid', 'kernel-params']
create_isolinux_mock.assert_called_once_with( create_isolinux_mock.assert_called_once_with(
'output_file', 'tmpdir/kernel-uuid', 'tmpdir/ramdisk-uuid', 'output_file', 'tmpdir/kernel-uuid', 'tmpdir/ramdisk-uuid',
kernel_params=params, configdrive='tmpdir/configdrive') kernel_params=params, inject_files=None)
@mock.patch.object(images, 'create_isolinux_image_for_bios', autospec=True) @mock.patch.object(images, 'create_isolinux_image_for_bios', autospec=True)
@mock.patch.object(images, 'fetch', autospec=True) @mock.patch.object(images, 'fetch', autospec=True)
@ -897,19 +884,17 @@ class FsImageTestCase(base.TestCase):
images.create_boot_iso('ctx', 'output_file', 'kernel-uuid', images.create_boot_iso('ctx', 'output_file', 'kernel-uuid',
'ramdisk-uuid', 'deploy_iso-uuid', 'ramdisk-uuid', 'deploy_iso-uuid',
'efiboot-uuid', 'root-uuid', 'efiboot-uuid', 'root-uuid',
'kernel-params', None, 'http://configdrive') 'kernel-params', None)
fetch_images_mock.assert_any_call( fetch_images_mock.assert_any_call(
'ctx', 'kernel-uuid', 'tmpdir/kernel-uuid') 'ctx', 'kernel-uuid', 'tmpdir/kernel-uuid')
fetch_images_mock.assert_any_call( fetch_images_mock.assert_any_call(
'ctx', 'ramdisk-uuid', 'tmpdir/ramdisk-uuid') 'ctx', 'ramdisk-uuid', 'tmpdir/ramdisk-uuid')
fetch_images_mock.assert_any_call(
'ctx', 'http://configdrive', 'tmpdir/configdrive')
params = ['root=UUID=root-uuid', 'kernel-params'] params = ['root=UUID=root-uuid', 'kernel-params']
create_isolinux_mock.assert_called_once_with( create_isolinux_mock.assert_called_once_with(
'output_file', 'tmpdir/kernel-uuid', 'tmpdir/ramdisk-uuid', 'output_file', 'tmpdir/kernel-uuid', 'tmpdir/ramdisk-uuid',
configdrive='tmpdir/configdrive', kernel_params=params) kernel_params=params, inject_files=None)
@mock.patch.object(images, 'create_isolinux_image_for_bios', autospec=True) @mock.patch.object(images, 'create_isolinux_image_for_bios', autospec=True)
@mock.patch.object(images, 'fetch', autospec=True) @mock.patch.object(images, 'fetch', autospec=True)
@ -924,8 +909,7 @@ class FsImageTestCase(base.TestCase):
images.create_boot_iso('ctx', 'output_file', 'kernel-uuid', images.create_boot_iso('ctx', 'output_file', 'kernel-uuid',
'ramdisk-uuid', 'deploy_iso-uuid', 'ramdisk-uuid', 'deploy_iso-uuid',
'efiboot-uuid', None, 'efiboot-uuid', None,
None, None, 'http://configdrive', None, None, base_iso=base_iso)
base_iso=base_iso)
fetch_images_mock.assert_any_call( fetch_images_mock.assert_any_call(
'ctx', base_iso, 'output_file') 'ctx', base_iso, 'output_file')

View File

@ -252,10 +252,9 @@ class RedfishImageUtilsTestCase(db_base.DbTestCase):
mock_create_boot_iso.assert_called_once_with( mock_create_boot_iso.assert_called_once_with(
mock.ANY, mock.ANY, 'http://kernel/img', 'http://ramdisk/img', mock.ANY, mock.ANY, 'http://kernel/img', 'http://ramdisk/img',
boot_mode='uefi', esp_image_href='http://bootloader/img', boot_mode='uefi', esp_image_href='http://bootloader/img',
configdrive_href=mock.ANY,
kernel_params='nofb nomodeset vga=normal', kernel_params='nofb nomodeset vga=normal',
root_uuid='1be26c0b-03f2-4d2e-ae87-c02d7f33c123', root_uuid='1be26c0b-03f2-4d2e-ae87-c02d7f33c123',
base_iso=None) base_iso=None, inject_files=None)
self.assertEqual(expected_url, url) self.assertEqual(expected_url, url)
@ -275,10 +274,9 @@ class RedfishImageUtilsTestCase(db_base.DbTestCase):
mock_create_boot_iso.assert_called_once_with( mock_create_boot_iso.assert_called_once_with(
mock.ANY, mock.ANY, 'http://kernel/img', 'http://ramdisk/img', mock.ANY, mock.ANY, 'http://kernel/img', 'http://ramdisk/img',
boot_mode='uefi', esp_image_href=None, boot_mode='uefi', esp_image_href=None,
configdrive_href=mock.ANY,
kernel_params='nofb nomodeset vga=normal', kernel_params='nofb nomodeset vga=normal',
root_uuid='1be26c0b-03f2-4d2e-ae87-c02d7f33c123', root_uuid='1be26c0b-03f2-4d2e-ae87-c02d7f33c123',
base_iso='/path/to/baseiso') base_iso='/path/to/baseiso', inject_files=None)
@mock.patch.object(image_utils.ImageHandler, 'publish_image', @mock.patch.object(image_utils.ImageHandler, 'publish_image',
autospec=True) autospec=True)
@ -304,10 +302,9 @@ class RedfishImageUtilsTestCase(db_base.DbTestCase):
mock_create_boot_iso.assert_called_once_with( mock_create_boot_iso.assert_called_once_with(
mock.ANY, mock.ANY, 'http://kernel/img', 'http://ramdisk/img', mock.ANY, mock.ANY, 'http://kernel/img', 'http://ramdisk/img',
boot_mode='bios', esp_image_href=None, boot_mode='bios', esp_image_href=None,
configdrive_href=mock.ANY,
kernel_params='nofb nomodeset vga=normal', kernel_params='nofb nomodeset vga=normal',
root_uuid='1be26c0b-03f2-4d2e-ae87-c02d7f33c123', root_uuid='1be26c0b-03f2-4d2e-ae87-c02d7f33c123',
base_iso=None) base_iso=None, inject_files=None)
self.assertEqual(expected_url, url) self.assertEqual(expected_url, url)
@ -330,10 +327,9 @@ class RedfishImageUtilsTestCase(db_base.DbTestCase):
mock_create_boot_iso.assert_called_once_with( mock_create_boot_iso.assert_called_once_with(
mock.ANY, mock.ANY, 'http://kernel/img', 'http://ramdisk/img', mock.ANY, mock.ANY, 'http://kernel/img', 'http://ramdisk/img',
boot_mode='bios', esp_image_href=None, boot_mode='bios', esp_image_href=None,
configdrive_href=mock.ANY,
kernel_params=kernel_params, kernel_params=kernel_params,
root_uuid='1be26c0b-03f2-4d2e-ae87-c02d7f33c123', root_uuid='1be26c0b-03f2-4d2e-ae87-c02d7f33c123',
base_iso='/path/to/baseiso') base_iso='/path/to/baseiso', inject_files=None)
def test__find_param(self): def test__find_param(self):
param_dict = { param_dict = {
@ -385,16 +381,15 @@ class RedfishImageUtilsTestCase(db_base.DbTestCase):
image_utils.prepare_deploy_iso(task, {}, 'deploy', d_info) image_utils.prepare_deploy_iso(task, {}, 'deploy', d_info)
mock__prepare_iso_image.assert_called_once_with( mock__prepare_iso_image.assert_called_once_with(
task, 'kernel', 'ramdisk', 'bootloader', params={}) task, 'kernel', 'ramdisk', 'bootloader', params={},
inject_files={})
find_mock.assert_has_calls(find_call_list) find_mock.assert_has_calls(find_call_list)
@mock.patch.object(image_utils, '_find_param', autospec=True) @mock.patch.object(image_utils, '_find_param', autospec=True)
@mock.patch.object(image_utils, '_prepare_iso_image', autospec=True) @mock.patch.object(image_utils, '_prepare_iso_image', autospec=True)
@mock.patch.object(images, 'create_vfat_image', autospec=True)
def test_prepare_deploy_iso_network_data( def test_prepare_deploy_iso_network_data(
self, mock_create_vfat_image, mock__prepare_iso_image, self, mock__prepare_iso_image, find_mock):
find_mock):
with task_manager.acquire(self.context, self.node.uuid, with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task: shared=True) as task:
@ -421,12 +416,17 @@ class RedfishImageUtilsTestCase(db_base.DbTestCase):
image_utils.prepare_deploy_iso(task, {}, 'deploy', d_info) image_utils.prepare_deploy_iso(task, {}, 'deploy', d_info)
mock_create_vfat_image.assert_called_once_with( expected_files = {
mock.ANY, mock.ANY) b"""{
"a": [
"b"
]
}""": 'openstack/latest/network_data.json'
}
mock__prepare_iso_image.assert_called_once_with( mock__prepare_iso_image.assert_called_once_with(
task, 'kernel', 'ramdisk', bootloader_href=None, task, 'kernel', 'ramdisk', bootloader_href=None,
configdrive=mock.ANY, params={}) params={}, inject_files=expected_files)
find_mock.assert_has_calls(find_call_list) find_mock.assert_has_calls(find_call_list)

View File

@ -0,0 +1,10 @@
---
fixes:
- |
Fixes permission issues when injecting network data into a virtual media.
other:
- |
The ``configdrive`` argument to some utils in ``ironic.common.images`` and
``ironic.drivers.modules.image_utils`` has been replaced with a new
``inject_files`` argument. The previous approach did not really work in
all situations and we don't expect 3rd party drivers to use it.