lts-backup-cli 2.0.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 (55) hide show
  1. lts_backup_cli-2.0.0/.gitignore +36 -0
  2. lts_backup_cli-2.0.0/CLAUDE.md +150 -0
  3. lts_backup_cli-2.0.0/LICENSE +29 -0
  4. lts_backup_cli-2.0.0/PKG-INFO +522 -0
  5. lts_backup_cli-2.0.0/README.md +487 -0
  6. lts_backup_cli-2.0.0/features/backup.feature +68 -0
  7. lts_backup_cli-2.0.0/features/credentials.feature +69 -0
  8. lts_backup_cli-2.0.0/features/discover.feature +77 -0
  9. lts_backup_cli-2.0.0/features/doctor.feature +39 -0
  10. lts_backup_cli-2.0.0/features/e2e.feature +67 -0
  11. lts_backup_cli-2.0.0/features/environment.py +48 -0
  12. lts_backup_cli-2.0.0/features/forget.feature +29 -0
  13. lts_backup_cli-2.0.0/features/list.feature +28 -0
  14. lts_backup_cli-2.0.0/features/restore.feature +72 -0
  15. lts_backup_cli-2.0.0/features/snapshot.feature +21 -0
  16. lts_backup_cli-2.0.0/features/steps/cli_steps.py +363 -0
  17. lts_backup_cli-2.0.0/features/steps/e2e_steps.py +85 -0
  18. lts_backup_cli-2.0.0/pyproject.toml +80 -0
  19. lts_backup_cli-2.0.0/src/lts_backup_cli/__init__.py +3 -0
  20. lts_backup_cli-2.0.0/src/lts_backup_cli/__main__.py +3 -0
  21. lts_backup_cli-2.0.0/src/lts_backup_cli/apache.py +45 -0
  22. lts_backup_cli-2.0.0/src/lts_backup_cli/cli.py +559 -0
  23. lts_backup_cli-2.0.0/src/lts_backup_cli/config.py +308 -0
  24. lts_backup_cli-2.0.0/src/lts_backup_cli/doctor.py +278 -0
  25. lts_backup_cli-2.0.0/src/lts_backup_cli/mariadb.py +157 -0
  26. lts_backup_cli-2.0.0/src/lts_backup_cli/restic.py +122 -0
  27. lts_backup_cli-2.0.0/src/lts_backup_cli/runner.py +172 -0
  28. lts_backup_cli-2.0.0/src/lts_backup_cli/schedule.py +111 -0
  29. lts_backup_cli-2.0.0/src/lts_backup_cli/services.py +604 -0
  30. lts_backup_cli-2.0.0/src/lts_backup_cli/sshconfig.py +142 -0
  31. lts_backup_cli-2.0.0/src/lts_backup_cli/validation.py +189 -0
  32. lts_backup_cli-2.0.0/src/lts_backup_cli/wizard.py +419 -0
  33. lts_backup_cli-2.0.0/tests/docker/Dockerfile.rocky8 +20 -0
  34. lts_backup_cli-2.0.0/tests/docker/Dockerfile.sftp +16 -0
  35. lts_backup_cli-2.0.0/tests/docker/Dockerfile.ubuntu2204 +17 -0
  36. lts_backup_cli-2.0.0/tests/docker/docker-compose.yml +38 -0
  37. lts_backup_cli-2.0.0/tests/docker/e2e.sh +25 -0
  38. lts_backup_cli-2.0.0/tests/docker/vhosts.conf +9 -0
  39. lts_backup_cli-2.0.0/tests/docker/web-setup.sh +15 -0
  40. lts_backup_cli-2.0.0/tests/smoke/smoke.sh +32 -0
  41. lts_backup_cli-2.0.0/tests/support/fake_runner.py +91 -0
  42. lts_backup_cli-2.0.0/tests/unit/conftest.py +73 -0
  43. lts_backup_cli-2.0.0/tests/unit/test_adapters.py +152 -0
  44. lts_backup_cli-2.0.0/tests/unit/test_apache.py +61 -0
  45. lts_backup_cli-2.0.0/tests/unit/test_config.py +122 -0
  46. lts_backup_cli-2.0.0/tests/unit/test_credentials.py +196 -0
  47. lts_backup_cli-2.0.0/tests/unit/test_discover.py +193 -0
  48. lts_backup_cli-2.0.0/tests/unit/test_doctor.py +177 -0
  49. lts_backup_cli-2.0.0/tests/unit/test_runner.py +32 -0
  50. lts_backup_cli-2.0.0/tests/unit/test_schedule.py +111 -0
  51. lts_backup_cli-2.0.0/tests/unit/test_services.py +251 -0
  52. lts_backup_cli-2.0.0/tests/unit/test_ssh_runner.py +57 -0
  53. lts_backup_cli-2.0.0/tests/unit/test_sshconfig.py +73 -0
  54. lts_backup_cli-2.0.0/tests/unit/test_validation.py +213 -0
  55. lts_backup_cli-2.0.0/tests/unit/test_wizard.py +197 -0
@@ -0,0 +1,36 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ build/
6
+ dist/
7
+ dist-smoke/
8
+ .mypy_cache/
9
+ .ruff_cache/
10
+ .pytest_cache/
11
+ .coverage
12
+ htmlcov/
13
+ test-reports/
14
+ # graphify: keep the human-readable report, ignore generated artefacts
15
+ graphify-out/*
16
+ !graphify-out/GRAPH_REPORT.md
17
+ # from the Bitbucket template
18
+ node_modules/
19
+ *.class
20
+ *.log
21
+ *.jar
22
+ target/
23
+ .idea/
24
+ TEST*.xml
25
+ .DS_Store
26
+ Thumbs.db
27
+ *.app
28
+ *.exe
29
+ *.war
30
+ *.mp4
31
+ *.tiff
32
+ *.avi
33
+ *.flv
34
+ *.mov
35
+ *.wmv
36
+ tests/docker/.e2e/
@@ -0,0 +1,150 @@
1
+ # CLAUDE.md — lts-backup-cli engineering guide
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).
4
+
5
+ ## Role
6
+
7
+ Work as three people at once:
8
+
9
+ - **Senior network architect**: think about SSH reachability, DNS resolution, ports, timeouts, key-based auth and least privilege. Every network dependency gets a `doctor` check.
10
+ - **Senior software engineer**: write clean, small and typed code that follows the principles below.
11
+ - **ISTQB-certified tester**: design tests deliberately, with risk-based prioritisation and explicit test techniques, and never treat "it ran once" as verification.
12
+
13
+ ## Ponytail rules (lazy senior dev)
14
+
15
+ Adapted from the Ponytail plugin (`DietrichGebert/ponytail`). These rules override any temptation to over-engineer.
16
+
17
+ 1. **The best code is the code never written.** Take the first option that fully works:
18
+ 1. Skip it if nobody asked for it.
19
+ 2. Reuse something already in this codebase.
20
+ 3. Use the stdlib.
21
+ 4. Use an installed dependency.
22
+ 5. Use one clear line.
23
+ 6. Otherwise write the minimum code.
24
+ 2. **Be lazy about the solution, never about the change.** Update every caller, test, fixture and doc the change touches.
25
+ 3. **No speculative code.** That means no wrapper, option, config or abstraction "for later".
26
+ 4. **Keep the structure the codebase already has.** Deletion beats addition.
27
+ 5. **Comment only the *why*,** in one line.
28
+ 6. **Mark known limits** with `# shortcut: <the limit>, <when to upgrade>`.
29
+ 7. **Non-trivial logic ships with a test.** That covers branches, parsers, loops and anything security-related.
30
+ 8. **Never cut** validation at trust boundaries, data-loss guards, security, or anything the user asked for.
31
+ 9. **End each work report** with what was skipped or not verified, plus any risk.
32
+
33
+ ## Design principles
34
+
35
+ ### SOLID, applied here
36
+
37
+ - **S**: each module has one reason to change. `restic.py` only knows restic, `mariadb.py` only knows MariaDB, and `sshconfig.py` only knows text blocks.
38
+ - **O**: new `doctor` checks are added as new `Check` objects in the registry, not as `if` chains.
39
+ - **L**: `SubprocessRunner`, `DryRunRunner` and test fakes are interchangeable wherever a `Runner` is expected.
40
+ - **I**: adapters expose only the operations the services use.
41
+ - **D**: services depend on the `Runner` protocol, and concrete runners are injected by `cli.py`, which is the composition root.
42
+
43
+ ### Patterns in use (and only these, unless a real need appears)
44
+
45
+ | Pattern | Where |
46
+ |---|---|
47
+ | Adapter | `ResticClient`, `MariaDBClient` wrap external binaries |
48
+ | Strategy | `doctor.Check` implementations |
49
+ | Facade | `services.py` (`BackupService`, `RestoreService`, `ForgetService`, `DiscoverService`) |
50
+ | Command | click subcommands in `cli.py` |
51
+ | Dependency Injection | `Runner` passed into adapters; prompt function passed into the wizard |
52
+
53
+ ### Code isolation (ports & adapters)
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`).
57
+ - **`cli.py`** parses arguments, builds objects, prints output and maps exceptions to exit codes. It holds no business logic.
58
+
59
+ ## Security rules (non-negotiable)
60
+
61
+ - Never use `shell=True`. Commands are always argv lists.
62
+ - **Over SSH the remote shell re-parses the command line**, so `SshRunner` `shlex.join`s every argv, and pipes go through `runner.pipeline()` (`bash -o pipefail -c` with each side quoted). Shell scripts sent to a server are **constants**: user data only travels as positional args (`"$1"`) or on stdin.
63
+ - Never put secrets in argv or logs:
64
+ - restic uses `--password-file`.
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.
67
+ - Secret files are created with mode `0600`, and secret dirs with mode `0700`.
68
+ - Validate **and** sanitise all user input at the boundary (`validation.py`): labels, hostnames, ports, paths, DB identifiers and unix users.
69
+ - `~/.ssh/config` edits:
70
+ - Only touch the content between `# >>> ltsbu:<label> >>>` and `# <<< ltsbu:<label> <<<`.
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.
74
+ - Removing a repo (discover) stops timers and drops config; it **never deletes restic data**.
75
+
76
+ ## Testing strategy (ISTQB-aligned)
77
+
78
+ | Level | Tool | Location | What |
79
+ |---|---|---|---|
80
+ | Unit / component | pytest | `tests/unit/` | pure functions, adapters' argv via fake `Runner`, services orchestration |
81
+ | 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 |
84
+
85
+ ### Test design techniques
86
+
87
+ - 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 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
+ ### Development workflow
92
+
93
+ - **Risk-based priority**: data loss (restore and dump cleanup) comes first, then security (secrets, injection), then connectivity.
94
+ - **TDD**: red → green → refactor. Write the failing test first and make it pass with the least code.
95
+ - **BDD**: a user-visible behaviour starts life as a Gherkin scenario in `features/*.feature`.
96
+
97
+ ## Domain notes (hard-won; keep them true)
98
+
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.
101
+ - **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
+ - **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
+ - **Tag filters are AND-ed with one comma-joined `--tag a,b`.** Repeated `--tag` flags would OR them.
104
+ - **The database is optional** everywhere. Servers without a db repo must never need MariaDB binaries (`doctor` only warns).
105
+ - **Snapshots are selected by the `server:` / `repo:` (/ `db:`) tags, never by hostname.**
106
+ - **Rotation runs after every backup** (`forget` with the repo's keep policy); the weekly timer adds `--prune`. A global `flock` on the admin box (`services.backup_lock`) keeps backups from overlapping. Default schedules are staggered deterministically (`schedule.default_schedule`).
107
+ - **`credentials` moves site DB users as validated fields** (user, host, `mysql_native_password` hash, db-level privileges), never as SQL text. The import rebuilds the SQL from validated values and sends it on stdin, because the bundle is untrusted input that runs as MariaDB root. With `--force`, an existing server **keeps its local connection settings** (they are per admin box), which is what the disaster-recovery flow relies on.
108
+ - **Restore runs on the server** against its localhost MariaDB; there is no cross-server restore.
109
+ - **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
+ - **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
+
115
+ ## Knowledge graph (graphify)
116
+
117
+ - After structural changes (new modules, moved responsibilities), run `/graphify . --update`.
118
+ - For architecture or "where does X happen" questions, query the graph first with `/graphify query "<question>"`.
119
+ - `graphify-out/` is gitignored except `GRAPH_REPORT.md`, which is committed as living architecture documentation.
120
+
121
+ ## Dev commands
122
+
123
+ ```bash
124
+ python3 -m venv .venv && . .venv/bin/activate
125
+ pip install -e '.[dev]'
126
+ ruff check . && ruff format --check . && mypy src # static
127
+ pytest -q --cov=lts_backup_cli # unit
128
+ behave --tags=~@e2e # BDD (fast, no network)
129
+ docker compose -f tests/docker/docker-compose.yml run --rm --build rocky8 # E2E (entrypoint runs behave @e2e)
130
+ 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
132
+ python -m build && twine check dist/* # packaging
133
+ python -m venv /tmp/smoke && /tmp/smoke/bin/pip install dist/*.whl \
134
+ && PATH=/tmp/smoke/bin:$PATH PYTHON=/tmp/smoke/bin/python sh tests/smoke/smoke.sh # smoke
135
+ ```
136
+
137
+ ## CI/CD (`bitbucket-pipelines.yml`)
138
+
139
+ 1. **PRs / every branch**: lint, type checks and unit tests (py3.9 and py3.12) run in parallel, then behave without `@e2e`, then smoke on the built wheel.
140
+ 2. **`main`**: everything above, plus E2E on Rocky 8.10 and Ubuntu 22.04 (Docker service), then build and `twine check`.
141
+ 3. **Tags `v*`**: build, then `twine upload` using the secured repository variable `PYPI_TOKEN` (deployment: production).
142
+
143
+ ## Definition of Done
144
+
145
+ - Tests were written first and pass: pytest, behave, and E2E for any behaviour that touches restic, MariaDB or SSH.
146
+ - `ruff`, `ruff format --check` and `mypy` are clean.
147
+ - `README.md` is updated for any user-visible change (commands, flags, config keys).
148
+ - No secrets in argv or logs, and inputs are validated.
149
+ - The graph is refreshed if the structure changed.
150
+ - The work report lists anything skipped or unverified.
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, joaomartins@logitools.pt and LTS, Lda.
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.