blob: 17ec098e5a0a93ba5010e6df2746bf957f5ef9a5 [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"
18)
19
20// install dispatches OSInstallationRequests to the appropriate installer
21// method
22func install(req *bpb.OSInstallationRequest, l logtree.LeveledLogger, isEFIBoot bool) error {
23 switch reqT := req.Type.(type) {
24 case *bpb.OSInstallationRequest_Metropolis:
25 return installMetropolis(reqT.Metropolis, l, isEFIBoot)
26 default:
27 return errors.New("unknown installation request type")
28 }
29}
30
31func installMetropolis(req *bpb.MetropolisInstallationRequest, l logtree.LeveledLogger, isEFIBoot bool) error {
32 if !isEFIBoot {
33 return errors.New("Monogon OS can only be installed on EFI-booted machines, this one is not")
34 }
35 // Download into a buffer as ZIP files cannot efficiently be read from
36 // HTTP in Go as the ReaderAt has no way of indicating continuous sections,
37 // thus a ton of small range requests would need to be used, causing
38 // a huge latency penalty as well as costing a lot of money on typical
39 // object storages. This should go away when we switch to a better bundle
40 // format which can be streamed.
41 var bundleRaw bytes.Buffer
42 b := backoff.NewExponentialBackOff()
43 err := backoff.Retry(func() error {
44 bundleRes, err := http.Get(req.BundleUrl)
45 if err != nil {
46 l.Warningf("Metropolis bundle request failed: %v", err)
47 return fmt.Errorf("HTTP request failed: %v", err)
48 }
49 defer bundleRes.Body.Close()
50 switch bundleRes.StatusCode {
51 case http.StatusTooEarly, http.StatusTooManyRequests,
52 http.StatusInternalServerError, http.StatusBadGateway,
53 http.StatusServiceUnavailable, http.StatusGatewayTimeout:
54 l.Warningf("Metropolis bundle request HTTP %d error, retrying", bundleRes.StatusCode)
55 return fmt.Errorf("HTTP error %d", bundleRes.StatusCode)
56 default:
57 // Non-standard code range used for proxy-related issue by various
58 // vendors. Treat as non-permanent error.
59 if bundleRes.StatusCode >= 520 && bundleRes.StatusCode < 599 {
60 l.Warningf("Metropolis bundle request HTTP %d error, retrying", bundleRes.StatusCode)
61 return fmt.Errorf("HTTP error %d", bundleRes.StatusCode)
62 }
63 if bundleRes.StatusCode != 200 {
64 l.Errorf("Metropolis bundle request permanent HTTP %d error, aborting", bundleRes.StatusCode)
65 return backoff.Permanent(fmt.Errorf("HTTP error %d", bundleRes.StatusCode))
66 }
67 }
68 if _, err := bundleRaw.ReadFrom(bundleRes.Body); err != nil {
69 l.Warningf("Metropolis bundle download failed, retrying: %v", err)
70 bundleRaw.Reset()
71 return err
72 }
73 return nil
74 }, b)
75 if err != nil {
76 return fmt.Errorf("error downloading Metropolis bundle: %v", err)
77 }
78 l.Info("Metropolis Bundle downloaded")
79 bundle, err := zip.NewReader(bytes.NewReader(bundleRaw.Bytes()), int64(bundleRaw.Len()))
80 if err != nil {
81 return fmt.Errorf("failed to open node bundle: %w", err)
82 }
83 efiPayload, err := bundle.Open("kernel_efi.efi")
84 if err != nil {
85 return fmt.Errorf("invalid bundle: %w", err)
86 }
87 defer efiPayload.Close()
88 systemImage, err := bundle.Open("verity_rootfs.img")
89 if err != nil {
90 return fmt.Errorf("invalid bundle: %w", err)
91 }
92 defer systemImage.Close()
93
94 nodeParamsRaw, err := proto.Marshal(req.NodeParameters)
95 if err != nil {
96 return fmt.Errorf("failed marshaling: %w", err)
97 }
98
99 installParams := osimage.Params{
100 PartitionSize: osimage.PartitionSizeInfo{
101 ESP: 128,
102 System: 4096,
103 Data: 128,
104 },
105 SystemImage: systemImage,
106 EFIPayload: efiPayload,
107 NodeParameters: bytes.NewReader(nodeParamsRaw),
108 OutputPath: filepath.Join("/dev", req.RootDevice),
109 }
110
111 be, err := osimage.Create(&installParams)
112 if err != nil {
113 return err
114 }
115 bootEntryIdx, err := efivarfs.CreateBootEntry(be)
116 if err != nil {
117 return fmt.Errorf("error creating EFI boot entry: %w", err)
118 }
119 if err := efivarfs.SetBootOrder(&efivarfs.BootOrder{uint16(bootEntryIdx)}); err != nil {
120 return fmt.Errorf("error setting EFI boot order: %w", err)
121 }
122 l.Info("Metropolis installation completed")
123 return nil
124}