@celilo/cli 0.26.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CELILO_CORE_MODULES.md +1 -1
- package/CELILO_SUBSYSTEMS.md +2 -0
- package/package.json +3 -3
- package/src/__integration__/container-services-cli.integration.test.ts +0 -4
- package/src/ansible/dependencies.test.ts +233 -289
- package/src/ansible/dependencies.ts +151 -83
- package/src/cli/commands/alerts-sweep.ts +14 -3
- package/src/cli/commands/machine-add.ts +0 -1
- package/src/cli/commands/machine-list.ts +10 -4
- package/src/cli/commands/machine-remove.ts +13 -7
- package/src/cli/commands/machine-status.ts +9 -11
- package/src/db/schema.ts +6 -4
- package/src/hooks/capability-loader.ts +6 -0
- package/src/hooks/define-hook.test.ts +4 -0
- package/src/hooks/types.ts +2 -1
- package/src/infrastructure/property-extractor.test.ts +0 -2
- package/src/manifest/contracts/v1.ts +19 -0
- package/src/manifest/schema.ts +1 -0
- package/src/services/alerting/inbound.test.ts +66 -0
- package/src/services/alerting/inbound.ts +35 -2
- package/src/services/alerting/sweep-runner.test.ts +5 -1
- package/src/services/alerting/sweep-runner.ts +14 -8
- package/src/services/aspect-runner.test.ts +0 -1
- package/src/services/audit/machines-reachable.test.ts +67 -8
- package/src/services/audit/machines-reachable.ts +18 -4
- package/src/services/deployed-systems.ts +31 -0
- package/src/services/fleet-checks.test.ts +232 -0
- package/src/services/fleet-checks.ts +275 -3
- package/src/services/infrastructure-selector.test.ts +0 -7
- package/src/services/infrastructure-selector.ts +24 -25
- package/src/services/infrastructure-variable-resolver.test.ts +0 -6
- package/src/services/infrastructure-variable-resolver.ts +0 -3
- package/src/services/machine-pool.test.ts +53 -85
- package/src/services/machine-pool.ts +68 -84
- package/src/services/machine-probe.test.ts +3 -4
- package/src/services/machine-probe.ts +2 -3
- package/src/services/module-deploy.ts +17 -39
- package/src/services/module-operations.test.ts +72 -1
- package/src/services/module-operations.ts +49 -10
- package/src/services/ssh-key-manager.test.ts +0 -10
- package/src/types/infrastructure.ts +11 -1
|
@@ -7,18 +7,17 @@ import { mkdtempSync, rmSync } from 'node:fs';
|
|
|
7
7
|
import { tmpdir } from 'node:os';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import { eq } from 'drizzle-orm';
|
|
10
|
-
import { closeDb, createDbClient } from '../db/client';
|
|
10
|
+
import { closeDb, createDbClient, getDb } from '../db/client';
|
|
11
11
|
import { runMigrations } from '../db/migrate';
|
|
12
|
-
import { machines } from '../db/schema';
|
|
12
|
+
import { machines, moduleInfrastructure, modules } from '../db/schema';
|
|
13
13
|
import {
|
|
14
14
|
addMachine,
|
|
15
|
-
assignModuleToMachine,
|
|
16
15
|
getMachine,
|
|
17
16
|
getMachineByHostname,
|
|
18
17
|
getMachineSshKey,
|
|
18
|
+
getModulesOnMachine,
|
|
19
19
|
listMachines,
|
|
20
20
|
removeMachine,
|
|
21
|
-
unassignModuleFromMachine,
|
|
22
21
|
} from './machine-pool';
|
|
23
22
|
|
|
24
23
|
describe('machine-pool', () => {
|
|
@@ -72,7 +71,6 @@ describe('machine-pool', () => {
|
|
|
72
71
|
},
|
|
73
72
|
role: 'host',
|
|
74
73
|
interfaces: [],
|
|
75
|
-
assignedModuleIds: [],
|
|
76
74
|
});
|
|
77
75
|
|
|
78
76
|
expect(machine.id).toBeDefined();
|
|
@@ -80,7 +78,6 @@ describe('machine-pool', () => {
|
|
|
80
78
|
expect(machine.zone).toBe('internal');
|
|
81
79
|
expect(machine.ipAddress).toBe('192.168.1.100');
|
|
82
80
|
expect(machine.hardware.cpu_cores).toBe(4);
|
|
83
|
-
expect(machine.assignedModuleIds).toEqual([]);
|
|
84
81
|
expect(machine.createdAt).toBeInstanceOf(Date);
|
|
85
82
|
});
|
|
86
83
|
|
|
@@ -94,7 +91,6 @@ describe('machine-pool', () => {
|
|
|
94
91
|
hardware: { cpu_cores: 2, memory_mb: 2048, disk_gb: 64 },
|
|
95
92
|
role: 'host',
|
|
96
93
|
interfaces: [],
|
|
97
|
-
assignedModuleIds: [],
|
|
98
94
|
});
|
|
99
95
|
|
|
100
96
|
// Verify key is encrypted in database
|
|
@@ -115,7 +111,6 @@ describe('machine-pool', () => {
|
|
|
115
111
|
hardware: { cpu_cores: 2, memory_mb: 2048, disk_gb: 64 },
|
|
116
112
|
role: 'host',
|
|
117
113
|
interfaces: [],
|
|
118
|
-
assignedModuleIds: [],
|
|
119
114
|
});
|
|
120
115
|
|
|
121
116
|
const externalMachine = await addMachine({
|
|
@@ -127,7 +122,6 @@ describe('machine-pool', () => {
|
|
|
127
122
|
hardware: { cpu_cores: 1, memory_mb: 1024, disk_gb: 25 },
|
|
128
123
|
role: 'host',
|
|
129
124
|
interfaces: [],
|
|
130
|
-
assignedModuleIds: [],
|
|
131
125
|
});
|
|
132
126
|
|
|
133
127
|
expect(dmzMachine.zone).toBe('dmz');
|
|
@@ -146,7 +140,6 @@ describe('machine-pool', () => {
|
|
|
146
140
|
hardware: { cpu_cores: 2, memory_mb: 2048, disk_gb: 64 },
|
|
147
141
|
role: 'host',
|
|
148
142
|
interfaces: [],
|
|
149
|
-
assignedModuleIds: [],
|
|
150
143
|
});
|
|
151
144
|
|
|
152
145
|
const retrieved = await getMachine(created.id);
|
|
@@ -173,7 +166,6 @@ describe('machine-pool', () => {
|
|
|
173
166
|
hardware: { cpu_cores: 4, memory_mb: 4096, disk_gb: 128 },
|
|
174
167
|
role: 'host',
|
|
175
168
|
interfaces: [],
|
|
176
|
-
assignedModuleIds: [],
|
|
177
169
|
});
|
|
178
170
|
|
|
179
171
|
const retrieved = await getMachineByHostname('unique-hostname');
|
|
@@ -204,7 +196,6 @@ describe('machine-pool', () => {
|
|
|
204
196
|
hardware: { cpu_cores: 2, memory_mb: 2048, disk_gb: 64 },
|
|
205
197
|
role: 'host',
|
|
206
198
|
interfaces: [],
|
|
207
|
-
assignedModuleIds: [],
|
|
208
199
|
});
|
|
209
200
|
|
|
210
201
|
await addMachine({
|
|
@@ -216,7 +207,6 @@ describe('machine-pool', () => {
|
|
|
216
207
|
hardware: { cpu_cores: 4, memory_mb: 4096, disk_gb: 128 },
|
|
217
208
|
role: 'host',
|
|
218
209
|
interfaces: [],
|
|
219
|
-
assignedModuleIds: [],
|
|
220
210
|
});
|
|
221
211
|
|
|
222
212
|
const result = await listMachines();
|
|
@@ -233,7 +223,6 @@ describe('machine-pool', () => {
|
|
|
233
223
|
hardware: { cpu_cores: 2, memory_mb: 2048, disk_gb: 64 },
|
|
234
224
|
role: 'host',
|
|
235
225
|
interfaces: [],
|
|
236
|
-
assignedModuleIds: [],
|
|
237
226
|
});
|
|
238
227
|
|
|
239
228
|
await addMachine({
|
|
@@ -245,7 +234,6 @@ describe('machine-pool', () => {
|
|
|
245
234
|
hardware: { cpu_cores: 1, memory_mb: 1024, disk_gb: 25 },
|
|
246
235
|
role: 'host',
|
|
247
236
|
interfaces: [],
|
|
248
|
-
assignedModuleIds: [],
|
|
249
237
|
});
|
|
250
238
|
|
|
251
239
|
const dmzMachines = await listMachines({ zone: 'dmz' });
|
|
@@ -269,7 +257,6 @@ describe('machine-pool', () => {
|
|
|
269
257
|
hardware: { cpu_cores: 2, memory_mb: 2048, disk_gb: 64 },
|
|
270
258
|
role: 'host',
|
|
271
259
|
interfaces: [],
|
|
272
|
-
assignedModuleIds: [],
|
|
273
260
|
});
|
|
274
261
|
|
|
275
262
|
const decryptedKey = await getMachineSshKey(machine.id);
|
|
@@ -281,10 +268,19 @@ describe('machine-pool', () => {
|
|
|
281
268
|
});
|
|
282
269
|
});
|
|
283
270
|
|
|
284
|
-
|
|
285
|
-
|
|
271
|
+
/**
|
|
272
|
+
* celilo#773. `assignModuleToMachine` / `unassignModuleFromMachine` are gone
|
|
273
|
+
* along with the column they wrote — both were exported, neither had a single
|
|
274
|
+
* caller, and the column they maintained had an append-only writer elsewhere
|
|
275
|
+
* with no removal path at all.
|
|
276
|
+
*
|
|
277
|
+
* Occupancy is derived now, so these assert the two divergences that actually
|
|
278
|
+
* bit the fleet rather than that a setter sets.
|
|
279
|
+
*/
|
|
280
|
+
describe('getModulesOnMachine', () => {
|
|
281
|
+
async function machineWithModuleDeployed(hostname: string, moduleId: string) {
|
|
286
282
|
const machine = await addMachine({
|
|
287
|
-
hostname
|
|
283
|
+
hostname,
|
|
288
284
|
zone: 'internal',
|
|
289
285
|
ipAddress: '192.168.1.100',
|
|
290
286
|
sshUser: 'ubuntu',
|
|
@@ -292,86 +288,59 @@ describe('machine-pool', () => {
|
|
|
292
288
|
hardware: { cpu_cores: 4, memory_mb: 4096, disk_gb: 128 },
|
|
293
289
|
role: 'host',
|
|
294
290
|
interfaces: [],
|
|
295
|
-
assignedModuleIds: [],
|
|
296
291
|
});
|
|
292
|
+
const db = getDb();
|
|
293
|
+
db.insert(modules)
|
|
294
|
+
.values({
|
|
295
|
+
id: moduleId,
|
|
296
|
+
name: moduleId,
|
|
297
|
+
version: '1.0.0',
|
|
298
|
+
manifestData: {},
|
|
299
|
+
sourcePath: `/tmp/${moduleId}`,
|
|
300
|
+
})
|
|
301
|
+
.run();
|
|
302
|
+
db.insert(moduleInfrastructure)
|
|
303
|
+
.values({
|
|
304
|
+
id: `infra-${moduleId}`,
|
|
305
|
+
moduleId,
|
|
306
|
+
infrastructureType: 'machine',
|
|
307
|
+
machineId: machine.id,
|
|
308
|
+
})
|
|
309
|
+
.run();
|
|
310
|
+
return machine;
|
|
311
|
+
}
|
|
297
312
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
313
|
+
it('reports a machine that is hosting a module as occupied', async () => {
|
|
314
|
+
// The live case: briq hosted a VERIFIED iptables and reported
|
|
315
|
+
// "None (available)", so a second module could be placed on top of it.
|
|
316
|
+
const machine = await machineWithModuleDeployed('briq', 'iptables');
|
|
317
|
+
expect(getModulesOnMachine(machine.id)).toEqual(['iptables']);
|
|
302
318
|
});
|
|
303
319
|
|
|
304
|
-
it('
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
sshKey: 'test-key',
|
|
311
|
-
hardware: { cpu_cores: 8, memory_mb: 16384, disk_gb: 256 },
|
|
312
|
-
role: 'host',
|
|
313
|
-
interfaces: [],
|
|
314
|
-
assignedModuleIds: [],
|
|
315
|
-
});
|
|
320
|
+
it('frees the machine when the module is removed, with no manual step', async () => {
|
|
321
|
+
// The other direction: the id used to survive the module forever, so
|
|
322
|
+
// placement rejected an empty box citing a module that no longer existed
|
|
323
|
+
// and `machine remove` refused to remove it. Nothing could clear it.
|
|
324
|
+
const machine = await machineWithModuleDeployed('rpi4-lr', 'homebridge');
|
|
325
|
+
expect(getModulesOnMachine(machine.id)).toEqual(['homebridge']);
|
|
316
326
|
|
|
317
|
-
|
|
318
|
-
await assignModuleToMachine(machine.id, 'module-2');
|
|
319
|
-
await assignModuleToMachine(machine.id, 'module-3');
|
|
327
|
+
getDb().delete(modules).where(eq(modules.id, 'homebridge')).run();
|
|
320
328
|
|
|
321
|
-
|
|
322
|
-
expect(updated?.assignedModuleIds).toEqual(['module-1', 'module-2', 'module-3']);
|
|
329
|
+
expect(getModulesOnMachine(machine.id)).toEqual([]);
|
|
323
330
|
});
|
|
324
331
|
|
|
325
|
-
it('
|
|
326
|
-
await expect(assignModuleToMachine('non-existent-id', 'module-id')).rejects.toThrow(
|
|
327
|
-
/Machine not found/,
|
|
328
|
-
);
|
|
329
|
-
});
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
describe('unassignModuleFromMachine', () => {
|
|
333
|
-
it('removes module from machine', async () => {
|
|
332
|
+
it('reports an empty machine as empty', async () => {
|
|
334
333
|
const machine = await addMachine({
|
|
335
|
-
hostname: '
|
|
334
|
+
hostname: 'spare',
|
|
336
335
|
zone: 'internal',
|
|
337
|
-
ipAddress: '192.168.1.
|
|
336
|
+
ipAddress: '192.168.1.101',
|
|
338
337
|
sshUser: 'ubuntu',
|
|
339
338
|
sshKey: 'test-key',
|
|
340
|
-
hardware: { cpu_cores:
|
|
341
|
-
role: 'host',
|
|
342
|
-
interfaces: [],
|
|
343
|
-
assignedModuleIds: ['homebridge'],
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
await unassignModuleFromMachine(machine.id, 'homebridge');
|
|
347
|
-
|
|
348
|
-
const updated = await getMachine(machine.id);
|
|
349
|
-
expect(updated?.assignedModuleIds).toEqual([]);
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
it('removes specific module from multiple assignments', async () => {
|
|
353
|
-
const machine = await addMachine({
|
|
354
|
-
hostname: 'test-machine',
|
|
355
|
-
zone: 'internal',
|
|
356
|
-
ipAddress: '192.168.1.100',
|
|
357
|
-
sshUser: 'ubuntu',
|
|
358
|
-
sshKey: 'test-key',
|
|
359
|
-
hardware: { cpu_cores: 8, memory_mb: 16384, disk_gb: 256 },
|
|
339
|
+
hardware: { cpu_cores: 2, memory_mb: 2048, disk_gb: 64 },
|
|
360
340
|
role: 'host',
|
|
361
341
|
interfaces: [],
|
|
362
|
-
assignedModuleIds: ['module-1', 'module-2', 'module-3'],
|
|
363
342
|
});
|
|
364
|
-
|
|
365
|
-
await unassignModuleFromMachine(machine.id, 'module-2');
|
|
366
|
-
|
|
367
|
-
const updated = await getMachine(machine.id);
|
|
368
|
-
expect(updated?.assignedModuleIds).toEqual(['module-1', 'module-3']);
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
it('throws error for non-existent machine', async () => {
|
|
372
|
-
await expect(unassignModuleFromMachine('non-existent-id', 'module-id')).rejects.toThrow(
|
|
373
|
-
/Machine not found/,
|
|
374
|
-
);
|
|
343
|
+
expect(getModulesOnMachine(machine.id)).toEqual([]);
|
|
375
344
|
});
|
|
376
345
|
});
|
|
377
346
|
|
|
@@ -386,7 +355,6 @@ describe('machine-pool', () => {
|
|
|
386
355
|
hardware: { cpu_cores: 2, memory_mb: 2048, disk_gb: 64 },
|
|
387
356
|
role: 'host',
|
|
388
357
|
interfaces: [],
|
|
389
|
-
assignedModuleIds: [],
|
|
390
358
|
});
|
|
391
359
|
|
|
392
360
|
await removeMachine(machine.id);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { and, eq, inArray } from 'drizzle-orm';
|
|
3
|
-
import { getDb } from '../db/client';
|
|
3
|
+
import { type DbClient, getDb } from '../db/client';
|
|
4
4
|
import {
|
|
5
5
|
type AllocatableZone,
|
|
6
6
|
type NetworkZone,
|
|
@@ -8,15 +8,11 @@ import {
|
|
|
8
8
|
ipAllocations,
|
|
9
9
|
machines,
|
|
10
10
|
moduleInfrastructure,
|
|
11
|
+
moduleSystems,
|
|
11
12
|
} from '../db/schema';
|
|
12
13
|
import { decryptSecret, encryptSecret } from '../secrets/encryption';
|
|
13
14
|
import { getOrCreateMasterKey } from '../secrets/master-key';
|
|
14
|
-
import type {
|
|
15
|
-
Machine,
|
|
16
|
-
MachineRole,
|
|
17
|
-
NetworkInterface,
|
|
18
|
-
ResourceAllocation,
|
|
19
|
-
} from '../types/infrastructure';
|
|
15
|
+
import type { Machine, MachineRole, NetworkInterface } from '../types/infrastructure';
|
|
20
16
|
import { EncryptionEnvelopeSchema, parseJsonWithValidation } from '../validation/schemas';
|
|
21
17
|
|
|
22
18
|
/**
|
|
@@ -57,7 +53,6 @@ export async function addMachine(
|
|
|
57
53
|
hardware: machine.hardware, // Drizzle auto-stringifies with mode: 'json'
|
|
58
54
|
role: machine.role ?? 'host',
|
|
59
55
|
interfaces: machine.interfaces ?? [],
|
|
60
|
-
assignedModuleIds: machine.assignedModuleIds, // Drizzle auto-stringifies with mode: 'json'
|
|
61
56
|
earmarkedModule: machine.earmarkedModule || null,
|
|
62
57
|
createdAt: now,
|
|
63
58
|
updatedAt: now,
|
|
@@ -75,7 +70,6 @@ export async function addMachine(
|
|
|
75
70
|
hardware: machine.hardware,
|
|
76
71
|
role: (machine.role ?? 'host') as MachineRole,
|
|
77
72
|
interfaces: (machine.interfaces ?? []) as NetworkInterface[],
|
|
78
|
-
assignedModuleIds: machine.assignedModuleIds,
|
|
79
73
|
earmarkedModule: machine.earmarkedModule || null,
|
|
80
74
|
apiOnly: false, // defaults to false; toggled later via separate flow
|
|
81
75
|
createdAt: now,
|
|
@@ -88,7 +82,6 @@ export async function addMachine(
|
|
|
88
82
|
*/
|
|
89
83
|
function rowToMachine(row: typeof machines.$inferSelect): Machine {
|
|
90
84
|
const hardware = row.hardware || { cpu_cores: 0, memory_mb: 0, disk_gb: 0, arch: 'unknown' };
|
|
91
|
-
const assignedModuleIds = Array.isArray(row.assignedModuleIds) ? row.assignedModuleIds : [];
|
|
92
85
|
const interfaces = Array.isArray(row.interfaces) ? (row.interfaces as NetworkInterface[]) : [];
|
|
93
86
|
|
|
94
87
|
return {
|
|
@@ -101,7 +94,6 @@ function rowToMachine(row: typeof machines.$inferSelect): Machine {
|
|
|
101
94
|
hardware,
|
|
102
95
|
role: (row.role as MachineRole) || 'host',
|
|
103
96
|
interfaces,
|
|
104
|
-
assignedModuleIds,
|
|
105
97
|
earmarkedModule: row.earmarkedModule ?? undefined,
|
|
106
98
|
apiOnly: row.apiOnly,
|
|
107
99
|
createdAt: new Date(row.createdAt),
|
|
@@ -180,12 +172,15 @@ export async function findMachineForModule(
|
|
|
180
172
|
if (!zone) return null;
|
|
181
173
|
const allMachines = await listMachines({ zone });
|
|
182
174
|
|
|
175
|
+
const db = getDb();
|
|
183
176
|
for (const machine of allMachines) {
|
|
184
177
|
// Skip machines earmarked for other modules
|
|
185
178
|
if (machine.earmarkedModule && machine.earmarkedModule !== moduleId) continue;
|
|
186
|
-
// Skip machines
|
|
187
|
-
|
|
188
|
-
|
|
179
|
+
// Skip machines occupied by other modules. Derived, not read from a stored
|
|
180
|
+
// snapshot — this preview offered an occupied machine's address with no
|
|
181
|
+
// error at all while the snapshot said the box was free (celilo#773).
|
|
182
|
+
const occupants = getModulesOnMachine(machine.id, db);
|
|
183
|
+
if (occupants.length > 0 && !occupants.includes(moduleId)) continue;
|
|
189
184
|
// Match role if required
|
|
190
185
|
if (requiredRole && (machine.role || 'host') !== requiredRole) continue;
|
|
191
186
|
return machine;
|
|
@@ -385,76 +380,65 @@ export async function removeMachine(id: string): Promise<void> {
|
|
|
385
380
|
}
|
|
386
381
|
|
|
387
382
|
/**
|
|
388
|
-
*
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
*
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
throw new Error(`Machine not found: ${machineId}`);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
const updatedModuleIds = machine.assignedModuleIds.filter((id) => id !== moduleId);
|
|
421
|
-
const db = getDb();
|
|
422
|
-
|
|
423
|
-
await db
|
|
424
|
-
.update(machines)
|
|
425
|
-
.set({
|
|
426
|
-
assignedModuleIds: updatedModuleIds, // Drizzle auto-stringifies with mode: 'json'
|
|
427
|
-
updatedAt: new Date(),
|
|
428
|
-
})
|
|
429
|
-
.where(eq(machines.id, machineId));
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
/**
|
|
433
|
-
* Get resource allocation for all modules on a machine
|
|
434
|
-
* Calculates total CPU, memory, and disk used by assigned modules
|
|
383
|
+
* Which modules occupy this machine, ANSWERED AT THE POINT OF USE (celilo#773).
|
|
384
|
+
*
|
|
385
|
+
* There used to be a `machines.assigned_module_ids` array holding this. It had
|
|
386
|
+
* one writer, which only ever appended, no reader that reconciled it, and no
|
|
387
|
+
* removal path at all — and it was load-bearing, because placement refuses a
|
|
388
|
+
* machine whose list is non-empty and does not name the module being placed.
|
|
389
|
+
* It diverged in both directions on the live fleet:
|
|
390
|
+
*
|
|
391
|
+
* - OVER-recorded, permanently: removing a module left its id behind, so
|
|
392
|
+
* placement rejected an empty machine citing a module that no longer
|
|
393
|
+
* existed, and `machine remove` refused to remove it. No command could
|
|
394
|
+
* clear the entry; the only remedy was editing the database.
|
|
395
|
+
* - UNDER-recorded: `briq` hosted a VERIFIED `iptables` and reported
|
|
396
|
+
* "None (available)", so a second module could be placed onto an occupied
|
|
397
|
+
* box — the exact collision the filter exists to prevent — and the
|
|
398
|
+
* `machine remove` guard was equally blind. Only a separately-set earmark
|
|
399
|
+
* was keeping placement off it.
|
|
400
|
+
*
|
|
401
|
+
* Both silent. Deriving costs one indexed query and cannot drift, and removal
|
|
402
|
+
* frees the machine for free: both source tables cascade on the module row.
|
|
403
|
+
*
|
|
404
|
+
* ⚠️ The UNION of both tables is deliberate, and is not the "two sources that
|
|
405
|
+
* disagree" problem the old column was. Neither is a copy — both are FK columns
|
|
406
|
+
* owned by the deploy path — and for a SAFETY guard the liberal read is the
|
|
407
|
+
* correct one: reporting an occupied box as free is how two modules land on one
|
|
408
|
+
* machine, while reporting a free box as occupied merely sends the operator to
|
|
409
|
+
* look. `module_infrastructure` records the CLAIM at selection time and
|
|
410
|
+
* `module_systems` the realized deployment, so a module mid-deploy is visible
|
|
411
|
+
* in the first before it appears in the second.
|
|
435
412
|
*/
|
|
436
|
-
export
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
throw new Error(`Machine not found: ${machineId}`);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// If no modules assigned, return zero allocation
|
|
443
|
-
if (machine.assignedModuleIds.length === 0) {
|
|
444
|
-
return { cpu: 0, memory: 0, disk: 0 };
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
const db = getDb();
|
|
448
|
-
|
|
449
|
-
// Get all module infrastructure records for this machine
|
|
450
|
-
const _infraRecords = await db
|
|
451
|
-
.select()
|
|
413
|
+
export function getModulesOnMachine(machineId: string, db: DbClient = getDb()): string[] {
|
|
414
|
+
const claimed = db
|
|
415
|
+
.select({ moduleId: moduleInfrastructure.moduleId })
|
|
452
416
|
.from(moduleInfrastructure)
|
|
453
|
-
.where(eq(moduleInfrastructure.machineId, machineId))
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
417
|
+
.where(eq(moduleInfrastructure.machineId, machineId))
|
|
418
|
+
.all();
|
|
419
|
+
const deployed = db
|
|
420
|
+
.select({ moduleId: moduleSystems.moduleId })
|
|
421
|
+
.from(moduleSystems)
|
|
422
|
+
.where(eq(moduleSystems.machineId, machineId))
|
|
423
|
+
.all();
|
|
424
|
+
|
|
425
|
+
return [...new Set([...claimed, ...deployed].map((r) => r.moduleId))].sort();
|
|
460
426
|
}
|
|
427
|
+
|
|
428
|
+
// `getModuleResourcesOnMachine` is deleted (celilo#773, Rule 1.2 / 7.6).
|
|
429
|
+
//
|
|
430
|
+
// It queried `module_infrastructure`, discarded the result into an unused
|
|
431
|
+
// variable, and returned `{cpu: 0, memory: 0, disk: 0}` behind a TODO. Its
|
|
432
|
+
// three callers subtracted that zero from the machine's hardware before
|
|
433
|
+
// comparing against the module's requirements — so the subtraction was
|
|
434
|
+
// ceremony and the comparison was really "is this machine big enough at all".
|
|
435
|
+
//
|
|
436
|
+
// That is a genuinely useful check and it survives, stated plainly, in
|
|
437
|
+
// `machineHasCapacity`. What is gone is the appearance of multi-tenant capacity
|
|
438
|
+
// accounting that never accounted for anything: a gate nobody has seen fail is
|
|
439
|
+
// not a gate, and one that cannot fail reads as protection that is not there.
|
|
440
|
+
//
|
|
441
|
+
// Real accounting needs a decision this issue does not make — whether a
|
|
442
|
+
// module's draw is its manifest MINIMUM (`requires.system`) or its deployed
|
|
443
|
+
// size, which for pool machines celilo does not own. That belongs to whoever
|
|
444
|
+
// builds capacity-aware placement for the machine pool.
|
|
@@ -21,8 +21,8 @@ describe('local machine reachability', () => {
|
|
|
21
21
|
test('a reachable local box produces no finding', async () => {
|
|
22
22
|
const findings = await auditMachinesReachable({
|
|
23
23
|
results: [
|
|
24
|
-
{
|
|
25
|
-
{
|
|
24
|
+
{ hostname: 'celilo-mgr', ipAddress: LOCAL_MACHINE_IP, reachable: true },
|
|
25
|
+
{ hostname: 'briq', ipAddress: '192.168.0.254', reachable: true },
|
|
26
26
|
],
|
|
27
27
|
});
|
|
28
28
|
|
|
@@ -33,9 +33,8 @@ describe('local machine reachability', () => {
|
|
|
33
33
|
// The skip must not blunt the check for the machines it exists to watch.
|
|
34
34
|
const findings = await auditMachinesReachable({
|
|
35
35
|
results: [
|
|
36
|
-
{
|
|
36
|
+
{ hostname: 'celilo-mgr', ipAddress: LOCAL_MACHINE_IP, reachable: true },
|
|
37
37
|
{
|
|
38
|
-
id: 'briq',
|
|
39
38
|
hostname: 'briq',
|
|
40
39
|
ipAddress: '192.168.0.254',
|
|
41
40
|
reachable: false,
|
|
@@ -38,7 +38,7 @@ export async function probeMachines(): Promise<MachineReachableResult[]> {
|
|
|
38
38
|
return Promise.all(
|
|
39
39
|
machines.map(async (m): Promise<MachineReachableResult> => {
|
|
40
40
|
if (m.ipAddress === LOCAL_MACHINE_IP) {
|
|
41
|
-
return {
|
|
41
|
+
return { hostname: m.hostname, ipAddress: m.ipAddress, reachable: true };
|
|
42
42
|
}
|
|
43
43
|
try {
|
|
44
44
|
await execFileAsync(
|
|
@@ -57,11 +57,10 @@ export async function probeMachines(): Promise<MachineReachableResult[]> {
|
|
|
57
57
|
],
|
|
58
58
|
{ timeout: 8000 },
|
|
59
59
|
);
|
|
60
|
-
return {
|
|
60
|
+
return { hostname: m.hostname, ipAddress: m.ipAddress, reachable: true };
|
|
61
61
|
} catch (err) {
|
|
62
62
|
const e = err as { stderr?: string; message?: string };
|
|
63
63
|
return {
|
|
64
|
-
id: m.id,
|
|
65
64
|
hostname: m.hostname,
|
|
66
65
|
ipAddress: m.ipAddress,
|
|
67
66
|
reachable: false,
|
|
@@ -59,38 +59,18 @@ export interface DeployResult {
|
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
): Promise<void> {
|
|
75
|
-
// Get current machine record
|
|
76
|
-
const machine = await db.select().from(machines).where(eq(machines.id, machineId)).get();
|
|
77
|
-
|
|
78
|
-
if (!machine) {
|
|
79
|
-
throw new Error(`Machine not found: ${machineId}`);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Add module ID if not already assigned
|
|
83
|
-
const assignedIds = machine.assignedModuleIds || [];
|
|
84
|
-
if (!assignedIds.includes(moduleId)) {
|
|
85
|
-
const updatedIds = [...assignedIds, moduleId];
|
|
86
|
-
await db
|
|
87
|
-
.update(machines)
|
|
88
|
-
.set({ assignedModuleIds: updatedIds })
|
|
89
|
-
.where(eq(machines.id, machineId))
|
|
90
|
-
.run();
|
|
91
|
-
log.success(`Machine ${machineId} updated with module assignment`);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
62
|
+
// `updateMachineAssignment` is deleted (celilo#773).
|
|
63
|
+
//
|
|
64
|
+
// It was the sole writer of `machines.assigned_module_ids`, it only ever
|
|
65
|
+
// APPENDED, and nothing ever removed an entry — `module remove` deletes the
|
|
66
|
+
// module row and never touches `machines`. So a machine accumulated the ids of
|
|
67
|
+
// modules that no longer existed and could never be freed except by editing the
|
|
68
|
+
// database.
|
|
69
|
+
//
|
|
70
|
+
// Nothing replaces it. Occupancy is now derived from `module_infrastructure`
|
|
71
|
+
// and `module_systems` at the point of use (`getModulesOnMachine`), both of
|
|
72
|
+
// which the deploy path already writes and both of which cascade on module
|
|
73
|
+
// removal — so the machine frees itself with no bookkeeping step to forget.
|
|
94
74
|
|
|
95
75
|
export interface DeployOptions {
|
|
96
76
|
debug?: boolean;
|
|
@@ -1203,13 +1183,11 @@ async function deployModuleImpl(
|
|
|
1203
1183
|
};
|
|
1204
1184
|
}
|
|
1205
1185
|
|
|
1206
|
-
if (plan.infrastructure?.type === '
|
|
1207
|
-
//
|
|
1208
|
-
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
plan.infrastructure.serviceId
|
|
1212
|
-
) {
|
|
1186
|
+
if (plan.infrastructure?.type === 'container_service' && plan.infrastructure.serviceId) {
|
|
1187
|
+
// Placeholder branch retained below; the machine branch is gone because
|
|
1188
|
+
// occupancy is no longer a stored fact to update (celilo#773).
|
|
1189
|
+
}
|
|
1190
|
+
if (plan.infrastructure?.type === 'container_service' && plan.infrastructure.serviceId) {
|
|
1213
1191
|
// TODO: extract Terraform outputs and persist them on
|
|
1214
1192
|
// module_infrastructure.containerMetadata. Until that lands,
|
|
1215
1193
|
// the deploy still succeeds — we just don't track which
|