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.
- pixie/__init__.py +20 -0
- pixie/_util.py +38 -0
- pixie/catalog/__init__.py +25 -0
- pixie/catalog/_fetcher.py +513 -0
- pixie/catalog/_routes.py +392 -0
- pixie/catalog/_schema.py +263 -0
- pixie/catalog/_store.py +234 -0
- pixie/deploy/__init__.py +26 -0
- pixie/deploy/_main.py +321 -0
- pixie/deploy/_templates.py +153 -0
- pixie/disks.py +85 -0
- pixie/events/__init__.py +28 -0
- pixie/events/_kinds.py +203 -0
- pixie/events/_log.py +210 -0
- pixie/events/_routes.py +42 -0
- pixie/exports/__init__.py +17 -0
- pixie/exports/_routes.py +211 -0
- pixie/exports/_store.py +156 -0
- pixie/exports/_supervisor.py +240 -0
- pixie/flash.py +1971 -0
- pixie/images.py +473 -0
- pixie/machines/__init__.py +16 -0
- pixie/machines/_routes.py +190 -0
- pixie/machines/_store.py +623 -0
- pixie/oras.py +547 -0
- pixie/pivot/__init__.py +180 -0
- pixie/pivot/nbdboot +348 -0
- pixie/pxe/__init__.py +19 -0
- pixie/pxe/_renderer.py +244 -0
- pixie/pxe/_routes.py +386 -0
- pixie/tftp/__init__.py +21 -0
- pixie/tftp/_supervisor.py +129 -0
- pixie/tui/__init__.py +185 -0
- pixie/tui/_app.py +2219 -0
- pixie/tui_catalog.py +657 -0
- pixie/web/__init__.py +6 -0
- pixie/web/_auth.py +70 -0
- pixie/web/_settings_store.py +211 -0
- pixie/web/_static/.gitkeep +0 -0
- pixie/web/_static/bootstrap-icons.min.css +5 -0
- pixie/web/_static/bootstrap.min.css +12 -0
- pixie/web/_static/fonts/bootstrap-icons.woff +0 -0
- pixie/web/_static/fonts/bootstrap-icons.woff2 +0 -0
- pixie/web/_static/htmx.min.js +1 -0
- pixie/web/_static/pixie-favicon.png +0 -0
- pixie/web/_static/sse.js +290 -0
- pixie/web/_table_state.py +261 -0
- pixie/web/_templates/_partials/page_description.html +22 -0
- pixie/web/_templates/_partials/recent_events.html +47 -0
- pixie/web/_templates/_partials/table_helpers.html +189 -0
- pixie/web/_templates/catalog.html +364 -0
- pixie/web/_templates/catalog_detail.html +386 -0
- pixie/web/_templates/dashboard.html +256 -0
- pixie/web/_templates/events.html +125 -0
- pixie/web/_templates/ipxe/bootstrap.j2 +12 -0
- pixie/web/_templates/ipxe/exit.j2 +10 -0
- pixie/web/_templates/ipxe/nbdboot.j2 +57 -0
- pixie/web/_templates/ipxe/pixie-live-env.j2 +55 -0
- pixie/web/_templates/ipxe/unavailable.j2 +14 -0
- pixie/web/_templates/layout.html +345 -0
- pixie/web/_templates/login.html +40 -0
- pixie/web/_templates/machine_detail.html +786 -0
- pixie/web/_templates/machines.html +186 -0
- pixie/web/_templates/settings.html +265 -0
- pixie/web/main.py +1910 -0
- pixie_lab-0.1.0.dist-info/METADATA +107 -0
- pixie_lab-0.1.0.dist-info/RECORD +70 -0
- pixie_lab-0.1.0.dist-info/WHEEL +4 -0
- pixie_lab-0.1.0.dist-info/entry_points.txt +3 -0
- pixie_lab-0.1.0.dist-info/licenses/LICENSE +674 -0
pixie/pivot/__init__.py
ADDED
|
@@ -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
|