blob: 109c375099b78c1be12b939e7143231f87fac5d8 [file] [log] [blame]
package main
import (
"context"
"crypto/ed25519"
"crypto/rand"
"testing"
"time"
"github.com/packethost/packngo"
"golang.org/x/time/rate"
"source.monogon.dev/cloud/bmaas/bmdb"
"source.monogon.dev/cloud/bmaas/bmdb/model"
"source.monogon.dev/cloud/lib/component"
"source.monogon.dev/cloud/shepherd/manager"
)
type recovererDut struct {
f *fakequinix
r *manager.Recoverer
bmdb *bmdb.Connection
ctx context.Context
}
func newRecovererDut(t *testing.T) *recovererDut {
t.Helper()
rc := manager.RecovererConfig{
ControlLoopConfig: manager.ControlLoopConfig{
DBQueryLimiter: rate.NewLimiter(rate.Every(time.Second), 10),
},
}
sc := providerConfig{
ProjectId: "noproject",
KeyLabel: "somekey",
DevicePrefix: "test-",
}
_, key, _ := ed25519.GenerateKey(rand.Reader)
k := manager.SSHKey{
Key: key,
}
f := newFakequinix(sc.ProjectId, 100)
provider, err := sc.New(&k, f)
if err != nil {
t.Fatalf("Could not create Provider: %v", err)
}
r, err := manager.NewRecoverer(provider, rc)
if err != nil {
t.Fatalf("Could not create Initializer: %v", err)
}
b := bmdb.BMDB{
Config: bmdb.Config{
Database: component.CockroachConfig{
InMemory: true,
},
ComponentName: "test",
RuntimeInfo: "test",
},
}
conn, err := b.Open(true)
if err != nil {
t.Fatalf("Could not create in-memory BMDB: %v", err)
}
ctx, ctxC := context.WithCancel(context.Background())
t.Cleanup(ctxC)
go manager.RunControlLoop(ctx, conn, r)
return &recovererDut{
f: f,
r: r,
bmdb: conn,
ctx: ctx,
}
}
// TestRecoverySmokes makes sure that the Initializer in recovery mode doesn't go
// up in flames on the happy path.
func TestRecoverySmokes(t *testing.T) {
dut := newRecovererDut(t)
f := dut.f
ctx := dut.ctx
conn := dut.bmdb
reservations, _ := f.ListReservations(ctx, "fake")
sess, err := conn.StartSession(ctx)
if err != nil {
t.Fatalf("Failed to create BMDB session: %v", err)
}
// Create test machine that should be selected for recovery.
// First in Fakequinix...
dev, _ := f.CreateDevice(ctx, &packngo.DeviceCreateRequest{
Hostname: "test-devices",
OS: "fake",
ProjectID: "fake",
HardwareReservationID: reservations[0].ID,
ProjectSSHKeys: []string{},
})
// ... and in BMDB.
err = sess.Transact(ctx, func(q *model.Queries) error {
machine, err := q.NewMachine(ctx)
if err != nil {
return err
}
err = q.MachineAddProvided(ctx, model.MachineAddProvidedParams{
MachineID: machine.MachineID,
Provider: model.ProviderEquinix,
ProviderID: dev.ID,
})
if err != nil {
return err
}
return q.MachineSetAgentStarted(ctx, model.MachineSetAgentStartedParams{
MachineID: machine.MachineID,
AgentStartedAt: time.Now().Add(time.Hour * -10),
AgentPublicKey: []byte("fakefakefakefake"),
})
})
if err != nil {
t.Fatalf("Failed to create test machine: %v", err)
}
// Expect to find 0 machines needing recovery.
deadline := time.Now().Add(10 * time.Second)
for {
if time.Now().After(deadline) {
t.Fatalf("Machines did not get processed in time")
}
time.Sleep(100 * time.Millisecond)
var machines []model.MachineProvided
err = sess.Transact(ctx, func(q *model.Queries) error {
var err error
machines, err = q.GetMachineForAgentRecovery(ctx, model.GetMachineForAgentRecoveryParams{
Limit: 100,
Provider: model.ProviderEquinix,
})
return err
})
if err != nil {
t.Fatalf("Failed to run Transaction: %v", err)
}
if len(machines) == 0 {
break
}
}
// Expect the target machine to have been rebooted.
dut.f.mu.Lock()
reboots := dut.f.reboots[dev.ID]
dut.f.mu.Unlock()
if want, got := 1, reboots; want != got {
t.Fatalf("Wanted %d reboot, got %d", want, got)
}
// Expect machine to now be available again for agent start.
var machines []model.MachineProvided
err = sess.Transact(ctx, func(q *model.Queries) error {
var err error
machines, err = q.GetMachinesForAgentStart(ctx, model.GetMachinesForAgentStartParams{
Limit: 100,
Provider: model.ProviderEquinix,
})
return err
})
if err != nil {
t.Fatalf("Failed to run Transaction: %v", err)
}
if want, got := 1, len(machines); want != got {
t.Fatalf("Wanted %d machine ready for agent start, got %d", want, got)
}
}