@homeflare/alchemy 0.10.1 → 0.12.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 (265) hide show
  1. package/README.md +3 -3
  2. package/dist/caddy/admin-guard.d.ts +1 -1
  3. package/dist/caddy/index.js.map +4 -4
  4. package/dist/cloudflare/client.d.ts +1 -1
  5. package/dist/cloudflare/index.js.map +2 -2
  6. package/dist/cloudflare/lock-rules.d.ts +1 -1
  7. package/dist/forgejo/branch-protection-form.d.ts +1 -1
  8. package/dist/forgejo/branch-protection.d.ts +1 -1
  9. package/dist/forgejo/index.js.map +10 -10
  10. package/dist/forgejo/org-actions-secrets.d.ts +1 -1
  11. package/dist/forgejo/org-label.d.ts +1 -1
  12. package/dist/forgejo/org-team.d.ts +1 -1
  13. package/dist/forgejo/repo-webhook.d.ts +1 -1
  14. package/dist/forgejo/repository.d.ts +2 -2
  15. package/dist/forgejo/repository.d.ts.map +1 -1
  16. package/dist/forgejo/resource.d.ts +2 -2
  17. package/dist/forgejo/team-member.d.ts +1 -1
  18. package/dist/launchd/index.js.map +2 -2
  19. package/dist/openbao/auth-role.d.ts +1 -1
  20. package/dist/openbao/bao-http.d.ts +4 -4
  21. package/dist/openbao/cloudflare-permission-groups.d.ts +1 -1
  22. package/dist/openbao/cloudflare-role-form.d.ts +2 -2
  23. package/dist/openbao/cloudflare-role-form.d.ts.map +1 -1
  24. package/dist/openbao/cloudflare-roles-expand.d.ts +1 -1
  25. package/dist/openbao/digest.d.ts +1 -1
  26. package/dist/openbao/forgejo-bootstrap-parse.d.ts +2 -2
  27. package/dist/openbao/forgejo-bootstrap-parse.d.ts.map +1 -1
  28. package/dist/openbao/forgejo-bootstrap.d.ts +1 -1
  29. package/dist/openbao/index.js +10 -8
  30. package/dist/openbao/index.js.map +18 -18
  31. package/dist/openbao/mount.d.ts +1 -1
  32. package/dist/openbao/pki-role.d.ts +1 -1
  33. package/dist/openbao/policy.d.ts +2 -2
  34. package/dist/openbao/proxmox-role-form.d.ts +2 -2
  35. package/dist/openbao/proxmox-role-form.d.ts.map +1 -1
  36. package/dist/openbao/proxmox-role.d.ts +10 -10
  37. package/dist/openbao/ssh-role.d.ts +3 -3
  38. package/dist/ownership/adopting.d.ts +34 -0
  39. package/dist/ownership/adopting.d.ts.map +1 -0
  40. package/dist/ownership/probe.d.ts +15 -5
  41. package/dist/ownership/probe.d.ts.map +1 -1
  42. package/dist/ownership/rows.d.ts +12 -0
  43. package/dist/ownership/rows.d.ts.map +1 -1
  44. package/dist/ownership/store-fixture.d.ts +10 -0
  45. package/dist/ownership/store-fixture.d.ts.map +1 -0
  46. package/dist/proxmox/acl.d.ts +2 -2
  47. package/dist/proxmox/acl.d.ts.map +1 -1
  48. package/dist/proxmox/api-token-form.d.ts +2 -2
  49. package/dist/proxmox/api-token.d.ts +11 -11
  50. package/dist/proxmox/ceph-daemon-form.d.ts +3 -3
  51. package/dist/proxmox/ceph-daemon.d.ts +19 -19
  52. package/dist/proxmox/ceph-daemon.d.ts.map +1 -1
  53. package/dist/proxmox/ceph-flag.d.ts +5 -5
  54. package/dist/proxmox/ceph-fs-wire.d.ts +5 -5
  55. package/dist/proxmox/ceph-fs.d.ts +16 -16
  56. package/dist/proxmox/ceph-osd-tree.d.ts +16 -16
  57. package/dist/proxmox/ceph-osd.d.ts +5 -5
  58. package/dist/proxmox/ceph-pool-form.d.ts +13 -13
  59. package/dist/proxmox/ceph-pool-form.d.ts.map +1 -1
  60. package/dist/proxmox/ceph-pool.d.ts +3 -3
  61. package/dist/proxmox/client.d.ts +2 -2
  62. package/dist/proxmox/credentials.d.ts +8 -8
  63. package/dist/proxmox/credentials.d.ts.map +1 -1
  64. package/dist/proxmox/fake-pve-lxc.d.ts +8 -0
  65. package/dist/proxmox/fake-pve-lxc.d.ts.map +1 -1
  66. package/dist/proxmox/firewall-alias-form.d.ts +1 -1
  67. package/dist/proxmox/firewall-alias.d.ts +1 -1
  68. package/dist/proxmox/generated/pbs.d.ts +1 -1
  69. package/dist/proxmox/generated/pve.d.ts +1 -1
  70. package/dist/proxmox/group.d.ts +8 -8
  71. package/dist/proxmox/ha-resource.d.ts +3 -2
  72. package/dist/proxmox/ha-resource.d.ts.map +1 -1
  73. package/dist/proxmox/ha-rule-form.d.ts +5 -5
  74. package/dist/proxmox/ha-rule.d.ts +3 -3
  75. package/dist/proxmox/index.d.ts +6 -0
  76. package/dist/proxmox/index.d.ts.map +1 -1
  77. package/dist/proxmox/index.js +465 -217
  78. package/dist/proxmox/index.js.map +63 -58
  79. package/dist/proxmox/lease-cache.d.ts +4 -4
  80. package/dist/proxmox/lxc-adoption.d.ts +32 -0
  81. package/dist/proxmox/lxc-adoption.d.ts.map +1 -0
  82. package/dist/proxmox/lxc-harness.d.ts +12 -0
  83. package/dist/proxmox/lxc-harness.d.ts.map +1 -1
  84. package/dist/proxmox/lxc-props.d.ts +2 -2
  85. package/dist/proxmox/lxc.d.ts +8 -8
  86. package/dist/proxmox/lxc.d.ts.map +1 -1
  87. package/dist/proxmox/members.d.ts +3 -3
  88. package/dist/proxmox/network-apply-read.d.ts +5 -5
  89. package/dist/proxmox/network-apply.d.ts +9 -9
  90. package/dist/proxmox/node-network-form.d.ts +6 -6
  91. package/dist/proxmox/node-network.d.ts +17 -17
  92. package/dist/proxmox/node-network.d.ts.map +1 -1
  93. package/dist/proxmox/notification-target.d.ts +3 -2
  94. package/dist/proxmox/notification-target.d.ts.map +1 -1
  95. package/dist/proxmox/pbs-datastore-guard.d.ts +1 -1
  96. package/dist/proxmox/pbs-datastore.d.ts +10 -10
  97. package/dist/proxmox/pbs-prune-job.d.ts +2 -2
  98. package/dist/proxmox/pbs-sync-job.d.ts +4 -4
  99. package/dist/proxmox/pbs-verify-job.d.ts +1 -1
  100. package/dist/proxmox/pool.d.ts +4 -4
  101. package/dist/proxmox/provision-baseline.d.ts +74 -0
  102. package/dist/proxmox/provision-baseline.d.ts.map +1 -0
  103. package/dist/proxmox/provision-bootstrap.d.ts +33 -0
  104. package/dist/proxmox/provision-bootstrap.d.ts.map +1 -0
  105. package/dist/proxmox/provision-cli-fake.d.ts +38 -0
  106. package/dist/proxmox/provision-cli-fake.d.ts.map +1 -0
  107. package/dist/proxmox/provision-declare.d.ts +25 -0
  108. package/dist/proxmox/provision-declare.d.ts.map +1 -0
  109. package/dist/proxmox/replication-job.d.ts +9 -8
  110. package/dist/proxmox/replication-job.d.ts.map +1 -1
  111. package/dist/proxmox/resource.d.ts +2 -2
  112. package/dist/proxmox/resource.d.ts.map +1 -1
  113. package/dist/proxmox/sdn-apply-read.d.ts +7 -7
  114. package/dist/proxmox/sdn-apply.d.ts +1 -1
  115. package/dist/proxmox/sdn-subnet-form.d.ts +1 -1
  116. package/dist/proxmox/sdn-subnet.d.ts +5 -5
  117. package/dist/proxmox/sdn-vnet.d.ts +5 -6
  118. package/dist/proxmox/sdn-vnet.d.ts.map +1 -1
  119. package/dist/proxmox/storage.d.ts +4 -3
  120. package/dist/proxmox/storage.d.ts.map +1 -1
  121. package/dist/proxmox/user.d.ts +3 -2
  122. package/dist/proxmox/user.d.ts.map +1 -1
  123. package/dist/proxmox/values.d.ts +1 -1
  124. package/dist/proxmox/zfs-pool-write.d.ts +1 -1
  125. package/dist/proxmox/zfs-pool.d.ts +5 -5
  126. package/dist/talos/credentials.d.ts +1 -1
  127. package/dist/talos/credentials.d.ts.map +1 -1
  128. package/dist/talos/index.js.map +1 -1
  129. package/docs/adopt-verify.md +21 -5
  130. package/docs/adopted-deploys.md +5 -3
  131. package/docs/ownership.md +27 -2
  132. package/docs/provision-baseline.md +106 -0
  133. package/docs/proxmox-lxc-adopt.md +83 -0
  134. package/docs/proxmox-lxc.md +12 -49
  135. package/docs/proxmox.md +8 -0
  136. package/package.json +1 -1
  137. package/src/caddy/admin-guard.ts +1 -1
  138. package/src/caddy/local-admin.ts +1 -1
  139. package/src/cloudflare/client.ts +1 -1
  140. package/src/cloudflare/lock-rules.ts +1 -1
  141. package/src/forgejo/branch-protection-form.ts +1 -1
  142. package/src/forgejo/branch-protection.ts +1 -1
  143. package/src/forgejo/client.ts +1 -1
  144. package/src/forgejo/org-actions-secrets.ts +1 -1
  145. package/src/forgejo/org-label.ts +1 -1
  146. package/src/forgejo/org-team.ts +1 -1
  147. package/src/forgejo/repo-webhook.ts +1 -1
  148. package/src/forgejo/repository.ts +2 -2
  149. package/src/forgejo/resource.ts +2 -2
  150. package/src/forgejo/team-member.ts +1 -1
  151. package/src/openbao/README.md +4 -4
  152. package/src/openbao/auth-role.ts +1 -1
  153. package/src/openbao/bao-http.test.ts +2 -2
  154. package/src/openbao/bao-http.ts +6 -6
  155. package/src/openbao/bao-status.test.ts +1 -1
  156. package/src/openbao/bao-status.ts +1 -1
  157. package/src/openbao/cloudflare-group-scope.test.ts +1 -1
  158. package/src/openbao/cloudflare-permission-groups.test.ts +1 -1
  159. package/src/openbao/cloudflare-permission-groups.ts +3 -3
  160. package/src/openbao/cloudflare-policy.ts +1 -1
  161. package/src/openbao/cloudflare-role-form.ts +2 -2
  162. package/src/openbao/cloudflare-roles-expand.ts +1 -1
  163. package/src/openbao/digest.ts +1 -1
  164. package/src/openbao/forgejo-bootstrap-parse.test.ts +3 -3
  165. package/src/openbao/forgejo-bootstrap-parse.ts +2 -2
  166. package/src/openbao/forgejo-bootstrap.ts +4 -4
  167. package/src/openbao/kubernetes-role-form.test.ts +5 -5
  168. package/src/openbao/mount.ts +1 -1
  169. package/src/openbao/pki-role.ts +1 -1
  170. package/src/openbao/policy-wire.test.ts +3 -3
  171. package/src/openbao/policy.ts +2 -2
  172. package/src/openbao/proxmox-role-form.ts +5 -5
  173. package/src/openbao/proxmox-role.ts +11 -11
  174. package/src/openbao/ssh-role.ts +3 -3
  175. package/src/ownership/adopting.test.ts +63 -0
  176. package/src/ownership/adopting.ts +46 -0
  177. package/src/ownership/ownership.test.ts +1 -34
  178. package/src/ownership/probe.ts +27 -14
  179. package/src/ownership/rows.ts +8 -2
  180. package/src/ownership/store-fixture.ts +50 -0
  181. package/src/proxmox/acl.ts +4 -4
  182. package/src/proxmox/adopt-noop.test.ts +6 -6
  183. package/src/proxmox/api-token-form.ts +4 -4
  184. package/src/proxmox/api-token.ts +13 -13
  185. package/src/proxmox/ceph-daemon-form.ts +3 -3
  186. package/src/proxmox/ceph-daemon.ts +20 -20
  187. package/src/proxmox/ceph-flag.ts +7 -7
  188. package/src/proxmox/ceph-fs-wire.ts +7 -7
  189. package/src/proxmox/ceph-fs.ts +18 -18
  190. package/src/proxmox/ceph-osd-tree.ts +16 -16
  191. package/src/proxmox/ceph-osd.ts +6 -6
  192. package/src/proxmox/ceph-pool-adopt.test.ts +14 -14
  193. package/src/proxmox/ceph-pool-form.ts +13 -13
  194. package/src/proxmox/ceph-pool.ts +6 -6
  195. package/src/proxmox/client.test.ts +3 -3
  196. package/src/proxmox/client.ts +3 -3
  197. package/src/proxmox/credentials.test.ts +6 -8
  198. package/src/proxmox/credentials.ts +8 -8
  199. package/src/proxmox/fake-pve-lxc.ts +13 -1
  200. package/src/proxmox/firewall-alias-form.ts +1 -1
  201. package/src/proxmox/firewall-alias.ts +1 -1
  202. package/src/proxmox/generated/pbs.ts +1 -1
  203. package/src/proxmox/generated/pve.ts +1 -1
  204. package/src/proxmox/group.ts +10 -10
  205. package/src/proxmox/ha-resource.ts +3 -2
  206. package/src/proxmox/ha-rule-form.ts +5 -5
  207. package/src/proxmox/ha-rule.ts +3 -3
  208. package/src/proxmox/index.test.ts +7 -0
  209. package/src/proxmox/index.ts +14 -0
  210. package/src/proxmox/lease-cache.test.ts +19 -19
  211. package/src/proxmox/lease-cache.ts +4 -4
  212. package/src/proxmox/lxc-adopt.test.ts +16 -23
  213. package/src/proxmox/lxc-adoption.ts +50 -0
  214. package/src/proxmox/lxc-create-form.ts +1 -1
  215. package/src/proxmox/lxc-guards.test.ts +10 -17
  216. package/src/proxmox/lxc-harness.ts +27 -4
  217. package/src/proxmox/lxc-judge.test.ts +5 -5
  218. package/src/proxmox/lxc-lifecycle.ts +1 -1
  219. package/src/proxmox/lxc-ownership.test.ts +19 -14
  220. package/src/proxmox/lxc-props.ts +2 -2
  221. package/src/proxmox/lxc-read.test.ts +3 -3
  222. package/src/proxmox/lxc-strict-adopt.test.ts +104 -0
  223. package/src/proxmox/lxc-volume.test.ts +9 -9
  224. package/src/proxmox/lxc-volume.ts +1 -1
  225. package/src/proxmox/lxc-wire.ts +1 -1
  226. package/src/proxmox/lxc.ts +19 -15
  227. package/src/proxmox/members.test.ts +1 -1
  228. package/src/proxmox/members.ts +3 -3
  229. package/src/proxmox/metric-server-otel.ts +2 -2
  230. package/src/proxmox/network-apply-read.ts +10 -10
  231. package/src/proxmox/network-apply.ts +10 -10
  232. package/src/proxmox/node-network-form.ts +7 -7
  233. package/src/proxmox/node-network.ts +18 -18
  234. package/src/proxmox/notification-target.ts +3 -2
  235. package/src/proxmox/pbs-datastore-guard.ts +1 -1
  236. package/src/proxmox/pbs-datastore.ts +10 -10
  237. package/src/proxmox/pbs-prune-job.ts +2 -2
  238. package/src/proxmox/pbs-sync-job.ts +4 -4
  239. package/src/proxmox/pbs-verify-job.ts +1 -1
  240. package/src/proxmox/pool.test.ts +2 -2
  241. package/src/proxmox/pool.ts +4 -4
  242. package/src/proxmox/provision-baseline.test.ts +87 -0
  243. package/src/proxmox/provision-baseline.ts +162 -0
  244. package/src/proxmox/provision-bootstrap.test.ts +132 -0
  245. package/src/proxmox/provision-bootstrap.ts +135 -0
  246. package/src/proxmox/provision-cli-fake.ts +217 -0
  247. package/src/proxmox/provision-declare.test.ts +111 -0
  248. package/src/proxmox/provision-declare.ts +103 -0
  249. package/src/proxmox/replication-job.ts +9 -8
  250. package/src/proxmox/resource.ts +3 -3
  251. package/src/proxmox/sdn-apply-read.test.ts +16 -16
  252. package/src/proxmox/sdn-apply-read.ts +7 -7
  253. package/src/proxmox/sdn-apply.ts +3 -3
  254. package/src/proxmox/sdn-subnet-form.ts +1 -1
  255. package/src/proxmox/sdn-subnet.ts +5 -5
  256. package/src/proxmox/sdn-vnet.ts +5 -6
  257. package/src/proxmox/storage.ts +4 -3
  258. package/src/proxmox/user.ts +3 -2
  259. package/src/proxmox/values.test.ts +2 -2
  260. package/src/proxmox/values.ts +1 -1
  261. package/src/proxmox/zfs-pool-write.ts +1 -1
  262. package/src/proxmox/zfs-pool.ts +6 -6
  263. package/src/talos/credentials.ts +3 -3
  264. package/src/talos/values.test.ts +9 -9
  265. package/src/verify/cli.test.ts +1 -1
@@ -13,7 +13,7 @@ import { judgeVolume, parseVolume, sizeBytes } from './lxc-volume.ts';
13
13
  const props = (over: Partial<LxcProps>): LxcProps => ({
14
14
  node: 'pve1',
15
15
  target: TARGET,
16
- vmid: 100,
16
+ vmid: 900,
17
17
  ...over,
18
18
  });
19
19
  const live = (config: Record<string, unknown>) => ({ unprivileged: 1, ...config });
@@ -36,7 +36,7 @@ describe('sizes and volumes, as pve-container parses them', () => {
36
36
 
37
37
  test('an options change on a volume declared as new is written with the LIVE volume id', () => {
38
38
  const verdict = judgeVolume('mp0', 'tank:200,mp=/data,backup=1', LIVE_MP);
39
- expect(verdict).toEqual({ put: 'tank:subvol-100-disk-0,mp=/data,backup=1,size=200G' });
39
+ expect(verdict).toEqual({ put: 'tank:subvol-900-disk-0,mp=/data,backup=1,size=200G' });
40
40
  });
41
41
 
42
42
  test('a fractional new-disk size grows by the same GiB', () => {
@@ -44,7 +44,7 @@ describe('sizes and volumes, as pve-container parses them', () => {
44
44
  });
45
45
  });
46
46
 
47
- const LIVE_MP = 'tank:subvol-100-disk-0,mp=/data,backup=0,size=200G';
47
+ const LIVE_MP = 'tank:subvol-900-disk-0,mp=/data,backup=0,size=200G';
48
48
 
49
49
  describe('scalars compare the way PVE stores them', () => {
50
50
  test.each([
@@ -106,7 +106,7 @@ describe('features follow check_ct_modify_config_perm', () => {
106
106
 
107
107
  test('clearing features that hold more than nesting is root@pam only, as PVE counts keys', () => {
108
108
  const change = judge(props({ features: '' }), live({ features: 'nesting=1,keyctl=0' }));
109
- expect(change.refuse[0]).toMatch(/pct set 100 --delete features/);
109
+ expect(change.refuse[0]).toMatch(/pct set 900 --delete features/);
110
110
  });
111
111
  });
112
112
 
@@ -147,7 +147,7 @@ describe('create', () => {
147
147
  ostemplate: 'local:vztmpl/t.tar.zst',
148
148
  start: '1',
149
149
  unprivileged: '1',
150
- vmid: '100',
150
+ vmid: '900',
151
151
  });
152
152
  });
153
153
 
@@ -72,7 +72,7 @@ export const readLive = (where: LxcWhere) =>
72
72
  'GET',
73
73
  'cluster/resources?type=vm',
74
74
  );
75
- // ⚠️ `Number()`, NOT `===` ON THE RAW VALUE: a vmid that came back as `"100"` would otherwise
75
+ // ⚠️ `Number()`, NOT `===` ON THE RAW VALUE: a vmid that came back as `"900"` would otherwise
76
76
  // match nothing, and "listed nowhere" is the one answer that turns into a create.
77
77
  const found = (rows ?? []).find((row) => Number(row.vmid) === where.vmid);
78
78
  if (found !== undefined && found.node === where.node && found.type === 'lxc') {
@@ -7,7 +7,8 @@
7
7
  * planned a create refuses a guest it then finds, and forgets its `creating` row; under
8
8
  * `--adopt` it records a matching one with no write and still refuses one that differs; and a
9
9
  * create interrupted after its POST resumes WITHOUT `--adopt` when the guest is what it wrote,
10
- * and asks for `--adopt` when someone changed it meanwhile.
10
+ * and asks for `--adopt` when someone changed it meanwhile — which then adopts it only as it
11
+ * now runs, never writing the declaration back over the change (lxc-adoption.ts).
11
12
  * ★ THE INTERRUPTION IS A CREATE TASK THAT ENDS IN AN ERROR AFTER THE FAKE BUILT THE GUEST — to the
12
13
  * engine the same as a deploy killed while it waited on the task: a live guest, and a
13
14
  * `creating` row with no attributes.
@@ -15,20 +16,12 @@
15
16
  import { afterEach, describe, expect, test } from 'bun:test';
16
17
  import { adopt } from 'alchemy/AdoptPolicy';
17
18
  import { type FakePve, fakePve, writesOf } from './fake-pve-lxc.ts';
18
- import { LIVE, TARGET, lxcEngine, seed } from './lxc-harness.ts';
19
+ import { KEY, NODE, TARGET, VMID, lxcEngine, pasted, seed } from './lxc-harness.ts';
19
20
  import { ProxmoxLxc } from './lxc.ts';
20
21
  import type { LxcProps } from './lxc-props.ts';
21
22
 
22
- const NODE = 'pve1';
23
- const VMID = 100;
24
- const KEY = `${NODE}/${String(VMID)}`;
25
23
  const TAKEOVER = 'already exists, and this stack holds no state for it';
26
24
 
27
- const pasted = (over: Partial<LxcProps> = {}): LxcProps => {
28
- const { lxc: _raw, ...config } = LIVE;
29
- return { ...(config as Partial<LxcProps>), node: NODE, target: TARGET, vmid: VMID, ...over };
30
- };
31
-
32
25
  let fake: FakePve | undefined;
33
26
  afterEach(() => {
34
27
  fake?.stop();
@@ -146,14 +139,26 @@ describe('an interrupted create', () => {
146
139
  expect(stack.status('door')).toBe('created');
147
140
  });
148
141
 
149
- test('asks for --adopt when the guest changed meanwhile, and resumes with it', async () => {
142
+ test('is our own guest, not an adoption: a declaration changed meanwhile is written', async () => {
143
+ const { pve, stack } = await interrupted();
144
+ const run = await stack.deploy(door({ hostname: 'door-renamed' }));
145
+ expect(run.failure).toBe('');
146
+ expect(writesOf(pve)).toEqual([`POST nodes/${NODE}/lxc`, `PUT nodes/${NODE}/lxc/150/config`]);
147
+ expect(pve.guests.get(`${NODE}/150`)?.['hostname']).toBe('door-renamed');
148
+ });
149
+
150
+ test('asks for --adopt when the guest changed meanwhile, and adopts it only as it runs', async () => {
150
151
  const { pve, stack } = await interrupted();
151
152
  const guest = pve.guests.get(`${NODE}/150`) ?? {};
152
153
  guest['hostname'] = 'someone-else';
153
154
  expect((await stack.deploy(door())).failure).toContain('Cannot resume creating');
154
- const resumed = await stack.deploy(door(), { adopt: true });
155
+ const refused = await stack.deploy(door(), { adopt: true });
156
+ expect(refused.failure).toContain('adopting it would change hostname');
157
+ expect(refused.failure).not.toContain('someone-else');
158
+ expect(writesOf(pve)).toEqual([`POST nodes/${NODE}/lxc`]);
159
+ const resumed = await stack.deploy(door({ hostname: 'someone-else' }), { adopt: true });
155
160
  expect(resumed.failure).toBe('');
156
- expect(resumed.warnings.join('\n')).toContain('differs from the declaration in hostname');
157
- expect(pve.guests.get(`${NODE}/150`)?.['hostname']).toBe('door-example');
161
+ expect(writesOf(pve)).toEqual([`POST nodes/${NODE}/lxc`]);
162
+ expect(pve.guests.get(`${NODE}/150`)?.['hostname']).toBe('someone-else');
158
163
  });
159
164
  });
@@ -58,7 +58,7 @@ export interface LxcProps extends WithTarget {
58
58
  /** MiB. */
59
59
  swap?: number;
60
60
  /**
61
- * `local-zfs:subvol-100-disk-0,size=40G` as read, or `local-zfs:40` (40 GiB, new volume) to
61
+ * `local-zfs:subvol-900-disk-0,size=40G` as read, or `local-zfs:40` (40 GiB, new volume) to
62
62
  * create. See lxc-volume.ts for what may change: options in place, size up only.
63
63
  */
64
64
  rootfs?: string;
@@ -79,7 +79,7 @@ export interface LxcProps extends WithTarget {
79
79
  console?: PveFlag;
80
80
  tty?: number;
81
81
  cmode?: string;
82
- /** `tank:subvol-100-disk-1,mp=/data,backup=1,size=100G`, or `tank:100,mp=/data` to create. */
82
+ /** `tank:subvol-900-disk-1,mp=/data,backup=1,size=100G`, or `tank:100,mp=/data` to create. */
83
83
  [mount: `mp${number}`]: string | undefined;
84
84
  /** `name=eth0,bridge=vmbr0,ip=dhcp`. ⚠️ An undeclared `hwaddr` keeps the live MAC. */
85
85
  [nic: `net${number}`]: string | undefined;
@@ -40,10 +40,10 @@ describe('absent means the cluster does not list the vmid', () => {
40
40
 
41
41
  test('a 500 for a guest the cluster lists on this node fails with PVE’s error', async () => {
42
42
  fake = fakePve();
43
- seed(fake, NODE, 100, LIVE);
43
+ seed(fake, NODE, 900, LIVE);
44
44
  const message = 'got timeout\n';
45
- fake.configErrors.set(`${NODE}/100`, { body: { data: null, message }, status: 500 });
46
- const run = await lxcEngine(fake).plan(declared(100));
45
+ fake.configErrors.set(`${NODE}/900`, { body: { data: null, message }, status: 500 });
46
+ const run = await lxcEngine(fake).plan(declared(900));
47
47
  expect(run.failure).toContain('got timeout');
48
48
  expect(run.actions).toEqual({});
49
49
  expect(writesOf(fake)).toEqual([]);
@@ -0,0 +1,104 @@
1
+ /**
2
+ * AN ADOPTION NEVER CHANGES A GUEST (lxc-adoption.ts), through Alchemy's real plan and apply over
3
+ * fake-pve-lxc.ts (placeholders only).
4
+ *
5
+ * ⛔ WHAT THESE PIN: a cold adoption that differs from the live guest in ANY key fails the plan —
6
+ * under `adopt(true)` and under `--adopt` alike — naming the keys and none of the values, before
7
+ * `judge`'s own refusals (which print a declared value); nothing is written and no row is left.
8
+ * A hand edit between the plan and the deploy is refused at apply with nothing written; the
9
+ * adoption row it leaves keeps refusing until the declaration says what is live, and then adopts
10
+ * with no write. An interrupted create that `--adopt` resumes is held to the same rule:
11
+ * lxc-ownership.test.ts.
12
+ * ★ MUTATION-CHECKED 2026-09-21: removing either `refuseAdoptedDrift` call in lxc.ts, or any one of
13
+ * the three adoption answers in ownership/adopting.ts, fails a test here or in
14
+ * lxc-ownership.test.ts.
15
+ */
16
+ import { afterEach, describe, expect, test } from 'bun:test';
17
+ import { adopt } from 'alchemy/AdoptPolicy';
18
+ import { type FakePve, fakePve, writesOf } from './fake-pve-lxc.ts';
19
+ import { KEY, NODE, VMID, lxcEngine, pasted, seed } from './lxc-harness.ts';
20
+ import { ProxmoxLxc } from './lxc.ts';
21
+ import type { LxcProps } from './lxc-props.ts';
22
+
23
+ const REFUSAL = 'adopting it would change';
24
+
25
+ let fake: FakePve | undefined;
26
+ afterEach(() => {
27
+ fake?.stop();
28
+ fake = undefined;
29
+ });
30
+ const live = () => {
31
+ fake = fakePve();
32
+ seed(fake, NODE, VMID);
33
+ return fake;
34
+ };
35
+
36
+ const drifted = (): LxcProps =>
37
+ pasted({ memory: 4096, net1: 'name=eth1,bridge=vmbr1,ip=198.51.100.4/24,mtu=9000' });
38
+
39
+ describe('a cold adoption that differs from the live guest fails the plan', () => {
40
+ test.each([
41
+ ['adopt(true)', () => ProxmoxLxc('ct', drifted()).pipe(adopt(true)), {}],
42
+ ['--adopt', () => ProxmoxLxc('ct', drifted()), { adopt: true }],
43
+ ])('under %s: keys named, values never, nothing written', async (_, declare, options) => {
44
+ const pve = live();
45
+ const stack = lxcEngine(pve);
46
+ const planned = await stack.plan(declare(), options);
47
+ expect(planned.failure).toContain(`CT ${String(VMID)} on ${NODE}: ${REFUSAL} memory, net1.`);
48
+ for (const value of ['4096', '8192', '9000', '1500']) {
49
+ expect(planned.failure).not.toContain(value);
50
+ }
51
+ const deployed = await stack.deploy(declare(), options);
52
+ expect(deployed.failure).toContain(`${REFUSAL} memory, net1`);
53
+ expect(writesOf(pve)).toEqual([]);
54
+ expect(stack.status('ct')).toBeUndefined();
55
+ });
56
+
57
+ test.each([
58
+ ['a device passthrough', { dev1: '/dev/fuse' }, 'dev1', ['/dev/fuse', 'pct set']],
59
+ ['an unprivileged flip', { unprivileged: 0 as const }, 'unprivileged', ['live 1']],
60
+ [
61
+ 'a smaller mount point',
62
+ { mp0: 'tank:subvol-900-disk-0,mp=/data,size=100G' },
63
+ 'mp0',
64
+ ['100G'],
65
+ ],
66
+ ])('%s is the adoption refusal, not the update one', async (_, change, key, hidden) => {
67
+ const pve = live();
68
+ const run = await lxcEngine(pve).plan(ProxmoxLxc('ct', pasted(change)).pipe(adopt(true)));
69
+ expect(run.failure).toContain(`${REFUSAL} ${key}.`);
70
+ for (const value of hidden) expect(run.failure).not.toContain(value);
71
+ expect(writesOf(pve)).toEqual([]);
72
+ });
73
+
74
+ test('an exact match still adopts, with no write and no warning', async () => {
75
+ const pve = live();
76
+ const run = await lxcEngine(pve).deploy(ProxmoxLxc('ct', pasted()).pipe(adopt(true)));
77
+ expect(run).toEqual({ actions: { ct: 'adopted' }, failure: '', warnings: [] });
78
+ expect(writesOf(pve)).toEqual([]);
79
+ });
80
+ });
81
+
82
+ describe('a hand edit between the plan and the deploy', () => {
83
+ test('is refused at apply, and the adoption holds until the declaration says what is live', async () => {
84
+ const pve = live();
85
+ const stack = lxcEngine(pve);
86
+ const declare = (over: Partial<LxcProps> = {}) =>
87
+ ProxmoxLxc('ct', pasted(over)).pipe(adopt(true));
88
+ // ★ Two config reads plan a cold adoption — the probe's and diff's — so the edit lands after
89
+ // the plan said `adopted` and before reconcile reads.
90
+ pve.edits.set(KEY, { after: 2, set: { memory: 4096 } });
91
+ const raced = await stack.deploy(declare());
92
+ expect(raced.failure).toContain(`${REFUSAL} memory.`);
93
+ expect(raced.failure).not.toContain('4096');
94
+ expect(writesOf(pve)).toEqual([]);
95
+ expect(pve.guests.get(KEY)?.['memory']).toBe(4096);
96
+ expect(stack.status('ct')).toBe('updating');
97
+ expect((await stack.plan(declare())).failure).toContain(`${REFUSAL} memory.`);
98
+ const caughtUp = await stack.deploy(declare({ memory: 4096 }));
99
+ expect(caughtUp.failure).toBe('');
100
+ expect(writesOf(pve)).toEqual([]);
101
+ expect(stack.status('ct')).toBe('updated');
102
+ expect((await stack.plan(declare({ memory: 4096 }))).actions).toEqual({ ct: 'noop' });
103
+ });
104
+ });
@@ -14,17 +14,17 @@ import { judge } from './lxc-judge.ts';
14
14
  import { type LxcProps, storedConfig } from './lxc-props.ts';
15
15
  import { judgeVolume } from './lxc-volume.ts';
16
16
 
17
- const LIVE_MP = 'tank:subvol-100-disk-0,mp=/data,size=200G';
17
+ const LIVE_MP = 'tank:subvol-900-disk-0,mp=/data,size=200G';
18
18
  const props = (over: Record<string, unknown>): LxcProps =>
19
- ({ node: 'pve1', target: TARGET, vmid: 100, ...over }) as unknown as LxcProps;
19
+ ({ node: 'pve1', target: TARGET, vmid: 900, ...over }) as unknown as LxcProps;
20
20
  const live = (config: Record<string, unknown>) => ({ unprivileged: 1, ...config });
21
21
 
22
22
  describe('mount points', () => {
23
23
  test('another volume on the same storage is refused, never written', () => {
24
- const verdict = judgeVolume('mp0', 'tank:subvol-100-disk-7,mp=/data,size=200G', LIVE_MP);
24
+ const verdict = judgeVolume('mp0', 'tank:subvol-900-disk-7,mp=/data,size=200G', LIVE_MP);
25
25
  expect(verdict.refuse).toMatch(/detach the live volume to unusedN/);
26
26
  expect(verdict.put).toBeUndefined();
27
- const change = judge(props({ mp0: 'tank:subvol-100-disk-7,mp=/data' }), live({ mp0: LIVE_MP }));
27
+ const change = judge(props({ mp0: 'tank:subvol-900-disk-7,mp=/data' }), live({ mp0: LIVE_MP }));
28
28
  expect(change.put).toEqual({});
29
29
  expect(change.refuse[0]).toMatch(/unusedN/);
30
30
  });
@@ -32,7 +32,7 @@ describe('mount points', () => {
32
32
  test('a bind mount is root@pam only, new or changed', () => {
33
33
  const added = judge(props({ mp1: '/srv/share,mp=/share' }), live({}));
34
34
  expect(added.put).toEqual({});
35
- expect(added.refuse[0]).toMatch(/pct set 100 --mp1/);
35
+ expect(added.refuse[0]).toMatch(/pct set 900 --mp1/);
36
36
  const bind = '/srv/share,mp=/share';
37
37
  expect(judgeVolume('mp1', `${bind},ro=1`, bind)).toEqual({ rootOnly: true });
38
38
  expect(judgeVolume('mp1', bind, bind)).toEqual({});
@@ -41,13 +41,13 @@ describe('mount points', () => {
41
41
  test('replicate defaults ON: an explicit replicate=0 is a value, replicate=1 is not', () => {
42
42
  expect(judgeVolume('mp0', `${LIVE_MP},replicate=1`, LIVE_MP)).toEqual({});
43
43
  const off = judgeVolume('mp0', LIVE_MP, `${LIVE_MP},replicate=0`);
44
- expect(off.put).toBe('tank:subvol-100-disk-0,mp=/data,size=200G');
44
+ expect(off.put).toBe('tank:subvol-900-disk-0,mp=/data,size=200G');
45
45
  });
46
46
 
47
47
  test('options and growth together: the PUT carries the live size, the resize the new one', () => {
48
48
  const verdict = judgeVolume('mp0', 'tank:300,mp=/data,backup=1', LIVE_MP);
49
49
  expect(verdict).toEqual({
50
- put: 'tank:subvol-100-disk-0,mp=/data,backup=1,size=200G',
50
+ put: 'tank:subvol-900-disk-0,mp=/data,backup=1,size=200G',
51
51
  resize: '300G',
52
52
  });
53
53
  });
@@ -57,7 +57,7 @@ describe('a create allocates new volumes only', () => {
57
57
  test('an existing volume id in a create is refused; the new-disk spelling is not', () => {
58
58
  const base = { ostemplate: 'local:vztmpl/t.tar.zst' };
59
59
  const existing = createRefusals(
60
- props({ ...base, rootfs: 'local-zfs:subvol-100-disk-0,size=8G' }),
60
+ props({ ...base, rootfs: 'local-zfs:subvol-900-disk-0,size=8G' }),
61
61
  );
62
62
  expect(existing.join('\n')).toMatch(/rootfs: .* names an existing volume/);
63
63
  expect(
@@ -84,7 +84,7 @@ describe('keys the resource does not manage', () => {
84
84
  mp0: LIVE_MP,
85
85
  parent: 'snap1',
86
86
  unprivileged: 1,
87
- unused0: 'tank:subvol-100-disk-9',
87
+ unused0: 'tank:subvol-900-disk-9',
88
88
  };
89
89
  expect(storedConfig(read)).toEqual({
90
90
  hostname: 'x',
@@ -14,7 +14,7 @@
14
14
  * ⛔ AN EXISTING VOLUME IS NEVER WRITTEN WITH THE NEW-DISK SPELLING. `tank:100` in a PUT means
15
15
  * "allocate a fresh 100 GiB volume", and PVE then moves the volume the guest was using to
16
16
  * `unusedN`. So a declared `tank:100,mp=/data` that matches a live
17
- * `tank:subvol-100-disk-1,mp=/data,size=100G` compares EQUAL, and an options change on it is
17
+ * `tank:subvol-900-disk-1,mp=/data,size=100G` compares EQUAL, and an options change on it is
18
18
  * written with the LIVE volume id — the declaration's spelling is never sent back for a volume
19
19
  * that exists.
20
20
  */
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * ★ EVERY RULE HERE IS READ OFF pve-container 6.1.14 (the PVE 9 line, `src/PVE/LXC/Config.pm` at
5
5
  * git.proxmox.com HEAD, 2026-09-21) or off a live guest's config read with
6
- * `pvesh get /nodes/<node>/lxc/100/config`. A rule nobody measured is a forever-diff waiting to
6
+ * `pvesh get /nodes/<node>/lxc/<vmid>/config`. A rule nobody measured is a forever-diff waiting to
7
7
  * happen: the plan reports an update, the deploy writes the same value back, and the next plan
8
8
  * reports it again.
9
9
  *
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * `Proxmox.Lxc` — a container, declared: adopted as it runs, or created from a template.
3
3
  *
4
- * ★ `hf-provision@pve` HOLDS EXACTLY THE ROLE THIS NEEDS: `LXCProvisioner` on `/` grants
5
- * VM.Allocate, VM.Audit, VM.Config.{CPU,Disk,HWType,Memory,Network,Options}, VM.PowerMgmt,
6
- * Datastore.AllocateSpace, Datastore.Audit, SDN.Use, Sys.Audit. What that role cannot do — device
4
+ * ★ THE PROVISIONING BASELINE HOLDS WHAT THIS NEEDS (provision-baseline.ts): on `/`, VM.Allocate,
5
+ * VM.Audit, VM.Config.{CPU,Disk,HWType,Memory,Network,Options}, VM.PowerMgmt,
6
+ * Datastore.AllocateSpace, Datastore.Audit, SDN.Use, Sys.Audit. What no token can do — device
7
7
  * passthrough, bind mounts, features beyond nesting — PVE keeps for root@pam, and lxc-judge.ts
8
8
  * refuses at plan with the `pct set` to run instead.
9
9
  *
@@ -17,11 +17,11 @@
17
17
  * ⛔ AND NOTHING HERE WRITES A VOLUME IT DID NOT ALLOCATE. A create takes `storage:GiB` only
18
18
  * (lxc-create-form.ts), and a deploy that planned a create never writes onto a guest it finds.
19
19
  *
20
- * ⚠️ AN ADOPTION ALWAYS PLANS `adopted`, EVEN WHEN IT WOULD WRITE. alchemy 2.0.0-beta.79 turns a
21
- * cold adoption's diff into an update whatever it answered (Plan.ts, `forceUpdateAfterAdoption`)
22
- * and prints no property diff for it. So `diff` LOGS the keys a deploy would write, by name
23
- * (never by value), as a warning — the only place a plan can say the declaration is not the live
24
- * config yet. A clean adoption logs nothing, and its deploy writes nothing (lxc-judge.ts).
20
+ * ⛔ AND AN ADOPTION NEVER CHANGES A GUEST (lxc-adoption.ts). A guest this stack adopts is taken
21
+ * over exactly as it runs: any key the declaration says otherwise FAILS the plan, by name (never
22
+ * by value), and reconcile asks again before it would write. Only an exact match adopts.
23
+ * ⚠️ AN UPDATE STILL PLANS `update` WITHOUT A PROPERTY DIFF, so `diff` LOGS the keys a deploy would
24
+ * write, by name, as a warning — the only place a plan says what the deploy will change.
25
25
  *
26
26
  * ⚠️ POWER STATE IS NOT DECLARED. `onboot` is config; start and stop are an operator's act, as for
27
27
  * `Proxmox.Vm`. `start: 1` starts a guest once, after the create that built it, and never again.
@@ -36,6 +36,7 @@ import { ownedRead } from '../ownership/probe.ts';
36
36
  import { provingResumes } from '../ownership/resume.ts';
37
37
  import { createRefusals } from './lxc-create-form.ts';
38
38
  import { identityRefusals } from './lxc-identity.ts';
39
+ import { refuseAdoptedDrift } from './lxc-adoption.ts';
39
40
  import { judge } from './lxc-judge.ts';
40
41
  import {
41
42
  LxcRefusedError,
@@ -75,8 +76,9 @@ const refused = (reasons: readonly string[]) =>
75
76
  * that follows, so reconcile can tell our own interrupted create from a guest somebody else put
76
77
  * at this vmid.
77
78
  */
78
- const diff = (news: Input<LxcProps>, olds: LxcProps, output: LxcAttributes | undefined) =>
79
+ const diff = (owner: LxcOwner, news: Input<LxcProps>, olds: LxcProps) =>
79
80
  Effect.gen(function* () {
81
+ const { output } = owner;
80
82
  const identity = identityRefusals(news, olds, output);
81
83
  if (identity.length > 0) return yield* refused(identity);
82
84
  if (output === undefined) return undefined;
@@ -96,6 +98,7 @@ const diff = (news: Input<LxcProps>, olds: LxcProps, output: LxcAttributes | und
96
98
  return { action: 'update' } as const;
97
99
  }
98
100
  const change = judge(news, live);
101
+ yield* refuseAdoptedDrift(owner, news, live, change);
99
102
  if (change.refuse.length > 0) return yield* refused(change.refuse);
100
103
  const moved = news.node !== output.node;
101
104
  if (change.drift.length === 0 && !moved) return { action: 'noop' } as const;
@@ -112,7 +115,8 @@ const diff = (news: Input<LxcProps>, olds: LxcProps, output: LxcAttributes | und
112
115
  /**
113
116
  * ⛔ THE SAME JUDGE AS `diff`, RE-RUN AGAINST A FRESH READ, AND AN EMPTY CHANGE WRITES NOTHING.
114
117
  * Apply routes `adopted` through reconcile (resource.ts has the Apply.ts lines), so this is what
115
- * runs on the first deploy after an adoption; for a guest that already matches it is two GETs.
118
+ * runs on the first deploy after an adoption; for a guest that already matches it is two GETs,
119
+ * and for one edited since the plan it is a refusal, never a write (lxc-adoption.ts).
116
120
  * ⛔ AND IT READS BACK AND JUDGES AGAIN. PVE answers 200 on writes that did nothing, so a change
117
121
  * that did not land is a failure here, not a state row claiming it did. ⚠️ A change PVE parked
118
122
  * as pending on a running guest DOES read back (GET config merges `[pending]` unless asked for
@@ -148,6 +152,7 @@ const reconcile = (owner: LxcOwner, news: LxcProps) =>
148
152
  'onto an existing guest: plan again, so the plan can show it and adopt it.',
149
153
  ]);
150
154
  }
155
+ yield* refuseAdoptedDrift(owner, news, live, change);
151
156
  if (change.refuse.length > 0) return yield* refused(change.refuse);
152
157
  if (change.drift.length > 0) yield* updateGuest(news, change, text(live['digest']));
153
158
  }
@@ -208,14 +213,13 @@ const handlers = {
208
213
  delete: ({ olds, output }: { olds: LxcProps; output: LxcAttributes }) =>
209
214
  destroyGuest(whereOf(olds, output), olds),
210
215
  diff: ({
216
+ fqn,
217
+ instanceId,
211
218
  news,
212
219
  olds,
213
220
  output,
214
- }: {
215
- news: Input<LxcProps>;
216
- olds: LxcProps;
217
- output: LxcAttributes | undefined;
218
- }) => diff(news, olds, output),
221
+ }: Asked & { news: Input<LxcProps>; olds: LxcProps; output: LxcAttributes | undefined }) =>
222
+ diff({ fqn, instanceId, output }, news, olds),
219
223
  list: () => Effect.succeed([]),
220
224
  read: ({
221
225
  fqn,
@@ -15,7 +15,7 @@ const transport = (cause: Error) =>
15
15
  new HttpClientError.HttpClientError({
16
16
  reason: new HttpClientError.TransportError({
17
17
  cause,
18
- request: { method: 'PUT', url: 'https://n2.mgmt.example.com:8006/api2/json/x' } as never,
18
+ request: { method: 'PUT', url: 'https://node-b.mgmt.example.com:8006/api2/json/x' } as never,
19
19
  }),
20
20
  });
21
21
 
@@ -1,10 +1,10 @@
1
1
  /**
2
- * TB4 cluster members: ordered failover, last-good preference, transport classification.
2
+ * C1 cluster members: ordered failover, last-good preference, transport classification.
3
3
  *
4
4
  * ★ ANY MEMBER'S :8006 API MANAGES THE WHOLE CLUSTER — PVE proxies node paths itself — so the
5
5
  * provider reaches the cluster through whichever member is up, not through one pinned node.
6
6
  *
7
- * ⛔ NEVER cluster-tb4.example.com: it resolves to 10.20.10.250, which nothing serves.
7
+ * ⛔ NEVER cluster-c1.example.com: it resolves to 192.0.2.250, which nothing serves.
8
8
  */
9
9
  import * as Effect from 'effect/Effect';
10
10
  import * as Result from 'effect/Result';
@@ -120,7 +120,7 @@ export type PveMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
120
120
  * elsewhere can repeat a write.
121
121
  *
122
122
  * ★ Reads fail over on any transport failure, including timeout — no side effect to duplicate.
123
- * MEASURED 2026-09-14: full TB4 plans stayed all-noop with the first member refused (127.0.0.1)
123
+ * MEASURED 2026-09-14: full C1 plans stayed all-noop with the first member refused (127.0.0.1)
124
124
  * and with it unresolvable (n9.invalid).
125
125
  */
126
126
  export const executeOnCluster = (
@@ -12,8 +12,8 @@
12
12
  * `Content-Type: application/json` (pve-manager PVE/Status/OpenTelemetry.pm:643) and gzips by
13
13
  * default; there is no protobuf option. vector's opentelemetry source and VictoriaMetrics both
14
14
  * take protobuf only — measured 2026-09-14 — so a server pointed straight at either fails every
15
- * push. That is why TB4's declaration points at each node's own vector bridge
16
- * (hosts/pve/vector/vector.yaml in homeflare-config) rather than at the estate's intake.
15
+ * push. That is why C1's declaration points at each node's own vector bridge
16
+ * (the estate's per-node vector config) rather than at the estate's intake.
17
17
  *
18
18
  * ⛔ `otel-headers` IS `never`, FOR THE REASON `token` IS. It is where an OTLP bearer token goes —
19
19
  * base64 of a JSON object of arbitrary HTTP headers — and Alchemy persists attributes UNENCRYPTED.
@@ -7,7 +7,7 @@
7
7
  * diff, and the only write in either file is the apply itself.
8
8
  *
9
9
  * ⛔ THE PENDING STATE IS NOT IN `data`, WHICH IS WHY THIS FILE DOES ITS OWN FETCH. MEASURED in the
10
- * cluster's own source on n2, 2026-09-13 — not inferred from documentation:
10
+ * cluster's own source on node-b, 2026-09-13 — not inferred from documentation:
11
11
  * PVE/API2/Network.pm my $tmp = PVE::INotify::read_file('interfaces', 1);
12
12
  * $rpcenv->set_result_attrib('changes', $changes) if $changes;
13
13
  * PVE/HTTPServer.pm if (my $diff = $rpcenv->get_result_attrib('changes')) {
@@ -24,9 +24,9 @@
24
24
  * `GET /nodes/{node}/network` answers with the STAGED config and a node about to be reconfigured
25
25
  * looks identical to one already running it. Same trap `sdn-apply.ts` documents for `?running=1`.
26
26
  *
27
- * ⚠️ `active` IS NOT A PENDING SIGNAL, AND THE OBVIOUS SHORTCUT IS MEASURABLY WRONG. On n2 and n3,
28
- * with nothing staged anywhere (VERIFIED 2026-09-13: `/etc/network/interfaces.new` absent on n2,
29
- * n3 and n4), `wlp91s0` already comes back with no `active` key at all. Counting interfaces that
27
+ * ⚠️ `active` IS NOT A PENDING SIGNAL, AND THE OBVIOUS SHORTCUT IS MEASURABLY WRONG. On node-b and node-c,
28
+ * with nothing staged anywhere (VERIFIED 2026-09-13: `/etc/network/interfaces.new` absent on node-b,
29
+ * node-c and node-d), `wlan0` already comes back with no `active` key at all. Counting interfaces that
30
30
  * are not up as "pending" would report an update on two of the three nodes forever.
31
31
  *
32
32
  * ⚠️ ONE THING GENUINELY CANNOT BE SEEN FROM HERE, AND IT IS SMALL. The diff is computed as
@@ -58,7 +58,7 @@ export type ClusterHealth = { quorate: boolean; offline: readonly string[] };
58
58
  *
59
59
  * ⛔ THIS STRING NEVER LEAVES THIS MODULE, AND THAT IS A SECRET RULE, NOT A STYLE ONE. Alchemy
60
60
  * persists attributes UNENCRYPTED (credentials.ts spells out why), and `/etc/network/interfaces`
61
- * is a file that can legally contain `wpa-psk` — n2 and n3 both carry a `wlp91s0`, MEASURED — as
61
+ * is a file that can legally contain `wpa-psk` — node-b and node-c both carry a `wlan0`, MEASURED — as
62
62
  * well as any `pre-up` command somebody wrote. A diff of that file in a state store is a secret
63
63
  * in a state store. Only the COUNT below ever escapes.
64
64
  */
@@ -70,7 +70,7 @@ const stagedDiff = (target: PveTarget, node: string) =>
70
70
  // provisioning lease, and a plan stays a plan even if the write role is missing entirely.
71
71
  // ⛔ THROUGH THE LEASE CACHE, NOT `mint`. This read needs no particular identity — only the
72
72
  // apply and its task poll do, and network-apply.ts mints for those itself. Minting here left
73
- // SIX extra `hf-read@pve` tokens on n2 per TB4 plan (one per node, for read and for diff),
73
+ // SIX extra `hf-read@pve` tokens on node-b per C1 plan (one per node, for read and for diff),
74
74
  // MEASURED 2026-09-14 while the cache itself made one read mint for the whole run.
75
75
  const credential = yield* leased(target, 'read');
76
76
  const envelope = (yield* pveEnvelopeWith(target, credential, 'GET', path)) as NetworkEnvelope;
@@ -108,9 +108,9 @@ export const pendingCount = (target: PveTarget, node: string) =>
108
108
  * Quorum, and every member that is not online.
109
109
  *
110
110
  * ⛔ QUORUM ALONE IS NOT THE CHECK, AND ON A THREE-NODE CLUSTER IT WOULD MISS THE ACCIDENT THIS
111
- * RESOURCE IS AFRAID OF. Losing ONE of three leaves `quorate: 1` — MEASURED shape on TB4:
112
- * `{"id":"cluster","name":"HF-TB4","nodes":3,"quorate":1,…}` plus one row per node with
113
- * `online: 1`. So a reload that drops n2 off the network is invisible to `quorate` and visible
111
+ * RESOURCE IS AFRAID OF. Losing ONE of three leaves `quorate: 1` — MEASURED shape on C1:
112
+ * `{"id":"cluster","name":"HF-C1","nodes":3,"quorate":1,…}` plus one row per node with
113
+ * `online: 1`. So a reload that drops node-b off the network is invisible to `quorate` and visible
114
114
  * only as `online: 0` on that row. Both are checked, and the node names come back so the failure
115
115
  * message can say WHICH member went away.
116
116
  *
@@ -140,7 +140,7 @@ const clusterHealth = (target: PveTarget) =>
140
140
  *
141
141
  * ★ A REASON RATHER THAN A BOOLEAN, BECAUSE THE CALLER'S ONLY USE FOR IT IS A REFUSAL MESSAGE.
142
142
  * "The deploy stopped because the cluster was degraded" is not actionable at three in the
143
- * morning; "quorate=true offline=[n3]" is. The two callers in network-apply.ts ask this before
143
+ * morning; "quorate=true offline=[node-c]" is. The two callers in network-apply.ts ask this before
144
144
  * the reload and again after it, and the second answer is the one that stops the chain from
145
145
  * reaching the next node.
146
146
  *
@@ -4,23 +4,23 @@
4
4
  * ⛔ WITHOUT IT EVERY NETWORK DECLARATION IS A GREEN PLAN THAT CHANGES NO NETWORK; WITH IT A GREEN
5
5
  * PLAN CAN TAKE A NODE OFF THE NETWORK. Writes under `nodes/{node}/network/{iface}` edit
6
6
  * `/etc/network/interfaces.new` and touch no interface. `PUT /nodes/{node}/network` is the apply.
7
- * MEASURED in the cluster's own source on n2, 2026-09-13 — `PVE/API2/Network.pm`:
7
+ * MEASURED in the cluster's own source on node-b, 2026-09-13 — `PVE/API2/Network.pm`:
8
8
  * rename($new_config_file, $current_config_file) if -e $new_config_file;
9
9
  * PVE::Tools::run_command(['ifreload', '-a'], errfunc => $err);
10
10
  * return $rpcenv->fork_worker('srvreload', 'networking', $authuser, $worker);
11
11
  *
12
- * ⛔ THIS IS THE MOST DANGEROUS RESOURCE IN THE PACKAGE, AND ON TB4 THE DANGER IS NOT ABSTRACT.
13
- * Ceph runs over `vmbr1.11` on n2, n3 AND n4 (MEASURED 2026-09-13: four pools, size 3 /
12
+ * ⛔ THIS IS THE MOST DANGEROUS RESOURCE IN THE PACKAGE, AND ON C1 THE DANGER IS NOT ABSTRACT.
13
+ * Ceph runs over `vmbr1.42` on node-b, node-c AND node-d (MEASURED 2026-09-13: four pools, size 3 /
14
14
  * min_size 2, mon+mgr+mds on all three). `ifreload -a` takes that node's OSDs, mons and MDS off
15
15
  * the cluster network for the length of the reload; two nodes at once puts every pool below
16
16
  * min_size and blocks IO for every guest with an RBD disk. The management address the API is
17
- * reached on, `vmbr0.10`, is reloaded by the same call.
17
+ * reached on, `vmbr0.41`, is reloaded by the same call.
18
18
  *
19
19
  * ⛔ AND UNLIKE `SdnApply` IT IS PER NODE, SO A HALF-RECONFIGURED CLUSTER IS THE DEFAULT FAILURE
20
20
  * MODE, NOT AN EDGE CASE. `PUT /cluster/sdn` publishes the whole cluster in one call; this
21
- * publishes exactly one node, so a stack holding three of these can apply n2, fail, and leave n3
22
- * and n4 on the old layout with Ceph spanning all three. Four things here exist only for that:
23
- * 1. `after` SERIALISES THEM — chain n3 after n2 and n4 after n3, so at most one node is ever
21
+ * publishes exactly one node, so a stack holding three of these can apply node-b, fail, and leave node-c
22
+ * and node-d on the old layout with Ceph spanning all three. Four things here exist only for that:
23
+ * 1. `after` SERIALISES THEM — chain node-c after node-b and node-d after node-c, so at most one node is ever
24
24
  * mid-reload. Three declared in parallel is the outage, and nothing in Alchemy stops you.
25
25
  * 2. `reconcile` REFUSES TO START on a cluster that is already degraded.
26
26
  * 3. `reconcile` RE-CHECKS quorum and every member's `online` flag AFTER the reload and dies if
@@ -58,7 +58,7 @@ import type { WithTarget } from './resource.ts';
58
58
 
59
59
  export interface NetworkApplyProps extends WithTarget {
60
60
  /**
61
- * The node whose staged interfaces file this applies — `n2`.
61
+ * The node whose staged interfaces file this applies — `node-b`.
62
62
  *
63
63
  * ⚠️ ONE APPLY PER NODE, AND NEVER TWO FOR THE SAME NODE. Two would each publish the other's
64
64
  * staged half, and both would run `ifreload` on a file the other had not finished writing.
@@ -99,7 +99,7 @@ export interface NetworkApplyAttributes {
99
99
  * `active`, `exists`, `families`, `method`, `method6`, `options`, `priority`, `link-type` and
100
100
  * ten more; `altnames` is returned and is not even in the schema. `priority` is the worst of
101
101
  * them: PVE ASSIGNS it from the order of the interfaces file, so the same logical layout
102
- * carries different numbers on different nodes (MEASURED: n4's differ from n2/n3's by one).
102
+ * carries different numbers on different nodes (MEASURED: node-d's differ from node-b/node-c's by one).
103
103
  * Every one of those is a forever-diff waiting for whoever writes `Proxmox.NetworkInterface`.
104
104
  * This resource compares a single integer against zero and never looks at a row at all.
105
105
  */
@@ -128,7 +128,7 @@ const read = (props: NetworkApplyProps) =>
128
128
  * decides it is whether the node is carrying a staged change right now.
129
129
  *
130
130
  * ★ THIS IS ALSO WHY DECLARING THE LIVE CLUSTER PLANS AS `noop`. VERIFIED 2026-09-13:
131
- * `/etc/network/interfaces.new` is absent on n2, n3 and n4, so `changes` is absent from all three
131
+ * `/etc/network/interfaces.new` is absent on node-b, node-c and node-d, so `changes` is absent from all three
132
132
  * answers, so `pending` is 0 on all three and every one of them plans `noop`.
133
133
  */
134
134
  const diff = (news: Input<NetworkApplyProps>) =>