@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.
Files changed (267) hide show
  1. package/README.md +1566 -0
  2. package/bin/celilo +16 -0
  3. package/drizzle/0000_complex_puma.sql +179 -0
  4. package/drizzle/0001_dizzy_wolfpack.sql +2 -0
  5. package/drizzle/0002_web_routes.sql +16 -0
  6. package/drizzle/0003_backup_storage.sql +32 -0
  7. package/drizzle/meta/0000_snapshot.json +1151 -0
  8. package/drizzle/meta/0001_snapshot.json +1167 -0
  9. package/drizzle/meta/0002_snapshot.json +1257 -0
  10. package/drizzle/meta/_journal.json +27 -0
  11. package/package.json +64 -0
  12. package/schemas/system_config.json +106 -0
  13. package/src/__integration__/container-services-cli.integration.test.ts +246 -0
  14. package/src/ansible/dependencies.test.ts +309 -0
  15. package/src/ansible/dependencies.ts +896 -0
  16. package/src/ansible/inventory.test.ts +463 -0
  17. package/src/ansible/inventory.ts +445 -0
  18. package/src/ansible/secrets.ts +222 -0
  19. package/src/ansible/validation.test.ts +92 -0
  20. package/src/ansible/validation.ts +272 -0
  21. package/src/api-clients/digitalocean.ts +94 -0
  22. package/src/api-clients/proxmox.ts +655 -0
  23. package/src/capabilities/logging-wrapper.test.ts +217 -0
  24. package/src/capabilities/lookup.test.ts +149 -0
  25. package/src/capabilities/lookup.ts +89 -0
  26. package/src/capabilities/public-web-helpers.test.ts +198 -0
  27. package/src/capabilities/public-web-publish.test.ts +458 -0
  28. package/src/capabilities/registration.test.ts +395 -0
  29. package/src/capabilities/registration.ts +200 -0
  30. package/src/capabilities/route-validation.test.ts +121 -0
  31. package/src/capabilities/route-validation.ts +96 -0
  32. package/src/capabilities/secret-ref.test.ts +313 -0
  33. package/src/capabilities/secret-validation.ts +157 -0
  34. package/src/capabilities/secrets.test.ts +750 -0
  35. package/src/capabilities/secrets.ts +244 -0
  36. package/src/capabilities/validation.test.ts +613 -0
  37. package/src/capabilities/validation.ts +160 -0
  38. package/src/capabilities/well-known.test.ts +238 -0
  39. package/src/capabilities/well-known.ts +222 -0
  40. package/src/cli/cli.test.ts +654 -0
  41. package/src/cli/command-registry.ts +742 -0
  42. package/src/cli/command-tree-parser.test.ts +180 -0
  43. package/src/cli/command-tree-parser.ts +193 -0
  44. package/src/cli/commands/backup-create.ts +137 -0
  45. package/src/cli/commands/backup-delete.ts +74 -0
  46. package/src/cli/commands/backup-import.ts +97 -0
  47. package/src/cli/commands/backup-list.ts +132 -0
  48. package/src/cli/commands/backup-name.ts +73 -0
  49. package/src/cli/commands/backup-prune.ts +98 -0
  50. package/src/cli/commands/backup-restore.ts +122 -0
  51. package/src/cli/commands/capability-info.ts +121 -0
  52. package/src/cli/commands/capability-list.ts +47 -0
  53. package/src/cli/commands/completion.ts +87 -0
  54. package/src/cli/commands/hook-run.ts +176 -0
  55. package/src/cli/commands/ipam.ts +607 -0
  56. package/src/cli/commands/machine-add.ts +235 -0
  57. package/src/cli/commands/machine-earmark.ts +82 -0
  58. package/src/cli/commands/machine-list.ts +77 -0
  59. package/src/cli/commands/machine-remove.ts +90 -0
  60. package/src/cli/commands/machine-status.ts +131 -0
  61. package/src/cli/commands/module-audit.ts +51 -0
  62. package/src/cli/commands/module-build.ts +60 -0
  63. package/src/cli/commands/module-config.ts +170 -0
  64. package/src/cli/commands/module-deploy.ts +71 -0
  65. package/src/cli/commands/module-generate.ts +236 -0
  66. package/src/cli/commands/module-health.ts +108 -0
  67. package/src/cli/commands/module-import.ts +80 -0
  68. package/src/cli/commands/module-list.ts +43 -0
  69. package/src/cli/commands/module-logs.ts +73 -0
  70. package/src/cli/commands/module-remove.ts +162 -0
  71. package/src/cli/commands/module-show.ts +208 -0
  72. package/src/cli/commands/module-status.ts +131 -0
  73. package/src/cli/commands/module-types.ts +189 -0
  74. package/src/cli/commands/module-upgrade.ts +192 -0
  75. package/src/cli/commands/package.ts +68 -0
  76. package/src/cli/commands/secret-list.ts +99 -0
  77. package/src/cli/commands/secret-set.ts +134 -0
  78. package/src/cli/commands/service-add-digitalocean.ts +133 -0
  79. package/src/cli/commands/service-add-proxmox.ts +342 -0
  80. package/src/cli/commands/service-config-get.ts +83 -0
  81. package/src/cli/commands/service-config-set.ts +145 -0
  82. package/src/cli/commands/service-list.ts +74 -0
  83. package/src/cli/commands/service-reconfigure.ts +230 -0
  84. package/src/cli/commands/service-remove.ts +103 -0
  85. package/src/cli/commands/service-verify.ts +240 -0
  86. package/src/cli/commands/status.ts +216 -0
  87. package/src/cli/commands/storage-add-local.ts +106 -0
  88. package/src/cli/commands/storage-add-s3.ts +114 -0
  89. package/src/cli/commands/storage-list.ts +72 -0
  90. package/src/cli/commands/storage-remove.ts +54 -0
  91. package/src/cli/commands/storage-set-default.ts +44 -0
  92. package/src/cli/commands/storage-verify.ts +54 -0
  93. package/src/cli/commands/system-config.ts +168 -0
  94. package/src/cli/commands/system-init.ts +314 -0
  95. package/src/cli/commands/system-secret-get.ts +98 -0
  96. package/src/cli/commands/system-secret-set.ts +76 -0
  97. package/src/cli/commands/system-vault-password.ts +34 -0
  98. package/src/cli/completion.test.ts +37 -0
  99. package/src/cli/completion.ts +482 -0
  100. package/src/cli/fuel-gauge.test.ts +208 -0
  101. package/src/cli/fuel-gauge.ts +405 -0
  102. package/src/cli/generate-zsh-completion.test.ts +95 -0
  103. package/src/cli/generate-zsh-completion.ts +497 -0
  104. package/src/cli/index.ts +1583 -0
  105. package/src/cli/interactive-config.test.ts +201 -0
  106. package/src/cli/interactive-config.ts +62 -0
  107. package/src/cli/parser.test.ts +227 -0
  108. package/src/cli/parser.ts +244 -0
  109. package/src/cli/prompts.test.ts +33 -0
  110. package/src/cli/prompts.ts +121 -0
  111. package/src/cli/types.ts +38 -0
  112. package/src/cli/validators.test.ts +235 -0
  113. package/src/cli/validators.ts +188 -0
  114. package/src/config/env.ts +41 -0
  115. package/src/config/paths.test.ts +172 -0
  116. package/src/config/paths.ts +108 -0
  117. package/src/db/client.ts +190 -0
  118. package/src/db/migrate.ts +30 -0
  119. package/src/db/schema.test.ts +221 -0
  120. package/src/db/schema.ts +434 -0
  121. package/src/hooks/capability-loader-firewall.test.ts +246 -0
  122. package/src/hooks/capability-loader.test.ts +100 -0
  123. package/src/hooks/capability-loader.ts +520 -0
  124. package/src/hooks/define-hook.test.ts +488 -0
  125. package/src/hooks/executor.test.ts +462 -0
  126. package/src/hooks/executor.ts +469 -0
  127. package/src/hooks/logger.test.ts +54 -0
  128. package/src/hooks/logger.ts +95 -0
  129. package/src/hooks/test-fixtures/failing-hook.ts +13 -0
  130. package/src/hooks/test-fixtures/no-default-hook.ts +6 -0
  131. package/src/hooks/test-fixtures/success-hook.ts +20 -0
  132. package/src/hooks/test-fixtures/unbranded-hook.ts +11 -0
  133. package/src/hooks/test-fixtures/void-hook.ts +13 -0
  134. package/src/hooks/types.ts +89 -0
  135. package/src/infrastructure/property-extractor.test.ts +194 -0
  136. package/src/infrastructure/property-extractor.ts +151 -0
  137. package/src/ipam/allocator.test.ts +442 -0
  138. package/src/ipam/allocator.ts +369 -0
  139. package/src/ipam/auto-allocator.test.ts +247 -0
  140. package/src/ipam/auto-allocator.ts +270 -0
  141. package/src/ipam/subnet-parser.test.ts +107 -0
  142. package/src/ipam/subnet-parser.ts +136 -0
  143. package/src/manifest/contracts/index.ts +61 -0
  144. package/src/manifest/contracts/v1.ts +118 -0
  145. package/src/manifest/json-schema-roundtrip.test.ts +99 -0
  146. package/src/manifest/schema.ts +367 -0
  147. package/src/manifest/template-validator.test.ts +231 -0
  148. package/src/manifest/template-validator.ts +322 -0
  149. package/src/manifest/validate.test.ts +1180 -0
  150. package/src/manifest/validate.ts +415 -0
  151. package/src/module/import.test.ts +355 -0
  152. package/src/module/import.ts +676 -0
  153. package/src/module/packaging/audit.ts +169 -0
  154. package/src/module/packaging/build.ts +228 -0
  155. package/src/module/packaging/checksum.ts +41 -0
  156. package/src/module/packaging/extract.ts +234 -0
  157. package/src/module/packaging/signature.ts +47 -0
  158. package/src/secrets/encryption.test.ts +284 -0
  159. package/src/secrets/encryption.ts +162 -0
  160. package/src/secrets/generators.test.ts +112 -0
  161. package/src/secrets/generators.ts +127 -0
  162. package/src/secrets/master-key.test.ts +159 -0
  163. package/src/secrets/master-key.ts +114 -0
  164. package/src/secrets/storage.test.ts +115 -0
  165. package/src/secrets/storage.ts +106 -0
  166. package/src/secrets/vault.test.ts +35 -0
  167. package/src/secrets/vault.ts +42 -0
  168. package/src/services/backup-create.ts +532 -0
  169. package/src/services/backup-metadata.ts +198 -0
  170. package/src/services/backup-restore.ts +229 -0
  171. package/src/services/backup-retention.ts +84 -0
  172. package/src/services/backup-storage.ts +281 -0
  173. package/src/services/build-stream.test.ts +122 -0
  174. package/src/services/build-stream.ts +201 -0
  175. package/src/services/config-interview.ts +694 -0
  176. package/src/services/container-service.test.ts +298 -0
  177. package/src/services/container-service.ts +401 -0
  178. package/src/services/cross-module-data-manager.test.ts +405 -0
  179. package/src/services/cross-module-data-manager.ts +412 -0
  180. package/src/services/deploy-ansible.ts +88 -0
  181. package/src/services/deploy-planner.ts +153 -0
  182. package/src/services/deploy-preflight.ts +274 -0
  183. package/src/services/deploy-ssh.ts +131 -0
  184. package/src/services/deploy-terraform.test.ts +55 -0
  185. package/src/services/deploy-terraform.ts +445 -0
  186. package/src/services/deploy-validation.ts +311 -0
  187. package/src/services/dns-auto-register.ts +211 -0
  188. package/src/services/health-runner.ts +184 -0
  189. package/src/services/infrastructure-selector.test.ts +485 -0
  190. package/src/services/infrastructure-selector.ts +245 -0
  191. package/src/services/infrastructure-variable-resolver.test.ts +751 -0
  192. package/src/services/infrastructure-variable-resolver.ts +234 -0
  193. package/src/services/machine-detector.ts +328 -0
  194. package/src/services/machine-pool.test.ts +405 -0
  195. package/src/services/machine-pool.ts +316 -0
  196. package/src/services/manifest-validation.ts +120 -0
  197. package/src/services/module-build.test.ts +290 -0
  198. package/src/services/module-build.ts +431 -0
  199. package/src/services/module-config.test.ts +237 -0
  200. package/src/services/module-config.ts +298 -0
  201. package/src/services/module-deploy.ts +862 -0
  202. package/src/services/module-types-drift.test.ts +73 -0
  203. package/src/services/module-types-generator.test.ts +288 -0
  204. package/src/services/module-types-generator.ts +189 -0
  205. package/src/services/proxmox-state-recovery.ts +140 -0
  206. package/src/services/schema-validation.ts +155 -0
  207. package/src/services/secret-schema-loader.test.ts +311 -0
  208. package/src/services/secret-schema-loader.ts +239 -0
  209. package/src/services/ssh-key-manager.test.ts +283 -0
  210. package/src/services/ssh-key-manager.ts +193 -0
  211. package/src/services/storage-providers/local.ts +105 -0
  212. package/src/services/storage-providers/s3.ts +182 -0
  213. package/src/services/storage-providers/types.ts +24 -0
  214. package/src/services/system-config-schema-types.ts +25 -0
  215. package/src/services/system-config-validator.test.ts +160 -0
  216. package/src/services/system-config-validator.ts +74 -0
  217. package/src/services/system-init.test.ts +153 -0
  218. package/src/services/system-init.ts +253 -0
  219. package/src/services/terraform-safety.ts +174 -0
  220. package/src/services/zone-detector.test.ts +110 -0
  221. package/src/services/zone-detector.ts +102 -0
  222. package/src/services/zone-policy.test.ts +97 -0
  223. package/src/services/zone-policy.ts +126 -0
  224. package/src/templates/generator.test.ts +645 -0
  225. package/src/templates/generator.ts +1119 -0
  226. package/src/templates/types.ts +62 -0
  227. package/src/test-utils/INTERACTIVE_PROMPTS.md +167 -0
  228. package/src/test-utils/cli-context-interactive.test.ts +152 -0
  229. package/src/test-utils/cli-context-server.test.ts +66 -0
  230. package/src/test-utils/cli-context.test.ts +273 -0
  231. package/src/test-utils/cli-context.ts +677 -0
  232. package/src/test-utils/cli-result.test.ts +282 -0
  233. package/src/test-utils/cli-result.ts +241 -0
  234. package/src/test-utils/cli.ts +55 -0
  235. package/src/test-utils/completion-harness.test.ts +126 -0
  236. package/src/test-utils/completion-harness.ts +82 -0
  237. package/src/test-utils/database.test.ts +182 -0
  238. package/src/test-utils/database.ts +126 -0
  239. package/src/test-utils/filesystem.test.ts +208 -0
  240. package/src/test-utils/filesystem.ts +142 -0
  241. package/src/test-utils/fixtures.test.ts +123 -0
  242. package/src/test-utils/fixtures.ts +160 -0
  243. package/src/test-utils/golden-diff.ts +197 -0
  244. package/src/test-utils/index.ts +77 -0
  245. package/src/test-utils/integration.ts +81 -0
  246. package/src/test-utils/module-fixtures.ts +468 -0
  247. package/src/test-utils/modules.test.ts +144 -0
  248. package/src/test-utils/modules.ts +183 -0
  249. package/src/test-utils/setup-test-db.ts +90 -0
  250. package/src/test-utils/value-extractor.test.ts +231 -0
  251. package/src/test-utils/value-extractor.ts +228 -0
  252. package/src/types/infrastructure.ts +157 -0
  253. package/src/utils/shell.test.ts +365 -0
  254. package/src/utils/shell.ts +159 -0
  255. package/src/validation/schemas.ts +166 -0
  256. package/src/variables/ansible-resolver.test.ts +142 -0
  257. package/src/variables/ansible-resolver.ts +69 -0
  258. package/src/variables/capability-self-ref.test.ts +220 -0
  259. package/src/variables/context.test.ts +1265 -0
  260. package/src/variables/context.ts +624 -0
  261. package/src/variables/declarative-derivation.test.ts +743 -0
  262. package/src/variables/declarative-derivation.ts +200 -0
  263. package/src/variables/parser.test.ts +231 -0
  264. package/src/variables/parser.ts +76 -0
  265. package/src/variables/resolver.test.ts +458 -0
  266. package/src/variables/resolver.ts +282 -0
  267. 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
+ }