--- type: reference title: Service Inventory description: shared/services.yml is the one inventory of deployable services; scripts/service-map.py is its only reader. The planner, both deploy allow-lists, the changed-test seam mapper, the Pi-hole hosts render, the Prometheus scrape facts, and the homepage probes all derive from it. tags: [deployment, inventory, planner, quadlet, prometheus, pihole, homepage] timestamp: 2026-10-13T00:00:00-07:00 --- # Service Inventory Before 2026-10-13 the same facts lived in four hand-maintained lists — the planner's path rules (`deploy-plan.py`), the `--service` allow-list in `deploy.sh`, the near-copy in `deploy-remote.sh`, and the changed-test mapper (`test-changed.sh`) — plus per-fact copies in `prometheus.yml`, `addn-hosts`, and `services.json`. They drifted on every swap (the planner still mapped the retired `qwen-sharp` template to frank-dragoon; the allow-list still listed the *arr stack under Dankserver after it moved to FrankBook-Pro). Now there is **one inventory and one reader**: - `shared/services.yml` — the data: every deployable service, its hosts, unit dir(s), unit files, path claims, tests, scrape/DNS/probe facts, aliases. - `scripts/service-map.py` — the only reader. CLI subcommands: `hosts`, `services`, `allows`, `plan`, `seams`, `owner`, `units`, `stack`, `get`, `render`, `check`. ## 1. Consumers |Consumer|Call| |---|---| |`scripts/deploy-plan.py`|thin wrapper → `plan --host TARGET [paths...]` (keeps the `--repo/--base/--head` and `--paths` CLIs)| |`scripts/deploy.sh` (`--service` gate)|`allows --host T --service S`| |`scripts/deploy-remote.sh` (`service_is_allowed`)|same| |`scripts/test-changed.sh`|`seams [paths...]` — a service's declared `tests` are its seams| |`dankclaw/pihole/addn-hosts`|`render pihole-hosts` — the tracked file is a rendered artifact| |`*/observability/quadlet/node-exporter.container`|`render node-exporter-unit --host H` — the MIRROR pair became one template; per-host title, LAN address (hosts map), and port (scrape job) fill it| |Prometheus scrape jobs, LAN DNS, homepage probes|`check` compares the tracked configs against the inventory's `scrape`/`dns`/`probe` fields (drift gate)| Selection rules the engine encodes (see `tests/fixtures/service-plan-golden.tsv`, the pinned characterization of every tracked path × every target): - **Host scoping** — a host's top-level directory can only select that host's services. `frankbook-pro/**` plans nothing on Dankserver (the old planner had a stale Dankserver *arr list). - **Quadlet coupling graph** — `Network=x.container` rides x's netns; `Network=y.network` joins a shared network. Changing the parent (or the network file) selects every rider transitively, in their own directories (`dankclaw/searxng/quadlet/gluetun.container` → gluetun + searxng + marreta; `frankbook-pro/gluetun/quadlet/gluetun.container` → the whole arr stack). - **Stacks** — a service's `stack` members deploy and restart as one handler (romm trio, hermes trio). - **Retirement** — removing a service from `services.yml` removes it from the planner, both allow-lists, and the seam mapper in one data edit; a pending deploy-state entry for it is auto-triaged (see [[Deploy State Triage]]). ## 2. Integrity gates (`check`) `python3 scripts/service-map.py check` runs, and `tests/service_inventory.bats` pins: - **schema** — required fields per entry, known keys only; - **units** — every unit file named by an entry exists under its `unit_dir` on the right host; each unit name is claimed by exactly one service per host; - **ownership** — every tracked quadlet `.container`/`.network` file is claimed by exactly one service (trash excluded); - **paths** — every claimed `files`/`dirs`/`retires` glob matches ≥1 tracked file; - **compose** — the container names in each host's `compose.yaml` have exactly one quadlet owner (the [[Quadlet Lifecycle]] two-owners trap); - **drift** — `addn-hosts` equals its render, each tracked node-exporter unit equals its per-host render; the Prometheus job set, the DNS names, and the homepage probe ports equal the inventory's fields. ## 3. Adding, moving, retiring a service - **Add**: append an entry (name, hosts, kind, unit_dir, units, paths, tests, …), add the quadlet file, run `check` — `plan`/`allows`/`seams` pick it up with zero script edits. - **Move between hosts**: edit `hosts` (and per-host `unit_dir`/`probe` maps). - **Retire**: delete the entry, move files to `.AGENTTRASH/` (see [[Testing Policy]] for the seam the mapper applies to trash paths). Field notes: `alias` carries deployable names that differ from the service name (`calibre-web` deploys the unit `calibre-server`); `probe` for multi-host services is a `{host: port}` map; `dependents` names services whose config embeds a reference to this one (config-only coupling — a `.env` edit selects both, a unit edit selects the graph closure). ## 4. Known cutover deltas (2026-10-13) The 17 deltas against the old planner are all in these classes: anubis paths now select `anubis` (the old planner had no rule at all); cross-host paths select nothing; `scripts/qwen-sharp-v22.4.0.jinja` (retired template) plans nothing while `scripts/qwen38-froggeric-v22.5.jinja` (the live frank-archon template, previously unmapped) plans frank-archon; `dankserver/romm/romm.container` (top-level quadlet dir the old unit-file rule missed) now plans the romm trio. --- Related: [[Deployment Flow]], [[Testing Policy]], [[Quadlet Lifecycle]], [[Container Network Namespaces]], [[Deploy State Triage]]