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