@celilo/cli 0.13.1 → 0.13.3
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/package.json +1 -1
- package/schemas/system_config.json +16 -0
- package/src/ansible/inventory.ts +2 -2
- package/src/cli/generate-zsh-completion.ts +2 -0
- package/src/services/aspect-runner.ts +2 -2
- package/src/services/module-deploy.ts +15 -3
- package/src/services/ssh-key-manager.test.ts +14 -0
- package/src/services/ssh-key-manager.ts +12 -0
- package/src/services/system-config-validator.test.ts +25 -0
package/package.json
CHANGED
|
@@ -74,6 +74,22 @@
|
|
|
74
74
|
"maximum": 4094,
|
|
75
75
|
"description": "VLAN tag for internal zone (not defaulted; internal is untagged)"
|
|
76
76
|
},
|
|
77
|
+
"network.secure-mgmt.subnet": {
|
|
78
|
+
"type": "string",
|
|
79
|
+
"pattern": "^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}/\\d{1,2}$",
|
|
80
|
+
"description": "Control-plane subnet CIDR — the network celilo-mgr itself occupies when it does not sit on the internal LAN (not defaulted — discovered at celilo-mgmt install, or recorded by firewall onboarding). Derives the firewall's trusted sources and the internal resolver's split-horizon view for celilo's own traffic."
|
|
81
|
+
},
|
|
82
|
+
"network.secure-mgmt.gateway": {
|
|
83
|
+
"type": "string",
|
|
84
|
+
"pattern": "^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$",
|
|
85
|
+
"description": "Control-plane gateway IP address (not defaulted — discovered from the default route)"
|
|
86
|
+
},
|
|
87
|
+
"network.secure-mgmt.vlan": {
|
|
88
|
+
"type": "integer",
|
|
89
|
+
"minimum": 1,
|
|
90
|
+
"maximum": 4094,
|
|
91
|
+
"description": "VLAN tag for the control-plane zone (not defaulted)"
|
|
92
|
+
},
|
|
77
93
|
"dns.primary": {
|
|
78
94
|
"type": "string",
|
|
79
95
|
"pattern": "^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$",
|
package/src/ansible/inventory.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { DbClient } from '../db/client';
|
|
|
6
6
|
import { machines, moduleConfigs, systemConfig } from '../db/schema';
|
|
7
7
|
import { getModuleSystems } from '../services/deployed-systems';
|
|
8
8
|
import { parseStoredConfigValue } from '../services/module-config';
|
|
9
|
-
import { getTempKeyPath } from '../services/ssh-key-manager';
|
|
9
|
+
import { LOCAL_MACHINE_IP, getTempKeyPath } from '../services/ssh-key-manager';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Inventory generation result
|
|
@@ -395,7 +395,7 @@ export async function generateInventory(
|
|
|
395
395
|
|
|
396
396
|
// Build host definition from machine. The local box (127.0.0.1)
|
|
397
397
|
// uses Ansible's local connection — no SSH key/host.
|
|
398
|
-
const isLocal = machine.ipAddress ===
|
|
398
|
+
const isLocal = machine.ipAddress === LOCAL_MACHINE_IP;
|
|
399
399
|
host = {
|
|
400
400
|
hostname: moduleHostname || machine.hostname,
|
|
401
401
|
ansibleHost: machine.ipAddress,
|
|
@@ -355,6 +355,8 @@ _celilo_system_config_keys() {
|
|
|
355
355
|
'network.secure.gateway:Secure gateway IP'
|
|
356
356
|
'network.internal.subnet:Internal subnet'
|
|
357
357
|
'network.internal.gateway:Internal gateway IP'
|
|
358
|
+
'network.secure-mgmt.subnet:Control-plane subnet (celilo-mgr own network)'
|
|
359
|
+
'network.secure-mgmt.gateway:Control-plane gateway IP'
|
|
358
360
|
'dns.primary:Primary DNS server'
|
|
359
361
|
'dns.fallback:Fallback DNS servers'
|
|
360
362
|
'routing.internal_gateway:Internal gateway IP'
|
|
@@ -51,7 +51,7 @@ import { executeAnsible } from './deploy-ansible';
|
|
|
51
51
|
import { getContainerSystemsInZones } from './deployed-systems';
|
|
52
52
|
import { getSystemsByZone } from './machine-pool';
|
|
53
53
|
import { executeProxmoxReconcile, planProxmoxReconcile } from './proxmox-reconcile';
|
|
54
|
-
import { writeTemporarySshKey } from './ssh-key-manager';
|
|
54
|
+
import { LOCAL_MACHINE_IP, writeTemporarySshKey } from './ssh-key-manager';
|
|
55
55
|
|
|
56
56
|
type DbClient = ReturnType<typeof getDb>;
|
|
57
57
|
|
|
@@ -231,7 +231,7 @@ export async function materializeAspectAnsible(args: {
|
|
|
231
231
|
// (ansible/inventory.ts). Without this the fan-out tried `ssh root@127.0.0.1`
|
|
232
232
|
// with an empty key file and the host failed UNREACHABLE with an opaque
|
|
233
233
|
// "error in libcrypto", so celilo could not configure its own resolv.conf.
|
|
234
|
-
const isLocal = t.ipAddress ===
|
|
234
|
+
const isLocal = t.ipAddress === LOCAL_MACHINE_IP;
|
|
235
235
|
const keyPath = t.machineId && !isLocal ? await writeTemporarySshKey(t.machineId) : undefined;
|
|
236
236
|
inventoryHosts.push({
|
|
237
237
|
hostname: t.hostname,
|
|
@@ -40,7 +40,7 @@ import { resolveInfrastructureVariables } from './infrastructure-variable-resolv
|
|
|
40
40
|
import { findMachineForModule } from './machine-pool';
|
|
41
41
|
import { checkProxmoxReachable, formatProxmoxUnreachableError } from './proxmox-preflight';
|
|
42
42
|
import { republishStaticWebConsumers } from './public-web-republish';
|
|
43
|
-
import { deleteTemporarySshKey, writeTemporarySshKey } from './ssh-key-manager';
|
|
43
|
+
import { LOCAL_MACHINE_IP, deleteTemporarySshKey, writeTemporarySshKey } from './ssh-key-manager';
|
|
44
44
|
import { buildTerraformEnvForService } from './terraform-env';
|
|
45
45
|
|
|
46
46
|
export interface DeployResult {
|
|
@@ -1129,8 +1129,20 @@ async function deployModuleImpl(
|
|
|
1129
1129
|
|
|
1130
1130
|
let machineId: string | undefined;
|
|
1131
1131
|
if (plan.infrastructure?.type === 'machine' && plan.infrastructure.machineId) {
|
|
1132
|
-
|
|
1133
|
-
|
|
1132
|
+
// The management box deploys to ITSELF (registered in the machine pool as
|
|
1133
|
+
// 127.0.0.1) over Ansible's local connection — it has no stored SSH key,
|
|
1134
|
+
// and pinning one here throws before Ansible ever runs. `inventory.ts` and
|
|
1135
|
+
// `aspect-runner.ts` both already skip the key for the local box; this call
|
|
1136
|
+
// site did not, so celilo-mgmt could not complete a self-deploy.
|
|
1137
|
+
const machine = db
|
|
1138
|
+
.select()
|
|
1139
|
+
.from(machines)
|
|
1140
|
+
.where(eq(machines.id, plan.infrastructure.machineId))
|
|
1141
|
+
.get();
|
|
1142
|
+
if (machine?.ipAddress !== LOCAL_MACHINE_IP) {
|
|
1143
|
+
machineId = plan.infrastructure.machineId;
|
|
1144
|
+
await writeTemporarySshKey(machineId);
|
|
1145
|
+
}
|
|
1134
1146
|
}
|
|
1135
1147
|
|
|
1136
1148
|
try {
|
|
@@ -10,6 +10,7 @@ import { closeDb } from '../db/client';
|
|
|
10
10
|
import { runMigrations } from '../db/migrate';
|
|
11
11
|
import { addMachine } from './machine-pool';
|
|
12
12
|
import {
|
|
13
|
+
LOCAL_MACHINE_IP,
|
|
13
14
|
ManagedSshKey,
|
|
14
15
|
cleanupTemporarySshKeys,
|
|
15
16
|
deleteTemporarySshKey,
|
|
@@ -326,3 +327,16 @@ describe('ssh-key-manager', () => {
|
|
|
326
327
|
});
|
|
327
328
|
});
|
|
328
329
|
});
|
|
330
|
+
|
|
331
|
+
// The management box is reached over Ansible's local connection, never by
|
|
332
|
+
// `ssh root@127.0.0.1`, so callers must skip key materialization for it. Three
|
|
333
|
+
// call sites decide this — inventory.ts, aspect-runner.ts and module-deploy.ts —
|
|
334
|
+
// and module-deploy.ts did NOT, so `celilo module deploy celilo-mgmt` threw
|
|
335
|
+
// "has no SSH key stored" before Ansible ever ran and the control plane could
|
|
336
|
+
// not self-deploy. The literal now lives in one place so a fourth caller finds
|
|
337
|
+
// it instead of retyping it.
|
|
338
|
+
describe('LOCAL_MACHINE_IP', () => {
|
|
339
|
+
it('is the loopback address the management box registers itself under', () => {
|
|
340
|
+
expect(LOCAL_MACHINE_IP).toBe('127.0.0.1');
|
|
341
|
+
});
|
|
342
|
+
});
|
|
@@ -17,6 +17,18 @@ function getTempKeysDir(): string {
|
|
|
17
17
|
return join(tmpdir(), 'celilo-ansible-keys');
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* The address the management box registers itself under in its own machine pool.
|
|
22
|
+
*
|
|
23
|
+
* celilo-mgr deploys to ITSELF, so it appears in the pool as a machine — but it
|
|
24
|
+
* has no stored SSH key and must be reached over Ansible's local connection
|
|
25
|
+
* rather than `ssh root@127.0.0.1`. Three call sites need to know this
|
|
26
|
+
* (`inventory.ts`, `aspect-runner.ts`, `module-deploy.ts`); one of them silently
|
|
27
|
+
* didn't, and celilo-mgmt could not complete a self-deploy as a result. Exported
|
|
28
|
+
* so the next caller finds it instead of retyping the literal.
|
|
29
|
+
*/
|
|
30
|
+
export const LOCAL_MACHINE_IP = '127.0.0.1';
|
|
31
|
+
|
|
20
32
|
/**
|
|
21
33
|
* Ensure the temp keys directory exists
|
|
22
34
|
*/
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
2
4
|
import type { SystemConfigProperty, SystemConfigSchema } from './system-config-schema-types';
|
|
3
5
|
import { validateKey, validateValue } from './system-config-validator';
|
|
4
6
|
|
|
@@ -158,3 +160,26 @@ describe('validateKey', () => {
|
|
|
158
160
|
expect(result.error).toContain('network.bridge');
|
|
159
161
|
});
|
|
160
162
|
});
|
|
163
|
+
|
|
164
|
+
// The shipped schema is what `celilo system config set` validates against, and it
|
|
165
|
+
// is a hand-maintained enumeration of zones. `secure-mgmt` was added to
|
|
166
|
+
// NETWORK_ZONES, IPAM, zone detection and the manifest schema but NOT here, so the
|
|
167
|
+
// zone existed everywhere except the one place its subnet could be RECORDED — and
|
|
168
|
+
// recording it is what derives firewall trust and the resolver's split-horizon
|
|
169
|
+
// view. It failed only on the live box, at `system config set`.
|
|
170
|
+
describe('shipped system_config.json covers every placement zone', () => {
|
|
171
|
+
const shipped = JSON.parse(
|
|
172
|
+
readFileSync(join(import.meta.dir, '../../schemas/system_config.json'), 'utf-8'),
|
|
173
|
+
) as SystemConfigSchema;
|
|
174
|
+
|
|
175
|
+
// `external` is deliberately absent: cloud/VPS systems carry their own
|
|
176
|
+
// addressing and celilo allocates nothing for them.
|
|
177
|
+
const ADDRESSABLE_ZONES = ['internal', 'dmz', 'app', 'secure', 'secure-mgmt'] as const;
|
|
178
|
+
|
|
179
|
+
for (const zone of ADDRESSABLE_ZONES) {
|
|
180
|
+
it(`accepts network.${zone}.subnet and .gateway`, () => {
|
|
181
|
+
expect(validateKey(`network.${zone}.subnet`, shipped).valid).toBe(true);
|
|
182
|
+
expect(validateKey(`network.${zone}.gateway`, shipped).valid).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
});
|