blob: a1aec0d2656647d6fc393da17e6eff54c8ae7161 [file] [log] [blame]
Lorenz Brunaadeb792023-03-27 15:53:56 +02001package main
2
3import (
4 "archive/zip"
5 "bytes"
6 "errors"
7 "fmt"
8 "net/http"
9 "path/filepath"
10
11 "github.com/cenkalti/backoff/v4"
12 "google.golang.org/protobuf/proto"
13
14 bpb "source.monogon.dev/cloud/bmaas/server/api"
15 "source.monogon.dev/metropolis/node/build/mkimage/osimage"
16 "source.monogon.dev/metropolis/pkg/efivarfs"
17 "source.monogon.dev/metropolis/pkg/logtree"
Tim Windelschmidtfac48742023-04-24 19:04:55 +020018 npb "source.monogon.dev/net/proto"
Lorenz Brunaadeb792023-03-27 15:53:56 +020019)
20
21// install dispatches OSInstallationRequests to the appropriate installer
22// method
Tim Windelschmidtfac48742023-04-24 19:04:55 +020023func install(req *bpb.OSInstallationRequest, netConfig *npb.Net, l logtree.LeveledLogger, isEFIBoot bool) error {
Lorenz Brunaadeb792023-03-27 15:53:56 +020024 switch reqT := req.Type.(type) {
25 case *bpb.OSInstallationRequest_Metropolis:
Tim Windelschmidtfac48742023-04-24 19:04:55 +020026 return installMetropolis(reqT.Metropolis, netConfig, l, isEFIBoot)
Lorenz Brunaadeb792023-03-27 15:53:56 +020027 default:
28 return errors.New("unknown installation request type")
29 }
30}
31
Tim Windelschmidtfac48742023-04-24 19:04:55 +020032func installMetropolis(req *bpb.MetropolisInstallationRequest, netConfig *npb.Net, l logtree.LeveledLogger, isEFIBoot bool) error {
Lorenz Brunaadeb792023-03-27 15:53:56 +020033 if !isEFIBoot {
34 return errors.New("Monogon OS can only be installed on EFI-booted machines, this one is not")
35 }
Tim Windelschmidtfac48742023-04-24 19:04:55 +020036
37 // Override the NodeParameters.NetworkConfig with the current NetworkConfig
38 // if it's missing.
39 if req.NodeParameters.NetworkConfig == nil {
40 req.NodeParameters.NetworkConfig = netConfig
41 }
42
Lorenz Brunaadeb792023-03-27 15:53:56 +020043 // Download into a buffer as ZIP files cannot efficiently be read from
44 // HTTP in Go as the ReaderAt has no way of indicating continuous sections,
45 // thus a ton of small range requests would need to be used, causing
46 // a huge latency penalty as well as costing a lot of money on typical
47 // object storages. This should go away when we switch to a better bundle
48 // format which can be streamed.
49 var bundleRaw bytes.Buffer
50 b := backoff.NewExponentialBackOff()
51 err := backoff.Retry(func() error {
52 bundleRes, err := http.Get(req.BundleUrl)
53 if err != nil {
54 l.Warningf("Metropolis bundle request failed: %v", err)
55 return fmt.Errorf("HTTP request failed: %v", err)
56 }
57 defer bundleRes.Body.Close()
58 switch bundleRes.StatusCode {
59 case http.StatusTooEarly, http.StatusTooManyRequests,
60 http.StatusInternalServerError, http.StatusBadGateway,
61 http.StatusServiceUnavailable, http.StatusGatewayTimeout:
62 l.Warningf("Metropolis bundle request HTTP %d error, retrying", bundleRes.StatusCode)
63 return fmt.Errorf("HTTP error %d", bundleRes.StatusCode)
64 default:
65 // Non-standard code range used for proxy-related issue by various
66 // vendors. Treat as non-permanent error.
67 if bundleRes.StatusCode >= 520 && bundleRes.StatusCode < 599 {
68 l.Warningf("Metropolis bundle request HTTP %d error, retrying", bundleRes.StatusCode)
69 return fmt.Errorf("HTTP error %d", bundleRes.StatusCode)
70 }
71 if bundleRes.StatusCode != 200 {
72 l.Errorf("Metropolis bundle request permanent HTTP %d error, aborting", bundleRes.StatusCode)
73 return backoff.Permanent(fmt.Errorf("HTTP error %d", bundleRes.StatusCode))
74 }
75 }
76 if _, err := bundleRaw.ReadFrom(bundleRes.Body); err != nil {
77 l.Warningf("Metropolis bundle download failed, retrying: %v", err)
78 bundleRaw.Reset()
79 return err
80 }
81 return nil
82 }, b)
83 if err != nil {
84 return fmt.Errorf("error downloading Metropolis bundle: %v", err)
85 }
86 l.Info("Metropolis Bundle downloaded")
87 bundle, err := zip.NewReader(bytes.NewReader(bundleRaw.Bytes()), int64(bundleRaw.Len()))
88 if err != nil {
89 return fmt.Errorf("failed to open node bundle: %w", err)
90 }
91 efiPayload, err := bundle.Open("kernel_efi.efi")
92 if err != nil {
93 return fmt.Errorf("invalid bundle: %w", err)
94 }
95 defer efiPayload.Close()
96 systemImage, err := bundle.Open("verity_rootfs.img")
97 if err != nil {
98 return fmt.Errorf("invalid bundle: %w", err)
99 }
100 defer systemImage.Close()
101
102 nodeParamsRaw, err := proto.Marshal(req.NodeParameters)
103 if err != nil {
104 return fmt.Errorf("failed marshaling: %w", err)
105 }
106
107 installParams := osimage.Params{
108 PartitionSize: osimage.PartitionSizeInfo{
109 ESP: 128,
110 System: 4096,
111 Data: 128,
112 },
113 SystemImage: systemImage,
114 EFIPayload: efiPayload,
115 NodeParameters: bytes.NewReader(nodeParamsRaw),
116 OutputPath: filepath.Join("/dev", req.RootDevice),
117 }
118
119 be, err := osimage.Create(&installParams)
120 if err != nil {
121 return err
122 }
Lorenz Brunca1cff02023-06-26 17:52:44 +0200123 bootEntryIdx, err := efivarfs.AddBootEntry(be)
Lorenz Brunaadeb792023-03-27 15:53:56 +0200124 if err != nil {
125 return fmt.Errorf("error creating EFI boot entry: %w", err)
126 }
Lorenz Brun9933ef02023-07-06 18:28:29 +0200127 if err := efivarfs.SetBootOrder(efivarfs.BootOrder{uint16(bootEntryIdx)}); err != nil {
Lorenz Brunaadeb792023-03-27 15:53:56 +0200128 return fmt.Errorf("error setting EFI boot order: %w", err)
129 }
130 l.Info("Metropolis installation completed")
131 return nil
132}