netboot 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. netboot-0.1.0/.gitignore +38 -0
  2. netboot-0.1.0/CHANGELOG.md +65 -0
  3. netboot-0.1.0/LICENSE +21 -0
  4. netboot-0.1.0/PKG-INFO +154 -0
  5. netboot-0.1.0/README.md +86 -0
  6. netboot-0.1.0/benchmarks/bench_netboot.py +116 -0
  7. netboot-0.1.0/docs/api.md +28 -0
  8. netboot-0.1.0/docs/cli.md +68 -0
  9. netboot-0.1.0/docs/configuration.md +65 -0
  10. netboot-0.1.0/docs/extending.md +42 -0
  11. netboot-0.1.0/docs/index.md +43 -0
  12. netboot-0.1.0/mkdocs.yml +44 -0
  13. netboot-0.1.0/pyproject.toml +61 -0
  14. netboot-0.1.0/src/netboot/AGENTS.md +244 -0
  15. netboot-0.1.0/src/netboot/__init__.py +394 -0
  16. netboot-0.1.0/src/netboot/__main__.py +4 -0
  17. netboot-0.1.0/src/netboot/_netutils.py +341 -0
  18. netboot-0.1.0/src/netboot/cmds/__init__.py +5 -0
  19. netboot-0.1.0/src/netboot/cmds/complete.py +24 -0
  20. netboot-0.1.0/src/netboot/cmds/initiate.py +36 -0
  21. netboot-0.1.0/src/netboot/content/__init__.py +88 -0
  22. netboot-0.1.0/src/netboot/dhcp.py +123 -0
  23. netboot-0.1.0/src/netboot/logging.py +17 -0
  24. netboot-0.1.0/src/netboot/main.py +232 -0
  25. netboot-0.1.0/src/netboot/py.typed +0 -0
  26. netboot-0.1.0/src/netboot/templates/__init__.py +123 -0
  27. netboot-0.1.0/src/netboot/templates/common.py +21 -0
  28. netboot-0.1.0/src/netboot/templates/jinja.py +18 -0
  29. netboot-0.1.0/src/netboot/templates/shell.py +32 -0
  30. netboot-0.1.0/src/netboot/utils/__init__.py +3 -0
  31. netboot-0.1.0/src/netboot/utils/config.py +12 -0
  32. netboot-0.1.0/src/netboot/utils/dicts.py +49 -0
  33. netboot-0.1.0/src/netboot/utils/misc.py +6 -0
  34. netboot-0.1.0/src/netboot/utils/net.py +61 -0
  35. netboot-0.1.0/tests/test_dhcp.py +54 -0
  36. netboot-0.1.0/tests/test_render.py +58 -0
  37. netboot-0.1.0/tests/test_review_fixes.py +111 -0
  38. netboot-0.1.0/tests/test_target.py +33 -0
  39. netboot-0.1.0/tests/test_utils.py +34 -0
@@ -0,0 +1,38 @@
1
+ # Agent configs / private notes — never tracked in this repo
2
+ .agents
3
+ *.local.md
4
+ CLAUDE*
5
+ .claude/
6
+
7
+ # Virtualenvs
8
+ .venv*/
9
+ venv/
10
+ env/
11
+ .env/
12
+
13
+ # Byte-compiled / optimized
14
+ __pycache__/
15
+ *.py[cod]
16
+ *$py.class
17
+
18
+ # Distribution / packaging
19
+ build/
20
+ dist/
21
+ *.egg-info/
22
+ *.egg
23
+ .eggs/
24
+
25
+ # Test / coverage
26
+ .pytest_cache/
27
+ .coverage
28
+ htmlcov/
29
+ .tox/
30
+
31
+ # Docs site build output
32
+ /site/
33
+
34
+ # Editors / OS
35
+ .vscode/
36
+ .idea/
37
+ *.swp
38
+ .DS_Store
@@ -0,0 +1,65 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
+ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.0] - 2026-07-21
10
+
11
+ First packaged release: the `netboot` library with the `pixie` command line.
12
+
13
+ ### Added
14
+ - Packaged as `netboot` (src layout, hatchling, `pixie` console script, `py.typed`).
15
+ Python 3.9+.
16
+ - PXE provisioning engine: `Pixie` with target/image/dhcpzone/repo lookup, a
17
+ render `PixieContext`, and an `initialize`/`complete` lifecycle.
18
+ - Event-hook system (`PixieEvent`, `Pixie(hooks=...)`) for customising lookup,
19
+ context construction and the init/complete lifecycle.
20
+ - Template rendering via a URI-aware Jinja2 loader plus a `%`-delimited shell
21
+ template engine, selecting sources by MAC / hostname / IP.
22
+ - Pluggable `DhcpServer` backends dispatched by URI scheme, discovered
23
+ recursively so plugin modules loaded via `--load-module` are honoured.
24
+ - `pixie` CLI built on `duho` (PXE is pronounced "pixie"; `netboot` is the
25
+ library/import package): `initiate` and `complete` commands with layered YAML
26
+ config (`config/pixie.yaml` via `yaconfiglib`), command discovery, and
27
+ `--load-module`/`--cmdspath`.
28
+ - App settings read through `duho.env.Env("pixie")`, so `PIXIE_*` variables
29
+ (notably `PIXIE_CMDS_PATH`) configure the CLI; the resolved accessor reaches
30
+ commands as `args._env_`.
31
+ - Config objects use `yaconfiglib`'s `TypedNamespace` (`_parse_<field>` coercers)
32
+ and `OpaqueMerge` (last-object-wins) so fully-built targets/zones with
33
+ factory-function field hints are merged as opaque values (requires
34
+ `yaconfiglib>=0.10.0`).
35
+ - Two-workflow CI (`test`, `Release`) and a MkDocs documentation site
36
+ (`docs/` + `mkdocs.yml`, API reference via mkdocstrings), plus a
37
+ `benchmarks/bench_netboot.py` micro-benchmark for the lookup/template hot paths.
38
+ - IP/MAC/DNS helpers vendored in-tree as `netboot._netutils`; DNS lookup of
39
+ hostname targets is the optional `netboot[dns]` extra (`dnspython`).
40
+
41
+ ### Changed
42
+ - Built on `duho` (args/command-discovery/app) rather than the in-house
43
+ `coquilib` layer; IP/MAC helpers are vendored in-tree; the `sys.path`
44
+ `vendor/` shim is gone. Version derives from installed package metadata.
45
+
46
+ ### Fixed
47
+ - Target resolution no longer no-ops when the id is an IP address (`self.ip`
48
+ self-assignment and a `resolve - True` typo).
49
+ - `utils.flatten` now produces a genuinely flat mapping and no longer raises
50
+ `TypeError` on nested lists, so shell-template rendering works.
51
+ - `DhcpServer(uri)` raises a clear error for an unknown scheme instead of
52
+ silently returning an inert base, and zone `dhcpservers` URIs are constructed
53
+ into backends.
54
+ - `make_context` no longer deletes `globals` off shared image/dhcpzone/target
55
+ objects, so a second target reusing an image keeps that image's globals.
56
+ - `_template_names` accepts the loader's template `**options`; template names
57
+ stringify the target IP and skip an unspecified address.
58
+ - Command dispatch introspects each module's `run` signature, so a user command
59
+ using duho's plain `run(args)` is dispatched via `duho.run_command`.
60
+ - Shell templates render `None` as empty instead of the literal `"None"`;
61
+ config value construction no longer swallows non-`TypeError` errors; repo
62
+ `joinpath` keeps `.local` a path so chained joins work.
63
+
64
+ [Unreleased]: https://github.com/jose-pr/netboot/compare/v0.1.0...HEAD
65
+ [0.1.0]: https://github.com/jose-pr/netboot/releases/tag/v0.1.0
netboot-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jose A.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
netboot-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,154 @@
1
+ Metadata-Version: 2.4
2
+ Name: netboot
3
+ Version: 0.1.0
4
+ Summary: PXE provisioning management: render netboot artifacts and drive DHCP for targets
5
+ Project-URL: Homepage, https://github.com/jose-pr/netboot/
6
+ Project-URL: Issues, https://github.com/jose-pr/netboot/issues
7
+ Project-URL: Repository, https://github.com/jose-pr/netboot
8
+ Author: Jose A.
9
+ License: MIT License
10
+
11
+ Copyright (c) 2026 Jose A.
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: LICENSE
31
+ Classifier: Development Status :: 4 - Beta
32
+ Classifier: Environment :: Console
33
+ Classifier: Intended Audience :: System Administrators
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Operating System :: OS Independent
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3.9
38
+ Classifier: Programming Language :: Python :: 3.10
39
+ Classifier: Programming Language :: Python :: 3.11
40
+ Classifier: Programming Language :: Python :: 3.12
41
+ Classifier: Programming Language :: Python :: 3.13
42
+ Classifier: Topic :: System :: Boot
43
+ Classifier: Topic :: System :: Installation/Setup
44
+ Classifier: Topic :: System :: Systems Administration
45
+ Classifier: Typing :: Typed
46
+ Requires-Python: >=3.9
47
+ Requires-Dist: duho>=0.3.0
48
+ Requires-Dist: jinja2
49
+ Requires-Dist: pathlib-next[uri]>=0.8.2
50
+ Requires-Dist: yaconfiglib>=0.10.0
51
+ Provides-Extra: config
52
+ Requires-Dist: pyyaml; extra == 'config'
53
+ Provides-Extra: dev
54
+ Requires-Dist: build; extra == 'dev'
55
+ Requires-Dist: dnspython; extra == 'dev'
56
+ Requires-Dist: hatchling; extra == 'dev'
57
+ Requires-Dist: pytest; extra == 'dev'
58
+ Requires-Dist: pytest-cov; extra == 'dev'
59
+ Requires-Dist: pyyaml; extra == 'dev'
60
+ Requires-Dist: twine; extra == 'dev'
61
+ Provides-Extra: dns
62
+ Requires-Dist: dnspython; extra == 'dns'
63
+ Provides-Extra: docs
64
+ Requires-Dist: mkdocs; extra == 'docs'
65
+ Requires-Dist: mkdocs-material; extra == 'docs'
66
+ Requires-Dist: mkdocstrings[python]; extra == 'docs'
67
+ Description-Content-Type: text/markdown
68
+
69
+ # netboot
70
+
71
+ PXE provisioning management: describe your netboot targets, images and DHCP
72
+ zones in config, and let `netboot` render the per-target boot artifacts and arm (or
73
+ disarm) DHCP for a machine as it enters and leaves the install process.
74
+
75
+ `netboot` is a small, hook-driven engine. A YAML config declares **targets**
76
+ (host/MAC/IP), **images** (what to boot), **dhcp zones** (the network a target
77
+ lives on) and content **repos** (where artifacts are fetched/served from). For a
78
+ given target `netboot` builds a render **context** and produces netboot files from
79
+ Jinja2 or shell-style templates, resolving names by MAC, hostname or IP with
80
+ sensible fallbacks. Backends and behaviour are extensible through an event-hook
81
+ system and pluggable `DhcpServer` handlers.
82
+
83
+ ## Install
84
+
85
+ ```sh
86
+ pip install netboot # once published
87
+ # or, from a checkout:
88
+ pip install .
89
+ ```
90
+
91
+ Optional extras:
92
+
93
+ | Extra | Enables |
94
+ | ----- | ------- |
95
+ | `netboot[config]` | YAML config loading for the CLI (`pyyaml`) |
96
+ | `netboot[dns]` | DNS resolution of hostname targets (`dnspython`) |
97
+ | `netboot[docs]` | Build the documentation site (`mkdocs`) |
98
+
99
+ Built on [`duho`](https://github.com/jose-pr/duho) (CLI/args/command discovery),
100
+ [`pathlib_next`](https://github.com/jose-pr/pathlib_next) (URI-aware paths),
101
+ [`yaconfiglib`](https://github.com/jose-pr/yaconfiglib) (layered YAML), and
102
+ Jinja2. IP/MAC/DNS helpers are vendored in-tree (`netboot._netutils`).
103
+
104
+ ## CLI
105
+
106
+ The command is `pixie` (PXE is pronounced "pixie"); `netboot` is the library
107
+ package it drives.
108
+
109
+ ```sh
110
+ # Initiate the PXE process for a target (render artifacts + arm DHCP):
111
+ pixie initiate my-host
112
+
113
+ # Complete it (post-boot cleanup, disarm DHCP):
114
+ pixie complete my-host
115
+ ```
116
+
117
+ Global options:
118
+
119
+ | Option | Purpose |
120
+ | ------ | ------- |
121
+ | `-c, --config PATH` | Explicit config file (yaml/cfg) |
122
+ | `--baseconfig DIR` | Base config directory to search (default `./config`) |
123
+ | `-l, --load-module M` | Import module(s) before building netboot (config/hook deps) |
124
+ | `--cmdspath PATH` | Extra directories/packages to search for commands |
125
+
126
+ A target is looked up by exact id, or by hostname prefix / MAC / IP.
127
+
128
+ ## Library
129
+
130
+ ```python
131
+ from netboot import Pixie
132
+
133
+ netboot = Pixie(**config) # config: the merged YAML mapping
134
+ target = netboot.lookup_target("my-host")
135
+ ctx = netboot.make_context(target) # render context (image + dhcpzone + target)
136
+ print(ctx.render("boot.ipxe.j2")) # render a template against the context
137
+ netboot.initialize(target) # arm DHCP for the target
138
+ netboot.complete(target) # cleanup once installed
139
+ ```
140
+
141
+ ## Extending
142
+
143
+ - **Hooks.** Pass `hooks=[...]` (callables or `"module.func"` import strings) to
144
+ `Pixie(...)`. Each hook `f(event, netboot, value, kwargs) -> value` is called for
145
+ every `PixieEvent` and may transform the value flowing through it — used to
146
+ customise lookup, context construction and the init/complete lifecycle.
147
+ - **DHCP backends.** Subclass `netboot.dhcp.DhcpServer`; the lowercased class name
148
+ is the URI scheme it handles (`class dnsmasq(DhcpServer)` → `dnsmasq://...`).
149
+ Import your plugin module via `--load-module` so it is registered before the
150
+ config builds the zones.
151
+
152
+ ## License
153
+
154
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,86 @@
1
+ # netboot
2
+
3
+ PXE provisioning management: describe your netboot targets, images and DHCP
4
+ zones in config, and let `netboot` render the per-target boot artifacts and arm (or
5
+ disarm) DHCP for a machine as it enters and leaves the install process.
6
+
7
+ `netboot` is a small, hook-driven engine. A YAML config declares **targets**
8
+ (host/MAC/IP), **images** (what to boot), **dhcp zones** (the network a target
9
+ lives on) and content **repos** (where artifacts are fetched/served from). For a
10
+ given target `netboot` builds a render **context** and produces netboot files from
11
+ Jinja2 or shell-style templates, resolving names by MAC, hostname or IP with
12
+ sensible fallbacks. Backends and behaviour are extensible through an event-hook
13
+ system and pluggable `DhcpServer` handlers.
14
+
15
+ ## Install
16
+
17
+ ```sh
18
+ pip install netboot # once published
19
+ # or, from a checkout:
20
+ pip install .
21
+ ```
22
+
23
+ Optional extras:
24
+
25
+ | Extra | Enables |
26
+ | ----- | ------- |
27
+ | `netboot[config]` | YAML config loading for the CLI (`pyyaml`) |
28
+ | `netboot[dns]` | DNS resolution of hostname targets (`dnspython`) |
29
+ | `netboot[docs]` | Build the documentation site (`mkdocs`) |
30
+
31
+ Built on [`duho`](https://github.com/jose-pr/duho) (CLI/args/command discovery),
32
+ [`pathlib_next`](https://github.com/jose-pr/pathlib_next) (URI-aware paths),
33
+ [`yaconfiglib`](https://github.com/jose-pr/yaconfiglib) (layered YAML), and
34
+ Jinja2. IP/MAC/DNS helpers are vendored in-tree (`netboot._netutils`).
35
+
36
+ ## CLI
37
+
38
+ The command is `pixie` (PXE is pronounced "pixie"); `netboot` is the library
39
+ package it drives.
40
+
41
+ ```sh
42
+ # Initiate the PXE process for a target (render artifacts + arm DHCP):
43
+ pixie initiate my-host
44
+
45
+ # Complete it (post-boot cleanup, disarm DHCP):
46
+ pixie complete my-host
47
+ ```
48
+
49
+ Global options:
50
+
51
+ | Option | Purpose |
52
+ | ------ | ------- |
53
+ | `-c, --config PATH` | Explicit config file (yaml/cfg) |
54
+ | `--baseconfig DIR` | Base config directory to search (default `./config`) |
55
+ | `-l, --load-module M` | Import module(s) before building netboot (config/hook deps) |
56
+ | `--cmdspath PATH` | Extra directories/packages to search for commands |
57
+
58
+ A target is looked up by exact id, or by hostname prefix / MAC / IP.
59
+
60
+ ## Library
61
+
62
+ ```python
63
+ from netboot import Pixie
64
+
65
+ netboot = Pixie(**config) # config: the merged YAML mapping
66
+ target = netboot.lookup_target("my-host")
67
+ ctx = netboot.make_context(target) # render context (image + dhcpzone + target)
68
+ print(ctx.render("boot.ipxe.j2")) # render a template against the context
69
+ netboot.initialize(target) # arm DHCP for the target
70
+ netboot.complete(target) # cleanup once installed
71
+ ```
72
+
73
+ ## Extending
74
+
75
+ - **Hooks.** Pass `hooks=[...]` (callables or `"module.func"` import strings) to
76
+ `Pixie(...)`. Each hook `f(event, netboot, value, kwargs) -> value` is called for
77
+ every `PixieEvent` and may transform the value flowing through it — used to
78
+ customise lookup, context construction and the init/complete lifecycle.
79
+ - **DHCP backends.** Subclass `netboot.dhcp.DhcpServer`; the lowercased class name
80
+ is the URI scheme it handles (`class dnsmasq(DhcpServer)` → `dnsmasq://...`).
81
+ Import your plugin module via `--load-module` so it is registered before the
82
+ config builds the zones.
83
+
84
+ ## License
85
+
86
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,116 @@
1
+ """netboot micro-benchmarks: the per-request hot paths.
2
+
3
+ Measures the two operations netboot runs most often per PXE request:
4
+
5
+ * ``lookup_target`` — a linear scan over the configured targets matching by
6
+ hostname prefix / MAC / IP;
7
+ * ``_template_names`` — the candidate template-name list built for every render.
8
+
9
+ Run:
10
+
11
+ python benchmarks/bench_netboot.py --iterations 20000
12
+ python benchmarks/bench_netboot.py --json-output results/netboot.json
13
+
14
+ Each metric reports min/median/max ms-per-call over the sample; compare on the
15
+ median (a single average hides run-to-run noise). Local timings are a sanity
16
+ check only — a release perf claim comes from CI.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import argparse
22
+ import json
23
+ import pathlib
24
+ import platform
25
+ import statistics
26
+ import sys
27
+ import timeit
28
+ from datetime import datetime, timezone
29
+
30
+ REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
31
+ sys.path.insert(0, (REPO_ROOT / "src").as_posix())
32
+
33
+ import netboot # noqa: E402
34
+
35
+
36
+ def _build_netboot(n_targets: int) -> "netboot.Pixie":
37
+ targets = {}
38
+ for i in range(n_targets):
39
+ targets[f"host{i}"] = {
40
+ "hostname": f"host{i}",
41
+ "ip": f"10.0.{i // 256}.{i % 256}",
42
+ "image": "debian",
43
+ }
44
+ return netboot.Pixie(
45
+ images={"debian": {"template_path": []}},
46
+ dhcpzones={"lan": {"network": "10.0.0.0/16"}},
47
+ targets=targets,
48
+ )
49
+
50
+
51
+ def _sample(fn, iterations: int, repeats: int = 7) -> "dict[str, float]":
52
+ # ms per call, one timing per repeat; report min/median/max across repeats.
53
+ per_call = [
54
+ (timeit.timeit(fn, number=iterations) / iterations) * 1000.0
55
+ for _ in range(repeats)
56
+ ]
57
+ return {
58
+ "min_ms": min(per_call),
59
+ "median_ms": statistics.median(per_call),
60
+ "max_ms": max(per_call),
61
+ }
62
+
63
+
64
+ def run_benchmarks(iterations: int, n_targets: int = 500) -> "dict[str, dict]":
65
+ p = _build_netboot(n_targets)
66
+ # Worst-case lookup: the last target, forcing a full scan.
67
+ last = f"host{n_targets - 1}"
68
+ ctx = p.make_context(p.lookup_target("host0"))
69
+
70
+ return {
71
+ "lookup_target_last": _sample(lambda: p.lookup_target(last), iterations),
72
+ "template_names": _sample(
73
+ lambda: ctx._template_names("boot.j2"), iterations
74
+ ),
75
+ }
76
+
77
+
78
+ def _report(iterations: int, results: "dict[str, dict]") -> dict:
79
+ return {
80
+ "generated": datetime.now(timezone.utc).isoformat(),
81
+ "iterations": iterations,
82
+ "python": platform.python_version(),
83
+ "platform": platform.platform(),
84
+ "netboot_version": netboot.Pixie.VERSION,
85
+ "metrics": results,
86
+ }
87
+
88
+
89
+ def main() -> None:
90
+ parser = argparse.ArgumentParser(description="Run netboot micro-benchmarks")
91
+ parser.add_argument("--iterations", type=int, default=20000)
92
+ parser.add_argument("--targets", type=int, default=500)
93
+ parser.add_argument(
94
+ "--json-output",
95
+ type=pathlib.Path,
96
+ help="Optional path to write the structured JSON report",
97
+ )
98
+ args = parser.parse_args()
99
+
100
+ results = run_benchmarks(args.iterations, args.targets)
101
+ report = _report(args.iterations, results)
102
+
103
+ for name, m in results.items():
104
+ print(
105
+ f"{name:24s} median={m['median_ms']:.6f} ms "
106
+ f"(min={m['min_ms']:.6f} max={m['max_ms']:.6f})"
107
+ )
108
+
109
+ if args.json_output is not None:
110
+ args.json_output.parent.mkdir(parents=True, exist_ok=True)
111
+ args.json_output.write_text(json.dumps(report, indent=2), encoding="utf-8")
112
+ print(f"\nwrote {args.json_output}")
113
+
114
+
115
+ if __name__ == "__main__":
116
+ main()
@@ -0,0 +1,28 @@
1
+ # API Reference
2
+
3
+ The engine and its context objects. Rendered from source via
4
+ [mkdocstrings](https://mkdocstrings.github.io/).
5
+
6
+ ## Engine
7
+
8
+ ::: netboot.Pixie
9
+
10
+ ## Context
11
+
12
+ ::: netboot.PixieContext
13
+
14
+ ## Targets and images
15
+
16
+ ::: netboot.PixieTarget
17
+
18
+ ::: netboot.PixieImage
19
+
20
+ ## Events
21
+
22
+ ::: netboot.PixieEvent
23
+
24
+ ## DHCP
25
+
26
+ ::: netboot.dhcp.DhcpZone
27
+
28
+ ::: netboot.dhcp.DhcpServer
@@ -0,0 +1,68 @@
1
+ # CLI
2
+
3
+ The `pixie` command line (the CLI of the `netboot` library; PXE is
4
+ pronounced "pixie") is built on [`duho`](https://github.com/jose-pr/duho): it
5
+ discovers commands, layers YAML config, then runs the selected command against a
6
+ single `Pixie` engine built from that config.
7
+
8
+ ```sh
9
+ # Initiate the PXE process for a target (render artifacts + arm DHCP):
10
+ pixie initiate my-host
11
+
12
+ # Complete it (post-boot cleanup, disarm DHCP):
13
+ pixie complete my-host
14
+ ```
15
+
16
+ A target argument is resolved by exact id, or by hostname prefix / MAC / IP.
17
+
18
+ ## Global options
19
+
20
+ | Option | Purpose |
21
+ | ------ | ------- |
22
+ | `-c, --config PATH` | Explicit config file (yaml/cfg); overrides discovery |
23
+ | `--baseconfig DIR` | Base config directory to search (default `./config`) |
24
+ | `-l, --load-module M` | Import module(s) before building netboot (config/hook deps) |
25
+ | `--cmdspath PATH` | Extra directories/packages to search for commands |
26
+ | `-v` / `-q` | Increase / decrease log verbosity (from duho's `LoggingArgs`) |
27
+
28
+ ## Commands
29
+
30
+ ### `initiate <target> [--iscsi]`
31
+
32
+ Look up the target, build its render context, produce the netboot artifacts and
33
+ arm every DHCP backend in the target's zone. `--iscsi` prepares the target as an
34
+ iSCSI LUN.
35
+
36
+ ### `complete <target>`
37
+
38
+ Run post-boot cleanup for the target and disarm its DHCP backends.
39
+
40
+ ## Adding your own commands
41
+
42
+ Point `--cmdspath` (or the `PIXIE_CMDS_PATH` environment variable) at a package or
43
+ directory of command modules. A netboot command module exposes:
44
+
45
+ ```python
46
+ def register(parser, args): # optional: add argparse arguments
47
+ ...
48
+
49
+ def run(netboot, args, conf): # required: the command body
50
+ ...
51
+ ```
52
+
53
+ A module that instead follows duho's plain `run(args)` contract is dispatched by
54
+ duho directly, so ordinary duho commands work too.
55
+
56
+ ## Environment
57
+
58
+ App settings are read through `duho.env.Env("pixie")`, so they live under the
59
+ `PIXIE_` prefix:
60
+
61
+ - `PIXIE_CMDS_PATH` — extra command sources, `os.pathsep`-separated (see above).
62
+ - A `pixie_env` Python module importable at startup (e.g. a `pixie_env.py` in
63
+ the working directory) may ship settings as `UPPER_CASE` module variables.
64
+ Note: as of current duho, these seeded values take precedence over real
65
+ `PIXIE_*` environment variables.
66
+
67
+ Commands receive the resolved accessor as `args._env_`, so a custom command can
68
+ read its own `PIXIE_<KEY>` settings without touching `os.environ`.
@@ -0,0 +1,65 @@
1
+ # Configuration
2
+
3
+ netboot loads a YAML config (via [`yaconfiglib`](https://github.com/jose-pr/yaconfiglib),
4
+ so `!include` and deep-merge are available). By default it reads `pixie.yaml` from
5
+ `./config`; override with `--config FILE` or `--baseconfig DIR`.
6
+
7
+ The top-level keys map to the `Pixie` engine's collections:
8
+
9
+ ```yaml
10
+ # Values shared into every render context.
11
+ globals:
12
+ domain: example.com
13
+
14
+ # Machines to provision, keyed by an id (hostname / MAC / IP).
15
+ targets:
16
+ web01:
17
+ hostname: web01
18
+ ip: 10.0.0.10
19
+ image: debian
20
+ "aa:bb:cc:dd:ee:ff": # a MAC-keyed target
21
+ image: debian
22
+
23
+ # What a target boots. template_path is searched for that image's templates.
24
+ images:
25
+ debian:
26
+ template_path: [templates/debian]
27
+ globals:
28
+ kernel: vmlinuz
29
+
30
+ # The network a target lives on, plus its DHCP backend(s).
31
+ dhcpzones:
32
+ lan:
33
+ network: 10.0.0.0/24
34
+ gateway: 10.0.0.1
35
+ nameservers: [10.0.0.53]
36
+ search: [example.com]
37
+ dhcpservers:
38
+ - dnsmasq://dhcp-host # scheme selects the DhcpServer backend
39
+
40
+ # Where boot artifacts are fetched / served from.
41
+ repos:
42
+ mirror:
43
+ address: mirror.example.com
44
+ services:
45
+ http: http://mirror.example.com/debian
46
+ local: /srv/mirror/debian
47
+ ```
48
+
49
+ ## How values resolve
50
+
51
+ - **Targets** normalise `ip`/`mac`/`hostname` at load time. If a field is
52
+ missing, netboot fills it in where it can (a MAC-shaped id becomes the `mac`; an
53
+ IP-shaped id becomes the `ip`; a hostname is resolved to an IP via DNS).
54
+ - **Zones** derive `network` from a CIDR `gateway`, and coerce `nameservers` /
55
+ `search` to lists. `dhcpservers` URIs are constructed into backends by scheme.
56
+ - **globals** are layered: engine globals, then per-image / per-zone / per-target
57
+ `globals`, are merged into the render context (later wins).
58
+
59
+ ## Templates
60
+
61
+ `templates` is a list of search paths (local or URI). For each render netboot looks
62
+ for a file named by the target's MAC (`aa-bb-cc-...`), hostname, or IP — falling
63
+ back to the bare template name. A `.j2` / `.jinja` / `.jinja2` file is rendered
64
+ with Jinja2; anything else is rendered with the `%`-delimited shell engine, whose
65
+ `%{UPPER_SNAKE}` placeholders come from the flattened context.
@@ -0,0 +1,42 @@
1
+ # Extending
2
+
3
+ ## Event hooks
4
+
5
+ Pass `hooks=[...]` to `Pixie(...)` — each entry is a callable or a
6
+ `"module.function"` import string. A hook
7
+
8
+ ```python
9
+ def my_hook(event, netboot, value, kwargs):
10
+ ...
11
+ return value
12
+ ```
13
+
14
+ is invoked for every `PixieEvent` and may transform the value flowing through it.
15
+ Events fire around object creation (`NewPixieObject`, `SetPixieProperty`,
16
+ `PixieInitiated`), lookup (`LookupTarget`, `FoundTarget`, `FoundTargetImage`,
17
+ `FoundTargetDhcpzone`), context construction (`PixieContextForTarget`), and the
18
+ init/complete lifecycle (`StartPixieInitialize` … `EndPixieComplete`). This is the
19
+ seam for customising how targets/images/zones are resolved and how the render
20
+ context is assembled.
21
+
22
+ ## Custom DHCP backends
23
+
24
+ `netboot.dhcp.DhcpServer` dispatches on the URI scheme of a zone's `dhcpservers`
25
+ entry: a subclass whose lowercased class name equals the scheme handles it.
26
+
27
+ ```python
28
+ from netboot.dhcp import DhcpServer
29
+
30
+ class dnsmasq(DhcpServer): # handles dnsmasq://...
31
+ def add_target(self, ctx):
32
+ ... # arm DHCP for ctx.target
33
+ def remove_target(self, ctx):
34
+ ... # disarm it
35
+ ```
36
+
37
+ Subclassing at any depth is honoured, so a backend may share an intermediate
38
+ base. Import your plugin module before the config builds the zones — pass it via
39
+ `--load-module your.plugin` (or list it under the config's module loading) so the
40
+ `DhcpServer` subclass is registered when `dnsmasq://...` is resolved.
41
+
42
+ An unknown scheme raises a clear `ValueError` rather than silently doing nothing.