pixie-lab 0.4.2__py3-none-any.whl → 0.4.3__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/events/_kinds.py CHANGED
@@ -197,24 +197,6 @@ right after the wipe so the freshly-empty log still records who reset
197
197
  it and when; carries the deleted-row count in ``details``."""
198
198
 
199
199
  # ---------- live-env fetch ------------------------------------------
200
- #
201
- # The operator "Fetch live-env" action pulls the netboot-pc bake into
202
- # PIXIE_LIVE_ENV_DIR. No subject (``subject_kind`` = ""): the live env
203
- # is per-pixie, not per-resource. Dotted ``.done`` / ``.failed`` names
204
- # mirror the catalog-fetch kinds so ``.failed`` auto-classifies as an
205
- # error via the ERROR_KINDS ``LIKE '%.failed'`` probe.
206
-
207
- LIVE_ENV_FETCH_DONE = "live_env.fetch.done"
208
- """``POST /ui/live-env/fetch`` downloaded + staged vmlinuz + initrd +
209
- live.squashfs into PIXIE_LIVE_ENV_DIR. Carries the src + content sha in
210
- ``details``."""
211
-
212
- LIVE_ENV_FETCH_FAILED = "live_env.fetch.failed"
213
- """The live-env fetch failed (bad src scheme, download error, or a
214
- tarball missing a required file). Carries the src + error in
215
- ``details``."""
216
-
217
-
218
200
  # The canonical closed set. Every kind above is registered here; the
219
201
  # ``EventsLog.emit`` call rejects anything not in this frozenset.
220
202
  KNOWN_EVENT_KINDS: frozenset[str] = frozenset(
@@ -248,8 +230,6 @@ KNOWN_EVENT_KINDS: frozenset[str] = frozenset(
248
230
  AUTH_LOGIN_SUCCEEDED,
249
231
  AUTH_LOGIN_FAILED,
250
232
  EVENTS_CLEARED,
251
- LIVE_ENV_FETCH_DONE,
252
- LIVE_ENV_FETCH_FAILED,
253
233
  }
254
234
  )
255
235
 
pixie/pxe/_renderer.py CHANGED
@@ -91,12 +91,12 @@ class RenderContext:
91
91
  # no-op is a legal value.
92
92
  extra_cmdline: str = ""
93
93
  # Disk-image content sha the live-env boot modes boot over
94
- # ephemeral nbdboot instead of the Debian live-boot squashfs.
95
- # Operator-set via PIXIE_LIVE_ENV_IMAGE_SHA / the Live env page.
96
- # Empty (default) keeps the squashfs path so existing deploys are
97
- # untouched; when set, ``render`` streams this image over NBD for
98
- # every LIVE_ENV_MODES machine (the machine's own bind still drives
99
- # what GET /pxe/<mac>/plan returns, so the on-target CLI dispatches
94
+ # ephemeral nbdboot. Operator-set via PIXIE_LIVE_ENV_IMAGE_SHA /
95
+ # the Live env page. Empty means no live-env image is configured,
96
+ # so LIVE_ENV_MODES machines degrade to the ``unavailable`` plan;
97
+ # when set, ``render`` streams this image over NBD for every
98
+ # LIVE_ENV_MODES machine (the machine's own bind still drives what
99
+ # GET /pxe/<mac>/plan returns, so the on-target CLI dispatches
100
100
  # inventory / interactive / flash correctly).
101
101
  live_env_image_sha: str = ""
102
102
 
@@ -113,7 +113,6 @@ class PlanRenderer:
113
113
  overlays: OverlaysStore,
114
114
  nbd: NbdServer,
115
115
  overlays_dir: Path,
116
- live_env_dir: Path | None = None,
117
116
  events: Any = None,
118
117
  ) -> None:
119
118
  self._catalog = catalog
@@ -128,29 +127,12 @@ class PlanRenderer:
128
127
  # booted) land alongside the rest of pixie's audit trail. None
129
128
  # for unit tests that don't wire an events sink.
130
129
  self._events = events
131
- # Where the netboot-pc bake's vmlinuz + initrd + squashfs are
132
- # staged on disk. When set + the three files exist, the
133
- # ``pixie-*`` boot modes chain into the live env; otherwise
134
- # they fall back to the ``unavailable`` plan so a bound
135
- # target does not silently boot nothing.
136
- self._live_env_dir = live_env_dir
137
130
  self._env = Environment(
138
131
  loader=FileSystemLoader(str(_TEMPLATES_DIR)),
139
132
  autoescape=select_autoescape(disabled_extensions=("j2",)),
140
133
  keep_trailing_newline=True,
141
134
  )
142
135
 
143
- def _live_env_ready(self) -> bool:
144
- """True iff the netboot-pc artifacts are staged on disk under
145
- ``self._live_env_dir``. Called per-render so an operator
146
- dropping the files in without a pixie restart takes effect
147
- on the next PXE hit."""
148
- if self._live_env_dir is None:
149
- return False
150
- return all(
151
- (self._live_env_dir / name).is_file() for name in ("vmlinuz", "initrd", "live.squashfs")
152
- )
153
-
154
136
  def render(self, machine: Machine, ctx: RenderContext) -> str:
155
137
  mode = machine.boot_mode
156
138
  # Per-machine ``extra_cmdline`` fully overrides the deploy-wide
@@ -341,52 +323,37 @@ class PlanRenderer:
341
323
  ) -> str:
342
324
  """Render a plan for one of the ``LIVE_ENV_MODES``.
343
325
 
344
- Two deliveries, picked by ``ctx.live_env_image_sha``
345
- (PIXIE_LIVE_ENV_IMAGE_SHA / the Live env page):
346
-
347
- * Set -> boot that disk image over EPHEMERAL nbdboot, the same
348
- code path the ``nbdboot`` mode uses. Proven on hardware to
349
- bring the NIC up via dracut where the squashfs live-boot
350
- hangs in the initramfs on some boards. If the configured
351
- image or its netboot bundle isn't fetched, degrade to
352
- ``unavailable`` (matching how ``nbdboot`` degrades) rather
353
- than silently falling back -- a configured-but-broken live
354
- env is an operator error worth surfacing.
355
- * Unset -> the historical Debian live-boot squashfs
356
- (``pixie-live-env.j2``), or ``unavailable`` when the
357
- netboot-pc bake artifacts have not been staged.
358
-
359
- Either way the machine's OWN ``boot_mode`` still drives what
326
+ The live env boots the disk image named by
327
+ ``ctx.live_env_image_sha`` (PIXIE_LIVE_ENV_IMAGE_SHA / the Live
328
+ env page) over EPHEMERAL nbdboot, the same code path the
329
+ ``nbdboot`` mode uses -- proven on hardware to bring the NIC up
330
+ via dracut where the retired Debian live-boot squashfs hung in
331
+ the initramfs on some boards. If no image is configured, or the
332
+ configured image / its netboot bundle isn't fetched, degrade to
333
+ ``unavailable`` (matching how ``nbdboot`` degrades) rather than
334
+ booting nothing -- a live-env mode with no bootable image is an
335
+ operator setup gap worth surfacing.
336
+
337
+ The machine's OWN ``boot_mode`` still drives what
360
338
  ``GET /pxe/<mac>/plan`` returns, so the on-target pixie CLI
361
- dispatches inventory / interactive / flash exactly as before."""
339
+ dispatches inventory / interactive / flash accordingly."""
362
340
  live_env_sha = ctx.live_env_image_sha
363
- if live_env_sha:
364
- resolved = self._resolve_bundle_and_blob(live_env_sha)
365
- if isinstance(resolved, str):
366
- return self._unavailable(
367
- machine,
368
- f"live-env image {live_env_sha[:12]} not bootable: {resolved}",
369
- )
370
- bundle_entry, blob = resolved
371
- return self._render_ephemeral_nbdboot(
372
- machine, ctx, live_env_sha, bundle_entry, blob, extra_cmdline
341
+ if not live_env_sha:
342
+ return self._unavailable(
343
+ machine,
344
+ f"boot_mode={mode!r} needs a live-env image; set "
345
+ "PIXIE_LIVE_ENV_IMAGE_SHA (or the Live env page) to a "
346
+ "fetched image's content sha",
373
347
  )
374
- if not self._live_env_ready():
375
- # netboot-pc bake artifacts have not been staged on this
376
- # deploy yet; degrade to the readable unavailable plan so a
377
- # bound target lands on a legible screen instead of a
378
- # bty-media initrd.
348
+ resolved = self._resolve_bundle_and_blob(live_env_sha)
349
+ if isinstance(resolved, str):
379
350
  return self._unavailable(
380
351
  machine,
381
- f"boot_mode={mode!r} needs pixie live-env media; "
382
- f"stage vmlinuz+initrd+squashfs under $PIXIE_LIVE_ENV_DIR",
352
+ f"live-env image {live_env_sha[:12]} not bootable: {resolved}",
383
353
  )
384
- return self._env.get_template("pixie-live-env.j2").render(
385
- mac=machine.mac,
386
- boot_mode=mode,
387
- host=ctx.host,
388
- port=ctx.port,
389
- extra_cmdline=extra_cmdline,
354
+ bundle_entry, blob = resolved
355
+ return self._render_ephemeral_nbdboot(
356
+ machine, ctx, live_env_sha, bundle_entry, blob, extra_cmdline
390
357
  )
391
358
 
392
359
  def _unavailable(self, machine: Machine, reason: str) -> str:
@@ -74,29 +74,16 @@ ENV_LIVE_ENV_EXTRA_CMDLINE = "PIXIE_LIVE_ENV_EXTRA_CMDLINE"
74
74
  # it never reaches a kernel cmdline.
75
75
  _UNEXPANDED_ENV_PLACEHOLDER = re.compile(r"^\$\{[A-Za-z_][A-Za-z0-9_]*(:[-+?=]?[^}]*)?\}$")
76
76
 
77
- # Where the operator "Fetch live-env" action pulls the netboot-pc bake
78
- # from: a single tarball ``src`` (``https://`` or ``oras://``, the same
79
- # schemes the catalog fetch speaks) that unpacks to vmlinuz + initrd +
80
- # live.squashfs. Defaults to the pixie GitHub release's stable-named
81
- # asset via ``/releases/latest/download/`` so a plain deploy can pull
82
- # the live env with no config; override for an air-gapped mirror.
83
- KEY_LIVE_ENV_SRC = "live_env.src"
84
- ENV_LIVE_ENV_SRC = "PIXIE_LIVE_ENV_SRC"
85
- DEFAULT_LIVE_ENV_SRC = (
86
- "https://github.com/safl/pixie/releases/latest/download/pixie-live-env-x86_64.tar.gz"
87
- )
88
-
89
77
  # Disk-image content sha the live-env boot modes
90
78
  # (pixie-inventory / -tui / -flash-once / -flash-always) boot over
91
- # EPHEMERAL nbdboot instead of the Debian live-boot squashfs. When set
92
- # (and the named image + its netboot bundle are fetched) the renderer
93
- # streams this image over NBD -- proven on hardware to bring the NIC up
94
- # via dracut where the squashfs live-boot hangs in the initramfs on some
95
- # boards. Empty (the default) keeps the historical squashfs path so
96
- # existing deploys are untouched. Resolution: DB override -> env ->
97
- # empty. A value that is not 64 lowercase hex chars is treated as unset
98
- # so a fat-fingered setting degrades to the squashfs path rather than
99
- # sending every live-env boot into the unavailable plan.
79
+ # EPHEMERAL nbdboot. When set (and the named image + its netboot bundle
80
+ # are fetched) the renderer streams this image over NBD -- proven on
81
+ # hardware to bring the NIC up via dracut where the retired squashfs
82
+ # live-boot hung in the initramfs on some boards. Empty (the default)
83
+ # means those modes degrade to the unavailable plan. Resolution: DB
84
+ # override -> env -> empty. A value that is not 64 lowercase hex chars
85
+ # is treated as unset so a fat-fingered setting degrades to unavailable
86
+ # cleanly.
100
87
  KEY_LIVE_ENV_IMAGE_SHA = "live_env.image_sha"
101
88
  ENV_LIVE_ENV_IMAGE_SHA = "PIXIE_LIVE_ENV_IMAGE_SHA"
102
89
 
@@ -216,17 +203,6 @@ class SettingsStore:
216
203
  return env
217
204
  return ""
218
205
 
219
- def resolve_live_env_src(self) -> str:
220
- """Effective live-env fetch src: DB override -> env -> default
221
- (the pixie GitHub release asset). Never empty; the caller feeds
222
- it straight to the live-env fetch, which raises on a bad
223
- scheme."""
224
- return (
225
- self.get(KEY_LIVE_ENV_SRC)
226
- or (os.environ.get(ENV_LIVE_ENV_SRC) or "").strip()
227
- or DEFAULT_LIVE_ENV_SRC
228
- )
229
-
230
206
  def resolve_live_env_image_sha(self) -> str:
231
207
  """Effective live-env disk-image content sha: DB override -> env
232
208
  -> empty.
@@ -126,67 +126,45 @@
126
126
  </div>
127
127
  </div>
128
128
 
129
- {# Live-env media readiness (OUTPUT ONLY). The dashboard reports
130
- whether the pixie-* boot modes have their media; the Fetch action +
131
- source / cmdline config live on the dedicated /ui/live-env pane. #}
132
- {% set lfs = stats.live_env_fetch_state or {} %}
129
+ {# Live-env readiness (OUTPUT ONLY). The dashboard reports whether the
130
+ pixie-* boot modes have a bootable image selected; the image + cmdline
131
+ config live on the dedicated /ui/live-env pane. #}
133
132
  <div class="card shadow-sm mb-4">
134
133
  <div class="card-header bg-light d-flex align-items-center gap-2">
135
- <h2 class="h5 mb-0"><i class="bi bi-boombox me-2"></i>Live-env media</h2>
134
+ <h2 class="h5 mb-0"><i class="bi bi-boombox me-2"></i>Live env</h2>
136
135
  <div class="ms-auto d-flex align-items-center gap-2">
137
- {% if lfs.state == 'fetching' %}
138
- <span class="badge text-bg-info"><span class="spinner-border spinner-border-sm me-1" role="status"></span>fetching</span>
139
- {% elif stats.live_env_ready %}
136
+ {% if stats.live_env_ready %}
140
137
  <span class="badge text-bg-success"><i class="bi bi-check-circle me-1"></i>ready</span>
141
138
  {% else %}
142
- <span class="badge text-bg-warning"><i class="bi bi-exclamation-triangle me-1"></i>not staged</span>
139
+ <span class="badge text-bg-warning"><i class="bi bi-exclamation-triangle me-1"></i>not ready</span>
143
140
  {% endif %}
144
141
  <a href="/ui/live-env" class="btn btn-sm btn-outline-primary text-nowrap"
145
- title="Fetch + configure the live env">
142
+ title="Select + configure the live env">
146
143
  <i class="bi bi-boombox me-1"></i>Manage
147
144
  </a>
148
145
  </div>
149
146
  </div>
150
147
  <div class="card-body">
151
148
  {% if stats.live_env_ready %}
152
- <p class="small mb-2">
153
- Staged at <code>{{ stats.live_env_dir }}</code>;
149
+ <p class="small mb-0">
150
+ Image <code>{{ stats.live_env_image_sha[:12] }}</code> selected and fetched;
154
151
  <code>pixie-tui</code> / <code>pixie-inventory</code> /
155
- <code>pixie-flash-*</code> can serve the live env.
152
+ <code>pixie-flash-*</code> boot it over ephemeral nbdboot.
156
153
  </p>
157
- {% else %}
158
- <p class="small text-warning mb-2">
159
- Live-env media is missing under <code>{{ stats.live_env_dir or '(unset)' }}</code>.
160
- Every <code>pixie-*</code> boot_mode falls back to the "unavailable" iPXE
161
- plan until it is staged. <a href="/ui/live-env">Fetch it &rarr;</a>
154
+ {% elif stats.live_env_image_sha %}
155
+ <p class="small text-warning mb-0">
156
+ Live-env image <code>{{ stats.live_env_image_sha[:12] }}</code> is selected but
157
+ not fetched. Every <code>pixie-*</code> boot_mode falls back to the "unavailable"
158
+ iPXE plan until the image is fetched. <a href="/ui/catalog">Fetch it &rarr;</a>
162
159
  </p>
163
- {% endif %}
164
- {% if lfs.state == 'error' %}
165
- <p class="small text-danger mb-2">
166
- <i class="bi bi-exclamation-triangle me-1"></i>Last fetch failed &mdash; see
167
- <a href="/ui/live-env">Live env</a>.
160
+ {% else %}
161
+ <p class="small text-warning mb-0">
162
+ No live-env image selected. Every <code>pixie-*</code> boot_mode falls back to the
163
+ "unavailable" iPXE plan until one is set. <a href="/ui/live-env">Select it &rarr;</a>
168
164
  </p>
169
165
  {% endif %}
170
- <dl class="row small mb-0">
171
- {% for name, size in stats.live_env_files.items() %}
172
- <dt class="col-sm-3 font-monospace">{{ name }}</dt>
173
- <dd class="col-sm-9">
174
- {% if size is not none %}
175
- <span class="text-success"><i class="bi bi-check-circle me-1"></i>{{ (size / 1024 / 1024) | round(1) }} MiB</span>
176
- {% else %}
177
- <span class="text-warning"><i class="bi bi-x-circle me-1"></i>missing</span>
178
- {% endif %}
179
- </dd>
180
- {% endfor %}
181
- </dl>
182
166
  </div>
183
167
  </div>
184
- {% if lfs.state == 'fetching' %}
185
- {# The fetch runs on the server's thread pool; poll the page so the
186
- spinner flips to "ready" (or an error) on its own. A big squashfs
187
- download takes a while, so 5 s is frequent enough without hammering. #}
188
- <script>setTimeout(function () { location.reload(); }, 5000);</script>
189
- {% endif %}
190
168
 
191
169
  <div class="card shadow-sm">
192
170
  <div class="card-header bg-light d-flex align-items-center">
@@ -6,8 +6,7 @@
6
6
  <nav class="navbar subnav-strip" aria-label="Section sub-navigation">
7
7
  <div class="container">
8
8
  <span class="subnav-jumps d-flex align-items-center flex-wrap">
9
- <a href="#media">Media</a>
10
- <a href="#fetch">Fetch</a>
9
+ <a href="#image">Image</a>
11
10
  <a href="#options">Options</a>
12
11
  </span>
13
12
  </div>
@@ -15,104 +14,64 @@
15
14
  {% endblock %}
16
15
 
17
16
  {% block content %}
18
- {% set lfs = live_env_fetch_state or {} %}
19
- {{ page_description('boombox', 'The pixie live env (netboot-pc bake: vmlinuz + initrd + live.squashfs) that the pixie-flash-once / pixie-flash-always / pixie-inventory / pixie-tui boot modes chain into. Fetch it from the configured source, or point that source at a mirror. Until all three files are staged, those boot modes fall back to an "unavailable" iPXE plan.') }}
17
+ {{ page_description('boombox', 'The pixie live env that the pixie-flash-once / pixie-flash-always / pixie-inventory / pixie-tui boot modes boot into. It is a fetched disk image (the pixie-live-env catalog entry) booted over ephemeral nbdboot: select it here by content sha. Until an image is selected and fetched, those boot modes fall back to an "unavailable" iPXE plan.') }}
20
18
 
21
19
  {% if flash_error %}
22
20
  <div class="alert alert-danger py-2">{{ flash_error }}</div>
23
21
  {% endif %}
24
22
 
25
- {# ---- Media state (output) --------------------------------------- #}
26
- <div class="card shadow-sm mb-4" id="media">
23
+ {# ---- Live-env image (the nbdboot selector) ---------------------- #}
24
+ <div class="card shadow-sm mb-4" id="image">
27
25
  <div class="card-header bg-light d-flex align-items-center gap-2">
28
- <h2 class="h5 mb-0"><i class="bi bi-boombox me-2"></i>Staged media</h2>
29
- {% if live_env_ready %}
30
- <span class="badge text-bg-success ms-auto"><i class="bi bi-check-circle me-1"></i>ready</span>
26
+ <h2 class="h5 mb-0"><i class="bi bi-boombox me-2"></i>Live-env image</h2>
27
+ {% if live_env_image_sha.effective %}
28
+ <span class="badge text-bg-success ms-auto"><i class="bi bi-check-circle me-1"></i>selected</span>
31
29
  {% else %}
32
- <span class="badge text-bg-warning ms-auto"><i class="bi bi-exclamation-triangle me-1"></i>not staged</span>
30
+ <span class="badge text-bg-warning ms-auto"><i class="bi bi-exclamation-triangle me-1"></i>none selected</span>
33
31
  {% endif %}
34
32
  </div>
35
33
  <div class="card-body">
36
- <p class="small text-muted mb-2">
37
- Staged under <code>{{ live_env_dir or '(unset)' }}</code>, served to targets at
38
- <code>/boot/pixie-live-env/</code>. The renderer re-checks these files on every
39
- <code>GET /pxe/&lt;mac&gt;</code>, so a fresh fetch takes effect without a restart.
34
+ <p class="small text-muted mb-3">
35
+ Fetch the <code>pixie-live-env</code> image on the
36
+ <a href="/ui/catalog">Catalog</a> page, then paste its
37
+ <code>content_sha256</code> here. The
38
+ <code>pixie-inventory</code> / <code>pixie-tui</code> /
39
+ <code>pixie-flash-*</code> modes boot it over
40
+ <strong>ephemeral nbdboot</strong> (a RO NBD lower + tmpfs
41
+ overlay); dracut brings the NIC up where the retired
42
+ live-boot squashfs hung in the initramfs on some boards. The
43
+ on-target pixie CLI phones home so inventory / flash work as
44
+ before.
40
45
  </p>
41
- <dl class="row small mb-0">
42
- {% for name, size in live_env_files.items() %}
43
- <dt class="col-sm-3 font-monospace">{{ name }}</dt>
44
- <dd class="col-sm-9">
45
- {% if size is not none %}
46
- <span class="text-success"><i class="bi bi-check-circle me-1"></i>{{ (size / 1024 / 1024) | round(1) }} MiB</span>
47
- {% else %}
48
- <span class="text-warning"><i class="bi bi-x-circle me-1"></i>missing</span>
49
- {% endif %}
50
- </dd>
51
- {% endfor %}
52
- </dl>
53
- </div>
54
- </div>
55
-
56
- {# ---- Fetch (action) --------------------------------------------- #}
57
- <div class="card shadow-sm mb-4" id="fetch">
58
- <div class="card-header bg-light fw-semibold">
59
- <i class="bi bi-cloud-download me-1"></i>Fetch live env
60
- </div>
61
- <div class="card-body">
62
- {% if lfs.state == 'fetching' %}
63
- <div class="alert alert-info py-2 small">
64
- <span class="spinner-border spinner-border-sm me-1" role="status"></span>
65
- Fetching live-env{% if lfs.phase %} ({{ lfs.phase }}){% endif %}... this can take a while for the ~1&nbsp;GiB squashfs.
66
- </div>
67
- {% elif lfs.state == 'error' %}
68
- <div class="alert alert-danger py-2 small">
69
- <i class="bi bi-exclamation-triangle me-1"></i>Last fetch failed: {{ lfs.error }}
70
- </div>
71
- {% elif lfs.state == 'done' %}
72
- <div class="alert alert-success py-2 small">
73
- <i class="bi bi-check-circle me-1"></i>Last fetch staged the live env{% if lfs.sha256 %} (sha {{ lfs.sha256[:12] }}){% endif %}.
74
- </div>
75
- {% endif %}
76
-
77
- {# Editable source, then the fetch button that consumes it. #}
78
- <form method="post" action="/ui/live-env/src/edit" class="row g-2 align-items-end mb-3">
79
- <div class="col-12 col-lg-9">
80
- <label for="live_env_src" class="form-label mb-1">
81
- Source
82
- <span class="badge text-bg-{% if live_env_src.override %}primary{% else %}secondary{% endif %} ms-1">
83
- {% if live_env_src.override %}override{% elif live_env_src.effective != live_env_src.default %}env{% else %}default{% endif %}
46
+ <form method="post" action="/ui/live-env/image/edit" class="row g-3">
47
+ <div class="col-12">
48
+ <label for="live_env_image_sha" class="form-label">
49
+ Image content sha
50
+ <span class="badge text-bg-{% if live_env_image_sha.override %}primary{% else %}secondary{% endif %} ms-1">
51
+ {% if live_env_image_sha.override %}override{% elif live_env_image_sha.effective %}env{% else %}default{% endif %}
84
52
  </span>
85
53
  </label>
86
- <input type="text" id="live_env_src" name="live_env_src"
54
+ <input type="text" id="live_env_image_sha" name="live_env_image_sha"
87
55
  class="form-control font-monospace"
88
- value="{{ live_env_src.override }}"
89
- placeholder="{{ live_env_src.default }}">
90
- </div>
91
- <div class="col-12 col-lg-3">
92
- <button type="submit" class="btn btn-outline-secondary w-100">
93
- <i class="bi bi-check2 me-1"></i>Save source
94
- </button>
95
- </div>
96
- <div class="col-12">
56
+ value="{{ live_env_image_sha.override }}"
57
+ placeholder="(unset - live-env modes degrade to the unavailable plan)"
58
+ pattern="[0-9a-fA-F]{64}">
97
59
  <div class="form-text">
98
- Tarball to download (<code>https://</code> or <code>oras://</code>); must unpack to
99
- <code>vmlinuz</code> + <code>initrd</code> + <code>live.squashfs</code>. Blank falls
100
- back to <code>{{ live_env_src.env }}</code> then the latest pixie GitHub release.
101
- Effective: <code>{{ live_env_src.effective }}</code>.
102
- {% if live_env_src.updated_at %}<br>Last updated: <code>{{ live_env_src.updated_at | fmt_ts }}</code>.{% endif %}
60
+ A fetched disk-image's <code>content_sha256</code> (64 hex), plus its
61
+ netboot bundle (via the entry's <code>netboot_src</code>), must both be
62
+ fetched. Blank falls back to <code>{{ live_env_image_sha.env }}</code>
63
+ then unset, in which case the live-env boot modes render the
64
+ <code>unavailable</code> plan. Effective:
65
+ {% if live_env_image_sha.effective %}<code>{{ live_env_image_sha.effective }}</code>{% else %}<em class="text-muted">(none)</em>{% endif %}.
66
+ {% if live_env_image_sha.updated_at %}<br>Last updated: <code>{{ live_env_image_sha.updated_at | fmt_ts }}</code>.{% endif %}
103
67
  </div>
104
68
  </div>
105
- </form>
106
-
107
- <form method="post" action="/ui/live-env/fetch" class="m-0">
108
- <button type="submit" class="btn btn-primary" {% if lfs.state == 'fetching' %}disabled{% endif %}>
109
- {% if lfs.state == 'fetching' %}
110
- <span class="spinner-border spinner-border-sm me-1" role="status"></span>Fetching...
111
- {% else %}
112
- <i class="bi bi-cloud-download me-1"></i>{% if live_env_ready %}Re-fetch live env{% else %}Fetch live env{% endif %}
113
- {% endif %}
114
- </button>
115
- <span class="text-muted small ms-2">Downloads + stages the three files; overwrites what's there.</span>
69
+ <div class="col-12">
70
+ <button type="submit" class="btn btn-primary"><i class="bi bi-check2 me-1"></i>Save</button>
71
+ <span class="text-muted small ms-2">
72
+ Takes effect on the NEXT <code>GET /pxe/&lt;mac&gt;</code>; a target already mid-boot must be power-cycled.
73
+ </span>
74
+ </div>
116
75
  </form>
117
76
  </div>
118
77
  </div>
@@ -158,48 +117,6 @@
158
117
  </span>
159
118
  </div>
160
119
  </form>
161
-
162
- <hr class="my-4">
163
-
164
- <form method="post" action="/ui/live-env/image/edit" class="row g-3">
165
- <div class="col-12">
166
- <label for="live_env_image_sha" class="form-label">
167
- Boot live env as disk image (nbdboot)
168
- <span class="badge text-bg-{% if live_env_image_sha.override %}primary{% else %}secondary{% endif %} ms-1">
169
- {% if live_env_image_sha.override %}override{% elif live_env_image_sha.effective %}env{% else %}default{% endif %}
170
- </span>
171
- </label>
172
- <input type="text" id="live_env_image_sha" name="live_env_image_sha"
173
- class="form-control font-monospace"
174
- value="{{ live_env_image_sha.override }}"
175
- placeholder="(unset - use the live-boot squashfs above)"
176
- pattern="[0-9a-fA-F]{64}">
177
- <div class="form-text">
178
- A fetched disk-image's <code>content_sha256</code> (64 hex). When set - and that
179
- image plus its netboot bundle are fetched - the <code>pixie-inventory</code> /
180
- <code>pixie-tui</code> / <code>pixie-flash-*</code> modes boot it over
181
- <strong>ephemeral nbdboot</strong> instead of fetching the squashfs. dracut brings
182
- the NIC up where the live-boot squashfs hangs in the initramfs on some boards; the
183
- on-target pixie CLI still phones home so inventory / flash work unchanged. Blank
184
- falls back to <code>{{ live_env_image_sha.env }}</code> then the squashfs path.
185
- Effective:
186
- {% if live_env_image_sha.effective %}<code>{{ live_env_image_sha.effective }}</code>{% else %}<em class="text-muted">(none - squashfs)</em>{% endif %}.
187
- {% if live_env_image_sha.updated_at %}<br>Last updated: <code>{{ live_env_image_sha.updated_at | fmt_ts }}</code>.{% endif %}
188
- </div>
189
- </div>
190
- <div class="col-12">
191
- <button type="submit" class="btn btn-primary"><i class="bi bi-check2 me-1"></i>Save</button>
192
- <span class="text-muted small ms-2">
193
- Takes effect on the NEXT <code>GET /pxe/&lt;mac&gt;</code>; a target already mid-boot must be power-cycled.
194
- </span>
195
- </div>
196
- </form>
197
120
  </div>
198
121
  </div>
199
-
200
- {% if lfs.state == 'fetching' %}
201
- {# The fetch runs on the server's thread pool; reload so the spinner
202
- flips to ready / error on its own. #}
203
- <script>setTimeout(function () { location.reload(); }, 5000);</script>
204
- {% endif %}
205
122
  {% endblock %}
pixie/web/main.py CHANGED
@@ -55,8 +55,6 @@ from pixie.events._kinds import (
55
55
  CATALOG_IMPORT_OK,
56
56
  EVENTS_CLEARED,
57
57
  EXPORT_NBDKIT_SPAWNED,
58
- LIVE_ENV_FETCH_DONE,
59
- LIVE_ENV_FETCH_FAILED,
60
58
  TFTP_STARTED,
61
59
  TFTP_STOPPED,
62
60
  )
@@ -83,12 +81,10 @@ from pixie.web._auth import (
83
81
  using_default_password,
84
82
  )
85
83
  from pixie.web._settings_store import (
86
- DEFAULT_LIVE_ENV_SRC,
87
84
  KEY_DATETIME_FORMAT,
88
85
  KEY_DISPLAY_TZ,
89
86
  KEY_LIVE_ENV_EXTRA_CMDLINE,
90
87
  KEY_LIVE_ENV_IMAGE_SHA,
91
- KEY_LIVE_ENV_SRC,
92
88
  SettingsStore,
93
89
  SettingValueError,
94
90
  format_ts,
@@ -101,7 +97,6 @@ _STATIC_DIR = _HERE / "_static"
101
97
 
102
98
  DEFAULT_STATE_DIR = Path("/var/lib/pixie")
103
99
  STATE_DIR_ENV = "PIXIE_DATA_DIR"
104
- LIVE_ENV_DIR_ENV = "PIXIE_LIVE_ENV_DIR"
105
100
  FETCH_POOL_SIZE_ENV = "PIXIE_FETCH_POOL_SIZE"
106
101
  DEFAULT_FETCH_POOL_SIZE = 4
107
102
 
@@ -171,24 +166,6 @@ def _resolve_tftp_bin() -> str:
171
166
  return (os.environ.get(TFTP_BIN_ENV) or "").strip() or DEFAULT_TFTP_BIN
172
167
 
173
168
 
174
- def _resolve_live_env_dir() -> Path | None:
175
- """Directory holding the pixie netboot-pc bake artifacts
176
- (vmlinuz + initrd + squashfs). Default: ``<state_dir>/live-env``.
177
- Explicit override via ``PIXIE_LIVE_ENV_DIR``. Returns the resolved
178
- path whether or not it exists yet -- readiness is decided per-render
179
- by stat'ing the individual files, and the serving mount is created
180
- with ``check_dir=False``, so a fresh deploy can stage the artifacts
181
- into a running container and have them picked up without a restart."""
182
- override = (os.environ.get(LIVE_ENV_DIR_ENV) or "").strip()
183
- if override:
184
- return Path(override)
185
- # Default sits under the state dir so an operator dropping
186
- # artifacts into ``/opt/pixie/data/live-env/`` on a compose
187
- # deploy is the whole install step.
188
- default = _resolve_state_dir() / "live-env"
189
- return default
190
-
191
-
192
169
  def _resolve_default_boot_mode() -> str:
193
170
  """Boot mode a freshly-discovered MAC auto-registers with.
194
171
 
@@ -403,16 +380,6 @@ def _deployment_envvar_docs() -> list[dict[str, str]]:
403
380
  " live-env by default."
404
381
  ),
405
382
  },
406
- {
407
- "name": LIVE_ENV_DIR_ENV,
408
- "default": "$PIXIE_DATA_DIR/live-env",
409
- "purpose": (
410
- "Where the pixie live-env vmlinuz + initrd +"
411
- " live.squashfs are staged. The pixie-* boot modes"
412
- " degrade to an unavailable plan when this dir is"
413
- " missing or incomplete."
414
- ),
415
- },
416
383
  {
417
384
  "name": "PIXIE_LIVE_ENV_EXTRA_CMDLINE",
418
385
  "default": "",
@@ -435,32 +402,18 @@ def _deployment_envvar_docs() -> list[dict[str, str]]:
435
402
  " exit-by-default behaviour."
436
403
  ),
437
404
  },
438
- {
439
- "name": "PIXIE_LIVE_ENV_SRC",
440
- "default": (
441
- "https://github.com/safl/pixie/releases/latest/download/"
442
- "pixie-live-env-x86_64.tar.gz"
443
- ),
444
- "purpose": (
445
- "Tarball the dashboard 'Fetch live-env' action pulls the"
446
- " netboot-pc bake from (https:// or oras://). Defaults to"
447
- " the latest pixie GitHub release; point at a mirror for"
448
- " air-gapped deploys. Live-editable override on the"
449
- " Live env page."
450
- ),
451
- },
452
405
  {
453
406
  "name": "PIXIE_LIVE_ENV_IMAGE_SHA",
454
407
  "default": "",
455
408
  "purpose": (
456
409
  "Disk-image content sha (64 hex) the live-env boot modes"
457
- " boot over EPHEMERAL nbdboot instead of the Debian"
458
- " live-boot squashfs. When set (and the image + its"
459
- " netboot bundle are fetched) the pixie-inventory / -tui /"
460
- " -flash-* modes stream this image over NBD -- dracut"
461
- " brings the NIC up where the squashfs live-boot hangs on"
462
- " some boards. Empty keeps the squashfs path. Live-editable"
463
- " override on the Live env page."
410
+ " boot over EPHEMERAL nbdboot. When set (and the image +"
411
+ " its netboot bundle are fetched) the pixie-inventory /"
412
+ " -tui / -flash-* modes stream this image over NBD --"
413
+ " dracut brings the NIC up where the retired squashfs"
414
+ " live-boot hung on some boards. Empty means those modes"
415
+ " degrade to the unavailable plan. Live-editable override"
416
+ " on the Live env page."
464
417
  ),
465
418
  },
466
419
  {
@@ -769,7 +722,6 @@ def create_app() -> FastAPI:
769
722
  bind=_resolve_nbd_bind(),
770
723
  nbdkit_bin=_resolve_nbdkit_bin(),
771
724
  )
772
- app.state.live_env_dir = _resolve_live_env_dir()
773
725
  app.state.default_boot_mode = _resolve_default_boot_mode()
774
726
  # Root for per-machine qcow2 overlay files. Sub-directory under
775
727
  # the state dir so a single ``PIXIE_DATA_DIR`` override still
@@ -783,7 +735,6 @@ def create_app() -> FastAPI:
783
735
  overlays=app.state.overlays_store,
784
736
  nbd=app.state.nbd_server,
785
737
  overlays_dir=app.state.overlays_dir,
786
- live_env_dir=app.state.live_env_dir,
787
738
  events=app.state.events_log,
788
739
  )
789
740
  app.state.tftp_server = (
@@ -801,11 +752,6 @@ def create_app() -> FastAPI:
801
752
  thread_name_prefix="pixie-fetch",
802
753
  )
803
754
  app.state.fetch_states = {}
804
- # Single-slot progress dict for the operator "Fetch live-env"
805
- # action (there is one live env per pixie, not one per resource).
806
- # Shape mirrors a fetch_states entry: state -> fetching / done /
807
- # error, plus phase + byte counters while the download runs.
808
- app.state.live_env_fetch_state = {}
809
755
 
810
756
  # SessionMiddleware signs the ``pixie-token`` cookie. Sliding TTL:
811
757
  # 7 days from last touch. ``https_only=False`` because pixie is
@@ -845,28 +791,6 @@ def create_app() -> FastAPI:
845
791
  templates.env.filters["humanize_hz"] = humanize_hz
846
792
  if _STATIC_DIR.is_dir():
847
793
  app.mount("/static", StaticFiles(directory=str(_STATIC_DIR)), name="static")
848
- # Serve the netboot-pc bake artifacts (vmlinuz + initrd + squashfs)
849
- # under ``/boot/pixie-live-env/`` so the live-env iPXE chain can
850
- # fetch them. Mounted UNCONDITIONALLY with ``check_dir=False``: on a
851
- # fresh deploy the dir doesn't exist at startup (the operator stages
852
- # it later via "Fetch live env"), and StaticFiles resolves each
853
- # request against the directory live -- so a post-startup fetch is
854
- # served without a pixie restart. Guarding the mount on ``is_dir()``
855
- # at startup was a bug: the renderer would emit a plan pointing at
856
- # ``/boot/pixie-live-env/vmlinuz`` that then 404'd until a restart.
857
- if app.state.live_env_dir is not None:
858
- # Create the dir so StaticFiles serves a clean 404 for a
859
- # not-yet-staged file (an empty/absent dir would otherwise raise
860
- # at request time) and picks up a later "Fetch live env" with no
861
- # restart. ``check_dir=False`` keeps app startup resilient if the
862
- # mkdir is somehow refused.
863
- with contextlib_suppress(OSError):
864
- app.state.live_env_dir.mkdir(parents=True, exist_ok=True)
865
- app.mount(
866
- "/boot/pixie-live-env",
867
- StaticFiles(directory=str(app.state.live_env_dir), check_dir=False),
868
- name="live-env",
869
- )
870
794
 
871
795
  # ---------- exception handlers -----------------------------------
872
796
 
@@ -1813,28 +1737,17 @@ def create_app() -> FastAPI:
1813
1737
  overlays=overlays_store, machines=machines_store, catalog=catalog, nbd=nbd
1814
1738
  )
1815
1739
  )
1816
- # Live-env media state. ``pixie-tui`` / ``pixie-inventory`` /
1817
- # ``pixie-flash-*`` all need vmlinuz + initrd + live.squashfs
1818
- # staged under ``PIXIE_LIVE_ENV_DIR`` (defaults to
1819
- # ``<state>/live-env``). Surface the staged / missing signal
1820
- # + per-file byte counts so the operator can tell at a glance
1821
- # whether the pixie boot modes are live or would fall back to
1822
- # the "unavailable" plan.
1823
- live_env_dir = request.app.state.live_env_dir
1824
- live_env_ready = False
1825
- live_env_files: dict[str, int | None] = {
1826
- "vmlinuz": None,
1827
- "initrd": None,
1828
- "live.squashfs": None,
1829
- }
1830
- if live_env_dir is not None:
1831
- for name in live_env_files:
1832
- p = live_env_dir / name
1833
- try:
1834
- live_env_files[name] = p.stat().st_size if p.is_file() else None
1835
- except OSError:
1836
- live_env_files[name] = None
1837
- live_env_ready = all(v is not None for v in live_env_files.values())
1740
+ # Live-env readiness. ``pixie-tui`` / ``pixie-inventory`` /
1741
+ # ``pixie-flash-*`` boot the disk image named by
1742
+ # ``live_env.image_sha`` (PIXIE_LIVE_ENV_IMAGE_SHA / the Live env
1743
+ # page) over ephemeral nbdboot. Ready == a sha is selected AND
1744
+ # its catalog image is fetched on disk; otherwise those modes
1745
+ # degrade to the "unavailable" plan. The renderer does the full
1746
+ # bundle+blob check at boot; this is the at-a-glance signal.
1747
+ live_env_image_sha = request.app.state.settings_store.resolve_live_env_image_sha()
1748
+ live_env_ready = bool(live_env_image_sha) and any(
1749
+ e.content_sha256 == live_env_image_sha for e in images if e.content_sha256
1750
+ )
1838
1751
  # Events summary: total row count + unacknowledged error
1839
1752
  # count + newest ts. ``events_ack_ts`` is a soft cursor an
1840
1753
  # operator advances by hitting Acknowledge on the dashboard;
@@ -1870,10 +1783,7 @@ def create_app() -> FastAPI:
1870
1783
  "events_last_ts": events_stats["last_ts"],
1871
1784
  "events_ack_ts": events_ack_ts,
1872
1785
  "live_env_ready": live_env_ready,
1873
- "live_env_dir": str(live_env_dir) if live_env_dir is not None else "",
1874
- "live_env_files": live_env_files,
1875
- "live_env_src": request.app.state.settings_store.resolve_live_env_src(),
1876
- "live_env_fetch_state": dict(request.app.state.live_env_fetch_state),
1786
+ "live_env_image_sha": live_env_image_sha,
1877
1787
  }
1878
1788
 
1879
1789
  @app.get("/ui/dashboard-live.json")
@@ -2529,35 +2439,15 @@ def create_app() -> FastAPI:
2529
2439
  return RedirectResponse(url="/ui/settings", status_code=status.HTTP_303_SEE_OTHER)
2530
2440
 
2531
2441
  def _live_env_context(request: Request, flash_error: str | None = None) -> dict[str, Any]:
2532
- """Render context for the dedicated /ui/live-env pane: staged
2533
- media state (ready + per-file sizes), the fetch source + extra
2534
- cmdline overrides (with provenance), and any in-flight fetch."""
2442
+ """Render context for the dedicated /ui/live-env pane: the
2443
+ selected live-env disk image (nbdboot), plus the extra-cmdline
2444
+ override, each with provenance."""
2535
2445
  state = request.app.state
2536
2446
  store: SettingsStore = state.settings_store
2537
- live_env_dir = state.live_env_dir
2538
- files: dict[str, int | None] = {"vmlinuz": None, "initrd": None, "live.squashfs": None}
2539
- if live_env_dir is not None:
2540
- for name in files:
2541
- p = live_env_dir / name
2542
- try:
2543
- files[name] = p.stat().st_size if p.is_file() else None
2544
- except OSError:
2545
- files[name] = None
2546
2447
  return {
2547
2448
  "version": pixie.__version__,
2548
2449
  "authed": True,
2549
2450
  "page": "live-env",
2550
- "live_env_ready": all(v is not None for v in files.values()),
2551
- "live_env_dir": str(live_env_dir) if live_env_dir is not None else "",
2552
- "live_env_files": files,
2553
- "live_env_fetch_state": dict(state.live_env_fetch_state),
2554
- "live_env_src": {
2555
- "override": store.get(KEY_LIVE_ENV_SRC) or "",
2556
- "effective": store.resolve_live_env_src(),
2557
- "default": DEFAULT_LIVE_ENV_SRC,
2558
- "env": "PIXIE_LIVE_ENV_SRC",
2559
- "updated_at": store.updated_at(KEY_LIVE_ENV_SRC) or "",
2560
- },
2561
2451
  "live_env_extra_cmdline": {
2562
2452
  "override": store.get(KEY_LIVE_ENV_EXTRA_CMDLINE) or "",
2563
2453
  "effective": store.resolve_live_env_extra_cmdline(),
@@ -2615,23 +2505,6 @@ def create_app() -> FastAPI:
2615
2505
  store.clear(KEY_LIVE_ENV_EXTRA_CMDLINE)
2616
2506
  return RedirectResponse(url="/ui/live-env", status_code=status.HTTP_303_SEE_OTHER)
2617
2507
 
2618
- @app.post("/ui/live-env/src/edit", response_model=None)
2619
- def ui_live_env_src_edit(
2620
- request: Request,
2621
- live_env_src: str = Form(""),
2622
- _auth: None = Depends(_require_ui_auth),
2623
- ) -> RedirectResponse:
2624
- """Persist the live-env fetch-src override. Blank clears it so
2625
- the value falls back to $PIXIE_LIVE_ENV_SRC then the default
2626
- GitHub-release URL."""
2627
- store: SettingsStore = request.app.state.settings_store
2628
- raw = (live_env_src or "").strip()
2629
- if raw:
2630
- store.set_value(KEY_LIVE_ENV_SRC, raw)
2631
- else:
2632
- store.clear(KEY_LIVE_ENV_SRC)
2633
- return RedirectResponse(url="/ui/live-env", status_code=status.HTTP_303_SEE_OTHER)
2634
-
2635
2508
  @app.post("/ui/live-env/image/edit", response_model=None)
2636
2509
  def ui_live_env_image_edit(
2637
2510
  request: Request,
@@ -2640,12 +2513,12 @@ def create_app() -> FastAPI:
2640
2513
  ) -> HTMLResponse | RedirectResponse:
2641
2514
  """Persist the live-env disk-image content sha. When set (and the
2642
2515
  image + its netboot bundle are fetched) the live-env boot modes
2643
- nbdboot this image ephemerally instead of fetching the squashfs.
2644
- Blank clears the override so the value falls back to
2645
- $PIXIE_LIVE_ENV_IMAGE_SHA then empty (the squashfs path). Rejects
2646
- anything that is not 64 lowercase hex chars so a fat-fingered sha
2647
- doesn't quietly send every live-env boot into the unavailable
2648
- plan."""
2516
+ nbdboot this image ephemerally. Blank clears the override so the
2517
+ value falls back to $PIXIE_LIVE_ENV_IMAGE_SHA then empty, in
2518
+ which case the live-env boot modes degrade to the unavailable
2519
+ plan. Rejects anything that is not 64 lowercase hex chars so a
2520
+ fat-fingered sha doesn't quietly send every live-env boot into
2521
+ the unavailable plan."""
2649
2522
  import re as _re
2650
2523
 
2651
2524
  store: SettingsStore = request.app.state.settings_store
@@ -2669,69 +2542,6 @@ def create_app() -> FastAPI:
2669
2542
  store.clear(KEY_LIVE_ENV_IMAGE_SHA)
2670
2543
  return RedirectResponse(url="/ui/live-env", status_code=status.HTTP_303_SEE_OTHER)
2671
2544
 
2672
- @app.post("/ui/live-env/fetch")
2673
- def ui_live_env_fetch(
2674
- request: Request,
2675
- _auth: None = Depends(_require_ui_auth),
2676
- ) -> RedirectResponse:
2677
- """Download the live-env tarball (from PIXIE_LIVE_ENV_SRC or its
2678
- override) and stage vmlinuz + initrd + live.squashfs under
2679
- PIXIE_LIVE_ENV_DIR, on the fetch pool. The in-app replacement
2680
- for a manual ``make build VARIANT=netboot-pc`` + copy. Progress
2681
- lands on ``app.state.live_env_fetch_state`` for the dashboard to
2682
- poll; idempotent while a fetch is already running."""
2683
- state = request.app.state
2684
- live_env_dir = state.live_env_dir
2685
- fs = state.live_env_fetch_state
2686
- if live_env_dir is None:
2687
- fs.clear()
2688
- fs.update(
2689
- {
2690
- "state": "error",
2691
- "error": "no live-env dir (PIXIE_LIVE_ENV_DIR unset or unwritable)",
2692
- "at_iso": _now_iso(),
2693
- }
2694
- )
2695
- return RedirectResponse(url="/ui/live-env", status_code=status.HTTP_303_SEE_OTHER)
2696
- if fs.get("state") == "fetching":
2697
- return RedirectResponse(url="/ui/live-env", status_code=status.HTTP_303_SEE_OTHER)
2698
-
2699
- src = state.settings_store.resolve_live_env_src()
2700
- fs.clear()
2701
- fs.update({"state": "fetching", "src": src, "phase": "starting", "at_iso": _now_iso()})
2702
- events = getattr(state, "events_log", None)
2703
-
2704
- def _run() -> None:
2705
- from pixie.catalog._live_env import stage_live_env
2706
-
2707
- def _report(payload: dict[str, Any]) -> None:
2708
- # Layer phase / byte counters onto the "fetching" pill.
2709
- fs.update(payload)
2710
-
2711
- try:
2712
- result = stage_live_env(src, live_env_dir, progress=_report)
2713
- except Exception as exc:
2714
- fs.clear()
2715
- fs.update({"state": "error", "src": src, "error": str(exc), "at_iso": _now_iso()})
2716
- if events is not None:
2717
- events.emit(
2718
- LIVE_ENV_FETCH_FAILED,
2719
- summary=f"live-env fetch failed: {exc}",
2720
- details={"src": src, "error": str(exc)},
2721
- )
2722
- return
2723
- fs.clear()
2724
- fs.update({"state": "done", "src": src, "sha256": result.sha256, "at_iso": _now_iso()})
2725
- if events is not None:
2726
- events.emit(
2727
- LIVE_ENV_FETCH_DONE,
2728
- summary=f"live-env staged from {src} (sha {result.sha256[:12]})",
2729
- details={"src": src, "sha256": result.sha256, "bytes": result.size},
2730
- )
2731
-
2732
- state.fetch_pool.submit(_run)
2733
- return RedirectResponse(url="/ui/live-env", status_code=status.HTTP_303_SEE_OTHER)
2734
-
2735
2545
  # ---------- feature routers --------------------------------------
2736
2546
  #
2737
2547
  # Catalog + blob + artifacts routes live at the same URL shape they
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pixie-lab
3
- Version: 0.4.2
3
+ Version: 0.4.3
4
4
  Summary: Bare-metal netboot appliance: catalog + fetch + NBD + PXE + TFTP + operator TUI in one container
5
5
  Project-URL: Homepage, https://github.com/safl/pixie
6
6
  Project-URL: Repository, https://github.com/safl/pixie
@@ -9,7 +9,6 @@ pixie/oras_pull.py,sha256=Xyqga4o8vtCZrUSv5YgLggqmXs36y6FXd-_E9a_iRVo,3326
9
9
  pixie/tui_catalog.py,sha256=nKiVd-SC_qfS3m6phQXRnqQdKN_nTqZbQFl2cIscIoQ,26305
10
10
  pixie/catalog/__init__.py,sha256=DWBJOL0goBieMLWFO2RXTc_w6e0OQ3f6qQVn-oiTq3c,2309
11
11
  pixie/catalog/_fetcher.py,sha256=xiMLccBoJsjNumy1DT38aJiqM4-wYSzHGtklRKnKWlE,26698
12
- pixie/catalog/_live_env.py,sha256=DWbPecbipSZ4KQAx60yi4q_gFncOZt4f1yj-QZ7ZBV0,5068
13
12
  pixie/catalog/_routes.py,sha256=8s_8bEDYL5zwg6gNAtxR7NOGPLY7ho9spqjPU83RIPE,14729
14
13
  pixie/catalog/_schema.py,sha256=KC6RcKlToX1DgcRjn25djKMx78-eS-4WzNRY267h0I0,9769
15
14
  pixie/catalog/_store.py,sha256=S7OM7A05Y5ES4RuhmwqQupDGNWXF--p_UMfUNOgQHsQ,9044
@@ -18,7 +17,7 @@ pixie/deploy/__init__.py,sha256=3PvSvrIRdqxLCz0nTdVSBiPzEtDl359XSUBgyMBkMT8,1019
18
17
  pixie/deploy/_main.py,sha256=Or2espz9oWNmnGfyEzIsEyvRwWEHL4uxyE6U9OdMZJs,15403
19
18
  pixie/deploy/_templates.py,sha256=WkkKRDU00FDWVZM0XeKR-BCtSBPvaZ2QHM1TBeq0RYg,5418
20
19
  pixie/events/__init__.py,sha256=3Quxl56ePkKD9-PjbomCOOZOpmqJ3wHcYa7VcT9KXv8,923
21
- pixie/events/_kinds.py,sha256=clGaTjEmbX13FibWiXREMb2WkYQpUio4lQpvHr8iLTM,10957
20
+ pixie/events/_kinds.py,sha256=LyyIrg4pGaat062sBIFrfGYGbZflsenfq7P-PkWhP04,10160
22
21
  pixie/events/_log.py,sha256=0XSdIYrPIEtmxjTq9L7gy1Q4qGotTxgHhMkM9fv8tyY,9041
23
22
  pixie/events/_routes.py,sha256=MbY69aZKDEzy4O8AizHtv_POPbdNMqz94imccFbtelM,1363
24
23
  pixie/exports/__init__.py,sha256=RiI69GG_bjkb_d1CNRyk9KI7wyMpLVvWc_NgNnWxOYc,546
@@ -31,7 +30,7 @@ pixie/machines/_store.py,sha256=zh89zj2s5CurMFO258i10ANW20VCTQk0kT8MgRhnWi0,3037
31
30
  pixie/pivot/__init__.py,sha256=k_yIUOrIu-DKCJ_8XmRtnoVV7Rr9URagHU2Y76HB2ew,6195
32
31
  pixie/pivot/nbdboot,sha256=stBWGXnSXMFJMMzmtip77dPwWxQLfOewXo9YOgw5u0I,19558
33
32
  pixie/pxe/__init__.py,sha256=AKBRKQYdlH7DdNBvES9EwYTzPOkeNbWBN1S7IH1UEyw,784
34
- pixie/pxe/_renderer.py,sha256=Tg2oMZQ_vSkEmsc2Rn_SXZ6Ed14LXUmZTOgzkLQC56g,18833
33
+ pixie/pxe/_renderer.py,sha256=KU9M0eZ5aRxI9Tsyej9MMXkBt5TooRoKY-a7pSQHvR8,17277
35
34
  pixie/pxe/_routes.py,sha256=VhWH5VXaaAC39Bt3asZPpp9ckTCOJdJx3DOLFVTn-2M,19169
36
35
  pixie/tftp/__init__.py,sha256=FuHn1KF5_NoGOM_dHXSbubX5lJr0tuMugJGFxxwuUws,774
37
36
  pixie/tftp/_supervisor.py,sha256=khpjgWqdMzdnhRaPYM4j25PoKijbBnvr-GjJLybP_rY,4659
@@ -45,9 +44,9 @@ pixie/web/_images.py,sha256=RhtTmzRhapr6zRbQN4SKaW_o3dhUhf6hyZGIlbxMh_k,6993
45
44
  pixie/web/_inventory.py,sha256=rJDNAfejmtlNPCPUr2mhLey19C5Tu_dmpia9KVeyT7E,10189
46
45
  pixie/web/_overlay_bind.py,sha256=7aKYqMaujxt-XRa1jB6mVEkbt-w_koXjLvUzBTzy2o8,4125
47
46
  pixie/web/_overlays.py,sha256=5yZp6pDiFj_WKdLx6vCoEKD8mXG3KjkMOQt4OGYYdyc,8571
48
- pixie/web/_settings_store.py,sha256=TrhjRN2ME1K3VsgJPJif7ZA3Tf7pM9k9oL57Hhh_YH0,12409
47
+ pixie/web/_settings_store.py,sha256=X9dO5dr31PvRUM-Y7cLilEiNG_7MCV8CpDW2Nc6V0Tc,11242
49
48
  pixie/web/_table_state.py,sha256=zGH5oZ-XlMqUFQqmYglYqT_fu1Yrtaw-g5yhPtAKAEc,9524
50
- pixie/web/main.py,sha256=1eu183QjBjcN5yKuFn0TGD5rNaDuF6U4TPED29EsSJ0,117004
49
+ pixie/web/main.py,sha256=r4ZmVBBjvj5Cs8nWOOHZkBVv2BttNeNOSnKc_fNb-1Q,108149
51
50
  pixie/web/_static/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
52
51
  pixie/web/_static/bootstrap-icons.min.css,sha256=pdY4ejLKO67E0CM2tbPtq1DJ3VGDVVdqAR6j3ZwdiE4,87008
53
52
  pixie/web/_static/bootstrap.min.css,sha256=NXgoHLHuUWz0lbqh8PRwCIl7_QhZE74v-FCwrYdCW1E,234717
@@ -58,12 +57,12 @@ pixie/web/_static/fonts/bootstrap-icons.woff,sha256=9VUTt7WRy4SjuH_w406iTUgx1v7c
58
57
  pixie/web/_static/fonts/bootstrap-icons.woff2,sha256=bHVxA2ShylYEJncW9tKJl7JjGf2weM8R4LQqtm_y6mE,134044
59
58
  pixie/web/_templates/catalog.html,sha256=CfjO7qcoy5226qy-24Qkx-ZvpoeRtW5gWsTkQzhi8sg,17465
60
59
  pixie/web/_templates/catalog_detail.html,sha256=G12HHq2iYKv5bn5wjGfppUPJ12iOm9G5TjDlr-DTsHc,18182
61
- pixie/web/_templates/dashboard.html,sha256=-77IP9kd1Mr9IVwp7aJMwEmuTL1jjlC8b0RyxDKOvJA,16051
60
+ pixie/web/_templates/dashboard.html,sha256=Ys3TBRpQF9PShMa2uFtTIKSWeAc6THF6JDRHaIXgOGA,14996
62
61
  pixie/web/_templates/events.html,sha256=ifawe--m5FMcicKZ2daCCycV7rGgpLJz7A309UrTolM,7666
63
62
  pixie/web/_templates/image_detail.html,sha256=--DjHCDgvripwr-9_lmnmaLCe4Iw2W_gXIGuKLMvcJw,6926
64
63
  pixie/web/_templates/images.html,sha256=PO8r0hZ6tXMqHZo_m1t_wgh4NEZP6u_72owH_Jbz2Zo,7266
65
64
  pixie/web/_templates/layout.html,sha256=9kQ1sWyAqMl9YgwJO1jIjwRbiG8bt2sUpJLNrbBZmzo,13988
66
- pixie/web/_templates/live_env.html,sha256=0goOWgrCXvONHEyZSGfqG6h_B0rPaEHvw7Cbt-UpL4I,11557
65
+ pixie/web/_templates/live_env.html,sha256=EtZB728ZG-v2kG-YAwox3R9moz0O24rBD_HGJXkpRQM,7132
67
66
  pixie/web/_templates/login.html,sha256=d60N8kmr7YaAoTKRBaOiGN5Srdy8-BgB8_Zz9TQvGwc,1810
68
67
  pixie/web/_templates/machine_detail.html,sha256=ijI5FKuyqOdDwCdLGM_H7yHgqn6fFMm6pOPVBLT9G00,49797
69
68
  pixie/web/_templates/machines.html,sha256=modlu9MNqpxOkDd90o-yV1g5yuEiC61I6JtXQk7_PUo,9803
@@ -75,10 +74,9 @@ pixie/web/_templates/_partials/table_helpers.html,sha256=xObkXvfOSoM9rB5Ni_W3nnU
75
74
  pixie/web/_templates/ipxe/bootstrap.j2,sha256=xt13H2dHf-SeLhWRQvptqGY51sJ_L24C2ExnLj9uXPc,490
76
75
  pixie/web/_templates/ipxe/exit.j2,sha256=pbTXTwjgiEsPZlRuv7_J0Q3BepXEqXnb82mv1sxDW24,371
77
76
  pixie/web/_templates/ipxe/nbdboot.j2,sha256=s1CwIKou5W6LRoC2MdZYwA3RcRN8AU3B1gbJmGLS9MM,3950
78
- pixie/web/_templates/ipxe/pixie-live-env.j2,sha256=dPWuYCFrkXwwatPSnTRSfWuMUMgvs3VLDHqbJTG1pP0,3048
79
77
  pixie/web/_templates/ipxe/unavailable.j2,sha256=zt4DYfYRXmOIjL13diYPxEApTP3CRnn5Beszeh3BPBw,440
80
- pixie_lab-0.4.2.dist-info/METADATA,sha256=KVZwvaxEXtrV_qtKhwUyIqJtMBUxyeYx6qyWC4zz_nc,4452
81
- pixie_lab-0.4.2.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
82
- pixie_lab-0.4.2.dist-info/entry_points.txt,sha256=G3vtKpg_gQtisiyS31EjdvC4QC52Kfw2K7iMsfsnj5A,71
83
- pixie_lab-0.4.2.dist-info/licenses/LICENSE,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
84
- pixie_lab-0.4.2.dist-info/RECORD,,
78
+ pixie_lab-0.4.3.dist-info/METADATA,sha256=5vQcLth91856S2hyQf638LF20qcMfqksG7kk_vHkRvU,4452
79
+ pixie_lab-0.4.3.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
80
+ pixie_lab-0.4.3.dist-info/entry_points.txt,sha256=G3vtKpg_gQtisiyS31EjdvC4QC52Kfw2K7iMsfsnj5A,71
81
+ pixie_lab-0.4.3.dist-info/licenses/LICENSE,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
82
+ pixie_lab-0.4.3.dist-info/RECORD,,
@@ -1,128 +0,0 @@
1
- """Fetch + stage the pixie live-env (the netboot-pc bake).
2
-
3
- The live env -- ``vmlinuz`` + ``initrd`` + ``live.squashfs`` that the
4
- ``pixie-flash-*`` / ``pixie-inventory`` / ``pixie-tui`` boot modes chain
5
- into -- was the one artifact pixie could not retrieve itself: it had to
6
- be baked locally (``make build VARIANT=netboot-pc``) or hand-copied into
7
- ``PIXIE_LIVE_ENV_DIR``. This module pulls it from a single tarball
8
- ``src`` (the same ``https://`` / ``oras://`` schemes the catalog fetch
9
- speaks) and stages the three files atomically, reusing the catalog
10
- fetch pipeline's curl download + resume plumbing.
11
-
12
- Contract with CI's ``publish-release`` job: the tarball carries exactly
13
- ``vmlinuz`` + ``initrd`` + ``live.squashfs`` at the archive root. Any
14
- other member is ignored; a missing one is a hard error (better a loud
15
- "tarball is wrong" than a live env that half-stages and boots into a
16
- mysterious squashfs-fetch hang).
17
- """
18
-
19
- from __future__ import annotations
20
-
21
- import os
22
- import shutil
23
- import tarfile
24
- import tempfile
25
- from dataclasses import dataclass, field
26
- from pathlib import Path
27
-
28
- from pixie._util import CHUNK
29
- from pixie.catalog._fetcher import (
30
- FetchError,
31
- ProgressReporter,
32
- _resolve_fetch_url,
33
- _stream_to_tmpfile,
34
- )
35
-
36
- # The three files a target's iPXE chain pulls from
37
- # ``/boot/pixie-live-env/``. Names match what ``pixie-live-env.j2``
38
- # references (note ``live.squashfs``, not the bake's raw ``*.squashfs``
39
- # -- the CI tar step renames it).
40
- LIVE_ENV_FILES: frozenset[str] = frozenset({"vmlinuz", "initrd", "live.squashfs"})
41
-
42
-
43
- @dataclass
44
- class LiveEnvResult:
45
- """Outcome of a successful stage: the src it came from, the sha256
46
- of the downloaded tarball, its byte size, and the staged file
47
- sizes."""
48
-
49
- src: str
50
- sha256: str
51
- size: int
52
- files: dict[str, int] = field(default_factory=dict)
53
-
54
-
55
- def _unpack_live_env_tar(tar_path: Path, live_env_dir: Path) -> dict[str, int]:
56
- """Extract the live-env trio from ``tar_path`` into ``live_env_dir``
57
- atomically. Returns ``{name: size_bytes}``.
58
-
59
- Hardening mirrors :func:`pixie.catalog._fetcher._unpack_netboot_bundle`:
60
- only basenames in :data:`LIVE_ENV_FILES` at the archive root are
61
- accepted; nested paths, parent traversal, and dotfiles are dropped.
62
- Files land in a staging subdir first, then each is ``os.replace``'d
63
- onto its final name so a target fetching mid-stage sees the old
64
- file or the new one, never a half-written squashfs.
65
- """
66
- live_env_dir.mkdir(parents=True, exist_ok=True)
67
- staging = Path(tempfile.mkdtemp(prefix=".staging-", dir=str(live_env_dir)))
68
- try:
69
- # ``r:*`` transparently handles .tar / .tar.gz / .tar.xz so the
70
- # CI side can pick whatever compression without a pixie change.
71
- with tarfile.open(str(tar_path), mode="r:*") as tar:
72
- for member in tar.getmembers():
73
- if not member.isfile():
74
- continue
75
- name = os.path.basename(member.name)
76
- if not name or name.startswith(".") or name != member.name:
77
- continue
78
- if name not in LIVE_ENV_FILES:
79
- continue
80
- extracted = tar.extractfile(member)
81
- if extracted is None:
82
- continue
83
- with (staging / name).open("wb") as out:
84
- shutil.copyfileobj(extracted, out, length=CHUNK)
85
-
86
- missing = LIVE_ENV_FILES - {p.name for p in staging.iterdir()}
87
- if missing:
88
- raise FetchError(
89
- f"live-env tarball {tar_path.name} missing required file(s): {sorted(missing)}"
90
- )
91
-
92
- sizes: dict[str, int] = {}
93
- for name in sorted(LIVE_ENV_FILES):
94
- final = live_env_dir / name
95
- os.replace(staging / name, final)
96
- sizes[name] = final.stat().st_size
97
- return sizes
98
- finally:
99
- shutil.rmtree(staging, ignore_errors=True)
100
-
101
-
102
- def stage_live_env(
103
- src: str,
104
- live_env_dir: Path,
105
- *,
106
- progress: ProgressReporter = None,
107
- ) -> LiveEnvResult:
108
- """Download the live-env tarball at ``src`` and stage its three
109
- files into ``live_env_dir``.
110
-
111
- Reuses the catalog fetch's curl transport (resume + retry + stall
112
- detection). The tarball lands in ``live_env_dir`` itself so the
113
- unpack + ``os.replace`` stay on one filesystem. Raises
114
- :class:`FetchError` on an empty/bad src, an unsupported scheme, a
115
- download failure, or a tarball missing a required file.
116
- """
117
- if not (src or "").strip():
118
- raise FetchError("no live-env src configured (set PIXIE_LIVE_ENV_SRC)")
119
- url, headers = _resolve_fetch_url(src.strip())
120
- live_env_dir.mkdir(parents=True, exist_ok=True)
121
- tmp_tar, sha256, size = _stream_to_tmpfile(url, headers, live_env_dir, progress)
122
- try:
123
- if progress is not None:
124
- progress({"phase": "unpacking"})
125
- sizes = _unpack_live_env_tar(tmp_tar, live_env_dir)
126
- finally:
127
- tmp_tar.unlink(missing_ok=True)
128
- return LiveEnvResult(src=src.strip(), sha256=sha256, size=size, files=sizes)
@@ -1,55 +0,0 @@
1
- #!ipxe
2
- # pixie plan for {{ mac }} - boot_mode={{ boot_mode }}
3
- #
4
- # Boot pixie's own live env. The netboot-pc bake produced vmlinuz +
5
- # initrd + squashfs artifacts; the pixie deploy staged them under
6
- # ``$PIXIE_LIVE_ENV_DIR`` and this HTTP process exposes them at
7
- # ``/boot/pixie-live-env/``. Debian live-boot cmdline shape:
8
- #
9
- # boot=live components fetch=<squashfs URL>
10
- #
11
- # handles the initrd -> squashfs pivot. ``pixie.server`` +
12
- # ``pixie.mac`` are the two knobs pixie-on-tty1.service reads on
13
- # every live-env boot; the pixie CLI then GETs
14
- # ``<server>/pxe/<mac>/plan`` and dispatches (auto-flash,
15
- # inventory + reboot, interactive wizard, or exit).
16
- #
17
- # ``ip=dhcp`` mirrors the nbdboot template's cmdline: the initrd's
18
- # network module (Debian live-boot, in this case) needs an explicit
19
- # DHCP directive to bring the primary NIC up before pixie CLI tries
20
- # to reach ``$pixie-server``. ``console=`` chain matches nbdboot too
21
- # so a target with a serial console reads output on both tty0 and
22
- # ttyS0/1 -- most bench-side debugging happens on serial.
23
-
24
- set pixie-base http://{{ host }}:{{ port }}
25
-
26
- # Cmdline matches bty's proven-working live-env kernel line
27
- # (bty/src/bty/web/_templates/ipxe_flash.j2 + ipxe_tui.j2). Deltas
28
- # from an earlier pixie shape that had drifted:
29
- #
30
- # - ``ip=dhcp`` REMOVED. Debian live-boot's ``fetch=`` handler
31
- # configures the network itself via its own DHCP client; keeping
32
- # both racing produced silent hangs on GIGABYTE MC12-LE0 hardware
33
- # where live-boot's ipconfig would return with no NIC configured
34
- # despite iPXE having successfully DHCP'd from the same interface.
35
- # bty never had it; verified live on 10.20.30.61 (matx-bmc) with
36
- # nothing else changing.
37
- # - ``plymouth.enable=0 modprobe.blacklist=nouveau,rndis_host
38
- # nouveau.modeset=0 systemd.gpt_auto=0`` ADDED. Same set bty
39
- # ships. plymouth stays off so the operator sees the full boot
40
- # stream (a wedge between two ``[ OK ] Started X`` lines is
41
- # immediately diagnostic); nouveau blacklist keeps NVIDIA
42
- # GPU quirks from stalling non-graphics targets; rndis_host
43
- # blacklist stops the kernel from claiming a USB-console-emulated
44
- # NIC before the real one appears; systemd.gpt_auto=0 keeps
45
- # systemd from auto-mounting random GPT partitions the flasher
46
- # is about to overwrite.
47
- #
48
- # ``console=`` chain matches bty too; LAST-wins for /dev/console.
49
- # The ttyS0 tail lands on most bench-side USB-serial adapters;
50
- # operators driving from a BMC's SoL on ttyS1 append their own
51
- # ``console=ttyS1,115200`` via PIXIE_LIVE_ENV_EXTRA_CMDLINE.
52
-
53
- kernel ${pixie-base}/boot/pixie-live-env/vmlinuz boot=live fetch=${pixie-base}/boot/pixie-live-env/live.squashfs components console=tty0 console=ttyS1,115200 console=ttyS0,115200 earlyprintk=ttyS0,115200 plymouth.enable=0 modprobe.blacklist=nouveau,rndis_host nouveau.modeset=0 systemd.gpt_auto=0 pixie.server=${pixie-base} pixie.mac={{ mac }} bty.server=${pixie-base} bty.mac={{ mac }}{% if extra_cmdline %} {{ extra_cmdline }}{% endif %}
54
- initrd ${pixie-base}/boot/pixie-live-env/initrd
55
- boot