From 5e98a45f8a9df93769b43374a0732e6b394b55e0 Mon Sep 17 00:00:00 2001 From: Alexei Znamensky <103110+russoz@users.noreply.github.com> Date: Thu, 7 May 2026 21:29:27 +1200 Subject: [PATCH] iso_create: add bootable ISO support via `boot_options` (#11991) * feat(iso_create): add bootable ISO support via El Torito boot_options Co-Authored-By: Claude Sonnet 4.6 * feat(changelogs): add fragment for iso_create bootable ISO support #11991 Co-Authored-By: Claude Sonnet 4.6 * Update plugins/modules/iso_create.py Co-authored-by: Felix Fontein --------- Co-authored-by: Claude Sonnet 4.6 Co-authored-by: Felix Fontein --- .../fragments/11991-iso-create-bootable.yml | 4 + plugins/modules/iso_create.py | 129 ++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 changelogs/fragments/11991-iso-create-bootable.yml diff --git a/changelogs/fragments/11991-iso-create-bootable.yml b/changelogs/fragments/11991-iso-create-bootable.yml new file mode 100644 index 0000000000..0403a51522 --- /dev/null +++ b/changelogs/fragments/11991-iso-create-bootable.yml @@ -0,0 +1,4 @@ +minor_changes: + - iso_create - add ``boot_options`` parameter to support creating bootable ISOs using El Torito boot records + (https://github.com/ansible-collections/community.general/issues/1685, + https://github.com/ansible-collections/community.general/pull/11991). diff --git a/plugins/modules/iso_create.py b/plugins/modules/iso_create.py index e51dcb0575..264ab6524a 100644 --- a/plugins/modules/iso_create.py +++ b/plugins/modules/iso_create.py @@ -79,6 +79,43 @@ options: - If not specified or set to V(false), then no UDF support is added. type: bool default: false + boot_options: + description: + - Options for making the ISO bootable using El Torito boot support. + - If not specified, the ISO will not be bootable. + type: dict + version_added: 13.0.0 + suboptions: + boot_file: + description: + - Local path to the boot image file to embed in the ISO. + - The file is added to the root of the ISO and referenced as the El Torito boot image. + - The boot image must not share a filename with any file added via O(src_files), or pycdlib will raise a duplicate entry error. + type: path + required: true + boot_catalog: + description: + - ISO9660 path for the El Torito boot catalog file inside the ISO. + type: str + default: '/BOOT.CAT;1' + media_name: + description: + - Media emulation type for the El Torito boot entry. + type: str + default: 'noemul' + choices: ['noemul', 'floppy', '1.2m', '1.44m', '2.88m'] + platform_id: + description: + - Target platform for the El Torito boot entry. + type: str + default: 'x86' + choices: ['x86', 'efi', 'mac'] + boot_info_table: + description: + - Whether to add a boot info table to the boot image. + - Required by some bootloaders such as ISOLINUX. + type: bool + default: false """ EXAMPLES = r""" @@ -106,6 +143,17 @@ EXAMPLES = r""" interchange_level: 3 joliet: 3 vol_ident: WIN_AUTOINSTALL + +- name: Create a bootable ISO file using ISOLINUX + community.general.iso_create: + src_files: + - /root/isocontents/isolinux.cfg + dest_iso: /tmp/boot.iso + interchange_level: 3 + boot_options: + boot_file: /root/isocontents/isolinux.bin + media_name: noemul + boot_info_table: true """ RETURN = r""" @@ -145,11 +193,43 @@ udf: returned: on success type: bool sample: false +boot_options: + description: Configured El Torito boot options, or V(null) if the ISO is not bootable. + returned: on success + type: dict + version_added: 13.0.0 + contains: + boot_file: + description: Local path to the boot image file. + type: str + sample: "/root/isolinux/isolinux.bin" + boot_catalog: + description: ISO9660 path of the boot catalog inside the ISO. + type: str + sample: "/BOOT.CAT;1" + media_name: + description: Media emulation type. + type: str + sample: "noemul" + platform_id: + description: Target platform identifier. + type: str + sample: "x86" + boot_info_table: + description: Whether a boot info table was added to the boot image. + type: bool + sample: false """ import os import traceback +PLATFORM_ID_MAP = { + "x86": b"\x00", + "efi": b"\xef", + "mac": b"\x02", +} + PYCDLIB_IMP_ERR = None try: import pycdlib @@ -213,6 +293,16 @@ def main(): rock_ridge=dict(type="str", choices=["1.09", "1.10", "1.12"]), joliet=dict(type="int", choices=[1, 2, 3]), udf=dict(type="bool", default=False), + boot_options=dict( + type="dict", + options=dict( + boot_file=dict(type="path", required=True), + boot_catalog=dict(type="str", default="/BOOT.CAT;1"), + media_name=dict(type="str", default="noemul", choices=["noemul", "floppy", "1.2m", "1.44m", "2.88m"]), + platform_id=dict(type="str", default="x86", choices=["x86", "efi", "mac"]), + boot_info_table=dict(type="bool", default=False), + ), + ), ) module = AnsibleModule( argument_spec=argument_spec, @@ -228,6 +318,12 @@ def main(): if not os.path.exists(src_file): module.fail_json(msg=f"Specified source file/directory path does not exist on local machine, {src_file}") + boot_options = module.params.get("boot_options") + if boot_options: + boot_file = boot_options["boot_file"] + if not os.path.exists(boot_file): + module.fail_json(msg=f"Specified boot file path does not exist on local machine, {boot_file}") + dest_iso = module.params.get("dest_iso") if dest_iso and len(dest_iso) == 0: module.fail_json(msg="Please specify the absolute path of the new created ISO file using dest_iso parameter.") @@ -259,6 +355,7 @@ def main(): rock_ridge=rock_ridge, joliet=use_joliet, udf=use_udf, + boot_options=boot_options, ) if not module.check_mode: iso_file = pycdlib.PyCdlib(always_consistent=True) @@ -319,6 +416,38 @@ def main(): use_udf=use_udf, ) + if boot_options: + boot_file = boot_options["boot_file"] + boot_file_basename = os.path.basename(boot_file) + if "." not in boot_file_basename: + boot_iso_path = f"/{boot_file_basename.upper()}.;1" + else: + boot_iso_path = f"/{boot_file_basename.upper()};1" + add_file( + module, + iso_file=iso_file, + src_file=boot_file, + file_path=f"/{boot_file_basename}", + rock_ridge=rock_ridge, + use_joliet=use_joliet, + use_udf=use_udf, + ) + boot_catalog_basename = os.path.basename(boot_options["boot_catalog"]).split(";")[0].lower() + eltorito_kwargs = dict( + bootcatfile=boot_options["boot_catalog"], + media_name=boot_options["media_name"], + platform_id=PLATFORM_ID_MAP[boot_options["platform_id"]], + boot_info_table=boot_options["boot_info_table"], + ) + if rock_ridge: + eltorito_kwargs["rr_bootcatfile"] = boot_catalog_basename + if use_joliet: + eltorito_kwargs["joliet_bootcatfile"] = f"/{boot_catalog_basename}" + try: + iso_file.add_eltorito(boot_iso_path, **eltorito_kwargs) + except Exception as err: + module.fail_json(msg=f"Failed to add El Torito boot record to ISO file: {err}") + iso_file.write(dest_iso) iso_file.close()