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