From 71211a54519e13c9ba5ba928352fa4fef001240b Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Thu, 16 Apr 2026 08:41:34 +0300 Subject: Add unified upload client script under scripts/, update contrib and docs - scripts/goprecords-upload-client.sh: unified POSIX sh script working on all host types (FreeBSD f0-f3, Rocky Linux pi0-pi3, Fedora earth, OpenBSD blowfish/fishfinger). Key addition over the old contrib/ version: non-root support via XDG_CONFIG_HOME token path so user-session installs (earth) work without requiring a hardcoded /etc path. - contrib/goprecords-upload-client.sh: updated to match the canonical script in scripts/ (same content). - README.md: updated manual-upload section to reference scripts/ as canonical location, added host-class table, added user-session systemd example, and expanded environment variable table to show root vs non-root defaults. - Deployed new script to f0, f1, f2, pi0-pi3; tested uploads from all hosts. Co-Authored-By: Claude Sonnet 4.6 --- README.md | 110 ++++++++++++++++++++++++++++-------- contrib/goprecords-upload-client.sh | 38 +++++++++---- scripts/goprecords-upload-client.sh | 82 +++++++++++++++++++++++++++ 3 files changed, 196 insertions(+), 34 deletions(-) create mode 100755 scripts/goprecords-upload-client.sh diff --git a/README.md b/README.md index 9f5570d..4db2dbc 100644 --- a/README.md +++ b/README.md @@ -254,52 +254,74 @@ If there are **no** keys in the auth database, uploads are accepted without **`A ### Manual hourly upload (single host, not config-managed) -Use this when the machine is **not** deployed from a Rex/Ansible repo. The reference script is **`contrib/goprecords-upload-client.sh`** (POSIX **`sh`**: Linux, FreeBSD, OpenBSD). +Use this when the machine is **not** deployed from a Rex/Ansible repo. The unified script is **`scripts/goprecords-upload-client.sh`** (POSIX **`sh`**: Linux, FreeBSD, OpenBSD; runs as root or as a regular user). A copy is also kept in **`contrib/`** for backward compatibility. -**SSH vs upload name:** **`GOPRECORDS_HOST`** must stay the **short** stats name (**`f0`**, **`pi2`**). For **SSH**, use a real host — e.g. **`ssh -p 22 paul@f0.lan.buetow.org`** or **`paul@192.168.1.130`**, not **`f0.lan`** (incomplete / invalid). OpenBSD frontends may use **port 2** in Rex; **f0–f3** and **Pis** use **port 22**. +The script works on all host types: + +| Host class | OS | Privilege | Token path | +|------------|----|-----------|------------| +| **f0–f3** | FreeBSD | root via `doas` | `/etc/goprecords-upload.token` | +| **pi0–pi3** | Rocky Linux (aarch64) | root via `sudo` | `/etc/goprecords-upload.token` | +| **earth** (laptop) | Fedora / Linux | user (no root) | `$XDG_CONFIG_HOME/goprecords-upload-/token` | +| **blowfish**, **fishfinger** | OpenBSD | root (Rex) | deployed by Rex from template | + +**SSH vs upload name:** **`GOPRECORDS_HOST`** must stay the **short** stats name (**`f0`**, **`pi2`**, **`earth`**). For **SSH**, use a real host — e.g. **`ssh -p 22 paul@f0.lan.buetow.org`** or **`paul@192.168.1.130`**, not **`f0.lan`** (incomplete / invalid). OpenBSD frontends may use **port 2** in Rex; **f0–f3** and **Pis** use **port 22**. **Privilege on FreeBSD:** many hosts (e.g. **f0–f3**) ship **`doas`** and no **`sudo`**. Use **`doas`** for install, writing the token, and one-off test runs below; on Linux (Pis) use **`sudo`**. +**Token path (non-root):** when run as a non-root user the script defaults to **`${XDG_CONFIG_HOME:-$HOME/.config}/goprecords-upload-${GOPRECORDS_HOST}/token`** (mode **`600`**). Override with **`GOPRECORDS_TOKEN_FILE`**. + **On each host** 1. Install **`curl`** and **`uptimed`**, and ensure **`uptimed`** is running. -2. Install the script (path is up to you; **`755`**, owned by **`root`**): +2. Install the script (path is up to you; **`755`**, owned by **`root`** for system-wide use or **`700`** in `~/.local/bin` for user installs): - **Linux** + **Linux (root)** ```bash - sudo install -m 755 contrib/goprecords-upload-client.sh /usr/local/bin/goprecords-upload-client.sh + sudo install -m 755 scripts/goprecords-upload-client.sh /usr/local/bin/goprecords-upload-client.sh ``` - **FreeBSD** + **FreeBSD (root)** ```bash - doas install -m 755 contrib/goprecords-upload-client.sh /usr/local/bin/goprecords-upload-client.sh + doas install -m 755 scripts/goprecords-upload-client.sh /usr/local/bin/goprecords-upload-client.sh ``` -3. Create a token on the goprecords server (hostname must match **`GOPRECORDS_HOST`** exactly, e.g. **`f0`**, **`pi2`**): + **Linux (user, e.g. earth)** + + ```bash + install -m 700 scripts/goprecords-upload-client.sh ~/.local/bin/goprecords-upload-client.sh + ``` + +3. Create a token on the goprecords server (hostname must match **`GOPRECORDS_HOST`** exactly, e.g. **`f0`**, **`pi2`**, **`earth`**): ```bash kubectl exec -n services deployment/goprecords -- \ goprecords --create-client-key f0 -stats-dir=/data/stats ``` -4. Save the printed secret once as **`root`**, mode **`600`**: +4. Save the printed secret, mode **`600`**: - **Linux** + **Linux / FreeBSD (root)** ```bash + # Linux umask 077 - sudo sh -c 'cat > /etc/goprecords-upload.token' + sudo sh -c ‘cat > /etc/goprecords-upload.token’ sudo chmod 600 /etc/goprecords-upload.token + # FreeBSD + umask 077 + doas sh -c ‘cat > /etc/goprecords-upload.token’ + doas chmod 600 /etc/goprecords-upload.token ``` - **FreeBSD** + **Linux (user, e.g. earth)** ```bash + mkdir -p ~/.config/goprecords-upload-earth umask 077 - doas sh -c 'cat > /etc/goprecords-upload.token' - doas chmod 600 /etc/goprecords-upload.token + cat > ~/.config/goprecords-upload-earth/token ``` 5. **FreeBSD — hourly cron** (example for **`f0`**; use **`/etc/crontab`** or **`root`**’s crontab). **`curl`** must be on **`PATH`** for cron (often **`/usr/local/bin`**): @@ -310,7 +332,7 @@ Use this when the machine is **not** deployed from a Rex/Ansible repo. The refer Repeat for **`f1`**, **`f2`**, **`f3`**, … with the matching **`GOPRECORDS_HOST`**. -6. **Linux (systemd) — hourly timer** (example for **`pi0`**). **`/etc/goprecords-upload.env`** (mode **`644`**, root): +6. **Linux (systemd) — hourly timer, system-wide** (example for **`pi0`**). **`/etc/goprecords-upload.env`** (mode **`644`**, root): ```ini GOPRECORDS_HOST=pi0 @@ -356,28 +378,70 @@ Use this when the machine is **not** deployed from a Rex/Ansible repo. The refer Use a different **`GOPRECORDS_HOST`** in **`/etc/goprecords-upload.env`** on **`pi1`**, **`pi2`**, **`pi3`**. +7. **Linux (systemd) — hourly timer, user session** (example for **`earth`** laptop). **`~/.config/systemd/user/goprecords-upload-earth.service`**: + + ```ini + [Unit] + Description=Upload uptimed stats to goprecords + + [Service] + Type=oneshot + Environment=GOPRECORDS_HOST=earth + ExecStart=%h/.local/bin/goprecords-upload-client.sh + ``` + + **`~/.config/systemd/user/goprecords-upload-earth.timer`**: + + ```ini + [Unit] + Description=Hourly uptimed upload to goprecords (earth) + + [Timer] + OnCalendar=hourly + OnActiveSec=90s + RandomizedDelaySec=300 + Persistent=true + + [Install] + WantedBy=timers.target + ``` + + Then (enable lingering so uploads continue without an active login session): + + ```bash + sudo loginctl enable-linger "$USER" + systemctl --user daemon-reload + systemctl --user enable --now goprecords-upload-earth.timer + ``` + **Environment (optional)** -| Variable | Default | -|----------|---------| -| **`GOPRECORDS_HOST`** | (required) | -| **`GOPRECORDS_TOKEN_FILE`** | **`/etc/goprecords-upload.token`** | -| **`GOPRECORDS_BASE_URL`** | **`https://goprecords.f3s.buetow.org`** | +| Variable | Default (root) | Default (non-root) | +|----------|----------------|--------------------| +| **`GOPRECORDS_HOST`** | (required) | (required) | +| **`GOPRECORDS_TOKEN_FILE`** | **`/etc/goprecords-upload.token`** | **`$XDG_CONFIG_HOME/goprecords-upload-/token`** | +| **`GOPRECORDS_BASE_URL`** | **`https://goprecords.f3s.buetow.org`** | same | **Test one run** -**Linux** +**Linux (root)** ```bash -sudo env GOPRECORDS_HOST=f0 /usr/local/bin/goprecords-upload-client.sh +sudo env GOPRECORDS_HOST=pi0 /usr/local/bin/goprecords-upload-client.sh ``` -**FreeBSD** +**FreeBSD (root)** ```bash doas env GOPRECORDS_HOST=f0 /usr/local/bin/goprecords-upload-client.sh ``` +**Linux (user)** + +```bash +GOPRECORDS_HOST=earth ~/.local/bin/goprecords-upload-client.sh +``` + ## Test Run the test subcommand (fixture comparison and import/query parity): diff --git a/contrib/goprecords-upload-client.sh b/contrib/goprecords-upload-client.sh index e235c51..7e04cd9 100755 --- a/contrib/goprecords-upload-client.sh +++ b/contrib/goprecords-upload-client.sh @@ -1,10 +1,20 @@ #!/bin/sh set -e GOPRECORDS_BASE_URL="${GOPRECORDS_BASE_URL:-https://goprecords.f3s.buetow.org}" -GOPRECORDS_HOST="${GOPRECORDS_HOST:?set GOPRECORDS_HOST (e.g. f0, pi0)}" -GOPRECORDS_TOKEN_FILE="${GOPRECORDS_TOKEN_FILE:-/etc/goprecords-upload.token}" +GOPRECORDS_HOST="${GOPRECORDS_HOST:?set GOPRECORDS_HOST (e.g. f0, pi0, earth)}" PATH="/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:${PATH}" +_default_token_file() { + if [ "$(id -u)" = "0" ]; then + printf '/etc/goprecords-upload.token' + else + config="${XDG_CONFIG_HOME:-${HOME}/.config}" + printf '%s/goprecords-upload-%s/token' "$config" "$GOPRECORDS_HOST" + fi +} + +GOPRECORDS_TOKEN_FILE="${GOPRECORDS_TOKEN_FILE:-$(_default_token_file)}" + if ! test -r "$GOPRECORDS_TOKEN_FILE"; then echo "goprecords-upload-client: cannot read $GOPRECORDS_TOKEN_FILE" >&2 exit 1 @@ -23,17 +33,21 @@ upload() { "${GOPRECORDS_BASE_URL}/upload/${GOPRECORDS_HOST}/${kind}" } -records_path= -if test -f /var/spool/uptimed/records; then - records_path=/var/spool/uptimed/records -elif test -f /var/db/uptimed/records; then - records_path=/var/db/uptimed/records -elif test -f /usr/local/var/uptimed/records; then - records_path=/usr/local/var/uptimed/records -else +_find_records() { + for p in \ + /var/spool/uptimed/records \ + /var/db/uptimed/records \ + /usr/local/var/uptimed/records; do + if test -f "$p"; then + printf '%s' "$p" + return 0 + fi + done echo "goprecords-upload-client: no uptimed records file found" >&2 exit 1 -fi +} + +records_path=$(_find_records) tmp=$(mktemp) trap 'rm -f "$tmp"' 0 INT TERM HUP @@ -51,6 +65,8 @@ fi if test -r /etc/os-release; then upload os.txt /etc/os-release +elif test -r /var/run/dmesg.boot; then + upload os.txt /var/run/dmesg.boot else uname -a >"$tmp" upload os.txt "$tmp" diff --git a/scripts/goprecords-upload-client.sh b/scripts/goprecords-upload-client.sh new file mode 100755 index 0000000..7e04cd9 --- /dev/null +++ b/scripts/goprecords-upload-client.sh @@ -0,0 +1,82 @@ +#!/bin/sh +set -e +GOPRECORDS_BASE_URL="${GOPRECORDS_BASE_URL:-https://goprecords.f3s.buetow.org}" +GOPRECORDS_HOST="${GOPRECORDS_HOST:?set GOPRECORDS_HOST (e.g. f0, pi0, earth)}" +PATH="/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:${PATH}" + +_default_token_file() { + if [ "$(id -u)" = "0" ]; then + printf '/etc/goprecords-upload.token' + else + config="${XDG_CONFIG_HOME:-${HOME}/.config}" + printf '%s/goprecords-upload-%s/token' "$config" "$GOPRECORDS_HOST" + fi +} + +GOPRECORDS_TOKEN_FILE="${GOPRECORDS_TOKEN_FILE:-$(_default_token_file)}" + +if ! test -r "$GOPRECORDS_TOKEN_FILE"; then + echo "goprecords-upload-client: cannot read $GOPRECORDS_TOKEN_FILE" >&2 + exit 1 +fi +TOKEN=$(tr -d '\n\r' <"$GOPRECORDS_TOKEN_FILE") + +upload() { + kind=$1 + file=$2 + if ! test -f "$file"; then + echo "goprecords-upload-client: skip $kind (no $file)" >&2 + return 0 + fi + curl -fsS -X PUT --data-binary "@${file}" \ + -H "Authorization: Bearer ${TOKEN}" \ + "${GOPRECORDS_BASE_URL}/upload/${GOPRECORDS_HOST}/${kind}" +} + +_find_records() { + for p in \ + /var/spool/uptimed/records \ + /var/db/uptimed/records \ + /usr/local/var/uptimed/records; do + if test -f "$p"; then + printf '%s' "$p" + return 0 + fi + done + echo "goprecords-upload-client: no uptimed records file found" >&2 + exit 1 +} + +records_path=$(_find_records) + +tmp=$(mktemp) +trap 'rm -f "$tmp"' 0 INT TERM HUP + +upload records "$records_path" + +if command -v uprecords >/dev/null 2>&1; then + uprecords -a -m 100 >"$tmp" + upload txt "$tmp" + uprecords -a | grep '^->' >"$tmp" || true + if test -s "$tmp"; then + upload cur.txt "$tmp" + fi +fi + +if test -r /etc/os-release; then + upload os.txt /etc/os-release +elif test -r /var/run/dmesg.boot; then + upload os.txt /var/run/dmesg.boot +else + uname -a >"$tmp" + upload os.txt "$tmp" +fi + +if test -r /proc/cpuinfo; then + upload cpuinfo.txt /proc/cpuinfo +elif test -r /var/run/dmesg.boot; then + upload cpuinfo.txt /var/run/dmesg.boot +else + sysctl hw.model hw.ncpu hw.machine >"$tmp" 2>/dev/null || uname -a >"$tmp" + upload cpuinfo.txt "$tmp" +fi -- cgit v1.2.3