netboot 0.2.0__tar.gz → 0.2.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.0 → netboot-0.2.1}/CHANGELOG.md +75 -1
- {netboot-0.2.0 → netboot-0.2.1}/PKG-INFO +21 -5
- {netboot-0.2.0 → netboot-0.2.1}/README.md +10 -4
- {netboot-0.2.0 → netboot-0.2.1}/docs/configuration.md +64 -5
- {netboot-0.2.0 → netboot-0.2.1}/docs/extending.md +7 -0
- {netboot-0.2.0 → netboot-0.2.1}/docs/index.md +4 -0
- {netboot-0.2.0 → netboot-0.2.1}/examples/README.md +5 -0
- {netboot-0.2.0 → netboot-0.2.1}/examples/config/pixie.yaml +11 -1
- {netboot-0.2.0 → netboot-0.2.1}/pyproject.toml +23 -2
- {netboot-0.2.0 → netboot-0.2.1}/src/netboot/AGENTS.md +66 -2
- {netboot-0.2.0 → netboot-0.2.1}/src/netboot/content/__init__.py +26 -19
- netboot-0.2.0/src/netboot/dhcp.py → netboot-0.2.1/src/netboot/dhcp/__init__.py +57 -5
- netboot-0.2.1/src/netboot/dhcp/dhcpd.py +204 -0
- netboot-0.2.1/src/netboot/dhcp/dnsmasq.py +311 -0
- netboot-0.2.1/src/netboot/dhcp/kea.py +209 -0
- netboot-0.2.1/src/netboot/dhcp/options.py +226 -0
- netboot-0.2.1/src/netboot/dhcp/windhcp.py +249 -0
- {netboot-0.2.0 → netboot-0.2.1}/src/netboot/engine.py +4 -0
- {netboot-0.2.0 → netboot-0.2.1}/tests/test_content.py +7 -3
- netboot-0.2.1/tests/test_dhcp_dhcpd.py +211 -0
- netboot-0.2.1/tests/test_dhcp_dnsmasq.py +225 -0
- netboot-0.2.1/tests/test_dhcp_kea.py +236 -0
- netboot-0.2.1/tests/test_dhcp_options.py +270 -0
- netboot-0.2.1/tests/test_dhcp_windhcp.py +230 -0
- {netboot-0.2.0 → netboot-0.2.1}/.gitignore +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/AGENTS.md +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/LICENSE +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/benchmarks/README.md +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/benchmarks/bench_netboot.py +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/benchmarks/results/netboot.json +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/docs/api.md +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/docs/changelog.md +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/docs/cli.md +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/examples/plugins/recording.py +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/examples/templates/debian/boot.cfg +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/examples/templates/debian/install.ks.j2 +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/mkdocs.yml +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/src/netboot/__init__.py +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/src/netboot/__main__.py +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/src/netboot/_version.py +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/src/netboot/cmds/__init__.py +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/src/netboot/cmds/complete.py +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/src/netboot/cmds/initiate.py +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/src/netboot/logging.py +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/src/netboot/main.py +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/src/netboot/py.typed +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/src/netboot/templates/__init__.py +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/src/netboot/templates/common.py +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/src/netboot/templates/jinja.py +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/src/netboot/templates/shell.py +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/src/netboot/utils/__init__.py +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/src/netboot/utils/config.py +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/src/netboot/utils/dicts.py +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/src/netboot/utils/misc.py +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/src/netboot/utils/net.py +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/tests/conftest.py +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/tests/test_api_contracts.py +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/tests/test_cli.py +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/tests/test_cli_discovery.py +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/tests/test_config_discovery.py +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/tests/test_dhcp.py +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/tests/test_engine_robustness.py +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/tests/test_hooks.py +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/tests/test_lookup.py +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/tests/test_render.py +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/tests/test_target.py +0 -0
- {netboot-0.2.0 → netboot-0.2.1}/tests/test_utils.py +0 -0
|
@@ -6,6 +6,79 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.2.1] - 2026-09-17
|
|
10
|
+
|
|
11
|
+
netboot ships DHCP backends. Additive: nothing documented changed behaviour,
|
|
12
|
+
and a config that named no `dhcpservers` scheme netboot implements works as
|
|
13
|
+
before.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- A repository `address` of the form `host:port` no longer sends the whole
|
|
17
|
+
string to DNS. The port is split off first, so `mirror.example:8080`
|
|
18
|
+
resolves `mirror.example` — the old lookup could only fail, and did so at
|
|
19
|
+
different speeds on different machines.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- `netboot.dhcp` is a package, and DHCP backends carry **client options**. A
|
|
23
|
+
`dhcpservers` entry is still a plain URI string; its query string now holds the
|
|
24
|
+
options the server should give the client (`?router=10.0.0.1`), alongside each
|
|
25
|
+
backend's own connection settings. `netboot.dhcp.options` models them:
|
|
26
|
+
`GENERIC_OPTIONS` for the names netboot translates, `option-<n>` for anything
|
|
27
|
+
else, `raw.<backend>=` for untranslated backend-native text, and
|
|
28
|
+
`options_builder=my.mod.fn` plus the new `PixieEvent.BuildDhcpOptions` for
|
|
29
|
+
building them from the context. Merge order, later winning: zone defaults, the
|
|
30
|
+
connection query, `image.dhcp_options`, `target.dhcp_options`, the callback,
|
|
31
|
+
the hook.
|
|
32
|
+
- **A dnsmasq backend** (`dnsmasq://`), the first netboot ships. It writes
|
|
33
|
+
`dhcp-host`/`dhcp-option`/`dhcp-boot` entries through `pathlib_next` paths, so
|
|
34
|
+
the same configuration serves a local dnsmasq or one reached over `sftp://`
|
|
35
|
+
(`netboot[ssh]`). `hostsfile`/`optsfile` may be a **directory** (one file per
|
|
36
|
+
target — preferred, and with `--dhcp-hostsdir`/`--dhcp-optsdir` dnsmasq re-reads
|
|
37
|
+
them with no signal at all) or a **file**, where netboot edits only its own
|
|
38
|
+
marked region and never touches a line it did not write. `reload=` runs a
|
|
39
|
+
command, over ssh when the URI names a host; omitting it is fine for a watched
|
|
40
|
+
directory and an error for a file, which dnsmasq never re-reads by itself.
|
|
41
|
+
- **A Kea backend** (`kea://`, `keas://` for https). It sends `reservation-add`
|
|
42
|
+
and `reservation-del` to the control agent, mapping `boot-file-name`,
|
|
43
|
+
`next-server` and `host-name` onto reservation *fields* and everything else
|
|
44
|
+
onto `option-data`. The `subnet-id` comes from the zone (`subnet_id:`) or is
|
|
45
|
+
discovered once by matching the zone's network against `config-get`; when
|
|
46
|
+
neither works netboot refuses rather than guessing, because a reservation in
|
|
47
|
+
the wrong subnet silently never matches. A `result: 2` from Kea is reported
|
|
48
|
+
with the likely cause (host_cmds not loaded, or a read-only hosts backend),
|
|
49
|
+
and an existing reservation for the same MAC is refused rather than
|
|
50
|
+
overwritten. Needs `netboot[kea]`.
|
|
51
|
+
- **An ISC dhcpd backend** (`dhcpd://`) over OMAPI, the only way to give dhcpd a
|
|
52
|
+
reservation without rewriting and reloading its config. Options are rendered
|
|
53
|
+
into the host's `statements`, which is dhcpd config *source*: text values are
|
|
54
|
+
quoted with `"` and `\` escaped, a value meant to be an address must look like
|
|
55
|
+
one, and a newline is refused outright — a value cannot end its statement and
|
|
56
|
+
start another. The OMAPI secret comes from `keyfile=` or
|
|
57
|
+
`$PIXIE_DHCPD_OMAPI_KEY`, never from the URI. Needs `netboot[dhcpd]`. Note
|
|
58
|
+
that a host added over OMAPI does not survive a dhcpd restart by itself: that
|
|
59
|
+
is dhcpd's design, not netboot forgetting it.
|
|
60
|
+
- **A Windows DHCP Server backend** (`windhcp://`), driving the `DhcpServer`
|
|
61
|
+
PowerShell module over **ssh** (the default, no dependency — it uses the system
|
|
62
|
+
`ssh` client, so your config, keys, agent and jump hosts apply) or **WinRM**
|
|
63
|
+
(`netboot[winrm]`). The transport host and the DHCP server are separate:
|
|
64
|
+
`server=` becomes `-ComputerName` when the cmdlets should act elsewhere. The
|
|
65
|
+
scope is the zone's network address, resolved when the target is applied and
|
|
66
|
+
overridable per zone. No value is interpolated into the script: parameters
|
|
67
|
+
travel as a JSON payload PowerShell parses, so a target name or option value
|
|
68
|
+
cannot become a statement.
|
|
69
|
+
- New extras, each carrying exactly one backend's dependency: `netboot[kea]`,
|
|
70
|
+
`netboot[dhcpd]`, `netboot[winrm]`, and `netboot[ssh]` for dnsmasq over
|
|
71
|
+
`sftp://`. A local dnsmasq and `windhcp://` over ssh need none. Verified on a
|
|
72
|
+
clean wheel install: importing `netboot.dhcp` loads no backend dependency,
|
|
73
|
+
those two backends construct, and the other two raise `ImportError` naming
|
|
74
|
+
their extra.
|
|
75
|
+
- Anything that varies per target is refused in a connection string and resolved
|
|
76
|
+
when the target is applied instead: `subnet_id`/`scope` belong to the zone,
|
|
77
|
+
`boot-file-name`/`next-server`/`tftp-server-name` to the image or target. The
|
|
78
|
+
error names where each belongs. `from netboot.dhcp import DhcpServer, DhcpZone`
|
|
79
|
+
is unchanged, and a backend module is imported only when a config names its
|
|
80
|
+
scheme.
|
|
81
|
+
|
|
9
82
|
## [0.2.0] - 2026-09-17
|
|
10
83
|
|
|
11
84
|
A correctness and hardening release from a full review of the code base.
|
|
@@ -447,7 +520,8 @@ First packaged release: the `netboot` library with the `pixie` command line.
|
|
|
447
520
|
config value construction no longer swallows non-`TypeError` errors; repo
|
|
448
521
|
`joinpath` keeps `.local` a path so chained joins work.
|
|
449
522
|
|
|
450
|
-
[Unreleased]: https://github.com/jose-pr/netboot/compare/v0.2.
|
|
523
|
+
[Unreleased]: https://github.com/jose-pr/netboot/compare/v0.2.1...HEAD
|
|
524
|
+
[0.2.1]: https://github.com/jose-pr/netboot/compare/v0.2.0...v0.2.1
|
|
451
525
|
[0.2.0]: https://github.com/jose-pr/netboot/compare/v0.1.3...v0.2.0
|
|
452
526
|
[0.1.3]: https://github.com/jose-pr/netboot/compare/v0.1.2...v0.1.3
|
|
453
527
|
[0.1.2]: https://github.com/jose-pr/netboot/compare/v0.1.1...v0.1.2
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: netboot
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.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
|
|
@@ -37,11 +37,15 @@ Requires-Dist: black; extra == 'dev'
|
|
|
37
37
|
Requires-Dist: build; extra == 'dev'
|
|
38
38
|
Requires-Dist: dnspython; extra == 'dev'
|
|
39
39
|
Requires-Dist: hatchling; extra == 'dev'
|
|
40
|
+
Requires-Dist: pypureomapi; extra == 'dev'
|
|
40
41
|
Requires-Dist: pytest; extra == 'dev'
|
|
41
42
|
Requires-Dist: pytest-cov; extra == 'dev'
|
|
43
|
+
Requires-Dist: pywinrm; extra == 'dev'
|
|
42
44
|
Requires-Dist: pyyaml; extra == 'dev'
|
|
43
45
|
Requires-Dist: requests; extra == 'dev'
|
|
44
46
|
Requires-Dist: twine; extra == 'dev'
|
|
47
|
+
Provides-Extra: dhcpd
|
|
48
|
+
Requires-Dist: pypureomapi; extra == 'dhcpd'
|
|
45
49
|
Provides-Extra: dns
|
|
46
50
|
Requires-Dist: dnspython; extra == 'dns'
|
|
47
51
|
Provides-Extra: docs
|
|
@@ -50,6 +54,12 @@ Requires-Dist: mkdocs-material; extra == 'docs'
|
|
|
50
54
|
Requires-Dist: mkdocstrings[python]; extra == 'docs'
|
|
51
55
|
Provides-Extra: http
|
|
52
56
|
Requires-Dist: requests; extra == 'http'
|
|
57
|
+
Provides-Extra: kea
|
|
58
|
+
Requires-Dist: requests; extra == 'kea'
|
|
59
|
+
Provides-Extra: ssh
|
|
60
|
+
Requires-Dist: pathlib-next[sftp]; extra == 'ssh'
|
|
61
|
+
Provides-Extra: winrm
|
|
62
|
+
Requires-Dist: pywinrm; extra == 'winrm'
|
|
53
63
|
Description-Content-Type: text/markdown
|
|
54
64
|
|
|
55
65
|
# netboot
|
|
@@ -88,6 +98,10 @@ Optional extras:
|
|
|
88
98
|
| `netboot[config]` | YAML config loading for the CLI (`pyyaml`) |
|
|
89
99
|
| `netboot[dns]` | The `dnspython` resolver backend (best coverage; without it hostname targets still resolve via the system/`nslookup` fallbacks) |
|
|
90
100
|
| `netboot[http]` | `http`/`https` repository services (`requests`); `file`/`tftp` repos and rendering need nothing extra |
|
|
101
|
+
| `netboot[kea]` | The `kea://` DHCP backend (`requests`) |
|
|
102
|
+
| `netboot[dhcpd]` | The `dhcpd://` DHCP backend over OMAPI (`pypureomapi`) |
|
|
103
|
+
| `netboot[winrm]` | `windhcp://` over WinRM (`pywinrm`); over ssh it needs nothing |
|
|
104
|
+
| `netboot[ssh]` | `dnsmasq://` with remote `sftp://` paths (`pathlib_next[sftp]`) |
|
|
91
105
|
| `netboot[docs]` | Build the documentation site (`mkdocs`) |
|
|
92
106
|
|
|
93
107
|
Built on [`duho`](https://github.com/jose-pr/duho) (CLI/args/command discovery),
|
|
@@ -140,10 +154,12 @@ pixie.complete(target) # cleanup once installed
|
|
|
140
154
|
`Pixie(...)`. Each hook `f(event, netboot, value, kwargs) -> value` is called for
|
|
141
155
|
every `PixieEvent` and may transform the value flowing through it — used to
|
|
142
156
|
customise lookup, context construction and the init/complete lifecycle.
|
|
143
|
-
- **DHCP backends.**
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
157
|
+
- **DHCP backends.** netboot ships four — `dnsmasq://`, `kea://`, `dhcpd://` and
|
|
158
|
+
`windhcp://` (Windows, over ssh or WinRM) — each imported only when a config
|
|
159
|
+
names its scheme. Write your own by subclassing `netboot.dhcp.DhcpServer`: the
|
|
160
|
+
lowercased class name is the URI scheme it handles, and `--load-module`
|
|
161
|
+
registers it before the config builds the zones. What the server should tell
|
|
162
|
+
the client rides in the URI query; see the configuration guide.
|
|
147
163
|
|
|
148
164
|
A complete runnable setup — config, both template engines and a DHCP plugin —
|
|
149
165
|
is in [`examples/`](https://github.com/jose-pr/netboot/tree/main/examples).
|
|
@@ -34,6 +34,10 @@ Optional extras:
|
|
|
34
34
|
| `netboot[config]` | YAML config loading for the CLI (`pyyaml`) |
|
|
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
|
+
| `netboot[kea]` | The `kea://` DHCP backend (`requests`) |
|
|
38
|
+
| `netboot[dhcpd]` | The `dhcpd://` DHCP backend over OMAPI (`pypureomapi`) |
|
|
39
|
+
| `netboot[winrm]` | `windhcp://` over WinRM (`pywinrm`); over ssh it needs nothing |
|
|
40
|
+
| `netboot[ssh]` | `dnsmasq://` with remote `sftp://` paths (`pathlib_next[sftp]`) |
|
|
37
41
|
| `netboot[docs]` | Build the documentation site (`mkdocs`) |
|
|
38
42
|
|
|
39
43
|
Built on [`duho`](https://github.com/jose-pr/duho) (CLI/args/command discovery),
|
|
@@ -86,10 +90,12 @@ pixie.complete(target) # cleanup once installed
|
|
|
86
90
|
`Pixie(...)`. Each hook `f(event, netboot, value, kwargs) -> value` is called for
|
|
87
91
|
every `PixieEvent` and may transform the value flowing through it — used to
|
|
88
92
|
customise lookup, context construction and the init/complete lifecycle.
|
|
89
|
-
- **DHCP backends.**
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
+
- **DHCP backends.** netboot ships four — `dnsmasq://`, `kea://`, `dhcpd://` and
|
|
94
|
+
`windhcp://` (Windows, over ssh or WinRM) — each imported only when a config
|
|
95
|
+
names its scheme. Write your own by subclassing `netboot.dhcp.DhcpServer`: the
|
|
96
|
+
lowercased class name is the URI scheme it handles, and `--load-module`
|
|
97
|
+
registers it before the config builds the zones. What the server should tell
|
|
98
|
+
the client rides in the URI query; see the configuration guide.
|
|
93
99
|
|
|
94
100
|
A complete runnable setup — config, both template engines and a DHCP plugin —
|
|
95
101
|
is in [`examples/`](https://github.com/jose-pr/netboot/tree/main/examples).
|
|
@@ -36,11 +36,9 @@ dhcpzones:
|
|
|
36
36
|
nameservers: [10.0.0.53]
|
|
37
37
|
search: [example.com]
|
|
38
38
|
dhcpservers:
|
|
39
|
-
# The scheme selects the
|
|
40
|
-
#
|
|
41
|
-
|
|
42
|
-
# ValueError for the unknown scheme.
|
|
43
|
-
- dnsmasq://dhcp-host
|
|
39
|
+
# The scheme selects the backend; the query carries what the server
|
|
40
|
+
# should tell the client. See "DHCP servers and options" below.
|
|
41
|
+
- dnsmasq:///?hostsfile=/etc/dnsmasq.d/netboot.d/&router=10.0.0.1
|
|
44
42
|
|
|
45
43
|
# Where boot artifacts are fetched / served from.
|
|
46
44
|
repos:
|
|
@@ -51,6 +49,67 @@ repos:
|
|
|
51
49
|
local: /srv/mirror/debian
|
|
52
50
|
```
|
|
53
51
|
|
|
52
|
+
## DHCP servers and options
|
|
53
|
+
|
|
54
|
+
Each entry under a zone's `dhcpservers` is a URI. Its **scheme** picks the
|
|
55
|
+
backend; its **query string** carries that backend's connection settings and the
|
|
56
|
+
DHCP options the server should give the client.
|
|
57
|
+
|
|
58
|
+
| Scheme | Server | How it applies a reservation | Needs |
|
|
59
|
+
| ------ | ------ | ---------------------------- | ----- |
|
|
60
|
+
| `dnsmasq://` | dnsmasq | writes `dhcp-host`/`dhcp-option` files, local or over `sftp://` | nothing locally; `netboot[ssh]` for remote paths |
|
|
61
|
+
| `kea://`, `keas://` | ISC Kea | `reservation-add` / `reservation-del` via the control agent | `netboot[kea]` |
|
|
62
|
+
| `dhcpd://` | ISC dhcpd | an OMAPI host object | `netboot[dhcpd]` |
|
|
63
|
+
| `windhcp://` | Windows DHCP | its own PowerShell cmdlets over ssh or WinRM | nothing over ssh; `netboot[winrm]` for WinRM |
|
|
64
|
+
|
|
65
|
+
Each has one thing that is easy to get wrong:
|
|
66
|
+
|
|
67
|
+
- **dnsmasq** re-reads `--dhcp-hostsfile`/`--dhcp-optsfile` only on SIGHUP, and
|
|
68
|
+
never re-reads its main config — but `--dhcp-hostsdir`/`--dhcp-optsdir` pick up
|
|
69
|
+
changed files by themselves, which is why a directory needs no `reload=` and a
|
|
70
|
+
plain file does.
|
|
71
|
+
- **Kea** needs the `host_cmds` hook loaded *and* a writable hosts backend; a
|
|
72
|
+
file-only Kea loads the hook and still refuses.
|
|
73
|
+
- **dhcpd** hosts added over OMAPI do not survive a restart by themselves — that
|
|
74
|
+
is dhcpd's design.
|
|
75
|
+
- **Windows** needs the `DhcpServer` module on the host PowerShell runs on, and
|
|
76
|
+
an account with DHCP-administrator rights.
|
|
77
|
+
|
|
78
|
+
### Options
|
|
79
|
+
|
|
80
|
+
Anything in the query that is not a connection setting is a client option:
|
|
81
|
+
`router`, `domain-name-servers`, `domain-name`, `domain-search`, `ntp-servers`,
|
|
82
|
+
`subnet-mask`, `broadcast-address`, `lease-time`, `vendor-class-identifier`, or
|
|
83
|
+
`option-<n>` for anything netboot does not model. Repeat a key for a list.
|
|
84
|
+
`raw.<backend>=` passes backend-native text through untranslated.
|
|
85
|
+
|
|
86
|
+
Options are merged in this order, later winning:
|
|
87
|
+
|
|
88
|
+
1. what the zone already knows (`gateway` → `router`, `nameservers`, `domain`…);
|
|
89
|
+
2. the server URI's query;
|
|
90
|
+
3. `images.<id>.dhcp_options`;
|
|
91
|
+
4. `targets.<id>.dhcp_options`;
|
|
92
|
+
5. an `options_builder=my.module.function` — `fn(ctx, options) -> options`;
|
|
93
|
+
6. any hook on `PixieEvent.BuildDhcpOptions`.
|
|
94
|
+
|
|
95
|
+
**Some things belong to the target, not the connection**, and netboot refuses
|
|
96
|
+
them in a URI with a message saying where they go: `subnet_id` and `scope` on the
|
|
97
|
+
**zone**, `boot-file-name`, `next-server`, `tftp-server-name` and `host-name` on
|
|
98
|
+
the **image** or **target**. A boot file pinned to a connection would hand every
|
|
99
|
+
target on that server the same one.
|
|
100
|
+
|
|
101
|
+
```yaml
|
|
102
|
+
dhcpzones:
|
|
103
|
+
lan:
|
|
104
|
+
network: 10.0.0.0/24
|
|
105
|
+
subnet_id: 44 # kea; windhcp uses `scope`
|
|
106
|
+
dhcpservers:
|
|
107
|
+
- kea://10.0.0.1:8000/?domain-name-servers=10.0.0.53
|
|
108
|
+
images:
|
|
109
|
+
debian:
|
|
110
|
+
dhcp_options: {boot-file-name: pxelinux.0, next-server: 10.0.0.2}
|
|
111
|
+
```
|
|
112
|
+
|
|
54
113
|
## Where templates are looked for
|
|
55
114
|
|
|
56
115
|
`templates` is the list of **template roots** — the CWD's `templates` directory
|
|
@@ -62,6 +62,13 @@ class dnsmasq(DhcpServer): # handles dnsmasq://...
|
|
|
62
62
|
... # disarm it
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
netboot ships four backends — `netboot.dhcp.dnsmasq`, `.kea`, `.dhcpd` and
|
|
66
|
+
`.windhcp` — and they are the worked examples: one writes files (locally or over
|
|
67
|
+
`sftp://`), one speaks a REST API, one a binary protocol, and one runs
|
|
68
|
+
PowerShell over ssh or WinRM. A backend gets its client options from
|
|
69
|
+
`self.options_for(ctx)` and translates them; see the configuration guide for
|
|
70
|
+
what an operator writes.
|
|
71
|
+
|
|
65
72
|
Subclassing at any depth is honoured, so a backend may share an intermediate
|
|
66
73
|
base. Import your plugin module before the config builds the zones — pass
|
|
67
74
|
`--load-module your.plugin` (repeat or colon-separate for several) so the
|
|
@@ -30,6 +30,10 @@ pip install .
|
|
|
30
30
|
| `netboot[config]` | YAML config loading for the CLI (`pyyaml`) |
|
|
31
31
|
| `netboot[dns]` | The `dnspython` resolver backend (best coverage; without it hostname targets still resolve via the system/`nslookup` fallbacks) |
|
|
32
32
|
| `netboot[http]` | `http`/`https` repository services (`requests`); `file`/`tftp` repos and rendering need nothing extra |
|
|
33
|
+
| `netboot[kea]` | The `kea://` DHCP backend (`requests`) |
|
|
34
|
+
| `netboot[dhcpd]` | The `dhcpd://` DHCP backend over OMAPI (`pypureomapi`) |
|
|
35
|
+
| `netboot[winrm]` | `windhcp://` over WinRM (`pywinrm`); over ssh it needs nothing |
|
|
36
|
+
| `netboot[ssh]` | `dnsmasq://` with remote `sftp://` paths (`pathlib_next[sftp]`) |
|
|
33
37
|
| `netboot[docs]` | Build this documentation site (`mkdocs`) |
|
|
34
38
|
|
|
35
39
|
Built on [`duho`](https://github.com/jose-pr/duho) (CLI/args/command discovery),
|
|
@@ -48,6 +48,11 @@ Expected output for `initiate web01`: the backend line, then the rendered
|
|
|
48
48
|
renders it: `%{UPPER_SNAKE}` placeholders from the flattened context, and a
|
|
49
49
|
bare `%` left alone (which is what lets a kickstart's `%packages` through).
|
|
50
50
|
`install.ks.j2` is Jinja, with `ctx`, `shell_quote`, `Path` and `Uri` in scope.
|
|
51
|
+
- **A real DHCP backend.** `config/pixie.yaml` carries a commented
|
|
52
|
+
`dnsmasq://` entry beside the recording stub: uncomment it (and drop the stub)
|
|
53
|
+
to write real reservation files. The image's `dhcp_options` — the boot file and
|
|
54
|
+
its server — reach whichever backend you use, because they belong to the image
|
|
55
|
+
rather than to the connection.
|
|
51
56
|
- **Repo URLs.** `ctx.repos["mirror"].service("tftp")` builds
|
|
52
57
|
`tftp://10.0.0.2/debian` from the repo's address and service path. An
|
|
53
58
|
`http`/`https` service would additionally need `pip install netboot[http]`.
|
|
@@ -17,6 +17,11 @@ images:
|
|
|
17
17
|
template_path: [debian] # relative: searched inside the template root
|
|
18
18
|
globals:
|
|
19
19
|
kernel: vmlinuz
|
|
20
|
+
# What this image tells DHCP clients to boot. Merged into the options every
|
|
21
|
+
# backend sends; see docs/configuration.md.
|
|
22
|
+
dhcp_options:
|
|
23
|
+
boot-file-name: pxelinux.0
|
|
24
|
+
next-server: 10.0.0.2
|
|
20
25
|
|
|
21
26
|
dhcpzones:
|
|
22
27
|
lan:
|
|
@@ -26,8 +31,13 @@ dhcpzones:
|
|
|
26
31
|
nameservers: [10.0.0.53]
|
|
27
32
|
search: [example.com]
|
|
28
33
|
dhcpservers:
|
|
29
|
-
# Handled by examples/plugins/recording.py, imported with --load-module
|
|
34
|
+
# Handled by examples/plugins/recording.py, imported with --load-module,
|
|
35
|
+
# so this example runs with no DHCP server at all.
|
|
30
36
|
- recording://dhcp-host
|
|
37
|
+
# A real one looks like this -- swap it in when you have a dnsmasq. The
|
|
38
|
+
# query carries the options the server hands the client; the boot file
|
|
39
|
+
# comes from the image below, because it varies per target.
|
|
40
|
+
# - dnsmasq:///?hostsfile=/etc/dnsmasq.d/netboot.d/&router=10.0.0.1
|
|
31
41
|
|
|
32
42
|
repos:
|
|
33
43
|
mirror:
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "netboot"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.1"
|
|
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"
|
|
@@ -57,13 +57,34 @@ config = ["pyyaml"]
|
|
|
57
57
|
# pathlib_next[uri] does not. Repos served over file/tftp, and rendering in
|
|
58
58
|
# general, need none of it, so this stays an extra rather than a dependency.
|
|
59
59
|
http = ["requests"]
|
|
60
|
+
# DHCP backends. Each is imported only when a config names its scheme, so a
|
|
61
|
+
# netboot install carries none of these unless it asks for them. dnsmasq needs
|
|
62
|
+
# nothing for a local server, and windhcp nothing over its default ssh
|
|
63
|
+
# transport (it uses the system ssh client).
|
|
64
|
+
kea = ["requests"] # kea:// control agent
|
|
65
|
+
dhcpd = ["pypureomapi"] # dhcpd:// OMAPI
|
|
66
|
+
winrm = ["pywinrm"] # windhcp://...?transport=winrm
|
|
67
|
+
ssh = ["pathlib_next[sftp]"] # dnsmasq:// over sftp:// paths
|
|
60
68
|
# The dnspython resolver backend for netimps' resolve(), used to look up
|
|
61
69
|
# hostname targets. netimps >=0.2.0 makes dnspython optional, so this extra is
|
|
62
70
|
# the only thing that installs it: with it, resolution has full record-type and
|
|
63
71
|
# explicit-nameserver support; without it, resolve() still works through
|
|
64
72
|
# netimps' system/nslookup backends (address and reverse records only).
|
|
65
73
|
dns = ["dnspython"]
|
|
66
|
-
dev = [
|
|
74
|
+
dev = [
|
|
75
|
+
"black",
|
|
76
|
+
"build",
|
|
77
|
+
"hatchling",
|
|
78
|
+
"pytest",
|
|
79
|
+
"pytest-cov",
|
|
80
|
+
"twine",
|
|
81
|
+
"pyyaml",
|
|
82
|
+
"dnspython",
|
|
83
|
+
"requests",
|
|
84
|
+
# Backend dependencies, so their tests run rather than skip.
|
|
85
|
+
"pypureomapi",
|
|
86
|
+
"pywinrm",
|
|
87
|
+
]
|
|
67
88
|
docs = ["mkdocs", "mkdocs-material", "mkdocstrings[python]"]
|
|
68
89
|
|
|
69
90
|
[project.scripts]
|
|
@@ -98,8 +98,8 @@ is the same object. `netboot.netutils` remains an alias for
|
|
|
98
98
|
- **`PixieEvent`** (`StrEnum`) — hook event names: `NewPixieObject`,
|
|
99
99
|
`StartPixieInit`, `SetPixieProperty`, `PixieInitiated`, `LookupTarget`,
|
|
100
100
|
`FoundTarget`, `FoundTargetImage`, `FoundTargetDhcpzone`,
|
|
101
|
-
`PixieContextForTarget`, `
|
|
102
|
-
`StartPixieComplete`, `EndPixieComplete`.
|
|
101
|
+
`PixieContextForTarget`, `BuildDhcpOptions`, `StartPixieInitialize`,
|
|
102
|
+
`EndPixieInitialize`, `StartPixieComplete`, `EndPixieComplete`.
|
|
103
103
|
|
|
104
104
|
- **`PixieTarget(**kwargs)`** (`argparse.Namespace` + `yaconfiglib.OpaqueMerge`)
|
|
105
105
|
— `_id`, `hostname`, `ip` (`IPAddress`), `mac` (`MACAddress`), `image`,
|
|
@@ -173,6 +173,12 @@ is the same object. `netboot.netutils` remains an alias for
|
|
|
173
173
|
skips anything unparseable with a warning, and returns `default` when no
|
|
174
174
|
server lies inside the zone's network.
|
|
175
175
|
|
|
176
|
+
- **`DhcpServer.SETTINGS`** — the query keys a backend reads as *connection*
|
|
177
|
+
settings. Every other key in a `dhcpservers` URI query is a **client option**
|
|
178
|
+
(see `netboot.dhcp.options`), so the two vocabularies must stay disjoint.
|
|
179
|
+
- **`DhcpServer.options_for(ctx) -> DhcpOptions`** — the merged client options
|
|
180
|
+
for this target on this server.
|
|
181
|
+
|
|
176
182
|
- **`DhcpZone(**kwargs)`** (`Namespace` + `OpaqueMerge`) — `network`
|
|
177
183
|
(`IPNetwork`), `gateway` (`IPAddress | None`), `domain` (`str | None`),
|
|
178
184
|
`search` (`list[str]`), `nameservers` (`list[IPAddress]`), `globals`,
|
|
@@ -182,6 +188,64 @@ is the same object. `netboot.netutils` remains an alias for
|
|
|
182
188
|
**`.get_local_server(servers, default)`** — first `server` contained in
|
|
183
189
|
`.network`, else `default`.
|
|
184
190
|
|
|
191
|
+
## DHCP backends (`netboot.dhcp.*`)
|
|
192
|
+
|
|
193
|
+
Four ship, each imported only when a config names its scheme — so importing
|
|
194
|
+
`netboot.dhcp` pulls in none of their dependencies. Every one takes its client
|
|
195
|
+
options from the URI query (see below) and translates them.
|
|
196
|
+
|
|
197
|
+
- **`dnsmasq://[host]/?hostsfile=&optsfile=&reload=&pidfile=`**
|
|
198
|
+
(`netboot.dhcp.dnsmasq`) — writes `dhcp-host`/`dhcp-option`/`dhcp-boot`
|
|
199
|
+
entries. Paths may carry their own scheme; a bare path is local when the URI
|
|
200
|
+
has no host and `sftp://<host>/<path>` when it does (`netboot[ssh]`). A
|
|
201
|
+
**directory** gets one file per target; a **file** gets a marked region netboot
|
|
202
|
+
owns. `reload=` runs a command, over ssh when the URI names a host — omit it
|
|
203
|
+
for a `--dhcp-hostsdir`, which dnsmasq re-reads by itself; a plain file
|
|
204
|
+
without it raises.
|
|
205
|
+
- **`kea://`, `keas://`** (`netboot.dhcp.kea`, `netboot[kea]`) —
|
|
206
|
+
`reservation-add`/`reservation-del` through the control agent. `?service=`
|
|
207
|
+
selects `dhcp4`/`dhcp6`. Needs the **host_cmds** hook and a writable hosts
|
|
208
|
+
backend; `result: 2` is reported with that cause. `subnet-id` comes from the
|
|
209
|
+
zone (`subnet_id`) or a matched `config-get`.
|
|
210
|
+
- **`dhcpd://host:7911/?keyname=&keyfile=`** (`netboot.dhcp.dhcpd`,
|
|
211
|
+
`netboot[dhcpd]`) — an OMAPI host object whose `statements` carry the options,
|
|
212
|
+
escaped. Secret from `keyfile=` or `$PIXIE_DHCPD_OMAPI_KEY`. A host added this
|
|
213
|
+
way does **not** survive a dhcpd restart by itself.
|
|
214
|
+
- **`windhcp://[user@]host/?transport=ssh|winrm&server=&auth=&port=&ssl=`**
|
|
215
|
+
(`netboot.dhcp.windhcp`) — the `DhcpServer` PowerShell cmdlets over the system
|
|
216
|
+
`ssh` client (default, no dependency) or WinRM (`netboot[winrm]`, password
|
|
217
|
+
from `$PIXIE_WINDHCP_PASSWORD`). `server=` becomes `-ComputerName`. Parameters
|
|
218
|
+
travel as a JSON payload, never interpolated into the script. The scope is the
|
|
219
|
+
zone's network address, overridable with `scope` **on the zone**.
|
|
220
|
+
|
|
221
|
+
Each raises `PixieLookupError` for a target with no MAC — every one of these
|
|
222
|
+
servers identifies a reservation by it.
|
|
223
|
+
|
|
224
|
+
## DHCP options (`netboot.dhcp.options`)
|
|
225
|
+
|
|
226
|
+
- **`GENERIC_OPTIONS`** — the option names netboot models and every backend
|
|
227
|
+
translates (`router`, `domain-name-servers`, `domain-name`, `domain-search`,
|
|
228
|
+
`next-server`, `boot-file-name`, `tftp-server-name`, `host-name`,
|
|
229
|
+
`subnet-mask`, `broadcast-address`, `lease-time`, `ntp-servers`,
|
|
230
|
+
`vendor-class-identifier`). A numeric `option-<n>` or a bare number is always
|
|
231
|
+
accepted, for anything not modelled.
|
|
232
|
+
- **`APPLY_TIME`** — names that are resolved **when a target is applied**, never
|
|
233
|
+
from a connection string: `subnet_id`/`scope` belong to the **zone**,
|
|
234
|
+
`boot-file-name`/`next-server`/`tftp-server-name` to the **image or target**,
|
|
235
|
+
`host-name` to the target itself. Passing one in a URI query raises
|
|
236
|
+
`PixieConfigError` naming where it belongs — a boot file pinned to a
|
|
237
|
+
connection would hand every target on that server the same one.
|
|
238
|
+
- **`DhcpOptions(dict)`** — the merged options, plus **`.raw`** /
|
|
239
|
+
**`.raw_for(backend)`**: backend-native text from `raw.<backend>=` in the
|
|
240
|
+
query, never translated.
|
|
241
|
+
- **`build_options(ctx, server) -> DhcpOptions`** — merges, later winning: zone
|
|
242
|
+
defaults (from `gateway`/`nameservers`/`domain`/`search`/`network`) → the
|
|
243
|
+
server URI's query → `image.dhcp_options` → `target.dhcp_options` → the
|
|
244
|
+
server's `options_builder=` callback (`fn(ctx, options) -> options`) → the
|
|
245
|
+
`PixieEvent.BuildDhcpOptions` hook. An unknown option name raises.
|
|
246
|
+
- **`split_query(uri, settings, backend)`** → `(settings, options, builder)`.
|
|
247
|
+
A repeated key becomes a list, which is how a multi-valued option is written.
|
|
248
|
+
|
|
185
249
|
## Content (`netboot.content`)
|
|
186
250
|
|
|
187
251
|
- **`Resource(**kwargs)`** — `path` (`Pathname`, coerced via `_parse_path`),
|
|
@@ -14,6 +14,20 @@ from ..utils.net import Host
|
|
|
14
14
|
_HTTP_SCHEMES = ("http", "https")
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
def _split_port(text: str) -> "tuple[str, _ty.Optional[int]]":
|
|
18
|
+
"""Split `host:port`, `[v6]:port` or a bare host into its two parts."""
|
|
19
|
+
if not text:
|
|
20
|
+
return "", None
|
|
21
|
+
if text.startswith("["): # [2001:db8::1]:8080
|
|
22
|
+
literal, _, rest = text.partition("]")
|
|
23
|
+
port = rest.lstrip(":")
|
|
24
|
+
return literal + "]", int(port) if port.isdigit() else None
|
|
25
|
+
head, sep, tail = text.rpartition(":")
|
|
26
|
+
if sep and tail.isdigit() and head and ":" not in head:
|
|
27
|
+
return head, int(tail)
|
|
28
|
+
return text, None
|
|
29
|
+
|
|
30
|
+
|
|
17
31
|
def _scheme_of(value: object) -> str:
|
|
18
32
|
"""The scheme of a configured service URI, or ``""`` for a bare path."""
|
|
19
33
|
text = str(value)
|
|
@@ -124,18 +138,18 @@ class Repository(_NS):
|
|
|
124
138
|
def _service_host(self, scheme: str) -> "tuple[str, _ty.Optional[int]]":
|
|
125
139
|
"""The authority for a relative service path: host and optional port.
|
|
126
140
|
|
|
127
|
-
|
|
128
|
-
|
|
141
|
+
The port is split off **before** anything is resolved: an address like
|
|
142
|
+
`mirror.example:8080` is a host and a port, and asking DNS for the whole
|
|
143
|
+
string is a lookup that can only fail (slowly, and differently on every
|
|
144
|
+
machine).
|
|
145
|
+
|
|
146
|
+
TLS is the exception to resolving at all: a certificate is issued for a
|
|
147
|
+
name, and a name-based virtual host needs one too, so `https` keeps the
|
|
129
148
|
address as configured. Other schemes use the resolved IP, because a PXE
|
|
130
149
|
client often has no working DNS when it fetches its boot files.
|
|
131
150
|
"""
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
configured
|
|
135
|
-
if scheme == "https" and configured
|
|
136
|
-
else str(self.address.try_ip())
|
|
137
|
-
)
|
|
138
|
-
if not text:
|
|
151
|
+
host, port = _split_port(str(self.address))
|
|
152
|
+
if not host:
|
|
139
153
|
LOGGER.warning(
|
|
140
154
|
"repository has no address, so %s:// URLs are built without a "
|
|
141
155
|
"host; give the repo an `address`, or write the service as a "
|
|
@@ -143,16 +157,9 @@ class Repository(_NS):
|
|
|
143
157
|
scheme,
|
|
144
158
|
)
|
|
145
159
|
return "", None
|
|
146
|
-
if
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return literal + "]", int(port) if port.isdigit() else None
|
|
150
|
-
head, sep, tail = text.rpartition(":")
|
|
151
|
-
if sep and tail.isdigit() and head and ":" not in head:
|
|
152
|
-
# "mirror.example:8080" is a host and a port, not a host whose name
|
|
153
|
-
# contains a colon (which percent-encoded into `%3A`).
|
|
154
|
-
return head, int(tail)
|
|
155
|
-
return text, None
|
|
160
|
+
if scheme == "https":
|
|
161
|
+
return host, port
|
|
162
|
+
return str(Host(host).try_ip()), port
|
|
156
163
|
|
|
157
164
|
def service(self, name: str):
|
|
158
165
|
"""The base URI for a service name, `None` for `.local` when unset.
|
|
@@ -1,17 +1,51 @@
|
|
|
1
|
+
import importlib as _importlib
|
|
1
2
|
import typing as _ty
|
|
2
3
|
from argparse import Namespace
|
|
3
4
|
|
|
4
|
-
from
|
|
5
|
+
from ..utils import net as netutils
|
|
5
6
|
from yaconfiglib import OpaqueMerge
|
|
6
7
|
|
|
7
|
-
from
|
|
8
|
-
from
|
|
8
|
+
from ..logging import LOGGER
|
|
9
|
+
from ..utils.net import IPAddress, IPInterface, IPNetwork
|
|
9
10
|
|
|
10
11
|
if _ty.TYPE_CHECKING:
|
|
11
|
-
from
|
|
12
|
+
from .. import PixieContext
|
|
12
13
|
|
|
13
14
|
from urllib.parse import urlparse
|
|
14
15
|
|
|
16
|
+
from .options import DhcpOptions, build_options, split_query
|
|
17
|
+
|
|
18
|
+
#: Backends netboot ships, and the extra each needs. Used only to turn "no
|
|
19
|
+
#: backend for scheme" into a message that names the fix.
|
|
20
|
+
_SHIPPED = {
|
|
21
|
+
"dnsmasq": "netboot[ssh] for remote paths (local needs nothing)",
|
|
22
|
+
"kea": "netboot[kea]",
|
|
23
|
+
"dhcpd": "netboot[dhcpd]",
|
|
24
|
+
"windhcp": "netboot[winrm] for WinRM (ssh needs nothing)",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#: Scheme -> the module that defines it, when they differ (`keas://` is the
|
|
28
|
+
#: same backend as `kea://`, over https).
|
|
29
|
+
_ALIASES = {"keas": "kea"}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _load_backend(scheme: "str|None") -> None:
|
|
33
|
+
"""Import the shipped backend for `scheme`, if there is one.
|
|
34
|
+
|
|
35
|
+
Keeps `import netboot.dhcp` free of requests/paramiko/pywinrm: a backend and
|
|
36
|
+
its dependency are only imported when a config actually names its scheme. A
|
|
37
|
+
missing *backend module* is not an error (a plugin may provide the scheme),
|
|
38
|
+
but a backend that fails to import for its own reasons must say so.
|
|
39
|
+
"""
|
|
40
|
+
if not scheme or not scheme.isidentifier():
|
|
41
|
+
return
|
|
42
|
+
module = _ALIASES.get(scheme, scheme)
|
|
43
|
+
try:
|
|
44
|
+
_importlib.import_module(f"{__name__}.{module}")
|
|
45
|
+
except ModuleNotFoundError as exc:
|
|
46
|
+
if exc.name != f"{__name__}.{module}":
|
|
47
|
+
raise # the backend imported, one of *its* imports is missing
|
|
48
|
+
|
|
15
49
|
|
|
16
50
|
def _iter_subclasses(cls: type) -> "_ty.Iterator[type]":
|
|
17
51
|
"""Yield every subclass of ``cls``, recursively (not just direct children).
|
|
@@ -35,10 +69,15 @@ class DhcpServer:
|
|
|
35
69
|
|
|
36
70
|
DEFAULT_SCHEME = None
|
|
37
71
|
|
|
72
|
+
#: Query keys this backend reads as connection settings. Everything else in
|
|
73
|
+
#: the query is a client option -- the two sets must stay disjoint.
|
|
74
|
+
SETTINGS: "frozenset[str]" = frozenset()
|
|
75
|
+
|
|
38
76
|
def __new__(cls, uri: str):
|
|
39
77
|
if cls is DhcpServer:
|
|
40
78
|
parsed = urlparse(uri)
|
|
41
79
|
scheme = parsed.scheme or cls.DEFAULT_SCHEME
|
|
80
|
+
_load_backend(scheme)
|
|
42
81
|
handlers = [
|
|
43
82
|
sub for sub in _iter_subclasses(cls) if sub.__name__.lower() == scheme
|
|
44
83
|
]
|
|
@@ -54,14 +93,27 @@ class DhcpServer:
|
|
|
54
93
|
)
|
|
55
94
|
if handlers:
|
|
56
95
|
return object.__new__(handlers[-1])
|
|
96
|
+
hint = _SHIPPED.get(scheme)
|
|
57
97
|
raise ValueError(
|
|
58
98
|
f"No DhcpServer backend registered for scheme {scheme!r} "
|
|
59
|
-
f"(uri={uri!r});
|
|
99
|
+
f"(uri={uri!r}); "
|
|
100
|
+
+ (
|
|
101
|
+
f"install {hint}"
|
|
102
|
+
if hint
|
|
103
|
+
else "import a plugin module providing it via --load-module"
|
|
104
|
+
)
|
|
60
105
|
)
|
|
61
106
|
return object.__new__(cls)
|
|
62
107
|
|
|
63
108
|
def __init__(self, uri: str):
|
|
64
109
|
self.uri = uri
|
|
110
|
+
self.settings, self.options, self.options_builder = split_query(
|
|
111
|
+
uri, self.SETTINGS, type(self).__name__
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def options_for(self, ctx) -> DhcpOptions:
|
|
115
|
+
"""The client options for this target on this server, fully merged."""
|
|
116
|
+
return build_options(ctx, self)
|
|
65
117
|
|
|
66
118
|
def remove_target(self, netboot: "PixieContext"):
|
|
67
119
|
"""Disarm this backend for the target in `netboot` (a `PixieContext`)."""
|