treewide: introduce osbase package and move things around

All except localregistry moved from metropolis/pkg to osbase,
localregistry moved to metropolis/test as its only used there anyway.

Change-Id: If1a4bf377364bef0ac23169e1b90379c71b06d72
Reviewed-on: https://review.monogon.dev/c/monogon/+/3079
Tested-by: Jenkins CI
Reviewed-by: Serge Bazanski <serge@monogon.tech>
diff --git a/metropolis/test/launch/BUILD.bazel b/metropolis/test/launch/BUILD.bazel
index cc5ef6c..24296d1 100644
--- a/metropolis/test/launch/BUILD.bazel
+++ b/metropolis/test/launch/BUILD.bazel
@@ -3,17 +3,50 @@
 go_library(
     name = "launch",
     srcs = [
-        "launch.go",
-        "log.go",
+        "cluster.go",
+        "insecure_key.go",
+        "metroctl.go",
+        "prefixed_stdio.go",
+        "swtpm.go",
     ],
     data = [
+        "//metropolis/node:image",
+        "//metropolis/test/nanoswitch:initramfs",
+        "//metropolis/test/swtpm/certtool",
+        "//metropolis/test/swtpm/swtpm_cert",
+        "//osbase/test/ktest:linux-testing",
+        "//third_party/edk2:firmware",
         "@com_github_bonzini_qboot//:qboot-bin",
+        "@swtpm",
+        "@swtpm//:swtpm_localca",
+        "@swtpm//:swtpm_setup",
     ],
     importpath = "source.monogon.dev/metropolis/test/launch",
-    visibility = ["//metropolis:__subpackages__"],
+    visibility = ["//visibility:public"],
     deps = [
-        "//metropolis/pkg/freeport",
+        "//go/qcow2",
+        "//metropolis/cli/metroctl/core",
+        "//metropolis/node",
+        "//metropolis/node/core/curator/proto/api",
+        "//metropolis/node/core/identity",
+        "//metropolis/node/core/rpc",
+        "//metropolis/node/core/rpc/resolver",
+        "//metropolis/proto/api",
+        "//metropolis/proto/common",
+        "//metropolis/test/localregistry",
+        "//osbase/logbuffer",
+        "//osbase/test/launch",
+        "@com_github_cenkalti_backoff_v4//:backoff",
+        "@com_github_kballard_go_shellquote//:go-shellquote",
         "@io_bazel_rules_go//go/runfiles:go_default_library",
+        "@io_k8s_client_go//kubernetes",
+        "@io_k8s_client_go//rest",
+        "@org_golang_google_grpc//:go_default_library",
+        "@org_golang_google_grpc//codes",
+        "@org_golang_google_grpc//status",
+        "@org_golang_google_protobuf//proto",
+        "@org_golang_x_net//proxy",
         "@org_golang_x_sys//unix",
+        "@org_uber_go_multierr//:multierr",
     ],
 )
diff --git a/metropolis/test/launch/cli/launch-cluster/BUILD.bazel b/metropolis/test/launch/cli/launch-cluster/BUILD.bazel
index 688228e..9e48795 100644
--- a/metropolis/test/launch/cli/launch-cluster/BUILD.bazel
+++ b/metropolis/test/launch/cli/launch-cluster/BUILD.bazel
@@ -8,7 +8,7 @@
     visibility = ["//visibility:private"],
     deps = [
         "//metropolis/cli/metroctl/core",
-        "//metropolis/test/launch/cluster",
+        "//metropolis/test/launch",
     ],
 )
 
diff --git a/metropolis/test/launch/cli/launch-cluster/main.go b/metropolis/test/launch/cli/launch-cluster/main.go
index c9b9dec..1529396 100644
--- a/metropolis/test/launch/cli/launch-cluster/main.go
+++ b/metropolis/test/launch/cli/launch-cluster/main.go
@@ -23,12 +23,12 @@
 	"os/signal"
 
 	metroctl "source.monogon.dev/metropolis/cli/metroctl/core"
-	"source.monogon.dev/metropolis/test/launch/cluster"
+	mlaunch "source.monogon.dev/metropolis/test/launch"
 )
 
 func main() {
 	ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
-	cl, err := cluster.LaunchCluster(ctx, cluster.ClusterOptions{
+	cl, err := mlaunch.LaunchCluster(ctx, mlaunch.ClusterOptions{
 		NumNodes:        3,
 		NodeLogsToFiles: true,
 	})
@@ -36,7 +36,7 @@
 		log.Fatalf("LaunchCluster: %v", err)
 	}
 
-	mpath, err := cluster.MetroctlRunfilePath()
+	mpath, err := mlaunch.MetroctlRunfilePath()
 	if err != nil {
 		log.Fatalf("MetroctlRunfilePath: %v", err)
 	}
diff --git a/metropolis/test/launch/cli/launch/BUILD.bazel b/metropolis/test/launch/cli/launch/BUILD.bazel
index 49df994..f0edefc 100644
--- a/metropolis/test/launch/cli/launch/BUILD.bazel
+++ b/metropolis/test/launch/cli/launch/BUILD.bazel
@@ -9,7 +9,7 @@
     deps = [
         "//metropolis/proto/api",
         "//metropolis/test/launch",
-        "//metropolis/test/launch/cluster",
+        "//osbase/test/launch",
     ],
 )
 
diff --git a/metropolis/test/launch/cli/launch/main.go b/metropolis/test/launch/cli/launch/main.go
index 2ae3a0c..71c7aa2 100644
--- a/metropolis/test/launch/cli/launch/main.go
+++ b/metropolis/test/launch/cli/launch/main.go
@@ -24,8 +24,9 @@
 	"path/filepath"
 
 	apb "source.monogon.dev/metropolis/proto/api"
-	"source.monogon.dev/metropolis/test/launch"
-	"source.monogon.dev/metropolis/test/launch/cluster"
+
+	mlaunch "source.monogon.dev/metropolis/test/launch"
+	"source.monogon.dev/osbase/test/launch"
 )
 
 func main() {
@@ -45,22 +46,22 @@
 	defer os.RemoveAll(sd)
 
 	var ports []uint16
-	for _, p := range cluster.NodePorts {
+	for _, p := range mlaunch.NodePorts {
 		ports = append(ports, uint16(p))
 	}
 	ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
 	doneC := make(chan error)
-	tpmf, err := cluster.NewTPMFactory(filepath.Join(ld, "tpm"))
+	tpmf, err := mlaunch.NewTPMFactory(filepath.Join(ld, "tpm"))
 	if err != nil {
 		log.Fatalf("NewTPMFactory: %v", err)
 	}
-	err = cluster.LaunchNode(ctx, ld, sd, tpmf, &cluster.NodeOptions{
+	err = mlaunch.LaunchNode(ctx, ld, sd, tpmf, &mlaunch.NodeOptions{
 		Name:       "test-node",
 		Ports:      launch.IdentityPortMap(ports),
 		SerialPort: os.Stdout,
 		NodeParameters: &apb.NodeParameters{
 			Cluster: &apb.NodeParameters_ClusterBootstrap_{
-				ClusterBootstrap: cluster.InsecureClusterBootstrap,
+				ClusterBootstrap: mlaunch.InsecureClusterBootstrap,
 			},
 		},
 	}, doneC)
diff --git a/metropolis/test/launch/cluster/cluster.go b/metropolis/test/launch/cluster.go
similarity index 99%
rename from metropolis/test/launch/cluster/cluster.go
rename to metropolis/test/launch/cluster.go
index dfeb457..7ae5f83 100644
--- a/metropolis/test/launch/cluster/cluster.go
+++ b/metropolis/test/launch/cluster.go
@@ -2,7 +2,7 @@
 // nodes and clusters in a virtualized environment using qemu. It's kept in a
 // separate package as it depends on a Metropolis node image, which might not be
 // required for some use of the launch library.
-package cluster
+package launch
 
 import (
 	"bytes"
@@ -48,8 +48,8 @@
 	"source.monogon.dev/metropolis/node/core/identity"
 	"source.monogon.dev/metropolis/node/core/rpc"
 	"source.monogon.dev/metropolis/node/core/rpc/resolver"
-	"source.monogon.dev/metropolis/pkg/localregistry"
-	"source.monogon.dev/metropolis/test/launch"
+	"source.monogon.dev/metropolis/test/localregistry"
+	"source.monogon.dev/osbase/test/launch"
 )
 
 const (
@@ -854,7 +854,7 @@
 		} else {
 			serialPort = newPrefixedStdio(99)
 		}
-		kernelPath, err := runfiles.Rlocation("_main/metropolis/test/ktest/vmlinux")
+		kernelPath, err := runfiles.Rlocation("_main/osbase/test/ktest/vmlinux")
 		if err != nil {
 			launch.Fatal("Failed to resolved nanoswitch kernel: %v", err)
 		}
diff --git a/metropolis/test/launch/cluster/BUILD.bazel b/metropolis/test/launch/cluster/BUILD.bazel
deleted file mode 100644
index efa04cb..0000000
--- a/metropolis/test/launch/cluster/BUILD.bazel
+++ /dev/null
@@ -1,52 +0,0 @@
-load("@io_bazel_rules_go//go:def.bzl", "go_library")
-
-go_library(
-    name = "cluster",
-    srcs = [
-        "cluster.go",
-        "insecure_key.go",
-        "metroctl.go",
-        "prefixed_stdio.go",
-        "swtpm.go",
-    ],
-    data = [
-        "//metropolis/node:image",
-        "//metropolis/test/ktest:linux-testing",
-        "//metropolis/test/nanoswitch:initramfs",
-        "//metropolis/test/swtpm/certtool",
-        "//metropolis/test/swtpm/swtpm_cert",
-        "//third_party/edk2:firmware",
-        "@com_github_bonzini_qboot//:qboot-bin",
-        "@swtpm",
-        "@swtpm//:swtpm_localca",
-        "@swtpm//:swtpm_setup",
-    ],
-    importpath = "source.monogon.dev/metropolis/test/launch/cluster",
-    visibility = ["//visibility:public"],
-    deps = [
-        "//go/qcow2",
-        "//metropolis/cli/metroctl/core",
-        "//metropolis/node",
-        "//metropolis/node/core/curator/proto/api",
-        "//metropolis/node/core/identity",
-        "//metropolis/node/core/rpc",
-        "//metropolis/node/core/rpc/resolver",
-        "//metropolis/pkg/localregistry",
-        "//metropolis/pkg/logbuffer",
-        "//metropolis/proto/api",
-        "//metropolis/proto/common",
-        "//metropolis/test/launch",
-        "@com_github_cenkalti_backoff_v4//:backoff",
-        "@com_github_kballard_go_shellquote//:go-shellquote",
-        "@io_bazel_rules_go//go/runfiles:go_default_library",
-        "@io_k8s_client_go//kubernetes",
-        "@io_k8s_client_go//rest",
-        "@org_golang_google_grpc//:go_default_library",
-        "@org_golang_google_grpc//codes",
-        "@org_golang_google_grpc//status",
-        "@org_golang_google_protobuf//proto",
-        "@org_golang_x_net//proxy",
-        "@org_golang_x_sys//unix",
-        "@org_uber_go_multierr//:multierr",
-    ],
-)
diff --git a/metropolis/test/launch/cluster/insecure_key.go b/metropolis/test/launch/insecure_key.go
similarity index 98%
rename from metropolis/test/launch/cluster/insecure_key.go
rename to metropolis/test/launch/insecure_key.go
index 48cd6d8..72af26f 100644
--- a/metropolis/test/launch/cluster/insecure_key.go
+++ b/metropolis/test/launch/insecure_key.go
@@ -14,7 +14,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package cluster
+package launch
 
 import (
 	"crypto/ed25519"
diff --git a/metropolis/test/launch/launch.go b/metropolis/test/launch/launch.go
deleted file mode 100644
index 953025d..0000000
--- a/metropolis/test/launch/launch.go
+++ /dev/null
@@ -1,335 +0,0 @@
-// Copyright 2020 The Monogon Project Authors.
-//
-// SPDX-License-Identifier: Apache-2.0
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-// launch implements test harnesses for running qemu VMs from tests.
-package launch
-
-import (
-	"bytes"
-	"context"
-	"errors"
-	"fmt"
-	"io"
-	"net"
-	"os"
-	"os/exec"
-	"strconv"
-	"strings"
-	"syscall"
-
-	"github.com/bazelbuild/rules_go/go/runfiles"
-	"golang.org/x/sys/unix"
-
-	"source.monogon.dev/metropolis/pkg/freeport"
-)
-
-type QemuValue map[string][]string
-
-// ToOption encodes structured data into a QEMU option. Example: "test", {"key1":
-// {"val1"}, "key2": {"val2", "val3"}} returns "test,key1=val1,key2=val2,key2=val3"
-func (value QemuValue) ToOption(name string) string {
-	var optionValues []string
-	if name != "" {
-		optionValues = append(optionValues, name)
-	}
-	for name, values := range value {
-		if len(values) == 0 {
-			optionValues = append(optionValues, name)
-		}
-		for _, val := range values {
-			optionValues = append(optionValues, fmt.Sprintf("%v=%v", name, val))
-		}
-	}
-	return strings.Join(optionValues, ",")
-}
-
-// PrettyPrintQemuArgs prints the given QEMU arguments to stderr.
-func PrettyPrintQemuArgs(name string, args []string) {
-	var argsFmt string
-	for _, arg := range args {
-		argsFmt += arg
-		if !strings.HasPrefix(arg, "-") {
-			argsFmt += "\n  "
-		} else {
-			argsFmt += " "
-		}
-	}
-	Log("Running %s:\n  %s\n", name, argsFmt)
-}
-
-// PortMap represents where VM ports are mapped to on the host. It maps from the VM
-// port number to the host port number.
-type PortMap map[uint16]uint16
-
-// ToQemuForwards generates QEMU hostfwd values (https://qemu.weilnetz.de/doc/qemu-
-// doc.html#:~:text=hostfwd=) for all mapped ports.
-func (p PortMap) ToQemuForwards() []string {
-	var hostfwdOptions []string
-	for vmPort, hostPort := range p {
-		hostfwdOptions = append(hostfwdOptions, fmt.Sprintf("tcp::%d-:%d", hostPort, vmPort))
-	}
-	return hostfwdOptions
-}
-
-// IdentityPortMap returns a port map where each given port is mapped onto itself
-// on the host. This is mainly useful for development against Metropolis. The dbg
-// command requires this mapping.
-func IdentityPortMap(ports []uint16) PortMap {
-	portMap := make(PortMap)
-	for _, port := range ports {
-		portMap[port] = port
-	}
-	return portMap
-}
-
-// ConflictFreePortMap returns a port map where each given port is mapped onto a
-// random free port on the host. This is intended for automated testing where
-// multiple instances of Metropolis nodes might be running. Please call this
-// function for each Launch command separately and as close to it as possible since
-// it cannot guarantee that the ports will remain free.
-func ConflictFreePortMap(ports []uint16) (PortMap, error) {
-	portMap := make(PortMap)
-	for _, port := range ports {
-		mappedPort, listenCloser, err := freeport.AllocateTCPPort()
-		if err != nil {
-			return portMap, fmt.Errorf("failed to get free host port: %w", err)
-		}
-		// Defer closing of the listening port until the function is done and all ports are
-		// allocated
-		defer listenCloser.Close()
-		portMap[port] = mappedPort
-	}
-	return portMap, nil
-}
-
-// GuestServiceMap maps an IP/port combination inside the virtual guest network
-// to a TCPAddr reachable by the host. If the guest connects to the virtual
-// address/port, this connection gets forwarded to the host.
-type GuestServiceMap map[*net.TCPAddr]net.TCPAddr
-
-// ToQemuForwards generates QEMU guestfwd values (https://qemu.weilnetz.de/doc/qemu-
-// doc.html#:~:text=guestfwd=) for all mapped addresses.
-func (p GuestServiceMap) ToQemuForwards() []string {
-	var guestfwdOptions []string
-	for guestAddr, hostAddr := range p {
-		guestfwdOptions = append(guestfwdOptions, fmt.Sprintf("tcp:%s-tcp:%s", guestAddr.String(), hostAddr.String()))
-	}
-	return guestfwdOptions
-}
-
-// NewSocketPair creates a new socket pair. By connecting both ends to different
-// instances you can connect them with a virtual "network cable". The ends can be
-// passed into the ConnectToSocket option.
-func NewSocketPair() (*os.File, *os.File, error) {
-	fds, err := unix.Socketpair(unix.AF_UNIX, syscall.SOCK_STREAM, 0)
-	if err != nil {
-		return nil, nil, fmt.Errorf("failed to call socketpair: %w", err)
-	}
-
-	fd1 := os.NewFile(uintptr(fds[0]), "network0")
-	fd2 := os.NewFile(uintptr(fds[1]), "network1")
-	return fd1, fd2, nil
-}
-
-// HostInterfaceMAC is the MAC address the host SLIRP network interface has if it
-// is not disabled (see DisableHostNetworkInterface in MicroVMOptions)
-var HostInterfaceMAC = net.HardwareAddr{0x02, 0x72, 0x82, 0xbf, 0xc3, 0x56}
-
-// MicroVMOptions contains all options to start a MicroVM
-type MicroVMOptions struct {
-	// Name is a human-readable identifier to be used in debug output.
-	Name string
-
-	// Path to the ELF kernel binary
-	KernelPath string
-
-	// Path to the Initramfs
-	InitramfsPath string
-
-	// Cmdline contains additional kernel commandline options
-	Cmdline string
-
-	// SerialPort is a File(descriptor) over which you can communicate with the serial
-	// port of the machine It can be set to an existing file descriptor (like
-	// os.Stdout/os.Stderr) or you can use NewSocketPair() to get one end to talk to
-	// from Go.
-	SerialPort io.Writer
-
-	// ExtraChardevs can be used similar to SerialPort, but can contain an arbitrary
-	// number of additional serial ports
-	ExtraChardevs []*os.File
-
-	// ExtraNetworkInterfaces can contain an arbitrary number of file descriptors which
-	// are mapped into the VM as virtio network interfaces. The first interface is
-	// always a SLIRP-backed interface for communicating with the host.
-	ExtraNetworkInterfaces []*os.File
-
-	// PortMap contains ports that are mapped to the host through the built-in SLIRP
-	// network interface.
-	PortMap PortMap
-
-	// GuestServiceMap contains TCP services made available in the guest virtual
-	// network which are running on the host.
-	GuestServiceMap GuestServiceMap
-
-	// DisableHostNetworkInterface disables the SLIRP-backed host network interface
-	// that is normally the first network interface. If this is set PortMap is ignored.
-	// Mostly useful for speeding up QEMU's startup time for tests.
-	DisableHostNetworkInterface bool
-
-	// PcapDump can be used to dump all network traffic to a pcap file.
-	// If unset, no dump is created.
-	PcapDump string
-}
-
-// RunMicroVM launches a tiny VM mostly intended for testing. Very quick to boot
-// (<40ms).
-func RunMicroVM(ctx context.Context, opts *MicroVMOptions) error {
-	// Generate options for all the file descriptors we'll be passing as virtio "serial
-	// ports"
-	var extraArgs []string
-	for idx := range opts.ExtraChardevs {
-		idxStr := strconv.Itoa(idx)
-		id := "extra" + idxStr
-		// That this works is pretty much a hack, but upstream QEMU doesn't have a
-		// bidirectional chardev backend not based around files/sockets on the disk which
-		// are a giant pain to work with. We're using QEMU's fdset functionality to make
-		// FDs available as pseudo-files and then "ab"using the pipe backend's fallback
-		// functionality to get a single bidirectional chardev backend backed by a passed-
-		// down RDWR fd. Ref https://lists.gnu.org/archive/html/qemu-devel/2015-
-		// 12/msg01256.html
-		addFdConf := QemuValue{
-			"set": {idxStr},
-			"fd":  {strconv.Itoa(idx + 3)},
-		}
-		chardevConf := QemuValue{
-			"id":   {id},
-			"path": {"/dev/fdset/" + idxStr},
-		}
-		deviceConf := QemuValue{
-			"chardev": {id},
-		}
-		extraArgs = append(extraArgs, "-add-fd", addFdConf.ToOption(""),
-			"-chardev", chardevConf.ToOption("pipe"), "-device", deviceConf.ToOption("virtserialport"))
-	}
-
-	for idx := range opts.ExtraNetworkInterfaces {
-		id := fmt.Sprintf("net%v", idx)
-		netdevConf := QemuValue{
-			"id": {id},
-			"fd": {strconv.Itoa(idx + 3 + len(opts.ExtraChardevs))},
-		}
-		extraArgs = append(extraArgs, "-netdev", netdevConf.ToOption("socket"), "-device", "virtio-net-device,netdev="+id)
-	}
-
-	// This sets up a minimum viable environment for our Linux kernel. It clears all
-	// standard QEMU configuration and sets up a MicroVM machine
-	// (https://github.com/qemu/qemu/blob/master/docs/microvm.rst) with all legacy
-	// emulation turned off. This means the only "hardware" the Linux kernel inside can
-	// communicate with is a single virtio-mmio region. Over that MMIO interface we run
-	// a paravirtualized RNG (since the kernel in there has nothing to gather that from
-	// and it delays booting), a single paravirtualized console and an arbitrary number
-	// of extra serial ports for talking to various things that might run inside. The
-	// kernel, initramfs and command line are mapped into VM memory at boot time and
-	// not loaded from any sort of disk. Booting and shutting off one of these VMs
-	// takes <100ms.
-	biosPath, err := runfiles.Rlocation("com_github_bonzini_qboot/bios.bin")
-	if err != nil {
-		return fmt.Errorf("while searching bios: %w", err)
-	}
-
-	baseArgs := []string{
-		"-nodefaults", "-no-user-config", "-nographic", "-no-reboot",
-		"-accel", "kvm", "-cpu", "host",
-		"-m", "1G",
-		// Needed until QEMU updates their bundled qboot version (needs
-		// https://github.com/bonzini/qboot/pull/28)
-		"-bios", biosPath,
-		"-M", "microvm,x-option-roms=off,pic=off,pit=off,rtc=off,isa-serial=off",
-		"-kernel", opts.KernelPath,
-		// We force using a triple-fault reboot strategy since otherwise the kernel first
-		// tries others (like ACPI) which are not available in this very restricted
-		// environment. Similarly we need to override the boot console since there's
-		// nothing on the ISA bus that the kernel could talk to. We also force quiet for
-		// performance reasons.
-		"-append", "reboot=t console=hvc0 quiet " + opts.Cmdline,
-		"-initrd", opts.InitramfsPath,
-		"-device", "virtio-rng-device,max-bytes=1024,period=1000",
-		"-device", "virtio-serial-device,max_ports=16",
-		"-chardev", "stdio,id=con0", "-device", "virtconsole,chardev=con0",
-	}
-
-	if !opts.DisableHostNetworkInterface {
-		qemuNetType := "user"
-		qemuNetConfig := QemuValue{
-			"id":        {"usernet0"},
-			"net":       {"10.42.0.0/24"},
-			"dhcpstart": {"10.42.0.10"},
-		}
-		if opts.PortMap != nil {
-			qemuNetConfig["hostfwd"] = opts.PortMap.ToQemuForwards()
-		}
-		if opts.GuestServiceMap != nil {
-			qemuNetConfig["guestfwd"] = opts.GuestServiceMap.ToQemuForwards()
-		}
-
-		baseArgs = append(baseArgs, "-netdev", qemuNetConfig.ToOption(qemuNetType),
-			"-device", "virtio-net-device,netdev=usernet0,mac="+HostInterfaceMAC.String())
-	}
-
-	if !opts.DisableHostNetworkInterface && opts.PcapDump != "" {
-		qemuNetDump := QemuValue{
-			"id":     {"usernet0"},
-			"netdev": {"usernet0"},
-			"file":   {opts.PcapDump},
-		}
-		extraArgs = append(extraArgs, "-object", qemuNetDump.ToOption("filter-dump"))
-	}
-
-	var stdErrBuf bytes.Buffer
-	cmd := exec.CommandContext(ctx, "qemu-system-x86_64", append(baseArgs, extraArgs...)...)
-	cmd.Stdout = opts.SerialPort
-	cmd.Stderr = &stdErrBuf
-
-	cmd.ExtraFiles = append(cmd.ExtraFiles, opts.ExtraChardevs...)
-	cmd.ExtraFiles = append(cmd.ExtraFiles, opts.ExtraNetworkInterfaces...)
-
-	PrettyPrintQemuArgs(opts.Name, cmd.Args)
-
-	err = cmd.Run()
-	// If it's a context error, just quit. There's no way to tell a
-	// killed-due-to-context vs killed-due-to-external-reason error returned by Run,
-	// so we approximate by looking at the context's status.
-	if err != nil && ctx.Err() != nil {
-		return ctx.Err()
-	}
-
-	var exerr *exec.ExitError
-	if err != nil && errors.As(err, &exerr) {
-		exerr.Stderr = stdErrBuf.Bytes()
-		newErr := QEMUError(*exerr)
-		return &newErr
-	}
-	return err
-}
-
-// QEMUError is a special type of ExitError used when QEMU fails. In addition to
-// normal ExitError features it prints stderr for debugging.
-type QEMUError exec.ExitError
-
-func (e *QEMUError) Error() string {
-	return fmt.Sprintf("%v: %v", e.String(), string(e.Stderr))
-}
diff --git a/metropolis/test/launch/log.go b/metropolis/test/launch/log.go
deleted file mode 100644
index 2637e24..0000000
--- a/metropolis/test/launch/log.go
+++ /dev/null
@@ -1,28 +0,0 @@
-package launch
-
-import (
-	"fmt"
-	"os"
-	"strings"
-)
-
-// Log is compatible with the output of ConciseString as used in the Metropolis
-// console log, making the output more readable in unified test logs.
-func Log(f string, args ...any) {
-	formatted := fmt.Sprintf(f, args...)
-	for i, line := range strings.Split(formatted, "\n") {
-		if len(line) == 0 {
-			continue
-		}
-		if i == 0 {
-			fmt.Printf("TT| %20s ! %s\n", "test launch", line)
-		} else {
-			fmt.Printf("TT| %20s | %s\n", "", line)
-		}
-	}
-}
-
-func Fatal(f string, args ...any) {
-	Log(f, args...)
-	os.Exit(1)
-}
diff --git a/metropolis/test/launch/cluster/metroctl.go b/metropolis/test/launch/metroctl.go
similarity index 99%
rename from metropolis/test/launch/cluster/metroctl.go
rename to metropolis/test/launch/metroctl.go
index e985a64..e3196a6 100644
--- a/metropolis/test/launch/cluster/metroctl.go
+++ b/metropolis/test/launch/metroctl.go
@@ -1,4 +1,4 @@
-package cluster
+package launch
 
 import (
 	"context"
diff --git a/metropolis/test/launch/cluster/prefixed_stdio.go b/metropolis/test/launch/prefixed_stdio.go
similarity index 94%
rename from metropolis/test/launch/cluster/prefixed_stdio.go
rename to metropolis/test/launch/prefixed_stdio.go
index 3ea3e18..c851c44 100644
--- a/metropolis/test/launch/cluster/prefixed_stdio.go
+++ b/metropolis/test/launch/prefixed_stdio.go
@@ -1,11 +1,11 @@
-package cluster
+package launch
 
 import (
 	"fmt"
 	"io"
 	"strings"
 
-	"source.monogon.dev/metropolis/pkg/logbuffer"
+	"source.monogon.dev/osbase/logbuffer"
 )
 
 // prefixedStdio is a io.ReadWriter which splits written bytes into lines,
diff --git a/metropolis/test/launch/cluster/swtpm.go b/metropolis/test/launch/swtpm.go
similarity index 98%
rename from metropolis/test/launch/cluster/swtpm.go
rename to metropolis/test/launch/swtpm.go
index 0f9b5c5..fa5cb78 100644
--- a/metropolis/test/launch/cluster/swtpm.go
+++ b/metropolis/test/launch/swtpm.go
@@ -1,4 +1,4 @@
-package cluster
+package launch
 
 import (
 	"context"
@@ -12,7 +12,7 @@
 
 	"github.com/bazelbuild/rules_go/go/runfiles"
 
-	"source.monogon.dev/metropolis/test/launch"
+	"source.monogon.dev/osbase/test/launch"
 )
 
 // A TPMFactory manufactures virtual TPMs using swtpm.