add new deploy script and cutting out pip as the installer
This commit is contained in:
65
README.md
65
README.md
@@ -1,41 +1,54 @@
|
|||||||
# pobsync
|
# 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.
|
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
|
## Design overview
|
||||||
|
|
||||||
- Runtime, config, logs and state live under **`/opt/pobsync`**
|
* Runtime, config, logs and state live under `/opt/pobsync`
|
||||||
- Backup data itself is stored under a configurable **`backup_root`** (e.g. `/srv/backups`)
|
* Backup data itself is stored under a configurable `backup_root` (e.g. `/srv/backups`)
|
||||||
- Two snapshot types:
|
* Two snapshot types:
|
||||||
- **scheduled**
|
* scheduled
|
||||||
Participates in retention pruning (daily / weekly / monthly / yearly)
|
Participates in retention pruning (daily / weekly / monthly / yearly)
|
||||||
- **manual**
|
* manual
|
||||||
Kept outside the scheduled prune chain, defaults to hardlinking from the latest scheduled snapshot
|
Kept outside the scheduled prune chain, defaults to hardlinking from the latest scheduled snapshot
|
||||||
- Minimal dependencies (currently only `PyYAML`)
|
* Minimal dependencies (currently only `PyYAML`)
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
|
* * *
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Python **3.11+**
|
* Python 3.11+
|
||||||
- `rsync`
|
* `rsync`
|
||||||
- `ssh`
|
* `ssh`
|
||||||
- Root or sudo access on the backup server
|
* Root or sudo access on the backup server
|
||||||
- SSH keys already configured between backup server and remotes
|
* SSH keys already configured between backup server and remotes
|
||||||
|
|
||||||
---
|
* * *
|
||||||
|
## Installation (canonical runtime under /opt/pobsync, no venv)
|
||||||
## Installation (system-wide, no venv)
|
|
||||||
|
|
||||||
This assumes you are installing as root or via sudo.
|
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 .
|
|
||||||
97
scripts/deploy
Executable file
97
scripts/deploy
Executable file
@@ -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}" <<EOF
|
||||||
|
#!/bin/sh
|
||||||
|
# managed-by=pobsync deploy
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
PREFIX="${PREFIX}"
|
||||||
|
export PYTHONPATH="\${PREFIX}/lib"
|
||||||
|
export PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
exec /usr/bin/python3 -m pobsync "\$@"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod 0755 "${WRAPPER}"
|
||||||
|
|
||||||
|
echo "OK"
|
||||||
|
echo "- deployed package to ${DST_PKG}"
|
||||||
|
echo "- wrote build info ${BUILD_FILE}"
|
||||||
|
echo "- installed entrypoint ${WRAPPER}"
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ import yaml
|
|||||||
|
|
||||||
from ..errors import InstallError
|
from ..errors import InstallError
|
||||||
from ..paths import PobsyncPaths
|
from ..paths import PobsyncPaths
|
||||||
from ..util import ensure_dir, is_absolute_non_root
|
from ..util import ensure_dir, is_absolute_non_root, write_text_atomic
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_EXCLUDES = [
|
DEFAULT_EXCLUDES = [
|
||||||
@@ -42,21 +42,16 @@ DEFAULT_RSYNC_ARGS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
# NOTE: This wrapper exists so cron can reliably call /opt/pobsync/bin/pobsync.
|
# Canonical entrypoint: always run code from /opt/pobsync/lib (or the given prefix).
|
||||||
# It forwards to the "real" pobsync on PATH when possible, and falls back to python -m pobsync.
|
|
||||||
WRAPPER_SH_TEMPLATE = """#!/bin/sh
|
WRAPPER_SH_TEMPLATE = """#!/bin/sh
|
||||||
# managed-by=pobsync
|
# managed-by=pobsync install
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
SELF="{self_path}"
|
PREFIX="{prefix}"
|
||||||
REAL="$(command -v pobsync || true)"
|
export PYTHONPATH="${{PREFIX}}/lib"
|
||||||
|
export PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
# Avoid recursion if PATH includes /opt/pobsync/bin
|
exec /usr/bin/python3 -m pobsync "$@"
|
||||||
if [ -n "${{REAL}}" ] && [ "${{REAL}}" != "${{SELF}}" ]; then
|
|
||||||
exec "${{REAL}}" "$@"
|
|
||||||
fi
|
|
||||||
|
|
||||||
exec python3 -m pobsync "$@"
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@@ -118,29 +113,7 @@ def write_yaml(path: Path, data: dict[str, Any], dry_run: bool, force: bool) ->
|
|||||||
return f"write {path}"
|
return f"write {path}"
|
||||||
|
|
||||||
|
|
||||||
def _write_text_file(path: Path, content: str, dry_run: bool, force: bool) -> str:
|
def _install_wrapper(prefix: Path, dry_run: bool) -> list[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.
|
|
||||||
"""
|
|
||||||
actions: list[str] = []
|
actions: list[str] = []
|
||||||
|
|
||||||
bin_dir = prefix / "bin"
|
bin_dir = prefix / "bin"
|
||||||
@@ -149,21 +122,18 @@ def _install_wrapper(prefix: Path, dry_run: bool, force: bool) -> list[str]:
|
|||||||
ensure_dir(bin_dir)
|
ensure_dir(bin_dir)
|
||||||
|
|
||||||
wrapper_path = bin_dir / "pobsync"
|
wrapper_path = bin_dir / "pobsync"
|
||||||
content = WRAPPER_SH_TEMPLATE.format(self_path=str(wrapper_path))
|
content = WRAPPER_SH_TEMPLATE.format(prefix=str(prefix))
|
||||||
actions.append(_write_text_file(wrapper_path, content, dry_run=dry_run, force=force))
|
|
||||||
|
|
||||||
actions.append(f"chmod 0755 {wrapper_path}")
|
actions.append(f"write {wrapper_path}")
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
|
write_text_atomic(wrapper_path, content)
|
||||||
os.chmod(wrapper_path, 0o755)
|
os.chmod(wrapper_path, 0o755)
|
||||||
|
|
||||||
|
actions.append(f"chmod 0755 {wrapper_path}")
|
||||||
return actions
|
return actions
|
||||||
|
|
||||||
|
|
||||||
def _ensure_system_log_dir(dry_run: bool) -> list[str]:
|
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] = []
|
actions: list[str] = []
|
||||||
log_dir = Path("/var/log/pobsync")
|
log_dir = Path("/var/log/pobsync")
|
||||||
actions.append(f"mkdir -p {log_dir}")
|
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)
|
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))
|
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.
|
# Option A canonical install support:
|
||||||
actions.extend(_install_wrapper(prefix, dry_run=dry_run, force=force))
|
# - 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))
|
actions.extend(_ensure_system_log_dir(dry_run=dry_run))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user