@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.
- package/dist/grafana/alert-rule-group-drop-warning.d.ts +25 -0
- package/dist/grafana/alert-rule-group-drop-warning.d.ts.map +1 -0
- package/dist/grafana/alert-rule-group-fixtures.d.ts +73 -0
- package/dist/grafana/alert-rule-group-fixtures.d.ts.map +1 -0
- package/dist/grafana/alert-rule-group-model.d.ts +28 -0
- package/dist/grafana/alert-rule-group-model.d.ts.map +1 -0
- package/dist/grafana/alert-rule-group.d.ts +60 -0
- package/dist/grafana/alert-rule-group.d.ts.map +1 -0
- package/dist/grafana/index.d.ts +6 -4
- package/dist/grafana/index.d.ts.map +1 -1
- package/dist/grafana/index.js +355 -139
- package/dist/grafana/index.js.map +14 -8
- package/dist/grafana/notification-policy-drop-warning.d.ts +27 -0
- package/dist/grafana/notification-policy-drop-warning.d.ts.map +1 -0
- package/dist/grafana/notification-policy-fixtures.d.ts +34 -0
- package/dist/grafana/notification-policy-fixtures.d.ts.map +1 -0
- package/dist/grafana/notification-policy-model.d.ts +53 -0
- package/dist/grafana/notification-policy-model.d.ts.map +1 -0
- package/dist/grafana/notification-policy.d.ts +57 -0
- package/dist/grafana/notification-policy.d.ts.map +1 -0
- package/dist/grafana/providers.d.ts +1 -1
- package/dist/grafana/providers.d.ts.map +1 -1
- package/dist/proxmox/index.js +349 -287
- package/dist/proxmox/index.js.map +8 -7
- package/dist/proxmox/network-apply-read.d.ts +12 -0
- package/dist/proxmox/network-apply-read.d.ts.map +1 -1
- package/dist/proxmox/network-apply.d.ts +2 -0
- package/dist/proxmox/network-apply.d.ts.map +1 -1
- package/dist/proxmox/node-network-form.d.ts +86 -54
- package/dist/proxmox/node-network-form.d.ts.map +1 -1
- package/dist/proxmox/node-network-wire.d.ts +100 -0
- package/dist/proxmox/node-network-wire.d.ts.map +1 -0
- package/dist/proxmox/node-network.d.ts +21 -106
- package/dist/proxmox/node-network.d.ts.map +1 -1
- package/docs/grafana-alerting-rules.md +143 -0
- package/docs/grafana-alerting.md +7 -4
- package/docs/grafana-notification-policy.md +127 -0
- package/docs/grafana.md +12 -6
- package/docs/upstream-conformance.md +30 -4
- package/package.json +1 -1
- package/src/grafana/alert-rule-group-drop-warning.test.ts +107 -0
- package/src/grafana/alert-rule-group-drop-warning.ts +41 -0
- package/src/grafana/alert-rule-group-fixtures.ts +46 -0
- package/src/grafana/alert-rule-group-model.test.ts +75 -0
- package/src/grafana/alert-rule-group-model.ts +66 -0
- package/src/grafana/alert-rule-group-provenance.test.ts +116 -0
- package/src/grafana/alert-rule-group.test.ts +164 -0
- package/src/grafana/alert-rule-group.ts +231 -0
- package/src/grafana/index.ts +18 -4
- package/src/grafana/notification-policy-drop-warning.test.ts +163 -0
- package/src/grafana/notification-policy-drop-warning.ts +60 -0
- package/src/grafana/notification-policy-fixtures.ts +27 -0
- package/src/grafana/notification-policy-model.test.ts +104 -0
- package/src/grafana/notification-policy-model.ts +100 -0
- package/src/grafana/notification-policy-refusals.test.ts +119 -0
- package/src/grafana/notification-policy.test.ts +86 -0
- package/src/grafana/notification-policy.ts +191 -0
- package/src/grafana/providers.ts +4 -0
- package/src/proxmox/constraints-host-forms.test.ts +2 -2
- package/src/proxmox/network-apply-read.ts +12 -0
- package/src/proxmox/network-apply.ts +2 -0
- package/src/proxmox/node-network-form.ts +93 -99
- package/src/proxmox/node-network-read-failure.test.ts +206 -0
- package/src/proxmox/node-network-wire.ts +239 -0
- 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 (
|
|
38
|
-
* declaration is reported ABSENT rather than retyped
|
|
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
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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 {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
export
|
|
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
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
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(
|
|
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
|
+
);
|