diff --git a/mantle/cmd/kola/testiso.go b/mantle/cmd/kola/testiso.go index f10ea2ebb0..e437a22c0d 100644 --- a/mantle/cmd/kola/testiso.go +++ b/mantle/cmd/kola/testiso.go @@ -67,17 +67,22 @@ var ( const ( installTimeout = 15 * time.Minute - scenarioPXEInstall = "pxe-install" - scenarioISOInstall = "iso-install" - scenarioISOLiveLogin = "iso-live-login" - scenarioLegacyInstall = "legacy-install" + scenarioPXEInstall = "pxe-install" + scenarioISOInstall = "iso-install" + // this might just be iso-install eventually since the pxe install already + // tests the over-the-network case; we're making it separate for now to + // more easily ratchet osmet into place + scenarioISOOfflineInstall = "iso-offline-install" + scenarioISOLiveLogin = "iso-live-login" + scenarioLegacyInstall = "legacy-install" ) var allScenarios = map[string]bool{ - scenarioPXEInstall: true, - scenarioISOInstall: true, - scenarioISOLiveLogin: true, - scenarioLegacyInstall: true, + scenarioPXEInstall: true, + scenarioISOInstall: true, + scenarioISOOfflineInstall: true, + scenarioISOLiveLogin: true, + scenarioLegacyInstall: true, } var signalCompleteString = "coreos-installer-test-OK" @@ -103,7 +108,7 @@ func init() { cmdTestIso.Flags().BoolVar(&console, "console", false, "Display qemu console to stdout") cmdTestIso.Flags().StringSliceVar(&pxeKernelArgs, "pxe-kargs", nil, "Additional kernel arguments for PXE") // FIXME move scenarioISOLiveLogin into the defaults once https://github.com/coreos/fedora-coreos-config/pull/339#issuecomment-613000050 is fixed - cmdTestIso.Flags().StringSliceVar(&scenarios, "scenarios", []string{scenarioPXEInstall, scenarioISOInstall}, fmt.Sprintf("Test scenarios (also available: %v)", []string{scenarioLegacyInstall, scenarioISOLiveLogin})) + cmdTestIso.Flags().StringSliceVar(&scenarios, "scenarios", []string{scenarioPXEInstall, scenarioISOInstall}, fmt.Sprintf("Test scenarios (also available: %v)", []string{scenarioLegacyInstall, scenarioISOLiveLogin, scenarioISOOfflineInstall})) cmdTestIso.Args = cobra.ExactArgs(0) root.AddCommand(cmdTestIso) @@ -172,6 +177,7 @@ func runTestIso(cmd *cobra.Command, args []string) error { } if noiso || nolive { delete(targetScenarios, scenarioISOInstall) + delete(targetScenarios, scenarioISOOfflineInstall) delete(targetScenarios, scenarioISOLiveLogin) } @@ -241,11 +247,22 @@ func runTestIso(cmd *cobra.Command, args []string) error { } ranTest = true instIso := baseInst // Pretend this is Rust and I wrote .copy() - if err := testLiveIso(instIso, completionfile); err != nil { + if err := testLiveIso(instIso, completionfile, false); err != nil { return err } printSuccess(scenarioISOInstall) } + if _, ok := targetScenarios[scenarioISOOfflineInstall]; ok { + if kola.CosaBuild.Meta.BuildArtifacts.LiveIso == nil { + return fmt.Errorf("build %s has no live ISO", kola.CosaBuild.Meta.Name) + } + ranTest = true + instIso := baseInst // Pretend this is Rust and I wrote .copy() + if err := testLiveIso(instIso, completionfile, true); err != nil { + return err + } + printSuccess(scenarioISOOfflineInstall) + } if _, ok := targetScenarios[scenarioISOLiveLogin]; ok { if kola.CosaBuild.Meta.BuildArtifacts.LiveIso == nil { return fmt.Errorf("build %s has no live ISO", kola.CosaBuild.Meta.Name) @@ -302,7 +319,7 @@ func printSuccess(mode string) { if kola.QEMUOptions.Native4k { metaltype = "metal4k" } - fmt.Printf("Successfully tested scenario:%s for %s on %s (%s)\n", mode, kola.CosaBuild.Meta.OstreeVersion, kola.QEMUOptions.Firmware, metaltype) + fmt.Printf("Successfully tested scenario %s for %s on %s (%s)\n", mode, kola.CosaBuild.Meta.OstreeVersion, kola.QEMUOptions.Firmware, metaltype) } func testPXE(inst platform.Install) error { @@ -336,7 +353,7 @@ func testPXE(inst platform.Install) error { return awaitCompletion(mach.QemuInst, completionChannel, []string{signalCompleteString}) } -func testLiveIso(inst platform.Install, completionfile string) error { +func testLiveIso(inst platform.Install, completionfile string, offline bool) error { inst.Builder = newQemuBuilder(false) completionChannel, err := inst.Builder.VirtioChannelRead("testisocompletion") if err != nil { @@ -389,7 +406,7 @@ func testLiveIso(inst platform.Install, completionfile string) error { }, } - mach, err := inst.InstallViaISOEmbed(nil, liveConfig, targetConfig) + mach, err := inst.InstallViaISOEmbed(nil, liveConfig, targetConfig, offline) if err != nil { return errors.Wrapf(err, "running iso install") } diff --git a/mantle/platform/metal.go b/mantle/platform/metal.go index 62a6c44216..234791b2c8 100644 --- a/mantle/platform/metal.go +++ b/mantle/platform/metal.go @@ -501,7 +501,7 @@ func generatePointerIgnitionString(spec2 bool, target string) string { return string(buf) } -func (inst *Install) InstallViaISOEmbed(kargs []string, liveIgnition, targetIgnition ignv3types.Config) (*InstalledMachine, error) { +func (inst *Install) InstallViaISOEmbed(kargs []string, liveIgnition, targetIgnition ignv3types.Config, offline bool) (*InstalledMachine, error) { if inst.CosaBuild.Meta.BuildArtifacts.Metal == nil { return nil, fmt.Errorf("Build %s must have a `metal` artifact", inst.CosaBuild.Meta.OstreeVersion) } @@ -549,18 +549,36 @@ func (inst *Install) InstallViaISOEmbed(kargs []string, liveIgnition, targetIgni return nil, errors.Wrapf(err, "setting up metal image") } - mux := http.NewServeMux() - mux.Handle("/", http.FileServer(http.Dir(tempdir))) - listener, err := net.Listen("tcp", ":0") - if err != nil { - return nil, err + var srcOpt string + var serializedTargetConfig string + if offline { + srcOpt = "--offline" + // we want to test that a full offline install works; that includes the + // final installed host booting offline + serializedTargetConfig = dataurl.EncodeBytes([]byte(renderedTarget)) + } else { + mux := http.NewServeMux() + mux.Handle("/", http.FileServer(http.Dir(tempdir))) + listener, err := net.Listen("tcp", ":0") + if err != nil { + return nil, err + } + port := listener.Addr().(*net.TCPAddr).Port + // Yeah this leaks + go func() { + http.Serve(listener, mux) + }() + baseurl := fmt.Sprintf("http://%s:%d", defaultQemuHostIPv4, port) + srcOpt = fmt.Sprintf("--image-url %s/%s", baseurl, metalname) + + // In this case; the target config is jut a tiny wrapper that wants to + // fetch our hosted target.ign config + + // TODO also use https://github.com/coreos/coreos-installer/issues/118#issuecomment-585572952 + // when it arrives + pointerIgnitionStr := generatePointerIgnitionString(inst.IgnitionSpec2, baseurl+"/target.ign") + serializedTargetConfig = dataurl.EncodeBytes([]byte(pointerIgnitionStr)) } - port := listener.Addr().(*net.TCPAddr).Port - // Yeah this leaks - go func() { - http.Serve(listener, mux) - }() - baseurl := fmt.Sprintf("http://%s:%d", defaultQemuHostIPv4, port) insecureOpt := "" if inst.Insecure { @@ -574,16 +592,12 @@ Wants=network-online.target [Service] RemainAfterExit=yes Type=oneshot -ExecStart=/usr/bin/coreos-installer install --image-url %s/%s --ignition %s %s %s +ExecStart=/usr/bin/coreos-installer install %s --ignition %s %s %s StandardOutput=kmsg+console StandardError=kmsg+console [Install] WantedBy=multi-user.target -`, baseurl, metalname, pointerIgnitionPath, insecureOpt, targetDevice) - // TODO also use https://github.com/coreos/coreos-installer/issues/118#issuecomment-585572952 - // when it arrives - pointerIgnitionStr := generatePointerIgnitionString(inst.IgnitionSpec2, baseurl+"/target.ign") - pointerIgnitionEnc := dataurl.EncodeBytes([]byte(pointerIgnitionStr)) +`, srcOpt, pointerIgnitionPath, insecureOpt, targetDevice) mode := 0644 rebootUnitP := string(rebootUnit) installerConfig := ignv3types.Config{ @@ -612,7 +626,7 @@ WantedBy=multi-user.target }, FileEmbedded1: ignv3types.FileEmbedded1{ Contents: ignv3types.FileContents{ - Source: &pointerIgnitionEnc, + Source: &serializedTargetConfig, }, Mode: &mode, }, @@ -653,6 +667,10 @@ WantedBy=multi-user.target qemubuilder := inst.Builder qemubuilder.AddInstallIso(isoEmbeddedPath) + if offline { + qemubuilder.Append("-nic", "none") + } + qinst, err := qemubuilder.Exec() if err != nil { return nil, err diff --git a/src/cmd-buildextend-installer b/src/cmd-buildextend-installer index fb02a064ea..015ab14293 100755 --- a/src/cmd-buildextend-installer +++ b/src/cmd-buildextend-installer @@ -43,6 +43,9 @@ parser = argparse.ArgumentParser() parser.add_argument("--build", help="Build ID") parser.add_argument("--force", action='store_true', default=False, help="Overwrite previously generated installer") +# Temporary switch while we ratchet osmet in place +parser.add_argument("--osmet", action='store_true', default=False, + help="Generate osmet file") args = parser.parse_args() # Identify the builds and target the latest build if none provided @@ -57,19 +60,13 @@ squashfs_compression = image_yaml.get('squashfs-compression', 'zstd') # Hacky mode switch, until we can drop support for the installer images is_live = os.path.basename(sys.argv[0]).endswith('-live') -is_fulliso = os.path.basename(sys.argv[0]).endswith('-fulliso') -if is_fulliso: - is_live = True - image_type = 'fulliso' -elif is_live: +if is_live: image_type = 'live' else: image_type = 'installer' meta_keys = {k: 'live-' + k if is_live else k for k in ('iso', 'kernel', 'initramfs')} config_src = image_type -if image_type == 'fulliso': - config_src = 'live' srcdir_prefix = f"src/config/{config_src}/" if not os.path.isdir(srcdir_prefix): @@ -142,12 +139,11 @@ def generate_iso(): tmpisofile = os.path.join(tmpdir, iso_name) img_metal_obj = buildmeta['images'].get('metal') - img_metal = None - if img_metal_obj is not None: - img_metal = os.path.join(builddir, img_metal_obj['path']) - img_src = img_metal - else: - img_src = os.path.join(builddir, buildmeta['images']['qemu']['path']) + if img_metal_obj is None: + raise Exception("ISO generation requires `metal` image") + + img_metal = os.path.join(builddir, img_metal_obj['path']) + img_metal_checksum = img_metal_obj['sha256'] # Find the directory under `/usr/lib/modules/` where the # kernel/initrd live. It will be the 2nd entity output by @@ -174,42 +170,26 @@ def generate_iso(): elif image_type == 'installer': initramfs_cpio_ext = os.path.join(tmpdir, 'legacy-stamp-initramfs') generate_initramfs_stampfile(tmpdir, initramfs_cpio_ext, 'etc/coreos-legacy-installer-initramfs') - if is_fulliso: - tmp_initramfs = os.path.join(tmpdir, 'initramfs') - - if img_metal is None: - raise Exception("fulliso requires `metal` image") - metal_dest_basename = os.path.basename(img_metal) + '.xz' - metal_dest = os.path.join(tmpisoroot, metal_dest_basename) - with open(metal_dest, 'wb') as f: - run_verbose(['xz', '-c9', '-T2', img_metal], stdout=f) - os.symlink(metal_dest_basename, os.path.join(tmpisoroot, 'image-metal.xz')) - - # In the fulliso case, we copy the squashfs to the ISO root. - print(f'Compressing squashfs with {squashfs_compression}') - run_verbose(['/usr/lib/coreos-assembler/gf-mksquashfs', - img_src, os.path.join(tmpisoroot, 'root.squashfs'), squashfs_compression]) - # Just pad with NUL bytes for the ISO image. We'll truncate the - # padding off again when copying the PXE initrd. - with open(tmp_initramfs, 'wb') as fdst: - with open(initramfs, 'rb') as fsrc: - shutil.copyfileobj(fsrc, fdst) - with open(initramfs_cpio_ext, 'rb') as fsrc: - shutil.copyfileobj(fsrc, fdst) - fdst.write(bytes(initrd_ignition_padding)) - os.rename(tmp_initramfs, initramfs) - elif is_live: + if is_live: tmp_squashfs = os.path.join(tmpdir, 'root.squashfs') tmp_cpio = os.path.join(tmpdir, 'root.cpio') tmp_initramfs = os.path.join(tmpdir, 'initramfs') + cpio_input_files = [os.path.basename(tmp_squashfs)] + if args.osmet: + tmp_osmet = os.path.join(tmpdir, img_metal_obj['path'] + '.osmet') + cpio_input_files += [os.path.basename(tmp_osmet)] + print(f'Generating osmet file') + run_verbose(['/usr/lib/coreos-assembler/osmet-pack', + img_metal, tmp_osmet, img_metal_checksum]) print(f'Compressing squashfs with {squashfs_compression}') run_verbose(['/usr/lib/coreos-assembler/gf-mksquashfs', - img_src, tmp_squashfs, squashfs_compression]) + img_metal, tmp_squashfs, squashfs_compression]) + # create a CPIO layer which holds the root squashfs and osmet binary run_verbose(['cpio', '-o', '-H', 'newc', '-R', 'root:root', '--quiet', '--reproducible', '--force-local', '-D', os.path.dirname(tmp_squashfs), '-O', tmp_cpio], - input=os.path.basename(tmp_squashfs).encode()) + input=('\n'.join(cpio_input_files).encode())) # Compression is redundant but the kernel requires it run_verbose(['gzip', '-1', tmp_cpio]) @@ -238,7 +218,7 @@ def generate_iso(): # Read and filter kernel arguments for substituting into ISO bootloader result = run_verbose(['/usr/lib/coreos-assembler/gf-get-kargs', - img_src], stdout=subprocess.PIPE, text=True) + img_metal], stdout=subprocess.PIPE, text=True) kargs_array = [karg for karg in result.stdout.split() if karg.split('=')[0] not in live_exclude_kargs] kargs_array.append(f"coreos.liveiso={volid}") diff --git a/src/osmet-pack b/src/osmet-pack new file mode 100755 index 0000000000..27fd85b6ea --- /dev/null +++ b/src/osmet-pack @@ -0,0 +1,69 @@ +#!/bin/bash +set -euo pipefail + +if [ ! -f /etc/cosa-supermin ]; then + dn=$(dirname "$0") + # shellcheck source=src/cmdlib.sh + . "${dn}"/cmdlib.sh + + img_src=$1; shift + osmet_dest=$1; shift + checksum=$1; shift + coreinst=${1:-${OSMET_PACK_COREOS_INSTALLER:-}} + + workdir=$(pwd) + TMPDIR=tmp/tmp-osmet-pack + rm -rf "${TMPDIR}" + mkdir -p "${TMPDIR}" + + if [[ $img_src == *.gz || $img_src == *.xz ]]; then + img="$(basename "$img_src")" + fatal "Cannot pack osmet from $img; not an uncompressed image" + fi + + set -- "${TMPDIR}/osmet.bin" "${checksum}" + if [ -n "${coreinst:-}" ]; then + cp "${coreinst}" "${TMPDIR}/coreos-installer" + set -- "$@" "${TMPDIR}/coreos-installer" + fi + + # stamp it with "osmet" serial so we find it easily in the VM + runvm -drive "if=none,id=osmet,format=raw,readonly=on,file=${img_src}" \ + -device virtio-blk,serial=osmet,drive=osmet -- \ + /usr/lib/coreos-assembler/osmet-pack "$@" + + mv "${TMPDIR}/osmet.bin" "${osmet_dest}" + rm -rf "${TMPDIR}" + + exit 0 +fi + +# This runs inside supermin + +osmet_dest=$1; shift +checksum=$1; shift +coreinst=${1:-} + +set -x + +# we want /dev/disk symlinks for coreos-installer +/usr/lib/systemd/systemd-udevd --daemon +/usr/sbin/udevadm trigger --settle + +rootfs=/dev/disk/by-id/virtio-osmet-part4 + +mkdir -p /sysroot +mount -o ro "${rootfs}" /sysroot +osname=$(ls /sysroot/ostree/deploy) +deploydir=$(find "/sysroot/ostree/deploy/$osname/deploy" -mindepth 1 -maxdepth 1 -type d) +# shellcheck disable=SC1090 +description=$(. "${deploydir}/etc/os-release" && echo "${PRETTY_NAME}") + +if [ -z "${coreinst}" ]; then + coreinst=${deploydir}/usr/bin/coreos-installer +fi + +RUST_BACKTRACE=full "${coreinst}" osmet pack /dev/disk/by-id/virtio-osmet \ + --description "${description}" \ + --checksum "${checksum}" \ + --output "${osmet_dest}"