blob: 07c44de1742cbae946fd0d4791f9ca37def6cead [file] [log] [blame]
// Copyright The Monogon Project Authors.
// SPDX-License-Identifier: Apache-2.0
package main
import (
"archive/zip"
_ "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/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
}
bundleBlob, err := structfs.OSPathBlob(filepath.Join(filepath.Dir(currPath), "bundle.zip"))
if err != nil {
return nil, err
}
bundleRaw, err := bundleBlob.Open()
if err != nil {
return nil, err
}
defer bundleRaw.Close()
bundle, err := zip.NewReader(bundleRaw.(io.ReaderAt), bundleBlob.Size())
if err != nil {
return nil, fmt.Errorf("failed to open node bundle: %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, &params); 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(&params)
if err != nil {
return nil, fmt.Errorf("failed marshaling: %w", err)
}
oParams, err := setupOSImageParams(bundle, 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, the bundle and node params to initramfs
self, err := structfs.OSPathBlob("/proc/self/exe")
if err != nil {
return nil, err
}
root := structfs.Tree{
structfs.File("init", self, structfs.WithPerm(0o755)),
structfs.File("params.pb", structfs.Bytes(nodeParamsRaw)),
structfs.File("bundle.zip", bundleBlob),
}
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
}