c/bmaas/bmdb: implement OS installation flow

This adds two new tags: OSInstallationRequest and
OSInstallationResponse. It also implements interacting with these tags
from the agent side.

This doesn't yet implement any admin/user-facing API to actually request
OS installation, for now we just exercise this in tests.

Change-Id: I2e31a8369a3a8670bb92bcacfb8231a0d5e1b9fd
Reviewed-on: https://review.monogon.dev/c/monogon/+/1011
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
Tested-by: Jenkins CI
diff --git a/cloud/bmaas/server/agent_callback_service.go b/cloud/bmaas/server/agent_callback_service.go
index f058213..b6e0e71 100644
--- a/cloud/bmaas/server/agent_callback_service.go
+++ b/cloud/bmaas/server/agent_callback_service.go
@@ -3,6 +3,7 @@
 import (
 	"context"
 	"crypto/ed25519"
+	"encoding/hex"
 	"errors"
 	"fmt"
 	"time"
@@ -57,6 +58,7 @@
 			return fmt.Errorf("AuthenticateAgentConnection: %w", err)
 		}
 		if len(agents) < 1 {
+			klog.Errorf("No agent for %s/%s", machineId.String(), hex.EncodeToString(pk))
 			return errAgentUnauthenticated
 		}
 		return nil
@@ -76,7 +78,7 @@
 	if req.HardwareReport != nil {
 		hwraw, err = proto.Marshal(req.HardwareReport)
 		if err != nil {
-			return nil, status.Errorf(codes.InvalidArgument, "could not serialize harcware report: %v", err)
+			return nil, status.Errorf(codes.InvalidArgument, "could not serialize hardware report: %v", err)
 		}
 	}
 
@@ -92,6 +94,13 @@
 				return fmt.Errorf("hardware report upsert: %w", err)
 			}
 		}
+		// Upsert os installation report if submitted.
+		if req.InstallationReport != nil {
+			err = q.MachineSetOSInstallationReport(ctx, model.MachineSetOSInstallationReportParams{
+				MachineID:  machineId,
+				Generation: req.InstallationReport.Generation,
+			})
+		}
 		return q.MachineSetAgentHeartbeat(ctx, model.MachineSetAgentHeartbeatParams{
 			MachineID:        machineId,
 			AgentHeartbeatAt: time.Now(),
@@ -101,6 +110,35 @@
 		klog.Errorf("Could not submit heartbeat: %v", err)
 		return nil, status.Error(codes.Unavailable, "could not submit heartbeat")
 	}
+	klog.Infof("Heartbeat from %s/%s", machineId.String(), hex.EncodeToString(pk))
 
-	return &apb.AgentHeartbeatResponse{}, nil
+	// Get installation request for machine if present.
+	var installRequest *apb.OSInstallationRequest
+	err = session.Transact(ctx, func(q *model.Queries) error {
+		reqs, err := q.GetExactMachineForOSInstallation(ctx, model.GetExactMachineForOSInstallationParams{
+			MachineID: machineId,
+			Limit:     1,
+		})
+		if err != nil {
+			return fmt.Errorf("GetExactMachineForOSInstallation: %w", err)
+		}
+		if len(reqs) > 0 {
+			raw := reqs[0].OsInstallationRequestRaw
+			var preq apb.OSInstallationRequest
+			if err := proto.Unmarshal(raw, &preq); err != nil {
+				return fmt.Errorf("could not decode stored OS installation request: %w", err)
+			}
+			installRequest = &preq
+		}
+		return nil
+	})
+	if err != nil {
+		// Do not fail entire request. Instead, just log an error.
+		// TODO(q3k): alert on this
+		klog.Errorf("Failure during OS installation request retrieval: %v", err)
+	}
+
+	return &apb.AgentHeartbeatResponse{
+		InstallationRequest: installRequest,
+	}, nil
 }