blob: 993dd645b08a687c3c56fbb4af80a1cda81366f7 [file] [log] [blame]
Lorenz Brun85ad26a2023-03-27 17:00:00 +02001package main
2
3import (
4 "context"
5 "encoding/base64"
6 "errors"
7 "fmt"
8 "io"
9 "net/http"
10 "os"
11 "strings"
12
13 "github.com/cenkalti/backoff/v4"
14 "google.golang.org/protobuf/proto"
15
16 "source.monogon.dev/metropolis/node/core/localstorage"
17 "source.monogon.dev/metropolis/pkg/supervisor"
18 apb "source.monogon.dev/metropolis/proto/api"
19)
20
21func nodeParamsFWCFG(ctx context.Context) (*apb.NodeParameters, error) {
22 bytes, err := os.ReadFile("/sys/firmware/qemu_fw_cfg/by_name/dev.monogon.metropolis/parameters.pb/raw")
23 if err != nil {
24 return nil, fmt.Errorf("could not read firmware enrolment file: %w", err)
25 }
26
27 config := apb.NodeParameters{}
28 err = proto.Unmarshal(bytes, &config)
29 if err != nil {
30 return nil, fmt.Errorf("could not unmarshal: %v", err)
31 }
32
33 return &config, nil
34}
35
36// nodeParamsGCPMetadata attempts to retrieve the node parameters from the
37// GCP metadata service. Returns nil if the metadata service is available,
38// but no node parameters are specified.
39func nodeParamsGCPMetadata(ctx context.Context) (*apb.NodeParameters, error) {
40 const metadataURL = "http://169.254.169.254/computeMetadata/v1/instance/attributes/metropolis-node-params"
41 req, err := http.NewRequestWithContext(ctx, "GET", metadataURL, nil)
42 if err != nil {
43 return nil, fmt.Errorf("could not create request: %w", err)
44 }
45 req.Header.Set("Metadata-Flavor", "Google")
46 resp, err := http.DefaultClient.Do(req)
47 if err != nil {
48 return nil, fmt.Errorf("HTTP request failed: %w", err)
49 }
50 defer resp.Body.Close()
51 if resp.StatusCode != http.StatusOK {
52 if resp.StatusCode == http.StatusNotFound {
53 return nil, nil
54 }
55 return nil, fmt.Errorf("non-200 status code: %d", resp.StatusCode)
56 }
57 decoded, err := io.ReadAll(base64.NewDecoder(base64.StdEncoding, resp.Body))
58 if err != nil {
59 return nil, fmt.Errorf("cannot decode base64: %w", err)
60 }
61 config := apb.NodeParameters{}
62 err = proto.Unmarshal(decoded, &config)
63 if err != nil {
64 return nil, fmt.Errorf("failed unmarshalling NodeParameters: %w", err)
65 }
66 return &config, nil
67}
68
69func getDMIBoardName() (string, error) {
70 b, err := os.ReadFile("/sys/devices/virtual/dmi/id/board_name")
71 if err != nil {
72 return "", fmt.Errorf("could not read board name: %w", err)
73 }
74 return strings.TrimRight(string(b), "\n"), nil
75}
76
77func isGCPInstance(boardName string) bool {
78 return boardName == "Google Compute Engine"
79}
80
81func getNodeParams(ctx context.Context, storage *localstorage.Root) (*apb.NodeParameters, error) {
82 boardName, err := getDMIBoardName()
83 if err != nil {
84 if errors.Is(err, os.ErrNotExist) {
85 supervisor.Logger(ctx).Infof("Board name: UNKNOWN")
86 } else {
87 supervisor.Logger(ctx).Warningf("Could not get board name, cannot detect platform: %v", err)
88 }
89 } else {
90 supervisor.Logger(ctx).Infof("Board name: %q", boardName)
91 }
92
93 // When running on GCP, attempt to retrieve the node parameters from the
94 // metadata server first. Retry until we get a response, since we need to
95 // wait for the network service to assign an IP address first.
96 if isGCPInstance(boardName) {
97 var params *apb.NodeParameters
98 op := func() error {
99 supervisor.Logger(ctx).Info("Running on GCP, attempting to retrieve node parameters from metadata server")
100 params, err = nodeParamsGCPMetadata(ctx)
101 return err
102 }
103 err := backoff.Retry(op, backoff.WithContext(backoff.NewExponentialBackOff(), ctx))
104 if err != nil {
105 supervisor.Logger(ctx).Errorf("Failed to retrieve node parameters: %v", err)
106 }
107 if params != nil {
108 supervisor.Logger(ctx).Info("Retrieved parameters from GCP metadata server")
109 return params, nil
110 }
111 supervisor.Logger(ctx).Infof("\"metropolis-node-params\" metadata not found")
112 }
113
114 // Retrieve node parameters from qemu's fwcfg interface or ESP.
115 // TODO(q3k): probably abstract this away and implement per platform/build/...
116 paramsFWCFG, err := nodeParamsFWCFG(ctx)
117 if err != nil {
118 if errors.Is(err, os.ErrNotExist) {
119 supervisor.Logger(ctx).Infof("No qemu fwcfg params.")
120 } else {
121 supervisor.Logger(ctx).Warningf("Could not retrieve node parameters from qemu fwcfg: %v", err)
122 }
123 paramsFWCFG = nil
124 } else {
125 supervisor.Logger(ctx).Infof("Retrieved node parameters from qemu fwcfg")
126 }
127 paramsESP, err := storage.ESP.Metropolis.NodeParameters.Unmarshal()
128 if err != nil {
129 if errors.Is(err, os.ErrNotExist) {
130 supervisor.Logger(ctx).Infof("No ESP node parameters.")
131 } else {
132 supervisor.Logger(ctx).Warningf("Could not retrieve node parameters from ESP: %v", err)
133 }
134 paramsESP = nil
135 } else {
136 supervisor.Logger(ctx).Infof("Retrieved node parameters from ESP")
137 }
138 if paramsFWCFG == nil && paramsESP == nil {
139 return nil, fmt.Errorf("could not find node parameters in ESP or qemu fwcfg")
140 }
141 if paramsFWCFG != nil && paramsESP != nil {
142 supervisor.Logger(ctx).Warningf("Node parameters found both in both ESP and qemu fwcfg, using the latter")
143 return paramsFWCFG, nil
144 } else if paramsFWCFG != nil {
145 return paramsFWCFG, nil
146 } else {
147 return paramsESP, nil
148 }
149}