lts-backup-cli 2.0.0__tar.gz → 3.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/CLAUDE.md +19 -16
- lts_backup_cli-3.2.0/PKG-INFO +589 -0
- lts_backup_cli-3.2.0/README.md +554 -0
- lts_backup_cli-3.2.0/features/backup.feature +115 -0
- lts_backup_cli-3.2.0/features/discover.feature +126 -0
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/features/doctor.feature +3 -2
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/features/e2e.feature +35 -6
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/features/list.feature +13 -2
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/features/restore.feature +18 -2
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/features/steps/cli_steps.py +82 -11
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/pyproject.toml +2 -2
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/src/lts_backup_cli/__init__.py +1 -1
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/src/lts_backup_cli/cli.py +49 -29
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/src/lts_backup_cli/config.py +27 -35
- lts_backup_cli-3.2.0/src/lts_backup_cli/docker.py +175 -0
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/src/lts_backup_cli/doctor.py +45 -29
- lts_backup_cli-3.2.0/src/lts_backup_cli/rsync.py +68 -0
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/src/lts_backup_cli/runner.py +5 -1
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/src/lts_backup_cli/services.py +181 -59
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/src/lts_backup_cli/sshconfig.py +0 -9
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/src/lts_backup_cli/validation.py +25 -9
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/src/lts_backup_cli/wizard.py +96 -31
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/tests/docker/Dockerfile.rocky8 +5 -3
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/tests/docker/Dockerfile.ubuntu2204 +5 -3
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/tests/docker/docker-compose.yml +6 -10
- lts_backup_cli-3.2.0/tests/docker/docker-shim.sh +25 -0
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/tests/docker/e2e.sh +7 -5
- lts_backup_cli-3.2.0/tests/docker/web-setup.sh +35 -0
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/tests/smoke/smoke.sh +11 -2
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/tests/support/fake_runner.py +16 -5
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/tests/unit/conftest.py +2 -1
- lts_backup_cli-3.2.0/tests/unit/test_config.py +188 -0
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/tests/unit/test_discover.py +54 -10
- lts_backup_cli-3.2.0/tests/unit/test_docker.py +186 -0
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/tests/unit/test_doctor.py +84 -15
- lts_backup_cli-3.2.0/tests/unit/test_rsync.py +122 -0
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/tests/unit/test_runner.py +7 -1
- lts_backup_cli-3.2.0/tests/unit/test_services.py +474 -0
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/tests/unit/test_sshconfig.py +0 -4
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/tests/unit/test_validation.py +41 -12
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/tests/unit/test_wizard.py +147 -3
- lts_backup_cli-2.0.0/PKG-INFO +0 -522
- lts_backup_cli-2.0.0/README.md +0 -487
- lts_backup_cli-2.0.0/features/backup.feature +0 -68
- lts_backup_cli-2.0.0/features/discover.feature +0 -77
- lts_backup_cli-2.0.0/tests/docker/Dockerfile.sftp +0 -16
- lts_backup_cli-2.0.0/tests/docker/web-setup.sh +0 -15
- lts_backup_cli-2.0.0/tests/unit/test_config.py +0 -122
- lts_backup_cli-2.0.0/tests/unit/test_services.py +0 -251
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/.gitignore +0 -0
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/LICENSE +0 -0
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/features/credentials.feature +0 -0
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/features/environment.py +0 -0
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/features/forget.feature +0 -0
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/features/snapshot.feature +0 -0
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/features/steps/e2e_steps.py +0 -0
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/src/lts_backup_cli/__main__.py +0 -0
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/src/lts_backup_cli/apache.py +0 -0
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/src/lts_backup_cli/mariadb.py +0 -0
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/src/lts_backup_cli/restic.py +0 -0
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/src/lts_backup_cli/schedule.py +0 -0
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/tests/docker/vhosts.conf +0 -0
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/tests/unit/test_adapters.py +0 -0
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/tests/unit/test_apache.py +0 -0
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/tests/unit/test_credentials.py +0 -0
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/tests/unit/test_schedule.py +0 -0
- {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/tests/unit/test_ssh_runner.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# CLAUDE.md — lts-backup-cli engineering guide
|
|
2
2
|
|
|
3
|
-
`lts-backup-cli` (command: `ltsbu`, module: `lts_backup_cli`) is a Python console application. Running on one **admin box
|
|
3
|
+
`lts-backup-cli` (command: `ltsbu`, module: `lts_backup_cli`) is a Python console application. Running on one **backup server** (the admin box, e.g. nas.mylogitools.pt), it **pulls** **servers** over SSH into **local restic repos**: one repo per **Apache/httpd vhost** (DocumentRoot), one **database repo** per server (MariaDB 10.4 databases, one snapshot each), one repo per extra **folder** and one **docker** repo per compose project (site) plus a host `docker` repo (volumes, container definitions, compose dirs, binds, plus a dump per MariaDB container), run by **systemd timers** (every 2h + weekly) and rotated after each run. `ltsbu discover <host>` creates and edits all of it. It targets **Rocky Linux 8.10** and **Ubuntu 22.04**, requires **Python >= 3.9**, and is distributed on **PyPI**. Requirements: `docs/SPEC3.md` (v3; `docs/SPEC2.md` and `docs/SPEC.md` are the superseded v2 and v1).
|
|
4
4
|
|
|
5
5
|
## Role
|
|
6
6
|
|
|
@@ -44,7 +44,7 @@ Adapted from the Ponytail plugin (`DietrichGebert/ponytail`). These rules overri
|
|
|
44
44
|
|
|
45
45
|
| Pattern | Where |
|
|
46
46
|
|---|---|
|
|
47
|
-
| Adapter | `ResticClient`, `MariaDBClient` wrap external binaries |
|
|
47
|
+
| Adapter | `ResticClient`, `MariaDBClient`, `Rsync` wrap external binaries |
|
|
48
48
|
| Strategy | `doctor.Check` implementations |
|
|
49
49
|
| Facade | `services.py` (`BackupService`, `RestoreService`, `ForgetService`, `DiscoverService`) |
|
|
50
50
|
| Command | click subcommands in `cli.py` |
|
|
@@ -52,8 +52,8 @@ Adapted from the Ponytail plugin (`DietrichGebert/ponytail`). These rules overri
|
|
|
52
52
|
|
|
53
53
|
### Code isolation (ports & adapters)
|
|
54
54
|
|
|
55
|
-
- **Pure core**: `validation.py`, `sshconfig.py` and `
|
|
56
|
-
- **The only place that spawns processes** is `runner.py`. Nothing else imports `subprocess`. Remote execution is `SshRunner`, a drop-in `Runner
|
|
55
|
+
- **Pure core**: `validation.py`, `sshconfig.py`, `apache.py` and `docker.py` (text in, text out), `config.py` (dataclasses, lint) and the rendering half of `schedule.py`. These modules do no I/O beyond reading and writing files.
|
|
56
|
+
- **The only place that spawns processes** is `runner.py`. Nothing else imports `subprocess`. Remote execution is `SshRunner`, a drop-in `Runner`. Services get the **local** runner (restic, rsync and pipelines run here) and reach the server with `services.runner_for` / `services.remote_argv` (one side of a local pipeline).
|
|
57
57
|
- **`cli.py`** parses arguments, builds objects, prints output and maps exceptions to exit codes. It holds no business logic.
|
|
58
58
|
|
|
59
59
|
## Security rules (non-negotiable)
|
|
@@ -63,14 +63,15 @@ Adapted from the Ponytail plugin (`DietrichGebert/ponytail`). These rules overri
|
|
|
63
63
|
- Never put secrets in argv or logs:
|
|
64
64
|
- restic uses `--password-file`.
|
|
65
65
|
- MariaDB uses `--defaults-extra-file=<0600 my.cnf>` on the server.
|
|
66
|
-
-
|
|
66
|
+
- The only secret written to a server (the MariaDB `my.cnf`) travels on **stdin** (`services.put_secret`, `umask 077`), never in argv. restic passwords never leave the backup server.
|
|
67
67
|
- Secret files are created with mode `0600`, and secret dirs with mode `0700`.
|
|
68
68
|
- Validate **and** sanitise all user input at the boundary (`validation.py`): labels, hostnames, ports, paths, DB identifiers and unix users.
|
|
69
69
|
- `~/.ssh/config` edits:
|
|
70
70
|
- Only touch the content between `# >>> ltsbu:<label> >>>` and `# <<< ltsbu:<label> <<<`.
|
|
71
71
|
- Back up the file before writing.
|
|
72
|
-
- Write atomically (temp file + `os.replace`
|
|
73
|
-
- DB dumps are streamed (`mariadb-dump | restic backup --stdin
|
|
72
|
+
- Write atomically (temp file + `os.replace`).
|
|
73
|
+
- DB dumps are streamed (`ssh … mariadb-dump | restic backup --stdin`, the pipe running on the backup server) and never touch a disk.
|
|
74
|
+
- rsync's `--delete` only ever targets the local mirror; restores push with rsync **without** `--delete`.
|
|
74
75
|
- Removing a repo (discover) stops timers and drops config; it **never deletes restic data**.
|
|
75
76
|
|
|
76
77
|
## Testing strategy (ISTQB-aligned)
|
|
@@ -79,13 +80,13 @@ Adapted from the Ponytail plugin (`DietrichGebert/ponytail`). These rules overri
|
|
|
79
80
|
|---|---|---|---|
|
|
80
81
|
| Unit / component | pytest | `tests/unit/` | pure functions, adapters' argv via fake `Runner`, services orchestration |
|
|
81
82
|
| Integration / system | behave | `features/` (default tags) | full CLI through `click.testing.CliRunner`, fake runner, temp `$HOME` |
|
|
82
|
-
| Smoke | sh | `tests/smoke/smoke.sh` | the **installed wheel** starts, every command is wired, empty/v1 configs fail correctly; CI after BDD and first in each E2E container |
|
|
83
|
-
| Acceptance / E2E | behave `@e2e` | `features/` + `tests/docker/` | real SSH, restic, MariaDB 10.4
|
|
83
|
+
| Smoke | sh | `tests/smoke/smoke.sh` | the **installed wheel** starts, every command is wired, empty/v1/v2 configs fail correctly; CI after BDD and first in each E2E container |
|
|
84
|
+
| Acceptance / E2E | behave `@e2e` | `features/` + `tests/docker/` | real SSH, rsync, restic, MariaDB 10.4 (docker CLI shimmed), Rocky 8.10 and Ubuntu 22.04 images |
|
|
84
85
|
|
|
85
86
|
### Test design techniques
|
|
86
87
|
|
|
87
88
|
- Use equivalence partitioning and boundary values for every validator, e.g. ports `0, 1, 65535, 65536` and label lengths `0, 1, 63, 64`.
|
|
88
|
-
- Use decision tables for restore modes (repo kind × `--target` / `--database` / `--into`).
|
|
89
|
+
- Use decision tables for restore modes (repo kind × `--target` / `--database` / `--into` / `--container` / `--image`).
|
|
89
90
|
- Use state transitions for SSH config blocks (absent → present → updated → removed, all idempotent) and for `discover` repos (new → edited → unchecked-kept → removed-on-confirm).
|
|
90
91
|
|
|
91
92
|
### Development workflow
|
|
@@ -96,8 +97,10 @@ Adapted from the Ponytail plugin (`DietrichGebert/ponytail`). These rules overri
|
|
|
96
97
|
|
|
97
98
|
## Domain notes (hard-won; keep them true)
|
|
98
99
|
|
|
99
|
-
- **Commands:** `discover`, `doctor`, `list`, `backup`, `snapshot`, `restore`, `forget` and `credentials` (`export` / `import`). `discover` is the wizard (create + edit + remove); restic's own `init` is only an action inside it.
|
|
100
|
-
- **Central pull:** ltsbu
|
|
100
|
+
- **Commands:** `discover`, `doctor`, `list`, `backup`, `snapshot`, `restore`, `forget` and `credentials` (`export` / `import`). `discover` is the wizard (create + edit + remove); restic's own `init` is only an action inside it. v3 is a clean break: a config without `version = 3` is rejected with a hint, never migrated.
|
|
101
|
+
- **Central pull (v3):** ltsbu, restic, the repos (`repo_root`, default `/srv/restic`) and their passwords live **only on the backup server**. Files are rsynced into `mirror_root/<server>/<repo>` (absolute layout kept, `--relative`) and restic backs up that mirror; dumps run on the server and are piped into local restic. A server needs only sshd + rsync (+ MariaDB client / docker). There is no storage SSH block and nothing restic-related on servers. v1/v2 configs are rejected, never migrated. A server with `ssh_host = "localhost"` uses the plain local runner.
|
|
102
|
+
- **Docker repos:** one per compose project (= one per site) plus the host repo `docker`. Discovery (`docker.PROBE`, `docker.propose`) gives each project its named volume dirs, compose working dir and bind sources that exist on the host (Portainer's `/data/compose/N` are container paths, dropped; shared binds are copied into each site), and the host repo `<DockerRootDir>/containers` (container definitions: the `docker run` Bitbucket runners exist only there) + `<DockerRootDir>/volumes` **excluding every project-owned volume**, so each volume is copied exactly once. Excludes `*-json.log` and the datadir volume of each dumped container. Containers with a `/var/lib/mysql` mount are dumped with `docker exec <c> mariadb-dump --all-databases` (constant script, name as argv). The files snapshot is tagged `files` so restore never picks a dump. Images listed in a docker repo's `images` are `docker save`d into restic on the **weekly** run only (GBs each; `<flat-name>.tar`, tag `image:<ref>`); `restore --image` streams one back into `docker load`. After each pull, mirror entries no repo path covers are removed (`rsync.stale`), so a dropped path leaves later snapshots. Reading the Docker root needs SSH as root.
|
|
103
|
+
- **rsync exit 24** (files vanished during the transfer) is a warning, not a failure: servers are live.
|
|
101
104
|
- **Binaries are resolved on the server** (`mariadb.probe_binaries`), never with a local `shutil.which`: MariaDB 10.4 may only ship the `mysql*` names.
|
|
102
105
|
- **One snapshot per database:** `--stdin-filename <db>.sql` gives each DB a stable path, so restic finds the parent and `forget`'s default grouping (host, paths) rotates per DB. Every backup passes `--host <server label>`.
|
|
103
106
|
- **Tag filters are AND-ed with one comma-joined `--tag a,b`.** Repeated `--tag` flags would OR them.
|
|
@@ -108,9 +111,9 @@ Adapted from the Ponytail plugin (`DietrichGebert/ponytail`). These rules overri
|
|
|
108
111
|
- **Restore runs on the server** against its localhost MariaDB; there is no cross-server restore.
|
|
109
112
|
- **ssh reads `~/.ssh/config` from the passwd home, not `$HOME`.** `sshconfig.default_path()` mirrors that. Tests must inject `ssh_config_path` and never touch the developer's real file.
|
|
110
113
|
- **The managed SSH block uses `StrictHostKeyChecking accept-new`.** Unattended runs on a fresh machine would otherwise fail with BatchMode.
|
|
111
|
-
- **Rocky 8: EPEL 8 has no restic.** Use the official release binary (README and `Dockerfile.rocky8`).
|
|
112
|
-
- **Ubuntu 22.04 ships restic 0.12.1.** Only use restic flags that exist in 0.12 (`--stdin-filename`, `--host`, `--keep-hourly` do).
|
|
113
|
-
- **E2E:** each OS image is the web server (sshd, vhost config,
|
|
114
|
+
- **Rocky 8: EPEL 8 has no restic.** Use the official release binary (README and `Dockerfile.rocky8`). restic is only needed on the backup server.
|
|
115
|
+
- **Ubuntu 22.04 ships restic 0.12.1.** Only use restic flags that exist in 0.12 (`--stdin-filename`, `--host`, `--keep-hourly` do) and keep `--stdin-filename` flat (no `/`).
|
|
116
|
+
- **E2E:** each OS image is the web server (sshd, rsync, vhost config, MariaDB client on the shared `/run/mysqld` socket, user `webadmin`, a `docker` CLI shim in `tests/docker/docker-shim.sh` whose `exec` runs locally against the shared MariaDB) **and** the backup server (restic, local repos under the scenario's tmp dir), which pulls from it over real SSH. systemd isn't available in the containers, so E2E runs `discover --no-timers` and unit files are covered by unit tests. The smoke script (`tests/smoke/smoke.sh`) runs first, against the installed package.
|
|
114
117
|
|
|
115
118
|
## Knowledge graph (graphify)
|
|
116
119
|
|
|
@@ -128,7 +131,7 @@ pytest -q --cov=lts_backup_cli # unit
|
|
|
128
131
|
behave --tags=~@e2e # BDD (fast, no network)
|
|
129
132
|
docker compose -f tests/docker/docker-compose.yml run --rm --build rocky8 # E2E (entrypoint runs behave @e2e)
|
|
130
133
|
docker compose -f tests/docker/docker-compose.yml run --rm --build ubuntu2204
|
|
131
|
-
docker compose -f tests/docker/docker-compose.yml down && rm -
|
|
134
|
+
docker compose -f tests/docker/docker-compose.yml down && docker run --rm --entrypoint rm -v "$PWD/tests/docker:/d" mariadb:10.4 -rf /d/.e2e
|
|
132
135
|
python -m build && twine check dist/* # packaging
|
|
133
136
|
python -m venv /tmp/smoke && /tmp/smoke/bin/pip install dist/*.whl \
|
|
134
137
|
&& PATH=/tmp/smoke/bin:$PATH PYTHON=/tmp/smoke/bin/python sh tests/smoke/smoke.sh # smoke
|