From 9a904fda8ac891503bd9f68a7f9fc32dceaad539 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Sun, 26 Jul 2026 20:44:10 +0200 Subject: [PATCH] refactor: replace install.sh with Python install.py --- CHANGELOG.md | 2 +- CONTRIBUTING.md | 2 +- __pycache__/install.cpython-314.pyc | Bin 0 -> 13306 bytes docs/configuration.md | 2 +- docs/deployment.md | 6 +- docs/security.md | 2 +- install.py | 263 ++++++++++++++++++++++++++++ install.sh | 153 ---------------- 8 files changed, 270 insertions(+), 160 deletions(-) create mode 100644 __pycache__/install.cpython-314.pyc create mode 100644 install.py delete mode 100755 install.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 1898145..b63abcf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,7 +44,7 @@ secrets. Its only dependency outside the standard library is the first-party `/etc/nuntius/config.toml` on first start. - **systemd unit** — installed inline from `docs/deployment.md` with `Restart=on-failure` and `EnvironmentFile=`. -- **Install script** — `install.sh`, idempotent, handles user creation, config +- **Install script** — `install.py`, idempotent, handles user creation, config generation, and secrets file mode `0600`. - **docs/** — `architecture.md`, `configuration.md`, `api-reference.md`, `deployment.md`, `security.md`, `library-usage.md`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8e8fcc1..b4ae0c5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -140,7 +140,7 @@ nuntius/ ├── examples/ │ └── minimal/ # Standalone usage of pkg/contactform (no HTTP server) ├── docs/ # Architecture, configuration, API, deployment, security, library usage -├── install.sh # One-shot server installer (idempotent) +├── install.py # One-shot server installer (idempotent) ├── justfile # install, build, test, run, uninstall ├── go.mod # Go 1.26, no external dependencies └── LICENSE # MIT diff --git a/__pycache__/install.cpython-314.pyc b/__pycache__/install.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..20c8e5f266f505ef53611771b9bcbc8c2f650f6b GIT binary patch literal 13306 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 `install.sh` lives in the repository root. The systemd unit is included inline in Option A below (you do not need to rsync it from the repo). `.env.example` is a template for the secrets file. Adjust the `rsync` source list to match what you keep checked in. +> `install.py` lives in the repository root. The systemd unit is included inline in Option A below (you do not need to rsync it from the repo). `.env.example` is a template for the secrets file. Adjust the `rsync` source list to match what you keep checked in. ## 2. Install on the Server (SSH Session) @@ -97,7 +97,7 @@ sudo journalctl -u nuntius -f The service file expects the binary at `/usr/local/bin/nuntius`, the env file at `/etc/nuntius/.env`, and the data dir at `/var/lib/nuntius` (the config's `data_dir: "./data"` resolves to `/var/lib/nuntius/data` because the unit sets `WorkingDirectory=/var/lib/nuntius`). If you'd rather use `/opt/nuntius/...` or any other layout, edit the systemd unit block above before installing. -### Option B — `install.sh` (idempotent) +### Option B — `install.py` (idempotent) Before running the script, copy the systemd unit from the block in Option A above into `/tmp/nuntius/nuntius.service` (the script reads it from the current working directory): @@ -106,7 +106,7 @@ ssh user@your-server cd /tmp/nuntius # Paste the [Unit]…[Install] block from Option A into this file: sudo $EDITOR /tmp/nuntius/nuntius.service -sudo bash install.sh +sudo python3 install.py ``` The script is idempotent: re-running on an already-installed host is safe. It: diff --git a/docs/security.md b/docs/security.md index b29e837..56dea66 100644 --- a/docs/security.md +++ b/docs/security.md @@ -100,7 +100,7 @@ Things nuntius does **not** do: Things you should do: -- Make sure `/etc/nuntius/.env` is `0600` and owned `root:nuntius` (the `install.sh` script does this for you). +- Make sure `/etc/nuntius/.env` is `0600` and owned `root:nuntius` (the `install.py` script does this for you). - Use an app-specific password or OAuth token if your provider supports it. nuntius uses `PLAIN` auth because that is what every SMTP provider offers; the credential is the secret, not the auth mechanism. - Rotate the password on the same cadence as any other production secret. - Restrict read access to `journalctl -u nuntius` if you do not want the client IPs in your local log files. diff --git a/install.py b/install.py new file mode 100644 index 0000000..6e77647 --- /dev/null +++ b/install.py @@ -0,0 +1,263 @@ +#!/usr/bin/env python3 +# +# install.py — install nuntius as a systemd service. +# +# nuntius ships as a single static binary. Build it on your workstation, upload +# the binary, the systemd unit, and the .env template to the server, then run +# this script there as root from that directory: +# +# scp bin/nuntius nuntius.service .env.example user@host:/tmp/nuntius/ +# ssh user@host +# cd /tmp/nuntius && sudo python3 install.py +# +# It expects ./nuntius and ./nuntius.service (and optionally ./.env.example) in +# the current working directory. Idempotent: re-running is safe — it skips the +# user, config, and .env when they already exist, and never starts the service. + +from __future__ import annotations + +import argparse +import logging +import os +import shutil +import subprocess +import sys +from typing import NoReturn + +NUNTIUS_USER = "nuntius" +BIN_DEST = "/usr/local/bin/nuntius" +CONFIG_DIR = "/etc/nuntius" +CONFIG_FILE = "/etc/nuntius/config.toml" +ENV_FILE = "/etc/nuntius/.env" +DATA_DIR = "/var/lib/nuntius" +SERVICE_DEST = "/etc/systemd/system/nuntius.service" + +# --------------------------------------------------------------------------- +# Coloured logging +# --------------------------------------------------------------------------- + + +class _ColourFormatter(logging.Formatter): + """Prefix each log line with a coloured level marker, mimicking the bash script.""" + + _COLOURS: dict[int, str] = { + logging.INFO: "\033[1;34m", # blue + logging.WARNING: "\033[1;33m", # yellow + logging.ERROR: "\033[1;31m", # red + } + _LABELS: dict[int, str] = { + logging.INFO: "==>", + logging.WARNING: "WARN:", + logging.ERROR: "ERROR:", + } + _RESET = "\033[0m" + + def format(self, record: logging.LogRecord) -> str: + colour = self._COLOURS.get(record.levelno, "") + label = self._LABELS.get(record.levelno, "") + if colour and label: + return f"{colour}{label}{self._RESET} {record.getMessage()}" + return record.getMessage() + + +def _setup_logging() -> None: + """Configure the root logger with coloured console output.""" + logger = logging.getLogger() + logger.setLevel(logging.INFO) + handler = logging.StreamHandler() + handler.setFormatter(_ColourFormatter()) + # Remove any handlers added by basicConfig or other imports. + logger.handlers.clear() + logger.addHandler(handler) + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _run(cmd: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: + """Run a command via subprocess and check for failure (unless told otherwise).""" + return subprocess.run(cmd, check=True, text=True, **kwargs) + + +# --------------------------------------------------------------------------- +# Install steps +# --------------------------------------------------------------------------- + + +def require_root() -> None: + """Exit if not running as root.""" + if os.geteuid() != 0: + logging.error("Run as root: sudo python3 install.py") + sys.exit(1) + + +def require_files() -> None: + """Verify that the binary and the systemd unit are present in CWD.""" + if not os.path.isfile("./nuntius"): + logging.error( + "./nuntius binary not found — copy it here first (e.g. rsync bin/nuntius)." + ) + sys.exit(1) + if not os.path.isfile("./nuntius.service"): + logging.error( + "./nuntius.service not found — paste the unit from docs/deployment.md (Option A)." + ) + sys.exit(1) + + +def create_user() -> None: + """Create the system user if it does not already exist.""" + try: + _run(["id", NUNTIUS_USER], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + logging.info("User %s already exists.", NUNTIUS_USER) + except subprocess.CalledProcessError: + logging.info("Creating system user %s…", NUNTIUS_USER) + _run( + [ + "useradd", + "--system", + "--shell", + "/usr/sbin/nologin", + "--home-dir", + DATA_DIR, + "--user-group", + NUNTIUS_USER, + ] + ) + + +def create_data_dir() -> None: + """Ensure the data directory exists with correct ownership and permissions.""" + logging.info("Setting up %s…", DATA_DIR) + os.makedirs(DATA_DIR, exist_ok=True) + shutil.chown(DATA_DIR, user=NUNTIUS_USER, group=NUNTIUS_USER) + os.chmod(DATA_DIR, 0o750) + + +def install_binary() -> None: + """Copy the nuntius binary to /usr/local/bin with mode 0755.""" + logging.info("Installing binary to %s…", BIN_DEST) + shutil.copy2("./nuntius", BIN_DEST) + os.chmod(BIN_DEST, 0o755) + + +def install_service() -> None: + """Copy the systemd unit file with mode 0644.""" + logging.info("Installing systemd unit %s…", SERVICE_DEST) + shutil.copy2("./nuntius.service", SERVICE_DEST) + os.chmod(SERVICE_DEST, 0o644) + + +def generate_config() -> None: + """Auto-generate the TOML config by running nuntius briefly if config is absent.""" + if os.path.isfile(CONFIG_FILE): + logging.info("Config %s already exists; leaving it untouched.", CONFIG_FILE) + return + + logging.info("Generating %s…", CONFIG_FILE) + os.makedirs(CONFIG_DIR, exist_ok=True) + + # nuntius writes the template config on first start; run it briefly, then + # let `timeout` send SIGTERM (graceful shutdown). The write happens at + # startup, before the timeout elapses. + try: + _run(["timeout", "-k", "5s", "4s", BIN_DEST], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + except subprocess.CalledProcessError: + # timeout exits 124 on SIGTERM, 137 on SIGKILL — both are expected. + pass + + if not os.path.isfile(CONFIG_FILE): + logging.warning("nuntius did not create %s; check the uploaded binary.", CONFIG_FILE) + + # Fix ownership in case nuntius wrote files as root. + _run(["chown", "-R", f"{NUNTIUS_USER}:{NUNTIUS_USER}", DATA_DIR]) + + +def install_env() -> None: + """Copy .env.example to /etc/nuntius/.env if it does not already exist.""" + if os.path.isfile(ENV_FILE): + logging.info("%s already exists; leaving it untouched.", ENV_FILE) + elif os.path.isfile("./.env.example"): + logging.info("Creating %s from .env.example (edit it!)…", ENV_FILE) + shutil.copy2("./.env.example", ENV_FILE) + os.chmod(ENV_FILE, 0o600) + _run(["chown", f"root:{NUNTIUS_USER}", ENV_FILE]) + else: + logging.warning( + ".env.example not found; skipping %s. Create it before starting.", ENV_FILE + ) + + +def enable_service() -> None: + """Reload systemd and enable the nuntius service (does not start it).""" + logging.info("Reloading systemd and enabling nuntius…") + _run(["systemctl", "daemon-reload"]) + _run(["systemctl", "enable", "nuntius.service"]) + + +def final_notes() -> None: + """Print post-install instructions.""" + print( + f""" +============================================================ +✓ nuntius installed and enabled, but NOT started. + +Next manual steps: + 1. Set the SMTP password: + sudo $EDITOR {ENV_FILE} + 2. Edit the auto-generated config (smtp.host, smtp.user, allowed_origins): + sudo $EDITOR {CONFIG_FILE} + 3. Start the service: + sudo systemctl start nuntius + sudo journalctl -u nuntius -f + 4. Add the nginx location block (see README.md), then: + sudo nginx -t && sudo systemctl reload nginx +============================================================ +""" + ) + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + + +def _build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="install.py", + description="install nuntius as a systemd service.", + epilog=( + "Idempotent: re-running is safe. The service is enabled but never started." + ), + ) + return parser + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + + +def main(argv: list[str] | None = None) -> NoReturn: + """Parse args and run all install steps in order.""" + _setup_logging() + parser = _build_parser() + parser.parse_args(argv) + + require_root() + require_files() + create_user() + create_data_dir() + install_binary() + install_service() + generate_config() + install_env() + enable_service() + final_notes() + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/install.sh b/install.sh deleted file mode 100755 index 6d77d0e..0000000 --- a/install.sh +++ /dev/null @@ -1,153 +0,0 @@ -#!/usr/bin/env bash -# -# install.sh — install nuntius as a systemd service. -# -# nuntius ships as a single static binary. Build it on your workstation, upload -# the binary, the systemd unit, and the .env template to the server, then run -# this script there as root from that directory: -# -# scp bin/nuntius nuntius.service .env.example user@host:/tmp/nuntius/ -# ssh user@host -# cd /tmp/nuntius && sudo bash install.sh -# -# It expects ./nuntius and ./nuntius.service (and optionally ./.env.example) in -# the current working directory. Idempotent: re-running is safe — it skips the -# user, config, and .env when they already exist, and never starts the service. - -set -euo pipefail - -NUNTIUS_USER="nuntius" -BIN_DEST="/usr/local/bin/nuntius" -CONFIG_DIR="/etc/nuntius" -CONFIG_FILE="${CONFIG_DIR}/config.toml" -ENV_FILE="${CONFIG_DIR}/.env" -DATA_DIR="/var/lib/nuntius" -SERVICE_DEST="/etc/systemd/system/nuntius.service" - -log() { printf '\033[1;34m==>\033[0m %s\n' "$*"; } -warn() { printf '\033[1;33mWARN:\033[0m %s\n' "$*" >&2; } -die() { printf '\033[1;31mERROR:\033[0m %s\n' "$*" >&2; exit 1; } - -usage() { - cat <<'EOF' -install.sh — install nuntius as a systemd service. - -Upload the binary, the systemd unit, and the .env template to the server, then -run this script there as root from that directory: - - scp bin/nuntius nuntius.service .env.example user@host:/tmp/nuntius/ - ssh user@host - cd /tmp/nuntius && sudo bash install.sh - -Idempotent: re-running is safe. The service is enabled but never started. -EOF - exit 0 -} - -require_root() { - [ "$(id -u)" -eq 0 ] || die "Run as root: sudo bash install.sh" -} - -require_files() { - [ -f ./nuntius ] || die "./nuntius binary not found — copy it here first (e.g. rsync bin/nuntius)." - [ -f ./nuntius.service ] || \ - die "./nuntius.service not found — paste the unit from docs/deployment.md (Option A)." -} - -create_user() { - if id "$NUNTIUS_USER" >/dev/null 2>&1; then - log "User ${NUNTIUS_USER} already exists." - else - log "Creating system user ${NUNTIUS_USER}…" - useradd --system --shell /usr/sbin/nologin --home-dir "$DATA_DIR" --user-group "$NUNTIUS_USER" - fi -} - -create_data_dir() { - log "Setting up ${DATA_DIR}…" - mkdir -p "$DATA_DIR" - chown "$NUNTIUS_USER:$NUNTIUS_USER" "$DATA_DIR" - chmod 750 "$DATA_DIR" -} - -install_binary() { - log "Installing binary to ${BIN_DEST}…" - install -m 0755 ./nuntius "$BIN_DEST" -} - -install_service() { - log "Installing systemd unit ${SERVICE_DEST}…" - install -m 0644 ./nuntius.service "$SERVICE_DEST" -} - -generate_config() { - if [ -f "$CONFIG_FILE" ]; then - log "Config ${CONFIG_FILE} already exists; leaving it untouched." - return - fi - log "Generating ${CONFIG_FILE}…" - mkdir -p "$CONFIG_DIR" - # nuntius writes the template config on first start; run it briefly, then - # let `timeout` send SIGTERM (graceful shutdown). No backgrounding or manual - # kill — the write happens at startup, before the timeout elapses. - timeout -k 5s 4s "$BIN_DEST" >/dev/null 2>&1 || true - [ -f "$CONFIG_FILE" ] || warn "nuntius did not create ${CONFIG_FILE}; check the uploaded binary." - chown -R "$NUNTIUS_USER:$NUNTIUS_USER" "$DATA_DIR" -} - -install_env() { - if [ -f "$ENV_FILE" ]; then - log "${ENV_FILE} already exists; leaving it untouched." - elif [ -f ./.env.example ]; then - log "Creating ${ENV_FILE} from .env.example (edit it!)…" - install -m 0600 ./.env.example "$ENV_FILE" - chown "root:$NUNTIUS_USER" "$ENV_FILE" - else - warn ".env.example not found; skipping ${ENV_FILE}. Create it before starting." - fi -} - -enable_service() { - log "Reloading systemd and enabling nuntius…" - systemctl daemon-reload - systemctl enable nuntius.service -} - -final_notes() { - cat <<'EOF' - -============================================================ -✓ nuntius installed and enabled, but NOT started. - -Next manual steps: - 1. Set the SMTP password: - sudo $EDITOR /etc/nuntius/.env - 2. Edit the auto-generated config (smtp.host, smtp.user, allowed_origins): - sudo $EDITOR /etc/nuntius/config.toml - 3. Start the service: - sudo systemctl start nuntius - sudo journalctl -u nuntius -f - 4. Add the nginx location block (see README.md), then: - sudo nginx -t && sudo systemctl reload nginx -============================================================ -EOF -} - -main() { - case "${1:-}" in - -h | --help) usage ;; - esac - - require_root - require_files - create_user - create_data_dir - install_binary - install_service - generate_config - install_env - enable_service - final_notes -} - -main "$@"