fadenstack 0.4.2__py3-none-any.whl
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.
- faden_cli/__init__.py +16 -0
- faden_cli/__main__.py +9 -0
- faden_cli/bundle/shared/.gitignore +25 -0
- faden_cli/bundle/shared/LICENSE +187 -0
- faden_cli/bundle/shared/NOTICE +15 -0
- faden_cli/bundle/shared/OFFICIAL.md +9 -0
- faden_cli/bundle/shared/README.md +16 -0
- faden_cli/bundle/shared/agent-binaries/.gitignore +5 -0
- faden_cli/bundle/shared/alloy/config.alloy +40 -0
- faden_cli/bundle/shared/clickhouse/config.d/system-logs.xml +26 -0
- faden_cli/bundle/shared/docker-compose.gpu.yaml +34 -0
- faden_cli/bundle/shared/docker-compose.yaml +998 -0
- faden_cli/bundle/shared/grafana/dashboards/templates/vllm-runtime.json +2099 -0
- faden_cli/bundle/shared/grafana/provisioning/dashboards/dashboard.yml +10 -0
- faden_cli/bundle/shared/grafana/provisioning/dashboards/json/faden-overview.json +211 -0
- faden_cli/bundle/shared/grafana/provisioning/datasources/ds.yaml +9 -0
- faden_cli/bundle/shared/grafana/provisioning/datasources/prometheus.yaml +15 -0
- faden_cli/bundle/shared/initdb/01-init.sql +32 -0
- faden_cli/bundle/shared/loki/local-config.yaml +32 -0
- faden_cli/bundle/shared/nginx/locations.conf +245 -0
- faden_cli/bundle/shared/nginx/maintenance.html +239 -0
- faden_cli/bundle/shared/nginx/nginx-tls.conf +79 -0
- faden_cli/bundle/shared/nginx/nginx.conf +44 -0
- faden_cli/bundle/shared/nginx/rate-limits.conf +42 -0
- faden_cli/bundle/shared/nginx/security-headers.conf +22 -0
- faden_cli/bundle/shared/nginx/select-config.sh +37 -0
- faden_cli/bundle/shared/prometheus/prometheus.yml +40 -0
- faden_cli/bundle/shared/rabbitmq/definitions.json.example +37 -0
- faden_cli/bundle/shared/tls/README.md +30 -0
- faden_cli/cli.py +158 -0
- faden_cli/commands/__init__.py +1 -0
- faden_cli/commands/admin.py +161 -0
- faden_cli/commands/backup.py +159 -0
- faden_cli/commands/config.py +146 -0
- faden_cli/commands/deploy.py +949 -0
- faden_cli/commands/dev/__init__.py +1 -0
- faden_cli/commands/dev/dev_doctor.py +46 -0
- faden_cli/commands/dev/dev_down.py +241 -0
- faden_cli/commands/dev/dev_group.py +24 -0
- faden_cli/commands/dev/dev_init.py +710 -0
- faden_cli/commands/dev/dev_pull.py +297 -0
- faden_cli/commands/dev/dev_status.py +151 -0
- faden_cli/commands/dev/dev_up.py +1392 -0
- faden_cli/commands/doctor.py +225 -0
- faden_cli/commands/down.py +75 -0
- faden_cli/commands/logs_cmd.py +41 -0
- faden_cli/commands/maintenance.py +74 -0
- faden_cli/commands/module.py +113 -0
- faden_cli/commands/observe.py +95 -0
- faden_cli/commands/ports.py +91 -0
- faden_cli/commands/rebuild.py +158 -0
- faden_cli/commands/restore.py +139 -0
- faden_cli/commands/runtime_images.py +174 -0
- faden_cli/commands/status.py +71 -0
- faden_cli/commands/tls.py +722 -0
- faden_cli/commands/tune.py +37 -0
- faden_cli/commands/up.py +64 -0
- faden_cli/commands/upgrade.py +470 -0
- faden_cli/commands/version.py +78 -0
- faden_cli/core/__init__.py +1 -0
- faden_cli/core/api_client.py +77 -0
- faden_cli/core/backup.py +717 -0
- faden_cli/core/bootstrap.py +291 -0
- faden_cli/core/bundle.py +110 -0
- faden_cli/core/clickhouse.py +61 -0
- faden_cli/core/compose.py +484 -0
- faden_cli/core/console.py +106 -0
- faden_cli/core/detect.py +580 -0
- faden_cli/core/dev_modules.py +149 -0
- faden_cli/core/docker_env.py +147 -0
- faden_cli/core/edge.py +94 -0
- faden_cli/core/env_gen.py +598 -0
- faden_cli/core/git.py +219 -0
- faden_cli/core/install.py +288 -0
- faden_cli/core/installation.py +186 -0
- faden_cli/core/local_node.py +719 -0
- faden_cli/core/maintenance.py +82 -0
- faden_cli/core/private_files.py +133 -0
- faden_cli/core/registry.py +574 -0
- faden_cli/core/release_feed.py +342 -0
- faden_cli/core/rmq.py +135 -0
- faden_cli/core/runtime_images.py +309 -0
- faden_cli/core/schedule.py +278 -0
- faden_cli/core/settings.py +159 -0
- faden_cli/core/sysinfo.py +303 -0
- faden_cli/core/tls.py +790 -0
- faden_cli/core/tune.py +154 -0
- faden_cli/core/update_check.py +254 -0
- faden_cli/core/workspace.py +155 -0
- faden_cli/templates/env.j2 +33 -0
- faden_cli/tui/__init__.py +1 -0
- faden_cli/tui/widgets/__init__.py +1 -0
- faden_cli/tui/wizard/__init__.py +1 -0
- fadenstack-0.4.2.dist-info/METADATA +304 -0
- fadenstack-0.4.2.dist-info/RECORD +99 -0
- fadenstack-0.4.2.dist-info/WHEEL +4 -0
- fadenstack-0.4.2.dist-info/entry_points.txt +3 -0
- fadenstack-0.4.2.dist-info/licenses/LICENSE +187 -0
- fadenstack-0.4.2.dist-info/licenses/NOTICE +15 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Maintenance mode: the front door answers with a maintenance page.
|
|
2
|
+
|
|
3
|
+
While ``nginx/maintenance.flag`` exists in the install, nginx answers every
|
|
4
|
+
page with ``nginx/maintenance.html`` -- which reloads itself once the
|
|
5
|
+
platform is back -- and the APIs with a 503 saying so (see
|
|
6
|
+
``shared/nginx/locations.conf``). Only ``/api/health`` and nginx's own
|
|
7
|
+
health check stay open. The flag is plain JSON the page reads to say why:
|
|
8
|
+
``{"reason": "upgrade" | "manual", "since": ..., "message": ...}``.
|
|
9
|
+
|
|
10
|
+
``faden upgrade`` puts it down for its restart and lifts it once the backend
|
|
11
|
+
is healthy again; ``faden maintenance on|off`` does it by hand.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
from collections.abc import Iterator
|
|
18
|
+
from contextlib import contextmanager
|
|
19
|
+
from datetime import UTC, datetime
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
FLAG = Path("nginx") / "maintenance.flag"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def flag_path(install_dir: Path) -> Path:
|
|
27
|
+
"""Where the flag lives in *install_dir* (nginx reads /etc/nginx/faden/maintenance.flag)."""
|
|
28
|
+
return install_dir / FLAG
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def state(install_dir: Path) -> dict[str, Any] | None:
|
|
32
|
+
"""What the flag says, or ``None`` when the platform is not in maintenance."""
|
|
33
|
+
path = flag_path(install_dir)
|
|
34
|
+
if not path.is_file():
|
|
35
|
+
return None
|
|
36
|
+
try:
|
|
37
|
+
found = json.loads(path.read_text(encoding="utf-8") or "{}")
|
|
38
|
+
except ValueError:
|
|
39
|
+
found = {}
|
|
40
|
+
return found if isinstance(found, dict) else {}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def enter(install_dir: Path, *, reason: str, message: str | None = None) -> Path:
|
|
44
|
+
"""Put the flag down: from now on nginx answers with the maintenance page."""
|
|
45
|
+
path = flag_path(install_dir)
|
|
46
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
doc: dict[str, Any] = {"reason": reason, "since": datetime.now(tz=UTC).isoformat(timespec="seconds")}
|
|
48
|
+
if message:
|
|
49
|
+
doc["message"] = message
|
|
50
|
+
# Readable by nginx's worker user whatever the umask: the install's nginx
|
|
51
|
+
# directory is bind-mounted read-only into the container.
|
|
52
|
+
path.write_text(json.dumps(doc) + "\n", encoding="utf-8")
|
|
53
|
+
path.chmod(0o644)
|
|
54
|
+
return path
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def leave(install_dir: Path) -> bool:
|
|
58
|
+
"""Lift the flag; whether there was one."""
|
|
59
|
+
path = flag_path(install_dir)
|
|
60
|
+
try:
|
|
61
|
+
path.unlink()
|
|
62
|
+
except FileNotFoundError:
|
|
63
|
+
return False
|
|
64
|
+
return True
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@contextmanager
|
|
68
|
+
def during(install_dir: Path, *, reason: str) -> Iterator[None]:
|
|
69
|
+
"""Maintenance for the block, lifted however it ends.
|
|
70
|
+
|
|
71
|
+
A failed upgrade must not leave the platform answering "maintenance":
|
|
72
|
+
whatever runs then is better reached than hidden. An operator who had
|
|
73
|
+
switched maintenance on by hand keeps it on.
|
|
74
|
+
"""
|
|
75
|
+
if state(install_dir) is not None:
|
|
76
|
+
yield
|
|
77
|
+
return
|
|
78
|
+
enter(install_dir, reason=reason)
|
|
79
|
+
try:
|
|
80
|
+
yield
|
|
81
|
+
finally:
|
|
82
|
+
leave(install_dir)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Files only the user who runs ``faden`` may read: ``.env``, backups, saved credentials.
|
|
2
|
+
|
|
3
|
+
``.env`` holds every password and key of an install, and a backup holds them and every database. Written
|
|
4
|
+
with the default umask they were readable by every local user: ``.env`` 0644 (0664 on a live install),
|
|
5
|
+
backups 0775 directories with 0664 dumps, manifest and ``.env.bak``, and root-owned 0644 volume archives.
|
|
6
|
+
|
|
7
|
+
Compose reads ``.env`` as the user who runs ``faden``, and no container mounts ``.env`` or the backups:
|
|
8
|
+
owner-only takes nothing from the services. Windows has no such bits; there these helpers write the files and
|
|
9
|
+
leave their access to the folder they are in.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import contextlib
|
|
15
|
+
import os
|
|
16
|
+
import stat
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import BinaryIO
|
|
19
|
+
|
|
20
|
+
#: Read and written by the owner only.
|
|
21
|
+
PRIVATE_FILE = 0o600
|
|
22
|
+
#: Entered and listed by the owner only.
|
|
23
|
+
PRIVATE_DIR = 0o700
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _posix() -> bool:
|
|
27
|
+
return os.name != "nt"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def open_private(path: Path, *, mode: int = PRIVATE_FILE) -> BinaryIO:
|
|
31
|
+
"""Open *path* for writing (truncated), with *mode* set before a byte is written.
|
|
32
|
+
|
|
33
|
+
``open()`` applies a mode to a file it creates only; a file that exists keeps its own, so the mode is set
|
|
34
|
+
on the open file as well. A file another user owns but this one may write (a group-writable ``.env``)
|
|
35
|
+
is still written; its mode stays, and ``faden deploy`` and ``faden upgrade`` say so.
|
|
36
|
+
"""
|
|
37
|
+
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC | getattr(os, "O_BINARY", 0)
|
|
38
|
+
fd = os.open(str(path), flags, mode)
|
|
39
|
+
if _posix():
|
|
40
|
+
try:
|
|
41
|
+
os.fchmod(fd, mode)
|
|
42
|
+
except PermissionError:
|
|
43
|
+
pass
|
|
44
|
+
except BaseException:
|
|
45
|
+
os.close(fd)
|
|
46
|
+
raise
|
|
47
|
+
return os.fdopen(fd, "wb")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def write_private(path: Path, data: bytes, *, mode: int = PRIVATE_FILE) -> Path:
|
|
51
|
+
"""Write *data* to *path*, readable by its owner only (or with *mode*) from the start."""
|
|
52
|
+
with open_private(path, mode=mode) as handle:
|
|
53
|
+
handle.write(data)
|
|
54
|
+
return path
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def make_private(path: Path, *, mode: int = PRIVATE_FILE) -> bool:
|
|
58
|
+
"""Give an existing file *mode*; ``False`` when it could not be changed (another user's file)."""
|
|
59
|
+
if not _posix() or not path.is_file() or path.is_symlink():
|
|
60
|
+
return True
|
|
61
|
+
try:
|
|
62
|
+
if stat.S_IMODE(path.stat().st_mode) != mode:
|
|
63
|
+
path.chmod(mode)
|
|
64
|
+
except OSError:
|
|
65
|
+
return False
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def private_dir(path: Path) -> Path:
|
|
70
|
+
"""Make *path* (and its parents) and give it :data:`PRIVATE_DIR`; the parents keep their modes."""
|
|
71
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
if _posix():
|
|
73
|
+
# A directory someone else owns, such as a mount point, keeps its mode: its files are still private.
|
|
74
|
+
with contextlib.suppress(OSError):
|
|
75
|
+
path.chmod(PRIVATE_DIR)
|
|
76
|
+
return path
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def tighten_tree(root: Path) -> list[Path]:
|
|
80
|
+
"""Make *root* and everything below it owner-only: directories 0700, files 0600.
|
|
81
|
+
|
|
82
|
+
For an install's backups, from before they were written that way. Returns what could not be changed,
|
|
83
|
+
such as a volume archive a helper container wrote as root; inside a 0700 directory nobody else reaches
|
|
84
|
+
it anyway. Symbolic links are left alone, and not followed.
|
|
85
|
+
"""
|
|
86
|
+
if not _posix() or not root.is_dir() or root.is_symlink():
|
|
87
|
+
return []
|
|
88
|
+
stuck: list[Path] = []
|
|
89
|
+
# Top-down and without following links: each directory is listed, then made private.
|
|
90
|
+
for current, _dirs, files in os.walk(root):
|
|
91
|
+
here = Path(current)
|
|
92
|
+
try:
|
|
93
|
+
if stat.S_IMODE(here.stat().st_mode) != PRIVATE_DIR:
|
|
94
|
+
here.chmod(PRIVATE_DIR)
|
|
95
|
+
except OSError:
|
|
96
|
+
stuck.append(here)
|
|
97
|
+
stuck.extend(here / name for name in files if not make_private(here / name))
|
|
98
|
+
return stuck
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def secure_install(install_dir: Path) -> list[Path]:
|
|
102
|
+
"""Make an install's secrets owner-only: ``.env``, the saved credentials, the backups.
|
|
103
|
+
|
|
104
|
+
``faden deploy`` and ``faden upgrade`` run it, so an install made before keeps nothing readable by
|
|
105
|
+
others. Warns about what could not be changed, and returns it. Files another user owns inside a
|
|
106
|
+
backups directory that is now private (volume archives older backups had written as root) are out of
|
|
107
|
+
everyone else's reach, and only mentioned.
|
|
108
|
+
"""
|
|
109
|
+
from faden_cli.core.console import info, warning # noqa: PLC0415
|
|
110
|
+
|
|
111
|
+
install_dir = Path(install_dir)
|
|
112
|
+
exposed = [
|
|
113
|
+
path
|
|
114
|
+
for path in (install_dir / ".env", install_dir / ".bootstrap-credentials")
|
|
115
|
+
if not make_private(path)
|
|
116
|
+
]
|
|
117
|
+
backups = install_dir / "backups"
|
|
118
|
+
stuck = tighten_tree(backups)
|
|
119
|
+
shielded: list[Path] = []
|
|
120
|
+
(exposed if backups in stuck else shielded).extend(stuck)
|
|
121
|
+
if exposed:
|
|
122
|
+
paths = " ".join(str(path) for path in exposed[:5])
|
|
123
|
+
more = f" (and {len(exposed) - 5} more)" if len(exposed) > 5 else "" # noqa: PLR2004
|
|
124
|
+
warning(
|
|
125
|
+
"Another user owns these, so they could not be made readable by their owner only:"
|
|
126
|
+
f' {paths}{more}.\n Fix with: sudo chown -R "$(id -un)" {paths}, then run this command again.'
|
|
127
|
+
)
|
|
128
|
+
if shielded:
|
|
129
|
+
info(
|
|
130
|
+
f"{len(shielded)} file(s) in the backups belong to another user (archives older backups wrote as"
|
|
131
|
+
" root); the backups directory is private, so nobody else reaches them."
|
|
132
|
+
)
|
|
133
|
+
return exposed
|