withcache 0.13.0__tar.gz → 0.13.2__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.
- {withcache-0.13.0 → withcache-0.13.2}/PKG-INFO +10 -8
- {withcache-0.13.0 → withcache-0.13.2}/README.md +9 -7
- {withcache-0.13.0 → withcache-0.13.2}/shim/build.zig.zon +1 -1
- {withcache-0.13.0 → withcache-0.13.2}/src/withcache/__init__.py +1 -1
- {withcache-0.13.0 → withcache-0.13.2}/src/withcache/_api.py +19 -6
- {withcache-0.13.0 → withcache-0.13.2}/src/withcache/_app.py +2 -2
- {withcache-0.13.0 → withcache-0.13.2}/src/withcache/client.py +26 -1
- {withcache-0.13.0 → withcache-0.13.2}/src/withcache/server.py +25 -11
- {withcache-0.13.0 → withcache-0.13.2}/tests/test_fastapi_catalog_api.py +16 -0
- {withcache-0.13.0 → withcache-0.13.2}/tests/test_withcache.py +66 -0
- {withcache-0.13.0 → withcache-0.13.2}/.gitignore +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/LICENSE +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/deploy/Containerfile +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/deploy/compose.local-build.yml +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/deploy/compose.yml +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/deploy/envvars.example +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/hatch_build.py +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/pyproject.toml +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/shim/build.zig +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/shim/shim.zig +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/src/withcache/_events_log.py +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/src/withcache/_settings_store.py +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/src/withcache/_shim.py +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/src/withcache/_table_state.py +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/src/withcache/_templates/ui/_layout.html +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/src/withcache/_templates/ui/_table_macros.html +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/src/withcache/_templates/ui/catalog.html +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/src/withcache/_templates/ui/dashboard.html +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/src/withcache/_templates/ui/events.html +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/src/withcache/_templates/ui/login.html +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/src/withcache/_templates/ui/misses.html +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/src/withcache/_templates/ui/settings.html +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/src/withcache/curlwithcache.py +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/src/withcache/oras.py +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/src/withcache/static/bootstrap-icons.min.css +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/src/withcache/static/bootstrap.min.css +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/src/withcache/static/fonts/bootstrap-icons.woff +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/src/withcache/static/fonts/bootstrap-icons.woff2 +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/src/withcache/static/htmx.min.js +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/src/withcache/wgetwithcache.py +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/tests/test_differential.py +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/tests/test_fastapi_admin_forms.py +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/tests/test_fastapi_blob.py +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/tests/test_fastapi_events.py +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/tests/test_fastapi_scaffold.py +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/tests/test_fastapi_settings_persistence.py +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/tests/test_fastapi_ui_pages.py +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/tests/test_fastapi_uvicorn_smoke.py +0 -0
- {withcache-0.13.0 → withcache-0.13.2}/tests/test_oras.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: withcache
|
|
3
|
-
Version: 0.13.
|
|
3
|
+
Version: 0.13.2
|
|
4
4
|
Summary: Operator-curated, URL-keyed artifact cache for a small lab (CUDA/ROCm/DOCA/firmware)
|
|
5
5
|
Project-URL: Homepage, https://github.com/safl/withcache
|
|
6
6
|
Author-email: "Simon A. F. Lund" <safl@safl.dk>
|
|
@@ -68,14 +68,16 @@ disk.
|
|
|
68
68
|
|
|
69
69
|
| Path | What it is |
|
|
70
70
|
|-------------------------------|-------------------------------------------------------------|
|
|
71
|
-
| `src/withcache/server.py` | The cache-host: blob store + miss table + **background download manager** +
|
|
71
|
+
| `src/withcache/server.py` | The cache-host: blob store + miss table + **background download manager** + events log |
|
|
72
|
+
| `src/withcache/_app.py` | FastAPI app factory + operator UI wiring (Bootstrap 5 + Bootstrap Icons + HTMX) |
|
|
72
73
|
| `src/withcache/_shim.py` | Shared shim core (find URL → probe → rewrite → exec) |
|
|
73
74
|
| `src/withcache/curlwithcache.py` / `wgetwithcache.py` | The Python `curl` / `wget` shims |
|
|
74
75
|
| `shim/shim.zig` | The native shim: one static binary, both tools via `argv[0]` |
|
|
75
76
|
| `deploy/Containerfile`, `deploy/compose.yml` | Single Podman/Docker host deploy |
|
|
76
77
|
|
|
77
|
-
The cache-host
|
|
78
|
-
|
|
78
|
+
The cache-host runs on FastAPI + Jinja + Bootstrap (matching bty +
|
|
79
|
+
nbdmux for one trio-consistent chrome); the shims (Python and
|
|
80
|
+
native) are dependency-free so they drop into any environment.
|
|
79
81
|
|
|
80
82
|
## Install
|
|
81
83
|
|
|
@@ -245,11 +247,11 @@ Notes & limits (all degrade gracefully; worst case is "no caching, curl still wo
|
|
|
245
247
|
|
|
246
248
|
`http://withcache-server:8081/` (Bootstrap 5 + Bootstrap Icons + HTMX, bundled
|
|
247
249
|
offline; matches bty's chrome for a consistent trio) is a five-page dashboard:
|
|
248
|
-
- **
|
|
249
|
-
- **
|
|
250
|
+
- **Dashboard** (landing): catalog + cache + activity summary, health checklist, and the last N audit events.
|
|
251
|
+
- **Catalog**: image catalog fetched from a nosi-style `catalog.toml` (URL configured on Settings > Catalog source). Add entries via the subnav's inline "Add ORAS" / "Add HTTPS" forms or "Fetch default" button, then **Download** each entry. Rows carry hits, size, download progress (live), and cached/failed pills.
|
|
250
252
|
- **Misses**: URLs clients asked for that aren't downloaded yet. Each with **Fetch** (promotes to a catalog entry AND downloads it) and **Dismiss** (forget it).
|
|
251
|
-
- **
|
|
252
|
-
- **Settings**: identity + storage paths + catalog source + logging (uvicorn level) + auth.
|
|
253
|
+
- **Events**: append-only audit log with a free-text filter and per-page pagination. Failure rows carry an ack button; the dashboard's Health tripwire flags unacknowledged failures.
|
|
254
|
+
- **Settings**: identity + storage paths + catalog source (editable) + logging (uvicorn level) + auth.
|
|
253
255
|
|
|
254
256
|
## Auth
|
|
255
257
|
|
|
@@ -45,14 +45,16 @@ disk.
|
|
|
45
45
|
|
|
46
46
|
| Path | What it is |
|
|
47
47
|
|-------------------------------|-------------------------------------------------------------|
|
|
48
|
-
| `src/withcache/server.py` | The cache-host: blob store + miss table + **background download manager** +
|
|
48
|
+
| `src/withcache/server.py` | The cache-host: blob store + miss table + **background download manager** + events log |
|
|
49
|
+
| `src/withcache/_app.py` | FastAPI app factory + operator UI wiring (Bootstrap 5 + Bootstrap Icons + HTMX) |
|
|
49
50
|
| `src/withcache/_shim.py` | Shared shim core (find URL → probe → rewrite → exec) |
|
|
50
51
|
| `src/withcache/curlwithcache.py` / `wgetwithcache.py` | The Python `curl` / `wget` shims |
|
|
51
52
|
| `shim/shim.zig` | The native shim: one static binary, both tools via `argv[0]` |
|
|
52
53
|
| `deploy/Containerfile`, `deploy/compose.yml` | Single Podman/Docker host deploy |
|
|
53
54
|
|
|
54
|
-
The cache-host
|
|
55
|
-
|
|
55
|
+
The cache-host runs on FastAPI + Jinja + Bootstrap (matching bty +
|
|
56
|
+
nbdmux for one trio-consistent chrome); the shims (Python and
|
|
57
|
+
native) are dependency-free so they drop into any environment.
|
|
56
58
|
|
|
57
59
|
## Install
|
|
58
60
|
|
|
@@ -222,11 +224,11 @@ Notes & limits (all degrade gracefully; worst case is "no caching, curl still wo
|
|
|
222
224
|
|
|
223
225
|
`http://withcache-server:8081/` (Bootstrap 5 + Bootstrap Icons + HTMX, bundled
|
|
224
226
|
offline; matches bty's chrome for a consistent trio) is a five-page dashboard:
|
|
225
|
-
- **
|
|
226
|
-
- **
|
|
227
|
+
- **Dashboard** (landing): catalog + cache + activity summary, health checklist, and the last N audit events.
|
|
228
|
+
- **Catalog**: image catalog fetched from a nosi-style `catalog.toml` (URL configured on Settings > Catalog source). Add entries via the subnav's inline "Add ORAS" / "Add HTTPS" forms or "Fetch default" button, then **Download** each entry. Rows carry hits, size, download progress (live), and cached/failed pills.
|
|
227
229
|
- **Misses**: URLs clients asked for that aren't downloaded yet. Each with **Fetch** (promotes to a catalog entry AND downloads it) and **Dismiss** (forget it).
|
|
228
|
-
- **
|
|
229
|
-
- **Settings**: identity + storage paths + catalog source + logging (uvicorn level) + auth.
|
|
230
|
+
- **Events**: append-only audit log with a free-text filter and per-page pagination. Failure rows carry an ack button; the dashboard's Health tripwire flags unacknowledged failures.
|
|
231
|
+
- **Settings**: identity + storage paths + catalog source (editable) + logging (uvicorn level) + auth.
|
|
230
232
|
|
|
231
233
|
## Auth
|
|
232
234
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
.name = .withcache_shim,
|
|
3
3
|
// Zig requires a literal here; keep it in lockstep with the project's
|
|
4
4
|
// single source (src/withcache/__init__.py) via `make bump` / `make version-check`.
|
|
5
|
-
.version = "0.13.
|
|
5
|
+
.version = "0.13.2",
|
|
6
6
|
.fingerprint = 0xd7d96c5ed212ccaa,
|
|
7
7
|
.minimum_zig_version = "0.16.0",
|
|
8
8
|
.paths = .{
|
|
@@ -22,6 +22,6 @@ inherit the framework floor.
|
|
|
22
22
|
from . import oras
|
|
23
23
|
from .client import blob_url, cache_base, is_cached, serve_url
|
|
24
24
|
|
|
25
|
-
__version__ = "0.13.
|
|
25
|
+
__version__ = "0.13.2"
|
|
26
26
|
|
|
27
27
|
__all__ = ["__version__", "blob_url", "cache_base", "is_cached", "oras", "serve_url"]
|
|
@@ -22,9 +22,11 @@ v0.11.0) promotes a missed URL to a catalog entry + enqueues the
|
|
|
22
22
|
Download in one click.
|
|
23
23
|
|
|
24
24
|
Streaming shape: FastAPI ``StreamingResponse`` with a generator
|
|
25
|
-
that reads chunks from the on-disk blob at 64 KiB reads
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
that reads chunks from the on-disk blob at 64 KiB reads. The
|
|
26
|
+
Downloads-page real-time progress tracker was retired in v0.12.0
|
|
27
|
+
along with that page; per-entry download progress now folds into
|
|
28
|
+
the Catalog page's Downloaded column, populated from the
|
|
29
|
+
DownloadManager's job list directly.
|
|
28
30
|
"""
|
|
29
31
|
|
|
30
32
|
from __future__ import annotations
|
|
@@ -254,9 +256,19 @@ def register_api_routes(app: FastAPI) -> None:
|
|
|
254
256
|
"""Insert one catalog entry.
|
|
255
257
|
|
|
256
258
|
Body: ``{"name": "...", "src": "...", "format?", "arch?",
|
|
257
|
-
"sha256?", "size_bytes?", "resolved_src?", "description?"
|
|
258
|
-
Only ``src`` is required; every other
|
|
259
|
-
gets persisted through the
|
|
259
|
+
"sha256?", "size_bytes?", "resolved_src?", "description?",
|
|
260
|
+
"netboot_ref?"}``. Only ``src`` is required; every other
|
|
261
|
+
field is optional and gets persisted through the
|
|
262
|
+
``catalog.toml`` round-trip.
|
|
263
|
+
|
|
264
|
+
``netboot_ref`` names another catalog entry (by ``name``)
|
|
265
|
+
whose bytes are a nosi netboot bundle (vmlinuz + initrd +
|
|
266
|
+
manifest) that nbdmux extracts at warm time so bty can
|
|
267
|
+
ramboot the image using its own kernel. Present iff the
|
|
268
|
+
entry is a disk image that has a matching netboot bundle;
|
|
269
|
+
the bundle entry itself never carries a ``netboot_ref``.
|
|
270
|
+
Withcache does not validate that the referenced entry
|
|
271
|
+
exists -- the sibling may be added in either order.
|
|
260
272
|
|
|
261
273
|
409 when an entry with the same ``name`` already exists;
|
|
262
274
|
rejecting on duplicate name (not src) because the display
|
|
@@ -276,6 +288,7 @@ def register_api_routes(app: FastAPI) -> None:
|
|
|
276
288
|
"sha256",
|
|
277
289
|
"size_bytes",
|
|
278
290
|
"description",
|
|
291
|
+
"netboot_ref",
|
|
279
292
|
}
|
|
280
293
|
if not isinstance(body, dict):
|
|
281
294
|
raise HTTPException(status_code=400, detail="body must be a JSON object")
|
|
@@ -20,7 +20,7 @@ import hashlib
|
|
|
20
20
|
import os
|
|
21
21
|
import sys
|
|
22
22
|
import threading
|
|
23
|
-
from collections.abc import
|
|
23
|
+
from collections.abc import AsyncGenerator
|
|
24
24
|
from pathlib import Path
|
|
25
25
|
from typing import Any
|
|
26
26
|
|
|
@@ -108,7 +108,7 @@ def create_app(
|
|
|
108
108
|
jinja = _build_jinja(_TEMPLATES_DIR)
|
|
109
109
|
|
|
110
110
|
@contextlib.asynccontextmanager
|
|
111
|
-
async def _lifespan(_app: FastAPI) ->
|
|
111
|
+
async def _lifespan(_app: FastAPI) -> AsyncGenerator[None]:
|
|
112
112
|
"""Start-of-request-cycle wiring for the daemon path.
|
|
113
113
|
|
|
114
114
|
Fires only when ``run_lifecycle=True`` (``server.main`` boots
|
|
@@ -45,6 +45,7 @@ __all__ = [
|
|
|
45
45
|
"cache_base",
|
|
46
46
|
"delete_catalog_entry",
|
|
47
47
|
"is_cached",
|
|
48
|
+
"is_healthy",
|
|
48
49
|
"list_catalog",
|
|
49
50
|
"serve_url",
|
|
50
51
|
]
|
|
@@ -121,6 +122,27 @@ def serve_url(
|
|
|
121
122
|
return blob_url(server, origin) if is_cached(server, origin, timeout, headers=headers) else None
|
|
122
123
|
|
|
123
124
|
|
|
125
|
+
def is_healthy(
|
|
126
|
+
server: str = "http://localhost:8081",
|
|
127
|
+
timeout: float = PROBE_TIMEOUT,
|
|
128
|
+
) -> bool:
|
|
129
|
+
"""True iff ``GET /healthz`` returns 200. Suitable for a startup
|
|
130
|
+
probe / reachability pill on a consumer's Settings page.
|
|
131
|
+
|
|
132
|
+
Bypasses :func:`_catalog_request` deliberately: the healthz body
|
|
133
|
+
is small JSON but we don't inspect it -- the status code is the
|
|
134
|
+
whole signal. Any transport / HTTP / OS failure returns
|
|
135
|
+
``False`` so callers can treat a False as "not reachable" without
|
|
136
|
+
having to catch a family of exceptions.
|
|
137
|
+
"""
|
|
138
|
+
url = f"{cache_base(server)}/healthz"
|
|
139
|
+
try:
|
|
140
|
+
with urllib.request.urlopen(url, timeout=timeout) as resp: # noqa: S310
|
|
141
|
+
return bool(resp.status == 200)
|
|
142
|
+
except (urllib.error.HTTPError, urllib.error.URLError, TimeoutError, OSError):
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
|
|
124
146
|
def _resolve_password(password: str | None) -> str | None:
|
|
125
147
|
"""``password`` if given, else ``$WITHCACHE_ADMIN_PASSWORD``, else ``None``.
|
|
126
148
|
Empty / whitespace values are treated as unset."""
|
|
@@ -199,7 +221,10 @@ def add_catalog_entry(
|
|
|
199
221
|
|
|
200
222
|
``entry`` must carry at least ``name`` + ``src``. Optional
|
|
201
223
|
fields: ``format``, ``arch``, ``sha256``, ``size_bytes``,
|
|
202
|
-
``resolved_src``, ``description
|
|
224
|
+
``resolved_src``, ``description``, ``netboot_ref`` (name of
|
|
225
|
+
a sibling entry carrying the matching nosi netboot bundle;
|
|
226
|
+
consumed by nbdmux to serve vmlinuz + initrd alongside the
|
|
227
|
+
disk-image export).
|
|
203
228
|
|
|
204
229
|
Raises :class:`WithcacheError` on any HTTP failure (409 if the
|
|
205
230
|
entry name already exists; 401 if auth is enabled and no valid
|
|
@@ -147,18 +147,31 @@ def _serialise_catalog(entries: list[dict[str, Any]]) -> bytes:
|
|
|
147
147
|
row can't smuggle arbitrary TOML through.
|
|
148
148
|
|
|
149
149
|
Scalar keys emitted: ``name``, ``src``, ``resolved_src``,
|
|
150
|
-
``format``, ``arch``, ``sha256``, ``description
|
|
151
|
-
``size_bytes``. ``resolved_src`` is
|
|
152
|
-
(for ``oras://`` refs; equal to
|
|
153
|
-
bty needs it to render iPXE flash
|
|
154
|
-
URL directly. ``description`` is
|
|
155
|
-
the /ui/images page has something
|
|
156
|
-
display.
|
|
150
|
+
``format``, ``arch``, ``sha256``, ``description``,
|
|
151
|
+
``netboot_ref``. Integer: ``size_bytes``. ``resolved_src`` is
|
|
152
|
+
the canonical fetch URL (for ``oras://`` refs; equal to
|
|
153
|
+
``src`` for plain HTTPS); bty needs it to render iPXE flash
|
|
154
|
+
chains against the byte URL directly. ``description`` is
|
|
155
|
+
operator-supplied prose so the /ui/images page has something
|
|
156
|
+
better than the URL to display. ``netboot_ref`` names a
|
|
157
|
+
sibling catalog entry carrying the matching nosi netboot
|
|
158
|
+
bundle (kernel + initrd) so nbdmux can pair the disk image
|
|
159
|
+
with its own kernel at warm time; see the ``add_catalog_entry``
|
|
160
|
+
docstring in ``_api.py`` for the pairing story.
|
|
157
161
|
"""
|
|
158
162
|
out: list[str] = ["version = 1", ""]
|
|
159
163
|
for e in entries:
|
|
160
164
|
out.append("[[images]]")
|
|
161
|
-
for key in (
|
|
165
|
+
for key in (
|
|
166
|
+
"name",
|
|
167
|
+
"src",
|
|
168
|
+
"resolved_src",
|
|
169
|
+
"format",
|
|
170
|
+
"arch",
|
|
171
|
+
"sha256",
|
|
172
|
+
"description",
|
|
173
|
+
"netboot_ref",
|
|
174
|
+
):
|
|
162
175
|
val = e.get(key)
|
|
163
176
|
if val is None or val == "":
|
|
164
177
|
continue
|
|
@@ -1053,9 +1066,10 @@ def main():
|
|
|
1053
1066
|
mgr = DownloadManager(store, workers=args.workers)
|
|
1054
1067
|
auth_password = os.environ.get("WITHCACHE_ADMIN_PASSWORD")
|
|
1055
1068
|
|
|
1056
|
-
#
|
|
1057
|
-
# hits Download on /ui/catalog to enqueue a fetch.
|
|
1058
|
-
#
|
|
1069
|
+
# Withcache is operator-populated: cache misses return 404 (v0.10.0+)
|
|
1070
|
+
# and the operator hits Download on /ui/catalog to enqueue a fetch.
|
|
1071
|
+
# The v0.12.0 UI consolidation folded Cached + Downloads into the
|
|
1072
|
+
# Catalog page's per-row status column and progress bar.
|
|
1059
1073
|
print(
|
|
1060
1074
|
f"withcache cache-host on http://{args.host}:{args.port} "
|
|
1061
1075
|
f"(data={store.data_dir}, keep_query={args.keep_query}, workers={args.workers}, "
|
|
@@ -167,12 +167,28 @@ class AddCatalogEntryTests(_CatalogApiBase):
|
|
|
167
167
|
"sha256": "b" * 64,
|
|
168
168
|
"size_bytes": 2048,
|
|
169
169
|
"description": "Debian sysdev",
|
|
170
|
+
"netboot_ref": "debian-netboot",
|
|
170
171
|
},
|
|
171
172
|
)
|
|
172
173
|
self.assertEqual(r.status_code, 201, r.text)
|
|
173
174
|
stored = self.catalog.entries[0]
|
|
174
175
|
self.assertEqual(stored["description"], "Debian sysdev")
|
|
175
176
|
self.assertEqual(stored["resolved_src"], "https://example/debian.img.gz")
|
|
177
|
+
self.assertEqual(stored["netboot_ref"], "debian-netboot")
|
|
178
|
+
|
|
179
|
+
def test_netboot_ref_persists_through_toml_round_trip(self) -> None:
|
|
180
|
+
"""netboot_ref must survive the catalog.toml write-then-reload."""
|
|
181
|
+
self.client.post(
|
|
182
|
+
"/catalog/entries",
|
|
183
|
+
json={
|
|
184
|
+
"name": "debian",
|
|
185
|
+
"src": "https://example/debian.img.gz",
|
|
186
|
+
"netboot_ref": "debian-netboot",
|
|
187
|
+
},
|
|
188
|
+
)
|
|
189
|
+
toml_path = os.path.join(self._tmpdir, "catalog.toml")
|
|
190
|
+
content = open(toml_path, encoding="utf-8").read()
|
|
191
|
+
self.assertIn('netboot_ref = "debian-netboot"', content)
|
|
176
192
|
|
|
177
193
|
def test_add_persists_to_disk(self) -> None:
|
|
178
194
|
self.client.post(
|
|
@@ -967,5 +967,71 @@ class TestHumanSize(unittest.TestCase):
|
|
|
967
967
|
self.assertIn("GiB", server.human_size(3 * 1024 * 1024 * 1024))
|
|
968
968
|
|
|
969
969
|
|
|
970
|
+
# --------------------------------------------------------------------------
|
|
971
|
+
# client.is_healthy: GET /healthz reachability probe consumed by bty's
|
|
972
|
+
# Settings pill so a red URL is caught at render time instead of on first
|
|
973
|
+
# flash.
|
|
974
|
+
# --------------------------------------------------------------------------
|
|
975
|
+
class _HealthzOK(http.server.BaseHTTPRequestHandler):
|
|
976
|
+
def do_GET(self):
|
|
977
|
+
if self.path == "/healthz":
|
|
978
|
+
body = b'{"status":"ok"}'
|
|
979
|
+
self.send_response(200)
|
|
980
|
+
self.send_header("Content-Type", "application/json")
|
|
981
|
+
self.send_header("Content-Length", str(len(body)))
|
|
982
|
+
self.end_headers()
|
|
983
|
+
self.wfile.write(body)
|
|
984
|
+
else:
|
|
985
|
+
self.send_response(404)
|
|
986
|
+
self.end_headers()
|
|
987
|
+
|
|
988
|
+
def log_message(self, format, *args):
|
|
989
|
+
pass
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
class _HealthzFail(http.server.BaseHTTPRequestHandler):
|
|
993
|
+
def do_GET(self):
|
|
994
|
+
self.send_response(503)
|
|
995
|
+
self.end_headers()
|
|
996
|
+
|
|
997
|
+
def log_message(self, format, *args):
|
|
998
|
+
pass
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
class TestIsHealthy(unittest.TestCase):
|
|
1002
|
+
def _serve(self, handler_cls):
|
|
1003
|
+
httpd = socketserver.TCPServer(("127.0.0.1", 0), handler_cls)
|
|
1004
|
+
port = httpd.server_address[1]
|
|
1005
|
+
t = threading.Thread(target=httpd.serve_forever, daemon=True)
|
|
1006
|
+
t.start()
|
|
1007
|
+
return httpd, port
|
|
1008
|
+
|
|
1009
|
+
def test_is_healthy_true_on_200(self):
|
|
1010
|
+
from withcache import client
|
|
1011
|
+
|
|
1012
|
+
httpd, port = self._serve(_HealthzOK)
|
|
1013
|
+
try:
|
|
1014
|
+
self.assertTrue(client.is_healthy(f"http://127.0.0.1:{port}", timeout=2.0))
|
|
1015
|
+
finally:
|
|
1016
|
+
httpd.shutdown()
|
|
1017
|
+
httpd.server_close()
|
|
1018
|
+
|
|
1019
|
+
def test_is_healthy_false_on_non_2xx(self):
|
|
1020
|
+
from withcache import client
|
|
1021
|
+
|
|
1022
|
+
httpd, port = self._serve(_HealthzFail)
|
|
1023
|
+
try:
|
|
1024
|
+
self.assertFalse(client.is_healthy(f"http://127.0.0.1:{port}", timeout=2.0))
|
|
1025
|
+
finally:
|
|
1026
|
+
httpd.shutdown()
|
|
1027
|
+
httpd.server_close()
|
|
1028
|
+
|
|
1029
|
+
def test_is_healthy_false_when_unreachable(self):
|
|
1030
|
+
from withcache import client
|
|
1031
|
+
|
|
1032
|
+
# 127.0.0.1:1 -- reserved and closed on any sane host.
|
|
1033
|
+
self.assertFalse(client.is_healthy("http://127.0.0.1:1", timeout=0.5))
|
|
1034
|
+
|
|
1035
|
+
|
|
970
1036
|
if __name__ == "__main__":
|
|
971
1037
|
unittest.main(verbosity=2)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|