Skip to content

Releases: jose-pr/netboot

v0.4.1

Choose a tag to compare

@github-actions github-actions released this 21 Oct 18:17

Changed

  • DUHO_TRACEBACK is read with duho's own truthy values: 0, false, no,
    off and 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 run gets the engine (run(netboot, args, conf)) when
    duho's accepts_positional says it takes three positional parameters. That
    differs from before only for a run whose signature cannot be read (a
    builtin or other C callable), which is now dispatched as a plain run(args),
    and for a functools.wraps wrapper, whose own signature is read rather than
    the wrapped function's.

Full Changelog: v0.4.0...v0.4.1

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 18 Oct 18:49

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 on anycfg>=0.14.0,<0.15 and tplx>=0.2.0,<0.3 instead of
    yaconfiglib and jinja2, and netboot[config] installs anycfg[yaml].
    Includes are confined by ANYCFG_CONFINE_TO; YACONFIGLIB_CONFINE_TO is
    not read, so rename it where you set it. A malformed file is still a
    one-line error, now naming file:line:col. netboot.utils.config.Namespace
    and the config classes build on anycfg.TypedNamespace / OpaqueMerge.
  • Every public module declares __all__; netboot.utils exports Namespace
    too. PixieTarget, PixieImage and PixieContext are defined in private
    modules and imported, as before, from netboot or netboot.engine.
  • netboot.dhcp.DhcpZone is DHCPZone (also importable from netboot), as
    every other public name spells DHCP; rename the import.
  • Pixie.lookup_image returns None when nothing matches, as
    lookup_target and lookup_dhcpzone do, rather than {}; a
    FoundTargetImage hook sees None too.
  • The command line is netboot.cli, one duho class per command
    (Initiate, Complete, Render, DhcpConfig); the console script is
    netboot.cli:main, and netboot.main re-exports main, PixieArgs,
    Pixie_ and parse_path. main(argv=None, *, name=None): argv is the
    first parameter, so main(["--version"]) works; pass name= by keyword.
    netboot.cmds is gone: netboot.cmds.dhcp_config.recipe is
    netboot.dhcp.condition_recipe, conditions_for is build_conditions, and
    a --cmdspath plugin with run(netboot, args, conf) works as before.
  • pixie no longer imports a pixie_env settings module from sys.path,
    which under python -m netboot meant the working directory: running it in
    a directory runs no code from there. Export PIXIE_* variables instead, or
    name a module with --load-module.
  • Building the engine does no DNS. A hostname-only target's ip is
    looked up by PixieTarget.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. A PixieTarget built directly has ip == "" until
    resolve_ip() is called.
  • initialize(target, **options) and complete(target, **options) hand
    options to their Start/End hooks as kwargs: initiate --iscsi now reaches
    them as kwargs["iscsi"].
  • A DHCP server's credentials are its own config entry's. A dhcpservers
    entry is a URI or a mapping of uri: and that backend's secrets (user,
    password; dhcpd keyname, 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_USER and
    PIXIE_DHCPD_OMAPI_KEY are no longer read, and
    netboot.dhcp.WINDHCP_PASSWORD_ENV_VAR, WINDHCP_USER_ENV_VAR and
    DHCPD_OMAPI_KEY_ENV_VAR are 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 --as passes 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 from netboot.
  • dhcp_options, dhcp_when and dhcp_complete are read as what a target is
    served, a pydhcp DHCPLease: next-server and boot-file-name are its
    BOOTP header fields siaddr and file, as they always reached the server,
    and file, sname and siaddr may be written as such. bootfile-name is
    option 67 itself and is now sent as an option. build_next_server is
    replaced by build_lease(ctx, server). A Kea client class from dhcp_when
    carries the boot file as its boot-file-name field, not as option 67.
  • An option with no type of its own (option-224, unless dhcp.options
    declares one) takes its octets as hex text or {hex: ...} in
    dhcp_options, as pydhcp's DHCPOptions.from_mapping reads them.
  • The DHCP backends are dhcpctl's. netboot depends on
    pydhcpctl>=0.1.0,<0.2 (imported as dhcpctl), and netboot.dhcp is a module that builds each
    target's reservation for it: DhcpZone, connect(uri), build_options,
    build_conditions, build_completion, reservation_for(ctx, server),
    arm and disarm. What a server is sent is unchanged, byte for byte,
    including the names netboot writes on it (# >>> netboot regions,
    netboot-<mac> Windows policies). Removed, with what to do:
    • DhcpServer and its scheme-by-class-name dispatch: subclass
      dhcpctl.DHCPServer and call dhcpctl.register_backend(scheme, cls) from
      the --load-module plugin, or declare it in the dhcpctl.backends entry
      point. A backend implements add(reservation) / remove(reservation)
      (were add_target(ctx) / remove_target(ctx)), keep(reservation) (was
      keep_target(ctx, completion)), and extras(reservation, phase) (was
      extras(ctx, phase)); the netboot context is reservation.context.
    • netboot.dhcp.dnsmasq/.kea/.dhcpd/.windhcp: the backends are
      dhcpctl.backends.DnsmasqBackend, KeaBackend, DHCPDBackend and
      WinDHCPBackend; to override one, register the subclass with
      replace=True.
    • netboot.dhcp.options, .conditions and .completion: GENERIC_OPTIONS,
      APPLY_TIME, DhcpOptions, split_query, DhcpCondition, MATCH_KEYS,
      DhcpCompletion and BOOT_OPTIONS are dhcpctl's (DHCPOptions,
      Condition, Completion, ...); build_options, build_conditions,
      build_completion and zone_defaults are in netboot.dhcp.
      ConditionMissing / ConditionUnsupported are
      dhcpctl.ConditionMissingError / ConditionUnsupportedError.
    • windhcp.PASSWORD_ENV_VAR / USER_ENV_VAR are
      netboot.dhcp.WINDHCP_PASSWORD_ENV_VAR / WINDHCP_USER_ENV_VAR, and
      dhcpd.KEY_ENV_VAR is DHCPD_OMAPI_KEY_ENV_VAR; the variables are the
      same PIXIE_* ones, now read only when the URI has no user or password
      of its own: windhcp://admin:pw@host and dhcpd://keyname:key@host work.
      Logs, errors and pixie dhcp-config show a server's URI with the password
      masked.
    • netboot.DhcpServer is no longer exported.
      The kea, dhcpd, winrm and ssh extras install dhcpctl's of the same
      name. An unknown scheme, or a URI a backend cannot use, is a
      PixieConfigError; a server refusing is a dhcpctl.ServerError, reported by
      the CLI as one line with status 2. PixieEvent.BuildDhcpOptions now 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.options section: an option of its own with a code and a pydhcp type,
    or an alias of a name or a code; dhcp_options, dhcp_when and
    dhcp_complete all read them. Each value is read by the
    option's type when the config is applied, so router: gateway is a
    PixieConfigError naming 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 a BuildDhcpOptions hook: it
    receives pydhcp's DHCPOptions, keyed by code,
    so write options[DHCPOptionCode.ROUTER] = ... or options[3] where it
    wrote options["router"], or return a mapping of option names.
    next-server is no longer an option a hook sees: it is the reservation's
    BOOTP field, still set from dhcp_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= and raw.<backend>= text are
    unknown-key errors. What to do: move options to the zone's settings or an
    image's or target's dhcp_options; replace an options_builder with a hook
    on PixieEvent.BuildDhcpOptions, which receives the same options plus the
    target and the server; for the iPXE chainload use dhcp_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 relative templates: root is joined to the directory of the config
    file, as !include is, and with no templates: the root is templates
    beside the config file (./config/templates by default). The working
    directory's ./templates is no longer prepended. What to do: a config that
    relied on ./templates names it, relative to the config file (the example
    config now says templates: ../templates).
  • The path layer is pathapi and uripath, not pathlib-next. netboot
    depends on pathapi>=0.1.0,<0.2 and pathapi-uri>=0.1.0,<0.2 (import
    package uripath) and no longer installs pathlib-next or uritools. The
    paths netboot returns are their classes: Repository.service(), .get()
    and .local are uripath.URIPath (were pathlib_next.uri.UriPath),
    parse_path() returns pathapi.LocalPath or URIPath, and a template's
    Path and Uri globals are pathapi.Path and uripath.URIPath. What to
    do: import them from there in a command module, hook or template that used
    pathlib_next...
Read more

v0.3.2

Choose a tag to compare

@github-actions github-actions released this 03 Oct 09:16

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, a dhcpd.conf group, a Kea client-classes entry.
    Printing contacts nothing, so the output goes straight into a change ticket;
    --apply runs it and --as prompts for an admin password. It is the only
    command that prompts, because initiate and complete run unattended and a
    prompt there would hang a pipeline. Run once per condition; routine arming then
    needs none of those rights.

  • windhcp:// takes transport=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_USER when
    the URI names none.

  • dhcp_complete: — what a finished target's DHCP entry becomes. A machine
    that still receives boot-file-name after its install boots the installer
    again; pixie complete removed the reservation to prevent that, which also
    threw away the fixed address. Now keep: true leaves the address and drops the
    boot options (firmware falls through to local disk), and options: serves
    something else instead — an iPXE sanboot script, for firmware with no
    local-disk fallback. Router, DNS and subnet mask are never touched, and a kept
    entry also loses its dhcp_when membership 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 inline if, 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. match takes
    user-class and vendor-class; options takes the usual option names. This
    is the iPXE chainload said once: undionly.kpxe for a PXE ROM,
    boot.ipxe for 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=shared gives 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, a dhcpd.conf group, the
    Kea client-classes JSON), and one that cannot express conditions at all
    (dnsmasq://, because the tag needs a dhcp-match line 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 raises PixieConfigError
      naming the repo instead of being treated as a hostname and handed to DNS as
      one. [v6]:port still produces http://[2001:db8::1]:8080/... — the host
      comes back unbracketed and pathlib_next brackets it itself.
    • "the target has no address" is is_wildcard, which also covers None
      and a %zone suffix (::%eth0); the check it replaced knew 0.0.0.0 and
      :: only, so a scoped unspecified address produced a template candidate
      named after it.
    • the windhcp:// netsh client id is MACAddress.hex() rather than
      formatting with separators and stripping them again.
  • netboot.utils.net.Host is now netimps' Host, subclassed rather than
    reimplemented. The local class claimed Host was "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) and Host() 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=True retries). 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 now True with matching hashes, where the old class
    returned NotImplemented; and faking resolution in a test means patching
    netimps.get_ip, which .ip() imports at call time, rather than netboot's
    own resolve re-export.

Full Changelog: v0.3.1...v0.3.2

v0.3.1

Choose a tag to compare

@github-actions github-actions released this 02 Oct 15:32

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 -Value takes String[]: Windows answered
    "Parameters for option value to be set for option ID 6 do not match with option
    definition" and, under ErrorActionPreference = '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 configured raw.<backend>
    fragments, and a subclass overrides it to decide per target. All four shipped
    backends consume it, and raw.<backend>.remove= is new: a fragment that
    belongs to teardown rather than to arming. For windhcp:// a fragment may be a
    PowerShell line or a {"Args": [...], "Ignore": bool} netsh command, which
    works under either method.
  • method=netsh verifies 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-servers option 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 with netsh ... 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:// takes method=powershell|netsh. The default is unchanged (the
    DhcpServer cmdlets); method=netsh drives netsh 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

Choose a tag to compare

@github-actions github-actions released this 02 Oct 13:59

Changed

  • The shell template engine now claims the .shtpl suffix only (it claimed
    every suffix before, as the documented fallback). The suffix picks the engine
    and the stem names the artifact, which is the convention install.ks.j2
    already used: rename boot.cfg to boot.cfg.shtpl and ctx.render("boot.cfg")
    keeps working unchanged. A file no engine claims is copied as is by the new
    CopyTemplate, last in the default template_types — so a static grub.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.
    Narrowing template_types so 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]), and ERBTemplate (.erb) / EppTemplate (.epp),
    which render through the system ruby and puppet epp render so an existing
    template behaves as its author tested it (PIXIE_RUBY / PIXIE_PUPPET name
    the program if it is not on PATH). 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 with templates_undefined differs 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.
  • ShellTemplate is configured by subclassing, through three class attributes:
    EXT (a string or a sequence, dots optional, case-insensitive; None or
    "*" claims any suffix), DELIMITER (default "%") and PATTERN —
    "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_undefined says. :=, :? 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() returns bytes for such a file. EXT narrows it to chosen
    suffixes; Template.BINARY is the flag any engine can set to be handed raw
    bytes. A non-UTF-8 file claimed by a text engine now raises
    TemplateEngineError naming the file, where it used to fail inside the read.
  • Loader.find_source() — the byte-returning counterpart of get_source()
    (which keeps jinja2's text contract).
  • A template-engine registry: register_template_type() (bare or as a
    decorator, with an optional priority=), unregister_template_type(),
    TEMPLATE_TYPES and by_priority(). Loader(template_types=None) — the new
    default — uses every registered engine, so a --load-module plugin 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 and FALLBACK_PRIORITY (-100) for CopyTemplate. 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 explicit template_types list still means what it says.
  • Template.EXT and JinjaTemplate.EXT, so suffix ownership is one declarative
    attribute per engine, plus netboot.templates.template_extensions() to read it
    back normalised.

Full Changelog: v0.2.4...v0.3.0

v0.2.4

Choose a tag to compare

@github-actions github-actions released this 28 Sep 11:45

Changed

  • duho floor 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, the PATHSEP env 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

Choose a tag to compare

@github-actions github-actions released this 23 Sep 15:09

Fixed

  • One DHCP server being offline no longer fails pixie initiate or
    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. complete logs 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

Choose a tag to compare

@github-actions github-actions released this 21 Sep 14:19

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.4 and
    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.
  • MACAddress no longer equals a str (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, and aa.bb.cc.dd.ee.ff is 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

Choose a tag to compare

@github-actions github-actions released this 17 Sep 15:17

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 address of the form host:port no longer sends the whole
    string to DNS. The port is split off first, so mirror.example:8080
    resolves mirror.example — the old lookup could only fail, and did so at
    different speeds on different machines.

Added

  • netboot.dhcp is a package, and DHCP backends carry client options. A
    dhcpservers entry 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.options models them:
    GENERIC_OPTIONS for the names netboot translates, option-<n> for anything
    else, raw.<backend>= for untranslated backend-native text, and
    options_builder=my.mod.fn plus the new PixieEvent.BuildDhcpOptions for
    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-boot entries through pathlib_next paths, so
    the same configuration serves a local dnsmasq or one reached over sftp://
    (netboot[ssh]). hostsfile/optsfile may be a directory (one file per
    target — preferred, and with --dhcp-hostsdir/--dhcp-optsdir dnsmasq 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 sends reservation-add
    and reservation-del to the control agent, mapping boot-file-name,
    next-server and host-name onto reservation fields and everything else
    onto option-data. The subnet-id comes from the zone (subnet_id:) or is
    discovered once by matching the zone's network against config-get; when
    neither works netboot refuses rather than guessing, because a reservation in
    the wrong subnet silently never matches. A result: 2 from 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. Needs netboot[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's statements, 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 from keyfile= or
    $PIXIE_DHCPD_OMAPI_KEY, never from the URI. Needs netboot[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 the DhcpServer
    PowerShell module over ssh (the default, no dependency — it uses the system
    ssh client, so your config, keys, agent and jump hosts apply) or WinRM
    (netboot[winrm]). The transport host and the DHCP server are separate:
    server= becomes -ComputerName when 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], and netboot[ssh] for dnsmasq over
    sftp://. A local dnsmasq and windhcp:// over ssh need none. Verified on a
    clean wheel install: importing netboot.dhcp loads no backend dependency,
    those two backends construct, and the other two raise ImportError naming
    their extra.
  • Anything that varies per target is refused in a connection string and resolved
    when the target is applied instead: subnet_id/scope belong to the zone,
    boot-file-name/next-server/tftp-server-name to 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

Choose a tag to compare

@github-actions github-actions released this 17 Sep 14:21

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, PixieLookupError and PixieConfigError. Netboot's deliberate
    failures are now distinguishable from bugs: lookups that fail or are
    ambiguous raise PixieLookupError (still a LookupError) 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/https repository services reach the network
    through requests, which nothing installed: pathlib_next[uri] does not
    depend on it, so Repository.service("http") failed with
    ModuleNotFoundError: No module named 'requests' on any install that did not
    happen to have it. Install netboot[http] for http-served repos; file and
    tftp repos, and rendering, need nothing extra. Without the extra those calls
    now raise ImportError naming 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 .j2 file 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 key templates_undefined decides, and means the same in both engines:
    strict (default) raises naming the variable, lenient renders an empty
    string with a warning — the old Jinja behaviour — and debug leaves the
    placeholder in the output. Set templates_undefined: lenient to keep
    templates that relied on blanks working.

Security

  • Config loading is hardened: the loader runs with allow_commands=False and
    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 !include from
    inventory exports rather than written by hand. Ordinary interpolation is
    unchanged; set YACONFIGLIB_CONFINE_TO to also restrict where !include may
    read from.
  • shell_quote now 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 raises ValueError. It also returns a str for a str
    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, %end or date +%Y failed
    to render at all. Only the documented braced form %{NAME} is substituted
    now; a bare %word is 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 StartPixieInit hook'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 its templates took effect — the config ended up half applied.
    A hook that returns None for StartPixieInit or NewPixieObject now raises
    PixieConfigError naming the contract instead of failing obscurely later.
  • A template name containing .. is refused. Names are resolved against every
    search root, so ../../etc/passwd reached outside them.
  • Arming DHCP is all or nothing. pxe_init stopped 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_complete takes 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.complete no 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, repos or resources no 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
    and 10.0.0.5/24 both configured, a host in the /24 now gets the /24.
  • A None config entry (targets: {host1:} — valid YAML meaning "all
    defaults") no longer crashes with AttributeError, and an entry that is
    neither a mapping nor an object raises PixieConfigError naming it.
  • lookup_image treats any falsy match result as "no match": a match
    override returning None used to crash the sort.
  • DhcpZone no 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 into None, and get_local_server accepts the strings
    config actually holds rather than raising on them. Two plugins claiming one
    URI scheme now log a warning naming both.
  • A Pixie subclass's annotated attribute keeps its class default when the
    config does not mention it (it was overwritten with None), 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 _id retry when building a config object only catches "this class takes
    no _id"; any other TypeError from the value class propagates instead of
    being hidden behind a second construction attempt.
  • Pixie.globals is 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_dhcpzone returns None instead of raising AttributeError
    when the target has no usable IP — the documented MAC-keyed target shape hit
    this on every initiate that did not name a dhcpzone.
  • Repository URLs are built correctly from an address that carries a port:
    mirror.example:8080 became 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
    hostless http:/path.
  • An https service 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 at module.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_ip beside target.ip, or domain beside DOMAIN) 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_path is resolved inside the template roots
    (config["templates"], ./templates by default) rather than against the
    process's working directory, so a config means the same thing whichever
    directory pixie runs from. An image's or target's template_path still
    extends the roots, which are searched last; absolute paths and URIs are used
    as given. A config that wrote template_path: [templates/debian] to reach
    ./templates/debian should 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 in templates or in an image's template_path stays a
    URI instead of being turned into a directory named http: under the working
    directory, so URI search paths are searched at all. A repository's local
    path accepts a Windows drive path (C:\\tftp), which was previously read as
    URI scheme c and then failed on every read.
  • An edited shell template is picked up again: the loader assigned its freshness
    check function to is_up_to_date, and a function object is always truthy,
    so a cached template was neve...
Read more