cloud/shepherd/equinix/manager: init

This adds implementation managing Equinix Metal server lifecycle as
part of the BMaaS project.

Co-authored-by: Mateusz Zalega <mateusz@monogon.tech>
Supersedes: https://review.monogon.dev/c/monogon/+/990
Change-Id: I5537b2d07763985ad27aecac544ed19f933d6727
Reviewed-on: https://review.monogon.dev/c/monogon/+/1129
Reviewed-by: Leopold Schabel <leo@monogon.tech>
Reviewed-by: Mateusz Zalega <mateusz@monogon.tech>
Tested-by: Jenkins CI
diff --git a/cloud/shepherd/equinix/manager/test_agent/BUILD.bazel b/cloud/shepherd/equinix/manager/test_agent/BUILD.bazel
new file mode 100644
index 0000000..8f03070
--- /dev/null
+++ b/cloud/shepherd/equinix/manager/test_agent/BUILD.bazel
@@ -0,0 +1,20 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_binary(
+    name = "test_agent",
+    embed = [":test_agent_lib"],
+    visibility = [
+        "//cloud/shepherd/equinix/manager:__pkg__",
+    ],
+)
+
+go_library(
+    name = "test_agent_lib",
+    srcs = ["main.go"],
+    importpath = "source.monogon.dev/cloud/shepherd/equinix/manager/test_agent",
+    visibility = ["//visibility:private"],
+    deps = [
+        "//cloud/agent/api",
+        "@org_golang_google_protobuf//proto",
+    ],
+)
diff --git a/cloud/shepherd/equinix/manager/test_agent/main.go b/cloud/shepherd/equinix/manager/test_agent/main.go
new file mode 100644
index 0000000..5dd5ccd
--- /dev/null
+++ b/cloud/shepherd/equinix/manager/test_agent/main.go
@@ -0,0 +1,52 @@
+// test_agent is used by the Equinix Metal Manager test code. Its only role
+// is to ensure successful delivery of the BMaaS agent executable to the test
+// hosts, together with its subsequent execution.
+package main
+
+import (
+	"crypto/ed25519"
+	"crypto/rand"
+	"fmt"
+	"io"
+	"os"
+
+	"google.golang.org/protobuf/proto"
+
+	apb "source.monogon.dev/cloud/agent/api"
+)
+
+func main() {
+	// The agent initialization message will arrive from Shepherd on Agent's
+	// standard input.
+	aimb, err := io.ReadAll(os.Stdin)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "while reading AgentInit message: %v\n", err)
+		return
+	}
+	var aim apb.TakeoverInit
+	if err := proto.Unmarshal(aimb, &aim); err != nil {
+		fmt.Fprintf(os.Stderr, "while unmarshaling TakeoverInit message: %v\n", err)
+		return
+	}
+
+	// Agent should send back apb.TakeoverResponse on its standard output.
+	pub, _, err := ed25519.GenerateKey(rand.Reader)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "while generating agent public key: %v\n", err)
+		return
+	}
+	arsp := apb.TakeoverResponse{
+		InitMessage: &aim,
+		Key:         pub,
+	}
+	arspb, err := proto.Marshal(&arsp)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "while marshaling TakeoverResponse message: %v\n", err)
+		return
+	}
+	if _, err := os.Stdout.Write(arspb); err != nil {
+		fmt.Fprintf(os.Stderr, "while writing TakeoverResponse message: %v\n", err)
+	}
+	// The agent must detach and/or terminate after sending back the reply.
+	// Failure to do so will leave the session hanging.
+}