@celilo/cli 0.1.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/README.md +1566 -0
- package/bin/celilo +16 -0
- package/drizzle/0000_complex_puma.sql +179 -0
- package/drizzle/0001_dizzy_wolfpack.sql +2 -0
- package/drizzle/0002_web_routes.sql +16 -0
- package/drizzle/0003_backup_storage.sql +32 -0
- package/drizzle/meta/0000_snapshot.json +1151 -0
- package/drizzle/meta/0001_snapshot.json +1167 -0
- package/drizzle/meta/0002_snapshot.json +1257 -0
- package/drizzle/meta/_journal.json +27 -0
- package/package.json +64 -0
- package/schemas/system_config.json +106 -0
- package/src/__integration__/container-services-cli.integration.test.ts +246 -0
- package/src/ansible/dependencies.test.ts +309 -0
- package/src/ansible/dependencies.ts +896 -0
- package/src/ansible/inventory.test.ts +463 -0
- package/src/ansible/inventory.ts +445 -0
- package/src/ansible/secrets.ts +222 -0
- package/src/ansible/validation.test.ts +92 -0
- package/src/ansible/validation.ts +272 -0
- package/src/api-clients/digitalocean.ts +94 -0
- package/src/api-clients/proxmox.ts +655 -0
- package/src/capabilities/logging-wrapper.test.ts +217 -0
- package/src/capabilities/lookup.test.ts +149 -0
- package/src/capabilities/lookup.ts +89 -0
- package/src/capabilities/public-web-helpers.test.ts +198 -0
- package/src/capabilities/public-web-publish.test.ts +458 -0
- package/src/capabilities/registration.test.ts +395 -0
- package/src/capabilities/registration.ts +200 -0
- package/src/capabilities/route-validation.test.ts +121 -0
- package/src/capabilities/route-validation.ts +96 -0
- package/src/capabilities/secret-ref.test.ts +313 -0
- package/src/capabilities/secret-validation.ts +157 -0
- package/src/capabilities/secrets.test.ts +750 -0
- package/src/capabilities/secrets.ts +244 -0
- package/src/capabilities/validation.test.ts +613 -0
- package/src/capabilities/validation.ts +160 -0
- package/src/capabilities/well-known.test.ts +238 -0
- package/src/capabilities/well-known.ts +222 -0
- package/src/cli/cli.test.ts +654 -0
- package/src/cli/command-registry.ts +742 -0
- package/src/cli/command-tree-parser.test.ts +180 -0
- package/src/cli/command-tree-parser.ts +193 -0
- package/src/cli/commands/backup-create.ts +137 -0
- package/src/cli/commands/backup-delete.ts +74 -0
- package/src/cli/commands/backup-import.ts +97 -0
- package/src/cli/commands/backup-list.ts +132 -0
- package/src/cli/commands/backup-name.ts +73 -0
- package/src/cli/commands/backup-prune.ts +98 -0
- package/src/cli/commands/backup-restore.ts +122 -0
- package/src/cli/commands/capability-info.ts +121 -0
- package/src/cli/commands/capability-list.ts +47 -0
- package/src/cli/commands/completion.ts +87 -0
- package/src/cli/commands/hook-run.ts +176 -0
- package/src/cli/commands/ipam.ts +607 -0
- package/src/cli/commands/machine-add.ts +235 -0
- package/src/cli/commands/machine-earmark.ts +82 -0
- package/src/cli/commands/machine-list.ts +77 -0
- package/src/cli/commands/machine-remove.ts +90 -0
- package/src/cli/commands/machine-status.ts +131 -0
- package/src/cli/commands/module-audit.ts +51 -0
- package/src/cli/commands/module-build.ts +60 -0
- package/src/cli/commands/module-config.ts +170 -0
- package/src/cli/commands/module-deploy.ts +71 -0
- package/src/cli/commands/module-generate.ts +236 -0
- package/src/cli/commands/module-health.ts +108 -0
- package/src/cli/commands/module-import.ts +80 -0
- package/src/cli/commands/module-list.ts +43 -0
- package/src/cli/commands/module-logs.ts +73 -0
- package/src/cli/commands/module-remove.ts +162 -0
- package/src/cli/commands/module-show.ts +208 -0
- package/src/cli/commands/module-status.ts +131 -0
- package/src/cli/commands/module-types.ts +189 -0
- package/src/cli/commands/module-upgrade.ts +192 -0
- package/src/cli/commands/package.ts +68 -0
- package/src/cli/commands/secret-list.ts +99 -0
- package/src/cli/commands/secret-set.ts +134 -0
- package/src/cli/commands/service-add-digitalocean.ts +133 -0
- package/src/cli/commands/service-add-proxmox.ts +342 -0
- package/src/cli/commands/service-config-get.ts +83 -0
- package/src/cli/commands/service-config-set.ts +145 -0
- package/src/cli/commands/service-list.ts +74 -0
- package/src/cli/commands/service-reconfigure.ts +230 -0
- package/src/cli/commands/service-remove.ts +103 -0
- package/src/cli/commands/service-verify.ts +240 -0
- package/src/cli/commands/status.ts +216 -0
- package/src/cli/commands/storage-add-local.ts +106 -0
- package/src/cli/commands/storage-add-s3.ts +114 -0
- package/src/cli/commands/storage-list.ts +72 -0
- package/src/cli/commands/storage-remove.ts +54 -0
- package/src/cli/commands/storage-set-default.ts +44 -0
- package/src/cli/commands/storage-verify.ts +54 -0
- package/src/cli/commands/system-config.ts +168 -0
- package/src/cli/commands/system-init.ts +314 -0
- package/src/cli/commands/system-secret-get.ts +98 -0
- package/src/cli/commands/system-secret-set.ts +76 -0
- package/src/cli/commands/system-vault-password.ts +34 -0
- package/src/cli/completion.test.ts +37 -0
- package/src/cli/completion.ts +482 -0
- package/src/cli/fuel-gauge.test.ts +208 -0
- package/src/cli/fuel-gauge.ts +405 -0
- package/src/cli/generate-zsh-completion.test.ts +95 -0
- package/src/cli/generate-zsh-completion.ts +497 -0
- package/src/cli/index.ts +1583 -0
- package/src/cli/interactive-config.test.ts +201 -0
- package/src/cli/interactive-config.ts +62 -0
- package/src/cli/parser.test.ts +227 -0
- package/src/cli/parser.ts +244 -0
- package/src/cli/prompts.test.ts +33 -0
- package/src/cli/prompts.ts +121 -0
- package/src/cli/types.ts +38 -0
- package/src/cli/validators.test.ts +235 -0
- package/src/cli/validators.ts +188 -0
- package/src/config/env.ts +41 -0
- package/src/config/paths.test.ts +172 -0
- package/src/config/paths.ts +108 -0
- package/src/db/client.ts +190 -0
- package/src/db/migrate.ts +30 -0
- package/src/db/schema.test.ts +221 -0
- package/src/db/schema.ts +434 -0
- package/src/hooks/capability-loader-firewall.test.ts +246 -0
- package/src/hooks/capability-loader.test.ts +100 -0
- package/src/hooks/capability-loader.ts +520 -0
- package/src/hooks/define-hook.test.ts +488 -0
- package/src/hooks/executor.test.ts +462 -0
- package/src/hooks/executor.ts +469 -0
- package/src/hooks/logger.test.ts +54 -0
- package/src/hooks/logger.ts +95 -0
- package/src/hooks/test-fixtures/failing-hook.ts +13 -0
- package/src/hooks/test-fixtures/no-default-hook.ts +6 -0
- package/src/hooks/test-fixtures/success-hook.ts +20 -0
- package/src/hooks/test-fixtures/unbranded-hook.ts +11 -0
- package/src/hooks/test-fixtures/void-hook.ts +13 -0
- package/src/hooks/types.ts +89 -0
- package/src/infrastructure/property-extractor.test.ts +194 -0
- package/src/infrastructure/property-extractor.ts +151 -0
- package/src/ipam/allocator.test.ts +442 -0
- package/src/ipam/allocator.ts +369 -0
- package/src/ipam/auto-allocator.test.ts +247 -0
- package/src/ipam/auto-allocator.ts +270 -0
- package/src/ipam/subnet-parser.test.ts +107 -0
- package/src/ipam/subnet-parser.ts +136 -0
- package/src/manifest/contracts/index.ts +61 -0
- package/src/manifest/contracts/v1.ts +118 -0
- package/src/manifest/json-schema-roundtrip.test.ts +99 -0
- package/src/manifest/schema.ts +367 -0
- package/src/manifest/template-validator.test.ts +231 -0
- package/src/manifest/template-validator.ts +322 -0
- package/src/manifest/validate.test.ts +1180 -0
- package/src/manifest/validate.ts +415 -0
- package/src/module/import.test.ts +355 -0
- package/src/module/import.ts +676 -0
- package/src/module/packaging/audit.ts +169 -0
- package/src/module/packaging/build.ts +228 -0
- package/src/module/packaging/checksum.ts +41 -0
- package/src/module/packaging/extract.ts +234 -0
- package/src/module/packaging/signature.ts +47 -0
- package/src/secrets/encryption.test.ts +284 -0
- package/src/secrets/encryption.ts +162 -0
- package/src/secrets/generators.test.ts +112 -0
- package/src/secrets/generators.ts +127 -0
- package/src/secrets/master-key.test.ts +159 -0
- package/src/secrets/master-key.ts +114 -0
- package/src/secrets/storage.test.ts +115 -0
- package/src/secrets/storage.ts +106 -0
- package/src/secrets/vault.test.ts +35 -0
- package/src/secrets/vault.ts +42 -0
- package/src/services/backup-create.ts +532 -0
- package/src/services/backup-metadata.ts +198 -0
- package/src/services/backup-restore.ts +229 -0
- package/src/services/backup-retention.ts +84 -0
- package/src/services/backup-storage.ts +281 -0
- package/src/services/build-stream.test.ts +122 -0
- package/src/services/build-stream.ts +201 -0
- package/src/services/config-interview.ts +694 -0
- package/src/services/container-service.test.ts +298 -0
- package/src/services/container-service.ts +401 -0
- package/src/services/cross-module-data-manager.test.ts +405 -0
- package/src/services/cross-module-data-manager.ts +412 -0
- package/src/services/deploy-ansible.ts +88 -0
- package/src/services/deploy-planner.ts +153 -0
- package/src/services/deploy-preflight.ts +274 -0
- package/src/services/deploy-ssh.ts +131 -0
- package/src/services/deploy-terraform.test.ts +55 -0
- package/src/services/deploy-terraform.ts +445 -0
- package/src/services/deploy-validation.ts +311 -0
- package/src/services/dns-auto-register.ts +211 -0
- package/src/services/health-runner.ts +184 -0
- package/src/services/infrastructure-selector.test.ts +485 -0
- package/src/services/infrastructure-selector.ts +245 -0
- package/src/services/infrastructure-variable-resolver.test.ts +751 -0
- package/src/services/infrastructure-variable-resolver.ts +234 -0
- package/src/services/machine-detector.ts +328 -0
- package/src/services/machine-pool.test.ts +405 -0
- package/src/services/machine-pool.ts +316 -0
- package/src/services/manifest-validation.ts +120 -0
- package/src/services/module-build.test.ts +290 -0
- package/src/services/module-build.ts +431 -0
- package/src/services/module-config.test.ts +237 -0
- package/src/services/module-config.ts +298 -0
- package/src/services/module-deploy.ts +862 -0
- package/src/services/module-types-drift.test.ts +73 -0
- package/src/services/module-types-generator.test.ts +288 -0
- package/src/services/module-types-generator.ts +189 -0
- package/src/services/proxmox-state-recovery.ts +140 -0
- package/src/services/schema-validation.ts +155 -0
- package/src/services/secret-schema-loader.test.ts +311 -0
- package/src/services/secret-schema-loader.ts +239 -0
- package/src/services/ssh-key-manager.test.ts +283 -0
- package/src/services/ssh-key-manager.ts +193 -0
- package/src/services/storage-providers/local.ts +105 -0
- package/src/services/storage-providers/s3.ts +182 -0
- package/src/services/storage-providers/types.ts +24 -0
- package/src/services/system-config-schema-types.ts +25 -0
- package/src/services/system-config-validator.test.ts +160 -0
- package/src/services/system-config-validator.ts +74 -0
- package/src/services/system-init.test.ts +153 -0
- package/src/services/system-init.ts +253 -0
- package/src/services/terraform-safety.ts +174 -0
- package/src/services/zone-detector.test.ts +110 -0
- package/src/services/zone-detector.ts +102 -0
- package/src/services/zone-policy.test.ts +97 -0
- package/src/services/zone-policy.ts +126 -0
- package/src/templates/generator.test.ts +645 -0
- package/src/templates/generator.ts +1119 -0
- package/src/templates/types.ts +62 -0
- package/src/test-utils/INTERACTIVE_PROMPTS.md +167 -0
- package/src/test-utils/cli-context-interactive.test.ts +152 -0
- package/src/test-utils/cli-context-server.test.ts +66 -0
- package/src/test-utils/cli-context.test.ts +273 -0
- package/src/test-utils/cli-context.ts +677 -0
- package/src/test-utils/cli-result.test.ts +282 -0
- package/src/test-utils/cli-result.ts +241 -0
- package/src/test-utils/cli.ts +55 -0
- package/src/test-utils/completion-harness.test.ts +126 -0
- package/src/test-utils/completion-harness.ts +82 -0
- package/src/test-utils/database.test.ts +182 -0
- package/src/test-utils/database.ts +126 -0
- package/src/test-utils/filesystem.test.ts +208 -0
- package/src/test-utils/filesystem.ts +142 -0
- package/src/test-utils/fixtures.test.ts +123 -0
- package/src/test-utils/fixtures.ts +160 -0
- package/src/test-utils/golden-diff.ts +197 -0
- package/src/test-utils/index.ts +77 -0
- package/src/test-utils/integration.ts +81 -0
- package/src/test-utils/module-fixtures.ts +468 -0
- package/src/test-utils/modules.test.ts +144 -0
- package/src/test-utils/modules.ts +183 -0
- package/src/test-utils/setup-test-db.ts +90 -0
- package/src/test-utils/value-extractor.test.ts +231 -0
- package/src/test-utils/value-extractor.ts +228 -0
- package/src/types/infrastructure.ts +157 -0
- package/src/utils/shell.test.ts +365 -0
- package/src/utils/shell.ts +159 -0
- package/src/validation/schemas.ts +166 -0
- package/src/variables/ansible-resolver.test.ts +142 -0
- package/src/variables/ansible-resolver.ts +69 -0
- package/src/variables/capability-self-ref.test.ts +220 -0
- package/src/variables/context.test.ts +1265 -0
- package/src/variables/context.ts +624 -0
- package/src/variables/declarative-derivation.test.ts +743 -0
- package/src/variables/declarative-derivation.ts +200 -0
- package/src/variables/parser.test.ts +231 -0
- package/src/variables/parser.ts +76 -0
- package/src/variables/resolver.test.ts +458 -0
- package/src/variables/resolver.ts +282 -0
- package/src/variables/types.ts +59 -0
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proxmox API Client
|
|
3
|
+
* Validates connection to Proxmox Virtual Environment
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import https from 'node:https';
|
|
7
|
+
import type { TestResult } from '../types/infrastructure';
|
|
8
|
+
|
|
9
|
+
export interface ProxmoxCredentials {
|
|
10
|
+
api_url: string;
|
|
11
|
+
api_token_id: string;
|
|
12
|
+
api_token_secret: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ProxmoxProviderConfig {
|
|
16
|
+
default_target_node: string;
|
|
17
|
+
lxc_template: string;
|
|
18
|
+
storage: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ProxmoxApiResponse<T> {
|
|
22
|
+
data: T;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface ProxmoxError {
|
|
26
|
+
success: false;
|
|
27
|
+
message: string;
|
|
28
|
+
details?: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type ProxmoxResult<T> = { success: true; data: T } | ProxmoxError;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Make an authenticated API request to Proxmox
|
|
35
|
+
* Helper function for all Proxmox API calls
|
|
36
|
+
*/
|
|
37
|
+
async function makeProxmoxRequest<T>(
|
|
38
|
+
credentials: ProxmoxCredentials,
|
|
39
|
+
path: string,
|
|
40
|
+
): Promise<ProxmoxResult<T>> {
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
try {
|
|
43
|
+
const { api_url, api_token_id, api_token_secret } = credentials;
|
|
44
|
+
const authHeader = `PVEAPIToken=${api_token_id}=${api_token_secret}`;
|
|
45
|
+
const fullUrl = `${api_url}${path}`;
|
|
46
|
+
const url = new URL(fullUrl);
|
|
47
|
+
|
|
48
|
+
if (process.env.DEBUG) {
|
|
49
|
+
console.log(`[Proxmox] Request: ${fullUrl}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const agent = new https.Agent({
|
|
53
|
+
rejectUnauthorized: false,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const req = https.request(
|
|
57
|
+
{
|
|
58
|
+
hostname: url.hostname,
|
|
59
|
+
port: url.port || 443,
|
|
60
|
+
path: url.pathname,
|
|
61
|
+
method: 'GET',
|
|
62
|
+
headers: {
|
|
63
|
+
Authorization: authHeader,
|
|
64
|
+
},
|
|
65
|
+
agent,
|
|
66
|
+
},
|
|
67
|
+
(res) => {
|
|
68
|
+
let body = '';
|
|
69
|
+
|
|
70
|
+
res.on('data', (chunk) => {
|
|
71
|
+
body += chunk;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
res.on('end', () => {
|
|
75
|
+
const statusCode = res.statusCode || 0;
|
|
76
|
+
|
|
77
|
+
if (statusCode === 401) {
|
|
78
|
+
resolve({
|
|
79
|
+
success: false,
|
|
80
|
+
message: 'Authentication failed - check API token credentials',
|
|
81
|
+
details: { status: statusCode },
|
|
82
|
+
});
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
87
|
+
resolve({
|
|
88
|
+
success: false,
|
|
89
|
+
message: `API request failed with status ${statusCode}`,
|
|
90
|
+
details: { status: statusCode, response: body },
|
|
91
|
+
});
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const data = JSON.parse(body) as ProxmoxApiResponse<T>;
|
|
97
|
+
resolve({ success: true, data: data.data });
|
|
98
|
+
} catch (error) {
|
|
99
|
+
resolve({
|
|
100
|
+
success: false,
|
|
101
|
+
message: 'Failed to parse API response',
|
|
102
|
+
details: { error: String(error), body },
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
},
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
req.on('error', (error) => {
|
|
110
|
+
resolve({
|
|
111
|
+
success: false,
|
|
112
|
+
message: `Connection error: ${error.message}`,
|
|
113
|
+
details: { error: String(error) },
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
req.end();
|
|
118
|
+
} catch (error) {
|
|
119
|
+
resolve({
|
|
120
|
+
success: false,
|
|
121
|
+
message: `Request failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
122
|
+
details: { error: String(error) },
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Check if a Proxmox node exists and is online
|
|
130
|
+
*/
|
|
131
|
+
async function checkNodeStatus(
|
|
132
|
+
credentials: ProxmoxCredentials,
|
|
133
|
+
nodeName: string,
|
|
134
|
+
): Promise<ProxmoxResult<{ status: string }>> {
|
|
135
|
+
return makeProxmoxRequest(credentials, `/nodes/${nodeName}/status`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* List all nodes in the Proxmox cluster/standalone
|
|
140
|
+
*/
|
|
141
|
+
async function listNodes(
|
|
142
|
+
credentials: ProxmoxCredentials,
|
|
143
|
+
): Promise<ProxmoxResult<Array<{ node: string; status: string; level?: string }>>> {
|
|
144
|
+
return makeProxmoxRequest(credentials, '/nodes');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Verify node name matches actual Proxmox configuration
|
|
149
|
+
* Returns actual node name and warns if mismatch
|
|
150
|
+
*/
|
|
151
|
+
async function verifyNodeName(
|
|
152
|
+
credentials: ProxmoxCredentials,
|
|
153
|
+
expectedNodeName: string,
|
|
154
|
+
): Promise<ProxmoxResult<{ actual_node: string; matches: boolean }>> {
|
|
155
|
+
const nodesResult = await listNodes(credentials);
|
|
156
|
+
|
|
157
|
+
if (!nodesResult.success) {
|
|
158
|
+
return nodesResult;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const nodes = nodesResult.data;
|
|
162
|
+
const nodeNames = nodes.map((n) => n.node);
|
|
163
|
+
|
|
164
|
+
// Check if expected node exists
|
|
165
|
+
if (!nodeNames.includes(expectedNodeName)) {
|
|
166
|
+
return {
|
|
167
|
+
success: false,
|
|
168
|
+
message: `Node '${expectedNodeName}' not found in Proxmox. Available nodes: ${nodeNames.join(', ')}.\n\nPossible causes:\n 1. Wrong node name configured in Celilo\n 2. Node hostname doesn't match Proxmox registration\n 3. Node not part of this cluster\n\nTo fix:\n 1. Check actual node name: ssh root@<node-ip> 'hostname'\n 2. Update Celilo service to use correct node name\n 3. Or update Proxmox node hostname:\n - hostnamectl set-hostname ${expectedNodeName}\n - Update /etc/hosts: echo '<node-ip> ${expectedNodeName}' >> /etc/hosts\n - Restart: systemctl restart pvedaemon pveproxy`,
|
|
169
|
+
details: {
|
|
170
|
+
expected: expectedNodeName,
|
|
171
|
+
available: nodeNames,
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
success: true,
|
|
178
|
+
data: {
|
|
179
|
+
actual_node: expectedNodeName,
|
|
180
|
+
matches: true,
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Check if storage exists on a node
|
|
187
|
+
*/
|
|
188
|
+
async function checkStorageStatus(
|
|
189
|
+
credentials: ProxmoxCredentials,
|
|
190
|
+
nodeName: string,
|
|
191
|
+
storageName: string,
|
|
192
|
+
): Promise<ProxmoxResult<{ active: number; enabled: number }>> {
|
|
193
|
+
return makeProxmoxRequest(credentials, `/nodes/${nodeName}/storage/${storageName}/status`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Check if an LXC template exists in storage
|
|
198
|
+
*/
|
|
199
|
+
async function checkTemplateExists(
|
|
200
|
+
credentials: ProxmoxCredentials,
|
|
201
|
+
nodeName: string,
|
|
202
|
+
storageName: string,
|
|
203
|
+
templateName: string,
|
|
204
|
+
): Promise<ProxmoxResult<boolean>> {
|
|
205
|
+
const result = await makeProxmoxRequest<Array<{ volid: string; content: string }>>(
|
|
206
|
+
credentials,
|
|
207
|
+
`/nodes/${nodeName}/storage/${storageName}/content`,
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
if (!result.success) {
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Check if any volume matches the template name
|
|
215
|
+
const templateExists = result.data.some((item) => item.volid.includes(templateName));
|
|
216
|
+
|
|
217
|
+
return { success: true, data: templateExists };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Required permissions for Celilo to deploy containers
|
|
222
|
+
* These are the minimum permissions needed for Terraform/Ansible deployment
|
|
223
|
+
*
|
|
224
|
+
* Note: VM.Monitor was removed in Proxmox 9.0+
|
|
225
|
+
* VM.Audit now covers monitoring functionality
|
|
226
|
+
*/
|
|
227
|
+
const REQUIRED_PERMISSIONS = [
|
|
228
|
+
'VM.Allocate', // Create VMs/containers
|
|
229
|
+
'VM.Audit', // View VM status (includes monitoring in Proxmox 9+)
|
|
230
|
+
'VM.Config.CDROM',
|
|
231
|
+
'VM.Config.CPU',
|
|
232
|
+
'VM.Config.Disk',
|
|
233
|
+
'VM.Config.Memory',
|
|
234
|
+
'VM.Config.Network',
|
|
235
|
+
'VM.Config.Options',
|
|
236
|
+
'VM.PowerMgmt', // Start/stop VMs
|
|
237
|
+
'Datastore.AllocateSpace', // Allocate disk space
|
|
238
|
+
'SDN.Use', // Use software-defined networking
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Check if API token has required permissions
|
|
243
|
+
* Returns list of missing permissions
|
|
244
|
+
*/
|
|
245
|
+
async function checkTokenPermissions(
|
|
246
|
+
credentials: ProxmoxCredentials,
|
|
247
|
+
): Promise<ProxmoxResult<{ missing: string[]; granted: string[]; privilegeSeparation?: boolean }>> {
|
|
248
|
+
// Get permissions for the token
|
|
249
|
+
// Proxmox API returns permissions as nested object: { "/path": { "permission": 1 } }
|
|
250
|
+
const result = await makeProxmoxRequest<Record<string, Record<string, number>>>(
|
|
251
|
+
credentials,
|
|
252
|
+
'/access/permissions',
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
if (!result.success) {
|
|
256
|
+
return result;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (process.env.DEBUG) {
|
|
260
|
+
console.log('[Proxmox] Permissions response:', JSON.stringify(result.data, null, 2));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Collect all granted permissions from all paths
|
|
264
|
+
const grantedPermissions = new Set<string>();
|
|
265
|
+
|
|
266
|
+
for (const path of Object.keys(result.data)) {
|
|
267
|
+
const pathPermissions = result.data[path];
|
|
268
|
+
if (typeof pathPermissions === 'object' && pathPermissions !== null) {
|
|
269
|
+
for (const permission of Object.keys(pathPermissions)) {
|
|
270
|
+
grantedPermissions.add(permission);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (process.env.DEBUG) {
|
|
276
|
+
console.log('[Proxmox] Granted permissions:', Array.from(grantedPermissions).sort());
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Check which required permissions are missing
|
|
280
|
+
const missing = REQUIRED_PERMISSIONS.filter((perm) => !grantedPermissions.has(perm));
|
|
281
|
+
const granted = REQUIRED_PERMISSIONS.filter((perm) => grantedPermissions.has(perm));
|
|
282
|
+
|
|
283
|
+
// Detect if token likely has privilege separation (if it has very few permissions)
|
|
284
|
+
// Tokens without privilege separation typically have 40-50+ permissions
|
|
285
|
+
// Tokens WITH separation typically have < 20 permissions (only what's explicitly granted)
|
|
286
|
+
const privilegeSeparation = grantedPermissions.size < 20;
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
success: true,
|
|
290
|
+
data: {
|
|
291
|
+
missing,
|
|
292
|
+
granted,
|
|
293
|
+
privilegeSeparation,
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Test connection to Proxmox API
|
|
300
|
+
* Verifies credentials, connectivity, and optionally checks node/storage/template configuration
|
|
301
|
+
*/
|
|
302
|
+
export async function testProxmoxConnection(
|
|
303
|
+
credentials: ProxmoxCredentials,
|
|
304
|
+
providerConfig?: ProxmoxProviderConfig,
|
|
305
|
+
): Promise<TestResult> {
|
|
306
|
+
const checks: string[] = [];
|
|
307
|
+
|
|
308
|
+
// Step 1: Check version (basic connectivity and authentication)
|
|
309
|
+
const versionResult = await makeProxmoxRequest<{ version: string; release?: string }>(
|
|
310
|
+
credentials,
|
|
311
|
+
'/version',
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
if (!versionResult.success) {
|
|
315
|
+
return {
|
|
316
|
+
success: false,
|
|
317
|
+
message: versionResult.message,
|
|
318
|
+
details: versionResult.details,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
checks.push(`Connected to Proxmox VE ${versionResult.data.version}`);
|
|
323
|
+
|
|
324
|
+
// Step 2: Check API token permissions
|
|
325
|
+
const permissionResult = await checkTokenPermissions(credentials);
|
|
326
|
+
|
|
327
|
+
if (!permissionResult.success) {
|
|
328
|
+
return {
|
|
329
|
+
success: false,
|
|
330
|
+
message: `Failed to check API token permissions: ${permissionResult.message}`,
|
|
331
|
+
details: permissionResult.details,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (permissionResult.data.missing.length > 0) {
|
|
336
|
+
const missingList = permissionResult.data.missing.join(', ');
|
|
337
|
+
const hasSeparation = permissionResult.data.privilegeSeparation;
|
|
338
|
+
|
|
339
|
+
// Build fix instructions based on whether privilege separation is enabled
|
|
340
|
+
let fixInstructions: string;
|
|
341
|
+
if (hasSeparation) {
|
|
342
|
+
fixInstructions =
|
|
343
|
+
'To fix:\n 1. In Proxmox UI: Datacenter → Permissions → API Tokens\n 2. Delete the existing token\n 3. Create a new token with "Privilege Separation" UNCHECKED\n 4. Update Celilo with the new token credentials';
|
|
344
|
+
} else {
|
|
345
|
+
fixInstructions = `To fix:\n 1. In Proxmox UI: Datacenter → Permissions → Users\n 2. Edit the 'root@pam' user\n 3. Grant the missing permissions to the user\n 4. OR check if the token has explicit permission overrides that need updating`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
success: false,
|
|
350
|
+
message: `API token is missing required permissions: ${missingList}\n\nRequired permissions:\n${REQUIRED_PERMISSIONS.map((p) => ` - ${p}`).join('\n')}\n\n${fixInstructions}`,
|
|
351
|
+
details: {
|
|
352
|
+
missing: permissionResult.data.missing,
|
|
353
|
+
granted: permissionResult.data.granted,
|
|
354
|
+
privilegeSeparation: hasSeparation,
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
checks.push('API token has all required permissions');
|
|
360
|
+
|
|
361
|
+
// If no providerConfig, return basic connectivity check
|
|
362
|
+
if (!providerConfig) {
|
|
363
|
+
return {
|
|
364
|
+
success: true,
|
|
365
|
+
message: checks.join('\n✓ '),
|
|
366
|
+
details: {
|
|
367
|
+
version: versionResult.data.version,
|
|
368
|
+
release: versionResult.data.release,
|
|
369
|
+
permissions: {
|
|
370
|
+
granted: permissionResult.data.granted,
|
|
371
|
+
privilegeSeparation: permissionResult.data.privilegeSeparation,
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Step 3: Verify node name matches Proxmox configuration
|
|
378
|
+
const nodeNameResult = await verifyNodeName(credentials, providerConfig.default_target_node);
|
|
379
|
+
|
|
380
|
+
if (!nodeNameResult.success) {
|
|
381
|
+
return {
|
|
382
|
+
success: false,
|
|
383
|
+
message: nodeNameResult.message,
|
|
384
|
+
details: nodeNameResult.details,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
checks.push(`Node '${providerConfig.default_target_node}' found in cluster`);
|
|
389
|
+
|
|
390
|
+
// Step 4: Check node is online
|
|
391
|
+
const nodeResult = await checkNodeStatus(credentials, providerConfig.default_target_node);
|
|
392
|
+
|
|
393
|
+
if (!nodeResult.success) {
|
|
394
|
+
return {
|
|
395
|
+
success: false,
|
|
396
|
+
message: `Node '${providerConfig.default_target_node}' not found or not accessible`,
|
|
397
|
+
details: nodeResult.details,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
checks.push(`Node '${providerConfig.default_target_node}' is online`);
|
|
402
|
+
|
|
403
|
+
// Step 5: Check storage is available
|
|
404
|
+
const storageResult = await checkStorageStatus(
|
|
405
|
+
credentials,
|
|
406
|
+
providerConfig.default_target_node,
|
|
407
|
+
providerConfig.storage,
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
if (!storageResult.success) {
|
|
411
|
+
return {
|
|
412
|
+
success: false,
|
|
413
|
+
message: `Storage '${providerConfig.storage}' not found or not accessible on node '${providerConfig.default_target_node}'`,
|
|
414
|
+
details: storageResult.details,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (!storageResult.data.active || !storageResult.data.enabled) {
|
|
419
|
+
return {
|
|
420
|
+
success: false,
|
|
421
|
+
message: `Storage '${providerConfig.storage}' is not active or enabled`,
|
|
422
|
+
details: { active: storageResult.data.active, enabled: storageResult.data.enabled },
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
checks.push(`Storage '${providerConfig.storage}' is available`);
|
|
427
|
+
|
|
428
|
+
// Step 6: Check template exists
|
|
429
|
+
// Extract storage name and template filename from the full template path
|
|
430
|
+
// Format: "local:vztmpl/ubuntu-22.04-standard_22.04-1_amd64.tar.zst"
|
|
431
|
+
const templateParts = providerConfig.lxc_template.split(':');
|
|
432
|
+
const templateStorage = templateParts[0] || providerConfig.storage;
|
|
433
|
+
const templateFilename = templateParts[1]?.split('/').pop() || providerConfig.lxc_template;
|
|
434
|
+
|
|
435
|
+
const templateResult = await checkTemplateExists(
|
|
436
|
+
credentials,
|
|
437
|
+
providerConfig.default_target_node,
|
|
438
|
+
templateStorage,
|
|
439
|
+
templateFilename,
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
if (!templateResult.success) {
|
|
443
|
+
return {
|
|
444
|
+
success: false,
|
|
445
|
+
message: `Failed to check template availability: ${templateResult.message}`,
|
|
446
|
+
details: templateResult.details,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (!templateResult.data) {
|
|
451
|
+
return {
|
|
452
|
+
success: false,
|
|
453
|
+
message: `Template '${templateFilename}' not found in storage '${templateStorage}'`,
|
|
454
|
+
details: { template: providerConfig.lxc_template },
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
checks.push(`Template '${templateFilename}' found`);
|
|
459
|
+
|
|
460
|
+
// All checks passed
|
|
461
|
+
return {
|
|
462
|
+
success: true,
|
|
463
|
+
message: checks.join('\n✓ '),
|
|
464
|
+
details: {
|
|
465
|
+
version: versionResult.data.version,
|
|
466
|
+
node: providerConfig.default_target_node,
|
|
467
|
+
storage: providerConfig.storage,
|
|
468
|
+
template: templateFilename,
|
|
469
|
+
permissions: {
|
|
470
|
+
granted: permissionResult.data.granted,
|
|
471
|
+
privilegeSeparation: permissionResult.data.privilegeSeparation,
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* List all storage on a node
|
|
479
|
+
*/
|
|
480
|
+
export async function listNodeStorage(
|
|
481
|
+
credentials: ProxmoxCredentials,
|
|
482
|
+
nodeName: string,
|
|
483
|
+
): Promise<
|
|
484
|
+
ProxmoxResult<Array<{ storage: string; content: string; active: number; enabled: number }>>
|
|
485
|
+
> {
|
|
486
|
+
return makeProxmoxRequest(credentials, `/nodes/${nodeName}/storage`);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* List available LXC templates in storage
|
|
491
|
+
*/
|
|
492
|
+
export async function listAvailableTemplates(
|
|
493
|
+
credentials: ProxmoxCredentials,
|
|
494
|
+
nodeName: string,
|
|
495
|
+
storageName: string,
|
|
496
|
+
): Promise<ProxmoxResult<Array<{ volid: string; content: string; size: number }>>> {
|
|
497
|
+
return makeProxmoxRequest(
|
|
498
|
+
credentials,
|
|
499
|
+
`/nodes/${nodeName}/storage/${storageName}/content?content=vztmpl`,
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Download an LXC template from Proxmox repository
|
|
505
|
+
* Returns task ID (UPID) for tracking download progress
|
|
506
|
+
*/
|
|
507
|
+
export async function downloadTemplate(
|
|
508
|
+
credentials: ProxmoxCredentials,
|
|
509
|
+
nodeName: string,
|
|
510
|
+
storageName: string,
|
|
511
|
+
templateUrl: string,
|
|
512
|
+
): Promise<ProxmoxResult<string>> {
|
|
513
|
+
return new Promise((resolve) => {
|
|
514
|
+
try {
|
|
515
|
+
const { api_url, api_token_id, api_token_secret } = credentials;
|
|
516
|
+
const authHeader = `PVEAPIToken=${api_token_id}=${api_token_secret}`;
|
|
517
|
+
const fullUrl = `${api_url}/nodes/${nodeName}/storage/${storageName}/download-url`;
|
|
518
|
+
const url = new URL(fullUrl);
|
|
519
|
+
|
|
520
|
+
if (process.env.DEBUG) {
|
|
521
|
+
console.log(`[Proxmox] Download template: ${fullUrl}`);
|
|
522
|
+
console.log(`[Proxmox] Template URL: ${templateUrl}`);
|
|
523
|
+
console.log(`[Proxmox] Storage: ${storageName}`);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const agent = new https.Agent({
|
|
527
|
+
rejectUnauthorized: false,
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
// POST request with form data
|
|
531
|
+
// Note: Proxmox requires 'filename' parameter for download-url endpoint
|
|
532
|
+
const filename = templateUrl.split('/').pop() || 'template.tar.zst';
|
|
533
|
+
const postData = new URLSearchParams({
|
|
534
|
+
url: templateUrl,
|
|
535
|
+
content: 'vztmpl',
|
|
536
|
+
filename: filename,
|
|
537
|
+
}).toString();
|
|
538
|
+
|
|
539
|
+
const req = https.request(
|
|
540
|
+
{
|
|
541
|
+
hostname: url.hostname,
|
|
542
|
+
port: url.port || 443,
|
|
543
|
+
path: url.pathname,
|
|
544
|
+
method: 'POST',
|
|
545
|
+
headers: {
|
|
546
|
+
Authorization: authHeader,
|
|
547
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
548
|
+
'Content-Length': Buffer.byteLength(postData),
|
|
549
|
+
},
|
|
550
|
+
agent,
|
|
551
|
+
},
|
|
552
|
+
(res) => {
|
|
553
|
+
let body = '';
|
|
554
|
+
|
|
555
|
+
res.on('data', (chunk) => {
|
|
556
|
+
body += chunk;
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
res.on('end', () => {
|
|
560
|
+
const statusCode = res.statusCode || 0;
|
|
561
|
+
|
|
562
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
563
|
+
if (process.env.DEBUG || statusCode === 400) {
|
|
564
|
+
console.error(`[Proxmox] Download failed: ${body}`);
|
|
565
|
+
}
|
|
566
|
+
resolve({
|
|
567
|
+
success: false,
|
|
568
|
+
message: `Download request failed with status ${statusCode}: ${body}`,
|
|
569
|
+
details: { status: statusCode, response: body },
|
|
570
|
+
});
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
try {
|
|
575
|
+
const data = JSON.parse(body) as ProxmoxApiResponse<string>;
|
|
576
|
+
resolve({ success: true, data: data.data });
|
|
577
|
+
} catch (error) {
|
|
578
|
+
resolve({
|
|
579
|
+
success: false,
|
|
580
|
+
message: 'Failed to parse download response',
|
|
581
|
+
details: { error: String(error), body },
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
},
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
req.on('error', (error) => {
|
|
589
|
+
resolve({
|
|
590
|
+
success: false,
|
|
591
|
+
message: `Download request failed: ${error.message}`,
|
|
592
|
+
details: { error: String(error) },
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
req.write(postData);
|
|
597
|
+
req.end();
|
|
598
|
+
} catch (error) {
|
|
599
|
+
resolve({
|
|
600
|
+
success: false,
|
|
601
|
+
message: `Download failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
602
|
+
details: { error: String(error) },
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Check status of a running task (UPID)
|
|
610
|
+
* Returns task status and completion percentage
|
|
611
|
+
*/
|
|
612
|
+
export async function checkTaskStatus(
|
|
613
|
+
credentials: ProxmoxCredentials,
|
|
614
|
+
nodeName: string,
|
|
615
|
+
upid: string,
|
|
616
|
+
): Promise<ProxmoxResult<{ status: string; exitstatus?: string }>> {
|
|
617
|
+
// UPID needs to be URL-encoded
|
|
618
|
+
const encodedUpid = encodeURIComponent(upid);
|
|
619
|
+
return makeProxmoxRequest(credentials, `/nodes/${nodeName}/tasks/${encodedUpid}/status`);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Build template URL for downloading from Proxmox repository
|
|
624
|
+
*/
|
|
625
|
+
export function buildTemplateUrl(ubuntuVersion: string): string {
|
|
626
|
+
// Proxmox mirrors Ubuntu cloud images
|
|
627
|
+
// Format: http://download.proxmox.com/images/system/ubuntu-{version}-standard_{version}-1_amd64.tar.zst
|
|
628
|
+
return `http://download.proxmox.com/images/system/ubuntu-${ubuntuVersion}-standard_${ubuntuVersion}-1_amd64.tar.zst`;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Extract template filename from full template path
|
|
633
|
+
* Format: "local:vztmpl/ubuntu-22.04-standard_22.04-1_amd64.tar.zst" -> "ubuntu-22.04-standard_22.04-1_amd64.tar.zst"
|
|
634
|
+
*/
|
|
635
|
+
export function extractTemplateFilename(templatePath: string): string {
|
|
636
|
+
const parts = templatePath.split(':');
|
|
637
|
+
const filename = parts[1]?.split('/').pop() || templatePath;
|
|
638
|
+
return filename;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Build full template path from components
|
|
643
|
+
*/
|
|
644
|
+
export function buildTemplatePath(storageName: string, ubuntuVersion: string): string {
|
|
645
|
+
const filename = `ubuntu-${ubuntuVersion}-standard_${ubuntuVersion}-1_amd64.tar.zst`;
|
|
646
|
+
return `${storageName}:vztmpl/${filename}`;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Build Proxmox API URL from IP address and port
|
|
651
|
+
* Automatically adds https:// and /api2/json
|
|
652
|
+
*/
|
|
653
|
+
export function buildProxmoxApiUrl(ipAddress: string, port = 8006): string {
|
|
654
|
+
return `https://${ipAddress}:${port}/api2/json`;
|
|
655
|
+
}
|