blob: 4df489d96498d4a0d886491408a9f8855b1851b1 [file] [log] [blame]
Serge Bazanski42e61c62021-03-18 15:07:18 +01001// Copyright 2020 The Monogon Project Authors.
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
Serge Bazanskia959cbd2021-06-17 15:56:51 +020017// cluster implements low-level clustering logic, especially logic regarding to
18// bootstrapping, registering into and joining a cluster. Its goal is to provide
19// the rest of the node code with the following:
20// - A mounted plaintext storage.
21// - Node credentials/identity.
22// - A locally running etcd server if the node is supposed to run one, and a
23// client connection to that etcd cluster if so.
24// - The state of the cluster as seen by the node, to enable code to respond to
25// node lifecycle changes.
Serge Bazanski42e61c62021-03-18 15:07:18 +010026package cluster
27
28import (
Serge Bazanskia959cbd2021-06-17 15:56:51 +020029 "context"
Leopold Schabela5545282021-12-04 23:29:44 +010030 "encoding/base64"
Serge Bazanskia959cbd2021-06-17 15:56:51 +020031 "errors"
Serge Bazanski42e61c62021-03-18 15:07:18 +010032 "fmt"
Leopold Schabela5545282021-12-04 23:29:44 +010033 "io"
Mateusz Zalega2930e992022-04-25 12:52:35 +020034 "net"
Leopold Schabela5545282021-12-04 23:29:44 +010035 "net/http"
Lorenz Brun764a2de2021-11-22 16:26:36 +010036 "os"
Mateusz Zalega2930e992022-04-25 12:52:35 +020037 "strings"
Serge Bazanskia959cbd2021-06-17 15:56:51 +020038 "sync"
Serge Bazanski42e61c62021-03-18 15:07:18 +010039
Leopold Schabela5545282021-12-04 23:29:44 +010040 "github.com/cenkalti/backoff/v4"
Serge Bazanskia959cbd2021-06-17 15:56:51 +020041 "google.golang.org/protobuf/proto"
42
Mateusz Zalega2930e992022-04-25 12:52:35 +020043 "source.monogon.dev/metropolis/node"
Serge Bazanskia959cbd2021-06-17 15:56:51 +020044 "source.monogon.dev/metropolis/node/core/consensus"
45 "source.monogon.dev/metropolis/node/core/localstorage"
46 "source.monogon.dev/metropolis/node/core/network"
Serge Bazanski6dff6d62022-01-28 18:15:14 +010047 "source.monogon.dev/metropolis/node/core/roleserve"
Serge Bazanskia959cbd2021-06-17 15:56:51 +020048 "source.monogon.dev/metropolis/pkg/event/memory"
49 "source.monogon.dev/metropolis/pkg/supervisor"
50 apb "source.monogon.dev/metropolis/proto/api"
Mateusz Zalega2930e992022-04-25 12:52:35 +020051 cpb "source.monogon.dev/metropolis/proto/common"
Serge Bazanskia959cbd2021-06-17 15:56:51 +020052 ppb "source.monogon.dev/metropolis/proto/private"
Serge Bazanski42e61c62021-03-18 15:07:18 +010053)
54
Serge Bazanskia959cbd2021-06-17 15:56:51 +020055type state struct {
56 mu sync.RWMutex
Serge Bazanski42e61c62021-03-18 15:07:18 +010057
Serge Bazanskia959cbd2021-06-17 15:56:51 +020058 oneway bool
Serge Bazanski42e61c62021-03-18 15:07:18 +010059
Serge Bazanskia959cbd2021-06-17 15:56:51 +020060 configuration *ppb.SealedConfiguration
Serge Bazanski42e61c62021-03-18 15:07:18 +010061}
62
Serge Bazanskia959cbd2021-06-17 15:56:51 +020063type Manager struct {
64 storageRoot *localstorage.Root
65 networkService *network.Service
Serge Bazanski6dff6d62022-01-28 18:15:14 +010066 roleServer *roleserve.Service
Serge Bazanskia959cbd2021-06-17 15:56:51 +020067 status memory.Value
68
69 state
70
71 // consensus is the spawned etcd/consensus service, if the Manager brought
72 // up a Node that should run one.
73 consensus *consensus.Service
74}
75
76// NewManager creates a new cluster Manager. The given localstorage Root must
77// be places, but not yet started (and will be started as the Manager makes
78// progress). The given network Service must already be running.
Serge Bazanski6dff6d62022-01-28 18:15:14 +010079func NewManager(storageRoot *localstorage.Root, networkService *network.Service, rs *roleserve.Service) *Manager {
Serge Bazanskia959cbd2021-06-17 15:56:51 +020080 return &Manager{
81 storageRoot: storageRoot,
82 networkService: networkService,
Serge Bazanski6dff6d62022-01-28 18:15:14 +010083 roleServer: rs,
Serge Bazanskia959cbd2021-06-17 15:56:51 +020084
85 state: state{},
86 }
87}
88
89func (m *Manager) lock() (*state, func()) {
90 m.mu.Lock()
91 return &m.state, m.mu.Unlock
92}
93
94func (m *Manager) rlock() (*state, func()) {
95 m.mu.RLock()
96 return &m.state, m.mu.RUnlock
97}
98
99// Run is the runnable of the Manager, to be started using the Supervisor. It
100// is one-shot, and should not be restarted.
101func (m *Manager) Run(ctx context.Context) error {
102 state, unlock := m.lock()
103 if state.oneway {
104 unlock()
105 // TODO(q3k): restart the entire system if this happens
106 return fmt.Errorf("cannot restart cluster manager")
107 }
108 state.oneway = true
109 unlock()
110
Lorenz Brun6c35e972021-12-14 03:08:23 +0100111 configuration, err := m.storageRoot.ESP.Metropolis.SealedConfiguration.Unseal()
Serge Bazanskia959cbd2021-06-17 15:56:51 +0200112 if err == nil {
113 supervisor.Logger(ctx).Info("Sealed configuration present. attempting to join cluster")
Mateusz Zalega2930e992022-04-25 12:52:35 +0200114
115 // Read Cluster Directory and unmarshal it. Since the node is already
116 // registered with the cluster, the directory won't be bootstrapped from
117 // Node Parameters.
118 cd, err := m.storageRoot.ESP.Metropolis.ClusterDirectory.Unmarshal()
119 if err != nil {
120 return fmt.Errorf("while reading cluster directory: %w", err)
121 }
122 return m.join(ctx, configuration, cd)
Serge Bazanskia959cbd2021-06-17 15:56:51 +0200123 }
124
125 if !errors.Is(err, localstorage.ErrNoSealed) {
126 return fmt.Errorf("unexpected sealed config error: %w", err)
127 }
128
129 supervisor.Logger(ctx).Info("No sealed configuration, looking for node parameters")
130
131 params, err := m.nodeParams(ctx)
132 if err != nil {
133 return fmt.Errorf("no parameters available: %w", err)
134 }
135
136 switch inner := params.Cluster.(type) {
137 case *apb.NodeParameters_ClusterBootstrap_:
Serge Bazanski5839e972021-11-16 15:46:19 +0100138 err = m.bootstrap(ctx, inner.ClusterBootstrap)
Serge Bazanskia959cbd2021-06-17 15:56:51 +0200139 case *apb.NodeParameters_ClusterRegister_:
Serge Bazanski5839e972021-11-16 15:46:19 +0100140 err = m.register(ctx, inner.ClusterRegister)
Serge Bazanskia959cbd2021-06-17 15:56:51 +0200141 default:
Serge Bazanski5839e972021-11-16 15:46:19 +0100142 err = fmt.Errorf("node parameters misconfigured: neither cluster_bootstrap nor cluster_register set")
Serge Bazanskia959cbd2021-06-17 15:56:51 +0200143 }
Serge Bazanski5839e972021-11-16 15:46:19 +0100144
145 if err == nil {
146 supervisor.Logger(ctx).Info("Cluster enrolment done.")
147 }
148 return err
Serge Bazanskia959cbd2021-06-17 15:56:51 +0200149}
150
Serge Bazanskia959cbd2021-06-17 15:56:51 +0200151func (m *Manager) nodeParamsFWCFG(ctx context.Context) (*apb.NodeParameters, error) {
Lorenz Brun764a2de2021-11-22 16:26:36 +0100152 bytes, err := os.ReadFile("/sys/firmware/qemu_fw_cfg/by_name/dev.monogon.metropolis/parameters.pb/raw")
Serge Bazanskia959cbd2021-06-17 15:56:51 +0200153 if err != nil {
154 return nil, fmt.Errorf("could not read firmware enrolment file: %w", err)
155 }
156
157 config := apb.NodeParameters{}
158 err = proto.Unmarshal(bytes, &config)
159 if err != nil {
160 return nil, fmt.Errorf("could not unmarshal: %v", err)
161 }
162
163 return &config, nil
164}
165
Leopold Schabela5545282021-12-04 23:29:44 +0100166// nodeParamsGCPMetadata attempts to retrieve the node parameters from the
167// GCP metadata service. Returns nil if the metadata service is available,
168// but no node parameters are specified.
169func (m *Manager) nodeParamsGCPMetadata(ctx context.Context) (*apb.NodeParameters, error) {
170 const metadataURL = "http://169.254.169.254/computeMetadata/v1/instance/attributes/metropolis-node-params"
171 req, err := http.NewRequestWithContext(ctx, "GET", metadataURL, nil)
172 if err != nil {
173 return nil, fmt.Errorf("could not create request: %w", err)
174 }
175 req.Header.Set("Metadata-Flavor", "Google")
176 resp, err := http.DefaultClient.Do(req)
177 if err != nil {
178 return nil, fmt.Errorf("HTTP request failed: %w", err)
179 }
180 defer resp.Body.Close()
181 if resp.StatusCode != http.StatusOK {
182 if resp.StatusCode == http.StatusNotFound {
183 return nil, nil
184 }
185 return nil, fmt.Errorf("non-200 status code: %d", resp.StatusCode)
186 }
187 decoded, err := io.ReadAll(base64.NewDecoder(base64.StdEncoding, resp.Body))
188 if err != nil {
189 return nil, fmt.Errorf("cannot decode base64: %w", err)
190 }
191 config := apb.NodeParameters{}
192 err = proto.Unmarshal(decoded, &config)
193 if err != nil {
194 return nil, fmt.Errorf("failed unmarshalling NodeParameters: %w", err)
195 }
196 return &config, nil
197}
198
Serge Bazanskia959cbd2021-06-17 15:56:51 +0200199func (m *Manager) nodeParams(ctx context.Context) (*apb.NodeParameters, error) {
Leopold Schabela5545282021-12-04 23:29:44 +0100200 boardName, err := getDMIBoardName()
201 if err != nil {
202 supervisor.Logger(ctx).Warningf("Could not get board name, cannot detect platform: %v", err)
203 }
204 supervisor.Logger(ctx).Infof("Board name: %q", boardName)
205
206 // When running on GCP, attempt to retrieve the node parameters from the
207 // metadata server first. Retry until we get a response, since we need to
208 // wait for the network service to assign an IP address first.
209 if isGCPInstance(boardName) {
210 var params *apb.NodeParameters
211 op := func() error {
212 supervisor.Logger(ctx).Info("Running on GCP, attempting to retrieve node parameters from metadata server")
213 params, err = m.nodeParamsGCPMetadata(ctx)
214 return err
215 }
216 err := backoff.Retry(op, backoff.WithContext(backoff.NewExponentialBackOff(), ctx))
217 if err != nil {
218 supervisor.Logger(ctx).Errorf("Failed to retrieve node parameters: %v", err)
219 }
220 if params != nil {
221 supervisor.Logger(ctx).Info("Retrieved parameters from GCP metadata server")
222 return params, nil
223 }
224 supervisor.Logger(ctx).Infof("\"metropolis-node-params\" metadata not found")
225 }
226
Serge Bazanskia959cbd2021-06-17 15:56:51 +0200227 // Retrieve node parameters from qemu's fwcfg interface or ESP.
228 // TODO(q3k): probably abstract this away and implement per platform/build/...
229 paramsFWCFG, err := m.nodeParamsFWCFG(ctx)
230 if err != nil {
231 supervisor.Logger(ctx).Warningf("Could not retrieve node parameters from qemu fwcfg: %v", err)
232 paramsFWCFG = nil
233 } else {
234 supervisor.Logger(ctx).Infof("Retrieved node parameters from qemu fwcfg")
235 }
Lorenz Brun6c35e972021-12-14 03:08:23 +0100236 paramsESP, err := m.storageRoot.ESP.Metropolis.NodeParameters.Unmarshal()
Serge Bazanskia959cbd2021-06-17 15:56:51 +0200237 if err != nil {
238 supervisor.Logger(ctx).Warningf("Could not retrieve node parameters from ESP: %v", err)
239 paramsESP = nil
240 } else {
241 supervisor.Logger(ctx).Infof("Retrieved node parameters from ESP")
242 }
243 if paramsFWCFG == nil && paramsESP == nil {
244 return nil, fmt.Errorf("could not find node parameters in ESP or qemu fwcfg")
245 }
246 if paramsFWCFG != nil && paramsESP != nil {
247 supervisor.Logger(ctx).Warningf("Node parameters found both in both ESP and qemu fwcfg, using the latter")
248 return paramsFWCFG, nil
249 } else if paramsFWCFG != nil {
250 return paramsFWCFG, nil
251 } else {
252 return paramsESP, nil
253 }
254}
255
Mateusz Zalega2930e992022-04-25 12:52:35 +0200256// logClusterDirectory verbosely logs the whole Cluster Directory passed to it.
257func logClusterDirectory(ctx context.Context, cd *cpb.ClusterDirectory) {
258 for _, node := range cd.Nodes {
Mateusz Zalega2930e992022-04-25 12:52:35 +0200259 var addresses []string
260 for _, add := range node.Addresses {
261 addresses = append(addresses, add.Host)
262 }
Mateusz Zalegade821502022-04-29 16:37:17 +0200263 supervisor.Logger(ctx).Infof(" Addresses: %s", strings.Join(addresses, ","))
Mateusz Zalega2930e992022-04-25 12:52:35 +0200264 }
265}
266
267// curatorRemote returns a host:port pair pointing at one of the cluster's
268// available Curator endpoints. It will return an empty string, and an error,
269// if the cluster directory is empty.
270// TODO(issues/117): use dynamic cluster client instead
271func curatorRemote(cd *cpb.ClusterDirectory) (string, error) {
272 if len(cd.Nodes) == 0 {
273 return "", fmt.Errorf("the Cluster Directory is empty.")
274 }
275 n := cd.Nodes[0]
276 if len(n.Addresses) == 0 {
277 return "", fmt.Errorf("the first node in the Cluster Directory doesn't have an associated Address.")
278 }
279 r := n.Addresses[0].Host
280 return net.JoinHostPort(r, node.CuratorServicePort.PortString()), nil
Serge Bazanskia959cbd2021-06-17 15:56:51 +0200281}