@homeflare/alchemy 0.34.0 → 0.36.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 (65) hide show
  1. package/dist/grafana/alert-rule-group-drop-warning.d.ts +25 -0
  2. package/dist/grafana/alert-rule-group-drop-warning.d.ts.map +1 -0
  3. package/dist/grafana/alert-rule-group-fixtures.d.ts +73 -0
  4. package/dist/grafana/alert-rule-group-fixtures.d.ts.map +1 -0
  5. package/dist/grafana/alert-rule-group-model.d.ts +28 -0
  6. package/dist/grafana/alert-rule-group-model.d.ts.map +1 -0
  7. package/dist/grafana/alert-rule-group.d.ts +60 -0
  8. package/dist/grafana/alert-rule-group.d.ts.map +1 -0
  9. package/dist/grafana/index.d.ts +6 -4
  10. package/dist/grafana/index.d.ts.map +1 -1
  11. package/dist/grafana/index.js +355 -139
  12. package/dist/grafana/index.js.map +14 -8
  13. package/dist/grafana/notification-policy-drop-warning.d.ts +27 -0
  14. package/dist/grafana/notification-policy-drop-warning.d.ts.map +1 -0
  15. package/dist/grafana/notification-policy-fixtures.d.ts +34 -0
  16. package/dist/grafana/notification-policy-fixtures.d.ts.map +1 -0
  17. package/dist/grafana/notification-policy-model.d.ts +53 -0
  18. package/dist/grafana/notification-policy-model.d.ts.map +1 -0
  19. package/dist/grafana/notification-policy.d.ts +57 -0
  20. package/dist/grafana/notification-policy.d.ts.map +1 -0
  21. package/dist/grafana/providers.d.ts +1 -1
  22. package/dist/grafana/providers.d.ts.map +1 -1
  23. package/dist/proxmox/index.js +349 -287
  24. package/dist/proxmox/index.js.map +8 -7
  25. package/dist/proxmox/network-apply-read.d.ts +12 -0
  26. package/dist/proxmox/network-apply-read.d.ts.map +1 -1
  27. package/dist/proxmox/network-apply.d.ts +2 -0
  28. package/dist/proxmox/network-apply.d.ts.map +1 -1
  29. package/dist/proxmox/node-network-form.d.ts +86 -54
  30. package/dist/proxmox/node-network-form.d.ts.map +1 -1
  31. package/dist/proxmox/node-network-wire.d.ts +100 -0
  32. package/dist/proxmox/node-network-wire.d.ts.map +1 -0
  33. package/dist/proxmox/node-network.d.ts +21 -106
  34. package/dist/proxmox/node-network.d.ts.map +1 -1
  35. package/docs/grafana-alerting-rules.md +143 -0
  36. package/docs/grafana-alerting.md +7 -4
  37. package/docs/grafana-notification-policy.md +127 -0
  38. package/docs/grafana.md +12 -6
  39. package/docs/upstream-conformance.md +30 -4
  40. package/package.json +1 -1
  41. package/src/grafana/alert-rule-group-drop-warning.test.ts +107 -0
  42. package/src/grafana/alert-rule-group-drop-warning.ts +41 -0
  43. package/src/grafana/alert-rule-group-fixtures.ts +46 -0
  44. package/src/grafana/alert-rule-group-model.test.ts +75 -0
  45. package/src/grafana/alert-rule-group-model.ts +66 -0
  46. package/src/grafana/alert-rule-group-provenance.test.ts +116 -0
  47. package/src/grafana/alert-rule-group.test.ts +164 -0
  48. package/src/grafana/alert-rule-group.ts +231 -0
  49. package/src/grafana/index.ts +18 -4
  50. package/src/grafana/notification-policy-drop-warning.test.ts +163 -0
  51. package/src/grafana/notification-policy-drop-warning.ts +60 -0
  52. package/src/grafana/notification-policy-fixtures.ts +27 -0
  53. package/src/grafana/notification-policy-model.test.ts +104 -0
  54. package/src/grafana/notification-policy-model.ts +100 -0
  55. package/src/grafana/notification-policy-refusals.test.ts +119 -0
  56. package/src/grafana/notification-policy.test.ts +86 -0
  57. package/src/grafana/notification-policy.ts +191 -0
  58. package/src/grafana/providers.ts +4 -0
  59. package/src/proxmox/constraints-host-forms.test.ts +2 -2
  60. package/src/proxmox/network-apply-read.ts +12 -0
  61. package/src/proxmox/network-apply.ts +2 -0
  62. package/src/proxmox/node-network-form.ts +93 -99
  63. package/src/proxmox/node-network-read-failure.test.ts +206 -0
  64. package/src/proxmox/node-network-wire.ts +239 -0
  65. package/src/proxmox/node-network.ts +155 -167
@@ -11,6 +11,9 @@
11
11
  * that apply — this one never calls it, exactly as `Proxmox.SdnZone` never calls
12
12
  * `Proxmox.SdnApply`.
13
13
  *
14
+ * ⛔ `Proxmox.NetworkApply` STAYS ON `client.ts`, NOT MIGRATED WITH THIS FILE — network-apply-
15
+ * read.ts's own header has the two MEASURED reasons distilled cannot represent yet.
16
+ *
14
17
  * ⛔ CEPH ON C1 RIDES TWO NETWORKS AND THIS API ONLY SEES ONE OF THEM. Measured from
15
18
  * `/etc/pve/ceph.conf` on 2026-09-13:
16
19
  *
@@ -18,7 +21,8 @@
18
21
  * cluster_network = 203.0.113.0/24 -> every OSD's cluster_addr: .102 / .103 / .104
19
22
  *
20
23
  * The cluster network — OSD REPLICATION, the traffic that rebuilds a lost replica — lives on
21
- * `203.0.113.10X/32` addresses carried by `tb0`, `tb1` and `dummy_c1`: the Thunderbolt mesh.
24
+ * `203.0.113.10X/32` addresses carried by `tb0`, `tb1` and `dummy_c1`: the Thunderbolt mesh —
25
+ * TB4's own fabric ports, EXCLUDED from this family by decision 28/29/9 (SdnFabric owns them).
22
26
  *
23
27
  * ⛔ AND THOSE ADDRESSES ARE INVISIBLE HERE, WHICH IS THE TRAP. They are declared in
24
28
  * `/etc/network/interfaces.d/sdn`, and PVE's network API does not parse that directory.
@@ -34,8 +38,9 @@
34
38
  * owns them, not this resource.
35
39
  *
36
40
  * ⚠️ Two ways this file limits the damage on the interfaces it CAN see: `cidr` is compared
37
- * unconditionally (the ⛔ on `matches`), and a live interface whose `type` disagrees with the
38
- * declaration is reported ABSENT rather than retyped (the ⛔ in `attributes`).
41
+ * unconditionally (node-network-wire.ts's own ⛔ on `matches`), and a live interface whose
42
+ * `type` disagrees with the declaration is reported ABSENT rather than retyped
43
+ * (node-network-wire.ts's `attributesOf`).
39
44
  *
40
45
  * ★ THE REASON IT EXISTS IS THAT THE STAGED READ CANNOT TELL YOU THE NETWORK IS RIGHT — it can
41
46
  * only tell you the FILE is. Once anything is staged, `GET /nodes/{node}/network/{iface}`
@@ -48,11 +53,9 @@
48
53
  * object to scope to, so that also buys the node's services, certificates, DNS and time.
49
54
  * Collected with every other family in docs/privileges.md.
50
55
  *
51
- * ★ A RECONCILE OVER A MATCHING INTERFACE IS A NO-WRITE, AND AN EARLIER DRAFT OF THIS COMMENT
52
- * SAID THE OPPOSITE. `pveOperations.reconcile` gates on `matches` BEFORE the PUT
53
- * (`resource.ts`: `else if (spec.updateForm !== undefined && !spec.matches(live, news))`), and
54
- * the ⛔ above that line names THIS family as the reason the guard was added — adopting an
55
- * interface a node already has would otherwise leave it holding a pending network change.
56
+ * ★ A RECONCILE OVER A MATCHING INTERFACE IS A NO-WRITE. `reconcile` below gates the PUT on
57
+ * `matches` first — adopting an interface a node already has must not leave it holding a
58
+ * pending network change, since a PUT under `/nodes/{node}/network/{iface}` STAGES one.
56
59
  *
57
60
  * ⚠️ WHAT IS STILL TRUE: when `matches` is false the PUT rewrites the whole stanza, and that can
58
61
  * ADD lines the file did not have — a `vlan-raw-device vmbr1` under `vmbr1.42`, where PVE had
@@ -64,124 +67,41 @@
64
67
  * them would put Alchemy one `destroy` away from staging the removal of a node's uplink — and
65
68
  * `delete_network` has NO in-use check at all: MEASURED, it deletes the hash entry and writes
66
69
  * the file. Adoption stays explicit.
70
+ *
71
+ * ★ MIGRATED OFF `client.ts`'s generic `pve()`/`pveHandlers` ONTO `@distilled.cloud/proxmox`'s
72
+ * typed `nodes.getNodeNetwork`/`createNodeNetwork`/`putNodeNetwork2`/`deleteNodeNetwork2`
73
+ * (2026-09-24, decision 43). Dual-path read and `read`'s `output`-branching wired like every
74
+ * other migrated family; node-network-wire.ts's `readInterfaceOrFail` has the new nuance.
67
75
  */
68
76
  import { Resource } from 'alchemy';
77
+ import { isResolved } from 'alchemy/Diff';
69
78
  import * as Provider from 'alchemy/Provider';
79
+ import * as nodes from '@distilled.cloud/proxmox/nodes';
70
80
  import * as Effect from 'effect/Effect';
81
+ import { guardWrite } from './distilled-guard.ts';
71
82
  import {
72
- createBody,
73
- readAttributes,
74
- same,
75
- sameComment,
76
- sameList,
77
- updateBody,
83
+ NODE_NETWORK_CREATE,
84
+ NODE_NETWORK_UPDATE,
85
+ type NodeNetworkProps,
86
+ createForm,
87
+ updateForm,
78
88
  } from './node-network-form.ts';
79
- import { type PveRequirements, type WithTarget, pveHandlers } from './resource.ts';
80
-
81
- /**
82
- * PVE's interface types. `unknown` is what a NIC it does not manage reports — node-b and node-c both
83
- * report `wlan0` that way, and node-d has no such interface at all.
84
- *
85
- * ⚠️ THAT ASYMMETRY IS A TRAP FOR A DECLARATION SHARED ACROSS NODES. On node-d the read answers 404,
86
- * which this provider cannot tell from "deleted", so reconcile POSTs — writing a stanza for a
87
- * card that is not in the machine. Declare per node what each node actually has; a physical
88
- * interface is discovered, not decided.
89
- */
90
- export type NodeNetworkType =
91
- | 'OVSBond'
92
- | 'OVSBridge'
93
- | 'OVSIntPort'
94
- | 'OVSPort'
95
- | 'alias'
96
- | 'bond'
97
- | 'bridge'
98
- | 'eth'
99
- | 'fabric'
100
- | 'unknown'
101
- | 'vlan'
102
- | 'vnet';
103
-
104
- export interface NodeNetworkProps extends WithTarget {
105
- /** Which node's file this stanza lives in. Interfaces are per node, never cluster-wide. */
106
- node: string;
107
- /** `vmbr0`, `bond0`, `vmbr1.42`. 2-20 characters, PVE's `pve-iface` format. */
108
- iface: string;
109
- /** ⛔ REQUIRED ON EVERY WRITE, update included, never used to retype — see `readAttributes`. */
110
- type: NodeNetworkType;
111
- /**
112
- * `198.51.100.12/24`. ⛔ THE ONE FIELD WITH NO UNMANAGED MODE: leaving it out is an instruction
113
- * to make the interface `manual`, not an instruction to leave its address alone. The ⛔ on
114
- * `updateBody` in node-network-form.ts has the measurement and the consequence.
115
- */
116
- cidr?: string;
117
- /** ⚠️ PVE allows exactly ONE default gateway per node and refuses a second with "Default
118
- * gateway already exists on interface '<other>'". On C1 it is vmbr0.41's. */
119
- gateway?: string;
120
- /**
121
- * `auto <iface>` in the file. Absent on read means off, which is why `bool`'s fallback is used.
122
- * ⚠️ UNDECLARED IS UNMANAGED ON AN UPDATE AND OFF ON A CREATE — the same asymmetry storage.ts
123
- * has, and it bites harder here: an interface created without `autostart` is one the node
124
- * will not bring up at boot. Every C1 bridge, bond and vlan carries it.
125
- */
126
- autostart?: boolean;
127
- /** 1280-65520. Unset leaves the file without an `mtu` line and the kernel default in force. */
128
- mtu?: number;
129
- /** ⚠️ Round-trips only after normalisation — see `comment` in node-network-form.ts. */
130
- comments?: string;
131
- /** Space-separated, a SET: `enp87s0`, or `bond0`. ⚠️ PVE refuses a port already used elsewhere. */
132
- bridge_ports?: string;
133
- /** `2-4094`, or `2 100-200`. Only written when `bridge_vlan_aware` is on. */
134
- bridge_vids?: string;
135
- /** ⛔ A `false` here is sent as `delete=`, never as `0` — the ⛔ on `body` says why. */
136
- bridge_vlan_aware?: boolean;
137
- /**
138
- * A bond's members, space separated. ⚠️ THE PARAMETER IS `slaves`, NOT `bond_slaves`, in BOTH
139
- * directions on this PVE: MEASURED, the POST/PUT schema names only `slaves` and the GET returns
140
- * `"slaves":"enp2s0f0np0 enp2s0f1np1"`. `bond_slaves` appears nowhere in Network.pm here.
141
- */
142
- slaves?: string;
143
- bond_mode?: string;
144
- /** ⚠️ Only written when `bond_mode` is `balance-xor` or `802.3ad`; ignored otherwise. */
145
- bond_xmit_hash_policy?: string;
146
- /** active-backup only. Kept hyphenated because that is the wire name. */
147
- 'bond-primary'?: string;
148
- /** ⚠️ DERIVED FROM A DOTTED NAME. `vmbr1.42` reports `vlan-id` 42 with no such line in the file;
149
- * declaring it there is harmless but adds a line the file did not have. */
150
- 'vlan-id'?: number;
151
- 'vlan-raw-device'?: string;
152
- }
89
+ import {
90
+ dropUnreadable,
91
+ matches,
92
+ readInterface,
93
+ readInterfaceOrFail,
94
+ } from './node-network-wire.ts';
95
+ import type { NodeNetworkAttributes } from './node-network-wire.ts';
96
+ import { runPve } from './distilled-pve.ts';
97
+ import { type PveRequirements } from './resource-spec.ts';
98
+ import { UNREADABLE, unreadableWarning } from './unreadable-read.ts';
153
99
 
154
- /**
155
- * ⛔ THE LAST SEVEN ARE REPORTED AND NEVER COMPARED — each is a MEASURED forever-diff, and the
156
- * evidence for every one of them is in node-network-form.ts, beside the code that reads them.
157
- */
158
- export interface NodeNetworkAttributes {
159
- node: string;
160
- iface: string;
161
- type: string;
162
- cidr: string;
163
- gateway: string;
164
- autostart: boolean;
165
- mtu: number;
166
- comments: string;
167
- bridge_ports: string;
168
- bridge_vids: string;
169
- bridge_vlan_aware: boolean;
170
- slaves: string;
171
- bond_mode: string;
172
- bond_xmit_hash_policy: string;
173
- 'bond-primary': string;
174
- 'vlan-id': number;
175
- 'vlan-raw-device': string;
176
- priority: number;
177
- method: string;
178
- families: string;
179
- active: boolean;
180
- exists: boolean;
181
- bond_miimon: string;
182
- bridge_stp: string;
183
- bridge_fd: string;
184
- }
100
+ export type { NodeNetworkAttributes, NodeNetworkType } from './node-network-wire.ts';
101
+ // ⚠️ MOVED TO node-network-form.ts IN THIS PR'S LINE-CAP TRIM — every field on it is already
102
+ // documented there against `body`/`createForm`/`updateForm`, so it travels with them rather
103
+ // than duplicating that documentation here. Nothing about its shape changed.
104
+ export type { NodeNetworkProps } from './node-network-form.ts';
185
105
 
186
106
  export interface ProxmoxNodeNetwork extends Resource<
187
107
  'Proxmox.NodeNetwork',
@@ -196,54 +116,122 @@ export const ProxmoxNodeNetwork = Resource<ProxmoxNodeNetwork>('Proxmox.NodeNetw
196
116
  defaultRemovalPolicy: 'retain',
197
117
  });
198
118
 
199
- const handlers = pveHandlers<NodeNetworkProps, NodeNetworkAttributes>({
200
- attributes: readAttributes,
201
- collection: (props) => `nodes/${props.node}/network`,
202
- /** ⚠️ No `delete` parameter on a POST — see the ⚠️ on `updateBody`. `iface` IS in the create
203
- * body though, and was missing until 2026-09-22 — the 🔴 on `createBody`. */
204
- createForm: createBody,
205
- /** The vendor rules both forms are checked against at plan time — resource-spec.ts. */
206
- endpoint: {
207
- create: 'pve:POST /nodes/{node}/network',
208
- update: 'pve:PUT /nodes/{node}/network/{iface}',
209
- },
210
- /**
211
- * ⛔ `cidr` IS THE ONLY FIELD COMPARED WHEN UNDECLARED, AND THAT ASYMMETRY IS THE POINT. Every
212
- * other field here follows storage.ts: undeclared means unmanaged, so it is neither sent nor
213
- * compared and a plan cannot report an update no write could satisfy. `cidr` cannot be
214
- * unmanaged, because PVE derives `method` from the presence of an address IN THE FORM — an
215
- * omission is an edit. Comparing it unconditionally is what makes that edit visible in `plan`
216
- * instead of at `ifreload` time.
217
- *
218
- * ⚠️ NOTHING IN THE REPORTED-ONLY BLOCK IS HERE, and that is what makes the C1 declaration
219
- * plan as `noop` on all three nodes at once — the ⛔ above `NodeNetworkAttributes` lists them
220
- * and the measurement behind each.
221
- */
222
- matches: (attributes, props) =>
223
- attributes.cidr === (props.cidr ?? '') &&
224
- same(props.gateway, attributes.gateway) &&
225
- same(props.autostart, attributes.autostart) &&
226
- same(props.mtu, attributes.mtu) &&
227
- sameComment(props.comments, attributes.comments) &&
228
- sameList(props.bridge_ports, attributes.bridge_ports) &&
229
- sameList(props.bridge_vids, attributes.bridge_vids) &&
230
- same(props.bridge_vlan_aware, attributes.bridge_vlan_aware) &&
231
- sameList(props.slaves, attributes.slaves) &&
232
- same(props.bond_mode, attributes.bond_mode) &&
233
- same(props.bond_xmit_hash_policy, attributes.bond_xmit_hash_policy) &&
234
- same(props['bond-primary'], attributes['bond-primary']) &&
235
- same(props['vlan-id'], attributes['vlan-id']) &&
236
- same(props['vlan-raw-device'], attributes['vlan-raw-device']),
237
- path: (props) => `nodes/${props.node}/network/${props.iface}`,
238
- updateForm: updateBody,
239
- });
240
-
241
119
  /**
242
- * ⚠️ THE DESTROY IS STAGED LIKE EVERY OTHER WRITE, WHICH MAKES IT THE ONE RECOVERABLE DESTROY IN
243
- * THIS PACKAGE. It removes the stanza from `interfaces.new` and nothing else;
244
- * `DELETE /nodes/{node}/network` puts the node back by unlinking that file. It becomes
245
- * irreversible the moment somebody applies — and PVE performs NO in-use check before removing
246
- * an interface other bridges or Ceph still stand on.
120
+ * The three fields distilled's generator renamed to an underscore (`bond-primary`, `vlan-id`,
121
+ * `vlan-raw-device`) — everything else round-trips as-is. `storage-form.ts`'s `underscored` is a
122
+ * generic transform for a free-form bag; this family's fields are all named ahead of time, so a
123
+ * small local map is clearer than importing a generic helper for three keys. Moved here from
124
+ * node-network-form.ts in this PR's line-cap trim — this is the only caller.
247
125
  */
126
+ const RENAMED: Readonly<Record<string, string>> = {
127
+ 'bond-primary': 'bond_primary',
128
+ 'vlan-id': 'vlan_id',
129
+ 'vlan-raw-device': 'vlan_raw_device',
130
+ };
131
+
132
+ const toDistilled = (form: Record<string, string>): Record<string, string> =>
133
+ Object.fromEntries(Object.entries(form).map(([key, value]) => [RENAMED[key] ?? key, value]));
134
+
135
+ /** The actual `createNodeNetwork` call body — `createForm`, translated once. See `toDistilled`. */
136
+ const toDistilledCreate = (props: NodeNetworkProps): nodes.CreateNodeNetworkRequest =>
137
+ ({
138
+ ...toDistilled(createForm(props)),
139
+ node: props.node,
140
+ }) as unknown as nodes.CreateNodeNetworkRequest;
141
+
142
+ /** The actual `putNodeNetwork2` call body — `updateForm`, translated once. See `toDistilledCreate`. */
143
+ const toDistilledUpdate = (props: NodeNetworkProps): nodes.PutNodeNetwork2Request =>
144
+ ({
145
+ ...toDistilled(updateForm(props)),
146
+ iface: props.iface,
147
+ node: props.node,
148
+ }) as unknown as nodes.PutNodeNetwork2Request;
149
+
248
150
  export const ProxmoxNodeNetworkProvider = () =>
249
- Provider.effect(ProxmoxNodeNetwork, Effect.succeed(ProxmoxNodeNetwork.Provider.of(handlers)));
151
+ Provider.effect(
152
+ ProxmoxNodeNetwork,
153
+ Effect.succeed(
154
+ ProxmoxNodeNetwork.Provider.of({
155
+ /**
156
+ * ⛔ EMPTY, AND HERE IT MATTERS MORE THAN ANYWHERE ELSE IN THIS PACKAGE. `GET
157
+ * /nodes/{node}/network` returns every interface the node has. Adoption stays explicit.
158
+ */
159
+ list: () => Effect.succeed([]),
160
+ // ⚠️ FOLDS ONLY WHEN `output` IS `undefined`, restored by adversarial review after a
161
+ // first draft dropped it — node-network-wire.ts's `readInterface` has the reasoning.
162
+ read: Effect.fn(function* ({ olds, output }) {
163
+ return dropUnreadable(
164
+ yield* output === undefined ? readInterface(olds) : readInterfaceOrFail(olds),
165
+ );
166
+ }),
167
+ diff: Effect.fn(function* ({ news, output }) {
168
+ if (!isResolved(news)) return undefined;
169
+ yield* guardWrite(NODE_NETWORK_CREATE, createForm(news), output === undefined);
170
+ yield* guardWrite(NODE_NETWORK_UPDATE, updateForm(news), false);
171
+ if (output === undefined) return undefined;
172
+ const live = yield* readInterfaceOrFail(news);
173
+ // ⛔ THE CRIES-WOLF FIX: a refused mint used to fall into `undefined` and force `update`
174
+ // on an interface that was plainly there — see unreadable-read.ts.
175
+ if (live === UNREADABLE) {
176
+ yield* unreadableWarning('Proxmox.NodeNetwork', `${news.node}/${news.iface}`);
177
+ return { action: 'noop' } as const;
178
+ }
179
+ if (live === undefined) {
180
+ yield* guardWrite(NODE_NETWORK_CREATE, createForm(news), true);
181
+ return { action: 'update' } as const;
182
+ }
183
+ return matches(live, news)
184
+ ? ({ action: 'noop' } as const)
185
+ : ({ action: 'update' } as const);
186
+ }),
187
+ /**
188
+ * ⛔ THE DESTROY IS STAGED LIKE EVERY OTHER WRITE, WHICH MAKES IT THE ONE RECOVERABLE
189
+ * DESTROY IN THIS PACKAGE. It removes the stanza from `interfaces.new` and nothing
190
+ * else; `DELETE /nodes/{node}/network` (a DIFFERENT, collection-level endpoint —
191
+ * `Proxmox.NetworkApply`'s own revert, never called here) puts the node back by
192
+ * unlinking that file. It becomes irreversible the moment somebody applies — and PVE
193
+ * performs NO in-use check before removing an interface other bridges or Ceph still
194
+ * stand on.
195
+ */
196
+ reconcile: Effect.fn(function* ({ news }) {
197
+ const before = dropUnreadable(yield* readInterface(news));
198
+ yield* guardWrite(NODE_NETWORK_CREATE, createForm(news), before === undefined);
199
+ yield* guardWrite(NODE_NETWORK_UPDATE, updateForm(news), false);
200
+ if (before === undefined) {
201
+ yield* runPve(
202
+ news.target,
203
+ 'provision',
204
+ true,
205
+ nodes.createNodeNetwork(toDistilledCreate(news)),
206
+ );
207
+ } else if (!matches(before, news)) {
208
+ yield* runPve(
209
+ news.target,
210
+ 'provision',
211
+ true,
212
+ nodes.putNodeNetwork2(toDistilledUpdate(news)),
213
+ );
214
+ }
215
+ const after = dropUnreadable(yield* readInterface(news));
216
+ if (after === undefined) {
217
+ return yield* Effect.die(
218
+ new Error(
219
+ `nodes/${news.node}/network/${news.iface}: wrote with no error, but the ` +
220
+ 'interface is still absent -- PVE wraps every answer in {"data":...} and can ' +
221
+ 'report success on a call that did nothing, so read back rather than trust it.',
222
+ ),
223
+ );
224
+ }
225
+ return after;
226
+ }),
227
+ delete: Effect.fn(function* ({ olds }) {
228
+ yield* runPve(
229
+ olds.target,
230
+ 'provision',
231
+ true,
232
+ nodes.deleteNodeNetwork2({ iface: olds.iface, node: olds.node }),
233
+ );
234
+ }),
235
+ }),
236
+ ),
237
+ );