netboot 0.2.4__tar.gz → 0.3.0__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.0}/AGENTS.md +9 -1
- {netboot-0.2.4 → netboot-0.3.0}/CHANGELOG.md +71 -2
- {netboot-0.2.4 → netboot-0.3.0}/PKG-INFO +26 -2
- {netboot-0.2.4 → netboot-0.3.0}/README.md +13 -1
- {netboot-0.2.4 → netboot-0.3.0}/docs/api.md +14 -0
- {netboot-0.2.4 → netboot-0.3.0}/docs/configuration.md +83 -4
- netboot-0.3.0/docs/extending.md +150 -0
- {netboot-0.2.4 → netboot-0.3.0}/examples/README.md +7 -5
- netboot-0.3.0/examples/templates/debian/boot.cfg.shtpl +8 -0
- {netboot-0.2.4 → netboot-0.3.0}/pyproject.toml +15 -1
- {netboot-0.2.4 → netboot-0.3.0}/src/netboot/AGENTS.md +106 -15
- {netboot-0.2.4 → netboot-0.3.0}/src/netboot/engine.py +5 -3
- {netboot-0.2.4 → netboot-0.3.0}/src/netboot/templates/__init__.py +111 -10
- netboot-0.3.0/src/netboot/templates/common.py +171 -0
- netboot-0.3.0/src/netboot/templates/copy.py +76 -0
- netboot-0.3.0/src/netboot/templates/data.py +160 -0
- netboot-0.3.0/src/netboot/templates/external.py +220 -0
- netboot-0.3.0/src/netboot/templates/handlebars.py +54 -0
- netboot-0.3.0/src/netboot/templates/jinja.py +39 -0
- netboot-0.3.0/src/netboot/templates/liquid.py +59 -0
- netboot-0.3.0/src/netboot/templates/mako.py +131 -0
- netboot-0.3.0/src/netboot/templates/mustache.py +55 -0
- netboot-0.3.0/src/netboot/templates/shell.py +161 -0
- netboot-0.3.0/tests/test_external_engines.py +244 -0
- netboot-0.3.0/tests/test_mako.py +133 -0
- netboot-0.3.0/tests/test_optional_engines.py +206 -0
- {netboot-0.2.4 → netboot-0.3.0}/tests/test_render.py +22 -22
- netboot-0.3.0/tests/test_shell_template.py +257 -0
- netboot-0.3.0/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/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 → netboot-0.3.0}/.gitignore +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/LICENSE +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/benchmarks/README.md +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/benchmarks/bench_netboot.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/benchmarks/results/netboot.json +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/docs/changelog.md +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/docs/cli.md +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/docs/index.md +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/examples/config/pixie.yaml +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/examples/plugins/recording.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/examples/templates/debian/install.ks.j2 +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/mkdocs.yml +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/src/netboot/__init__.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/src/netboot/__main__.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/src/netboot/_version.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/src/netboot/cmds/__init__.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/src/netboot/cmds/complete.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/src/netboot/cmds/initiate.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/src/netboot/content/__init__.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/src/netboot/dhcp/__init__.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/src/netboot/dhcp/dhcpd.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/src/netboot/dhcp/dnsmasq.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/src/netboot/dhcp/kea.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/src/netboot/dhcp/options.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/src/netboot/dhcp/windhcp.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/src/netboot/logging.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/src/netboot/main.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/src/netboot/py.typed +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/src/netboot/utils/__init__.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/src/netboot/utils/config.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/src/netboot/utils/dicts.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/src/netboot/utils/misc.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/src/netboot/utils/net.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/tests/conftest.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/tests/test_api_contracts.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/tests/test_cli.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/tests/test_cli_discovery.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/tests/test_config_discovery.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/tests/test_content.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/tests/test_dhcp.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/tests/test_dhcp_dhcpd.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/tests/test_dhcp_dnsmasq.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/tests/test_dhcp_kea.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/tests/test_dhcp_options.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/tests/test_dhcp_windhcp.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/tests/test_engine_robustness.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/tests/test_hooks.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/tests/test_lookup.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/tests/test_target.py +0 -0
- {netboot-0.2.4 → netboot-0.3.0}/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,75 @@ 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.0] - 2026-10-02
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- **The shell template engine now claims the `.shtpl` suffix only** (it claimed
|
|
11
|
+
*every* suffix before, as the documented fallback). The suffix picks the engine
|
|
12
|
+
and the stem names the artifact, which is the convention `install.ks.j2`
|
|
13
|
+
already used: rename `boot.cfg` to `boot.cfg.shtpl` and `ctx.render("boot.cfg")`
|
|
14
|
+
keeps working unchanged. A file no engine claims is **copied as is** by the new
|
|
15
|
+
`CopyTemplate`, last in the default `template_types` — so a static `grub.cfg`
|
|
16
|
+
needs no engine, and a file that *does* carry `%{NAME}` placeholders ships them
|
|
17
|
+
unrendered until it is renamed. That one case logs a warning naming the rename.
|
|
18
|
+
Narrowing `template_types` so nothing claims a file raises
|
|
19
|
+
`netboot.templates.TemplateEngineError`, which names the file and every
|
|
20
|
+
engine's suffixes.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- **Seven more template engines**, each claiming its own suffix and costing
|
|
24
|
+
nothing when unused: `MakoTemplate` (`.mako`, `netboot[mako]`),
|
|
25
|
+
`LiquidTemplate` (`.liquid`, `netboot[liquid]`), `HandlebarsTemplate`
|
|
26
|
+
(`.hbs`/`.handlebars`, `netboot[handlebars]`), `MustacheTemplate` (`.mustache`,
|
|
27
|
+
`netboot[mustache]`), and `ERBTemplate` (`.erb`) / `EppTemplate` (`.epp`),
|
|
28
|
+
which render through the system `ruby` and `puppet epp render` so an existing
|
|
29
|
+
template behaves as its author tested it (`PIXIE_RUBY` / `PIXIE_PUPPET` name
|
|
30
|
+
the program if it is not on `PATH`). Every engine is registered whether or not
|
|
31
|
+
its dependency is present — a claimed file reports what is missing instead of
|
|
32
|
+
being copied — and nothing is imported until a file claims it. What each engine
|
|
33
|
+
does with `templates_undefined` differs by library and is tabulated in the
|
|
34
|
+
configuration guide; handlebars and mustache cannot fail on an undefined name
|
|
35
|
+
at all.
|
|
36
|
+
- `template_data()` / `DataView` / `jsonable()` — a lazy mapping view of the
|
|
37
|
+
render context for engines that read data rather than evaluate Python
|
|
38
|
+
(`{{ target.hostname }}` beside `{{ ctx.target.hostname }}`), and its
|
|
39
|
+
JSON-serialisable form for the subprocess engines.
|
|
40
|
+
- `SubprocessTemplate` — the base for an engine that renders through another
|
|
41
|
+
language's own tooling: program lookup with an env override, a temporary
|
|
42
|
+
template and values file, a timeout, and the program's stderr in the error.
|
|
43
|
+
- `ShellTemplate` is configured by subclassing, through three class attributes:
|
|
44
|
+
`EXT` (a string or a sequence, dots optional, case-insensitive; `None` or
|
|
45
|
+
`"*"` claims any suffix), `DELIMITER` (default `"%"`) and `PATTERN` —
|
|
46
|
+
`"braced"` (default, `%{NAME}`), `"unbraced"` (`%NAME`) or `"all"`.
|
|
47
|
+
- POSIX default expressions in the braced form: `%{NAME:-fallback}` substitutes
|
|
48
|
+
the fallback when the value is unset *or* empty, `%{NAME-fallback}` only when
|
|
49
|
+
it is unset. One layer of `'`/`"` quotes is stripped, the fallback is literal
|
|
50
|
+
text, and a placeholder with a default never fails whatever
|
|
51
|
+
`templates_undefined` says. `:=`, `:?` and `:+` are not implemented and stay
|
|
52
|
+
literal text.
|
|
53
|
+
- `CopyTemplate` — the copy-as-is engine described above, working in **bytes**:
|
|
54
|
+
the loader no longer decodes a file before knowing which engine claims it, so
|
|
55
|
+
the copy is byte-exact for anything (CRLF, latin-1, a binary) and
|
|
56
|
+
`ctx.render()` returns `bytes` for such a file. `EXT` narrows it to chosen
|
|
57
|
+
suffixes; `Template.BINARY` is the flag any engine can set to be handed raw
|
|
58
|
+
bytes. A non-UTF-8 file claimed by a *text* engine now raises
|
|
59
|
+
`TemplateEngineError` naming the file, where it used to fail inside the read.
|
|
60
|
+
- `Loader.find_source()` — the byte-returning counterpart of `get_source()`
|
|
61
|
+
(which keeps jinja2's text contract).
|
|
62
|
+
- **A template-engine registry**: `register_template_type()` (bare or as a
|
|
63
|
+
decorator, with an optional `priority=`), `unregister_template_type()`,
|
|
64
|
+
`TEMPLATE_TYPES` and `by_priority()`. `Loader(template_types=None)` — the new
|
|
65
|
+
default — uses every registered engine, so a `--load-module` plugin is picked
|
|
66
|
+
up without rebuilding the list, and the list is snapshotted at construction.
|
|
67
|
+
- **`Template.PRIORITY`**, so engine selection does not depend on registration
|
|
68
|
+
order: engines are consulted highest first, `DEFAULT_PRIORITY` (0) for an
|
|
69
|
+
ordinary engine and `FALLBACK_PRIORITY` (-100) for `CopyTemplate`. A plugin is
|
|
70
|
+
registered after the shipped engines, so without this every plugin would sit
|
|
71
|
+
behind the catch-all and never see a file. Equal priorities keep the order they
|
|
72
|
+
were given, so an explicit `template_types` list still means what it says.
|
|
73
|
+
- `Template.EXT` and `JinjaTemplate.EXT`, so suffix ownership is one declarative
|
|
74
|
+
attribute per engine, plus `netboot.templates.template_extensions()` to read it
|
|
75
|
+
back normalised.
|
|
8
76
|
|
|
9
77
|
## [0.2.4] - 2026-09-28
|
|
10
78
|
|
|
@@ -563,7 +631,8 @@ First packaged release: the `netboot` library with the `pixie` command line.
|
|
|
563
631
|
config value construction no longer swallows non-`TypeError` errors; repo
|
|
564
632
|
`joinpath` keeps `.local` a path so chained joins work.
|
|
565
633
|
|
|
566
|
-
[Unreleased]: https://github.com/jose-pr/netboot/compare/v0.
|
|
634
|
+
[Unreleased]: https://github.com/jose-pr/netboot/compare/v0.3.0...HEAD
|
|
635
|
+
[0.3.0]: https://github.com/jose-pr/netboot/compare/v0.2.4...v0.3.0
|
|
567
636
|
[0.2.4]: https://github.com/jose-pr/netboot/compare/v0.2.3...v0.2.4
|
|
568
637
|
[0.2.3]: https://github.com/jose-pr/netboot/compare/v0.2.2...v0.2.3
|
|
569
638
|
[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.0
|
|
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
|
|
@@ -176,11 +176,90 @@ not a mapping is an error naming the file.
|
|
|
176
176
|
|
|
177
177
|
`templates` is a list of search paths (local or URI). For each render netboot looks
|
|
178
178
|
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
|
-
|
|
179
|
+
back to the bare template name.
|
|
180
|
+
|
|
181
|
+
**The suffix picks the engine, and the stem names the artifact.** A `.j2` /
|
|
182
|
+
`.jinja` / `.jinja2` file is rendered with Jinja2; a `.shtpl` file with the
|
|
183
|
+
`%`-delimited shell engine, whose `%{UPPER_SNAKE}` placeholders come from the
|
|
184
|
+
flattened context. Seven more engines ship for template trees that already exist
|
|
185
|
+
in another language:
|
|
186
|
+
|
|
187
|
+
| Engine | Suffixes | Needs | `templates_undefined` |
|
|
188
|
+
| ------ | -------- | ----- | --------------------- |
|
|
189
|
+
| `JinjaTemplate` | `.j2` `.jinja` `.jinja2` | — | all three |
|
|
190
|
+
| `MakoTemplate` | `.mako` | `netboot[mako]` | `strict`, `lenient`; `debug` behaves as `lenient` |
|
|
191
|
+
| `LiquidTemplate` | `.liquid` | `netboot[liquid]` | all three (`debug` names the variable in the output) |
|
|
192
|
+
| `HandlebarsTemplate` | `.hbs` `.handlebars` | `netboot[handlebars]` | **always lenient** — the library cannot fail |
|
|
193
|
+
| `MustacheTemplate` | `.mustache` | `netboot[mustache]` | **always lenient** (logs under `strict`) |
|
|
194
|
+
| `ERBTemplate` | `.erb` | `ruby` on `PATH` (or `PIXIE_RUBY`) | n/a — missing data is Ruby's `nil` |
|
|
195
|
+
| `EppTemplate` | `.epp` | `puppet` on `PATH` (or `PIXIE_PUPPET`) | n/a — missing data is Puppet's `undef` |
|
|
196
|
+
| `ShellTemplate` | `.shtpl` | — | all three |
|
|
197
|
+
| `CopyTemplate` | anything else | — | n/a — nothing is substituted |
|
|
198
|
+
|
|
199
|
+
Every optional engine is **registered whether or not its library is installed**,
|
|
200
|
+
so a `.liquid` file tells you to `pip install netboot[liquid]` instead of being
|
|
201
|
+
quietly copied. None of them is imported until a file claims it, so an install
|
|
202
|
+
that uses none pays nothing.
|
|
203
|
+
|
|
204
|
+
Jinja and mako evaluate Python, so a template reaches into `ctx` directly. The
|
|
205
|
+
data languages (liquid, handlebars, mustache) and the external ones (ERB, EPP)
|
|
206
|
+
get a **mapping view** of the same context instead: `{{ ctx.target.hostname }}`
|
|
207
|
+
and `{{ target.hostname }}` both resolve, an address or a path arrives as the
|
|
208
|
+
string a template would have printed, and the engine's own machinery
|
|
209
|
+
(`ctx._netboot_`, the renderer) is left out. ERB additionally sets each top-level
|
|
210
|
+
name as an instance variable (`@target`), and EPP as a parameter (`$target`).
|
|
211
|
+
|
|
212
|
+
ERB and EPP run the real `ruby` and `puppet` as a subprocess, with the context
|
|
213
|
+
marshalled to JSON — which is the point: an existing `.erb` renders the way its
|
|
214
|
+
author tested it, rather than the way a reimplementation guesses. Set `PIXIE_RUBY`
|
|
215
|
+
or `PIXIE_PUPPET` if the program is installed but not on `PATH`. Because a template is also found by its stem, the file behind
|
|
216
|
+
an artifact called `boot.cfg` is `boot.cfg.shtpl` and `ctx.render("boot.cfg")`
|
|
217
|
+
still finds it.
|
|
218
|
+
|
|
219
|
+
Anything else is **copied as is** — a static `grub.cfg`, an EFI binary or a
|
|
220
|
+
license file is a template that needs no engine. The copy engine works in
|
|
221
|
+
**bytes**: the loader never decodes the file, so the result is byte-exact
|
|
222
|
+
whatever it holds (CRLF line endings, latin-1 text, something that is not text at
|
|
223
|
+
all) and `ctx.render()` returns `bytes` for it rather than `str`.
|
|
224
|
+
|
|
225
|
+
Before 0.3.0 the shell engine claimed every suffix, so a file that *does* carry
|
|
226
|
+
`%{NAME}` placeholders now ships them unrendered: rename it to `*.shtpl` once.
|
|
227
|
+
That is the one mistake a copy can hide, so a copy that still finds placeholders
|
|
228
|
+
logs a warning naming the rename.
|
|
182
229
|
|
|
183
230
|
Only the braced form is substituted, so a bare `%word` is left alone — a kickstart
|
|
184
231
|
file keeps its `%packages`, `%pre`, `%post` and `%end` sections, and a script keeps
|
|
185
232
|
`date +%Y`. Write `%%` for a literal `%` next to a brace, `%{NAME}` to substitute.
|
|
186
|
-
An unknown `%{NAME}`
|
|
233
|
+
An unknown `%{NAME}` follows `templates_undefined` (above).
|
|
234
|
+
|
|
235
|
+
`%{NAME:-fallback}` renders `fallback` when `NAME` is unset **or empty**, and
|
|
236
|
+
`%{NAME-fallback}` only when it is unset — the two POSIX forms, so a template can
|
|
237
|
+
carry its own default instead of requiring the variable. One layer of `'`/`"`
|
|
238
|
+
quotes is stripped (`%{NAME:-'a default'}`), the fallback is literal text (no
|
|
239
|
+
nested placeholders, and no `}` inside it), and a placeholder that has a default
|
|
240
|
+
never fails whatever `templates_undefined` says. The other POSIX forms (`:=`,
|
|
241
|
+
`:?`, `:+`) are not implemented and stay literal text.
|
|
242
|
+
|
|
243
|
+
The engine is configured by subclassing, not by config, for a tree that uses
|
|
244
|
+
another convention:
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
from netboot.templates.shell import ShellTemplate
|
|
248
|
+
|
|
249
|
+
class DollarTemplate(ShellTemplate):
|
|
250
|
+
EXT = (".tpl", ".cfg") # a single string is fine; `None` claims any suffix
|
|
251
|
+
DELIMITER = "$"
|
|
252
|
+
PATTERN = "all" # 'braced' (default), 'unbraced', or 'all'
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Register the class with `netboot.templates.register_template_type` (see
|
|
256
|
+
[Extending](extending.md#custom-template-engines)) or pass it in
|
|
257
|
+
`Loader(..., template_types=[...])`. Engines are consulted in `PRIORITY` order,
|
|
258
|
+
highest first, so a registered engine is asked before the catch-all
|
|
259
|
+
`CopyTemplate` whatever order they were registered in. The default is every
|
|
260
|
+
registered engine: `JinjaTemplate`, `ShellTemplate`, then `CopyTemplate`. Narrow
|
|
261
|
+
it — dropping the copy engine, or giving it a suffix list of its own — and a file
|
|
262
|
+
nothing claims raises `netboot.templates.TemplateEngineError`, naming the file and
|
|
263
|
+
what each engine handles. A file that is not valid UTF-8 raises the same error when the engine
|
|
264
|
+
that claims it needs text; set `BINARY = True` on an engine to be handed the raw
|
|
265
|
+
bytes instead, as `CopyTemplate` does.
|
|
@@ -0,0 +1,150 @@
|
|
|
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.
|
|
@@ -6,8 +6,8 @@ a laptop.
|
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
config/pixie.yaml the config `pixie` discovers (./config/pixie.yaml)
|
|
9
|
-
templates/debian/boot.cfg
|
|
10
|
-
templates/debian/install.ks.j2
|
|
9
|
+
templates/debian/boot.cfg.shtpl shell-engine template (%{NAME} placeholders)
|
|
10
|
+
templates/debian/install.ks.j2 Jinja template (full context access)
|
|
11
11
|
plugins/recording.py a DhcpServer backend that prints instead of arming
|
|
12
12
|
```
|
|
13
13
|
|
|
@@ -44,9 +44,11 @@ Expected output for `initiate web01`: the backend line, then the rendered
|
|
|
44
44
|
zone by containment, so it names its `dhcpzone` — otherwise zone lookup finds
|
|
45
45
|
nothing. Either target can be selected by id, hostname, MAC or IP, in any MAC
|
|
46
46
|
spelling.
|
|
47
|
-
- **Both template engines.**
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
- **Both template engines.** The suffix picks the engine and the stem names the
|
|
48
|
+
artifact: `boot.cfg.shtpl` is rendered by the shell engine and asked for as
|
|
49
|
+
`boot.cfg` — `%{UPPER_SNAKE}` placeholders from the flattened context, a bare
|
|
50
|
+
`%` left alone (which is what lets a kickstart's `%packages` through), and
|
|
51
|
+
`%{NAME:-fallback}` for a value the context may not carry.
|
|
50
52
|
`install.ks.j2` is Jinja, with `ctx`, `shell_quote`, `Path` and `Uri` in scope.
|
|
51
53
|
- **A real DHCP backend.** `config/pixie.yaml` carries a commented
|
|
52
54
|
`dnsmasq://` entry beside the recording stub: uncomment it (and drop the stub)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Shell-engine template: the `.shtpl` suffix is what picks the engine, and the
|
|
2
|
+
# rendered artifact is named by the stem (`boot.cfg`). Only %%{NAME} substitutes;
|
|
3
|
+
# a bare % is literal text, so this comment survives and so does a shell line
|
|
4
|
+
# like: date +%Y%m%d. %%{NAME:-fallback} fills in a value the context lacks.
|
|
5
|
+
default=install
|
|
6
|
+
label install
|
|
7
|
+
kernel %{IMAGE_GLOBALS_KERNEL}
|
|
8
|
+
append ip=%{TARGET_IP} hostname=%{TARGET_HOSTNAME} domain=%{DHCPZONE_DOMAIN}
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "netboot"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
authors = [{ name = "Jose A." }]
|
|
9
9
|
description = "PXE provisioning management: render netboot artifacts and drive DHCP for targets"
|
|
10
10
|
readme = "README.md"
|
|
@@ -52,6 +52,15 @@ dependencies = [
|
|
|
52
52
|
[project.optional-dependencies]
|
|
53
53
|
# Config-file loading for the CLI (YAML with !include).
|
|
54
54
|
config = ["pyyaml"]
|
|
55
|
+
# The mako template engine (`*.mako`). netboot.templates registers the engine
|
|
56
|
+
# either way, so a .mako file without the extra raises ImportError naming it
|
|
57
|
+
# rather than falling through to the copy engine.
|
|
58
|
+
mako = ["mako"]
|
|
59
|
+
# Further optional template engines, same deal: the engine is always registered,
|
|
60
|
+
# the library is imported only when a file claims it.
|
|
61
|
+
liquid = ["python-liquid"]
|
|
62
|
+
handlebars = ["pybars3"]
|
|
63
|
+
mustache = ["chevron"]
|
|
55
64
|
# http/https repository services. pathlib_next's uri scheme handler for
|
|
56
65
|
# http(s) imports `requests` at module import, and nothing else installs it --
|
|
57
66
|
# pathlib_next[uri] does not. Repos served over file/tftp, and rendering in
|
|
@@ -84,6 +93,11 @@ dev = [
|
|
|
84
93
|
# Backend dependencies, so their tests run rather than skip.
|
|
85
94
|
"pypureomapi",
|
|
86
95
|
"pywinrm",
|
|
96
|
+
# Template engines behind an extra, so their tests run rather than skip.
|
|
97
|
+
"mako",
|
|
98
|
+
"python-liquid",
|
|
99
|
+
"pybars3",
|
|
100
|
+
"chevron",
|
|
87
101
|
]
|
|
88
102
|
docs = ["mkdocs", "mkdocs-material", "mkdocstrings[python]"]
|
|
89
103
|
|