cloud/bmaas/server: init
This adds the BMaaS server alongside its first functionality: serving an
Agent heartbeat API.
This allows (untrusted) Agents to communicate with the rest of the
system by submitting heartbeats which may include a hardware report.
The BMaaS server will likely grow to implement further functionality as
described in its README.
Change-Id: I1ede02121b3700079cbb11295525f4c167ee1e7d
Reviewed-on: https://review.monogon.dev/c/monogon/+/988
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
Tested-by: Jenkins CI
diff --git a/cloud/bmaas/server/agent_callback_service_test.go b/cloud/bmaas/server/agent_callback_service_test.go
new file mode 100644
index 0000000..bc3201a
--- /dev/null
+++ b/cloud/bmaas/server/agent_callback_service_test.go
@@ -0,0 +1,116 @@
+package server
+
+import (
+ "context"
+ "crypto/ed25519"
+ "crypto/rand"
+ "testing"
+ "time"
+
+ "github.com/google/uuid"
+ "google.golang.org/grpc"
+
+ "source.monogon.dev/cloud/bmaas/bmdb"
+ "source.monogon.dev/cloud/bmaas/bmdb/model"
+ apb "source.monogon.dev/cloud/bmaas/server/api"
+ "source.monogon.dev/cloud/lib/component"
+ "source.monogon.dev/metropolis/node/core/rpc"
+)
+
+func dut() *Server {
+ return &Server{
+ Config: Config{
+ Component: component.ComponentConfig{
+ GRPCListenAddress: ":0",
+ DevCerts: true,
+ DevCertsPath: "/tmp/foo",
+ },
+ BMDB: bmdb.BMDB{
+ Config: bmdb.Config{
+ Database: component.CockroachConfig{
+ InMemory: true,
+ },
+ },
+ },
+ PublicListenAddress: ":0",
+ },
+ }
+}
+
+// TestAgentCallbackService exercises the basic flow for submitting an agent
+// heartbeat and hardware report.
+func TestAgentCallbackService(t *testing.T) {
+ s := dut()
+ ctx, ctxC := context.WithCancel(context.Background())
+ defer ctxC()
+ s.Start(ctx)
+
+ pub, priv, err := ed25519.GenerateKey(rand.Reader)
+ if err != nil {
+ t.Fatalf("could not generate keypair: %v", err)
+ }
+
+ sess, err := s.bmdb.StartSession(ctx)
+ if err != nil {
+ t.Fatalf("could not start session")
+ }
+
+ heartbeat := func(mid uuid.UUID) error {
+ creds, err := rpc.NewEphemeralCredentials(priv, nil)
+ if err != nil {
+ t.Fatalf("could not generate ephemeral credentials: %v", err)
+ }
+ conn, err := grpc.Dial(s.ListenPublic, grpc.WithTransportCredentials(creds))
+ if err != nil {
+ t.Fatalf("Dial failed: %v", err)
+ }
+ defer conn.Close()
+
+ stub := apb.NewAgentCallbackClient(conn)
+ _, err = stub.Heartbeat(ctx, &apb.AgentHeartbeatRequest{
+ MachineId: mid.String(),
+ HardwareReport: &apb.AgentHardwareReport{},
+ })
+ return err
+ }
+
+ // First, attempt to heartbeat for some totally made up machine ID. That should
+ // fail.
+ if err := heartbeat(uuid.New()); err == nil {
+ t.Errorf("heartbeat for made up UUID should've failed")
+ }
+
+ // Create an actual machine in the BMDB alongside the expected pubkey within an
+ // AgentStarted tag.
+ var machine model.Machine
+ err = sess.Transact(ctx, func(q *model.Queries) error {
+ machine, err = q.NewMachine(ctx)
+ if err != nil {
+ return err
+ }
+ err = q.MachineAddProvided(ctx, model.MachineAddProvidedParams{
+ MachineID: machine.MachineID,
+ Provider: model.ProviderEquinix,
+ ProviderID: "123",
+ })
+ if err != nil {
+ return err
+ }
+ return q.MachineSetAgentStarted(ctx, model.MachineSetAgentStartedParams{
+ MachineID: machine.MachineID,
+ AgentStartedAt: time.Now(),
+ AgentPublicKey: pub,
+ })
+ })
+ if err != nil {
+ t.Fatalf("could not create machine: %v", err)
+ }
+
+ // Now heartbeat with correct machine ID and key. This should succeed.
+ if err := heartbeat(machine.MachineID); err != nil {
+ t.Errorf("heartbeat should've succeeded, got: %v", err)
+ }
+
+ // TODO(q3k): test hardware report being attached once we have some debug API
+ // for tags.
+}