"""Build a NoCloud cidata ISO for cloud-init. Cirros 0.6.x — and most cloud images — look for a NoCloud datasource at boot: an ISO9660 volume labeled ``cidata`` containing two files, ``user-data`` and ``meta-data``. We attach it as a second drive so cloud-init proceeds without spending ~17 minutes timing out trying to reach a non-existent metadata service. This script is intentionally self-contained and uses only pycdlib (pure Python) — no system mkisofs/xorriso/cloud-localds dependency. Usage: uv run python tools/build_cidata.py vm/images/cidata.iso The defaults bake in the ``cirros`` user with the documented Cirros password, enable SSH password auth (so future Metasploit-class images work without changes), and set a hostname. Override via flags if needed. """ from __future__ import annotations import argparse import io import sys from pathlib import Path import pycdlib DEFAULT_USER_DATA = """\ #cloud-config hostname: cis490 manage_etc_hosts: true users: - name: cis490 plain_text_passwd: cis490 lock_passwd: false sudo: ALL=(ALL) NOPASSWD:ALL shell: /bin/sh ssh_pwauth: true disable_root: false chpasswd: expire: false list: | root:cis490 cis490:cis490 runcmd: - [ sh, -c, "echo CIS490_BOOT_OK > /tmp/.cis490-boot" ] """ DEFAULT_META_DATA = """\ instance-id: cis490-vm-001 local-hostname: cis490 """ def build_cidata(out_path: Path, user_data: bytes, meta_data: bytes) -> None: iso = pycdlib.PyCdlib() # Joliet=3 + Rock Ridge so cloud-init reads filenames correctly on Linux. iso.new(joliet=3, vol_ident="cidata", interchange_level=3, rock_ridge="1.09") iso.add_fp( io.BytesIO(user_data), len(user_data), iso_path="/USERDATA.;1", rr_name="user-data", joliet_path="/user-data", ) iso.add_fp( io.BytesIO(meta_data), len(meta_data), iso_path="/METADATA.;1", rr_name="meta-data", joliet_path="/meta-data", ) iso.write(str(out_path)) iso.close() def main() -> int: parser = argparse.ArgumentParser(prog="build_cidata") parser.add_argument("out_path", type=Path) parser.add_argument( "--user-data", type=Path, default=None, help="path to a custom cloud-config user-data file", ) parser.add_argument( "--meta-data", type=Path, default=None, help="path to a custom meta-data file", ) args = parser.parse_args() user_data = ( args.user_data.read_bytes() if args.user_data else DEFAULT_USER_DATA.encode() ) meta_data = ( args.meta_data.read_bytes() if args.meta_data else DEFAULT_META_DATA.encode() ) args.out_path.parent.mkdir(parents=True, exist_ok=True) build_cidata(args.out_path, user_data, meta_data) print(f"wrote {args.out_path} ({args.out_path.stat().st_size} bytes)") return 0 if __name__ == "__main__": sys.exit(main())