@celilo/cli 0.3.16 → 0.3.18
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/src/api-clients/proxmox.ts +77 -45
- package/src/cli/command-registry.ts +23 -35
- package/src/cli/commands/completion.ts +12 -11
- package/src/cli/commands/module-check.ts +158 -0
- package/src/cli/commands/module-import-routing.test.ts +52 -0
- package/src/cli/commands/module-import.ts +70 -27
- package/src/cli/commands/module-publish.test.ts +3 -90
- package/src/cli/commands/module-publish.ts +14 -118
- package/src/cli/commands/proxmox-template-selection.test.ts +150 -0
- package/src/cli/commands/proxmox-template-selection.ts +258 -0
- package/src/cli/commands/service-add-proxmox.ts +49 -127
- package/src/cli/commands/service-reconfigure.ts +36 -79
- package/src/cli/commands/service-verify.ts +20 -79
- package/src/cli/commands/{doctor.test.ts → system-doctor.test.ts} +1 -1
- package/src/cli/commands/{doctor.ts → system-doctor.ts} +93 -18
- package/src/cli/commands/system-update.ts +1 -1
- package/src/cli/completion.ts +29 -8
- package/src/cli/index.ts +25 -30
- package/src/manifest/schema.ts +9 -1
- package/src/module/import.ts +4 -2
- package/src/registry/client.ts +14 -1
- package/src/services/bus-interview.ts +13 -1
- package/src/services/bus-secret-flow.test.ts +94 -0
- package/src/services/config-interview.ts +66 -6
- package/src/services/module-deploy.ts +19 -1
- package/src/services/module-validator/capability-versions.test.ts +90 -0
- package/src/services/module-validator/capability-versions.ts +115 -0
- package/src/services/module-validator/contract-version.test.ts +24 -0
- package/src/services/module-validator/contract-version.ts +69 -0
- package/src/services/module-validator/git-hygiene.test.ts +141 -0
- package/src/services/module-validator/git-hygiene.ts +144 -0
- package/src/services/module-validator/index.test.ts +67 -0
- package/src/services/module-validator/index.ts +74 -0
- package/src/services/module-validator/manifest-schema.ts +42 -0
- package/src/services/module-validator/types.ts +43 -0
- package/src/services/module-validator/typescript-build.test.ts +58 -0
- package/src/services/module-validator/typescript-build.ts +115 -0
- package/src/services/module-validator/workspace-deps.test.ts +137 -0
- package/src/services/module-validator/workspace-deps.ts +187 -0
- package/src/services/terminal-responder.ts +75 -0
- package/src/system/prereqs.test.ts +374 -0
- package/src/system/prereqs.ts +377 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proxmox template selection — shared between `service add proxmox` and
|
|
3
|
+
* `service reconfigure`. Queries Proxmox's live appliance catalog
|
|
4
|
+
* (`pveam available`) and prompts the user to pick an Ubuntu LXC template by
|
|
5
|
+
* canonical name.
|
|
6
|
+
*
|
|
7
|
+
* Why this exists: Proxmox bumps template revisions over time
|
|
8
|
+
* (ubuntu-24.04 went from -1 to -2, 25.04 ships as -1.1) and an older home-lab
|
|
9
|
+
* deployment guide naming `-1` produces 404s on the mirror. Using the catalog
|
|
10
|
+
* means the canonical filename always matches what `pveam download` accepts.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as p from '@clack/prompts';
|
|
14
|
+
import {
|
|
15
|
+
type ProxmoxAppliance,
|
|
16
|
+
type ProxmoxCredentials,
|
|
17
|
+
checkTaskStatus,
|
|
18
|
+
downloadAppliance,
|
|
19
|
+
listAvailableAppliances,
|
|
20
|
+
} from '../../api-clients/proxmox';
|
|
21
|
+
import { FuelGauge } from '../fuel-gauge';
|
|
22
|
+
import { promptText } from '../prompts';
|
|
23
|
+
import { validateRequired } from '../validators';
|
|
24
|
+
|
|
25
|
+
export interface UbuntuTemplateChoice {
|
|
26
|
+
/** Canonical filename, e.g. "ubuntu-24.04-standard_24.04-2_amd64.tar.zst" */
|
|
27
|
+
template: string;
|
|
28
|
+
/**
|
|
29
|
+
* 'catalog' = filename came from Proxmox's aplinfo, so it's known good for
|
|
30
|
+
* downloadAppliance. 'manual' = user typed it because aplinfo was unavailable;
|
|
31
|
+
* caller should expect they'll need to `pveam download` themselves.
|
|
32
|
+
*/
|
|
33
|
+
source: 'catalog' | 'manual';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type TemplateSelectionResult =
|
|
37
|
+
| { kind: 'selected'; choice: UbuntuTemplateChoice }
|
|
38
|
+
| { kind: 'cancelled' }
|
|
39
|
+
| { kind: 'error'; message: string };
|
|
40
|
+
|
|
41
|
+
interface ParsedUbuntu {
|
|
42
|
+
appliance: ProxmoxAppliance;
|
|
43
|
+
major: number;
|
|
44
|
+
minor: number;
|
|
45
|
+
isLts: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseUbuntuPackage(appliance: ProxmoxAppliance): ParsedUbuntu | null {
|
|
49
|
+
const match = appliance.package.match(/^ubuntu-(\d+)\.(\d+)-standard$/);
|
|
50
|
+
if (!match) return null;
|
|
51
|
+
const major = Number(match[1]);
|
|
52
|
+
const minor = Number(match[2]);
|
|
53
|
+
// Ubuntu LTS = even major, .04 minor (20.04, 22.04, 24.04, 26.04, ...).
|
|
54
|
+
const isLts = minor === 4 && major % 2 === 0;
|
|
55
|
+
return { appliance, major, minor, isLts };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function compareDescending(a: ParsedUbuntu, b: ParsedUbuntu): number {
|
|
59
|
+
if (a.major !== b.major) return b.major - a.major;
|
|
60
|
+
return b.minor - a.minor;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Build the option list used by the select prompt and tests.
|
|
65
|
+
* Exported so tests can assert on filtering/sorting without mocking @clack.
|
|
66
|
+
*/
|
|
67
|
+
export function buildUbuntuOptions(appliances: ProxmoxAppliance[]): ParsedUbuntu[] {
|
|
68
|
+
return appliances
|
|
69
|
+
.map(parseUbuntuPackage)
|
|
70
|
+
.filter((entry): entry is ParsedUbuntu => entry !== null)
|
|
71
|
+
.filter((entry) => (entry.appliance.section ?? 'system') === 'system')
|
|
72
|
+
.sort(compareDescending);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Pre-select rule:
|
|
77
|
+
* 1. Exact match on currentTemplate (reconfigure with current revision still on mirror).
|
|
78
|
+
* 2. Family match on currentTemplate package (e.g. user has 24.04 -1 cached, mirror has -2).
|
|
79
|
+
* 3. Newest LTS in the catalog.
|
|
80
|
+
* 4. Newest entry overall.
|
|
81
|
+
*/
|
|
82
|
+
export function pickInitialTemplate(
|
|
83
|
+
options: ParsedUbuntu[],
|
|
84
|
+
currentTemplate?: string,
|
|
85
|
+
): string | undefined {
|
|
86
|
+
if (options.length === 0) return undefined;
|
|
87
|
+
|
|
88
|
+
if (currentTemplate) {
|
|
89
|
+
const exact = options.find((e) => e.appliance.template === currentTemplate);
|
|
90
|
+
if (exact) return exact.appliance.template;
|
|
91
|
+
|
|
92
|
+
const familyName = currentTemplate.match(/^(ubuntu-\d+\.\d+-standard)/)?.[1];
|
|
93
|
+
if (familyName) {
|
|
94
|
+
const family = options.find((e) => e.appliance.package === familyName);
|
|
95
|
+
if (family) return family.appliance.template;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const newestLts = options.find((e) => e.isLts);
|
|
100
|
+
return (newestLts ?? options[0]).appliance.template;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function selectUbuntuApplianceFromCatalog(
|
|
104
|
+
credentials: ProxmoxCredentials,
|
|
105
|
+
nodeName: string,
|
|
106
|
+
opts: { currentTemplate?: string } = {},
|
|
107
|
+
): Promise<TemplateSelectionResult> {
|
|
108
|
+
console.log('\nFetching Proxmox template catalog...');
|
|
109
|
+
const result = await listAvailableAppliances(credentials, nodeName);
|
|
110
|
+
|
|
111
|
+
if (!result.success) {
|
|
112
|
+
console.log(`⚠ Could not fetch template catalog from Proxmox: ${result.message}`);
|
|
113
|
+
console.log(' You can still configure the service by entering a template filename manually.');
|
|
114
|
+
console.log(
|
|
115
|
+
' After saving, run `pveam download <storage> <template>` on the Proxmox host before deploying.',
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const manual = await promptText({
|
|
119
|
+
message: 'Template filename:',
|
|
120
|
+
placeholder: 'ubuntu-24.04-standard_24.04-2_amd64.tar.zst',
|
|
121
|
+
validate: validateRequired('Template filename'),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (typeof manual !== 'string' || manual.trim() === '') {
|
|
125
|
+
return { kind: 'cancelled' };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { kind: 'selected', choice: { template: manual.trim(), source: 'manual' } };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const options = buildUbuntuOptions(result.data);
|
|
132
|
+
|
|
133
|
+
if (options.length === 0) {
|
|
134
|
+
return {
|
|
135
|
+
kind: 'error',
|
|
136
|
+
message:
|
|
137
|
+
'Proxmox aplinfo returned no Ubuntu standard templates. Run `pveam update` on the Proxmox host and re-try.',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const initialValue = pickInitialTemplate(options, opts.currentTemplate);
|
|
142
|
+
|
|
143
|
+
const selection = await p.select<string>({
|
|
144
|
+
message: 'Default Ubuntu template:',
|
|
145
|
+
options: options.map((e) => ({
|
|
146
|
+
value: e.appliance.template,
|
|
147
|
+
label: `Ubuntu ${e.major}.${String(e.minor).padStart(2, '0')}${e.isLts ? ' LTS' : ''}`,
|
|
148
|
+
hint: e.isLts
|
|
149
|
+
? `revision ${e.appliance.version}`
|
|
150
|
+
: `revision ${e.appliance.version} · non-LTS`,
|
|
151
|
+
})),
|
|
152
|
+
initialValue,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (p.isCancel(selection)) {
|
|
156
|
+
return { kind: 'cancelled' };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { kind: 'selected', choice: { template: selection, source: 'catalog' } };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface ApplianceDownloadOutcome {
|
|
163
|
+
/** True iff the template ended up available in the chosen storage. */
|
|
164
|
+
ready: boolean;
|
|
165
|
+
/**
|
|
166
|
+
* One of:
|
|
167
|
+
* 'started-failed' — POST to aplinfo failed (Proxmox rejected the request).
|
|
168
|
+
* 'task-failed' — Download started but pveam exited non-OK (typically exit 8 = HTTP error).
|
|
169
|
+
* 'timeout' — Polling exhausted the budget without seeing 'stopped'.
|
|
170
|
+
* 'ok' — pveam exited 0 and template should now exist in storage.
|
|
171
|
+
*/
|
|
172
|
+
reason: 'started-failed' | 'task-failed' | 'timeout' | 'ok';
|
|
173
|
+
/** The pveam exit status when reason='task-failed', or null otherwise. */
|
|
174
|
+
exitStatus?: string;
|
|
175
|
+
/** The error message from Proxmox when reason='started-failed'. */
|
|
176
|
+
startError?: string;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface RunApplianceDownloadInput {
|
|
180
|
+
credentials: ProxmoxCredentials;
|
|
181
|
+
targetNode: string;
|
|
182
|
+
templateStorage: string;
|
|
183
|
+
templateFilename: string;
|
|
184
|
+
/** Polling budget in 5s ticks; defaults to 60 (= 5 minutes). */
|
|
185
|
+
maxAttempts?: number;
|
|
186
|
+
/** Override the default 5000ms tick — primarily to keep tests fast. */
|
|
187
|
+
pollIntervalMs?: number;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Trigger a `pveam download` on the Proxmox host and poll until it finishes.
|
|
192
|
+
* Drives a FuelGauge for user-visible progress. Distinguishes real failures
|
|
193
|
+
* (Proxmox rejected the start, or pveam exited non-OK) from timeout, so the
|
|
194
|
+
* caller can render an accurate next-steps message instead of always blaming
|
|
195
|
+
* "5-minute timeout" for any non-success path (the bug that motivated this
|
|
196
|
+
* refactor).
|
|
197
|
+
*/
|
|
198
|
+
export async function runApplianceDownload(
|
|
199
|
+
input: RunApplianceDownloadInput,
|
|
200
|
+
): Promise<ApplianceDownloadOutcome> {
|
|
201
|
+
const {
|
|
202
|
+
credentials,
|
|
203
|
+
targetNode,
|
|
204
|
+
templateStorage,
|
|
205
|
+
templateFilename,
|
|
206
|
+
maxAttempts = 60,
|
|
207
|
+
pollIntervalMs = 5000,
|
|
208
|
+
} = input;
|
|
209
|
+
|
|
210
|
+
const startResult = await downloadAppliance(
|
|
211
|
+
credentials,
|
|
212
|
+
targetNode,
|
|
213
|
+
templateStorage,
|
|
214
|
+
templateFilename,
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
if (!startResult.success) {
|
|
218
|
+
console.log(`\n✗ Failed to start download: ${startResult.message}`);
|
|
219
|
+
return { ready: false, reason: 'started-failed', startError: startResult.message };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const upid = startResult.data;
|
|
223
|
+
const gauge = new FuelGauge(`Downloading ${templateFilename}`);
|
|
224
|
+
gauge.start();
|
|
225
|
+
|
|
226
|
+
let attempts = 0;
|
|
227
|
+
while (attempts < maxAttempts) {
|
|
228
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
229
|
+
attempts++;
|
|
230
|
+
|
|
231
|
+
const statusResult = await checkTaskStatus(credentials, targetNode, upid);
|
|
232
|
+
if (!statusResult.success) {
|
|
233
|
+
gauge.addOutput(`Waiting for status... (${attempts * 5}s elapsed)`);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (statusResult.data.status !== 'stopped') {
|
|
238
|
+
gauge.addOutput(`Status: ${statusResult.data.status} (${attempts * 5}s elapsed)`);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const exit = statusResult.data.exitstatus;
|
|
243
|
+
if (exit === 'OK') {
|
|
244
|
+
gauge.stop(true);
|
|
245
|
+
return { ready: true, reason: 'ok' };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
gauge.stop(false);
|
|
249
|
+
console.log(`\n✗ pveam download failed: ${exit ?? 'unknown exit status'}`);
|
|
250
|
+
return { ready: false, reason: 'task-failed', exitStatus: exit ?? undefined };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
gauge.stop(false);
|
|
254
|
+
console.log(
|
|
255
|
+
`\n✗ Template download did not complete within ${(maxAttempts * pollIntervalMs) / 1000}s`,
|
|
256
|
+
);
|
|
257
|
+
return { ready: false, reason: 'timeout' };
|
|
258
|
+
}
|
|
@@ -7,10 +7,6 @@ import * as p from '@clack/prompts';
|
|
|
7
7
|
import {
|
|
8
8
|
buildProxmoxApiUrl,
|
|
9
9
|
buildTemplatePath,
|
|
10
|
-
buildTemplateUrl,
|
|
11
|
-
checkTaskStatus,
|
|
12
|
-
downloadTemplate,
|
|
13
|
-
extractTemplateFilename,
|
|
14
10
|
listAvailableTemplates,
|
|
15
11
|
listNodeStorage,
|
|
16
12
|
} from '../../api-clients/proxmox';
|
|
@@ -20,10 +16,13 @@ import {
|
|
|
20
16
|
testConnection as testServiceConnection,
|
|
21
17
|
updateVerificationStatus,
|
|
22
18
|
} from '../../services/container-service';
|
|
23
|
-
import { FuelGauge } from '../fuel-gauge';
|
|
24
19
|
import { celiloIntro, celiloOutro, promptPassword, promptText } from '../prompts';
|
|
25
20
|
import type { CommandResult } from '../types';
|
|
26
21
|
import { validateRequired } from '../validators';
|
|
22
|
+
import {
|
|
23
|
+
runApplianceDownload,
|
|
24
|
+
selectUbuntuApplianceFromCatalog,
|
|
25
|
+
} from './proxmox-template-selection';
|
|
27
26
|
|
|
28
27
|
/**
|
|
29
28
|
* Handle service add proxmox command
|
|
@@ -123,35 +122,19 @@ export async function handleServiceAddProxmox(
|
|
|
123
122
|
validate: validateRequired('Storage'),
|
|
124
123
|
});
|
|
125
124
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
if (p.isCancel(ubuntuVersion)) {
|
|
137
|
-
p.cancel('Operation cancelled');
|
|
138
|
-
return { success: false, error: 'Cancelled by user' };
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Find storage that supports vztmpl content
|
|
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
|
+
};
|
|
142
133
|
console.log('\nFinding storage for templates...');
|
|
143
|
-
const storageListResult = await listNodeStorage(
|
|
144
|
-
{
|
|
145
|
-
api_url: apiUrl,
|
|
146
|
-
api_token_id: apiTokenId,
|
|
147
|
-
api_token_secret: apiTokenSecret,
|
|
148
|
-
},
|
|
149
|
-
targetNode,
|
|
150
|
-
);
|
|
134
|
+
const storageListResult = await listNodeStorage(credentials, targetNode);
|
|
151
135
|
|
|
152
|
-
let templateStorage = 'local';
|
|
136
|
+
let templateStorage = 'local';
|
|
153
137
|
if (storageListResult.success) {
|
|
154
|
-
// Find first active storage that supports 'vztmpl' content
|
|
155
138
|
const vztmplStorage = storageListResult.data.find(
|
|
156
139
|
(s) => s.active && s.enabled && s.content.includes('vztmpl'),
|
|
157
140
|
);
|
|
@@ -163,37 +146,34 @@ export async function handleServiceAddProxmox(
|
|
|
163
146
|
}
|
|
164
147
|
}
|
|
165
148
|
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
const
|
|
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);
|
|
169
161
|
|
|
170
|
-
// Check if template
|
|
162
|
+
// Check if the template is already cached in the chosen storage.
|
|
171
163
|
console.log(`\nChecking if template '${templateFilename}' exists...`);
|
|
172
|
-
const templatesResult = await listAvailableTemplates(
|
|
173
|
-
{
|
|
174
|
-
api_url: apiUrl,
|
|
175
|
-
api_token_id: apiTokenId,
|
|
176
|
-
api_token_secret: apiTokenSecret,
|
|
177
|
-
},
|
|
178
|
-
targetNode,
|
|
179
|
-
templateStorage,
|
|
180
|
-
);
|
|
164
|
+
const templatesResult = await listAvailableTemplates(credentials, targetNode, templateStorage);
|
|
181
165
|
|
|
182
166
|
let templateExists = false;
|
|
183
167
|
if (templatesResult.success) {
|
|
184
168
|
templateExists = templatesResult.data.some((t) => t.volid.includes(templateFilename));
|
|
185
169
|
}
|
|
186
170
|
|
|
187
|
-
// Save the service FIRST so credentials aren't lost if
|
|
171
|
+
// Save the service FIRST so credentials aren't lost if the download fails.
|
|
188
172
|
const service = await addContainerService({
|
|
189
173
|
name,
|
|
190
174
|
providerName: 'proxmox',
|
|
191
175
|
zones: zones as unknown as NetworkZone[],
|
|
192
|
-
apiCredentials:
|
|
193
|
-
api_url: apiUrl,
|
|
194
|
-
api_token_id: apiTokenId,
|
|
195
|
-
api_token_secret: apiTokenSecret,
|
|
196
|
-
},
|
|
176
|
+
apiCredentials: credentials,
|
|
197
177
|
providerConfig: {
|
|
198
178
|
default_target_node: targetNode,
|
|
199
179
|
lxc_template: lxcTemplate,
|
|
@@ -203,7 +183,6 @@ export async function handleServiceAddProxmox(
|
|
|
203
183
|
|
|
204
184
|
console.log(`✓ Service '${service.serviceId}' saved\n`);
|
|
205
185
|
|
|
206
|
-
// Now attempt template download (service is already saved)
|
|
207
186
|
let templateReady = templateExists;
|
|
208
187
|
|
|
209
188
|
if (!templateExists) {
|
|
@@ -220,89 +199,32 @@ export async function handleServiceAddProxmox(
|
|
|
220
199
|
}
|
|
221
200
|
|
|
222
201
|
if (shouldDownload) {
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
const downloadResult = await downloadTemplate(
|
|
226
|
-
{
|
|
227
|
-
api_url: apiUrl,
|
|
228
|
-
api_token_id: apiTokenId,
|
|
229
|
-
api_token_secret: apiTokenSecret,
|
|
230
|
-
},
|
|
202
|
+
const downloadOutcome = await runApplianceDownload({
|
|
203
|
+
credentials,
|
|
231
204
|
targetNode,
|
|
232
205
|
templateStorage,
|
|
233
|
-
|
|
234
|
-
);
|
|
235
|
-
|
|
236
|
-
if (!downloadResult.success) {
|
|
237
|
-
console.log(`\n✗ Failed to start download: ${downloadResult.message}`);
|
|
238
|
-
} else {
|
|
239
|
-
// Wait for download to complete with fuel-gauge progress
|
|
240
|
-
const upid = downloadResult.data;
|
|
241
|
-
const gauge = new FuelGauge(`Downloading ${templateFilename}`);
|
|
242
|
-
gauge.start();
|
|
243
|
-
|
|
244
|
-
let downloadComplete = false;
|
|
245
|
-
let attempts = 0;
|
|
246
|
-
const maxAttempts = 60;
|
|
247
|
-
|
|
248
|
-
while (!downloadComplete && attempts < maxAttempts) {
|
|
249
|
-
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
250
|
-
|
|
251
|
-
const statusResult = await checkTaskStatus(
|
|
252
|
-
{
|
|
253
|
-
api_url: apiUrl,
|
|
254
|
-
api_token_id: apiTokenId,
|
|
255
|
-
api_token_secret: apiTokenSecret,
|
|
256
|
-
},
|
|
257
|
-
targetNode,
|
|
258
|
-
upid,
|
|
259
|
-
);
|
|
260
|
-
|
|
261
|
-
if (statusResult.success) {
|
|
262
|
-
if (statusResult.data.status === 'stopped') {
|
|
263
|
-
if (statusResult.data.exitstatus === 'OK') {
|
|
264
|
-
downloadComplete = true;
|
|
265
|
-
templateReady = true;
|
|
266
|
-
gauge.stop(true);
|
|
267
|
-
} else {
|
|
268
|
-
gauge.stop(false);
|
|
269
|
-
console.log(`\n✗ Template download failed: ${statusResult.data.exitstatus}`);
|
|
270
|
-
break;
|
|
271
|
-
}
|
|
272
|
-
} else {
|
|
273
|
-
gauge.addOutput(`Status: ${statusResult.data.status} (${attempts * 5}s elapsed)`);
|
|
274
|
-
}
|
|
275
|
-
} else {
|
|
276
|
-
gauge.addOutput(`Waiting for status... (${attempts * 5}s elapsed)`);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
attempts++;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (!downloadComplete && !templateReady) {
|
|
283
|
-
gauge.stop(false);
|
|
284
|
-
console.log('\n✗ Template download timed out after 5 minutes');
|
|
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
|
-
}
|
|
206
|
+
templateFilename,
|
|
207
|
+
});
|
|
208
|
+
templateReady = downloadOutcome.ready;
|
|
301
209
|
} else {
|
|
302
210
|
console.log(
|
|
303
211
|
'\nTemplate not downloaded. Service saved but will need a template before deployment.',
|
|
304
212
|
);
|
|
305
213
|
}
|
|
214
|
+
|
|
215
|
+
if (!templateReady) {
|
|
216
|
+
console.log(
|
|
217
|
+
'\nTemplate is not available. The service has been saved but needs a template.\n',
|
|
218
|
+
);
|
|
219
|
+
console.log('Next steps:');
|
|
220
|
+
console.log(
|
|
221
|
+
` 1. Try a different version: celilo service reconfigure ${service.serviceId}`,
|
|
222
|
+
);
|
|
223
|
+
console.log(
|
|
224
|
+
` 2. Download manually: ssh root@${ipAddress} pveam download ${templateStorage} ${templateFilename}`,
|
|
225
|
+
);
|
|
226
|
+
console.log(` 3. Then verify: celilo service verify ${service.serviceId}`);
|
|
227
|
+
}
|
|
306
228
|
} else {
|
|
307
229
|
console.log(`✓ Template '${templateFilename}' found`);
|
|
308
230
|
}
|
|
@@ -5,29 +5,24 @@
|
|
|
5
5
|
|
|
6
6
|
import * as p from '@clack/prompts';
|
|
7
7
|
import {
|
|
8
|
+
type ProxmoxCredentials,
|
|
8
9
|
buildTemplatePath,
|
|
9
|
-
buildTemplateUrl,
|
|
10
|
-
checkTaskStatus,
|
|
11
|
-
downloadTemplate,
|
|
12
10
|
extractTemplateFilename,
|
|
13
11
|
listAvailableTemplates,
|
|
14
12
|
listNodeStorage,
|
|
15
13
|
} from '../../api-clients/proxmox';
|
|
16
14
|
import {
|
|
17
|
-
|
|
15
|
+
getContainerServiceByServiceId,
|
|
18
16
|
getServiceCredentials,
|
|
19
17
|
updateServiceProviderConfig,
|
|
20
18
|
} from '../../services/container-service';
|
|
21
|
-
import { FuelGauge } from '../fuel-gauge';
|
|
22
19
|
import { celiloIntro, celiloOutro, promptText } from '../prompts';
|
|
23
20
|
import type { CommandResult } from '../types';
|
|
24
21
|
import { validateRequired } from '../validators';
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
api_token_secret: string;
|
|
30
|
-
}
|
|
22
|
+
import {
|
|
23
|
+
runApplianceDownload,
|
|
24
|
+
selectUbuntuApplianceFromCatalog,
|
|
25
|
+
} from './proxmox-template-selection';
|
|
31
26
|
|
|
32
27
|
interface ProxmoxProviderConfig {
|
|
33
28
|
default_target_node: string;
|
|
@@ -48,7 +43,7 @@ export async function handleServiceReconfigure(
|
|
|
48
43
|
}
|
|
49
44
|
|
|
50
45
|
try {
|
|
51
|
-
const service = await
|
|
46
|
+
const service = await getContainerServiceByServiceId(serviceId);
|
|
52
47
|
if (!service) {
|
|
53
48
|
return { success: false, error: `Service '${serviceId}' not found` };
|
|
54
49
|
}
|
|
@@ -87,27 +82,8 @@ export async function handleServiceReconfigure(
|
|
|
87
82
|
validate: validateRequired('Storage'),
|
|
88
83
|
});
|
|
89
84
|
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
const versionMatch = currentTemplateFile.match(/ubuntu-(\d+\.\d+)-/);
|
|
93
|
-
const currentVersion = versionMatch?.[1] || '22.04';
|
|
94
|
-
|
|
95
|
-
const ubuntuVersion = await p.select({
|
|
96
|
-
message: 'Ubuntu LTS version for default template:',
|
|
97
|
-
options: [
|
|
98
|
-
{ value: '24.04', label: 'Ubuntu 24.04 LTS (Noble Numbat)' },
|
|
99
|
-
{ value: '22.04', label: 'Ubuntu 22.04 LTS (Jammy Jellyfish)' },
|
|
100
|
-
{ value: '20.04', label: 'Ubuntu 20.04 LTS (Focal Fossa)' },
|
|
101
|
-
],
|
|
102
|
-
initialValue: currentVersion,
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
if (p.isCancel(ubuntuVersion)) {
|
|
106
|
-
p.cancel('Operation cancelled');
|
|
107
|
-
return { success: false, error: 'Cancelled by user' };
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Find template storage
|
|
85
|
+
// Find template storage. Done before catalog selection so the user only
|
|
86
|
+
// ever sees the chosen storage in subsequent messages.
|
|
111
87
|
console.log('\nFinding storage for templates...');
|
|
112
88
|
const storageListResult = await listNodeStorage(credentials, targetNode);
|
|
113
89
|
|
|
@@ -122,10 +98,22 @@ export async function handleServiceReconfigure(
|
|
|
122
98
|
}
|
|
123
99
|
}
|
|
124
100
|
|
|
125
|
-
|
|
126
|
-
|
|
101
|
+
// Pick a template from Proxmox's live catalog. Pre-selects the family
|
|
102
|
+
// matching the existing volid (e.g. user has 24.04 -1 cached, mirror has -2).
|
|
103
|
+
const selectionResult = await selectUbuntuApplianceFromCatalog(credentials, targetNode, {
|
|
104
|
+
currentTemplate: extractTemplateFilename(currentConfig.lxc_template),
|
|
105
|
+
});
|
|
106
|
+
if (selectionResult.kind === 'cancelled') {
|
|
107
|
+
p.cancel('Operation cancelled');
|
|
108
|
+
return { success: false, error: 'Cancelled by user' };
|
|
109
|
+
}
|
|
110
|
+
if (selectionResult.kind === 'error') {
|
|
111
|
+
return { success: false, error: selectionResult.message };
|
|
112
|
+
}
|
|
113
|
+
const templateFilename = selectionResult.choice.template;
|
|
114
|
+
const lxcTemplate = buildTemplatePath(templateStorage, templateFilename);
|
|
127
115
|
|
|
128
|
-
// Check if new template exists
|
|
116
|
+
// Check if new template exists in storage.
|
|
129
117
|
console.log(`\nChecking if template '${templateFilename}' exists...`);
|
|
130
118
|
const templatesResult = await listAvailableTemplates(credentials, targetNode, templateStorage);
|
|
131
119
|
|
|
@@ -147,54 +135,23 @@ export async function handleServiceReconfigure(
|
|
|
147
135
|
'\nTemplate not downloaded. Service will be updated but may fail verification.',
|
|
148
136
|
);
|
|
149
137
|
} else {
|
|
150
|
-
const
|
|
151
|
-
const downloadResult = await downloadTemplate(
|
|
138
|
+
const outcome = await runApplianceDownload({
|
|
152
139
|
credentials,
|
|
153
140
|
targetNode,
|
|
154
141
|
templateStorage,
|
|
155
|
-
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
if (!
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const upid = downloadResult.data;
|
|
166
|
-
const gauge = new FuelGauge(`Downloading ${templateFilename}`);
|
|
167
|
-
gauge.start();
|
|
168
|
-
|
|
169
|
-
let downloadComplete = false;
|
|
170
|
-
let attempts = 0;
|
|
171
|
-
|
|
172
|
-
while (!downloadComplete && attempts < 60) {
|
|
173
|
-
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
174
|
-
const statusResult = await checkTaskStatus(credentials, targetNode, upid);
|
|
175
|
-
|
|
176
|
-
if (statusResult.success && statusResult.data.status === 'stopped') {
|
|
177
|
-
if (statusResult.data.exitstatus === 'OK') {
|
|
178
|
-
downloadComplete = true;
|
|
179
|
-
gauge.stop(true);
|
|
180
|
-
} else {
|
|
181
|
-
gauge.stop(false);
|
|
182
|
-
return {
|
|
183
|
-
success: false,
|
|
184
|
-
error: `Template download failed: ${statusResult.data.exitstatus}\n\nThis usually means the Proxmox host cannot reach download.proxmox.com.\n\nTroubleshooting:\n 1. Try a different Ubuntu version: celilo service reconfigure ${serviceId}\n 2. Download manually: pveam download ${templateStorage} ${templateFilename}\n 3. Check DNS and firewall settings on the Proxmox host`,
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
} else {
|
|
188
|
-
gauge.addOutput(`Status: downloading... (${attempts * 5}s elapsed)`);
|
|
189
|
-
}
|
|
190
|
-
attempts++;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (!downloadComplete) {
|
|
194
|
-
gauge.stop(false);
|
|
142
|
+
templateFilename,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (!outcome.ready) {
|
|
146
|
+
const detail =
|
|
147
|
+
outcome.reason === 'task-failed'
|
|
148
|
+
? `pveam download exited with status: ${outcome.exitStatus ?? 'unknown'}`
|
|
149
|
+
: outcome.reason === 'started-failed'
|
|
150
|
+
? `Proxmox rejected the download request: ${outcome.startError ?? 'unknown error'}`
|
|
151
|
+
: 'Template download did not complete in time';
|
|
195
152
|
return {
|
|
196
153
|
success: false,
|
|
197
|
-
error:
|
|
154
|
+
error: `${detail}\n\nTroubleshooting:\n 1. SSH into your Proxmox host and run: pveam update && pveam download ${templateStorage} ${templateFilename}\n 2. Re-try: celilo service reconfigure ${serviceId}\n 3. Check DNS and firewall settings on the Proxmox host`,
|
|
198
155
|
};
|
|
199
156
|
}
|
|
200
157
|
}
|