netboot 0.2.4__tar.gz → 0.3.1__tar.gz
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.
- {netboot-0.2.4 → netboot-0.3.1}/AGENTS.md +9 -1
- {netboot-0.2.4 → netboot-0.3.1}/CHANGELOG.md +112 -2
- {netboot-0.2.4 → netboot-0.3.1}/PKG-INFO +26 -2
- {netboot-0.2.4 → netboot-0.3.1}/README.md +13 -1
- {netboot-0.2.4 → netboot-0.3.1}/docs/api.md +14 -0
- {netboot-0.2.4 → netboot-0.3.1}/docs/configuration.md +113 -7
- netboot-0.3.1/docs/extending.md +215 -0
- {netboot-0.2.4 → netboot-0.3.1}/examples/README.md +7 -5
- netboot-0.3.1/examples/templates/debian/boot.cfg.shtpl +8 -0
- {netboot-0.2.4 → netboot-0.3.1}/pyproject.toml +15 -1
- {netboot-0.2.4 → netboot-0.3.1}/src/netboot/AGENTS.md +141 -21
- {netboot-0.2.4 → netboot-0.3.1}/src/netboot/dhcp/__init__.py +40 -1
- {netboot-0.2.4 → netboot-0.3.1}/src/netboot/dhcp/dhcpd.py +1 -1
- {netboot-0.2.4 → netboot-0.3.1}/src/netboot/dhcp/dnsmasq.py +10 -3
- {netboot-0.2.4 → netboot-0.3.1}/src/netboot/dhcp/kea.py +1 -1
- {netboot-0.2.4 → netboot-0.3.1}/src/netboot/dhcp/options.py +26 -5
- netboot-0.3.1/src/netboot/dhcp/windhcp.py +517 -0
- {netboot-0.2.4 → netboot-0.3.1}/src/netboot/engine.py +5 -3
- {netboot-0.2.4 → netboot-0.3.1}/src/netboot/templates/__init__.py +111 -10
- netboot-0.3.1/src/netboot/templates/common.py +171 -0
- netboot-0.3.1/src/netboot/templates/copy.py +76 -0
- netboot-0.3.1/src/netboot/templates/data.py +160 -0
- netboot-0.3.1/src/netboot/templates/external.py +220 -0
- netboot-0.3.1/src/netboot/templates/handlebars.py +54 -0
- netboot-0.3.1/src/netboot/templates/jinja.py +39 -0
- netboot-0.3.1/src/netboot/templates/liquid.py +59 -0
- netboot-0.3.1/src/netboot/templates/mako.py +131 -0
- netboot-0.3.1/src/netboot/templates/mustache.py +55 -0
- netboot-0.3.1/src/netboot/templates/shell.py +161 -0
- netboot-0.3.1/tests/test_dhcp_extras.py +243 -0
- netboot-0.3.1/tests/test_dhcp_windhcp.py +447 -0
- netboot-0.3.1/tests/test_dhcp_windhcp_powershell.py +331 -0
- netboot-0.3.1/tests/test_external_engines.py +244 -0
- netboot-0.3.1/tests/test_mako.py +133 -0
- netboot-0.3.1/tests/test_optional_engines.py +206 -0
- {netboot-0.2.4 → netboot-0.3.1}/tests/test_render.py +22 -22
- netboot-0.3.1/tests/test_shell_template.py +257 -0
- netboot-0.3.1/tests/test_template_registry.py +150 -0
- netboot-0.2.4/docs/extending.md +0 -77
- netboot-0.2.4/examples/templates/debian/boot.cfg +0 -6
- netboot-0.2.4/src/netboot/dhcp/windhcp.py +0 -249
- netboot-0.2.4/src/netboot/templates/common.py +0 -50
- netboot-0.2.4/src/netboot/templates/jinja.py +0 -24
- netboot-0.2.4/src/netboot/templates/shell.py +0 -85
- netboot-0.2.4/tests/test_dhcp_windhcp.py +0 -230
- {netboot-0.2.4 → netboot-0.3.1}/.gitignore +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/LICENSE +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/benchmarks/README.md +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/benchmarks/bench_netboot.py +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/benchmarks/results/netboot.json +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/docs/changelog.md +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/docs/cli.md +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/docs/index.md +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/examples/config/pixie.yaml +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/examples/plugins/recording.py +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/examples/templates/debian/install.ks.j2 +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/mkdocs.yml +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/src/netboot/__init__.py +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/src/netboot/__main__.py +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/src/netboot/_version.py +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/src/netboot/cmds/__init__.py +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/src/netboot/cmds/complete.py +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/src/netboot/cmds/initiate.py +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/src/netboot/content/__init__.py +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/src/netboot/logging.py +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/src/netboot/main.py +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/src/netboot/py.typed +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/src/netboot/utils/__init__.py +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/src/netboot/utils/config.py +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/src/netboot/utils/dicts.py +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/src/netboot/utils/misc.py +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/src/netboot/utils/net.py +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/tests/conftest.py +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/tests/test_api_contracts.py +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/tests/test_cli.py +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/tests/test_cli_discovery.py +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/tests/test_config_discovery.py +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/tests/test_content.py +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/tests/test_dhcp.py +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/tests/test_dhcp_dhcpd.py +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/tests/test_dhcp_dnsmasq.py +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/tests/test_dhcp_kea.py +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/tests/test_dhcp_options.py +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/tests/test_engine_robustness.py +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/tests/test_hooks.py +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/tests/test_lookup.py +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/tests/test_target.py +0 -0
- {netboot-0.2.4 → netboot-0.3.1}/tests/test_utils.py +0 -0
|
@@ -36,7 +36,15 @@ python -m venv .venv/3.14-nt-amd64
|
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
Install **every extra that has tests**: without `http` the repository-service
|
|
39
|
-
tests skip rather than run, and `pytest -rs` is how you see that happen.
|
|
39
|
+
tests skip rather than run, and `pytest -rs` is how you see that happen. The
|
|
40
|
+
`dev` extra pulls in the optional template engines' libraries (`mako`,
|
|
41
|
+
`python-liquid`, `pybars3`, `chevron`) for the same reason.
|
|
42
|
+
|
|
43
|
+
`tests/test_external_engines.py` needs **`ruby`** and **`puppet`**, which this
|
|
44
|
+
Windows box does not have: those cases skip here and run under WSL
|
|
45
|
+
(`FedoraLinux-44` has both). A change to `.erb`/`.epp` rendering is unverified
|
|
46
|
+
until the suite has run there — the engine-shape cases (registration, suffixes,
|
|
47
|
+
the missing-program error) do run everywhere.
|
|
40
48
|
|
|
41
49
|
## Commands
|
|
42
50
|
|
|
@@ -4,7 +4,115 @@ All notable changes to this project are documented here. The format is based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
|
|
5
5
|
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
-
## [
|
|
7
|
+
## [0.3.1] - 2026-10-02
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **`windhcp://` could not set a multi-valued option** (two name servers, two NTP
|
|
11
|
+
servers) with its default PowerShell method, and the failure took the rest of
|
|
12
|
+
the reservation with it. The value was sent comma-joined, and
|
|
13
|
+
`Set-DhcpServerv4OptionValue -Value` takes `String[]`: Windows answered
|
|
14
|
+
"Parameters for option value to be set for option ID 6 do not match with option
|
|
15
|
+
definition" and, under `ErrorActionPreference = 'Stop'`, every option after it
|
|
16
|
+
was abandoned — so the target got a router and **no boot file**. Values are now
|
|
17
|
+
sent as a list. Present since the backend shipped in 0.2.0; found by running it
|
|
18
|
+
against a real Windows Server 2025 DHCP server rather than reading the script.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- **`DhcpServer.extras(ctx, phase)`** — one documented extension point for
|
|
22
|
+
backend-native fragments netboot does not model: an extra statement, an extra
|
|
23
|
+
command, or a condition such as the iPXE chainload (serve the iPXE binary to a
|
|
24
|
+
PXE ROM, the script to iPXE). It defaults to the configured `raw.<backend>`
|
|
25
|
+
fragments, and a subclass overrides it to decide per target. All four shipped
|
|
26
|
+
backends consume it, and `raw.<backend>.remove=` is new: a fragment that
|
|
27
|
+
belongs to teardown rather than to arming. For `windhcp://` a fragment may be a
|
|
28
|
+
PowerShell line or a `{"Args": [...], "Ignore": bool}` netsh command, which
|
|
29
|
+
works under either `method`.
|
|
30
|
+
- `method=netsh` **verifies what the server actually applied**. netsh exits 0 and
|
|
31
|
+
prints its success line even when it rejected what it was told — measured: a
|
|
32
|
+
`domain-name-servers` option with one unreachable address keeps the other, says
|
|
33
|
+
"not a valid DNS Server", says "Command completed successfully", exits 0. So
|
|
34
|
+
after applying, netboot reads the state back with `netsh ... dump` (the one
|
|
35
|
+
netsh output that is command syntax rather than localised prose) and fails
|
|
36
|
+
naming any option that is missing or short of values. The cmdlet method needs
|
|
37
|
+
none of this: Windows raises there by itself.
|
|
38
|
+
- `windhcp://` takes **`method=powershell|netsh`**. The default is unchanged (the
|
|
39
|
+
`DhcpServer` cmdlets); `method=netsh` drives `netsh dhcp server ...` instead,
|
|
40
|
+
for a host where that PowerShell module is not installed. It is independent of
|
|
41
|
+
`transport=`, and netsh is invoked from PowerShell with each argument an array
|
|
42
|
+
element, so no value is spliced into a command line. Success is `$LASTEXITCODE`
|
|
43
|
+
rather than netsh's localised success line, a removal tolerates a reservation
|
|
44
|
+
that is not there, option data types are declared per option (unmapped ones as
|
|
45
|
+
`STRING`), and a multi-valued option becomes one argument per value.
|
|
46
|
+
|
|
47
|
+
## [0.3.0] - 2026-10-02
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
- **The shell template engine now claims the `.shtpl` suffix only** (it claimed
|
|
51
|
+
*every* suffix before, as the documented fallback). The suffix picks the engine
|
|
52
|
+
and the stem names the artifact, which is the convention `install.ks.j2`
|
|
53
|
+
already used: rename `boot.cfg` to `boot.cfg.shtpl` and `ctx.render("boot.cfg")`
|
|
54
|
+
keeps working unchanged. A file no engine claims is **copied as is** by the new
|
|
55
|
+
`CopyTemplate`, last in the default `template_types` — so a static `grub.cfg`
|
|
56
|
+
needs no engine, and a file that *does* carry `%{NAME}` placeholders ships them
|
|
57
|
+
unrendered until it is renamed. That one case logs a warning naming the rename.
|
|
58
|
+
Narrowing `template_types` so nothing claims a file raises
|
|
59
|
+
`netboot.templates.TemplateEngineError`, which names the file and every
|
|
60
|
+
engine's suffixes.
|
|
61
|
+
|
|
62
|
+
### Added
|
|
63
|
+
- **Seven more template engines**, each claiming its own suffix and costing
|
|
64
|
+
nothing when unused: `MakoTemplate` (`.mako`, `netboot[mako]`),
|
|
65
|
+
`LiquidTemplate` (`.liquid`, `netboot[liquid]`), `HandlebarsTemplate`
|
|
66
|
+
(`.hbs`/`.handlebars`, `netboot[handlebars]`), `MustacheTemplate` (`.mustache`,
|
|
67
|
+
`netboot[mustache]`), and `ERBTemplate` (`.erb`) / `EppTemplate` (`.epp`),
|
|
68
|
+
which render through the system `ruby` and `puppet epp render` so an existing
|
|
69
|
+
template behaves as its author tested it (`PIXIE_RUBY` / `PIXIE_PUPPET` name
|
|
70
|
+
the program if it is not on `PATH`). Every engine is registered whether or not
|
|
71
|
+
its dependency is present — a claimed file reports what is missing instead of
|
|
72
|
+
being copied — and nothing is imported until a file claims it. What each engine
|
|
73
|
+
does with `templates_undefined` differs by library and is tabulated in the
|
|
74
|
+
configuration guide; handlebars and mustache cannot fail on an undefined name
|
|
75
|
+
at all.
|
|
76
|
+
- `template_data()` / `DataView` / `jsonable()` — a lazy mapping view of the
|
|
77
|
+
render context for engines that read data rather than evaluate Python
|
|
78
|
+
(`{{ target.hostname }}` beside `{{ ctx.target.hostname }}`), and its
|
|
79
|
+
JSON-serialisable form for the subprocess engines.
|
|
80
|
+
- `SubprocessTemplate` — the base for an engine that renders through another
|
|
81
|
+
language's own tooling: program lookup with an env override, a temporary
|
|
82
|
+
template and values file, a timeout, and the program's stderr in the error.
|
|
83
|
+
- `ShellTemplate` is configured by subclassing, through three class attributes:
|
|
84
|
+
`EXT` (a string or a sequence, dots optional, case-insensitive; `None` or
|
|
85
|
+
`"*"` claims any suffix), `DELIMITER` (default `"%"`) and `PATTERN` —
|
|
86
|
+
`"braced"` (default, `%{NAME}`), `"unbraced"` (`%NAME`) or `"all"`.
|
|
87
|
+
- POSIX default expressions in the braced form: `%{NAME:-fallback}` substitutes
|
|
88
|
+
the fallback when the value is unset *or* empty, `%{NAME-fallback}` only when
|
|
89
|
+
it is unset. One layer of `'`/`"` quotes is stripped, the fallback is literal
|
|
90
|
+
text, and a placeholder with a default never fails whatever
|
|
91
|
+
`templates_undefined` says. `:=`, `:?` and `:+` are not implemented and stay
|
|
92
|
+
literal text.
|
|
93
|
+
- `CopyTemplate` — the copy-as-is engine described above, working in **bytes**:
|
|
94
|
+
the loader no longer decodes a file before knowing which engine claims it, so
|
|
95
|
+
the copy is byte-exact for anything (CRLF, latin-1, a binary) and
|
|
96
|
+
`ctx.render()` returns `bytes` for such a file. `EXT` narrows it to chosen
|
|
97
|
+
suffixes; `Template.BINARY` is the flag any engine can set to be handed raw
|
|
98
|
+
bytes. A non-UTF-8 file claimed by a *text* engine now raises
|
|
99
|
+
`TemplateEngineError` naming the file, where it used to fail inside the read.
|
|
100
|
+
- `Loader.find_source()` — the byte-returning counterpart of `get_source()`
|
|
101
|
+
(which keeps jinja2's text contract).
|
|
102
|
+
- **A template-engine registry**: `register_template_type()` (bare or as a
|
|
103
|
+
decorator, with an optional `priority=`), `unregister_template_type()`,
|
|
104
|
+
`TEMPLATE_TYPES` and `by_priority()`. `Loader(template_types=None)` — the new
|
|
105
|
+
default — uses every registered engine, so a `--load-module` plugin is picked
|
|
106
|
+
up without rebuilding the list, and the list is snapshotted at construction.
|
|
107
|
+
- **`Template.PRIORITY`**, so engine selection does not depend on registration
|
|
108
|
+
order: engines are consulted highest first, `DEFAULT_PRIORITY` (0) for an
|
|
109
|
+
ordinary engine and `FALLBACK_PRIORITY` (-100) for `CopyTemplate`. A plugin is
|
|
110
|
+
registered after the shipped engines, so without this every plugin would sit
|
|
111
|
+
behind the catch-all and never see a file. Equal priorities keep the order they
|
|
112
|
+
were given, so an explicit `template_types` list still means what it says.
|
|
113
|
+
- `Template.EXT` and `JinjaTemplate.EXT`, so suffix ownership is one declarative
|
|
114
|
+
attribute per engine, plus `netboot.templates.template_extensions()` to read it
|
|
115
|
+
back normalised.
|
|
8
116
|
|
|
9
117
|
## [0.2.4] - 2026-09-28
|
|
10
118
|
|
|
@@ -563,7 +671,9 @@ First packaged release: the `netboot` library with the `pixie` command line.
|
|
|
563
671
|
config value construction no longer swallows non-`TypeError` errors; repo
|
|
564
672
|
`joinpath` keeps `.local` a path so chained joins work.
|
|
565
673
|
|
|
566
|
-
[Unreleased]: https://github.com/jose-pr/netboot/compare/v0.
|
|
674
|
+
[Unreleased]: https://github.com/jose-pr/netboot/compare/v0.3.1...HEAD
|
|
675
|
+
[0.3.1]: https://github.com/jose-pr/netboot/compare/v0.3.0...v0.3.1
|
|
676
|
+
[0.3.0]: https://github.com/jose-pr/netboot/compare/v0.2.4...v0.3.0
|
|
567
677
|
[0.2.4]: https://github.com/jose-pr/netboot/compare/v0.2.3...v0.2.4
|
|
568
678
|
[0.2.3]: https://github.com/jose-pr/netboot/compare/v0.2.2...v0.2.3
|
|
569
679
|
[0.2.2]: https://github.com/jose-pr/netboot/compare/v0.2.1...v0.2.2
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: netboot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: PXE provisioning management: render netboot artifacts and drive DHCP for targets
|
|
5
5
|
Project-URL: Homepage, https://github.com/jose-pr/netboot/
|
|
6
6
|
Project-URL: Issues, https://github.com/jose-pr/netboot/issues
|
|
@@ -35,11 +35,15 @@ Requires-Dist: pyyaml; extra == 'config'
|
|
|
35
35
|
Provides-Extra: dev
|
|
36
36
|
Requires-Dist: black; extra == 'dev'
|
|
37
37
|
Requires-Dist: build; extra == 'dev'
|
|
38
|
+
Requires-Dist: chevron; extra == 'dev'
|
|
38
39
|
Requires-Dist: dnspython; extra == 'dev'
|
|
39
40
|
Requires-Dist: hatchling; extra == 'dev'
|
|
41
|
+
Requires-Dist: mako; extra == 'dev'
|
|
42
|
+
Requires-Dist: pybars3; extra == 'dev'
|
|
40
43
|
Requires-Dist: pypureomapi; extra == 'dev'
|
|
41
44
|
Requires-Dist: pytest; extra == 'dev'
|
|
42
45
|
Requires-Dist: pytest-cov; extra == 'dev'
|
|
46
|
+
Requires-Dist: python-liquid; extra == 'dev'
|
|
43
47
|
Requires-Dist: pywinrm; extra == 'dev'
|
|
44
48
|
Requires-Dist: pyyaml; extra == 'dev'
|
|
45
49
|
Requires-Dist: requests; extra == 'dev'
|
|
@@ -52,10 +56,18 @@ Provides-Extra: docs
|
|
|
52
56
|
Requires-Dist: mkdocs; extra == 'docs'
|
|
53
57
|
Requires-Dist: mkdocs-material; extra == 'docs'
|
|
54
58
|
Requires-Dist: mkdocstrings[python]; extra == 'docs'
|
|
59
|
+
Provides-Extra: handlebars
|
|
60
|
+
Requires-Dist: pybars3; extra == 'handlebars'
|
|
55
61
|
Provides-Extra: http
|
|
56
62
|
Requires-Dist: requests; extra == 'http'
|
|
57
63
|
Provides-Extra: kea
|
|
58
64
|
Requires-Dist: requests; extra == 'kea'
|
|
65
|
+
Provides-Extra: liquid
|
|
66
|
+
Requires-Dist: python-liquid; extra == 'liquid'
|
|
67
|
+
Provides-Extra: mako
|
|
68
|
+
Requires-Dist: mako; extra == 'mako'
|
|
69
|
+
Provides-Extra: mustache
|
|
70
|
+
Requires-Dist: chevron; extra == 'mustache'
|
|
59
71
|
Provides-Extra: ssh
|
|
60
72
|
Requires-Dist: pathlib-next[sftp]; extra == 'ssh'
|
|
61
73
|
Provides-Extra: winrm
|
|
@@ -99,11 +111,21 @@ Optional extras:
|
|
|
99
111
|
| `netboot[dns]` | The `dnspython` resolver backend (best coverage; without it hostname targets still resolve via the system/`nslookup` fallbacks) |
|
|
100
112
|
| `netboot[http]` | `http`/`https` repository services (`requests`); `file`/`tftp` repos and rendering need nothing extra |
|
|
101
113
|
| `netboot[kea]` | The `kea://` DHCP backend (`requests`) |
|
|
114
|
+
| `netboot[mako]` | `*.mako` templates (`mako`) |
|
|
115
|
+
| `netboot[liquid]` | `*.liquid` templates (`python-liquid`) |
|
|
116
|
+
| `netboot[handlebars]` | `*.hbs` / `*.handlebars` templates (`pybars3`) |
|
|
117
|
+
| `netboot[mustache]` | `*.mustache` templates (`chevron`) |
|
|
102
118
|
| `netboot[dhcpd]` | The `dhcpd://` DHCP backend over OMAPI (`pypureomapi`) |
|
|
103
119
|
| `netboot[winrm]` | `windhcp://` over WinRM (`pywinrm`); over ssh it needs nothing |
|
|
104
120
|
| `netboot[ssh]` | `dnsmasq://` with remote `sftp://` paths (`pathlib_next[sftp]`) |
|
|
105
121
|
| `netboot[docs]` | Build the documentation site (`mkdocs`) |
|
|
106
122
|
|
|
123
|
+
`*.erb` and `*.epp` templates need no extra, but do need `ruby` or `puppet`
|
|
124
|
+
installed: they render through the real program, so an existing template behaves
|
|
125
|
+
as its author tested it. `PIXIE_RUBY` / `PIXIE_PUPPET` name it when it is not on
|
|
126
|
+
`PATH`. Jinja2 (`.j2`) and the `%{NAME}` shell engine (`.shtpl`) are built in, and
|
|
127
|
+
a file no engine claims is copied unchanged.
|
|
128
|
+
|
|
107
129
|
Built on [`duho`](https://github.com/jose-pr/duho) (CLI/args/command discovery),
|
|
108
130
|
[`pathlib_next`](https://github.com/jose-pr/pathlib-next) (URI-aware paths),
|
|
109
131
|
[`yaconfiglib`](https://github.com/jose-pr/yaconfiglib) (layered YAML), and
|
|
@@ -175,7 +197,9 @@ python -m venv .venv/3.14
|
|
|
175
197
|
```
|
|
176
198
|
|
|
177
199
|
Install every extra that has tests — without `http` the repository-service tests
|
|
178
|
-
skip instead of running, which `-rs` makes visible
|
|
200
|
+
skip instead of running, which `-rs` makes visible (`dev` carries the template
|
|
201
|
+
engines' libraries for the same reason; the `.erb`/`.epp` tests skip unless
|
|
202
|
+
`ruby`/`puppet` are installed). Develop on the latest Python
|
|
179
203
|
and also run the floor (3.9) before finishing a piece of work. More detail, plus
|
|
180
204
|
the layout and CI notes, in
|
|
181
205
|
[`AGENTS.md`](https://github.com/jose-pr/netboot/blob/main/AGENTS.md).
|
|
@@ -35,11 +35,21 @@ Optional extras:
|
|
|
35
35
|
| `netboot[dns]` | The `dnspython` resolver backend (best coverage; without it hostname targets still resolve via the system/`nslookup` fallbacks) |
|
|
36
36
|
| `netboot[http]` | `http`/`https` repository services (`requests`); `file`/`tftp` repos and rendering need nothing extra |
|
|
37
37
|
| `netboot[kea]` | The `kea://` DHCP backend (`requests`) |
|
|
38
|
+
| `netboot[mako]` | `*.mako` templates (`mako`) |
|
|
39
|
+
| `netboot[liquid]` | `*.liquid` templates (`python-liquid`) |
|
|
40
|
+
| `netboot[handlebars]` | `*.hbs` / `*.handlebars` templates (`pybars3`) |
|
|
41
|
+
| `netboot[mustache]` | `*.mustache` templates (`chevron`) |
|
|
38
42
|
| `netboot[dhcpd]` | The `dhcpd://` DHCP backend over OMAPI (`pypureomapi`) |
|
|
39
43
|
| `netboot[winrm]` | `windhcp://` over WinRM (`pywinrm`); over ssh it needs nothing |
|
|
40
44
|
| `netboot[ssh]` | `dnsmasq://` with remote `sftp://` paths (`pathlib_next[sftp]`) |
|
|
41
45
|
| `netboot[docs]` | Build the documentation site (`mkdocs`) |
|
|
42
46
|
|
|
47
|
+
`*.erb` and `*.epp` templates need no extra, but do need `ruby` or `puppet`
|
|
48
|
+
installed: they render through the real program, so an existing template behaves
|
|
49
|
+
as its author tested it. `PIXIE_RUBY` / `PIXIE_PUPPET` name it when it is not on
|
|
50
|
+
`PATH`. Jinja2 (`.j2`) and the `%{NAME}` shell engine (`.shtpl`) are built in, and
|
|
51
|
+
a file no engine claims is copied unchanged.
|
|
52
|
+
|
|
43
53
|
Built on [`duho`](https://github.com/jose-pr/duho) (CLI/args/command discovery),
|
|
44
54
|
[`pathlib_next`](https://github.com/jose-pr/pathlib-next) (URI-aware paths),
|
|
45
55
|
[`yaconfiglib`](https://github.com/jose-pr/yaconfiglib) (layered YAML), and
|
|
@@ -111,7 +121,9 @@ python -m venv .venv/3.14
|
|
|
111
121
|
```
|
|
112
122
|
|
|
113
123
|
Install every extra that has tests — without `http` the repository-service tests
|
|
114
|
-
skip instead of running, which `-rs` makes visible
|
|
124
|
+
skip instead of running, which `-rs` makes visible (`dev` carries the template
|
|
125
|
+
engines' libraries for the same reason; the `.erb`/`.epp` tests skip unless
|
|
126
|
+
`ruby`/`puppet` are installed). Develop on the latest Python
|
|
115
127
|
and also run the floor (3.9) before finishing a piece of work. More detail, plus
|
|
116
128
|
the layout and CI notes, in
|
|
117
129
|
[`AGENTS.md`](https://github.com/jose-pr/netboot/blob/main/AGENTS.md).
|
|
@@ -39,8 +39,22 @@ The engine and its context objects. Rendered from source via
|
|
|
39
39
|
|
|
40
40
|
::: netboot.templates.JinjaTemplate
|
|
41
41
|
|
|
42
|
+
::: netboot.templates.MakoTemplate
|
|
43
|
+
|
|
44
|
+
::: netboot.templates.LiquidTemplate
|
|
45
|
+
|
|
46
|
+
::: netboot.templates.HandlebarsTemplate
|
|
47
|
+
|
|
48
|
+
::: netboot.templates.MustacheTemplate
|
|
49
|
+
|
|
50
|
+
::: netboot.templates.ERBTemplate
|
|
51
|
+
|
|
52
|
+
::: netboot.templates.EppTemplate
|
|
53
|
+
|
|
42
54
|
::: netboot.templates.ShellTemplate
|
|
43
55
|
|
|
56
|
+
::: netboot.templates.CopyTemplate
|
|
57
|
+
|
|
44
58
|
## Utilities
|
|
45
59
|
|
|
46
60
|
::: netboot.utils.dicts
|
|
@@ -60,7 +60,7 @@ DHCP options the server should give the client.
|
|
|
60
60
|
| `dnsmasq://` | dnsmasq | writes `dhcp-host`/`dhcp-option` files, local or over `sftp://` | nothing locally; `netboot[ssh]` for remote paths |
|
|
61
61
|
| `kea://`, `keas://` | ISC Kea | `reservation-add` / `reservation-del` via the control agent | `netboot[kea]` |
|
|
62
62
|
| `dhcpd://` | ISC dhcpd | an OMAPI host object | `netboot[dhcpd]` |
|
|
63
|
-
| `windhcp://` | Windows DHCP | its
|
|
63
|
+
| `windhcp://` | Windows DHCP | its PowerShell cmdlets *or* `netsh`, over ssh or WinRM | nothing over ssh; `netboot[winrm]` for WinRM |
|
|
64
64
|
|
|
65
65
|
Each has one thing that is easy to get wrong:
|
|
66
66
|
|
|
@@ -72,8 +72,35 @@ Each has one thing that is easy to get wrong:
|
|
|
72
72
|
file-only Kea loads the hook and still refuses.
|
|
73
73
|
- **dhcpd** hosts added over OMAPI do not survive a restart by themselves — that
|
|
74
74
|
is dhcpd's design.
|
|
75
|
-
- **Windows**
|
|
76
|
-
|
|
75
|
+
- **Windows** validates some option values as it stores them, whichever method
|
|
76
|
+
you use: a name server that does not answer is refused rather than stored. An
|
|
77
|
+
arm that fails part-way leaves the reservation with the options applied so far
|
|
78
|
+
— the next successful arm fixes it, and `pixie complete` removes it.
|
|
79
|
+
- **Windows** needs an account with DHCP-administrator rights, and — for the
|
|
80
|
+
default `method=powershell` — the `DhcpServer` module on the host the shell runs
|
|
81
|
+
on. Where that module is missing (Server Core without the RSAT feature, or an
|
|
82
|
+
older release) `method=netsh` drives `netsh dhcp server ...` instead. The two
|
|
83
|
+
choices are independent: `transport=` is how netboot gets a shell (`ssh` or
|
|
84
|
+
`winrm`), `method=` is what it runs there.
|
|
85
|
+
|
|
86
|
+
netsh is still invoked *from* PowerShell, with each argument an element of a
|
|
87
|
+
JSON array — so no value is ever spliced into a command line, which is the same
|
|
88
|
+
guarantee the cmdlet path gives. Extra commands (a policy, say) go through
|
|
89
|
+
`raw.windhcp=` or a subclass's `extras()`; see
|
|
90
|
+
[Extra commands and conditions](extending.md#extra-commands-and-conditions),
|
|
91
|
+
which is also where the iPXE chainload recipe lives.
|
|
92
|
+
|
|
93
|
+
**netsh cannot be trusted to report failure.** Given an option value it dislikes
|
|
94
|
+
— a name server that does not answer — it keeps the rest, prints "not a valid DNS
|
|
95
|
+
Server", prints "Command completed successfully" and exits 0 (measured on
|
|
96
|
+
Windows Server 2025). So netboot reads the state back afterwards with
|
|
97
|
+
`netsh ... dump`, the one netsh output that is command syntax rather than
|
|
98
|
+
localised prose, and fails naming any option that did not land or lost values.
|
|
99
|
+
The cmdlet method needs no such check: Windows raises there by itself. A few behaviours are
|
|
100
|
+
netsh's own rather than netboot's: an option's data type has to be declared
|
|
101
|
+
(netboot maps the modelled options, and sends anything else as `STRING`), a
|
|
102
|
+
multi-valued option is passed as one argument per value, and the reservation is
|
|
103
|
+
created as `BOTH` (DHCP and BOOTP) to match what the cmdlets do by default.
|
|
77
104
|
|
|
78
105
|
### Options
|
|
79
106
|
|
|
@@ -176,11 +203,90 @@ not a mapping is an error naming the file.
|
|
|
176
203
|
|
|
177
204
|
`templates` is a list of search paths (local or URI). For each render netboot looks
|
|
178
205
|
for a file named by the target's MAC (`aa-bb-cc-...`), hostname, or IP — falling
|
|
179
|
-
back to the bare template name.
|
|
180
|
-
|
|
181
|
-
|
|
206
|
+
back to the bare template name.
|
|
207
|
+
|
|
208
|
+
**The suffix picks the engine, and the stem names the artifact.** A `.j2` /
|
|
209
|
+
`.jinja` / `.jinja2` file is rendered with Jinja2; a `.shtpl` file with the
|
|
210
|
+
`%`-delimited shell engine, whose `%{UPPER_SNAKE}` placeholders come from the
|
|
211
|
+
flattened context. Seven more engines ship for template trees that already exist
|
|
212
|
+
in another language:
|
|
213
|
+
|
|
214
|
+
| Engine | Suffixes | Needs | `templates_undefined` |
|
|
215
|
+
| ------ | -------- | ----- | --------------------- |
|
|
216
|
+
| `JinjaTemplate` | `.j2` `.jinja` `.jinja2` | — | all three |
|
|
217
|
+
| `MakoTemplate` | `.mako` | `netboot[mako]` | `strict`, `lenient`; `debug` behaves as `lenient` |
|
|
218
|
+
| `LiquidTemplate` | `.liquid` | `netboot[liquid]` | all three (`debug` names the variable in the output) |
|
|
219
|
+
| `HandlebarsTemplate` | `.hbs` `.handlebars` | `netboot[handlebars]` | **always lenient** — the library cannot fail |
|
|
220
|
+
| `MustacheTemplate` | `.mustache` | `netboot[mustache]` | **always lenient** (logs under `strict`) |
|
|
221
|
+
| `ERBTemplate` | `.erb` | `ruby` on `PATH` (or `PIXIE_RUBY`) | n/a — missing data is Ruby's `nil` |
|
|
222
|
+
| `EppTemplate` | `.epp` | `puppet` on `PATH` (or `PIXIE_PUPPET`) | n/a — missing data is Puppet's `undef` |
|
|
223
|
+
| `ShellTemplate` | `.shtpl` | — | all three |
|
|
224
|
+
| `CopyTemplate` | anything else | — | n/a — nothing is substituted |
|
|
225
|
+
|
|
226
|
+
Every optional engine is **registered whether or not its library is installed**,
|
|
227
|
+
so a `.liquid` file tells you to `pip install netboot[liquid]` instead of being
|
|
228
|
+
quietly copied. None of them is imported until a file claims it, so an install
|
|
229
|
+
that uses none pays nothing.
|
|
230
|
+
|
|
231
|
+
Jinja and mako evaluate Python, so a template reaches into `ctx` directly. The
|
|
232
|
+
data languages (liquid, handlebars, mustache) and the external ones (ERB, EPP)
|
|
233
|
+
get a **mapping view** of the same context instead: `{{ ctx.target.hostname }}`
|
|
234
|
+
and `{{ target.hostname }}` both resolve, an address or a path arrives as the
|
|
235
|
+
string a template would have printed, and the engine's own machinery
|
|
236
|
+
(`ctx._netboot_`, the renderer) is left out. ERB additionally sets each top-level
|
|
237
|
+
name as an instance variable (`@target`), and EPP as a parameter (`$target`).
|
|
238
|
+
|
|
239
|
+
ERB and EPP run the real `ruby` and `puppet` as a subprocess, with the context
|
|
240
|
+
marshalled to JSON — which is the point: an existing `.erb` renders the way its
|
|
241
|
+
author tested it, rather than the way a reimplementation guesses. Set `PIXIE_RUBY`
|
|
242
|
+
or `PIXIE_PUPPET` if the program is installed but not on `PATH`. Because a template is also found by its stem, the file behind
|
|
243
|
+
an artifact called `boot.cfg` is `boot.cfg.shtpl` and `ctx.render("boot.cfg")`
|
|
244
|
+
still finds it.
|
|
245
|
+
|
|
246
|
+
Anything else is **copied as is** — a static `grub.cfg`, an EFI binary or a
|
|
247
|
+
license file is a template that needs no engine. The copy engine works in
|
|
248
|
+
**bytes**: the loader never decodes the file, so the result is byte-exact
|
|
249
|
+
whatever it holds (CRLF line endings, latin-1 text, something that is not text at
|
|
250
|
+
all) and `ctx.render()` returns `bytes` for it rather than `str`.
|
|
251
|
+
|
|
252
|
+
Before 0.3.0 the shell engine claimed every suffix, so a file that *does* carry
|
|
253
|
+
`%{NAME}` placeholders now ships them unrendered: rename it to `*.shtpl` once.
|
|
254
|
+
That is the one mistake a copy can hide, so a copy that still finds placeholders
|
|
255
|
+
logs a warning naming the rename.
|
|
182
256
|
|
|
183
257
|
Only the braced form is substituted, so a bare `%word` is left alone — a kickstart
|
|
184
258
|
file keeps its `%packages`, `%pre`, `%post` and `%end` sections, and a script keeps
|
|
185
259
|
`date +%Y`. Write `%%` for a literal `%` next to a brace, `%{NAME}` to substitute.
|
|
186
|
-
An unknown `%{NAME}`
|
|
260
|
+
An unknown `%{NAME}` follows `templates_undefined` (above).
|
|
261
|
+
|
|
262
|
+
`%{NAME:-fallback}` renders `fallback` when `NAME` is unset **or empty**, and
|
|
263
|
+
`%{NAME-fallback}` only when it is unset — the two POSIX forms, so a template can
|
|
264
|
+
carry its own default instead of requiring the variable. One layer of `'`/`"`
|
|
265
|
+
quotes is stripped (`%{NAME:-'a default'}`), the fallback is literal text (no
|
|
266
|
+
nested placeholders, and no `}` inside it), and a placeholder that has a default
|
|
267
|
+
never fails whatever `templates_undefined` says. The other POSIX forms (`:=`,
|
|
268
|
+
`:?`, `:+`) are not implemented and stay literal text.
|
|
269
|
+
|
|
270
|
+
The engine is configured by subclassing, not by config, for a tree that uses
|
|
271
|
+
another convention:
|
|
272
|
+
|
|
273
|
+
```python
|
|
274
|
+
from netboot.templates.shell import ShellTemplate
|
|
275
|
+
|
|
276
|
+
class DollarTemplate(ShellTemplate):
|
|
277
|
+
EXT = (".tpl", ".cfg") # a single string is fine; `None` claims any suffix
|
|
278
|
+
DELIMITER = "$"
|
|
279
|
+
PATTERN = "all" # 'braced' (default), 'unbraced', or 'all'
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Register the class with `netboot.templates.register_template_type` (see
|
|
283
|
+
[Extending](extending.md#custom-template-engines)) or pass it in
|
|
284
|
+
`Loader(..., template_types=[...])`. Engines are consulted in `PRIORITY` order,
|
|
285
|
+
highest first, so a registered engine is asked before the catch-all
|
|
286
|
+
`CopyTemplate` whatever order they were registered in. The default is every
|
|
287
|
+
registered engine: `JinjaTemplate`, `ShellTemplate`, then `CopyTemplate`. Narrow
|
|
288
|
+
it — dropping the copy engine, or giving it a suffix list of its own — and a file
|
|
289
|
+
nothing claims raises `netboot.templates.TemplateEngineError`, naming the file and
|
|
290
|
+
what each engine handles. A file that is not valid UTF-8 raises the same error when the engine
|
|
291
|
+
that claims it needs text; set `BINARY = True` on an engine to be handed the raw
|
|
292
|
+
bytes instead, as `CopyTemplate` does.
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# Extending
|
|
2
|
+
|
|
3
|
+
## Event hooks
|
|
4
|
+
|
|
5
|
+
Pass `hooks=[...]` to `Pixie(...)` — each entry is a callable or a
|
|
6
|
+
`"module.function"` import string. A hook
|
|
7
|
+
|
|
8
|
+
```python
|
|
9
|
+
def my_hook(event, netboot, value, kwargs):
|
|
10
|
+
...
|
|
11
|
+
return value
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
is invoked for every `PixieEvent` and may transform the value flowing through it.
|
|
15
|
+
Events fire around object creation (`NewPixieObject`, `SetPixieProperty`,
|
|
16
|
+
`PixieInitiated`), lookup (`LookupTarget`, `FoundTarget`, `FoundTargetImage`,
|
|
17
|
+
`FoundTargetDhcpzone`), context construction (`PixieContextForTarget`), and the
|
|
18
|
+
init/complete lifecycle (`StartPixieInitialize` … `EndPixieComplete`). This is the
|
|
19
|
+
seam for customising how targets/images/zones are resolved and how the render
|
|
20
|
+
context is assembled.
|
|
21
|
+
|
|
22
|
+
### The contract
|
|
23
|
+
|
|
24
|
+
- **Always return.** Every hook is handed the previous hook's return value, and
|
|
25
|
+
the last return value is what netboot uses. A hook that falls off the end
|
|
26
|
+
returns `None`, which is then what the engine gets — a lookup hook that
|
|
27
|
+
forgets to `return value` turns every lookup into "not found".
|
|
28
|
+
- **The signature is positional**, and `kwargs` arrives as a plain `dict` (the
|
|
29
|
+
fourth argument), not as `**kwargs`.
|
|
30
|
+
- **`netboot` is `None` for `NewPixieObject`**, which fires before the instance
|
|
31
|
+
exists; `value` there is the class about to be instantiated, and returning a
|
|
32
|
+
subclass is how you swap in your own.
|
|
33
|
+
- **`value` and `kwargs` differ per event.** `SetPixieProperty` gets a
|
|
34
|
+
`(name, value)` tuple plus `origin=`/`rawvalue=`; the lookup events get the
|
|
35
|
+
object found (or the query, for `LookupTarget`) plus `target=`; the lifecycle
|
|
36
|
+
events get the target, then the built context.
|
|
37
|
+
- **`PixieEvent` is a string enum whose values are prefixed** — the value of
|
|
38
|
+
`PixieEvent.LookupTarget` is the string `"PixieEvent.LookupTarget"`. Compare
|
|
39
|
+
against the enum member, not a bare name.
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from netboot import PixieEvent
|
|
43
|
+
|
|
44
|
+
def only_on_lookup(event, netboot, value, kwargs):
|
|
45
|
+
if event is not PixieEvent.FoundTarget:
|
|
46
|
+
return value # pass everything else through
|
|
47
|
+
return value or fallback_target()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Custom template engines
|
|
51
|
+
|
|
52
|
+
An engine is a class with two things: the suffixes it claims (`EXT`) and
|
|
53
|
+
`render()`. Register it and it joins the engines every `Loader` consults.
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from netboot.templates import Template, register_template_type
|
|
57
|
+
|
|
58
|
+
@register_template_type
|
|
59
|
+
class MakoTemplate(Template):
|
|
60
|
+
EXT = ".mako" # a string, or a sequence of them
|
|
61
|
+
|
|
62
|
+
def __init__(self, template: str) -> None:
|
|
63
|
+
self._source = template
|
|
64
|
+
|
|
65
|
+
def render(self, **extras):
|
|
66
|
+
ctx = self._globals_["ctx"] # the PixieContext being rendered
|
|
67
|
+
return my_mako_render(self._source, ctx)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Import the module before anything renders — `--load-module your.plugin`, the same
|
|
71
|
+
flag a DHCP backend plugin needs.
|
|
72
|
+
|
|
73
|
+
**Priority, not registration order, decides who gets a file.** Engines are
|
|
74
|
+
consulted highest `PRIORITY` first, and a plugin is necessarily registered
|
|
75
|
+
*after* the shipped engines — if order decided, every plugin would sit behind the
|
|
76
|
+
catch-all `CopyTemplate` and never see a file. So:
|
|
77
|
+
|
|
78
|
+
| `PRIORITY` | Meaning |
|
|
79
|
+
| ---------- | ------- |
|
|
80
|
+
| `DEFAULT_PRIORITY` (`0`) | an ordinary engine claiming its own suffixes — the default, and enough to outrank the copy engine |
|
|
81
|
+
| above `0` | override a shipped engine on a suffix it also claims (`priority=10` to take `.j2` from `JinjaTemplate`) |
|
|
82
|
+
| `FALLBACK_PRIORITY` (`-100`) | a last resort, where `CopyTemplate` sits |
|
|
83
|
+
|
|
84
|
+
Engines of *equal* priority keep the order they were given, so an explicit
|
|
85
|
+
`Loader(..., template_types=[...])` still means what it says. Set the priority on
|
|
86
|
+
the class, or at registration for a class you do not own:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
register_template_type(SomeonesEngine, priority=10)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`unregister_template_type(cls)` removes one again, and a `Loader` snapshots the
|
|
93
|
+
registry when it is built, so importing a plugin halfway through a run never
|
|
94
|
+
changes an engine already in use.
|
|
95
|
+
|
|
96
|
+
Other attributes on the contract: **`BINARY = True`** to be handed the file's raw
|
|
97
|
+
`bytes` instead of decoded text (and to return bytes, as `CopyTemplate` does),
|
|
98
|
+
and `EXT = None` to claim *every* suffix — which only makes sense together with a
|
|
99
|
+
low priority.
|
|
100
|
+
|
|
101
|
+
The shipped engines are the worked examples, and they cover the three shapes an
|
|
102
|
+
engine takes:
|
|
103
|
+
|
|
104
|
+
- **evaluates Python** (`JinjaTemplate`, `MakoTemplate`) — hand the engine `ctx`
|
|
105
|
+
itself and let the template reach into it.
|
|
106
|
+
- **reads data only** (`LiquidTemplate`, `HandlebarsTemplate`,
|
|
107
|
+
`MustacheTemplate`) — build the namespace with
|
|
108
|
+
`netboot.templates.template_data(self._globals_, extras)`, which returns a lazy
|
|
109
|
+
mapping view of the context. liquid cannot traverse attributes at all, so this
|
|
110
|
+
is not optional for that class of engine.
|
|
111
|
+
- **shells out** (`ERBTemplate`, `EppTemplate`) — subclass
|
|
112
|
+
`SubprocessTemplate`, set `COMMAND`, `ENV_VAR` and `EXT`, and implement
|
|
113
|
+
`command(program, template_path, values_path)`. The base marshals the context
|
|
114
|
+
with `jsonable()`, writes template and values into a temporary directory, runs
|
|
115
|
+
the program with a timeout, and turns a non-zero exit into a
|
|
116
|
+
`TemplateEngineError` carrying the program's own stderr.
|
|
117
|
+
|
|
118
|
+
An engine that needs an optional library imports it in a helper, not at module
|
|
119
|
+
scope, and raises `ImportError` naming the extra — that way the class can be
|
|
120
|
+
registered unconditionally, and a file it claims reports what is missing rather
|
|
121
|
+
than falling through to the copy engine.
|
|
122
|
+
|
|
123
|
+
## Custom DHCP backends
|
|
124
|
+
|
|
125
|
+
`netboot.dhcp.DhcpServer` dispatches on the URI scheme of a zone's `dhcpservers`
|
|
126
|
+
entry: a subclass whose lowercased class name equals the scheme handles it.
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from netboot.dhcp import DhcpServer
|
|
130
|
+
|
|
131
|
+
class dnsmasq(DhcpServer): # handles dnsmasq://...
|
|
132
|
+
def add_target(self, ctx):
|
|
133
|
+
... # arm DHCP for ctx.target
|
|
134
|
+
def remove_target(self, ctx):
|
|
135
|
+
... # disarm it
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
netboot ships four backends — `netboot.dhcp.dnsmasq`, `.kea`, `.dhcpd` and
|
|
139
|
+
`.windhcp` — and they are the worked examples: one writes files (locally or over
|
|
140
|
+
`sftp://`), one speaks a REST API, one a binary protocol, and one runs
|
|
141
|
+
PowerShell over ssh or WinRM. A backend gets its client options from
|
|
142
|
+
`self.options_for(ctx)` and translates them; see the configuration guide for
|
|
143
|
+
what an operator writes.
|
|
144
|
+
|
|
145
|
+
Subclassing at any depth is honoured, so a backend may share an intermediate
|
|
146
|
+
base. Import your plugin module before the config builds the zones — pass
|
|
147
|
+
`--load-module your.plugin` (repeat or colon-separate for several) so the
|
|
148
|
+
`DhcpServer` subclass is registered when `dnsmasq://...` is resolved.
|
|
149
|
+
|
|
150
|
+
An unknown scheme raises a clear `ValueError` rather than silently doing nothing.
|
|
151
|
+
|
|
152
|
+
### Extra commands and conditions
|
|
153
|
+
|
|
154
|
+
netboot models the options it can translate everywhere, and `extras()` is the way
|
|
155
|
+
in for everything else — an extra statement, an extra command, or a condition
|
|
156
|
+
that makes the server answer two kinds of client differently. The canonical case
|
|
157
|
+
is the **iPXE chainload**: hand a PXE ROM the iPXE binary, then hand iPXE itself
|
|
158
|
+
the script, or iPXE loads iPXE forever.
|
|
159
|
+
|
|
160
|
+
Two ways in, both ending up in the same list:
|
|
161
|
+
|
|
162
|
+
```yaml
|
|
163
|
+
dhcpservers:
|
|
164
|
+
# from config, per connection
|
|
165
|
+
- dhcpd://key@dhcp01/?raw.dhcpd=if exists user-class and option user-class = "iPXE" { filename "boot.ipxe"; } else { filename "undionly.kpxe"; }
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
# from code, which is the one that can look at the target
|
|
170
|
+
from netboot.dhcp.dhcpd import dhcpd
|
|
171
|
+
|
|
172
|
+
class dhcpd(dhcpd): # still handles dhcpd://
|
|
173
|
+
def extras(self, ctx, phase="add"):
|
|
174
|
+
lines = super().extras(ctx, phase) # keep the config's own fragments
|
|
175
|
+
if phase == "add" and ctx.image.globals.get("ipxe"):
|
|
176
|
+
lines.append(
|
|
177
|
+
'if exists user-class and option user-class = "iPXE" '
|
|
178
|
+
'{ filename "boot.ipxe"; } else { filename "undionly.kpxe"; }'
|
|
179
|
+
)
|
|
180
|
+
return lines
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`phase` is `add` or `remove`. A bare `raw.<backend>=` belongs to `add`;
|
|
184
|
+
`raw.<backend>.remove=` is for a fragment that undoes something at teardown (a
|
|
185
|
+
policy created while arming, say). What a fragment *is* belongs to the backend:
|
|
186
|
+
|
|
187
|
+
| Backend | A fragment is | Where it lands |
|
|
188
|
+
| ------- | ------------- | -------------- |
|
|
189
|
+
| `dhcpd://` | dhcpd config **statements** | appended to the host object's `statements`, after the modelled options — so a conditional written there wins |
|
|
190
|
+
| `dnsmasq://` | config **lines** (`dhcp-match=set:ipxe,77,iPXE`, `dhcp-boot=tag:ipxe,boot.ipxe`) | the options file for that target's tag |
|
|
191
|
+
| `kea://` | an **`option-data` entry** | the reservation's `option-data` |
|
|
192
|
+
| `windhcp://` | a **PowerShell line**, or a `{"Args": [...], "Ignore": false}` **netsh command** | after the reservation and its options |
|
|
193
|
+
|
|
194
|
+
A netsh command works under either windhcp `method`: with `method=netsh` it joins
|
|
195
|
+
the command list, and with `method=powershell` it becomes the `netsh` call it
|
|
196
|
+
describes rather than being dropped. `Args` may be a list, or one string that is
|
|
197
|
+
split on whitespace — a query string has nowhere to put a list. `Ignore` marks a
|
|
198
|
+
command whose failure is acceptable, which is what a teardown usually wants.
|
|
199
|
+
|
|
200
|
+
Fragments are emitted **verbatim**: netboot does not parse, validate or escape
|
|
201
|
+
them. That is the point of an escape hatch, and its risk — a value that ends a
|
|
202
|
+
dhcpd statement early, or a PowerShell line that does more than it looks like,
|
|
203
|
+
is yours to get right. Where netboot builds the text itself (a netsh command
|
|
204
|
+
under `method=powershell`) it quotes it properly.
|
|
205
|
+
|
|
206
|
+
Order is defined: modelled options first, then `extras()`, in the order given
|
|
207
|
+
(unsuffixed `raw.<backend>` before `raw.<backend>.add`). A condition that
|
|
208
|
+
overrides an option therefore comes after the thing it overrides, which is what
|
|
209
|
+
both dhcpd's last-write-wins statements and a Windows policy need.
|
|
210
|
+
|
|
211
|
+
A declarative, cross-backend way to say "serve this file to iPXE and that one to
|
|
212
|
+
a PXE ROM" is not here yet, because the backends disagree about where a condition
|
|
213
|
+
lives — dhcpd puts it on the host, Windows in a scope-level policy, dnsmasq in a
|
|
214
|
+
tag, Kea in a client class. `extras()` is what makes it expressible today.
|
|
215
|
+
|