@celilo/cli 0.5.0-alpha.10 → 0.5.0-alpha.12
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/package.json
CHANGED
|
@@ -33,7 +33,12 @@ import { FuelGauge } from '../fuel-gauge';
|
|
|
33
33
|
|
|
34
34
|
const UBUNTU_2404_AMD64_URL =
|
|
35
35
|
'https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img';
|
|
36
|
-
|
|
36
|
+
// Stored under the storage's import/ namespace, so the filename must end in an
|
|
37
|
+
// extension PVE accepts for `import` content (UPLOAD_IMPORT_EXT_RE_1 =
|
|
38
|
+
// ova|qcow2|raw|vmdk) — a `.img` is rejected with "invalid filename or wrong
|
|
39
|
+
// extension". Ubuntu's cloud image is qcow2 format despite the upstream `.img`
|
|
40
|
+
// URL, so we download it under a `.qcow2` name. The URL above is unchanged.
|
|
41
|
+
const UBUNTU_2404_AMD64_FILENAME = 'noble-server-cloudimg-amd64.qcow2';
|
|
37
42
|
const DEFAULT_TEMPLATE_NAME = 'ubuntu-2404-cloudinit';
|
|
38
43
|
|
|
39
44
|
export interface VmTemplateBuildParams {
|
|
@@ -23,14 +23,29 @@ interface TerraformState {
|
|
|
23
23
|
instances?: Array<{
|
|
24
24
|
attributes?: {
|
|
25
25
|
vmid?: number;
|
|
26
|
+
// proxmox_lxc carries the IP here ("10.0.10.12/24" or "dhcp").
|
|
26
27
|
network?: Array<{
|
|
27
28
|
ip?: string;
|
|
28
29
|
}>;
|
|
30
|
+
// proxmox_vm_qemu carries the cloud-init IP here ("ip=10.0.10.12/24,gw=…").
|
|
31
|
+
ipconfig0?: string;
|
|
29
32
|
};
|
|
30
33
|
}>;
|
|
31
34
|
}>;
|
|
32
35
|
}
|
|
33
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Extract the CIDR from a qemu cloud-init `ipconfig0` string
|
|
39
|
+
* ("ip=10.0.10.12/24,gw=10.0.10.1" → "10.0.10.12/24"). Returns undefined for a
|
|
40
|
+
* DHCP config or anything without an explicit `ip=`.
|
|
41
|
+
*/
|
|
42
|
+
function extractIpFromIpconfig(ipconfig0: string | undefined): string | undefined {
|
|
43
|
+
if (!ipconfig0) return undefined;
|
|
44
|
+
const match = ipconfig0.match(/(?:^|,)ip=([^,]+)/);
|
|
45
|
+
const value = match?.[1];
|
|
46
|
+
return value && value !== 'dhcp' ? value : undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
34
49
|
/**
|
|
35
50
|
* Ensure module_configs has vmid and target_ip from Terraform state
|
|
36
51
|
* This recovers from state drift scenarios where container was deleted/recreated
|
|
@@ -82,19 +97,23 @@ export async function ensureProxmoxConfigFromState(
|
|
|
82
97
|
);
|
|
83
98
|
}
|
|
84
99
|
|
|
85
|
-
// Find proxmox_lxc
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
100
|
+
// Find the provisioned guest: an LXC (proxmox_lxc) or a VM (proxmox_vm_qemu).
|
|
101
|
+
const guest = state.resources?.find(
|
|
102
|
+
(r) => r.type === 'proxmox_lxc' || r.type === 'proxmox_vm_qemu',
|
|
103
|
+
);
|
|
104
|
+
if (!guest || !guest.instances || guest.instances.length === 0) {
|
|
105
|
+
throw new Error('No proxmox_lxc or proxmox_vm_qemu resource found in Terraform state');
|
|
89
106
|
}
|
|
90
107
|
|
|
91
|
-
const attributes =
|
|
108
|
+
const attributes = guest.instances[0].attributes;
|
|
92
109
|
if (!attributes) {
|
|
93
|
-
throw new Error(
|
|
110
|
+
throw new Error(`No attributes found in ${guest.type} resource`);
|
|
94
111
|
}
|
|
95
112
|
|
|
96
113
|
const vmid = attributes.vmid;
|
|
97
|
-
|
|
114
|
+
// LXC exposes the IP at network[0].ip; a qemu VM exposes it via the cloud-init
|
|
115
|
+
// ipconfig0 ("ip=<cidr>,gw=<gw>"). Use whichever the guest type provides.
|
|
116
|
+
const containerIp = attributes.network?.[0]?.ip ?? extractIpFromIpconfig(attributes.ipconfig0);
|
|
98
117
|
|
|
99
118
|
if (!vmid) {
|
|
100
119
|
throw new Error('vmid not found in Terraform state');
|
|
@@ -790,7 +790,28 @@ describe("targetNodeFromTfState (ISS-0090 — terraform state is celilo's placem
|
|
|
790
790
|
expect(targetNodeFromTfState(state)).toBe('node3');
|
|
791
791
|
});
|
|
792
792
|
|
|
793
|
-
test('
|
|
793
|
+
test('reads the node from a proxmox_vm_qemu target_node attribute (type:vm)', () => {
|
|
794
|
+
const state = {
|
|
795
|
+
resources: [
|
|
796
|
+
{
|
|
797
|
+
type: 'proxmox_vm_qemu',
|
|
798
|
+
instances: [{ attributes: { target_node: 'node3', id: 'node3/qemu/208' } }],
|
|
799
|
+
},
|
|
800
|
+
],
|
|
801
|
+
};
|
|
802
|
+
expect(targetNodeFromTfState(state)).toBe('node3');
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
test('parses the qemu resource id when target_node is absent (type:vm)', () => {
|
|
806
|
+
const state = {
|
|
807
|
+
resources: [
|
|
808
|
+
{ type: 'proxmox_vm_qemu', instances: [{ attributes: { id: 'node3/qemu/208' } }] },
|
|
809
|
+
],
|
|
810
|
+
};
|
|
811
|
+
expect(targetNodeFromTfState(state)).toBe('node3');
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
test('returns null when there is no proxmox_lxc/proxmox_vm_qemu resource (fresh/empty state)', () => {
|
|
794
815
|
expect(targetNodeFromTfState({ resources: [] })).toBeNull();
|
|
795
816
|
expect(targetNodeFromTfState({})).toBeNull();
|
|
796
817
|
});
|
|
@@ -216,10 +216,11 @@ export function injectProxmoxDns(content: string, hasNameserver: boolean): strin
|
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
/**
|
|
219
|
-
* Extract the node a
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
* resource id (`<node>/lxc/<vmid>`
|
|
219
|
+
* Extract the node a Proxmox guest is deployed on from a parsed terraform state
|
|
220
|
+
* object — an LXC (proxmox_lxc) or a VM (proxmox_vm_qemu). Pure logic (Rule 10),
|
|
221
|
+
* split from the file read for testability. Prefers the explicit `target_node`
|
|
222
|
+
* attribute; falls back to parsing the resource id (`<node>/lxc/<vmid>` for an
|
|
223
|
+
* LXC, `<node>/qemu/<vmid>` for a VM). Returns null when there's no such
|
|
223
224
|
* resource (e.g. an empty/fresh state).
|
|
224
225
|
*/
|
|
225
226
|
export function targetNodeFromTfState(state: {
|
|
@@ -228,15 +229,17 @@ export function targetNodeFromTfState(state: {
|
|
|
228
229
|
instances?: Array<{ attributes?: { target_node?: string; id?: string } }>;
|
|
229
230
|
}>;
|
|
230
231
|
}): string | null {
|
|
231
|
-
const
|
|
232
|
-
|
|
232
|
+
const guest = state.resources?.find(
|
|
233
|
+
(r) => r.type === 'proxmox_lxc' || r.type === 'proxmox_vm_qemu',
|
|
234
|
+
);
|
|
235
|
+
const attrs = guest?.instances?.[0]?.attributes;
|
|
233
236
|
if (!attrs) {
|
|
234
237
|
return null;
|
|
235
238
|
}
|
|
236
239
|
if (attrs.target_node) {
|
|
237
240
|
return attrs.target_node;
|
|
238
241
|
}
|
|
239
|
-
// id format: "<node>/lxc/<vmid>"
|
|
242
|
+
// id format: "<node>/lxc/<vmid>" (LXC) or "<node>/qemu/<vmid>" (VM)
|
|
240
243
|
return attrs.id?.split('/')[0] || null;
|
|
241
244
|
}
|
|
242
245
|
|
|
@@ -745,6 +748,10 @@ export async function generateTemplates(options: GenerateOptions): Promise<Gener
|
|
|
745
748
|
default_target_node: string;
|
|
746
749
|
lxc_template: string;
|
|
747
750
|
storage: string;
|
|
751
|
+
// Optional: only set once a cloud-init VM template has been built/selected
|
|
752
|
+
// for the service (see service reconfigure). Required by `type: vm` modules;
|
|
753
|
+
// absent for services that only deploy LXCs.
|
|
754
|
+
vm_template?: string;
|
|
748
755
|
};
|
|
749
756
|
|
|
750
757
|
// ISS-0090: deploy follows REALITY. Resolve the node from Proxmox (it sees
|
|
@@ -774,6 +781,13 @@ export async function generateTemplates(options: GenerateOptions): Promise<Gener
|
|
|
774
781
|
// any stale __infra_target_node a prior generate left behind.
|
|
775
782
|
upsertModuleConfig(db, moduleId, '__infra_lxc_template', providerConfig.lxc_template);
|
|
776
783
|
upsertModuleConfig(db, moduleId, '__infra_storage', providerConfig.storage);
|
|
784
|
+
// vm_template is only present once a VM template exists for the service.
|
|
785
|
+
// Persist it when set so `type: vm` modules can resolve $self:vm_template;
|
|
786
|
+
// a `type: vm` deploy against a service without one then fails loudly at
|
|
787
|
+
// variable resolution (the intended "no VM template configured" error).
|
|
788
|
+
if (providerConfig.vm_template) {
|
|
789
|
+
upsertModuleConfig(db, moduleId, '__infra_vm_template', providerConfig.vm_template);
|
|
790
|
+
}
|
|
777
791
|
deleteModuleConfig(db, moduleId, '__infra_target_node');
|
|
778
792
|
|
|
779
793
|
log.success(`Infrastructure resolved: target_node=${decision.node} (${decision.source})`);
|
|
@@ -816,7 +830,7 @@ export async function generateTemplates(options: GenerateOptions): Promise<Gener
|
|
|
816
830
|
|
|
817
831
|
// lxc_template / storage are provider config (intent, not drift-prone) — read
|
|
818
832
|
// them back from the __infra_* rows persisted above.
|
|
819
|
-
const infraKeys = ['lxc_template', 'storage'];
|
|
833
|
+
const infraKeys = ['lxc_template', 'storage', 'vm_template'];
|
|
820
834
|
for (const key of infraKeys) {
|
|
821
835
|
const infraConfig = db
|
|
822
836
|
.select()
|