blob: 914b0be19d818db6fb11219464ac075c935a4d04 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
Lorenz Brunaadeb792023-03-27 15:53:56 +02004package main
5
6import (
7 "archive/zip"
8 "bytes"
Lorenz Brun54a5a052023-10-02 16:40:11 +02009 _ "embed"
Lorenz Brunaadeb792023-03-27 15:53:56 +020010 "errors"
11 "fmt"
12 "net/http"
Tim Windelschmidt58321122024-09-10 02:26:03 +020013 "os"
Lorenz Brunaadeb792023-03-27 15:53:56 +020014 "path/filepath"
15
16 "github.com/cenkalti/backoff/v4"
17 "google.golang.org/protobuf/proto"
18
19 bpb "source.monogon.dev/cloud/bmaas/server/api"
Serge Bazanski3c5d0632024-09-12 10:49:12 +000020 "source.monogon.dev/go/logging"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020021 "source.monogon.dev/osbase/blockdev"
Tim Windelschmidtc2290c22024-08-15 19:56:00 +020022 "source.monogon.dev/osbase/build/mkimage/osimage"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020023 "source.monogon.dev/osbase/efivarfs"
Tim Windelschmidt10ef8f92024-08-13 15:35:10 +020024 npb "source.monogon.dev/osbase/net/proto"
Jan Schärc1b6df42025-03-20 08:52:18 +000025 "source.monogon.dev/osbase/structfs"
Lorenz Brunaadeb792023-03-27 15:53:56 +020026)
27
Tim Windelschmidt1f51cf42024-10-01 17:04:28 +020028//go:embed metropolis/node/core/abloader/abloader.efi
Lorenz Brun54a5a052023-10-02 16:40:11 +020029var abloader []byte
30
Jan Schärc1b6df42025-03-20 08:52:18 +000031// zipBlob looks up a file in a [zip.Reader] and adapts it to [structfs.Blob].
32func zipBlob(reader *zip.Reader, name string) (zipFileBlob, error) {
33 for _, file := range reader.File {
34 if file.Name == name {
35 return zipFileBlob{file}, nil
36 }
37 }
38 return zipFileBlob{}, fmt.Errorf("file %q not found", name)
Lorenz Brunad131882023-06-28 16:42:20 +020039}
40
Jan Schärc1b6df42025-03-20 08:52:18 +000041type zipFileBlob struct {
42 *zip.File
43}
44
45func (f zipFileBlob) Size() int64 {
46 return int64(f.File.UncompressedSize64)
Lorenz Brunad131882023-06-28 16:42:20 +020047}
48
Lorenz Brunaadeb792023-03-27 15:53:56 +020049// install dispatches OSInstallationRequests to the appropriate installer
50// method
Serge Bazanski3c5d0632024-09-12 10:49:12 +000051func install(req *bpb.OSInstallationRequest, netConfig *npb.Net, l logging.Leveled) error {
Lorenz Brunaadeb792023-03-27 15:53:56 +020052 switch reqT := req.Type.(type) {
53 case *bpb.OSInstallationRequest_Metropolis:
Tim Windelschmidt58321122024-09-10 02:26:03 +020054 return installMetropolis(reqT.Metropolis, netConfig, l)
Lorenz Brunaadeb792023-03-27 15:53:56 +020055 default:
56 return errors.New("unknown installation request type")
57 }
58}
59
Serge Bazanski3c5d0632024-09-12 10:49:12 +000060func installMetropolis(req *bpb.MetropolisInstallationRequest, netConfig *npb.Net, l logging.Leveled) error {
Tim Windelschmidt58321122024-09-10 02:26:03 +020061 // Validate we are running via EFI.
62 if _, err := os.Stat("/sys/firmware/efi"); os.IsNotExist(err) {
Tim Windelschmidt1f51cf42024-10-01 17:04:28 +020063 // nolint:ST1005
Lorenz Brunaadeb792023-03-27 15:53:56 +020064 return errors.New("Monogon OS can only be installed on EFI-booted machines, this one is not")
65 }
Tim Windelschmidtfac48742023-04-24 19:04:55 +020066
67 // Override the NodeParameters.NetworkConfig with the current NetworkConfig
68 // if it's missing.
69 if req.NodeParameters.NetworkConfig == nil {
70 req.NodeParameters.NetworkConfig = netConfig
71 }
72
Lorenz Brunaadeb792023-03-27 15:53:56 +020073 // Download into a buffer as ZIP files cannot efficiently be read from
74 // HTTP in Go as the ReaderAt has no way of indicating continuous sections,
75 // thus a ton of small range requests would need to be used, causing
76 // a huge latency penalty as well as costing a lot of money on typical
77 // object storages. This should go away when we switch to a better bundle
78 // format which can be streamed.
79 var bundleRaw bytes.Buffer
80 b := backoff.NewExponentialBackOff()
81 err := backoff.Retry(func() error {
82 bundleRes, err := http.Get(req.BundleUrl)
83 if err != nil {
84 l.Warningf("Metropolis bundle request failed: %v", err)
Tim Windelschmidt327cdba2024-05-21 13:51:32 +020085 return fmt.Errorf("HTTP request failed: %w", err)
Lorenz Brunaadeb792023-03-27 15:53:56 +020086 }
87 defer bundleRes.Body.Close()
88 switch bundleRes.StatusCode {
89 case http.StatusTooEarly, http.StatusTooManyRequests,
90 http.StatusInternalServerError, http.StatusBadGateway,
91 http.StatusServiceUnavailable, http.StatusGatewayTimeout:
92 l.Warningf("Metropolis bundle request HTTP %d error, retrying", bundleRes.StatusCode)
93 return fmt.Errorf("HTTP error %d", bundleRes.StatusCode)
94 default:
95 // Non-standard code range used for proxy-related issue by various
96 // vendors. Treat as non-permanent error.
97 if bundleRes.StatusCode >= 520 && bundleRes.StatusCode < 599 {
98 l.Warningf("Metropolis bundle request HTTP %d error, retrying", bundleRes.StatusCode)
99 return fmt.Errorf("HTTP error %d", bundleRes.StatusCode)
100 }
101 if bundleRes.StatusCode != 200 {
102 l.Errorf("Metropolis bundle request permanent HTTP %d error, aborting", bundleRes.StatusCode)
103 return backoff.Permanent(fmt.Errorf("HTTP error %d", bundleRes.StatusCode))
104 }
105 }
106 if _, err := bundleRaw.ReadFrom(bundleRes.Body); err != nil {
107 l.Warningf("Metropolis bundle download failed, retrying: %v", err)
108 bundleRaw.Reset()
109 return err
110 }
111 return nil
112 }, b)
113 if err != nil {
Tim Windelschmidt327cdba2024-05-21 13:51:32 +0200114 return fmt.Errorf("error downloading Metropolis bundle: %w", err)
Lorenz Brunaadeb792023-03-27 15:53:56 +0200115 }
116 l.Info("Metropolis Bundle downloaded")
117 bundle, err := zip.NewReader(bytes.NewReader(bundleRaw.Bytes()), int64(bundleRaw.Len()))
118 if err != nil {
119 return fmt.Errorf("failed to open node bundle: %w", err)
120 }
Jan Schärc1b6df42025-03-20 08:52:18 +0000121 efiPayload, err := zipBlob(bundle, "kernel_efi.efi")
Lorenz Brunaadeb792023-03-27 15:53:56 +0200122 if err != nil {
123 return fmt.Errorf("invalid bundle: %w", err)
124 }
Jan Schärc1b6df42025-03-20 08:52:18 +0000125 systemImage, err := zipBlob(bundle, "verity_rootfs.img")
Lorenz Brunaadeb792023-03-27 15:53:56 +0200126 if err != nil {
127 return fmt.Errorf("invalid bundle: %w", err)
128 }
Lorenz Brunaadeb792023-03-27 15:53:56 +0200129
130 nodeParamsRaw, err := proto.Marshal(req.NodeParameters)
131 if err != nil {
132 return fmt.Errorf("failed marshaling: %w", err)
133 }
134
Lorenz Brunad131882023-06-28 16:42:20 +0200135 rootDev, err := blockdev.Open(filepath.Join("/dev", req.RootDevice))
136 if err != nil {
137 return fmt.Errorf("failed to open root device: %w", err)
138 }
139
Lorenz Brunaadeb792023-03-27 15:53:56 +0200140 installParams := osimage.Params{
141 PartitionSize: osimage.PartitionSizeInfo{
Lorenz Brun35fcf032023-06-29 04:15:58 +0200142 ESP: 384,
Lorenz Brunaadeb792023-03-27 15:53:56 +0200143 System: 4096,
144 Data: 128,
145 },
146 SystemImage: systemImage,
Jan Schärc1b6df42025-03-20 08:52:18 +0000147 EFIPayload: efiPayload,
148 ABLoader: structfs.Bytes(abloader),
149 NodeParameters: structfs.Bytes(nodeParamsRaw),
Lorenz Brunad131882023-06-28 16:42:20 +0200150 Output: rootDev,
Lorenz Brunaadeb792023-03-27 15:53:56 +0200151 }
152
Tim Windelschmidtcc27faa2024-08-01 02:18:35 +0200153 be, err := osimage.Write(&installParams)
Lorenz Brunaadeb792023-03-27 15:53:56 +0200154 if err != nil {
155 return err
156 }
Lorenz Brunca1cff02023-06-26 17:52:44 +0200157 bootEntryIdx, err := efivarfs.AddBootEntry(be)
Lorenz Brunaadeb792023-03-27 15:53:56 +0200158 if err != nil {
159 return fmt.Errorf("error creating EFI boot entry: %w", err)
160 }
Lorenz Brun9933ef02023-07-06 18:28:29 +0200161 if err := efivarfs.SetBootOrder(efivarfs.BootOrder{uint16(bootEntryIdx)}); err != nil {
Lorenz Brunaadeb792023-03-27 15:53:56 +0200162 return fmt.Errorf("error setting EFI boot order: %w", err)
163 }
164 l.Info("Metropolis installation completed")
165 return nil
166}