blob: 97636f76189c37db8431321c8c666effd02b31b1 [file] [log] [blame]
Serge Bazanski4abeb132022-10-11 11:32:19 +02001package server
2
3import (
4 "context"
5 "crypto/ed25519"
Serge Bazanski6c9535b2023-01-03 13:17:42 +01006 "encoding/hex"
Serge Bazanski4abeb132022-10-11 11:32:19 +02007 "errors"
8 "fmt"
9 "time"
10
11 "github.com/google/uuid"
12 "google.golang.org/grpc/codes"
13 "google.golang.org/grpc/status"
14 "google.golang.org/protobuf/proto"
Tim Windelschmidt4264b8c2023-06-12 23:54:58 +020015 "k8s.io/klog/v2"
Serge Bazanski4abeb132022-10-11 11:32:19 +020016
Serge Bazanski4abeb132022-10-11 11:32:19 +020017 apb "source.monogon.dev/cloud/bmaas/server/api"
Tim Windelschmidt53087302023-06-27 16:36:31 +020018
19 "source.monogon.dev/cloud/bmaas/bmdb/model"
Serge Bazanski4abeb132022-10-11 11:32:19 +020020 "source.monogon.dev/metropolis/node/core/rpc"
21)
22
23type agentCallbackService struct {
24 s *Server
25}
26
27var (
28 errAgentUnauthenticated = errors.New("machine id or public key unknown")
29)
30
31func (a *agentCallbackService) Heartbeat(ctx context.Context, req *apb.AgentHeartbeatRequest) (*apb.AgentHeartbeatResponse, error) {
32 // Extract ED25519 self-signed certificate from client connection.
33 cert, err := rpc.GetPeerCertificate(ctx)
34 if err != nil {
35 return nil, err
36 }
37 pk := cert.PublicKey.(ed25519.PublicKey)
38 machineId, err := uuid.Parse(req.MachineId)
39 if err != nil {
40 return nil, status.Error(codes.InvalidArgument, "machine_id invalid")
41 }
42
Serge Bazanski42f13462023-04-19 15:00:06 +020043 session, err := a.s.session(ctx)
Serge Bazanski4abeb132022-10-11 11:32:19 +020044 if err != nil {
45 klog.Errorf("Could not start session: %v", err)
46 return nil, status.Error(codes.Unavailable, "could not start session")
47 }
48
49 // Verify that machine ID and connection public key match up to a machine in the
50 // BMDB. Prevent leaking information about a machine's existence to unauthorized
51 // agents.
52 err = session.Transact(ctx, func(q *model.Queries) error {
53 agents, err := q.AuthenticateAgentConnection(ctx, model.AuthenticateAgentConnectionParams{
54 MachineID: machineId,
55 AgentPublicKey: pk,
56 })
57 if err != nil {
58 return fmt.Errorf("AuthenticateAgentConnection: %w", err)
59 }
60 if len(agents) < 1 {
Serge Bazanski6c9535b2023-01-03 13:17:42 +010061 klog.Errorf("No agent for %s/%s", machineId.String(), hex.EncodeToString(pk))
Serge Bazanski4abeb132022-10-11 11:32:19 +020062 return errAgentUnauthenticated
63 }
64 return nil
65 })
66 if err != nil {
67 if errors.Is(err, errAgentUnauthenticated) {
68 return nil, status.Error(codes.Unauthenticated, err.Error())
69 }
70 klog.Errorf("Could not authenticate agent: %v", err)
71 return nil, status.Error(codes.Unavailable, "could not authenticate agent")
72 }
73
74 // Request is now authenticated.
75
76 // Serialize hardware report if submitted alongside heartbeat.
77 var hwraw []byte
78 if req.HardwareReport != nil {
79 hwraw, err = proto.Marshal(req.HardwareReport)
80 if err != nil {
Serge Bazanski6c9535b2023-01-03 13:17:42 +010081 return nil, status.Errorf(codes.InvalidArgument, "could not serialize hardware report: %v", err)
Serge Bazanski4abeb132022-10-11 11:32:19 +020082 }
83 }
84
Tim Windelschmidt53087302023-06-27 16:36:31 +020085 var installRaw []byte
86 if req.InstallationReport != nil {
87 installRaw, err = proto.Marshal(req.InstallationReport)
88 if err != nil {
89 return nil, status.Errorf(codes.InvalidArgument, "could not serialize installation report: %v", err)
90 }
91 }
92
Serge Bazanski4abeb132022-10-11 11:32:19 +020093 // Upsert heartbeat time and hardware report.
94 err = session.Transact(ctx, func(q *model.Queries) error {
95 // Upsert hardware report if submitted.
Tim Windelschmidt53087302023-06-27 16:36:31 +020096 if len(hwraw) != 0 {
Serge Bazanski4abeb132022-10-11 11:32:19 +020097 err = q.MachineSetHardwareReport(ctx, model.MachineSetHardwareReportParams{
98 MachineID: machineId,
99 HardwareReportRaw: hwraw,
100 })
101 if err != nil {
102 return fmt.Errorf("hardware report upsert: %w", err)
103 }
104 }
Serge Bazanski6c9535b2023-01-03 13:17:42 +0100105 // Upsert os installation report if submitted.
Tim Windelschmidt53087302023-06-27 16:36:31 +0200106 if len(installRaw) != 0 {
107 var result model.MachineOsInstallationResult
108 switch req.InstallationReport.Result.(type) {
109 case *apb.OSInstallationReport_Success_:
110 result = model.MachineOsInstallationResultSuccess
111 case *apb.OSInstallationReport_Error_:
112 result = model.MachineOsInstallationResultError
113 default:
114 return fmt.Errorf("unknown installation report result: %T", req.InstallationReport.Result)
115 }
Serge Bazanski6c9535b2023-01-03 13:17:42 +0100116 err = q.MachineSetOSInstallationReport(ctx, model.MachineSetOSInstallationReportParams{
Tim Windelschmidt53087302023-06-27 16:36:31 +0200117 MachineID: machineId,
118 Generation: req.InstallationReport.Generation,
119 OsInstallationResult: result,
120 OsInstallationReportRaw: installRaw,
Serge Bazanski6c9535b2023-01-03 13:17:42 +0100121 })
122 }
Serge Bazanski4abeb132022-10-11 11:32:19 +0200123 return q.MachineSetAgentHeartbeat(ctx, model.MachineSetAgentHeartbeatParams{
124 MachineID: machineId,
125 AgentHeartbeatAt: time.Now(),
126 })
127 })
128 if err != nil {
129 klog.Errorf("Could not submit heartbeat: %v", err)
130 return nil, status.Error(codes.Unavailable, "could not submit heartbeat")
131 }
Serge Bazanski6c9535b2023-01-03 13:17:42 +0100132 klog.Infof("Heartbeat from %s/%s", machineId.String(), hex.EncodeToString(pk))
Serge Bazanski4abeb132022-10-11 11:32:19 +0200133
Serge Bazanski6c9535b2023-01-03 13:17:42 +0100134 // Get installation request for machine if present.
135 var installRequest *apb.OSInstallationRequest
136 err = session.Transact(ctx, func(q *model.Queries) error {
137 reqs, err := q.GetExactMachineForOSInstallation(ctx, model.GetExactMachineForOSInstallationParams{
138 MachineID: machineId,
139 Limit: 1,
140 })
141 if err != nil {
142 return fmt.Errorf("GetExactMachineForOSInstallation: %w", err)
143 }
144 if len(reqs) > 0 {
145 raw := reqs[0].OsInstallationRequestRaw
146 var preq apb.OSInstallationRequest
147 if err := proto.Unmarshal(raw, &preq); err != nil {
148 return fmt.Errorf("could not decode stored OS installation request: %w", err)
149 }
150 installRequest = &preq
151 }
152 return nil
153 })
154 if err != nil {
155 // Do not fail entire request. Instead, just log an error.
156 // TODO(q3k): alert on this
157 klog.Errorf("Failure during OS installation request retrieval: %v", err)
158 }
159
160 return &apb.AgentHeartbeatResponse{
161 InstallationRequest: installRequest,
162 }, nil
Serge Bazanski4abeb132022-10-11 11:32:19 +0200163}