From: Simon Glass <sjg@chromium.org> The rewritten ISO already ships /autoinstall.yaml, but the snippet only declares packages and late-commands. Anyone invoking subiquity with the 'autoinstall' kernel argument hits a halt on the missing identity and storage sections, so there is no way to drive an end-to-end install from the test harness. Add -A / --autoinstall. When set, the script: - appends 'autoinstall' to the BLS kernel cmdline so subiquity picks up /autoinstall.yaml on first boot - emits a complete autoinstall.yaml with interactive-sections: [], identity (hostname, username, hashed password), storage layout (direct), refresh-installer disabled, and openssh-server so a test can SSH in afterwards The password is supplied as plaintext via --password and hashed with 'openssl passwd -6' at build time, keeping the script's runtime dependencies unchanged - Python's crypt module is gone in 3.13. --hostname and --username round out the identity block. Guard against --autoinstall being combined with --no-target-bls (the yaml is mandatory in unattended mode) and fail early if openssl is missing. Signed-off-by: Simon Glass <sjg@chromium.org> --- scripts/ubuntu-iso-to-uboot.py | 94 +++++++++++++++++++++++++++++++--- 1 file changed, 88 insertions(+), 6 deletions(-) diff --git a/scripts/ubuntu-iso-to-uboot.py b/scripts/ubuntu-iso-to-uboot.py index 1c09e226d16..c22bd9423f2 100755 --- a/scripts/ubuntu-iso-to-uboot.py +++ b/scripts/ubuntu-iso-to-uboot.py @@ -65,6 +65,7 @@ import argparse import os import re import shutil +import subprocess import sys import tempfile from pathlib import Path @@ -313,7 +314,26 @@ mksquashfs '{stage}' '{modified}' -noappend -comp xz -no-progress return modified -def autoinstall_yaml() -> str: +def hash_password(password: str) -> str: + """Return a SHA-512 crypt hash for @password via openssl passwd -6. + + Subiquity's identity.password field wants a crypt(3) hash, not a plaintext + password. Shelling out to openssl keeps the script's dependency list + unchanged (Python's crypt module is gone in 3.13). + """ + out = subprocess.run( + ['openssl', 'passwd', '-6', '-stdin'], + input=password, capture_output=True, text=True, check=True, + ) + return out.stdout.strip() + + +def autoinstall_yaml( + unattended: bool = False, + hostname: str = 'ubuntu-uboot', + username: str = 'ubuntu', + password_hash: str = '', +) -> str: """Return an autoinstall snippet that seeds BLS entries on the installed ESP. @@ -329,11 +349,33 @@ def autoinstall_yaml() -> str: The kernel-install path is bypassed because curtin's chroot has /boot/efi as a plain directory rather than the ESP mountpoint, so systemd-boot-efi's 90-loaderentry.install plugin silently exits without writing entries. + + When @unattended is True, also emit identity and storage sections so + subiquity can complete without user input - required for the + test_distro_ubuntu_iso_install CI path. """ - return ( - '#cloud-config\n' - 'autoinstall:\n' - ' version: 1\n' + head = '''\ +#cloud-config +autoinstall: + version: 1 +''' + unattended_block = '' + if unattended: + unattended_block = f'''\ + interactive-sections: [] + refresh-installer: + update: no + identity: + hostname: {hostname} + username: {username} + password: '{password_hash}' + storage: + layout: + name: direct + ssh: + install-server: true +''' + body = ( ' late-commands:\n' # Write BLS entries to the install target's ESP. The kernel # and initrd are copied alongside on the ESP so the entry can @@ -371,6 +413,7 @@ options root=UUID=%s ro console=ttyS0,115200 console=tty0\\n"\ fi' ''' ) + return head + unattended_block + body def main() -> None: @@ -402,6 +445,20 @@ def main() -> None: help='path inside the ISO of the install squashfs to ' 'modify for interactive installs ' '(default: %(default)s; set to empty to skip)') + p.add_argument('-A', '--autoinstall', action='store_true', + help='build for unattended autoinstall: append ' + '"autoinstall" to the BLS cmdline and ship a complete ' + 'autoinstall.yaml so subiquity runs without user input') + p.add_argument('--hostname', default='ubuntu-uboot', + help='hostname for the autoinstalled system ' + '(default: %(default)s)') + p.add_argument('--username', default='ubuntu', + help='username created by autoinstall ' + '(default: %(default)s)') + p.add_argument('--password', default='ubuntu', + help='plaintext password for the autoinstall user; ' + 'hashed with `openssl passwd -6` before being ' + 'written to autoinstall.yaml (default: %(default)s)') p.add_argument('-v', '--verbose', action='store_true', help='show progress markers and subprocess output') args = p.parse_args() @@ -414,7 +471,11 @@ def main() -> None: tout.fatal(f'ISO not found: {args.iso}') if not args.uboot.is_file(): tout.fatal(f'EFI app not found: {args.uboot}') + if args.autoinstall and args.no_target_bls: + tout.fatal('--autoinstall requires target-BLS wiring; drop -N') check_tools() + if args.autoinstall and not shutil.which('openssl'): + tout.fatal('openssl is required when --autoinstall is set') tout.notice(f'=> Reading boot config from {args.iso}') # Extract the volume label and ESP partition GUID from xorriso's report @@ -423,6 +484,19 @@ def main() -> None: cmdline = args.cmdline if cmdline is None: cmdline = parse_grub_cmdline(args.iso, args.kernel) + if args.autoinstall: + # Force subiquity onto the serial console so the CI harness can watch + # progress: drop the live-ISO 'quiet splash' (kernel would otherwise + # silently swallow the reboot line), pin the console to ttyS0, and ask + # systemd-journald to mirror its stream onto the console so subiquity's + # own events land on serial too. + tokens = [t for t in cmdline.split() if t not in ('quiet', 'splash')] + for extra in ('console=ttyS0,115200', + 'systemd.journald.forward_to_console=1', + 'autoinstall'): + if extra not in tokens: + tokens.append(extra) + cmdline = ' '.join(tokens) tout.notice(f' Volume label: {vol_id}') tout.notice(f' ESP GUID: {esp_guid}') tout.notice(f' Cmdline: {cmdline}') @@ -446,7 +520,15 @@ def main() -> None: file_maps = [(entry, '/loader/entry.conf')] if not args.no_target_bls: ai = work / 'autoinstall.yaml' - ai.write_text(autoinstall_yaml()) + if args.autoinstall: + ai.write_text(autoinstall_yaml( + unattended=True, + hostname=args.hostname, + username=args.username, + password_hash=hash_password(args.password), + )) + else: + ai.write_text(autoinstall_yaml()) file_maps.append((ai, '/autoinstall.yaml')) if args.install_squashfs: modified_sqfs = inject_first_boot_unit( -- 2.43.0