@celilo/cli 0.5.0-alpha.1 → 0.5.0-alpha.11
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/0009_dns_registrations.sql +13 -0
- package/drizzle/0010_dns_internal_records.sql +12 -0
- package/drizzle/0011_backups_name.sql +1 -0
- package/drizzle/meta/_journal.json +22 -1
- package/package.json +3 -3
- package/src/ansible/inventory.test.ts +10 -10
- package/src/ansible/validation.test.ts +25 -15
- package/src/api-clients/proxmox.test.ts +211 -1
- package/src/api-clients/proxmox.ts +399 -8
- package/src/cli/command-registry.ts +83 -6
- package/src/cli/commands/backup-delete.ts +10 -7
- package/src/cli/commands/backup-import.ts +11 -8
- package/src/cli/commands/backup-restore.ts +11 -8
- package/src/cli/commands/dns.ts +57 -0
- package/src/cli/commands/events.test.ts +4 -4
- package/src/cli/commands/events.ts +89 -24
- package/src/cli/commands/machine-add.ts +178 -163
- package/src/cli/commands/machine-remove.ts +10 -7
- package/src/cli/commands/module-config.test.ts +78 -0
- package/src/cli/commands/module-config.ts +18 -3
- package/src/cli/commands/module-import.ts +9 -5
- package/src/cli/commands/module-publish.ts +24 -0
- package/src/cli/commands/module-remove.ts +20 -9
- package/src/cli/commands/module-status.ts +15 -0
- package/src/cli/commands/module-upgrade.test.ts +37 -0
- package/src/cli/commands/module-upgrade.ts +26 -6
- package/src/cli/commands/proxmox-node-list.ts +101 -0
- package/src/cli/commands/proxmox-template-selection.ts +16 -15
- package/src/cli/commands/proxmox-vm-template-build.ts +171 -0
- package/src/cli/commands/publish/alpha.test.ts +26 -0
- package/src/cli/commands/publish/alpha.ts +23 -0
- package/src/cli/commands/publish/types.ts +7 -2
- package/src/cli/commands/publish/workspace.ts +11 -1
- package/src/cli/commands/restore.ts +29 -0
- package/src/cli/commands/service-add-digitalocean.ts +120 -109
- package/src/cli/commands/service-add-proxmox.ts +283 -209
- package/src/cli/commands/service-reconfigure.test.ts +115 -0
- package/src/cli/commands/service-reconfigure.ts +252 -129
- package/src/cli/commands/service-remove.ts +19 -13
- package/src/cli/commands/service-verify.ts +9 -10
- package/src/cli/commands/storage-add-local.ts +120 -107
- package/src/cli/commands/storage-add-s3.ts +145 -131
- package/src/cli/commands/storage-remove.ts +11 -8
- package/src/cli/commands/system-doctor.ts +135 -40
- package/src/cli/commands/system-init.ts +119 -128
- package/src/cli/commands/system-migrate.test.ts +40 -0
- package/src/cli/commands/system-migrate.ts +65 -0
- package/src/cli/completion.ts +23 -0
- package/src/cli/index.ts +91 -7
- package/src/cli/service-credential.ts +54 -0
- package/src/config/paths.test.ts +61 -48
- package/src/db/client.ts +15 -146
- package/src/db/migrate.ts +14 -6
- package/src/db/schema-introspection.ts +88 -0
- package/src/db/schema.ts +74 -0
- package/src/hooks/capability-loader-firewall.test.ts +3 -3
- package/src/hooks/capability-loader.ts +43 -2
- package/src/hooks/run-named-hook.ts +28 -2
- package/src/hooks/types.ts +2 -1
- package/src/infrastructure/property-extractor.test.ts +15 -0
- package/src/infrastructure/property-extractor.ts +12 -0
- package/src/manifest/contracts/v1.ts +16 -0
- package/src/manifest/schema.ts +17 -0
- package/src/manifest/validate.test.ts +53 -0
- package/src/services/bus-interview.test.ts +2 -2
- package/src/services/bus-interview.ts +232 -0
- package/src/services/bus-secret-flow.test.ts +2 -2
- package/src/services/celilo-mgmt-hooks.test.ts +3 -2
- package/src/services/deploy-preflight.ts +25 -0
- package/src/services/deploy-validation.test.ts +54 -4
- package/src/services/deploy-validation.ts +27 -36
- package/src/services/dns-internal-records.test.ts +126 -0
- package/src/services/dns-internal-records.ts +119 -0
- package/src/services/dns-provider-backfill.test.ts +2 -2
- package/src/services/dns-provider-backfill.ts +14 -2
- package/src/services/dns-registrations.test.ts +120 -0
- package/src/services/dns-registrations.ts +108 -0
- package/src/services/events-daemon.test.ts +59 -0
- package/src/services/events-daemon.ts +191 -57
- package/src/services/fleet-checks.test.ts +508 -0
- package/src/services/fleet-checks.ts +678 -0
- package/src/services/module-build.test.ts +43 -38
- package/src/services/module-config.ts +12 -0
- package/src/services/module-deploy.ts +7 -6
- package/src/services/module-subscriptions.test.ts +88 -0
- package/src/services/module-subscriptions.ts +50 -1
- package/src/services/module-validator/bundled-deps.test.ts +55 -0
- package/src/services/module-validator/bundled-deps.ts +115 -0
- package/src/services/module-validator/capability-versions.test.ts +1 -1
- package/src/services/placement-reconcile.test.ts +86 -0
- package/src/services/placement-reconcile.ts +108 -0
- package/src/services/programmatic-responder.ts +34 -0
- package/src/services/terminal-responder.ts +113 -0
- package/src/templates/generator.test.ts +92 -12
- package/src/templates/generator.ts +165 -80
- package/src/test-utils/fixtures.test.ts +1 -1
- package/src/test-utils/integration-guard.ts +33 -0
- package/src/types/infrastructure.ts +6 -0
- package/src/variables/computed/computed-integration.test.ts +3 -3
- package/src/variables/computed/computed.test.ts +5 -5
- package/src/variables/declarative-derivation.test.ts +6 -6
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
* Configure a Proxmox container service
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import * as p from '@clack/prompts';
|
|
7
6
|
import {
|
|
8
7
|
buildProxmoxApiUrl,
|
|
9
8
|
buildTemplatePath,
|
|
@@ -11,18 +10,31 @@ import {
|
|
|
11
10
|
listNodeStorage,
|
|
12
11
|
} from '../../api-clients/proxmox';
|
|
13
12
|
import type { NetworkZone } from '../../db/schema';
|
|
13
|
+
import {
|
|
14
|
+
askConfirm,
|
|
15
|
+
askMultiselect,
|
|
16
|
+
askSelect,
|
|
17
|
+
askText,
|
|
18
|
+
withInterviewSession,
|
|
19
|
+
} from '../../services/bus-interview';
|
|
14
20
|
import {
|
|
15
21
|
addContainerService,
|
|
16
22
|
testConnection as testServiceConnection,
|
|
17
23
|
updateVerificationStatus,
|
|
18
24
|
} from '../../services/container-service';
|
|
19
|
-
import { celiloIntro, celiloOutro
|
|
25
|
+
import { celiloIntro, celiloOutro } from '../prompts';
|
|
26
|
+
import { resolveServiceCredential } from '../service-credential';
|
|
20
27
|
import type { CommandResult } from '../types';
|
|
21
|
-
import { validateRequired } from '../validators';
|
|
22
28
|
import {
|
|
23
29
|
runApplianceDownload,
|
|
24
30
|
selectUbuntuApplianceFromCatalog,
|
|
25
31
|
} from './proxmox-template-selection';
|
|
32
|
+
import { buildCloudInitTemplate, detectExistingVmTemplates } from './proxmox-vm-template-build';
|
|
33
|
+
|
|
34
|
+
// Sentinel VM-template choices returned by the select. Any other value is an
|
|
35
|
+
// existing template name.
|
|
36
|
+
const VM_BUILD = '__build__';
|
|
37
|
+
const VM_SKIP = '__skip__';
|
|
26
38
|
|
|
27
39
|
/**
|
|
28
40
|
* Handle service add proxmox command
|
|
@@ -32,233 +44,295 @@ import {
|
|
|
32
44
|
*/
|
|
33
45
|
export async function handleServiceAddProxmox(
|
|
34
46
|
_args: string[],
|
|
35
|
-
|
|
47
|
+
flags: Record<string, boolean | string> = {},
|
|
36
48
|
): Promise<CommandResult> {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
49
|
+
// Non-secret prompts route through the bus interview (ISS-0127); the API
|
|
50
|
+
// token id + secret are service credentials that travel by flag/env only
|
|
51
|
+
// (D7). `withInterviewSession` renders bus questions locally on a TTY.
|
|
52
|
+
const scope = 'service-add:proxmox';
|
|
53
|
+
|
|
54
|
+
return withInterviewSession(async () => {
|
|
55
|
+
try {
|
|
56
|
+
celiloIntro('Add Proxmox Container Service');
|
|
57
|
+
|
|
58
|
+
const name = await askText({
|
|
59
|
+
scope,
|
|
60
|
+
key: 'name',
|
|
61
|
+
message: 'Human-readable name',
|
|
62
|
+
defaultValue: 'Proxmox Home Lab',
|
|
63
|
+
placeholder: 'Proxmox Home Lab',
|
|
64
|
+
required: true,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const zones = await askMultiselect({
|
|
68
|
+
scope,
|
|
69
|
+
key: 'zones',
|
|
70
|
+
message: 'Zones this service can provision to',
|
|
71
|
+
options: [
|
|
72
|
+
{ value: 'internal', label: 'internal' },
|
|
73
|
+
{ value: 'dmz', label: 'dmz' },
|
|
74
|
+
{ value: 'app', label: 'app' },
|
|
75
|
+
{ value: 'secure', label: 'secure' },
|
|
76
|
+
{ value: 'external', label: 'external' },
|
|
77
|
+
],
|
|
78
|
+
required: true,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
console.log('\nProxmox Configuration');
|
|
82
|
+
console.log('─────────────────────');
|
|
83
|
+
|
|
84
|
+
const ipAddress = await askText({
|
|
85
|
+
scope,
|
|
86
|
+
key: 'ip_address',
|
|
87
|
+
message: 'Proxmox IP address',
|
|
88
|
+
placeholder: 'e.g., 192.168.1.100 or proxmox.local',
|
|
89
|
+
required: true,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const port = await askText({
|
|
93
|
+
scope,
|
|
94
|
+
key: 'port',
|
|
95
|
+
message: 'Proxmox API port',
|
|
96
|
+
defaultValue: '8006',
|
|
97
|
+
placeholder: '8006',
|
|
98
|
+
required: true,
|
|
99
|
+
type: 'integer',
|
|
100
|
+
pattern: '^[0-9]+$',
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Build the full API URL
|
|
104
|
+
const apiUrl = buildProxmoxApiUrl(ipAddress, Number(port));
|
|
105
|
+
|
|
106
|
+
// API token ID + secret are service credentials — flag/env only (D7).
|
|
107
|
+
const apiTokenId = await resolveServiceCredential({
|
|
108
|
+
field: 'API token ID',
|
|
109
|
+
flag: 'api-token-id',
|
|
110
|
+
envVar: 'PROXMOX_API_TOKEN_ID',
|
|
111
|
+
flagValue: flags['api-token-id'],
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const apiTokenSecret = await resolveServiceCredential({
|
|
115
|
+
field: 'API token secret',
|
|
116
|
+
flag: 'api-token-secret',
|
|
117
|
+
envVar: 'PROXMOX_API_TOKEN_SECRET',
|
|
118
|
+
flagValue: flags['api-token-secret'],
|
|
119
|
+
});
|
|
66
120
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
121
|
+
const targetNode = await askText({
|
|
122
|
+
scope,
|
|
123
|
+
key: 'default_target_node',
|
|
124
|
+
message: 'Default target node',
|
|
125
|
+
defaultValue: 'pve',
|
|
126
|
+
placeholder: 'pve',
|
|
127
|
+
required: true,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const storage = await askText({
|
|
131
|
+
scope,
|
|
132
|
+
key: 'storage',
|
|
133
|
+
message: 'Default storage',
|
|
134
|
+
defaultValue: 'local-lvm',
|
|
135
|
+
placeholder: 'local-lvm',
|
|
136
|
+
required: true,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Build credentials here — needed for VM template detection below and
|
|
140
|
+
// for LXC template catalog lookup further down.
|
|
141
|
+
const credentials = {
|
|
142
|
+
api_url: apiUrl,
|
|
143
|
+
api_token_id: apiTokenId,
|
|
144
|
+
api_token_secret: apiTokenSecret,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// VM template for `requires.system.type: vm` modules.
|
|
148
|
+
// Auto-detect existing templates on the node; offer to build if none found.
|
|
149
|
+
// See v2/PROXMOX_VM_TEMPLATE.md.
|
|
150
|
+
let vmTemplate: string | undefined;
|
|
151
|
+
const existingTemplates = await detectExistingVmTemplates(credentials, targetNode);
|
|
152
|
+
if (existingTemplates.length > 0) {
|
|
153
|
+
const templateChoice = await askSelect({
|
|
154
|
+
scope,
|
|
155
|
+
key: 'vm_template',
|
|
156
|
+
message: 'VM template for VM-type modules',
|
|
157
|
+
options: [
|
|
158
|
+
...existingTemplates.map((t) => ({
|
|
159
|
+
value: t.name,
|
|
160
|
+
label: `${t.name} (VMID ${t.vmid})`,
|
|
161
|
+
})),
|
|
162
|
+
{ value: VM_BUILD, label: 'Build a new Ubuntu 24.04 cloud-init template now' },
|
|
163
|
+
{ value: VM_SKIP, label: 'Skip — I only need LXC modules' },
|
|
164
|
+
],
|
|
165
|
+
});
|
|
166
|
+
if (templateChoice === VM_BUILD) {
|
|
167
|
+
vmTemplate = await buildCloudInitTemplate({
|
|
168
|
+
credentials,
|
|
169
|
+
nodeName: targetNode,
|
|
170
|
+
diskStorage: storage,
|
|
171
|
+
});
|
|
172
|
+
} else if (templateChoice !== VM_SKIP) {
|
|
173
|
+
vmTemplate = templateChoice;
|
|
91
174
|
}
|
|
92
|
-
return undefined;
|
|
93
|
-
},
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
// Build the full API URL
|
|
97
|
-
const apiUrl = buildProxmoxApiUrl(ipAddress, Number(port));
|
|
98
|
-
|
|
99
|
-
const apiTokenId = await promptText({
|
|
100
|
-
message: 'API Token ID:',
|
|
101
|
-
defaultValue: 'root@pam!celilo',
|
|
102
|
-
placeholder: 'root@pam!celilo',
|
|
103
|
-
validate: validateRequired('API token ID'),
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
const apiTokenSecret = await promptPassword({
|
|
107
|
-
message: 'API Token Secret:',
|
|
108
|
-
validate: validateRequired('API token secret'),
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
const targetNode = await promptText({
|
|
112
|
-
message: 'Default target node:',
|
|
113
|
-
defaultValue: 'pve',
|
|
114
|
-
placeholder: 'pve',
|
|
115
|
-
validate: validateRequired('Target node'),
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
const storage = await promptText({
|
|
119
|
-
message: 'Default storage:',
|
|
120
|
-
defaultValue: 'local-lvm',
|
|
121
|
-
placeholder: 'local-lvm',
|
|
122
|
-
validate: validateRequired('Storage'),
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
// Find storage that supports vztmpl content. We do this BEFORE prompting
|
|
126
|
-
// for a template so the user only ever sees one storage in subsequent
|
|
127
|
-
// messages, and so the saved volid uses the right storage.
|
|
128
|
-
const credentials = {
|
|
129
|
-
api_url: apiUrl,
|
|
130
|
-
api_token_id: apiTokenId,
|
|
131
|
-
api_token_secret: apiTokenSecret,
|
|
132
|
-
};
|
|
133
|
-
console.log('\nFinding storage for templates...');
|
|
134
|
-
const storageListResult = await listNodeStorage(credentials, targetNode);
|
|
135
|
-
|
|
136
|
-
let templateStorage = 'local';
|
|
137
|
-
if (storageListResult.success) {
|
|
138
|
-
const vztmplStorage = storageListResult.data.find(
|
|
139
|
-
(s) => s.active && s.enabled && s.content.includes('vztmpl'),
|
|
140
|
-
);
|
|
141
|
-
if (vztmplStorage) {
|
|
142
|
-
templateStorage = vztmplStorage.storage;
|
|
143
|
-
console.log(`✓ Using storage '${templateStorage}' for templates`);
|
|
144
175
|
} else {
|
|
145
|
-
|
|
176
|
+
const shouldBuild = await askConfirm({
|
|
177
|
+
scope,
|
|
178
|
+
key: 'vm_template_build',
|
|
179
|
+
message:
|
|
180
|
+
'No VM templates found. Build a Ubuntu 24.04 cloud-init template now? (~2–10 min)',
|
|
181
|
+
defaultValue: false,
|
|
182
|
+
});
|
|
183
|
+
if (shouldBuild) {
|
|
184
|
+
vmTemplate = await buildCloudInitTemplate({
|
|
185
|
+
credentials,
|
|
186
|
+
nodeName: targetNode,
|
|
187
|
+
diskStorage: storage,
|
|
188
|
+
});
|
|
189
|
+
} else {
|
|
190
|
+
const manualEntry = await askText({
|
|
191
|
+
scope,
|
|
192
|
+
key: 'vm_template_manual',
|
|
193
|
+
message: 'VM template name (optional — leave blank to skip)',
|
|
194
|
+
placeholder: 'e.g., ubuntu-2404-cloudinit',
|
|
195
|
+
});
|
|
196
|
+
vmTemplate = manualEntry.trim() || undefined;
|
|
197
|
+
}
|
|
146
198
|
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Pick a template from Proxmox's live catalog (replaces the old hardcoded
|
|
150
|
-
// version select that built `-1`-revision URLs by hand).
|
|
151
|
-
const selectionResult = await selectUbuntuApplianceFromCatalog(credentials, targetNode);
|
|
152
|
-
if (selectionResult.kind === 'cancelled') {
|
|
153
|
-
p.cancel('Operation cancelled');
|
|
154
|
-
return { success: false, error: 'Cancelled by user' };
|
|
155
|
-
}
|
|
156
|
-
if (selectionResult.kind === 'error') {
|
|
157
|
-
return { success: false, error: selectionResult.message };
|
|
158
|
-
}
|
|
159
|
-
const templateFilename = selectionResult.choice.template;
|
|
160
|
-
const lxcTemplate = buildTemplatePath(templateStorage, templateFilename);
|
|
161
199
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
200
|
+
// Find storage that supports vztmpl content. We do this BEFORE prompting
|
|
201
|
+
// for a template so the user only ever sees one storage in subsequent
|
|
202
|
+
// messages, and so the saved volid uses the right storage.
|
|
203
|
+
console.log('\nFinding storage for templates...');
|
|
204
|
+
const storageListResult = await listNodeStorage(credentials, targetNode);
|
|
165
205
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
206
|
+
let templateStorage = 'local';
|
|
207
|
+
if (storageListResult.success) {
|
|
208
|
+
const vztmplStorage = storageListResult.data.find(
|
|
209
|
+
(s) => s.active && s.enabled && s.content.includes('vztmpl'),
|
|
210
|
+
);
|
|
211
|
+
if (vztmplStorage) {
|
|
212
|
+
templateStorage = vztmplStorage.storage;
|
|
213
|
+
console.log(`✓ Using storage '${templateStorage}' for templates`);
|
|
214
|
+
} else {
|
|
215
|
+
console.log(`⚠ No storage found with 'vztmpl' support, using '${templateStorage}'`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
170
218
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
zones: zones as unknown as NetworkZone[],
|
|
176
|
-
apiCredentials: credentials,
|
|
177
|
-
providerConfig: {
|
|
178
|
-
default_target_node: targetNode,
|
|
179
|
-
lxc_template: lxcTemplate,
|
|
180
|
-
storage,
|
|
181
|
-
},
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
console.log(`✓ Service '${service.serviceId}' saved\n`);
|
|
185
|
-
|
|
186
|
-
let templateReady = templateExists;
|
|
187
|
-
|
|
188
|
-
if (!templateExists) {
|
|
189
|
-
console.log(`✗ Template '${templateFilename}' not found in storage '${templateStorage}'`);
|
|
190
|
-
|
|
191
|
-
const shouldDownload = await p.confirm({
|
|
192
|
-
message: 'Download template now?',
|
|
193
|
-
initialValue: true,
|
|
219
|
+
// Pick a template from Proxmox's live catalog (replaces the old hardcoded
|
|
220
|
+
// version select that built `-1`-revision URLs by hand).
|
|
221
|
+
const selectionResult = await selectUbuntuApplianceFromCatalog(credentials, targetNode, {
|
|
222
|
+
scope,
|
|
194
223
|
});
|
|
195
|
-
|
|
196
|
-
if (p.isCancel(shouldDownload)) {
|
|
197
|
-
p.cancel('Operation cancelled');
|
|
224
|
+
if (selectionResult.kind === 'cancelled') {
|
|
198
225
|
return { success: false, error: 'Cancelled by user' };
|
|
199
226
|
}
|
|
227
|
+
if (selectionResult.kind === 'error') {
|
|
228
|
+
return { success: false, error: selectionResult.message };
|
|
229
|
+
}
|
|
230
|
+
const templateFilename = selectionResult.choice.template;
|
|
231
|
+
const lxcTemplate = buildTemplatePath(templateStorage, templateFilename);
|
|
232
|
+
|
|
233
|
+
// Check if the template is already cached in the chosen storage.
|
|
234
|
+
console.log(`\nChecking if template '${templateFilename}' exists...`);
|
|
235
|
+
const templatesResult = await listAvailableTemplates(
|
|
236
|
+
credentials,
|
|
237
|
+
targetNode,
|
|
238
|
+
templateStorage,
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
let templateExists = false;
|
|
242
|
+
if (templatesResult.success) {
|
|
243
|
+
templateExists = templatesResult.data.some((t) => t.volid.includes(templateFilename));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Save the service FIRST so credentials aren't lost if the download fails.
|
|
247
|
+
const service = await addContainerService({
|
|
248
|
+
name,
|
|
249
|
+
providerName: 'proxmox',
|
|
250
|
+
zones: zones as unknown as NetworkZone[],
|
|
251
|
+
apiCredentials: credentials,
|
|
252
|
+
providerConfig: {
|
|
253
|
+
default_target_node: targetNode,
|
|
254
|
+
lxc_template: lxcTemplate,
|
|
255
|
+
storage,
|
|
256
|
+
...(vmTemplate ? { vm_template: vmTemplate } : {}),
|
|
257
|
+
},
|
|
258
|
+
});
|
|
200
259
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
260
|
+
console.log(`✓ Service '${service.serviceId}' saved\n`);
|
|
261
|
+
|
|
262
|
+
let templateReady = templateExists;
|
|
263
|
+
|
|
264
|
+
if (!templateExists) {
|
|
265
|
+
console.log(`✗ Template '${templateFilename}' not found in storage '${templateStorage}'`);
|
|
266
|
+
|
|
267
|
+
const shouldDownload = await askConfirm({
|
|
268
|
+
scope,
|
|
269
|
+
key: 'download_template',
|
|
270
|
+
message: 'Download template now?',
|
|
271
|
+
defaultValue: true,
|
|
207
272
|
});
|
|
208
|
-
|
|
273
|
+
|
|
274
|
+
if (shouldDownload) {
|
|
275
|
+
const downloadOutcome = await runApplianceDownload({
|
|
276
|
+
credentials,
|
|
277
|
+
targetNode,
|
|
278
|
+
templateStorage,
|
|
279
|
+
templateFilename,
|
|
280
|
+
});
|
|
281
|
+
templateReady = downloadOutcome.ready;
|
|
282
|
+
} else {
|
|
283
|
+
console.log(
|
|
284
|
+
'\nTemplate not downloaded. Service saved but will need a template before deployment.',
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (!templateReady) {
|
|
289
|
+
console.log(
|
|
290
|
+
'\nTemplate is not available. The service has been saved but needs a template.\n',
|
|
291
|
+
);
|
|
292
|
+
console.log('Next steps:');
|
|
293
|
+
console.log(
|
|
294
|
+
` 1. Try a different version: celilo service reconfigure ${service.serviceId}`,
|
|
295
|
+
);
|
|
296
|
+
console.log(
|
|
297
|
+
` 2. Download manually: ssh root@${ipAddress} pveam download ${templateStorage} ${templateFilename}`,
|
|
298
|
+
);
|
|
299
|
+
console.log(` 3. Then verify: celilo service verify ${service.serviceId}`);
|
|
300
|
+
}
|
|
209
301
|
} else {
|
|
210
|
-
console.log(
|
|
211
|
-
'\nTemplate not downloaded. Service saved but will need a template before deployment.',
|
|
212
|
-
);
|
|
302
|
+
console.log(`✓ Template '${templateFilename}' found`);
|
|
213
303
|
}
|
|
214
304
|
|
|
215
|
-
|
|
305
|
+
// Test connection
|
|
306
|
+
console.log('\nTesting connection...');
|
|
307
|
+
const testResult = await testServiceConnection(service);
|
|
308
|
+
await updateVerificationStatus(service.id, testResult);
|
|
309
|
+
|
|
310
|
+
if (!testResult.success) {
|
|
311
|
+
console.log(`✗ Connection test failed: ${testResult.message}`);
|
|
216
312
|
console.log(
|
|
217
|
-
'\
|
|
313
|
+
'\nService saved but not verified. The service will not be used until verification succeeds.',
|
|
218
314
|
);
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
`
|
|
315
|
+
|
|
316
|
+
celiloOutro(
|
|
317
|
+
`Service '${service.serviceId}' (${name}) added but not verified.\n\nNext steps:\n - Fix the connection issue\n - Re-verify: celilo service verify ${service.serviceId}\n - Check status: celilo service list`,
|
|
222
318
|
);
|
|
223
|
-
|
|
224
|
-
|
|
319
|
+
} else {
|
|
320
|
+
console.log(`✓ ${testResult.message}`);
|
|
321
|
+
|
|
322
|
+
celiloOutro(
|
|
323
|
+
`Service '${service.serviceId}' (${name}) added and verified successfully!\n\nService ID: ${service.serviceId}\n\nNext steps:\n - Generate module: celilo module generate <module-id>\n - List services: celilo service list`,
|
|
225
324
|
);
|
|
226
|
-
console.log(` 3. Then verify: celilo service verify ${service.serviceId}`);
|
|
227
325
|
}
|
|
228
|
-
} else {
|
|
229
|
-
console.log(`✓ Template '${templateFilename}' found`);
|
|
230
|
-
}
|
|
231
326
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
celiloOutro(
|
|
244
|
-
`Service '${service.serviceId}' (${name}) added but not verified.\n\nNext steps:\n - Fix the connection issue\n - Re-verify: celilo service verify ${service.serviceId}\n - Check status: celilo service list`,
|
|
245
|
-
);
|
|
246
|
-
} else {
|
|
247
|
-
console.log(`✓ ${testResult.message}`);
|
|
248
|
-
|
|
249
|
-
celiloOutro(
|
|
250
|
-
`Service '${service.serviceId}' (${name}) added and verified successfully!\n\nService ID: ${service.serviceId}\n\nNext steps:\n - Generate module: celilo module generate <module-id>\n - List services: celilo service list`,
|
|
251
|
-
);
|
|
327
|
+
return {
|
|
328
|
+
success: true,
|
|
329
|
+
message: `Added Proxmox service: ${service.id}`,
|
|
330
|
+
};
|
|
331
|
+
} catch (error) {
|
|
332
|
+
return {
|
|
333
|
+
success: false,
|
|
334
|
+
error: `Failed to add Proxmox service: ${error instanceof Error ? error.message : String(error)}`,
|
|
335
|
+
};
|
|
252
336
|
}
|
|
253
|
-
|
|
254
|
-
return {
|
|
255
|
-
success: true,
|
|
256
|
-
message: `Added Proxmox service: ${service.id}`,
|
|
257
|
-
};
|
|
258
|
-
} catch (error) {
|
|
259
|
-
return {
|
|
260
|
-
success: false,
|
|
261
|
-
error: `Failed to add Proxmox service: ${error instanceof Error ? error.message : String(error)}`,
|
|
262
|
-
};
|
|
263
|
-
}
|
|
337
|
+
});
|
|
264
338
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
type ProxmoxProviderConfig,
|
|
4
|
+
buildVmTemplateChoices,
|
|
5
|
+
resolveReconfiguredProviderConfig,
|
|
6
|
+
} from './service-reconfigure';
|
|
7
|
+
|
|
8
|
+
describe('buildVmTemplateChoices — VM-template reconfigure menu', () => {
|
|
9
|
+
const templates = [
|
|
10
|
+
{ vmid: 9000, name: 'ubuntu-2404-cloudinit' },
|
|
11
|
+
{ vmid: 9001, name: 'ubuntu-2204-cloudinit' },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
test('current set + detected templates: keep first, others (current deduped), build, clear', () => {
|
|
15
|
+
const choices = buildVmTemplateChoices('ubuntu-2404-cloudinit', templates);
|
|
16
|
+
expect(choices.map((c) => c.value)).toEqual([
|
|
17
|
+
'__keep__',
|
|
18
|
+
'ubuntu-2204-cloudinit', // the other detected template — current is not re-listed
|
|
19
|
+
'__build__',
|
|
20
|
+
'__clear__',
|
|
21
|
+
]);
|
|
22
|
+
expect(choices[0].label).toBe('Keep current (ubuntu-2404-cloudinit)');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('current unset: no keep/clear; detected templates, build, then skip', () => {
|
|
26
|
+
const choices = buildVmTemplateChoices(undefined, templates);
|
|
27
|
+
expect(choices.map((c) => c.value)).toEqual([
|
|
28
|
+
'ubuntu-2404-cloudinit',
|
|
29
|
+
'ubuntu-2204-cloudinit',
|
|
30
|
+
'__build__',
|
|
31
|
+
'__skip__',
|
|
32
|
+
]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('current set, no templates detected: keep, build, clear', () => {
|
|
36
|
+
const choices = buildVmTemplateChoices('ubuntu-2404-cloudinit', []);
|
|
37
|
+
expect(choices.map((c) => c.value)).toEqual(['__keep__', '__build__', '__clear__']);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('current unset, no templates detected: build, skip', () => {
|
|
41
|
+
const choices = buildVmTemplateChoices(undefined, []);
|
|
42
|
+
expect(choices.map((c) => c.value)).toEqual(['__build__', '__skip__']);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('a detected template carries its VMID in the label', () => {
|
|
46
|
+
const choices = buildVmTemplateChoices(undefined, [{ vmid: 9002, name: 'debian-12' }]);
|
|
47
|
+
expect(choices[0]).toEqual({ value: 'debian-12', label: 'debian-12 (VMID 9002)' });
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('resolveReconfiguredProviderConfig — drop-bug guard (ISS-0128)', () => {
|
|
52
|
+
const current: ProxmoxProviderConfig = {
|
|
53
|
+
default_target_node: 'pve',
|
|
54
|
+
lxc_template: 'local:vztmpl/ubuntu-24.04.tar.zst',
|
|
55
|
+
storage: 'local-lvm',
|
|
56
|
+
vm_template: 'ubuntu-2404-cloudinit',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
test('keeping the VM template (edit echoes current) preserves it while applying other edits', () => {
|
|
60
|
+
const result = resolveReconfiguredProviderConfig({
|
|
61
|
+
default_target_node: 'pve2',
|
|
62
|
+
lxc_template: 'local:vztmpl/ubuntu-24.04-2.tar.zst',
|
|
63
|
+
storage: 'fast-lvm',
|
|
64
|
+
vm_template: 'ubuntu-2404-cloudinit', // VM_KEEP resolves to the current value
|
|
65
|
+
});
|
|
66
|
+
expect(result).toEqual({
|
|
67
|
+
default_target_node: 'pve2',
|
|
68
|
+
lxc_template: 'local:vztmpl/ubuntu-24.04-2.tar.zst',
|
|
69
|
+
storage: 'fast-lvm',
|
|
70
|
+
vm_template: 'ubuntu-2404-cloudinit',
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('the pre-fix behavior would have dropped vm_template — guard against regressing', () => {
|
|
75
|
+
// Reconfiguring only the LXC fields must NOT lose the VM template: the
|
|
76
|
+
// handler threads currentConfig.vm_template through as the edit value.
|
|
77
|
+
const result = resolveReconfiguredProviderConfig({
|
|
78
|
+
default_target_node: current.default_target_node,
|
|
79
|
+
lxc_template: 'local:vztmpl/new.tar.zst',
|
|
80
|
+
storage: current.storage,
|
|
81
|
+
vm_template: current.vm_template,
|
|
82
|
+
});
|
|
83
|
+
expect(result.vm_template).toBe('ubuntu-2404-cloudinit');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('clearing (undefined edit) removes the field entirely, not sets it undefined', () => {
|
|
87
|
+
const result = resolveReconfiguredProviderConfig({
|
|
88
|
+
default_target_node: 'pve',
|
|
89
|
+
lxc_template: current.lxc_template,
|
|
90
|
+
storage: current.storage,
|
|
91
|
+
vm_template: undefined,
|
|
92
|
+
});
|
|
93
|
+
expect('vm_template' in result).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('setting a new template overwrites the old', () => {
|
|
97
|
+
const result = resolveReconfiguredProviderConfig({
|
|
98
|
+
default_target_node: 'pve',
|
|
99
|
+
lxc_template: current.lxc_template,
|
|
100
|
+
storage: current.storage,
|
|
101
|
+
vm_template: 'debian-12-cloudinit',
|
|
102
|
+
});
|
|
103
|
+
expect(result.vm_template).toBe('debian-12-cloudinit');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('no VM template edit leaves it absent', () => {
|
|
107
|
+
const result = resolveReconfiguredProviderConfig({
|
|
108
|
+
default_target_node: 'pve',
|
|
109
|
+
lxc_template: 'local:vztmpl/x.tar.zst',
|
|
110
|
+
storage: 'local-lvm',
|
|
111
|
+
vm_template: undefined,
|
|
112
|
+
});
|
|
113
|
+
expect('vm_template' in result).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
});
|