@celilo/cli 0.3.30 → 0.4.0-alpha.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/drizzle/0005_module_operations.sql +12 -0
- package/drizzle/0006_base_module_aspects.sql +15 -0
- package/drizzle/0007_module_systems.sql +17 -0
- package/drizzle/meta/_journal.json +21 -0
- package/package.json +5 -4
- package/schemas/system_config.json +14 -28
- package/src/ansible/inventory.test.ts +46 -62
- package/src/ansible/inventory.ts +48 -25
- package/src/capabilities/registration.ts +25 -7
- package/src/capabilities/validation.test.ts +30 -0
- package/src/capabilities/validation.ts +8 -0
- package/src/cli/backup-rename.test.ts +95 -0
- package/src/cli/cli.test.ts +17 -23
- package/src/cli/command-registry.ts +199 -0
- package/src/cli/commands/backup-list.ts +1 -1
- package/src/cli/commands/events.ts +96 -0
- package/src/cli/commands/machine-add.ts +103 -59
- package/src/cli/commands/module-import.ts +153 -4
- package/src/cli/commands/module-remove.ts +86 -17
- package/src/cli/commands/module-status.ts +6 -2
- package/src/cli/commands/publish/alpha.test.ts +185 -0
- package/src/cli/commands/publish/alpha.ts +226 -0
- package/src/cli/commands/publish/changesets.test.ts +89 -0
- package/src/cli/commands/publish/changesets.ts +144 -0
- package/src/cli/commands/publish/consumer-pins.test.ts +155 -0
- package/src/cli/commands/publish/consumer-pins.ts +149 -0
- package/src/cli/commands/publish/execute.ts +131 -0
- package/src/cli/commands/publish/global-install.test.ts +154 -0
- package/src/cli/commands/publish/global-install.ts +171 -0
- package/src/cli/commands/publish/helpers.ts +227 -0
- package/src/cli/commands/publish/index.ts +365 -0
- package/src/cli/commands/publish/module-registry.test.ts +40 -0
- package/src/cli/commands/publish/module-registry.ts +64 -0
- package/src/cli/commands/publish/plan.ts +107 -0
- package/src/cli/commands/publish/preflight.ts +238 -0
- package/src/cli/commands/publish/types.ts +264 -0
- package/src/cli/commands/publish/workspace.test.ts +323 -0
- package/src/cli/commands/publish/workspace.ts +596 -0
- package/src/cli/commands/restore.ts +126 -0
- package/src/cli/commands/storage-add-local.ts +1 -1
- package/src/cli/commands/storage-add-s3.ts +1 -1
- package/src/cli/commands/subscribers-add.ts +68 -0
- package/src/cli/commands/subscribers-list.ts +48 -0
- package/src/cli/commands/subscribers-remove.ts +38 -0
- package/src/cli/commands/subscribers-serve.ts +77 -0
- package/src/cli/commands/subscribers-status.ts +33 -0
- package/src/cli/commands/subscribers-test.ts +71 -0
- package/src/cli/commands/system-apply-config-equivalence.test.ts +108 -0
- package/src/cli/commands/system-apply-config.test.ts +70 -0
- package/src/cli/commands/system-apply-config.ts +130 -0
- package/src/cli/commands/system-audit.ts +2 -1
- package/src/cli/commands/system-init-deprecation.test.ts +90 -0
- package/src/cli/commands/system-init.ts +36 -70
- package/src/cli/commands/system-update.ts +3 -2
- package/src/cli/completion.ts +22 -1
- package/src/cli/index.ts +214 -6
- package/src/cli/interactive-config.test.ts +19 -0
- package/src/cli/restore-command.test.ts +131 -0
- package/src/db/client.ts +42 -0
- package/src/db/schema.test.ts +13 -16
- package/src/db/schema.ts +161 -9
- package/src/hooks/capability-loader-firewall.test.ts +6 -15
- package/src/hooks/capability-loader.test.ts +2 -3
- package/src/hooks/capability-loader.ts +36 -2
- package/src/hooks/define-hook.test.ts +4 -0
- package/src/hooks/executor.test.ts +18 -0
- package/src/hooks/executor.ts +21 -2
- package/src/hooks/load-hook-config.test.ts +26 -24
- package/src/hooks/load-hook-config.ts +11 -2
- package/src/hooks/run-named-hook.ts +16 -0
- package/src/hooks/types.ts +9 -1
- package/src/manifest/contracts/v1.ts +70 -0
- package/src/manifest/schema.ts +262 -16
- package/src/manifest/validate-privileged.test.ts +84 -0
- package/src/manifest/validate.test.ts +156 -0
- package/src/manifest/validate.ts +69 -0
- package/src/module/import.ts +12 -0
- package/src/services/aspect-approvals.test.ts +231 -0
- package/src/services/aspect-approvals.ts +120 -0
- package/src/services/aspect-runner.test.ts +493 -0
- package/src/services/aspect-runner.ts +438 -0
- package/src/services/aspect-template-resolver.test.ts +101 -0
- package/src/services/aspect-template-resolver.ts +122 -0
- package/src/services/backup-create.ts +104 -25
- package/src/services/backup-envelope-roundtrip.test.ts +199 -0
- package/src/services/backup-in-flight-refusal.test.ts +163 -0
- package/src/services/backup-manifest.test.ts +115 -0
- package/src/services/backup-manifest.ts +163 -0
- package/src/services/backup-restore.ts +154 -19
- package/src/services/build-bus/delivery-events.ts +92 -0
- package/src/services/build-bus/event-factory.ts +54 -0
- package/src/services/build-bus/fan-out.test.ts +279 -0
- package/src/services/build-bus/fan-out.ts +161 -0
- package/src/services/build-bus/hook-dispatch-mgmt.test.ts +157 -0
- package/src/services/build-bus/hook-dispatch.test.ts +207 -0
- package/src/services/build-bus/hook-dispatch.ts +198 -0
- package/src/services/build-bus/hook-dispatcher.ts +115 -0
- package/src/services/build-bus/index.ts +41 -0
- package/src/services/build-bus/receiver-server.test.ts +179 -0
- package/src/services/build-bus/receiver-server.ts +159 -0
- package/src/services/build-bus/status.test.ts +212 -0
- package/src/services/build-bus/status.ts +213 -0
- package/src/services/build-bus/subscriber-store.ts +113 -0
- package/src/services/celilo-events.test.ts +70 -0
- package/src/services/celilo-events.ts +92 -0
- package/src/services/celilo-mgmt-hooks.test.ts +296 -0
- package/src/services/config-interview.ts +13 -95
- package/src/services/cross-module-data-manager.ts +2 -31
- package/src/services/cross-module-read.test.ts +250 -0
- package/src/services/cross-module-read.ts +232 -0
- package/src/services/deploy-validation.ts +7 -0
- package/src/services/deployed-systems.test.ts +235 -0
- package/src/services/deployed-systems.ts +308 -0
- package/src/services/dns-provider-backfill.ts +75 -0
- package/src/services/health-runner.ts +19 -3
- package/src/services/infrastructure-variable-resolver.test.ts +6 -32
- package/src/services/infrastructure-variable-resolver.ts +3 -13
- package/src/services/machine-detector.ts +104 -48
- package/src/services/machine-pool.ts +145 -2
- package/src/services/module-config.ts +78 -120
- package/src/services/module-deploy.ts +113 -40
- package/src/services/module-operations.test.ts +154 -0
- package/src/services/module-operations.ts +154 -0
- package/src/services/module-subscriptions.test.ts +58 -0
- package/src/services/module-subscriptions.ts +24 -1
- package/src/services/module-types-generator.test.ts +3 -3
- package/src/services/module-types-generator.ts +7 -2
- package/src/services/proxmox-reconcile.test.ts +333 -0
- package/src/services/proxmox-reconcile.ts +156 -0
- package/src/services/proxmox-state-recovery.ts +3 -24
- package/src/services/restore-from-file.test.ts +177 -0
- package/src/services/restore-from-file.ts +355 -0
- package/src/services/restore-preflight.test.ts +127 -0
- package/src/services/restore-preflight.ts +118 -0
- package/src/services/storage-providers/s3.ts +10 -2
- package/src/services/system-identity.ts +30 -0
- package/src/services/system-init.test.ts +64 -21
- package/src/services/system-init.ts +28 -26
- package/src/templates/generator.test.ts +7 -16
- package/src/templates/generator.ts +28 -115
- package/src/test-utils/integration.ts +5 -2
- package/src/types/infrastructure.ts +8 -0
- package/src/variables/computed/computed-integration.test.ts +191 -0
- package/src/variables/computed/computed.test.ts +177 -0
- package/src/variables/computed/evaluate.ts +271 -0
- package/src/variables/computed/marker.ts +53 -0
- package/src/variables/computed/parse.ts +262 -0
- package/src/variables/computed/provider-lookup.ts +130 -0
- package/src/variables/context.test.ts +89 -28
- package/src/variables/context.ts +196 -191
- package/src/variables/parser.ts +3 -3
- package/src/variables/resolver.test.ts +61 -0
- package/src/variables/resolver.ts +81 -0
- package/src/variables/types.ts +23 -1
- package/src/services/dns-auto-register.ts +0 -211
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import type { ModuleManifest } from '@/manifest/schema';
|
|
13
13
|
import { and, eq } from 'drizzle-orm';
|
|
14
14
|
import { resolveInfrastructureVariables } from './infrastructure-variable-resolver';
|
|
15
|
+
import { upsertModuleConfig } from './module-config';
|
|
15
16
|
|
|
16
17
|
const TEST_DB_PATH = './test-infra-resolver.db';
|
|
17
18
|
|
|
@@ -268,26 +269,9 @@ describe('resolveInfrastructureVariables - Proxmox Container Service', () => {
|
|
|
268
269
|
});
|
|
269
270
|
|
|
270
271
|
// Create IPAM-allocated config (mimics what IPAM does)
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
key: 'vmid',
|
|
275
|
-
value: '100',
|
|
276
|
-
valueJson: null,
|
|
277
|
-
},
|
|
278
|
-
{
|
|
279
|
-
moduleId,
|
|
280
|
-
key: 'target_ip',
|
|
281
|
-
value: '10.0.10.5',
|
|
282
|
-
valueJson: null,
|
|
283
|
-
},
|
|
284
|
-
{
|
|
285
|
-
moduleId,
|
|
286
|
-
key: 'hostname',
|
|
287
|
-
value: 'homebridge',
|
|
288
|
-
valueJson: null,
|
|
289
|
-
},
|
|
290
|
-
]);
|
|
272
|
+
upsertModuleConfig(db, moduleId, 'vmid', '100');
|
|
273
|
+
upsertModuleConfig(db, moduleId, 'target_ip', '10.0.10.5');
|
|
274
|
+
upsertModuleConfig(db, moduleId, 'hostname', 'homebridge');
|
|
291
275
|
|
|
292
276
|
// Resolve variables (no Terraform outputs = Proxmox)
|
|
293
277
|
const result = await resolveInfrastructureVariables(moduleId, manifest, null, db);
|
|
@@ -422,12 +406,7 @@ describe('resolveInfrastructureVariables - Digital Ocean Container Service', ()
|
|
|
422
406
|
});
|
|
423
407
|
|
|
424
408
|
// Create hostname config (user-configured)
|
|
425
|
-
|
|
426
|
-
moduleId,
|
|
427
|
-
key: 'hostname',
|
|
428
|
-
value: 'dns-ext',
|
|
429
|
-
valueJson: null,
|
|
430
|
-
});
|
|
409
|
+
upsertModuleConfig(db, moduleId, 'hostname', 'dns-ext');
|
|
431
410
|
|
|
432
411
|
// Terraform outputs (wrapped format)
|
|
433
412
|
const terraformOutputs = {
|
|
@@ -502,12 +481,7 @@ describe('resolveInfrastructureVariables - User Override', () => {
|
|
|
502
481
|
});
|
|
503
482
|
|
|
504
483
|
// User manually set ip.primary
|
|
505
|
-
|
|
506
|
-
moduleId,
|
|
507
|
-
key: 'ip.primary',
|
|
508
|
-
value: '198.51.100.50', // User override
|
|
509
|
-
valueJson: null,
|
|
510
|
-
});
|
|
484
|
+
upsertModuleConfig(db, moduleId, 'ip.primary', '198.51.100.50');
|
|
511
485
|
|
|
512
486
|
const result = await resolveInfrastructureVariables(moduleId, manifest, null, db);
|
|
513
487
|
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
extractTerraformProperties,
|
|
8
8
|
} from '@/infrastructure/property-extractor';
|
|
9
9
|
import type { ModuleManifest } from '@/manifest/schema';
|
|
10
|
+
import { upsertModuleConfig } from '@/services/module-config';
|
|
10
11
|
import type { Machine } from '@/types/infrastructure';
|
|
11
12
|
import { and, eq } from 'drizzle-orm';
|
|
12
13
|
|
|
@@ -190,19 +191,8 @@ export async function resolveInfrastructureVariables(
|
|
|
190
191
|
continue;
|
|
191
192
|
}
|
|
192
193
|
|
|
193
|
-
// Store in moduleConfigs
|
|
194
|
-
|
|
195
|
-
.insert(moduleConfigs)
|
|
196
|
-
.values({
|
|
197
|
-
moduleId,
|
|
198
|
-
key: variable.name,
|
|
199
|
-
value,
|
|
200
|
-
valueJson: null,
|
|
201
|
-
})
|
|
202
|
-
.onConflictDoUpdate({
|
|
203
|
-
target: [moduleConfigs.moduleId, moduleConfigs.key],
|
|
204
|
-
set: { value, updatedAt: new Date() },
|
|
205
|
-
});
|
|
194
|
+
// Store in moduleConfigs (always via the typed-storage helper)
|
|
195
|
+
upsertModuleConfig(db, moduleId, variable.name, value);
|
|
206
196
|
|
|
207
197
|
resolved[variable.name] = value;
|
|
208
198
|
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Machine Detector
|
|
3
|
-
* Auto-detects machine information
|
|
3
|
+
* Auto-detects machine information, either over SSH (remote machines) or
|
|
4
|
+
* locally (the management box adding itself — 127.0.0.1, no SSH needed).
|
|
5
|
+
*
|
|
6
|
+
* The detection logic is identical either way; only command EXECUTION
|
|
7
|
+
* differs. Each detector takes a `CommandRunner` so the SSH and local
|
|
8
|
+
* paths share the same parsing.
|
|
4
9
|
*/
|
|
5
10
|
|
|
6
11
|
import { execSync } from 'node:child_process';
|
|
@@ -18,6 +23,9 @@ export class DetectionError extends Error {
|
|
|
18
23
|
}
|
|
19
24
|
}
|
|
20
25
|
|
|
26
|
+
/** Runs a shell command and returns trimmed stdout (throws on failure). */
|
|
27
|
+
export type CommandRunner = (command: string) => string;
|
|
28
|
+
|
|
21
29
|
/**
|
|
22
30
|
* Execute SSH command and return output
|
|
23
31
|
*/
|
|
@@ -38,11 +46,31 @@ function sshExec(ip: string, user: string, keyPath: string, command: string): st
|
|
|
38
46
|
}
|
|
39
47
|
}
|
|
40
48
|
|
|
49
|
+
/** CommandRunner that SSHes to a remote machine. */
|
|
50
|
+
function sshRunner(ip: string, user: string, keyPath: string): CommandRunner {
|
|
51
|
+
return (command) => sshExec(ip, user, keyPath, command);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** CommandRunner that runs the command locally (for the self/localhost box). */
|
|
55
|
+
export const localRunner: CommandRunner = (command) => {
|
|
56
|
+
try {
|
|
57
|
+
return execSync(command, {
|
|
58
|
+
encoding: 'utf8',
|
|
59
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
60
|
+
timeout: 10000,
|
|
61
|
+
shell: '/bin/bash',
|
|
62
|
+
}).trim();
|
|
63
|
+
} catch (error) {
|
|
64
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
65
|
+
throw new DetectionError(`Local command failed: ${message}`);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
41
69
|
/**
|
|
42
70
|
* Detect hostname
|
|
43
71
|
*/
|
|
44
|
-
function detectHostname(
|
|
45
|
-
const output =
|
|
72
|
+
function detectHostname(run: CommandRunner): string {
|
|
73
|
+
const output = run('hostname');
|
|
46
74
|
if (!output) {
|
|
47
75
|
throw new DetectionError('Failed to detect hostname: empty output');
|
|
48
76
|
}
|
|
@@ -52,8 +80,8 @@ function detectHostname(ip: string, user: string, keyPath: string): string {
|
|
|
52
80
|
/**
|
|
53
81
|
* Detect CPU cores
|
|
54
82
|
*/
|
|
55
|
-
function detectCpuCores(
|
|
56
|
-
const output =
|
|
83
|
+
function detectCpuCores(run: CommandRunner): number {
|
|
84
|
+
const output = run('nproc || grep -c ^processor /proc/cpuinfo');
|
|
57
85
|
const cores = Number.parseInt(output, 10);
|
|
58
86
|
if (Number.isNaN(cores) || cores <= 0) {
|
|
59
87
|
throw new DetectionError(`Invalid CPU cores detected: ${output}`);
|
|
@@ -64,9 +92,9 @@ function detectCpuCores(ip: string, user: string, keyPath: string): number {
|
|
|
64
92
|
/**
|
|
65
93
|
* Detect memory in MB
|
|
66
94
|
*/
|
|
67
|
-
function detectMemory(
|
|
95
|
+
function detectMemory(run: CommandRunner): number {
|
|
68
96
|
// Get memory line, parse in JavaScript
|
|
69
|
-
const output =
|
|
97
|
+
const output = run('grep MemTotal /proc/meminfo');
|
|
70
98
|
|
|
71
99
|
// Parse "MemTotal: 1048576 kB" -> extract number
|
|
72
100
|
const match = output.match(/MemTotal:\s+(\d+)\s+kB/);
|
|
@@ -84,9 +112,9 @@ function detectMemory(ip: string, user: string, keyPath: string): number {
|
|
|
84
112
|
/**
|
|
85
113
|
* Detect disk space in GB
|
|
86
114
|
*/
|
|
87
|
-
function detectDisk(
|
|
115
|
+
function detectDisk(run: CommandRunner): number {
|
|
88
116
|
// Get root filesystem size, parse in JavaScript
|
|
89
|
-
const output =
|
|
117
|
+
const output = run('df -BG / | tail -1');
|
|
90
118
|
|
|
91
119
|
// Parse "Filesystem 1G-blocks Used Available Use% Mounted" -> extract second field
|
|
92
120
|
// Example: "/dev/sda1 20G 5G 14G 27% /"
|
|
@@ -112,9 +140,9 @@ function detectDisk(ip: string, user: string, keyPath: string): number {
|
|
|
112
140
|
/**
|
|
113
141
|
* Detect CPU architecture (arm64, x64, etc.)
|
|
114
142
|
*/
|
|
115
|
-
function detectArch(
|
|
143
|
+
function detectArch(run: CommandRunner): string {
|
|
116
144
|
try {
|
|
117
|
-
const output =
|
|
145
|
+
const output = run('dpkg --print-architecture 2>/dev/null || uname -m');
|
|
118
146
|
const raw = output.trim();
|
|
119
147
|
// Normalize: aarch64 → arm64, x86_64 → amd64
|
|
120
148
|
if (raw === 'aarch64' || raw === 'arm64') return 'arm64';
|
|
@@ -128,22 +156,17 @@ function detectArch(ip: string, user: string, keyPath: string): string {
|
|
|
128
156
|
/**
|
|
129
157
|
* Detect OS information
|
|
130
158
|
*/
|
|
131
|
-
function detectOsInfo(
|
|
159
|
+
function detectOsInfo(run: CommandRunner): string {
|
|
132
160
|
try {
|
|
133
161
|
// Try /etc/os-release first (most modern systems)
|
|
134
|
-
const output =
|
|
135
|
-
ip,
|
|
136
|
-
user,
|
|
137
|
-
keyPath,
|
|
138
|
-
"cat /etc/os-release | grep PRETTY_NAME | cut -d'\"' -f2",
|
|
139
|
-
);
|
|
162
|
+
const output = run("cat /etc/os-release | grep PRETTY_NAME | cut -d'\"' -f2");
|
|
140
163
|
if (output) {
|
|
141
164
|
return output;
|
|
142
165
|
}
|
|
143
166
|
} catch {
|
|
144
167
|
// Fall back to uname if /etc/os-release not available
|
|
145
168
|
try {
|
|
146
|
-
const output =
|
|
169
|
+
const output = run('uname -s -r');
|
|
147
170
|
return output || 'Unknown Linux';
|
|
148
171
|
} catch {
|
|
149
172
|
return 'Unknown Linux';
|
|
@@ -201,37 +224,36 @@ function parseIpTextOutput(output: string): Array<{ name: string; ipAddress: str
|
|
|
201
224
|
}
|
|
202
225
|
|
|
203
226
|
/**
|
|
204
|
-
* Detect
|
|
205
|
-
*
|
|
227
|
+
* Detect network interfaces using the given runner. `fallbackIp` is used
|
|
228
|
+
* to synthesize a single interface when detection yields nothing.
|
|
206
229
|
*/
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
sshKeyPath: string,
|
|
230
|
+
async function detectNetworkInterfacesWith(
|
|
231
|
+
run: CommandRunner,
|
|
232
|
+
fallbackIp: string,
|
|
211
233
|
): Promise<{ interfaces: NetworkInterface[]; role: MachineRole }> {
|
|
212
234
|
let rawInterfaces: Array<{ name: string; ipAddress: string }>;
|
|
213
235
|
|
|
214
236
|
try {
|
|
215
|
-
const jsonOutput =
|
|
237
|
+
const jsonOutput = run('ip -j addr show');
|
|
216
238
|
rawInterfaces = parseIpJsonOutput(jsonOutput);
|
|
217
239
|
} catch {
|
|
218
240
|
try {
|
|
219
|
-
const textOutput =
|
|
241
|
+
const textOutput = run('ip addr show');
|
|
220
242
|
rawInterfaces = parseIpTextOutput(textOutput);
|
|
221
243
|
} catch {
|
|
222
244
|
// Can't detect interfaces - return single interface from known IP
|
|
223
|
-
const zone = await detectZoneFromIp(
|
|
245
|
+
const zone = await detectZoneFromIp(fallbackIp);
|
|
224
246
|
return {
|
|
225
|
-
interfaces: [{ name: 'unknown', ipAddress:
|
|
247
|
+
interfaces: [{ name: 'unknown', ipAddress: fallbackIp, zone }],
|
|
226
248
|
role: 'host',
|
|
227
249
|
};
|
|
228
250
|
}
|
|
229
251
|
}
|
|
230
252
|
|
|
231
253
|
if (rawInterfaces.length === 0) {
|
|
232
|
-
const zone = await detectZoneFromIp(
|
|
254
|
+
const zone = await detectZoneFromIp(fallbackIp);
|
|
233
255
|
return {
|
|
234
|
-
interfaces: [{ name: 'unknown', ipAddress:
|
|
256
|
+
interfaces: [{ name: 'unknown', ipAddress: fallbackIp, zone }],
|
|
235
257
|
role: 'host',
|
|
236
258
|
};
|
|
237
259
|
}
|
|
@@ -261,6 +283,50 @@ export async function detectNetworkInterfaces(
|
|
|
261
283
|
return { interfaces, role };
|
|
262
284
|
}
|
|
263
285
|
|
|
286
|
+
/**
|
|
287
|
+
* Detect all network interfaces on a remote machine via SSH.
|
|
288
|
+
*/
|
|
289
|
+
export async function detectNetworkInterfaces(
|
|
290
|
+
ip: string,
|
|
291
|
+
sshUser: string,
|
|
292
|
+
sshKeyPath: string,
|
|
293
|
+
): Promise<{ interfaces: NetworkInterface[]; role: MachineRole }> {
|
|
294
|
+
return detectNetworkInterfacesWith(sshRunner(ip, sshUser, sshKeyPath), ip);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Detect network interfaces on the local (management) box — no SSH.
|
|
299
|
+
*/
|
|
300
|
+
export async function detectNetworkInterfacesLocal(): Promise<{
|
|
301
|
+
interfaces: NetworkInterface[];
|
|
302
|
+
role: MachineRole;
|
|
303
|
+
}> {
|
|
304
|
+
return detectNetworkInterfacesWith(localRunner, '127.0.0.1');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Detect machine information using the given runner.
|
|
309
|
+
*/
|
|
310
|
+
function detectMachineInfoWith(run: CommandRunner): DetectedMachineInfo {
|
|
311
|
+
const hostname = detectHostname(run);
|
|
312
|
+
const cpu_cores = detectCpuCores(run);
|
|
313
|
+
const memory_mb = detectMemory(run);
|
|
314
|
+
const disk_gb = detectDisk(run);
|
|
315
|
+
const arch = detectArch(run);
|
|
316
|
+
const osInfo = detectOsInfo(run);
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
hostname,
|
|
320
|
+
osInfo,
|
|
321
|
+
hardware: {
|
|
322
|
+
cpu_cores,
|
|
323
|
+
memory_mb,
|
|
324
|
+
disk_gb,
|
|
325
|
+
arch,
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
264
330
|
/**
|
|
265
331
|
* Detect machine information via SSH
|
|
266
332
|
*
|
|
@@ -286,24 +352,14 @@ export async function detectMachineInfo(
|
|
|
286
352
|
throw new DetectionError('SSH key path is required');
|
|
287
353
|
}
|
|
288
354
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const cpu_cores = detectCpuCores(ip, sshUser, sshKeyPath);
|
|
292
|
-
const memory_mb = detectMemory(ip, sshUser, sshKeyPath);
|
|
293
|
-
const disk_gb = detectDisk(ip, sshUser, sshKeyPath);
|
|
294
|
-
const arch = detectArch(ip, sshUser, sshKeyPath);
|
|
295
|
-
const osInfo = detectOsInfo(ip, sshUser, sshKeyPath);
|
|
355
|
+
return detectMachineInfoWith(sshRunner(ip, sshUser, sshKeyPath));
|
|
356
|
+
}
|
|
296
357
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
memory_mb,
|
|
303
|
-
disk_gb,
|
|
304
|
-
arch,
|
|
305
|
-
},
|
|
306
|
-
};
|
|
358
|
+
/**
|
|
359
|
+
* Detect machine information on the local (management) box — no SSH.
|
|
360
|
+
*/
|
|
361
|
+
export async function detectMachineInfoLocal(): Promise<DetectedMachineInfo> {
|
|
362
|
+
return detectMachineInfoWith(localRunner);
|
|
307
363
|
}
|
|
308
364
|
|
|
309
365
|
/**
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { eq } from 'drizzle-orm';
|
|
2
|
+
import { and, eq, inArray } from 'drizzle-orm';
|
|
3
3
|
import { getDb } from '../db/client';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
type NetworkZone,
|
|
6
|
+
containerServices,
|
|
7
|
+
ipAllocations,
|
|
8
|
+
machines,
|
|
9
|
+
moduleInfrastructure,
|
|
10
|
+
} from '../db/schema';
|
|
5
11
|
import { decryptSecret, encryptSecret } from '../secrets/encryption';
|
|
6
12
|
import { getOrCreateMasterKey } from '../secrets/master-key';
|
|
7
13
|
import type {
|
|
@@ -70,6 +76,7 @@ export async function addMachine(
|
|
|
70
76
|
interfaces: (machine.interfaces ?? []) as NetworkInterface[],
|
|
71
77
|
assignedModuleIds: machine.assignedModuleIds,
|
|
72
78
|
earmarkedModule: machine.earmarkedModule || null,
|
|
79
|
+
apiOnly: false, // defaults to false; toggled later via separate flow
|
|
73
80
|
createdAt: now,
|
|
74
81
|
updatedAt: now,
|
|
75
82
|
};
|
|
@@ -95,6 +102,7 @@ function rowToMachine(row: typeof machines.$inferSelect): Machine {
|
|
|
95
102
|
interfaces,
|
|
96
103
|
assignedModuleIds,
|
|
97
104
|
earmarkedModule: row.earmarkedModule ?? undefined,
|
|
105
|
+
apiOnly: row.apiOnly,
|
|
98
106
|
createdAt: new Date(row.createdAt),
|
|
99
107
|
updatedAt: new Date(row.updatedAt),
|
|
100
108
|
};
|
|
@@ -231,6 +239,141 @@ export async function listMachines(filters?: MachineFilters): Promise<Machine[]>
|
|
|
231
239
|
return results.map(rowToMachine);
|
|
232
240
|
}
|
|
233
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Return every machine in any of the given zones — the discovery
|
|
244
|
+
* query the aspect runner uses to find fan-out targets per
|
|
245
|
+
* v2/CELILO_BASE.md.
|
|
246
|
+
*
|
|
247
|
+
* Filters:
|
|
248
|
+
* - `excludeApiOnly` (default `true`): drops machines marked
|
|
249
|
+
* `api_only` (the greenwave / ISP-modem case). Aspects use
|
|
250
|
+
* Ansible, so api_only systems are unreachable. v2/CELILO_BASE.md
|
|
251
|
+
* D8.
|
|
252
|
+
* - `excludeHostnames` (default `[]`): drops named hostnames.
|
|
253
|
+
* Aspect authors that need to skip the system running their own
|
|
254
|
+
* primary deploy pass it here; the framework doesn't auto-skip
|
|
255
|
+
* (v2/CELILO_BASE.md D3).
|
|
256
|
+
*
|
|
257
|
+
* NOTE on container_service systems: this Phase 1 implementation
|
|
258
|
+
* covers MACHINE-based systems only (the machines table). LXCs/VMs
|
|
259
|
+
* provisioned via container_service live in
|
|
260
|
+
* `module_infrastructure` and inherit zone from `ip_allocations`;
|
|
261
|
+
* supporting them is a future iteration. For Phase 1's
|
|
262
|
+
* aspect-fanout test (and forgejo's e2e), all targets are machine
|
|
263
|
+
* pool entries, so this is sufficient.
|
|
264
|
+
*/
|
|
265
|
+
export async function getSystemsByZone(
|
|
266
|
+
zones: string[],
|
|
267
|
+
options: { excludeApiOnly?: boolean; excludeHostnames?: string[] } = {},
|
|
268
|
+
): Promise<Machine[]> {
|
|
269
|
+
if (zones.length === 0) return [];
|
|
270
|
+
const db = getDb();
|
|
271
|
+
const excludeApiOnly = options.excludeApiOnly ?? true;
|
|
272
|
+
const excludeHostnames = new Set(options.excludeHostnames ?? []);
|
|
273
|
+
|
|
274
|
+
const rows = await db
|
|
275
|
+
.select()
|
|
276
|
+
.from(machines)
|
|
277
|
+
.where(inArray(machines.zone, zones as NetworkZone[]));
|
|
278
|
+
|
|
279
|
+
return rows
|
|
280
|
+
.map(rowToMachine)
|
|
281
|
+
.filter((m) => !(excludeApiOnly && m.apiOnly))
|
|
282
|
+
.filter((m) => !excludeHostnames.has(m.hostname));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* A container_service-provisioned system (Proxmox LXC, Digital
|
|
287
|
+
* Ocean droplet, etc.) — the complement to a machine-pool entry.
|
|
288
|
+
* Surfaced by `getContainerSystemsByZone` for SC5 Proxmox
|
|
289
|
+
* reconciliation: each LXC's owning module has terraform state we
|
|
290
|
+
* may need to update.
|
|
291
|
+
*
|
|
292
|
+
* `containerMetadata` is the raw JSON from the db (vmid, droplet
|
|
293
|
+
* ID, container_ip, etc. — provider-specific shape); callers
|
|
294
|
+
* inspect it as needed.
|
|
295
|
+
*/
|
|
296
|
+
export interface ContainerSystem {
|
|
297
|
+
/** UUID of the module_infrastructure row. */
|
|
298
|
+
infrastructureId: string;
|
|
299
|
+
/** Module that owns this provisioned system (and its terraform state). */
|
|
300
|
+
moduleId: string;
|
|
301
|
+
/** Container service this system was provisioned through. */
|
|
302
|
+
serviceId: string;
|
|
303
|
+
providerName: 'proxmox' | 'digitalocean' | 'aws' | 'gcp' | 'azure';
|
|
304
|
+
/** Zone from `ip_allocations` (the authoritative zone for the LXC's IP). */
|
|
305
|
+
zone: NetworkZone;
|
|
306
|
+
/** Container IP in CIDR (e.g., "10.0.20.42/24"). May be null if allocation absent. */
|
|
307
|
+
containerIp: string | null;
|
|
308
|
+
containerMetadata: Record<string, unknown> | null;
|
|
309
|
+
apiOnly: boolean;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Enumerate container_service-provisioned systems whose IP-allocation
|
|
314
|
+
* zone matches the input. Used by SC5's Proxmox reconciler to find
|
|
315
|
+
* LXCs whose terraform config may need rewriting when an aspect
|
|
316
|
+
* declares `proxmox_reconcile.tfvars`.
|
|
317
|
+
*
|
|
318
|
+
* Filters api_only systems by default. The aspect runner reads the
|
|
319
|
+
* same row to decide if Ansible should even reach the system.
|
|
320
|
+
*
|
|
321
|
+
* Phase 1 simulator note: cele2e test environments add systems via
|
|
322
|
+
* `celilo machine add`, which writes to the machines table — NOT
|
|
323
|
+
* module_infrastructure. So this function returns [] for those
|
|
324
|
+
* tests. It only matters in production where container_service
|
|
325
|
+
* (Proxmox) is real.
|
|
326
|
+
*/
|
|
327
|
+
export async function getContainerSystemsByZone(
|
|
328
|
+
zones: string[],
|
|
329
|
+
options: { excludeApiOnly?: boolean } = {},
|
|
330
|
+
): Promise<ContainerSystem[]> {
|
|
331
|
+
if (zones.length === 0) return [];
|
|
332
|
+
const db = getDb();
|
|
333
|
+
const excludeApiOnly = options.excludeApiOnly ?? true;
|
|
334
|
+
|
|
335
|
+
// ip_allocations is the authoritative source for which LXC sits
|
|
336
|
+
// in which zone (Proxmox doesn't expose zone directly; celilo's
|
|
337
|
+
// IPAM is what assigned it). Join it to module_infrastructure to
|
|
338
|
+
// get the rest of the metadata.
|
|
339
|
+
const rows = await db
|
|
340
|
+
.select({
|
|
341
|
+
infraId: moduleInfrastructure.id,
|
|
342
|
+
moduleId: moduleInfrastructure.moduleId,
|
|
343
|
+
serviceId: moduleInfrastructure.serviceId,
|
|
344
|
+
containerMetadata: moduleInfrastructure.containerMetadata,
|
|
345
|
+
apiOnly: moduleInfrastructure.apiOnly,
|
|
346
|
+
ipZone: ipAllocations.zone,
|
|
347
|
+
containerIp: ipAllocations.containerIp,
|
|
348
|
+
providerName: containerServices.providerName,
|
|
349
|
+
})
|
|
350
|
+
.from(moduleInfrastructure)
|
|
351
|
+
.innerJoin(ipAllocations, eq(ipAllocations.moduleId, moduleInfrastructure.moduleId))
|
|
352
|
+
.innerJoin(containerServices, eq(containerServices.id, moduleInfrastructure.serviceId))
|
|
353
|
+
.where(
|
|
354
|
+
and(
|
|
355
|
+
eq(moduleInfrastructure.infrastructureType, 'container_service'),
|
|
356
|
+
// ip_allocations.zone is narrower than NetworkZone (no 'external');
|
|
357
|
+
// safe to cast — any 'external' input would just match zero rows.
|
|
358
|
+
inArray(ipAllocations.zone, zones as Array<'dmz' | 'app' | 'secure' | 'internal'>),
|
|
359
|
+
),
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
return rows
|
|
363
|
+
.filter((r) => !(excludeApiOnly && r.apiOnly))
|
|
364
|
+
.filter((r): r is typeof r & { serviceId: string } => r.serviceId !== null)
|
|
365
|
+
.map((r) => ({
|
|
366
|
+
infrastructureId: r.infraId,
|
|
367
|
+
moduleId: r.moduleId,
|
|
368
|
+
serviceId: r.serviceId,
|
|
369
|
+
providerName: r.providerName,
|
|
370
|
+
zone: r.ipZone,
|
|
371
|
+
containerIp: r.containerIp ?? null,
|
|
372
|
+
containerMetadata: r.containerMetadata ?? null,
|
|
373
|
+
apiOnly: r.apiOnly,
|
|
374
|
+
}));
|
|
375
|
+
}
|
|
376
|
+
|
|
234
377
|
/**
|
|
235
378
|
* Remove a machine from the pool
|
|
236
379
|
*/
|