| // Copyright The Monogon Project Authors. |
| // SPDX-License-Identifier: Apache-2.0 |
| |
| package main |
| |
| import ( |
| _ "embed" |
| "fmt" |
| "io" |
| "os" |
| "path/filepath" |
| |
| "github.com/cavaliergopher/cpio" |
| "github.com/klauspost/compress/zstd" |
| "golang.org/x/sys/unix" |
| "google.golang.org/protobuf/proto" |
| |
| apb "source.monogon.dev/metropolis/proto/api" |
| netapi "source.monogon.dev/osbase/net/proto" |
| |
| "source.monogon.dev/osbase/bootparam" |
| "source.monogon.dev/osbase/build/mkimage/osimage" |
| "source.monogon.dev/osbase/kexec" |
| netdump "source.monogon.dev/osbase/net/dump" |
| "source.monogon.dev/osbase/oci" |
| "source.monogon.dev/osbase/structfs" |
| ) |
| |
| //go:embed third_party/linux/bzImage |
| var kernel []byte |
| |
| //go:embed third_party/ucode.cpio |
| var ucode []byte |
| |
| //go:embed initramfs.cpio.zst |
| var initramfs []byte |
| |
| // newMemfile creates a new file which is not located on a specific filesystem, |
| // but is instead backed by anonymous memory. |
| func newMemfile(name string, flags int) (*os.File, error) { |
| fd, err := unix.MemfdCreate(name, flags) |
| if err != nil { |
| return nil, fmt.Errorf("memfd_create failed: %w", err) |
| } |
| return os.NewFile(uintptr(fd), name), nil |
| } |
| |
| func writeCPIO(w io.Writer, root structfs.Tree) error { |
| cpioW := cpio.NewWriter(w) |
| for path, node := range root.Walk() { |
| switch { |
| case node.Mode.IsDir(): |
| err := cpioW.WriteHeader(&cpio.Header{ |
| Name: "/" + path, |
| Mode: cpio.TypeDir | (cpio.FileMode(node.Mode) & cpio.ModePerm), |
| }) |
| if err != nil { |
| return err |
| } |
| case node.Mode.IsRegular(): |
| err := cpioW.WriteHeader(&cpio.Header{ |
| Name: "/" + path, |
| Size: node.Content.Size(), |
| Mode: cpio.TypeReg | (cpio.FileMode(node.Mode) & cpio.ModePerm), |
| }) |
| if err != nil { |
| return err |
| } |
| content, err := node.Content.Open() |
| if err != nil { |
| return fmt.Errorf("cpio write %q: %w", path, err) |
| } |
| _, err = io.Copy(cpioW, content) |
| content.Close() |
| if err != nil { |
| return fmt.Errorf("cpio write %q: %w", path, err) |
| } |
| default: |
| return fmt.Errorf("cpio write %q: unsupported file type %s", path, node.Mode.Type().String()) |
| } |
| } |
| return cpioW.Close() |
| } |
| |
| func setupTakeover(nodeParamsRaw []byte, target string) ([]string, error) { |
| // Validate we are running via EFI. |
| if _, err := os.Stat("/sys/firmware/efi"); os.IsNotExist(err) { |
| //nolint:ST1005 |
| return nil, fmt.Errorf("Monogon OS can only be installed on EFI-booted machines, this one is not") |
| } |
| |
| currPath, err := os.Executable() |
| if err != nil { |
| return nil, err |
| } |
| |
| image, err := oci.ReadLayout(filepath.Join(filepath.Dir(currPath), "osimage")) |
| if err != nil { |
| return nil, fmt.Errorf("failed to read OS image: %w", err) |
| } |
| |
| // Dump the current network configuration |
| netconf, warnings, err := netdump.Dump() |
| if err != nil { |
| return nil, fmt.Errorf("failed to dump network configuration: %w", err) |
| } |
| |
| if len(netconf.Nameserver) == 0 { |
| netconf.Nameserver = []*netapi.Nameserver{{ |
| Ip: "8.8.8.8", |
| }, { |
| Ip: "1.1.1.1", |
| }} |
| } |
| |
| var params apb.NodeParameters |
| if err := proto.Unmarshal(nodeParamsRaw, ¶ms); err != nil { |
| return nil, fmt.Errorf("failed to unmarshal node parameters: %w", err) |
| } |
| |
| // Override the NodeParameters.NetworkConfig with the current NetworkConfig |
| // if it's missing. |
| if params.NetworkConfig == nil { |
| params.NetworkConfig = netconf |
| } |
| |
| // Marshal NodeParameters again. |
| nodeParamsRaw, err = proto.Marshal(¶ms) |
| if err != nil { |
| return nil, fmt.Errorf("failed marshaling: %w", err) |
| } |
| |
| oParams, err := setupOSImageParams(image, nodeParamsRaw, target) |
| if err != nil { |
| return nil, err |
| } |
| |
| // Validate that this installation will not fail because of disk issues |
| if _, err := osimage.Plan(oParams); err != nil { |
| return nil, fmt.Errorf("failed to plan installation: %w", err) |
| } |
| |
| // Load data from embedded files into memfiles as the kexec load syscall |
| // requires file descriptors. |
| kernelFile, err := newMemfile("kernel", 0) |
| if err != nil { |
| return nil, fmt.Errorf("failed to create kernel memfile: %w", err) |
| } |
| initramfsFile, err := newMemfile("initramfs", 0) |
| if err != nil { |
| return nil, fmt.Errorf("failed to create initramfs memfile: %w", err) |
| } |
| if _, err := kernelFile.Write(kernel); err != nil { |
| return nil, fmt.Errorf("failed to write kernel into memory-backed file: %w", err) |
| } |
| if _, err := initramfsFile.Write(ucode); err != nil { |
| return nil, fmt.Errorf("failed to write ucode into memory-backed file: %w", err) |
| } |
| if _, err := initramfsFile.Write(initramfs); err != nil { |
| return nil, fmt.Errorf("failed to write initramfs into memory-backed file: %w", err) |
| } |
| |
| // Append this executable, node params and OS image to initramfs. |
| self, err := structfs.OSPathBlob("/proc/self/exe") |
| if err != nil { |
| return nil, err |
| } |
| imageLayout, err := oci.CreateLayout(image) |
| if err != nil { |
| return nil, err |
| } |
| root := structfs.Tree{ |
| structfs.File("init", self, structfs.WithPerm(0o755)), |
| structfs.File("params.pb", structfs.Bytes(nodeParamsRaw)), |
| structfs.Dir("osimage", imageLayout), |
| } |
| compressedW, err := zstd.NewWriter(initramfsFile, zstd.WithEncoderLevel(1)) |
| if err != nil { |
| return nil, fmt.Errorf("while creating zstd writer: %w", err) |
| } |
| err = writeCPIO(compressedW, root) |
| if err != nil { |
| return nil, err |
| } |
| err = compressedW.Close() |
| if err != nil { |
| return nil, err |
| } |
| |
| initParams := bootparam.Params{ |
| bootparam.Param{Param: "quiet"}, |
| bootparam.Param{Param: launchModeEnv, Value: launchModeInit}, |
| bootparam.Param{Param: EnvInstallTarget, Value: target}, |
| bootparam.Param{Param: "init", Value: "/init"}, |
| } |
| |
| var customConsoles bool |
| cmdline, err := os.ReadFile("/proc/cmdline") |
| if err != nil { |
| warnings = append(warnings, fmt.Errorf("unable to read current kernel command line: %w", err)) |
| } else { |
| params, _, err := bootparam.Unmarshal(string(cmdline)) |
| // If the existing command line is well-formed, add all existing console |
| // parameters to the console for the agent |
| if err == nil { |
| for _, p := range params { |
| if p.Param == "console" { |
| initParams = append(initParams, p) |
| customConsoles = true |
| } |
| } |
| } |
| } |
| if !customConsoles { |
| // Add the "default" console on x86 |
| initParams = append(initParams, bootparam.Param{Param: "console", Value: "ttyS0,115200"}) |
| } |
| agentCmdline, err := bootparam.Marshal(initParams, "") |
| if err != nil { |
| return nil, fmt.Errorf("failed to marshal bootparams: %w", err) |
| } |
| // Stage agent payload into kernel memory |
| if err := kexec.FileLoad(kernelFile, initramfsFile, agentCmdline); err != nil { |
| return nil, fmt.Errorf("failed to load kexec payload: %w", err) |
| } |
| var warningsStrs []string |
| for _, w := range warnings { |
| warningsStrs = append(warningsStrs, w.Error()) |
| } |
| return warningsStrs, nil |
| } |