m/t/launch: multi-node launches, prefixed stdout

This reinstantiates //:launch-test2, with some small fixes for usability
(prefixed stdout and GetNodes retries to handle cluster connectivity
issues as the cluster grows).

We also drive-by port //:launch-test2 and //:launch to use the new and
shiny clicontext package.

Change-Id: I62a1d827b2087f1173abf19e792a2088dc8b80bb
Reviewed-on: https://review.monogon.dev/c/monogon/+/485
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
diff --git a/metropolis/test/launch/cluster/prefixed_stdio.go b/metropolis/test/launch/cluster/prefixed_stdio.go
new file mode 100644
index 0000000..059c3cb
--- /dev/null
+++ b/metropolis/test/launch/cluster/prefixed_stdio.go
@@ -0,0 +1,39 @@
+package cluster
+
+import (
+	"fmt"
+	"io"
+	"strings"
+
+	"source.monogon.dev/metropolis/pkg/logbuffer"
+)
+
+// prefixedStdio is a io.ReadWriter which splits written bytes into lines,
+// prefixes them with some known prefix, and spits them to os.Stdout.
+//
+// io.Reader is implemented for compatibility with code which expects an
+// io.ReadWriter, but always returns EOF.
+type prefixedStdio struct {
+	*logbuffer.LineBuffer
+}
+
+// newPrefixedStdio returns a prefixedStdio that prefixes all lines with <num>|,
+// used to distinguish different VMs used within the launch codebase.
+func newPrefixedStdio(num int) prefixedStdio {
+	return prefixedStdio{
+		logbuffer.NewLineBuffer(1024, func(l *logbuffer.Line) {
+			s := strings.TrimSpace(l.String())
+			// TODO(q3k): don't just skip lines containing escape sequences, strip the
+			// sequences out. Or stop parsing qemu logs and instead dial log endpoint in
+			// spawned nodes.
+			if strings.Contains(s, "\u001b") {
+				return
+			}
+			fmt.Printf("%02d| %s\n", num, s)
+		}),
+	}
+}
+
+func (p prefixedStdio) Read(_ []byte) (int, error) {
+	return 0, io.EOF
+}