| 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) | 
 | 	} | 
 | } |