nanio-orchestrator 0.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 (79) hide show
  1. nanio_orchestrator-0.2.0/LICENSE +21 -0
  2. nanio_orchestrator-0.2.0/PKG-INFO +663 -0
  3. nanio_orchestrator-0.2.0/README.md +619 -0
  4. nanio_orchestrator-0.2.0/nanio_orchestrator/__init__.py +3 -0
  5. nanio_orchestrator-0.2.0/nanio_orchestrator/__main__.py +5 -0
  6. nanio_orchestrator-0.2.0/nanio_orchestrator/api/__init__.py +1 -0
  7. nanio_orchestrator-0.2.0/nanio_orchestrator/api/audit.py +48 -0
  8. nanio_orchestrator-0.2.0/nanio_orchestrator/api/buckets.py +512 -0
  9. nanio_orchestrator-0.2.0/nanio_orchestrator/api/config.py +661 -0
  10. nanio_orchestrator-0.2.0/nanio_orchestrator/api/credentials.py +142 -0
  11. nanio_orchestrator-0.2.0/nanio_orchestrator/api/health.py +41 -0
  12. nanio_orchestrator-0.2.0/nanio_orchestrator/api/migrations.py +469 -0
  13. nanio_orchestrator-0.2.0/nanio_orchestrator/api/pools.py +645 -0
  14. nanio_orchestrator-0.2.0/nanio_orchestrator/api/vhosts.py +663 -0
  15. nanio_orchestrator-0.2.0/nanio_orchestrator/app.py +160 -0
  16. nanio_orchestrator-0.2.0/nanio_orchestrator/audit_log.py +59 -0
  17. nanio_orchestrator-0.2.0/nanio_orchestrator/auth.py +100 -0
  18. nanio_orchestrator-0.2.0/nanio_orchestrator/backup.py +99 -0
  19. nanio_orchestrator-0.2.0/nanio_orchestrator/bucket_sync.py +270 -0
  20. nanio_orchestrator-0.2.0/nanio_orchestrator/cli.py +492 -0
  21. nanio_orchestrator-0.2.0/nanio_orchestrator/config.py +104 -0
  22. nanio_orchestrator-0.2.0/nanio_orchestrator/credentials.py +128 -0
  23. nanio_orchestrator-0.2.0/nanio_orchestrator/db.py +469 -0
  24. nanio_orchestrator-0.2.0/nanio_orchestrator/drift.py +95 -0
  25. nanio_orchestrator-0.2.0/nanio_orchestrator/install.py +331 -0
  26. nanio_orchestrator-0.2.0/nanio_orchestrator/migration_engine.py +1194 -0
  27. nanio_orchestrator-0.2.0/nanio_orchestrator/models.py +462 -0
  28. nanio_orchestrator-0.2.0/nanio_orchestrator/nginx/__init__.py +1 -0
  29. nanio_orchestrator-0.2.0/nanio_orchestrator/nginx/executor.py +107 -0
  30. nanio_orchestrator-0.2.0/nanio_orchestrator/nginx/generator.py +289 -0
  31. nanio_orchestrator-0.2.0/nanio_orchestrator/nginx/parser.py +255 -0
  32. nanio_orchestrator-0.2.0/nanio_orchestrator/nginx/templates/nanio_options.toml.j2 +13 -0
  33. nanio_orchestrator-0.2.0/nanio_orchestrator/nginx/templates/nanio_service.j2 +29 -0
  34. nanio_orchestrator-0.2.0/nanio_orchestrator/nginx/templates/node_nginx_http.conf.j2 +19 -0
  35. nanio_orchestrator-0.2.0/nanio_orchestrator/nginx/templates/node_nginx_nanio.conf.j2 +23 -0
  36. nanio_orchestrator-0.2.0/nanio_orchestrator/nginx/templates/upstream.conf.j2 +19 -0
  37. nanio_orchestrator-0.2.0/nanio_orchestrator/nginx/templates/vhost.conf.j2 +105 -0
  38. nanio_orchestrator-0.2.0/nanio_orchestrator/rebuild.py +472 -0
  39. nanio_orchestrator-0.2.0/nanio_orchestrator/s3client.py +445 -0
  40. nanio_orchestrator-0.2.0/nanio_orchestrator/sidecar.py +259 -0
  41. nanio_orchestrator-0.2.0/nanio_orchestrator/web/__init__.py +1 -0
  42. nanio_orchestrator-0.2.0/nanio_orchestrator/web/routes.py +278 -0
  43. nanio_orchestrator-0.2.0/nanio_orchestrator/web/static/app.js +911 -0
  44. nanio_orchestrator-0.2.0/nanio_orchestrator/web/static/style.css +232 -0
  45. nanio_orchestrator-0.2.0/nanio_orchestrator/web/templates/audit.html +70 -0
  46. nanio_orchestrator-0.2.0/nanio_orchestrator/web/templates/base.html +40 -0
  47. nanio_orchestrator-0.2.0/nanio_orchestrator/web/templates/buckets.html +459 -0
  48. nanio_orchestrator-0.2.0/nanio_orchestrator/web/templates/config.html +69 -0
  49. nanio_orchestrator-0.2.0/nanio_orchestrator/web/templates/dashboard.html +162 -0
  50. nanio_orchestrator-0.2.0/nanio_orchestrator/web/templates/login.html +93 -0
  51. nanio_orchestrator-0.2.0/nanio_orchestrator/web/templates/migrations.html +115 -0
  52. nanio_orchestrator-0.2.0/nanio_orchestrator/web/templates/pools.html +181 -0
  53. nanio_orchestrator-0.2.0/nanio_orchestrator/web/templates/settings.html +318 -0
  54. nanio_orchestrator-0.2.0/nanio_orchestrator/web/templates/vhosts.html +202 -0
  55. nanio_orchestrator-0.2.0/nanio_orchestrator.egg-info/PKG-INFO +663 -0
  56. nanio_orchestrator-0.2.0/nanio_orchestrator.egg-info/SOURCES.txt +77 -0
  57. nanio_orchestrator-0.2.0/nanio_orchestrator.egg-info/dependency_links.txt +1 -0
  58. nanio_orchestrator-0.2.0/nanio_orchestrator.egg-info/entry_points.txt +2 -0
  59. nanio_orchestrator-0.2.0/nanio_orchestrator.egg-info/requires.txt +16 -0
  60. nanio_orchestrator-0.2.0/nanio_orchestrator.egg-info/top_level.txt +1 -0
  61. nanio_orchestrator-0.2.0/pyproject.toml +79 -0
  62. nanio_orchestrator-0.2.0/setup.cfg +4 -0
  63. nanio_orchestrator-0.2.0/tests/test_audit.py +33 -0
  64. nanio_orchestrator-0.2.0/tests/test_auth.py +52 -0
  65. nanio_orchestrator-0.2.0/tests/test_buckets.py +290 -0
  66. nanio_orchestrator-0.2.0/tests/test_config.py +74 -0
  67. nanio_orchestrator-0.2.0/tests/test_credentials.py +112 -0
  68. nanio_orchestrator-0.2.0/tests/test_db.py +62 -0
  69. nanio_orchestrator-0.2.0/tests/test_drift.py +57 -0
  70. nanio_orchestrator-0.2.0/tests/test_encryption.py +68 -0
  71. nanio_orchestrator-0.2.0/tests/test_health.py +18 -0
  72. nanio_orchestrator-0.2.0/tests/test_live_migration.py +763 -0
  73. nanio_orchestrator-0.2.0/tests/test_migrations.py +600 -0
  74. nanio_orchestrator-0.2.0/tests/test_node_config.py +58 -0
  75. nanio_orchestrator-0.2.0/tests/test_pools.py +115 -0
  76. nanio_orchestrator-0.2.0/tests/test_rebuild.py +1056 -0
  77. nanio_orchestrator-0.2.0/tests/test_s3client.py +47 -0
  78. nanio_orchestrator-0.2.0/tests/test_vhosts.py +260 -0
  79. nanio_orchestrator-0.2.0/tests/test_web.py +40 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nuno Cardoso
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,663 @@
1
+ Metadata-Version: 2.4
2
+ Name: nanio-orchestrator
3
+ Version: 0.2.0
4
+ Summary: Nginx configuration manager and gateway orchestrator for nanio S3-compatible storage clusters
5
+ Author-email: Nuno Kisc <nuno@kisc.pt>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/nunokisc/nanio-orchestrator
8
+ Project-URL: Repository, https://github.com/nunokisc/nanio-orchestrator
9
+ Project-URL: Documentation, https://github.com/nunokisc/nanio-orchestrator/tree/main/docs
10
+ Project-URL: Bug Tracker, https://github.com/nunokisc/nanio-orchestrator/issues
11
+ Keywords: nginx,s3,storage,orchestrator,nanio,gateway
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: No Input/Output (Daemon)
14
+ Classifier: Framework :: FastAPI
15
+ Classifier: Intended Audience :: System Administrators
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Topic :: Internet :: Proxy Servers
24
+ Classifier: Topic :: System :: Systems Administration
25
+ Requires-Python: >=3.9
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: fastapi>=0.110
29
+ Requires-Dist: uvicorn[standard]>=0.29
30
+ Requires-Dist: aiosqlite>=0.20
31
+ Requires-Dist: jinja2>=3.1
32
+ Requires-Dist: python-multipart>=0.0.9
33
+ Requires-Dist: pydantic-settings>=2.2
34
+ Requires-Dist: click>=8.1
35
+ Requires-Dist: aiofiles>=23.2
36
+ Requires-Dist: cryptography>=42.0
37
+ Provides-Extra: dev
38
+ Requires-Dist: pytest>=8.0; extra == "dev"
39
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
40
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
41
+ Requires-Dist: httpx>=0.27; extra == "dev"
42
+ Requires-Dist: ruff>=0.4; extra == "dev"
43
+ Dynamic: license-file
44
+
45
+ # nanio-orchestrator
46
+
47
+ Nginx configuration manager and gateway orchestrator for a distributed nanio S3-compatible storage cluster.
48
+ It is a **control plane tool only** — traffic never flows through it. If the orchestrator is stopped or
49
+ crashes, nginx keeps serving traffic exactly as configured.
50
+
51
+ ## Architecture
52
+
53
+ ```
54
+ CLIENT (S3 SDK / browser / aws-cli)
55
+ │ HTTPS
56
+
57
+ NGINX (gateway machine)
58
+ │ proxy_pass only
59
+ ├─► upstream pool-2025 → nanio instances → /data/2025/
60
+ ├─► upstream pool-2026 → nanio instances → /data/2026/
61
+ └─► upstream pool-cdn → nginx instances → serve files via root/alias
62
+
63
+ ORCHESTRATOR (:8080, internal only)
64
+ │ writes config files + signals nginx
65
+ ├─► /etc/nginx/nanio/pools/*.conf (upstream blocks)
66
+ ├─► /etc/nginx/nanio/pools/*.meta.json (sidecar: pool type, description, encrypted credentials)
67
+ ├─► /etc/nginx/nanio/vhosts/*.conf (server blocks, proxy_pass only)
68
+ ├─► /etc/nginx/nanio/vhosts/*.meta.json (sidecar: default pool)
69
+ ├─► SQLite at /opt/nanio-orchestrator/data/orchestrator.db
70
+ ├─► SQLite backup at /opt/nanio-orchestrator/data/orchestrator.db.bak (+ rotated copies)
71
+ └─► /opt/nanio-orchestrator/data/migrations/*.state.json (in-progress migration state — alongside DB)
72
+ ```
73
+
74
+ ## Quick Start — Production
75
+
76
+ ### Method A: pipx (recommended — installs into an isolated env, exposes the CLI globally)
77
+
78
+ ```bash
79
+ pipx install nanio-orchestrator
80
+ sudo nanio-orchestrator install
81
+ ```
82
+
83
+ ### Method B: uv tool
84
+
85
+ ```bash
86
+ uv tool install nanio-orchestrator
87
+ sudo nanio-orchestrator install
88
+ ```
89
+
90
+ ### Method C: pip (into a venv)
91
+
92
+ ```bash
93
+ python3 -m venv /opt/nanio-orchestrator/venv
94
+ /opt/nanio-orchestrator/venv/bin/pip install nanio-orchestrator
95
+ sudo /opt/nanio-orchestrator/venv/bin/nanio-orchestrator install
96
+ ```
97
+
98
+ After install, follow the printed instructions to configure and start the service.
99
+
100
+ ### Required nginx.conf Change
101
+
102
+ The orchestrator writes config files under `/etc/nginx/nanio/` but nginx only loads them if you
103
+ add the following includes to your nginx `http {}` block (e.g. `/etc/nginx/nginx.conf`):
104
+
105
+ ```nginx
106
+ http {
107
+ # ... existing config ...
108
+
109
+ include /etc/nginx/nanio/pools/*.conf; # upstream blocks
110
+ include /etc/nginx/nanio/vhosts/*.conf; # server blocks
111
+ }
112
+ ```
113
+
114
+ > The install command prints this reminder. Without these includes nginx serves no nanio traffic,
115
+ > and `nginx -t` will report "unknown upstream" errors for any vhost that references a pool.
116
+
117
+ ## Quick Start — Development
118
+
119
+ ```bash
120
+ git clone https://github.com/nunokisc/nanio-orchestrator
121
+ cd nanio-orchestrator
122
+ make install-dev # creates .venv, installs deps
123
+ make run # dev server at http://localhost:8080
124
+ ```
125
+
126
+ Or manually:
127
+
128
+ ```bash
129
+ python3 -m venv .venv
130
+ source .venv/bin/activate
131
+ pip install -e ".[dev]"
132
+ python -m nanio_orchestrator
133
+ ```
134
+
135
+ Dev mode is auto-detected when `dev.env` exists or `DEV=true` is set. In dev mode:
136
+ - DB at `./dev-data/orchestrator.db`
137
+ - Nginx config at `./dev-data/nginx/`
138
+ - All nginx commands are **dry-run** (printed, not executed)
139
+ - uvicorn `--reload` enabled
140
+ - Default API key: `dev`
141
+
142
+ ## Configuration Reference
143
+
144
+ All settings via `/etc/nanio-orchestrator/config.env` (production) or `dev.env` (development).
145
+ Every variable is prefixed with `NANIO_ORCHESTRATOR_`.
146
+
147
+ ### Core
148
+
149
+ | Variable | Default | Description |
150
+ |----------|---------|-------------|
151
+ | `HOST` | `0.0.0.0` | Bind address |
152
+ | `PORT` | `8080` | Listen port |
153
+ | `API_KEY` | `changeme` | API authentication key |
154
+ | `DB_PATH` | `/opt/nanio-orchestrator/data/orchestrator.db` | SQLite database path |
155
+ | `NGINX_CONFIG_DIR` | `/etc/nginx/nanio` | Root directory for generated nginx configs |
156
+ | `LOG_LEVEL` | `info` | Log level (`debug`, `info`, `warning`, `error`) |
157
+ | `LOG_FILE` | _(unset)_ | Path to a rotating log file, e.g. `/var/log/nanio-orchestrator/nanio.log`. Up to 10 MB per file, 5 rotated copies. When unset, logs go to stderr only. |
158
+ | `SESSION_TTL` | `28800` | Web UI session duration in seconds (8 hours) |
159
+ | `SECRET` | _(unset)_ | Fernet key for credential encryption at rest. Generate with: `python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"` |
160
+
161
+ ### S3 / Bucket Sync
162
+
163
+ | Variable | Default | Description |
164
+ |----------|---------|-------------|
165
+ | `S3_ACCESS_KEY` | _(unset)_ | Global S3 access key (used when no per-pool credentials are set) |
166
+ | `S3_SECRET_KEY` | _(unset)_ | Global S3 secret key |
167
+ | `BUCKET_SYNC_INTERVAL` | `300` | Seconds between automatic bucket list syncs |
168
+
169
+ ### Migrations (rclone)
170
+
171
+ | Variable | Default | Description |
172
+ |----------|---------|-------------|
173
+ | `RCLONE_PATH` | `rclone` | Path to the rclone binary |
174
+ | `MIGRATION_MAX_PARALLEL` | `2` | Maximum concurrent migrations |
175
+ | `MIGRATION_BANDWIDTH_LIMIT` | _(unset)_ | rclone `--bwlimit` value, e.g. `50M` |
176
+ | `MIGRATION_CHECKERS` | `8` | rclone `--checkers` value |
177
+ | `MIGRATION_TRANSFERS` | `4` | rclone `--transfers` value |
178
+ | `MIGRATION_MAX_COPY_PASSES` | `10` | Maximum convergence loop passes during the `copying` phase before entering `write_routing`. |
179
+ | `S3_REQUEST_TIMEOUT` | `3600` | Socket timeout in seconds for S3 HTTP requests. Increase for buckets with very large objects. |
180
+
181
+ ### Drift Detection
182
+
183
+ | Variable | Default | Description |
184
+ |----------|---------|-------------|
185
+ | `DRIFT_INTERVAL` | `60` | Seconds between drift checks |
186
+
187
+ ### Database Backup
188
+
189
+ | Variable | Default | Description |
190
+ |----------|---------|-------------|
191
+ | `DB_BACKUP_PATH` | `<DB_PATH>.bak` | Backup file path (defaults to DB path + `.bak`) |
192
+ | `DB_BACKUP_INTERVAL` | `300` | Seconds between timed backups |
193
+ | `DB_BACKUP_ROTATE` | `3` | Number of backup copies to keep (`.bak`, `.bak.2`, `.bak.3`) |
194
+
195
+ ## Authentication
196
+
197
+ | Client | Method | Details |
198
+ |--------|--------|---------|
199
+ | **API** (`/api/*`) | `X-Orchestrator-Key` header | Missing/wrong key returns `401` |
200
+ | **Web UI** (`/web/*`, `/`) | Session cookie | Log in at `/login`; HMAC-signed `nanio_session` cookie issued with configurable TTL |
201
+
202
+ Public endpoints (no auth required): `/api/health`, `/api/docs`, `/api/redoc`, `/api/openapi.json`, `/login`, `/logout`, `/static/*`.
203
+
204
+ ## API Reference
205
+
206
+ All endpoints under `/api/*` require the `X-Orchestrator-Key` header, except `/api/health`.
207
+
208
+ ### Pools
209
+
210
+ | Method | Endpoint | Description |
211
+ |--------|----------|-------------|
212
+ | GET | `/api/pools` | List all pools |
213
+ | POST | `/api/pools` | Create pool |
214
+ | GET | `/api/pools/:id` | Get pool |
215
+ | PUT | `/api/pools/:id` | Update pool |
216
+ | DELETE | `/api/pools/:id` | Delete pool (rejected if routes reference it) |
217
+ | GET | `/api/pools/:id/members` | List pool members |
218
+ | POST | `/api/pools/:id/members` | Add member |
219
+ | PUT | `/api/pools/:id/members/:mid` | Update member |
220
+ | DELETE | `/api/pools/:id/members/:mid` | Remove member |
221
+ | GET | `/api/pools/:id/members/:mid/node-config` | Generate node config (query params) |
222
+ | POST | `/api/pools/:id/members/:mid/node-config` | Generate node config (body) |
223
+ | GET | `/api/pools/:id/node-config-summary` | Node config summary for all members |
224
+ | GET | `/api/pools/:id/buckets/status` | List all buckets on a nanio pool with routing status. Returns each bucket's status (`routed`, `via_default`, `orphaned`, `unrouted`) and which vhosts serve it. Only available for `nanio` pools. |
225
+
226
+ ### Pool Credentials
227
+
228
+ Per-pool S3 credentials, encrypted at rest with Fernet. Requires `SECRET` to be set.
229
+
230
+ > **nanio pools only.** Credentials are only supported for pools of type `nanio`. Attempting to get, set, or remove credentials on an `http` pool returns HTTP 400.
231
+
232
+ | Method | Endpoint | Description |
233
+ |--------|----------|-------------|
234
+ | GET | `/api/pools/:id/credentials` | Get credentials (access key masked) |
235
+ | PUT | `/api/pools/:id/credentials` | Store or replace credentials |
236
+ | DELETE | `/api/pools/:id/credentials` | Remove credentials |
237
+
238
+ ### Vhosts + Routes
239
+
240
+ | Method | Endpoint | Description |
241
+ |--------|----------|-------------|
242
+ | GET | `/api/vhosts` | List all vhosts |
243
+ | POST | `/api/vhosts` | Create vhost |
244
+ | GET | `/api/vhosts/:id` | Get vhost |
245
+ | PUT | `/api/vhosts/:id` | Update vhost |
246
+ | DELETE | `/api/vhosts/:id` | Delete vhost (rejected if routes exist) |
247
+ | GET | `/api/vhosts/:id/routes` | List routes |
248
+ | POST | `/api/vhosts/:id/routes` | Add route |
249
+ | PUT | `/api/vhosts/:id/routes/:rid` | Update route |
250
+ | DELETE | `/api/vhosts/:id/routes/:rid` | Delete route |
251
+ | GET | `/api/vhosts/:id/preview` | Preview rendered server block |
252
+
253
+ **SSL enforcement**: `ssl: true` requires both `ssl_certificate` and `ssl_certificate_key` to be provided. The API returns HTTP 422 if either is missing.
254
+
255
+ **Pool-type consistency**: If a vhost has a `nanio` default pool, all routes must also point to `nanio` pools. Mixed types within a vhost are rejected. Vhosts with no default pool are unrestricted.
256
+
257
+ **Additional configurations** (`extra_blocks`): Vhosts accept an optional `extra_blocks` array for injecting raw nginx directives into specific zones of the generated server block:
258
+
259
+ ```json
260
+ "extra_blocks": [
261
+ { "zone": "top", "content": "add_header X-Frame-Options SAMEORIGIN;" },
262
+ { "zone": "ssl", "content": "ssl_stapling on;\nssl_stapling_verify on;" },
263
+ { "zone": "proxy", "content": "proxy_read_timeout 300s;" },
264
+ { "zone": "end", "content": "# custom footer" }
265
+ ]
266
+ ```
267
+
268
+ | Zone | Inserted after |
269
+ |------|----------------|
270
+ | `top` | `server_name` directive (and after IP rules, if any) |
271
+ | `ssl` | SSL certificate directives |
272
+ | `proxy` | Proxy buffering directives |
273
+ | `end` | Before the closing `}` of the server block |
274
+
275
+ This is intended for advanced per-vhost nginx settings that the orchestrator does not model natively. Content is injected verbatim — it must be valid nginx syntax or `nginx -t` will reject the config.
276
+
277
+ **IP Access Control** (`ip_rule_mode` + `ip_rule_ips`): Per-vhost IP allowlist or denylist, placed at server-block level (applies to all routes):
278
+
279
+ ```json
280
+ {
281
+ "ip_rule_mode": "allow",
282
+ "ip_rule_ips": ["10.0.0.0/8", "192.168.1.5"]
283
+ }
284
+ ```
285
+
286
+ | Mode | Generated nginx |
287
+ |------|----------------|
288
+ | `allow` | `allow <ip>; ... deny all;` — only listed IPs can access the vhost |
289
+ | `deny` | `deny <ip>;` — listed IPs are blocked, everything else is allowed |
290
+ | `null` / omitted | No IP restrictions |
291
+
292
+ Accepted formats: IPv4 (`1.2.3.4`), IPv4 CIDR (`10.0.0.0/8`), IPv6, IPv6 CIDR. Values are validated on write. Both fields are stored in the vhost sidecar `.meta.json` and are fully recovered by the rebuild-from-disk operation.
293
+
294
+ ### Bucket Sync
295
+
296
+ Tracks buckets discovered on the default pool of each vhost. Background sync runs every `BUCKET_SYNC_INTERVAL` seconds.
297
+
298
+ | Method | Endpoint | Description |
299
+ |--------|----------|-------------|
300
+ | GET | `/api/vhosts/:id/buckets` | List buckets with routing status (`unrouted`, `routed`, `migrating`, `ignored`). Pass `?fetch_counts=true` to include object counts. |
301
+ | POST | `/api/vhosts/:id/buckets/sync` | Trigger an immediate bucket list sync |
302
+ | POST | `/api/vhosts/:id/buckets/:bucket/promote` | Promote a bucket: create it on the target pool, add an nginx route, optionally start migration. See promote request body below. |
303
+ | POST | `/api/vhosts/:id/buckets/:bucket/ignore` | Mark a bucket as ignored |
304
+
305
+ > **Bucket sync** only runs on vhosts whose default pool is of type `nanio`. Vhosts backed by an `http` pool are silently skipped — they have no S3 ListBuckets semantics.
306
+
307
+ **Promote request body** (`POST /api/vhosts/:id/buckets/:bucket/promote`):
308
+
309
+ | Field | Type | Default | Description |
310
+ |-------|------|---------|-------------|
311
+ | `pool_id` | int | — | Target pool for the new route |
312
+ | `migrate` | bool | `false` | Start rclone migration after creating the route |
313
+ | `allow_orphan` | bool | `false` | Allow routing to a different pool without migration when the source bucket already has objects. Existing objects remain on the source pool and will not be accessible via this route until migrated manually. |
314
+
315
+ **Conflict behaviour** when `migrate=false` and the source bucket has objects:
316
+ - Routing to **the same pool as the default** → allowed (just creates an explicit route, no data loss).
317
+ - Routing to **a different pool** → returns HTTP 400 with `allow_orphan` in the message. Re-submit with `allow_orphan=true` to acknowledge data will remain on the source. The Web UI shows an inline conflict box with "Enable migration & route" or "Route without migration" options.
318
+
319
+ | Method | Endpoint | Description |
320
+ |--------|----------|-------------|
321
+ | POST | `/api/vhosts/:id/buckets/:bucket/migrate` | Start (or restart) object migration for a routed bucket (uses rclone engine) |
322
+ | GET | `/api/vhosts/:id/buckets/orphans` | Scan routed buckets for orphan objects still on the default pool |
323
+ | POST | `/api/vhosts/:id/buckets/:bucket/purge-orphan` | Delete all objects from the default pool copy of a routed bucket |
324
+
325
+ ### Migrations (rclone)
326
+
327
+ Full bucket migrations using rclone.
328
+
329
+ Phases: `pending → copying → write_routing → verifying → switching → done`
330
+
331
+ - **copying**: rclone copies data in a convergence loop (up to `MIGRATION_MAX_COPY_PASSES` passes). Ends early if counts stabilise across passes.
332
+ - **write_routing**: nginx is reconfigured so writes go directly to the destination pool while reads still come from the source (with 404-fallback to destination). Freezes new writes to the source.
333
+ - **verifying**: a copy→check convergence loop (up to `MIGRATION_MAX_COPY_PASSES` passes). Each pass does a final `rclone copy` followed by `rclone check`. If check passes cleanly the migration proceeds to switching. If differences are still found, the loop retries — this handles buckets that are still receiving uploads during migration. The loop aborts early if the diff count stops decreasing between passes (source diverging faster than rclone can copy).
334
+ - **switching**: the nginx route is flipped to the destination pool and the DB is updated atomically. Fails hard if the route cannot be found — no silent data loss.
335
+ - **done**: migration complete. Source data is **never deleted automatically**. The migration record tracks `orphaned_source_pool_id`, `orphaned_source_prefix`, and `orphaned_at` so operators can locate and clean up the source bucket at their own pace.
336
+ - **error** / **cancelled**: terminal failure states.
337
+
338
+ > **Source data is never purged automatically.** When a migration reaches `done`, the orchestrator records where the original data lives (pool + prefix + timestamp). Use `nanio-orchestrator orphaned list` or `GET /api/migrations/orphaned` to review, and delete the source objects manually when ready.
339
+
340
+ > **A route must exist before starting a migration.** Use `POST /api/vhosts/:id/buckets/:bucket/promote` (Buckets page) to create the bucket route first. The Migrations page only accepts buckets that are already routed — it validates that the route exists and points to `src_pool_id` before creating the migration record.
341
+
342
+ - **copy** mode (default): additive — only copies objects from source to destination, never deletes at the destination.
343
+ - **sync** mode: mirror — destination becomes identical to the source. A pre-flight guard aborts the migration if the source bucket is empty to prevent accidental data loss.
344
+
345
+ **Pre-flight validation** (at `POST /api/migrations` time):
346
+ - **Both pools must be of type `nanio`.** Migrations between `http` pools or from/to an `http` pool are rejected with HTTP 400.
347
+ - Both pools must have at least one enabled member.
348
+ - Source and destination must be different pools.
349
+ - A route `/{bucket}/` must exist in the vhost and point to `src_pool_id`.
350
+ - The source bucket must exist and contain at least one object on the source pool.
351
+ - No active migration for the same bucket can already be running.
352
+
353
+ **Destination bucket with existing objects**: In `copy` mode, if the destination bucket already has objects (e.g. from a previously failed migration attempt), the migration **proceeds with a warning** — rclone copy is additive and skips objects that already exist at the destination unchanged. In `sync` mode, the migration is **aborted** if the destination has objects to prevent accidental data loss.
354
+
355
+ **Audit log**: Every migration lifecycle event is written to the audit log — `start_migration` when created, `migration_done` on completion, `migration_cancelled` on cancellation, and `migration_error` on failure. Per-step details are also written to the migration's own log (`GET /api/migrations/:id/log`), including early-abort error messages.
356
+
357
+ | Method | Endpoint | Description |
358
+ |--------|----------|--------------|
359
+ | POST | `/api/migrations` | Start a new migration. Body: `{bucket, src_pool_id, dst_pool_id, mode}` where `mode` is `"copy"` (default) or `"sync"`. Requires an nginx route for the bucket pointing to `src_pool_id`. |
360
+ | GET | `/api/migrations` | List migrations (filter with `?phase=`) |
361
+ | GET | `/api/migrations/stale` | List active migrations that cannot proceed — source pool has no members, or source bucket has disappeared. |
362
+ | GET | `/api/migrations/orphaned` | List completed migrations that have orphaned source data pending manual cleanup |
363
+ | GET | `/api/migrations/source-buckets` | List buckets available to migrate from a given pool (`?pool_id=`). Returns buckets from `bucket_sync` and routed paths. Used by the Migrations UI to populate the bucket selector. |
364
+ | GET | `/api/migrations/:id` | Get migration details (includes `orphaned_source_pool_id`, `orphaned_source_prefix`, `orphaned_at`) |
365
+ | POST | `/api/migrations/:id/cancel` | Cancel a running migration |
366
+ | GET | `/api/migrations/:id/log` | Get migration log entries |
367
+
368
+ ### Config Operations
369
+
370
+ | Method | Endpoint | Description |
371
+ |--------|----------|-------------|
372
+ | GET | `/api/config/status` | Drift status per file |
373
+ | POST | `/api/config/validate` | Run `nginx -t` |
374
+ | POST | `/api/config/reload` | Run `nginx -s reload` |
375
+ | POST | `/api/config/sync` | Re-import disk state → DB |
376
+ | POST | `/api/config/rebuild` | Rebuild all files from DB → disk → reload |
377
+ | POST | `/api/config/absorb-file` | Accept a drifted file: import disk state into DB |
378
+ | POST | `/api/config/rewrite-file` | Rewrite a single file from DB state + reload |
379
+ | GET | `/api/config/preview/pool/:id` | Preview upstream config (no apply) |
380
+ | GET | `/api/config/preview/vhost/:id` | Preview server block config (no apply) |
381
+ | POST | `/api/config/rebuild-from-disk` | Reconstruct DB from nginx configs + sidecar files (see [DB Resilience](#db-resilience)) |
382
+ | POST | `/api/config/backup` | Trigger an immediate database backup |
383
+ | GET | `/api/config/settings` | Current effective settings (secrets masked) |
384
+ | PUT | `/api/config/settings/:key` | Update a single setting in the config file (takes effect after restart) |
385
+ | POST | `/api/config/settings/restart` | Restart the service to apply pending config changes (requires sudoers rule installed by `nanio-orchestrator install`) |
386
+
387
+ ### Health + Audit
388
+
389
+ | Method | Endpoint | Description |
390
+ |--------|----------|-------------|
391
+ | GET | `/api/health` | Health check (no auth required) |
392
+ | GET | `/api/audit` | Audit log (`?page=&entity_type=&from=&to=`) |
393
+
394
+ ## How Nginx Config is Managed
395
+
396
+ ### Write Path
397
+
398
+ Every config change follows this exact sequence:
399
+
400
+ 1. Render new config from DB state (Jinja2 templates)
401
+ 2. Write to `<file>.tmp`
402
+ 3. Run `nginx -t` — if it fails: delete `.tmp`, return error, stop
403
+ 4. `os.rename(<file>.tmp, <file>)` — atomic on POSIX
404
+ 5. Run `nginx -s reload`
405
+ 6. Update DB: sha256, content snapshot, audit log with nginx output
406
+ 7. Trigger a DB backup
407
+
408
+ ### Sidecar Files
409
+
410
+ Alongside each nginx config file the orchestrator writes a `.meta.json` sidecar containing
411
+ data that cannot be reconstructed from the nginx config alone:
412
+
413
+ ```
414
+ /etc/nginx/nanio/
415
+ ├── pools/
416
+ │ ├── pool-2025.conf # upstream block
417
+ │ └── pool-2025.meta.json # type, description, encrypted credentials
418
+ └── vhosts/
419
+ ├── s3.xpto.pt.conf # server block
420
+ └── s3.xpto.pt.meta.json # default_pool_id + name
421
+
422
+ /opt/nanio-orchestrator/data/
423
+ ├── orchestrator.db
424
+ ├── orchestrator.db.bak
425
+ └── migrations/
426
+ ├── migration-7.state.json # in-progress migration state (alongside DB, not in nginx dir)
427
+ └── migration-7.done.json # permanent completion record (written when migration reaches 'done')
428
+ ```
429
+
430
+ Sidecars are written atomically (`.tmp` → rename) and are the foundation for
431
+ [DB resilience](#db-resilience).
432
+
433
+ ### Drift Detection
434
+
435
+ Background check every `DRIFT_INTERVAL` seconds:
436
+ - SHA256 each managed file on disk
437
+ - Compare with the last known hash in DB
438
+ - If mismatch: alert in dashboard and `GET /api/config/status`
439
+ - **Never auto-corrects** — the operator decides
440
+
441
+ ### Pool Types
442
+
443
+ | Type | Members | Nginx `backup` flag | Credentials | Description |
444
+ |------|---------|---------------------|-------------|-------------|
445
+ | `nanio` | All `active` | Never | ✓ (S3 access/secret key) | Shared storage — any member handles any request |
446
+ | `http` | `primary` + `replica` | Yes, for replicas | ✗ | Read-only HTTP serve with failover |
447
+
448
+ Pool type also determines what is available in the Web UI:
449
+ - **S3 credentials** are only shown and editable for `nanio` pools.
450
+ - **Migrations** can only be created between two `nanio` pools.
451
+ - **Bucket sync** and the **Buckets** management page only operate on vhosts whose default pool is `nanio`.
452
+
453
+ ### Node Config Generator
454
+
455
+ Generates config snippets for upstream nodes (rendered only, never deployed):
456
+ - **nanio-only**: nanio `options.toml` + systemd unit
457
+ - **nginx-only**: nginx server block for file serving
458
+ - **nginx-nanio**: both nanio config and nginx proxy config
459
+
460
+ Access via API or the "Node Setup" button in the Web UI.
461
+
462
+ ## DB Resilience
463
+
464
+ The database is not the source of truth — the nginx config files and their sidecar files are.
465
+ The DB can be fully rebuilt from disk after loss or corruption.
466
+
467
+ ### Automatic Backup
468
+
469
+ The DB is backed up automatically:
470
+ - After every successful nginx reload
471
+ - On a periodic timer (`DB_BACKUP_INTERVAL`, default 60 s)
472
+ - On demand via `POST /api/config/backup`
473
+
474
+ Backups are rotated: `.bak`, `.bak.2`, `.bak.3`, … up to `DB_BACKUP_ROTATE` copies.
475
+
476
+ ### Rebuild from Disk
477
+
478
+ If the DB is lost or corrupted, reconstruct it without downtime (nginx keeps running):
479
+
480
+ ```bash
481
+ # Preview what would be imported
482
+ nanio-orchestrator rebuild-db --dry-run
483
+
484
+ # Rebuild (safe — DB must be empty)
485
+ nanio-orchestrator rebuild-db
486
+
487
+ # Rebuild over existing data
488
+ nanio-orchestrator rebuild-db --force
489
+ ```
490
+
491
+ Or via API:
492
+
493
+ ```bash
494
+ curl -X POST http://localhost:8080/api/config/rebuild-from-disk \
495
+ -H "X-Orchestrator-Key: <key>"
496
+
497
+ # Force over existing data
498
+ curl -X POST "http://localhost:8080/api/config/rebuild-from-disk?force=true" \
499
+ -H "X-Orchestrator-Key: <key>"
500
+ ```
501
+
502
+ What is recovered:
503
+
504
+ | Data | Source | Recovered? |
505
+ |------|--------|-----------|
506
+ | Pools (name, lb_method, keepalive) | `pools/*.conf` | ✓ |
507
+ | Pool members | `pools/*.conf` | ✓ |
508
+ | Pool type, description | `pools/*.meta.json` | ✓ |
509
+ | Encrypted credentials | `pools/*.meta.json` | ✓ |
510
+ | Vhosts (server_name, SSL, ports) | `vhosts/*.conf` | ✓ |
511
+ | Routes | `vhosts/*.conf` | ✓ |
512
+ | Vhost default_pool_id | `vhosts/*.meta.json` | ✓ |
513
+ | In-progress migrations | `data/migrations/*.state.json` | ✓ (reset to pending, will auto-resume) |
514
+ | Completed migration records | `data/migrations/*.done.json` | ✓ (orphaned source info preserved) |
515
+ | config_files sha256 records | recomputed from disk | ✓ |
516
+ | bucket_sync | live `ListBuckets` call per pool member | ✓ (best-effort — requires pool members to be reachable) |
517
+ | audit_log | — | ✗ (historical only) |
518
+
519
+ After rebuild, restart the service so migrations resume:
520
+
521
+ ```bash
522
+ systemctl restart nanio-orchestrator
523
+ ```
524
+
525
+ ## Web UI
526
+
527
+ The web UI is served at `/` and requires a session cookie obtained via `/login`.
528
+
529
+ | Page | URL | Description |
530
+ |------|-----|-------------|
531
+ | Dashboard | `/` | Overview: pools, vhosts, drift count, active migrations, unrouted buckets |
532
+ | Pools | `/web/pools` | Manage pools and members |
533
+ | Vhosts | `/web/vhosts` | Manage vhosts and routes |
534
+ | Buckets | `/web/buckets` or via Vhosts page | Bucket list, promote, migrate, orphan scan and purge per vhost |
535
+ | Config | `/web/config` | Config file drift status, per-file actions |
536
+ | Migrations | `/web/migrations` | Start (copy or sync mode) and monitor rclone migrations. Bucket selector is populated from the source pool's existing routes. Stale migrations (source pool lost members or source bucket disappeared) are flagged. |
537
+ | Audit | `/web/audit` | Last 100 audit log entries |
538
+ | Settings | `/web/settings` | View all current settings (secrets masked) |
539
+
540
+ ## CLI Reference
541
+
542
+ ```
543
+ nanio-orchestrator [serve] Start the server (default command)
544
+ nanio-orchestrator install Production install (run as root)
545
+ nanio-orchestrator rebuild-db Rebuild DB from disk
546
+ --dry-run Preview without writing
547
+ --force Overwrite existing DB data
548
+
549
+ nanio-orchestrator config show Print all settings grouped by category
550
+ nanio-orchestrator config get <key> Print the value of a single setting
551
+ nanio-orchestrator config set <key> <val> Write a setting to the config file
552
+ nanio-orchestrator config generate-secret Generate a Fernet key for SECRET
553
+ --set Also write it to the config file
554
+ nanio-orchestrator config edit Open the config file in $EDITOR
555
+ nanio-orchestrator config validate Run nginx -t
556
+ nanio-orchestrator config reload Run nginx -s reload
557
+ nanio-orchestrator config rebuild Regenerate all config files from DB + reload
558
+
559
+ nanio-orchestrator orphaned list List all completed migrations with orphaned source data
560
+ ```
561
+
562
+ ### `config show` example
563
+
564
+ ```
565
+ Core
566
+ host 0.0.0.0 Bind address
567
+ port 8080 Listen port
568
+ api_key chan**** API authentication key
569
+ log_level info Log level (debug/info/warning/error)
570
+ session_ttl 28800 Web UI session duration (seconds)
571
+
572
+ Database
573
+ db_path /opt/.../orchestrator.db SQLite database file path
574
+ db_backup_path /opt/.../orchestrator.db.bak Backup path (default: db_path + .bak)
575
+ ...
576
+
577
+ Config file (production): /etc/nanio-orchestrator/config.env
578
+ ```
579
+
580
+ ### `config set` example
581
+
582
+ ```bash
583
+ nanio-orchestrator config set api_key mysecretkey
584
+ nanio-orchestrator config set log_level debug
585
+ nanio-orchestrator config set migration_max_parallel 4
586
+ ```
587
+
588
+ Accepts the short key name (without `NANIO_ORCHESTRATOR_` prefix). Updates the active config file in place, handling commented-out lines.
589
+
590
+ ## Offline / Air-gapped Deployment
591
+
592
+ ```bash
593
+ # On a machine with internet:
594
+ make build # produces dist/nanio_orchestrator-*.whl
595
+
596
+ # Copy the wheel to the target server, then:
597
+ python3 -m venv /opt/nanio-orchestrator/venv
598
+ /opt/nanio-orchestrator/venv/bin/pip install nanio_orchestrator-*.whl
599
+ /opt/nanio-orchestrator/venv/bin/nanio-orchestrator install
600
+ ```
601
+
602
+ The wheel bundles all dependencies. No internet required on the target server.
603
+
604
+ ## Troubleshooting
605
+
606
+ ### `nginx -t` fails after config change
607
+
608
+ The orchestrator never applies a config that fails validation. Check the error output
609
+ in the API response or audit log. Common causes:
610
+ - Missing SSL certificates referenced in the vhost config
611
+ - Upstream pool name conflicts with an existing nginx config
612
+ - `include /etc/nginx/nanio/pools/*.conf;` and `include /etc/nginx/nanio/vhosts/*.conf;` not added to the `http {}` block in `nginx.conf`
613
+
614
+ ### Drift detected
615
+
616
+ A file was modified outside the orchestrator. Options:
617
+ 1. **Accept the change**: `POST /api/config/sync` to import disk state into DB
618
+ 2. **Restore from DB**: `POST /api/config/rebuild` to overwrite disk with DB state
619
+
620
+ ### Service won't start
621
+
622
+ ```bash
623
+ journalctl -u nanio-orchestrator -f # check logs
624
+ nanio-orchestrator config validate # test nginx config
625
+ ```
626
+
627
+ Common causes:
628
+ - DB path not writable
629
+ - Port 8080 already in use (change `PORT` in config.env)
630
+ - Python version too old (requires 3.9+)
631
+
632
+ ### Credentials API returns 500
633
+
634
+ `SECRET` is not set or is not a valid Fernet key. Generate and set one:
635
+
636
+ ```bash
637
+ nanio-orchestrator config generate-secret --set
638
+ ```
639
+
640
+ Or manually:
641
+
642
+ ```bash
643
+ python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
644
+ # Then: nanio-orchestrator config set secret <generated-key>
645
+ ```
646
+
647
+ Restart the service after setting the key.
648
+
649
+ ### API returns 401
650
+
651
+ All API endpoints (except `/api/health`) require `X-Orchestrator-Key` set to
652
+ `NANIO_ORCHESTRATOR_API_KEY`.
653
+
654
+ ### Web UI keeps redirecting to /login
655
+
656
+ - Cookies blocked? Make sure the browser allows cookies for the host.
657
+ - Behind a TLS-terminating proxy? Ensure `X-Forwarded-Proto: https` is forwarded so the `Secure` cookie flag is set correctly.
658
+ - Session expired? Default TTL is 8 hours. Increase `SESSION_TTL` if needed.
659
+ - API key changed? Old cookies are immediately invalidated; re-login.
660
+
661
+ ## License
662
+
663
+ MIT