pixie-lab 0.4.0__py3-none-any.whl → 0.4.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pixie/_partition.py +93 -9
- pixie/catalog/_fetcher.py +15 -4
- pixie/catalog/catalog.toml +24 -0
- pixie/machines/_routes.py +8 -6
- pixie/machines/_store.py +29 -4
- pixie/oras_pull.py +85 -0
- pixie/pxe/_renderer.py +127 -59
- pixie/pxe/_routes.py +45 -3
- pixie/web/_settings_store.py +63 -6
- pixie/web/_templates/live_env.html +36 -0
- pixie/web/main.py +90 -1
- {pixie_lab-0.4.0.dist-info → pixie_lab-0.4.2.dist-info}/METADATA +1 -1
- {pixie_lab-0.4.0.dist-info → pixie_lab-0.4.2.dist-info}/RECORD +16 -15
- {pixie_lab-0.4.0.dist-info → pixie_lab-0.4.2.dist-info}/WHEEL +0 -0
- {pixie_lab-0.4.0.dist-info → pixie_lab-0.4.2.dist-info}/entry_points.txt +0 -0
- {pixie_lab-0.4.0.dist-info → pixie_lab-0.4.2.dist-info}/licenses/LICENSE +0 -0
pixie/_partition.py
CHANGED
|
@@ -25,11 +25,41 @@ from __future__ import annotations
|
|
|
25
25
|
import json
|
|
26
26
|
import shutil
|
|
27
27
|
import subprocess
|
|
28
|
+
from collections.abc import Callable
|
|
28
29
|
from dataclasses import dataclass
|
|
29
30
|
from pathlib import Path
|
|
31
|
+
from typing import Any
|
|
30
32
|
|
|
31
33
|
_SECTOR_BYTES = 512
|
|
32
34
|
|
|
35
|
+
# GPT partition type GUIDs (lowercase) that name a Linux root filesystem
|
|
36
|
+
# across architectures. sfdisk reports GPT ``type`` as the type GUID.
|
|
37
|
+
_LINUX_ROOT_GUIDS = frozenset(
|
|
38
|
+
{
|
|
39
|
+
"4f68bce3-e8cd-4db1-96e7-fbcaf984b709", # root (x86-64)
|
|
40
|
+
"44479540-f297-41b2-9af7-d131d5f0458a", # root (x86)
|
|
41
|
+
"b921b045-1df0-41c3-af44-4c6f280d3fae", # root (arm64/aarch64)
|
|
42
|
+
"69dad710-2ce4-4e3c-b16c-21a1d49abed3", # root (arm/32-bit)
|
|
43
|
+
"72ec70a6-cf74-40e6-bd49-4bda08e8f224", # root (riscv64)
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
# Generic "Linux filesystem" data GUID (and the MBR type 0x83), used when
|
|
47
|
+
# an image does not tag its root with an arch-specific root GUID.
|
|
48
|
+
_LINUX_FS_TYPES = frozenset({"0fc63daf-8483-4772-8e79-3d69d8477de4", "83"})
|
|
49
|
+
# Partition types that are never the root: firmware / boot-chain / swap.
|
|
50
|
+
# Skipped in the last-resort "largest remaining" tier so a BIOS-boot stub
|
|
51
|
+
# or the ESP is never mistaken for the root.
|
|
52
|
+
_NON_ROOT_TYPES = frozenset(
|
|
53
|
+
{
|
|
54
|
+
"c12a7328-f81f-11d2-ba4b-00a0c93ec93b", # EFI System
|
|
55
|
+
"21686148-6449-6e6f-744e-656564454649", # BIOS boot
|
|
56
|
+
"0657fd6d-a4ab-43c4-84e5-0933c84b4f4f", # Linux swap
|
|
57
|
+
"bc13c2ff-59e6-4262-a352-b275fd6f7172", # XBOOTLDR (/boot)
|
|
58
|
+
"ef", # MBR EFI System
|
|
59
|
+
"82", # MBR Linux swap
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
|
|
33
63
|
|
|
34
64
|
class PartitionNotFound(RuntimeError):
|
|
35
65
|
"""Raised when a requested partition number is not present on
|
|
@@ -47,15 +77,13 @@ class PartitionInfo:
|
|
|
47
77
|
size_bytes: int
|
|
48
78
|
|
|
49
79
|
|
|
50
|
-
def
|
|
51
|
-
"""Return
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
the specified partition is absent, or sfdisk fails to parse.
|
|
80
|
+
def _sfdisk_partitions(blob: Path) -> list[dict[str, Any]]:
|
|
81
|
+
"""Return sfdisk's partition list for ``blob`` (``node``/``start``/
|
|
82
|
+
``size``/``type`` dicts). Raises :class:`PartitionNotFound` on a
|
|
83
|
+
missing blob, a missing / non-JSON sfdisk, or an unpartitioned image.
|
|
55
84
|
"""
|
|
56
85
|
if not blob.is_file():
|
|
57
86
|
raise PartitionNotFound(f"blob {blob!s} does not exist")
|
|
58
|
-
|
|
59
87
|
try:
|
|
60
88
|
result = subprocess.run(
|
|
61
89
|
["sfdisk", "--json", str(blob)],
|
|
@@ -69,14 +97,70 @@ def partition_info(blob: Path, partition_number: int = 1) -> PartitionInfo:
|
|
|
69
97
|
raise PartitionNotFound(
|
|
70
98
|
f"sfdisk --json exited rc={exc.returncode}: {exc.stderr.strip()}"
|
|
71
99
|
) from exc
|
|
72
|
-
|
|
73
100
|
try:
|
|
74
101
|
data = json.loads(result.stdout)
|
|
75
102
|
except json.JSONDecodeError as exc:
|
|
76
103
|
raise PartitionNotFound(f"sfdisk output was not JSON: {exc}") from exc
|
|
77
|
-
|
|
78
104
|
table = data.get("partitiontable") or {}
|
|
79
|
-
|
|
105
|
+
return table.get("partitions") or []
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _partition_number(node: str) -> int | None:
|
|
109
|
+
"""Trailing partition index off an sfdisk ``node`` (``/dev/loop0p3``
|
|
110
|
+
-> 3, ``disk.img2`` -> 2). ``None`` when there is no trailing digit."""
|
|
111
|
+
i = len(node)
|
|
112
|
+
while i > 0 and node[i - 1].isdigit():
|
|
113
|
+
i -= 1
|
|
114
|
+
return int(node[i:]) if i < len(node) else None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def root_partition_number(blob: Path) -> int:
|
|
118
|
+
"""Pick the partition most likely to hold the Linux root filesystem.
|
|
119
|
+
|
|
120
|
+
nosi disk images do NOT agree on partition order: the ubuntu / debian
|
|
121
|
+
cloud images put root at p1 (ESP + BIOS-boot land at p13-15), while
|
|
122
|
+
arch / fedora put BIOS-boot + ESP first and root at p3. Hardcoding p1
|
|
123
|
+
extracts a 1 MiB BIOS-boot stub on the latter, which then fails to
|
|
124
|
+
mount at boot. Select by GPT type instead: an explicit Linux-root
|
|
125
|
+
type GUID wins; else the largest generic-Linux-filesystem partition;
|
|
126
|
+
else the largest partition that is not firmware / boot / swap.
|
|
127
|
+
|
|
128
|
+
Raises :class:`PartitionNotFound` if the table has no viable candidate.
|
|
129
|
+
"""
|
|
130
|
+
parts = _sfdisk_partitions(blob)
|
|
131
|
+
cands = []
|
|
132
|
+
for entry in parts:
|
|
133
|
+
num = _partition_number(str(entry.get("node") or ""))
|
|
134
|
+
size = entry.get("size")
|
|
135
|
+
if num is None or size is None:
|
|
136
|
+
continue
|
|
137
|
+
cands.append((num, str(entry.get("type") or "").strip().lower(), int(size)))
|
|
138
|
+
if not cands:
|
|
139
|
+
raise PartitionNotFound(f"no partitions on {blob!s}")
|
|
140
|
+
|
|
141
|
+
tiers: tuple[Callable[[str], bool], ...] = (
|
|
142
|
+
lambda t: t in _LINUX_ROOT_GUIDS,
|
|
143
|
+
lambda t: t in _LINUX_FS_TYPES,
|
|
144
|
+
lambda t: t not in _NON_ROOT_TYPES,
|
|
145
|
+
)
|
|
146
|
+
for tier in tiers:
|
|
147
|
+
matched = [c for c in cands if tier(c[1])]
|
|
148
|
+
if matched:
|
|
149
|
+
# Largest matching partition wins the tier.
|
|
150
|
+
return max(matched, key=lambda c: c[2])[0]
|
|
151
|
+
|
|
152
|
+
raise PartitionNotFound(
|
|
153
|
+
f"no root-filesystem candidate among {len(cands)} partition(s) on {blob!s}"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def partition_info(blob: Path, partition_number: int = 1) -> PartitionInfo:
|
|
158
|
+
"""Return the byte range of ``partition_number`` on ``blob``.
|
|
159
|
+
|
|
160
|
+
Raises :class:`PartitionNotFound` if the blob is unpartitioned,
|
|
161
|
+
the specified partition is absent, or sfdisk fails to parse.
|
|
162
|
+
"""
|
|
163
|
+
parts = _sfdisk_partitions(blob)
|
|
80
164
|
|
|
81
165
|
# sfdisk names partitions as ``<blob_path><N>`` for MBR / GPT
|
|
82
166
|
# alike; match by the trailing number after the shared prefix.
|
pixie/catalog/_fetcher.py
CHANGED
|
@@ -35,7 +35,11 @@ from pathlib import Path
|
|
|
35
35
|
from typing import Any
|
|
36
36
|
|
|
37
37
|
from pixie import oras
|
|
38
|
-
from pixie._partition import
|
|
38
|
+
from pixie._partition import (
|
|
39
|
+
PartitionNotFound,
|
|
40
|
+
extract_partition,
|
|
41
|
+
root_partition_number,
|
|
42
|
+
)
|
|
39
43
|
from pixie._util import CHUNK, now_iso
|
|
40
44
|
from pixie.catalog._schema import CatalogEntry
|
|
41
45
|
from pixie.catalog._store import CatalogStore
|
|
@@ -445,7 +449,12 @@ def _extract_rootfs_if_partitioned(
|
|
|
445
449
|
blob_path: Path,
|
|
446
450
|
progress: Callable[[dict[str, Any]], None] | None,
|
|
447
451
|
) -> None:
|
|
448
|
-
"""
|
|
452
|
+
"""Extract the Linux root partition into ``blobs/<sha>/rootfs.raw``.
|
|
453
|
+
|
|
454
|
+
The root is selected by GPT type, not by a fixed partition number:
|
|
455
|
+
nosi images disagree on order (ubuntu/debian root=p1, arch/fedora
|
|
456
|
+
root=p3 behind BIOS-boot + ESP), so a hardcoded p1 would extract a
|
|
457
|
+
1 MiB BIOS-boot stub on the latter and fail to mount at boot.
|
|
449
458
|
|
|
450
459
|
Silent on PartitionNotFound: the fetch still succeeds and the
|
|
451
460
|
ephemeral / persist serve paths fall back to using the blob
|
|
@@ -458,7 +467,8 @@ def _extract_rootfs_if_partitioned(
|
|
|
458
467
|
if progress is not None:
|
|
459
468
|
progress({"phase": "extracting-rootfs"})
|
|
460
469
|
try:
|
|
461
|
-
|
|
470
|
+
part_num = root_partition_number(blob_path)
|
|
471
|
+
info = extract_partition(blob_path, rootfs, partition_number=part_num)
|
|
462
472
|
except PartitionNotFound as exc:
|
|
463
473
|
_log.info(
|
|
464
474
|
"fetch %r: skipping rootfs extract (%s); ephemeral / persist will serve the whole blob",
|
|
@@ -467,8 +477,9 @@ def _extract_rootfs_if_partitioned(
|
|
|
467
477
|
)
|
|
468
478
|
return
|
|
469
479
|
_log.info(
|
|
470
|
-
"fetch %r: extracted partition
|
|
480
|
+
"fetch %r: extracted partition %d (%d bytes at offset %d) -> %s",
|
|
471
481
|
entry.name,
|
|
482
|
+
part_num,
|
|
472
483
|
info.size_bytes,
|
|
473
484
|
info.start_bytes,
|
|
474
485
|
rootfs,
|
pixie/catalog/catalog.toml
CHANGED
|
@@ -79,3 +79,27 @@ src = "oras://ghcr.io/safl/nosi/fedora-44-headless-netboot:latest"
|
|
|
79
79
|
format = "tar.gz"
|
|
80
80
|
arch = "x86_64"
|
|
81
81
|
description = "Netboot bundle for fedora-44-headless: vmlinuz + initrd so pixie can nbdboot it over NBD."
|
|
82
|
+
|
|
83
|
+
# pixie-live-env -- pixie's OWN image (not a nosi subset entry): the
|
|
84
|
+
# nosi arch-headless base with the pixie CLI + pixie-on-tty1 service
|
|
85
|
+
# injected. This is what the live-env boot modes (pixie-flash-once /
|
|
86
|
+
# -always / pixie-inventory / pixie-tui) nbdboot ephemerally. Fetch it
|
|
87
|
+
# from the Catalog tab, then set PIXIE_LIVE_ENV_IMAGE_SHA to the
|
|
88
|
+
# content_sha256 pixie assigns after the fetch (the same sha the build
|
|
89
|
+
# pipeline publishes as the pixie-live-env-x86_64.content-sha256 release
|
|
90
|
+
# sidecar). Its netboot_src is arch-headless's own bundle: the transform
|
|
91
|
+
# only touches userspace, so the kernel/initrd are reused unchanged.
|
|
92
|
+
[[images]]
|
|
93
|
+
name = "pixie-live-env"
|
|
94
|
+
src = "https://github.com/safl/pixie/releases/latest/download/pixie-live-env-x86_64.img.gz"
|
|
95
|
+
format = "img.gz"
|
|
96
|
+
arch = "x86_64"
|
|
97
|
+
description = "pixie live-env (nosi arch-headless + the pixie CLI + pixie-on-tty1 service). nbdbooted ephemerally for the live-env boot modes; set PIXIE_LIVE_ENV_IMAGE_SHA to its content_sha256."
|
|
98
|
+
netboot_src = "oras://ghcr.io/safl/nosi/arch-headless-netboot:latest"
|
|
99
|
+
|
|
100
|
+
[[images]]
|
|
101
|
+
name = "nosi arch-headless netboot bundle"
|
|
102
|
+
src = "oras://ghcr.io/safl/nosi/arch-headless-netboot:latest"
|
|
103
|
+
format = "tar.gz"
|
|
104
|
+
arch = "x86_64"
|
|
105
|
+
description = "Netboot bundle for arch-headless: vmlinuz + initrd so pixie can nbdboot the pixie-live-env image over NBD."
|
pixie/machines/_routes.py
CHANGED
|
@@ -20,7 +20,7 @@ from pydantic import BaseModel, Field
|
|
|
20
20
|
from pixie.events._kinds import MACHINE_BINDING_CHANGED, MACHINE_BOUND, MACHINE_DELETED
|
|
21
21
|
from pixie.machines._store import (
|
|
22
22
|
BOOT_MODES,
|
|
23
|
-
|
|
23
|
+
UNCOMMITTED_BOOT_MODES,
|
|
24
24
|
BadMac,
|
|
25
25
|
MachinesStore,
|
|
26
26
|
normalise_mac,
|
|
@@ -165,12 +165,14 @@ def upsert_machine(
|
|
|
165
165
|
details: dict[str, Any] = {"boot_mode": row.boot_mode}
|
|
166
166
|
if row.image_content_sha256:
|
|
167
167
|
details["image_content_sha256"] = row.image_content_sha256
|
|
168
|
-
# ``machine.bound`` on a fresh MAC or on a row that was
|
|
169
|
-
#
|
|
170
|
-
#
|
|
171
|
-
#
|
|
168
|
+
# ``machine.bound`` on a fresh MAC or on a row that was only
|
|
169
|
+
# auto-registered (an uncommitted mode -- ipxe-exit or
|
|
170
|
+
# pixie-inventory -- with no bound image, including a machine
|
|
171
|
+
# that auto-inventoried then flipped back to ipxe-exit);
|
|
172
|
+
# ``machine.binding.changed`` when the mode or image actually
|
|
173
|
+
# shifted from a real prior operator bind.
|
|
172
174
|
was_bound = previous is not None and (
|
|
173
|
-
bool(previous.image_content_sha256) or previous.boot_mode
|
|
175
|
+
bool(previous.image_content_sha256) or previous.boot_mode not in UNCOMMITTED_BOOT_MODES
|
|
174
176
|
)
|
|
175
177
|
changed = previous is not None and (
|
|
176
178
|
previous.boot_mode != row.boot_mode
|
pixie/machines/_store.py
CHANGED
|
@@ -46,7 +46,24 @@ target boots into ``unavailable.j2``. Kept close to :data:`BOOT_MODES`
|
|
|
46
46
|
so a reader adding a mode sees both spots at once."""
|
|
47
47
|
|
|
48
48
|
BOOT_MODES: frozenset[str] = frozenset({"ipxe-exit", "nbdboot"}) | LIVE_ENV_MODES
|
|
49
|
-
|
|
49
|
+
|
|
50
|
+
# A freshly-discovered MAC auto-registers with this mode. Default is
|
|
51
|
+
# ``pixie-inventory``: non-destructive (boots the live env, collects
|
|
52
|
+
# lshw + disks, POSTs them back, reboots to firmware) and immediately
|
|
53
|
+
# useful -- it populates the machine's inventory and unblocks the flash
|
|
54
|
+
# modes, which require it. Inventory is one-shot: on the first inventory
|
|
55
|
+
# POST the binding auto-flips to ``ipxe-exit`` (see pxe/_routes.py) so a
|
|
56
|
+
# PXE-first machine doesn't re-inventory + boot-loop. Overridable per
|
|
57
|
+
# deploy via ``PIXIE_DEFAULT_BOOT_MODE`` (resolved in web/main.py).
|
|
58
|
+
# ``ipxe-exit`` degrades safely when no live env is staged: the renderer
|
|
59
|
+
# falls back to an exit plan, so a bare deploy behaves like the old
|
|
60
|
+
# default until the operator fetches the live env.
|
|
61
|
+
DEFAULT_BOOT_MODE = "pixie-inventory"
|
|
62
|
+
|
|
63
|
+
# The non-committal auto states: a machine sitting in one of these with
|
|
64
|
+
# no bound image has not been meaningfully operator-bound yet. Used to
|
|
65
|
+
# tell a first ``machine.bound`` from a later ``machine.binding.changed``.
|
|
66
|
+
UNCOMMITTED_BOOT_MODES: frozenset[str] = frozenset({"ipxe-exit", "pixie-inventory"})
|
|
50
67
|
|
|
51
68
|
# Presentation metadata for the six boot modes. Ordered
|
|
52
69
|
# passthrough -> diagnostic -> interactive -> destructive so the
|
|
@@ -480,10 +497,18 @@ class MachinesStore:
|
|
|
480
497
|
row = conn.execute("SELECT * FROM machines WHERE mac = ?", (canon,)).fetchone()
|
|
481
498
|
return _row(row)
|
|
482
499
|
|
|
483
|
-
def touch_seen(
|
|
500
|
+
def touch_seen(
|
|
501
|
+
self, mac: str, *, ip: str = "", default_boot_mode: str = DEFAULT_BOOT_MODE
|
|
502
|
+
) -> Machine:
|
|
484
503
|
"""Discovery-side write: create-or-update ``last_seen_at`` +
|
|
485
|
-
optionally ``last_seen_ip``. Does NOT touch operator fields.
|
|
504
|
+
optionally ``last_seen_ip``. Does NOT touch operator fields.
|
|
505
|
+
|
|
506
|
+
A brand-new MAC is auto-registered with ``default_boot_mode``
|
|
507
|
+
(the deploy's ``PIXIE_DEFAULT_BOOT_MODE``, resolved by the
|
|
508
|
+
caller; falls back to :data:`DEFAULT_BOOT_MODE`). Unknown values
|
|
509
|
+
are refused here so a bad env can't seed an unrenderable mode."""
|
|
486
510
|
canon = normalise_mac(mac)
|
|
511
|
+
mode = default_boot_mode if default_boot_mode in BOOT_MODES else DEFAULT_BOOT_MODE
|
|
487
512
|
now = now_iso()
|
|
488
513
|
with _DB_WRITE_LOCK, self._conn() as conn:
|
|
489
514
|
existing = conn.execute("SELECT * FROM machines WHERE mac = ?", (canon,)).fetchone()
|
|
@@ -495,7 +520,7 @@ class MachinesStore:
|
|
|
495
520
|
discovered_at, last_seen_at, last_seen_ip, updated_at
|
|
496
521
|
) VALUES (?, ?, '', ?, ?, ?, ?)
|
|
497
522
|
""",
|
|
498
|
-
(canon,
|
|
523
|
+
(canon, mode, now, now, ip, now),
|
|
499
524
|
)
|
|
500
525
|
else:
|
|
501
526
|
conn.execute(
|
pixie/oras_pull.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""CLI: download a single ``oras://`` artifact blob to a file.
|
|
2
|
+
|
|
3
|
+
Build pipelines -- the ``pixie-live-env`` image bake under ``cijoe/`` --
|
|
4
|
+
need to pull a nosi base disk image the same way the operator UI's
|
|
5
|
+
Fetch does, i.e. through :mod:`pixie.oras`. Those pipelines run under
|
|
6
|
+
cijoe's own interpreter where ``import pixie`` is unavailable, so this
|
|
7
|
+
module exposes the pull as a ``python -m pixie.oras_pull`` entry point.
|
|
8
|
+
A build step then shells out to the repo's uv env::
|
|
9
|
+
|
|
10
|
+
uv run python -m pixie.oras_pull <oras-ref> <out-path>
|
|
11
|
+
|
|
12
|
+
and reuses the exact resolve + streaming download the appliance uses,
|
|
13
|
+
with no ``oras`` CLI dependency.
|
|
14
|
+
|
|
15
|
+
Reuses :func:`pixie.oras.resolve_ref` (anonymous token -> manifest ->
|
|
16
|
+
layer pick) and :func:`pixie.catalog._fetcher._stream_to_tmpfile` (the
|
|
17
|
+
curl Range-resume download) so the downloaded bytes and their digest
|
|
18
|
+
match a live Fetch byte-for-byte. The downloaded blob's sha256 is
|
|
19
|
+
verified against the layer digest the registry resolved to; a mismatch
|
|
20
|
+
is a hard error rather than a silently corrupt base image.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import argparse
|
|
26
|
+
import contextlib
|
|
27
|
+
import sys
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
from pixie.catalog._fetcher import FetchError, _stream_to_tmpfile
|
|
31
|
+
from pixie.oras import OrasError, resolve_ref
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def pull(ref: str, out: Path) -> str:
|
|
35
|
+
"""Resolve ``ref`` and download its blob to ``out`` atomically.
|
|
36
|
+
|
|
37
|
+
Returns the ``sha256:<hex>`` digest the registry resolved the
|
|
38
|
+
reference to (for a tag ref this is frozen at resolve time). Raises
|
|
39
|
+
:class:`pixie.oras.OrasError` on resolution failure,
|
|
40
|
+
:class:`pixie.catalog._fetcher.FetchError` on the download, and
|
|
41
|
+
:class:`ValueError` if the downloaded bytes do not hash to the
|
|
42
|
+
resolved digest.
|
|
43
|
+
"""
|
|
44
|
+
resolved = resolve_ref(ref)
|
|
45
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
# Stage into a scratch dir on the SAME filesystem as ``out`` so the
|
|
47
|
+
# final rename is atomic.
|
|
48
|
+
tmp_dir = out.parent / ".oras-pull-tmp"
|
|
49
|
+
tmp_path, sha256, _size = _stream_to_tmpfile(resolved.blob_url, resolved.headers, tmp_dir)
|
|
50
|
+
|
|
51
|
+
expected = resolved.digest.split(":", 1)[1] if ":" in resolved.digest else resolved.digest
|
|
52
|
+
if sha256 != expected:
|
|
53
|
+
tmp_path.unlink(missing_ok=True)
|
|
54
|
+
raise ValueError(
|
|
55
|
+
f"oras blob digest mismatch for {ref}: registry resolved "
|
|
56
|
+
f"{resolved.digest} but downloaded bytes hash to sha256:{sha256}"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
tmp_path.replace(out)
|
|
60
|
+
# Best-effort scratch-dir cleanup; harmless if other files remain.
|
|
61
|
+
with contextlib.suppress(OSError):
|
|
62
|
+
tmp_dir.rmdir()
|
|
63
|
+
return resolved.digest
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def main(argv: list[str] | None = None) -> int:
|
|
67
|
+
parser = argparse.ArgumentParser(
|
|
68
|
+
prog="python -m pixie.oras_pull",
|
|
69
|
+
description="Download a single oras:// artifact blob to a file.",
|
|
70
|
+
)
|
|
71
|
+
parser.add_argument("ref", help="oras:// reference (tag or @sha256:<digest>)")
|
|
72
|
+
parser.add_argument("out", type=Path, help="output file path")
|
|
73
|
+
args = parser.parse_args(argv)
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
digest = pull(args.ref, args.out)
|
|
77
|
+
except (OrasError, FetchError, ValueError, OSError) as exc:
|
|
78
|
+
print(f"pixie.oras_pull: {exc}", file=sys.stderr)
|
|
79
|
+
return 1
|
|
80
|
+
print(f"pulled {args.ref} -> {args.out} ({digest})")
|
|
81
|
+
return 0
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__": # pragma: no cover
|
|
85
|
+
raise SystemExit(main())
|
pixie/pxe/_renderer.py
CHANGED
|
@@ -90,6 +90,15 @@ class RenderContext:
|
|
|
90
90
|
# Empty by default; the template shims it in unconditionally so
|
|
91
91
|
# no-op is a legal value.
|
|
92
92
|
extra_cmdline: str = ""
|
|
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
|
|
100
|
+
# inventory / interactive / flash correctly).
|
|
101
|
+
live_env_image_sha: str = ""
|
|
93
102
|
|
|
94
103
|
|
|
95
104
|
class PlanRenderer:
|
|
@@ -155,23 +164,7 @@ class PlanRenderer:
|
|
|
155
164
|
if mode == "nbdboot":
|
|
156
165
|
return self._render_nbdboot(machine, ctx, effective_extra)
|
|
157
166
|
if mode in LIVE_ENV_MODES:
|
|
158
|
-
|
|
159
|
-
# netboot-pc bake artifacts have not been staged on
|
|
160
|
-
# this deploy yet; degrade to the readable
|
|
161
|
-
# unavailable plan so a bound target lands on a
|
|
162
|
-
# legible screen instead of a bty-media initrd.
|
|
163
|
-
return self._unavailable(
|
|
164
|
-
machine,
|
|
165
|
-
f"boot_mode={mode!r} needs pixie live-env media; "
|
|
166
|
-
f"stage vmlinuz+initrd+squashfs under $PIXIE_LIVE_ENV_DIR",
|
|
167
|
-
)
|
|
168
|
-
return self._env.get_template("pixie-live-env.j2").render(
|
|
169
|
-
mac=machine.mac,
|
|
170
|
-
boot_mode=mode,
|
|
171
|
-
host=ctx.host,
|
|
172
|
-
port=ctx.port,
|
|
173
|
-
extra_cmdline=effective_extra,
|
|
174
|
-
)
|
|
167
|
+
return self._render_live_env(machine, ctx, mode, effective_extra)
|
|
175
168
|
# Unknown mode: refuse loudly rather than falling through.
|
|
176
169
|
return self._env.get_template("unavailable.j2").render(
|
|
177
170
|
mac=machine.mac,
|
|
@@ -190,46 +183,10 @@ class PlanRenderer:
|
|
|
190
183
|
return self._unavailable(
|
|
191
184
|
machine, "no image bound; set image_content_sha256 to a fetched entry"
|
|
192
185
|
)
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
machine,
|
|
198
|
-
f"no catalog entry with content_sha256={image_sha[:12]}; re-fetch it",
|
|
199
|
-
)
|
|
200
|
-
if not disk_entry.netboot_src:
|
|
201
|
-
return self._unavailable(
|
|
202
|
-
machine,
|
|
203
|
-
f"catalog entry {disk_entry.name!r} has no netboot_src; "
|
|
204
|
-
"advertise a sibling bundle before selecting nbdboot",
|
|
205
|
-
)
|
|
206
|
-
bundle_entry = self._catalog.get_entry_by_src(disk_entry.netboot_src)
|
|
207
|
-
if bundle_entry is None:
|
|
208
|
-
return self._unavailable(
|
|
209
|
-
machine,
|
|
210
|
-
f"netboot_src {disk_entry.netboot_src} has no catalog entry",
|
|
211
|
-
)
|
|
212
|
-
if not bundle_entry.content_sha256:
|
|
213
|
-
return self._unavailable(
|
|
214
|
-
machine,
|
|
215
|
-
f"netboot bundle {bundle_entry.name!r} not fetched yet",
|
|
216
|
-
)
|
|
217
|
-
artifact_dir = self._catalog.artifact_dir(bundle_entry.content_sha256)
|
|
218
|
-
if not (artifact_dir / "manifest.json").is_file():
|
|
219
|
-
return self._unavailable(
|
|
220
|
-
machine,
|
|
221
|
-
f"netboot bundle {bundle_entry.name!r} not unpacked "
|
|
222
|
-
f"(manifest.json missing under artifacts/{bundle_entry.content_sha256[:12]})",
|
|
223
|
-
)
|
|
224
|
-
|
|
225
|
-
# Blob for the disk image must exist too; that's what the NBD
|
|
226
|
-
# export streams over the wire.
|
|
227
|
-
blob = self._catalog.blob_path(image_sha)
|
|
228
|
-
if not blob.is_file():
|
|
229
|
-
return self._unavailable(
|
|
230
|
-
machine,
|
|
231
|
-
f"disk image {disk_entry.name!r} blob missing on disk; re-fetch it",
|
|
232
|
-
)
|
|
186
|
+
resolved = self._resolve_bundle_and_blob(image_sha)
|
|
187
|
+
if isinstance(resolved, str):
|
|
188
|
+
return self._unavailable(machine, resolved)
|
|
189
|
+
bundle_entry, blob = resolved
|
|
233
190
|
|
|
234
191
|
# Persistent overlay path: if the machine's bind carries a
|
|
235
192
|
# non-blank ``overlay_alias``, serve that alias's qcow2 via
|
|
@@ -299,7 +256,64 @@ class PlanRenderer:
|
|
|
299
256
|
)
|
|
300
257
|
|
|
301
258
|
# Ephemeral path: nbdkit's cow filter on the shared blob.
|
|
302
|
-
# Idempotent per name+blob.
|
|
259
|
+
# Idempotent per name+blob. Shared with the configured live-env
|
|
260
|
+
# image path (see :meth:`_render_live_env`).
|
|
261
|
+
return self._render_ephemeral_nbdboot(
|
|
262
|
+
machine, ctx, image_sha, bundle_entry, blob, extra_cmdline
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
def _resolve_bundle_and_blob(self, image_sha: str) -> tuple[CatalogEntry, Path] | str:
|
|
266
|
+
"""Walk ``catalog[image_sha] -> netboot_src -> bundle`` and check
|
|
267
|
+
the bundle is unpacked + the disk-image blob is present on disk.
|
|
268
|
+
|
|
269
|
+
Returns ``(bundle_entry, blob_path)`` on success, or an
|
|
270
|
+
operator-readable reason string on the first missing link.
|
|
271
|
+
Shared by the machine-bound ``nbdboot`` mode and the configured
|
|
272
|
+
live-env image path so BOTH degrade to the same
|
|
273
|
+
``unavailable`` reasons -- the live-env nbdboot is deliberately
|
|
274
|
+
the identical resolution the ``nbdboot`` mode uses, just against
|
|
275
|
+
a deploy-configured sha instead of the machine's bound one."""
|
|
276
|
+
disk_entry = self._catalog_entry_by_sha(image_sha)
|
|
277
|
+
if disk_entry is None:
|
|
278
|
+
return f"no catalog entry with content_sha256={image_sha[:12]}; re-fetch it"
|
|
279
|
+
if not disk_entry.netboot_src:
|
|
280
|
+
return (
|
|
281
|
+
f"catalog entry {disk_entry.name!r} has no netboot_src; "
|
|
282
|
+
"advertise a sibling bundle before selecting nbdboot"
|
|
283
|
+
)
|
|
284
|
+
bundle_entry = self._catalog.get_entry_by_src(disk_entry.netboot_src)
|
|
285
|
+
if bundle_entry is None:
|
|
286
|
+
return f"netboot_src {disk_entry.netboot_src} has no catalog entry"
|
|
287
|
+
if not bundle_entry.content_sha256:
|
|
288
|
+
return f"netboot bundle {bundle_entry.name!r} not fetched yet"
|
|
289
|
+
artifact_dir = self._catalog.artifact_dir(bundle_entry.content_sha256)
|
|
290
|
+
if not (artifact_dir / "manifest.json").is_file():
|
|
291
|
+
return (
|
|
292
|
+
f"netboot bundle {bundle_entry.name!r} not unpacked "
|
|
293
|
+
f"(manifest.json missing under artifacts/{bundle_entry.content_sha256[:12]})"
|
|
294
|
+
)
|
|
295
|
+
# Blob for the disk image must exist too; that's what the NBD
|
|
296
|
+
# export streams over the wire.
|
|
297
|
+
blob = self._catalog.blob_path(image_sha)
|
|
298
|
+
if not blob.is_file():
|
|
299
|
+
return f"disk image {disk_entry.name!r} blob missing on disk; re-fetch it"
|
|
300
|
+
return (bundle_entry, blob)
|
|
301
|
+
|
|
302
|
+
def _render_ephemeral_nbdboot(
|
|
303
|
+
self,
|
|
304
|
+
machine: Machine,
|
|
305
|
+
ctx: RenderContext,
|
|
306
|
+
image_sha: str,
|
|
307
|
+
bundle_entry: CatalogEntry,
|
|
308
|
+
blob: Path,
|
|
309
|
+
extra_cmdline: str,
|
|
310
|
+
) -> str:
|
|
311
|
+
"""Ensure an ephemeral nbdkit export for ``blob`` (idempotent per
|
|
312
|
+
name+blob) and render ``nbdboot.j2`` with ``persist=False`` -- an
|
|
313
|
+
overlay-on-tmpfs root, nothing persists across reboots. The
|
|
314
|
+
cmdline keeps ``pixie.server`` / ``pixie.mac`` (the template
|
|
315
|
+
always emits them) so an nbdboot'd live-env image's on-target
|
|
316
|
+
pixie CLI phones home to ``GET /pxe/<mac>/plan``."""
|
|
303
317
|
export_name = _export_name_for(image_sha)
|
|
304
318
|
try:
|
|
305
319
|
port = self._ensure_export(export_name, image_sha, blob)
|
|
@@ -307,7 +321,6 @@ class PlanRenderer:
|
|
|
307
321
|
return self._unavailable(
|
|
308
322
|
machine, f"nbdkit refused to start for export {export_name!r}: {exc}"
|
|
309
323
|
)
|
|
310
|
-
|
|
311
324
|
return self._env.get_template("nbdboot.j2").render(
|
|
312
325
|
mac=machine.mac,
|
|
313
326
|
host=ctx.host,
|
|
@@ -321,6 +334,61 @@ class PlanRenderer:
|
|
|
321
334
|
persist=False,
|
|
322
335
|
)
|
|
323
336
|
|
|
337
|
+
# ---------- live-env resolution --------------------------------
|
|
338
|
+
|
|
339
|
+
def _render_live_env(
|
|
340
|
+
self, machine: Machine, ctx: RenderContext, mode: str, extra_cmdline: str
|
|
341
|
+
) -> str:
|
|
342
|
+
"""Render a plan for one of the ``LIVE_ENV_MODES``.
|
|
343
|
+
|
|
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
|
|
360
|
+
``GET /pxe/<mac>/plan`` returns, so the on-target pixie CLI
|
|
361
|
+
dispatches inventory / interactive / flash exactly as before."""
|
|
362
|
+
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
|
|
373
|
+
)
|
|
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.
|
|
379
|
+
return self._unavailable(
|
|
380
|
+
machine,
|
|
381
|
+
f"boot_mode={mode!r} needs pixie live-env media; "
|
|
382
|
+
f"stage vmlinuz+initrd+squashfs under $PIXIE_LIVE_ENV_DIR",
|
|
383
|
+
)
|
|
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,
|
|
390
|
+
)
|
|
391
|
+
|
|
324
392
|
def _unavailable(self, machine: Machine, reason: str) -> str:
|
|
325
393
|
_log.info("pxe %s unavailable: %s", machine.mac, reason)
|
|
326
394
|
slug = reason.split(";", 1)[0].replace(" ", "-").lower()[:60]
|
pixie/pxe/_routes.py
CHANGED
|
@@ -21,13 +21,14 @@ from fastapi import APIRouter, HTTPException, Request
|
|
|
21
21
|
from fastapi.responses import PlainTextResponse
|
|
22
22
|
|
|
23
23
|
from pixie.events._kinds import (
|
|
24
|
+
MACHINE_BINDING_CHANGED,
|
|
24
25
|
MACHINE_DISCOVERED,
|
|
25
26
|
MACHINE_INVENTORY_UPDATED,
|
|
26
27
|
PXE_PLAN_RENDERED,
|
|
27
28
|
PXE_PLAN_UNAVAILABLE,
|
|
28
29
|
PXE_STATUS_RECEIVED,
|
|
29
30
|
)
|
|
30
|
-
from pixie.machines._store import BadMac, MachinesStore, normalise_mac
|
|
31
|
+
from pixie.machines._store import DEFAULT_BOOT_MODE, BadMac, MachinesStore, normalise_mac
|
|
31
32
|
from pixie.pxe._renderer import PlanRenderer, RenderContext
|
|
32
33
|
|
|
33
34
|
_log = logging.getLogger(__name__)
|
|
@@ -79,10 +80,23 @@ def _render_context(request: Request) -> RenderContext:
|
|
|
79
80
|
# (envvars-style pin) -> empty. Known-good values per hardware are
|
|
80
81
|
# documented in docs/src/hardware-quirks.md.
|
|
81
82
|
extra_cmdline = ""
|
|
83
|
+
# Disk-image content sha the live-env boot modes nbdboot ephemerally
|
|
84
|
+
# instead of fetching the Debian live-boot squashfs. Same resolution
|
|
85
|
+
# shape: /ui/live-env override (state.db) -> $PIXIE_LIVE_ENV_IMAGE_SHA
|
|
86
|
+
# -> empty (empty keeps the squashfs path). Proven on hardware; see
|
|
87
|
+
# the renderer's _render_live_env.
|
|
88
|
+
live_env_image_sha = ""
|
|
82
89
|
settings = getattr(request.app.state, "settings_store", None)
|
|
83
90
|
if settings is not None:
|
|
84
91
|
extra_cmdline = settings.resolve_live_env_extra_cmdline()
|
|
85
|
-
|
|
92
|
+
live_env_image_sha = settings.resolve_live_env_image_sha()
|
|
93
|
+
return RenderContext(
|
|
94
|
+
host=host,
|
|
95
|
+
port=port,
|
|
96
|
+
nbd_host=nbd_host,
|
|
97
|
+
extra_cmdline=extra_cmdline,
|
|
98
|
+
live_env_image_sha=live_env_image_sha,
|
|
99
|
+
)
|
|
86
100
|
|
|
87
101
|
|
|
88
102
|
@router.get("/pxe-bootstrap.ipxe", response_class=PlainTextResponse)
|
|
@@ -132,6 +146,33 @@ async def pxe_inventory(request: Request, mac: str) -> PlainTextResponse:
|
|
|
132
146
|
summary=f"{canon} posted inventory",
|
|
133
147
|
details=details,
|
|
134
148
|
)
|
|
149
|
+
|
|
150
|
+
# Inventory is one-shot: once a machine has reported, flip a
|
|
151
|
+
# pixie-inventory binding to ipxe-exit so a PXE-first machine does
|
|
152
|
+
# not re-inventory and boot-loop. Mirrors pixie-flash-once's
|
|
153
|
+
# self-terminating flip (see pxe_status). Preserve the row's other
|
|
154
|
+
# fields; the collected inventory stays. Rebinding to
|
|
155
|
+
# pixie-inventory buys exactly one more pass.
|
|
156
|
+
row = machines.get(canon)
|
|
157
|
+
if row is not None and row.boot_mode == "pixie-inventory":
|
|
158
|
+
with contextlib.suppress(ValueError):
|
|
159
|
+
machines.upsert_binding(
|
|
160
|
+
canon,
|
|
161
|
+
boot_mode="ipxe-exit",
|
|
162
|
+
image_content_sha256=row.image_content_sha256,
|
|
163
|
+
labels=list(row.labels),
|
|
164
|
+
target_disk_serial=row.target_disk_serial,
|
|
165
|
+
extra_cmdline=row.extra_cmdline,
|
|
166
|
+
overlay_alias=row.overlay_alias,
|
|
167
|
+
)
|
|
168
|
+
if log is not None:
|
|
169
|
+
log.emit(
|
|
170
|
+
MACHINE_BINDING_CHANGED,
|
|
171
|
+
subject_kind="machine",
|
|
172
|
+
subject_id=canon,
|
|
173
|
+
summary=f"{canon}: inventory collected, auto-exit (pixie-inventory is one-shot)",
|
|
174
|
+
details={"boot_mode": "ipxe-exit", "reason": "inventory-once"},
|
|
175
|
+
)
|
|
135
176
|
return PlainTextResponse("", status_code=204)
|
|
136
177
|
|
|
137
178
|
|
|
@@ -168,7 +209,8 @@ def pxe_plan(request: Request, mac: str) -> PlainTextResponse:
|
|
|
168
209
|
# event fires exactly once (touch_seen itself can't emit -- stores
|
|
169
210
|
# hold no EventsLog by design).
|
|
170
211
|
is_new = machines.get(canon) is None
|
|
171
|
-
|
|
212
|
+
default_mode = getattr(request.app.state, "default_boot_mode", DEFAULT_BOOT_MODE)
|
|
213
|
+
row = machines.touch_seen(canon, ip=_client_ip(request), default_boot_mode=default_mode)
|
|
172
214
|
|
|
173
215
|
ctx = _render_context(request)
|
|
174
216
|
body = _get_renderer(request).render(row, ctx)
|
pixie/web/_settings_store.py
CHANGED
|
@@ -18,6 +18,7 @@ from __future__ import annotations
|
|
|
18
18
|
|
|
19
19
|
import contextlib
|
|
20
20
|
import os
|
|
21
|
+
import re
|
|
21
22
|
import sqlite3
|
|
22
23
|
from collections.abc import Generator
|
|
23
24
|
from datetime import UTC, datetime
|
|
@@ -66,6 +67,13 @@ DEFAULT_DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
|
|
|
66
67
|
KEY_LIVE_ENV_EXTRA_CMDLINE = "live_env.extra_cmdline"
|
|
67
68
|
ENV_LIVE_ENV_EXTRA_CMDLINE = "PIXIE_LIVE_ENV_EXTRA_CMDLINE"
|
|
68
69
|
|
|
70
|
+
# A value that is nothing but an unexpanded shell placeholder --
|
|
71
|
+
# ``${VAR}`` / ``${VAR:-default}`` / ``${VAR:+x}`` -- which is what a
|
|
72
|
+
# compose runner that doesn't expand ``${VAR:-}`` (podman-compose)
|
|
73
|
+
# leaks into the container env. Such a value is treated as "unset" so
|
|
74
|
+
# it never reaches a kernel cmdline.
|
|
75
|
+
_UNEXPANDED_ENV_PLACEHOLDER = re.compile(r"^\$\{[A-Za-z_][A-Za-z0-9_]*(:[-+?=]?[^}]*)?\}$")
|
|
76
|
+
|
|
69
77
|
# Where the operator "Fetch live-env" action pulls the netboot-pc bake
|
|
70
78
|
# from: a single tarball ``src`` (``https://`` or ``oras://``, the same
|
|
71
79
|
# schemes the catalog fetch speaks) that unpacks to vmlinuz + initrd +
|
|
@@ -78,6 +86,23 @@ DEFAULT_LIVE_ENV_SRC = (
|
|
|
78
86
|
"https://github.com/safl/pixie/releases/latest/download/pixie-live-env-x86_64.tar.gz"
|
|
79
87
|
)
|
|
80
88
|
|
|
89
|
+
# Disk-image content sha the live-env boot modes
|
|
90
|
+
# (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.
|
|
100
|
+
KEY_LIVE_ENV_IMAGE_SHA = "live_env.image_sha"
|
|
101
|
+
ENV_LIVE_ENV_IMAGE_SHA = "PIXIE_LIVE_ENV_IMAGE_SHA"
|
|
102
|
+
|
|
103
|
+
# 64 lowercase hex chars: a catalog entry's ``content_sha256``.
|
|
104
|
+
_SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
|
|
105
|
+
|
|
81
106
|
|
|
82
107
|
class SettingValueError(ValueError):
|
|
83
108
|
"""A stored override failed validation at resolve time. The
|
|
@@ -175,12 +200,21 @@ class SettingsStore:
|
|
|
175
200
|
silently.
|
|
176
201
|
|
|
177
202
|
Empty when both DB override + env are unset -- the template
|
|
178
|
-
expects a string and skips the append when it's empty.
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
203
|
+
expects a string and skips the append when it's empty.
|
|
204
|
+
|
|
205
|
+
Defends against a compose runner that fails to expand
|
|
206
|
+
``${VAR:-}``: podman-compose passes the unset default through
|
|
207
|
+
LITERALLY, so the container env holds the string
|
|
208
|
+
``${PIXIE_LIVE_ENV_EXTRA_CMDLINE:-}``. A value that is nothing
|
|
209
|
+
but an unexpanded ``${...}`` placeholder is treated as empty so
|
|
210
|
+
it can't leak onto every live-env / nbdboot kernel cmdline."""
|
|
211
|
+
override = self.get(KEY_LIVE_ENV_EXTRA_CMDLINE)
|
|
212
|
+
if override:
|
|
213
|
+
return override
|
|
214
|
+
env = (os.environ.get(ENV_LIVE_ENV_EXTRA_CMDLINE) or "").strip()
|
|
215
|
+
if env and not _UNEXPANDED_ENV_PLACEHOLDER.match(env):
|
|
216
|
+
return env
|
|
217
|
+
return ""
|
|
184
218
|
|
|
185
219
|
def resolve_live_env_src(self) -> str:
|
|
186
220
|
"""Effective live-env fetch src: DB override -> env -> default
|
|
@@ -193,6 +227,29 @@ class SettingsStore:
|
|
|
193
227
|
or DEFAULT_LIVE_ENV_SRC
|
|
194
228
|
)
|
|
195
229
|
|
|
230
|
+
def resolve_live_env_image_sha(self) -> str:
|
|
231
|
+
"""Effective live-env disk-image content sha: DB override -> env
|
|
232
|
+
-> empty.
|
|
233
|
+
|
|
234
|
+
When non-empty (and the named image + its netboot bundle are
|
|
235
|
+
fetched) the live-env boot modes nbdboot this image ephemerally
|
|
236
|
+
instead of fetching the Debian live-boot squashfs. Empty is the
|
|
237
|
+
default and keeps the squashfs path, so an existing deploy that
|
|
238
|
+
never sets this is untouched.
|
|
239
|
+
|
|
240
|
+
A value that is not 64 lowercase hex chars is treated as unset:
|
|
241
|
+
the renderer then falls back to the squashfs path rather than
|
|
242
|
+
degrading every live-env boot to the unavailable plan. The
|
|
243
|
+
Settings / Live env form validates the shape at set time, so a
|
|
244
|
+
bad value can only reach here from a stray env-var."""
|
|
245
|
+
override = self.get(KEY_LIVE_ENV_IMAGE_SHA)
|
|
246
|
+
if override and _SHA256_RE.match(override.strip()):
|
|
247
|
+
return override.strip()
|
|
248
|
+
env = (os.environ.get(ENV_LIVE_ENV_IMAGE_SHA) or "").strip()
|
|
249
|
+
if env and _SHA256_RE.match(env):
|
|
250
|
+
return env
|
|
251
|
+
return ""
|
|
252
|
+
|
|
196
253
|
|
|
197
254
|
def parse_iso_utc(raw: str) -> datetime | None:
|
|
198
255
|
"""Parse the ISO-8601 strings pixie writes to state.db. Accepts a
|
|
@@ -158,6 +158,42 @@
|
|
|
158
158
|
</span>
|
|
159
159
|
</div>
|
|
160
160
|
</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/<mac></code>; a target already mid-boot must be power-cycled.
|
|
194
|
+
</span>
|
|
195
|
+
</div>
|
|
196
|
+
</form>
|
|
161
197
|
</div>
|
|
162
198
|
</div>
|
|
163
199
|
|
pixie/web/main.py
CHANGED
|
@@ -66,7 +66,12 @@ from pixie.exports._routes import router as exports_router
|
|
|
66
66
|
from pixie.exports._store import ExportsStore, OverlaysStore
|
|
67
67
|
from pixie.exports._supervisor import DEFAULT_PORT_BASE, NbdServer
|
|
68
68
|
from pixie.machines._routes import router as machines_router
|
|
69
|
-
from pixie.machines._store import
|
|
69
|
+
from pixie.machines._store import (
|
|
70
|
+
BOOT_MODES,
|
|
71
|
+
DEFAULT_BOOT_MODE,
|
|
72
|
+
IMAGE_BOOT_MODES,
|
|
73
|
+
MachinesStore,
|
|
74
|
+
)
|
|
70
75
|
from pixie.pxe._renderer import PlanRenderer
|
|
71
76
|
from pixie.pxe._routes import router as pxe_router
|
|
72
77
|
from pixie.tftp import DEFAULT_TFTP_ROOT, TftpServer
|
|
@@ -82,6 +87,7 @@ from pixie.web._settings_store import (
|
|
|
82
87
|
KEY_DATETIME_FORMAT,
|
|
83
88
|
KEY_DISPLAY_TZ,
|
|
84
89
|
KEY_LIVE_ENV_EXTRA_CMDLINE,
|
|
90
|
+
KEY_LIVE_ENV_IMAGE_SHA,
|
|
85
91
|
KEY_LIVE_ENV_SRC,
|
|
86
92
|
SettingsStore,
|
|
87
93
|
SettingValueError,
|
|
@@ -183,6 +189,18 @@ def _resolve_live_env_dir() -> Path | None:
|
|
|
183
189
|
return default
|
|
184
190
|
|
|
185
191
|
|
|
192
|
+
def _resolve_default_boot_mode() -> str:
|
|
193
|
+
"""Boot mode a freshly-discovered MAC auto-registers with.
|
|
194
|
+
|
|
195
|
+
``PIXIE_DEFAULT_BOOT_MODE`` override -> :data:`DEFAULT_BOOT_MODE`
|
|
196
|
+
(``pixie-inventory``). An unknown value falls back to the default
|
|
197
|
+
rather than seeding an unrenderable mode on every new machine."""
|
|
198
|
+
override = (os.environ.get("PIXIE_DEFAULT_BOOT_MODE") or "").strip()
|
|
199
|
+
if override in BOOT_MODES:
|
|
200
|
+
return override
|
|
201
|
+
return DEFAULT_BOOT_MODE
|
|
202
|
+
|
|
203
|
+
|
|
186
204
|
def _resolve_state_dir() -> Path:
|
|
187
205
|
"""Where pixie writes state.db + blobs/ + artifacts/.
|
|
188
206
|
|
|
@@ -405,6 +423,18 @@ def _deployment_envvar_docs() -> list[dict[str, str]]:
|
|
|
405
423
|
" the Live env page for the live-editable override."
|
|
406
424
|
),
|
|
407
425
|
},
|
|
426
|
+
{
|
|
427
|
+
"name": "PIXIE_DEFAULT_BOOT_MODE",
|
|
428
|
+
"default": "pixie-inventory",
|
|
429
|
+
"purpose": (
|
|
430
|
+
"Boot mode a freshly-discovered MAC auto-registers with."
|
|
431
|
+
" Default 'pixie-inventory' is non-destructive (collects"
|
|
432
|
+
" lshw + disks, posts them, exits) and one-shot: the first"
|
|
433
|
+
" inventory POST flips it to 'ipxe-exit' so a PXE-first"
|
|
434
|
+
" box does not re-inventory. Set 'ipxe-exit' for the old"
|
|
435
|
+
" exit-by-default behaviour."
|
|
436
|
+
),
|
|
437
|
+
},
|
|
408
438
|
{
|
|
409
439
|
"name": "PIXIE_LIVE_ENV_SRC",
|
|
410
440
|
"default": (
|
|
@@ -419,6 +449,20 @@ def _deployment_envvar_docs() -> list[dict[str, str]]:
|
|
|
419
449
|
" Live env page."
|
|
420
450
|
),
|
|
421
451
|
},
|
|
452
|
+
{
|
|
453
|
+
"name": "PIXIE_LIVE_ENV_IMAGE_SHA",
|
|
454
|
+
"default": "",
|
|
455
|
+
"purpose": (
|
|
456
|
+
"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."
|
|
464
|
+
),
|
|
465
|
+
},
|
|
422
466
|
{
|
|
423
467
|
"name": "PIXIE_SEED_CATALOG",
|
|
424
468
|
"default": "1",
|
|
@@ -726,6 +770,7 @@ def create_app() -> FastAPI:
|
|
|
726
770
|
nbdkit_bin=_resolve_nbdkit_bin(),
|
|
727
771
|
)
|
|
728
772
|
app.state.live_env_dir = _resolve_live_env_dir()
|
|
773
|
+
app.state.default_boot_mode = _resolve_default_boot_mode()
|
|
729
774
|
# Root for per-machine qcow2 overlay files. Sub-directory under
|
|
730
775
|
# the state dir so a single ``PIXIE_DATA_DIR`` override still
|
|
731
776
|
# relocates everything (catalog blobs + overlays + state.db)
|
|
@@ -2520,6 +2565,13 @@ def create_app() -> FastAPI:
|
|
|
2520
2565
|
"env": "PIXIE_LIVE_ENV_EXTRA_CMDLINE",
|
|
2521
2566
|
"updated_at": store.updated_at(KEY_LIVE_ENV_EXTRA_CMDLINE) or "",
|
|
2522
2567
|
},
|
|
2568
|
+
"live_env_image_sha": {
|
|
2569
|
+
"override": store.get(KEY_LIVE_ENV_IMAGE_SHA) or "",
|
|
2570
|
+
"effective": store.resolve_live_env_image_sha(),
|
|
2571
|
+
"default": "",
|
|
2572
|
+
"env": "PIXIE_LIVE_ENV_IMAGE_SHA",
|
|
2573
|
+
"updated_at": store.updated_at(KEY_LIVE_ENV_IMAGE_SHA) or "",
|
|
2574
|
+
},
|
|
2523
2575
|
"flash_error": flash_error,
|
|
2524
2576
|
}
|
|
2525
2577
|
|
|
@@ -2580,6 +2632,43 @@ def create_app() -> FastAPI:
|
|
|
2580
2632
|
store.clear(KEY_LIVE_ENV_SRC)
|
|
2581
2633
|
return RedirectResponse(url="/ui/live-env", status_code=status.HTTP_303_SEE_OTHER)
|
|
2582
2634
|
|
|
2635
|
+
@app.post("/ui/live-env/image/edit", response_model=None)
|
|
2636
|
+
def ui_live_env_image_edit(
|
|
2637
|
+
request: Request,
|
|
2638
|
+
live_env_image_sha: str = Form(""),
|
|
2639
|
+
_auth: None = Depends(_require_ui_auth),
|
|
2640
|
+
) -> HTMLResponse | RedirectResponse:
|
|
2641
|
+
"""Persist the live-env disk-image content sha. When set (and the
|
|
2642
|
+
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."""
|
|
2649
|
+
import re as _re
|
|
2650
|
+
|
|
2651
|
+
store: SettingsStore = request.app.state.settings_store
|
|
2652
|
+
raw = (live_env_image_sha or "").strip().lower()
|
|
2653
|
+
if raw and not _re.match(r"^[0-9a-f]{64}$", raw):
|
|
2654
|
+
return templates.TemplateResponse(
|
|
2655
|
+
request,
|
|
2656
|
+
"live_env.html",
|
|
2657
|
+
_live_env_context(
|
|
2658
|
+
request,
|
|
2659
|
+
flash_error=(
|
|
2660
|
+
"Live-env image sha must be 64 hex chars "
|
|
2661
|
+
"(a fetched disk-image's content_sha256)."
|
|
2662
|
+
),
|
|
2663
|
+
),
|
|
2664
|
+
status_code=400,
|
|
2665
|
+
)
|
|
2666
|
+
if raw:
|
|
2667
|
+
store.set_value(KEY_LIVE_ENV_IMAGE_SHA, raw)
|
|
2668
|
+
else:
|
|
2669
|
+
store.clear(KEY_LIVE_ENV_IMAGE_SHA)
|
|
2670
|
+
return RedirectResponse(url="/ui/live-env", status_code=status.HTTP_303_SEE_OTHER)
|
|
2671
|
+
|
|
2583
2672
|
@app.post("/ui/live-env/fetch")
|
|
2584
2673
|
def ui_live_env_fetch(
|
|
2585
2674
|
request: Request,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pixie-lab
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
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
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
pixie/__init__.py,sha256=XhfE52sqfEOUpzoYuuO8GpRNTCjigL28LBiNKVrHf2A,615
|
|
2
|
-
pixie/_partition.py,sha256=
|
|
2
|
+
pixie/_partition.py,sha256=xLQdXGACAAEmt4e1w1SuyhAy-5jZ3hm8oEpCBXw_0Ag,9461
|
|
3
3
|
pixie/_util.py,sha256=dmaialB1eGUJd85oynoJTEAIVb1o2X8XdV_cNbrf6eU,554
|
|
4
4
|
pixie/disks.py,sha256=RTATvHaBEpDJzC5y1YiLe1P1GzkHfTDRRMUZDbCdPio,3388
|
|
5
5
|
pixie/flash.py,sha256=X1gnwq2mhzo8CmrZQgcA5RowFjHKLbRPkHUc8976bVc,78373
|
|
6
6
|
pixie/images.py,sha256=zteGZkPeMdwCiA5oUXYl2mhqjAITi30HuWRvkY1z6BQ,17450
|
|
7
7
|
pixie/oras.py,sha256=zKKqYcSghgguOIIrGcJ6C3hZusmORZmDfv7zZDDFslM,22509
|
|
8
|
+
pixie/oras_pull.py,sha256=Xyqga4o8vtCZrUSv5YgLggqmXs36y6FXd-_E9a_iRVo,3326
|
|
8
9
|
pixie/tui_catalog.py,sha256=nKiVd-SC_qfS3m6phQXRnqQdKN_nTqZbQFl2cIscIoQ,26305
|
|
9
10
|
pixie/catalog/__init__.py,sha256=DWBJOL0goBieMLWFO2RXTc_w6e0OQ3f6qQVn-oiTq3c,2309
|
|
10
|
-
pixie/catalog/_fetcher.py,sha256=
|
|
11
|
+
pixie/catalog/_fetcher.py,sha256=xiMLccBoJsjNumy1DT38aJiqM4-wYSzHGtklRKnKWlE,26698
|
|
11
12
|
pixie/catalog/_live_env.py,sha256=DWbPecbipSZ4KQAx60yi4q_gFncOZt4f1yj-QZ7ZBV0,5068
|
|
12
13
|
pixie/catalog/_routes.py,sha256=8s_8bEDYL5zwg6gNAtxR7NOGPLY7ho9spqjPU83RIPE,14729
|
|
13
14
|
pixie/catalog/_schema.py,sha256=KC6RcKlToX1DgcRjn25djKMx78-eS-4WzNRY267h0I0,9769
|
|
14
15
|
pixie/catalog/_store.py,sha256=S7OM7A05Y5ES4RuhmwqQupDGNWXF--p_UMfUNOgQHsQ,9044
|
|
15
|
-
pixie/catalog/catalog.toml,sha256=
|
|
16
|
+
pixie/catalog/catalog.toml,sha256=nL97qrh0IkfTFmaSMMzWPBi465V9_R6aFcSjzKHKi14,4883
|
|
16
17
|
pixie/deploy/__init__.py,sha256=3PvSvrIRdqxLCz0nTdVSBiPzEtDl359XSUBgyMBkMT8,1019
|
|
17
18
|
pixie/deploy/_main.py,sha256=Or2espz9oWNmnGfyEzIsEyvRwWEHL4uxyE6U9OdMZJs,15403
|
|
18
19
|
pixie/deploy/_templates.py,sha256=WkkKRDU00FDWVZM0XeKR-BCtSBPvaZ2QHM1TBeq0RYg,5418
|
|
@@ -25,13 +26,13 @@ pixie/exports/_routes.py,sha256=y6exOqYA4KtN3mHbYJWqHTOaQHk_OaaUWosotAX6NgM,7569
|
|
|
25
26
|
pixie/exports/_store.py,sha256=MCRwzsbCD2VdFGLHGyshCqnmD0Axm9GyQIZBOgK2FxE,13829
|
|
26
27
|
pixie/exports/_supervisor.py,sha256=jOR8S1f8btmvngEQNVGZQ2-pcxmn_xU1rlviCjMUrW4,14623
|
|
27
28
|
pixie/machines/__init__.py,sha256=iSyEkYm6_oP5TNRVks-HrudMjX2Wr7v6t3RSOyl3eps,725
|
|
28
|
-
pixie/machines/_routes.py,sha256=
|
|
29
|
-
pixie/machines/_store.py,sha256=
|
|
29
|
+
pixie/machines/_routes.py,sha256=I5o_pLYIUya-N7Z23YRZZUEY9-_PMJJbHtZFTWm8SDM,8731
|
|
30
|
+
pixie/machines/_store.py,sha256=zh89zj2s5CurMFO258i10ANW20VCTQk0kT8MgRhnWi0,30379
|
|
30
31
|
pixie/pivot/__init__.py,sha256=k_yIUOrIu-DKCJ_8XmRtnoVV7Rr9URagHU2Y76HB2ew,6195
|
|
31
32
|
pixie/pivot/nbdboot,sha256=stBWGXnSXMFJMMzmtip77dPwWxQLfOewXo9YOgw5u0I,19558
|
|
32
33
|
pixie/pxe/__init__.py,sha256=AKBRKQYdlH7DdNBvES9EwYTzPOkeNbWBN1S7IH1UEyw,784
|
|
33
|
-
pixie/pxe/_renderer.py,sha256=
|
|
34
|
-
pixie/pxe/_routes.py,sha256=
|
|
34
|
+
pixie/pxe/_renderer.py,sha256=Tg2oMZQ_vSkEmsc2Rn_SXZ6Ed14LXUmZTOgzkLQC56g,18833
|
|
35
|
+
pixie/pxe/_routes.py,sha256=VhWH5VXaaAC39Bt3asZPpp9ckTCOJdJx3DOLFVTn-2M,19169
|
|
35
36
|
pixie/tftp/__init__.py,sha256=FuHn1KF5_NoGOM_dHXSbubX5lJr0tuMugJGFxxwuUws,774
|
|
36
37
|
pixie/tftp/_supervisor.py,sha256=khpjgWqdMzdnhRaPYM4j25PoKijbBnvr-GjJLybP_rY,4659
|
|
37
38
|
pixie/tui/__init__.py,sha256=EgULtZPOjzs66FosDeSGLFqacsSkal_xBmpBy0fIeZ8,8563
|
|
@@ -44,9 +45,9 @@ pixie/web/_images.py,sha256=RhtTmzRhapr6zRbQN4SKaW_o3dhUhf6hyZGIlbxMh_k,6993
|
|
|
44
45
|
pixie/web/_inventory.py,sha256=rJDNAfejmtlNPCPUr2mhLey19C5Tu_dmpia9KVeyT7E,10189
|
|
45
46
|
pixie/web/_overlay_bind.py,sha256=7aKYqMaujxt-XRa1jB6mVEkbt-w_koXjLvUzBTzy2o8,4125
|
|
46
47
|
pixie/web/_overlays.py,sha256=5yZp6pDiFj_WKdLx6vCoEKD8mXG3KjkMOQt4OGYYdyc,8571
|
|
47
|
-
pixie/web/_settings_store.py,sha256=
|
|
48
|
+
pixie/web/_settings_store.py,sha256=TrhjRN2ME1K3VsgJPJif7ZA3Tf7pM9k9oL57Hhh_YH0,12409
|
|
48
49
|
pixie/web/_table_state.py,sha256=zGH5oZ-XlMqUFQqmYglYqT_fu1Yrtaw-g5yhPtAKAEc,9524
|
|
49
|
-
pixie/web/main.py,sha256=
|
|
50
|
+
pixie/web/main.py,sha256=1eu183QjBjcN5yKuFn0TGD5rNaDuF6U4TPED29EsSJ0,117004
|
|
50
51
|
pixie/web/_static/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
51
52
|
pixie/web/_static/bootstrap-icons.min.css,sha256=pdY4ejLKO67E0CM2tbPtq1DJ3VGDVVdqAR6j3ZwdiE4,87008
|
|
52
53
|
pixie/web/_static/bootstrap.min.css,sha256=NXgoHLHuUWz0lbqh8PRwCIl7_QhZE74v-FCwrYdCW1E,234717
|
|
@@ -62,7 +63,7 @@ pixie/web/_templates/events.html,sha256=ifawe--m5FMcicKZ2daCCycV7rGgpLJz7A309UrT
|
|
|
62
63
|
pixie/web/_templates/image_detail.html,sha256=--DjHCDgvripwr-9_lmnmaLCe4Iw2W_gXIGuKLMvcJw,6926
|
|
63
64
|
pixie/web/_templates/images.html,sha256=PO8r0hZ6tXMqHZo_m1t_wgh4NEZP6u_72owH_Jbz2Zo,7266
|
|
64
65
|
pixie/web/_templates/layout.html,sha256=9kQ1sWyAqMl9YgwJO1jIjwRbiG8bt2sUpJLNrbBZmzo,13988
|
|
65
|
-
pixie/web/_templates/live_env.html,sha256=
|
|
66
|
+
pixie/web/_templates/live_env.html,sha256=0goOWgrCXvONHEyZSGfqG6h_B0rPaEHvw7Cbt-UpL4I,11557
|
|
66
67
|
pixie/web/_templates/login.html,sha256=d60N8kmr7YaAoTKRBaOiGN5Srdy8-BgB8_Zz9TQvGwc,1810
|
|
67
68
|
pixie/web/_templates/machine_detail.html,sha256=ijI5FKuyqOdDwCdLGM_H7yHgqn6fFMm6pOPVBLT9G00,49797
|
|
68
69
|
pixie/web/_templates/machines.html,sha256=modlu9MNqpxOkDd90o-yV1g5yuEiC61I6JtXQk7_PUo,9803
|
|
@@ -76,8 +77,8 @@ pixie/web/_templates/ipxe/exit.j2,sha256=pbTXTwjgiEsPZlRuv7_J0Q3BepXEqXnb82mv1sx
|
|
|
76
77
|
pixie/web/_templates/ipxe/nbdboot.j2,sha256=s1CwIKou5W6LRoC2MdZYwA3RcRN8AU3B1gbJmGLS9MM,3950
|
|
77
78
|
pixie/web/_templates/ipxe/pixie-live-env.j2,sha256=dPWuYCFrkXwwatPSnTRSfWuMUMgvs3VLDHqbJTG1pP0,3048
|
|
78
79
|
pixie/web/_templates/ipxe/unavailable.j2,sha256=zt4DYfYRXmOIjL13diYPxEApTP3CRnn5Beszeh3BPBw,440
|
|
79
|
-
pixie_lab-0.4.
|
|
80
|
-
pixie_lab-0.4.
|
|
81
|
-
pixie_lab-0.4.
|
|
82
|
-
pixie_lab-0.4.
|
|
83
|
-
pixie_lab-0.4.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|