@homeflare/alchemy 0.54.0 → 0.55.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/ceph/ceph-argv-config.d.ts +6 -3
- package/dist/ceph/ceph-argv-config.d.ts.map +1 -1
- package/dist/ceph/ceph-argv.d.ts +8 -0
- package/dist/ceph/ceph-argv.d.ts.map +1 -1
- package/dist/ceph/ceph-config-option-form.d.ts +48 -0
- package/dist/ceph/ceph-config-option-form.d.ts.map +1 -0
- package/dist/ceph/ceph-config-option-reconcile.d.ts +22 -0
- package/dist/ceph/ceph-config-option-reconcile.d.ts.map +1 -0
- package/dist/ceph/ceph-config-options.d.ts +15 -6
- package/dist/ceph/ceph-config-options.d.ts.map +1 -1
- package/dist/ceph/config-option.d.ts +10 -0
- package/dist/ceph/config-option.d.ts.map +1 -0
- package/dist/ceph/index.d.ts +7 -3
- package/dist/ceph/index.d.ts.map +1 -1
- package/dist/ceph/index.js +363 -8
- package/dist/ceph/index.js.map +17 -7
- package/dist/linux/index.js +33 -2
- package/dist/linux/index.js.map +3 -3
- package/dist/linux/sudo-runner.d.ts +12 -0
- package/dist/linux/sudo-runner.d.ts.map +1 -1
- package/dist/proxmox/index.d.ts +1 -0
- package/dist/proxmox/index.d.ts.map +1 -1
- package/dist/proxmox/index.js +101 -1
- package/dist/proxmox/index.js.map +5 -4
- package/dist/proxmox/scoped-api.d.ts +49 -0
- package/dist/proxmox/scoped-api.d.ts.map +1 -0
- package/docs/pve-scoped-api.md +23 -0
- package/package.json +1 -1
- package/src/ceph/ceph-argv-config.ts +27 -19
- package/src/ceph/ceph-argv.test.ts +37 -1
- package/src/ceph/ceph-argv.ts +25 -0
- package/src/ceph/ceph-config-option-form.ts +73 -0
- package/src/ceph/ceph-config-option-reconcile.ts +88 -0
- package/src/ceph/ceph-config-option.test.ts +213 -0
- package/src/ceph/ceph-config-options.test.ts +28 -11
- package/src/ceph/ceph-config-options.ts +29 -8
- package/src/ceph/config-option.ts +112 -0
- package/src/ceph/index.ts +7 -3
- package/src/linux/sudo-runner-privileged-read.test.ts +92 -0
- package/src/linux/sudo-runner.ts +54 -1
- package/src/proxmox/index.ts +13 -0
- package/src/proxmox/scoped-api.test.ts +52 -0
- package/src/proxmox/scoped-api.ts +148 -0
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `undefined` when `value` is a safe ceph config value — exported (like `boundedEntityProblem`)
|
|
3
|
-
* so the shape itself is directly testable
|
|
4
|
-
* `cephCommandProblem` never reaches this check on any real argv (`namedOptionProblem` refuses
|
|
5
|
-
* first) — this stays exercised until K-D's first named option arms the path for real.
|
|
3
|
+
* so the shape itself is directly testable independently of whichever options are named.
|
|
6
4
|
*/
|
|
7
5
|
export declare const configValueProblem: (value: string) => string | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* `undefined` when `name` may be declared for `who`, adopted, read by value or written — see
|
|
8
|
+
* ceph-config-options.ts's header for why the list starts empty and stays reviewed line by line.
|
|
9
|
+
*/
|
|
10
|
+
export declare const namedOptionProblem: (who: string, name: string) => string | undefined;
|
|
8
11
|
/** `config get|set|rm <who> <name> [value]`, called with the operands after `config` stripped. */
|
|
9
12
|
export declare const configProblem: (rest: readonly string[]) => string | undefined;
|
|
10
13
|
//# sourceMappingURL=ceph-argv-config.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ceph-argv-config.d.ts","sourceRoot":"","sources":["../../src/ceph/ceph-argv-config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ceph-argv-config.d.ts","sourceRoot":"","sources":["../../src/ceph/ceph-argv-config.ts"],"names":[],"mappings":"AAwCA;;;GAGG;AACH,eAAO,MAAM,kBAAkB,UAAW,MAAM,KAAG,MAAM,GAAG,SAG0B,CAAC;AAEvF;;;GAGG;AACH,eAAO,MAAM,kBAAkB,QAAS,MAAM,QAAQ,MAAM,KAAG,MAAM,GAAG,SAUvE,CAAC;AAEF,kGAAkG;AAClG,eAAO,MAAM,aAAa,SAAU,SAAS,MAAM,EAAE,KAAG,MAAM,GAAG,SAmBhE,CAAC"}
|
package/dist/ceph/ceph-argv.d.ts
CHANGED
|
@@ -16,4 +16,12 @@ export declare const authGetArgv: (entity: string) => readonly string[];
|
|
|
16
16
|
export declare const authGetOrCreateArgv: (entity: string, caps: Readonly<Record<(typeof DAEMONS)[number], string>>) => readonly string[];
|
|
17
17
|
export declare const authCapsArgv: (entity: string, caps: Readonly<Record<(typeof DAEMONS)[number], string>>) => readonly string[];
|
|
18
18
|
export declare const quorumStatusArgv: () => readonly string[];
|
|
19
|
+
/**
|
|
20
|
+
* `config set <who> <name> <value>` — `Ceph.ConfigOption`'s write. ⚠️ The VALUE is in argv, so it
|
|
21
|
+
* lands in the runner's log and the node's sudo journal; acceptable only because the named option
|
|
22
|
+
* list carries no secret-valued option (ceph-config-options.ts).
|
|
23
|
+
*/
|
|
24
|
+
export declare const configSetArgv: (who: string, name: string, value: string) => readonly string[];
|
|
25
|
+
/** `config rm <who> <name>` — `Ceph.ConfigOption`'s delete; idempotent on the ceph side. */
|
|
26
|
+
export declare const configRmArgv: (who: string, name: string) => readonly string[];
|
|
19
27
|
//# sourceMappingURL=ceph-argv.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ceph-argv.d.ts","sourceRoot":"","sources":["../../src/ceph/ceph-argv.ts"],"names":[],"mappings":"AA2BA,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAE3D,kFAAkF;AAClF,eAAO,MAAM,QAAQ,kBAAkB,CAAC;AAExC,mEAAmE;AACnE,eAAO,MAAM,aAAa,gBAAgB,CAAC;AAK3C,yFAAyF;AACzF,eAAO,MAAM,oBAAoB,WAAY,MAAM,KAAG,MAAM,GAAG,SAGiD,CAAC;AAejH,QAAA,MAAM,OAAO,YAAI,KAAK,EAAE,KAAK,EAAE,KAAK,CAAU,CAAC;AA6E/C;;;GAGG;AACH,eAAO,MAAM,kBAAkB,SAAU,SAAS,MAAM,EAAE,KAAG,MAAM,GAAG,SAWrE,CAAC;AAEF,kGAAkG;AAClG,eAAO,MAAM,WAAW,WAAY,MAAM,KAAG,SAAS,MAAM,EAM3D,CAAC;AAEF,eAAO,MAAM,mBAAmB,WACtB,MAAM,QACR,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,KACvD,SAAS,MAAM,EAOjB,CAAC;AAEF,eAAO,MAAM,YAAY,WACf,MAAM,QACR,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,KACvD,SAAS,MAAM,EAOjB,CAAC;AAEF,eAAO,MAAM,gBAAgB,QAAO,SAAS,MAAM,EAIlD,CAAC"}
|
|
1
|
+
{"version":3,"file":"ceph-argv.d.ts","sourceRoot":"","sources":["../../src/ceph/ceph-argv.ts"],"names":[],"mappings":"AA2BA,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAE3D,kFAAkF;AAClF,eAAO,MAAM,QAAQ,kBAAkB,CAAC;AAExC,mEAAmE;AACnE,eAAO,MAAM,aAAa,gBAAgB,CAAC;AAK3C,yFAAyF;AACzF,eAAO,MAAM,oBAAoB,WAAY,MAAM,KAAG,MAAM,GAAG,SAGiD,CAAC;AAejH,QAAA,MAAM,OAAO,YAAI,KAAK,EAAE,KAAK,EAAE,KAAK,CAAU,CAAC;AA6E/C;;;GAGG;AACH,eAAO,MAAM,kBAAkB,SAAU,SAAS,MAAM,EAAE,KAAG,MAAM,GAAG,SAWrE,CAAC;AAEF,kGAAkG;AAClG,eAAO,MAAM,WAAW,WAAY,MAAM,KAAG,SAAS,MAAM,EAM3D,CAAC;AAEF,eAAO,MAAM,mBAAmB,WACtB,MAAM,QACR,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,KACvD,SAAS,MAAM,EAOjB,CAAC;AAEF,eAAO,MAAM,YAAY,WACf,MAAM,QACR,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,KACvD,SAAS,MAAM,EAOjB,CAAC;AAEF,eAAO,MAAM,gBAAgB,QAAO,SAAS,MAAM,EAIlD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,aAAa,QAAS,MAAM,QAAQ,MAAM,SAAS,MAAM,KAAG,SAAS,MAAM,EAQvF,CAAC;AAEF,4FAA4F;AAC5F,eAAO,MAAM,YAAY,QAAS,MAAM,QAAQ,MAAM,KAAG,SAAS,MAAM,EAOvE,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Props, attributes and the pure helpers for `Ceph.ConfigOption` (K-D) — split from
|
|
3
|
+
* config-option.ts the same way ceph-auth-form.ts is split from auth-entity.ts: nothing here
|
|
4
|
+
* talks to a cluster, so every rule is unit-tested without PVE or ssh.
|
|
5
|
+
* Design: docs/plans/2026-09-26-ceph-config-options.md.
|
|
6
|
+
*
|
|
7
|
+
* ⛔ VALIDATION IS THE TRANSPORT'S OWN ALLOWLIST, NOT A SECOND COPY OF IT. `configOptionProblem`
|
|
8
|
+
* builds the exact `config set` argv the write would run and asks `cephCommandProblem` — so the
|
|
9
|
+
* named list (ceph-config-options.ts), the per-option `<who>` bound, the lockout classes and the
|
|
10
|
+
* flag-shaped-value refusal can never disagree between plan time and write time.
|
|
11
|
+
*/
|
|
12
|
+
import type { ListNodeCephCfgDbResponseBodyItem } from '@distilled.cloud/proxmox/nodes';
|
|
13
|
+
import type { WithTarget } from '../proxmox/resource-spec.ts';
|
|
14
|
+
export interface CephConfigOptionProps extends WithTarget {
|
|
15
|
+
/**
|
|
16
|
+
* The PVE node whose `GET /nodes/{node}/ceph/cfg/db` answers the mon config db — the plan-time
|
|
17
|
+
* read. Any node of the Ceph cluster answers the same db.
|
|
18
|
+
*/
|
|
19
|
+
node: string;
|
|
20
|
+
/** ssh destinations of the mon hosts the write (`config set`/`rm`) is tried against, in order. */
|
|
21
|
+
monNodes: readonly string[];
|
|
22
|
+
/** The config section: `global`, `mon`, `osd`, `mgr`, `mds`, `client`, optionally `.<id>`. */
|
|
23
|
+
who: string;
|
|
24
|
+
/** The option name — must be on `NAMED_CONFIG_OPTIONS` for exactly this `who`. */
|
|
25
|
+
name: string;
|
|
26
|
+
/**
|
|
27
|
+
* The value as ceph stores it — a string, compared byte-for-byte with what the config db reports.
|
|
28
|
+
* ⚠️ Spell it the way ceph echoes it back (`false`, not `0`), or every plan is an `update`.
|
|
29
|
+
*/
|
|
30
|
+
value: string;
|
|
31
|
+
}
|
|
32
|
+
export interface CephConfigOptionAttributes {
|
|
33
|
+
who: string;
|
|
34
|
+
name: string;
|
|
35
|
+
/** The mon-db value observed after the last reconcile. The only field `matches` compares. */
|
|
36
|
+
value: string;
|
|
37
|
+
}
|
|
38
|
+
/** One row of the mon config db, as the PVE API reports it (`ceph config dump`'s shape). */
|
|
39
|
+
export type CfgDbRow = Pick<ListNodeCephCfgDbResponseBodyItem, 'section' | 'name' | 'value' | 'mask'>;
|
|
40
|
+
/** `undefined` when props may be declared at all; else why not. Runs before any read or write. */
|
|
41
|
+
export declare const configOptionProblem: (props: CephConfigOptionProps) => string | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* The live row for exactly `{who, name}` with NO mask. ⚠️ A masked row (`osd/class:ssd`) is a
|
|
44
|
+
* different setting the bare `config set <who>` neither writes nor removes, so it is never matched.
|
|
45
|
+
*/
|
|
46
|
+
export declare const findLive: (rows: readonly CfgDbRow[], who: string, name: string) => CephConfigOptionAttributes | undefined;
|
|
47
|
+
export declare const matches: (live: CephConfigOptionAttributes | undefined, props: CephConfigOptionProps) => boolean;
|
|
48
|
+
//# sourceMappingURL=ceph-config-option-form.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ceph-config-option-form.d.ts","sourceRoot":"","sources":["../../src/ceph/ceph-config-option-form.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,iCAAiC,EAAE,MAAM,gCAAgC,CAAC;AACxF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAG9D,MAAM,WAAW,qBAAsB,SAAQ,UAAU;IACvD;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,kGAAkG;IAClG,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,8FAA8F;IAC9F,GAAG,EAAE,MAAM,CAAC;IACZ,kFAAkF;IAClF,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,0BAA0B;IACzC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,6FAA6F;IAC7F,KAAK,EAAE,MAAM,CAAC;CACf;AAED,4FAA4F;AAC5F,MAAM,MAAM,QAAQ,GAAG,IAAI,CACzB,iCAAiC,EACjC,SAAS,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CACtC,CAAC;AAEF,kGAAkG;AAClG,eAAO,MAAM,mBAAmB,UAAW,qBAAqB,KAAG,MAAM,GAAG,SAM3E,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,QAAQ,SACb,SAAS,QAAQ,EAAE,OACpB,MAAM,QACL,MAAM,KACX,0BAA0B,GAAG,SAG/B,CAAC;AAEF,eAAO,MAAM,OAAO,SACZ,0BAA0B,GAAG,SAAS,SACrC,qBAAqB,YACuB,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The observe -> ensure -> sync loop and the delete for `Ceph.ConfigOption` (K-D) — plain
|
|
3
|
+
* functions, so a fake-runner test drives the real loop (same split as ceph-auth-reconcile.ts).
|
|
4
|
+
*
|
|
5
|
+
* ★ OBSERVE IS THE PVE API, WRITES ARE THE SSH MON TRANSPORT. The PVE API is read-only for the
|
|
6
|
+
* config db (`GET /nodes/{node}/ceph/cfg/db`; no write endpoint exists), so writes go through
|
|
7
|
+
* ceph-transport.ts's `runCephCommand` — the one transport every Ceph family in this kit uses,
|
|
8
|
+
* allowlist-checked before it dials — and each write is followed by `assertFreshQuorum`.
|
|
9
|
+
* ★ A DEPLOY WITH NO DRIFT MAKES ZERO WRITES (S10). If the observed mon-db value already equals
|
|
10
|
+
* props — an adopted, already-set option included — nothing reaches ssh at all.
|
|
11
|
+
* ⛔ A FAILED READ IS NEVER "ABSENT". The caller's `readRows` propagates its own failure; only a
|
|
12
|
+
* successful read with no matching row means the option is unset.
|
|
13
|
+
* ⛔ READ BACK AFTER EVERY WRITE and refuse to record a value the mon db does not report.
|
|
14
|
+
*/
|
|
15
|
+
import * as Effect from 'effect/Effect';
|
|
16
|
+
import { type CephConfigOptionAttributes, type CephConfigOptionProps, type CfgDbRow } from './ceph-config-option-form.ts';
|
|
17
|
+
import { type CephTransportOptions } from './ceph-transport.ts';
|
|
18
|
+
export type CephConfigDeps = Pick<CephTransportOptions, 'dial' | 'log'>;
|
|
19
|
+
export declare const reconcileCephConfigOption: <E, R>(props: CephConfigOptionProps, readRows: Effect.Effect<readonly CfgDbRow[], E, R>, deps: CephConfigDeps) => Effect.Effect<CephConfigOptionAttributes, E | Error, R>;
|
|
20
|
+
/** Idempotent: an option already absent from the mon db is success, with nothing sent. */
|
|
21
|
+
export declare const deleteCephConfigOption: <E, R>(props: Pick<CephConfigOptionProps, 'who' | 'name' | 'monNodes'>, readRows: Effect.Effect<readonly CfgDbRow[], E, R>, deps: CephConfigDeps) => Effect.Effect<void, E | Error, R>;
|
|
22
|
+
//# sourceMappingURL=ceph-config-option-reconcile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ceph-config-option-reconcile.d.ts","sourceRoot":"","sources":["../../src/ceph/ceph-config-option-reconcile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,EACL,KAAK,0BAA0B,EAC/B,KAAK,qBAAqB,EAC1B,KAAK,QAAQ,EAId,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,KAAK,oBAAoB,EAAqC,MAAM,qBAAqB,CAAC;AAEnG,MAAM,MAAM,cAAc,GAAG,IAAI,CAAC,oBAAoB,EAAE,MAAM,GAAG,KAAK,CAAC,CAAC;AAsBxE,eAAO,MAAM,yBAAyB,GAAI,CAAC,EAAE,CAAC,SACrC,qBAAqB,YAClB,MAAM,CAAC,MAAM,CAAC,SAAS,QAAQ,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,QAC5C,cAAc,KACnB,MAAM,CAAC,MAAM,CAAC,0BAA0B,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,CAuBrD,CAAC;AAEL,0FAA0F;AAC1F,eAAO,MAAM,sBAAsB,GAAI,CAAC,EAAE,CAAC,SAClC,IAAI,CAAC,qBAAqB,EAAE,KAAK,GAAG,MAAM,GAAG,UAAU,CAAC,YACrD,MAAM,CAAC,MAAM,CAAC,SAAS,QAAQ,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,QAC5C,cAAc,KACnB,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,CAK/B,CAAC"}
|
|
@@ -3,10 +3,14 @@
|
|
|
3
3
|
* through the mon-command transport (ceph-argv.ts's `configProblem`). Design:
|
|
4
4
|
* docs/plans/2026-09-26-ceph-config-options.md, D-C1.
|
|
5
5
|
*
|
|
6
|
-
* ⛔ THE LIST
|
|
7
|
-
*
|
|
8
|
-
* transport's allowlist
|
|
9
|
-
*
|
|
6
|
+
* ⛔ THE LIST STARTED EMPTY, AND EACH ADDITION IS ITS OWN REVIEWED PR LINE. `Ceph.ConfigOption`
|
|
7
|
+
* (K-D, config-option.ts) is the only declarer; it refuses any `{who, name}` this map does not
|
|
8
|
+
* carry before planning, and the transport's allowlist (ceph-argv-config.ts) refuses it again
|
|
9
|
+
* before anything dials. One entry per line, each with a comment saying what it does and why it
|
|
10
|
+
* is managed — never a batch.
|
|
11
|
+
* ★ EACH ENTRY ALSO NAMES THE `<who>` IT MAY BE SET FOR. This is the config-options design's
|
|
12
|
+
* acceptance test #1 ("`<who>` bounded to the declared daemons"): an option listed for `mgr`
|
|
13
|
+
* can never be written to `global`, `client.admin` or any other section through this transport.
|
|
10
14
|
* ⛔ NEVER ADD ONE OF THE LOCKOUT CLASSES BELOW. `mon_host`, `public_network`, `cluster_network`,
|
|
11
15
|
* `public_addr`, `cluster_addr`, every `auth_*_required` and every `key`/`keyring`-bearing
|
|
12
16
|
* option re-address the mons or de-authenticate the whole cluster — cutting every VM disk — and
|
|
@@ -16,8 +20,13 @@
|
|
|
16
20
|
* cluster to measure this against yet); a secret-bearing setting belongs in a vault-backed flow,
|
|
17
21
|
* never a `ceph config set` argv, which lands in the runner's log and the node's sudo journal.
|
|
18
22
|
*/
|
|
19
|
-
/**
|
|
20
|
-
|
|
23
|
+
/**
|
|
24
|
+
* option name -> the `<who>` sections it may be set for. Add one reviewed line per PR, never a
|
|
25
|
+
* batch; the structural test (ceph-config-options.test.ts) runs the lockout guard over every key.
|
|
26
|
+
*/
|
|
27
|
+
export declare const NAMED_CONFIG_OPTIONS: ReadonlyMap<string, readonly string[]>;
|
|
28
|
+
/** The `<who>` sections `name` may be set for — empty when the option is not on the list. */
|
|
29
|
+
export declare const allowedWhoFor: (name: string) => readonly string[];
|
|
21
30
|
/** Whether `name` is a class this transport must never read a value for, adopt, or write. */
|
|
22
31
|
export declare const isLockoutConfigOption: (name: string) => boolean;
|
|
23
32
|
//# sourceMappingURL=ceph-config-options.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ceph-config-options.d.ts","sourceRoot":"","sources":["../../src/ceph/ceph-config-options.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"ceph-config-options.d.ts","sourceRoot":"","sources":["../../src/ceph/ceph-config-options.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH;;;GAGG;AACH,eAAO,MAAM,oBAAoB,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAOtE,CAAC;AAEH,6FAA6F;AAC7F,eAAO,MAAM,aAAa,SAAU,MAAM,KAAG,SAAS,MAAM,EACtB,CAAC;AAgBvC,6FAA6F;AAC7F,eAAO,MAAM,qBAAqB,SAAU,MAAM,KAAG,OACE,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Resource } from 'alchemy';
|
|
2
|
+
import * as Provider from 'alchemy/Provider';
|
|
3
|
+
import type { PveRequirements } from '../proxmox/resource-spec.ts';
|
|
4
|
+
import { type CephConfigOptionAttributes, type CephConfigOptionProps } from './ceph-config-option-form.ts';
|
|
5
|
+
export type { CephConfigOptionAttributes, CephConfigOptionProps, } from './ceph-config-option-form.ts';
|
|
6
|
+
export interface CephConfigOption extends Resource<'Ceph.ConfigOption', CephConfigOptionProps, CephConfigOptionAttributes, never, PveRequirements> {
|
|
7
|
+
}
|
|
8
|
+
export declare const CephConfigOption: import("alchemy").ResourceClass<CephConfigOption>;
|
|
9
|
+
export declare const CephConfigOptionProvider: () => import("effect/Layer").Layer<Provider.Provider<CephConfigOption>, never, import("effect/http/HttpClient").HttpClient>;
|
|
10
|
+
//# sourceMappingURL=config-option.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-option.d.ts","sourceRoot":"","sources":["../../src/ceph/config-option.ts"],"names":[],"mappings":"AA0BA,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAGnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAI7C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAEnE,OAAO,EACL,KAAK,0BAA0B,EAC/B,KAAK,qBAAqB,EAI3B,MAAM,8BAA8B,CAAC;AAMtC,YAAY,EACV,0BAA0B,EAC1B,qBAAqB,GACtB,MAAM,8BAA8B,CAAC;AAEtC,MAAM,WAAW,gBAAiB,SAAQ,QAAQ,CAChD,mBAAmB,EACnB,qBAAqB,EACrB,0BAA0B,EAC1B,KAAK,EACL,eAAe,CAChB;CAAG;AAEJ,eAAO,MAAM,gBAAgB,mDAE3B,CAAC;AAWH,eAAO,MAAM,wBAAwB,6HAsClC,CAAC"}
|
package/dist/ceph/index.d.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Ceph providers for Alchemy — the mon-command transport
|
|
2
|
+
* Ceph providers for Alchemy — the mon-command transport, `Ceph.AuthEntity` (K-A4) and
|
|
3
|
+
* `Ceph.ConfigOption` (K-D).
|
|
3
4
|
*
|
|
4
5
|
* ⛔ THIS BARREL IS THE PUBLIC API, DELIBERATELY SMALLER THAN THE DIRECTORY — see
|
|
5
6
|
* `../openbao/index.ts`'s header for why. The allowlist internals (ceph-argv.ts), the parsing
|
|
6
|
-
* (ceph-auth-parse.ts) and the reconcile
|
|
7
|
-
*
|
|
7
|
+
* (ceph-auth-parse.ts) and the reconcile loops (ceph-auth-reconcile.ts,
|
|
8
|
+
* ceph-config-option-reconcile.ts) are reached by path from tests, not published here.
|
|
8
9
|
*/
|
|
9
10
|
export type { CephAuthEntityAttributes, CephAuthEntityProps, CephCaps } from './auth-entity.ts';
|
|
10
11
|
export { CephAuthEntity, CephAuthEntityProvider } from './auth-entity.ts';
|
|
11
12
|
export type { CephDial, CephExecResult, CephTransportOptions } from './ceph-transport.ts';
|
|
12
13
|
export { assertFreshQuorum, runCephCommand, sshCephDial } from './ceph-transport.ts';
|
|
14
|
+
export type { CephConfigOptionAttributes, CephConfigOptionProps } from './config-option.ts';
|
|
15
|
+
export { CephConfigOption, CephConfigOptionProvider } from './config-option.ts';
|
|
16
|
+
export { NAMED_CONFIG_OPTIONS } from './ceph-config-options.ts';
|
|
13
17
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/ceph/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ceph/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ceph/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,YAAY,EAAE,wBAAwB,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAChG,OAAO,EAAE,cAAc,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAC1E,YAAY,EAAE,QAAQ,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC1F,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACrF,YAAY,EAAE,0BAA0B,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC5F,OAAO,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AAChF,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC"}
|
package/dist/ceph/index.js
CHANGED
|
@@ -187,7 +187,10 @@ var canonical = (text) => text.trimEnd();
|
|
|
187
187
|
var sha256 = (text) => new Bun.CryptoHasher("sha256").update(canonical(text)).digest("hex");
|
|
188
188
|
|
|
189
189
|
// src/ceph/ceph-config-options.ts
|
|
190
|
-
var NAMED_CONFIG_OPTIONS = new
|
|
190
|
+
var NAMED_CONFIG_OPTIONS = new Map([
|
|
191
|
+
["mgr/prometheus/exclude_perf_counters", ["mgr"]]
|
|
192
|
+
]);
|
|
193
|
+
var allowedWhoFor = (name) => NAMED_CONFIG_OPTIONS.get(name) ?? [];
|
|
191
194
|
var LOCKOUT_EXACT = new Set([
|
|
192
195
|
"mon_host",
|
|
193
196
|
"public_network",
|
|
@@ -195,20 +198,23 @@ var LOCKOUT_EXACT = new Set([
|
|
|
195
198
|
"public_addr",
|
|
196
199
|
"cluster_addr"
|
|
197
200
|
]);
|
|
198
|
-
var LOCKOUT_PATTERN = /^auth_\w*_required$|(^|_)key(ring)?($|_)/;
|
|
201
|
+
var LOCKOUT_PATTERN = /^auth_\w*_required$|(^|[_/])key(ring)?($|[_/])/;
|
|
199
202
|
var isLockoutConfigOption = (name) => LOCKOUT_EXACT.has(name) || LOCKOUT_PATTERN.test(name);
|
|
200
203
|
|
|
201
204
|
// src/ceph/ceph-argv-config.ts
|
|
202
205
|
var WHO = /^(global|mon|osd|mgr|mds|client)(\.[A-Za-z0-9_-]{1,32})?$/;
|
|
203
|
-
var OPTION_NAME = /^[a-z][a-z0-9_]{0,63}$/;
|
|
206
|
+
var OPTION_NAME = /^(?:[a-z][a-z0-9_]{0,63}|mgr\/[a-z][a-z0-9_]{0,31}\/[a-z][a-z0-9_]{0,63})$/;
|
|
204
207
|
var CONFIG_VALUE = /^(?:[A-Za-z0-9][A-Za-z0-9 ._:/=,-]{0,255}|-[0-9]{1,255})$/;
|
|
205
208
|
var configValueProblem = (value) => CONFIG_VALUE.test(value) ? undefined : `config value ${JSON.stringify(value)} contains characters outside the safe set`;
|
|
206
|
-
var namedOptionProblem = (name) => {
|
|
209
|
+
var namedOptionProblem = (who, name) => {
|
|
207
210
|
if (!OPTION_NAME.test(name))
|
|
208
211
|
return `${JSON.stringify(name)} is not a plain ceph option name`;
|
|
209
212
|
if (isLockoutConfigOption(name))
|
|
210
213
|
return `${name} is a lockout-class option and can never be declared`;
|
|
211
|
-
|
|
214
|
+
if (!NAMED_CONFIG_OPTIONS.has(name))
|
|
215
|
+
return `${name} is not on the named option list (each entry is a reviewed PR line — see ceph-config-options.ts)`;
|
|
216
|
+
const allowed = allowedWhoFor(name);
|
|
217
|
+
return allowed.includes(who) ? undefined : `${name} may only be set for ${allowed.join("/")} on the named option list, not ${JSON.stringify(who)}`;
|
|
212
218
|
};
|
|
213
219
|
var configProblem = (rest) => {
|
|
214
220
|
const [verb, who, name, ...extra] = rest;
|
|
@@ -218,13 +224,13 @@ var configProblem = (rest) => {
|
|
|
218
224
|
if (name === undefined)
|
|
219
225
|
return "config takes a `<name>` from the named option list";
|
|
220
226
|
if (verb === "get" || verb === "rm") {
|
|
221
|
-
return extra.length === 0 ? namedOptionProblem(name) : `config ${String(verb)} takes exactly \`<who> <name>\``;
|
|
227
|
+
return extra.length === 0 ? namedOptionProblem(who, name) : `config ${String(verb)} takes exactly \`<who> <name>\``;
|
|
222
228
|
}
|
|
223
229
|
if (verb === "set") {
|
|
224
230
|
const [value, ...more] = extra;
|
|
225
231
|
if (value === undefined || more.length !== 0)
|
|
226
232
|
return "config set takes exactly `<who> <name> <value>`";
|
|
227
|
-
const named = namedOptionProblem(name);
|
|
233
|
+
const named = namedOptionProblem(who, name);
|
|
228
234
|
return named !== undefined ? named : configValueProblem(value);
|
|
229
235
|
}
|
|
230
236
|
return `ceph config ${String(verb)} is not on the allowlist`;
|
|
@@ -335,6 +341,23 @@ var quorumStatusArgv = () => [
|
|
|
335
341
|
"quorum_status",
|
|
336
342
|
...JSON_FORMAT
|
|
337
343
|
];
|
|
344
|
+
var configSetArgv = (who, name, value) => [
|
|
345
|
+
CEPH_BIN,
|
|
346
|
+
"config",
|
|
347
|
+
"set",
|
|
348
|
+
who,
|
|
349
|
+
name,
|
|
350
|
+
value,
|
|
351
|
+
...JSON_FORMAT
|
|
352
|
+
];
|
|
353
|
+
var configRmArgv = (who, name) => [
|
|
354
|
+
CEPH_BIN,
|
|
355
|
+
"config",
|
|
356
|
+
"rm",
|
|
357
|
+
who,
|
|
358
|
+
name,
|
|
359
|
+
...JSON_FORMAT
|
|
360
|
+
];
|
|
338
361
|
|
|
339
362
|
// src/ceph/ceph-auth-parse.ts
|
|
340
363
|
var isRecord = (value) => typeof value === "object" && value !== null;
|
|
@@ -1598,13 +1621,345 @@ var CephAuthEntityProvider = () => Provider.effect(CephAuthEntity, Effect3.succe
|
|
|
1598
1621
|
reconcile: ({ news, output }) => reconcileCephAuthEntity(news, output, {}),
|
|
1599
1622
|
delete: ({ output }) => Effect3.die(refusedDelete(output.entity))
|
|
1600
1623
|
})));
|
|
1624
|
+
// src/ceph/config-option.ts
|
|
1625
|
+
import * as nodes from "@distilled.cloud/proxmox/nodes";
|
|
1626
|
+
import { Resource as Resource2 } from "alchemy";
|
|
1627
|
+
import { Unowned } from "alchemy/AdoptPolicy";
|
|
1628
|
+
import { isResolved as isResolved2 } from "alchemy/Diff";
|
|
1629
|
+
import * as Provider2 from "alchemy/Provider";
|
|
1630
|
+
import * as Effect10 from "effect/Effect";
|
|
1631
|
+
|
|
1632
|
+
// src/proxmox/credentials.ts
|
|
1633
|
+
var VALID_TIER = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
1634
|
+
var mintTier = (target, role) => {
|
|
1635
|
+
if (target.scheme !== "pve")
|
|
1636
|
+
return role;
|
|
1637
|
+
const tier = target.roles?.[role];
|
|
1638
|
+
if (tier === undefined)
|
|
1639
|
+
return role;
|
|
1640
|
+
if (!VALID_TIER.test(tier)) {
|
|
1641
|
+
throw new Error(`PveTarget.roles.${role} = ${JSON.stringify(tier)} is not a valid OpenBao tier name -- ` + 'it would be interpolated straight into "<mount>/creds/<tier>" and must be a plain name ' + '(letters, digits, ".", "_", "-", not starting with one of those and not "..").');
|
|
1642
|
+
}
|
|
1643
|
+
return tier;
|
|
1644
|
+
};
|
|
1645
|
+
|
|
1646
|
+
// src/proxmox/distilled-pve.ts
|
|
1647
|
+
import * as Effect7 from "effect/Effect";
|
|
1648
|
+
import * as Result2 from "effect/Result";
|
|
1649
|
+
import { credentials as distilledPveCredentials } from "@distilled.cloud/proxmox/Credentials";
|
|
1650
|
+
import * as ProxmoxRetry from "@distilled.cloud/proxmox/Retry";
|
|
1651
|
+
|
|
1652
|
+
// src/proxmox/lease-cache.ts
|
|
1653
|
+
import * as Cache from "effect/Cache";
|
|
1654
|
+
import * as Duration from "effect/Duration";
|
|
1655
|
+
import * as Effect5 from "effect/Effect";
|
|
1656
|
+
import * as Exit from "effect/Exit";
|
|
1657
|
+
|
|
1658
|
+
// src/proxmox/mint.ts
|
|
1659
|
+
import * as Effect4 from "effect/Effect";
|
|
1660
|
+
import * as Option2 from "effect/Option";
|
|
1661
|
+
import * as FetchHttpClient2 from "effect/http/FetchHttpClient";
|
|
1662
|
+
import * as Headers2 from "effect/http/Headers";
|
|
1663
|
+
import * as HttpClient3 from "effect/http/HttpClient";
|
|
1664
|
+
import * as HttpClientRequest2 from "effect/http/HttpClientRequest";
|
|
1665
|
+
|
|
1666
|
+
// src/proxmox/credential-errors.ts
|
|
1667
|
+
import * as Data from "effect/Data";
|
|
1668
|
+
|
|
1669
|
+
class PveCredentialDenied extends Data.TaggedError("PveCredentialDenied") {
|
|
1670
|
+
get message() {
|
|
1671
|
+
return `OpenBao GET /v1/${this.mount}/creds/${this.tier} -> 403: ${this.detail}. This identity's ` + "AppRole has no grant for this role -- a refused read, not evidence the object is gone.";
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
// src/proxmox/mint.ts
|
|
1676
|
+
var baoVariable = (env, name) => env[`BAO_${name}`] ?? env[`VAULT_${name}`] ?? "";
|
|
1677
|
+
var mint = (target, role, env = process.env) => Effect4.gen(function* () {
|
|
1678
|
+
const tier = mintTier(target, role);
|
|
1679
|
+
const path = `${target.mount}/creds/${tier}`;
|
|
1680
|
+
const refuse = (status2, detail) => new Error(`OpenBao GET /v1/${path} -> ${status2 === 0 ? "no response" : status2}: ${detail}`);
|
|
1681
|
+
const address = baoVariable(env, "AGENT_ADDR") || baoVariable(env, "ADDR") || "https://127.0.0.1:8200";
|
|
1682
|
+
const socket = address.startsWith("unix://") ? address.slice("unix://".length) : undefined;
|
|
1683
|
+
const base = socket === undefined ? address.replace(/\/+$/, "") : "http://localhost";
|
|
1684
|
+
const headers = { "X-Vault-Request": "true" };
|
|
1685
|
+
const namespace = baoVariable(env, "NAMESPACE");
|
|
1686
|
+
if (namespace !== "")
|
|
1687
|
+
headers["X-Vault-Namespace"] = namespace;
|
|
1688
|
+
const token = baoVariable(env, "TOKEN");
|
|
1689
|
+
if (token !== "")
|
|
1690
|
+
headers["X-Vault-Token"] = token;
|
|
1691
|
+
if (socket !== undefined && !("Bun" in globalThis)) {
|
|
1692
|
+
return yield* Effect4.fail(refuse(0, "a unix:// address needs Bun fetch; this is not Bun"));
|
|
1693
|
+
}
|
|
1694
|
+
const client = yield* HttpClient3.HttpClient;
|
|
1695
|
+
const redacted = yield* Headers2.CurrentRedactedNames;
|
|
1696
|
+
const defaults = Option2.getOrElse(yield* Effect4.serviceOption(FetchHttpClient2.RequestInit), () => ({}));
|
|
1697
|
+
const request = HttpClientRequest2.get(`${base}/v1/${path}`).pipe(HttpClientRequest2.setHeaders(headers));
|
|
1698
|
+
const exchange = client.execute(request).pipe(Effect4.flatMap((response) => Effect4.map(response.text, (text2) => ({ status: response.status, text: text2 }))), Effect4.timeout("60 seconds"), Effect4.provideService(Headers2.CurrentRedactedNames, [...redacted, "x-vault-token"]));
|
|
1699
|
+
const init = { ...defaults, unix: socket };
|
|
1700
|
+
const { status, text } = yield* (socket === undefined ? exchange : Effect4.provideService(exchange, FetchHttpClient2.RequestInit, init)).pipe(Effect4.mapError((cause) => refuse(0, cause.message)));
|
|
1701
|
+
let parsed;
|
|
1702
|
+
try {
|
|
1703
|
+
parsed = JSON.parse(text);
|
|
1704
|
+
} catch {
|
|
1705
|
+
parsed = undefined;
|
|
1706
|
+
}
|
|
1707
|
+
if (status < 200 || status >= 300) {
|
|
1708
|
+
const errors = parsed?.errors;
|
|
1709
|
+
const isOpenBaoBody = Array.isArray(errors);
|
|
1710
|
+
const detail = isOpenBaoBody ? errors.map(String).join("; ") : "not an OpenBao error body";
|
|
1711
|
+
if (status === 403 && isOpenBaoBody) {
|
|
1712
|
+
return yield* Effect4.fail(new PveCredentialDenied({ detail, mount: target.mount, tier }));
|
|
1713
|
+
}
|
|
1714
|
+
return yield* Effect4.fail(refuse(status, detail || "(no errors given)"));
|
|
1715
|
+
}
|
|
1716
|
+
const tokenId = parsed?.data?.token_id;
|
|
1717
|
+
const secret = parsed?.data?.secret;
|
|
1718
|
+
if (typeof tokenId !== "string" || typeof secret !== "string") {
|
|
1719
|
+
return yield* Effect4.fail(new Error(`${target.mount}/creds/${tier} returned no token_id/secret. Either this approle lacks ` + `the grant, or the mount is not ${target.mount}. Say which mount and role you needed ` + `than falling back to a stored credential -- every stored PVE token is read-only.`));
|
|
1720
|
+
}
|
|
1721
|
+
const lease = parsed?.lease_duration;
|
|
1722
|
+
const leaseSeconds = typeof lease === "number" ? lease : 0;
|
|
1723
|
+
return { leaseSeconds, secret, tokenId };
|
|
1724
|
+
});
|
|
1725
|
+
var authorization = (credential, target) => target.scheme === "pbs" ? `PBSAPIToken=${credential.tokenId}:${credential.secret}` : `PVEAPIToken=${credential.tokenId}=${credential.secret}`;
|
|
1726
|
+
|
|
1727
|
+
// src/proxmox/lease-cache.ts
|
|
1728
|
+
var REMINT_MARGIN_SECONDS = 60;
|
|
1729
|
+
var keyOf = (target, role) => ({
|
|
1730
|
+
mount: target.mount,
|
|
1731
|
+
scheme: target.scheme,
|
|
1732
|
+
tier: mintTier(target, role)
|
|
1733
|
+
});
|
|
1734
|
+
var timeToLive = (exit) => Exit.isSuccess(exit) && exit.value.leaseSeconds > REMINT_MARGIN_SECONDS ? Duration.seconds(exit.value.leaseSeconds - REMINT_MARGIN_SECONDS) : Duration.zero;
|
|
1735
|
+
var makeLeases = (mintFor) => Cache.makeWith(mintFor, { capacity: 64, requireServicesAt: "lookup", timeToLive });
|
|
1736
|
+
var targetForKey = (key) => key.scheme === "pbs" ? { api: "https://pbs.invalid/api2/json", mount: key.mount, scheme: "pbs" } : { members: [], mount: key.mount, roles: { read: key.tier }, scheme: "pve" };
|
|
1737
|
+
var mintForKey = (key, env = process.env) => mint(targetForKey(key), key.scheme === "pbs" ? key.tier : "read", env);
|
|
1738
|
+
var leases = Effect5.runSync(makeLeases(mintForKey));
|
|
1739
|
+
var leased = (target, role) => Cache.get(leases, keyOf(target, role));
|
|
1740
|
+
|
|
1741
|
+
// src/proxmox/members.ts
|
|
1742
|
+
import * as Cause2 from "effect/Cause";
|
|
1743
|
+
import * as Effect6 from "effect/Effect";
|
|
1744
|
+
import * as Result from "effect/Result";
|
|
1745
|
+
import * as HttpClient5 from "effect/http/HttpClient";
|
|
1746
|
+
import * as HttpClientError from "effect/http/HttpClientError";
|
|
1747
|
+
var PVE_API_PORT = 8006;
|
|
1748
|
+
var pveApiBase = (member) => `https://${member}:${String(PVE_API_PORT)}/api2/json`;
|
|
1749
|
+
var MEMBER_TIMEOUT = "20 seconds";
|
|
1750
|
+
var lastGoodByMount = new Map;
|
|
1751
|
+
var noteGoodMember = (target, member) => {
|
|
1752
|
+
lastGoodByMount.set(target.mount, member);
|
|
1753
|
+
};
|
|
1754
|
+
var orderedMembers = (target) => {
|
|
1755
|
+
const preferred = lastGoodByMount.get(target.mount);
|
|
1756
|
+
if (preferred === undefined || !target.members.includes(preferred))
|
|
1757
|
+
return target.members;
|
|
1758
|
+
return [preferred, ...target.members.filter((member) => member !== preferred)];
|
|
1759
|
+
};
|
|
1760
|
+
var errorCode = (cause) => {
|
|
1761
|
+
if (cause !== null && typeof cause === "object") {
|
|
1762
|
+
if ("code" in cause && typeof cause.code === "string")
|
|
1763
|
+
return cause.code;
|
|
1764
|
+
if ("cause" in cause)
|
|
1765
|
+
return errorCode(cause.cause);
|
|
1766
|
+
}
|
|
1767
|
+
return;
|
|
1768
|
+
};
|
|
1769
|
+
var isTimeoutCause = (cause) => {
|
|
1770
|
+
const code = errorCode(cause);
|
|
1771
|
+
if (code === "AbortError" || code === "ETIMEDOUT" || code === "UND_ERR_CONNECT_TIMEOUT") {
|
|
1772
|
+
return true;
|
|
1773
|
+
}
|
|
1774
|
+
return cause instanceof Error && /timed out|timeout/i.test(cause.message);
|
|
1775
|
+
};
|
|
1776
|
+
var PRE_SEND_CODES = new Set(["ConnectionRefused", "ECONNREFUSED", "ENOTFOUND", "EAI_AGAIN"]);
|
|
1777
|
+
var isCertificateCode = (code) => /^(ERR_TLS_CERT_|CERT_|UNABLE_TO_VERIFY_|DEPTH_ZERO_SELF_SIGNED|SELF_SIGNED_CERT)/.test(code);
|
|
1778
|
+
var isPreSendCause = (cause) => {
|
|
1779
|
+
const code = errorCode(cause);
|
|
1780
|
+
return code !== undefined && (PRE_SEND_CODES.has(code) || isCertificateCode(code));
|
|
1781
|
+
};
|
|
1782
|
+
var isTransportFailure = (cause) => {
|
|
1783
|
+
if (Cause2.isTimeoutError(cause))
|
|
1784
|
+
return true;
|
|
1785
|
+
if (!HttpClientError.isHttpClientError(cause))
|
|
1786
|
+
return false;
|
|
1787
|
+
const tag = cause.reason._tag;
|
|
1788
|
+
return tag === "TransportError" || tag === "InvalidUrlError" || tag === "EncodeError";
|
|
1789
|
+
};
|
|
1790
|
+
var isPreSendTransport = (cause) => {
|
|
1791
|
+
if (!HttpClientError.isHttpClientError(cause))
|
|
1792
|
+
return false;
|
|
1793
|
+
const { reason } = cause;
|
|
1794
|
+
if (reason._tag === "InvalidUrlError" || reason._tag === "EncodeError")
|
|
1795
|
+
return true;
|
|
1796
|
+
if (reason._tag === "TransportError") {
|
|
1797
|
+
if (isTimeoutCause(reason.cause))
|
|
1798
|
+
return false;
|
|
1799
|
+
return isPreSendCause(reason.cause);
|
|
1800
|
+
}
|
|
1801
|
+
return false;
|
|
1802
|
+
};
|
|
1803
|
+
var describeTransport = (cause) => {
|
|
1804
|
+
if (Cause2.isTimeoutError(cause))
|
|
1805
|
+
return `timed out after ${String(MEMBER_TIMEOUT)}`;
|
|
1806
|
+
if (HttpClientError.isHttpClientError(cause))
|
|
1807
|
+
return cause.message;
|
|
1808
|
+
return String(cause);
|
|
1809
|
+
};
|
|
1810
|
+
var executeOnCluster = (target, method, path, build, timeout4 = MEMBER_TIMEOUT) => Effect6.gen(function* () {
|
|
1811
|
+
const client = yield* HttpClient5.HttpClient;
|
|
1812
|
+
const isWrite = method !== "GET";
|
|
1813
|
+
const attempts = [];
|
|
1814
|
+
for (const member of orderedMembers(target)) {
|
|
1815
|
+
const outcome = yield* client.execute(build(pveApiBase(member))).pipe(Effect6.timeout(timeout4), Effect6.result);
|
|
1816
|
+
if (Result.isSuccess(outcome)) {
|
|
1817
|
+
noteGoodMember(target, member);
|
|
1818
|
+
return outcome.success;
|
|
1819
|
+
}
|
|
1820
|
+
const why = describeTransport(outcome.failure);
|
|
1821
|
+
attempts.push({ member, why });
|
|
1822
|
+
const mayRetry = isWrite ? isPreSendTransport(outcome.failure) : isTransportFailure(outcome.failure);
|
|
1823
|
+
if (!mayRetry) {
|
|
1824
|
+
return yield* Effect6.fail({ attempts, method, path, tag: "cluster" });
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
return yield* Effect6.fail({ attempts, method, path, tag: "cluster" });
|
|
1828
|
+
});
|
|
1829
|
+
|
|
1830
|
+
// src/proxmox/distilled-pve.ts
|
|
1831
|
+
class PveClusterExhausted extends Error {
|
|
1832
|
+
attempts;
|
|
1833
|
+
constructor(attempts) {
|
|
1834
|
+
super(`all members failed: ${attempts.map((a) => `${a.member} (${a.why})`).join("; ")}`);
|
|
1835
|
+
this.attempts = attempts;
|
|
1836
|
+
this.name = "PveClusterExhausted";
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
var runPveWith = (target, credential, isWrite, op, timeout5 = MEMBER_TIMEOUT) => Effect7.gen(function* () {
|
|
1840
|
+
const attempts = [];
|
|
1841
|
+
for (const member of orderedMembers(target)) {
|
|
1842
|
+
const credLayer = distilledPveCredentials({
|
|
1843
|
+
baseUrl: pveApiBase(member),
|
|
1844
|
+
secret: credential.secret,
|
|
1845
|
+
tokenId: credential.tokenId
|
|
1846
|
+
});
|
|
1847
|
+
const outcome = yield* op.pipe(ProxmoxRetry.none, Effect7.provide(credLayer), Effect7.timeout(timeout5), Effect7.result);
|
|
1848
|
+
if (Result2.isSuccess(outcome)) {
|
|
1849
|
+
noteGoodMember(target, member);
|
|
1850
|
+
return outcome.success;
|
|
1851
|
+
}
|
|
1852
|
+
const mayRetry = isWrite ? isPreSendTransport(outcome.failure) : isTransportFailure(outcome.failure);
|
|
1853
|
+
if (!mayRetry)
|
|
1854
|
+
return yield* Effect7.fail(outcome.failure);
|
|
1855
|
+
attempts.push({ member, why: describeTransport(outcome.failure) });
|
|
1856
|
+
}
|
|
1857
|
+
return yield* Effect7.fail(new PveClusterExhausted(attempts));
|
|
1858
|
+
});
|
|
1859
|
+
var runPve = (target, role, isWrite, op) => Effect7.gen(function* () {
|
|
1860
|
+
const credential = yield* leased(target, role);
|
|
1861
|
+
return yield* runPveWith(target, credential, isWrite, op);
|
|
1862
|
+
});
|
|
1863
|
+
|
|
1864
|
+
// src/proxmox/unreadable-read.ts
|
|
1865
|
+
import * as Effect8 from "effect/Effect";
|
|
1866
|
+
var UNREADABLE = Symbol("pve-unreadable");
|
|
1867
|
+
var readOrUnreadable = (effect2) => effect2.pipe(Effect8.catchTag("PveCredentialDenied", () => Effect8.succeed(UNREADABLE)));
|
|
1868
|
+
var unreadableWarning = (type, identity, mount, tier) => Effect8.logWarning(`${type} ${identity}: the read of ${mount}/creds/${tier} was refused (no grant for this ` + "credential's role) -- reporting noop rather than forcing an update with nothing compared. " + "Re-plan on a lane that can read this object before trusting this row.");
|
|
1869
|
+
|
|
1870
|
+
// src/ceph/ceph-config-option-form.ts
|
|
1871
|
+
var configOptionProblem = (props) => {
|
|
1872
|
+
if (props.monNodes.length === 0)
|
|
1873
|
+
return "monNodes must name at least one mon ssh destination";
|
|
1874
|
+
const problem = cephCommandProblem(configSetArgv(props.who, props.name, props.value));
|
|
1875
|
+
return problem === undefined ? undefined : `Ceph.ConfigOption ${props.who}/${props.name}: ${problem}`;
|
|
1876
|
+
};
|
|
1877
|
+
var findLive = (rows, who, name) => {
|
|
1878
|
+
const row = rows.find((r) => r.section === who && r.name === name && (r.mask ?? "") === "");
|
|
1879
|
+
return row === undefined ? undefined : { name, value: String(row.value), who };
|
|
1880
|
+
};
|
|
1881
|
+
var matches = (live, props) => live !== undefined && live.value === props.value;
|
|
1882
|
+
|
|
1883
|
+
// src/ceph/ceph-config-option-reconcile.ts
|
|
1884
|
+
import * as Effect9 from "effect/Effect";
|
|
1885
|
+
var asError2 = (cause) => cause instanceof Error ? cause : new Error(String(cause));
|
|
1886
|
+
var runWrite = (transport, argv) => Effect9.gen(function* () {
|
|
1887
|
+
const result3 = yield* Effect9.tryPromise({
|
|
1888
|
+
catch: asError2,
|
|
1889
|
+
try: () => runCephCommand(transport, argv)
|
|
1890
|
+
});
|
|
1891
|
+
if (result3.exitCode !== 0) {
|
|
1892
|
+
return yield* Effect9.fail(new Error(`ceph ${argv.slice(1, 3).join(" ")} failed on mon ${result3.node} (exit ` + `${String(result3.exitCode)}): ${result3.stderr.trim().slice(0, 300)}`));
|
|
1893
|
+
}
|
|
1894
|
+
yield* Effect9.tryPromise({ catch: asError2, try: () => assertFreshQuorum(transport) });
|
|
1895
|
+
});
|
|
1896
|
+
var reconcileCephConfigOption = (props, readRows, deps) => Effect9.gen(function* () {
|
|
1897
|
+
const problem = configOptionProblem(props);
|
|
1898
|
+
if (problem !== undefined)
|
|
1899
|
+
return yield* Effect9.fail(new Error(problem));
|
|
1900
|
+
const live = findLive(yield* readRows, props.who, props.name);
|
|
1901
|
+
if (live !== undefined && matches(live, props))
|
|
1902
|
+
return live;
|
|
1903
|
+
yield* runWrite({ nodes: props.monNodes, ...deps }, configSetArgv(props.who, props.name, props.value));
|
|
1904
|
+
const after = findLive(yield* readRows, props.who, props.name);
|
|
1905
|
+
if (after === undefined || !matches(after, props)) {
|
|
1906
|
+
return yield* Effect9.fail(new Error(`Ceph.ConfigOption ${props.who}/${props.name}: 'ceph config set' succeeded but the mon ` + `config db reports ${after === undefined ? "no value" : JSON.stringify(after.value)}, ` + `not ${JSON.stringify(props.value)}. Not recorded — check \`ceph config dump\`.`));
|
|
1907
|
+
}
|
|
1908
|
+
return after;
|
|
1909
|
+
});
|
|
1910
|
+
var deleteCephConfigOption = (props, readRows, deps) => Effect9.gen(function* () {
|
|
1911
|
+
const live = findLive(yield* readRows, props.who, props.name);
|
|
1912
|
+
if (live === undefined)
|
|
1913
|
+
return;
|
|
1914
|
+
yield* runWrite({ nodes: props.monNodes, ...deps }, configRmArgv(props.who, props.name));
|
|
1915
|
+
});
|
|
1916
|
+
|
|
1917
|
+
// src/ceph/config-option.ts
|
|
1918
|
+
var CephConfigOption = Resource2("Ceph.ConfigOption", {
|
|
1919
|
+
defaultRemovalPolicy: "retain"
|
|
1920
|
+
});
|
|
1921
|
+
var readRows = (props) => runPve(props.target, "read", false, nodes.listNodeCephCfgDb({ node: props.node }));
|
|
1922
|
+
var refuse = (props) => {
|
|
1923
|
+
const problem = configOptionProblem(props);
|
|
1924
|
+
return problem === undefined ? Effect10.void : Effect10.fail(new Error(problem));
|
|
1925
|
+
};
|
|
1926
|
+
var CephConfigOptionProvider = () => Provider2.effect(CephConfigOption, Effect10.succeed(CephConfigOption.Provider.of({
|
|
1927
|
+
list: () => Effect10.succeed([]),
|
|
1928
|
+
read: Effect10.fn(function* ({ olds, output }) {
|
|
1929
|
+
const live = findLive(yield* readRows(olds), olds.who, olds.name);
|
|
1930
|
+
if (live === undefined)
|
|
1931
|
+
return;
|
|
1932
|
+
return output !== undefined ? live : Unowned(live);
|
|
1933
|
+
}),
|
|
1934
|
+
diff: Effect10.fn(function* ({ news, output }) {
|
|
1935
|
+
if (!isResolved2(news))
|
|
1936
|
+
return;
|
|
1937
|
+
yield* refuse(news);
|
|
1938
|
+
if (output === undefined)
|
|
1939
|
+
return;
|
|
1940
|
+
if (output.who !== news.who || output.name !== news.name)
|
|
1941
|
+
return { action: "replace" };
|
|
1942
|
+
const rows = yield* readOrUnreadable(readRows(news));
|
|
1943
|
+
if (rows === UNREADABLE) {
|
|
1944
|
+
yield* unreadableWarning("Ceph.ConfigOption", `${news.who}/${news.name}`, news.target.mount, mintTier(news.target, "read"));
|
|
1945
|
+
return { action: "noop" };
|
|
1946
|
+
}
|
|
1947
|
+
const live = findLive(rows, news.who, news.name);
|
|
1948
|
+
return { action: matches(live, news) ? "noop" : "update" };
|
|
1949
|
+
}),
|
|
1950
|
+
reconcile: ({ news }) => reconcileCephConfigOption(news, readRows(news), {}),
|
|
1951
|
+
delete: ({ olds }) => deleteCephConfigOption(olds, readRows(olds), {})
|
|
1952
|
+
})));
|
|
1601
1953
|
export {
|
|
1602
1954
|
CephAuthEntity,
|
|
1603
1955
|
CephAuthEntityProvider,
|
|
1956
|
+
CephConfigOption,
|
|
1957
|
+
CephConfigOptionProvider,
|
|
1958
|
+
NAMED_CONFIG_OPTIONS,
|
|
1604
1959
|
assertFreshQuorum,
|
|
1605
1960
|
runCephCommand,
|
|
1606
1961
|
sshCephDial
|
|
1607
1962
|
};
|
|
1608
1963
|
|
|
1609
|
-
//# debugId=
|
|
1964
|
+
//# debugId=6A14FAFFC2879E5164756E2164756E21
|
|
1610
1965
|
//# sourceMappingURL=index.js.map
|