pixie-lab 0.3.2__py3-none-any.whl → 0.4.1__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/deploy/_main.py CHANGED
@@ -311,10 +311,32 @@ def _cmd_purge(args: argparse.Namespace) -> int:
311
311
  shutil.rmtree(data_dir)
312
312
  print(f"pixie-lab purge: removed {data_dir}", file=sys.stderr)
313
313
 
314
- # 4. Delete the deploy directory itself.
314
+ # 4. Delete the deploy directory itself. ``dest`` often sits under a
315
+ # root-owned parent (e.g. /opt), where an unprivileged operator can
316
+ # empty the dir but can't rmdir it -- rmtree clears the contents,
317
+ # then raises on the final rmdir. Report that plainly instead of
318
+ # dumping a traceback; the state is gone either way, so it's a
319
+ # caveat, not a failure.
315
320
  if remove_dir and dest.exists():
316
- shutil.rmtree(dest)
317
- print(f"pixie-lab purge: removed {dest}", file=sys.stderr)
321
+ try:
322
+ shutil.rmtree(dest)
323
+ print(f"pixie-lab purge: removed {dest}", file=sys.stderr)
324
+ except OSError as exc:
325
+ leftover = sorted(p.name for p in dest.iterdir()) if dest.is_dir() else []
326
+ if not leftover:
327
+ print(
328
+ f"pixie-lab purge: emptied {dest}, but could not remove the "
329
+ f"directory itself ({exc.strerror or exc}) -- its parent is not "
330
+ f"writable (a root-owned /opt, say). The empty dir is harmless; "
331
+ f"you can redeploy into it or 'sudo rmdir' it.",
332
+ file=sys.stderr,
333
+ )
334
+ else:
335
+ print(
336
+ f"pixie-lab purge: could not fully remove {dest} "
337
+ f"({exc.strerror or exc}); leftover: {', '.join(leftover)}",
338
+ file=sys.stderr,
339
+ )
318
340
 
319
341
  return 0
320
342
 
pixie/events/_kinds.py CHANGED
@@ -21,7 +21,9 @@ from __future__ import annotations
21
21
  # ---------- catalog + fetch pipeline ---------------------------------
22
22
  #
23
23
  # Every catalog-store mutation emits one of these. ``subject_kind`` is
24
- # always ``"entry"``; ``subject_id`` is the catalog entry name.
24
+ # ``"entry"`` (``subject_id`` = the catalog entry name) for the per-row
25
+ # kinds; the ``catalog.import.*`` pair is catalog-scoped instead
26
+ # (``subject_kind = "catalog"``) since one import touches many entries.
25
27
 
26
28
  CATALOG_ENTRY_ADDED = "catalog.entry.added"
27
29
  """A new row landed via POST /catalog/entries, POST /ui/catalog/add,
@@ -95,36 +97,37 @@ EXPORT_NBDKIT_EXITED = "export.nbdkit.exited"
95
97
  (``_refresh_row`` noticed no live proc for a ``running`` row).
96
98
  ``details.previous_port`` + ``details.error`` explain."""
97
99
 
98
- # ---------- persistent overlays (per-machine qcow2) -----------------
100
+ # ---------- persistent overlays (alias-keyed volumes) ---------------
99
101
  #
100
- # ``subject_kind`` is always ``"machine"``; ``subject_id`` is the
101
- # canonical MAC. ``details`` names the (image_sha, profile) triple.
102
+ # ``subject_kind`` is always ``"overlay"``; ``subject_id`` is the
103
+ # globally-unique ``alias`` (``""`` for a fleet-wide bulk action). An
104
+ # overlay is a single-writer qcow2 volume over one base image, not a
105
+ # per-machine file; ``details`` carries ``alias`` / ``image_sha`` /
106
+ # ``qcow2_path`` as relevant.
102
107
 
103
108
  OVERLAY_CREATED = "overlay.created"
104
- """A per-machine qcow2 overlay was materialised for the first time
105
- for a ``(mac, image_sha, profile)`` triple. ``details`` carries
106
- ``image_sha``, ``profile``, and ``qcow2_path``."""
109
+ """An alias-keyed qcow2 overlay was materialised for the first time
110
+ (lazy-created on the first nbdboot render, or after a Reset).
111
+ ``details`` carries ``alias``, ``image_sha``, and ``qcow2_path``."""
107
112
 
108
113
  OVERLAY_RESET = "overlay.reset"
109
- """Operator hit Reset on the machine detail page: the qemu-nbd was
110
- terminated, the qcow2 was unlinked, the overlays row was deleted.
114
+ """Operator hit Reset (one alias) or Prune (bulk) on the Overlays
115
+ page: the qemu-nbd was terminated, the qcow2 unlinked, the overlays
116
+ row deleted. ``subject_id`` is the alias for a single Reset, ``""``
117
+ for a bulk Prune (``details.pruned`` counts the reclaimed volumes).
111
118
  Next plan render lazily creates a fresh overlay from the base."""
112
119
 
113
- OVERLAY_BOOTED = "overlay.booted"
114
- """Plan render for an already-existing overlay row (heartbeat).
115
- Emitted on every persistent nbdboot render so an operator can grep
116
- the events log for "which overlays am I actually using?"."""
117
-
118
120
  # ---------- machines + PXE ------------------------------------------
119
121
  #
120
122
  # ``subject_kind`` is always ``"machine"``; ``subject_id`` is the
121
123
  # canonical MAC address.
122
124
 
123
125
  MACHINE_DISCOVERED = "machine.discovered"
124
- """First-ever GET /pxe/<mac> for a MAC pixie has not seen before.
125
- Emitted from ``machines_store.touch_seen`` when a new row is
126
- created; subsequent hits do NOT emit (they update ``last_seen_at``
127
- via the ``pxe.plan.rendered`` event instead)."""
126
+ """First-ever GET /pxe/<mac> for a MAC pixie has not seen before. The
127
+ ``/pxe/<mac>`` route detects the absent row before ``touch_seen``
128
+ auto-registers it and emits this once; subsequent hits do NOT emit
129
+ (they update ``last_seen_at`` via the ``pxe.plan.rendered`` event
130
+ instead). ``details.boot_mode`` is the auto-registered default."""
128
131
 
129
132
  MACHINE_BOUND = "machine.bound"
130
133
  """First bind for a MAC: PUT /machines/<mac> or POST
@@ -232,7 +235,6 @@ KNOWN_EVENT_KINDS: frozenset[str] = frozenset(
232
235
  EXPORT_NBDKIT_EXITED,
233
236
  OVERLAY_CREATED,
234
237
  OVERLAY_RESET,
235
- OVERLAY_BOOTED,
236
238
  MACHINE_DISCOVERED,
237
239
  MACHINE_BOUND,
238
240
  MACHINE_BINDING_CHANGED,
pixie/exports/_store.py CHANGED
@@ -33,33 +33,80 @@ CREATE TABLE IF NOT EXISTS exports (
33
33
 
34
34
  CREATE INDEX IF NOT EXISTS idx_exports_content_sha
35
35
  ON exports(content_sha256);
36
+ """
36
37
 
37
- -- One row per (mac, image_sha, profile) triple; the qcow2 file at
38
- -- ``qcow2_path`` has ``backing_file`` pointing at the catalog blob
39
- -- for ``image_sha`` and holds every write the machine has made under
40
- -- that profile. Separate from the ``exports`` table above because the
41
- -- ephemeral (nbdkit) and persistent (qemu-nbd) paths have different
42
- -- identity (per-content vs per-triple) and lifecycle (respawn on
43
- -- restart vs create-on-first-bind).
38
+ # An overlay is a globally-named writable volume over ONE base image.
39
+ # Identity is its ``alias`` (globally unique), NOT the machine: the
40
+ # qcow2 at ``qcow2_path`` has ``backing_file`` pointing at the catalog
41
+ # blob for ``image_sha`` and holds every write made through it. Exactly
42
+ # one machine may hold exclusive write at a time -- ``attached_mac``
43
+ # (empty = free) records which; qemu-nbd's qcow2 image-lock is the
44
+ # backstop. Separate table from ``exports`` because the ephemeral
45
+ # (nbdkit, per-content) and persistent (qemu-nbd, per-alias) paths have
46
+ # different identity + lifecycle.
47
+ _OVERLAYS_SCHEMA = """
44
48
  CREATE TABLE IF NOT EXISTS overlays (
45
- mac TEXT NOT NULL,
49
+ alias TEXT PRIMARY KEY,
46
50
  image_sha TEXT NOT NULL,
47
- profile TEXT NOT NULL,
48
51
  qcow2_path TEXT NOT NULL,
52
+ attached_mac TEXT NOT NULL DEFAULT '',
49
53
  nbd_port INTEGER NOT NULL DEFAULT 0,
50
54
  status TEXT NOT NULL DEFAULT 'idle',
51
55
  error TEXT NOT NULL DEFAULT '',
52
56
  created_at TEXT NOT NULL,
53
- last_boot_at TEXT NOT NULL DEFAULT '',
54
- PRIMARY KEY (mac, image_sha, profile)
57
+ last_boot_at TEXT NOT NULL DEFAULT ''
55
58
  );
56
- CREATE INDEX IF NOT EXISTS idx_overlays_mac
57
- ON overlays(mac);
58
59
  CREATE INDEX IF NOT EXISTS idx_overlays_image_sha
59
60
  ON overlays(image_sha);
61
+ CREATE INDEX IF NOT EXISTS idx_overlays_attached_mac
62
+ ON overlays(attached_mac);
60
63
  """
61
64
 
62
65
 
66
+ def _migrate_overlays_schema(conn: sqlite3.Connection) -> None:
67
+ """Re-key the pre-alias ``overlays`` table (PK
68
+ ``(mac, image_sha, profile)``) to the alias-keyed shape. For each
69
+ old row a globally-unique ``alias = <profile>-<mac_slug>`` is minted
70
+ (dedup on the rare collision) and ``attached_mac`` is seeded with the
71
+ row's old ``mac`` so the machine that owned it keeps its exclusive
72
+ hold. The qcow2 ``qcow2_path`` is left untouched -- no large files
73
+ move during migration. Idempotent: a table that already has an
74
+ ``alias`` column, or no overlays table at all, is left alone."""
75
+ cols = {row[1] for row in conn.execute("PRAGMA table_info(overlays)").fetchall()}
76
+ if not cols or "alias" in cols:
77
+ return
78
+ old_rows = conn.execute("SELECT * FROM overlays").fetchall()
79
+ conn.execute("ALTER TABLE overlays RENAME TO overlays_pre_alias")
80
+ conn.executescript(_OVERLAYS_SCHEMA)
81
+ seen: set[str] = set()
82
+ for r in old_rows:
83
+ mac_slug = str(r["mac"]).replace(":", "-")
84
+ base = f"{r['profile']}-{mac_slug}"
85
+ alias = base
86
+ n = 1
87
+ while alias in seen:
88
+ n += 1
89
+ alias = f"{base}-{n}"
90
+ seen.add(alias)
91
+ conn.execute(
92
+ """
93
+ INSERT INTO overlays (
94
+ alias, image_sha, qcow2_path, attached_mac,
95
+ nbd_port, status, error, created_at, last_boot_at
96
+ ) VALUES (?, ?, ?, ?, 0, 'idle', '', ?, ?)
97
+ """,
98
+ (
99
+ alias,
100
+ r["image_sha"],
101
+ r["qcow2_path"],
102
+ r["mac"],
103
+ r["created_at"],
104
+ r["last_boot_at"],
105
+ ),
106
+ )
107
+ conn.execute("DROP TABLE overlays_pre_alias")
108
+
109
+
63
110
  @dataclass
64
111
  class Export:
65
112
  """One registered NBD export."""
@@ -99,6 +146,8 @@ class ExportsStore:
99
146
  def _ensure_schema(self) -> None:
100
147
  with self._conn() as conn:
101
148
  conn.executescript(_SCHEMA)
149
+ _migrate_overlays_schema(conn)
150
+ conn.executescript(_OVERLAYS_SCHEMA)
102
151
 
103
152
  @contextlib.contextmanager
104
153
  def _conn(self) -> Generator[sqlite3.Connection]:
@@ -182,18 +231,20 @@ def _row_to_export(row: sqlite3.Row) -> Export:
182
231
 
183
232
  @dataclass
184
233
  class Overlay:
185
- """One persistent per-machine qcow2 overlay for ``boot_mode=nbdboot``.
234
+ """A globally-named writable volume over one base image.
186
235
 
187
- Identity is the ``(mac, image_sha, profile)`` triple. The qcow2 at
188
- ``qcow2_path`` has ``backing_file`` pointing at the catalog blob
189
- for ``image_sha``; a qemu-nbd subprocess serves it while the row's
190
- ``status == 'running'``. Deleting the row is the operator's
191
- "Reset overlay" action -- the caller unlinks the qcow2 too."""
236
+ Identity is the ``alias`` (globally unique), not a machine: the
237
+ qcow2 at ``qcow2_path`` has ``backing_file`` pointing at the catalog
238
+ blob for ``image_sha`` and holds every write made through it.
239
+ ``attached_mac`` records the single machine currently holding
240
+ exclusive write ("" = free); a qemu-nbd subprocess serves it while
241
+ ``status == 'running'``. Deleting the row is the operator's "Reset"
242
+ action -- the caller unlinks the qcow2 too."""
192
243
 
193
- mac: str
244
+ alias: str
194
245
  image_sha: str
195
- profile: str
196
246
  qcow2_path: str
247
+ attached_mac: str = ""
197
248
  nbd_port: int = 0
198
249
  status: str = "idle" # "idle" | "running" | "error"
199
250
  error: str = ""
@@ -202,10 +253,10 @@ class Overlay:
202
253
 
203
254
  def to_dict(self) -> dict[str, Any]:
204
255
  out: dict[str, Any] = {
205
- "mac": self.mac,
256
+ "alias": self.alias,
206
257
  "image_sha": self.image_sha,
207
- "profile": self.profile,
208
258
  "qcow2_path": self.qcow2_path,
259
+ "attached_mac": self.attached_mac,
209
260
  "status": self.status,
210
261
  "created_at": self.created_at,
211
262
  }
@@ -219,12 +270,12 @@ class Overlay:
219
270
 
220
271
 
221
272
  class OverlaysStore:
222
- """Repository over the ``overlays`` table on the shared state.db.
273
+ """Repository over the alias-keyed ``overlays`` table.
223
274
 
224
- Same DB path as :class:`ExportsStore` + :class:`CatalogStore`.
225
- Kept as a sibling class rather than a method-set on ExportsStore
226
- so the ephemeral vs persistent lifecycles read as distinct
227
- concerns at the call site."""
275
+ Same DB path as :class:`ExportsStore` + :class:`CatalogStore`. The
276
+ table is created + migrated by :meth:`ExportsStore._ensure_schema`
277
+ (constructed first at startup), so this is a thin data-access
278
+ sibling."""
228
279
 
229
280
  def __init__(self, db_path: Path) -> None:
230
281
  self.db_path = Path(db_path)
@@ -240,31 +291,21 @@ class OverlaysStore:
240
291
  conn.close()
241
292
 
242
293
  def list_all(self) -> list[Overlay]:
243
- """Every overlay row across every (mac, image, profile) triple.
244
- Named ``list_all`` (not ``list``) so ``list[Overlay]`` in
245
- sibling method signatures below resolves to the built-in
246
- rather than the shadowed method."""
247
294
  with self._conn() as conn:
248
- rows = conn.execute(
249
- "SELECT * FROM overlays ORDER BY mac, image_sha, profile"
250
- ).fetchall()
295
+ rows = conn.execute("SELECT * FROM overlays ORDER BY alias").fetchall()
251
296
  return [_row_to_overlay(r) for r in rows]
252
297
 
253
- def get(self, mac: str, image_sha: str, profile: str) -> Overlay | None:
298
+ def get(self, alias: str) -> Overlay | None:
254
299
  with self._conn() as conn:
255
- row = conn.execute(
256
- "SELECT * FROM overlays WHERE mac = ? AND image_sha = ? AND profile = ?",
257
- (mac, image_sha, profile),
258
- ).fetchone()
300
+ row = conn.execute("SELECT * FROM overlays WHERE alias = ?", (alias,)).fetchone()
259
301
  return _row_to_overlay(row) if row else None
260
302
 
261
- def list_for_machine_and_image(self, mac: str, image_sha: str) -> list[Overlay]:
262
- """Every profile this ``(mac, image_sha)`` combo has ever created.
263
- Drives the bind-form overlay picker."""
303
+ def list_for_image(self, image_sha: str) -> list[Overlay]:
304
+ """Every overlay over this base image. Drives the bind-form
305
+ picker (which aliases a machine could attach for this image)."""
264
306
  with self._conn() as conn:
265
307
  rows = conn.execute(
266
- "SELECT * FROM overlays WHERE mac = ? AND image_sha = ? ORDER BY profile",
267
- (mac, image_sha),
308
+ "SELECT * FROM overlays WHERE image_sha = ? ORDER BY alias", (image_sha,)
268
309
  ).fetchall()
269
310
  return [_row_to_overlay(r) for r in rows]
270
311
 
@@ -273,15 +314,15 @@ class OverlaysStore:
273
314
  conn.execute(
274
315
  """
275
316
  INSERT OR REPLACE INTO overlays (
276
- mac, image_sha, profile, qcow2_path,
317
+ alias, image_sha, qcow2_path, attached_mac,
277
318
  nbd_port, status, error, created_at, last_boot_at
278
319
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
279
320
  """,
280
321
  (
281
- ov.mac,
322
+ ov.alias,
282
323
  ov.image_sha,
283
- ov.profile,
284
324
  ov.qcow2_path,
325
+ ov.attached_mac,
285
326
  ov.nbd_port,
286
327
  ov.status,
287
328
  ov.error,
@@ -290,55 +331,52 @@ class OverlaysStore:
290
331
  ),
291
332
  )
292
333
 
293
- def delete(self, mac: str, image_sha: str, profile: str) -> bool:
334
+ def delete(self, alias: str) -> bool:
294
335
  with _DB_WRITE_LOCK, self._conn() as conn:
295
- cur = conn.execute(
296
- "DELETE FROM overlays WHERE mac = ? AND image_sha = ? AND profile = ?",
297
- (mac, image_sha, profile),
298
- )
336
+ cur = conn.execute("DELETE FROM overlays WHERE alias = ?", (alias,))
299
337
  return cur.rowcount > 0
300
338
 
301
- def update_runtime(
302
- self,
303
- mac: str,
304
- image_sha: str,
305
- profile: str,
306
- *,
307
- nbd_port: int,
308
- status: str,
309
- error: str = "",
310
- ) -> None:
339
+ def update_runtime(self, alias: str, *, nbd_port: int, status: str, error: str = "") -> None:
311
340
  """Refresh transient port + status after a qemu-nbd spawn."""
312
341
  with _DB_WRITE_LOCK, self._conn() as conn:
313
342
  conn.execute(
314
- """
315
- UPDATE overlays
316
- SET nbd_port = ?, status = ?, error = ?
317
- WHERE mac = ? AND image_sha = ? AND profile = ?
318
- """,
319
- (nbd_port, status, error, mac, image_sha, profile),
343
+ "UPDATE overlays SET nbd_port = ?, status = ?, error = ? WHERE alias = ?",
344
+ (nbd_port, status, error, alias),
320
345
  )
321
346
 
322
- def touch_last_boot(self, mac: str, image_sha: str, profile: str) -> None:
323
- """Record the timestamp of the most recent plan render for this
324
- overlay. Used by the UI's "last used" column + future auto-GC."""
347
+ def touch_last_boot(self, alias: str) -> None:
348
+ with _DB_WRITE_LOCK, self._conn() as conn:
349
+ conn.execute("UPDATE overlays SET last_boot_at = ? WHERE alias = ?", (now_iso(), alias))
350
+
351
+ def attach(self, alias: str, mac: str) -> None:
352
+ """Record ``mac`` as the exclusive writer of ``alias``.
353
+ Single-writer is enforced at the call site (bind route + renderer
354
+ refuse a hand-off to a different mac); this just records it."""
355
+ with _DB_WRITE_LOCK, self._conn() as conn:
356
+ conn.execute("UPDATE overlays SET attached_mac = ? WHERE alias = ?", (mac, alias))
357
+
358
+ def detach(self, alias: str) -> None:
359
+ """Release the writer hold on ``alias`` (attached_mac -> '')."""
360
+ with _DB_WRITE_LOCK, self._conn() as conn:
361
+ conn.execute("UPDATE overlays SET attached_mac = '' WHERE alias = ?", (alias,))
362
+
363
+ def detach_mac(self, mac: str, *, keep: str = "") -> None:
364
+ """Release every alias ``mac`` holds except ``keep`` -- called
365
+ when a machine rebinds away so a stale hold can't block another
366
+ machine from attaching."""
325
367
  with _DB_WRITE_LOCK, self._conn() as conn:
326
368
  conn.execute(
327
- """
328
- UPDATE overlays
329
- SET last_boot_at = ?
330
- WHERE mac = ? AND image_sha = ? AND profile = ?
331
- """,
332
- (now_iso(), mac, image_sha, profile),
369
+ "UPDATE overlays SET attached_mac = '' WHERE attached_mac = ? AND alias != ?",
370
+ (mac, keep),
333
371
  )
334
372
 
335
373
 
336
374
  def _row_to_overlay(row: sqlite3.Row) -> Overlay:
337
375
  return Overlay(
338
- mac=row["mac"],
376
+ alias=row["alias"],
339
377
  image_sha=row["image_sha"],
340
- profile=row["profile"],
341
378
  qcow2_path=row["qcow2_path"],
379
+ attached_mac=row["attached_mac"],
342
380
  nbd_port=row["nbd_port"] or 0,
343
381
  status=row["status"],
344
382
  error=row["error"],
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
- DEFAULT_BOOT_MODE,
23
+ UNCOMMITTED_BOOT_MODES,
24
24
  BadMac,
25
25
  MachinesStore,
26
26
  normalise_mac,
@@ -66,13 +66,15 @@ class BindBody(BaseModel):
66
66
  "the global PIXIE_LIVE_ENV_EXTRA_CMDLINE. Single line."
67
67
  ),
68
68
  )
69
- overlay_profile: str = Field(
69
+ overlay_alias: str = Field(
70
70
  default="",
71
71
  description=(
72
- "Persistent-overlay profile name for nbdboot. Blank means "
73
- "ephemeral tmpfs (writes vanish on reboot); non-blank names a "
74
- "qcow2 overlay that persists on pixie's data volume. "
75
- "Alphanumeric-leading; a-z / A-Z / 0-9 / . _ - (max 64 chars)."
72
+ "Globally-unique persistent-overlay alias for nbdboot. Blank "
73
+ "means ephemeral tmpfs (writes vanish on reboot); non-blank "
74
+ "attaches a named writable volume (qcow2 over a base image) "
75
+ "that persists on pixie's data volume. Single-writer: one "
76
+ "machine per alias. Alphanumeric-leading; a-z / A-Z / 0-9 / "
77
+ ". _ - (max 64 chars)."
76
78
  ),
77
79
  )
78
80
 
@@ -133,14 +135,28 @@ def upsert_machine(
133
135
  # not supply any new ones -- honours a partial-update PUT
134
136
  # without needing PATCH.
135
137
  labels_arg = labels if labels else (list(previous.labels) if previous else [])
138
+ # Single-writer + alias-implies-image resolution is shared with
139
+ # the UI bind route (see pixie.web._overlay_bind): attaching an
140
+ # alias held by another machine raises ValueError -> 422 here.
141
+ from pixie.web._overlay_bind import overlay_state, resolve_overlay_bind
142
+
143
+ overlays, overlays_dir = overlay_state(request.app.state)
144
+ image_sha, resolved_alias = resolve_overlay_bind(
145
+ overlays=overlays,
146
+ overlays_dir=overlays_dir,
147
+ mac=canon,
148
+ boot_mode=body.boot_mode,
149
+ image_sha=body.image_content_sha256.strip().lower(),
150
+ alias=body.overlay_alias,
151
+ )
136
152
  row = _get_machines(request).upsert_binding(
137
153
  canon,
138
154
  boot_mode=body.boot_mode,
139
- image_content_sha256=body.image_content_sha256.strip().lower(),
155
+ image_content_sha256=image_sha,
140
156
  labels=labels_arg,
141
157
  target_disk_serial=body.target_disk_serial,
142
158
  extra_cmdline=body.extra_cmdline,
143
- overlay_profile=body.overlay_profile,
159
+ overlay_alias=resolved_alias,
144
160
  )
145
161
  except ValueError as exc:
146
162
  raise HTTPException(status_code=422, detail=str(exc)) from exc
@@ -149,12 +165,14 @@ def upsert_machine(
149
165
  details: dict[str, Any] = {"boot_mode": row.boot_mode}
150
166
  if row.image_content_sha256:
151
167
  details["image_content_sha256"] = row.image_content_sha256
152
- # ``machine.bound`` on a fresh MAC or on a row that was
153
- # discovered-only (previous.boot_mode default with no bound
154
- # image); ``machine.binding.changed`` when the mode or image
155
- # actually shifted between the previous state and the new one.
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.
156
174
  was_bound = previous is not None and (
157
- bool(previous.image_content_sha256) or previous.boot_mode != DEFAULT_BOOT_MODE
175
+ bool(previous.image_content_sha256) or previous.boot_mode not in UNCOMMITTED_BOOT_MODES
158
176
  )
159
177
  changed = previous is not None and (
160
178
  previous.boot_mode != row.boot_mode