@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,607 @@
1
+ /**
2
+ * IPAM (IP Address Management) commands
3
+ * Manage VMID and IP address reservations and allocations
4
+ */
5
+
6
+ import { getDb } from '../../db/client';
7
+ import {
8
+ inferZoneFromIP,
9
+ listReservations,
10
+ listVMIDReservations,
11
+ reserveIP,
12
+ reserveVMID,
13
+ unreserveIP,
14
+ unreserveVMID,
15
+ } from '../../ipam/allocator';
16
+ import { getArg, getFlag, validateRequiredArgs } from '../parser';
17
+ import type { CommandResult } from '../types';
18
+
19
+ /**
20
+ * Handle IPAM VMID reserve command
21
+ *
22
+ * Usage: celilo ipam vmid reserve <vmid-or-range> --reason <reason>
23
+ *
24
+ * @param args - Command arguments
25
+ * @param flags - Command flags
26
+ * @returns Command result
27
+ */
28
+ export async function handleIpamVmidReserve(
29
+ args: string[],
30
+ flags: Record<string, string | boolean>,
31
+ ): Promise<CommandResult> {
32
+ const error = validateRequiredArgs(args, 1);
33
+ if (error) {
34
+ return {
35
+ success: false,
36
+ error: `${error}\n\nUsage: celilo ipam vmid reserve <vmid-or-range> --reason <reason>`,
37
+ };
38
+ }
39
+
40
+ const vmidArg = getArg(args, 0);
41
+ if (!vmidArg) {
42
+ return {
43
+ success: false,
44
+ error: 'VMID or range is required',
45
+ };
46
+ }
47
+
48
+ // Parse reason flag
49
+ const reason = getFlag(flags, 'reason');
50
+ if (!reason) {
51
+ return {
52
+ success: false,
53
+ error: 'Reason is required. Use --reason "description"',
54
+ };
55
+ }
56
+
57
+ const db = getDb();
58
+
59
+ try {
60
+ // Parse VMID or range
61
+ if (vmidArg.includes('-')) {
62
+ // Range format: "2100-2110"
63
+ const [startStr, endStr] = vmidArg.split('-');
64
+ const start = Number.parseInt(startStr, 10);
65
+ const end = Number.parseInt(endStr, 10);
66
+
67
+ if (Number.isNaN(start) || Number.isNaN(end)) {
68
+ return {
69
+ success: false,
70
+ error: `Invalid VMID range: ${vmidArg}`,
71
+ };
72
+ }
73
+
74
+ if (start >= end) {
75
+ return {
76
+ success: false,
77
+ error: 'Invalid range: start must be less than end',
78
+ };
79
+ }
80
+
81
+ // Reserve each VMID in range
82
+ for (let vmid = start; vmid <= end; vmid++) {
83
+ await reserveVMID(vmid, reason, db);
84
+ }
85
+
86
+ return {
87
+ success: true,
88
+ message: `Reserved VMIDs ${start}-${end}: ${reason}`,
89
+ };
90
+ }
91
+
92
+ // Single VMID
93
+ const vmid = Number.parseInt(vmidArg, 10);
94
+ if (Number.isNaN(vmid)) {
95
+ return {
96
+ success: false,
97
+ error: `Invalid VMID: ${vmidArg}`,
98
+ };
99
+ }
100
+
101
+ await reserveVMID(vmid, reason, db);
102
+
103
+ return {
104
+ success: true,
105
+ message: `Reserved VMID ${vmid}: ${reason}`,
106
+ };
107
+ } catch (err) {
108
+ return {
109
+ success: false,
110
+ error: err instanceof Error ? err.message : String(err),
111
+ };
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Handle IPAM VMID unreserve command
117
+ *
118
+ * Usage: celilo ipam vmid unreserve <vmid-or-range>
119
+ *
120
+ * @param args - Command arguments
121
+ * @param flags - Command flags
122
+ * @returns Command result
123
+ */
124
+ export async function handleIpamVmidUnreserve(
125
+ args: string[],
126
+ _flags: Record<string, string | boolean>,
127
+ ): Promise<CommandResult> {
128
+ const error = validateRequiredArgs(args, 1);
129
+ if (error) {
130
+ return {
131
+ success: false,
132
+ error: `${error}\n\nUsage: celilo ipam vmid unreserve <vmid-or-range>`,
133
+ };
134
+ }
135
+
136
+ const vmidArg = getArg(args, 0);
137
+ if (!vmidArg) {
138
+ return {
139
+ success: false,
140
+ error: 'VMID or range is required',
141
+ };
142
+ }
143
+
144
+ const db = getDb();
145
+
146
+ try {
147
+ // Parse VMID or range
148
+ if (vmidArg.includes('-')) {
149
+ // Range format: "2100-2110"
150
+ const [startStr, endStr] = vmidArg.split('-');
151
+ const start = Number.parseInt(startStr, 10);
152
+ const end = Number.parseInt(endStr, 10);
153
+
154
+ if (Number.isNaN(start) || Number.isNaN(end)) {
155
+ return {
156
+ success: false,
157
+ error: `Invalid VMID range: ${vmidArg}`,
158
+ };
159
+ }
160
+
161
+ if (start >= end) {
162
+ return {
163
+ success: false,
164
+ error: 'Invalid range: start must be less than end',
165
+ };
166
+ }
167
+
168
+ // Unreserve each VMID in range
169
+ for (let vmid = start; vmid <= end; vmid++) {
170
+ await unreserveVMID(vmid, db);
171
+ }
172
+
173
+ return {
174
+ success: true,
175
+ message: `Unreserved VMIDs ${start}-${end}`,
176
+ };
177
+ }
178
+
179
+ // Single VMID
180
+ const vmid = Number.parseInt(vmidArg, 10);
181
+ if (Number.isNaN(vmid)) {
182
+ return {
183
+ success: false,
184
+ error: `Invalid VMID: ${vmidArg}`,
185
+ };
186
+ }
187
+
188
+ await unreserveVMID(vmid, db);
189
+
190
+ return {
191
+ success: true,
192
+ message: `Unreserved VMID ${vmid}`,
193
+ };
194
+ } catch (err) {
195
+ return {
196
+ success: false,
197
+ error: err instanceof Error ? err.message : String(err),
198
+ };
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Handle IPAM VMID list-reservations command
204
+ *
205
+ * Usage: celilo ipam vmid list-reservations
206
+ *
207
+ * @param args - Command arguments
208
+ * @param flags - Command flags
209
+ * @returns Command result
210
+ */
211
+ export async function handleIpamVmidListReservations(
212
+ _args: string[],
213
+ _flags: Record<string, string | boolean>,
214
+ ): Promise<CommandResult> {
215
+ const db = getDb();
216
+
217
+ try {
218
+ const reservations = await listVMIDReservations(db);
219
+
220
+ if (reservations.length === 0) {
221
+ return {
222
+ success: true,
223
+ message: 'No VMID reservations',
224
+ };
225
+ }
226
+
227
+ const lines = ['VMID Reservations:', ''];
228
+ for (const reservation of reservations) {
229
+ lines.push(`VMID ${reservation.vmid}: ${reservation.reason}`);
230
+ lines.push(` Reserved: ${new Date(reservation.reservedAt).toLocaleString()}`);
231
+ }
232
+
233
+ return {
234
+ success: true,
235
+ message: lines.join('\n'),
236
+ data: reservations,
237
+ };
238
+ } catch (err) {
239
+ return {
240
+ success: false,
241
+ error: err instanceof Error ? err.message : String(err),
242
+ };
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Handle IPAM IP exclude command
248
+ *
249
+ * Usage: celilo ipam ip exclude <ip-range> --reason <reason> [--zone <zone>]
250
+ *
251
+ * @param args - Command arguments
252
+ * @param flags - Command flags
253
+ * @returns Command result
254
+ */
255
+ export async function handleIpamIpReserve(
256
+ args: string[],
257
+ flags: Record<string, string | boolean>,
258
+ ): Promise<CommandResult> {
259
+ const error = validateRequiredArgs(args, 1);
260
+ if (error) {
261
+ return {
262
+ success: false,
263
+ error: `${error}\n\nUsage: celilo ipam ip exclude <ip-range> --reason <reason> [--zone <zone>]`,
264
+ };
265
+ }
266
+
267
+ const ipArg = getArg(args, 0);
268
+ if (!ipArg) {
269
+ return {
270
+ success: false,
271
+ error: 'IP or range is required',
272
+ };
273
+ }
274
+
275
+ // Parse reason flag
276
+ const reason = getFlag(flags, 'reason');
277
+ if (!reason) {
278
+ return {
279
+ success: false,
280
+ error: 'Reason is required. Use --reason "description"',
281
+ };
282
+ }
283
+
284
+ const db = getDb();
285
+
286
+ try {
287
+ // Parse IP or range
288
+ let ipStart: string;
289
+ let ipEnd: string | null = null;
290
+
291
+ if (ipArg.includes('-')) {
292
+ // Range format: "10.0.10.1-10.0.10.9"
293
+ const [start, end] = ipArg.split('-');
294
+ ipStart = start.trim();
295
+ ipEnd = end.trim();
296
+ } else {
297
+ // Single IP
298
+ ipStart = ipArg;
299
+ }
300
+
301
+ // Infer zone from IP, or use explicit --zone override
302
+ const explicitZone = getFlag(flags, 'zone');
303
+ const zone = explicitZone || (await inferZoneFromIP(ipStart, db));
304
+
305
+ if (!zone) {
306
+ return {
307
+ success: false,
308
+ error: `Cannot determine zone for IP ${ipStart}. It doesn't match any configured zone subnet. Use --zone to specify manually.`,
309
+ };
310
+ }
311
+
312
+ if (zone !== 'dmz' && zone !== 'app' && zone !== 'secure' && zone !== 'internal') {
313
+ return {
314
+ success: false,
315
+ error: `Invalid zone: ${zone}. Must be internal, dmz, app, or secure`,
316
+ };
317
+ }
318
+
319
+ await reserveIP(ipStart, zone, reason, ipEnd, db);
320
+
321
+ return {
322
+ success: true,
323
+ message: `Excluded IP ${ipEnd ? `${ipStart}-${ipEnd}` : ipStart} in zone ${zone}: ${reason}`,
324
+ };
325
+ } catch (err) {
326
+ return {
327
+ success: false,
328
+ error: err instanceof Error ? err.message : String(err),
329
+ };
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Handle IPAM IP include command (remove exclusion)
335
+ *
336
+ * Usage: celilo ipam ip include <ip-or-range> [--zone <zone>]
337
+ *
338
+ * @param args - Command arguments
339
+ * @param flags - Command flags
340
+ * @returns Command result
341
+ */
342
+ export async function handleIpamIpUnreserve(
343
+ args: string[],
344
+ flags: Record<string, string | boolean>,
345
+ ): Promise<CommandResult> {
346
+ const error = validateRequiredArgs(args, 1);
347
+ if (error) {
348
+ return {
349
+ success: false,
350
+ error: `${error}\n\nUsage: celilo ipam ip include <ip-or-range> [--zone <zone>]`,
351
+ };
352
+ }
353
+
354
+ const ipArg = getArg(args, 0);
355
+ if (!ipArg) {
356
+ return {
357
+ success: false,
358
+ error: 'IP is required',
359
+ };
360
+ }
361
+
362
+ const db = getDb();
363
+
364
+ try {
365
+ // Parse IP (take first part if range)
366
+ const ipStart = ipArg.includes('-') ? ipArg.split('-')[0].trim() : ipArg;
367
+
368
+ // Infer zone from IP, or use explicit --zone override
369
+ const explicitZone = getFlag(flags, 'zone');
370
+ const zone = explicitZone || (await inferZoneFromIP(ipStart, db));
371
+
372
+ if (!zone) {
373
+ return {
374
+ success: false,
375
+ error: `Cannot determine zone for IP ${ipStart}. Use --zone to specify manually.`,
376
+ };
377
+ }
378
+
379
+ if (zone !== 'dmz' && zone !== 'app' && zone !== 'secure' && zone !== 'internal') {
380
+ return {
381
+ success: false,
382
+ error: `Invalid zone: ${zone}. Must be internal, dmz, app, or secure`,
383
+ };
384
+ }
385
+
386
+ await unreserveIP(ipStart, zone, db);
387
+
388
+ return {
389
+ success: true,
390
+ message: `Removed exclusion for IP ${ipArg} in zone ${zone}`,
391
+ };
392
+ } catch (err) {
393
+ return {
394
+ success: false,
395
+ error: err instanceof Error ? err.message : String(err),
396
+ };
397
+ }
398
+ }
399
+
400
+ /**
401
+ * Handle IPAM IP list-exclusions command
402
+ *
403
+ * Usage: celilo ipam ip list-exclusions
404
+ *
405
+ * @param args - Command arguments
406
+ * @param flags - Command flags
407
+ * @returns Command result
408
+ */
409
+ export async function handleIpamIpListReservations(
410
+ _args: string[],
411
+ _flags: Record<string, string | boolean>,
412
+ ): Promise<CommandResult> {
413
+ const db = getDb();
414
+
415
+ try {
416
+ const reservations = await listReservations(db);
417
+
418
+ if (reservations.length === 0) {
419
+ return {
420
+ success: true,
421
+ message: 'No IP exclusions',
422
+ };
423
+ }
424
+
425
+ const lines = ['IP Exclusions:', ''];
426
+ for (const reservation of reservations) {
427
+ const range = reservation.ipEnd
428
+ ? `${reservation.ipStart}-${reservation.ipEnd}`
429
+ : reservation.ipStart;
430
+ lines.push(`${range} (${reservation.zone}): ${reservation.reason}`);
431
+ lines.push(` Excluded: ${new Date(reservation.reservedAt).toLocaleString()}`);
432
+ }
433
+
434
+ return {
435
+ success: true,
436
+ message: lines.join('\n'),
437
+ data: reservations,
438
+ };
439
+ } catch (err) {
440
+ return {
441
+ success: false,
442
+ error: err instanceof Error ? err.message : String(err),
443
+ };
444
+ }
445
+ }
446
+
447
+ /**
448
+ * Handle IPAM show command - comprehensive summary
449
+ *
450
+ * Usage: celilo ipam show
451
+ *
452
+ * @param args - Command arguments
453
+ * @param flags - Command flags
454
+ * @returns Command result
455
+ */
456
+ export async function handleIpamShow(
457
+ _args: string[],
458
+ _flags: Record<string, string | boolean>,
459
+ ): Promise<CommandResult> {
460
+ const db = getDb();
461
+
462
+ try {
463
+ const { ipAllocations } = await import('../../db/schema');
464
+ const allocations = await db.select().from(ipAllocations).all();
465
+ const ipReservations = await listReservations(db);
466
+ const vmidReservations = await listVMIDReservations(db);
467
+
468
+ const lines: string[] = ['IPAM Allocations Summary', ''];
469
+
470
+ // VMID section
471
+ lines.push('VMID Allocations:');
472
+ if (allocations.length === 0) {
473
+ lines.push(' (none)');
474
+ } else {
475
+ for (const allocation of allocations) {
476
+ lines.push(` ${allocation.vmid}: ${allocation.moduleId} (allocated)`);
477
+ }
478
+ }
479
+ lines.push('');
480
+
481
+ lines.push('VMID Reservations:');
482
+ if (vmidReservations.length === 0) {
483
+ lines.push(' (none)');
484
+ } else {
485
+ for (const reservation of vmidReservations) {
486
+ lines.push(` ${reservation.vmid}: ${reservation.reason}`);
487
+ }
488
+ }
489
+ lines.push('');
490
+
491
+ // IP section - group by zone
492
+ const zones = ['internal', 'dmz', 'app', 'secure'];
493
+ for (const zone of zones) {
494
+ const zoneAllocations = allocations.filter((a) => a.zone === zone);
495
+ const zoneReservations = ipReservations.filter((r) => r.zone === zone);
496
+
497
+ if (zoneAllocations.length === 0 && zoneReservations.length === 0) {
498
+ continue; // Skip empty zones
499
+ }
500
+
501
+ const zoneName = zone.toUpperCase();
502
+ lines.push(`IP Allocations (${zoneName}):`);
503
+
504
+ if (zoneAllocations.length === 0) {
505
+ lines.push(' (none)');
506
+ } else {
507
+ for (const allocation of zoneAllocations) {
508
+ lines.push(` ${allocation.containerIp}: ${allocation.moduleId} (allocated)`);
509
+ }
510
+ }
511
+ lines.push('');
512
+
513
+ lines.push(`IP Exclusions (${zoneName}):`);
514
+ if (zoneReservations.length === 0) {
515
+ lines.push(' (none)');
516
+ } else {
517
+ for (const reservation of zoneReservations) {
518
+ const range = reservation.ipEnd
519
+ ? `${reservation.ipStart}-${reservation.ipEnd}`
520
+ : reservation.ipStart;
521
+ lines.push(` ${range}: ${reservation.reason}`);
522
+ }
523
+ }
524
+ lines.push('');
525
+ }
526
+
527
+ // Summary
528
+ lines.push('Summary:');
529
+ lines.push(` VMIDs: ${allocations.length} allocated, ${vmidReservations.length} reserved`);
530
+
531
+ const totalIpAllocations = allocations.length;
532
+ const totalIpReservations = ipReservations.reduce((sum, r) => {
533
+ if (r.ipEnd) {
534
+ // Calculate range size
535
+ const startOctet = Number.parseInt(r.ipStart.split('.')[3], 10);
536
+ const endOctet = Number.parseInt(r.ipEnd.split('.')[3], 10);
537
+ return sum + (endOctet - startOctet + 1);
538
+ }
539
+ return sum + 1;
540
+ }, 0);
541
+ lines.push(` IPs: ${totalIpAllocations} allocated, ${totalIpReservations} excluded`);
542
+
543
+ return {
544
+ success: true,
545
+ message: lines.join('\n'),
546
+ data: {
547
+ allocations,
548
+ ipReservations,
549
+ vmidReservations,
550
+ },
551
+ };
552
+ } catch (err) {
553
+ return {
554
+ success: false,
555
+ error: err instanceof Error ? err.message : String(err),
556
+ };
557
+ }
558
+ }
559
+
560
+ /**
561
+ * Handle IPAM list-allocations command
562
+ *
563
+ * Usage: celilo ipam list-allocations
564
+ *
565
+ * @param args - Command arguments
566
+ * @param flags - Command flags
567
+ * @returns Command result
568
+ */
569
+ export async function handleIpamListAllocations(
570
+ _args: string[],
571
+ _flags: Record<string, string | boolean>,
572
+ ): Promise<CommandResult> {
573
+ const db = getDb();
574
+
575
+ try {
576
+ const { ipAllocations } = await import('../../db/schema');
577
+ const allocations = await db.select().from(ipAllocations).all();
578
+
579
+ if (allocations.length === 0) {
580
+ return {
581
+ success: true,
582
+ message: 'No IPAM allocations',
583
+ };
584
+ }
585
+
586
+ const lines = ['IPAM Allocations:', ''];
587
+ for (const allocation of allocations) {
588
+ lines.push(`Module: ${allocation.moduleId}`);
589
+ lines.push(` VMID: ${allocation.vmid}`);
590
+ lines.push(` IP: ${allocation.containerIp}`);
591
+ lines.push(` Zone: ${allocation.zone}`);
592
+ lines.push(` Allocated: ${new Date(allocation.allocatedAt).toLocaleString()}`);
593
+ lines.push('');
594
+ }
595
+
596
+ return {
597
+ success: true,
598
+ message: lines.join('\n'),
599
+ data: allocations,
600
+ };
601
+ } catch (err) {
602
+ return {
603
+ success: false,
604
+ error: err instanceof Error ? err.message : String(err),
605
+ };
606
+ }
607
+ }