From 7caaf46588de87c263e64c7331dddb9c53133277 Mon Sep 17 00:00:00 2001 From: Peter van Arkel Date: Tue, 3 Feb 2026 22:39:13 +0100 Subject: [PATCH] add new deploy script and cutting out pip as the installer --- README.md | 65 +++++++++++++--------- scripts/deploy | 97 +++++++++++++++++++++++++++++++++ src/pobsync/commands/install.py | 60 ++++++-------------- 3 files changed, 152 insertions(+), 70 deletions(-) create mode 100755 scripts/deploy diff --git a/README.md b/README.md index 4d5abde..bcb241b 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,54 @@ # pobsync -`pobsync` is a pull-based backup tool for sysadmins. +`pobsync` is a pull-based backup tool for sysadmins. It creates rsync-based snapshots with hardlinking (`--link-dest`) and stores them centrally on a backup server. -Backups are **pulled over SSH**, not pushed, and are designed to be run from cron or manually. - ---- +Backups are pulled over SSH, not pushed, and are designed to be run from cron or manually. +* * * ## Design overview -- Runtime, config, logs and state live under **`/opt/pobsync`** -- Backup data itself is stored under a configurable **`backup_root`** (e.g. `/srv/backups`) -- Two snapshot types: - - **scheduled** - Participates in retention pruning (daily / weekly / monthly / yearly) - - **manual** - Kept outside the scheduled prune chain, defaults to hardlinking from the latest scheduled snapshot -- Minimal dependencies (currently only `PyYAML`) - ---- + * Runtime, config, logs and state live under `/opt/pobsync` + * Backup data itself is stored under a configurable `backup_root` (e.g. `/srv/backups`) + * Two snapshot types: + * scheduled +Participates in retention pruning (daily / weekly / monthly / yearly) + * manual +Kept outside the scheduled prune chain, defaults to hardlinking from the latest scheduled snapshot + * Minimal dependencies (currently only `PyYAML`) +* * * ## Requirements -- Python **3.11+** -- `rsync` -- `ssh` -- Root or sudo access on the backup server -- SSH keys already configured between backup server and remotes + * Python 3.11+ + * `rsync` + * `ssh` + * Root or sudo access on the backup server + * SSH keys already configured between backup server and remotes ---- - -## Installation (system-wide, no venv) +* * * +## Installation (canonical runtime under /opt/pobsync, no venv) This assumes you are installing as root or via sudo. -From the repository root: +1) Clone the repo + + git clone https://code.hosting.hippogrief.nl/hippogrief/pobsync.git + cd pobsync + +2) Deploy the runtime into `/opt/pobsync` (copies code into `/opt/pobsync/lib` and installs `/opt/pobsync/bin/pobsync`) + + sudo ./scripts/deploy --prefix /opt/pobsync + +3) Initialize runtime layout and global config + + sudo /opt/pobsync/bin/pobsync install --backup-root /mnt/backups/pobsync + sudo /opt/pobsync/bin/pobsync doctor + +## Updating + + cd /path/to/pobsync + git pull + sudo ./scripts/deploy --prefix /opt/pobsync + sudo /opt/pobsync/bin/pobsync doctor -```bash -python3 -m pip install --upgrade pip -sudo python3 -m pip install . \ No newline at end of file diff --git a/scripts/deploy b/scripts/deploy new file mode 100755 index 0000000..e65825e --- /dev/null +++ b/scripts/deploy @@ -0,0 +1,97 @@ +#!/bin/sh +# Deploy pobsync runtime into /opt/pobsync without pip/venv. +# Copies python package sources into /opt/pobsync/lib and installs a stable entrypoint in /opt/pobsync/bin. + +set -eu + +PREFIX="/opt/pobsync" + +usage() { + echo "Usage: $0 [--prefix /opt/pobsync]" >&2 + exit 2 +} + +while [ $# -gt 0 ]; do + case "$1" in + --prefix) + [ $# -ge 2 ] || usage + PREFIX="$2" + shift 2 + ;; + -h|--help) + usage + ;; + *) + echo "Unknown arg: $1" >&2 + usage + ;; + esac +done + +# Determine repo root from this script location +SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" +REPO_ROOT="$(CDPATH= cd -- "${SCRIPT_DIR}/.." && pwd)" + +SRC_PKG="${REPO_ROOT}/src/pobsync" +if [ ! -d "${SRC_PKG}" ]; then + echo "ERROR: expected python package at ${SRC_PKG}" >&2 + exit 1 +fi + +BIN_DIR="${PREFIX}/bin" +LIB_DIR="${PREFIX}/lib" +DST_PKG="${LIB_DIR}/pobsync" +BUILD_FILE="${DST_PKG}/_build.txt" + +mkdir -p "${BIN_DIR}" "${LIB_DIR}" + +# Copy code into /opt/pobsync/lib/pobsync +# We use rsync if available (clean updates with --delete), otherwise fall back to cp -a. +if command -v rsync >/dev/null 2>&1; then + rsync -a --delete \ + --exclude '__pycache__/' \ + --exclude '*.pyc' \ + --exclude '*.pyo' \ + --exclude '*.pyd' \ + "${SRC_PKG}/" "${DST_PKG}/" +else + # Fallback: wipe + copy + rm -rf "${DST_PKG}" + mkdir -p "${DST_PKG}" + cp -a "${SRC_PKG}/." "${DST_PKG}/" +fi + +# Write build info (best-effort) +GIT_SHA="unknown" +if command -v git >/dev/null 2>&1 && [ -d "${REPO_ROOT}/.git" ]; then + GIT_SHA="$(cd "${REPO_ROOT}" && git rev-parse HEAD 2>/dev/null || echo unknown)" +fi + +NOW_UTC="$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo unknown)" +{ + echo "deployed_at_utc=${NOW_UTC}" + echo "git_sha=${GIT_SHA}" + echo "repo_root=${REPO_ROOT}" +} > "${BUILD_FILE}" + +# Install stable entrypoint that always runs code from /opt/pobsync/lib +WRAPPER="${BIN_DIR}/pobsync" +cat > "${WRAPPER}" < return f"write {path}" -def _write_text_file(path: Path, content: str, dry_run: bool, force: bool) -> str: - """ - Write a plain text file with optional backup behavior similar to write_yaml(). - """ - if path.exists() and not force: - return f"skip existing {path}" - if path.exists() and force: - bak = path.with_suffix(path.suffix + ".bak") - if not dry_run: - bak.write_text(path.read_text(encoding="utf-8"), encoding="utf-8") - if not dry_run: - path.write_text(content, encoding="utf-8") - return f"overwrite {path} (backup {bak})" - - if not dry_run: - path.write_text(content, encoding="utf-8") - return f"write {path}" - - -def _install_wrapper(prefix: Path, dry_run: bool, force: bool) -> list[str]: - """ - Ensure prefix/bin/pobsync exists so cron entries can use it. - """ +def _install_wrapper(prefix: Path, dry_run: bool) -> list[str]: actions: list[str] = [] bin_dir = prefix / "bin" @@ -149,21 +122,18 @@ def _install_wrapper(prefix: Path, dry_run: bool, force: bool) -> list[str]: ensure_dir(bin_dir) wrapper_path = bin_dir / "pobsync" - content = WRAPPER_SH_TEMPLATE.format(self_path=str(wrapper_path)) - actions.append(_write_text_file(wrapper_path, content, dry_run=dry_run, force=force)) + content = WRAPPER_SH_TEMPLATE.format(prefix=str(prefix)) - actions.append(f"chmod 0755 {wrapper_path}") + actions.append(f"write {wrapper_path}") if not dry_run: + write_text_atomic(wrapper_path, content) os.chmod(wrapper_path, 0o755) + actions.append(f"chmod 0755 {wrapper_path}") return actions def _ensure_system_log_dir(dry_run: bool) -> list[str]: - """ - Best-effort: create /var/log/pobsync to match cron redirection. - Not fatal if it fails (e.g., insufficient permissions in a non-root install attempt). - """ actions: list[str] = [] log_dir = Path("/var/log/pobsync") actions.append(f"mkdir -p {log_dir}") @@ -194,8 +164,10 @@ def run_install( global_cfg = build_default_global_config(paths.home, backup_root=backup_root, retention=retention) actions.append(write_yaml(paths.global_config_path, global_cfg, dry_run=dry_run, force=force)) - # Install polish: make cron-compatible wrapper path and cron log directory. - actions.extend(_install_wrapper(prefix, dry_run=dry_run, force=force)) + # Option A canonical install support: + # - Provide a stable entrypoint at /opt/pobsync/bin/pobsync (or prefix/bin/pobsync) + # - Ensure /var/log/pobsync exists for cron redirection + actions.extend(_install_wrapper(prefix, dry_run=dry_run)) actions.extend(_ensure_system_log_dir(dry_run=dry_run)) return {