blob: 29641bd9b6e409559d89893151bba9e1383475b1 [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"
Tim Windelschmidt58321122024-09-10 02:26:03 +020011 "os"
Lorenz Brunaadeb792023-03-27 15:53:56 +020012 "path/filepath"
13
14 "github.com/cenkalti/backoff/v4"
15 "google.golang.org/protobuf/proto"
16
17 bpb "source.monogon.dev/cloud/bmaas/server/api"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020018 "source.monogon.dev/osbase/blockdev"
Tim Windelschmidtc2290c22024-08-15 19:56:00 +020019 "source.monogon.dev/osbase/build/mkimage/osimage"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020020 "source.monogon.dev/osbase/efivarfs"
21 "source.monogon.dev/osbase/logtree"
Tim Windelschmidt10ef8f92024-08-13 15:35:10 +020022 npb "source.monogon.dev/osbase/net/proto"
Lorenz Brunaadeb792023-03-27 15:53:56 +020023)
24
Lorenz Brun54a5a052023-10-02 16:40:11 +020025//go:embed metropolis/node/core/abloader/abloader_bin.efi
26var abloader []byte
27
Lorenz Brunad131882023-06-28 16:42:20 +020028// FileSizedReader is a small adapter from fs.File to fs.SizedReader
29// Panics on Stat() failure, so should only be used with sources where Stat()
30// cannot fail.
31type FileSizedReader struct {
32 fs.File
33}
34
35func (f FileSizedReader) Size() int64 {
36 stat, err := f.Stat()
37 if err != nil {
38 panic(err)
39 }
40 return stat.Size()
41}
42
Lorenz Brunaadeb792023-03-27 15:53:56 +020043// install dispatches OSInstallationRequests to the appropriate installer
44// method
Tim Windelschmidt58321122024-09-10 02:26:03 +020045func install(req *bpb.OSInstallationRequest, netConfig *npb.Net, l logtree.LeveledLogger) error {
Lorenz Brunaadeb792023-03-27 15:53:56 +020046 switch reqT := req.Type.(type) {
47 case *bpb.OSInstallationRequest_Metropolis:
Tim Windelschmidt58321122024-09-10 02:26:03 +020048 return installMetropolis(reqT.Metropolis, netConfig, l)
Lorenz Brunaadeb792023-03-27 15:53:56 +020049 default:
50 return errors.New("unknown installation request type")
51 }
52}
53
Tim Windelschmidt58321122024-09-10 02:26:03 +020054func installMetropolis(req *bpb.MetropolisInstallationRequest, netConfig *npb.Net, l logtree.LeveledLogger) error {
55 // Validate we are running via EFI.
56 if _, err := os.Stat("/sys/firmware/efi"); os.IsNotExist(err) {
Tim Windelschmidt73e98822024-04-18 23:13:49 +020057 //nolint:ST1005
Lorenz Brunaadeb792023-03-27 15:53:56 +020058 return errors.New("Monogon OS can only be installed on EFI-booted machines, this one is not")
59 }
Tim Windelschmidtfac48742023-04-24 19:04:55 +020060
61 // Override the NodeParameters.NetworkConfig with the current NetworkConfig
62 // if it's missing.
63 if req.NodeParameters.NetworkConfig == nil {
64 req.NodeParameters.NetworkConfig = netConfig
65 }
66
Lorenz Brunaadeb792023-03-27 15:53:56 +020067 // Download into a buffer as ZIP files cannot efficiently be read from
68 // HTTP in Go as the ReaderAt has no way of indicating continuous sections,
69 // thus a ton of small range requests would need to be used, causing
70 // a huge latency penalty as well as costing a lot of money on typical
71 // object storages. This should go away when we switch to a better bundle
72 // format which can be streamed.
73 var bundleRaw bytes.Buffer
74 b := backoff.NewExponentialBackOff()
75 err := backoff.Retry(func() error {
76 bundleRes, err := http.Get(req.BundleUrl)
77 if err != nil {
78 l.Warningf("Metropolis bundle request failed: %v", err)
Tim Windelschmidt327cdba2024-05-21 13:51:32 +020079 return fmt.Errorf("HTTP request failed: %w", err)
Lorenz Brunaadeb792023-03-27 15:53:56 +020080 }
81 defer bundleRes.Body.Close()
82 switch bundleRes.StatusCode {
83 case http.StatusTooEarly, http.StatusTooManyRequests,
84 http.StatusInternalServerError, http.StatusBadGateway,
85 http.StatusServiceUnavailable, http.StatusGatewayTimeout:
86 l.Warningf("Metropolis bundle request HTTP %d error, retrying", bundleRes.StatusCode)
87 return fmt.Errorf("HTTP error %d", bundleRes.StatusCode)
88 default:
89 // Non-standard code range used for proxy-related issue by various
90 // vendors. Treat as non-permanent error.
91 if bundleRes.StatusCode >= 520 && bundleRes.StatusCode < 599 {
92 l.Warningf("Metropolis bundle request HTTP %d error, retrying", bundleRes.StatusCode)
93 return fmt.Errorf("HTTP error %d", bundleRes.StatusCode)
94 }
95 if bundleRes.StatusCode != 200 {
96 l.Errorf("Metropolis bundle request permanent HTTP %d error, aborting", bundleRes.StatusCode)
97 return backoff.Permanent(fmt.Errorf("HTTP error %d", bundleRes.StatusCode))
98 }
99 }
100 if _, err := bundleRaw.ReadFrom(bundleRes.Body); err != nil {
101 l.Warningf("Metropolis bundle download failed, retrying: %v", err)
102 bundleRaw.Reset()
103 return err
104 }
105 return nil
106 }, b)
107 if err != nil {
Tim Windelschmidt327cdba2024-05-21 13:51:32 +0200108 return fmt.Errorf("error downloading Metropolis bundle: %w", err)
Lorenz Brunaadeb792023-03-27 15:53:56 +0200109 }
110 l.Info("Metropolis Bundle downloaded")
111 bundle, err := zip.NewReader(bytes.NewReader(bundleRaw.Bytes()), int64(bundleRaw.Len()))
112 if err != nil {
113 return fmt.Errorf("failed to open node bundle: %w", err)
114 }
115 efiPayload, err := bundle.Open("kernel_efi.efi")
116 if err != nil {
117 return fmt.Errorf("invalid bundle: %w", err)
118 }
119 defer efiPayload.Close()
120 systemImage, err := bundle.Open("verity_rootfs.img")
121 if err != nil {
122 return fmt.Errorf("invalid bundle: %w", err)
123 }
124 defer systemImage.Close()
125
126 nodeParamsRaw, err := proto.Marshal(req.NodeParameters)
127 if err != nil {
128 return fmt.Errorf("failed marshaling: %w", err)
129 }
130
Lorenz Brunad131882023-06-28 16:42:20 +0200131 rootDev, err := blockdev.Open(filepath.Join("/dev", req.RootDevice))
132 if err != nil {
133 return fmt.Errorf("failed to open root device: %w", err)
134 }
135
Lorenz Brunaadeb792023-03-27 15:53:56 +0200136 installParams := osimage.Params{
137 PartitionSize: osimage.PartitionSizeInfo{
Lorenz Brun35fcf032023-06-29 04:15:58 +0200138 ESP: 384,
Lorenz Brunaadeb792023-03-27 15:53:56 +0200139 System: 4096,
140 Data: 128,
141 },
142 SystemImage: systemImage,
Lorenz Brunad131882023-06-28 16:42:20 +0200143 EFIPayload: FileSizedReader{efiPayload},
Lorenz Brun54a5a052023-10-02 16:40:11 +0200144 ABLoader: bytes.NewReader(abloader),
Lorenz Brunaadeb792023-03-27 15:53:56 +0200145 NodeParameters: bytes.NewReader(nodeParamsRaw),
Lorenz Brunad131882023-06-28 16:42:20 +0200146 Output: rootDev,
Lorenz Brunaadeb792023-03-27 15:53:56 +0200147 }
148
Tim Windelschmidtcc27faa2024-08-01 02:18:35 +0200149 be, err := osimage.Write(&installParams)
Lorenz Brunaadeb792023-03-27 15:53:56 +0200150 if err != nil {
151 return err
152 }
Lorenz Brunca1cff02023-06-26 17:52:44 +0200153 bootEntryIdx, err := efivarfs.AddBootEntry(be)
Lorenz Brunaadeb792023-03-27 15:53:56 +0200154 if err != nil {
155 return fmt.Errorf("error creating EFI boot entry: %w", err)
156 }
Lorenz Brun9933ef02023-07-06 18:28:29 +0200157 if err := efivarfs.SetBootOrder(efivarfs.BootOrder{uint16(bootEntryIdx)}); err != nil {
Lorenz Brunaadeb792023-03-27 15:53:56 +0200158 return fmt.Errorf("error setting EFI boot order: %w", err)
159 }
160 l.Info("Metropolis installation completed")
161 return nil
162}