netboot 0.2.1__tar.gz → 0.2.3__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.1 → netboot-0.2.3}/CHANGELOG.md +37 -1
- {netboot-0.2.1 → netboot-0.2.3}/PKG-INFO +3 -3
- {netboot-0.2.1 → netboot-0.2.3}/pyproject.toml +8 -8
- {netboot-0.2.1 → netboot-0.2.3}/src/netboot/AGENTS.md +20 -13
- {netboot-0.2.1 → netboot-0.2.3}/src/netboot/engine.py +29 -30
- {netboot-0.2.1 → netboot-0.2.3}/src/netboot/utils/net.py +2 -2
- {netboot-0.2.1 → netboot-0.2.3}/tests/test_config_discovery.py +19 -0
- {netboot-0.2.1 → netboot-0.2.3}/tests/test_engine_robustness.py +27 -16
- {netboot-0.2.1 → netboot-0.2.3}/.gitignore +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/AGENTS.md +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/LICENSE +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/README.md +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/benchmarks/README.md +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/benchmarks/bench_netboot.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/benchmarks/results/netboot.json +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/docs/api.md +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/docs/changelog.md +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/docs/cli.md +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/docs/configuration.md +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/docs/extending.md +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/docs/index.md +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/examples/README.md +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/examples/config/pixie.yaml +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/examples/plugins/recording.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/examples/templates/debian/boot.cfg +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/examples/templates/debian/install.ks.j2 +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/mkdocs.yml +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/src/netboot/__init__.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/src/netboot/__main__.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/src/netboot/_version.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/src/netboot/cmds/__init__.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/src/netboot/cmds/complete.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/src/netboot/cmds/initiate.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/src/netboot/content/__init__.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/src/netboot/dhcp/__init__.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/src/netboot/dhcp/dhcpd.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/src/netboot/dhcp/dnsmasq.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/src/netboot/dhcp/kea.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/src/netboot/dhcp/options.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/src/netboot/dhcp/windhcp.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/src/netboot/logging.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/src/netboot/main.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/src/netboot/py.typed +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/src/netboot/templates/__init__.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/src/netboot/templates/common.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/src/netboot/templates/jinja.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/src/netboot/templates/shell.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/src/netboot/utils/__init__.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/src/netboot/utils/config.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/src/netboot/utils/dicts.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/src/netboot/utils/misc.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/tests/conftest.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/tests/test_api_contracts.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/tests/test_cli.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/tests/test_cli_discovery.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/tests/test_content.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/tests/test_dhcp.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/tests/test_dhcp_dhcpd.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/tests/test_dhcp_dnsmasq.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/tests/test_dhcp_kea.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/tests/test_dhcp_options.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/tests/test_dhcp_windhcp.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/tests/test_hooks.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/tests/test_lookup.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/tests/test_render.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/tests/test_target.py +0 -0
- {netboot-0.2.1 → netboot-0.2.3}/tests/test_utils.py +0 -0
|
@@ -6,6 +6,40 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.2.3] - 2026-09-23
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- One DHCP server being offline no longer fails `pixie initiate` or
|
|
13
|
+
`pixie complete`. Arming is meant to be best effort, but since 0.2.0 it
|
|
14
|
+
stopped at the first failing server and rolled the others back. `initiate`
|
|
15
|
+
now tries every server in the zone, logs each failure as a warning, and fails
|
|
16
|
+
only when no server could be armed. `complete` logs a failing server as a
|
|
17
|
+
warning and still exits 0, so read the log if you need to know every server
|
|
18
|
+
was disarmed.
|
|
19
|
+
|
|
20
|
+
## [0.2.2] - 2026-09-21
|
|
21
|
+
|
|
22
|
+
netboot moves to netimps 0.3.1 and yaconfiglib 0.13. If a plugin, hook or
|
|
23
|
+
template compares `target.mac` with text, read the `MACAddress` entry below.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- Dependencies move to their new series: `netimps>=0.3.1,<0.4` and
|
|
27
|
+
`yaconfiglib>=0.13.0,<0.14`. The old ceilings made netboot uninstallable
|
|
28
|
+
beside either release. netimps 0.3.0 is skipped on purpose: it raised from
|
|
29
|
+
`resolve()` when no resolver was reachable, which would have failed every
|
|
30
|
+
command at startup, and 0.3.1 answers `[]` again. netboot uses none of the
|
|
31
|
+
yaconfiglib APIs 0.13.0 removed.
|
|
32
|
+
- **`MACAddress` no longer equals a `str`** (netimps 0.3.0, re-exported as
|
|
33
|
+
`netboot.utils.net.MACAddress`). `target.mac == "aa:bb:cc:dd:ee:ff"` is now
|
|
34
|
+
`False` — in a plugin, a hook or a Jinja template alike. Compare with
|
|
35
|
+
`MACAddress.try_parse(text) == target.mac`. In exchange a MAC is found in a
|
|
36
|
+
set or dict whichever spelling built it. netboot's own target lookup already
|
|
37
|
+
compared parsed MACs and is unaffected. A MAC that mixes separators
|
|
38
|
+
(`00-11:22-33:44-55`) is now rejected, and `aa.bb.cc.dd.ee.ff` is accepted.
|
|
39
|
+
- `resolve()` keeps trying backends after an empty answer, so a hostname
|
|
40
|
+
target known only to the hosts file or NSS now gets its IP on Windows and
|
|
41
|
+
macOS. A name that exists nowhere costs two or three lookups instead of one.
|
|
42
|
+
|
|
9
43
|
## [0.2.1] - 2026-09-17
|
|
10
44
|
|
|
11
45
|
netboot ships DHCP backends. Additive: nothing documented changed behaviour,
|
|
@@ -520,7 +554,9 @@ First packaged release: the `netboot` library with the `pixie` command line.
|
|
|
520
554
|
config value construction no longer swallows non-`TypeError` errors; repo
|
|
521
555
|
`joinpath` keeps `.local` a path so chained joins work.
|
|
522
556
|
|
|
523
|
-
[Unreleased]: https://github.com/jose-pr/netboot/compare/v0.2.
|
|
557
|
+
[Unreleased]: https://github.com/jose-pr/netboot/compare/v0.2.3...HEAD
|
|
558
|
+
[0.2.3]: https://github.com/jose-pr/netboot/compare/v0.2.2...v0.2.3
|
|
559
|
+
[0.2.2]: https://github.com/jose-pr/netboot/compare/v0.2.1...v0.2.2
|
|
524
560
|
[0.2.1]: https://github.com/jose-pr/netboot/compare/v0.2.0...v0.2.1
|
|
525
561
|
[0.2.0]: https://github.com/jose-pr/netboot/compare/v0.1.3...v0.2.0
|
|
526
562
|
[0.1.3]: https://github.com/jose-pr/netboot/compare/v0.1.2...v0.1.3
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: netboot
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
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
|
|
@@ -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.3"
|
|
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]
|
|
@@ -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
|
|
@@ -149,12 +150,12 @@ is the same object. `netboot.netutils` remains an alias for
|
|
|
149
150
|
process's working directory; an absolute path or a URI is used as given.
|
|
150
151
|
- **`.pxe_init(netboot) -> Self`** / **`.pxe_complete(netboot) -> Self`** —
|
|
151
152
|
arm/disarm every `dhcpzone.dhcpservers` for this context; called by
|
|
152
|
-
`Pixie.initialize`/`.complete`, not usually invoked directly.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
153
|
+
`Pixie.initialize`/`.complete`, not usually invoked directly. Both try
|
|
154
|
+
every backend. `pxe_init` logs a backend that raises as a **warning** and
|
|
155
|
+
carries on; it raises (the first error) only when **no** backend could be
|
|
156
|
+
armed, and nothing is rolled back, so a target may end up armed on some
|
|
157
|
+
servers and not others. `pxe_complete` never raises for a backend: each
|
|
158
|
+
failure is logged as a warning and the rest are still disarmed.
|
|
158
159
|
|
|
159
160
|
## DHCP (`netboot.dhcp`)
|
|
160
161
|
|
|
@@ -337,7 +338,8 @@ servers identifies a reservation by it.
|
|
|
337
338
|
- **`Host(address: str | Host | None = None)`** — a hostname-or-IP repo
|
|
338
339
|
address. **`.try_ip() -> IPAddress | str`** — resolves to an `IPAddress`
|
|
339
340
|
(an IP literal as-is, a hostname through one forward lookup); falls back to
|
|
340
|
-
the original string on resolution failure
|
|
341
|
+
the original string on resolution failure, an unreachable resolver
|
|
342
|
+
included. The `dns` extra is optional —
|
|
341
343
|
without it netimps resolves through its system/`nslookup` backends.
|
|
342
344
|
Equality/hash by `.address`.
|
|
343
345
|
- **`flatten(map, _prefix="") -> dict`** — recursively flattens a
|
|
@@ -368,13 +370,18 @@ servers identifies a reservation by it.
|
|
|
368
370
|
- `parse(value, type, **kw)` raises on bad input; `try_parse(value, type)`
|
|
369
371
|
returns `None` instead; `is_valid(value, type)` returns a bool. Networks
|
|
370
372
|
parse non-strict, so `"10.0.0.5/24"` normalises rather than raising.
|
|
371
|
-
- `MACAddress` — colon/hyphen/Cisco-dot/bare text
|
|
372
|
-
`.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`.
|
|
373
377
|
- `resolve(query, rdtype=None)` → a list of **native** records (`A`/`AAAA`
|
|
374
378
|
are `IPv4Address`/`IPv6Address`, not strings); `[]` on a genuine lookup
|
|
375
379
|
failure, but a malformed query or unknown record type raises `ValueError`
|
|
376
380
|
rather than looking like "no such record". `rdtype=None` auto-selects:
|
|
377
|
-
`"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.
|
|
378
385
|
- `ping(dst)` → a `PingResult` that is truthy on success and also carries
|
|
379
386
|
`.rtt_ms`/`.ttl`.
|
|
380
387
|
- `get_interfaces()` / `iter_addresses()` — real adapter enumeration with
|
|
@@ -7,6 +7,7 @@ whole implementation.
|
|
|
7
7
|
|
|
8
8
|
import datetime
|
|
9
9
|
import enum as _enum
|
|
10
|
+
import logging
|
|
10
11
|
import typing as _ty
|
|
11
12
|
from argparse import Namespace
|
|
12
13
|
from copy import deepcopy
|
|
@@ -199,51 +200,49 @@ class PixieContext(Namespace):
|
|
|
199
200
|
return self.repos.get(resource.src)
|
|
200
201
|
|
|
201
202
|
def pxe_init(self, config: "Pixie"):
|
|
202
|
-
"""Arm every DHCP backend for this target,
|
|
203
|
+
"""Arm every DHCP backend for this target, succeeding if any one does.
|
|
203
204
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
backends back before re-raising.
|
|
205
|
+
Every backend is tried. A backend that fails is logged as a warning and
|
|
206
|
+
skipped, so one server being offline does not block the others; the
|
|
207
|
+
first error is raised only when no backend could be armed.
|
|
208
208
|
"""
|
|
209
|
-
|
|
209
|
+
first_error = None
|
|
210
|
+
armed = 0
|
|
210
211
|
for dhcpserver in self.dhcpzone.dhcpservers:
|
|
211
212
|
try:
|
|
212
213
|
dhcpserver.add_target(self)
|
|
213
|
-
except Exception:
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
214
|
+
except Exception as exc:
|
|
215
|
+
LOGGER.warning(
|
|
216
|
+
"could not arm %s on %s: %s",
|
|
217
|
+
self.target._id,
|
|
218
|
+
getattr(dhcpserver, "uri", dhcpserver),
|
|
219
|
+
exc,
|
|
220
|
+
exc_info=LOGGER.isEnabledFor(logging.DEBUG),
|
|
221
|
+
)
|
|
222
|
+
first_error = first_error or exc
|
|
223
|
+
else:
|
|
224
|
+
armed += 1
|
|
225
|
+
if first_error is not None and not armed:
|
|
226
|
+
raise first_error
|
|
225
227
|
return self
|
|
226
228
|
|
|
227
229
|
def pxe_complete(self, config: "Pixie"):
|
|
228
|
-
"""Disarm every DHCP backend
|
|
230
|
+
"""Disarm every DHCP backend; a failure is a warning, never an error.
|
|
229
231
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
the first error is raised once they have all had their turn.
|
|
232
|
+
Every backend is tried, and one that fails is logged and skipped, so
|
|
233
|
+
the rest are still disarmed.
|
|
233
234
|
"""
|
|
234
|
-
first_error = None
|
|
235
235
|
for dhcpserver in self.dhcpzone.dhcpservers:
|
|
236
236
|
try:
|
|
237
237
|
dhcpserver.remove_target(self)
|
|
238
238
|
except Exception as exc:
|
|
239
|
-
LOGGER.
|
|
240
|
-
"could not disarm %s on %s",
|
|
239
|
+
LOGGER.warning(
|
|
240
|
+
"could not disarm %s on %s: %s",
|
|
241
241
|
self.target._id,
|
|
242
242
|
getattr(dhcpserver, "uri", dhcpserver),
|
|
243
|
+
exc,
|
|
244
|
+
exc_info=LOGGER.isEnabledFor(logging.DEBUG),
|
|
243
245
|
)
|
|
244
|
-
first_error = first_error or exc
|
|
245
|
-
if first_error is not None:
|
|
246
|
-
raise first_error
|
|
247
246
|
return self
|
|
248
247
|
|
|
249
248
|
def _template_names(self, suffix: Union[list[str], str], **options) -> list[str]:
|
|
@@ -706,7 +705,7 @@ class Pixie:
|
|
|
706
705
|
def initialize(self, target: "PixieTarget"):
|
|
707
706
|
"""Arm DHCP for `target` and return its context.
|
|
708
707
|
|
|
709
|
-
|
|
708
|
+
A backend that fails is a warning; it raises only if none could be armed.
|
|
710
709
|
"""
|
|
711
710
|
target = self.hook(PixieEvent.StartPixieInitialize, target)
|
|
712
711
|
ctx = self.make_context(target)
|
|
@@ -716,7 +715,7 @@ class Pixie:
|
|
|
716
715
|
def complete(self, target: "PixieTarget"):
|
|
717
716
|
"""Disarm DHCP for `target` and return its context.
|
|
718
717
|
|
|
719
|
-
Every backend is tried
|
|
718
|
+
Every backend is tried; one that fails is logged as a warning.
|
|
720
719
|
"""
|
|
721
720
|
target = self.hook(PixieEvent.StartPixieComplete, target)
|
|
722
721
|
ctx = self.make_context(target, require_image=False)
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
The IP/MAC machinery used to live in an in-tree copy of an earlier version of
|
|
4
4
|
that library. It has since been published, gaining fixes this copy never had --
|
|
5
5
|
real prefix lengths and MTU in interface enumeration, DNS errors that surface
|
|
6
|
-
instead of being swallowed, and a ``ping`` that
|
|
7
|
-
|
|
6
|
+
instead of being swallowed, and a ``ping`` that speaks each platform's own
|
|
7
|
+
flags -- so the copy is gone.
|
|
8
8
|
|
|
9
9
|
This module re-exports netimps under netboot's own name rather than wrapping
|
|
10
10
|
it: the point of adopting a library is to use its vocabulary. Only
|
|
@@ -90,3 +90,22 @@ def test_resolve_auto_selects_the_record_type():
|
|
|
90
90
|
"netimps.resolve no longer defaults rdtype to None (auto-select); "
|
|
91
91
|
"Host.try_ip relies on that, netimps >= 0.2.1"
|
|
92
92
|
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_resolve_is_not_strict_by_default():
|
|
96
|
+
"""Guards `netimps>=0.3.1`: a resolver outage answers `[]`, not a raise.
|
|
97
|
+
|
|
98
|
+
0.3.0 raised `ResolutionError` from `resolve()` when no backend could even
|
|
99
|
+
ask; 0.3.1 made that opt-in with `strict=True`. `PixieTarget` handles `[]`
|
|
100
|
+
with a warning, but a raise would escape its construction -- and every
|
|
101
|
+
target is built at startup, so one outage would fail every command.
|
|
102
|
+
"""
|
|
103
|
+
strict = inspect.signature(netutils.resolve).parameters.get("strict")
|
|
104
|
+
assert strict is not None, (
|
|
105
|
+
"netimps.resolve has no strict parameter: an unreachable resolver "
|
|
106
|
+
"raises instead of answering [] (netimps >= 0.3.1)"
|
|
107
|
+
)
|
|
108
|
+
assert strict.default is False, (
|
|
109
|
+
"netimps.resolve is strict by default; PixieTarget relies on [] for "
|
|
110
|
+
"an unreachable resolver (netimps >= 0.3.1)"
|
|
111
|
+
)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Engine behaviour under partly-broken input and partly-failing backends.
|
|
2
2
|
|
|
3
3
|
Each case here is a crash or a silent wrong answer the 2026-09-17 review
|
|
4
|
-
reproduced:
|
|
4
|
+
reproduced: an offline DHCP server, a retired image blocking cleanup, a global
|
|
5
5
|
that replaced the render context, a zone chosen by declaration order.
|
|
6
6
|
"""
|
|
7
7
|
|
|
@@ -61,28 +61,39 @@ class rollback(_Recorder): # scheme `rollback://`
|
|
|
61
61
|
pass
|
|
62
62
|
|
|
63
63
|
|
|
64
|
-
def
|
|
64
|
+
def test_arming_survives_a_failing_backend_with_a_warning(caplog):
|
|
65
65
|
engine = _engine()
|
|
66
66
|
zone = engine.dhcpzones["lan"]
|
|
67
|
-
zone.dhcpservers[
|
|
68
|
-
with
|
|
67
|
+
zone.dhcpservers[0].fail_on = "add"
|
|
68
|
+
with caplog.at_level(logging.WARNING, logger="netboot"):
|
|
69
69
|
engine.initialize(engine.lookup_target("host1"))
|
|
70
|
-
# b
|
|
71
|
-
assert _Recorder.calls == [
|
|
72
|
-
|
|
73
|
-
("arm", "rollback://b"),
|
|
74
|
-
("disarm", "rollback://a"),
|
|
75
|
-
]
|
|
70
|
+
# a is offline; b is still armed and nothing is rolled back.
|
|
71
|
+
assert _Recorder.calls == [("arm", "rollback://a"), ("arm", "rollback://b")]
|
|
72
|
+
assert "could not arm host1 on rollback://a" in caplog.text
|
|
76
73
|
|
|
77
74
|
|
|
78
|
-
def
|
|
75
|
+
def test_arming_raises_when_every_backend_fails():
|
|
79
76
|
engine = _engine()
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
with pytest.raises(RuntimeError):
|
|
77
|
+
for server in engine.dhcpzones["lan"].dhcpservers:
|
|
78
|
+
server.fail_on = "add"
|
|
79
|
+
with pytest.raises(RuntimeError, match="rollback://a refused to arm"):
|
|
80
|
+
engine.initialize(engine.lookup_target("host1"))
|
|
81
|
+
assert _Recorder.calls == [("arm", "rollback://a"), ("arm", "rollback://b")]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_disarm_failures_are_warnings(caplog):
|
|
85
|
+
engine = _engine()
|
|
86
|
+
for server in engine.dhcpzones["lan"].dhcpservers:
|
|
87
|
+
server.fail_on = "remove"
|
|
88
|
+
with caplog.at_level(logging.WARNING, logger="netboot"):
|
|
83
89
|
engine.complete(engine.lookup_target("host1"))
|
|
84
|
-
#
|
|
85
|
-
assert
|
|
90
|
+
# Every backend is tried, and none of the failures is raised.
|
|
91
|
+
assert _Recorder.calls == [
|
|
92
|
+
("disarm", "rollback://a"),
|
|
93
|
+
("disarm", "rollback://b"),
|
|
94
|
+
]
|
|
95
|
+
assert "could not disarm host1 on rollback://a" in caplog.text
|
|
96
|
+
assert "could not disarm host1 on rollback://b" in caplog.text
|
|
86
97
|
|
|
87
98
|
|
|
88
99
|
def test_complete_works_when_the_image_is_no_longer_configured(caplog):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|