pixie-lab 0.1.0__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 (70) hide show
  1. pixie/__init__.py +20 -0
  2. pixie/_util.py +38 -0
  3. pixie/catalog/__init__.py +25 -0
  4. pixie/catalog/_fetcher.py +513 -0
  5. pixie/catalog/_routes.py +392 -0
  6. pixie/catalog/_schema.py +263 -0
  7. pixie/catalog/_store.py +234 -0
  8. pixie/deploy/__init__.py +26 -0
  9. pixie/deploy/_main.py +321 -0
  10. pixie/deploy/_templates.py +153 -0
  11. pixie/disks.py +85 -0
  12. pixie/events/__init__.py +28 -0
  13. pixie/events/_kinds.py +203 -0
  14. pixie/events/_log.py +210 -0
  15. pixie/events/_routes.py +42 -0
  16. pixie/exports/__init__.py +17 -0
  17. pixie/exports/_routes.py +211 -0
  18. pixie/exports/_store.py +156 -0
  19. pixie/exports/_supervisor.py +240 -0
  20. pixie/flash.py +1971 -0
  21. pixie/images.py +473 -0
  22. pixie/machines/__init__.py +16 -0
  23. pixie/machines/_routes.py +190 -0
  24. pixie/machines/_store.py +623 -0
  25. pixie/oras.py +547 -0
  26. pixie/pivot/__init__.py +180 -0
  27. pixie/pivot/nbdboot +348 -0
  28. pixie/pxe/__init__.py +19 -0
  29. pixie/pxe/_renderer.py +244 -0
  30. pixie/pxe/_routes.py +386 -0
  31. pixie/tftp/__init__.py +21 -0
  32. pixie/tftp/_supervisor.py +129 -0
  33. pixie/tui/__init__.py +185 -0
  34. pixie/tui/_app.py +2219 -0
  35. pixie/tui_catalog.py +657 -0
  36. pixie/web/__init__.py +6 -0
  37. pixie/web/_auth.py +70 -0
  38. pixie/web/_settings_store.py +211 -0
  39. pixie/web/_static/.gitkeep +0 -0
  40. pixie/web/_static/bootstrap-icons.min.css +5 -0
  41. pixie/web/_static/bootstrap.min.css +12 -0
  42. pixie/web/_static/fonts/bootstrap-icons.woff +0 -0
  43. pixie/web/_static/fonts/bootstrap-icons.woff2 +0 -0
  44. pixie/web/_static/htmx.min.js +1 -0
  45. pixie/web/_static/pixie-favicon.png +0 -0
  46. pixie/web/_static/sse.js +290 -0
  47. pixie/web/_table_state.py +261 -0
  48. pixie/web/_templates/_partials/page_description.html +22 -0
  49. pixie/web/_templates/_partials/recent_events.html +47 -0
  50. pixie/web/_templates/_partials/table_helpers.html +189 -0
  51. pixie/web/_templates/catalog.html +364 -0
  52. pixie/web/_templates/catalog_detail.html +386 -0
  53. pixie/web/_templates/dashboard.html +256 -0
  54. pixie/web/_templates/events.html +125 -0
  55. pixie/web/_templates/ipxe/bootstrap.j2 +12 -0
  56. pixie/web/_templates/ipxe/exit.j2 +10 -0
  57. pixie/web/_templates/ipxe/nbdboot.j2 +57 -0
  58. pixie/web/_templates/ipxe/pixie-live-env.j2 +55 -0
  59. pixie/web/_templates/ipxe/unavailable.j2 +14 -0
  60. pixie/web/_templates/layout.html +345 -0
  61. pixie/web/_templates/login.html +40 -0
  62. pixie/web/_templates/machine_detail.html +786 -0
  63. pixie/web/_templates/machines.html +186 -0
  64. pixie/web/_templates/settings.html +265 -0
  65. pixie/web/main.py +1910 -0
  66. pixie_lab-0.1.0.dist-info/METADATA +107 -0
  67. pixie_lab-0.1.0.dist-info/RECORD +70 -0
  68. pixie_lab-0.1.0.dist-info/WHEEL +4 -0
  69. pixie_lab-0.1.0.dist-info/entry_points.txt +3 -0
  70. pixie_lab-0.1.0.dist-info/licenses/LICENSE +674 -0
@@ -0,0 +1,129 @@
1
+ """TftpServer: manage one ``in.tftpd`` child.
2
+
3
+ Argv shape:
4
+
5
+ in.tftpd --foreground --listen --address <bind>:<port> --secure <root>
6
+
7
+ ``--listen`` runs in daemon-listener mode (not inetd); ``--foreground``
8
+ keeps it as a supervised child so our poll() sees it live (without it,
9
+ tftpd-hpa double-forks and the parent exits rc=0, which our start()
10
+ grace period would mis-flag as a startup failure); ``--secure`` chroots
11
+ into ``<root>`` so a malformed RRQ can't wander outside the served
12
+ directory.
13
+
14
+ The wrapper is deliberately narrow -- no reload, no diff-sync, no
15
+ multi-server. TFTP is boot-time-critical + operator-visible; a single
16
+ supervised in.tftpd is the correct shape.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import contextlib
22
+ import logging
23
+ import subprocess
24
+ import sys
25
+ import threading
26
+ import time
27
+ from pathlib import Path
28
+
29
+ _log = logging.getLogger(__name__)
30
+
31
+ DEFAULT_TFTP_BIN = "in.tftpd"
32
+ # NBPs get baked into the container image at build time (Containerfile
33
+ # copies undionly.kpxe + ipxe.efi from the ipxe package into this path).
34
+ # Runtime code should treat this path as read-only.
35
+ DEFAULT_TFTP_ROOT = Path("/usr/share/pixie/tftp")
36
+ DEFAULT_TFTP_PORT = 69
37
+ _SPAWN_STARTUP_GRACE = 0.2 # seconds -- give in.tftpd a moment to bind or fail
38
+
39
+
40
+ class TftpServer:
41
+ """Wraps a single ``in.tftpd`` child. Thread-safe.
42
+
43
+ * ``bind`` -- interface to listen on (default ``0.0.0.0``).
44
+ * ``port`` -- UDP port (default 69). Non-root callers must
45
+ override; udp/69 requires ``CAP_NET_BIND_SERVICE`` or root.
46
+ * ``root`` -- directory served. Callers guarantee this exists
47
+ + is readable; the class does not create it.
48
+ * ``bin`` -- path to the ``in.tftpd`` binary. Overridable for
49
+ unit tests that pretend they have a tftp server.
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ *,
55
+ bind: str = "0.0.0.0",
56
+ port: int = DEFAULT_TFTP_PORT,
57
+ root: Path = DEFAULT_TFTP_ROOT,
58
+ bin: str = DEFAULT_TFTP_BIN,
59
+ ) -> None:
60
+ self.bind = bind
61
+ self.port = port
62
+ self.root = Path(root)
63
+ self.bin = bin
64
+ self._proc: subprocess.Popen[bytes] | None = None
65
+ self._lock = threading.Lock()
66
+
67
+ def start(self) -> None:
68
+ """Spawn ``in.tftpd``. Idempotent: no-op if already running.
69
+
70
+ Raises :class:`RuntimeError` if the binary is missing, the
71
+ root directory does not exist, or the child exits within the
72
+ spawn grace period.
73
+ """
74
+ with self._lock:
75
+ if self._proc is not None and self._proc.poll() is None:
76
+ return
77
+ self._proc = None
78
+
79
+ if not self.root.is_dir():
80
+ raise RuntimeError(f"tftp root does not exist: {self.root!s}")
81
+
82
+ argv = [
83
+ self.bin,
84
+ # ``--foreground`` is essential -- without it,
85
+ # tftpd-hpa's ``--listen`` mode daemonises (double-forks
86
+ # + parent exits rc=0) and our supervisor mis-detects
87
+ # the immediate parent-exit as a startup failure even
88
+ # though the grandchild is happily bound to udp/69.
89
+ # Verified live 2026-07-14 on 10.20.30.10 booting a
90
+ # real UEFI target.
91
+ "--foreground",
92
+ "--listen",
93
+ "--address",
94
+ f"{self.bind}:{self.port}",
95
+ "--secure",
96
+ str(self.root),
97
+ ]
98
+ _log.info("tftp spawn: %s", " ".join(argv))
99
+ try:
100
+ proc = subprocess.Popen(argv, stdout=sys.stderr, stderr=sys.stderr)
101
+ except FileNotFoundError as exc:
102
+ raise RuntimeError(f"tftp binary not found: {self.bin!r}") from exc
103
+
104
+ time.sleep(_SPAWN_STARTUP_GRACE)
105
+ if proc.poll() is not None:
106
+ rc = proc.returncode
107
+ raise RuntimeError(
108
+ f"in.tftpd exited immediately (rc={rc}, bind={self.bind}:{self.port}, "
109
+ f"root={self.root!s}); check binary exists + port + root permissions"
110
+ )
111
+ self._proc = proc
112
+
113
+ def stop(self) -> None:
114
+ """Terminate the child. Idempotent."""
115
+ with self._lock:
116
+ proc = self._proc
117
+ self._proc = None
118
+ if proc is None:
119
+ return
120
+ if proc.poll() is None:
121
+ proc.terminate()
122
+ with contextlib.suppress(subprocess.TimeoutExpired):
123
+ proc.wait(timeout=3)
124
+ if proc.poll() is None:
125
+ proc.kill()
126
+
127
+ def is_running(self) -> bool:
128
+ with self._lock:
129
+ return self._proc is not None and self._proc.poll() is None
pixie/tui/__init__.py ADDED
@@ -0,0 +1,185 @@
1
+ """pixie.tui - the pixie terminal interface.
2
+
3
+ Module name is historical (Rich-based wizard module); the
4
+ console script is ``pixie``. This module is intentionally
5
+ lightweight: it imports nothing from :mod:`rich` at module level
6
+ so an install without the ``[tui]`` extra can still ``import
7
+ pixie.tui`` for introspection without crashing. The actual Rich-
8
+ based app lives in :mod:`pixie.tui._app`, which is loaded only
9
+ when ``pixie`` is invoked.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import sys
16
+
17
+ import pixie
18
+
19
+ # Default ``--server`` value for the wizard. ``pixie`` is the
20
+ # canonical LAN-DNS / mDNS hostname operators are encouraged to point
21
+ # at their pixie server, so ``pixie --mac X`` against a fresh box Just
22
+ # Works without any flags. Owned here (the [tui]-extra-free entry
23
+ # module) so the argparse default and ``BtyTui``'s constructor default
24
+ # can both depend on it without the import dragging in Rich.
25
+ DEFAULT_SERVER = "pixie"
26
+
27
+
28
+ def main(argv: list[str] | None = None, *, prog: str = "pixie") -> None:
29
+ """Console-script entry point for ``pixie``.
30
+
31
+ Defers loading the Rich-based app until invocation time so a
32
+ missing ``[tui]`` extra produces a clear "reinstall with extras"
33
+ message rather than a raw ``ModuleNotFoundError``.
34
+
35
+ The deploy-bootstrap surface lives in :mod:`pixie.deploy` (the
36
+ ``pixie-lab init`` console script) so this script stays lean -- one
37
+ job, one wizard.
38
+ """
39
+ parser = argparse.ArgumentParser(
40
+ prog=prog,
41
+ description=(
42
+ f"{prog}: flash images onto target disks, locally or via PXE. "
43
+ f"Three modes:\n\n"
44
+ f" {prog} - interactive wizard\n"
45
+ f" (local image-root only)\n"
46
+ f" {prog} --catalog <URL> - interactive wizard with\n"
47
+ f" the given catalog pre-loaded\n"
48
+ f" (equivalent to picking [c]\n"
49
+ f" on the source screen and\n"
50
+ f" typing the URL).\n"
51
+ f" {prog} --mac <MAC> - server-driven mode:\n"
52
+ f" fetches a plan from\n"
53
+ f" --server's /pxe/<MAC>/plan\n"
54
+ f" and acts on it (auto-flash,\n"
55
+ f" interactive, or local-boot,\n"
56
+ f" whatever the server says).\n\n"
57
+ "The operator-facing surface is intentionally narrow: in\n"
58
+ "server-driven mode every knob (image, target disk, catalog\n"
59
+ "overlay) comes from the pixie's machine record, not the\n"
60
+ "cmdline. --catalog is only useful for hand-driven runs.\n\n"
61
+ "To bootstrap a pixie + withcache + nbdmux container deploy,\n"
62
+ "use the sibling ``pixie-lab init`` script (runnable via\n"
63
+ "``uvx pixie-lab init`` without installing)."
64
+ ),
65
+ formatter_class=argparse.RawDescriptionHelpFormatter,
66
+ )
67
+ parser.add_argument(
68
+ "--version",
69
+ action="version",
70
+ version=f"{prog} {pixie.__version__}",
71
+ )
72
+ parser.add_argument(
73
+ "--server",
74
+ type=str,
75
+ default=DEFAULT_SERVER,
76
+ help=f"pixie base URL or hostname. Default ``{DEFAULT_SERVER}`` "
77
+ "(operator convenience: pair with a LAN DNS entry pointing at "
78
+ "the pixie server and ``pixie --mac X`` just works). The netboot "
79
+ "and USB-PXE paths pass this explicitly via ``pixie.server=`` "
80
+ "on the kernel cmdline. Bare hostnames are accepted; missing "
81
+ "scheme defaults to ``http://``.",
82
+ )
83
+ parser.add_argument(
84
+ "--mac",
85
+ type=str,
86
+ default=None,
87
+ help="Self-MAC of this client (e.g. ``aa:bb:cc:dd:ee:ff``). "
88
+ "When supplied, pixie switches to server-driven mode: it "
89
+ "GETs ``<server>/pxe/<mac>/plan`` and dispatches on the "
90
+ "returned plan (auto-flash, interactive, or no-op). The "
91
+ "live env passes this via ``pixie.mac=`` on the kernel cmdline.",
92
+ )
93
+ parser.add_argument(
94
+ "--catalog",
95
+ type=str,
96
+ default=None,
97
+ help="Catalog URL or path to pre-load (http(s):// for HTTP, "
98
+ "oras:// for OCI, or a local path). When given, the SELECT_CATALOG "
99
+ "screen is skipped and the wizard jumps straight to SELECT_IMAGE "
100
+ "with this catalog overlaying the local image-root (equivalent "
101
+ "to picking ``[c]`` on the source screen and typing the URL). "
102
+ "Ignored in server-driven mode (``--mac`` set) because the server "
103
+ "supplies the catalog as part of /pxe/<mac>/plan.",
104
+ )
105
+ args = parser.parse_args(argv)
106
+
107
+ # Lifecycle progress -- the launch path has two slow phases an
108
+ # operator stares at without feedback otherwise:
109
+ #
110
+ # 1. ``from pixie.tui._app import BtyTui`` (1-3s): pulls Rich
111
+ # + pixie.catalog + pixie.flash + withcache.oras into the
112
+ # interpreter. On slower hardware (low-end mini-PCs, EPYC
113
+ # bringup boxes) this is several seconds of "blinking
114
+ # cursor".
115
+ # 2. ``BtyTui(...).run()`` -> the wizard prints its first
116
+ # header (Rich is no-alt-screen, so prior stderr output
117
+ # stays visible above the header). On the live env's
118
+ # framebuffer console first print is typically under a
119
+ # second after import.
120
+ #
121
+ # Print progress to stderr BEFORE the import + the run. The
122
+ # operator sees: wrapper banner (from /usr/local/sbin/pixie-on-tty1
123
+ # on the live env) -> these progress lines -> the pixie header.
124
+ # The blank-screen window narrows to a few hundred ms while
125
+ # Rich's Console initialises.
126
+ #
127
+ # Also mirror to ``/run/pixie.status`` so an operator who Alt-F2'd
128
+ # to tty2 can ``cat`` it without having to read tty1's transient
129
+ # output. ``/run`` is tmpfs on the live env so this is forgotten
130
+ # on reboot; cheap to write.
131
+ def _progress(msg: str) -> None:
132
+ line = f"{prog}: {msg}"
133
+ print(line, file=sys.stderr, flush=True)
134
+ try:
135
+ with open("/run/pixie.status", "a", encoding="utf-8") as f:
136
+ f.write(line + "\n")
137
+ except OSError:
138
+ pass
139
+
140
+ _progress(f"v{pixie.__version__} starting...")
141
+ _progress("loading UI dependencies (Rich)...")
142
+ try:
143
+ from pixie.tui._app import BtyTui
144
+ except ImportError as exc:
145
+ print(
146
+ f"{prog} {pixie.__version__}: required dependency is not installed "
147
+ f"({exc.name or exc}); reinstall with "
148
+ '`pipx install "pixie-lab[tui]"`',
149
+ file=sys.stderr,
150
+ )
151
+ sys.exit(1)
152
+ _progress("dependencies loaded")
153
+ if args.mac:
154
+ _progress(f"server-driven mode: server={args.server} mac={args.mac}")
155
+ _progress("starting interface (first paint may take a few seconds)...")
156
+
157
+ # Lifecycle bookends, broadcast via /dev/kmsg + /dev/console
158
+ # (the same fanout the chain-test markers + flash milestones
159
+ # use). The pair lets an operator on IPMI SoL, on the kernel
160
+ # serial log, or tailing ``journalctl -u pixie-on-tty1`` follow
161
+ # along regardless of which pixie mode runs (auto-flash from a
162
+ # plan, interactive wizard, USB-local). The mid-flight
163
+ # markers (``auto-flash starting``, ``download NN%``,
164
+ # ``write NN%``, ``flash complete; rebooting``) fire between
165
+ # these bookends as they always have.
166
+ #
167
+ # The import lives here, not at module top, because the
168
+ # missing-dep branch above must still produce a clean
169
+ # "reinstall with extras" message instead of crashing on the
170
+ # import itself; once we reach this line, ``_app`` is known
171
+ # to import cleanly.
172
+ from pixie.tui._app import _emit_console_marker
173
+
174
+ _emit_console_marker(f"pixie: entered v{pixie.__version__}")
175
+ try:
176
+ BtyTui(server=args.server, mac=args.mac, catalog=args.catalog).run()
177
+ finally:
178
+ # ``finally`` so the marker fires for every exit path:
179
+ # clean run, SystemExit from a sys.exit(N) deep inside
180
+ # the wizard, KeyboardInterrupt, an unhandled exception,
181
+ # and the post-flash ``_do_reboot`` (which returns
182
+ # promptly before systemd kills us). Best-effort under
183
+ # contextlib.suppress inside ``_emit_console_marker``;
184
+ # never raises.
185
+ _emit_console_marker(f"pixie: exiting v{pixie.__version__}")