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.
Files changed (67) hide show
  1. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/CLAUDE.md +19 -16
  2. lts_backup_cli-3.2.0/PKG-INFO +589 -0
  3. lts_backup_cli-3.2.0/README.md +554 -0
  4. lts_backup_cli-3.2.0/features/backup.feature +115 -0
  5. lts_backup_cli-3.2.0/features/discover.feature +126 -0
  6. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/features/doctor.feature +3 -2
  7. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/features/e2e.feature +35 -6
  8. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/features/list.feature +13 -2
  9. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/features/restore.feature +18 -2
  10. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/features/steps/cli_steps.py +82 -11
  11. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/pyproject.toml +2 -2
  12. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/src/lts_backup_cli/__init__.py +1 -1
  13. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/src/lts_backup_cli/cli.py +49 -29
  14. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/src/lts_backup_cli/config.py +27 -35
  15. lts_backup_cli-3.2.0/src/lts_backup_cli/docker.py +175 -0
  16. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/src/lts_backup_cli/doctor.py +45 -29
  17. lts_backup_cli-3.2.0/src/lts_backup_cli/rsync.py +68 -0
  18. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/src/lts_backup_cli/runner.py +5 -1
  19. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/src/lts_backup_cli/services.py +181 -59
  20. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/src/lts_backup_cli/sshconfig.py +0 -9
  21. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/src/lts_backup_cli/validation.py +25 -9
  22. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/src/lts_backup_cli/wizard.py +96 -31
  23. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/tests/docker/Dockerfile.rocky8 +5 -3
  24. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/tests/docker/Dockerfile.ubuntu2204 +5 -3
  25. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/tests/docker/docker-compose.yml +6 -10
  26. lts_backup_cli-3.2.0/tests/docker/docker-shim.sh +25 -0
  27. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/tests/docker/e2e.sh +7 -5
  28. lts_backup_cli-3.2.0/tests/docker/web-setup.sh +35 -0
  29. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/tests/smoke/smoke.sh +11 -2
  30. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/tests/support/fake_runner.py +16 -5
  31. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/tests/unit/conftest.py +2 -1
  32. lts_backup_cli-3.2.0/tests/unit/test_config.py +188 -0
  33. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/tests/unit/test_discover.py +54 -10
  34. lts_backup_cli-3.2.0/tests/unit/test_docker.py +186 -0
  35. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/tests/unit/test_doctor.py +84 -15
  36. lts_backup_cli-3.2.0/tests/unit/test_rsync.py +122 -0
  37. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/tests/unit/test_runner.py +7 -1
  38. lts_backup_cli-3.2.0/tests/unit/test_services.py +474 -0
  39. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/tests/unit/test_sshconfig.py +0 -4
  40. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/tests/unit/test_validation.py +41 -12
  41. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/tests/unit/test_wizard.py +147 -3
  42. lts_backup_cli-2.0.0/PKG-INFO +0 -522
  43. lts_backup_cli-2.0.0/README.md +0 -487
  44. lts_backup_cli-2.0.0/features/backup.feature +0 -68
  45. lts_backup_cli-2.0.0/features/discover.feature +0 -77
  46. lts_backup_cli-2.0.0/tests/docker/Dockerfile.sftp +0 -16
  47. lts_backup_cli-2.0.0/tests/docker/web-setup.sh +0 -15
  48. lts_backup_cli-2.0.0/tests/unit/test_config.py +0 -122
  49. lts_backup_cli-2.0.0/tests/unit/test_services.py +0 -251
  50. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/.gitignore +0 -0
  51. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/LICENSE +0 -0
  52. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/features/credentials.feature +0 -0
  53. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/features/environment.py +0 -0
  54. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/features/forget.feature +0 -0
  55. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/features/snapshot.feature +0 -0
  56. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/features/steps/e2e_steps.py +0 -0
  57. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/src/lts_backup_cli/__main__.py +0 -0
  58. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/src/lts_backup_cli/apache.py +0 -0
  59. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/src/lts_backup_cli/mariadb.py +0 -0
  60. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/src/lts_backup_cli/restic.py +0 -0
  61. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/src/lts_backup_cli/schedule.py +0 -0
  62. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/tests/docker/vhosts.conf +0 -0
  63. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/tests/unit/test_adapters.py +0 -0
  64. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/tests/unit/test_apache.py +0 -0
  65. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/tests/unit/test_credentials.py +0 -0
  66. {lts_backup_cli-2.0.0 → lts_backup_cli-3.2.0}/tests/unit/test_schedule.py +0 -0
  67. {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**, it backs up **servers** over SSH: one restic repo per **Apache/httpd vhost** (DocumentRoot), one **database repo** per server (MariaDB 10.4 databases, one snapshot each) and one repo per extra **folder**, all stored on **SFTP storage**, 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/SPEC2.md` (v2; `docs/SPEC.md` is the superseded v1).
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 `apache.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 a runner already scoped to the server (`services.runner_for`).
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
- - Secrets reach a server on **stdin** (`services.put_secret`, `umask 077`), never in argv.
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` locally, temp file + `mv` on a server).
73
- - DB dumps are streamed (`mariadb-dump | restic backup --stdin`) and never touch a disk.
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, SFTP storage, Rocky 8.10 and Ubuntu 22.04 images |
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. v2 is a clean break: a config without `version = 2` is rejected with a hint, never migrated.
100
- - **Central pull:** ltsbu runs on the admin box; restic and the MariaDB client run **on the web server** over SSH, and the server reaches the storage through its own managed block `ltsbu:storage-<server>` in **its** `~/.ssh/config`. A server with `ssh_host = "localhost"` uses the plain local runner.
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, restic, MariaDB client on the shared `/run/mysqld` socket, user `webadmin`) **and** the admin box, which reaches 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
+ - **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 -rf tests/docker/.e2e
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