Repository navigation
Releases: jose-pr/netboot
Release list
v0.4.1
Changed
DUHO_TRACEBACKis read with duho's own truthy values:0,false,no,
offand an empty value no longer print a traceback on a reported error,
where any non-empty value did. The status stays 2.- A command module's
rungets the engine (run(netboot, args, conf)) when
duho'saccepts_positionalsays it takes three positional parameters. That
differs from before only for arunwhose signature cannot be read (a
builtin or other C callable), which is now dispatched as a plainrun(args),
and for afunctools.wrapswrapper, whose own signature is read rather than
the wrapped function's.
Full Changelog: v0.4.0...v0.4.1
v0.4.0
Added
pixie render <target> NAME... [--out DIR]renders a target's artifacts to
standard output or a directory, arming nothing.
Changed
- Configuration is loaded by
anycfg, the new name of yaconfiglib:
netboot depends onanycfg>=0.14.0,<0.15andtplx>=0.2.0,<0.3instead of
yaconfiglibandjinja2, andnetboot[config]installsanycfg[yaml].
Includes are confined byANYCFG_CONFINE_TO;YACONFIGLIB_CONFINE_TOis
not read, so rename it where you set it. A malformed file is still a
one-line error, now namingfile:line:col.netboot.utils.config.Namespace
and the config classes build onanycfg.TypedNamespace/OpaqueMerge. - Every public module declares
__all__;netboot.utilsexportsNamespace
too.PixieTarget,PixieImageandPixieContextare defined in private
modules and imported, as before, fromnetbootornetboot.engine. netboot.dhcp.DhcpZoneisDHCPZone(also importable fromnetboot), as
every other public name spells DHCP; rename the import.Pixie.lookup_imagereturnsNonewhen nothing matches, as
lookup_targetandlookup_dhcpzonedo, rather than{}; a
FoundTargetImagehook seesNonetoo.- The command line is
netboot.cli, one duho class per command
(Initiate,Complete,Render,DhcpConfig); the console script is
netboot.cli:main, andnetboot.mainre-exportsmain,PixieArgs,
Pixie_andparse_path.main(argv=None, *, name=None):argvis the
first parameter, somain(["--version"])works; passname=by keyword.
netboot.cmdsis gone:netboot.cmds.dhcp_config.recipeis
netboot.dhcp.condition_recipe,conditions_forisbuild_conditions, and
a--cmdspathplugin withrun(netboot, args, conf)works as before. pixieno longer imports apixie_envsettings module fromsys.path,
which underpython -m netbootmeant the working directory: running it in
a directory runs no code from there. ExportPIXIE_*variables instead, or
name a module with--load-module.- Building the engine does no DNS. A hostname-only target's
ipis
looked up byPixieTarget.resolve_ip(), which the engine calls for the
target a command acts on (and for every hostname-only target when an IP
query matches no configured address), so an unresolvable target no longer
slows every command. APixieTargetbuilt directly hasip == ""until
resolve_ip()is called. initialize(target, **options)andcomplete(target, **options)hand
optionsto their Start/End hooks as kwargs:initiate --iscsinow reaches
them askwargs["iscsi"].- A DHCP server's credentials are its own config entry's. A
dhcpservers
entry is a URI or a mapping ofuri:and that backend's secrets (user,
password; dhcpdkeyname,omapi_key), written in the config or taken
from the environment there with{{ env.NAME }}, which config files can now
use.PIXIE_WINDHCP_PASSWORD,PIXIE_WINDHCP_USERand
PIXIE_DHCPD_OMAPI_KEYare no longer read, and
netboot.dhcp.WINDHCP_PASSWORD_ENV_VAR,WINDHCP_USER_ENV_VARand
DHCPD_OMAPI_KEY_ENV_VARare gone: move each value into the entries of the
servers that use it (password: "{{ env.PIXIE_WINDHCP_PASSWORD }}"keeps a
variable you already set). An entry's secrets override a user or password
in the URI.pixie dhcp-config --aspasses the prompted user and password
to each server directly, overriding the URI's. - The errors are in
netboot.exceptions(PixieError,PixieConfigError,
PixieLookupError), still re-exported fromnetboot. dhcp_options,dhcp_whenanddhcp_completeare read as what a target is
served, a pydhcpDHCPLease:next-serverandboot-file-nameare its
BOOTP header fieldssiaddrandfile, as they always reached the server,
andfile,snameandsiaddrmay be written as such.bootfile-nameis
option 67 itself and is now sent as an option.build_next_serveris
replaced bybuild_lease(ctx, server). A Kea client class fromdhcp_when
carries the boot file as itsboot-file-namefield, not as option 67.- An option with no type of its own (
option-224, unlessdhcp.options
declares one) takes its octets as hex text or{hex: ...}in
dhcp_options, as pydhcp'sDHCPOptions.from_mappingreads them. - The DHCP backends are
dhcpctl's. netboot depends on
pydhcpctl>=0.1.0,<0.2(imported asdhcpctl), andnetboot.dhcpis a module that builds each
target's reservation for it:DhcpZone,connect(uri),build_options,
build_conditions,build_completion,reservation_for(ctx, server),
armanddisarm. What a server is sent is unchanged, byte for byte,
including the names netboot writes on it (# >>> netbootregions,
netboot-<mac>Windows policies). Removed, with what to do:DhcpServerand its scheme-by-class-name dispatch: subclass
dhcpctl.DHCPServerand calldhcpctl.register_backend(scheme, cls)from
the--load-moduleplugin, or declare it in thedhcpctl.backendsentry
point. A backend implementsadd(reservation)/remove(reservation)
(wereadd_target(ctx)/remove_target(ctx)),keep(reservation)(was
keep_target(ctx, completion)), andextras(reservation, phase)(was
extras(ctx, phase)); the netboot context isreservation.context.netboot.dhcp.dnsmasq/.kea/.dhcpd/.windhcp: the backends are
dhcpctl.backends.DnsmasqBackend,KeaBackend,DHCPDBackendand
WinDHCPBackend; to override one, register the subclass with
replace=True.netboot.dhcp.options,.conditionsand.completion:GENERIC_OPTIONS,
APPLY_TIME,DhcpOptions,split_query,DhcpCondition,MATCH_KEYS,
DhcpCompletionandBOOT_OPTIONSare dhcpctl's (DHCPOptions,
Condition,Completion, ...);build_options,build_conditions,
build_completionandzone_defaultsare innetboot.dhcp.
ConditionMissing/ConditionUnsupportedare
dhcpctl.ConditionMissingError/ConditionUnsupportedError.windhcp.PASSWORD_ENV_VAR/USER_ENV_VARare
netboot.dhcp.WINDHCP_PASSWORD_ENV_VAR/WINDHCP_USER_ENV_VAR, and
dhcpd.KEY_ENV_VARisDHCPD_OMAPI_KEY_ENV_VAR; the variables are the
samePIXIE_*ones, now read only when the URI has no user or password
of its own:windhcp://admin:pw@hostanddhcpd://keyname:key@hostwork.
Logs, errors andpixie dhcp-configshow a server's URI with the password
masked.netboot.DhcpServeris no longer exported.
Thekea,dhcpd,winrmandsshextras install dhcpctl's of the same
name. An unknown scheme, or a URI a backend cannot use, is a
PixieConfigError; a server refusing is adhcpctl.ServerError, reported by
the CLI as one line with status 2.PixieEvent.BuildDhcpOptionsnow fires
once per server for every backend, including one that never asks for its
options.
- DHCP options are pydhcp's, through dhcpctl. Every IANA option is named by
its pydhcp name (dns,bootfile-name,interface-mtu, ...) or
option-<n>, and the names netboot already took still work
(LEGACY_OPTION_NAMES). A config declares more in a new top-level
dhcp.optionssection: an option of its own with a code and a pydhcp type,
or an alias of a name or a code;dhcp_options,dhcp_whenand
dhcp_completeall read them. Each value is read by the
option's type when the config is applied, sorouter: gatewayis a
PixieConfigErrornaming the image or target; a list option takes one
address per item, so write several as a YAML list or a repeated URI key,
not one comma-joined string. Kea is sent options by code, so a zone with a
gateway or a lease time no longer sends Kea option names it does not define
(router,lease-time). What to do in aBuildDhcpOptionshook: it
receives pydhcp'sDHCPOptions, keyed by code,
so writeoptions[DHCPOptionCode.ROUTER] = ...oroptions[3]where it
wroteoptions["router"], or return a mapping of option names.
next-serveris no longer an option a hook sees: it is the reservation's
BOOTP field, still set fromdhcp_options: {next-server: ...}. - A server URI says only how to reach the server. Its query takes the
backend's connection settings and nothing else: DHCP options
(?router=...),options_builder=andraw.<backend>=text are
unknown-key errors. What to do: move options to the zone's settings or an
image's or target'sdhcp_options; replace anoptions_builderwith a hook
onPixieEvent.BuildDhcpOptions, which receives the same options plus the
target and the server; for the iPXE chainload usedhcp_when:, and for other
backend-native text register a backend subclass whose
extras(reservation, phase)returns it. - Template roots resolve against the config file's directory. Through the
CLI, a relativetemplates:root is joined to the directory of the config
file, as!includeis, and with notemplates:the root istemplates
beside the config file (./config/templatesby default). The working
directory's./templatesis no longer prepended. What to do: a config that
relied on./templatesnames it, relative to the config file (the example
config now saystemplates: ../templates). - The path layer is
pathapianduripath, notpathlib-next. netboot
depends onpathapi>=0.1.0,<0.2andpathapi-uri>=0.1.0,<0.2(import
packageuripath) and no longer installspathlib-nextoruritools. The
paths netboot returns are their classes:Repository.service(),.get()
and.localareuripath.URIPath(werepathlib_next.uri.UriPath),
parse_path()returnspathapi.LocalPathorURIPath, and a template's
PathandUriglobals arepathapi.Pathanduripath.URIPath. What to
do: import them from there in a command module, hook or template that used
pathlib_next...
v0.3.2
Added
-
pixie dhcp-config [target] [--apply] [--as USER]— prints the server-side
configuration a config needs but netboot may not be able to create: the Windows
policy and its user class, adhcpd.confgroup, a Keaclient-classesentry.
Printing contacts nothing, so the output goes straight into a change ticket;
--applyruns it and--asprompts for an admin password. It is the only
command that prompts, becauseinitiateandcompleterun unattended and a
prompt there would hang a pipeline. Run once per condition; routine arming then
needs none of those rights. -
windhcp://takestransport=local— run PowerShell on this machine, with
server=pointing the cmdlets at the DHCP server, which is the ordinary RSAT
shape and needs no sshd, WinRM or credential. An empty host
(windhcp:///?server=dhcp01) defaults to it; a named host still uses a
transport. The account for WinRM can now come from$PIXIE_WINDHCP_USERwhen
the URI names none. -
dhcp_complete:— what a finished target's DHCP entry becomes. A machine
that still receivesboot-file-nameafter its install boots the installer
again;pixie completeremoved the reservation to prevent that, which also
threw away the fixed address. Nowkeep: trueleaves the address and drops the
boot options (firmware falls through to local disk), andoptions:serves
something else instead — an iPXEsanbootscript, for firmware with no
local-disk fallback. Router, DNS and subnet mask are never touched, and a kept
entry also loses itsdhcp_whenmembership so the condition cannot keep serving
it the installer. The default is unchanged. All four backends implement it;
verified against a real Windows Server 2025 for each of the three outcomes. -
dhcp_when:— conditional options, declared once and translated per
backend. A mapping keyed by name, where the name is the construct each
server uses: a Windows scope policy, a dhcpd group or inlineif, a Kea client
class. It layers zone → image → target by name, so two targets share one
policy and a target can replace an image's condition.matchtakes
user-classandvendor-class;optionstakes the usual option names. This
is the iPXE chainload said once:undionly.kpxefor a PXE ROM,
boot.ipxefor iPXE.On Windows a condition becomes two policies for that target — one matched on
its MAC plus the test, one on the MAC alone — and the overridden option is
kept off the reservation, because a reservation's options outrank every
policy and would otherwise make the condition dead while the configuration
looked right.conditions=sharedgives the other shape: one policy per
condition for the whole scope, which an administrator can create once on a host
where netboot cannot create policies at all.A backend that cannot create the construct says exactly what to run
(Add-DhcpServerv4Class/Add-DhcpServerv4Policy, adhcpd.confgroup, the
Keaclient-classesJSON), and one that cannot express conditions at all
(dnsmasq://, because the tag needs adhcp-matchline in config netboot does
not own) refuses rather than arming a target whose chainload would never fire.
Either way the zone's other servers are armed, since arming is best effort. -
DhcpServer.ensure_condition()/.condition_recipe()/
.remove_condition_member()— the contract above, for a plugin backend.
Changed
-
netimps>=0.3.3(was>=0.3.1), and three things netboot hand-rolled are
now the library's. No behaviour netboot documented changes, except where noted:- repository addresses are split by
normalize_host, so a malformed one
(mirror.example:http,mirror.example:) now raisesPixieConfigError
naming the repo instead of being treated as a hostname and handed to DNS as
one.[v6]:portstill produceshttp://[2001:db8::1]:8080/...— the host
comes back unbracketed andpathlib_nextbrackets it itself. - "the target has no address" is
is_wildcard, which also coversNone
and a%zonesuffix (::%eth0); the check it replaced knew0.0.0.0and
::only, so a scoped unspecified address produced a template candidate
named after it. - the
windhcp://netsh client id isMACAddress.hex()rather than
formatting with separators and stripping them again.
- repository addresses are split by
-
netboot.utils.net.Hostis now netimps'Host, subclassed rather than
reimplemented. The local class claimedHostwas "a netboot concept"; netimps
has had the same type since 0.2.0, written for the same reason, and this
module's stated principle is to use the library's vocabulary. Nothing is
removed:.try_ip(),.address(readable and writable) andHost()with
no argument all behave as before, so this is not an API break.What you gain for free:
.ip()caches its result, failures included, so a
repo asked for several services resolves once where the old class resolved
every time (refresh=Trueretries). Also inherited:.value(the same text as
.address),.is_address(no DNS), and, on netimps 0.3.3,.fqdn.Two behaviour differences worth knowing, neither of them a documented contract:
Host("a") == "a"is nowTruewith matching hashes, where the old class
returnedNotImplemented; and faking resolution in a test means patching
netimps.get_ip, which.ip()imports at call time, rather than netboot's
ownresolvere-export.
Full Changelog: v0.3.1...v0.3.2
v0.3.1
Fixed
windhcp://could not set a multi-valued option (two name servers, two NTP
servers) with its default PowerShell method, and the failure took the rest of
the reservation with it. The value was sent comma-joined, and
Set-DhcpServerv4OptionValue -ValuetakesString[]: Windows answered
"Parameters for option value to be set for option ID 6 do not match with option
definition" and, underErrorActionPreference = 'Stop', every option after it
was abandoned — so the target got a router and no boot file. Values are now
sent as a list. Present since the backend shipped in 0.2.0; found by running it
against a real Windows Server 2025 DHCP server rather than reading the script.
Added
DhcpServer.extras(ctx, phase)— one documented extension point for
backend-native fragments netboot does not model: an extra statement, an extra
command, or a condition such as the iPXE chainload (serve the iPXE binary to a
PXE ROM, the script to iPXE). It defaults to the configuredraw.<backend>
fragments, and a subclass overrides it to decide per target. All four shipped
backends consume it, andraw.<backend>.remove=is new: a fragment that
belongs to teardown rather than to arming. Forwindhcp://a fragment may be a
PowerShell line or a{"Args": [...], "Ignore": bool}netsh command, which
works under eithermethod.method=netshverifies what the server actually applied. netsh exits 0 and
prints its success line even when it rejected what it was told — measured: a
domain-name-serversoption with one unreachable address keeps the other, says
"not a valid DNS Server", says "Command completed successfully", exits 0. So
after applying, netboot reads the state back withnetsh ... dump(the one
netsh output that is command syntax rather than localised prose) and fails
naming any option that is missing or short of values. The cmdlet method needs
none of this: Windows raises there by itself.windhcp://takesmethod=powershell|netsh. The default is unchanged (the
DhcpServercmdlets);method=netshdrivesnetsh dhcp server ...instead,
for a host where that PowerShell module is not installed. It is independent of
transport=, and netsh is invoked from PowerShell with each argument an array
element, so no value is spliced into a command line. Success is$LASTEXITCODE
rather than netsh's localised success line, a removal tolerates a reservation
that is not there, option data types are declared per option (unmapped ones as
STRING), and a multi-valued option becomes one argument per value.
Full Changelog: v0.3.0...v0.3.1
v0.3.0
Changed
- The shell template engine now claims the
.shtplsuffix only (it claimed
every suffix before, as the documented fallback). The suffix picks the engine
and the stem names the artifact, which is the conventioninstall.ks.j2
already used: renameboot.cfgtoboot.cfg.shtplandctx.render("boot.cfg")
keeps working unchanged. A file no engine claims is copied as is by the new
CopyTemplate, last in the defaulttemplate_types— so a staticgrub.cfg
needs no engine, and a file that does carry%{NAME}placeholders ships them
unrendered until it is renamed. That one case logs a warning naming the rename.
Narrowingtemplate_typesso nothing claims a file raises
netboot.templates.TemplateEngineError, which names the file and every
engine's suffixes.
Added
- Seven more template engines, each claiming its own suffix and costing
nothing when unused:MakoTemplate(.mako,netboot[mako]),
LiquidTemplate(.liquid,netboot[liquid]),HandlebarsTemplate
(.hbs/.handlebars,netboot[handlebars]),MustacheTemplate(.mustache,
netboot[mustache]), andERBTemplate(.erb) /EppTemplate(.epp),
which render through the systemrubyandpuppet epp renderso an existing
template behaves as its author tested it (PIXIE_RUBY/PIXIE_PUPPETname
the program if it is not onPATH). Every engine is registered whether or not
its dependency is present — a claimed file reports what is missing instead of
being copied — and nothing is imported until a file claims it. What each engine
does withtemplates_undefineddiffers by library and is tabulated in the
configuration guide; handlebars and mustache cannot fail on an undefined name
at all. template_data()/DataView/jsonable()— a lazy mapping view of the
render context for engines that read data rather than evaluate Python
({{ target.hostname }}beside{{ ctx.target.hostname }}), and its
JSON-serialisable form for the subprocess engines.SubprocessTemplate— the base for an engine that renders through another
language's own tooling: program lookup with an env override, a temporary
template and values file, a timeout, and the program's stderr in the error.ShellTemplateis configured by subclassing, through three class attributes:
EXT(a string or a sequence, dots optional, case-insensitive;Noneor
"*"claims any suffix),DELIMITER(default"%") andPATTERN—
"braced"(default,%{NAME}),"unbraced"(%NAME) or"all".- POSIX default expressions in the braced form:
%{NAME:-fallback}substitutes
the fallback when the value is unset or empty,%{NAME-fallback}only when
it is unset. One layer of'/"quotes is stripped, the fallback is literal
text, and a placeholder with a default never fails whatever
templates_undefinedsays.:=,:?and:+are not implemented and stay
literal text. CopyTemplate— the copy-as-is engine described above, working in bytes:
the loader no longer decodes a file before knowing which engine claims it, so
the copy is byte-exact for anything (CRLF, latin-1, a binary) and
ctx.render()returnsbytesfor such a file.EXTnarrows it to chosen
suffixes;Template.BINARYis the flag any engine can set to be handed raw
bytes. A non-UTF-8 file claimed by a text engine now raises
TemplateEngineErrornaming the file, where it used to fail inside the read.Loader.find_source()— the byte-returning counterpart ofget_source()
(which keeps jinja2's text contract).- A template-engine registry:
register_template_type()(bare or as a
decorator, with an optionalpriority=),unregister_template_type(),
TEMPLATE_TYPESandby_priority().Loader(template_types=None)— the new
default — uses every registered engine, so a--load-moduleplugin is picked
up without rebuilding the list, and the list is snapshotted at construction. Template.PRIORITY, so engine selection does not depend on registration
order: engines are consulted highest first,DEFAULT_PRIORITY(0) for an
ordinary engine andFALLBACK_PRIORITY(-100) forCopyTemplate. A plugin is
registered after the shipped engines, so without this every plugin would sit
behind the catch-all and never see a file. Equal priorities keep the order they
were given, so an explicittemplate_typeslist still means what it says.Template.EXTandJinjaTemplate.EXT, so suffix ownership is one declarative
attribute per engine, plusnetboot.templates.template_extensions()to read it
back normalised.
Full Changelog: v0.2.4...v0.3.0
v0.2.4
Changed
duhofloor moves to>=0.6.0,<0.7(pre-1.0 minor, documented API break).
None of that break reaches netboot:app()'s renamed internal dispatch
dest, the stricter--loglevel, thePATHSEPenv var rename to
<PREFIX>PATHSEP, and RunPath ordering are all surfaces netboot's CLI does
not touch, so nothing changes besides the pin.
Full Changelog: v0.2.3...v0.2.4
v0.2.3
Fixed
- One DHCP server being offline no longer fails
pixie initiateor
pixie complete. Arming is meant to be best effort, but since 0.2.0 it
stopped at the first failing server and rolled the others back.initiate
now tries every server in the zone, logs each failure as a warning, and fails
only when no server could be armed.completelogs a failing server as a
warning and still exits 0, so read the log if you need to know every server
was disarmed.
Full Changelog: v0.2.2...v0.2.3
v0.2.2
netboot moves to netimps 0.3.1 and yaconfiglib 0.13. If a plugin, hook or
template compares target.mac with text, read the MACAddress entry below.
Changed
- Dependencies move to their new series:
netimps>=0.3.1,<0.4and
yaconfiglib>=0.13.0,<0.14. The old ceilings made netboot uninstallable
beside either release. netimps 0.3.0 is skipped on purpose: it raised from
resolve()when no resolver was reachable, which would have failed every
command at startup, and 0.3.1 answers[]again. netboot uses none of the
yaconfiglib APIs 0.13.0 removed. MACAddressno longer equals astr(netimps 0.3.0, re-exported as
netboot.utils.net.MACAddress).target.mac == "aa:bb:cc:dd:ee:ff"is now
False— in a plugin, a hook or a Jinja template alike. Compare with
MACAddress.try_parse(text) == target.mac. In exchange a MAC is found in a
set or dict whichever spelling built it. netboot's own target lookup already
compared parsed MACs and is unaffected. A MAC that mixes separators
(00-11:22-33:44-55) is now rejected, andaa.bb.cc.dd.ee.ffis accepted.resolve()keeps trying backends after an empty answer, so a hostname
target known only to the hosts file or NSS now gets its IP on Windows and
macOS. A name that exists nowhere costs two or three lookups instead of one.
Full Changelog: v0.2.1...v0.2.2
v0.2.1
netboot ships DHCP backends. Additive: nothing documented changed behaviour,
and a config that named no dhcpservers scheme netboot implements works as
before.
Fixed
- A repository
addressof the formhost:portno longer sends the whole
string to DNS. The port is split off first, somirror.example:8080
resolvesmirror.example— the old lookup could only fail, and did so at
different speeds on different machines.
Added
netboot.dhcpis a package, and DHCP backends carry client options. A
dhcpserversentry is still a plain URI string; its query string now holds the
options the server should give the client (?router=10.0.0.1), alongside each
backend's own connection settings.netboot.dhcp.optionsmodels them:
GENERIC_OPTIONSfor the names netboot translates,option-<n>for anything
else,raw.<backend>=for untranslated backend-native text, and
options_builder=my.mod.fnplus the newPixieEvent.BuildDhcpOptionsfor
building them from the context. Merge order, later winning: zone defaults, the
connection query,image.dhcp_options,target.dhcp_options, the callback,
the hook.- A dnsmasq backend (
dnsmasq://), the first netboot ships. It writes
dhcp-host/dhcp-option/dhcp-bootentries throughpathlib_nextpaths, so
the same configuration serves a local dnsmasq or one reached oversftp://
(netboot[ssh]).hostsfile/optsfilemay be a directory (one file per
target — preferred, and with--dhcp-hostsdir/--dhcp-optsdirdnsmasq re-reads
them with no signal at all) or a file, where netboot edits only its own
marked region and never touches a line it did not write.reload=runs a
command, over ssh when the URI names a host; omitting it is fine for a watched
directory and an error for a file, which dnsmasq never re-reads by itself. - A Kea backend (
kea://,keas://for https). It sendsreservation-add
andreservation-delto the control agent, mappingboot-file-name,
next-serverandhost-nameonto reservation fields and everything else
ontooption-data. Thesubnet-idcomes from the zone (subnet_id:) or is
discovered once by matching the zone's network againstconfig-get; when
neither works netboot refuses rather than guessing, because a reservation in
the wrong subnet silently never matches. Aresult: 2from Kea is reported
with the likely cause (host_cmds not loaded, or a read-only hosts backend),
and an existing reservation for the same MAC is refused rather than
overwritten. Needsnetboot[kea]. - An ISC dhcpd backend (
dhcpd://) over OMAPI, the only way to give dhcpd a
reservation without rewriting and reloading its config. Options are rendered
into the host'sstatements, which is dhcpd config source: text values are
quoted with"and\escaped, a value meant to be an address must look like
one, and a newline is refused outright — a value cannot end its statement and
start another. The OMAPI secret comes fromkeyfile=or
$PIXIE_DHCPD_OMAPI_KEY, never from the URI. Needsnetboot[dhcpd]. Note
that a host added over OMAPI does not survive a dhcpd restart by itself: that
is dhcpd's design, not netboot forgetting it. - A Windows DHCP Server backend (
windhcp://), driving theDhcpServer
PowerShell module over ssh (the default, no dependency — it uses the system
sshclient, so your config, keys, agent and jump hosts apply) or WinRM
(netboot[winrm]). The transport host and the DHCP server are separate:
server=becomes-ComputerNamewhen the cmdlets should act elsewhere. The
scope is the zone's network address, resolved when the target is applied and
overridable per zone. No value is interpolated into the script: parameters
travel as a JSON payload PowerShell parses, so a target name or option value
cannot become a statement. - New extras, each carrying exactly one backend's dependency:
netboot[kea],
netboot[dhcpd],netboot[winrm], andnetboot[ssh]for dnsmasq over
sftp://. A local dnsmasq andwindhcp://over ssh need none. Verified on a
clean wheel install: importingnetboot.dhcploads no backend dependency,
those two backends construct, and the other two raiseImportErrornaming
their extra. - Anything that varies per target is refused in a connection string and resolved
when the target is applied instead:subnet_id/scopebelong to the zone,
boot-file-name/next-server/tftp-server-nameto the image or target. The
error names where each belongs.from netboot.dhcp import DhcpServer, DhcpZone
is unchanged, and a backend module is imported only when a config names its
scheme.
Full Changelog: v0.2.0...v0.2.1
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 neve...