diff --git a/SYSTEMD.md b/SYSTEMD.md index fa9f6b7..a17e6bc 100644 --- a/SYSTEMD.md +++ b/SYSTEMD.md @@ -12,7 +12,40 @@ step, so after a `git pull` you only need to restart the services. If you are upgrading from a version whose units pointed at `run.py`, re-copy the unit files below before restarting. -## Installation +## Quick install (recommended) + +The shipped unit files contain the author's user and paths (`User=cort`, +`/home/cort/hblink4`), so they will not work as-is on your system. This script +asks for your service user and group, fills in your paths, installs both units +and reloads systemd: + +```bash +sudo ./scripts/install_services.sh +``` + +It offers sensible defaults (the user who invoked `sudo`, and that user's +primary group), backs up any units you already have, checks that your venv +exists and that the service user can write to the installation directory, and +optionally enables and starts the services. + +To see what it would produce without touching the live system: + +```bash +sudo DESTDIR=/tmp/preview ./scripts/install_services.sh +``` + +The manual steps below do the same thing by hand. + +## Manual installation + +> **⚠️ The shipped unit files contain the author's user and paths** — +> `User=cort`, `Group=cort`, and `/home/cort/hblink4` throughout. **They will not +> work as-is on your system.** You must change the user, the group, and every +> path to match your installation. +> +> Edit the *installed* copies in `/etc/systemd/system/`, not the ones in the +> repository — editing the repo copies makes them show up as local changes and +> conflict on the next `git pull`. ### 1. Install the service files @@ -23,15 +56,39 @@ sudo cp hblink4.service /etc/systemd/system/ sudo cp hblink4-dash.service /etc/systemd/system/ ``` -### 2. Reload systemd +### 2. Set your user and paths -Tell systemd to reload its configuration: +Four settings in each file need to match your system: `User=`, `Group=`, +`WorkingDirectory=`, and `ExecStart=`. Edit them with your editor of choice: + +```bash +sudo nano /etc/systemd/system/hblink4.service +sudo nano /etc/systemd/system/hblink4-dash.service +``` + +Or, run this from your HBlink4 directory to substitute them automatically: + +```bash +sudo sed -i "s|/home/cort/hblink4|$PWD|g; s|^User=cort$|User=$USER|; s|^Group=cort$|Group=$(id -gn)|" \ + /etc/systemd/system/hblink4.service /etc/systemd/system/hblink4-dash.service +``` + +Check the result before continuing — no `cort` or `/home/cort` should remain: + +```bash +grep -nE "^(User|Group|WorkingDirectory|ExecStart)=" /etc/systemd/system/hblink4*.service +``` + +### 3. Reload systemd + +Tell systemd to reload its configuration. **This is required after any edit to a +unit file** — systemd will keep using the old version until you do: ```bash sudo systemctl daemon-reload ``` -### 3. Enable services (optional) +### 4. Enable services (optional) To start the services automatically at boot: @@ -40,7 +97,7 @@ sudo systemctl enable hblink4 sudo systemctl enable hblink4-dash ``` -### 4. Start the services +### 5. Start the services ```bash sudo systemctl start hblink4 @@ -92,22 +149,26 @@ sudo systemctl disable hblink4-dash ## Configuration -The service files are configured to: -- Run as user `cort` in group `cort` -- Use the Python virtual environment at `/home/cort/hblink4/venv` +As shipped, the service files: +- Run as user `cort` in group `cort` — **change this to your own user and group** +- Use the Python virtual environment at `/home/cort/hblink4/venv` — **change to your path** - Automatically restart on failure (after 10 seconds) - Log to systemd journal (view with `journalctl`) - Start after network is available - Dashboard starts after and depends on HBlink4 -### Customization +### Changing a unit file later -If you need to modify the services (different user, paths, etc.), edit the files before copying them: +Always edit the copy in `/etc/systemd/system/`, then reload and restart: -1. Edit `hblink4.service` and/or `hblink4-dash.service` -2. Change `User=`, `Group=`, `WorkingDirectory=`, or `ExecStart=` as needed -3. Copy to `/etc/systemd/system/` -4. Run `sudo systemctl daemon-reload` +```bash +sudo nano /etc/systemd/system/hblink4.service +sudo systemctl daemon-reload # required, or systemd keeps the old version +sudo systemctl restart hblink4 +``` + +Editing the copy in the repository has no effect on a running service, and +leaves you with local changes that conflict on the next `git pull`. ## Security Features diff --git a/hblink4-dash.service b/hblink4-dash.service index 6da3c34..152a9d0 100644 --- a/hblink4-dash.service +++ b/hblink4-dash.service @@ -1,3 +1,7 @@ +# NOTE: this file ships with the author's user and install paths. Change User=, +# Group=, WorkingDirectory= and ExecStart= below to match your system. +# Edit the installed copy in /etc/systemd/system/, not this one, and run +# "sudo systemctl daemon-reload" after any change. See SYSTEMD.md. [Unit] Description=HBlink4 Dashboard After=network.target hblink4.service diff --git a/hblink4.service b/hblink4.service index 0ce7131..2d1efc4 100644 --- a/hblink4.service +++ b/hblink4.service @@ -1,3 +1,7 @@ +# NOTE: this file ships with the author's user and install paths. Change User=, +# Group=, WorkingDirectory= and ExecStart= below to match your system. +# Edit the installed copy in /etc/systemd/system/, not this one, and run +# "sudo systemctl daemon-reload" after any change. See SYSTEMD.md. [Unit] Description=HBlink4 DMR Server After=network.target diff --git a/readme.md b/readme.md index e0a0726..257db4a 100644 --- a/readme.md +++ b/readme.md @@ -74,7 +74,12 @@ See the [Configuration Guide](docs/configuration.md) for complete documentation ## Running ### Production (systemd services) -For production deployments with automatic startup, see [SYSTEMD.md](SYSTEMD.md). +```bash +sudo ./scripts/install_services.sh +``` +Asks for your service user/group, fills in your paths, installs both units and +reloads systemd. See [SYSTEMD.md](SYSTEMD.md) for manual installation and +service management. ### Development ```bash diff --git a/scripts/README.md b/scripts/README.md index de4bf51..07f2216 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -1,5 +1,29 @@ # HBlink4 Scripts +## install_services.sh + +Installs the systemd unit files with your user, group and paths filled in — the +shipped units carry the author's (`User=cort`, `/home/cort/hblink4`) and will not +work unmodified. + +```bash +sudo ./scripts/install_services.sh +``` + +Prompts for the service user and group (defaulting to whoever ran `sudo` and +that user's primary group), rewrites both units, installs them to +`/etc/systemd/system/`, runs `systemctl daemon-reload`, and optionally enables +and starts the services. Any existing units are backed up as +`.bak-` first. + +Preview the result without touching the live system: + +```bash +sudo DESTDIR=/tmp/preview ./scripts/install_services.sh +``` + +See [SYSTEMD.md](../SYSTEMD.md) for manual installation and service management. + ## filter_user_csv.py Filters the DMR user database to only include US and Canada entries, reducing file size and memory usage. diff --git a/scripts/install_services.sh b/scripts/install_services.sh new file mode 100755 index 0000000..cd0e9b4 --- /dev/null +++ b/scripts/install_services.sh @@ -0,0 +1,157 @@ +#!/bin/bash +# +# Install the HBlink4 systemd units, with your user, group and paths filled in. +# +# sudo ./scripts/install_services.sh +# +# Prompts for the service user and group (sensible defaults are offered), +# rewrites the shipped unit files accordingly, installs them to +# /etc/systemd/system/, and reloads systemd. Existing units are backed up first. +# +# Set DESTDIR to install somewhere else (useful for inspecting the result +# without touching the live system): +# +# sudo DESTDIR=/tmp/preview ./scripts/install_services.sh +# + +set -euo pipefail + +RED='\033[0;31m'; GREEN='\033[0;32m'; BLUE='\033[0;34m'; YELLOW='\033[1;33m'; NC='\033[0m' + +die() { echo -e "${RED}✗ $*${NC}" >&2; exit 1; } +note() { echo -e "${BLUE}$*${NC}"; } +ok() { echo -e "${GREEN}✓ $*${NC}"; } +warn() { echo -e "${YELLOW}⚠ $*${NC}"; } + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +UNIT_DIR="${DESTDIR:-/etc/systemd/system}" +UNITS=(hblink4.service hblink4-dash.service) +LIVE_INSTALL=0 +[ -z "${DESTDIR:-}" ] && LIVE_INSTALL=1 + +if [ "$LIVE_INSTALL" = 1 ] && [ "$(id -u)" -ne 0 ]; then + die "This script must be run with sudo: sudo $0" +fi + +# ---------------------------------------------------------------- sanity checks + +for u in "${UNITS[@]}"; do + [ -f "$REPO_ROOT/$u" ] || die "Missing $REPO_ROOT/$u -- run this from the HBlink4 checkout." +done + +[ -x "$REPO_ROOT/venv/bin/python" ] || die \ + "No virtual environment at $REPO_ROOT/venv + Create it first: + python3 -m venv venv && source venv/bin/activate && pip install -r requirements.txt" + +[ -f "$REPO_ROOT/config/config.json" ] || warn \ + "No config/config.json yet -- copy config/config_sample.json and edit it before starting." + +echo +note "HBlink4 service installer" +echo " Install directory : $REPO_ROOT" +echo " Unit directory : $UNIT_DIR" +echo + +# ------------------------------------------------------------------- user/group + +# Default to whoever invoked sudo, falling back to the directory's owner -- +# HBlink4 must run as a user that can write logs/ and dashboard/data/. +DEFAULT_USER="${SUDO_USER:-$(stat -c '%U' "$REPO_ROOT")}" +DEFAULT_GROUP="$(id -gn "$DEFAULT_USER" 2>/dev/null || echo "$DEFAULT_USER")" + +read -r -p "Service user [$DEFAULT_USER]: " SVC_USER +SVC_USER="${SVC_USER:-$DEFAULT_USER}" +read -r -p "Service group [$DEFAULT_GROUP]: " SVC_GROUP +SVC_GROUP="${SVC_GROUP:-$DEFAULT_GROUP}" + +id "$SVC_USER" >/dev/null 2>&1 || die "No such user: $SVC_USER" +getent group "$SVC_GROUP" >/dev/null 2>&1 || die "No such group: $SVC_GROUP" + +# The service user must be able to write persistence and logs. +if [ "$(id -u)" -eq 0 ] && ! runuser -u "$SVC_USER" -- test -w "$REPO_ROOT" 2>/dev/null; then + warn "$SVC_USER cannot write to $REPO_ROOT" + warn "The dashboard needs write access for dashboard/data/ and logs/." +fi + +echo +note "Will install as:" +echo " User=$SVC_USER" +echo " Group=$SVC_GROUP" +echo " WorkingDirectory=$REPO_ROOT" +echo " ExecStart=$REPO_ROOT/venv/bin/python $REPO_ROOT/hblink4/hblink.py $REPO_ROOT/config/config.json" +echo " ExecStart=$REPO_ROOT/venv/bin/python $REPO_ROOT/dashboard/server.py" +echo +read -r -p "Proceed? [y/N]: " CONFIRM +case "$CONFIRM" in [yY]|[yY][eE][sS]) ;; *) echo "Aborted."; exit 0 ;; esac + +# ---------------------------------------------------------------------- install + +mkdir -p "$UNIT_DIR" +STAMP="$(date +%Y%m%d-%H%M%S)" + +for u in "${UNITS[@]}"; do + if [ -f "$UNIT_DIR/$u" ]; then + cp -p "$UNIT_DIR/$u" "$UNIT_DIR/$u.bak-$STAMP" + echo " backed up existing $u -> $u.bak-$STAMP" + fi + + # Substitute only the shipped author values; anchored so nothing else matches. + sed -e "s|/home/cort/hblink4|$REPO_ROOT|g" \ + -e "s|^User=cort$|User=$SVC_USER|" \ + -e "s|^Group=cort$|Group=$SVC_GROUP|" \ + "$REPO_ROOT/$u" > "$UNIT_DIR/$u" + chmod 644 "$UNIT_DIR/$u" + ok "installed $UNIT_DIR/$u" +done + +# Confirm the substitution produced what we intended, rather than leaving a +# half-rewritten unit in place. (Checked against the chosen values, not against +# the author's literal strings -- those are legitimate if you happen to use the +# same username or path.) +for u in "${UNITS[@]}"; do + grep -q "^User=$SVC_USER$" "$UNIT_DIR/$u" || die "$u: User= was not set correctly" + grep -q "^Group=$SVC_GROUP$" "$UNIT_DIR/$u" || die "$u: Group= was not set correctly" + grep -q "^WorkingDirectory=$REPO_ROOT$" "$UNIT_DIR/$u" || die "$u: WorkingDirectory= was not set correctly" + grep -q "^ExecStart=$REPO_ROOT/venv/bin/python " "$UNIT_DIR/$u" || die "$u: ExecStart= was not set correctly" +done + +if [ "$LIVE_INSTALL" = 0 ]; then + echo + ok "Wrote units to $UNIT_DIR (DESTDIR set -- systemd not touched)." + exit 0 +fi + +systemctl daemon-reload +ok "systemctl daemon-reload" + +# ------------------------------------------------------------- enable and start + +echo +read -r -p "Enable services at boot? [y/N]: " DO_ENABLE +case "$DO_ENABLE" in + [yY]|[yY][eE][sS]) systemctl enable "${UNITS[@]}" >/dev/null && ok "enabled at boot" ;; +esac + +read -r -p "Start (or restart) the services now? [y/N]: " DO_START +case "$DO_START" in + [yY]|[yY][eE][sS]) + systemctl restart "${UNITS[@]}" + sleep 2 + echo + for u in "${UNITS[@]}"; do + if systemctl is-active --quiet "$u"; then + ok "$u is running" + else + warn "$u did not start -- journalctl -u ${u%.service} -n 30" + fi + done + ;; +esac + +echo +note "Done. Useful commands:" +echo " systemctl status hblink4 hblink4-dash" +echo " journalctl -u hblink4 -f" +echo +note "If you edit a unit later, edit $UNIT_DIR/ and run 'systemctl daemon-reload'."