@fredlackey/cli-proxmox 0.0.1
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/COMMANDS.md +453 -0
- package/LICENSE +13 -0
- package/README.md +64 -0
- package/package.json +43 -0
- package/src/bin/proxmox.js +4 -0
- package/src/commands/cluster.js +54 -0
- package/src/commands/configure.js +105 -0
- package/src/commands/node.js +81 -0
- package/src/commands/snapshot.js +157 -0
- package/src/commands/storage.js +95 -0
- package/src/commands/task.js +155 -0
- package/src/commands/vm.js +521 -0
- package/src/index.js +89 -0
- package/src/utils/config.js +132 -0
- package/src/utils/errors.js +43 -0
- package/src/utils/output.js +140 -0
- package/src/utils/proxmox-client.js +127 -0
- package/src/utils/readline.js +67 -0
- package/src/utils/runtime.js +39 -0
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `proxmox vm` subcommands: list, get, start, stop, shutdown, reboot,
|
|
3
|
+
* create, clone, delete, config, resize, status, suspend, resume.
|
|
4
|
+
*
|
|
5
|
+
* Every subcommand accepts the standard credential flags and --node, which
|
|
6
|
+
* falls back to the configured defaultNode. `list` targets a single node.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Command } from 'commander';
|
|
10
|
+
import { getRuntime } from '../utils/runtime.js';
|
|
11
|
+
import { createOutput } from '../utils/output.js';
|
|
12
|
+
import { resolveCredentials, resolveNode } from '../utils/config.js';
|
|
13
|
+
import {
|
|
14
|
+
createProxmoxClient,
|
|
15
|
+
withCredentialOptions,
|
|
16
|
+
withNodeOption,
|
|
17
|
+
} from '../utils/proxmox-client.js';
|
|
18
|
+
|
|
19
|
+
export function vmCommand() {
|
|
20
|
+
const cmd = new Command('vm');
|
|
21
|
+
cmd.description('Virtual machine operations (QEMU)');
|
|
22
|
+
|
|
23
|
+
// ── list ──────────────────────────────────────────────────────────
|
|
24
|
+
withNodeOption(
|
|
25
|
+
withCredentialOptions(
|
|
26
|
+
cmd.command('list').description('List all QEMU VMs on a node')
|
|
27
|
+
)
|
|
28
|
+
).action(async (opts) => {
|
|
29
|
+
const runtime = getRuntime();
|
|
30
|
+
const out = createOutput(runtime);
|
|
31
|
+
const creds = resolveCredentials(opts);
|
|
32
|
+
const node = resolveNode(opts, creds);
|
|
33
|
+
const client = createProxmoxClient(creds);
|
|
34
|
+
|
|
35
|
+
const data = await client.get(`nodes/${node}/qemu`);
|
|
36
|
+
const list = Array.isArray(data) ? data : [];
|
|
37
|
+
list.sort((a, b) => (a.vmid ?? 0) - (b.vmid ?? 0));
|
|
38
|
+
|
|
39
|
+
out.heading('Virtual machines');
|
|
40
|
+
if (runtime.interactive) {
|
|
41
|
+
if (!list.length) {
|
|
42
|
+
out.dim('(no VMs)');
|
|
43
|
+
} else {
|
|
44
|
+
for (const v of list) {
|
|
45
|
+
const vmid = String(v.vmid ?? '-').padEnd(6);
|
|
46
|
+
const status = (v.status || '').padEnd(8);
|
|
47
|
+
const name = v.name || '';
|
|
48
|
+
out.info(`${vmid} ${status} ${name}`);
|
|
49
|
+
}
|
|
50
|
+
out.dim(`${list.length} total on ${node}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
out.set('node', node);
|
|
54
|
+
out.set('vms', list);
|
|
55
|
+
out.set('total', list.length);
|
|
56
|
+
out.flush();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// ── get ───────────────────────────────────────────────────────────
|
|
60
|
+
withNodeOption(
|
|
61
|
+
withCredentialOptions(
|
|
62
|
+
cmd.command('get')
|
|
63
|
+
.description('Get the full configuration of a VM')
|
|
64
|
+
.requiredOption('--vmid <id>', 'VM ID')
|
|
65
|
+
)
|
|
66
|
+
).action(async (opts) => {
|
|
67
|
+
const runtime = getRuntime();
|
|
68
|
+
const out = createOutput(runtime);
|
|
69
|
+
const creds = resolveCredentials(opts);
|
|
70
|
+
const node = resolveNode(opts, creds);
|
|
71
|
+
const client = createProxmoxClient(creds);
|
|
72
|
+
|
|
73
|
+
const data = await client.get(`nodes/${node}/qemu/${opts.vmid}/config`);
|
|
74
|
+
|
|
75
|
+
out.heading('VM config');
|
|
76
|
+
if (runtime.interactive) {
|
|
77
|
+
out.info(`Node: ${node}`);
|
|
78
|
+
out.info(`VMID: ${opts.vmid}`);
|
|
79
|
+
if (data && typeof data === 'object') {
|
|
80
|
+
for (const [k, v] of Object.entries(data)) {
|
|
81
|
+
const display = typeof v === 'object' ? JSON.stringify(v) : v;
|
|
82
|
+
out.info(`${k.padEnd(12)} ${display}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
out.set('node', node);
|
|
87
|
+
out.set('vmid', opts.vmid);
|
|
88
|
+
out.set('config', data);
|
|
89
|
+
out.flush();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// ── start ─────────────────────────────────────────────────────────
|
|
93
|
+
withNodeOption(
|
|
94
|
+
withCredentialOptions(
|
|
95
|
+
cmd.command('start')
|
|
96
|
+
.description('Start a VM')
|
|
97
|
+
.requiredOption('--vmid <id>', 'VM ID')
|
|
98
|
+
)
|
|
99
|
+
).action(async (opts) => {
|
|
100
|
+
const runtime = getRuntime();
|
|
101
|
+
const out = createOutput(runtime);
|
|
102
|
+
const creds = resolveCredentials(opts);
|
|
103
|
+
const node = resolveNode(opts, creds);
|
|
104
|
+
const client = createProxmoxClient(creds);
|
|
105
|
+
|
|
106
|
+
const upid = await client.post(`nodes/${node}/qemu/${opts.vmid}/status/start`);
|
|
107
|
+
|
|
108
|
+
out.heading('VM start');
|
|
109
|
+
if (runtime.interactive) {
|
|
110
|
+
out.success(`Start requested for VM ${opts.vmid} on ${node}`);
|
|
111
|
+
if (upid) out.info(`UPID: ${upid}`);
|
|
112
|
+
}
|
|
113
|
+
out.set('node', node);
|
|
114
|
+
out.set('vmid', opts.vmid);
|
|
115
|
+
out.set('action', 'start');
|
|
116
|
+
out.set('upid', upid);
|
|
117
|
+
out.flush();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ── stop ──────────────────────────────────────────────────────────
|
|
121
|
+
withNodeOption(
|
|
122
|
+
withCredentialOptions(
|
|
123
|
+
cmd.command('stop')
|
|
124
|
+
.description('Force-stop a VM (power off, no graceful ACPI)')
|
|
125
|
+
.requiredOption('--vmid <id>', 'VM ID')
|
|
126
|
+
)
|
|
127
|
+
).action(async (opts) => {
|
|
128
|
+
const runtime = getRuntime();
|
|
129
|
+
const out = createOutput(runtime);
|
|
130
|
+
const creds = resolveCredentials(opts);
|
|
131
|
+
const node = resolveNode(opts, creds);
|
|
132
|
+
const client = createProxmoxClient(creds);
|
|
133
|
+
|
|
134
|
+
const upid = await client.post(`nodes/${node}/qemu/${opts.vmid}/status/stop`);
|
|
135
|
+
|
|
136
|
+
out.heading('VM stop');
|
|
137
|
+
if (runtime.interactive) {
|
|
138
|
+
out.success(`Stop (force) requested for VM ${opts.vmid} on ${node}`);
|
|
139
|
+
if (upid) out.info(`UPID: ${upid}`);
|
|
140
|
+
}
|
|
141
|
+
out.set('node', node);
|
|
142
|
+
out.set('vmid', opts.vmid);
|
|
143
|
+
out.set('action', 'stop');
|
|
144
|
+
out.set('upid', upid);
|
|
145
|
+
out.flush();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// ── shutdown ──────────────────────────────────────────────────────
|
|
149
|
+
withNodeOption(
|
|
150
|
+
withCredentialOptions(
|
|
151
|
+
cmd.command('shutdown')
|
|
152
|
+
.description('Gracefully shut down a VM via ACPI (needs guest agent)')
|
|
153
|
+
.requiredOption('--vmid <id>', 'VM ID')
|
|
154
|
+
.option('--timeout <seconds>', 'Seconds to wait before giving up')
|
|
155
|
+
.option('--force', 'Force-stop if graceful shutdown times out')
|
|
156
|
+
)
|
|
157
|
+
).action(async (opts) => {
|
|
158
|
+
const runtime = getRuntime();
|
|
159
|
+
const out = createOutput(runtime);
|
|
160
|
+
const creds = resolveCredentials(opts);
|
|
161
|
+
const node = resolveNode(opts, creds);
|
|
162
|
+
const client = createProxmoxClient(creds);
|
|
163
|
+
|
|
164
|
+
const body = {};
|
|
165
|
+
if (opts.timeout) body.timeout = parseInt(opts.timeout, 10);
|
|
166
|
+
if (opts.force) body.forceStop = 1;
|
|
167
|
+
|
|
168
|
+
const upid = await client.post(`nodes/${node}/qemu/${opts.vmid}/status/shutdown`, body);
|
|
169
|
+
|
|
170
|
+
out.heading('VM shutdown');
|
|
171
|
+
if (runtime.interactive) {
|
|
172
|
+
out.success(`Shutdown requested for VM ${opts.vmid} on ${node}`);
|
|
173
|
+
if (upid) out.info(`UPID: ${upid}`);
|
|
174
|
+
}
|
|
175
|
+
out.set('node', node);
|
|
176
|
+
out.set('vmid', opts.vmid);
|
|
177
|
+
out.set('action', 'shutdown');
|
|
178
|
+
out.set('upid', upid);
|
|
179
|
+
out.flush();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// ── reboot ────────────────────────────────────────────────────────
|
|
183
|
+
withNodeOption(
|
|
184
|
+
withCredentialOptions(
|
|
185
|
+
cmd.command('reboot')
|
|
186
|
+
.description('Reboot a VM')
|
|
187
|
+
.requiredOption('--vmid <id>', 'VM ID')
|
|
188
|
+
)
|
|
189
|
+
).action(async (opts) => {
|
|
190
|
+
const runtime = getRuntime();
|
|
191
|
+
const out = createOutput(runtime);
|
|
192
|
+
const creds = resolveCredentials(opts);
|
|
193
|
+
const node = resolveNode(opts, creds);
|
|
194
|
+
const client = createProxmoxClient(creds);
|
|
195
|
+
|
|
196
|
+
const upid = await client.post(`nodes/${node}/qemu/${opts.vmid}/status/reboot`);
|
|
197
|
+
|
|
198
|
+
out.heading('VM reboot');
|
|
199
|
+
if (runtime.interactive) {
|
|
200
|
+
out.success(`Reboot requested for VM ${opts.vmid} on ${node}`);
|
|
201
|
+
if (upid) out.info(`UPID: ${upid}`);
|
|
202
|
+
}
|
|
203
|
+
out.set('node', node);
|
|
204
|
+
out.set('vmid', opts.vmid);
|
|
205
|
+
out.set('action', 'reboot');
|
|
206
|
+
out.set('upid', upid);
|
|
207
|
+
out.flush();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// ── create ────────────────────────────────────────────────────────
|
|
211
|
+
withNodeOption(
|
|
212
|
+
withCredentialOptions(
|
|
213
|
+
cmd.command('create')
|
|
214
|
+
.description('Create a new QEMU VM')
|
|
215
|
+
.requiredOption('--vmid <id>', 'VM ID (unique integer)')
|
|
216
|
+
.option('--name <name>', 'VM name')
|
|
217
|
+
.option('--memory <mb>', 'Memory in MB (default: 2048)')
|
|
218
|
+
.option('--cores <n>', 'CPU cores (default: 1)')
|
|
219
|
+
.option('--sockets <n>', 'CPU sockets (default: 1)')
|
|
220
|
+
.option('--ostype <type>', 'OS type (default: l26 for Linux)')
|
|
221
|
+
.option('--scsi0 <spec>', 'SCSI disk (e.g., local-lvm:32)')
|
|
222
|
+
.option('--ide2 <spec>', 'CD-ROM/ISO (e.g., local:iso/ubuntu.iso,media=cdrom)')
|
|
223
|
+
.option('--net0 <spec>', 'Network (e.g., virtio,bridge=vmbr0)')
|
|
224
|
+
.option('--boot <order>', 'Boot order (e.g., order=scsi0;ide2;net0)')
|
|
225
|
+
.option('--agent <0|1>', 'QEMU guest agent (0 or 1)')
|
|
226
|
+
.option('--cpu <type>', 'CPU type (e.g., host, kvm64)')
|
|
227
|
+
.option('--scsihw <type>', 'SCSI controller (default: virtio-scsi-single)')
|
|
228
|
+
)
|
|
229
|
+
).action(async (opts) => {
|
|
230
|
+
const runtime = getRuntime();
|
|
231
|
+
const out = createOutput(runtime);
|
|
232
|
+
const creds = resolveCredentials(opts);
|
|
233
|
+
const node = resolveNode(opts, creds);
|
|
234
|
+
const client = createProxmoxClient(creds);
|
|
235
|
+
|
|
236
|
+
const body = { vmid: parseInt(opts.vmid, 10) };
|
|
237
|
+
if (opts.name) body.name = opts.name;
|
|
238
|
+
if (opts.memory) body.memory = parseInt(opts.memory, 10);
|
|
239
|
+
if (opts.cores) body.cores = parseInt(opts.cores, 10);
|
|
240
|
+
if (opts.sockets) body.sockets = parseInt(opts.sockets, 10);
|
|
241
|
+
if (opts.ostype) body.ostype = opts.ostype;
|
|
242
|
+
if (opts.scsi0) body.scsi0 = opts.scsi0;
|
|
243
|
+
if (opts.ide2) body.ide2 = opts.ide2;
|
|
244
|
+
if (opts.net0) body.net0 = opts.net0;
|
|
245
|
+
if (opts.boot) body.boot = opts.boot;
|
|
246
|
+
if (opts.agent) body.agent = opts.agent;
|
|
247
|
+
if (opts.cpu) body.cpu = opts.cpu;
|
|
248
|
+
if (opts.scsihw) body.scsihw = opts.scsihw;
|
|
249
|
+
|
|
250
|
+
const upid = await client.post(`nodes/${node}/qemu`, body);
|
|
251
|
+
|
|
252
|
+
out.heading('VM create');
|
|
253
|
+
if (runtime.interactive) {
|
|
254
|
+
out.success(`Create requested for VM ${opts.vmid} on ${node}`);
|
|
255
|
+
if (upid) out.info(`UPID: ${upid}`);
|
|
256
|
+
}
|
|
257
|
+
out.set('node', node);
|
|
258
|
+
out.set('vmid', parseInt(opts.vmid, 10));
|
|
259
|
+
out.set('action', 'create');
|
|
260
|
+
out.set('upid', upid);
|
|
261
|
+
out.flush();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// ── clone ────────────────────────────────────────────────────────
|
|
265
|
+
withNodeOption(
|
|
266
|
+
withCredentialOptions(
|
|
267
|
+
cmd.command('clone')
|
|
268
|
+
.description('Clone a VM or template to a new VM')
|
|
269
|
+
.requiredOption('--vmid <id>', 'Source VM ID to clone from')
|
|
270
|
+
.requiredOption('--newid <id>', 'New VM ID for the clone')
|
|
271
|
+
.option('--name <name>', 'Name for the cloned VM')
|
|
272
|
+
.option('--full', 'Full clone (not linked)')
|
|
273
|
+
.option('--target <node>', 'Target node (for cross-node clone)')
|
|
274
|
+
.option('--storage <name>', 'Target storage for full clone')
|
|
275
|
+
)
|
|
276
|
+
).action(async (opts) => {
|
|
277
|
+
const runtime = getRuntime();
|
|
278
|
+
const out = createOutput(runtime);
|
|
279
|
+
const creds = resolveCredentials(opts);
|
|
280
|
+
const node = resolveNode(opts, creds);
|
|
281
|
+
const client = createProxmoxClient(creds);
|
|
282
|
+
|
|
283
|
+
const body = { newid: parseInt(opts.newid, 10) };
|
|
284
|
+
if (opts.name) body.name = opts.name;
|
|
285
|
+
if (opts.full) body.full = 1;
|
|
286
|
+
if (opts.target) body.target = opts.target;
|
|
287
|
+
if (opts.storage) body.storage = opts.storage;
|
|
288
|
+
|
|
289
|
+
const upid = await client.post(`nodes/${node}/qemu/${opts.vmid}/clone`, body);
|
|
290
|
+
|
|
291
|
+
out.heading('VM clone');
|
|
292
|
+
if (runtime.interactive) {
|
|
293
|
+
out.success(`Clone of VM ${opts.vmid} to ${opts.newid} requested on ${node}`);
|
|
294
|
+
if (upid) out.info(`UPID: ${upid}`);
|
|
295
|
+
}
|
|
296
|
+
out.set('node', node);
|
|
297
|
+
out.set('sourceVmid', parseInt(opts.vmid, 10));
|
|
298
|
+
out.set('newid', parseInt(opts.newid, 10));
|
|
299
|
+
out.set('action', 'clone');
|
|
300
|
+
out.set('upid', upid);
|
|
301
|
+
out.flush();
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// ── delete ───────────────────────────────────────────────────────
|
|
305
|
+
withNodeOption(
|
|
306
|
+
withCredentialOptions(
|
|
307
|
+
cmd.command('delete')
|
|
308
|
+
.description('Delete a VM')
|
|
309
|
+
.requiredOption('--vmid <id>', 'VM ID to delete')
|
|
310
|
+
.option('--purge', 'Remove from backup/replication jobs too')
|
|
311
|
+
)
|
|
312
|
+
).action(async (opts) => {
|
|
313
|
+
const runtime = getRuntime();
|
|
314
|
+
const out = createOutput(runtime);
|
|
315
|
+
const creds = resolveCredentials(opts);
|
|
316
|
+
const node = resolveNode(opts, creds);
|
|
317
|
+
const client = createProxmoxClient(creds);
|
|
318
|
+
|
|
319
|
+
const params = opts.purge ? { purge: 1 } : undefined;
|
|
320
|
+
const upid = await client.delete(
|
|
321
|
+
`nodes/${node}/qemu/${opts.vmid}${params ? '?purge=1' : ''}`
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
out.heading('VM delete');
|
|
325
|
+
if (runtime.interactive) {
|
|
326
|
+
out.success(`Delete requested for VM ${opts.vmid} on ${node}`);
|
|
327
|
+
if (upid) out.info(`UPID: ${upid}`);
|
|
328
|
+
}
|
|
329
|
+
out.set('node', node);
|
|
330
|
+
out.set('vmid', parseInt(opts.vmid, 10));
|
|
331
|
+
out.set('action', 'delete');
|
|
332
|
+
out.set('upid', upid);
|
|
333
|
+
out.flush();
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// ── config (update) ──────────────────────────────────────────────
|
|
337
|
+
withNodeOption(
|
|
338
|
+
withCredentialOptions(
|
|
339
|
+
cmd.command('config')
|
|
340
|
+
.description('Update VM hardware configuration (CPU, memory, network, etc.)')
|
|
341
|
+
.requiredOption('--vmid <id>', 'VM ID')
|
|
342
|
+
.option('--memory <mb>', 'Memory in MB')
|
|
343
|
+
.option('--cores <n>', 'CPU cores')
|
|
344
|
+
.option('--sockets <n>', 'CPU sockets')
|
|
345
|
+
.option('--cpu <type>', 'CPU type (e.g., host, kvm64)')
|
|
346
|
+
.option('--name <name>', 'VM name')
|
|
347
|
+
.option('--ostype <type>', 'OS type')
|
|
348
|
+
.option('--net0 <spec>', 'Network interface 0')
|
|
349
|
+
.option('--net1 <spec>', 'Network interface 1')
|
|
350
|
+
.option('--scsi0 <spec>', 'SCSI disk 0')
|
|
351
|
+
.option('--scsi1 <spec>', 'SCSI disk 1')
|
|
352
|
+
.option('--ide2 <spec>', 'IDE device 2 (CD-ROM)')
|
|
353
|
+
.option('--boot <order>', 'Boot order')
|
|
354
|
+
.option('--agent <0|1>', 'QEMU guest agent (0 or 1)')
|
|
355
|
+
.option('--onboot <0|1>', 'Start on boot (0 or 1)')
|
|
356
|
+
.option('--balloon <mb>', 'Balloon memory minimum in MB')
|
|
357
|
+
.option('--scsihw <type>', 'SCSI controller type')
|
|
358
|
+
.option('--delete <keys>', 'Comma-separated list of settings to delete')
|
|
359
|
+
)
|
|
360
|
+
).action(async (opts) => {
|
|
361
|
+
const runtime = getRuntime();
|
|
362
|
+
const out = createOutput(runtime);
|
|
363
|
+
const creds = resolveCredentials(opts);
|
|
364
|
+
const node = resolveNode(opts, creds);
|
|
365
|
+
const client = createProxmoxClient(creds);
|
|
366
|
+
|
|
367
|
+
const body = {};
|
|
368
|
+
const skip = new Set([
|
|
369
|
+
'baseUrl', 'tokenId', 'tokenSecret', 'verifySsl',
|
|
370
|
+
'node', 'vmid', 'json', 'interactive',
|
|
371
|
+
]);
|
|
372
|
+
for (const [key, val] of Object.entries(opts)) {
|
|
373
|
+
if (skip.has(key)) continue;
|
|
374
|
+
if (val === undefined || val === true && key !== 'delete') continue;
|
|
375
|
+
if (['memory', 'cores', 'sockets', 'balloon'].includes(key)) {
|
|
376
|
+
body[key] = parseInt(val, 10);
|
|
377
|
+
} else {
|
|
378
|
+
body[key] = val;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (Object.keys(body).length === 0) {
|
|
383
|
+
const err = new Error(
|
|
384
|
+
'No configuration changes specified. Use flags like --memory, --cores, --name, etc.'
|
|
385
|
+
);
|
|
386
|
+
err.code = 'missing_required_value';
|
|
387
|
+
throw err;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
await client.put(`nodes/${node}/qemu/${opts.vmid}/config`, body);
|
|
391
|
+
|
|
392
|
+
out.heading('VM config');
|
|
393
|
+
if (runtime.interactive) {
|
|
394
|
+
out.success(`Config updated for VM ${opts.vmid} on ${node}`);
|
|
395
|
+
out.info(`Updated: ${Object.keys(body).join(', ')}`);
|
|
396
|
+
}
|
|
397
|
+
out.set('node', node);
|
|
398
|
+
out.set('vmid', parseInt(opts.vmid, 10));
|
|
399
|
+
out.set('updated', Object.keys(body));
|
|
400
|
+
out.flush();
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// ── resize ───────────────────────────────────────────────────────
|
|
404
|
+
withNodeOption(
|
|
405
|
+
withCredentialOptions(
|
|
406
|
+
cmd.command('resize')
|
|
407
|
+
.description('Resize a VM disk (grow only, works while VM is running)')
|
|
408
|
+
.requiredOption('--vmid <id>', 'VM ID')
|
|
409
|
+
.requiredOption('--disk <name>', 'Disk name (e.g., scsi0, virtio0)')
|
|
410
|
+
.requiredOption('--size <size>', 'New size or delta (e.g., +10G, 50G)')
|
|
411
|
+
)
|
|
412
|
+
).action(async (opts) => {
|
|
413
|
+
const runtime = getRuntime();
|
|
414
|
+
const out = createOutput(runtime);
|
|
415
|
+
const creds = resolveCredentials(opts);
|
|
416
|
+
const node = resolveNode(opts, creds);
|
|
417
|
+
const client = createProxmoxClient(creds);
|
|
418
|
+
|
|
419
|
+
const body = { disk: opts.disk, size: opts.size };
|
|
420
|
+
await client.put(`nodes/${node}/qemu/${opts.vmid}/resize`, body);
|
|
421
|
+
|
|
422
|
+
out.heading('VM resize');
|
|
423
|
+
if (runtime.interactive) {
|
|
424
|
+
out.success(`Resized disk ${opts.disk} on VM ${opts.vmid} (${opts.size})`);
|
|
425
|
+
}
|
|
426
|
+
out.set('node', node);
|
|
427
|
+
out.set('vmid', parseInt(opts.vmid, 10));
|
|
428
|
+
out.set('disk', opts.disk);
|
|
429
|
+
out.set('size', opts.size);
|
|
430
|
+
out.flush();
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
// ── status ───────────────────────────────────────────────────────
|
|
434
|
+
withNodeOption(
|
|
435
|
+
withCredentialOptions(
|
|
436
|
+
cmd.command('status')
|
|
437
|
+
.description('Get current runtime status of a VM')
|
|
438
|
+
.requiredOption('--vmid <id>', 'VM ID')
|
|
439
|
+
)
|
|
440
|
+
).action(async (opts) => {
|
|
441
|
+
const runtime = getRuntime();
|
|
442
|
+
const out = createOutput(runtime);
|
|
443
|
+
const creds = resolveCredentials(opts);
|
|
444
|
+
const node = resolveNode(opts, creds);
|
|
445
|
+
const client = createProxmoxClient(creds);
|
|
446
|
+
|
|
447
|
+
const data = await client.get(`nodes/${node}/qemu/${opts.vmid}/status/current`);
|
|
448
|
+
|
|
449
|
+
out.heading('VM status');
|
|
450
|
+
if (runtime.interactive) {
|
|
451
|
+
out.info(`Node: ${node}`);
|
|
452
|
+
out.info(`VMID: ${opts.vmid}`);
|
|
453
|
+
if (data && typeof data === 'object') {
|
|
454
|
+
for (const [k, v] of Object.entries(data)) {
|
|
455
|
+
const display = typeof v === 'object' ? JSON.stringify(v) : v;
|
|
456
|
+
out.info(`${k.padEnd(12)} ${display}`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
out.set('node', node);
|
|
461
|
+
out.set('vmid', opts.vmid);
|
|
462
|
+
out.set('status', data);
|
|
463
|
+
out.flush();
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
// ── suspend ──────────────────────────────────────────────────────
|
|
467
|
+
withNodeOption(
|
|
468
|
+
withCredentialOptions(
|
|
469
|
+
cmd.command('suspend')
|
|
470
|
+
.description('Suspend (pause) a VM to memory')
|
|
471
|
+
.requiredOption('--vmid <id>', 'VM ID')
|
|
472
|
+
)
|
|
473
|
+
).action(async (opts) => {
|
|
474
|
+
const runtime = getRuntime();
|
|
475
|
+
const out = createOutput(runtime);
|
|
476
|
+
const creds = resolveCredentials(opts);
|
|
477
|
+
const node = resolveNode(opts, creds);
|
|
478
|
+
const client = createProxmoxClient(creds);
|
|
479
|
+
|
|
480
|
+
const upid = await client.post(`nodes/${node}/qemu/${opts.vmid}/status/suspend`);
|
|
481
|
+
|
|
482
|
+
out.heading('VM suspend');
|
|
483
|
+
if (runtime.interactive) {
|
|
484
|
+
out.success(`Suspend requested for VM ${opts.vmid} on ${node}`);
|
|
485
|
+
if (upid) out.info(`UPID: ${upid}`);
|
|
486
|
+
}
|
|
487
|
+
out.set('node', node);
|
|
488
|
+
out.set('vmid', opts.vmid);
|
|
489
|
+
out.set('action', 'suspend');
|
|
490
|
+
out.set('upid', upid);
|
|
491
|
+
out.flush();
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// ── resume ───────────────────────────────────────────────────────
|
|
495
|
+
withNodeOption(
|
|
496
|
+
withCredentialOptions(
|
|
497
|
+
cmd.command('resume')
|
|
498
|
+
.description('Resume a suspended VM')
|
|
499
|
+
.requiredOption('--vmid <id>', 'VM ID')
|
|
500
|
+
)
|
|
501
|
+
).action(async (opts) => {
|
|
502
|
+
const runtime = getRuntime();
|
|
503
|
+
const out = createOutput(runtime);
|
|
504
|
+
const creds = resolveCredentials(opts);
|
|
505
|
+
const node = resolveNode(opts, creds);
|
|
506
|
+
const client = createProxmoxClient(creds);
|
|
507
|
+
|
|
508
|
+
await client.post(`nodes/${node}/qemu/${opts.vmid}/status/resume`);
|
|
509
|
+
|
|
510
|
+
out.heading('VM resume');
|
|
511
|
+
if (runtime.interactive) {
|
|
512
|
+
out.success(`Resume requested for VM ${opts.vmid} on ${node}`);
|
|
513
|
+
}
|
|
514
|
+
out.set('node', node);
|
|
515
|
+
out.set('vmid', parseInt(opts.vmid, 10));
|
|
516
|
+
out.set('action', 'resume');
|
|
517
|
+
out.flush();
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
return cmd;
|
|
521
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Program entry point. Builds the commander tree, installs the --json /
|
|
3
|
+
* --interactive global flag hook, and catches any thrown error from an
|
|
4
|
+
* async action and routes it through the structured fatalError emitter.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Command } from 'commander';
|
|
8
|
+
import { setForceJson, setForceInteractive } from './utils/runtime.js';
|
|
9
|
+
import { fatalError } from './utils/errors.js';
|
|
10
|
+
import { configureCommand } from './commands/configure.js';
|
|
11
|
+
import { nodeCommand } from './commands/node.js';
|
|
12
|
+
import { vmCommand } from './commands/vm.js';
|
|
13
|
+
import { snapshotCommand } from './commands/snapshot.js';
|
|
14
|
+
import { storageCommand } from './commands/storage.js';
|
|
15
|
+
import { taskCommand } from './commands/task.js';
|
|
16
|
+
import { clusterCommand } from './commands/cluster.js';
|
|
17
|
+
|
|
18
|
+
export async function cli(argv) {
|
|
19
|
+
// Peek at argv before commander parses so that even pre-parse errors
|
|
20
|
+
// respect the caller's preferred output mode.
|
|
21
|
+
if (argv.includes('--json')) setForceJson(true);
|
|
22
|
+
if (argv.includes('--interactive')) setForceInteractive(true);
|
|
23
|
+
|
|
24
|
+
const program = new Command();
|
|
25
|
+
|
|
26
|
+
program
|
|
27
|
+
.name('proxmox')
|
|
28
|
+
.description('AI-first CLI for managing Proxmox VE nodes, VMs, storage, and snapshots')
|
|
29
|
+
.version('0.0.1')
|
|
30
|
+
.option('--json', 'force JSON output (default when stdout is not a TTY)')
|
|
31
|
+
.option('--interactive', 'force interactive/human-friendly output')
|
|
32
|
+
.hook('preAction', (thisCommand, actionCommand) => {
|
|
33
|
+
const opts = actionCommand.optsWithGlobals();
|
|
34
|
+
if (opts.json) setForceJson(true);
|
|
35
|
+
if (opts.interactive) setForceInteractive(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
program.addCommand(configureCommand());
|
|
39
|
+
program.addCommand(nodeCommand());
|
|
40
|
+
program.addCommand(vmCommand());
|
|
41
|
+
program.addCommand(snapshotCommand());
|
|
42
|
+
program.addCommand(storageCommand());
|
|
43
|
+
program.addCommand(taskCommand());
|
|
44
|
+
program.addCommand(clusterCommand());
|
|
45
|
+
|
|
46
|
+
applyHelpOnError(program);
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
await program.parseAsync(argv);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
fatalError(err);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Recursively install an exitOverride that prints help on commander parse
|
|
57
|
+
* errors (missing arg, unknown option, etc.) instead of a one-line message.
|
|
58
|
+
*/
|
|
59
|
+
function applyHelpOnError(cmd) {
|
|
60
|
+
const HELP_CODES = new Set([
|
|
61
|
+
'commander.missingArgument',
|
|
62
|
+
'commander.missingMandatoryOptionValue',
|
|
63
|
+
'commander.invalidArgument',
|
|
64
|
+
'commander.invalidOptionArgument',
|
|
65
|
+
'commander.unknownOption',
|
|
66
|
+
'commander.unknownCommand',
|
|
67
|
+
'commander.excessArguments',
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
cmd.exitOverride((err) => {
|
|
71
|
+
if (err.code === 'commander.help' ||
|
|
72
|
+
err.code === 'commander.version' ||
|
|
73
|
+
err.code === 'commander.helpDisplayed') {
|
|
74
|
+
process.exit(err.exitCode ?? 0);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (HELP_CODES.has(err.code)) {
|
|
78
|
+
process.stderr.write('\n');
|
|
79
|
+
cmd.outputHelp({ error: true });
|
|
80
|
+
process.exit(err.exitCode ?? 1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
throw err;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
for (const sub of cmd.commands ?? []) {
|
|
87
|
+
applyHelpOnError(sub);
|
|
88
|
+
}
|
|
89
|
+
}
|