blob: 6d1447f1947b7e6df2103792dd8c0c7efd89a03d [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
Tim Windelschmidtb6308cd2023-10-10 21:19:03 +02004package main
Serge Bazanskiae004682023-04-18 13:28:48 +02005
6import (
7 "context"
Tim Windelschmidtb6308cd2023-10-10 21:19:03 +02008 "crypto/ed25519"
9 "crypto/rand"
Serge Bazanskiae004682023-04-18 13:28:48 +020010 "testing"
11 "time"
12
13 "github.com/packethost/packngo"
14 "golang.org/x/time/rate"
15
16 "source.monogon.dev/cloud/bmaas/bmdb"
17 "source.monogon.dev/cloud/bmaas/bmdb/model"
18 "source.monogon.dev/cloud/lib/component"
Tim Windelschmidtb6308cd2023-10-10 21:19:03 +020019 "source.monogon.dev/cloud/shepherd/manager"
Serge Bazanskiae004682023-04-18 13:28:48 +020020)
21
22type recovererDut struct {
23 f *fakequinix
Tim Windelschmidtb6308cd2023-10-10 21:19:03 +020024 r *manager.Recoverer
Serge Bazanskiae004682023-04-18 13:28:48 +020025 bmdb *bmdb.Connection
26 ctx context.Context
27}
28
29func newRecovererDut(t *testing.T) *recovererDut {
30 t.Helper()
31
Tim Windelschmidtb6308cd2023-10-10 21:19:03 +020032 rc := manager.RecovererConfig{
33 ControlLoopConfig: manager.ControlLoopConfig{
Serge Bazanskiae004682023-04-18 13:28:48 +020034 DBQueryLimiter: rate.NewLimiter(rate.Every(time.Second), 10),
35 },
36 }
37
Tim Windelschmidtb6308cd2023-10-10 21:19:03 +020038 sc := providerConfig{
39 ProjectId: "noproject",
40 KeyLabel: "somekey",
41 DevicePrefix: "test-",
42 }
43
44 _, key, _ := ed25519.GenerateKey(rand.Reader)
45 k := manager.SSHKey{
46 Key: key,
47 }
48
49 f := newFakequinix(sc.ProjectId, 100)
50 provider, err := sc.New(&k, f)
51 if err != nil {
52 t.Fatalf("Could not create Provider: %v", err)
53 }
54
55 r, err := manager.NewRecoverer(provider, rc)
Serge Bazanskiae004682023-04-18 13:28:48 +020056 if err != nil {
57 t.Fatalf("Could not create Initializer: %v", err)
58 }
59
60 b := bmdb.BMDB{
61 Config: bmdb.Config{
62 Database: component.CockroachConfig{
63 InMemory: true,
64 },
65 ComponentName: "test",
66 RuntimeInfo: "test",
67 },
68 }
69 conn, err := b.Open(true)
70 if err != nil {
71 t.Fatalf("Could not create in-memory BMDB: %v", err)
72 }
73
74 ctx, ctxC := context.WithCancel(context.Background())
75 t.Cleanup(ctxC)
76
Tim Windelschmidtb6308cd2023-10-10 21:19:03 +020077 go manager.RunControlLoop(ctx, conn, r)
Serge Bazanskiae004682023-04-18 13:28:48 +020078
79 return &recovererDut{
80 f: f,
81 r: r,
82 bmdb: conn,
83 ctx: ctx,
84 }
85}
86
87// TestRecoverySmokes makes sure that the Initializer in recovery mode doesn't go
88// up in flames on the happy path.
89func TestRecoverySmokes(t *testing.T) {
90 dut := newRecovererDut(t)
91 f := dut.f
92 ctx := dut.ctx
93 conn := dut.bmdb
94
95 reservations, _ := f.ListReservations(ctx, "fake")
96
97 sess, err := conn.StartSession(ctx)
98 if err != nil {
99 t.Fatalf("Failed to create BMDB session: %v", err)
100 }
101
102 // Create test machine that should be selected for recovery.
103 // First in Fakequinix...
104 dev, _ := f.CreateDevice(ctx, &packngo.DeviceCreateRequest{
105 Hostname: "test-devices",
106 OS: "fake",
107 ProjectID: "fake",
108 HardwareReservationID: reservations[0].ID,
109 ProjectSSHKeys: []string{},
110 })
111 // ... and in BMDB.
112 err = sess.Transact(ctx, func(q *model.Queries) error {
113 machine, err := q.NewMachine(ctx)
114 if err != nil {
115 return err
116 }
117 err = q.MachineAddProvided(ctx, model.MachineAddProvidedParams{
118 MachineID: machine.MachineID,
119 Provider: model.ProviderEquinix,
120 ProviderID: dev.ID,
121 })
122 if err != nil {
123 return err
124 }
125 return q.MachineSetAgentStarted(ctx, model.MachineSetAgentStartedParams{
126 MachineID: machine.MachineID,
127 AgentStartedAt: time.Now().Add(time.Hour * -10),
128 AgentPublicKey: []byte("fakefakefakefake"),
129 })
130 })
131 if err != nil {
132 t.Fatalf("Failed to create test machine: %v", err)
133 }
134
135 // Expect to find 0 machines needing recovery.
136 deadline := time.Now().Add(10 * time.Second)
137 for {
138 if time.Now().After(deadline) {
139 t.Fatalf("Machines did not get processed in time")
140 }
141 time.Sleep(100 * time.Millisecond)
142
143 var machines []model.MachineProvided
144 err = sess.Transact(ctx, func(q *model.Queries) error {
145 var err error
Tim Windelschmidt0e749612023-08-07 17:42:59 +0000146 machines, err = q.GetMachineForAgentRecovery(ctx, model.GetMachineForAgentRecoveryParams{
147 Limit: 100,
148 Provider: model.ProviderEquinix,
149 })
Serge Bazanskiae004682023-04-18 13:28:48 +0200150 return err
151 })
152 if err != nil {
153 t.Fatalf("Failed to run Transaction: %v", err)
154 }
155 if len(machines) == 0 {
156 break
157 }
158 }
159
160 // Expect the target machine to have been rebooted.
161 dut.f.mu.Lock()
162 reboots := dut.f.reboots[dev.ID]
163 dut.f.mu.Unlock()
164 if want, got := 1, reboots; want != got {
165 t.Fatalf("Wanted %d reboot, got %d", want, got)
166 }
167
168 // Expect machine to now be available again for agent start.
169 var machines []model.MachineProvided
170 err = sess.Transact(ctx, func(q *model.Queries) error {
171 var err error
Tim Windelschmidt0e749612023-08-07 17:42:59 +0000172 machines, err = q.GetMachinesForAgentStart(ctx, model.GetMachinesForAgentStartParams{
173 Limit: 100,
174 Provider: model.ProviderEquinix,
175 })
Serge Bazanskiae004682023-04-18 13:28:48 +0200176 return err
177 })
178 if err != nil {
179 t.Fatalf("Failed to run Transaction: %v", err)
180 }
181 if want, got := 1, len(machines); want != got {
182 t.Fatalf("Wanted %d machine ready for agent start, got %d", want, got)
183 }
184}