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.
- netboot-0.1.0/.gitignore +38 -0
- netboot-0.1.0/CHANGELOG.md +65 -0
- netboot-0.1.0/LICENSE +21 -0
- netboot-0.1.0/PKG-INFO +154 -0
- netboot-0.1.0/README.md +86 -0
- netboot-0.1.0/benchmarks/bench_netboot.py +116 -0
- netboot-0.1.0/docs/api.md +28 -0
- netboot-0.1.0/docs/cli.md +68 -0
- netboot-0.1.0/docs/configuration.md +65 -0
- netboot-0.1.0/docs/extending.md +42 -0
- netboot-0.1.0/docs/index.md +43 -0
- netboot-0.1.0/mkdocs.yml +44 -0
- netboot-0.1.0/pyproject.toml +61 -0
- netboot-0.1.0/src/netboot/AGENTS.md +244 -0
- netboot-0.1.0/src/netboot/__init__.py +394 -0
- netboot-0.1.0/src/netboot/__main__.py +4 -0
- netboot-0.1.0/src/netboot/_netutils.py +341 -0
- netboot-0.1.0/src/netboot/cmds/__init__.py +5 -0
- netboot-0.1.0/src/netboot/cmds/complete.py +24 -0
- netboot-0.1.0/src/netboot/cmds/initiate.py +36 -0
- netboot-0.1.0/src/netboot/content/__init__.py +88 -0
- netboot-0.1.0/src/netboot/dhcp.py +123 -0
- netboot-0.1.0/src/netboot/logging.py +17 -0
- netboot-0.1.0/src/netboot/main.py +232 -0
- netboot-0.1.0/src/netboot/py.typed +0 -0
- netboot-0.1.0/src/netboot/templates/__init__.py +123 -0
- netboot-0.1.0/src/netboot/templates/common.py +21 -0
- netboot-0.1.0/src/netboot/templates/jinja.py +18 -0
- netboot-0.1.0/src/netboot/templates/shell.py +32 -0
- netboot-0.1.0/src/netboot/utils/__init__.py +3 -0
- netboot-0.1.0/src/netboot/utils/config.py +12 -0
- netboot-0.1.0/src/netboot/utils/dicts.py +49 -0
- netboot-0.1.0/src/netboot/utils/misc.py +6 -0
- netboot-0.1.0/src/netboot/utils/net.py +61 -0
- netboot-0.1.0/tests/test_dhcp.py +54 -0
- netboot-0.1.0/tests/test_render.py +58 -0
- netboot-0.1.0/tests/test_review_fixes.py +111 -0
- netboot-0.1.0/tests/test_target.py +33 -0
- netboot-0.1.0/tests/test_utils.py +34 -0
netboot-0.1.0/.gitignore
ADDED
|
@@ -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).
|
netboot-0.1.0/README.md
ADDED
|
@@ -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.
|