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.
Files changed (99) hide show
  1. faden_cli/__init__.py +16 -0
  2. faden_cli/__main__.py +9 -0
  3. faden_cli/bundle/shared/.gitignore +25 -0
  4. faden_cli/bundle/shared/LICENSE +187 -0
  5. faden_cli/bundle/shared/NOTICE +15 -0
  6. faden_cli/bundle/shared/OFFICIAL.md +9 -0
  7. faden_cli/bundle/shared/README.md +16 -0
  8. faden_cli/bundle/shared/agent-binaries/.gitignore +5 -0
  9. faden_cli/bundle/shared/alloy/config.alloy +40 -0
  10. faden_cli/bundle/shared/clickhouse/config.d/system-logs.xml +26 -0
  11. faden_cli/bundle/shared/docker-compose.gpu.yaml +34 -0
  12. faden_cli/bundle/shared/docker-compose.yaml +998 -0
  13. faden_cli/bundle/shared/grafana/dashboards/templates/vllm-runtime.json +2099 -0
  14. faden_cli/bundle/shared/grafana/provisioning/dashboards/dashboard.yml +10 -0
  15. faden_cli/bundle/shared/grafana/provisioning/dashboards/json/faden-overview.json +211 -0
  16. faden_cli/bundle/shared/grafana/provisioning/datasources/ds.yaml +9 -0
  17. faden_cli/bundle/shared/grafana/provisioning/datasources/prometheus.yaml +15 -0
  18. faden_cli/bundle/shared/initdb/01-init.sql +32 -0
  19. faden_cli/bundle/shared/loki/local-config.yaml +32 -0
  20. faden_cli/bundle/shared/nginx/locations.conf +245 -0
  21. faden_cli/bundle/shared/nginx/maintenance.html +239 -0
  22. faden_cli/bundle/shared/nginx/nginx-tls.conf +79 -0
  23. faden_cli/bundle/shared/nginx/nginx.conf +44 -0
  24. faden_cli/bundle/shared/nginx/rate-limits.conf +42 -0
  25. faden_cli/bundle/shared/nginx/security-headers.conf +22 -0
  26. faden_cli/bundle/shared/nginx/select-config.sh +37 -0
  27. faden_cli/bundle/shared/prometheus/prometheus.yml +40 -0
  28. faden_cli/bundle/shared/rabbitmq/definitions.json.example +37 -0
  29. faden_cli/bundle/shared/tls/README.md +30 -0
  30. faden_cli/cli.py +158 -0
  31. faden_cli/commands/__init__.py +1 -0
  32. faden_cli/commands/admin.py +161 -0
  33. faden_cli/commands/backup.py +159 -0
  34. faden_cli/commands/config.py +146 -0
  35. faden_cli/commands/deploy.py +949 -0
  36. faden_cli/commands/dev/__init__.py +1 -0
  37. faden_cli/commands/dev/dev_doctor.py +46 -0
  38. faden_cli/commands/dev/dev_down.py +241 -0
  39. faden_cli/commands/dev/dev_group.py +24 -0
  40. faden_cli/commands/dev/dev_init.py +710 -0
  41. faden_cli/commands/dev/dev_pull.py +297 -0
  42. faden_cli/commands/dev/dev_status.py +151 -0
  43. faden_cli/commands/dev/dev_up.py +1392 -0
  44. faden_cli/commands/doctor.py +225 -0
  45. faden_cli/commands/down.py +75 -0
  46. faden_cli/commands/logs_cmd.py +41 -0
  47. faden_cli/commands/maintenance.py +74 -0
  48. faden_cli/commands/module.py +113 -0
  49. faden_cli/commands/observe.py +95 -0
  50. faden_cli/commands/ports.py +91 -0
  51. faden_cli/commands/rebuild.py +158 -0
  52. faden_cli/commands/restore.py +139 -0
  53. faden_cli/commands/runtime_images.py +174 -0
  54. faden_cli/commands/status.py +71 -0
  55. faden_cli/commands/tls.py +722 -0
  56. faden_cli/commands/tune.py +37 -0
  57. faden_cli/commands/up.py +64 -0
  58. faden_cli/commands/upgrade.py +470 -0
  59. faden_cli/commands/version.py +78 -0
  60. faden_cli/core/__init__.py +1 -0
  61. faden_cli/core/api_client.py +77 -0
  62. faden_cli/core/backup.py +717 -0
  63. faden_cli/core/bootstrap.py +291 -0
  64. faden_cli/core/bundle.py +110 -0
  65. faden_cli/core/clickhouse.py +61 -0
  66. faden_cli/core/compose.py +484 -0
  67. faden_cli/core/console.py +106 -0
  68. faden_cli/core/detect.py +580 -0
  69. faden_cli/core/dev_modules.py +149 -0
  70. faden_cli/core/docker_env.py +147 -0
  71. faden_cli/core/edge.py +94 -0
  72. faden_cli/core/env_gen.py +598 -0
  73. faden_cli/core/git.py +219 -0
  74. faden_cli/core/install.py +288 -0
  75. faden_cli/core/installation.py +186 -0
  76. faden_cli/core/local_node.py +719 -0
  77. faden_cli/core/maintenance.py +82 -0
  78. faden_cli/core/private_files.py +133 -0
  79. faden_cli/core/registry.py +574 -0
  80. faden_cli/core/release_feed.py +342 -0
  81. faden_cli/core/rmq.py +135 -0
  82. faden_cli/core/runtime_images.py +309 -0
  83. faden_cli/core/schedule.py +278 -0
  84. faden_cli/core/settings.py +159 -0
  85. faden_cli/core/sysinfo.py +303 -0
  86. faden_cli/core/tls.py +790 -0
  87. faden_cli/core/tune.py +154 -0
  88. faden_cli/core/update_check.py +254 -0
  89. faden_cli/core/workspace.py +155 -0
  90. faden_cli/templates/env.j2 +33 -0
  91. faden_cli/tui/__init__.py +1 -0
  92. faden_cli/tui/widgets/__init__.py +1 -0
  93. faden_cli/tui/wizard/__init__.py +1 -0
  94. fadenstack-0.4.2.dist-info/METADATA +304 -0
  95. fadenstack-0.4.2.dist-info/RECORD +99 -0
  96. fadenstack-0.4.2.dist-info/WHEEL +4 -0
  97. fadenstack-0.4.2.dist-info/entry_points.txt +3 -0
  98. fadenstack-0.4.2.dist-info/licenses/LICENSE +187 -0
  99. 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