cloud/bmaas: implement webug

Webug (pronounced: /wɛbʌɡ/, not /wiːbʌɡ/) is a web debug interface for
BMDB, inspired by the great web debug interfaces of old.

It uses the new BMDB reflection API to access most
machine/tag/work/backoff information, plus sqlc queries to access
session information.

Change-Id: If0e65b6fc33ad92baef9c6d98333f90a02efa1b3
Reviewed-on: https://review.monogon.dev/c/monogon/+/1132
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
Tested-by: Jenkins CI
diff --git a/cloud/bmaas/bmdb/webug/templates/base.html b/cloud/bmaas/bmdb/webug/templates/base.html
new file mode 100644
index 0000000..3b8df8e
--- /dev/null
+++ b/cloud/bmaas/bmdb/webug/templates/base.html
@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>BMDB webug</title>
+<style>
+    body {
+        font-family: sans-serif;
+        background: #fff;
+    }
+
+    /* Logotype. */
+    h1 {
+        clear: both;
+        padding-left: 1em;
+        padding-top: 0.5em;
+    }
+    h1 a {
+        text-decoration: none;
+    }
+    h1 a, h1 a:visited, h1 a:hover, h1 a:active {
+        color: inherit;
+    }
+    h1 span.red {
+        background-color: red;
+        color: white;
+        padding: 0.1em;
+        border-radius: 0.4em;
+    }
+    h1 span.info {
+        font-size: 0.5em;
+        font-weight: normal;
+        font-style: italic;
+    }
+
+    /* Section headers. */
+    h2 {
+        clear: both;
+        width: 100%;
+        text-align: center;
+        font-size: 120%;
+        background: #eeeeff;
+    }
+
+    /* Stylish tables. */
+    table, th, td {
+        background-color: #eee;
+        padding: 0.2em 0.4em 0.2em 0.4em;
+    }
+    table th {
+        background-color: #c0c0c0;
+    }
+    table {
+        background-color: #fff;
+        border-spacing: 0.2em;
+    }
+
+    /* Colouring of the Work History log in machine.html. */
+    tr.EventFailed td, tr.EventCanceled td {
+        background-color: #f8e8e8;
+    }
+    tr.EventFinished td {
+        background-color: #e8f8e8;
+    }
+
+    /* Generic font style tags for any element. */
+    .small {
+        font-size: 0.8em;
+    }
+    .faint {
+        color: #333;
+    }
+    .mono {
+        font-family: monospace;
+    }
+    .error {
+        color: #f00;
+    }
+
+    /* For simple column style layouts. */
+    .vsplit {
+        display: flex;
+        flex-direction: row;
+        flex-wrap: nowrap;
+        align-items: stretch;
+    }
+    .column {
+        flex-grow: 1;
+        padding: 0.5em;
+    }
+</style>
+<h1><a href="/">we<span class="red">bug</span></a> <span class="info">BMDB at {{ .BMDBAddress }} schema {{ .BMDBSchema }}</span></h1>
\ No newline at end of file
diff --git a/cloud/bmaas/bmdb/webug/templates/fragment_tag.html b/cloud/bmaas/bmdb/webug/templates/fragment_tag.html
new file mode 100644
index 0000000..f7ee320
--- /dev/null
+++ b/cloud/bmaas/bmdb/webug/templates/fragment_tag.html
@@ -0,0 +1,5 @@
+{{- if eq .Type.Name "Provided" -}}
+    {{- template "fragment_tag_provided.html" . -}}
+{{- else -}}
+    {{- template "fragment_tag_default.html" . -}}
+{{- end -}}
diff --git a/cloud/bmaas/bmdb/webug/templates/fragment_tag_default.html b/cloud/bmaas/bmdb/webug/templates/fragment_tag_default.html
new file mode 100644
index 0000000..464d517
--- /dev/null
+++ b/cloud/bmaas/bmdb/webug/templates/fragment_tag_default.html
@@ -0,0 +1 @@
+<b>{{ .Type.Name }}</b>(...)
diff --git a/cloud/bmaas/bmdb/webug/templates/fragment_tag_provided.html b/cloud/bmaas/bmdb/webug/templates/fragment_tag_provided.html
new file mode 100644
index 0000000..776a63d
--- /dev/null
+++ b/cloud/bmaas/bmdb/webug/templates/fragment_tag_provided.html
@@ -0,0 +1,3 @@
+{{- $provider := (.Field "provider").HumanValue }}
+{{- $pid := (.Field "provider_id").HumanValue }}
+<b>{{ .Type.Name }}</b>({{- $provider }}, <a href="provider/{{ $provider }}/{{ $pid }}" style="font-family: mono">{{ $pid }}</a>)
diff --git a/cloud/bmaas/bmdb/webug/templates/machine.html b/cloud/bmaas/bmdb/webug/templates/machine.html
new file mode 100644
index 0000000..1d90014
--- /dev/null
+++ b/cloud/bmaas/bmdb/webug/templates/machine.html
@@ -0,0 +1,176 @@
+{{ template "base.html" .Base }}
+<h2>Machine {{ .Machine.ID }}</h2>
+
+{{ $sessions := .Sessions }}
+
+<table>
+    <tr>
+        <td><b>Machine ID</b></td>
+        <td class="mono">{{ .Machine.ID }}</td>
+    </tr>
+    <tr>
+        <td><b>Created</b></td>
+        <td>{{ .Machine.Created }}</td>
+    </tr>
+    <tr>
+        <td><b>Active Backoffs</b></td>
+        <td>{{ len .Machine.ActiveBackoffs }}</td>
+    </tr>
+    <tr>
+        <td><b>Active Work</b></td>
+        <td>{{ len .Machine.Work }}</td>
+    </tr>
+</table>
+
+<div class="vsplit">
+    <div class="column">
+        <h2>Tags</h2>
+        {{ range $name, $tag := .Machine.Tags }}
+        <table>
+            <tr>
+                <th colspan="2">
+                    {{ template "fragment_tag.html" $tag }}
+                </th>
+            </tr>
+            {{ range $tag.Fields }}
+            <tr>
+                <td>
+                    <b>{{ .Type.NativeName }}:</b>
+                </td>
+                <td class="mono">
+                    {{ .HumanValue }}
+                </td>
+            </tr>
+            {{ end }}
+        </table>
+        {{ else }}
+        <i>No tags.</i>
+        {{ end }}
+        <h2>Work</h2>
+        {{ range $name, $work := .Machine.Work }}
+        <table>
+            <tr>
+                <th colspan="3">
+                    <b>{{ $work.Process }}</b>
+                </th>
+            </tr>
+            <tr>
+                <td><b>Process:</b></td>
+                <td class="mono" colspan="2">
+                    {{ $work.Process }}
+                </td>
+            </tr>
+            {{ $sessionOrErr := index $sessions $name }}
+            {{ if ne $sessionOrErr.Error "" }}
+            <tr>
+                <td colspan="3" class="error">
+                    Could not retrieve session information: {{ $sessionOrErr.Error }}
+                </td>
+            </tr>
+            {{ else }}
+            {{ $session := $sessionOrErr.Session }}
+            <tr>
+                <td rowspan="5" style="vertical-align: top;"><b>Session</b></td>
+                <td><b>ID:</b></td>
+                <td class="mono" colspan="2">
+                    <a href="/session/{{ $session.SessionID }}">{{ $session.SessionID }}</a>
+                </td>
+            </tr>
+            <tr>
+                <td><b>Component:</b></td>
+                <td class="mono">{{ $session.SessionComponentName }}</td>
+            </tr>
+            <tr>
+                <td><b>Runtime:</b></td>
+                <td class="mono">{{ $session.SessionRuntimeInfo }}</td>
+            </tr>
+            <tr>
+                <td><b>Created At:</b></td>
+                <td>{{ $session.SessionCreatedAt }}</td>
+            </tr>
+            <tr>
+                <td><b>Liveness:</b></td>
+                <td>Interval {{ $session.SessionIntervalSeconds }}s, deadline {{ $session.SessionDeadline }}</td>
+            </tr>
+            {{ end }}
+        </table>
+        {{ else }}
+        <i>No active work.</i>
+        {{ end }}
+        <h2>Backoffs</h2>
+        <h3>Active</h3>
+        {{ range $name, $backoff := .Machine.ActiveBackoffs }}
+        <table>
+            <tr>
+                <th colspan="2">
+                    <b>{{ $backoff.Process }}</b>
+                </th>
+            </tr>
+            <tr>
+                <td><b>Process:</b></td>
+                <td class="mono">{{ $backoff.Process }}</td>
+            </tr>
+            <tr>
+                <td><b>Until:</b></td>
+                <td class="mono">{{ $backoff.Until }}</td>
+            </tr>
+            <tr>
+                <td><b>Cause:</b></td>
+                <td class="mono">{{ $backoff.Cause }}</td>
+            </tr>
+        </table>
+        {{ else }}
+        <i>No active backoffs.</i>
+        {{ end }}
+        <h3>Expired</h3>
+        {{ range $name, $backoff := .Machine.ExpiredBackoffs }}
+        <table style="opacity: 0.4">
+            <tr>
+                <th colspan="2">
+                    <b>{{ $backoff.Process }}</b>
+                </th>
+            </tr>
+            <tr>
+                <td><b>Process:</b></td>
+                <td class="mono">{{ $backoff.Process }}</td>
+            </tr>
+            <tr>
+                <td><b>Until:</b></td>
+                <td class="mono">{{ $backoff.Until }}</td>
+            </tr>
+            <tr>
+                <td><b>Cause:</b></td>
+                <td class="mono">{{ $backoff.Cause }}</td>
+            </tr>
+        </table>
+        {{ else }}
+        <i>No expired backoffs.</i>
+        {{ end }}
+    </div>
+    <div class="column">
+        <h2>Work History</h2>
+        {{ if ne .HistoryError "" }}
+        <b class="error">Unavailable: {{ .HistoryError }}</b>
+        {{ else }}
+        <i>Note: reverse chronological order.</i>
+        <table>
+            <tr>
+                <th>Time</th>
+                <th>Process</th>
+                <th>Event</th>
+            </tr>
+            {{ range .History }}
+            <tr class="Event{{.Event}}">
+                <td>{{ .Timestamp }}</td>
+                <td><b>{{ .Process }}</b></td>
+                {{ if eq .Event "Failed" }}
+                <td>{{ .Event }}: <span class="mono">{{ .FailedCause.String }}</span></td>
+                {{ else }}
+                <td>{{ .Event }}</td>
+                {{ end }}
+            </tr>
+            {{ end }}
+        </table>
+        {{ end }}
+    </div>
+</div>
\ No newline at end of file
diff --git a/cloud/bmaas/bmdb/webug/templates/machines.html b/cloud/bmaas/bmdb/webug/templates/machines.html
new file mode 100644
index 0000000..d8658a1
--- /dev/null
+++ b/cloud/bmaas/bmdb/webug/templates/machines.html
@@ -0,0 +1,36 @@
+{{ template "base.html" .Base }}
+<h2>Machine List</h2>
+<p>
+    Click on a Machine ID to explore it further. Commonly viewed tags are expanded.
+</p>
+<table>
+    <tr>
+        <th>Machine ID</th>
+        <th>Work</th>
+        <th>Backoffs</th>
+        <th>Tags</th>
+    </tr>
+    {{ range .Machines -}}
+    <tr>
+        <td class="mono"><a href="/machine/{{ .ID }}">{{ .ID }}</a></td>
+        <td>
+            {{- range $process, $work := .Work -}}
+            <b><a href="/session/{{ $work.SessionID }}">{{ $process }}</a></b>
+            {{- end -}}
+        </td>
+        <td>
+            {{- range $process, $backoff := .Backoffs -}}
+            <b>{{ $backoff.Process }}</b>(<span class="small">{{ summarizeError .Cause }}</span>)
+            {{- end -}}
+        </td>
+        <td>
+            {{- range $name, $tag := .Tags -}}
+            {{- template "fragment_tag.html" $tag -}}
+            {{- end -}}
+        </td>
+    </tr>
+    {{ end -}}
+</table>
+<p class="small faint mono">
+    {{ .NMachines }} rows, rendered in {{ .RenderTime }}. Query: {{ .Query }}
+</p>
\ No newline at end of file