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,180 @@
1
+ """Pixie's initramfs pivot overlay.
2
+
3
+ Rationale
4
+ ---------
5
+
6
+ The mode-specific pivot script (``/scripts/nbdboot`` -- the one that
7
+ parses ``pixie.nbd``/``pixie.image`` off the kernel cmdline, sets up
8
+ NBD + overlayfs, then pivot_roots) used to be BAKED INTO nosi's
9
+ netboot bundle via pixie-media's live-build hook chain (nosi
10
+ inherits pixie-media). That coupled two projects at the artifact
11
+ level:
12
+
13
+ * nosi could not publish a "generic" kernel + initrd anyone could
14
+ netboot for their own purposes without carrying pixie's mode-
15
+ specific pivot script.
16
+ * Renaming or updating the pivot script required rebuilding every
17
+ nosi image.
18
+
19
+ Fix: pixie ships the pivot script here and serves it as a
20
+ supplementary initramfs overlay (newc-cpio, gzip-compressed) at
21
+ ``GET /pivot/nbdboot.cpio.gz``. iPXE's ``nbdboot.j2`` template
22
+ loads two ``initrd`` directives -- nosi's own initrd first, then
23
+ this overlay -- and Linux concatenates them; the overlay's
24
+ ``/scripts/nbdboot`` wins for the ``boot=nbdboot`` dispatch. Nosi's
25
+ netboot bundle becomes just "kernel + initrd + Debian
26
+ initramfs-tools" -- no pixie flavour.
27
+
28
+ This module owns:
29
+
30
+ - The script bytes (:data:`NBDBOOT_SCRIPT`, loaded from
31
+ ``pivot/nbdboot`` in the source tree).
32
+ - The newc-cpio + gzip builder (:func:`build_pivot_cpio_gz`) that
33
+ wraps the script into a ``/scripts/nbdboot`` entry the kernel
34
+ can find at boot.
35
+
36
+ Downstream state:
37
+
38
+ - ``pxe/_renderer.py`` emits ``boot=nbdboot`` on the kernel
39
+ cmdline; the ``nbdboot.j2`` template drops a second ``initrd``
40
+ directive pointing at :func:`pixie.web.main`'s
41
+ ``/pivot/nbdboot.cpio.gz`` route.
42
+ - ``pixie-media`` no longer bakes any pivot script (the dead
43
+ ``ramboot-init`` live-build variant was retired in PR #62).
44
+ - ``nosi`` renamed its baked dispatch to ``/scripts/nbdboot``; the
45
+ overlay is now belt-and-braces (nosi's stock dispatch would work
46
+ on its own), but keeping it in place lets pixie iterate on the
47
+ pivot logic without a nosi rebuild.
48
+ """
49
+
50
+ from __future__ import annotations
51
+
52
+ import gzip
53
+ import io
54
+ from pathlib import Path
55
+
56
+ _HERE = Path(__file__).resolve().parent
57
+
58
+ # The script path in the source tree. Loaded once at import time so
59
+ # the cpio builder can hash / re-emit without a filesystem walk.
60
+ _NBDBOOT_SRC = _HERE / "nbdboot"
61
+ NBDBOOT_SCRIPT: bytes = _NBDBOOT_SRC.read_bytes()
62
+
63
+
64
+ # newc-cpio format: 110-byte ASCII header + name + NUL-pad-to-4 +
65
+ # data + pad-to-4. See
66
+ # https://www.kernel.org/doc/Documentation/early-userspace/buffer-format.rst
67
+ _NEWC_MAGIC = b"070701"
68
+
69
+
70
+ def _newc_header(
71
+ *,
72
+ ino: int,
73
+ mode: int,
74
+ name: bytes,
75
+ filesize: int,
76
+ nlink: int = 1,
77
+ mtime: int = 0,
78
+ ) -> bytes:
79
+ """Build one 110-byte newc header. ``name`` is the path bytes
80
+ (no trailing NUL; namesize includes the NUL). ``mode`` combines
81
+ file-type + permission bits (e.g. ``0o040755`` for a directory,
82
+ ``0o100755`` for a regular executable). ``mtime=0`` gives byte-
83
+ identical output across builds -- pixie's cpio contents only
84
+ change when ``NBDBOOT_SCRIPT`` changes, so builders that hash
85
+ the response body see a stable digest."""
86
+ namesize = len(name) + 1 # NUL terminator counted
87
+ fields = (
88
+ _NEWC_MAGIC,
89
+ b"%08x" % ino,
90
+ b"%08x" % mode,
91
+ b"%08x" % 0, # uid = root
92
+ b"%08x" % 0, # gid = root
93
+ b"%08x" % nlink,
94
+ b"%08x" % mtime,
95
+ b"%08x" % filesize,
96
+ b"%08x" % 0, # devmajor
97
+ b"%08x" % 0, # devminor
98
+ b"%08x" % 0, # rdevmajor
99
+ b"%08x" % 0, # rdevminor
100
+ b"%08x" % namesize,
101
+ b"%08x" % 0, # check (unused for newc)
102
+ )
103
+ header = b"".join(fields)
104
+ assert len(header) == 110, f"newc header must be 110 bytes, got {len(header)}"
105
+ return header
106
+
107
+
108
+ def _pad4(buf: io.BytesIO) -> None:
109
+ """Zero-pad the buffer to the next 4-byte boundary. newc requires
110
+ every header + data blob start at a 4-byte-aligned offset."""
111
+ over = buf.tell() & 3
112
+ if over:
113
+ buf.write(b"\0" * (4 - over))
114
+
115
+
116
+ def _emit_entry(
117
+ buf: io.BytesIO,
118
+ *,
119
+ ino: int,
120
+ mode: int,
121
+ name: bytes,
122
+ data: bytes,
123
+ ) -> None:
124
+ """Append one newc-cpio entry (header + name + pad + data + pad)
125
+ to ``buf``. The trailer is emitted separately by the caller."""
126
+ buf.write(_newc_header(ino=ino, mode=mode, name=name, filesize=len(data)))
127
+ buf.write(name)
128
+ buf.write(b"\0") # namesize includes the NUL
129
+ _pad4(buf)
130
+ buf.write(data)
131
+ _pad4(buf)
132
+
133
+
134
+ def build_pivot_cpio() -> bytes:
135
+ """Build the pivot overlay as an uncompressed newc cpio. Contents:
136
+
137
+ - ``scripts`` directory (mode 0755) -- Debian live-boot's ``/init``
138
+ calls ``. /scripts/${BOOT}``, so a base initrd already has this
139
+ directory; including it in the overlay is harmless (cpio
140
+ overlay semantics: same-path entries overwrite / merge).
141
+ - ``scripts/nbdboot`` regular file (mode 0755) -- the pivot
142
+ script.
143
+ - ``TRAILER!!!`` sentinel that terminates the archive.
144
+
145
+ ``mtime=0`` on every entry so the output is byte-identical
146
+ across runs given the same input script.
147
+ """
148
+ buf = io.BytesIO()
149
+ ino = 1
150
+ _emit_entry(buf, ino=ino, mode=0o040755, name=b"scripts", data=b"")
151
+ ino += 1
152
+ _emit_entry(
153
+ buf,
154
+ ino=ino,
155
+ mode=0o100755,
156
+ name=b"scripts/nbdboot",
157
+ data=NBDBOOT_SCRIPT,
158
+ )
159
+ ino += 1
160
+ # TRAILER!!!: a header with filesize=0 and the special name.
161
+ buf.write(_newc_header(ino=0, mode=0, name=b"TRAILER!!!", filesize=0, nlink=1))
162
+ buf.write(b"TRAILER!!!\0")
163
+ _pad4(buf)
164
+ return buf.getvalue()
165
+
166
+
167
+ def build_pivot_cpio_gz() -> bytes:
168
+ """Same as :func:`build_pivot_cpio` but gzipped. iPXE + Linux both
169
+ accept plain-cpio or gzipped-cpio as an initrd blob; gzip halves
170
+ the served size for what would otherwise be a mostly-ASCII shell
171
+ script, which matters when a target is on a bench-side console
172
+ watching the transfer.
173
+
174
+ ``mtime=0`` on the gzip header + no filename record so the
175
+ compressed bytes are also byte-identical across builds."""
176
+ raw = build_pivot_cpio()
177
+ buf = io.BytesIO()
178
+ with gzip.GzipFile(fileobj=buf, mode="wb", mtime=0, compresslevel=9) as gz:
179
+ gz.write(raw)
180
+ return buf.getvalue()
pixie/pivot/nbdboot ADDED
@@ -0,0 +1,348 @@
1
+ # initramfs-tools boot driver -- sourced by ``/init`` when the kernel
2
+ # cmdline carries ``boot=nbdboot``. Defines ``mountroot`` (the
3
+ # contract initramfs-tools' /init calls to populate ``/root`` before
4
+ # pivoting + ``exec /sbin/init``).
5
+ #
6
+ # Sequence inside mountroot():
7
+ #
8
+ # 1. Parse ``pixie.*`` cmdline params -- nbd endpoint, image
9
+ # export name, overlay size, server URL, MAC.
10
+ # 2. modprobe nbd + overlay.
11
+ # 3. nbd-client connects to nbdmux; ``/dev/nbd0`` appears.
12
+ # 4. partx scans /dev/nbd0; pick the largest partition as the
13
+ # root (good-enough heuristic for nosi disk images; can be
14
+ # overridden via ``pixie.root_part=<devnode>``).
15
+ # 5. mount root read-only at /lower.
16
+ # 6. mount tmpfs (size = pixie.overlay_size, default 10G) at /upper.
17
+ # 7. overlayfs(lower=/lower, upper=/upper/up, work=/upper/work)
18
+ # at ``/root`` (the path initramfs-tools' /init will
19
+ # ``pivot_root`` into).
20
+ # 8. Best-effort POST status to pixie.server so the machine's
21
+ # timeline reflects "nbdboot up" before pivot_root.
22
+ #
23
+ # Failure handling: on any unrecoverable step we drop to a
24
+ # ``panic`` (initramfs-tools' single-shell recovery) with the
25
+ # reason. The operator sees the message on tty/serial.
26
+
27
+ # shellcheck disable=SC2034
28
+ PREREQ=""
29
+ prereqs() { echo "$PREREQ"; }
30
+ case "${1:-}" in
31
+ prereqs) prereqs; exit 0 ;;
32
+ esac
33
+
34
+ # Diagnostic trace helper -- prints to kernel log buffer (visible in
35
+ # dmesg + on the boot console) so the operator can tell whether
36
+ # ``mountroot`` was even entered and, if so, which step failed. Cheap
37
+ # to leave in place; the boot output already carries kernel spew.
38
+ _nbdboot_trace() { echo "nbdboot: $*" >/dev/kmsg 2>/dev/null || echo "nbdboot: $*"; }
39
+
40
+ # initramfs-tools' ``panic`` spawns ``sh -i`` on /dev/console and
41
+ # RETURNS if that shell exits. On serial without a controlling
42
+ # terminal the shell exits immediately, so ``panic`` becomes a no-op
43
+ # and mountroot merrily continues through every subsequent broken
44
+ # step, papering over the real failure. Use ``_nbdboot_die`` instead
45
+ # to trace, best-effort POST, and then hang PID 1 so the operator
46
+ # can actually read the message on the boot console.
47
+ _nbdboot_die() {
48
+ _nbdboot_trace "FATAL: $*"
49
+ _post_status "nbdboot.die"
50
+ # Replace this shell with a permanent sleep so /init can't
51
+ # continue past a failed step. The kernel keeps PID 1 alive and
52
+ # the console stays readable.
53
+ exec /bin/busybox sleep 2147483647
54
+ }
55
+
56
+ _nbdboot_trace "script sourced (BOOT=nbdboot)"
57
+
58
+ # initramfs-tools' standard library (provides ``log_begin_msg``,
59
+ # ``log_end_msg``, ``panic``, ``configure_networking``, etc.)
60
+ # shellcheck disable=SC1091
61
+ . /scripts/functions
62
+
63
+ _get_cmdline() {
64
+ # ``$1`` is the parameter name (without ``=``). Reads /proc/cmdline
65
+ # and prints just the value, or empty if the parameter is absent.
66
+ sed -n "s/.*\\<$1=\\([^ ]*\\).*/\\1/p" /proc/cmdline
67
+ }
68
+
69
+ _post_status() {
70
+ # Best-effort POST. ``$1`` = status string. Silent on failure;
71
+ # the boot continues regardless.
72
+ server="$(_get_cmdline pixie.server)"
73
+ mac="$(_get_cmdline pixie.mac)"
74
+ [ -n "$server" ] && [ -n "$mac" ] || return 0
75
+ # busybox-static's ``wget`` does plain HTTP POST when we use
76
+ # ``--post-data``; the body is irrelevant to pixie (server reads
77
+ # ``status`` from a query param shape historically). For the
78
+ # nbdboot variant we just touch the endpoint -- the URL alone
79
+ # carries the signal.
80
+ /bin/busybox wget -q -O /dev/null \
81
+ --post-data="status=$1" \
82
+ "${server}/pxe/${mac}/status" || true
83
+ }
84
+
85
+ mountroot() {
86
+ _nbdboot_trace "mountroot() entered"
87
+ nbd_url="$(_get_cmdline pixie.nbd)"
88
+ image="$(_get_cmdline pixie.image)"
89
+ overlay_size="$(_get_cmdline pixie.overlay_size)"
90
+ root_part_override="$(_get_cmdline pixie.root_part)"
91
+
92
+ # Defaults: 10 GiB tmpfs is enough for typical CI workloads
93
+ # writing logs + scratch state; operator caps with pixie.overlay_size.
94
+ : "${overlay_size:=10G}"
95
+
96
+ _nbdboot_trace "cmdline nbd=${nbd_url:-<unset>} image=${image:-<unset>} overlay_size=${overlay_size} root_part=${root_part_override:-<auto>}"
97
+
98
+ if [ -z "$nbd_url" ] || [ -z "$image" ]; then
99
+ _nbdboot_die "missing pixie.nbd or pixie.image on kernel cmdline"
100
+ fi
101
+
102
+ # nbd_url is ``tcp://<host>:<port>``. Strip the scheme and split.
103
+ nbd_host="${nbd_url#tcp://}"
104
+ nbd_host="${nbd_host%%:*}"
105
+ nbd_port="${nbd_url##*:}"
106
+
107
+ _nbdboot_trace "modprobe nbd (nbds_max=1 max_part=16) + overlay"
108
+ log_begin_msg "nbdboot: modprobe nbd + overlay"
109
+ # ``max_part=16`` is essential: without it the nbd module leaves
110
+ # /dev/nbd0 as a single flat device and NEVER exposes the
111
+ # partition device nodes we scan for below. Debian's default is
112
+ # ``max_part=0``.
113
+ #
114
+ # Linux keeps module parameters from the FIRST load and silently
115
+ # ignores params passed to subsequent modprobe calls. If nbd got
116
+ # pre-loaded during initramfs boot (e.g. by udev on a modalias),
117
+ # our ``max_part=16`` would drop on the floor and partition
118
+ # nodes would never appear. Force a clean reload.
119
+ rmmod nbd 2>/dev/null || true
120
+ modprobe nbd nbds_max=1 max_part=16 || _nbdboot_die "modprobe nbd failed"
121
+ modprobe overlay || _nbdboot_die "modprobe overlay failed"
122
+ _nbdboot_trace "nbd sysfs max_part=$(cat /sys/module/nbd/parameters/max_part 2>/dev/null || echo '<unreadable>')"
123
+ log_end_msg
124
+
125
+ # iPXE releases its DHCP lease when it chainloads the kernel, so
126
+ # userspace has to redo network setup before nbd-client can talk
127
+ # to nbdmux. configure_networking() (from /scripts/functions) does
128
+ # DHCP via ipconfig(8) on the auto-detected NIC. Fatal on
129
+ # failure -- nbd-client cannot succeed without a route to the
130
+ # server.
131
+ _nbdboot_trace "configure_networking (DHCP via ipconfig)"
132
+ log_begin_msg "nbdboot: configure_networking"
133
+ configure_networking || _nbdboot_die "configure_networking failed (DHCP)"
134
+ log_end_msg
135
+ _nbdboot_trace "network up: $(/bin/busybox ip -o -4 addr show 2>/dev/null | /bin/busybox awk '{print $2, $4}' | /bin/busybox tr '\n' ';')"
136
+
137
+ # ``-persist`` keeps nbd-client's supervisor process alive after
138
+ # the initial attach so it reconnects if the TCP session drops.
139
+ # Ubuntu's systemd-networkd resets the network stack a few seconds
140
+ # into userspace boot, which was killing the socket the initramfs
141
+ # opened and leaving /dev/nbd0 dead. jbd2 on the /boot partition
142
+ # then queues writes forever (observed 120s hung-task warnings).
143
+ # With -persist the client reconnects transparently, and
144
+ # nbdkit's cow filter is per-nbdkit-instance not per-connection,
145
+ # so the writable overlay survives the drop.
146
+ _nbdboot_trace "nbd-client -persist ${nbd_host}:${nbd_port} -name ${image}"
147
+ log_begin_msg "nbdboot: nbd-client ${nbd_host}:${nbd_port} -name ${image}"
148
+ if ! nbd-client -persist "$nbd_host" "$nbd_port" -name "$image" /dev/nbd0; then
149
+ _post_status "nbdboot.nbd_connect_failed"
150
+ _nbdboot_die "nbd-client failed to connect to ${nbd_host}:${nbd_port}"
151
+ fi
152
+ log_end_msg
153
+
154
+ # ``nbd-client`` returns as soon as the client-side attach ioctl
155
+ # is done, but the kernel commits the capacity change + partition
156
+ # scan asynchronously via a workqueue. Racing partprobe before
157
+ # the capacity event lands leaves /dev/nbd0 sized 0 and produces
158
+ # NO partition nodes at all (been there). Poll for size > 0
159
+ # before scanning; give up after ~10s.
160
+ i=0
161
+ while [ "$i" -lt 100 ]; do
162
+ nbd0_size="$(/bin/busybox blockdev --getsize64 /dev/nbd0 2>/dev/null || echo 0)"
163
+ [ "${nbd0_size:-0}" -gt 0 ] && break
164
+ /bin/busybox sleep 0.1
165
+ i=$((i + 1))
166
+ done
167
+ _nbdboot_trace "nbd0 capacity ready after ${i} tick(s): ${nbd0_size:-0} bytes"
168
+
169
+ udevadm settle --timeout=10 || true
170
+ /bin/busybox blockdev --rereadpt /dev/nbd0 2>/dev/null || true
171
+ udevadm settle --timeout=10 || true
172
+
173
+ # First try the kernel's own async partition scan. On Linux 6.12
174
+ # with this nbd driver it consistently returns no partition nodes
175
+ # for GPT disks (BLKRRPART returns 0 but the scanner gives up
176
+ # somewhere between reading LBA 0 and validating the GPT header
177
+ # on LBA 1). Poll briefly so we don't over-wait if the answer is
178
+ # "not going to happen".
179
+ i=0
180
+ while [ "$i" -lt 20 ]; do
181
+ [ -b /dev/nbd0p1 ] && break
182
+ /bin/busybox sleep 0.1
183
+ i=$((i + 1))
184
+ done
185
+ if [ ! -b /dev/nbd0p1 ]; then
186
+ # Kernel didn't produce partition nodes. Use partx from
187
+ # userspace: it reads the partition table itself (via
188
+ # libblkid) and installs each partition on the disk via
189
+ # BLKPG_ADD_PARTITION ioctls, bypassing the in-kernel
190
+ # scanner. Same end state (``/dev/nbd0pN`` nodes appear
191
+ # for udev to notice) without a loop-device wrapper.
192
+ _px_out="$(/usr/bin/partx --add --verbose /dev/nbd0 2>&1)"; _px_rc=$?
193
+ _nbdboot_trace "partx --add rc=${_px_rc} out='${_px_out}'"
194
+ udevadm settle --timeout=10 || true
195
+ fi
196
+ _nbdboot_trace "nbd0 nodes after scan: $(/bin/busybox ls -1 /dev/nbd0* 2>/dev/null | /bin/busybox tr '\n' ' ')"
197
+
198
+ if [ -n "$root_part_override" ]; then
199
+ root_part="$root_part_override"
200
+ elif [ -b /dev/nbd0p1 ]; then
201
+ # Native partition scan worked -- pick the largest partition.
202
+ root_part="$(
203
+ /bin/busybox find /dev -maxdepth 1 -name 'nbd0p*' -print0 \
204
+ | xargs -0 -I{} /bin/busybox sh -c 'echo "$(blockdev --getsize64 "$1") $1"' _ {} \
205
+ | sort -n | tail -1 | cut -d' ' -f2
206
+ )"
207
+ else
208
+ # No partition table (or scan didn't fire) -- treat the
209
+ # whole device as a raw filesystem.
210
+ root_part=/dev/nbd0
211
+ fi
212
+ _nbdboot_trace "picked root_part=${root_part:-<none>}"
213
+ if [ -z "$root_part" ] || [ ! -e "$root_part" ]; then
214
+ _post_status "nbdboot.no_root_partition"
215
+ _nbdboot_die "could not pick a root partition on /dev/nbd0"
216
+ fi
217
+
218
+ mkdir -p /lower /upper /root
219
+ _nbdboot_trace "$(/sbin/blkid -p ${root_part} 2>&1)"
220
+ # Try common in-kernel filesystems explicitly, then fall back to
221
+ # auto-detect. modprobe each candidate first because initramfs
222
+ # kernel modules aren't preloaded and mount(2) with -t auto only
223
+ # tries filesystems whose modules are already registered.
224
+ modprobe ext4 2>/dev/null || true
225
+ modprobe xfs 2>/dev/null || true
226
+ modprobe btrfs 2>/dev/null || true
227
+ _mnt_out=""; _mnt_rc=1
228
+ for fstype in ext4 xfs btrfs auto; do
229
+ _nbdboot_trace "mount -t ${fstype} -o ro ${root_part} -> /lower"
230
+ _mnt_out="$(mount -t "$fstype" -o ro "$root_part" /lower 2>&1)"; _mnt_rc=$?
231
+ [ "$_mnt_rc" -eq 0 ] && break
232
+ _nbdboot_trace " -> rc=${_mnt_rc} out='${_mnt_out}'"
233
+ done
234
+ if [ "$_mnt_rc" -ne 0 ]; then
235
+ _post_status "nbdboot.mount_root_failed"
236
+ _nbdboot_die "failed to mount ${root_part} as ext4/xfs/btrfs/auto"
237
+ fi
238
+ _nbdboot_trace "mounted ${root_part}; ls /lower: $(/bin/busybox ls /lower 2>&1 | /bin/busybox tr '\n' ' ')"
239
+
240
+ _nbdboot_trace "tmpfs(${overlay_size}) -> /upper"
241
+ log_begin_msg "nbdboot: tmpfs(${overlay_size}) at /upper"
242
+ if ! mount -t tmpfs -o "size=${overlay_size}" tmpfs /upper; then
243
+ _post_status "nbdboot.mount_tmpfs_failed"
244
+ _nbdboot_die "failed to mount tmpfs at /upper"
245
+ fi
246
+ mkdir -p /upper/up /upper/work
247
+ log_end_msg
248
+
249
+ _nbdboot_trace "overlay(lower=/lower,upper=/upper/up,work=/upper/work) -> /root"
250
+ log_begin_msg "nbdboot: overlayfs at /root"
251
+ if ! mount -t overlay overlay \
252
+ -o "lowerdir=/lower,upperdir=/upper/up,workdir=/upper/work" \
253
+ /root
254
+ then
255
+ _post_status "nbdboot.mount_overlay_failed"
256
+ _nbdboot_die "failed to mount overlayfs at /root"
257
+ fi
258
+ log_end_msg
259
+
260
+ # Rewrite the guest's /etc/fstab in the overlay upper before we
261
+ # hand control back to /init. Cloud disk images (nosi's Ubuntu +
262
+ # Debian) list /boot and /boot/efi as separate ext4/vfat mounts
263
+ # off partitions we can only reach through the loop stack over
264
+ # nbd. Those mounts have NO overlayfs wrapper, so ext4 journal
265
+ # writes to /boot land straight in nbdkit's cow filter through
266
+ # loop + nbd, and something in that chain wedges jbd2 within
267
+ # 120s (observed on GIGABYTE MC12-LE0 booting nosi's
268
+ # ubuntu-2604-headless). Root is fine because mountroot() above
269
+ # already wrapped it in overlay + tmpfs.
270
+ #
271
+ # Strip /boot and /boot/efi from fstab: the bootloader has
272
+ # already consumed /boot before we ever ran, and nothing in
273
+ # userspace needs to write to it. The overlay upper wins over
274
+ # the lower's fstab because /root is the overlay merge and /init
275
+ # pivots into that.
276
+ # Overlay upperdir is /upper/up (see the ``mount -t overlay``
277
+ # invocation above); writing to /upper/etc/fstab lands OUTSIDE
278
+ # the overlay and the pivoted root still sees the lower's fstab.
279
+ # The strip has to go into the actual upperdir.
280
+ if [ -e /lower/etc/fstab ]; then
281
+ mkdir -p /upper/up/etc
282
+ /bin/busybox awk '$2 != "/boot" && $2 != "/boot/efi" { print }' \
283
+ /lower/etc/fstab > /upper/up/etc/fstab
284
+ _nbdboot_trace "fstab: rewrote /etc/fstab in overlay upper (dropped /boot, /boot/efi) -- $(wc -l < /upper/up/etc/fstab) lines"
285
+ fi
286
+
287
+ # Prevent Ubuntu/Debian's systemd-networkd + cloud-init from
288
+ # touching the NIC the initramfs already configured. Under
289
+ # nbdboot the root filesystem lives on that NIC (NBD over TCP),
290
+ # so anything that flushes the IP + re-DHCPs -- which networkd
291
+ # + netplan + cloud-init's network stage all do by default --
292
+ # tears down the socket the kernel was reading root pages from,
293
+ # then wedges every process on the ensuing D-state page fault
294
+ # because the reconnect needs an IP that needs networkd that
295
+ # needs pages from a dead NBD. Circular deadlock, no hung-task
296
+ # warning (nothing is runnable long enough to trip 120s), silent
297
+ # console freeze after journald flush.
298
+ #
299
+ # Mask by symlink-to-/dev/null in the overlay upper's systemd
300
+ # unit dir. Fresh cloud install case: no runtime override needed
301
+ # since we don't get a chance to reconfigure networking anyway.
302
+ # Also masks the .socket unit so socket-activation can't
303
+ # resurrect networkd.
304
+ mkdir -p /upper/up/etc/systemd/system
305
+ for unit in \
306
+ systemd-networkd.service \
307
+ systemd-networkd.socket \
308
+ systemd-networkd-wait-online.service \
309
+ cloud-init.service \
310
+ cloud-init-local.service \
311
+ cloud-config.service \
312
+ cloud-final.service \
313
+ ; do
314
+ ln -sf /dev/null "/upper/up/etc/systemd/system/${unit}"
315
+ done
316
+ _nbdboot_trace "network: masked systemd-networkd + cloud-init units so the initramfs-owned NIC survives userspace"
317
+
318
+ # With networkd + cloud-init masked, nothing in userspace populates
319
+ # /etc/resolv.conf. Propagate the DHCP-supplied DNS servers +
320
+ # search domain from initramfs-tools' /run/net-*.conf into the
321
+ # overlay upper so ``apt-get update`` / ``curl`` / ``ping <host>``
322
+ # work in the pivoted root. First-match wins: the loop breaks on
323
+ # the first ``/run/net-*.conf`` that carries a non-empty
324
+ # ``DNSSERVERS`` line, which matches the single interface the
325
+ # initramfs actually configured.
326
+ for net_conf in /run/net-*.conf; do
327
+ [ -e "$net_conf" ] || continue
328
+ # shellcheck source=/dev/null
329
+ . "$net_conf" 2>/dev/null || continue
330
+ [ -n "${DNSSERVERS:-}" ] || continue
331
+ {
332
+ echo "# Written by nbdboot initramfs from ${net_conf}."
333
+ echo "# nbdboot masks systemd-networkd + cloud-init so nothing"
334
+ echo "# in userspace would otherwise reseed this file."
335
+ [ -n "${DOMAINSEARCH:-}" ] && echo "search ${DOMAINSEARCH}"
336
+ for _ns in ${DNSSERVERS}; do
337
+ echo "nameserver ${_ns}"
338
+ done
339
+ } > /upper/up/etc/resolv.conf
340
+ _nbdboot_trace "network: wrote /etc/resolv.conf from ${net_conf} (nameservers: ${DNSSERVERS})"
341
+ break
342
+ done
343
+
344
+ _nbdboot_trace "mountroot() done -- returning to /init for pivot_root"
345
+ _post_status "nbdboot.up"
346
+ # initramfs-tools' /init pivot_root's into /root and exec's
347
+ # /sbin/init from here.
348
+ }
pixie/pxe/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ """PXE plan rendering + boot flow.
2
+
3
+ An operator points a target's DHCP + TFTP chain at pixie's
4
+ ``/pxe-bootstrap.ipxe`` (fetched over HTTP by iPXE); the bootstrap
5
+ chain-loads ``/pxe/<mac>``, which returns the target-specific iPXE
6
+ plan derived from the machine's boot mode:
7
+
8
+ * ``ipxe-exit`` -> chain out of iPXE, boot the local disk.
9
+ * ``nbdboot`` -> image-native kernel + initrd from the artifacts
10
+ directory of the image's netboot bundle, root over NBD from the
11
+ auto-created export against the disk-image blob.
12
+
13
+ Both templates are content-addressed: every URL the plan emits
14
+ carries a sha256 as its cache-safe identifier, so a catalog rename
15
+ or bump-and-redeploy of pixie does not silently boot a different
16
+ image than the operator intended.
17
+ """
18
+
19
+ from __future__ import annotations