diff --git a/mantle/cmd/kola/testiso.go b/mantle/cmd/kola/testiso.go index 2851ec1c6b..52a71c46c0 100644 --- a/mantle/cmd/kola/testiso.go +++ b/mantle/cmd/kola/testiso.go @@ -70,6 +70,8 @@ const ( scenarioPXEInstall = "pxe-install" scenarioISOInstall = "iso-install" + scenarioMinISOInstall = "miniso-install" + scenarioPXEOfflineInstall = "pxe-offline-install" scenarioISOOfflineInstall = "iso-offline-install" @@ -82,6 +84,7 @@ var allScenarios = map[string]bool{ scenarioPXEOfflineInstall: true, scenarioISOInstall: true, scenarioISOOfflineInstall: true, + scenarioMinISOInstall: true, scenarioISOLiveLogin: true, scenarioISOAsDisk: true, } @@ -187,7 +190,8 @@ func init() { cmdTestIso.Flags().BoolVar(&console, "console", false, "Connect qemu console to terminal, turn off automatic initramfs failure checking") cmdTestIso.Flags().BoolVar(&pxeAppendRootfs, "pxe-append-rootfs", false, "Append rootfs to PXE initrd instead of fetching at runtime") cmdTestIso.Flags().StringSliceVar(&pxeKernelArgs, "pxe-kargs", nil, "Additional kernel arguments for PXE") - cmdTestIso.Flags().StringSliceVar(&scenarios, "scenarios", []string{scenarioPXEInstall, scenarioISOOfflineInstall, scenarioPXEOfflineInstall, scenarioISOLiveLogin, scenarioISOAsDisk}, fmt.Sprintf("Test scenarios (also available: %v)", []string{scenarioISOInstall})) + // XXX: add scenarioMinISOInstall to the default set once the feature is stable + cmdTestIso.Flags().StringSliceVar(&scenarios, "scenarios", []string{scenarioPXEInstall, scenarioISOOfflineInstall, scenarioPXEOfflineInstall, scenarioISOLiveLogin, scenarioISOAsDisk}, fmt.Sprintf("Test scenarios (also available: %v)", []string{scenarioISOInstall, scenarioMinISOInstall})) cmdTestIso.Args = cobra.ExactArgs(0) root.AddCommand(cmdTestIso) @@ -299,6 +303,7 @@ func runTestIso(cmd *cobra.Command, args []string) error { if noiso { delete(targetScenarios, scenarioISOInstall) delete(targetScenarios, scenarioISOOfflineInstall) + delete(targetScenarios, scenarioMinISOInstall) delete(targetScenarios, scenarioISOLiveLogin) } @@ -375,7 +380,7 @@ func runTestIso(cmd *cobra.Command, args []string) error { } ranTest = true instIso := baseInst // Pretend this is Rust and I wrote .copy() - if err := testLiveIso(ctx, instIso, filepath.Join(outputDir, scenarioISOInstall), false); err != nil { + if err := testLiveIso(ctx, instIso, filepath.Join(outputDir, scenarioISOInstall), false, false); err != nil { return errors.Wrapf(err, "scenario %s", scenarioISOInstall) } printSuccess(scenarioISOInstall) @@ -386,7 +391,7 @@ func runTestIso(cmd *cobra.Command, args []string) error { } ranTest = true instIso := baseInst // Pretend this is Rust and I wrote .copy() - if err := testLiveIso(ctx, instIso, filepath.Join(outputDir, scenarioISOOfflineInstall), true); err != nil { + if err := testLiveIso(ctx, instIso, filepath.Join(outputDir, scenarioISOOfflineInstall), true, false); err != nil { return errors.Wrapf(err, "scenario %s", scenarioISOOfflineInstall) } printSuccess(scenarioISOOfflineInstall) @@ -417,6 +422,17 @@ func runTestIso(cmd *cobra.Command, args []string) error { fmt.Printf("%s unsupported on %s; skipping\n", scenarioISOAsDisk, system.RpmArch()) } } + if _, ok := targetScenarios[scenarioMinISOInstall]; 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(ctx, instIso, filepath.Join(outputDir, scenarioMinISOInstall), false, true); err != nil { + return errors.Wrapf(err, "scenario %s", scenarioMinISOInstall) + } + printSuccess(scenarioMinISOInstall) + } if !ranTest { panic("Nothing was tested!") @@ -552,7 +568,7 @@ func testPXE(ctx context.Context, inst platform.Install, outdir string, offline return awaitCompletion(ctx, mach.QemuInst, outdir, completionChannel, mach.BootStartedErrorChannel, []string{liveOKSignal, signalCompleteString}) } -func testLiveIso(ctx context.Context, inst platform.Install, outdir string, offline bool) error { +func testLiveIso(ctx context.Context, inst platform.Install, outdir string, offline, minimal bool) error { tmpd, err := ioutil.TempDir("", "kola-testiso") if err != nil { return err @@ -589,7 +605,7 @@ func testLiveIso(ctx context.Context, inst platform.Install, outdir string, offl targetConfig.AddSystemdUnit("coreos-test-installer-multipathed.service", multipathedRoot, conf.Enable) } - mach, err := inst.InstallViaISOEmbed(nil, liveConfig, targetConfig, outdir, offline) + mach, err := inst.InstallViaISOEmbed(nil, liveConfig, targetConfig, outdir, offline, minimal) if err != nil { return errors.Wrapf(err, "running iso install") } diff --git a/mantle/platform/metal.go b/mantle/platform/metal.go index be77226e4d..837eaf94e5 100644 --- a/mantle/platform/metal.go +++ b/mantle/platform/metal.go @@ -541,7 +541,7 @@ func (inst *Install) runPXE(kern *kernelSetup, offline bool) (*InstalledMachine, return &instmachine, nil } -func (inst *Install) InstallViaISOEmbed(kargs []string, liveIgnition, targetIgnition conf.Conf, outdir string, offline bool) (*InstalledMachine, error) { +func (inst *Install) InstallViaISOEmbed(kargs []string, liveIgnition, targetIgnition conf.Conf, outdir string, offline, minimal bool) (*InstalledMachine, error) { if !inst.Native4k && inst.CosaBuild.Meta.BuildArtifacts.Metal == nil { return nil, fmt.Errorf("Build %s must have a `metal` artifact", inst.CosaBuild.Meta.OstreeVersion) } else if inst.Native4k && inst.CosaBuild.Meta.BuildArtifacts.Metal4KNative == nil { @@ -550,6 +550,9 @@ func (inst *Install) InstallViaISOEmbed(kargs []string, liveIgnition, targetIgni if inst.CosaBuild.Meta.BuildArtifacts.LiveIso == nil { return nil, fmt.Errorf("Build %s must have a live ISO", inst.CosaBuild.Meta.Name) } + if minimal && offline { // ideally this'd be one enum parameter + panic("Can't run minimal install offline") + } // XXX: we do support this now, via `coreos-installer iso kargs` if len(inst.kargs) > 0 { @@ -623,7 +626,34 @@ func (inst *Install) InstallViaISOEmbed(kargs []string, liveIgnition, targetIgni http.Serve(listener, mux) }() baseurl := fmt.Sprintf("http://%s:%d", defaultQemuHostIPv4, port) - srcOpt = fmt.Sprintf("--image-url %s/%s", baseurl, metalname) + + // This is subtle but: for the minimal case, while we need networking to fetch the + // rootfs, the primary install flow will still rely on osmet. So let's keep srcOpt + // as "" to exercise that path. In the future, this could be a separate scenario + // (likely we should drop the "offline" naming and have a "remote" tag on the + // opposite scenarios instead which fetch the metal image, so then we'd have + // "[min]iso-install" and "[min]iso-remote-install"). + if !minimal { + srcOpt = fmt.Sprintf("--image-url %s/%s", baseurl, metalname) + } + + if minimal { + minisopath := filepath.Join(tempdir, "minimal.iso") + // This is obviously also available in the build dir, but to be realistic, + // let's take it from --rootfs-output + rootfs_path := filepath.Join(tempdir, "rootfs.img") + // Ideally we'd use the coreos-installer of the target build here, because it's part + // of the test workflow, but that's complex... Sadly, probably easiest is to spin up + // a VM just to get the minimal ISO. + cmd := exec.Command("coreos-installer", "iso", "extract", "minimal-iso", srcisopath, + "--output", minisopath, "--rootfs-output", rootfs_path, + "--rootfs-url", baseurl+"/rootfs.img") + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return nil, errors.Wrapf(err, "running coreos-installer iso extract minimal") + } + srcisopath = minisopath + } // In this case; the target config is jut a tiny wrapper that wants to // fetch our hosted target.ign config diff --git a/src/cmd-buildextend-live b/src/cmd-buildextend-live index d392bf4325..7ac8f9b394 100755 --- a/src/cmd-buildextend-live +++ b/src/cmd-buildextend-live @@ -51,6 +51,8 @@ parser.add_argument("--fast", action='store_true', default=False, help="Reduce compression for development (FCOS only)") parser.add_argument("--force", action='store_true', default=False, help="Overwrite previously generated installer") +parser.add_argument("--miniso", action='store_true', default=False, + help="Enable minimal ISO packing (temporary)") args = parser.parse_args() # Identify the builds and target the latest build if none provided @@ -117,9 +119,11 @@ for d in (tmpdir, tmpisoroot, tmpisocoreos, tmpisoimages, tmpisoimagespxe, tmpisoisolinux, tmpinitrd_base, tmpinitrd_rootfs): os.mkdir(d) -# Number of padding bytes at the end of the ISO initramfs for embedding -# an Ignition config -initrd_ignition_padding = 256 * 1024 +# Size of file used to embed an Ignition config within a CPIO. +ignition_img_size = 256 * 1024 + +# Size of the file used to embed miniso data. +miniso_data_file_size = 16 * 1024 # The kernel requires that uncompressed cpio archives appended to an initrd @@ -265,9 +269,9 @@ def generate_iso(): with open(stamppath, 'w') as fh: fh.write(args.build + '\n') - # Add Ignition padding file to ISO image + # Add placeholder for Ignition CPIO file with open(os.path.join(tmpisoimages, ignition_img), 'wb') as fdst: - fdst.write(bytes(initrd_ignition_padding)) + fdst.write(bytes(ignition_img_size)) # Add osmet files tmp_osmet = os.path.join(tmpinitrd_rootfs, img_metal_obj['path'] + '.osmet') @@ -582,19 +586,32 @@ boot fh.write('\n') # Define inputs and outputs - genisoargs += ['-o', tmpisofile, tmpisoroot] + genisoargs_final = genisoargs + ['-o', tmpisofile, tmpisoroot] + + if args.miniso: + miniso_data = os.path.join(tmpisocoreos, "miniso.dat") + with open(miniso_data, 'wb') as f: + f.truncate(miniso_data_file_size) - run_verbose(genisoargs) + run_verbose(genisoargs_final) # Add MBR, and GPT with ESP, for x86_64 BIOS/UEFI boot when ISO is # copied to a USB stick if basearch == "x86_64": run_verbose(['/usr/bin/isohybrid', '--uefi', tmpisofile]) + if args.miniso: + genisoargs_minimal = genisoargs + ['-o', f'{tmpisofile}.minimal', tmpisoroot] + os.unlink(iso_rootfs) + os.unlink(miniso_data) + run_verbose(genisoargs_minimal) + if basearch == "x86_64": + run_verbose(['/usr/bin/isohybrid', '--uefi', f'{tmpisofile}.minimal']) + isoinfo = run_verbose(['isoinfo', '-lR', '-i', tmpisofile], stdout=subprocess.PIPE, text=True).stdout - # We've already created a file in the ISO with initrd_ignition_padding + # We've already created a file in the ISO with ignition_img_size # bytes of zeroes. Find the byte offset of that file within the ISO # image and write it into a custom header at the end of the ISO 9660 # System Area, which is 32 KB at the start of the image "reserved for @@ -642,8 +659,8 @@ boot with open(tmpisofile, 'r+b') as isofh: # Verify that the calculated byte range is empty isofh.seek(offset) - if isofh.read(initrd_ignition_padding) != bytes(initrd_ignition_padding): - raise Exception(f'ISO image {initrd_ignition_padding} bytes at {offset} are not zero') + if isofh.read(ignition_img_size) != bytes(ignition_img_size): + raise Exception(f'ISO image {ignition_img_size} bytes at {offset} are not zero') # Write header at the end of the System Area isofh.seek(ISO_SYSTEM_AREA_SIZE - (struct.calcsize(INITRDFMT) + @@ -658,8 +675,13 @@ boot offsets[i + 1] = file_offset_in_iso(isoinfo, os.path.basename(fn)) + offset_in_file isofh.write(struct.pack(KARGSFMT, b'coreKarg', karg_embed_area_length, *offsets)) # Magic number + offset + length - isofh.write(struct.pack(INITRDFMT, b'coreiso+', offset, initrd_ignition_padding)) - print(f'Embedded {initrd_ignition_padding} bytes Ignition config space at {offset}') + isofh.write(struct.pack(INITRDFMT, b'coreiso+', offset, ignition_img_size)) + print(f'Embedded {ignition_img_size} bytes Ignition config space at {offset}') + + if args.miniso: + # this consumes the minimal image + run_verbose(['coreos-installer', 'iso', 'extract', 'pack-minimal-iso', + tmpisofile, f'{tmpisofile}.minimal', "--consume"]) buildmeta['images'].update({ 'live-iso': {