@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,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IPAM (IP Address Management) Allocator
|
|
3
|
+
* Automatically allocates VMID and container IP addresses from zone subnets
|
|
4
|
+
* Prevents conflicts and tracks allocations
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { and, eq } from 'drizzle-orm';
|
|
8
|
+
import type { BunSQLiteDatabase } from 'drizzle-orm/bun-sqlite';
|
|
9
|
+
import type { DbClient } from '../db/client';
|
|
10
|
+
import { ipAllocations, ipReservations, systemConfig, vmidReservations } from '../db/schema';
|
|
11
|
+
import type { NewIpAllocation, NewIpReservation, NewVmidReservation } from '../db/schema';
|
|
12
|
+
import type * as schema from '../db/schema';
|
|
13
|
+
import { generateIPsInSubnet, isIPInRange, isInSubnet, stripCIDR } from './subnet-parser';
|
|
14
|
+
|
|
15
|
+
// Type that accepts both database client and transaction
|
|
16
|
+
type DbOrTransaction = BunSQLiteDatabase<typeof schema> | DbClient;
|
|
17
|
+
|
|
18
|
+
/** Zones that support IPAM auto-allocation of VMID and container IP */
|
|
19
|
+
export type IpamZone = 'dmz' | 'app' | 'secure' | 'internal';
|
|
20
|
+
|
|
21
|
+
const IPAM_ZONES: IpamZone[] = ['internal', 'dmz', 'app', 'secure'];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Infer which zone an IP address belongs to by checking configured zone subnets.
|
|
25
|
+
* Returns null if the IP doesn't match any known zone.
|
|
26
|
+
*/
|
|
27
|
+
export async function inferZoneFromIP(ip: string, db: DbOrTransaction): Promise<IpamZone | null> {
|
|
28
|
+
const bareIp = stripCIDR(ip);
|
|
29
|
+
for (const zone of IPAM_ZONES) {
|
|
30
|
+
const subnetKey = `network.${zone}.subnet`;
|
|
31
|
+
const records = await db
|
|
32
|
+
.select()
|
|
33
|
+
.from(systemConfig)
|
|
34
|
+
.where(eq(systemConfig.key, subnetKey))
|
|
35
|
+
.all();
|
|
36
|
+
if (records.length > 0 && isInSubnet(bareIp, records[0].value)) {
|
|
37
|
+
return zone;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface IPAMAllocation {
|
|
44
|
+
vmid: number;
|
|
45
|
+
containerIp: string; // CIDR format (e.g., "10.0.10.10/24")
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Allocate VMID and container IP for a module
|
|
50
|
+
* Automatically selects next available from zone subnet
|
|
51
|
+
*/
|
|
52
|
+
export async function allocateResources(
|
|
53
|
+
moduleId: string,
|
|
54
|
+
zone: IpamZone,
|
|
55
|
+
db: DbOrTransaction,
|
|
56
|
+
): Promise<IPAMAllocation> {
|
|
57
|
+
// Allocate VMID (sequential from 2100)
|
|
58
|
+
const vmid = await allocateVMID(db);
|
|
59
|
+
|
|
60
|
+
// Get zone subnet from system config
|
|
61
|
+
const subnetKey = `network.${zone}.subnet`;
|
|
62
|
+
const subnetRecords = await db
|
|
63
|
+
.select()
|
|
64
|
+
.from(systemConfig)
|
|
65
|
+
.where(eq(systemConfig.key, subnetKey))
|
|
66
|
+
.all();
|
|
67
|
+
|
|
68
|
+
if (subnetRecords.length === 0) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`Network zone '${zone}' not configured. Run: celilo system config set ${subnetKey} "10.0.X.0/24"`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const subnet = subnetRecords[0].value;
|
|
75
|
+
|
|
76
|
+
// Allocate IP from zone subnet
|
|
77
|
+
const containerIp = await allocateIPFromSubnet(subnet, zone, db);
|
|
78
|
+
|
|
79
|
+
// Record allocation
|
|
80
|
+
const newAllocation: NewIpAllocation = {
|
|
81
|
+
moduleId,
|
|
82
|
+
vmid,
|
|
83
|
+
containerIp,
|
|
84
|
+
zone,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
await db.insert(ipAllocations).values(newAllocation).run();
|
|
88
|
+
|
|
89
|
+
return { vmid, containerIp };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Allocate next available VMID
|
|
94
|
+
* Starts from 2100 and increments sequentially
|
|
95
|
+
* Skips reserved VMIDs
|
|
96
|
+
*/
|
|
97
|
+
export async function allocateVMID(db: DbOrTransaction): Promise<number> {
|
|
98
|
+
// Get all allocated VMIDs
|
|
99
|
+
const allocations = await db.select().from(ipAllocations).all();
|
|
100
|
+
const allocatedVMIDs = new Set(allocations.map((a: typeof ipAllocations.$inferSelect) => a.vmid));
|
|
101
|
+
|
|
102
|
+
// Get all reserved VMIDs
|
|
103
|
+
const reservations = await db.select().from(vmidReservations).all();
|
|
104
|
+
const reservedVMIDs = new Set(
|
|
105
|
+
reservations.map((r: typeof vmidReservations.$inferSelect) => r.vmid),
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Find next available VMID starting from 2100
|
|
109
|
+
let candidateVMID =
|
|
110
|
+
allocations.length === 0
|
|
111
|
+
? 2100
|
|
112
|
+
: Math.max(...allocations.map((a: typeof ipAllocations.$inferSelect) => a.vmid)) + 1;
|
|
113
|
+
|
|
114
|
+
// Keep incrementing until we find an unreserved VMID
|
|
115
|
+
while (allocatedVMIDs.has(candidateVMID) || reservedVMIDs.has(candidateVMID)) {
|
|
116
|
+
candidateVMID++;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return candidateVMID;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Allocate next available IP from subnet
|
|
124
|
+
* Skips allocated IPs and reserved IPs
|
|
125
|
+
* Reserves .1-.9 for infrastructure
|
|
126
|
+
*/
|
|
127
|
+
export async function allocateIPFromSubnet(
|
|
128
|
+
subnet: string,
|
|
129
|
+
zone: IpamZone,
|
|
130
|
+
db: DbOrTransaction,
|
|
131
|
+
): Promise<string> {
|
|
132
|
+
// Get all allocated IPs in this subnet
|
|
133
|
+
const allocations = await db
|
|
134
|
+
.select()
|
|
135
|
+
.from(ipAllocations)
|
|
136
|
+
.where(eq(ipAllocations.zone, zone))
|
|
137
|
+
.all();
|
|
138
|
+
const allocatedIPs = new Set(
|
|
139
|
+
allocations.map((a: typeof ipAllocations.$inferSelect) => a.containerIp),
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// Get all reservations in this zone
|
|
143
|
+
const reservations = await db
|
|
144
|
+
.select()
|
|
145
|
+
.from(ipReservations)
|
|
146
|
+
.where(eq(ipReservations.zone, zone))
|
|
147
|
+
.all();
|
|
148
|
+
|
|
149
|
+
// Find first available IP
|
|
150
|
+
for (const candidateIP of generateIPsInSubnet(subnet)) {
|
|
151
|
+
// Skip if already allocated
|
|
152
|
+
if (allocatedIPs.has(candidateIP)) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Skip if reserved
|
|
157
|
+
const isReserved = reservations.some((r: typeof ipReservations.$inferSelect) =>
|
|
158
|
+
isIPInRange(stripCIDR(candidateIP), r.ipStart, r.ipEnd),
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
if (isReserved) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Found available IP
|
|
166
|
+
return candidateIP;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
throw new Error(`No available IPs in subnet ${subnet} (zone: ${zone})`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Deallocate resources for a module
|
|
174
|
+
* Removes VMID and IP allocation
|
|
175
|
+
*/
|
|
176
|
+
export async function deallocateResources(moduleId: string, db: DbOrTransaction): Promise<void> {
|
|
177
|
+
await db.delete(ipAllocations).where(eq(ipAllocations.moduleId, moduleId)).run();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get allocation for a module
|
|
182
|
+
*/
|
|
183
|
+
export async function getAllocation(
|
|
184
|
+
moduleId: string,
|
|
185
|
+
db: DbOrTransaction,
|
|
186
|
+
): Promise<IPAMAllocation | null> {
|
|
187
|
+
const allocations = await db
|
|
188
|
+
.select()
|
|
189
|
+
.from(ipAllocations)
|
|
190
|
+
.where(eq(ipAllocations.moduleId, moduleId))
|
|
191
|
+
.all();
|
|
192
|
+
|
|
193
|
+
if (allocations.length === 0) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const allocation = allocations[0];
|
|
198
|
+
return {
|
|
199
|
+
vmid: allocation.vmid,
|
|
200
|
+
containerIp: allocation.containerIp,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Reserve an IP or IP range
|
|
206
|
+
* Prevents IPAM from allocating reserved IPs
|
|
207
|
+
*/
|
|
208
|
+
export async function reserveIP(
|
|
209
|
+
ipStart: string,
|
|
210
|
+
zone: IpamZone,
|
|
211
|
+
reason: string,
|
|
212
|
+
ipEnd: string | null,
|
|
213
|
+
db: DbOrTransaction,
|
|
214
|
+
): Promise<void> {
|
|
215
|
+
const newReservation: NewIpReservation = {
|
|
216
|
+
ipStart: stripCIDR(ipStart),
|
|
217
|
+
ipEnd: ipEnd ? stripCIDR(ipEnd) : null,
|
|
218
|
+
zone,
|
|
219
|
+
reason,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
await db.insert(ipReservations).values(newReservation).run();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Remove IP reservation
|
|
227
|
+
*/
|
|
228
|
+
export async function unreserveIP(
|
|
229
|
+
ipStart: string,
|
|
230
|
+
zone: IpamZone,
|
|
231
|
+
db: DbOrTransaction,
|
|
232
|
+
): Promise<void> {
|
|
233
|
+
const ip = stripCIDR(ipStart);
|
|
234
|
+
await db
|
|
235
|
+
.delete(ipReservations)
|
|
236
|
+
.where(and(eq(ipReservations.ipStart, ip), eq(ipReservations.zone, zone)));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* List all IP reservations
|
|
241
|
+
*/
|
|
242
|
+
export async function listReservations(db: DbOrTransaction) {
|
|
243
|
+
return await db.select().from(ipReservations).all();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Get all allocated IPs in a subnet (for debugging/status)
|
|
248
|
+
*/
|
|
249
|
+
export async function getAllocatedIPsInSubnet(
|
|
250
|
+
subnet: string,
|
|
251
|
+
db: DbOrTransaction,
|
|
252
|
+
): Promise<string[]> {
|
|
253
|
+
const allocations = await db.select().from(ipAllocations).all();
|
|
254
|
+
|
|
255
|
+
return allocations
|
|
256
|
+
.filter((a: typeof ipAllocations.$inferSelect) => isInSubnet(a.containerIp, subnet))
|
|
257
|
+
.map((a: typeof ipAllocations.$inferSelect) => a.containerIp);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Check if a specific IP is available
|
|
262
|
+
*/
|
|
263
|
+
export async function isIPAvailable(
|
|
264
|
+
ip: string,
|
|
265
|
+
zone: IpamZone,
|
|
266
|
+
db: DbOrTransaction,
|
|
267
|
+
): Promise<boolean> {
|
|
268
|
+
// Check if allocated
|
|
269
|
+
const allocations = await db
|
|
270
|
+
.select()
|
|
271
|
+
.from(ipAllocations)
|
|
272
|
+
.where(eq(ipAllocations.containerIp, ip))
|
|
273
|
+
.all();
|
|
274
|
+
|
|
275
|
+
if (allocations.length > 0) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Check if reserved
|
|
280
|
+
const reservations = await db
|
|
281
|
+
.select()
|
|
282
|
+
.from(ipReservations)
|
|
283
|
+
.where(eq(ipReservations.zone, zone))
|
|
284
|
+
.all();
|
|
285
|
+
|
|
286
|
+
const isReserved = reservations.some((r: typeof ipReservations.$inferSelect) =>
|
|
287
|
+
isIPInRange(stripCIDR(ip), r.ipStart, r.ipEnd),
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
return !isReserved;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Reserve a VMID
|
|
295
|
+
* Prevents IPAM from allocating reserved VMIDs
|
|
296
|
+
*/
|
|
297
|
+
export async function reserveVMID(
|
|
298
|
+
vmid: number,
|
|
299
|
+
reason: string,
|
|
300
|
+
db: DbOrTransaction,
|
|
301
|
+
): Promise<void> {
|
|
302
|
+
// Check if VMID is already allocated
|
|
303
|
+
const allocations = await db
|
|
304
|
+
.select()
|
|
305
|
+
.from(ipAllocations)
|
|
306
|
+
.where(eq(ipAllocations.vmid, vmid))
|
|
307
|
+
.all();
|
|
308
|
+
|
|
309
|
+
if (allocations.length > 0) {
|
|
310
|
+
throw new Error(`VMID ${vmid} is already allocated to module ${allocations[0].moduleId}`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Check if VMID is already reserved
|
|
314
|
+
const reservations = await db
|
|
315
|
+
.select()
|
|
316
|
+
.from(vmidReservations)
|
|
317
|
+
.where(eq(vmidReservations.vmid, vmid))
|
|
318
|
+
.all();
|
|
319
|
+
|
|
320
|
+
if (reservations.length > 0) {
|
|
321
|
+
throw new Error(`VMID ${vmid} is already reserved: ${reservations[0].reason}`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const newReservation: NewVmidReservation = {
|
|
325
|
+
vmid,
|
|
326
|
+
reason,
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
await db.insert(vmidReservations).values(newReservation).run();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Remove VMID reservation
|
|
334
|
+
*/
|
|
335
|
+
export async function unreserveVMID(vmid: number, db: DbOrTransaction): Promise<void> {
|
|
336
|
+
await db.delete(vmidReservations).where(eq(vmidReservations.vmid, vmid)).run();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* List all VMID reservations
|
|
341
|
+
*/
|
|
342
|
+
export async function listVMIDReservations(db: DbOrTransaction) {
|
|
343
|
+
return await db.select().from(vmidReservations).all();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Check if a specific VMID is available
|
|
348
|
+
*/
|
|
349
|
+
export async function isVMIDAvailable(vmid: number, db: DbOrTransaction): Promise<boolean> {
|
|
350
|
+
// Check if allocated
|
|
351
|
+
const allocations = await db
|
|
352
|
+
.select()
|
|
353
|
+
.from(ipAllocations)
|
|
354
|
+
.where(eq(ipAllocations.vmid, vmid))
|
|
355
|
+
.all();
|
|
356
|
+
|
|
357
|
+
if (allocations.length > 0) {
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Check if reserved
|
|
362
|
+
const reservations = await db
|
|
363
|
+
.select()
|
|
364
|
+
.from(vmidReservations)
|
|
365
|
+
.where(eq(vmidReservations.vmid, vmid))
|
|
366
|
+
.all();
|
|
367
|
+
|
|
368
|
+
return reservations.length === 0;
|
|
369
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IPAM Auto-Allocator Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Database } from 'bun:sqlite';
|
|
6
|
+
import { beforeEach, describe, expect, test } from 'bun:test';
|
|
7
|
+
import { drizzle } from 'drizzle-orm/bun-sqlite';
|
|
8
|
+
import type { DbClient } from '../db/client';
|
|
9
|
+
import * as schema from '../db/schema';
|
|
10
|
+
import { allocateForModule, deallocateForModule, getAllocation } from './auto-allocator';
|
|
11
|
+
|
|
12
|
+
describe('IPAM Auto-Allocator', () => {
|
|
13
|
+
let db: Database;
|
|
14
|
+
let drizzleDb: DbClient;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
// Create in-memory database for testing
|
|
18
|
+
db = new Database(':memory:');
|
|
19
|
+
drizzleDb = drizzle(db, { schema });
|
|
20
|
+
|
|
21
|
+
// Create required tables
|
|
22
|
+
db.exec(`
|
|
23
|
+
CREATE TABLE ip_allocations (
|
|
24
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
25
|
+
module_id TEXT NOT NULL REFERENCES modules(id) ON DELETE CASCADE,
|
|
26
|
+
vmid INTEGER NOT NULL UNIQUE,
|
|
27
|
+
container_ip TEXT NOT NULL UNIQUE,
|
|
28
|
+
zone TEXT NOT NULL,
|
|
29
|
+
allocated_at INTEGER NOT NULL
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
CREATE TABLE vmid_reservations (
|
|
33
|
+
vmid INTEGER PRIMARY KEY NOT NULL,
|
|
34
|
+
reason TEXT,
|
|
35
|
+
created_at TEXT NOT NULL
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
CREATE TABLE ip_reservations (
|
|
39
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
40
|
+
zone TEXT NOT NULL,
|
|
41
|
+
ip_start TEXT NOT NULL,
|
|
42
|
+
ip_end TEXT,
|
|
43
|
+
reason TEXT,
|
|
44
|
+
created_at TEXT NOT NULL
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
CREATE TABLE system_config (
|
|
48
|
+
key TEXT PRIMARY KEY NOT NULL,
|
|
49
|
+
value TEXT NOT NULL,
|
|
50
|
+
created_at TEXT NOT NULL,
|
|
51
|
+
updated_at TEXT NOT NULL
|
|
52
|
+
);
|
|
53
|
+
`);
|
|
54
|
+
|
|
55
|
+
// Insert test zone subnet configuration
|
|
56
|
+
db.prepare(
|
|
57
|
+
`INSERT INTO system_config (key, value, created_at, updated_at)
|
|
58
|
+
VALUES (?, ?, datetime('now'), datetime('now'))`,
|
|
59
|
+
).run('network.dmz.subnet', '10.0.10.0/24');
|
|
60
|
+
|
|
61
|
+
db.prepare(
|
|
62
|
+
`INSERT INTO system_config (key, value, created_at, updated_at)
|
|
63
|
+
VALUES (?, ?, datetime('now'), datetime('now'))`,
|
|
64
|
+
).run('network.app.subnet', '10.0.20.0/24');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('allocateForModule', () => {
|
|
68
|
+
test('allocates first VMID starting at 200', async () => {
|
|
69
|
+
const allocation = await allocateForModule('test-module', 'dmz', drizzleDb, db);
|
|
70
|
+
|
|
71
|
+
expect(allocation.vmid).toBe(200);
|
|
72
|
+
expect(allocation.moduleId).toBe('test-module');
|
|
73
|
+
expect(allocation.zone).toBe('dmz');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('allocates first IP starting at .10 in CIDR format', async () => {
|
|
77
|
+
const allocation = await allocateForModule('test-module', 'dmz', drizzleDb, db);
|
|
78
|
+
|
|
79
|
+
expect(allocation.containerIp).toBe('10.0.10.10/24');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('allocates sequential VMIDs for multiple modules', async () => {
|
|
83
|
+
const alloc1 = await allocateForModule('module-1', 'dmz', drizzleDb, db);
|
|
84
|
+
const alloc2 = await allocateForModule('module-2', 'dmz', drizzleDb, db);
|
|
85
|
+
const alloc3 = await allocateForModule('module-3', 'app', drizzleDb, db);
|
|
86
|
+
|
|
87
|
+
expect(alloc1.vmid).toBe(200);
|
|
88
|
+
expect(alloc2.vmid).toBe(201);
|
|
89
|
+
expect(alloc3.vmid).toBe(202);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('allocates sequential IPs within same zone', async () => {
|
|
93
|
+
const alloc1 = await allocateForModule('module-1', 'dmz', drizzleDb, db);
|
|
94
|
+
const alloc2 = await allocateForModule('module-2', 'dmz', drizzleDb, db);
|
|
95
|
+
|
|
96
|
+
expect(alloc1.containerIp).toBe('10.0.10.10/24');
|
|
97
|
+
expect(alloc2.containerIp).toBe('10.0.10.11/24');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('allocates IPs from different zone subnets', async () => {
|
|
101
|
+
const dmzAlloc = await allocateForModule('module-dmz', 'dmz', drizzleDb, db);
|
|
102
|
+
const appAlloc = await allocateForModule('module-app', 'app', drizzleDb, db);
|
|
103
|
+
|
|
104
|
+
expect(dmzAlloc.containerIp).toBe('10.0.10.10/24');
|
|
105
|
+
expect(appAlloc.containerIp).toBe('10.0.20.10/24');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('skips reserved VMIDs', async () => {
|
|
109
|
+
// Reserve VMID 200
|
|
110
|
+
db.prepare(
|
|
111
|
+
`INSERT INTO vmid_reservations (vmid, reason, created_at)
|
|
112
|
+
VALUES (?, ?, datetime('now'))`,
|
|
113
|
+
).run(200, 'Test reservation');
|
|
114
|
+
|
|
115
|
+
const allocation = await allocateForModule('test-module', 'dmz', drizzleDb, db);
|
|
116
|
+
|
|
117
|
+
expect(allocation.vmid).toBe(201);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('skips reserved IP addresses', async () => {
|
|
121
|
+
// Reserve IP 10.0.10.10
|
|
122
|
+
db.prepare(
|
|
123
|
+
`INSERT INTO ip_reservations (zone, ip_start, ip_end, reason, created_at)
|
|
124
|
+
VALUES (?, ?, ?, ?, datetime('now'))`,
|
|
125
|
+
).run('dmz', '10.0.10.10', null, 'Test reservation');
|
|
126
|
+
|
|
127
|
+
const allocation = await allocateForModule('test-module', 'dmz', drizzleDb, db);
|
|
128
|
+
|
|
129
|
+
expect(allocation.containerIp).toBe('10.0.10.11/24');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('skips reserved IP ranges', async () => {
|
|
133
|
+
// Reserve range 10.0.10.10-10.0.10.12
|
|
134
|
+
db.prepare(
|
|
135
|
+
`INSERT INTO ip_reservations (zone, ip_start, ip_end, reason, created_at)
|
|
136
|
+
VALUES (?, ?, ?, ?, datetime('now'))`,
|
|
137
|
+
).run('dmz', '10.0.10.10', '10.0.10.12', 'Test range reservation');
|
|
138
|
+
|
|
139
|
+
const allocation = await allocateForModule('test-module', 'dmz', drizzleDb, db);
|
|
140
|
+
|
|
141
|
+
expect(allocation.containerIp).toBe('10.0.10.13/24');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('reuses existing allocation for same module', async () => {
|
|
145
|
+
const alloc1 = await allocateForModule('test-module', 'dmz', drizzleDb, db);
|
|
146
|
+
const alloc2 = await allocateForModule('test-module', 'dmz', drizzleDb, db);
|
|
147
|
+
|
|
148
|
+
expect(alloc2.vmid).toBe(alloc1.vmid);
|
|
149
|
+
expect(alloc2.containerIp).toBe(alloc1.containerIp);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('throws error when zone subnet not configured', async () => {
|
|
153
|
+
await expect(allocateForModule('test-module', 'invalid-zone', drizzleDb, db)).rejects.toThrow(
|
|
154
|
+
'Zone subnet not configured',
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('getAllocation', () => {
|
|
160
|
+
test('returns null when no allocation exists', () => {
|
|
161
|
+
const allocation = getAllocation('nonexistent-module', drizzleDb);
|
|
162
|
+
|
|
163
|
+
expect(allocation).toBeNull();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('returns existing allocation', async () => {
|
|
167
|
+
await allocateForModule('test-module', 'dmz', drizzleDb, db);
|
|
168
|
+
|
|
169
|
+
const allocation = getAllocation('test-module', drizzleDb);
|
|
170
|
+
|
|
171
|
+
expect(allocation).not.toBeNull();
|
|
172
|
+
expect(allocation?.moduleId).toBe('test-module');
|
|
173
|
+
expect(allocation?.vmid).toBe(200);
|
|
174
|
+
expect(allocation?.containerIp).toBe('10.0.10.10/24');
|
|
175
|
+
expect(allocation?.zone).toBe('dmz');
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('deallocateForModule', () => {
|
|
180
|
+
test('removes allocation for module', async () => {
|
|
181
|
+
await allocateForModule('test-module', 'dmz', drizzleDb, db);
|
|
182
|
+
|
|
183
|
+
const removed = await deallocateForModule('test-module', drizzleDb);
|
|
184
|
+
|
|
185
|
+
expect(removed).toBe(true);
|
|
186
|
+
|
|
187
|
+
const allocation = getAllocation('test-module', drizzleDb);
|
|
188
|
+
expect(allocation).toBeNull();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('returns false when no allocation exists', async () => {
|
|
192
|
+
const removed = await deallocateForModule('nonexistent-module', drizzleDb);
|
|
193
|
+
|
|
194
|
+
expect(removed).toBe(false);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('makes VMID and IP available for reallocation', async () => {
|
|
198
|
+
await allocateForModule('module-1', 'dmz', drizzleDb, db);
|
|
199
|
+
await allocateForModule('module-2', 'dmz', drizzleDb, db);
|
|
200
|
+
|
|
201
|
+
// Remove first module
|
|
202
|
+
await deallocateForModule('module-1', drizzleDb);
|
|
203
|
+
|
|
204
|
+
// Allocate new module - should get VMID 200 (first available)
|
|
205
|
+
const newAlloc = await allocateForModule('module-3', 'dmz', drizzleDb, db);
|
|
206
|
+
|
|
207
|
+
expect(newAlloc.vmid).toBe(200);
|
|
208
|
+
expect(newAlloc.containerIp).toBe('10.0.10.10/24');
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('VMID allocation edge cases', () => {
|
|
213
|
+
test('handles large VMID gaps with reservations', async () => {
|
|
214
|
+
// Reserve range 200-299
|
|
215
|
+
for (let i = 200; i < 300; i++) {
|
|
216
|
+
db.prepare(
|
|
217
|
+
`INSERT INTO vmid_reservations (vmid, reason, created_at)
|
|
218
|
+
VALUES (?, ?, datetime('now'))`,
|
|
219
|
+
).run(i, 'Reserved');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const allocation = await allocateForModule('test-module', 'dmz', drizzleDb, db);
|
|
223
|
+
|
|
224
|
+
expect(allocation.vmid).toBe(300);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe('IP allocation edge cases', () => {
|
|
229
|
+
test('handles subnet with many allocations', async () => {
|
|
230
|
+
// Allocate first 10 IPs
|
|
231
|
+
for (let i = 0; i < 10; i++) {
|
|
232
|
+
await allocateForModule(`module-${i}`, 'dmz', drizzleDb, db);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const allocation = await allocateForModule('module-10', 'dmz', drizzleDb, db);
|
|
236
|
+
|
|
237
|
+
expect(allocation.containerIp).toBe('10.0.10.20/24'); // .10-.19 taken, .20 is next
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('skips infrastructure range (.1-.9)', async () => {
|
|
241
|
+
const allocation = await allocateForModule('test-module', 'dmz', drizzleDb, db);
|
|
242
|
+
|
|
243
|
+
// Should start at .10, not .1 or .2
|
|
244
|
+
expect(allocation.containerIp).toBe('10.0.10.10/24');
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
});
|