hetzner-robot-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +440 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +934 -0
- package/dist/client.d.ts +256 -0
- package/dist/client.js +656 -0
- package/dist/config.d.ts +41 -0
- package/dist/config.js +109 -0
- package/dist/formatter.d.ts +85 -0
- package/dist/formatter.js +617 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +9 -0
- package/dist/types.d.ts +352 -0
- package/dist/types.js +5 -0
- package/package.json +89 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,934 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command, Option } from 'commander';
|
|
3
|
+
import { select, confirm, input, checkbox } from '@inquirer/prompts';
|
|
4
|
+
import { config } from 'dotenv';
|
|
5
|
+
import { readFileSync } from 'fs';
|
|
6
|
+
import { HetznerRobotClient } from './client.js';
|
|
7
|
+
import { requireCredentials, promptLogin, clearConfig, getCredentials, } from './config.js';
|
|
8
|
+
import * as fmt from './formatter.js';
|
|
9
|
+
config();
|
|
10
|
+
let client = null;
|
|
11
|
+
/**
|
|
12
|
+
* Get or create API client with credentials.
|
|
13
|
+
* Credential sources (in order): CLI flags, env vars, config file, interactive prompt.
|
|
14
|
+
*/
|
|
15
|
+
async function getClient(options) {
|
|
16
|
+
if (client)
|
|
17
|
+
return client;
|
|
18
|
+
const { user } = options;
|
|
19
|
+
let password = options.password;
|
|
20
|
+
if (password === '-') {
|
|
21
|
+
try {
|
|
22
|
+
password = readFileSync(0, 'utf-8').trim();
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
throw new Error('Failed to read password from stdin');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (user && password) {
|
|
29
|
+
client = new HetznerRobotClient(user, password);
|
|
30
|
+
return client;
|
|
31
|
+
}
|
|
32
|
+
const creds = await requireCredentials();
|
|
33
|
+
client = new HetznerRobotClient(creds.user, creds.password);
|
|
34
|
+
return client;
|
|
35
|
+
}
|
|
36
|
+
function asyncAction(fn) {
|
|
37
|
+
return async (...args) => {
|
|
38
|
+
const options = args[args.length - 1];
|
|
39
|
+
try {
|
|
40
|
+
const apiClient = await getClient(options);
|
|
41
|
+
await fn(apiClient, ...args.slice(0, -1));
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (error instanceof Error) {
|
|
45
|
+
if (error.message.includes('ExitPromptError') || error.name === 'ExitPromptError') {
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
console.error(fmt.error(error.message));
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
console.error(fmt.error('An unknown error occurred'));
|
|
52
|
+
}
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Output data as JSON or formatted table based on options.
|
|
59
|
+
*/
|
|
60
|
+
function output(data, formatter, options) {
|
|
61
|
+
console.log(options.json ? fmt.formatJson(data) : formatter(data));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Confirm destructive action unless --yes flag is set.
|
|
65
|
+
* Returns true if confirmed, false if aborted.
|
|
66
|
+
*/
|
|
67
|
+
async function confirmAction(message, options, defaultValue = false) {
|
|
68
|
+
if (options.yes)
|
|
69
|
+
return true;
|
|
70
|
+
const confirmed = await confirm({ message, default: defaultValue });
|
|
71
|
+
if (!confirmed) {
|
|
72
|
+
console.log('Aborted.');
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
const program = new Command();
|
|
78
|
+
program
|
|
79
|
+
.name('hetzner')
|
|
80
|
+
.description('Feature-complete CLI for Hetzner Robot API (dedicated servers)')
|
|
81
|
+
.version('1.0.0')
|
|
82
|
+
.option('-u, --user <username>', 'Hetzner Robot web service username')
|
|
83
|
+
.option('-p, --password <password>', 'Hetzner Robot web service password (use - to read from stdin)')
|
|
84
|
+
.option('--json', 'Output raw JSON instead of formatted tables');
|
|
85
|
+
const auth = program.command('auth').description('Authentication management');
|
|
86
|
+
auth
|
|
87
|
+
.command('login')
|
|
88
|
+
.description('Interactively configure credentials')
|
|
89
|
+
.action(async () => {
|
|
90
|
+
try {
|
|
91
|
+
await promptLogin();
|
|
92
|
+
console.log('');
|
|
93
|
+
console.log(fmt.success('Authentication configured successfully.'));
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (error instanceof Error && error.name === 'ExitPromptError') {
|
|
97
|
+
process.exit(0);
|
|
98
|
+
}
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
auth
|
|
103
|
+
.command('logout')
|
|
104
|
+
.description('Clear saved credentials')
|
|
105
|
+
.action(() => {
|
|
106
|
+
clearConfig();
|
|
107
|
+
console.log(fmt.success('Credentials cleared.'));
|
|
108
|
+
});
|
|
109
|
+
auth
|
|
110
|
+
.command('status')
|
|
111
|
+
.description('Check authentication status')
|
|
112
|
+
.action(() => {
|
|
113
|
+
const creds = getCredentials();
|
|
114
|
+
if (creds) {
|
|
115
|
+
console.log(fmt.success(`Authenticated as: ${creds.user}`));
|
|
116
|
+
console.log(fmt.info('Source: ' + (process.env.HETZNER_ROBOT_USER ? 'environment variables' : 'config file')));
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
console.log(fmt.warning('Not authenticated. Run: hetzner auth login'));
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
auth
|
|
123
|
+
.command('test')
|
|
124
|
+
.description('Test API credentials')
|
|
125
|
+
.action(asyncAction(async (client) => {
|
|
126
|
+
const servers = await client.listServers();
|
|
127
|
+
console.log(fmt.success(`Authenticated successfully. Found ${servers.length} server(s).`));
|
|
128
|
+
}));
|
|
129
|
+
const server = program.command('server').alias('servers').description('Server management');
|
|
130
|
+
server
|
|
131
|
+
.command('list')
|
|
132
|
+
.alias('ls')
|
|
133
|
+
.description('List all servers')
|
|
134
|
+
.action(asyncAction(async (client, options) => {
|
|
135
|
+
const servers = await client.listServers();
|
|
136
|
+
output(servers, fmt.formatServerList, options);
|
|
137
|
+
}));
|
|
138
|
+
server
|
|
139
|
+
.command('get <server>')
|
|
140
|
+
.alias('show')
|
|
141
|
+
.description('Get server details')
|
|
142
|
+
.action(asyncAction(async (client, serverIdOrIp, options) => {
|
|
143
|
+
const { server: srv } = await client.getServer(serverIdOrIp);
|
|
144
|
+
output(srv, fmt.formatServerDetails, options);
|
|
145
|
+
}));
|
|
146
|
+
server
|
|
147
|
+
.command('rename <server> <name>')
|
|
148
|
+
.description('Rename a server')
|
|
149
|
+
.action(asyncAction(async (client, serverIdOrIp, name) => {
|
|
150
|
+
await client.updateServerName(serverIdOrIp, name);
|
|
151
|
+
console.log(fmt.success(`Server renamed to: ${name}`));
|
|
152
|
+
}));
|
|
153
|
+
const reset = program.command('reset').description('Server reset operations');
|
|
154
|
+
reset
|
|
155
|
+
.command('options [server]')
|
|
156
|
+
.description('Show reset options for server(s)')
|
|
157
|
+
.action(asyncAction(async (client, serverIdOrIp, options) => {
|
|
158
|
+
if (serverIdOrIp) {
|
|
159
|
+
const { reset: rst } = await client.getResetOptions(serverIdOrIp);
|
|
160
|
+
output(rst, fmt.formatResetOptions, options);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
const resets = await client.listResetOptions();
|
|
164
|
+
if (options.json) {
|
|
165
|
+
console.log(fmt.formatJson(resets));
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
for (const { reset: rst } of resets) {
|
|
169
|
+
console.log(fmt.formatResetOptions(rst));
|
|
170
|
+
console.log('');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}));
|
|
175
|
+
reset
|
|
176
|
+
.command('execute <servers...>')
|
|
177
|
+
.alias('run')
|
|
178
|
+
.description('Reset one or more servers')
|
|
179
|
+
.addOption(new Option('-t, --type <type>', 'Reset type')
|
|
180
|
+
.choices(['sw', 'hw', 'man', 'power', 'power_long'])
|
|
181
|
+
.default('sw'))
|
|
182
|
+
.option('-i, --interactive', 'Interactively select reset type')
|
|
183
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
184
|
+
.action(asyncAction(async (client, servers, options) => {
|
|
185
|
+
let resetType = options.type;
|
|
186
|
+
if (options.interactive) {
|
|
187
|
+
resetType = (await select({
|
|
188
|
+
message: 'Select reset type:',
|
|
189
|
+
choices: [
|
|
190
|
+
{ value: 'sw', name: 'Software reset (ACPI) - Recommended' },
|
|
191
|
+
{ value: 'hw', name: 'Hardware reset (forced)' },
|
|
192
|
+
{ value: 'power', name: 'Power cycle' },
|
|
193
|
+
{ value: 'power_long', name: 'Long power cycle (10+ seconds)' },
|
|
194
|
+
{ value: 'man', name: 'Manual reset (technician)' },
|
|
195
|
+
],
|
|
196
|
+
}));
|
|
197
|
+
}
|
|
198
|
+
if (!options.yes) {
|
|
199
|
+
console.log('');
|
|
200
|
+
console.log(fmt.warning(`About to reset ${servers.length} server(s) with type: ${resetType}`));
|
|
201
|
+
console.log(`Servers: ${servers.join(', ')}`);
|
|
202
|
+
console.log('');
|
|
203
|
+
const confirmed = await confirm({
|
|
204
|
+
message: 'Are you sure you want to proceed?',
|
|
205
|
+
default: false,
|
|
206
|
+
});
|
|
207
|
+
if (!confirmed) {
|
|
208
|
+
console.log('Aborted.');
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
console.log('');
|
|
213
|
+
for (const srv of servers) {
|
|
214
|
+
try {
|
|
215
|
+
const { reset: rst } = await client.resetServer(srv, resetType);
|
|
216
|
+
console.log(fmt.formatResetResult(rst, resetType));
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
console.log(fmt.error(`Failed to reset ${srv}: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}));
|
|
223
|
+
const boot = program.command('boot').description('Boot configuration (rescue, linux, vnc, windows)');
|
|
224
|
+
boot
|
|
225
|
+
.command('status <server>')
|
|
226
|
+
.description('Show boot configuration status')
|
|
227
|
+
.action(asyncAction(async (client, serverIdOrIp, options) => {
|
|
228
|
+
const { boot: bootConfig } = await client.getBootConfig(serverIdOrIp);
|
|
229
|
+
output(bootConfig, (b) => fmt.formatBootConfig(b, parseInt(serverIdOrIp) || 0), options);
|
|
230
|
+
}));
|
|
231
|
+
const rescue = boot.command('rescue').description('Rescue system management');
|
|
232
|
+
rescue
|
|
233
|
+
.command('activate <server>')
|
|
234
|
+
.description('Activate rescue system')
|
|
235
|
+
.option('-o, --os <os>', 'Operating system (linux, linuxold, vkvm)', 'linux')
|
|
236
|
+
.option('-a, --arch <arch>', 'Architecture (64 or 32)', '64')
|
|
237
|
+
.option('-k, --keys <fingerprints...>', 'SSH key fingerprints')
|
|
238
|
+
.action(asyncAction(async (client, serverIdOrIp, options) => {
|
|
239
|
+
const { rescue: rsc } = await client.activateRescue(serverIdOrIp, options.os, parseInt(options.arch), options.keys);
|
|
240
|
+
console.log(fmt.formatRescueActivation(rsc));
|
|
241
|
+
}));
|
|
242
|
+
rescue
|
|
243
|
+
.command('deactivate <server>')
|
|
244
|
+
.description('Deactivate rescue system')
|
|
245
|
+
.action(asyncAction(async (client, serverIdOrIp) => {
|
|
246
|
+
await client.deactivateRescue(serverIdOrIp);
|
|
247
|
+
console.log(fmt.success('Rescue system deactivated.'));
|
|
248
|
+
}));
|
|
249
|
+
rescue
|
|
250
|
+
.command('last <server>')
|
|
251
|
+
.description('Show last rescue activation details')
|
|
252
|
+
.action(asyncAction(async (client, serverIdOrIp, options) => {
|
|
253
|
+
const { rescue: rsc } = await client.getLastRescue(serverIdOrIp);
|
|
254
|
+
output(rsc, fmt.formatRescueActivation, options);
|
|
255
|
+
}));
|
|
256
|
+
const linux = boot.command('linux').description('Linux installation management');
|
|
257
|
+
linux
|
|
258
|
+
.command('activate <server>')
|
|
259
|
+
.description('Activate Linux installation')
|
|
260
|
+
.requiredOption('-d, --dist <dist>', 'Distribution (e.g., Debian-1210-bookworm-amd64-base)')
|
|
261
|
+
.option('-a, --arch <arch>', 'Architecture (64 or 32)', '64')
|
|
262
|
+
.option('-l, --lang <lang>', 'Language', 'en')
|
|
263
|
+
.option('-k, --keys <fingerprints...>', 'SSH key fingerprints')
|
|
264
|
+
.action(asyncAction(async (client, serverIdOrIp, options) => {
|
|
265
|
+
const { linux: lnx } = await client.activateLinux(serverIdOrIp, options.dist, parseInt(options.arch), options.lang, options.keys);
|
|
266
|
+
console.log(fmt.formatLinuxActivation(lnx));
|
|
267
|
+
}));
|
|
268
|
+
linux
|
|
269
|
+
.command('deactivate <server>')
|
|
270
|
+
.description('Deactivate Linux installation')
|
|
271
|
+
.action(asyncAction(async (client, serverIdOrIp) => {
|
|
272
|
+
await client.deactivateLinux(serverIdOrIp);
|
|
273
|
+
console.log(fmt.success('Linux installation deactivated.'));
|
|
274
|
+
}));
|
|
275
|
+
linux
|
|
276
|
+
.command('options <server>')
|
|
277
|
+
.description('Show available Linux distributions')
|
|
278
|
+
.action(asyncAction(async (client, serverIdOrIp, options) => {
|
|
279
|
+
const { linux: lnx } = await client.getLinux(serverIdOrIp);
|
|
280
|
+
output(lnx, (l) => {
|
|
281
|
+
const lines = [fmt.heading('Available Linux Distributions'), ''];
|
|
282
|
+
for (const dist of l.dist) {
|
|
283
|
+
lines.push(` ${dist}`);
|
|
284
|
+
}
|
|
285
|
+
lines.push('', fmt.info(`Languages: ${l.lang.join(', ')}`));
|
|
286
|
+
lines.push(fmt.info(`Architectures: ${l.arch.join(', ')}-bit`));
|
|
287
|
+
return lines.join('\n');
|
|
288
|
+
}, options);
|
|
289
|
+
}));
|
|
290
|
+
const ip = program.command('ip').alias('ips').description('IP address management');
|
|
291
|
+
ip.command('list')
|
|
292
|
+
.alias('ls')
|
|
293
|
+
.description('List all IPs')
|
|
294
|
+
.action(asyncAction(async (client, options) => {
|
|
295
|
+
const ips = await client.listIps();
|
|
296
|
+
output(ips, fmt.formatIpList, options);
|
|
297
|
+
}));
|
|
298
|
+
ip.command('get <ip>')
|
|
299
|
+
.alias('show')
|
|
300
|
+
.description('Get IP details')
|
|
301
|
+
.action(asyncAction(async (client, ipAddr, options) => {
|
|
302
|
+
const { ip: ipData } = await client.getIp(ipAddr);
|
|
303
|
+
output(ipData, fmt.formatIpDetails, options);
|
|
304
|
+
}));
|
|
305
|
+
ip.command('update <ip>')
|
|
306
|
+
.description('Update IP traffic warning settings')
|
|
307
|
+
.option('--warnings <enabled>', 'Enable/disable traffic warnings (true/false)')
|
|
308
|
+
.option('--hourly <mb>', 'Hourly traffic limit in MB')
|
|
309
|
+
.option('--daily <mb>', 'Daily traffic limit in MB')
|
|
310
|
+
.option('--monthly <gb>', 'Monthly traffic limit in GB')
|
|
311
|
+
.action(asyncAction(async (client, ipAddr, options) => {
|
|
312
|
+
await client.updateIp(ipAddr, options.warnings ? options.warnings === 'true' : undefined, options.hourly ? parseInt(options.hourly) : undefined, options.daily ? parseInt(options.daily) : undefined, options.monthly ? parseInt(options.monthly) : undefined);
|
|
313
|
+
console.log(fmt.success(`IP ${ipAddr} updated.`));
|
|
314
|
+
}));
|
|
315
|
+
const mac = ip.command('mac').description('MAC address management');
|
|
316
|
+
mac
|
|
317
|
+
.command('get <ip>')
|
|
318
|
+
.description('Get separate MAC address for IP')
|
|
319
|
+
.action(asyncAction(async (client, ipAddr, options) => {
|
|
320
|
+
const { mac: macData } = await client.getIpMac(ipAddr);
|
|
321
|
+
output(macData, (m) => `IP: ${m.ip}\nMAC: ${m.mac}`, options);
|
|
322
|
+
}));
|
|
323
|
+
mac
|
|
324
|
+
.command('generate <ip>')
|
|
325
|
+
.description('Generate separate MAC address for IP')
|
|
326
|
+
.action(asyncAction(async (client, ipAddr) => {
|
|
327
|
+
const { mac: macData } = await client.generateIpMac(ipAddr);
|
|
328
|
+
console.log(fmt.success(`Generated MAC: ${macData.mac}`));
|
|
329
|
+
}));
|
|
330
|
+
mac
|
|
331
|
+
.command('delete <ip>')
|
|
332
|
+
.description('Delete separate MAC address')
|
|
333
|
+
.action(asyncAction(async (client, ipAddr) => {
|
|
334
|
+
await client.deleteIpMac(ipAddr);
|
|
335
|
+
console.log(fmt.success('MAC address deleted.'));
|
|
336
|
+
}));
|
|
337
|
+
const subnet = program.command('subnet').alias('subnets').description('Subnet management');
|
|
338
|
+
subnet
|
|
339
|
+
.command('list')
|
|
340
|
+
.alias('ls')
|
|
341
|
+
.description('List all subnets')
|
|
342
|
+
.action(asyncAction(async (client, options) => {
|
|
343
|
+
const subnets = await client.listSubnets();
|
|
344
|
+
output(subnets, fmt.formatSubnetList, options);
|
|
345
|
+
}));
|
|
346
|
+
subnet
|
|
347
|
+
.command('get <subnet>')
|
|
348
|
+
.alias('show')
|
|
349
|
+
.description('Get subnet details')
|
|
350
|
+
.action(asyncAction(async (client, netIp, options) => {
|
|
351
|
+
const { subnet: subnetData } = await client.getSubnet(netIp);
|
|
352
|
+
output(subnetData, (s) => fmt.formatSubnetList([{ subnet: s }]), options);
|
|
353
|
+
}));
|
|
354
|
+
const failover = program.command('failover').description('Failover IP management');
|
|
355
|
+
failover
|
|
356
|
+
.command('list')
|
|
357
|
+
.alias('ls')
|
|
358
|
+
.description('List all failover IPs')
|
|
359
|
+
.action(asyncAction(async (client, options) => {
|
|
360
|
+
const failovers = await client.listFailovers();
|
|
361
|
+
output(failovers, fmt.formatFailoverList, options);
|
|
362
|
+
}));
|
|
363
|
+
failover
|
|
364
|
+
.command('get <ip>')
|
|
365
|
+
.alias('show')
|
|
366
|
+
.description('Get failover IP details')
|
|
367
|
+
.action(asyncAction(async (client, failoverIp, options) => {
|
|
368
|
+
const { failover: fo } = await client.getFailover(failoverIp);
|
|
369
|
+
output(fo, (f) => fmt.formatFailoverList([{ failover: f }]), options);
|
|
370
|
+
}));
|
|
371
|
+
failover
|
|
372
|
+
.command('switch <failover-ip> <target-server-ip>')
|
|
373
|
+
.description('Switch failover IP routing to another server')
|
|
374
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
375
|
+
.action(asyncAction(async (client, failoverIp, targetServerIp, options) => {
|
|
376
|
+
if (!await confirmAction(`Route ${failoverIp} to ${targetServerIp}?`, options, true))
|
|
377
|
+
return;
|
|
378
|
+
const { failover: fo } = await client.switchFailover(failoverIp, targetServerIp);
|
|
379
|
+
console.log(fmt.formatFailoverSwitch(fo));
|
|
380
|
+
}));
|
|
381
|
+
failover
|
|
382
|
+
.command('delete <ip>')
|
|
383
|
+
.description('Delete failover IP routing')
|
|
384
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
385
|
+
.action(asyncAction(async (client, failoverIp, options) => {
|
|
386
|
+
if (!await confirmAction(`Delete routing for ${failoverIp}?`, options))
|
|
387
|
+
return;
|
|
388
|
+
await client.deleteFailoverRouting(failoverIp);
|
|
389
|
+
console.log(fmt.success('Failover routing deleted.'));
|
|
390
|
+
}));
|
|
391
|
+
const rdns = program.command('rdns').description('Reverse DNS management');
|
|
392
|
+
rdns
|
|
393
|
+
.command('list')
|
|
394
|
+
.alias('ls')
|
|
395
|
+
.description('List all reverse DNS entries')
|
|
396
|
+
.action(asyncAction(async (client, options) => {
|
|
397
|
+
const entries = await client.listRdns();
|
|
398
|
+
output(entries, fmt.formatRdnsList, options);
|
|
399
|
+
}));
|
|
400
|
+
rdns
|
|
401
|
+
.command('get <ip>')
|
|
402
|
+
.alias('show')
|
|
403
|
+
.description('Get reverse DNS entry for IP')
|
|
404
|
+
.action(asyncAction(async (client, ipAddr, options) => {
|
|
405
|
+
const { rdns: entry } = await client.getRdns(ipAddr);
|
|
406
|
+
output(entry, (e) => `IP: ${e.ip}\nPTR: ${e.ptr}`, options);
|
|
407
|
+
}));
|
|
408
|
+
rdns
|
|
409
|
+
.command('set <ip> <ptr>')
|
|
410
|
+
.description('Create or update reverse DNS entry')
|
|
411
|
+
.action(asyncAction(async (client, ipAddr, ptr) => {
|
|
412
|
+
try {
|
|
413
|
+
await client.createRdns(ipAddr, ptr);
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
await client.updateRdns(ipAddr, ptr);
|
|
417
|
+
}
|
|
418
|
+
console.log(fmt.success(`rDNS set: ${ipAddr} -> ${ptr}`));
|
|
419
|
+
}));
|
|
420
|
+
rdns
|
|
421
|
+
.command('delete <ip>')
|
|
422
|
+
.description('Delete reverse DNS entry')
|
|
423
|
+
.action(asyncAction(async (client, ipAddr) => {
|
|
424
|
+
await client.deleteRdns(ipAddr);
|
|
425
|
+
console.log(fmt.success('rDNS entry deleted.'));
|
|
426
|
+
}));
|
|
427
|
+
const key = program.command('key').alias('keys').description('SSH key management');
|
|
428
|
+
key
|
|
429
|
+
.command('list')
|
|
430
|
+
.alias('ls')
|
|
431
|
+
.description('List all SSH keys')
|
|
432
|
+
.action(asyncAction(async (client, options) => {
|
|
433
|
+
const keys = await client.listSshKeys();
|
|
434
|
+
output(keys, fmt.formatSshKeyList, options);
|
|
435
|
+
}));
|
|
436
|
+
key
|
|
437
|
+
.command('get <fingerprint>')
|
|
438
|
+
.alias('show')
|
|
439
|
+
.description('Get SSH key details')
|
|
440
|
+
.action(asyncAction(async (client, fingerprint, options) => {
|
|
441
|
+
const { key: keyData } = await client.getSshKey(fingerprint);
|
|
442
|
+
output(keyData, fmt.formatSshKeyDetails, options);
|
|
443
|
+
}));
|
|
444
|
+
key
|
|
445
|
+
.command('add <name>')
|
|
446
|
+
.description('Add a new SSH key')
|
|
447
|
+
.option('-f, --file <path>', 'Path to public key file')
|
|
448
|
+
.option('-d, --data <key>', 'Public key data')
|
|
449
|
+
.action(asyncAction(async (client, name, options) => {
|
|
450
|
+
let keyData;
|
|
451
|
+
if (options.file) {
|
|
452
|
+
keyData = readFileSync(options.file, 'utf-8').trim();
|
|
453
|
+
}
|
|
454
|
+
else if (options.data) {
|
|
455
|
+
keyData = options.data;
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
keyData = await input({
|
|
459
|
+
message: 'Paste public key:',
|
|
460
|
+
validate: (v) => v.length > 0 || 'Key data is required',
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
const { key: newKey } = await client.createSshKey(name, keyData);
|
|
464
|
+
console.log(fmt.success(`SSH key added: ${newKey.fingerprint}`));
|
|
465
|
+
}));
|
|
466
|
+
key
|
|
467
|
+
.command('rename <fingerprint> <name>')
|
|
468
|
+
.description('Rename an SSH key')
|
|
469
|
+
.action(asyncAction(async (client, fingerprint, name) => {
|
|
470
|
+
await client.updateSshKey(fingerprint, name);
|
|
471
|
+
console.log(fmt.success(`SSH key renamed to: ${name}`));
|
|
472
|
+
}));
|
|
473
|
+
key
|
|
474
|
+
.command('delete <fingerprint>')
|
|
475
|
+
.alias('rm')
|
|
476
|
+
.description('Delete an SSH key')
|
|
477
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
478
|
+
.action(asyncAction(async (client, fingerprint, options) => {
|
|
479
|
+
if (!await confirmAction(`Delete SSH key ${fingerprint}?`, options))
|
|
480
|
+
return;
|
|
481
|
+
await client.deleteSshKey(fingerprint);
|
|
482
|
+
console.log(fmt.success('SSH key deleted.'));
|
|
483
|
+
}));
|
|
484
|
+
const firewall = program.command('firewall').description('Firewall management');
|
|
485
|
+
firewall
|
|
486
|
+
.command('get <server>')
|
|
487
|
+
.alias('show')
|
|
488
|
+
.description('Get firewall configuration')
|
|
489
|
+
.action(asyncAction(async (client, serverIdOrIp, options) => {
|
|
490
|
+
const { firewall: fw } = await client.getFirewall(serverIdOrIp);
|
|
491
|
+
output(fw, fmt.formatFirewall, options);
|
|
492
|
+
}));
|
|
493
|
+
firewall
|
|
494
|
+
.command('enable <server>')
|
|
495
|
+
.description('Enable firewall')
|
|
496
|
+
.action(asyncAction(async (client, serverIdOrIp) => {
|
|
497
|
+
await client.updateFirewall(serverIdOrIp, 'active');
|
|
498
|
+
console.log(fmt.success('Firewall enabled.'));
|
|
499
|
+
}));
|
|
500
|
+
firewall
|
|
501
|
+
.command('disable <server>')
|
|
502
|
+
.description('Disable firewall')
|
|
503
|
+
.action(asyncAction(async (client, serverIdOrIp) => {
|
|
504
|
+
await client.updateFirewall(serverIdOrIp, 'disabled');
|
|
505
|
+
console.log(fmt.success('Firewall disabled.'));
|
|
506
|
+
}));
|
|
507
|
+
firewall
|
|
508
|
+
.command('delete <server>')
|
|
509
|
+
.description('Delete all firewall rules')
|
|
510
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
511
|
+
.action(asyncAction(async (client, serverIdOrIp, options) => {
|
|
512
|
+
if (!await confirmAction('Delete all firewall rules?', options))
|
|
513
|
+
return;
|
|
514
|
+
await client.deleteFirewall(serverIdOrIp);
|
|
515
|
+
console.log(fmt.success('Firewall rules deleted.'));
|
|
516
|
+
}));
|
|
517
|
+
const fwTemplate = firewall.command('template').description('Firewall template management');
|
|
518
|
+
fwTemplate
|
|
519
|
+
.command('list')
|
|
520
|
+
.alias('ls')
|
|
521
|
+
.description('List firewall templates')
|
|
522
|
+
.action(asyncAction(async (client, options) => {
|
|
523
|
+
const templates = await client.listFirewallTemplates();
|
|
524
|
+
output(templates, fmt.formatFirewallTemplateList, options);
|
|
525
|
+
}));
|
|
526
|
+
fwTemplate
|
|
527
|
+
.command('get <id>')
|
|
528
|
+
.alias('show')
|
|
529
|
+
.description('Get firewall template details')
|
|
530
|
+
.action(asyncAction(async (client, templateId, options) => {
|
|
531
|
+
const { firewall_template: tmpl } = await client.getFirewallTemplate(parseInt(templateId));
|
|
532
|
+
output(tmpl, (t) => fmt.formatFirewallTemplateList([{ firewall_template: t }]), options);
|
|
533
|
+
}));
|
|
534
|
+
fwTemplate
|
|
535
|
+
.command('delete <id>')
|
|
536
|
+
.alias('rm')
|
|
537
|
+
.description('Delete firewall template')
|
|
538
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
539
|
+
.action(asyncAction(async (client, templateId, options) => {
|
|
540
|
+
if (!await confirmAction(`Delete firewall template ${templateId}?`, options))
|
|
541
|
+
return;
|
|
542
|
+
await client.deleteFirewallTemplate(parseInt(templateId));
|
|
543
|
+
console.log(fmt.success('Firewall template deleted.'));
|
|
544
|
+
}));
|
|
545
|
+
const vswitch = program.command('vswitch').description('vSwitch management');
|
|
546
|
+
vswitch
|
|
547
|
+
.command('list')
|
|
548
|
+
.alias('ls')
|
|
549
|
+
.description('List all vSwitches')
|
|
550
|
+
.action(asyncAction(async (client, options) => {
|
|
551
|
+
const vswitches = await client.listVSwitches();
|
|
552
|
+
output(vswitches, fmt.formatVSwitchList, options);
|
|
553
|
+
}));
|
|
554
|
+
vswitch
|
|
555
|
+
.command('get <id>')
|
|
556
|
+
.alias('show')
|
|
557
|
+
.description('Get vSwitch details')
|
|
558
|
+
.action(asyncAction(async (client, vswitchId, options) => {
|
|
559
|
+
const { vswitch: vs } = await client.getVSwitch(parseInt(vswitchId));
|
|
560
|
+
output(vs, fmt.formatVSwitchDetails, options);
|
|
561
|
+
}));
|
|
562
|
+
vswitch
|
|
563
|
+
.command('create <name> <vlan>')
|
|
564
|
+
.description('Create a new vSwitch')
|
|
565
|
+
.action(asyncAction(async (client, name, vlan) => {
|
|
566
|
+
const { vswitch: vs } = await client.createVSwitch(name, parseInt(vlan));
|
|
567
|
+
console.log(fmt.success(`vSwitch created: ID ${vs.id}`));
|
|
568
|
+
}));
|
|
569
|
+
vswitch
|
|
570
|
+
.command('update <id>')
|
|
571
|
+
.description('Update vSwitch')
|
|
572
|
+
.option('-n, --name <name>', 'New name')
|
|
573
|
+
.option('-v, --vlan <vlan>', 'New VLAN ID')
|
|
574
|
+
.action(asyncAction(async (client, vswitchId, options) => {
|
|
575
|
+
await client.updateVSwitch(parseInt(vswitchId), options.name, options.vlan ? parseInt(options.vlan) : undefined);
|
|
576
|
+
console.log(fmt.success('vSwitch updated.'));
|
|
577
|
+
}));
|
|
578
|
+
vswitch
|
|
579
|
+
.command('delete <id>')
|
|
580
|
+
.alias('rm')
|
|
581
|
+
.description('Delete vSwitch')
|
|
582
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
583
|
+
.option('--date <date>', 'Cancellation date (YYYY-MM-DD)')
|
|
584
|
+
.action(asyncAction(async (client, vswitchId, options) => {
|
|
585
|
+
if (!await confirmAction(`Delete vSwitch ${vswitchId}?`, options))
|
|
586
|
+
return;
|
|
587
|
+
await client.deleteVSwitch(parseInt(vswitchId), options.date);
|
|
588
|
+
console.log(fmt.success('vSwitch deleted.'));
|
|
589
|
+
}));
|
|
590
|
+
vswitch
|
|
591
|
+
.command('add-server <vswitch-id> <server>')
|
|
592
|
+
.description('Add server to vSwitch')
|
|
593
|
+
.action(asyncAction(async (client, vswitchId, serverIdOrIp) => {
|
|
594
|
+
await client.addServerToVSwitch(parseInt(vswitchId), serverIdOrIp);
|
|
595
|
+
console.log(fmt.success('Server added to vSwitch.'));
|
|
596
|
+
}));
|
|
597
|
+
vswitch
|
|
598
|
+
.command('remove-server <vswitch-id> <server>')
|
|
599
|
+
.description('Remove server from vSwitch')
|
|
600
|
+
.action(asyncAction(async (client, vswitchId, serverIdOrIp) => {
|
|
601
|
+
await client.removeServerFromVSwitch(parseInt(vswitchId), serverIdOrIp);
|
|
602
|
+
console.log(fmt.success('Server removed from vSwitch.'));
|
|
603
|
+
}));
|
|
604
|
+
const storagebox = program.command('storagebox').alias('storage').description('Storage Box management');
|
|
605
|
+
storagebox
|
|
606
|
+
.command('list')
|
|
607
|
+
.alias('ls')
|
|
608
|
+
.description('List all storage boxes')
|
|
609
|
+
.action(asyncAction(async (client, options) => {
|
|
610
|
+
const boxes = await client.listStorageBoxes();
|
|
611
|
+
output(boxes, fmt.formatStorageBoxList, options);
|
|
612
|
+
}));
|
|
613
|
+
storagebox
|
|
614
|
+
.command('get <id>')
|
|
615
|
+
.alias('show')
|
|
616
|
+
.description('Get storage box details')
|
|
617
|
+
.action(asyncAction(async (client, boxId, options) => {
|
|
618
|
+
const { storagebox: box } = await client.getStorageBox(parseInt(boxId));
|
|
619
|
+
output(box, fmt.formatStorageBoxDetails, options);
|
|
620
|
+
}));
|
|
621
|
+
storagebox
|
|
622
|
+
.command('update <id>')
|
|
623
|
+
.description('Update storage box settings')
|
|
624
|
+
.option('-n, --name <name>', 'Storage box name')
|
|
625
|
+
.option('--webdav <enabled>', 'Enable/disable WebDAV')
|
|
626
|
+
.option('--samba <enabled>', 'Enable/disable Samba/CIFS')
|
|
627
|
+
.option('--ssh <enabled>', 'Enable/disable SSH/SFTP')
|
|
628
|
+
.option('--external <enabled>', 'Enable/disable external reachability')
|
|
629
|
+
.option('--zfs <enabled>', 'Enable/disable ZFS')
|
|
630
|
+
.action(asyncAction(async (client, boxId, options) => {
|
|
631
|
+
await client.updateStorageBox(parseInt(boxId), options.name, options.webdav ? options.webdav === 'true' : undefined, options.samba ? options.samba === 'true' : undefined, options.ssh ? options.ssh === 'true' : undefined, options.external ? options.external === 'true' : undefined, options.zfs ? options.zfs === 'true' : undefined);
|
|
632
|
+
console.log(fmt.success('Storage box updated.'));
|
|
633
|
+
}));
|
|
634
|
+
storagebox
|
|
635
|
+
.command('reset-password <id>')
|
|
636
|
+
.description('Reset storage box password')
|
|
637
|
+
.action(asyncAction(async (client, boxId) => {
|
|
638
|
+
const { password } = await client.resetStorageBoxPassword(parseInt(boxId));
|
|
639
|
+
console.log(fmt.success('Password reset.'));
|
|
640
|
+
console.log(`New password: ${fmt.colorize(password, 'yellow')}`);
|
|
641
|
+
}));
|
|
642
|
+
const snapshot = storagebox.command('snapshot').description('Storage box snapshot management');
|
|
643
|
+
snapshot
|
|
644
|
+
.command('list <box-id>')
|
|
645
|
+
.alias('ls')
|
|
646
|
+
.description('List storage box snapshots')
|
|
647
|
+
.action(asyncAction(async (client, boxId, options) => {
|
|
648
|
+
const snapshots = await client.listStorageBoxSnapshots(parseInt(boxId));
|
|
649
|
+
output(snapshots, fmt.formatStorageBoxSnapshots, options);
|
|
650
|
+
}));
|
|
651
|
+
snapshot
|
|
652
|
+
.command('create <box-id>')
|
|
653
|
+
.description('Create a new snapshot')
|
|
654
|
+
.action(asyncAction(async (client, boxId) => {
|
|
655
|
+
const { snapshot: snap } = await client.createStorageBoxSnapshot(parseInt(boxId));
|
|
656
|
+
console.log(fmt.success(`Snapshot created: ${snap.name}`));
|
|
657
|
+
}));
|
|
658
|
+
snapshot
|
|
659
|
+
.command('delete <box-id> <name>')
|
|
660
|
+
.alias('rm')
|
|
661
|
+
.description('Delete a snapshot')
|
|
662
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
663
|
+
.action(asyncAction(async (client, boxId, snapshotName, options) => {
|
|
664
|
+
if (!await confirmAction(`Delete snapshot ${snapshotName}?`, options))
|
|
665
|
+
return;
|
|
666
|
+
await client.deleteStorageBoxSnapshot(parseInt(boxId), snapshotName);
|
|
667
|
+
console.log(fmt.success('Snapshot deleted.'));
|
|
668
|
+
}));
|
|
669
|
+
snapshot
|
|
670
|
+
.command('revert <box-id> <name>')
|
|
671
|
+
.description('Revert to a snapshot')
|
|
672
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
673
|
+
.action(asyncAction(async (client, boxId, snapshotName, options) => {
|
|
674
|
+
if (!options.yes) {
|
|
675
|
+
console.log(fmt.warning('This will revert your storage box to the snapshot state.'));
|
|
676
|
+
}
|
|
677
|
+
if (!await confirmAction(`Revert to snapshot ${snapshotName}?`, options))
|
|
678
|
+
return;
|
|
679
|
+
await client.revertStorageBoxSnapshot(parseInt(boxId), snapshotName);
|
|
680
|
+
console.log(fmt.success('Reverted to snapshot.'));
|
|
681
|
+
}));
|
|
682
|
+
const subaccount = storagebox.command('subaccount').description('Storage box subaccount management');
|
|
683
|
+
subaccount
|
|
684
|
+
.command('list <box-id>')
|
|
685
|
+
.alias('ls')
|
|
686
|
+
.description('List storage box subaccounts')
|
|
687
|
+
.action(asyncAction(async (client, boxId, options) => {
|
|
688
|
+
const subaccounts = await client.listStorageBoxSubaccounts(parseInt(boxId));
|
|
689
|
+
output(subaccounts, fmt.formatStorageBoxSubaccounts, options);
|
|
690
|
+
}));
|
|
691
|
+
subaccount
|
|
692
|
+
.command('create <box-id> <home-directory>')
|
|
693
|
+
.description('Create a new subaccount')
|
|
694
|
+
.option('--samba <enabled>', 'Enable Samba')
|
|
695
|
+
.option('--ssh <enabled>', 'Enable SSH')
|
|
696
|
+
.option('--webdav <enabled>', 'Enable WebDAV')
|
|
697
|
+
.option('--external <enabled>', 'Enable external reachability')
|
|
698
|
+
.option('--readonly <enabled>', 'Read-only access')
|
|
699
|
+
.option('--comment <comment>', 'Comment')
|
|
700
|
+
.action(asyncAction(async (client, boxId, homeDir, options) => {
|
|
701
|
+
const { subaccount: sub } = await client.createStorageBoxSubaccount(parseInt(boxId), homeDir, options.samba ? options.samba === 'true' : undefined, options.ssh ? options.ssh === 'true' : undefined, options.external ? options.external === 'true' : undefined, options.webdav ? options.webdav === 'true' : undefined, options.readonly ? options.readonly === 'true' : undefined, options.comment);
|
|
702
|
+
console.log(fmt.success(`Subaccount created: ${sub.username}`));
|
|
703
|
+
}));
|
|
704
|
+
subaccount
|
|
705
|
+
.command('delete <box-id> <username>')
|
|
706
|
+
.alias('rm')
|
|
707
|
+
.description('Delete a subaccount')
|
|
708
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
709
|
+
.action(asyncAction(async (client, boxId, username, options) => {
|
|
710
|
+
if (!await confirmAction(`Delete subaccount ${username}?`, options))
|
|
711
|
+
return;
|
|
712
|
+
await client.deleteStorageBoxSubaccount(parseInt(boxId), username);
|
|
713
|
+
console.log(fmt.success('Subaccount deleted.'));
|
|
714
|
+
}));
|
|
715
|
+
const traffic = program.command('traffic').description('Traffic analytics');
|
|
716
|
+
traffic
|
|
717
|
+
.command('query')
|
|
718
|
+
.description('Query traffic data')
|
|
719
|
+
.option('-i, --ip <ips...>', 'IP addresses to query')
|
|
720
|
+
.option('-s, --subnet <subnets...>', 'Subnets to query')
|
|
721
|
+
.option('--from <date>', 'Start date (YYYY-MM-DD)')
|
|
722
|
+
.option('--to <date>', 'End date (YYYY-MM-DD)')
|
|
723
|
+
.option('-t, --type <type>', 'Query type (day, month, year)', 'month')
|
|
724
|
+
.action(asyncAction(async (client, options) => {
|
|
725
|
+
const now = new Date();
|
|
726
|
+
const from = options.from || new Date(now.getFullYear(), now.getMonth(), 1).toISOString().split('T')[0];
|
|
727
|
+
const to = options.to || now.toISOString().split('T')[0];
|
|
728
|
+
const { traffic: trafficData } = await client.getTraffic(options.ip || [], options.subnet || [], from, to, options.type);
|
|
729
|
+
output(trafficData, fmt.formatTraffic, options);
|
|
730
|
+
}));
|
|
731
|
+
const wol = program.command('wol').description('Wake on LAN');
|
|
732
|
+
wol
|
|
733
|
+
.command('status <server>')
|
|
734
|
+
.description('Check WoL status for server')
|
|
735
|
+
.action(asyncAction(async (client, serverIdOrIp, options) => {
|
|
736
|
+
const { wol: wolData } = await client.getWol(serverIdOrIp);
|
|
737
|
+
output(wolData, (w) => `Server: ${w.server_number} (${w.server_ip})\n${fmt.info('Wake on LAN is available for this server.')}`, options);
|
|
738
|
+
}));
|
|
739
|
+
wol
|
|
740
|
+
.command('send <server>')
|
|
741
|
+
.description('Send Wake on LAN packet')
|
|
742
|
+
.action(asyncAction(async (client, serverIdOrIp) => {
|
|
743
|
+
const { wol: wolData } = await client.sendWol(serverIdOrIp);
|
|
744
|
+
console.log(fmt.formatWolResult(wolData));
|
|
745
|
+
}));
|
|
746
|
+
const cancel = program.command('cancel').description('Server cancellation');
|
|
747
|
+
cancel
|
|
748
|
+
.command('status <server>')
|
|
749
|
+
.description('Get cancellation status')
|
|
750
|
+
.action(asyncAction(async (client, serverIdOrIp, options) => {
|
|
751
|
+
const { cancellation } = await client.getCancellation(serverIdOrIp);
|
|
752
|
+
output(cancellation, fmt.formatCancellation, options);
|
|
753
|
+
}));
|
|
754
|
+
cancel
|
|
755
|
+
.command('request <server>')
|
|
756
|
+
.description('Request server cancellation')
|
|
757
|
+
.option('--date <date>', 'Cancellation date (YYYY-MM-DD)')
|
|
758
|
+
.option('--reason <reasons...>', 'Cancellation reasons')
|
|
759
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
760
|
+
.action(asyncAction(async (client, serverIdOrIp, options) => {
|
|
761
|
+
if (!options.yes) {
|
|
762
|
+
console.log(fmt.warning('This will cancel your server!'));
|
|
763
|
+
}
|
|
764
|
+
if (!await confirmAction('Are you sure you want to cancel this server?', options))
|
|
765
|
+
return;
|
|
766
|
+
const { cancellation } = await client.cancelServer(serverIdOrIp, options.date, options.reason);
|
|
767
|
+
console.log(fmt.success('Cancellation requested.'));
|
|
768
|
+
console.log(fmt.formatCancellation(cancellation));
|
|
769
|
+
}));
|
|
770
|
+
cancel
|
|
771
|
+
.command('revoke <server>')
|
|
772
|
+
.description('Revoke server cancellation')
|
|
773
|
+
.action(asyncAction(async (client, serverIdOrIp) => {
|
|
774
|
+
await client.revokeCancellation(serverIdOrIp);
|
|
775
|
+
console.log(fmt.success('Cancellation revoked.'));
|
|
776
|
+
}));
|
|
777
|
+
const order = program.command('order').description('Server ordering');
|
|
778
|
+
order
|
|
779
|
+
.command('products')
|
|
780
|
+
.description('List available server products')
|
|
781
|
+
.action(asyncAction(async (client, options) => {
|
|
782
|
+
const products = await client.listServerProducts();
|
|
783
|
+
output(products, fmt.formatServerProductList, options);
|
|
784
|
+
}));
|
|
785
|
+
order
|
|
786
|
+
.command('market')
|
|
787
|
+
.description('List server market (auction) products')
|
|
788
|
+
.action(asyncAction(async (client, options) => {
|
|
789
|
+
const products = await client.listServerMarketProducts();
|
|
790
|
+
output(products, fmt.formatServerMarketProductList, options);
|
|
791
|
+
}));
|
|
792
|
+
order
|
|
793
|
+
.command('transactions')
|
|
794
|
+
.description('List order transactions')
|
|
795
|
+
.action(asyncAction(async (client, options) => {
|
|
796
|
+
const transactions = await client.listServerTransactions();
|
|
797
|
+
output(transactions, fmt.formatTransactionList, options);
|
|
798
|
+
}));
|
|
799
|
+
order
|
|
800
|
+
.command('transaction <id>')
|
|
801
|
+
.description('Get order transaction details')
|
|
802
|
+
.action(asyncAction(async (client, transactionId, options) => {
|
|
803
|
+
const { transaction } = await client.getServerTransaction(transactionId);
|
|
804
|
+
output(transaction, (t) => fmt.formatTransactionList([{ transaction: t }]), options);
|
|
805
|
+
}));
|
|
806
|
+
program
|
|
807
|
+
.command('interactive')
|
|
808
|
+
.alias('i')
|
|
809
|
+
.description('Interactive mode for common operations')
|
|
810
|
+
.action(asyncAction(async (client) => {
|
|
811
|
+
while (true) {
|
|
812
|
+
const action = await select({
|
|
813
|
+
message: 'What would you like to do?',
|
|
814
|
+
choices: [
|
|
815
|
+
{ value: 'servers', name: 'List servers' },
|
|
816
|
+
{ value: 'reset', name: 'Reset a server' },
|
|
817
|
+
{ value: 'rescue', name: 'Activate rescue mode' },
|
|
818
|
+
{ value: 'failover', name: 'Switch failover IP' },
|
|
819
|
+
{ value: 'keys', name: 'Manage SSH keys' },
|
|
820
|
+
{ value: 'exit', name: 'Exit' },
|
|
821
|
+
],
|
|
822
|
+
pageSize: 10,
|
|
823
|
+
});
|
|
824
|
+
if (action === 'exit') {
|
|
825
|
+
console.log('Goodbye!');
|
|
826
|
+
break;
|
|
827
|
+
}
|
|
828
|
+
if (action === 'servers') {
|
|
829
|
+
const servers = await client.listServers();
|
|
830
|
+
console.log('');
|
|
831
|
+
console.log(fmt.formatServerList(servers));
|
|
832
|
+
console.log('');
|
|
833
|
+
}
|
|
834
|
+
if (action === 'reset') {
|
|
835
|
+
const servers = await client.listServers();
|
|
836
|
+
if (servers.length === 0) {
|
|
837
|
+
console.log(fmt.info('No servers found.'));
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
const selected = await checkbox({
|
|
841
|
+
message: 'Select servers to reset:',
|
|
842
|
+
choices: servers.map(({ server }) => ({
|
|
843
|
+
value: server.server_ip,
|
|
844
|
+
name: `${server.server_number} - ${server.server_ip} (${server.server_name || 'unnamed'})`,
|
|
845
|
+
})),
|
|
846
|
+
});
|
|
847
|
+
if (selected.length === 0) {
|
|
848
|
+
console.log('No servers selected.');
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
const resetType = (await select({
|
|
852
|
+
message: 'Select reset type:',
|
|
853
|
+
choices: [
|
|
854
|
+
{ value: 'sw', name: 'Software reset (ACPI)' },
|
|
855
|
+
{ value: 'hw', name: 'Hardware reset (forced)' },
|
|
856
|
+
{ value: 'power', name: 'Power cycle' },
|
|
857
|
+
],
|
|
858
|
+
}));
|
|
859
|
+
const confirmed = await confirm({
|
|
860
|
+
message: `Reset ${selected.length} server(s) with ${resetType}?`,
|
|
861
|
+
default: false,
|
|
862
|
+
});
|
|
863
|
+
if (confirmed) {
|
|
864
|
+
for (const srv of selected) {
|
|
865
|
+
try {
|
|
866
|
+
const { reset: rst } = await client.resetServer(srv, resetType);
|
|
867
|
+
console.log(fmt.formatResetResult(rst, resetType));
|
|
868
|
+
}
|
|
869
|
+
catch (error) {
|
|
870
|
+
console.log(fmt.error(`Failed to reset ${srv}: ${error instanceof Error ? error.message : 'Unknown'}`));
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
if (action === 'rescue') {
|
|
876
|
+
const servers = await client.listServers();
|
|
877
|
+
if (servers.length === 0) {
|
|
878
|
+
console.log(fmt.info('No servers found.'));
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
const serverIp = await select({
|
|
882
|
+
message: 'Select server:',
|
|
883
|
+
choices: servers.map(({ server }) => ({
|
|
884
|
+
value: server.server_ip,
|
|
885
|
+
name: `${server.server_number} - ${server.server_ip} (${server.server_name || 'unnamed'})`,
|
|
886
|
+
})),
|
|
887
|
+
});
|
|
888
|
+
const os = await select({
|
|
889
|
+
message: 'Select rescue OS:',
|
|
890
|
+
choices: [
|
|
891
|
+
{ value: 'linux', name: 'Linux (64-bit)' },
|
|
892
|
+
{ value: 'linuxold', name: 'Linux (32-bit)' },
|
|
893
|
+
{ value: 'vkvm', name: 'vKVM' },
|
|
894
|
+
],
|
|
895
|
+
});
|
|
896
|
+
const { rescue } = await client.activateRescue(serverIp, os, 64);
|
|
897
|
+
console.log('');
|
|
898
|
+
console.log(fmt.formatRescueActivation(rescue));
|
|
899
|
+
}
|
|
900
|
+
if (action === 'failover') {
|
|
901
|
+
const failovers = await client.listFailovers();
|
|
902
|
+
if (failovers.length === 0) {
|
|
903
|
+
console.log(fmt.info('No failover IPs found.'));
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
906
|
+
const failoverIp = await select({
|
|
907
|
+
message: 'Select failover IP:',
|
|
908
|
+
choices: failovers.map(({ failover }) => ({
|
|
909
|
+
value: failover.ip,
|
|
910
|
+
name: `${failover.ip} -> ${failover.active_server_ip}`,
|
|
911
|
+
})),
|
|
912
|
+
});
|
|
913
|
+
const servers = await client.listServers();
|
|
914
|
+
const targetIp = await select({
|
|
915
|
+
message: 'Route to server:',
|
|
916
|
+
choices: servers.map(({ server }) => ({
|
|
917
|
+
value: server.server_ip,
|
|
918
|
+
name: `${server.server_number} - ${server.server_ip}`,
|
|
919
|
+
})),
|
|
920
|
+
});
|
|
921
|
+
const { failover: fo } = await client.switchFailover(failoverIp, targetIp);
|
|
922
|
+
console.log('');
|
|
923
|
+
console.log(fmt.formatFailoverSwitch(fo));
|
|
924
|
+
}
|
|
925
|
+
if (action === 'keys') {
|
|
926
|
+
const keys = await client.listSshKeys();
|
|
927
|
+
console.log('');
|
|
928
|
+
console.log(fmt.formatSshKeyList(keys));
|
|
929
|
+
console.log('');
|
|
930
|
+
}
|
|
931
|
+
console.log('');
|
|
932
|
+
}
|
|
933
|
+
}));
|
|
934
|
+
program.parse();
|