blob: 95420dcff32086050a9f510c8a9afa00ded6ec0a [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
Tim Windelschmidt7a1b27d2024-02-22 23:54:58 +01004package main
5
6import (
7 "archive/zip"
8 "bytes"
9 _ "embed"
10 "fmt"
11 "io"
12 "os"
13 "path/filepath"
14
15 "github.com/cavaliergopher/cpio"
16 "github.com/klauspost/compress/zstd"
17 "golang.org/x/sys/unix"
18 "google.golang.org/protobuf/proto"
19
20 apb "source.monogon.dev/metropolis/proto/api"
21 netapi "source.monogon.dev/osbase/net/proto"
22
23 "source.monogon.dev/osbase/bootparam"
24 "source.monogon.dev/osbase/build/mkimage/osimage"
25 "source.monogon.dev/osbase/kexec"
26 netdump "source.monogon.dev/osbase/net/dump"
27)
28
29//go:embed third_party/linux/bzImage
30var kernel []byte
31
32//go:embed third_party/ucode.cpio
33var ucode []byte
34
35//go:embed initramfs.cpio.zst
36var initramfs []byte
37
38// newMemfile creates a new file which is not located on a specific filesystem,
39// but is instead backed by anonymous memory.
40func newMemfile(name string, flags int) (*os.File, error) {
41 fd, err := unix.MemfdCreate(name, flags)
42 if err != nil {
43 return nil, fmt.Errorf("memfd_create failed: %w", err)
44 }
45 return os.NewFile(uintptr(fd), name), nil
46}
47
48func setupTakeover(nodeParamsRaw []byte, target string) ([]string, error) {
49 // Validate we are running via EFI.
50 if _, err := os.Stat("/sys/firmware/efi"); os.IsNotExist(err) {
51 //nolint:ST1005
52 return nil, fmt.Errorf("Monogon OS can only be installed on EFI-booted machines, this one is not")
53 }
54
55 currPath, err := os.Executable()
56 if err != nil {
57 return nil, err
58 }
59
60 bundleRaw, err := os.Open(filepath.Join(filepath.Dir(currPath), "bundle.zip"))
61 if err != nil {
62 return nil, err
63 }
64
65 bundleStat, err := bundleRaw.Stat()
66 if err != nil {
67 return nil, err
68 }
69
70 bundle, err := zip.NewReader(bundleRaw, bundleStat.Size())
71 if err != nil {
72 return nil, fmt.Errorf("failed to open node bundle: %w", err)
73 }
74
75 // Dump the current network configuration
76 netconf, warnings, err := netdump.Dump()
77 if err != nil {
78 return nil, fmt.Errorf("failed to dump network configuration: %w", err)
79 }
80
81 if len(netconf.Nameserver) == 0 {
82 netconf.Nameserver = []*netapi.Nameserver{{
83 Ip: "8.8.8.8",
84 }, {
85 Ip: "1.1.1.1",
86 }}
87 }
88
89 var params apb.NodeParameters
90 if err := proto.Unmarshal(nodeParamsRaw, &params); err != nil {
91 return nil, fmt.Errorf("failed to unmarshal node parameters: %w", err)
92 }
93
94 // Override the NodeParameters.NetworkConfig with the current NetworkConfig
95 // if it's missing.
96 if params.NetworkConfig == nil {
97 params.NetworkConfig = netconf
98 }
99
100 // Marshal NodeParameters again.
101 nodeParamsRaw, err = proto.Marshal(&params)
102 if err != nil {
103 return nil, fmt.Errorf("failed marshaling: %w", err)
104 }
105
106 oParams, err := setupOSImageParams(bundle, nodeParamsRaw, target)
107 if err != nil {
108 return nil, err
109 }
110
111 // Validate that this installation will not fail because of disk issues
112 if _, err := osimage.Plan(oParams); err != nil {
113 return nil, fmt.Errorf("failed to plan installation: %w", err)
114 }
115
116 // Load data from embedded files into memfiles as the kexec load syscall
117 // requires file descriptors.
118 kernelFile, err := newMemfile("kernel", 0)
119 if err != nil {
120 return nil, fmt.Errorf("failed to create kernel memfile: %w", err)
121 }
122 initramfsFile, err := newMemfile("initramfs", 0)
123 if err != nil {
124 return nil, fmt.Errorf("failed to create initramfs memfile: %w", err)
125 }
126 if _, err := kernelFile.ReadFrom(bytes.NewReader(kernel)); err != nil {
127 return nil, fmt.Errorf("failed to read kernel into memory-backed file: %w", err)
128 }
129 if _, err := initramfsFile.ReadFrom(bytes.NewReader(ucode)); err != nil {
130 return nil, fmt.Errorf("failed to read ucode into memory-backed file: %w", err)
131 }
132 if _, err := initramfsFile.ReadFrom(bytes.NewReader(initramfs)); err != nil {
133 return nil, fmt.Errorf("failed to read initramfs into memory-backed file: %w", err)
134 }
135
136 // Append this executable, the bundle and node params to initramfs
137 compressedW, err := zstd.NewWriter(initramfsFile, zstd.WithEncoderLevel(1))
138 if err != nil {
139 return nil, fmt.Errorf("while creating zstd writer: %w", err)
140 }
141 {
142 self, err := os.Open("/proc/self/exe")
143 if err != nil {
144 return nil, err
145 }
146 selfStat, err := self.Stat()
147 if err != nil {
148 return nil, err
149 }
150
151 cpioW := cpio.NewWriter(compressedW)
152 cpioW.WriteHeader(&cpio.Header{
153 Name: "/init",
154 Size: selfStat.Size(),
155 Mode: cpio.TypeReg | 0o755,
156 })
157 io.Copy(cpioW, self)
158 cpioW.Close()
159 }
160 {
161 cpioW := cpio.NewWriter(compressedW)
162 cpioW.WriteHeader(&cpio.Header{
163 Name: "/bundle.zip",
164 Size: bundleStat.Size(),
165 Mode: cpio.TypeReg | 0o644,
166 })
167 bundleRaw.Seek(0, io.SeekStart)
168 io.Copy(cpioW, bundleRaw)
169 cpioW.Close()
170 }
171 {
172 cpioW := cpio.NewWriter(compressedW)
173 cpioW.WriteHeader(&cpio.Header{
174 Name: "/params.pb",
175 Size: int64(len(nodeParamsRaw)),
176 Mode: cpio.TypeReg | 0o644,
177 })
178 cpioW.Write(nodeParamsRaw)
179 cpioW.Close()
180 }
181 compressedW.Close()
182
183 initParams := bootparam.Params{
184 bootparam.Param{Param: "quiet"},
185 bootparam.Param{Param: launchModeEnv, Value: launchModeInit},
186 bootparam.Param{Param: EnvInstallTarget, Value: target},
187 bootparam.Param{Param: "init", Value: "/init"},
188 }
189
190 var customConsoles bool
191 cmdline, err := os.ReadFile("/proc/cmdline")
192 if err != nil {
193 warnings = append(warnings, fmt.Errorf("unable to read current kernel command line: %w", err))
194 } else {
195 params, _, err := bootparam.Unmarshal(string(cmdline))
196 // If the existing command line is well-formed, add all existing console
197 // parameters to the console for the agent
198 if err == nil {
199 for _, p := range params {
200 if p.Param == "console" {
201 initParams = append(initParams, p)
202 customConsoles = true
203 }
204 }
205 }
206 }
207 if !customConsoles {
208 // Add the "default" console on x86
209 initParams = append(initParams, bootparam.Param{Param: "console", Value: "ttyS0,115200"})
210 }
211 agentCmdline, err := bootparam.Marshal(initParams, "")
212 if err != nil {
213 return nil, fmt.Errorf("failed to marshal bootparams: %w", err)
214 }
215 // Stage agent payload into kernel memory
216 if err := kexec.FileLoad(kernelFile, initramfsFile, agentCmdline); err != nil {
217 return nil, fmt.Errorf("failed to load kexec payload: %w", err)
218 }
219 var warningsStrs []string
220 for _, w := range warnings {
221 warningsStrs = append(warningsStrs, w.Error())
222 }
223 return warningsStrs, nil
224}