netboot 0.2.0__tar.gz → 0.2.2__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.2}/CHANGELOG.md +99 -1
- {netboot-0.2.0 → netboot-0.2.2}/PKG-INFO +23 -7
- {netboot-0.2.0 → netboot-0.2.2}/README.md +10 -4
- {netboot-0.2.0 → netboot-0.2.2}/docs/configuration.md +64 -5
- {netboot-0.2.0 → netboot-0.2.2}/docs/extending.md +7 -0
- {netboot-0.2.0 → netboot-0.2.2}/docs/index.md +4 -0
- {netboot-0.2.0 → netboot-0.2.2}/examples/README.md +5 -0
- {netboot-0.2.0 → netboot-0.2.2}/examples/config/pixie.yaml +11 -1
- {netboot-0.2.0 → netboot-0.2.2}/pyproject.toml +30 -9
- {netboot-0.2.0 → netboot-0.2.2}/src/netboot/AGENTS.md +80 -9
- {netboot-0.2.0 → netboot-0.2.2}/src/netboot/content/__init__.py +26 -19
- netboot-0.2.0/src/netboot/dhcp.py → netboot-0.2.2/src/netboot/dhcp/__init__.py +57 -5
- netboot-0.2.2/src/netboot/dhcp/dhcpd.py +204 -0
- netboot-0.2.2/src/netboot/dhcp/dnsmasq.py +311 -0
- netboot-0.2.2/src/netboot/dhcp/kea.py +209 -0
- netboot-0.2.2/src/netboot/dhcp/options.py +226 -0
- netboot-0.2.2/src/netboot/dhcp/windhcp.py +249 -0
- {netboot-0.2.0 → netboot-0.2.2}/src/netboot/engine.py +4 -0
- {netboot-0.2.0 → netboot-0.2.2}/src/netboot/utils/net.py +2 -2
- {netboot-0.2.0 → netboot-0.2.2}/tests/test_config_discovery.py +19 -0
- {netboot-0.2.0 → netboot-0.2.2}/tests/test_content.py +7 -3
- netboot-0.2.2/tests/test_dhcp_dhcpd.py +211 -0
- netboot-0.2.2/tests/test_dhcp_dnsmasq.py +225 -0
- netboot-0.2.2/tests/test_dhcp_kea.py +236 -0
- netboot-0.2.2/tests/test_dhcp_options.py +270 -0
- netboot-0.2.2/tests/test_dhcp_windhcp.py +230 -0
- {netboot-0.2.0 → netboot-0.2.2}/.gitignore +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/AGENTS.md +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/LICENSE +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/benchmarks/README.md +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/benchmarks/bench_netboot.py +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/benchmarks/results/netboot.json +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/docs/api.md +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/docs/changelog.md +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/docs/cli.md +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/examples/plugins/recording.py +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/examples/templates/debian/boot.cfg +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/examples/templates/debian/install.ks.j2 +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/mkdocs.yml +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/src/netboot/__init__.py +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/src/netboot/__main__.py +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/src/netboot/_version.py +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/src/netboot/cmds/__init__.py +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/src/netboot/cmds/complete.py +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/src/netboot/cmds/initiate.py +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/src/netboot/logging.py +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/src/netboot/main.py +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/src/netboot/py.typed +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/src/netboot/templates/__init__.py +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/src/netboot/templates/common.py +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/src/netboot/templates/jinja.py +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/src/netboot/templates/shell.py +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/src/netboot/utils/__init__.py +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/src/netboot/utils/config.py +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/src/netboot/utils/dicts.py +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/src/netboot/utils/misc.py +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/tests/conftest.py +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/tests/test_api_contracts.py +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/tests/test_cli.py +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/tests/test_cli_discovery.py +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/tests/test_dhcp.py +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/tests/test_engine_robustness.py +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/tests/test_hooks.py +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/tests/test_lookup.py +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/tests/test_render.py +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/tests/test_target.py +0 -0
- {netboot-0.2.0 → netboot-0.2.2}/tests/test_utils.py +0 -0
|
@@ -6,6 +6,102 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.2.2] - 2026-09-21
|
|
10
|
+
|
|
11
|
+
netboot moves to netimps 0.3.1 and yaconfiglib 0.13. If a plugin, hook or
|
|
12
|
+
template compares `target.mac` with text, read the `MACAddress` entry below.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- Dependencies move to their new series: `netimps>=0.3.1,<0.4` and
|
|
16
|
+
`yaconfiglib>=0.13.0,<0.14`. The old ceilings made netboot uninstallable
|
|
17
|
+
beside either release. netimps 0.3.0 is skipped on purpose: it raised from
|
|
18
|
+
`resolve()` when no resolver was reachable, which would have failed every
|
|
19
|
+
command at startup, and 0.3.1 answers `[]` again. netboot uses none of the
|
|
20
|
+
yaconfiglib APIs 0.13.0 removed.
|
|
21
|
+
- **`MACAddress` no longer equals a `str`** (netimps 0.3.0, re-exported as
|
|
22
|
+
`netboot.utils.net.MACAddress`). `target.mac == "aa:bb:cc:dd:ee:ff"` is now
|
|
23
|
+
`False` — in a plugin, a hook or a Jinja template alike. Compare with
|
|
24
|
+
`MACAddress.try_parse(text) == target.mac`. In exchange a MAC is found in a
|
|
25
|
+
set or dict whichever spelling built it. netboot's own target lookup already
|
|
26
|
+
compared parsed MACs and is unaffected. A MAC that mixes separators
|
|
27
|
+
(`00-11:22-33:44-55`) is now rejected, and `aa.bb.cc.dd.ee.ff` is accepted.
|
|
28
|
+
- `resolve()` keeps trying backends after an empty answer, so a hostname
|
|
29
|
+
target known only to the hosts file or NSS now gets its IP on Windows and
|
|
30
|
+
macOS. A name that exists nowhere costs two or three lookups instead of one.
|
|
31
|
+
|
|
32
|
+
## [0.2.1] - 2026-09-17
|
|
33
|
+
|
|
34
|
+
netboot ships DHCP backends. Additive: nothing documented changed behaviour,
|
|
35
|
+
and a config that named no `dhcpservers` scheme netboot implements works as
|
|
36
|
+
before.
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
- A repository `address` of the form `host:port` no longer sends the whole
|
|
40
|
+
string to DNS. The port is split off first, so `mirror.example:8080`
|
|
41
|
+
resolves `mirror.example` — the old lookup could only fail, and did so at
|
|
42
|
+
different speeds on different machines.
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
- `netboot.dhcp` is a package, and DHCP backends carry **client options**. A
|
|
46
|
+
`dhcpservers` entry is still a plain URI string; its query string now holds the
|
|
47
|
+
options the server should give the client (`?router=10.0.0.1`), alongside each
|
|
48
|
+
backend's own connection settings. `netboot.dhcp.options` models them:
|
|
49
|
+
`GENERIC_OPTIONS` for the names netboot translates, `option-<n>` for anything
|
|
50
|
+
else, `raw.<backend>=` for untranslated backend-native text, and
|
|
51
|
+
`options_builder=my.mod.fn` plus the new `PixieEvent.BuildDhcpOptions` for
|
|
52
|
+
building them from the context. Merge order, later winning: zone defaults, the
|
|
53
|
+
connection query, `image.dhcp_options`, `target.dhcp_options`, the callback,
|
|
54
|
+
the hook.
|
|
55
|
+
- **A dnsmasq backend** (`dnsmasq://`), the first netboot ships. It writes
|
|
56
|
+
`dhcp-host`/`dhcp-option`/`dhcp-boot` entries through `pathlib_next` paths, so
|
|
57
|
+
the same configuration serves a local dnsmasq or one reached over `sftp://`
|
|
58
|
+
(`netboot[ssh]`). `hostsfile`/`optsfile` may be a **directory** (one file per
|
|
59
|
+
target — preferred, and with `--dhcp-hostsdir`/`--dhcp-optsdir` dnsmasq re-reads
|
|
60
|
+
them with no signal at all) or a **file**, where netboot edits only its own
|
|
61
|
+
marked region and never touches a line it did not write. `reload=` runs a
|
|
62
|
+
command, over ssh when the URI names a host; omitting it is fine for a watched
|
|
63
|
+
directory and an error for a file, which dnsmasq never re-reads by itself.
|
|
64
|
+
- **A Kea backend** (`kea://`, `keas://` for https). It sends `reservation-add`
|
|
65
|
+
and `reservation-del` to the control agent, mapping `boot-file-name`,
|
|
66
|
+
`next-server` and `host-name` onto reservation *fields* and everything else
|
|
67
|
+
onto `option-data`. The `subnet-id` comes from the zone (`subnet_id:`) or is
|
|
68
|
+
discovered once by matching the zone's network against `config-get`; when
|
|
69
|
+
neither works netboot refuses rather than guessing, because a reservation in
|
|
70
|
+
the wrong subnet silently never matches. A `result: 2` from Kea is reported
|
|
71
|
+
with the likely cause (host_cmds not loaded, or a read-only hosts backend),
|
|
72
|
+
and an existing reservation for the same MAC is refused rather than
|
|
73
|
+
overwritten. Needs `netboot[kea]`.
|
|
74
|
+
- **An ISC dhcpd backend** (`dhcpd://`) over OMAPI, the only way to give dhcpd a
|
|
75
|
+
reservation without rewriting and reloading its config. Options are rendered
|
|
76
|
+
into the host's `statements`, which is dhcpd config *source*: text values are
|
|
77
|
+
quoted with `"` and `\` escaped, a value meant to be an address must look like
|
|
78
|
+
one, and a newline is refused outright — a value cannot end its statement and
|
|
79
|
+
start another. The OMAPI secret comes from `keyfile=` or
|
|
80
|
+
`$PIXIE_DHCPD_OMAPI_KEY`, never from the URI. Needs `netboot[dhcpd]`. Note
|
|
81
|
+
that a host added over OMAPI does not survive a dhcpd restart by itself: that
|
|
82
|
+
is dhcpd's design, not netboot forgetting it.
|
|
83
|
+
- **A Windows DHCP Server backend** (`windhcp://`), driving the `DhcpServer`
|
|
84
|
+
PowerShell module over **ssh** (the default, no dependency — it uses the system
|
|
85
|
+
`ssh` client, so your config, keys, agent and jump hosts apply) or **WinRM**
|
|
86
|
+
(`netboot[winrm]`). The transport host and the DHCP server are separate:
|
|
87
|
+
`server=` becomes `-ComputerName` when the cmdlets should act elsewhere. The
|
|
88
|
+
scope is the zone's network address, resolved when the target is applied and
|
|
89
|
+
overridable per zone. No value is interpolated into the script: parameters
|
|
90
|
+
travel as a JSON payload PowerShell parses, so a target name or option value
|
|
91
|
+
cannot become a statement.
|
|
92
|
+
- New extras, each carrying exactly one backend's dependency: `netboot[kea]`,
|
|
93
|
+
`netboot[dhcpd]`, `netboot[winrm]`, and `netboot[ssh]` for dnsmasq over
|
|
94
|
+
`sftp://`. A local dnsmasq and `windhcp://` over ssh need none. Verified on a
|
|
95
|
+
clean wheel install: importing `netboot.dhcp` loads no backend dependency,
|
|
96
|
+
those two backends construct, and the other two raise `ImportError` naming
|
|
97
|
+
their extra.
|
|
98
|
+
- Anything that varies per target is refused in a connection string and resolved
|
|
99
|
+
when the target is applied instead: `subnet_id`/`scope` belong to the zone,
|
|
100
|
+
`boot-file-name`/`next-server`/`tftp-server-name` to the image or target. The
|
|
101
|
+
error names where each belongs. `from netboot.dhcp import DhcpServer, DhcpZone`
|
|
102
|
+
is unchanged, and a backend module is imported only when a config names its
|
|
103
|
+
scheme.
|
|
104
|
+
|
|
9
105
|
## [0.2.0] - 2026-09-17
|
|
10
106
|
|
|
11
107
|
A correctness and hardening release from a full review of the code base.
|
|
@@ -447,7 +543,9 @@ First packaged release: the `netboot` library with the `pixie` command line.
|
|
|
447
543
|
config value construction no longer swallows non-`TypeError` errors; repo
|
|
448
544
|
`joinpath` keeps `.local` a path so chained joins work.
|
|
449
545
|
|
|
450
|
-
[Unreleased]: https://github.com/jose-pr/netboot/compare/v0.2.
|
|
546
|
+
[Unreleased]: https://github.com/jose-pr/netboot/compare/v0.2.2...HEAD
|
|
547
|
+
[0.2.2]: https://github.com/jose-pr/netboot/compare/v0.2.1...v0.2.2
|
|
548
|
+
[0.2.1]: https://github.com/jose-pr/netboot/compare/v0.2.0...v0.2.1
|
|
451
549
|
[0.2.0]: https://github.com/jose-pr/netboot/compare/v0.1.3...v0.2.0
|
|
452
550
|
[0.1.3]: https://github.com/jose-pr/netboot/compare/v0.1.2...v0.1.3
|
|
453
551
|
[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.2
|
|
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
|
|
@@ -27,9 +27,9 @@ Classifier: Typing :: Typed
|
|
|
27
27
|
Requires-Python: >=3.9
|
|
28
28
|
Requires-Dist: duho<0.6,>=0.5.0
|
|
29
29
|
Requires-Dist: jinja2<4,>=3.0
|
|
30
|
-
Requires-Dist: netimps<0.
|
|
30
|
+
Requires-Dist: netimps<0.4,>=0.3.1
|
|
31
31
|
Requires-Dist: pathlib-next[uri]<0.10,>=0.9.9
|
|
32
|
-
Requires-Dist: yaconfiglib<0.
|
|
32
|
+
Requires-Dist: yaconfiglib<0.14,>=0.13.0
|
|
33
33
|
Provides-Extra: config
|
|
34
34
|
Requires-Dist: pyyaml; extra == 'config'
|
|
35
35
|
Provides-Extra: dev
|
|
@@ -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.2"
|
|
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"
|
|
@@ -30,10 +30,12 @@ classifiers = [
|
|
|
30
30
|
]
|
|
31
31
|
dependencies = [
|
|
32
32
|
"duho>=0.5.0,<0.6",
|
|
33
|
-
# Above the .0 deliberately:
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
|
|
33
|
+
# Above the .0 deliberately: 0.3.0 made resolve() raise ResolutionError
|
|
34
|
+
# when no backend could even ask (a resolver outage), and 0.3.1 restored
|
|
35
|
+
# [] as the default (strict=False). PixieTarget and Host.try_ip rely on
|
|
36
|
+
# that: every target is built at startup, so a raise would fail every
|
|
37
|
+
# command. Don't normalize this back to 0.3.0.
|
|
38
|
+
"netimps>=0.3.1,<0.4",
|
|
37
39
|
# Above the .0 deliberately, measured 2026-09-17: recursive `!include`
|
|
38
40
|
# globs (`sub/**/*.yaml`) break on pathlib_next >=0.9.5 unless yaconfiglib
|
|
39
41
|
# is >=0.12.0, and yaconfiglib 0.12.0 itself requires pathlib-next>=0.9.9.
|
|
@@ -44,9 +46,7 @@ dependencies = [
|
|
|
44
46
|
# markupsafe.soft_unicode, removed in MarkupSafe 2.1, so an unpinned
|
|
45
47
|
# resolve can install a jinja2/markupsafe pair that fails on import.
|
|
46
48
|
"jinja2>=3.0,<4",
|
|
47
|
-
|
|
48
|
-
# pathlib-next this project uses, recursive config discovery needs 0.12.0.
|
|
49
|
-
"yaconfiglib>=0.12.0,<0.13",
|
|
49
|
+
"yaconfiglib>=0.13.0,<0.14",
|
|
50
50
|
]
|
|
51
51
|
|
|
52
52
|
[project.optional-dependencies]
|
|
@@ -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`,
|
|
@@ -110,9 +110,10 @@ is the same object. `netboot.netutils` remains an alias for
|
|
|
110
110
|
non-MAC `_id` fills `hostname`; then, if `ip` is still unset and a hostname
|
|
111
111
|
is known, **exactly one** forward lookup
|
|
112
112
|
(`netboot.utils.net.resolve`, no reverse lookup) fills it. A name that does
|
|
113
|
-
not resolve
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
not resolve — including when no resolver is reachable at all — leaves `ip`
|
|
114
|
+
unset and logs a warning; it is never retried, and never raises. `hostname`
|
|
115
|
+
is lower-cased. The `dns` extra is optional: without it netimps still
|
|
116
|
+
resolves through its system/`nslookup` backends.
|
|
116
117
|
|
|
117
118
|
- **`PixieImage(**kwargs)`** (`content.Resource`) — `template_path`,
|
|
118
119
|
`globals`, plus `Resource`'s `src`/`path`, which address the image's
|
|
@@ -173,6 +174,12 @@ is the same object. `netboot.netutils` remains an alias for
|
|
|
173
174
|
skips anything unparseable with a warning, and returns `default` when no
|
|
174
175
|
server lies inside the zone's network.
|
|
175
176
|
|
|
177
|
+
- **`DhcpServer.SETTINGS`** — the query keys a backend reads as *connection*
|
|
178
|
+
settings. Every other key in a `dhcpservers` URI query is a **client option**
|
|
179
|
+
(see `netboot.dhcp.options`), so the two vocabularies must stay disjoint.
|
|
180
|
+
- **`DhcpServer.options_for(ctx) -> DhcpOptions`** — the merged client options
|
|
181
|
+
for this target on this server.
|
|
182
|
+
|
|
176
183
|
- **`DhcpZone(**kwargs)`** (`Namespace` + `OpaqueMerge`) — `network`
|
|
177
184
|
(`IPNetwork`), `gateway` (`IPAddress | None`), `domain` (`str | None`),
|
|
178
185
|
`search` (`list[str]`), `nameservers` (`list[IPAddress]`), `globals`,
|
|
@@ -182,6 +189,64 @@ is the same object. `netboot.netutils` remains an alias for
|
|
|
182
189
|
**`.get_local_server(servers, default)`** — first `server` contained in
|
|
183
190
|
`.network`, else `default`.
|
|
184
191
|
|
|
192
|
+
## DHCP backends (`netboot.dhcp.*`)
|
|
193
|
+
|
|
194
|
+
Four ship, each imported only when a config names its scheme — so importing
|
|
195
|
+
`netboot.dhcp` pulls in none of their dependencies. Every one takes its client
|
|
196
|
+
options from the URI query (see below) and translates them.
|
|
197
|
+
|
|
198
|
+
- **`dnsmasq://[host]/?hostsfile=&optsfile=&reload=&pidfile=`**
|
|
199
|
+
(`netboot.dhcp.dnsmasq`) — writes `dhcp-host`/`dhcp-option`/`dhcp-boot`
|
|
200
|
+
entries. Paths may carry their own scheme; a bare path is local when the URI
|
|
201
|
+
has no host and `sftp://<host>/<path>` when it does (`netboot[ssh]`). A
|
|
202
|
+
**directory** gets one file per target; a **file** gets a marked region netboot
|
|
203
|
+
owns. `reload=` runs a command, over ssh when the URI names a host — omit it
|
|
204
|
+
for a `--dhcp-hostsdir`, which dnsmasq re-reads by itself; a plain file
|
|
205
|
+
without it raises.
|
|
206
|
+
- **`kea://`, `keas://`** (`netboot.dhcp.kea`, `netboot[kea]`) —
|
|
207
|
+
`reservation-add`/`reservation-del` through the control agent. `?service=`
|
|
208
|
+
selects `dhcp4`/`dhcp6`. Needs the **host_cmds** hook and a writable hosts
|
|
209
|
+
backend; `result: 2` is reported with that cause. `subnet-id` comes from the
|
|
210
|
+
zone (`subnet_id`) or a matched `config-get`.
|
|
211
|
+
- **`dhcpd://host:7911/?keyname=&keyfile=`** (`netboot.dhcp.dhcpd`,
|
|
212
|
+
`netboot[dhcpd]`) — an OMAPI host object whose `statements` carry the options,
|
|
213
|
+
escaped. Secret from `keyfile=` or `$PIXIE_DHCPD_OMAPI_KEY`. A host added this
|
|
214
|
+
way does **not** survive a dhcpd restart by itself.
|
|
215
|
+
- **`windhcp://[user@]host/?transport=ssh|winrm&server=&auth=&port=&ssl=`**
|
|
216
|
+
(`netboot.dhcp.windhcp`) — the `DhcpServer` PowerShell cmdlets over the system
|
|
217
|
+
`ssh` client (default, no dependency) or WinRM (`netboot[winrm]`, password
|
|
218
|
+
from `$PIXIE_WINDHCP_PASSWORD`). `server=` becomes `-ComputerName`. Parameters
|
|
219
|
+
travel as a JSON payload, never interpolated into the script. The scope is the
|
|
220
|
+
zone's network address, overridable with `scope` **on the zone**.
|
|
221
|
+
|
|
222
|
+
Each raises `PixieLookupError` for a target with no MAC — every one of these
|
|
223
|
+
servers identifies a reservation by it.
|
|
224
|
+
|
|
225
|
+
## DHCP options (`netboot.dhcp.options`)
|
|
226
|
+
|
|
227
|
+
- **`GENERIC_OPTIONS`** — the option names netboot models and every backend
|
|
228
|
+
translates (`router`, `domain-name-servers`, `domain-name`, `domain-search`,
|
|
229
|
+
`next-server`, `boot-file-name`, `tftp-server-name`, `host-name`,
|
|
230
|
+
`subnet-mask`, `broadcast-address`, `lease-time`, `ntp-servers`,
|
|
231
|
+
`vendor-class-identifier`). A numeric `option-<n>` or a bare number is always
|
|
232
|
+
accepted, for anything not modelled.
|
|
233
|
+
- **`APPLY_TIME`** — names that are resolved **when a target is applied**, never
|
|
234
|
+
from a connection string: `subnet_id`/`scope` belong to the **zone**,
|
|
235
|
+
`boot-file-name`/`next-server`/`tftp-server-name` to the **image or target**,
|
|
236
|
+
`host-name` to the target itself. Passing one in a URI query raises
|
|
237
|
+
`PixieConfigError` naming where it belongs — a boot file pinned to a
|
|
238
|
+
connection would hand every target on that server the same one.
|
|
239
|
+
- **`DhcpOptions(dict)`** — the merged options, plus **`.raw`** /
|
|
240
|
+
**`.raw_for(backend)`**: backend-native text from `raw.<backend>=` in the
|
|
241
|
+
query, never translated.
|
|
242
|
+
- **`build_options(ctx, server) -> DhcpOptions`** — merges, later winning: zone
|
|
243
|
+
defaults (from `gateway`/`nameservers`/`domain`/`search`/`network`) → the
|
|
244
|
+
server URI's query → `image.dhcp_options` → `target.dhcp_options` → the
|
|
245
|
+
server's `options_builder=` callback (`fn(ctx, options) -> options`) → the
|
|
246
|
+
`PixieEvent.BuildDhcpOptions` hook. An unknown option name raises.
|
|
247
|
+
- **`split_query(uri, settings, backend)`** → `(settings, options, builder)`.
|
|
248
|
+
A repeated key becomes a list, which is how a multi-valued option is written.
|
|
249
|
+
|
|
185
250
|
## Content (`netboot.content`)
|
|
186
251
|
|
|
187
252
|
- **`Resource(**kwargs)`** — `path` (`Pathname`, coerced via `_parse_path`),
|
|
@@ -273,7 +338,8 @@ is the same object. `netboot.netutils` remains an alias for
|
|
|
273
338
|
- **`Host(address: str | Host | None = None)`** — a hostname-or-IP repo
|
|
274
339
|
address. **`.try_ip() -> IPAddress | str`** — resolves to an `IPAddress`
|
|
275
340
|
(an IP literal as-is, a hostname through one forward lookup); falls back to
|
|
276
|
-
the original string on resolution failure
|
|
341
|
+
the original string on resolution failure, an unreachable resolver
|
|
342
|
+
included. The `dns` extra is optional —
|
|
277
343
|
without it netimps resolves through its system/`nslookup` backends.
|
|
278
344
|
Equality/hash by `.address`.
|
|
279
345
|
- **`flatten(map, _prefix="") -> dict`** — recursively flattens a
|
|
@@ -304,13 +370,18 @@ is the same object. `netboot.netutils` remains an alias for
|
|
|
304
370
|
- `parse(value, type, **kw)` raises on bad input; `try_parse(value, type)`
|
|
305
371
|
returns `None` instead; `is_valid(value, type)` returns a bool. Networks
|
|
306
372
|
parse non-strict, so `"10.0.0.5/24"` normalises rather than raising.
|
|
307
|
-
- `MACAddress` — colon/hyphen/Cisco-dot/bare text
|
|
308
|
-
`.as_str(sep)`, `.packed`,
|
|
373
|
+
- `MACAddress` — colon/hyphen/per-octet-dot/Cisco-dot/bare text (one
|
|
374
|
+
separator per address), `int` or `bytes`; `.as_str(sep)`, `.packed`,
|
|
375
|
+
`.oui`, hashable and ordered. It equals only another `MACAddress`, never a
|
|
376
|
+
`str` — compare text with `MACAddress.try_parse(text) == mac`.
|
|
309
377
|
- `resolve(query, rdtype=None)` → a list of **native** records (`A`/`AAAA`
|
|
310
378
|
are `IPv4Address`/`IPv6Address`, not strings); `[]` on a genuine lookup
|
|
311
379
|
failure, but a malformed query or unknown record type raises `ValueError`
|
|
312
380
|
rather than looking like "no such record". `rdtype=None` auto-selects:
|
|
313
|
-
`"ptr"` when `query` is an address literal, `"a"` otherwise.
|
|
381
|
+
`"ptr"` when `query` is an address literal, `"a"` otherwise. Backends are
|
|
382
|
+
tried until one answers non-empty, so hosts-file and NSS-only names
|
|
383
|
+
resolve too. An unreachable resolver also answers `[]`; netimps'
|
|
384
|
+
`strict=True` raises `netimps.ResolutionError` instead.
|
|
314
385
|
- `ping(dst)` → a `PingResult` that is truthy on success and also carries
|
|
315
386
|
`.rtt_ms`/`.ttl`.
|
|
316
387
|
- `get_interfaces()` / `iter_addresses()` — real adapter enumeration with
|
|
@@ -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.
|