Repository navigation
v0.2.0
A correctness and hardening release from a full review of the code base.
Several documented behaviours changed, so read Changed before upgrading:
target lookup, shell_quote, shell-template placeholders, relative
template_path resolution and undefined template variables all behave
differently on purpose.
Added
PixieError,PixieLookupErrorandPixieConfigError. Netboot's deliberate
failures are now distinguishable from bugs: lookups that fail or are
ambiguous raisePixieLookupError(still aLookupError) instead of a plain
Exception, and the messages name the target and what was configured.netboot.logging.quiet_noisy_dependencies(insecure_warnings=False).netboot[http]extra.http/httpsrepository services reach the network
throughrequests, which nothing installed:pathlib_next[uri]does not
depend on it, soRepository.service("http")failed with
ModuleNotFoundError: No module named 'requests'on any install that did not
happen to have it. Installnetboot[http]for http-served repos;fileand
tftprepos, and rendering, need nothing extra. Without the extra those calls
now raiseImportErrornaming it instead of failing inside the library.
Changed
- A template variable with no value now raises by default. The two engines
disagreed: the shell engine raised while Jinja rendered an empty string, so a
typo in a.j2file shipped a boot artifact with a blank where a kernel path
belonged, and the behaviour depended on the file's suffix. The new top-level
config keytemplates_undefineddecides, and means the same in both engines:
strict(default) raises naming the variable,lenientrenders an empty
string with a warning — the old Jinja behaviour — anddebugleaves the
placeholder in the output. Settemplates_undefined: lenientto keep
templates that relied on blanks working.
Security
- Config loading is hardened: the loader runs with
allow_commands=Falseand
sandbox=True, so{{ ... }}interpolation in a config value renders in
jinja2's sandbox and a config document can no longer run commands. Previously
a value such as{{ ''.__class__.__mro__[1].__subclasses__() }}was evaluated
unsandboxed — and a netboot config is often assembled by!includefrom
inventory exports rather than written by hand. Ordinary interpolation is
unchanged; setYACONFIGLIB_CONFINE_TOto also restrict where!includemay
read from. shell_quotenow actually quotes. It wrapped values in"without escaping
anything, so a value containing`,$,"or;was expanded or
executed by the shell that read the generated script — a password with$in
it was silently corrupted, and a value taken from an inventory could run
commands. It now defaults to POSIX single-quoting (quote="'"), escaping an
embedded'as'\'';quote='"'escapes",\,`and$, and any
other quote character raisesValueError. It also returns astrfor astr
input instead of always a one-element list, so{{ shell_quote(v) }}renders
the value rather than['"v"']. Templates that relied on indexing the result
of a scalar call must drop the[0].
Fixed
- Shell-engine templates render kickstart files. The engine substituted both
%{NAME}and a bare%name, and treated an unknown bare one as an error, so
any file containing%packages,%pre,%post,%endordate +%Yfailed
to render at all. Only the documented braced form%{NAME}is substituted
now; a bare%wordis literal text, including one that names a context
variable.%%still yields a literal%and an unknown%{NAME}still
raises. A template written with bare placeholders must brace them. - A
StartPixieInithook's return value is what gets built. The result was
stored but the original config was read, so a hook written in the usual
non-mutating style (return {**value, "targets": ...}) had its targets
ignored while itstemplatestook effect — the config ended up half applied.
A hook that returnsNoneforStartPixieInitorNewPixieObjectnow raises
PixieConfigErrornaming the contract instead of failing obscurely later. - A template name containing
..is refused. Names are resolved against every
search root, so../../etc/passwdreached outside them. - Arming DHCP is all or nothing.
pxe_initstopped at the first backend that
raised, leaving the target armed on the earlier ones — it could boot an
installer from one server while another handed out its normal lease. The
already-armed backends are now rolled back before the error propagates.
pxe_completetakes the opposite approach: every backend is disarmed even if
one fails, and the first error is raised afterwards, instead of leaving the
rest armed. Pixie.completeno longer needs the target's image to exist. Retiring an
image from the config used to make the machines that used it impossible to
clean up, because building the context failed before DHCP could be disarmed.- A global named
target,image,dhcpzone,reposorresourcesno longer
replaces the render context's own field of that name; it is dropped with a
warning. - A zone is chosen by longest prefix, not declaration order: with
10.0.0.0/16
and10.0.0.5/24both configured, a host in the /24 now gets the /24. - A
Noneconfig entry (targets: {host1:}— valid YAML meaning "all
defaults") no longer crashes withAttributeError, and an entry that is
neither a mapping nor an object raisesPixieConfigErrornaming it. lookup_imagetreats any falsymatchresult as "no match": amatch
override returningNoneused to crash the sort.DhcpZoneno longer normalises the caller's lists in place (they may be
shared through a config merge), drops an unparseable nameserver with a warning
instead of turning it intoNone, andget_local_serveraccepts the strings
config actually holds rather than raising on them. Two plugins claiming one
URI scheme now log a warning naming both.- A
Pixiesubclass's annotated attribute keeps its class default when the
config does not mention it (it was overwritten withNone), and an
Optional[...]annotation no longer crashes construction — its origin is not
a constructor, and on 3.9 it is not even a class. - The
_idretry when building a config object only catches "this class takes
no_id"; any otherTypeErrorfrom the value class propagates instead of
being hidden behind a second construction attempt. Pixie.globalsis really the deep copy the docs promised. The copy made in
__init__was immediately overwritten by the annotated-attribute loop, so
nested values stayed shared with the caller's config dict (mutating
engine.globals["a"]["b"]changed the caller's mapping) and any global whose
key started with_was silently dropped. Both are fixed:_-prefixed
globals are ordinary variable names and are kept.Pixie.lookup_dhcpzonereturnsNoneinstead of raisingAttributeError
when the target has no usable IP — the documented MAC-keyed target shape hit
this on everyinitiatethat did not name adhcpzone.- Repository URLs are built correctly from an
addressthat carries a port:
mirror.example:8080became a hostname with an escaped colon
(mirror.example%3A8080) instead of a host and a port.[2001:db8::1]:8080
works too, and a repo with no address warns instead of silently producing a
hostlesshttp:/path. - An
httpsservice keeps the configured name rather than substituting the
resolved IP, which broke certificate validation and name-based virtual hosts.
Other schemes still resolve, since a PXE client often has no DNS yet. repo / "/sub"extends the repository root instead of replacing it, and a
trailing slash no longer doubles up.- A command module whose body is
main(netboot, args, conf)gets netboot's
contract. Dispatch looked only atmodule.run, while duho resolves
main→run→call, so such a command was handed to duho's
single-argument dispatch and failed on the signature. - Two context values that flatten to the same shell-template variable (a global
target_ipbesidetarget.ip, ordomainbesideDOMAIN) now log a warning
naming both; the later one still wins, but silently building an artifact from
the wrong value is what this prevents. - A relative
template_pathis resolved inside the template roots
(config["templates"],./templatesby default) rather than against the
process's working directory, so a config means the same thing whichever
directorypixieruns from. An image's or target'stemplate_pathstill
extends the roots, which are searched last; absolute paths and URIs are used
as given. A config that wrotetemplate_path: [templates/debian]to reach
./templates/debianshould now write[debian]. - Rendered artifacts keep the template's final newline. The Jinja engine dropped
it while the shell engine kept it, so the same content rendered differently
depending on the file's suffix, and a kickstart or iPXE script could end
without a newline. - MAC-less targets no longer all look for
00-00-00-00-00-00.<name>first: the
null MAC is skipped when building candidate template names, as the unset IP
already was. One stray file of that name used to apply to every such target. - A
http://...entry intemplatesor in an image'stemplate_pathstays a
URI instead of being turned into a directory namedhttp:under the working
directory, so URI search paths are searched at all. A repository'slocal
path accepts a Windows drive path (C:\\tftp), which was previously read as
URI schemecand then failed on every read. - An edited shell template is picked up again: the loader assigned its freshness
check function tois_up_to_date, and a function object is always truthy,
so a cached template was never reloaded. Loaderworks without aPixieContextin the environment: it resolves a
plain template name instead of raisingAttributeError.- Template selection is deterministic. Within a search directory an exact
filename now wins, and several files sharing a stem (bootmatching
boot.j2,boot.sh,boot.j2.bak) resolve lowest-name-first instead of in
whatever order the filesystem listed them — a leftover.bak/.orig/
.rpmnewcopy could previously be rendered instead of the real template. - Templates are read as UTF-8 rather than the machine's locale encoding, so the
same template tree renders identically on every host (on Windows a non-ASCII
template could raiseUnicodeDecodeErroror decode wrongly). - An image with no
template_pathrenders instead of raisingAttributeError
fromPixieContext.searchpaths; it now defaults to[]as targets already
did. - A target whose hostname does not resolve no longer hangs the process.
PixieTargetconstruction retried the same DNS lookup in an endless loop
while the name stayed unresolved; because every target is built at startup,
one not-yet-in-DNS entry made everypixiecommand hang and flood the
resolver. Resolution is now a single lookup: it fillsipon success, and on
failure logs a warning and leavesipunset. A malformed hostname is warned
about instead of aborting construction.
Changed
Pixie.lookup_targetmatches exactly before it matches by prefix. It
previously returned the first target whose hostname started with the query,
so with targetsnode10andnode1in that order,pixie initiate node1
acted onnode10. Exact id, hostname, MAC and IP matches across the whole
table now win; a hostname prefix is the fallback, a query matching more than
one target raisesLookupError(the CLI reports it and exits 1) instead of
choosing one, and an empty query matches nothing rather than the first entry.
MAC queries are parsed, soAA-BB-CC-00-00-01andaabb.cc00.0001now match
a target keyedaa:bb:cc:00:00:01; previously only the colon spelling did.pathlib_next[uri]floor raised to>=0.9.9. Measured: recursive
!includeglobs (sub/**/*.yaml) fail on pathlib_next 0.9.5+ unless
yaconfiglib is 0.12.0+, and yaconfiglib 0.12.0 itself requires
pathlib-next>=0.9.9 — so the old>=0.9.0claim described a combination that
cannot work.tests/test_config_discovery.pynow fails below the floor
instead of passing quietly.yaconfiglibmoves to the 0.12 series (>=0.12.0,<0.13). The APIs netboot
uses are unchanged; the previous<0.12ceiling made netboot uninstallable
alongside yaconfiglib 0.12.- The package logger is named
netboot, notNETBOOT, and-v/-qnow reach
it: duho sets the level of the logger named after the parser, which never
matched the one netboot writes to, so the verbosity flags had no effect on
netboot's own output. Anything filtering on the old name must usenetboot. - Importing
netbootno longer has logging side effects. It used to quiet
urllib3/paramiko and disable urllib3'sInsecureRequestWarning
process-wide — a decision belonging to the application, not to a library, and
it imported urllib3 as a side effect ofimport netboot. ThepixieCLI
callsquiet_noisy_dependencies(); embedders opt in. - The CLI reports operator errors as one line and exits 2 instead of printing a
traceback: a missing or malformed config, a non-mapping config, an unknown
image or zone, or a sandbox refusal.-vstill shows the traceback, and an
unexpected exception is still raised in full.--versionand usage errors
saypixierather than the class namePixie_. - An absolute Windows path is accepted for
--config/--baseconfig/template
paths.C:\\srv\\tftpwas parsed as URI schemec, which then failed with
NotImplementedError; a single-letter scheme is now read as a drive letter. - An empty or comment-only config loads as
{}rather than raising
AttributeError, a scalartemplates:is accepted as a one-element list, and
a command returning a non-int value is warned about and treated as success
rather than crashing after the command's work is done. - License metadata is PEP 639:
license = "MIT"pluslicense-files, and the
legacyLicense :: OSI Approved :: MIT Licenseclassifier is gone (building
now needshatchling>=1.27). The wheel carries the licence at
netboot-<version>.dist-info/licenses/LICENSE. - Packaging excludes
*.local.*from both sdist and wheel. A personal override
such aspixie.local.yamlorAGENTS.local.mdpreviously shipped, because a
dotfile pattern does not match a name that has no leading dot. - The engine moved to
netboot.engine, leaving the package root as a surface
(44 lines).from netboot import Pixieand every other documented import are
unchanged;netboot.engine.Pixieis the same object.netboot._version
holds the version lookup. netboot.utilsno longer re-exportsargparse.Namespaceas
netboot.utils.Namespace, where it read as netboot's own config base. The
config base isnetboot.utils.config.Namespace, as documented.PixieContext.templatesis gone. It was an annotation only: nothing ever set
it, so reading it raisedAttributeError. Search paths are
PixieContext.searchpaths.jinja2is now>=3.0,<4, previously unpinned. jinja2 2.x imports
markupsafe.soft_unicode, removed in MarkupSafe 2.1, so an unconstrained
resolve could install a pair that raisesImportErroronimport jinja2.
Documentation
- The shipped API header (
src/netboot/AGENTS.md) is corrected where it did not
match the code:hook'svalueis positional-only and every hook must return
a value;Pathin a Jinja template ispathlib_next.Path, not the stdlib's;
netboot.netutilsis an attribute, not an importable module path;
PixieContext.resourcesstarts empty and is a hook's slot to fill; template
search tries each candidate name across all search paths (not each path
across all names); importingnetboot.loggingdoes import urllib3 and
disables its insecure-request warning process-wide. docs/cli.mdno longer saysinitiaterenders artifacts or that--iscsi
prepares an iSCSI LUN — both are hook extension points;--helpsaid the same
and was corrected too. The--configrow explains that a.cfg/.inisuffix
is parsed as INI, not YAML.docs/extending.mddocuments the hook contract: the positional signature, the
dictfourth argument,netboot=NoneforNewPixieObject, the prefixed
PixieEventstring values, and that a hook must return the value.docs/configuration.mdexample is runnable as written (the MAC-keyed target
names its zone) and says that adnsmasq://backend has to be provided and
imported. The shell-template section documents that only%{NAME}substitutes.- The public API is documented in the source, so the API Reference renders
prose rather than bare signatures: 41 docstrings added, leaving only dunders
undocumented (which the reference filters out anyway). - The API Reference page covers the content, template, utility and CLI modules
as well as the engine, and renders members that have no docstring; the site
gains a Changelog page. - README/docs install instructions name
netboot[config], which thepixie
CLI needs to read a config file, and the README's LICENSE links are absolute
so they resolve on PyPI. examples/holds a complete runnable setup — config, both template engines,
and aDhcpServerplugin that prints instead of arming real DHCP — and a
repo-rootAGENTS.mdcovers layout, environments, commands, CI and releasing
for contributors. The README gains Development and Releasing sections.benchmarks/documents its metrics and schema and keeps results in the
trackedbenchmarks/results/, written by a new--saveflag, so a
before/after comparison survives in history. The metric that claimed to
measure a worst-case target scan actually measured an indexed hit; it is now
a pair,lookup_target_by_idandlookup_target_scan_by_ip.- CI: the release workflow runs a non-deploying docs gate and dispatches the
docs workflow for the tag (a release created withGITHUB_TOKENstarts no
workflow run, sorelease: publishedalone never deployed), publishing uses
skip-existing, the test workflow drops to read-only permissions and also
runs on Windows and macOS, and docs redeploy whensrc/,README.mdor
CHANGELOG.mdchange.
Full Changelog: v0.1.3...v0.2.0