Merge "Support burning configdrive into boot ISO"

This commit is contained in:
Zuul 2020-04-09 14:53:34 +00:00 committed by Gerrit Code Review
commit 117879f80b
4 changed files with 203 additions and 69 deletions

View File

@ -19,6 +19,7 @@
Handling of VM disk images.
"""
import contextlib
import os
import shutil
@ -154,8 +155,67 @@ def _generate_cfg(kernel_params, template, options):
return utils.render_template(template, options)
def create_isolinux_image_for_bios(output_file, kernel, ramdisk,
kernel_params=None):
def _read_dir(root_dir, prefix_dir=None):
"""Gather files under given directory.
:param root_dir: a directory to traverse.
:returns: a dict mapping absolute paths to relative to the `root_dir`.
"""
files_info = {}
if not prefix_dir:
prefix_dir = root_dir
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(
output_file, kernel, ramdisk, kernel_params=None, configdrive=None):
"""Creates an isolinux image on the specified file.
Copies the provided kernel, ramdisk to a directory, generates the isolinux
@ -169,6 +229,8 @@ def create_isolinux_image_for_bios(output_file, kernel, ramdisk,
: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
as the kernel cmdline.
:param configdrive: ISO9660 or FAT-formatted OpenStack config drive
image. This image will be written onto the built ISO image. Optional.
:raises: ImageCreationFailed, if image creation failed while copying files
or while running command to generate iso.
"""
@ -200,11 +262,15 @@ def create_isolinux_image_for_bios(output_file, kernel, ramdisk,
if ldlinux_src:
files_info[ldlinux_src] = LDLINUX_BIN
try:
_create_root_fs(tmpdir, files_info)
except (OSError, IOError) as e:
LOG.exception("Creating the filesystem root failed.")
raise exception.ImageCreationFailed(image_type='iso', error=e)
with _collect_files(configdrive) as cfgdrv_files:
files_info.update(cfgdrv_files)
try:
_create_root_fs(tmpdir, files_info)
except EnvironmentError as e:
LOG.exception("Creating the filesystem root failed.")
raise exception.ImageCreationFailed(image_type='iso', error=e)
cfg = _generate_cfg(kernel_params,
CONF.isolinux_config_template, options)
@ -213,7 +279,8 @@ def create_isolinux_image_for_bios(output_file, kernel, ramdisk,
utils.write_to_file(isolinux_cfg, cfg)
try:
utils.execute('mkisofs', '-r', '-V', "VMEDIA_BOOT_ISO",
utils.execute('mkisofs', '-r', '-V',
'config-2' if configdrive else 'VMEDIA_BOOT_ISO',
'-cache-inodes', '-J', '-l', '-no-emul-boot',
'-boot-load-size', '4', '-boot-info-table',
'-b', ISOLINUX_BIN, '-o', output_file, tmpdir)
@ -222,9 +289,9 @@ def create_isolinux_image_for_bios(output_file, kernel, ramdisk,
raise exception.ImageCreationFailed(image_type='iso', error=e)
def create_esp_image_for_uefi(output_file, kernel, ramdisk,
deploy_iso=None, esp_image=None,
kernel_params=None):
def create_esp_image_for_uefi(
output_file, kernel, ramdisk, deploy_iso=None, esp_image=None,
kernel_params=None, configdrive=None):
"""Creates an ESP image on the specified file.
Copies the provided kernel, ramdisk and EFI system partition image (ESP) to
@ -244,6 +311,8 @@ def create_esp_image_for_uefi(output_file, kernel, ramdisk,
: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
as the kernel cmdline.
:param configdrive: ISO9660 or FAT-formatted OpenStack config drive
image. This image will be written onto the built ISO image. Optional.
:raises: ImageCreationFailed, if image creation failed while copying files
or while running command to generate iso.
"""
@ -290,16 +359,20 @@ def create_esp_image_for_uefi(output_file, kernel, ramdisk,
files_info.update(uefi_path_info)
try:
_create_root_fs(tmpdir, files_info)
with _collect_files(configdrive) as cfgdrv_files:
files_info.update(cfgdrv_files)
except (OSError, IOError) as e:
LOG.exception("Creating the filesystem root failed.")
raise exception.ImageCreationFailed(image_type='iso', error=e)
try:
_create_root_fs(tmpdir, files_info)
finally:
if deploy_iso:
_umount_without_raise(mountdir)
except EnvironmentError as e:
LOG.exception("Creating the filesystem root failed.")
raise exception.ImageCreationFailed(
image_type='iso', error=e)
finally:
if deploy_iso:
_umount_without_raise(mountdir)
# Generate and copy grub config file.
grub_conf = _generate_cfg(kernel_params,
@ -308,8 +381,9 @@ def create_esp_image_for_uefi(output_file, kernel, ramdisk,
# Create the boot_iso.
try:
utils.execute('mkisofs', '-r', '-V', "VMEDIA_BOOT_ISO", '-l',
'-e', e_img_rel_path, '-no-emul-boot',
utils.execute('mkisofs', '-r', '-V',
'config-2' if configdrive else 'VMEDIA_BOOT_ISO',
'-l', '-e', e_img_rel_path, '-no-emul-boot',
'-o', output_file, tmpdir)
except processutils.ProcessExecutionError as e:
@ -437,7 +511,8 @@ def get_temp_url_for_glance_image(context, image_uuid):
def create_boot_iso(context, output_filename, kernel_href,
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):
"""Creates a bootable ISO image for a node.
Given the hrefs for kernel, ramdisk, root partition's UUID and
@ -455,12 +530,15 @@ def create_boot_iso(context, output_filename, kernel_href,
ISO is desired.
:param esp_image_href: URL or glance UUID of FAT12/16/32-formatted EFI
system partition image containing the EFI boot loader (e.g. GRUB2)
for each hardware architecture to boot. This image will be embedded
into the ISO image. If not specified, the `deploy_iso_href` option
for each hardware architecture to boot. This image will be written
onto the ISO image. If not specified, the `deploy_iso_href` option
is only required for building UEFI-bootable ISO.
:param kernel_params: a string containing whitespace separated values
kernel cmdline arguments of the form K=V or K (optional).
: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.
:raises: ImageCreationFailed, if creating boot ISO failed.
"""
with utils.tempdir() as tmpdir:
@ -470,6 +548,14 @@ def create_boot_iso(context, output_filename, kernel_href,
fetch(context, kernel_href, kernel_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 = []
if root_uuid:
params.append('root=UUID=%s' % root_uuid)
@ -493,17 +579,15 @@ def create_boot_iso(context, output_filename, kernel_href,
elif CONF.esp_image:
esp_image_path = CONF.esp_image
create_esp_image_for_uefi(output_filename,
kernel_path,
ramdisk_path,
deploy_iso=deploy_iso_path,
esp_image=esp_image_path,
kernel_params=params)
create_esp_image_for_uefi(
output_filename, kernel_path, ramdisk_path,
deploy_iso=deploy_iso_path, esp_image=esp_image_path,
kernel_params=params, configdrive=configdrive_path)
else:
create_isolinux_image_for_bios(output_filename,
kernel_path,
ramdisk_path,
params)
create_isolinux_image_for_bios(
output_filename, kernel_path, ramdisk_path,
kernel_params=params, configdrive=configdrive_path)
def is_whole_disk_image(ctx, instance_info):

View File

@ -20,6 +20,7 @@ from urllib import parse as urlparse
from ironic_lib import utils as ironic_utils
from oslo_log import log
from oslo_serialization import base64
from oslo_utils import importutils
from ironic.common import boot_devices
@ -411,7 +412,8 @@ class RedfishVirtualMediaBoot(base.BootInterface):
@classmethod
def _prepare_iso_image(cls, task, kernel_href, ramdisk_href,
bootloader_href=None, root_uuid=None, params=None):
bootloader_href=None, configdrive=None,
root_uuid=None, params=None):
"""Prepare an ISO to boot the node.
Build bootable ISO out of `kernel_href` and `ramdisk_href` (and
@ -423,6 +425,9 @@ class RedfishVirtualMediaBoot(base.BootInterface):
:param ramdisk_href: URL or Glance UUID of the ramdisk to use
:param bootloader_href: URL or Glance UUID of the EFI bootloader
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 params: a dictionary containing 'parameter name'->'value'
mapping to be passed to kernel command line.
@ -467,24 +472,48 @@ class RedfishVirtualMediaBoot(base.BootInterface):
'params': kernel_params})
with tempfile.NamedTemporaryFile(
dir=CONF.tempdir, suffix='.iso') as fileobj:
boot_iso_tmp_file = fileobj.name
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)
dir=CONF.tempdir, suffix='.iso') as boot_fileobj:
iso_object_name = cls._get_iso_image_name(task.node)
with tempfile.NamedTemporaryFile(
dir=CONF.tempdir, suffix='.img') as cfgdrv_fileobj:
image_url = cls._publish_image(boot_iso_tmp_file, iso_object_name)
configdrive_href = configdrive
LOG.debug("Created ISO %(name)s in Swift for node %(node)s, exposed "
"as temporary URL %(url)s", {'node': task.node.uuid,
'name': iso_object_name,
'url': image_url})
if configdrive:
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.info("Burning configdrive %(url)s to boot ISO image "
"for node %(node)s", {'url': configdrive_href,
'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)
iso_object_name = cls._get_iso_image_name(task.node)
image_url = cls._publish_image(
boot_iso_tmp_file, iso_object_name)
LOG.debug("Created ISO %(name)s in object store for node %(node)s, "
"exposed as temporary URL "
"%(url)s", {'node': task.node.uuid,
'name': iso_object_name,
'url': image_url})
return image_url

View File

@ -413,6 +413,21 @@ class FsImageTestCase(base.TestCase):
options)
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, 'walk', autospec=True)
@mock.patch.object(utils, 'mount', autospec=True)
@ -749,8 +764,8 @@ class FsImageTestCase(base.TestCase):
params = ['root=UUID=root-uuid', 'kernel-params']
create_isolinux_mock.assert_called_once_with(
'output_file', 'tmpdir/kernel-uuid', 'tmpdir/ramdisk-uuid',
deploy_iso='tmpdir/deploy_iso-uuid', esp_image=None,
kernel_params=params)
deploy_iso='tmpdir/deploy_iso-uuid',
esp_image=None, kernel_params=params, configdrive=None)
@mock.patch.object(images, 'create_esp_image_for_uefi', autospec=True)
@mock.patch.object(images, 'fetch', autospec=True)
@ -778,7 +793,7 @@ class FsImageTestCase(base.TestCase):
create_isolinux_mock.assert_called_once_with(
'output_file', 'tmpdir/kernel-uuid', 'tmpdir/ramdisk-uuid',
deploy_iso=None, esp_image='tmpdir/efiboot-uuid',
kernel_params=params)
kernel_params=params, configdrive=None)
@mock.patch.object(images, 'create_esp_image_for_uefi', autospec=True)
@mock.patch.object(images, 'fetch', autospec=True)
@ -805,8 +820,8 @@ class FsImageTestCase(base.TestCase):
params = ['root=UUID=root-uuid', 'kernel-params']
create_isolinux_mock.assert_called_once_with(
'output_file', 'tmpdir/kernel-href', 'tmpdir/ramdisk-href',
deploy_iso='tmpdir/deploy_iso-href', esp_image=None,
kernel_params=params)
deploy_iso='tmpdir/deploy_iso-href',
esp_image=None, kernel_params=params, configdrive=None)
@mock.patch.object(images, 'create_esp_image_for_uefi', autospec=True)
@mock.patch.object(images, 'fetch', autospec=True)
@ -834,7 +849,7 @@ class FsImageTestCase(base.TestCase):
create_isolinux_mock.assert_called_once_with(
'output_file', 'tmpdir/kernel-href', 'tmpdir/ramdisk-href',
deploy_iso=None, esp_image='tmpdir/efiboot-href',
kernel_params=params)
kernel_params=params, configdrive=None)
@mock.patch.object(images, 'create_isolinux_image_for_bios', autospec=True)
@mock.patch.object(images, 'fetch', autospec=True)
@ -847,25 +862,27 @@ class FsImageTestCase(base.TestCase):
images.create_boot_iso('ctx', 'output_file', 'kernel-uuid',
'ramdisk-uuid', 'deploy_iso-uuid',
'efiboot-uuid', 'root-uuid', 'kernel-params',
'bios')
'efiboot-uuid', 'root-uuid',
'kernel-params', 'bios', 'configdrive')
fetch_images_mock.assert_any_call(
'ctx', 'kernel-uuid', 'tmpdir/kernel-uuid')
fetch_images_mock.assert_any_call(
'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
# was not called with parameters, this did not
# work, So I instead assert that there were only
# Two calls to the mock validating the above
# asserts.
self.assertEqual(2, fetch_images_mock.call_count)
self.assertEqual(3, fetch_images_mock.call_count)
params = ['root=UUID=root-uuid', 'kernel-params']
create_isolinux_mock.assert_called_once_with('output_file',
'tmpdir/kernel-uuid',
'tmpdir/ramdisk-uuid',
params)
create_isolinux_mock.assert_called_once_with(
'output_file', 'tmpdir/kernel-uuid', 'tmpdir/ramdisk-uuid',
kernel_params=params, configdrive='tmpdir/configdrive')
@mock.patch.object(images, 'create_isolinux_image_for_bios', autospec=True)
@mock.patch.object(images, 'fetch', autospec=True)
@ -879,19 +896,20 @@ class FsImageTestCase(base.TestCase):
images.create_boot_iso('ctx', 'output_file', 'kernel-uuid',
'ramdisk-uuid', 'deploy_iso-uuid',
'efiboot-uuid', 'root-uuid', 'kernel-params',
None)
'efiboot-uuid', 'root-uuid',
'kernel-params', None, 'http://configdrive')
fetch_images_mock.assert_any_call(
'ctx', 'kernel-uuid', 'tmpdir/kernel-uuid')
fetch_images_mock.assert_any_call(
'ctx', 'ramdisk-uuid', 'tmpdir/ramdisk-uuid')
fetch_images_mock.assert_any_call(
'ctx', 'http://configdrive', 'tmpdir/configdrive')
params = ['root=UUID=root-uuid', 'kernel-params']
create_isolinux_mock.assert_called_once_with('output_file',
'tmpdir/kernel-uuid',
'tmpdir/ramdisk-uuid',
params)
create_isolinux_mock.assert_called_once_with(
'output_file', 'tmpdir/kernel-uuid', 'tmpdir/ramdisk-uuid',
configdrive='tmpdir/configdrive', kernel_params=params)
@mock.patch.object(image_service, 'get_image_service', autospec=True)
def test_get_glance_image_properties_no_such_prop(self,

View File

@ -364,6 +364,7 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
mock_create_boot_iso.assert_called_once_with(
mock.ANY, mock.ANY, 'http://kernel/img', 'http://ramdisk/img',
boot_mode='uefi', esp_image_href='http://bootloader/img',
configdrive_href=mock.ANY,
kernel_params='nofb nomodeset vga=normal',
root_uuid='1be26c0b-03f2-4d2e-ae87-c02d7f33c123')
@ -393,6 +394,7 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
mock_create_boot_iso.assert_called_once_with(
mock.ANY, mock.ANY, 'http://kernel/img', 'http://ramdisk/img',
boot_mode=None, esp_image_href=None,
configdrive_href=mock.ANY,
kernel_params='nofb nomodeset vga=normal',
root_uuid='1be26c0b-03f2-4d2e-ae87-c02d7f33c123')
@ -416,6 +418,7 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
mock_create_boot_iso.assert_called_once_with(
mock.ANY, mock.ANY, 'http://kernel/img', 'http://ramdisk/img',
boot_mode=None, esp_image_href=None,
configdrive_href=mock.ANY,
kernel_params=kernel_params,
root_uuid='1be26c0b-03f2-4d2e-ae87-c02d7f33c123')