| Tim Windelschmidt | 6d33a43 | 2025-02-04 14:34:25 +0100 | [diff] [blame] | 1 | // Copyright The Monogon Project Authors. |
| 2 | // SPDX-License-Identifier: Apache-2.0 |
| 3 | |
| Lorenz Brun | aadeb79 | 2023-03-27 15:53:56 +0200 | [diff] [blame] | 4 | package main |
| 5 | |
| 6 | import ( |
| 7 | "archive/zip" |
| 8 | "bytes" |
| Lorenz Brun | 54a5a05 | 2023-10-02 16:40:11 +0200 | [diff] [blame] | 9 | _ "embed" |
| Lorenz Brun | aadeb79 | 2023-03-27 15:53:56 +0200 | [diff] [blame] | 10 | "errors" |
| 11 | "fmt" |
| 12 | "net/http" |
| Tim Windelschmidt | 5832112 | 2024-09-10 02:26:03 +0200 | [diff] [blame] | 13 | "os" |
| Lorenz Brun | aadeb79 | 2023-03-27 15:53:56 +0200 | [diff] [blame] | 14 | "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 Bazanski | 3c5d063 | 2024-09-12 10:49:12 +0000 | [diff] [blame] | 20 | "source.monogon.dev/go/logging" |
| Tim Windelschmidt | 9f21f53 | 2024-05-07 15:14:20 +0200 | [diff] [blame] | 21 | "source.monogon.dev/osbase/blockdev" |
| Tim Windelschmidt | c2290c2 | 2024-08-15 19:56:00 +0200 | [diff] [blame] | 22 | "source.monogon.dev/osbase/build/mkimage/osimage" |
| Tim Windelschmidt | 9f21f53 | 2024-05-07 15:14:20 +0200 | [diff] [blame] | 23 | "source.monogon.dev/osbase/efivarfs" |
| Tim Windelschmidt | 10ef8f9 | 2024-08-13 15:35:10 +0200 | [diff] [blame] | 24 | npb "source.monogon.dev/osbase/net/proto" |
| Jan Schär | c1b6df4 | 2025-03-20 08:52:18 +0000 | [diff] [blame] | 25 | "source.monogon.dev/osbase/structfs" |
| Lorenz Brun | aadeb79 | 2023-03-27 15:53:56 +0200 | [diff] [blame] | 26 | ) |
| 27 | |
| Tim Windelschmidt | 1f51cf4 | 2024-10-01 17:04:28 +0200 | [diff] [blame] | 28 | //go:embed metropolis/node/core/abloader/abloader.efi |
| Lorenz Brun | 54a5a05 | 2023-10-02 16:40:11 +0200 | [diff] [blame] | 29 | var abloader []byte |
| 30 | |
| Jan Schär | c1b6df4 | 2025-03-20 08:52:18 +0000 | [diff] [blame] | 31 | // zipBlob looks up a file in a [zip.Reader] and adapts it to [structfs.Blob]. |
| 32 | func 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 Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 39 | } |
| 40 | |
| Jan Schär | c1b6df4 | 2025-03-20 08:52:18 +0000 | [diff] [blame] | 41 | type zipFileBlob struct { |
| 42 | *zip.File |
| 43 | } |
| 44 | |
| 45 | func (f zipFileBlob) Size() int64 { |
| 46 | return int64(f.File.UncompressedSize64) |
| Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 47 | } |
| 48 | |
| Lorenz Brun | aadeb79 | 2023-03-27 15:53:56 +0200 | [diff] [blame] | 49 | // install dispatches OSInstallationRequests to the appropriate installer |
| 50 | // method |
| Serge Bazanski | 3c5d063 | 2024-09-12 10:49:12 +0000 | [diff] [blame] | 51 | func install(req *bpb.OSInstallationRequest, netConfig *npb.Net, l logging.Leveled) error { |
| Lorenz Brun | aadeb79 | 2023-03-27 15:53:56 +0200 | [diff] [blame] | 52 | switch reqT := req.Type.(type) { |
| 53 | case *bpb.OSInstallationRequest_Metropolis: |
| Tim Windelschmidt | 5832112 | 2024-09-10 02:26:03 +0200 | [diff] [blame] | 54 | return installMetropolis(reqT.Metropolis, netConfig, l) |
| Lorenz Brun | aadeb79 | 2023-03-27 15:53:56 +0200 | [diff] [blame] | 55 | default: |
| 56 | return errors.New("unknown installation request type") |
| 57 | } |
| 58 | } |
| 59 | |
| Serge Bazanski | 3c5d063 | 2024-09-12 10:49:12 +0000 | [diff] [blame] | 60 | func installMetropolis(req *bpb.MetropolisInstallationRequest, netConfig *npb.Net, l logging.Leveled) error { |
| Tim Windelschmidt | 5832112 | 2024-09-10 02:26:03 +0200 | [diff] [blame] | 61 | // Validate we are running via EFI. |
| 62 | if _, err := os.Stat("/sys/firmware/efi"); os.IsNotExist(err) { |
| Tim Windelschmidt | 1f51cf4 | 2024-10-01 17:04:28 +0200 | [diff] [blame] | 63 | // nolint:ST1005 |
| Lorenz Brun | aadeb79 | 2023-03-27 15:53:56 +0200 | [diff] [blame] | 64 | return errors.New("Monogon OS can only be installed on EFI-booted machines, this one is not") |
| 65 | } |
| Tim Windelschmidt | fac4874 | 2023-04-24 19:04:55 +0200 | [diff] [blame] | 66 | |
| 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 Brun | aadeb79 | 2023-03-27 15:53:56 +0200 | [diff] [blame] | 73 | // 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 Windelschmidt | 327cdba | 2024-05-21 13:51:32 +0200 | [diff] [blame] | 85 | return fmt.Errorf("HTTP request failed: %w", err) |
| Lorenz Brun | aadeb79 | 2023-03-27 15:53:56 +0200 | [diff] [blame] | 86 | } |
| 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 Windelschmidt | 327cdba | 2024-05-21 13:51:32 +0200 | [diff] [blame] | 114 | return fmt.Errorf("error downloading Metropolis bundle: %w", err) |
| Lorenz Brun | aadeb79 | 2023-03-27 15:53:56 +0200 | [diff] [blame] | 115 | } |
| 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är | c1b6df4 | 2025-03-20 08:52:18 +0000 | [diff] [blame] | 121 | efiPayload, err := zipBlob(bundle, "kernel_efi.efi") |
| Lorenz Brun | aadeb79 | 2023-03-27 15:53:56 +0200 | [diff] [blame] | 122 | if err != nil { |
| 123 | return fmt.Errorf("invalid bundle: %w", err) |
| 124 | } |
| Jan Schär | c1b6df4 | 2025-03-20 08:52:18 +0000 | [diff] [blame] | 125 | systemImage, err := zipBlob(bundle, "verity_rootfs.img") |
| Lorenz Brun | aadeb79 | 2023-03-27 15:53:56 +0200 | [diff] [blame] | 126 | if err != nil { |
| 127 | return fmt.Errorf("invalid bundle: %w", err) |
| 128 | } |
| Lorenz Brun | aadeb79 | 2023-03-27 15:53:56 +0200 | [diff] [blame] | 129 | |
| 130 | nodeParamsRaw, err := proto.Marshal(req.NodeParameters) |
| 131 | if err != nil { |
| 132 | return fmt.Errorf("failed marshaling: %w", err) |
| 133 | } |
| 134 | |
| Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 135 | 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 Brun | aadeb79 | 2023-03-27 15:53:56 +0200 | [diff] [blame] | 140 | installParams := osimage.Params{ |
| 141 | PartitionSize: osimage.PartitionSizeInfo{ |
| Lorenz Brun | 35fcf03 | 2023-06-29 04:15:58 +0200 | [diff] [blame] | 142 | ESP: 384, |
| Lorenz Brun | aadeb79 | 2023-03-27 15:53:56 +0200 | [diff] [blame] | 143 | System: 4096, |
| 144 | Data: 128, |
| 145 | }, |
| 146 | SystemImage: systemImage, |
| Jan Schär | c1b6df4 | 2025-03-20 08:52:18 +0000 | [diff] [blame] | 147 | EFIPayload: efiPayload, |
| 148 | ABLoader: structfs.Bytes(abloader), |
| 149 | NodeParameters: structfs.Bytes(nodeParamsRaw), |
| Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 150 | Output: rootDev, |
| Lorenz Brun | aadeb79 | 2023-03-27 15:53:56 +0200 | [diff] [blame] | 151 | } |
| 152 | |
| Tim Windelschmidt | cc27faa | 2024-08-01 02:18:35 +0200 | [diff] [blame] | 153 | be, err := osimage.Write(&installParams) |
| Lorenz Brun | aadeb79 | 2023-03-27 15:53:56 +0200 | [diff] [blame] | 154 | if err != nil { |
| 155 | return err |
| 156 | } |
| Lorenz Brun | ca1cff0 | 2023-06-26 17:52:44 +0200 | [diff] [blame] | 157 | bootEntryIdx, err := efivarfs.AddBootEntry(be) |
| Lorenz Brun | aadeb79 | 2023-03-27 15:53:56 +0200 | [diff] [blame] | 158 | if err != nil { |
| 159 | return fmt.Errorf("error creating EFI boot entry: %w", err) |
| 160 | } |
| Lorenz Brun | 9933ef0 | 2023-07-06 18:28:29 +0200 | [diff] [blame] | 161 | if err := efivarfs.SetBootOrder(efivarfs.BootOrder{uint16(bootEntryIdx)}); err != nil { |
| Lorenz Brun | aadeb79 | 2023-03-27 15:53:56 +0200 | [diff] [blame] | 162 | return fmt.Errorf("error setting EFI boot order: %w", err) |
| 163 | } |
| 164 | l.Info("Metropolis installation completed") |
| 165 | return nil |
| 166 | } |