blob: cbd912a04b57b347a030c8e6542cbd5c93e606df [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
Serge Bazanski4abeb132022-10-11 11:32:19 +02004package server
5
6import (
7 "context"
8 "crypto/ed25519"
Serge Bazanski6c9535b2023-01-03 13:17:42 +01009 "encoding/hex"
Serge Bazanski4abeb132022-10-11 11:32:19 +020010 "errors"
11 "fmt"
12 "time"
13
14 "github.com/google/uuid"
15 "google.golang.org/grpc/codes"
16 "google.golang.org/grpc/status"
17 "google.golang.org/protobuf/proto"
Tim Windelschmidt4264b8c2023-06-12 23:54:58 +020018 "k8s.io/klog/v2"
Serge Bazanski4abeb132022-10-11 11:32:19 +020019
Serge Bazanski4abeb132022-10-11 11:32:19 +020020 apb "source.monogon.dev/cloud/bmaas/server/api"
Tim Windelschmidt53087302023-06-27 16:36:31 +020021
22 "source.monogon.dev/cloud/bmaas/bmdb/model"
Serge Bazanski4abeb132022-10-11 11:32:19 +020023 "source.monogon.dev/metropolis/node/core/rpc"
24)
25
26type agentCallbackService struct {
27 s *Server
28}
29
30var (
31 errAgentUnauthenticated = errors.New("machine id or public key unknown")
32)
33
34func (a *agentCallbackService) Heartbeat(ctx context.Context, req *apb.AgentHeartbeatRequest) (*apb.AgentHeartbeatResponse, error) {
35 // Extract ED25519 self-signed certificate from client connection.
36 cert, err := rpc.GetPeerCertificate(ctx)
37 if err != nil {
38 return nil, err
39 }
40 pk := cert.PublicKey.(ed25519.PublicKey)
41 machineId, err := uuid.Parse(req.MachineId)
42 if err != nil {
43 return nil, status.Error(codes.InvalidArgument, "machine_id invalid")
44 }
45
Serge Bazanski42f13462023-04-19 15:00:06 +020046 session, err := a.s.session(ctx)
Serge Bazanski4abeb132022-10-11 11:32:19 +020047 if err != nil {
48 klog.Errorf("Could not start session: %v", err)
49 return nil, status.Error(codes.Unavailable, "could not start session")
50 }
51
52 // Verify that machine ID and connection public key match up to a machine in the
53 // BMDB. Prevent leaking information about a machine's existence to unauthorized
54 // agents.
55 err = session.Transact(ctx, func(q *model.Queries) error {
56 agents, err := q.AuthenticateAgentConnection(ctx, model.AuthenticateAgentConnectionParams{
57 MachineID: machineId,
58 AgentPublicKey: pk,
59 })
60 if err != nil {
61 return fmt.Errorf("AuthenticateAgentConnection: %w", err)
62 }
63 if len(agents) < 1 {
Serge Bazanski6c9535b2023-01-03 13:17:42 +010064 klog.Errorf("No agent for %s/%s", machineId.String(), hex.EncodeToString(pk))
Serge Bazanski4abeb132022-10-11 11:32:19 +020065 return errAgentUnauthenticated
66 }
67 return nil
68 })
69 if err != nil {
70 if errors.Is(err, errAgentUnauthenticated) {
71 return nil, status.Error(codes.Unauthenticated, err.Error())
72 }
73 klog.Errorf("Could not authenticate agent: %v", err)
74 return nil, status.Error(codes.Unavailable, "could not authenticate agent")
75 }
76
77 // Request is now authenticated.
78
79 // Serialize hardware report if submitted alongside heartbeat.
80 var hwraw []byte
81 if req.HardwareReport != nil {
82 hwraw, err = proto.Marshal(req.HardwareReport)
83 if err != nil {
Serge Bazanski6c9535b2023-01-03 13:17:42 +010084 return nil, status.Errorf(codes.InvalidArgument, "could not serialize hardware report: %v", err)
Serge Bazanski4abeb132022-10-11 11:32:19 +020085 }
86 }
87
Tim Windelschmidt53087302023-06-27 16:36:31 +020088 var installRaw []byte
89 if req.InstallationReport != nil {
90 installRaw, err = proto.Marshal(req.InstallationReport)
91 if err != nil {
92 return nil, status.Errorf(codes.InvalidArgument, "could not serialize installation report: %v", err)
93 }
94 }
95
Serge Bazanski4abeb132022-10-11 11:32:19 +020096 // Upsert heartbeat time and hardware report.
97 err = session.Transact(ctx, func(q *model.Queries) error {
98 // Upsert hardware report if submitted.
Tim Windelschmidt53087302023-06-27 16:36:31 +020099 if len(hwraw) != 0 {
Serge Bazanski4abeb132022-10-11 11:32:19 +0200100 err = q.MachineSetHardwareReport(ctx, model.MachineSetHardwareReportParams{
101 MachineID: machineId,
102 HardwareReportRaw: hwraw,
103 })
104 if err != nil {
105 return fmt.Errorf("hardware report upsert: %w", err)
106 }
107 }
Serge Bazanski6c9535b2023-01-03 13:17:42 +0100108 // Upsert os installation report if submitted.
Tim Windelschmidt53087302023-06-27 16:36:31 +0200109 if len(installRaw) != 0 {
110 var result model.MachineOsInstallationResult
111 switch req.InstallationReport.Result.(type) {
112 case *apb.OSInstallationReport_Success_:
113 result = model.MachineOsInstallationResultSuccess
114 case *apb.OSInstallationReport_Error_:
115 result = model.MachineOsInstallationResultError
116 default:
117 return fmt.Errorf("unknown installation report result: %T", req.InstallationReport.Result)
118 }
Serge Bazanski6c9535b2023-01-03 13:17:42 +0100119 err = q.MachineSetOSInstallationReport(ctx, model.MachineSetOSInstallationReportParams{
Tim Windelschmidt53087302023-06-27 16:36:31 +0200120 MachineID: machineId,
121 Generation: req.InstallationReport.Generation,
122 OsInstallationResult: result,
123 OsInstallationReportRaw: installRaw,
Serge Bazanski6c9535b2023-01-03 13:17:42 +0100124 })
125 }
Serge Bazanski4abeb132022-10-11 11:32:19 +0200126 return q.MachineSetAgentHeartbeat(ctx, model.MachineSetAgentHeartbeatParams{
127 MachineID: machineId,
128 AgentHeartbeatAt: time.Now(),
129 })
130 })
131 if err != nil {
132 klog.Errorf("Could not submit heartbeat: %v", err)
133 return nil, status.Error(codes.Unavailable, "could not submit heartbeat")
134 }
Serge Bazanski6c9535b2023-01-03 13:17:42 +0100135 klog.Infof("Heartbeat from %s/%s", machineId.String(), hex.EncodeToString(pk))
Serge Bazanski4abeb132022-10-11 11:32:19 +0200136
Serge Bazanski6c9535b2023-01-03 13:17:42 +0100137 // Get installation request for machine if present.
138 var installRequest *apb.OSInstallationRequest
139 err = session.Transact(ctx, func(q *model.Queries) error {
140 reqs, err := q.GetExactMachineForOSInstallation(ctx, model.GetExactMachineForOSInstallationParams{
141 MachineID: machineId,
142 Limit: 1,
143 })
144 if err != nil {
145 return fmt.Errorf("GetExactMachineForOSInstallation: %w", err)
146 }
147 if len(reqs) > 0 {
148 raw := reqs[0].OsInstallationRequestRaw
149 var preq apb.OSInstallationRequest
150 if err := proto.Unmarshal(raw, &preq); err != nil {
151 return fmt.Errorf("could not decode stored OS installation request: %w", err)
152 }
153 installRequest = &preq
154 }
155 return nil
156 })
157 if err != nil {
158 // Do not fail entire request. Instead, just log an error.
159 // TODO(q3k): alert on this
160 klog.Errorf("Failure during OS installation request retrieval: %v", err)
161 }
162
163 return &apb.AgentHeartbeatResponse{
164 InstallationRequest: installRequest,
165 }, nil
Serge Bazanski4abeb132022-10-11 11:32:19 +0200166}