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/bmdb/model/migrations/1672743627_installation_tags.down.sql b/cloud/bmaas/bmdb/model/migrations/1672743627_installation_tags.down.sql
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/cloud/bmaas/bmdb/model/migrations/1672743627_installation_tags.down.sql
diff --git a/cloud/bmaas/bmdb/model/migrations/1672743627_installation_tags.up.sql b/cloud/bmaas/bmdb/model/migrations/1672743627_installation_tags.up.sql
new file mode 100644
index 0000000..37f7b3a
--- /dev/null
+++ b/cloud/bmaas/bmdb/model/migrations/1672743627_installation_tags.up.sql
@@ -0,0 +1,19 @@
+CREATE TABLE machine_os_installation_request (
+    machine_id UUID NOT NULL REFERENCES machines(machine_id) ON DELETE RESTRICT,
+    -- Version of this request, for example monotonic epoch counter. Used to
+    -- match successful installation (represented by OS installation report) to
+    -- pending request, making sure that we don't perform spurious re-installs.
+    generation INT NOT NULL,
+    -- Serialized cloud.bmaas.server.api.OSInstallationRequest.
+    os_installation_request_raw BYTEA NOT NULL,
+    CONSTRAINT "primary" PRIMARY KEY (machine_id)
+);
+
+CREATE TABLE machine_os_installation_report (
+    machine_id UUID NOT NULL REFERENCES machines(machine_id) ON DELETE RESTRICT,
+    -- Matches generation in machine_os_installation_request. Not constrained on
+    -- purpose, as a mismatch between generations implies an actionable
+    -- installation request and is a valid state of the system.
+    generation INT NOT NULL,
+    CONSTRAINT "primary" PRIMARY KEY (machine_id)
+);
diff --git a/cloud/bmaas/bmdb/model/queries.sql b/cloud/bmaas/bmdb/model/queries.sql
index 4e32cff..8f40aac 100644
--- a/cloud/bmaas/bmdb/model/queries.sql
+++ b/cloud/bmaas/bmdb/model/queries.sql
@@ -42,8 +42,6 @@
   AND session_id = $2
   AND process = $3;
 
--- Example tag processing queries follow.
-
 -- name: MachineAddProvided :exec
 INSERT INTO machine_provided (
     machine_id, provider, provider_id
@@ -79,6 +77,25 @@
     hardware_report_raw = $2
 ;
 
+-- name: MachineSetOSInstallationRequest :exec
+INSERT INTO machine_os_installation_request (
+    machine_id, generation, os_installation_request_raw
+) VALUES (
+    $1, $2, $3
+) ON CONFLICT (machine_id) DO UPDATE SET
+    generation = $2,
+    os_installation_request_raw = $3
+;
+
+-- name: MachineSetOSInstallationReport :exec
+INSERT INTO machine_os_installation_report (
+    machine_id, generation
+) VALUES (
+    $1, $2
+) ON CONFLICT (machine_id) DO UPDATE SET
+    generation = $2
+;
+
 -- name: GetMachinesForAgentStart :many
 -- Get machines that need agent installed for the first time. Machine can be
 -- assumed to be 'new', with no previous attempts or failures.
@@ -126,6 +143,25 @@
   AND work.machine_id IS NULL
 LIMIT $1;
 
+-- name: GetExactMachineForOSInstallation :many
+SELECT
+    machine_os_installation_request.*
+FROM machines
+LEFT JOIN machine_os_installation_request ON machines.machine_id = machine_os_installation_request.machine_id
+LEFT JOIN machine_os_installation_report ON machines.machine_id = machine_os_installation_report.machine_id
+WHERE
+    -- We are only interested in one concrete machine.
+    machines.machine_id = $1
+    -- We must have an installation request.
+    AND machine_os_installation_request.machine_id IS NOT NULL
+    -- And we either must have no installation report, or the installation
+    -- report's generation must not match the installation request's generation.
+    AND (
+        machine_os_installation_report.machine_id IS NULL
+        OR machine_os_installation_report.generation != machine_os_installation_request.generation
+    )
+LIMIT $2;
+
 -- name: AuthenticateAgentConnection :many
 SELECT
     machine_agent_started.*
@@ -133,4 +169,4 @@
 INNER JOIN machine_agent_started ON machines.machine_id = machine_agent_started.machine_id
 WHERE
     machines.machine_id = $1
-    AND machine_agent_started.agent_public_key = $2;
\ No newline at end of file
+    AND machine_agent_started.agent_public_key = $2;