blob: 6082337d0fc12e7d35e226ff9001b90b6361e7e4 [file] [log] [blame]
Lorenz Brunaadeb792023-03-27 15:53:56 +02001package main
2
3import (
4 "archive/zip"
5 "bytes"
Lorenz Brun54a5a052023-10-02 16:40:11 +02006 _ "embed"
Lorenz Brunaadeb792023-03-27 15:53:56 +02007 "errors"
8 "fmt"
Lorenz Brunad131882023-06-28 16:42:20 +02009 "io/fs"
Lorenz Brunaadeb792023-03-27 15:53:56 +020010 "net/http"
11 "path/filepath"
12
13 "github.com/cenkalti/backoff/v4"
14 "google.golang.org/protobuf/proto"
15
16 bpb "source.monogon.dev/cloud/bmaas/server/api"
17 "source.monogon.dev/metropolis/node/build/mkimage/osimage"
Tim Windelschmidtfac48742023-04-24 19:04:55 +020018 npb "source.monogon.dev/net/proto"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020019 "source.monogon.dev/osbase/blockdev"
20 "source.monogon.dev/osbase/efivarfs"
21 "source.monogon.dev/osbase/logtree"
Lorenz Brunaadeb792023-03-27 15:53:56 +020022)
23
Lorenz Brun54a5a052023-10-02 16:40:11 +020024//go:embed metropolis/node/core/abloader/abloader_bin.efi
25var abloader []byte
26
Lorenz Brunad131882023-06-28 16:42:20 +020027// FileSizedReader is a small adapter from fs.File to fs.SizedReader
28// Panics on Stat() failure, so should only be used with sources where Stat()
29// cannot fail.
30type FileSizedReader struct {
31 fs.File
32}
33
34func (f FileSizedReader) Size() int64 {
35 stat, err := f.Stat()
36 if err != nil {
37 panic(err)
38 }
39 return stat.Size()
40}
41
Lorenz Brunaadeb792023-03-27 15:53:56 +020042// install dispatches OSInstallationRequests to the appropriate installer
43// method
Tim Windelschmidtfac48742023-04-24 19:04:55 +020044func install(req *bpb.OSInstallationRequest, netConfig *npb.Net, l logtree.LeveledLogger, isEFIBoot bool) error {
Lorenz Brunaadeb792023-03-27 15:53:56 +020045 switch reqT := req.Type.(type) {
46 case *bpb.OSInstallationRequest_Metropolis:
Tim Windelschmidtfac48742023-04-24 19:04:55 +020047 return installMetropolis(reqT.Metropolis, netConfig, l, isEFIBoot)
Lorenz Brunaadeb792023-03-27 15:53:56 +020048 default:
49 return errors.New("unknown installation request type")
50 }
51}
52
Tim Windelschmidtfac48742023-04-24 19:04:55 +020053func installMetropolis(req *bpb.MetropolisInstallationRequest, netConfig *npb.Net, l logtree.LeveledLogger, isEFIBoot bool) error {
Lorenz Brunaadeb792023-03-27 15:53:56 +020054 if !isEFIBoot {
Tim Windelschmidt73e98822024-04-18 23:13:49 +020055 //nolint:ST1005
Lorenz Brunaadeb792023-03-27 15:53:56 +020056 return errors.New("Monogon OS can only be installed on EFI-booted machines, this one is not")
57 }
Tim Windelschmidtfac48742023-04-24 19:04:55 +020058
59 // Override the NodeParameters.NetworkConfig with the current NetworkConfig
60 // if it's missing.
61 if req.NodeParameters.NetworkConfig == nil {
62 req.NodeParameters.NetworkConfig = netConfig
63 }
64
Lorenz Brunaadeb792023-03-27 15:53:56 +020065 // Download into a buffer as ZIP files cannot efficiently be read from
66 // HTTP in Go as the ReaderAt has no way of indicating continuous sections,
67 // thus a ton of small range requests would need to be used, causing
68 // a huge latency penalty as well as costing a lot of money on typical
69 // object storages. This should go away when we switch to a better bundle
70 // format which can be streamed.
71 var bundleRaw bytes.Buffer
72 b := backoff.NewExponentialBackOff()
73 err := backoff.Retry(func() error {
74 bundleRes, err := http.Get(req.BundleUrl)
75 if err != nil {
76 l.Warningf("Metropolis bundle request failed: %v", err)
Tim Windelschmidt327cdba2024-05-21 13:51:32 +020077 return fmt.Errorf("HTTP request failed: %w", err)
Lorenz Brunaadeb792023-03-27 15:53:56 +020078 }
79 defer bundleRes.Body.Close()
80 switch bundleRes.StatusCode {
81 case http.StatusTooEarly, http.StatusTooManyRequests,
82 http.StatusInternalServerError, http.StatusBadGateway,
83 http.StatusServiceUnavailable, http.StatusGatewayTimeout:
84 l.Warningf("Metropolis bundle request HTTP %d error, retrying", bundleRes.StatusCode)
85 return fmt.Errorf("HTTP error %d", bundleRes.StatusCode)
86 default:
87 // Non-standard code range used for proxy-related issue by various
88 // vendors. Treat as non-permanent error.
89 if bundleRes.StatusCode >= 520 && bundleRes.StatusCode < 599 {
90 l.Warningf("Metropolis bundle request HTTP %d error, retrying", bundleRes.StatusCode)
91 return fmt.Errorf("HTTP error %d", bundleRes.StatusCode)
92 }
93 if bundleRes.StatusCode != 200 {
94 l.Errorf("Metropolis bundle request permanent HTTP %d error, aborting", bundleRes.StatusCode)
95 return backoff.Permanent(fmt.Errorf("HTTP error %d", bundleRes.StatusCode))
96 }
97 }
98 if _, err := bundleRaw.ReadFrom(bundleRes.Body); err != nil {
99 l.Warningf("Metropolis bundle download failed, retrying: %v", err)
100 bundleRaw.Reset()
101 return err
102 }
103 return nil
104 }, b)
105 if err != nil {
Tim Windelschmidt327cdba2024-05-21 13:51:32 +0200106 return fmt.Errorf("error downloading Metropolis bundle: %w", err)
Lorenz Brunaadeb792023-03-27 15:53:56 +0200107 }
108 l.Info("Metropolis Bundle downloaded")
109 bundle, err := zip.NewReader(bytes.NewReader(bundleRaw.Bytes()), int64(bundleRaw.Len()))
110 if err != nil {
111 return fmt.Errorf("failed to open node bundle: %w", err)
112 }
113 efiPayload, err := bundle.Open("kernel_efi.efi")
114 if err != nil {
115 return fmt.Errorf("invalid bundle: %w", err)
116 }
117 defer efiPayload.Close()
118 systemImage, err := bundle.Open("verity_rootfs.img")
119 if err != nil {
120 return fmt.Errorf("invalid bundle: %w", err)
121 }
122 defer systemImage.Close()
123
124 nodeParamsRaw, err := proto.Marshal(req.NodeParameters)
125 if err != nil {
126 return fmt.Errorf("failed marshaling: %w", err)
127 }
128
Lorenz Brunad131882023-06-28 16:42:20 +0200129 rootDev, err := blockdev.Open(filepath.Join("/dev", req.RootDevice))
130 if err != nil {
131 return fmt.Errorf("failed to open root device: %w", err)
132 }
133
Lorenz Brunaadeb792023-03-27 15:53:56 +0200134 installParams := osimage.Params{
135 PartitionSize: osimage.PartitionSizeInfo{
Lorenz Brun35fcf032023-06-29 04:15:58 +0200136 ESP: 384,
Lorenz Brunaadeb792023-03-27 15:53:56 +0200137 System: 4096,
138 Data: 128,
139 },
140 SystemImage: systemImage,
Lorenz Brunad131882023-06-28 16:42:20 +0200141 EFIPayload: FileSizedReader{efiPayload},
Lorenz Brun54a5a052023-10-02 16:40:11 +0200142 ABLoader: bytes.NewReader(abloader),
Lorenz Brunaadeb792023-03-27 15:53:56 +0200143 NodeParameters: bytes.NewReader(nodeParamsRaw),
Lorenz Brunad131882023-06-28 16:42:20 +0200144 Output: rootDev,
Lorenz Brunaadeb792023-03-27 15:53:56 +0200145 }
146
147 be, err := osimage.Create(&installParams)
148 if err != nil {
149 return err
150 }
Lorenz Brunca1cff02023-06-26 17:52:44 +0200151 bootEntryIdx, err := efivarfs.AddBootEntry(be)
Lorenz Brunaadeb792023-03-27 15:53:56 +0200152 if err != nil {
153 return fmt.Errorf("error creating EFI boot entry: %w", err)
154 }
Lorenz Brun9933ef02023-07-06 18:28:29 +0200155 if err := efivarfs.SetBootOrder(efivarfs.BootOrder{uint16(bootEntryIdx)}); err != nil {
Lorenz Brunaadeb792023-03-27 15:53:56 +0200156 return fmt.Errorf("error setting EFI boot order: %w", err)
157 }
158 l.Info("Metropolis installation completed")
159 return nil
160}