elestio 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +279 -264
- package/bin/elestio.js +5 -5
- package/package.json +42 -42
- package/src/api.js +105 -105
- package/src/cli.js +838 -829
- package/src/commands/access.js +132 -132
- package/src/commands/actions.js +427 -427
- package/src/commands/auth.js +146 -146
- package/src/commands/backups.js +215 -215
- package/src/commands/billing.js +106 -106
- package/src/commands/cicd.js +486 -484
- package/src/commands/projects.js +145 -145
- package/src/commands/services.js +294 -294
- package/src/commands/templates.js +188 -188
- package/src/commands/volumes.js +117 -117
- package/src/config.js +84 -84
- package/src/utils.js +162 -162
|
@@ -1,188 +1,188 @@
|
|
|
1
|
-
import { apiRequestNoAuth } from '../api.js';
|
|
2
|
-
import { formatTable, colors, truncate, outputJson } from '../utils.js';
|
|
3
|
-
|
|
4
|
-
let templatesCache = null;
|
|
5
|
-
let sizesCache = null;
|
|
6
|
-
|
|
7
|
-
export async function getTemplates() {
|
|
8
|
-
if (templatesCache) return templatesCache;
|
|
9
|
-
const response = await apiRequestNoAuth('/api/servers/getTemplates');
|
|
10
|
-
templatesCache = response.instances || [];
|
|
11
|
-
return templatesCache;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export async function searchTemplates(query, category = null) {
|
|
15
|
-
const templates = await getTemplates();
|
|
16
|
-
const q = query?.toLowerCase() || '';
|
|
17
|
-
|
|
18
|
-
return templates.filter(t => {
|
|
19
|
-
const matchesQuery = !q ||
|
|
20
|
-
t.title?.toLowerCase().includes(q) ||
|
|
21
|
-
t.description?.toLowerCase().includes(q);
|
|
22
|
-
const matchesCategory = !category ||
|
|
23
|
-
t.category?.toLowerCase().includes(category.toLowerCase());
|
|
24
|
-
return matchesQuery && matchesCategory;
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const TEMPLATE_ALIASES = {
|
|
29
|
-
'cicd': 'CI-CD-Target',
|
|
30
|
-
'ci-cd': 'CI-CD-Target',
|
|
31
|
-
'postgres': 'PostgreSQL',
|
|
32
|
-
'pg': 'PostgreSQL',
|
|
33
|
-
'mysql': 'MySQL',
|
|
34
|
-
'mariadb': 'MariaDB',
|
|
35
|
-
'mongo': 'MongoDB',
|
|
36
|
-
'mongodb': 'MongoDB',
|
|
37
|
-
'elastic': 'Elasticsearch',
|
|
38
|
-
'elasticsearch': 'Elasticsearch',
|
|
39
|
-
'wp': 'Wordpress',
|
|
40
|
-
'wordpress': 'Wordpress',
|
|
41
|
-
'k8s': 'K3S',
|
|
42
|
-
'kubernetes': 'K3S'
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
export async function findTemplate(nameOrId) {
|
|
46
|
-
const templates = await getTemplates();
|
|
47
|
-
|
|
48
|
-
const byId = templates.find(t => String(t.id) === String(nameOrId));
|
|
49
|
-
if (byId) return byId;
|
|
50
|
-
|
|
51
|
-
const aliasedName = TEMPLATE_ALIASES[nameOrId?.toLowerCase()];
|
|
52
|
-
if (aliasedName) {
|
|
53
|
-
const byAlias = templates.find(t => t.title?.toLowerCase() === aliasedName.toLowerCase());
|
|
54
|
-
if (byAlias) return byAlias;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const byTitle = templates.find(t => t.title?.toLowerCase() === nameOrId?.toLowerCase());
|
|
58
|
-
if (byTitle) return byTitle;
|
|
59
|
-
|
|
60
|
-
return templates.find(t => t.title?.toLowerCase().includes(nameOrId?.toLowerCase()));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export async function listTemplates(category = null, json = false) {
|
|
64
|
-
const templates = category
|
|
65
|
-
? await searchTemplates(null, category)
|
|
66
|
-
: await getTemplates();
|
|
67
|
-
|
|
68
|
-
if (json) {
|
|
69
|
-
outputJson(templates.map(t => ({ id: t.id, title: t.title, category: t.category, version: t.version || 'latest' })));
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const columns = [
|
|
74
|
-
{ key: 'id', label: 'ID' },
|
|
75
|
-
{ key: 'title', label: 'Name' },
|
|
76
|
-
{ key: 'category', label: 'Category' },
|
|
77
|
-
{ key: 'version', label: 'Version' }
|
|
78
|
-
];
|
|
79
|
-
|
|
80
|
-
const data = templates.map(t => ({
|
|
81
|
-
id: t.id,
|
|
82
|
-
title: t.title,
|
|
83
|
-
category: truncate(t.category, 25),
|
|
84
|
-
version: t.version || 'latest'
|
|
85
|
-
}));
|
|
86
|
-
|
|
87
|
-
console.log(`\n${colors.bold}Templates (${templates.length})${colors.reset}\n`);
|
|
88
|
-
console.log(formatTable(data, columns));
|
|
89
|
-
console.log('');
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export async function getServerSizes() {
|
|
93
|
-
if (sizesCache) return sizesCache;
|
|
94
|
-
const response = await apiRequestNoAuth('/api/servers/getServerSizes');
|
|
95
|
-
sizesCache = response.instances || [];
|
|
96
|
-
return sizesCache;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export async function filterSizes(provider = null, country = null) {
|
|
100
|
-
const sizes = await getServerSizes();
|
|
101
|
-
return sizes.filter(s => {
|
|
102
|
-
const matchesProvider = !provider || s.providerName?.toLowerCase() === provider.toLowerCase();
|
|
103
|
-
const matchesCountry = !country ||
|
|
104
|
-
s.Country?.toLowerCase().includes(country.toLowerCase()) ||
|
|
105
|
-
s.CountryCode?.toLowerCase() === country.toLowerCase();
|
|
106
|
-
return matchesProvider && matchesCountry;
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export async function validateCombo(provider, datacenter, serverType) {
|
|
111
|
-
const sizes = await getServerSizes();
|
|
112
|
-
return sizes.find(s =>
|
|
113
|
-
s.providerName?.toLowerCase() === provider?.toLowerCase() &&
|
|
114
|
-
s.regionID?.toLowerCase() === datacenter?.toLowerCase() &&
|
|
115
|
-
s.title?.toLowerCase() === serverType?.toLowerCase()
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export async function listSizes(provider = null, country = null, json = false) {
|
|
120
|
-
const sizes = await filterSizes(provider, country);
|
|
121
|
-
|
|
122
|
-
if (json) {
|
|
123
|
-
outputJson(sizes.map(s => ({
|
|
124
|
-
provider: s.providerName,
|
|
125
|
-
region: s.regionID,
|
|
126
|
-
location: `${s.City}, ${s.CountryCode}`,
|
|
127
|
-
size: s.title,
|
|
128
|
-
cpu: s.vCPU,
|
|
129
|
-
ramGB: s.ramGB,
|
|
130
|
-
storageGB: s.storageSizeGB,
|
|
131
|
-
pricePerHour: s.pricePerHour,
|
|
132
|
-
priceMonthly: (parseFloat(s.pricePerHour) * 24 * 30).toFixed(0)
|
|
133
|
-
})));
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const byProvider = {};
|
|
138
|
-
sizes.forEach(s => {
|
|
139
|
-
if (!byProvider[s.providerName]) byProvider[s.providerName] = [];
|
|
140
|
-
byProvider[s.providerName].push(s);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
console.log(`\n${colors.bold}Server Sizes (${sizes.length} options)${colors.reset}\n`);
|
|
144
|
-
|
|
145
|
-
Object.entries(byProvider).forEach(([providerName, providerSizes]) => {
|
|
146
|
-
console.log(`${colors.cyan}${providerName}${colors.reset}`);
|
|
147
|
-
|
|
148
|
-
const columns = [
|
|
149
|
-
{ key: 'regionID', label: 'Region' },
|
|
150
|
-
{ key: 'location', label: 'Location' },
|
|
151
|
-
{ key: 'title', label: 'Size' },
|
|
152
|
-
{ key: 'vCPU', label: 'CPU' },
|
|
153
|
-
{ key: 'ramGB', label: 'RAM' },
|
|
154
|
-
{ key: 'storage', label: 'Storage' },
|
|
155
|
-
{ key: 'price', label: 'Price/mo' }
|
|
156
|
-
];
|
|
157
|
-
|
|
158
|
-
const data = providerSizes.slice(0, 20).map(s => ({
|
|
159
|
-
regionID: s.regionID,
|
|
160
|
-
location: `${s.City}, ${s.CountryCode}`,
|
|
161
|
-
title: s.title,
|
|
162
|
-
vCPU: s.vCPU,
|
|
163
|
-
ramGB: s.ramGB + 'GB',
|
|
164
|
-
storage: s.storageSizeGB + 'GB ' + (s.storageType || ''),
|
|
165
|
-
price: '$' + (parseFloat(s.pricePerHour) * 24 * 30).toFixed(0)
|
|
166
|
-
}));
|
|
167
|
-
|
|
168
|
-
console.log(formatTable(data, columns));
|
|
169
|
-
if (providerSizes.length > 20) {
|
|
170
|
-
console.log(` ... and ${providerSizes.length - 20} more`);
|
|
171
|
-
}
|
|
172
|
-
console.log('');
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
export async function listCategories(json = false) {
|
|
177
|
-
const templates = await getTemplates();
|
|
178
|
-
const categories = [...new Set(templates.map(t => t.category))].filter(Boolean).sort();
|
|
179
|
-
|
|
180
|
-
if (json) {
|
|
181
|
-
outputJson(categories);
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
console.log(`\n${colors.bold}Template Categories${colors.reset}\n`);
|
|
186
|
-
categories.forEach(c => console.log(` ${c}`));
|
|
187
|
-
console.log('');
|
|
188
|
-
}
|
|
1
|
+
import { apiRequestNoAuth } from '../api.js';
|
|
2
|
+
import { formatTable, colors, truncate, outputJson } from '../utils.js';
|
|
3
|
+
|
|
4
|
+
let templatesCache = null;
|
|
5
|
+
let sizesCache = null;
|
|
6
|
+
|
|
7
|
+
export async function getTemplates() {
|
|
8
|
+
if (templatesCache) return templatesCache;
|
|
9
|
+
const response = await apiRequestNoAuth('/api/servers/getTemplates');
|
|
10
|
+
templatesCache = response.instances || [];
|
|
11
|
+
return templatesCache;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function searchTemplates(query, category = null) {
|
|
15
|
+
const templates = await getTemplates();
|
|
16
|
+
const q = query?.toLowerCase() || '';
|
|
17
|
+
|
|
18
|
+
return templates.filter(t => {
|
|
19
|
+
const matchesQuery = !q ||
|
|
20
|
+
t.title?.toLowerCase().includes(q) ||
|
|
21
|
+
t.description?.toLowerCase().includes(q);
|
|
22
|
+
const matchesCategory = !category ||
|
|
23
|
+
t.category?.toLowerCase().includes(category.toLowerCase());
|
|
24
|
+
return matchesQuery && matchesCategory;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const TEMPLATE_ALIASES = {
|
|
29
|
+
'cicd': 'CI-CD-Target',
|
|
30
|
+
'ci-cd': 'CI-CD-Target',
|
|
31
|
+
'postgres': 'PostgreSQL',
|
|
32
|
+
'pg': 'PostgreSQL',
|
|
33
|
+
'mysql': 'MySQL',
|
|
34
|
+
'mariadb': 'MariaDB',
|
|
35
|
+
'mongo': 'MongoDB',
|
|
36
|
+
'mongodb': 'MongoDB',
|
|
37
|
+
'elastic': 'Elasticsearch',
|
|
38
|
+
'elasticsearch': 'Elasticsearch',
|
|
39
|
+
'wp': 'Wordpress',
|
|
40
|
+
'wordpress': 'Wordpress',
|
|
41
|
+
'k8s': 'K3S',
|
|
42
|
+
'kubernetes': 'K3S'
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export async function findTemplate(nameOrId) {
|
|
46
|
+
const templates = await getTemplates();
|
|
47
|
+
|
|
48
|
+
const byId = templates.find(t => String(t.id) === String(nameOrId));
|
|
49
|
+
if (byId) return byId;
|
|
50
|
+
|
|
51
|
+
const aliasedName = TEMPLATE_ALIASES[nameOrId?.toLowerCase()];
|
|
52
|
+
if (aliasedName) {
|
|
53
|
+
const byAlias = templates.find(t => t.title?.toLowerCase() === aliasedName.toLowerCase());
|
|
54
|
+
if (byAlias) return byAlias;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const byTitle = templates.find(t => t.title?.toLowerCase() === nameOrId?.toLowerCase());
|
|
58
|
+
if (byTitle) return byTitle;
|
|
59
|
+
|
|
60
|
+
return templates.find(t => t.title?.toLowerCase().includes(nameOrId?.toLowerCase()));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function listTemplates(category = null, json = false) {
|
|
64
|
+
const templates = category
|
|
65
|
+
? await searchTemplates(null, category)
|
|
66
|
+
: await getTemplates();
|
|
67
|
+
|
|
68
|
+
if (json) {
|
|
69
|
+
outputJson(templates.map(t => ({ id: t.id, title: t.title, category: t.category, version: t.version || 'latest' })));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const columns = [
|
|
74
|
+
{ key: 'id', label: 'ID' },
|
|
75
|
+
{ key: 'title', label: 'Name' },
|
|
76
|
+
{ key: 'category', label: 'Category' },
|
|
77
|
+
{ key: 'version', label: 'Version' }
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
const data = templates.map(t => ({
|
|
81
|
+
id: t.id,
|
|
82
|
+
title: t.title,
|
|
83
|
+
category: truncate(t.category, 25),
|
|
84
|
+
version: t.version || 'latest'
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
console.log(`\n${colors.bold}Templates (${templates.length})${colors.reset}\n`);
|
|
88
|
+
console.log(formatTable(data, columns));
|
|
89
|
+
console.log('');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function getServerSizes() {
|
|
93
|
+
if (sizesCache) return sizesCache;
|
|
94
|
+
const response = await apiRequestNoAuth('/api/servers/getServerSizes');
|
|
95
|
+
sizesCache = response.instances || [];
|
|
96
|
+
return sizesCache;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function filterSizes(provider = null, country = null) {
|
|
100
|
+
const sizes = await getServerSizes();
|
|
101
|
+
return sizes.filter(s => {
|
|
102
|
+
const matchesProvider = !provider || s.providerName?.toLowerCase() === provider.toLowerCase();
|
|
103
|
+
const matchesCountry = !country ||
|
|
104
|
+
s.Country?.toLowerCase().includes(country.toLowerCase()) ||
|
|
105
|
+
s.CountryCode?.toLowerCase() === country.toLowerCase();
|
|
106
|
+
return matchesProvider && matchesCountry;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function validateCombo(provider, datacenter, serverType) {
|
|
111
|
+
const sizes = await getServerSizes();
|
|
112
|
+
return sizes.find(s =>
|
|
113
|
+
s.providerName?.toLowerCase() === provider?.toLowerCase() &&
|
|
114
|
+
s.regionID?.toLowerCase() === datacenter?.toLowerCase() &&
|
|
115
|
+
s.title?.toLowerCase() === serverType?.toLowerCase()
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function listSizes(provider = null, country = null, json = false) {
|
|
120
|
+
const sizes = await filterSizes(provider, country);
|
|
121
|
+
|
|
122
|
+
if (json) {
|
|
123
|
+
outputJson(sizes.map(s => ({
|
|
124
|
+
provider: s.providerName,
|
|
125
|
+
region: s.regionID,
|
|
126
|
+
location: `${s.City}, ${s.CountryCode}`,
|
|
127
|
+
size: s.title,
|
|
128
|
+
cpu: s.vCPU,
|
|
129
|
+
ramGB: s.ramGB,
|
|
130
|
+
storageGB: s.storageSizeGB,
|
|
131
|
+
pricePerHour: s.pricePerHour,
|
|
132
|
+
priceMonthly: (parseFloat(s.pricePerHour) * 24 * 30).toFixed(0)
|
|
133
|
+
})));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const byProvider = {};
|
|
138
|
+
sizes.forEach(s => {
|
|
139
|
+
if (!byProvider[s.providerName]) byProvider[s.providerName] = [];
|
|
140
|
+
byProvider[s.providerName].push(s);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
console.log(`\n${colors.bold}Server Sizes (${sizes.length} options)${colors.reset}\n`);
|
|
144
|
+
|
|
145
|
+
Object.entries(byProvider).forEach(([providerName, providerSizes]) => {
|
|
146
|
+
console.log(`${colors.cyan}${providerName}${colors.reset}`);
|
|
147
|
+
|
|
148
|
+
const columns = [
|
|
149
|
+
{ key: 'regionID', label: 'Region' },
|
|
150
|
+
{ key: 'location', label: 'Location' },
|
|
151
|
+
{ key: 'title', label: 'Size' },
|
|
152
|
+
{ key: 'vCPU', label: 'CPU' },
|
|
153
|
+
{ key: 'ramGB', label: 'RAM' },
|
|
154
|
+
{ key: 'storage', label: 'Storage' },
|
|
155
|
+
{ key: 'price', label: 'Price/mo' }
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
const data = providerSizes.slice(0, 20).map(s => ({
|
|
159
|
+
regionID: s.regionID,
|
|
160
|
+
location: `${s.City}, ${s.CountryCode}`,
|
|
161
|
+
title: s.title,
|
|
162
|
+
vCPU: s.vCPU,
|
|
163
|
+
ramGB: s.ramGB + 'GB',
|
|
164
|
+
storage: s.storageSizeGB + 'GB ' + (s.storageType || ''),
|
|
165
|
+
price: '$' + (parseFloat(s.pricePerHour) * 24 * 30).toFixed(0)
|
|
166
|
+
}));
|
|
167
|
+
|
|
168
|
+
console.log(formatTable(data, columns));
|
|
169
|
+
if (providerSizes.length > 20) {
|
|
170
|
+
console.log(` ... and ${providerSizes.length - 20} more`);
|
|
171
|
+
}
|
|
172
|
+
console.log('');
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function listCategories(json = false) {
|
|
177
|
+
const templates = await getTemplates();
|
|
178
|
+
const categories = [...new Set(templates.map(t => t.category))].filter(Boolean).sort();
|
|
179
|
+
|
|
180
|
+
if (json) {
|
|
181
|
+
outputJson(categories);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
console.log(`\n${colors.bold}Template Categories${colors.reset}\n`);
|
|
186
|
+
categories.forEach(c => console.log(` ${c}`));
|
|
187
|
+
console.log('');
|
|
188
|
+
}
|
package/src/commands/volumes.js
CHANGED
|
@@ -1,117 +1,117 @@
|
|
|
1
|
-
import { apiRequest } from '../api.js';
|
|
2
|
-
import { loadConfig } from '../config.js';
|
|
3
|
-
import { doAction } from './actions.js';
|
|
4
|
-
import { log, colors, formatTable, outputJson } from '../utils.js';
|
|
5
|
-
|
|
6
|
-
export async function listVolumes(projectId = null, json = false) {
|
|
7
|
-
const config = loadConfig();
|
|
8
|
-
const pid = projectId || config.defaultProject;
|
|
9
|
-
if (!pid) throw new Error('Project ID required');
|
|
10
|
-
|
|
11
|
-
const response = await apiRequest('/api/volumes/getVolumes', 'POST', { projectID: String(pid) });
|
|
12
|
-
if (response.status !== 'OK') throw new Error(response.message || 'Failed to list volumes');
|
|
13
|
-
|
|
14
|
-
const volumes = response.data?.volumes || [];
|
|
15
|
-
|
|
16
|
-
if (json) { outputJson(volumes); return volumes; }
|
|
17
|
-
if (volumes.length === 0) { log('info', `No volumes in project ${pid}`); return []; }
|
|
18
|
-
|
|
19
|
-
const columns = [
|
|
20
|
-
{ key: 'id', label: 'ID' },
|
|
21
|
-
{ key: 'name', label: 'Name' },
|
|
22
|
-
{ key: 'size', label: 'Size' },
|
|
23
|
-
{ key: 'provider', label: 'Provider' },
|
|
24
|
-
{ key: 'region', label: 'Region' },
|
|
25
|
-
{ key: 'attached', label: 'Attached' }
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
const data = volumes.map(v => ({
|
|
29
|
-
id: v.volumeID || v.id,
|
|
30
|
-
name: v.volumeName || v.name,
|
|
31
|
-
size: (v.volume || v.size) + 'GB',
|
|
32
|
-
provider: v.providerName || v.provider,
|
|
33
|
-
region: v.datacenter || v.region,
|
|
34
|
-
attached: v.serverID ? 'Yes' : 'No'
|
|
35
|
-
}));
|
|
36
|
-
|
|
37
|
-
console.log(`\n${colors.bold}Volumes (${volumes.length})${colors.reset}\n`);
|
|
38
|
-
console.log(formatTable(data, columns));
|
|
39
|
-
console.log('');
|
|
40
|
-
return volumes;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export async function createVolume(options = {}) {
|
|
44
|
-
const config = loadConfig();
|
|
45
|
-
const { name, size = 10, provider = config.defaults?.provider || 'hetzner', datacenter = config.defaults?.datacenter || 'fsn1', projectId = config.defaultProject, serverId = null, storageType = 'NVME' } = options;
|
|
46
|
-
|
|
47
|
-
if (!name) throw new Error('Volume name required');
|
|
48
|
-
if (!projectId) throw new Error('Project ID required');
|
|
49
|
-
|
|
50
|
-
const price = (size * 0.05).toFixed(2);
|
|
51
|
-
log('info', `Creating volume "${name}" (${size}GB ${storageType} @ ${provider}/${datacenter})...`);
|
|
52
|
-
|
|
53
|
-
const body = {
|
|
54
|
-
projectID: String(projectId), providerName: provider, datacenter,
|
|
55
|
-
volumeName: name, price, isMoveData: false, volume: size, blockStorageType: storageType
|
|
56
|
-
};
|
|
57
|
-
if (serverId) body.selectedServerID = String(serverId);
|
|
58
|
-
|
|
59
|
-
const response = await apiRequest('/api/volumes/createVolume', 'POST', body);
|
|
60
|
-
if (response.status !== 'OK' && !response.volumeID) throw new Error(response.message || 'Failed to create volume');
|
|
61
|
-
|
|
62
|
-
log('success', `Volume "${name}" created`);
|
|
63
|
-
return response;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export async function getServiceVolumes(vmID, json = false) {
|
|
67
|
-
const result = await doAction(vmID, 'getServiceVolume');
|
|
68
|
-
const volumes = Array.isArray(result.data) ? result.data : (result.data?.volumes || result.volumes || []);
|
|
69
|
-
|
|
70
|
-
if (json) { outputJson(volumes); return volumes; }
|
|
71
|
-
if (volumes.length === 0) { log('info', 'No volumes attached'); return []; }
|
|
72
|
-
|
|
73
|
-
console.log(`\n${colors.bold}Attached Volumes${colors.reset}\n`);
|
|
74
|
-
volumes.forEach(v => console.log(` ${v.volumeID || v.id}: ${v.volumeName || v.name} (${v.volumeSizeInGB || v.volume || v.size || 'N/A'}GB)`));
|
|
75
|
-
console.log('');
|
|
76
|
-
return volumes;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export async function createServiceVolume(vmID, options = {}) {
|
|
80
|
-
const { name, size = 10, storageType = 'NVME' } = options;
|
|
81
|
-
if (!name) throw new Error('Volume name required');
|
|
82
|
-
|
|
83
|
-
log('info', `Creating and attaching volume "${name}" to ${vmID}...`);
|
|
84
|
-
const result = await doAction(vmID, 'createServiceVolume', { volumeName: name, volume: size, blockStorageType: storageType, isMoveData: false });
|
|
85
|
-
log('success', `Volume "${name}" created and attached`);
|
|
86
|
-
return result;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export async function resizeVolume(vmID, volumeID, newSize) {
|
|
90
|
-
if (!newSize || newSize < 10) throw new Error('New size must be at least 10GB');
|
|
91
|
-
log('info', `Resizing volume ${volumeID} to ${newSize}GB...`);
|
|
92
|
-
const result = await doAction(vmID, 'resizeServiceVolume', { volumeID: String(volumeID), volume: newSize, volumeName: '', isMoveData: false, currentSize: 0 });
|
|
93
|
-
log('success', `Volume resized to ${newSize}GB`);
|
|
94
|
-
return result;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export async function detachVolume(vmID, volumeID, options = {}) {
|
|
98
|
-
const { keepVolume = true } = options;
|
|
99
|
-
log('info', `Detaching volume ${volumeID} from ${vmID}...`);
|
|
100
|
-
const result = await doAction(vmID, 'detachServiceVolume', { volumeID: String(volumeID), isKeepVolume: keepVolume, isMoveData: false });
|
|
101
|
-
log('success', `Volume detached${keepVolume ? '' : ' and deleted'}`);
|
|
102
|
-
return result;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export async function deleteServiceVolume(vmID, volumeID) {
|
|
106
|
-
log('info', `Deleting volume ${volumeID}...`);
|
|
107
|
-
const result = await doAction(vmID, 'deleteServiceVolume', { volumeID: String(volumeID) });
|
|
108
|
-
log('success', 'Volume deleted');
|
|
109
|
-
return result;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export async function setVolumeProtection(vmID, volumeID, enabled = true) {
|
|
113
|
-
log('info', `${enabled ? 'Enabling' : 'Disabling'} protection for volume ${volumeID}...`);
|
|
114
|
-
const result = await doAction(vmID, 'manageServiceVolumeProtection', { volumeID: String(volumeID), isVolumeProtection: enabled });
|
|
115
|
-
log('success', `Volume protection ${enabled ? 'enabled' : 'disabled'}`);
|
|
116
|
-
return result;
|
|
117
|
-
}
|
|
1
|
+
import { apiRequest } from '../api.js';
|
|
2
|
+
import { loadConfig } from '../config.js';
|
|
3
|
+
import { doAction } from './actions.js';
|
|
4
|
+
import { log, colors, formatTable, outputJson } from '../utils.js';
|
|
5
|
+
|
|
6
|
+
export async function listVolumes(projectId = null, json = false) {
|
|
7
|
+
const config = loadConfig();
|
|
8
|
+
const pid = projectId || config.defaultProject;
|
|
9
|
+
if (!pid) throw new Error('Project ID required');
|
|
10
|
+
|
|
11
|
+
const response = await apiRequest('/api/volumes/getVolumes', 'POST', { projectID: String(pid) });
|
|
12
|
+
if (response.status !== 'OK') throw new Error(response.message || 'Failed to list volumes');
|
|
13
|
+
|
|
14
|
+
const volumes = response.data?.volumes || [];
|
|
15
|
+
|
|
16
|
+
if (json) { outputJson(volumes); return volumes; }
|
|
17
|
+
if (volumes.length === 0) { log('info', `No volumes in project ${pid}`); return []; }
|
|
18
|
+
|
|
19
|
+
const columns = [
|
|
20
|
+
{ key: 'id', label: 'ID' },
|
|
21
|
+
{ key: 'name', label: 'Name' },
|
|
22
|
+
{ key: 'size', label: 'Size' },
|
|
23
|
+
{ key: 'provider', label: 'Provider' },
|
|
24
|
+
{ key: 'region', label: 'Region' },
|
|
25
|
+
{ key: 'attached', label: 'Attached' }
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const data = volumes.map(v => ({
|
|
29
|
+
id: v.volumeID || v.id,
|
|
30
|
+
name: v.volumeName || v.name,
|
|
31
|
+
size: (v.volume || v.size) + 'GB',
|
|
32
|
+
provider: v.providerName || v.provider,
|
|
33
|
+
region: v.datacenter || v.region,
|
|
34
|
+
attached: v.serverID ? 'Yes' : 'No'
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
console.log(`\n${colors.bold}Volumes (${volumes.length})${colors.reset}\n`);
|
|
38
|
+
console.log(formatTable(data, columns));
|
|
39
|
+
console.log('');
|
|
40
|
+
return volumes;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function createVolume(options = {}) {
|
|
44
|
+
const config = loadConfig();
|
|
45
|
+
const { name, size = 10, provider = config.defaults?.provider || 'hetzner', datacenter = config.defaults?.datacenter || 'fsn1', projectId = config.defaultProject, serverId = null, storageType = 'NVME' } = options;
|
|
46
|
+
|
|
47
|
+
if (!name) throw new Error('Volume name required');
|
|
48
|
+
if (!projectId) throw new Error('Project ID required');
|
|
49
|
+
|
|
50
|
+
const price = (size * 0.05).toFixed(2);
|
|
51
|
+
log('info', `Creating volume "${name}" (${size}GB ${storageType} @ ${provider}/${datacenter})...`);
|
|
52
|
+
|
|
53
|
+
const body = {
|
|
54
|
+
projectID: String(projectId), providerName: provider, datacenter,
|
|
55
|
+
volumeName: name, price, isMoveData: false, volume: size, blockStorageType: storageType
|
|
56
|
+
};
|
|
57
|
+
if (serverId) body.selectedServerID = String(serverId);
|
|
58
|
+
|
|
59
|
+
const response = await apiRequest('/api/volumes/createVolume', 'POST', body);
|
|
60
|
+
if (response.status !== 'OK' && !response.volumeID) throw new Error(response.message || 'Failed to create volume');
|
|
61
|
+
|
|
62
|
+
log('success', `Volume "${name}" created`);
|
|
63
|
+
return response;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function getServiceVolumes(vmID, json = false) {
|
|
67
|
+
const result = await doAction(vmID, 'getServiceVolume');
|
|
68
|
+
const volumes = Array.isArray(result.data) ? result.data : (result.data?.volumes || result.volumes || []);
|
|
69
|
+
|
|
70
|
+
if (json) { outputJson(volumes); return volumes; }
|
|
71
|
+
if (volumes.length === 0) { log('info', 'No volumes attached'); return []; }
|
|
72
|
+
|
|
73
|
+
console.log(`\n${colors.bold}Attached Volumes${colors.reset}\n`);
|
|
74
|
+
volumes.forEach(v => console.log(` ${v.volumeID || v.id}: ${v.volumeName || v.name} (${v.volumeSizeInGB || v.volume || v.size || 'N/A'}GB)`));
|
|
75
|
+
console.log('');
|
|
76
|
+
return volumes;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function createServiceVolume(vmID, options = {}) {
|
|
80
|
+
const { name, size = 10, storageType = 'NVME' } = options;
|
|
81
|
+
if (!name) throw new Error('Volume name required');
|
|
82
|
+
|
|
83
|
+
log('info', `Creating and attaching volume "${name}" to ${vmID}...`);
|
|
84
|
+
const result = await doAction(vmID, 'createServiceVolume', { volumeName: name, volume: size, blockStorageType: storageType, isMoveData: false });
|
|
85
|
+
log('success', `Volume "${name}" created and attached`);
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function resizeVolume(vmID, volumeID, newSize) {
|
|
90
|
+
if (!newSize || newSize < 10) throw new Error('New size must be at least 10GB');
|
|
91
|
+
log('info', `Resizing volume ${volumeID} to ${newSize}GB...`);
|
|
92
|
+
const result = await doAction(vmID, 'resizeServiceVolume', { volumeID: String(volumeID), volume: newSize, volumeName: '', isMoveData: false, currentSize: 0 });
|
|
93
|
+
log('success', `Volume resized to ${newSize}GB`);
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function detachVolume(vmID, volumeID, options = {}) {
|
|
98
|
+
const { keepVolume = true } = options;
|
|
99
|
+
log('info', `Detaching volume ${volumeID} from ${vmID}...`);
|
|
100
|
+
const result = await doAction(vmID, 'detachServiceVolume', { volumeID: String(volumeID), isKeepVolume: keepVolume, isMoveData: false });
|
|
101
|
+
log('success', `Volume detached${keepVolume ? '' : ' and deleted'}`);
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function deleteServiceVolume(vmID, volumeID) {
|
|
106
|
+
log('info', `Deleting volume ${volumeID}...`);
|
|
107
|
+
const result = await doAction(vmID, 'deleteServiceVolume', { volumeID: String(volumeID) });
|
|
108
|
+
log('success', 'Volume deleted');
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function setVolumeProtection(vmID, volumeID, enabled = true) {
|
|
113
|
+
log('info', `${enabled ? 'Enabling' : 'Disabling'} protection for volume ${volumeID}...`);
|
|
114
|
+
const result = await doAction(vmID, 'manageServiceVolumeProtection', { volumeID: String(volumeID), isVolumeProtection: enabled });
|
|
115
|
+
log('success', `Volume protection ${enabled ? 'enabled' : 'disabled'}`);
|
|
116
|
+
return result;
|
|
117
|
+
}
|