blob: f05821384e16130b58444c4fa34971437371fc02 [file] [log] [blame]
Serge Bazanski4abeb132022-10-11 11:32:19 +02001package server
2
3import (
4 "context"
5 "crypto/ed25519"
6 "errors"
7 "fmt"
8 "time"
9
10 "github.com/google/uuid"
11 "google.golang.org/grpc/codes"
12 "google.golang.org/grpc/status"
13 "google.golang.org/protobuf/proto"
14 "k8s.io/klog"
15
16 "source.monogon.dev/cloud/bmaas/bmdb/model"
17 apb "source.monogon.dev/cloud/bmaas/server/api"
18 "source.monogon.dev/metropolis/node/core/rpc"
19)
20
21type agentCallbackService struct {
22 s *Server
23}
24
25var (
26 errAgentUnauthenticated = errors.New("machine id or public key unknown")
27)
28
29func (a *agentCallbackService) Heartbeat(ctx context.Context, req *apb.AgentHeartbeatRequest) (*apb.AgentHeartbeatResponse, error) {
30 // Extract ED25519 self-signed certificate from client connection.
31 cert, err := rpc.GetPeerCertificate(ctx)
32 if err != nil {
33 return nil, err
34 }
35 pk := cert.PublicKey.(ed25519.PublicKey)
36 machineId, err := uuid.Parse(req.MachineId)
37 if err != nil {
38 return nil, status.Error(codes.InvalidArgument, "machine_id invalid")
39 }
40
41 // TODO(q3k): don't start a session for every RPC.
42 session, err := a.s.bmdb.StartSession(ctx)
43 if err != nil {
44 klog.Errorf("Could not start session: %v", err)
45 return nil, status.Error(codes.Unavailable, "could not start session")
46 }
47
48 // Verify that machine ID and connection public key match up to a machine in the
49 // BMDB. Prevent leaking information about a machine's existence to unauthorized
50 // agents.
51 err = session.Transact(ctx, func(q *model.Queries) error {
52 agents, err := q.AuthenticateAgentConnection(ctx, model.AuthenticateAgentConnectionParams{
53 MachineID: machineId,
54 AgentPublicKey: pk,
55 })
56 if err != nil {
57 return fmt.Errorf("AuthenticateAgentConnection: %w", err)
58 }
59 if len(agents) < 1 {
60 return errAgentUnauthenticated
61 }
62 return nil
63 })
64 if err != nil {
65 if errors.Is(err, errAgentUnauthenticated) {
66 return nil, status.Error(codes.Unauthenticated, err.Error())
67 }
68 klog.Errorf("Could not authenticate agent: %v", err)
69 return nil, status.Error(codes.Unavailable, "could not authenticate agent")
70 }
71
72 // Request is now authenticated.
73
74 // Serialize hardware report if submitted alongside heartbeat.
75 var hwraw []byte
76 if req.HardwareReport != nil {
77 hwraw, err = proto.Marshal(req.HardwareReport)
78 if err != nil {
79 return nil, status.Errorf(codes.InvalidArgument, "could not serialize harcware report: %v", err)
80 }
81 }
82
83 // Upsert heartbeat time and hardware report.
84 err = session.Transact(ctx, func(q *model.Queries) error {
85 // Upsert hardware report if submitted.
86 if hwraw != nil {
87 err = q.MachineSetHardwareReport(ctx, model.MachineSetHardwareReportParams{
88 MachineID: machineId,
89 HardwareReportRaw: hwraw,
90 })
91 if err != nil {
92 return fmt.Errorf("hardware report upsert: %w", err)
93 }
94 }
95 return q.MachineSetAgentHeartbeat(ctx, model.MachineSetAgentHeartbeatParams{
96 MachineID: machineId,
97 AgentHeartbeatAt: time.Now(),
98 })
99 })
100 if err != nil {
101 klog.Errorf("Could not submit heartbeat: %v", err)
102 return nil, status.Error(codes.Unavailable, "could not submit heartbeat")
103 }
104
105 return &apb.AgentHeartbeatResponse{}, nil
106}