diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md
index e77a8bb..f370b4f 100644
--- a/docs/architecture/overview.md
+++ b/docs/architecture/overview.md
@@ -217,7 +217,7 @@ flowchart LR
end
subgraph etc["/etc/hispec"]
- ENV["instances/<name>.env
HISPEC_DAEMON, HISPEC_CONFIG"]
+ ENV["instances/<name>.env
HISPEC_DAEMON, HISPEC_HOST"]
YML["<name>.yaml
deployed config"]
end
@@ -231,8 +231,9 @@ flowchart LR
One systemd template unit, `hispec@.service`, runs every daemon. An
instance `hispec@` reads `/etc/hispec/instances/.env`, which
-names the daemon script (`HISPEC_DAEMON`, relative to `daemons/`) and its config
-(`HISPEC_CONFIG`). Running a second unit of the same hardware model — two
+names the daemon script (`HISPEC_DAEMON`, relative to `daemons/`). Its config
+is `/etc/hispec/.yaml`, derived from the instance name rather than
+declared. Running a second unit of the same hardware model, two
Lakeshores, five filter wheels — is two env files pointing at the same script
with different configs.
diff --git a/docs/operations/systemd.md b/docs/operations/systemd.md
index 293cc3e..32efa28 100644
--- a/docs/operations/systemd.md
+++ b/docs/operations/systemd.md
@@ -36,7 +36,7 @@ An instance gets its identity from two files:
| File | What it says |
|---|---|
-| `/etc/hispec/instances/.env` | which daemon script to run (`HISPEC_DAEMON`) and which config to hand it (`HISPEC_CONFIG`) |
+| `/etc/hispec/instances/.env` | which daemon script to run (`HISPEC_DAEMON`) and which host should run it (`HISPEC_HOST`) |
| `/etc/hispec/.yaml` | the config itself: serial ports, addresses, limits, keyword names |
So running a second Lakeshore is not new code and not a new unit. It is a
@@ -239,7 +239,6 @@ matching `systemd/instances/.env`:
```sh
HISPEC_DAEMON=
-HISPEC_CONFIG=/etc/hispec/.yaml
HISPEC_HOST=
```
@@ -368,10 +367,11 @@ Common causes, in the order they bite:
- **`Failed to load environment files`**: `/etc/hispec/instances/.env`
is missing or unreadable. The instance name in the command must match the
file name exactly.
-- **`No such file or directory`** on the config: `HISPEC_CONFIG` points
- somewhere that does not exist. The config has to be deployed to `/etc/hispec/`
- as well as the `.env`. `hispec deploy ` copies both; copying one by
- hand and forgetting the other is the usual mistake.
+- **`No such file or directory`** on the config: the unit passes
+ `/etc/hispec/.yaml`, derived from the instance name, and nothing is
+ there. The config has to be deployed as well as the `.env`. `hispec deploy
+ ` copies both; copying one by hand and forgetting the other is the
+ usual mistake.
- **A serial port or USB error**: the device is unplugged, powered off, or
claimed by another process. Two instances pointing at the same port will do
this to each other, and so will a daemon left running from a manual test.
@@ -505,12 +505,17 @@ sudo ./systemd/install.sh alice bob # operator usernames
`install.sh` is idempotent. Re-run it:
- to enrol more operators (`sudo ./systemd/install.sh carol`);
-- after a pull that changed dependencies in `pyproject.toml`, or
- `hispec@.service`, the polkit rule or `hispec-enable` under `systemd/`;
+- after a pull that changed dependencies in `pyproject.toml`, or the unit or
+ polkit rule in `instrumentctl`;
- to repair a host `hispec doctor` says is broken.
You do **not** need it to add a daemon or to pick up code changes;
-[`hispec deploy`](#deploying-an-instance) and a `git pull` cover those. It:
+[`hispec deploy`](#deploying-an-instance) and a `git pull` cover those.
+
+The script itself only bootstraps a pinned `uv` and builds the venv at
+`/opt/hispec/venv`, which is also where the `hispec` command comes from. It
+then hands over to `hispec install`, which lives in `instrumentctl` and does
+the rest:
- creates the `hispec-ops` group and the unprivileged `hispec` system user the
daemons run as;
@@ -518,17 +523,17 @@ You do **not** need it to add a daemon or to pick up code changes;
- creates `/etc/hispec/{,instances/}` and `/var/log/hispec/`, group-owned by
`hispec-ops` and setgid so files created by one operator stay readable by the
rest;
-- creates `/etc/hispec/secrets.env`, writable by `hispec-ops`;
-- creates the venv at `/opt/hispec/venv` with the repo `pip install -e`'d into
- it, which also provides the `hispec` CLI;
-- installs `hispec@.service` (then runs `daemon-reload`), the polkit rule, the
- sudoers drop-in, `/usr/local/bin/hispec` and `/usr/local/sbin/hispec-enable`;
-- removes the retired `hispec-fei-start`, `hispec-fei-stop` and `hispec-doctor`
- scripts (now `hispec start hsfei`, `hispec stop hsfei` and `hispec doctor`);
-- migrates any instance still running under the old `hispec-daemon@` name,
- preserving whether it was enabled and whether it was up;
+- creates `/etc/hispec/secrets.env`, writable by `hispec-ops`, and records this
+ host's role in `/etc/hispec/host`;
+- renders `hispec@.service` (then runs `daemon-reload`), the polkit rule, the
+ sudoers drop-in, `/usr/local/bin/hispec` and `/usr/local/sbin/hispec-enable`
+ from `src/hispec/cli/instrument.toml`, reporting only what it changes;
- lists instances that are deployed but not set to start at boot.
+Because those five are rendered rather than committed, `hispec doctor`
+compares what is installed against what `install` would write now, which is
+what catches a host whose unit predates a template change.
+
`HISPEC_REPO_DIR` and `HISPEC_VENV_DIR` override the paths.
`/usr/local/bin/hispec` is a tiny wrapper that runs the CLI from the venv. The
@@ -552,7 +557,8 @@ and `/var/log/hispec`; device nodes under `/dev` are unaffected by both.
Starting and stopping a unit is the polkit action
`org.freedesktop.systemd1.manage-units`, and systemd tells polkit which unit is
-involved, so `systemd/polkit/49-hispec.rules` can allow it for `hispec@*` and
+involved, so `/etc/polkit-1/rules.d/49-hispec.rules` can allow it for
+`hispec@*` and
nothing else. Enabling and disabling is
`org.freedesktop.systemd1.manage-unit-files`, and systemd does **not** pass a
unit name with it. A polkit rule for it would grant `hispec-ops` enable/disable
diff --git a/pyproject.toml b/pyproject.toml
index 025e753..36e29d1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -33,7 +33,7 @@ dependencies = [
# influxdb extra: needed by daemons/generic/keygrabber
"libby[influxdb]@git+https://github.com/CaltechOpticalObservatories/libby.git",
"hardware_device_base@git+https://github.com/COO-Utilities/hardware_device_base",
- "instrumentctl@git+https://github.com/CaltechOpticalObservatories/instrumentctl.git",
+ "instrumentctl@git+https://github.com/CaltechOpticalObservatories/instrumentctl.git@v0.2.0",
"camera_daemon@git+https://github.com/COO-Utilities/camera-daemon.git"
]
diff --git a/src/hispec/cli/instrument.toml b/src/hispec/cli/instrument.toml
index d656d31..2729b67 100644
--- a/src/hispec/cli/instrument.toml
+++ b/src/hispec/cli/instrument.toml
@@ -10,9 +10,10 @@
[instrument]
name = "hispec"
+display_name = "HISPEC"
user = "hispec"
ops_group = "hispec-ops"
-# Instance files set HISPEC_DAEMON, HISPEC_CONFIG and HISPEC_HOST
+# Instance files set HISPEC_DAEMON and HISPEC_HOST
env_prefix = "HISPEC"
docs_url = "https://caltechopticalobservatories.github.io/hispec/operations/systemd.html"
diff --git a/systemd/README.md b/systemd/README.md
index 73d1e3d..79d2778 100644
--- a/systemd/README.md
+++ b/systemd/README.md
@@ -11,12 +11,25 @@ directory.
| Path | What it is |
| --- | --- |
-| `hispec@.service` | The template unit. One file runs every daemon; `hispec@` is an instance of it. Installed to `/etc/systemd/system/`. |
-| `instances/.env` | Per-instance settings: `HISPEC_DAEMON` (script, relative to `daemons/`) and `HISPEC_CONFIG` (deployed config path). Deployed to `/etc/hispec/instances/`. |
-| `polkit/49-hispec.rules` | Lets `hispec-ops` members start/stop/restart `hispec@*` without sudo. Installed to `/etc/polkit-1/rules.d/`. |
-| `bin/hispec` | Wrapper that runs the `hispec` CLI from the venv. Installed to `/usr/local/bin/`. |
-| `bin/hispec-enable` | Root helper behind `hispec enable` / `hispec deploy`: enables/disables instances at boot, which `systemctl enable` cannot be granted per-unit. Installed to `/usr/local/sbin/`, invoked via `sudo -n` by a NOPASSWD drop-in. |
-| `install.sh` | Host setup for admins / IT: users, groups, directories, venv, and all of the above. Idempotent, run as root. Not needed to add a daemon. |
+| `instances/.env` | Per-instance settings: `HISPEC_DAEMON` (script, relative to `daemons/`) and `HISPEC_HOST` (which host runs it). Deployed to `/etc/hispec/instances/`; the config path is derived as `/etc/hispec/.yaml`. |
+| `install.sh` | Host setup for admins / IT. Bootstraps uv, builds the venv, then hands over to `hispec install`. Idempotent, run as root. Not needed to add a daemon. |
+
+`hispec install` writes the rest, rendered from
+`src/hispec/cli/instrument.toml`:
+
+| Installed | What it is |
+| --- | --- |
+| `/etc/systemd/system/hispec@.service` | The template unit. One file runs every daemon; `hispec@` is an instance of it. |
+| `/etc/polkit-1/rules.d/49-hispec.rules` | Lets `hispec-ops` members start/stop/restart `hispec@*` without sudo. |
+| `/usr/local/bin/hispec` | Wrapper that runs the CLI from the venv. |
+| `/usr/local/sbin/hispec-enable` | Root helper behind `hispec enable` / `hispec deploy`: enables/disables instances at boot, which `systemctl enable` cannot be granted per-unit. Invoked via `sudo -n` by a NOPASSWD drop-in. |
+| `/etc/sudoers.d/hispec-ops` | That drop-in. |
+
+The templates live in
+[instrumentctl](https://github.com/CaltechOpticalObservatories/instrumentctl),
+so a change to the unit or the polkit rule is made there and reaches a host
+through `install.sh`. `hispec doctor` compares what is installed against what
+`install` would write now.
## Quick reference
diff --git a/systemd/bin/hispec b/systemd/bin/hispec
deleted file mode 100755
index 60e8383..0000000
--- a/systemd/bin/hispec
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/usr/bin/env bash
-# Front door for the hispec CLI, installed to /usr/local/bin.
-#
-# The CLI itself lives in the venv, installed editable from the repo, so a
-# `git pull` updates it with no reinstall. This wrapper only exists so that a
-# missing or broken venv produces an instruction rather than "command not
-# found", since `hispec doctor` is exactly what people run when things break.
-
-VENV_DIR=${HISPEC_VENV_DIR:-/opt/hispec/venv}
-REPO_DIR=${HISPEC_REPO_DIR:-/opt/hispec/app}
-
-if [[ ! -x $VENV_DIR/bin/hispec ]]; then
- echo "hispec: $VENV_DIR/bin/hispec is missing; the venv needs (re)installing." >&2
- echo " admin: sudo $REPO_DIR/systemd/install.sh" >&2
- exit 1
-fi
-exec "$VENV_DIR/bin/hispec" "$@"
diff --git a/systemd/bin/hispec-enable b/systemd/bin/hispec-enable
deleted file mode 100755
index fc08789..0000000
--- a/systemd/bin/hispec-enable
+++ /dev/null
@@ -1,60 +0,0 @@
-#!/usr/bin/env bash
-# Enable (or disable) hispec@ units at boot.
-#
-# `systemctl enable` needs the org.freedesktop.systemd1.manage-unit-files
-# polkit action, which polkit cannot scope to a single unit by name. Granting
-# it to hispec-ops would grant enable/disable on every unit on the host. This
-# helper is the narrow alternative: it runs as root via a sudoers drop-in that
-# is NOPASSWD for hispec-ops (see install.sh), and it only ever touches
-# hispec@ for a name that already has an instance file deployed.
-#
-# Operators normally reach it through `hispec enable` / `hispec deploy`, which
-# call it as `sudo -n hispec-enable ...`. It stays a standalone root-owned
-# script rather than part of the CLI because the CLI runs from the venv, which
-# the hispec user can write to, and root must not run code from there.
-#
-# hispec-enable hsfei_adc hsfei_ms # enable at boot (does not start)
-# hispec-enable --now hsfei_adc # enable and start
-# hispec-enable --disable hsfei_adc # disable at boot (does not stop)
-
-set -euo pipefail
-export PATH=/usr/sbin:/usr/bin:/sbin:/bin
-
-INSTANCE_DIR=/etc/hispec/instances
-
-verb=enable
-now=()
-
-usage() {
- awk 'NR>1 && /^#/ {sub(/^# ?/, ""); print; next} NR>1 {exit}' "$0"
- exit "${1:-0}"
-}
-
-while [[ $# -gt 0 ]]; do
- case "$1" in
- --disable) verb=disable; shift ;;
- --now) now=(--now); shift ;;
- -h|--help) usage 0 ;;
- --*) echo "hispec-enable: unknown option $1" >&2; usage 1 ;;
- *) break ;;
- esac
-done
-
-[[ $# -gt 0 ]] || usage 1
-
-# Validate before doing anything, so a typo in the third name does not leave
-# the first two half-applied.
-for name in "$@"; do
- if [[ ! $name =~ ^[a-z][a-z0-9_]*$ ]]; then
- echo "hispec-enable: refusing '$name': not a valid instance name" >&2
- exit 1
- fi
- if [[ $verb == enable && ! -f "$INSTANCE_DIR/$name.env" ]]; then
- echo "hispec-enable: no $INSTANCE_DIR/$name.env; deploy the instance file first" >&2
- exit 1
- fi
-done
-
-for name in "$@"; do
- systemctl "$verb" ${now[@]+"${now[@]}"} "hispec@$name.service"
-done
diff --git a/systemd/hispec@.service b/systemd/hispec@.service
deleted file mode 100644
index c1a96f6..0000000
--- a/systemd/hispec@.service
+++ /dev/null
@@ -1,63 +0,0 @@
-# HISPEC hardware daemon: systemd template unit.
-#
-# This is a template: one file manages any daemons//