cmd

package
v0.10.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 24, 2025 License: Apache-2.0 Imports: 45 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var BuildISOCmd = cli.Command{
	Name:    "build-iso",
	Aliases: []string{"bi"},
	Usage:   "Builds an ISO from a container image or github release",
	Flags: []cli.Flag{
		&cli.StringFlag{
			Name:    "cloud-config",
			Aliases: []string{"c"},
			Usage:   "The cloud config to embed in the ISO",
		},
		&cli.StringFlag{
			Name:    "override-name",
			Aliases: []string{"n"},
			Value:   "",
			Usage:   "Overrride default ISO file name",
		},
		&cli.StringFlag{
			Name:    "output",
			Aliases: []string{"o"},
			Usage:   "Output directory (defaults to current directory)",
		},
		&cli.BoolFlag{
			Name:  "date",
			Value: false,
			Usage: "Adds a date suffix into the generated ISO file",
		},
		&cli.StringFlag{
			Name:  "overlay-rootfs",
			Usage: "Path of the overlayed rootfs data",
		},
		&cli.StringFlag{
			Name:  "overlay-uefi",
			Usage: "Path of the overlayed uefi data",
		},
		&cli.StringFlag{
			Name:  "overlay-iso",
			Usage: "Path of the overlayed iso data",
		},
		&cli.StringFlag{
			Name:    "loglevel",
			Aliases: []string{"l"},
			Usage:   "Set the log level",
			Value:   "info",
		},
	},
	ArgsUsage: "<source>",
	Action: func(ctx *cli.Context) error {
		internal.Log = sdkTypes.NewKairosLogger("aurora", ctx.String("loglevel"), false)
		source := ctx.Args().Get(0)
		if source == "" {

			ctx.Command.Subcommands = nil
			cli.ShowCommandHelp(ctx, ctx.Command.Name)
			fmt.Println("")

			return errors.New("no source defined")
		}

		cloudConfig := ""
		var err error
		if ctx.String("cloud-config") != "" {

			cloudConfig, err = config.ReadCloudConfig(ctx.String("cloud-config"), map[string]interface{}{})
			if err != nil {
				return fmt.Errorf("reading cloud config: %w", err)
			}
		}
		r := schema.ReleaseArtifact{
			ContainerImage: source,
		}
		isoOptions := schema.ISO{
			OverrideName:  ctx.String("override-name"),
			IncludeDate:   ctx.Bool("date"),
			OverlayISO:    ctx.String("overlay-iso"),
			OverlayRootfs: ctx.String("overlay-rootfs"),
			OverlayUEFI:   ctx.String("overlay-uefi"),
		}

		if err := validateISOOptions(isoOptions); err != nil {
			return err
		}

		c := schema.Config{
			ISO:         isoOptions,
			State:       ctx.String("output"),
			CloudConfig: cloudConfig,
		}

		if c.State == "" {
			c.State = "/tmp/auroraboot"
		}

		d := deployer.NewDeployer(c, r, herd.EnableInit)
		for _, step := range []func() error{
			d.PrepDirs,
			d.StepCopyCloudConfig,
			d.StepDumpSource,
			d.StepGenISO,
		} {
			if err := step(); err != nil {
				return err
			}
		}

		if err := d.Run(ctx.Context); err != nil {
			return err
		}

		err = d.CollectErrors()

		errCleanup := d.CleanTmpDirs()
		if errCleanup != nil {

			err = multierror.Append(err, errCleanup)
		}

		return err
	},
}
View Source
var BuildUKICmd = cli.Command{
	Name:      "build-uki",
	Aliases:   []string{"bu"},
	Usage:     "Builds a UKI artifact from a container image",
	ArgsUsage: "<source>",
	Description: "Build a UKI artifact from a container image\n\n" +
		"SourceImage - should be provided as uri in following format <sourceType>:<sourceName>\n" +
		"    * <sourceType> - might be [\"dir\", \"file\", \"oci\", \"docker\"], as default is \"docker\"\n" +
		"    * <sourceName> - is path to file or directory, image name with tag version\n" +
		"The following files are expected inside the public keys directory for SecureBoot auto enroll:\n" +
		"    - db.auth\n" +
		"    - KEK.auth\n" +
		"    - PK.auth\n",
	Flags: []cli.Flag{
		&cli.StringFlag{
			Name:    "name",
			Aliases: []string{"n"},
			Value:   "",
			Usage:   "Basename of the generated artifact (ignored for uki output type)",
		},
		&cli.StringFlag{
			Name:    "output-dir",
			Aliases: []string{"d"},
			Value:   ".",
			Usage:   "Output dir for artifact",
		},
		&cli.StringFlag{
			Name:    "output-type",
			Aliases: []string{"t"},
			Value:   string(constants.DefaultOutput),
			Usage:   fmt.Sprintf("Artifact output type [%s]", strings.Join(constants.OutPutTypes(), ", ")),
		},
		&cli.StringFlag{
			Name:    "overlay-rootfs",
			Aliases: []string{"o"},
			Usage:   "Dir with files to be applied to the system rootfs. All the files under this dir will be copied into the rootfs of the uki respecting the directory structure under the dir.",
		},
		&cli.StringFlag{
			Name:    "overlay-iso",
			Aliases: []string{"i"},
			Usage:   "Dir with files to be copied to the ISO rootfs.",
		},
		&cli.StringFlag{
			Name:  "boot-branding",
			Value: "Kairos",
			Usage: "Boot title branding",
		},
		&cli.BoolFlag{
			Name:  "include-version-in-config",
			Value: false,
			Usage: "Include the OS version in the .config file",
		},
		&cli.BoolFlag{
			Name:  "include-cmdline-in-config",
			Value: false,
			Usage: "Include the cmdline in the .config file. Only the extra values are included.",
		},
		&cli.StringSliceFlag{
			Name:    "extra-cmdline",
			Aliases: []string{"c"},
			Usage:   "Add extra efi files with this cmdline for the default 'norole' artifacts. This creates efi files with the default cmdline and extra efi files with the default+provided cmdline.",
		},
		&cli.StringFlag{
			Name:    "extend-cmdline",
			Aliases: []string{"x"},
			Usage:   "Extend the default cmdline for the default 'norole' artifacts. This creates efi files with the default+provided cmdline.",
		},
		&cli.StringSliceFlag{
			Name:    "single-efi-cmdline",
			Aliases: []string{"s"},
			Usage:   "Add one extra efi file with the default+provided cmdline. The syntax is '--single-efi-cmdline \"My Entry: cmdline,options,here\"'. The boot entry name is the text under which it appears in systemd-boot menu.",
		},
		&cli.StringFlag{
			Name:     "public-keys",
			Usage:    "Directory with the public keys for auto enrolling them under Secure Boot",
			Required: false,
		},
		&cli.StringFlag{
			Name:     "tpm-pcr-private-key",
			Usage:    "Private key for signing the EFI PCR policy, Can be a PKCS11 URI or a PEM file",
			Required: true,
		},
		&cli.StringFlag{
			Name:     "sb-key",
			Usage:    "Private key to sign the EFI files for SecureBoot",
			Required: true,
		},
		&cli.StringFlag{
			Name:     "sb-cert",
			Usage:    "Certificate to sign the EFI files for SecureBoot",
			Required: true,
		},
		&cli.StringFlag{
			Name:    "default-entry",
			Aliases: []string{"e"},
			Usage:   "Default entry selected in the boot menu. Supported glob wildcard patterns are \"?\", \"*\", and \"[...]\". If not selected, the default entry with install-mode is selected.",
		},
		&cli.Int64Flag{
			Name:  "efi-size-warn",
			Value: 1024,
			Usage: "EFI file size warning threshold in megabytes",
		},
		&cli.StringFlag{
			Name:  "secure-boot-enroll",
			Value: "if-safe",
			Usage: "The value of secure-boot-enroll option of systemd-boot. Possible values: off|manual|if-safe|force. Minimum systemd version: 253. Docs: https://manpages.debian.org/experimental/systemd-boot/loader.conf.5.en.html. !! Danger: this feature might soft-brick your device if used improperly !!",
		},
		&cli.StringFlag{
			Name:  "splash",
			Usage: "Path to the custom logo splash BMP file.",
		},
	},
	Before: func(ctx *cli.Context) error {

		if len(ctx.StringSlice("extra-cmdline")) > 0 && ctx.String("extend-cmdline") != "" {
			return errors.New("extra-cmdline and extend-cmdline flags are mutually exclusive")
		}

		artifact := ctx.String("output-type")
		if artifact != string(constants.DefaultOutput) && artifact != string(constants.IsoOutput) && artifact != string(constants.ContainerOutput) {
			return fmt.Errorf("invalid output type: %s", artifact)
		}

		if overlayRootfs := ctx.String("overlay-rootfs"); overlayRootfs != "" {
			ol, err := os.Stat(overlayRootfs)
			if err != nil {
				return fmt.Errorf("overlay-rootfs directory does not exist: %s", overlayRootfs)
			}
			if !ol.IsDir() {
				return fmt.Errorf("overlay-rootfs is not a directory: %s", overlayRootfs)
			}
		}

		if overlayIso := ctx.String("overlay-iso"); overlayIso != "" {
			ol, err := os.Stat(overlayIso)
			if err != nil {
				return fmt.Errorf("overlay directory does not exist: %s", overlayIso)
			}
			if !ol.IsDir() {
				return fmt.Errorf("overlay is not a directory: %s", overlayIso)
			}

			if artifact != string(constants.IsoOutput) {
				return fmt.Errorf("overlay-iso is only supported for iso artifacts")
			}
		}

		keysDir := ctx.String("public-keys")
		if keysDir != "" {
			_, err := os.Stat(keysDir)
			if err != nil {
				return fmt.Errorf("keys directory does not exist: %s", keysDir)
			}

			requiredFiles := []string{"db.auth", "KEK.auth", "PK.auth"}

			for _, file := range requiredFiles {
				_, err = os.Stat(filepath.Join(keysDir, file))
				if err != nil {
					return fmt.Errorf("keys directory does not contain required file: %s", file)
				}
			}
		} else {
			fmt.Println("Warning: public-keys directory is not set, Secure Boot auto enroll will not work. You can set it with --public-keys flag.")
		}

		tpmPCRPrivateKey := ctx.String("tpm-pcr-private-key")
		_, err := os.Stat(tpmPCRPrivateKey)
		if err != nil {
			return fmt.Errorf("tpm-pcr-private-key does not exist: %s", tpmPCRPrivateKey)
		}

		sbKey := ctx.String("sb-key")

		if !strings.Contains(sbKey, "pkcs11") {
			_, err = os.Stat(sbKey)
			if err != nil {
				return fmt.Errorf("sb-key does not exist: %s", sbKey)
			}
		}
		sbCert := ctx.String("sb-cert")
		_, err = os.Stat(sbCert)
		if err != nil {
			return fmt.Errorf("sb-cert does not exist: %s", sbCert)
		}

		return CheckRoot()
	},
	Action: func(ctx *cli.Context) error {
		args := ctx.Args()
		if args.Len() < 1 {
			return errors.New("no image provided")
		}

		logLevel := "info"
		if ctx.Bool("debug") {
			logLevel = "debug"
		}
		logger := sdkTypes.NewKairosLogger("auroraboot", logLevel, false)

		config := ops.NewConfig(
			ops.WithImageExtractor(v1.OCIImageExtractor{}),
			ops.WithLogger(logger),
		)

		if err := checkBuildUKIDeps(config.Arch); err != nil {
			return err
		}

		artifactsTempDir, err := os.MkdirTemp("", "auroraboot-build-uki-artifacts-")
		if err != nil {
			return err
		}
		defer os.RemoveAll(artifactsTempDir)

		logger.Info("Extracting image to a temporary directory")

		sourceDir, err := os.MkdirTemp("", "auroraboot-build-uki-")
		if err != nil {
			return err
		}

		if err = os.Chmod(sourceDir, 0755); err != nil {
			return err
		}

		imgSource, err := v1.NewSrcFromURI(args.Get(0))
		if err != nil {
			return fmt.Errorf("not a valid rootfs source image argument: %s", args.Get(0))
		}

		e := elemental.NewElemental(config)
		_, err = e.DumpSource(sourceDir, imgSource)
		defer os.RemoveAll(sourceDir)

		if overlayRootfs := ctx.String("overlay-rootfs"); overlayRootfs != "" {

			absolutePath, err := filepath.Abs(overlayRootfs)
			if err != nil {
				return fmt.Errorf("converting overlay-rootfs to absolute path: %w", err)
			}
			logger.Infof("Adding files from %s to rootfs", absolutePath)
			overlay, err := v1.NewSrcFromURI(fmt.Sprintf("dir:%s", absolutePath))
			if err != nil {
				return fmt.Errorf("error creating overlay image: %s", err)
			}
			if _, err = e.DumpSource(sourceDir, overlay); err != nil {
				return fmt.Errorf("error copying overlay image: %s", err)
			}
		}

		kairosVersion, err := findKairosVersion(sourceDir)
		if err != nil {
			return err
		}

		outputName := utils.NameFromRootfs(sourceDir)

		logger.Info("Creating additional directories in the rootfs")
		if err := setupDirectoriesAndFiles(sourceDir); err != nil {
			return err
		}

		logger.Info("Copying kernel")
		if err := copyKernel(sourceDir, artifactsTempDir); err != nil {
			return fmt.Errorf("copying kernel: %w", err)
		}

		if err := os.RemoveAll(filepath.Join(sourceDir, "boot")); err != nil {
			return fmt.Errorf("cleaning up the source directory: %w", err)
		}

		logger.Info("Creating an initramfs file")
		if err := createInitramfs(sourceDir, artifactsTempDir); err != nil {
			return err
		}

		extendCmdline := ctx.String("extend-cmdline")
		boodBranding := ctx.String("boot-branding")
		extraCmdlines := ctx.StringSlice("extra-cmdline")
		singleEfiCmdlines := ctx.StringSlice("single-efi-cmdline")

		entries := append(
			GetUkiCmdline(extendCmdline, boodBranding, extraCmdlines),
			GetUkiSingleCmdlines(boodBranding, singleEfiCmdlines, logger)...)

		for _, entry := range entries {
			logger.Info(fmt.Sprintf("Running ukify for cmdline: %s: %s", entry.Title, entry.Cmdline))

			logger.Infof("Generating: %s.efi", entry.FileName)

			stub, err := getEfiStub(config.Arch)
			if err != nil {
				return err
			}
			// Get systemd-boot info (we can sign it at the same time)
			var systemdBoot string
			var outputSystemdBootEfi string
			if utils.IsAmd64(config.Arch) {
				systemdBoot = constants.UkiSystemdBootx86
				outputSystemdBootEfi = constants.EfiFallbackNamex86
			} else if utils.IsArm64(config.Arch) {
				systemdBoot = constants.UkiSystemdBootArm
				outputSystemdBootEfi = constants.EfiFallbackNameArm
			} else {
				return fmt.Errorf("unsupported arch: %s", config.Arch)
			}

			if logger.GetLevel().String() == "debug" {
				slog.SetLogLoggerLevel(slog.LevelDebug)
			}
			builder := &uki.Builder{
				Arch:          config.Arch,
				Version:       kairosVersion,
				SdStubPath:    stub,
				KernelPath:    filepath.Join(artifactsTempDir, "vmlinuz"),
				InitrdPath:    filepath.Join(artifactsTempDir, "initrd"),
				Cmdline:       entry.Cmdline,
				OsRelease:     filepath.Join(sourceDir, "etc/os-release"),
				OutUKIPath:    entry.FileName + ".efi",
				PCRKey:        ctx.String("tpm-pcr-private-key"),
				SBKey:         ctx.String("sb-key"),
				SBCert:        ctx.String("sb-cert"),
				SdBootPath:    systemdBoot,
				OutSdBootPath: outputSystemdBootEfi,
				Splash:        ctx.String("splash"),
			}

			if err := os.Chdir(sourceDir); err != nil {
				return fmt.Errorf("changing to %s directory: %w", sourceDir, err)
			}

			if err := builder.Build(); err != nil {
				return err
			}

			logger.Info("Creating kairos and loader conf files")
			if err := createConfFiles(sourceDir, entry.Cmdline, entry.Title, entry.FileName, kairosVersion, ctx.Bool("include-version-in-config"), ctx.Bool("include-cmdline-in-config")); err != nil {
				return err
			}
		}

		if err := createSystemdConf(sourceDir, ctx.String("default-entry"), ctx.String("secure-boot-enroll")); err != nil {
			return err
		}

		switch ctx.String("output-type") {
		case string(constants.IsoOutput):
			var absolutePathIso string
			if overlayIsoDir := ctx.String("overlay-iso"); overlayIsoDir != "" {
				absolutePathIso, err = filepath.Abs(overlayIsoDir)
				if err != nil {
					return fmt.Errorf("converting overlay-iso to absolute path: %w", err)
				}
			}
			if err := createISO(e, sourceDir, ctx.String("output-dir"), absolutePathIso, ctx.String("public-keys"), outputName, ctx.String("name"), entries, logger); err != nil {
				return err
			}
		case string(constants.ContainerOutput):

			temp, err := os.MkdirTemp("", "uki-transient-*")
			if err != nil {
				return err
			}
			defer os.RemoveAll(temp)

			if err := createArtifact(sourceDir, temp, ctx.String("public-keys"), entries, logger); err != nil {
				return err
			}

			if err := createContainer(temp, ctx.String("output-dir"), ctx.String("name"), outputName, logger); err != nil {
				return err
			}
		case string(constants.DefaultOutput):
			if err := createArtifact(sourceDir, ctx.String("output-dir"), ctx.String("public-keys"), entries, logger); err != nil {
				return err
			}
		}

		logger.Infof("Done building %s at: %s", ctx.String("output-type"), ctx.String("output-dir"))

		return nil
	},
}

Use: "build-uki SourceImage", Short: "Build a UKI artifact from a container image",

View Source
var GenKeyCmd = cli.Command{
	Name:      "genkey",
	Aliases:   []string{"gk"},
	Usage:     "Generate secureboot keys under the uuid generated by NAME",
	ArgsUsage: "<name>",
	Flags: []cli.Flag{
		&cli.StringFlag{
			Name:    "output",
			Aliases: []string{"o"},
			Value:   "keys/",
			Usage:   "Output directory for the keys",
		},
		&cli.StringFlag{
			Name:    "expiration-in-days",
			Aliases: []string{"e"},
			Value:   "365",
			Usage:   "In how many days from today should the certificates expire",
		},
		&cli.BoolFlag{
			Name:  skipMicrosoftCertsFlag,
			Value: false,
			Usage: "When set to true, microsoft certs are not included in the KEK and db files. THIS COULD BRICK YOUR SYSTEM! (https://wiki.archlinux.org/title/Unified_Extensible_Firmware_Interface/Secure_Boot#Enrolling_Option_ROM_digests). Only use this if you are sure your hardware doesn't need the microsoft certs!",
		},
		&cli.StringFlag{
			Name:  customCertDirFlag,
			Usage: "Path to a directory containing custom certificates to enroll",
		},
	},
	Action: func(ctx *cli.Context) error {

		logger := sdkTypes.NewKairosLogger("auroraboot", "debug", false)

		skipMicrosoftCerts := ctx.Bool(skipMicrosoftCertsFlag)

		name := ctx.Args().Get(0)
		uuid := sbctl.CreateUUID()
		guid := efiutil.StringToGUID(string(uuid))
		output := ctx.String("output")
		if output == "" {
			return errors.New("output not set")
		}
		if err := os.MkdirAll(output, 0700); err != nil {
			return fmt.Errorf("Error creating output directory: %w", err)
		}

		customDerDir := ""
		var err error
		if customCertDir := ctx.String(customCertDirFlag); customCertDir != "" {
			customDerDir, err = prepareCustomDerDir(logger, customCertDir)
			if err != nil {
				return fmt.Errorf("Error preparing custom certs directory: %w", err)
			}
			defer os.RemoveAll(customDerDir)
		}

		for _, keyType := range []string{"PK", "KEK", "db"} {
			logger.Infof("Generating %s", keyType)
			key := filepath.Join(output, fmt.Sprintf("%s.key", keyType))
			pem := filepath.Join(output, fmt.Sprintf("%s.pem", keyType))
			der := filepath.Join(output, fmt.Sprintf("%s.der", keyType))

			args := []string{
				"req", "-nodes", "-x509", "-subj", fmt.Sprintf("/CN=%s-%s/", name, keyType),
				"-keyout", key,
				"-out", pem,
			}
			if expirationInDays := ctx.String("expiration-in-days"); expirationInDays != "" {
				args = append(args, "-days", expirationInDays)
			}
			cmd := exec.Command("openssl", args...)
			out, err := cmd.CombinedOutput()
			if err != nil {
				return fmt.Errorf("Error generating %s: %w / %s", keyType, err, string(out))
			}
			logger.Infof("%s generated at %s and %s", keyType, key, pem)

			logger.Infof("Converting %s.pem to DER", keyType)
			cmd = exec.Command(
				"openssl", "x509", "-outform", "DER", "-in", pem, "-out", der,
			)
			if out, err = cmd.CombinedOutput(); err != nil {
				return fmt.Errorf("Error generating %s: %w / %s", keyType, err, string(out))
			}
			logger.Infof("%s generated at %s", keyType, der)

			if err = generateAuthKeys(*guid, output, keyType, customDerDir, skipMicrosoftCerts); err != nil {
				return fmt.Errorf("Error generating auth keys: %w", err)
			}

			if customDerDir != "" && keyType != "PK" {
				err = appendCustomDerCerts(keyType, customDerDir, output)
				if err != nil {
					return fmt.Errorf("Error appending custom der certs: %w", err)
				}
			}
		}

		logger.Infof("Generating policy encryption key")
		tpmPrivate := filepath.Join(output, "tpm2-pcr-private.pem")
		cmd := exec.Command(
			"openssl", "genrsa", "-out", tpmPrivate, "2048",
		)
		if out, err := cmd.CombinedOutput(); err != nil {
			return fmt.Errorf("Error generating tpm2-pcr-private.pem: %w / %s", err, string(out))
		}

		return nil
	},
}
View Source
var NetBootCmd = cli.Command{
	Name:      "netboot",
	Aliases:   []string{"nb"},
	Usage:     "Extract artifacts for netboot from a given ISO",
	ArgsUsage: "<iso-file> <output-dir> <output-artifact-prefix>",
	Flags: []cli.Flag{
		&cli.BoolFlag{
			Name:  "debug",
			Usage: "Enable debug logging",
		},
	},
	Action: func(c *cli.Context) error {
		iso := c.Args().Get(0)
		if iso == "" {
			c.Command.Subcommands = nil
			cli.ShowCommandHelp(c, c.Command.Name)
			fmt.Println("")
			return fmt.Errorf("iso-file is required")
		}
		output := c.Args().Get(1)
		if output == "" {
			c.Command.Subcommands = nil
			cli.ShowCommandHelp(c, c.Command.Name)
			fmt.Println("")
			return fmt.Errorf("output-dir is required")
		}

		name := c.Args().Get(2)
		if name == "" {
			c.Command.Subcommands = nil
			cli.ShowCommandHelp(c, c.Command.Name)
			fmt.Println("")
			return fmt.Errorf("output-artifact-prefix is required")
		}
		loglevel := "info"
		if c.Bool("debug") {
			loglevel = "debug"
		}
		internal.Log = types.NewKairosLogger("AuroraBoot", loglevel, false)
		isoGet := func() string {
			return iso
		}
		outputGet := func() string {
			return output
		}
		f := ops.ExtractNetboot(isoGet, outputGet, name)
		return f(c.Context)
	},
}
View Source
var RedFishDeployCmd = cli.Command{
	Name:  "redfish",
	Usage: "Deploy ISO to server via RedFish (EXPERIMENTAL)",
	Subcommands: []*cli.Command{
		{
			Name:  "deploy",
			Usage: "Deploy ISO to server via RedFish",
			Flags: []cli.Flag{
				&cli.StringFlag{
					Name:     "endpoint",
					Usage:    "RedFish endpoint URL",
					Required: true,
				},
				&cli.StringFlag{
					Name:     "username",
					Usage:    "RedFish username",
					Required: true,
				},
				&cli.StringFlag{
					Name:     "password",
					Usage:    "RedFish password",
					Required: true,
				},
				&cli.StringFlag{
					Name:  "vendor",
					Usage: "Hardware vendor (generic, supermicro, ilo, dmtf)",
					Value: "generic",
				},
				&cli.BoolFlag{
					Name:  "verify-ssl",
					Usage: "Verify SSL certificates",
					Value: true,
				},
				&cli.IntFlag{
					Name:  "min-memory",
					Usage: "Minimum required memory in GiB",
					Value: 4,
				},
				&cli.IntFlag{
					Name:  "min-cpus",
					Usage: "Minimum required CPUs",
					Value: 2,
				},
				&cli.StringSliceFlag{
					Name:  "required-features",
					Usage: "Required hardware features",
					Value: cli.NewStringSlice("UEFI"),
				},
				&cli.DurationFlag{
					Name:  "timeout",
					Usage: "Operation timeout",
					Value: 30 * time.Minute,
				},
			},
			Action: func(c *cli.Context) error {
				endpoint := c.String("endpoint")
				username := c.String("username")
				password := c.String("password")
				vendor := c.String("vendor")
				verifySSL := c.Bool("verify-ssl")
				minMemory := c.Int("min-memory")
				minCPUs := c.Int("min-cpus")
				requiredFeatures := c.StringSlice("required-features")
				timeout := c.Duration("timeout")
				isoPath := c.Args().First()

				if isoPath == "" {
					return fmt.Errorf("ISO path is required")
				}

				vendorType := redfish.VendorType(vendor)
				client, err := redfish.NewVendorClient(vendorType, endpoint, username, password, verifySSL, timeout)
				if err != nil {
					return fmt.Errorf("creating RedFish client: %w", err)
				}

				inspector := hardware.NewInspector(client)

				sysInfo, err := inspector.InspectSystem()
				if err != nil {
					return fmt.Errorf("inspecting system: %w", err)
				}

				fmt.Printf("System: %s %s (SN: %s)\n",
					sysInfo.Manufacturer, sysInfo.Model, sysInfo.SerialNumber)

				reqs := &hardware.Requirements{
					MinMemoryGiB:     minMemory,
					MinCPUs:          minCPUs,
					RequiredFeatures: requiredFeatures,
				}
				if err := inspector.ValidateRequirements(sysInfo, reqs); err != nil {
					return fmt.Errorf("validating requirements: %w", err)
				}

				status, err := client.DeployISO(isoPath)
				if err != nil {
					return fmt.Errorf("deploying ISO: %w", err)
				}

				fmt.Printf("Deployment started: %s\n", status.Message)

				for {
					status, err := client.GetDeploymentStatus()
					if err != nil {
						return fmt.Errorf("getting deployment status: %w", err)
					}

					fmt.Printf("Deployment status: %s (%d%%)\n", status.State, status.Progress)

					if status.State == "Completed" {
						fmt.Println("Deployment completed successfully")
						break
					} else if status.State == "Failed" {
						return fmt.Errorf("deployment failed: %s", status.Message)
					}

					time.Sleep(5 * time.Second)
				}

				return nil
			},
		},
	},
}
View Source
var SysextCmd = cli.Command{
	Name:      "sysext",
	Usage:     "Generate a sysextension from the last layer of the given CONTAINER",
	ArgsUsage: "<name> <container>",

	Flags: []cli.Flag{
		&cli.StringFlag{
			Name:     "private-key",
			Value:    "",
			Usage:    "Private key to sign the sysext with",
			Required: true,
		},
		&cli.StringFlag{
			Name:     "certificate",
			Usage:    "Certificate to sign the sysext with",
			Required: true,
		},
		&cli.BoolFlag{
			Name:  "service-load",
			Value: false,
			Usage: "Make systemctl reload the service when loading the sysext. This is useful for sysext that provide systemd service files.",
		},
		&cli.StringFlag{
			Name:  "output",
			Usage: "Output dir",
		},
		&cli.StringFlag{
			Name:  "arch",
			Value: "amd64",
			Usage: "Arch to get the image from and build the sysext for. Accepts amd64 and arm64 values.",
		},
	},
	Before: func(ctx *cli.Context) error {
		arch := ctx.String("arch")
		if arch != "amd64" && arch != "arm64" {
			return fmt.Errorf("unsupported architecture: %s", arch)
		}
		return nil
	},
	Action: func(ctx *cli.Context) error {
		level := "warn"
		if ctx.Bool("debug") {
			level = "debug"
		}
		logger := sdkTypes.NewKairosLogger("auroraboot", level, false)
		args := ctx.Args()

		name := args.Get(0)
		if _, err := os.Stat(fmt.Sprintf("%s.sysext.raw", name)); err == nil {
			_ = os.Remove(fmt.Sprintf("%s.sysext.raw", name))
		}
		logger.Info("🚀 Start sysext creation")

		dir, err := os.MkdirTemp("", "auroraboot-sysext-")
		if err != nil {
			return fmt.Errorf("creating temp directory: %w", err)
		}
		defer func(path string) {
			err := os.RemoveAll(path)
			if err != nil {
				logger.Logger.Error().Str("dir", dir).Err(err).Msg("⛔ removing dir")
			}
		}(dir)
		logger.Logger.Debug().Str("dir", dir).Msg("creating directory")

		logger.Info("💿 Getting image info")
		platform := fmt.Sprintf("linux/%s", ctx.String("arch"))
		image, err := utils.GetImage(args.Get(1), platform, nil, nil)
		if err != nil {
			logger.Logger.Error().Str("image", args.Get(1)).Err(err).Msg("⛔ getting image")
			return err
		}

		AllowList := regexp.MustCompile(`^usr/*|^/usr/*`)

		logger.Info("📤 Extracting archives from image layer")
		err = sysext.ExtractFilesFromLastLayer(image, dir, logger, AllowList)
		if err != nil {
			logger.Logger.Error().Str("image", args.Get(1)).Err(err).Msg("⛔ extracting layer")
		}

		err = os.MkdirAll(filepath.Join(dir, "/usr/lib/extension-release.d/"), os.ModeDir|os.ModePerm)
		if err != nil {
			logger.Logger.Error().Str("dir", filepath.Join(dir, "/usr/lib/extension-release.d/")).Err(err).Msg("⛔ creating dir")
			return err
		}

		arch := "x86-64"
		if ctx.String("arch") == "arm64" {
			arch = "arm64"
		}

		extensionData := fmt.Sprintf("ID=_any\nARCHITECTURE=%s", arch)

		if ctx.Bool("service-reload") {
			extensionData = fmt.Sprintf("%s\nEXTENSION_RELOAD_MANAGER=1", extensionData)
		}
		err = os.WriteFile(filepath.Join(dir, "/usr/lib/extension-release.d/", fmt.Sprintf("extension-release.%s", name)), []byte(extensionData), os.ModePerm)
		if err != nil {
			logger.Logger.Error().Str("file", fmt.Sprintf("extension-release.%s", name)).Err(err).Msg("⛔ creating releasefile")
			return err
		}

		logger.Logger.Info().Msg("📦 Packing sysext into raw image")

		outputFile := fmt.Sprintf("%s.sysext.raw", name)
		if outputDir := ctx.String("output"); outputDir != "" {
			outputFile = filepath.Join(outputDir, outputFile)
		}

		command := exec.Command(
			"systemd-repart",
			"--make-ddi=sysext",
			"--image-policy=root=verity+signed+absent:usr=verity+signed+absent",
			fmt.Sprintf("--architecture=%s", arch),

			fmt.Sprintf("--seed=%s", uuid.NewV5(uuid.NamespaceDNS, "kairos-sysext")),
			fmt.Sprintf("--copy-source=%s", dir),
			outputFile,
			fmt.Sprintf("--private-key=%s", ctx.String("private-key")),
			fmt.Sprintf("--certificate=%s", ctx.String("certificate")),
		)
		out, err := command.CombinedOutput()
		logger.Logger.Debug().Str("output", string(out)).Msg("building sysext")
		if err != nil {
			logger.Logger.Error().Err(err).
				Str("command", strings.Join(command.Args, " ")).
				Str("output", string(out)).
				Msg("⛔ building sysext")
			return err
		}

		logger.Logger.Info().Str("output", outputFile).Msg("🎉 Done sysext creation")
		return nil
	},
}

Use: "build-uki SourceImage", Short: "Build a UKI artifact from a container image",

View Source
var UkiPXECmd = cli.Command{
	Name:      "uki-pxe",
	Usage:     "Serve PXE boot files using a specified ISO file",
	ArgsUsage: "ISO_FILE",
	Flags: []cli.Flag{
		&cli.StringFlag{
			Name:    "loglevel",
			Aliases: []string{"l"},
			Usage:   "Set the log level",
			Value:   "info",
		},
	},
	Before: func(c *cli.Context) error {

		if c.NArg() != 1 {
			return cli.Exit("exactly one argument required: the path to the ISO file", 1)
		}

		isoFile := c.Args().First()
		if _, err := os.Stat(isoFile); err != nil {
			if os.IsNotExist(err) {
				return cli.Exit("iso file does not exist", 1)
			}
			return cli.Exit("error checking iso file: "+err.Error(), 1)
		}
		return nil
	},
	Action: func(context *cli.Context) error {
		log := types.NewKairosLogger("pxe", context.String("loglevel"), false)
		return utils.ServeUkiPXE(context.Args().First(), log)
	},
}
View Source
var WebCMD = cli.Command{
	Name:    "web",
	Aliases: []string{"w"},
	Usage:   "Starts a ui",
	Flags: []cli.Flag{
		&cli.StringFlag{
			Name:  "address",
			Usage: "Listen address",
			Value: ":8080",
		},
		&cli.StringFlag{
			Name:  "artifact-dir",
			Usage: "Artifact directory",
			Value: "/tmp/artifacts",
		},
		&cli.StringFlag{
			Name:  "builds-dir",
			Usage: "Directory to store build jobs and their artifacts",
			Value: "/tmp/kairos-builds",
		},
		&cli.BoolFlag{
			Name:  "create-worker",
			Usage: "Start a local worker in a goroutine",
			Value: false,
		},
	},
	Action: func(c *cli.Context) error {
		os.MkdirAll(c.String("artifact-dir"), os.ModePerm)
		os.MkdirAll(c.String("builds-dir"), os.ModePerm)

		if c.Bool("create-worker") {
			workerID := "local-worker"

			addr := c.String("address")
			_, port, err := net.SplitHostPort(addr)
			if err != nil {
				return fmt.Errorf("invalid address format: %v", err)
			}
			workerAddr := "http://localhost:" + port
			w := worker.NewWorker(workerAddr, workerID)
			go func() {
				if err := w.Start(c.Context); err != nil {

					fmt.Printf("Worker error: %v\n", err)
				}
			}()
		}

		return web.App(web.AppConfig{
			EnableLogger: true,
			ListenAddr:   c.String("address"),
			OutDir:       c.String("artifact-dir"),
			BuildsDir:    c.String("builds-dir"),
		})
	},
}
View Source
var WorkerCmd = cli.Command{
	Name:  "worker",
	Usage: "Start a build worker",
	Flags: []cli.Flag{
		&cli.StringFlag{
			Name:     "endpoint",
			Usage:    "API endpoint URL (e.g., http://localhost:8080)",
			Required: true,
		},
		&cli.StringFlag{
			Name:     "worker-id",
			Usage:    "Unique worker identifier",
			Required: true,
		},
	},
	Action: func(ctx *cli.Context) error {
		w := worker.NewWorker(ctx.String("endpoint"), ctx.String("worker-id"))
		fmt.Printf("Starting worker %s, connecting to %s\n", ctx.String("worker-id"), ctx.String("endpoint"))
		return w.Start(ctx.Context)
	},
}

Functions

func CheckRoot added in v0.3.0

func CheckRoot() error

CheckRoot is a helper which can add it to commands that require root

func GetApp added in v0.3.0

func GetApp(version string) *cli.App

func GetUkiCmdline added in v0.3.0

func GetUkiCmdline(cmdlineExtend, bootBranding string, extraCmdlines []string) []utils.BootEntry

GetUkiCmdline returns the cmdline to be used for the kernel. The cmdline can be overridden by the user using the cmdline flag. For each cmdline passed, we generate a uki file with that cmdline extend-cmdline will just extend the default cmdline so we only create one efi file. Artifact name is the default one extra-cmdline will create a new efi file for each cmdline passed. artifact name is generated from the cmdline

func GetUkiSingleCmdlines added in v0.3.0

func GetUkiSingleCmdlines(bootBranding string, cmdlines []string, logger sdkTypes.KairosLogger) []utils.BootEntry

GetUkiSingleCmdlines returns the single-efi-cmdline as passed by the user.

func NameFromCmdline added in v0.3.0

func NameFromCmdline(basename, cmdline string) string

NameFromCmdline returns the name of the efi/conf file based on the cmdline we want to have at least 1 efi file that its the default, that is the one we ship with the iso/media/whatever install medium that one has the default cmdline + the install cmdline For that one, we use it as the BASE one, configs will only trigger for that install stanza if we are on install media so we dont have to worry about it, but we want to provide a clean name for it so in that case we dont add anything to the efi name/conf name/cmdline inside the config For the other ones, we add the cmdline to the efi name and the cmdline to the conf file so you get - norole.efi - norole.conf - norole_interactive-install.efi - norole_interactive-install.conf This is mostly for convenience in generating the names as the real data is stored in the config file but it can easily be used to identify the efi file and the conf file. All names are returns in lowercase because FAT doesn't handle case in a predictable way.

func ZstdFile added in v0.3.0

func ZstdFile(sourcePath, targetPath string) error

Types

type RedFishDeployConfig added in v0.6.5

type RedFishDeployConfig struct {
	Endpoint         string        `arg:"--endpoint" help:"RedFish endpoint URL"`
	Username         string        `arg:"--username" help:"RedFish username"`
	Password         string        `arg:"--password" help:"RedFish password"`
	Vendor           string        `arg:"--vendor" help:"Hardware vendor (generic, supermicro, ilo)" default:"generic"`
	VerifySSL        bool          `arg:"--verify-ssl" help:"Verify SSL certificates" default:"true"`
	MinMemory        int           `arg:"--min-memory" help:"Minimum required memory in GB" default:"4"`
	MinCPUs          int           `arg:"--min-cpus" help:"Minimum required CPUs" default:"2"`
	RequiredFeatures []string      `arg:"--required-features" help:"Required hardware features"`
	Timeout          time.Duration `arg:"--timeout" help:"Operation timeout" default:"5m"`
	ISO              string        `arg:"positional" help:"Path to ISO file"`
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL