pixie-lab 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. pixie/__init__.py +20 -0
  2. pixie/_util.py +38 -0
  3. pixie/catalog/__init__.py +25 -0
  4. pixie/catalog/_fetcher.py +513 -0
  5. pixie/catalog/_routes.py +392 -0
  6. pixie/catalog/_schema.py +263 -0
  7. pixie/catalog/_store.py +234 -0
  8. pixie/deploy/__init__.py +26 -0
  9. pixie/deploy/_main.py +321 -0
  10. pixie/deploy/_templates.py +153 -0
  11. pixie/disks.py +85 -0
  12. pixie/events/__init__.py +28 -0
  13. pixie/events/_kinds.py +203 -0
  14. pixie/events/_log.py +210 -0
  15. pixie/events/_routes.py +42 -0
  16. pixie/exports/__init__.py +17 -0
  17. pixie/exports/_routes.py +211 -0
  18. pixie/exports/_store.py +156 -0
  19. pixie/exports/_supervisor.py +240 -0
  20. pixie/flash.py +1971 -0
  21. pixie/images.py +473 -0
  22. pixie/machines/__init__.py +16 -0
  23. pixie/machines/_routes.py +190 -0
  24. pixie/machines/_store.py +623 -0
  25. pixie/oras.py +547 -0
  26. pixie/pivot/__init__.py +180 -0
  27. pixie/pivot/nbdboot +348 -0
  28. pixie/pxe/__init__.py +19 -0
  29. pixie/pxe/_renderer.py +244 -0
  30. pixie/pxe/_routes.py +386 -0
  31. pixie/tftp/__init__.py +21 -0
  32. pixie/tftp/_supervisor.py +129 -0
  33. pixie/tui/__init__.py +185 -0
  34. pixie/tui/_app.py +2219 -0
  35. pixie/tui_catalog.py +657 -0
  36. pixie/web/__init__.py +6 -0
  37. pixie/web/_auth.py +70 -0
  38. pixie/web/_settings_store.py +211 -0
  39. pixie/web/_static/.gitkeep +0 -0
  40. pixie/web/_static/bootstrap-icons.min.css +5 -0
  41. pixie/web/_static/bootstrap.min.css +12 -0
  42. pixie/web/_static/fonts/bootstrap-icons.woff +0 -0
  43. pixie/web/_static/fonts/bootstrap-icons.woff2 +0 -0
  44. pixie/web/_static/htmx.min.js +1 -0
  45. pixie/web/_static/pixie-favicon.png +0 -0
  46. pixie/web/_static/sse.js +290 -0
  47. pixie/web/_table_state.py +261 -0
  48. pixie/web/_templates/_partials/page_description.html +22 -0
  49. pixie/web/_templates/_partials/recent_events.html +47 -0
  50. pixie/web/_templates/_partials/table_helpers.html +189 -0
  51. pixie/web/_templates/catalog.html +364 -0
  52. pixie/web/_templates/catalog_detail.html +386 -0
  53. pixie/web/_templates/dashboard.html +256 -0
  54. pixie/web/_templates/events.html +125 -0
  55. pixie/web/_templates/ipxe/bootstrap.j2 +12 -0
  56. pixie/web/_templates/ipxe/exit.j2 +10 -0
  57. pixie/web/_templates/ipxe/nbdboot.j2 +57 -0
  58. pixie/web/_templates/ipxe/pixie-live-env.j2 +55 -0
  59. pixie/web/_templates/ipxe/unavailable.j2 +14 -0
  60. pixie/web/_templates/layout.html +345 -0
  61. pixie/web/_templates/login.html +40 -0
  62. pixie/web/_templates/machine_detail.html +786 -0
  63. pixie/web/_templates/machines.html +186 -0
  64. pixie/web/_templates/settings.html +265 -0
  65. pixie/web/main.py +1910 -0
  66. pixie_lab-0.1.0.dist-info/METADATA +107 -0
  67. pixie_lab-0.1.0.dist-info/RECORD +70 -0
  68. pixie_lab-0.1.0.dist-info/WHEEL +4 -0
  69. pixie_lab-0.1.0.dist-info/entry_points.txt +3 -0
  70. pixie_lab-0.1.0.dist-info/licenses/LICENSE +674 -0
@@ -0,0 +1,186 @@
1
+ {% extends "layout.html" %}
2
+ {% from "_partials/table_helpers.html" import search_input, sort_header, pagination_inline %}
3
+ {% from "_partials/recent_events.html" import recent_events_card %}
4
+ {% from "_partials/page_description.html" import page_description %}
5
+ {% block title %}Machines - pixie{% endblock %}
6
+ {% block subnav %}
7
+ <nav class="navbar subnav-strip" aria-label="Section sub-navigation">
8
+ <div class="container">
9
+ {# LEFT: anchor links to in-page sections. Don't repeat the
10
+ top-nav label ("Machines"); use section-specific verbs. #}
11
+ <span class="subnav-jumps d-flex align-items-center flex-wrap">
12
+ <a href="#list">List</a>
13
+ </span>
14
+ {# RIGHT: no inline actions on this page. Bindings are edited
15
+ per-machine on the detail page. #}
16
+ </div>
17
+ </nav>
18
+ {% endblock %}
19
+ {% block content %}
20
+ {{ page_description('pc-display', 'Every MAC pixie has seen. A row appears here on the target\'s first PXE contact; the last-seen columns refresh every 5 s while the page is open. Click a MAC to bind its boot mode, edit labels, or inspect the inventory and events for that target.') }}
21
+ <div class="card" id="list">
22
+ <div class="card-header bg-light fw-semibold d-flex flex-wrap align-items-center gap-3 py-2">
23
+ <span class="text-nowrap"><i class="bi bi-pc-display me-1"></i>Machines</span>
24
+ <span class="vr"></span>
25
+ {{ search_input('/ui/machines', q, preserved, 'MAC / boot mode / sha / last IP') }}
26
+ {{ pagination_inline(page_state, preserved, label='machines') }}
27
+ </div>
28
+ {% if machines %}
29
+ <div class="table-responsive">
30
+ <table class="table table-hover align-middle mb-0">
31
+ <thead class="table-light">
32
+ <tr>
33
+ {{ sort_header('MAC', 'mac', sort, preserved) }}
34
+ {{ sort_header('Boot mode', 'boot_mode', sort, preserved) }}
35
+ <th scope="col">Image</th>
36
+ <th scope="col">Inventory</th>
37
+ {{ sort_header('Last seen', 'last_seen_at', sort, preserved) }}
38
+ <th scope="col" class="text-end">Actions</th>
39
+ </tr>
40
+ </thead>
41
+ <tbody>
42
+ {% for m in machines %}
43
+ <tr data-machine-row="{{ m.mac }}">
44
+ <td data-cell="mac-labels">
45
+ <a href="/ui/machines/{{ m.mac }}" class="text-decoration-none">
46
+ <code>{{ m.mac }}</code>
47
+ </a>
48
+ {% if m.labels %}
49
+ <div class="mt-1">
50
+ {% for lbl in m.labels %}
51
+ <span class="badge text-bg-light border me-1">{{ lbl }}</span>
52
+ {% endfor %}
53
+ </div>
54
+ {% endif %}
55
+ </td>
56
+ <td data-cell="boot-mode">
57
+ {% if m.boot_mode == 'nbdboot' %}
58
+ <span class="badge text-bg-primary">
59
+ <i class="bi bi-hdd-network me-1"></i>nbdboot
60
+ </span>
61
+ {% else %}
62
+ <span class="badge text-bg-secondary">
63
+ <i class="bi bi-box-arrow-right me-1"></i>{{ m.boot_mode }}
64
+ </span>
65
+ {% endif %}
66
+ </td>
67
+ <td data-cell="image">
68
+ {% if m.image_content_sha256 %}
69
+ <code title="{{ m.image_content_sha256 }}">{{ m.image_content_sha256[:12] }}</code>
70
+ {% else %}
71
+ <span class="text-muted small">-</span>
72
+ {% endif %}
73
+ </td>
74
+ <td data-cell="inventory">
75
+ {% if m.inventory %}
76
+ {% set d = m.inventory.get("disks") or [] %}
77
+ <span class="small">
78
+ <i class="bi bi-hdd me-1"></i>{{ d|length }} disk{{ '' if d|length == 1 else 's' }}
79
+ </span>
80
+ {% if m.inventory.get("lshw") %}
81
+ <span class="text-muted small">+ lshw</span>
82
+ {% endif %}
83
+ {% else %}
84
+ <span class="text-muted small">-</span>
85
+ {% endif %}
86
+ </td>
87
+ <td class="small text-muted" data-cell="last-seen">
88
+ {{ m.last_seen_at | fmt_ts }}
89
+ {% if m.last_seen_ip %}<br><code>{{ m.last_seen_ip }}</code>{% endif %}
90
+ </td>
91
+ <td class="text-end text-nowrap">
92
+ {# The MAC cell already links to the detail page;
93
+ an "open" icon here is redundant. Keep only
94
+ the destructive action, which needs its own
95
+ control (a confirm-prompt on a link would be
96
+ surprising). #}
97
+ <form method="post" action="/ui/machines/delete" class="d-inline"
98
+ onsubmit="return confirm('Delete machine record for {{ m.mac }}? Inventory + binding will be lost.');">
99
+ <input type="hidden" name="mac" value="{{ m.mac }}">
100
+ <button type="submit"
101
+ class="btn btn-sm btn-outline-danger"
102
+ title="Delete {{ m.mac }}"
103
+ aria-label="Delete {{ m.mac }}">
104
+ <i class="bi bi-trash" aria-hidden="true"></i>
105
+ </button>
106
+ </form>
107
+ </td>
108
+ </tr>
109
+ {% endfor %}
110
+ </tbody>
111
+ </table>
112
+ </div>
113
+ {% else %}
114
+ <div class="card-body">
115
+ <p class="text-muted mb-0">
116
+ <i class="bi bi-inbox me-1"></i>
117
+ {% if q %}
118
+ No machines match <code>{{ q }}</code>.
119
+ {% else %}
120
+ No machines yet. Machines appear when a PXE-booting target hits
121
+ <code>/pxe/&lt;mac&gt;</code>; open a machine to edit its binding.
122
+ {% endif %}
123
+ </p>
124
+ </div>
125
+ {% endif %}
126
+ </div>
127
+
128
+ <script>
129
+ // Live refresh: every 5 s hit /ui/machines-live.json and update the
130
+ // per-machine cells (boot_mode badge, image sha, inventory count,
131
+ // last_seen_at + IP). Only rows currently on the page get updated
132
+ // (rows on other pagination pages aren't visible; a full poll of
133
+ // them would just churn the state dict). A new MAC that shows up
134
+ // server-side but wasn't rendered here does NOT get injected --
135
+ // pagination + sort would be wrong without a re-render; the
136
+ // operator sees it on their next page load.
137
+ (function () {
138
+ const POLL_MS = 5000;
139
+ const rows = document.querySelectorAll("[data-machine-row]");
140
+ if (!rows.length) return;
141
+
142
+ function bootBadge(mode) {
143
+ if (mode === "nbdboot") {
144
+ return `<span class="badge text-bg-primary"><i class="bi bi-hdd-network me-1"></i>nbdboot</span>`;
145
+ }
146
+ return `<span class="badge text-bg-secondary"><i class="bi bi-box-arrow-right me-1"></i>${mode}</span>`;
147
+ }
148
+
149
+ async function poll() {
150
+ try {
151
+ const r = await fetch("/ui/machines-live.json", {credentials: "same-origin"});
152
+ if (!r.ok) return;
153
+ const rowsData = await r.json();
154
+ rows.forEach((tr) => {
155
+ const mac = tr.getAttribute("data-machine-row");
156
+ const d = rowsData[mac];
157
+ if (!d) return;
158
+ const setCell = (key, html) => {
159
+ const cell = tr.querySelector(`[data-cell="${key}"]`);
160
+ if (cell && cell.innerHTML !== html) cell.innerHTML = html;
161
+ };
162
+ setCell("boot-mode", bootBadge(d.boot_mode));
163
+ setCell("image", d.image_content_sha256
164
+ ? `<code title="${d.image_content_sha256}">${d.image_content_sha256.slice(0,12)}</code>`
165
+ : `<span class="text-muted small">-</span>`);
166
+ let inv;
167
+ if (d.inventory_at) {
168
+ const disks = d.disks_count || 0;
169
+ inv = `<span class="small"><i class="bi bi-hdd me-1"></i>${disks} disk${disks === 1 ? "" : "s"}</span>`;
170
+ if (d.has_lshw) inv += ` <span class="text-muted small">+ lshw</span>`;
171
+ } else {
172
+ inv = `<span class="text-muted small">-</span>`;
173
+ }
174
+ setCell("inventory", inv);
175
+ let seen = d.last_seen_at_display || d.last_seen_at || "-";
176
+ if (d.last_seen_ip) seen += `<br><code>${d.last_seen_ip}</code>`;
177
+ setCell("last-seen", seen);
178
+ });
179
+ } catch (e) { /* swallow; try again next tick */ }
180
+ }
181
+ poll();
182
+ setInterval(poll, POLL_MS);
183
+ })();
184
+ </script>
185
+ {{ recent_events_card(machine_events, "machine") }}
186
+ {% endblock %}
@@ -0,0 +1,265 @@
1
+ {% extends "layout.html" %}
2
+ {% from "_partials/page_description.html" import page_description %}
3
+ {% block title %}Settings - pixie{% endblock %}
4
+ {% block subnav %}
5
+ <nav class="navbar subnav-strip" aria-label="Section sub-navigation">
6
+ <div class="container">
7
+ <span class="subnav-jumps d-flex align-items-center flex-wrap">
8
+ <a href="#display">Display</a>
9
+ <a href="#live-env">Live-env</a>
10
+ <a href="#deployment">Deployment</a>
11
+ </span>
12
+ </div>
13
+ </nav>
14
+ {% endblock %}
15
+ {% block content %}
16
+ {{ page_description('sliders', 'Operator-editable knobs that pixie persists across restarts, alongside the environment variables the deploy consumes at startup. UI edits below win over env-var defaults, so tuning a knob live in the browser does not need a redeploy. Env-var defaults are documented in the Deployment card at the bottom.') }}
17
+ {% if flash_error %}
18
+ <div class="alert alert-danger">
19
+ <i class="bi bi-exclamation-triangle me-1"></i>{{ flash_error }}
20
+ </div>
21
+ {% endif %}
22
+
23
+ <div class="card shadow-sm mb-4" id="display">
24
+ <div class="card-header bg-light fw-semibold d-flex flex-wrap align-items-center gap-3 py-2">
25
+ <span class="text-nowrap"><i class="bi bi-clock me-1"></i>Display</span>
26
+ <span class="text-muted small">
27
+ timezone + strftime pattern for every operator-facing timestamp
28
+ </span>
29
+ </div>
30
+ <div class="card-body">
31
+ <form method="post" action="/ui/settings/display/edit" class="row g-3">
32
+ <div class="col-md-6">
33
+ <label for="timezone" class="form-label">
34
+ Display timezone
35
+ <span class="badge text-bg-{% if display_tz.override %}primary{% else %}secondary{% endif %} ms-1">
36
+ {% if display_tz.override %}override{% elif display_tz.effective != display_tz.default %}env{% else %}default{% endif %}
37
+ </span>
38
+ </label>
39
+ <input type="text" id="timezone" name="timezone"
40
+ class="form-control font-monospace"
41
+ value="{{ display_tz.override }}"
42
+ placeholder="{{ display_tz.default }}"
43
+ list="tz-suggestions">
44
+ <datalist id="tz-suggestions">
45
+ <option value="UTC">
46
+ <option value="Europe/Copenhagen">
47
+ <option value="Europe/London">
48
+ <option value="Europe/Berlin">
49
+ <option value="America/New_York">
50
+ <option value="America/Los_Angeles">
51
+ <option value="Asia/Tokyo">
52
+ </datalist>
53
+ <div class="form-text">
54
+ IANA timezone name. Blank falls back to
55
+ <code>{{ display_tz.env }}</code> then <code>{{ display_tz.default }}</code>.
56
+ Currently effective: <code>{{ display_tz.effective }}</code>.
57
+ {% if display_tz.updated_at %}
58
+ <br>Last updated: <code>{{ display_tz.updated_at | fmt_ts }}</code>.
59
+ {% endif %}
60
+ </div>
61
+ </div>
62
+ <div class="col-md-6">
63
+ <label for="datetime_format" class="form-label">
64
+ Datetime format
65
+ <span class="badge text-bg-{% if datetime_format.override %}primary{% else %}secondary{% endif %} ms-1">
66
+ {% if datetime_format.override %}override{% elif datetime_format.effective != datetime_format.default %}env{% else %}default{% endif %}
67
+ </span>
68
+ </label>
69
+ <input type="text" id="datetime_format" name="datetime_format"
70
+ class="form-control font-monospace"
71
+ value="{{ datetime_format.override }}"
72
+ placeholder="{{ datetime_format.default }}">
73
+ <div class="form-text">
74
+ Python <code>strftime</code> pattern (e.g.
75
+ <code>%Y-%m-%d %H:%M:%S %Z</code>,
76
+ <code>%d %b %Y %H:%M</code>). Blank falls back to
77
+ <code>{{ datetime_format.env }}</code> then the default.
78
+ Currently effective: <code>{{ datetime_format.effective }}</code>.
79
+ {% if datetime_format.updated_at %}
80
+ <br>Last updated: <code>{{ datetime_format.updated_at | fmt_ts }}</code>.
81
+ {% endif %}
82
+ </div>
83
+ </div>
84
+ <div class="col-12">
85
+ <button type="submit" class="btn btn-primary">
86
+ <i class="bi bi-check2 me-1"></i>Save
87
+ </button>
88
+ <span class="text-muted small ms-2">
89
+ Blank fields CLEAR the override. Env / default takes over.
90
+ </span>
91
+ </div>
92
+ </form>
93
+ </div>
94
+ </div>
95
+
96
+ <div class="card shadow-sm mb-4" id="live-env">
97
+ <div class="card-header bg-light fw-semibold d-flex flex-wrap align-items-center gap-3 py-2">
98
+ <span class="text-nowrap"><i class="bi bi-cpu me-1"></i>Live-env</span>
99
+ <span class="text-muted small">
100
+ per-deploy kernel-cmdline workarounds for hardware quirks
101
+ </span>
102
+ </div>
103
+ <div class="card-body">
104
+ <form method="post" action="/ui/settings/live-env/edit" class="row g-3">
105
+ <div class="col-12">
106
+ <label for="extra_cmdline" class="form-label">
107
+ Extra kernel cmdline
108
+ <span class="badge text-bg-{% if live_env_extra_cmdline.override %}primary{% else %}secondary{% endif %} ms-1">
109
+ {% if live_env_extra_cmdline.override %}override{% elif live_env_extra_cmdline.effective %}env{% else %}default{% endif %}
110
+ </span>
111
+ </label>
112
+ <input type="text" id="extra_cmdline" name="extra_cmdline"
113
+ class="form-control font-monospace"
114
+ value="{{ live_env_extra_cmdline.override }}"
115
+ placeholder="e.g. pci=realloc=on,nocrs"
116
+ list="live-env-cmdline-suggestions">
117
+ <datalist id="live-env-cmdline-suggestions">
118
+ <option value="pci=realloc=on,nocrs">
119
+ </datalist>
120
+ <div class="form-text">
121
+ Tokens appended verbatim to the <code>pixie-live-env</code>
122
+ kernel cmdline (after <code>pixie.mac=</code> /
123
+ <code>bty.mac=</code>, so last-token-wins on any
124
+ conflict). Whitespace-separated; single line.
125
+ Blank falls back to <code>{{ live_env_extra_cmdline.env }}</code>
126
+ then no append.
127
+ Currently effective:
128
+ {% if live_env_extra_cmdline.effective %}
129
+ <code>{{ live_env_extra_cmdline.effective }}</code>
130
+ {% else %}
131
+ <em class="text-muted">(none)</em>
132
+ {% endif %}.
133
+ Known-good values per hardware in
134
+ <a href="https://github.com/safl/pixie/blob/main/docs/src/hardware-quirks.md" target="_blank" rel="noreferrer">docs/hardware-quirks.md</a>.
135
+ {% if live_env_extra_cmdline.updated_at %}
136
+ <br>Last updated: <code>{{ live_env_extra_cmdline.updated_at | fmt_ts }}</code>.
137
+ {% endif %}
138
+ </div>
139
+ </div>
140
+ <div class="col-12">
141
+ <button type="submit" class="btn btn-primary">
142
+ <i class="bi bi-check2 me-1"></i>Save
143
+ </button>
144
+ <span class="text-muted small ms-2">
145
+ Takes effect on the NEXT <code>GET /pxe/&lt;mac&gt;</code>;
146
+ a target already mid-boot must be power-cycled.
147
+ </span>
148
+ </div>
149
+ </form>
150
+ </div>
151
+ </div>
152
+
153
+ <div class="card shadow-sm mb-4" id="deployment">
154
+ <div class="card-header bg-light fw-semibold d-flex flex-wrap align-items-center gap-3 py-2">
155
+ <span class="text-nowrap"><i class="bi bi-gear-wide-connected me-1"></i>Deployment</span>
156
+ <span class="text-muted small">
157
+ environment variables consumed at startup + DHCP handoff
158
+ </span>
159
+ </div>
160
+ <div class="card-body">
161
+ <p class="text-muted small">
162
+ Environment variables the pixie process reads at startup.
163
+ The compose file bundled by <code>pixie deploy</code> sets
164
+ the important ones from <code>envvars</code>; anything not
165
+ set falls back to the default in the table. Changes here
166
+ require a container restart (they are read once at
167
+ startup), unlike the UI knobs above which are live.
168
+ </p>
169
+
170
+ <div class="table-responsive">
171
+ <table class="table table-sm mb-0 align-middle">
172
+ <thead>
173
+ <tr>
174
+ <th scope="col">Variable</th>
175
+ <th scope="col">Default</th>
176
+ <th scope="col">Purpose</th>
177
+ </tr>
178
+ </thead>
179
+ <tbody>
180
+ {% for row in deployment_envvars %}
181
+ <tr>
182
+ <td><code>{{ row.name }}</code></td>
183
+ <td>
184
+ {% if row.default %}<code>{{ row.default }}</code>
185
+ {% else %}<em class="text-muted small">(none)</em>{% endif %}
186
+ </td>
187
+ <td class="small">{{ row.purpose }}</td>
188
+ </tr>
189
+ {% endfor %}
190
+ </tbody>
191
+ </table>
192
+ </div>
193
+
194
+ <h3 class="h6 fw-semibold mt-4 mb-2">
195
+ <i class="bi bi-router me-1"></i>Pointing DHCP at pixie
196
+ </h3>
197
+ <p class="small mb-2">
198
+ Pixie chainloads iPXE via TFTP: the DHCP server tells the
199
+ target's PXE firmware where to fetch the iPXE bootloader,
200
+ and iPXE then chains to pixie's HTTP endpoint. The two
201
+ options below cover the common firmware split:
202
+ </p>
203
+ <div class="row g-3">
204
+ <div class="col-md-6">
205
+ <div class="border rounded p-3 h-100 bg-body-tertiary">
206
+ <div class="fw-semibold mb-1">
207
+ <i class="bi bi-hdd-network me-1"></i>Legacy BIOS / PXE
208
+ </div>
209
+ <div class="small text-muted mb-2">
210
+ Target boots via PXE ROM. DHCP hands out an
211
+ iPXE binary that pixie serves over TFTP;
212
+ iPXE re-DHCPs and pixie sees the second
213
+ request come in from an iPXE user-class
214
+ + chains to <code>/pxe-bootstrap.ipxe</code>.
215
+ </div>
216
+ <pre class="mb-0 small"><code># dnsmasq
217
+ dhcp-boot=tag:!ipxe,undionly.kpxe,{{ deployment.host }},{{ deployment.host }}
218
+ dhcp-userclass=set:ipxe,iPXE
219
+ dhcp-boot=tag:ipxe,http://{{ deployment.host }}:{{ deployment.port }}/pxe-bootstrap.ipxe</code></pre>
220
+ </div>
221
+ </div>
222
+ <div class="col-md-6">
223
+ <div class="border rounded p-3 h-100 bg-body-tertiary">
224
+ <div class="fw-semibold mb-1">
225
+ <i class="bi bi-hdd-network-fill me-1"></i>UEFI
226
+ </div>
227
+ <div class="small text-muted mb-2">
228
+ Target boots via UEFI PXE. Same shape but
229
+ the DHCP hand-off must serve an EFI iPXE
230
+ binary. Same iPXE user-class re-DHCP dance
231
+ lands on <code>/pxe-bootstrap.ipxe</code>.
232
+ </div>
233
+ <pre class="mb-0 small"><code># dnsmasq
234
+ dhcp-boot=tag:!ipxe,ipxe.efi,{{ deployment.host }},{{ deployment.host }}
235
+ dhcp-userclass=set:ipxe,iPXE
236
+ dhcp-boot=tag:ipxe,http://{{ deployment.host }}:{{ deployment.port }}/pxe-bootstrap.ipxe</code></pre>
237
+ </div>
238
+ </div>
239
+ </div>
240
+
241
+ <div class="mt-3 small text-muted">
242
+ <div>
243
+ <span class="fw-semibold">TFTP listen:</span>
244
+ {% if deployment.tftp_enabled %}
245
+ <code>{{ deployment.tftp_bind }}:{{ deployment.tftp_port }}/udp</code>
246
+ (root <code>{{ deployment.tftp_root }}</code>)
247
+ {% else %}
248
+ <em>disabled</em> (set <code>PIXIE_TFTP_ENABLED=1</code>
249
+ to bind udp/69)
250
+ {% endif %}
251
+ </div>
252
+ <div>
253
+ <span class="fw-semibold">Bootstrap URL:</span>
254
+ <code>http://{{ deployment.host }}:{{ deployment.port }}/pxe-bootstrap.ipxe</code>
255
+ </div>
256
+ <div>
257
+ <span class="fw-semibold">NBD listen:</span>
258
+ <code>{{ deployment.nbd_bind }}</code> from port
259
+ <code>{{ deployment.nbd_port_base }}</code> upward
260
+ (one export per bound nbdboot image)
261
+ </div>
262
+ </div>
263
+ </div>
264
+ </div>
265
+ {% endblock %}