Skip to content

API reference

Connecting

dhcpctl.connect(uri, *, secrets=None, owner=DEFAULT_OWNER, codes=None)

The backend for uri's scheme, built from uri. Does no I/O.

secrets supplies what the URI must not carry (see DHCPServer.secret); owner names the entries this server writes (see DHCPServer.owner); codes is the codemap its option names are read with (an OptionCodes from option_codes, or a mapping of declarations).

:raises UnknownBackendError: no backend serves the scheme. :raises ConfigError: the URI cannot be used by that backend.

Source code in src/dhcpctl/_registry.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
def connect(
    uri: str,
    *,
    secrets: Secrets = None,
    owner: str = DEFAULT_OWNER,
    codes: Any = None,
) -> DHCPServer:
    """The backend for `uri`'s scheme, built from `uri`. Does no I/O.

    `secrets` supplies what the URI must not carry (see `DHCPServer.secret`);
    `owner` names the entries this server writes (see `DHCPServer.owner`);
    `codes` is the codemap its option names are read with (an `OptionCodes`
    from `option_codes`, or a mapping of declarations).

    :raises UnknownBackendError: no backend serves the scheme.
    :raises ConfigError: the URI cannot be used by that backend.
    """
    scheme = urlsplit(uri).scheme
    if not scheme:
        raise ConfigError(f"{uri!r} has no scheme, so no backend can be chosen")
    return get_backend(scheme)(uri, secrets=secrets, owner=owner, codes=codes)

dhcpctl.register_backend(scheme, backend, *, replace=False)

Serve scheme with backend (a class, or "module:Class" to import lazily).

:raises ValueError: scheme is already served by another backend and replace is false.

Source code in src/dhcpctl/_registry.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
def register_backend(
    scheme: str, backend: Union[str, Type[DHCPServer]], *, replace: bool = False
) -> None:
    """Serve `scheme` with `backend` (a class, or `"module:Class"` to import lazily).

    :raises ValueError: `scheme` is already served by another backend and
        `replace` is false.
    """
    scheme = scheme.lower()
    current = _REGISTRY.get(scheme)
    if current is not None and current != backend and not replace:
        raise ValueError(
            f"scheme {scheme!r} is already served by {_label(current)}; pass "
            f"replace=True to serve it with {_label(backend)} instead"
        )
    _REGISTRY[scheme] = backend

dhcpctl.unregister_backend(scheme)

Stop serving scheme; True if it was registered.

Source code in src/dhcpctl/_registry.py
59
60
61
def unregister_backend(scheme: str) -> bool:
    """Stop serving `scheme`; True if it was registered."""
    return _REGISTRY.pop(scheme.lower(), None) is not None

dhcpctl.backend_names()

Every registered scheme, sorted.

Source code in src/dhcpctl/_registry.py
64
65
66
67
def backend_names() -> Tuple[str, ...]:
    """Every registered scheme, sorted."""
    _load_entry_points()
    return tuple(sorted(_REGISTRY))

dhcpctl.get_backend(scheme)

The backend class serving scheme, imported if it was registered by name.

:raises UnknownBackendError: no backend serves scheme.

Source code in src/dhcpctl/_registry.py
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
def get_backend(scheme: str) -> Type[DHCPServer]:
    """The backend class serving `scheme`, imported if it was registered by name.

    :raises UnknownBackendError: no backend serves `scheme`.
    """
    scheme = scheme.lower()
    backend = _REGISTRY.get(scheme)
    if backend is None:
        _load_entry_points()
        backend = _REGISTRY.get(scheme)
    if backend is None:
        raise UnknownBackendError(
            f"no dhcpctl backend serves scheme {scheme!r}; registered: "
            f"{', '.join(sorted(_REGISTRY)) or 'none'}. Register one with "
            f"dhcpctl.register_backend or the {ENTRY_POINT_GROUP!r} entry point"
        )
    if isinstance(backend, str):
        module, _, name = backend.partition(":")
        backend = getattr(importlib.import_module(module), name)
        _REGISTRY[scheme] = backend
    return backend

The contract

dhcpctl.DHCPServer(uri, *, secrets=None, owner=DEFAULT_OWNER, codes=None)

A DHCP server reached through one backend, described by a URI.

Build one with dhcpctl.connect(uri), which picks the backend by the URI's scheme. Constructing a backend validates its URI and does no I/O.

A backend overrides add and remove, and keep, ensure_condition, condition_recipe and remove_condition_member where it supports them. complete and extras are the same for every backend.

Source code in src/dhcpctl/_server.py
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
def __init__(
    self,
    uri: str,
    *,
    secrets: Secrets = None,
    owner: str = DEFAULT_OWNER,
    codes: Any = None,
) -> None:
    self.uri = uri
    base = option_codes(self.OPTION_NAMES) if self.OPTION_NAMES else PYDHCP_CODES
    #: The codemap this server reads option names with: the caller's
    #: declarations over the backend's own spellings over pydhcp's.
    self.codes = OptionCodes.of(codes).over(base)
    parts = urlsplit(uri)
    #: The URI's scheme.
    self.scheme = parts.scheme.lower()
    #: The backend's own query keys: a URI says only how to reach the server.
    self.settings: Dict[str, str] = {}
    for key, value in parse_qsl(parts.query, keep_blank_values=True):
        if key not in self.SETTINGS:
            raise ConfigError(
                f"unknown key {key!r} in a {self.scheme} connection: its query "
                f"takes only how to reach the server "
                f"({', '.join(sorted(self.SETTINGS)) or 'nothing'}); options "
                f"belong to the reservation"
            )
        self.settings[key] = value
    if not owner or set(owner) - NAME_CHARS:
        raise ConfigError(
            f"owner {owner!r} may only contain letters, digits, '-' and '_'"
        )
    #: Names what this server writes on the DHCP server: the dnsmasq region
    #: markers, Windows policy names, recipe comments. Entries written under
    #: one owner are not found under another.
    self.owner = owner
    self._secrets = secrets
    self._uri_secrets = {
        key: unquote(value)
        for part, key in self.URI_SECRETS.items()
        for value in (getattr(parts, part),)
        if value
    }

redacted_uri property

uri with any password replaced by ***: what messages and logs show.

add(reservation)

Arm the server for this reservation.

Source code in src/dhcpctl/_server.py
145
146
def add(self, reservation: Reservation) -> None:
    """Arm the server for this reservation."""

complete(reservation)

Disarm, honouring reservation.completion.

remove unless the entry is kept; when it is, the client first leaves every condition (a kept entry still matching one would keep being served the installer) and then keep replaces its boot options.

Source code in src/dhcpctl/_server.py
189
190
191
192
193
194
195
196
197
198
199
200
def complete(self, reservation: Reservation) -> None:
    """Disarm, honouring `reservation.completion`.

    `remove` unless the entry is kept; when it is, the client first leaves
    every condition (a kept entry still matching one would keep being served
    the installer) and then `keep` replaces its boot options.
    """
    if not reservation.completion.keep:
        return self.remove(reservation)
    for condition in reservation.conditions.values():
        self.remove_condition_member(reservation, condition)
    return self.keep(reservation)

condition_recipe(reservation, condition)

What a privileged operator applies for condition, as text, not run.

The same text ConditionMissingError carries.

Source code in src/dhcpctl/_server.py
175
176
177
178
179
180
def condition_recipe(self, reservation: Reservation, condition: Condition) -> str:
    """What a privileged operator applies for `condition`, as text, not run.

    The same text `ConditionMissingError` carries.
    """
    return ""

ensure_condition(reservation, condition)

The name of the construct serving condition, creating it if needed.

The ladder every backend walks: (1) it exists -- use it; (2) it is absent and can be created -- create it; (3) it cannot be created -- ConditionMissingError carrying condition_recipe(); (4) this backend has no such concept -- ConditionUnsupportedError, the base behaviour.

Source code in src/dhcpctl/_server.py
162
163
164
165
166
167
168
169
170
171
172
173
def ensure_condition(self, reservation: Reservation, condition: Condition) -> str:
    """The name of the construct serving `condition`, creating it if needed.

    The ladder every backend walks: (1) it exists -- use it; (2) it is
    absent and can be created -- create it; (3) it cannot be created --
    `ConditionMissingError` carrying `condition_recipe()`; (4) this backend
    has no such concept -- `ConditionUnsupportedError`, the base behaviour.
    """
    raise ConditionUnsupportedError(
        f"{type(self).__name__} cannot serve conditional options, so "
        f"dhcp_when.{condition.name} cannot be applied on {self}"
    )

extras(reservation, phase='add')

Backend-native fragments to apply with the reservation, verbatim.

reservation.extras[phase]. A subclass may override this to decide fragments per client (reservation.context is the caller's), calling super() to keep these. What a fragment is belongs to the backend.

Source code in src/dhcpctl/_server.py
245
246
247
248
249
250
251
252
253
254
def extras(self, reservation: Reservation, phase: str = "add") -> List[Any]:
    """Backend-native fragments to apply with the reservation, verbatim.

    `reservation.extras[phase]`. A subclass may override this to decide
    fragments per client (`reservation.context` is the caller's), calling
    `super()` to keep these. What a fragment is belongs to the backend.
    """
    if phase not in PHASES:
        raise ValueError(f"phase must be one of {PHASES}, not {phase!r}")
    return list(reservation.extras.get(phase, ()))

keep(reservation)

Keep the entry with reservation.completion's options instead.

The base refuses, so a backend that cannot keep an entry is reported rather than silently ignoring the request.

Source code in src/dhcpctl/_server.py
151
152
153
154
155
156
157
158
159
160
def keep(self, reservation: Reservation) -> None:
    """Keep the entry with `reservation.completion`'s options instead.

    The base refuses, so a backend that cannot keep an entry is reported
    rather than silently ignoring the request.
    """
    raise NotImplementedError(
        f"{type(self).__name__} cannot keep a reservation on completion, so "
        f"completion.keep cannot be honoured on {self}"
    )

remove(reservation)

Remove this reservation; removing one that is not there succeeds.

Source code in src/dhcpctl/_server.py
148
149
def remove(self, reservation: Reservation) -> None:
    """Remove this reservation; removing one that is not there succeeds."""

remove_condition_member(reservation, condition)

Detach this client from condition, leaving the construct for others.

Source code in src/dhcpctl/_server.py
182
183
184
185
def remove_condition_member(
    self, reservation: Reservation, condition: Condition
) -> None:
    """Detach this client from `condition`, leaving the construct for others."""

render(reservation, action='add')

What action would send this server for reservation, contacting nothing: the backend's own text, one step after another.

action is add, remove, keep or complete. A step that needs an answer from the server is given the answer "there is nothing yet", so the text is what a server holding nothing for this client would get.

:raises NotImplementedError: the backend cannot record what it sends. :raises ValueError: an unknown action.

Source code in src/dhcpctl/_server.py
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
def render(self, reservation: Reservation, action: str = "add") -> str:
    """What `action` would send this server for `reservation`, contacting
    nothing: the backend's own text, one step after another.

    `action` is `add`, `remove`, `keep` or `complete`. A step that needs an
    answer from the server is given the answer "there is nothing yet", so
    the text is what a server holding nothing for this client would get.

    :raises NotImplementedError: the backend cannot record what it sends.
    :raises ValueError: an unknown `action`.
    """
    if action not in self.RENDERED:
        raise ValueError(f"action must be one of {self.RENDERED}, not {action!r}")
    sent: List[str] = []
    with self._recording(sent):
        getattr(self, action)(reservation)
    return "\n".join(sent) + ("\n" if sent else "")

secret(key)

The secret named key: the caller's if it gives one, else the URI's.

The caller's secrets override the URI's, so one URI serves several logins; a caller's None leaves the URI's in place, and "" is an empty secret. They are asked when the value is needed, not at construction. None when neither has it.

Source code in src/dhcpctl/_server.py
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
def secret(self, key: str) -> Optional[str]:
    """The secret named `key`: the caller's if it gives one, else the URI's.

    The caller's `secrets` override the URI's, so one URI serves several
    logins; a caller's None leaves the URI's in place, and `""` is an empty
    secret. They are asked when the value is needed, not at construction.
    None when neither has it.
    """
    source = self._secrets
    given = None
    if callable(source):
        given = source(key)
    elif source is not None:
        given = source.get(key)
    if given is not None:
        return given
    return self._uri_secrets.get(key)

Reservations

dhcpctl.Reservation(target, scope=Scope(), lease=_no_address(), conditions=dict(), completion=Completion(), extras=dict(), context=None) dataclass

One client's reservation, as a backend applies it.

lease is what the server answers the client with: a pydhcp DHCPLease holding its address (0.0.0.0 for a MAC-only reservation), the fully merged options, and the BOOTP header fields siaddr (the next server), sname and file (the boot file). conditions are the named conditions that apply to it; completion what happens when it completes; extras per-phase backend-native fragments the backend emits verbatim; context is the caller's, carried untouched for a backend subclass that decides fragments per client.

ip_text property

The address as text, or "" when there is none (0.0.0.0).

from_mapping(data, *, codes=None) classmethod

A reservation from a mapping in the shape to_mapping writes.

Only target.name is required. lease is read by DHCPLease.from_mapping with the codes codemap (pydhcp's), its address optional; conditions and completion as parse_conditions and parse_completion read them.

:raises ConfigError: an unknown key, or a field that cannot be read.

Source code in src/dhcpctl/_model.py
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
@classmethod
def from_mapping(
    cls,
    data: Mapping[str, Any],
    *,
    codes: "Optional[type[OptionCodes]]" = None,
) -> "Reservation":
    """A reservation from a mapping in the shape `to_mapping` writes.

    Only `target.name` is required. `lease` is read by
    `DHCPLease.from_mapping` with the `codes` codemap (pydhcp's), its
    address optional; `conditions` and `completion` as `parse_conditions`
    and `parse_completion` read them.

    :raises ConfigError: an unknown key, or a field that cannot be read.
    """
    codes = codes or PYDHCP_CODES
    _known(
        "reservation",
        data,
        ("target", "scope", "lease", "conditions", "completion", "extras"),
    )
    target = _known("target", data.get("target"), ("name", "mac", "hostname"))
    if not target.get("name"):
        raise ConfigError("reservation target needs a `name`")
    name = str(target["name"])
    scope = _known(
        "scope",
        data.get("scope") or {},
        ("name", "network", "subnet-id", "scope-id"),
    )
    try:
        mac = target.get("mac")
        network = scope.get("network")
        built_target = Target(
            name,
            mac=netimps.parse(mac, netimps.MACAddress) if mac else None,
            hostname=str(target.get("hostname") or ""),
        )
        built_scope = Scope(
            name=str(scope.get("name") or ""),
            network=netimps.parse(network, netimps.IPNetwork) if network else None,
            subnet_id=scope.get("subnet-id"),
            scope_id=scope.get("scope-id"),
        )
    except ValueError as exc:
        raise ConfigError(f"reservation {name!r}: {exc}") from exc
    where = f"reservation {name}"
    extras = _known("extras", data.get("extras") or {}, PHASES)
    for phase, items in extras.items():
        if not isinstance(items, list):
            raise ConfigError(
                f"{where}: extras.{phase} must be a list, got {type(items).__name__}"
            )
    return cls(
        built_target,
        built_scope,
        codes.read_lease(data.get("lease"), where),
        conditions=parse_conditions(data.get("conditions"), where, codes),
        completion=parse_completion(data.get("completion"), where, codes)
        or Completion(),
        extras={phase: list(items) for phase, items in extras.items()},
    )

from_text(text, format, *, codes=None) classmethod

pydhcp.packet.structured.loads then from_mapping, with their errors.

Source code in src/dhcpctl/_model.py
204
205
206
207
208
209
210
211
212
213
@classmethod
def from_text(
    cls,
    text: str,
    format: str,
    *,
    codes: "Optional[type[OptionCodes]]" = None,
) -> "Reservation":
    """`pydhcp.packet.structured.loads` then `from_mapping`, with their errors."""
    return cls.from_mapping(structured.loads(text, format), codes=codes)

require_ip(what)

The address, or ConfigError saying what needs one.

Source code in src/dhcpctl/_model.py
92
93
94
95
96
97
98
def require_ip(self, what: str) -> ipaddress.IPv4Address:
    """The address, or `ConfigError` saying `what` needs one."""
    if self.lease.ip.is_unspecified:
        raise ConfigError(
            f"target {self.target.name!r} has no ip address, and {what}"
        )
    return self.lease.ip

to_mapping()

The reservation as plain data, the form from_mapping reads.

target, scope, lease (DHCPLease.to_mapping's form), and conditions, completion and extras when there are any. An unset field is left out. context is the caller's and is not written.

Source code in src/dhcpctl/_model.py
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
def to_mapping(self) -> Dict[str, Any]:
    """The reservation as plain data, the form `from_mapping` reads.

    `target`, `scope`, `lease` (`DHCPLease.to_mapping`'s form), and
    `conditions`, `completion` and `extras` when there are any. An unset
    field is left out. `context` is the caller's and is not written.
    """
    mapping: Dict[str, Any] = {
        "target": _drop_unset(
            name=self.target.name,
            mac=str(self.target.mac) if self.target.mac is not None else None,
            hostname=self.target.hostname,
        )
    }
    scope = _drop_unset(
        name=self.scope.name,
        network=str(self.scope.network) if self.scope.network is not None else None,
        **{"subnet-id": self.scope.subnet_id, "scope-id": self.scope.scope_id},
    )
    if scope:
        mapping["scope"] = scope
    mapping["lease"] = self.lease.to_mapping()
    if self.conditions:
        mapping["conditions"] = {
            name: condition.to_mapping()
            for name, condition in self.conditions.items()
        }
    if self.completion.keep:
        mapping["completion"] = self.completion.to_mapping()
    if self.extras:
        mapping["extras"] = {
            phase: list(items) for phase, items in self.extras.items()
        }
    return mapping

to_text(format)

to_mapping as a "json", "yaml", "toml" or "ini" document, written by pydhcp.packet.structured.dumps, with its errors.

Source code in src/dhcpctl/_model.py
199
200
201
202
def to_text(self, format: str) -> str:
    """`to_mapping` as a `"json"`, `"yaml"`, `"toml"` or `"ini"` document,
    written by `pydhcp.packet.structured.dumps`, with its errors."""
    return structured.dumps(self.to_mapping(), format)

dhcpctl.Target(name, mac=None, hostname='') dataclass

The client a reservation is for.

name labels it on the server (the dhcpd host name, the Windows reservation name, the dnsmasq tag) and in messages. Its address is the lease's.

require_mac(what)

The MAC, or MissingMACError saying what is identified by one.

Source code in src/dhcpctl/_model.py
37
38
39
40
41
42
43
def require_mac(self, what: str) -> MACAddress:
    """The MAC, or `MissingMACError` saying `what` is identified by one."""
    if self.mac is None or str(self.mac) in ("", NULL_MAC):
        raise MissingMACError(
            f"target {self.name!r} has no MAC address, and {what}"
        )
    return self.mac

dhcpctl.Scope(name='', network=None, subnet_id=None, scope_id=None) dataclass

Where the reservation lives on the server.

network identifies a Windows scope and a Kea subnet; subnet_id and scope_id override what is derived from it. name labels it in messages and keys per-scope caches.

What is served

dhcpctl.DHCPLease, dhcpctl.DHCPOptions and dhcpctl.DHCPOptionCode are pydhcp's, re-exported: see pydhcp's reference.

dhcpctl.OptionCodes

Bases: OptionCodeEnum

A codemap: pydhcp's option codes and names, and what a config declares.

Build one with option_codes; PYDHCP_CODES is pydhcp's names alone. As a pydhcp OptionCodeEnum, each codemap has its own codecs and its own unnamed members, so a declared type never reaches pydhcp's registry. Names are read by pydhcp's resolve.

field(name) classmethod

The lease header field name names (next-server is siaddr), or None.

Source code in src/dhcpctl/_options.py
104
105
106
107
108
109
@classmethod
def field(cls, name: Any) -> Optional[str]:
    """The lease header field `name` names (`next-server` is `siaddr`), or None."""
    if not isinstance(name, str):
        return None
    return cls._fields.get(_member(name))

knows(name) classmethod

Does name name an option or a header field of this codemap?

Source code in src/dhcpctl/_options.py
111
112
113
114
115
116
117
118
119
120
@classmethod
def knows(cls, name: Any) -> bool:
    """Does `name` name an option or a header field of this codemap?"""
    if cls.field(name):
        return True
    try:
        cls.resolve(name)
    except (TypeError, ValueError):
        return False
    return True

lease(declared, where='') classmethod

What a config mapping serves: its options, and the header fields (next-server, file, ...) among them, as a lease with no address.

A DHCPLease is returned as it is.

:raises ConfigError: an unknown name, or a value its option or field refuses.

Source code in src/dhcpctl/_options.py
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
@classmethod
def lease(
    cls, declared: "Union[Mapping[Any, Any], DHCPLease, None]", where: str = ""
) -> DHCPLease:
    """What a config mapping serves: its options, and the header fields
    (`next-server`, `file`, ...) among them, as a lease with no address.

    A `DHCPLease` is returned as it is.

    :raises ConfigError: an unknown name, or a value its option or field
        refuses.
    """
    if isinstance(declared, DHCPLease):
        return declared
    fields: Dict[str, Any] = {}
    options: Dict[Any, Any] = {}
    for name, value in (declared or {}).items():
        field = cls.field(name)
        if field:
            if value not in (None, ""):
                fields[field] = value
        else:
            options[name] = value
    try:
        return DHCPLease(
            UNSPECIFIED, options=cls.options(options, where), strict=False, **fields
        )
    except (TypeError, ValueError) as exc:
        if isinstance(exc, ConfigError):
            raise
        raise ConfigError(f"{exc}{_on(where)}") from exc

of(codes) staticmethod

codes as a codemap: itself, a declarations mapping, or pydhcp's.

Source code in src/dhcpctl/_options.py
194
195
196
197
198
199
@staticmethod
def of(codes: Any) -> "type[OptionCodes]":
    """`codes` as a codemap: itself, a declarations mapping, or pydhcp's."""
    if isinstance(codes, type) and issubclass(codes, OptionCodes):
        return codes
    return option_codes(codes) if codes else PYDHCP_CODES

options(declared, where='') classmethod

A bag with this codemap from a mapping of option names to values.

pydhcp's DHCPOptions.from_mapping reads it: a value is what the codec takes, an enum its member name, octets {"hex": ...} or, for an opaque option, hex text. A DHCPOptions is copied, keeping its own codemap.

:raises ConfigError: an unknown name, or a value its option's codec refuses (pydhcp's message, and (on 'where')).

Source code in src/dhcpctl/_options.py
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
@classmethod
def options(
    cls, declared: "Union[Mapping[Any, Any], DHCPOptions, None]", where: str = ""
) -> DHCPOptions:
    """A bag with this codemap from a mapping of option names to values.

    pydhcp's `DHCPOptions.from_mapping` reads it: a value is what the codec
    takes, an enum its member name, octets `{"hex": ...}` or, for an
    opaque option, hex text. A `DHCPOptions` is copied, keeping its own
    codemap.

    :raises ConfigError: an unknown name, or a value its option's codec
        refuses (pydhcp's message, and ` (on 'where')`).
    """
    if isinstance(declared, DHCPOptions):
        return declared.copy()
    try:
        return DHCPOptions.from_mapping(declared or {}, codemap=cls)
    except (TypeError, ValueError) as exc:
        raise ConfigError(f"{exc}{_on(where)}") from exc

over(base) classmethod

This codemap's declarations, every layer of them, made over base.

Source code in src/dhcpctl/_options.py
186
187
188
189
190
191
192
@classmethod
def over(cls, base: "type[OptionCodes]") -> "type[OptionCodes]":
    """This codemap's declarations, every layer of them, made over `base`."""
    stacked = base
    for declared in cls._layers:
        stacked = option_codes(declared, base=stacked)
    return stacked

read_lease(data, where='') classmethod

A lease from what DHCPLease.to_mapping writes, its address optional.

:raises ConfigError: what pydhcp refuses, with its message.

Source code in src/dhcpctl/_options.py
175
176
177
178
179
180
181
182
183
184
@classmethod
def read_lease(cls, data: Any, where: str = "") -> DHCPLease:
    """A lease from what `DHCPLease.to_mapping` writes, its address optional.

    :raises ConfigError: what pydhcp refuses, with its message.
    """
    try:
        return DHCPLease.from_mapping(data or {}, codemap=cls, strict=False)
    except (TypeError, ValueError) as exc:
        raise ConfigError(f"{exc}{_on(where)}") from exc

dhcpctl.option_codes(declared=None, *, base=None)

A codemap: base's members (pydhcp's by default) and declared's.

declared maps a name to {code: <n>, type: <pydhcp codec name>} (an option of the caller's own; type applies to this codemap only), or to a code, another name or a header field (an alias). Every declaration is checked here.

:raises ConfigError: an unknown key or type, a code pydhcp refuses, a name that cannot be one (a header field, option-<n>, a number, not a Python identifier), a pydhcp name given another code, an alias of nothing, or a loop.

Source code in src/dhcpctl/_options.py
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
def option_codes(
    declared: "Optional[Mapping[str, Any]]" = None,
    *,
    base: "Optional[type[OptionCodes]]" = None,
) -> "type[OptionCodes]":
    """A codemap: `base`'s members (pydhcp's by default) and `declared`'s.

    `declared` maps a name to `{code: <n>, type: <pydhcp codec name>}` (an
    option of the caller's own; `type` applies to this codemap only), or to a
    code, another name or a header field (an alias). Every declaration is
    checked here.

    :raises ConfigError: an unknown key or type, a code pydhcp refuses, a name
        that cannot be one (a header field, `option-<n>`, a number, not a
        Python identifier), a pydhcp name given another code, an alias of
        nothing, or a loop.
    """
    source = DHCPOptionCode if base is None else base
    members = {name: int(member) for name, member in source.__members__.items()}
    fields: Dict[str, str] = (
        {_member(name): field for name, field in BOOT_FIELDS.items()}
        if base is None
        else dict(base._fields)
    )
    types: Dict[int, type] = {}
    entries: Dict[str, Any] = {}
    written: Dict[str, str] = {}
    for name, entry in (declared or {}).items():
        where = f"options.{name}"
        key = _member(name)
        if key in written:
            raise ConfigError(
                f"{where}: {name!r} and {written[key]!r} are one name, declared twice"
            )
        written[key] = str(name)
        if (
            _member(name) in {_member(field) for field in BOOT_FIELDS}
            or key.replace("OPTION_", "", 1).isdigit()
            or not key.isidentifier()
        ):
            raise ConfigError(f"{where}: {name!r} cannot be declared as a name")
        if isinstance(entry, Mapping):
            unknown = set(entry) - {"code", "type"}
            if unknown or "code" not in entry:
                raise ConfigError(
                    f"{where} takes `code` and an optional `type`"
                    + (f", not {', '.join(sorted(unknown))}" if unknown else "")
                )
            raw = entry["code"]
            if isinstance(raw, bool) or not isinstance(raw, (int, str)):
                raise ConfigError(f"{where}.code must be an option code, not {raw!r}")
            try:
                code = int(DHCPOptionCode.from_code(int(raw)))
            except ValueError as exc:
                raise ConfigError(f"{where}.code: {exc}") from exc
            if "type" in entry:
                types[code] = _codec(entry["type"], where)
            entries[key] = code
        elif isinstance(entry, (int, str)) and not isinstance(entry, bool):
            entries[key] = entry
        else:
            raise ConfigError(
                f"{where} must be a code, a name or {{code:, type:}}, got "
                f"{type(entry).__name__}"
            )

    def code_of(key: str, seen: Tuple[str, ...], shown: Any = "") -> Any:
        """The code `key` names, or the header field it names, as a str."""
        entry = entries.get(key)
        if entry is None:
            if key in fields:
                return fields[key]
            try:
                return int(source.resolve(shown or key))
            except ValueError:
                raise ConfigError(f"no option {shown or key!r} to alias") from None
        if isinstance(entry, int):
            return entry
        if key in seen:
            chain = [written.get(k, k) for k in seen + (key,)]
            raise ConfigError(f"option names {' -> '.join(chain)} loop")
        return code_of(_member(entry), seen + (key,), entry)

    resolved = {}
    for key in entries:
        name = written[key]
        try:
            code = code_of(key, ())
        except (ConfigError, ValueError) as exc:
            raise ConfigError(f"options.{name}: {exc}") from exc
        known = DHCPOptionCode.__members__.get(key)
        if known is not None and int(known) != code:
            raise ConfigError(
                f"options.{name}: {name!r} is pydhcp's name for option "
                f"{int(known)}, not {code}"
            )
        if isinstance(code, str):
            fields[key] = code
            members.pop(key, None)
        else:
            resolved[key] = code
            fields.pop(key, None)
    members.update(resolved)
    codemap = OptionCodes("OptionCodes", list(members.items()), module=__name__)
    for code in range(256):
        codemap(code).register_type(types.get(code) or source(code).get_type())
    codemap._fields = fields
    layers = () if base is None else base._layers
    codemap._layers = layers + ((dict(declared),) if declared else ())
    return codemap

dhcpctl.plain_items(options)

(code, value) with each value as plain data (codec.to_json()).

A helper for a backend that writes text: a list option a list of strings (["10.0.0.1"]), a number an int, an opaque option hex text.

Source code in src/dhcpctl/_options.py
332
333
334
335
336
337
338
def plain_items(options: DHCPOptions) -> List[Tuple[Any, Any]]:
    """`(code, value)` with each value as plain data (`codec.to_json()`).

    A helper for a backend that writes text: a list option a list of strings
    (`["10.0.0.1"]`), a number an `int`, an opaque option hex text.
    """
    return [(code, _plain(value)) for code, value in options.items()]

Conditions and completion

dhcpctl.Condition(name, match, lease, codes=None)

One named condition: a test, and what to serve when it matches.

name is the construct's name on every backend. match maps a key from MATCH_KEYS to the value the client must send; several keys mean all of them. lease is what is served: a DHCPLease with no address (its options, siaddr, sname, file), or a config mapping read with the codes codemap (codes.lease).

Source code in src/dhcpctl/_conditions.py
48
49
50
51
52
53
54
55
56
57
def __init__(
    self,
    name: str,
    match: "Mapping[str, Any]",
    lease: "Union[DHCPLease, Mapping[Any, Any]]",
    codes: "Optional[type[OptionCodes]]" = None,
) -> None:
    self.name = name
    self.match = dict(match)
    self.lease = (codes or PYDHCP_CODES).lease(lease)

option_codes property

The match as {option code: value}, for a backend that tests by code.

to_mapping()

{match: ..., lease: ...}, lease as DHCPLease.to_mapping writes it: the entry parse_conditions reads back.

Source code in src/dhcpctl/_conditions.py
73
74
75
76
def to_mapping(self) -> Dict[str, Any]:
    """`{match: ..., lease: ...}`, `lease` as `DHCPLease.to_mapping` writes
    it: the entry `parse_conditions` reads back."""
    return {"match": dict(self.match), "lease": self.lease.to_mapping()}

dhcpctl.parse_conditions(declared, where='', codes=None)

Build conditions from a {name: {match: ..., options: ...}} mapping, or {name: {match: ..., lease: ...}} as Condition.to_mapping writes it.

where names the declaring object in error messages; option names are read with the codes codemap (pydhcp's), a config's options by codes.lease.

:raises ConfigError: the mapping cannot be used as given.

Source code in src/dhcpctl/_conditions.py
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
def parse_conditions(
    declared: Any, where: str = "", codes: "Optional[type[OptionCodes]]" = None
) -> Dict[str, Condition]:
    """Build conditions from a `{name: {match: ..., options: ...}}` mapping, or
    `{name: {match: ..., lease: ...}}` as `Condition.to_mapping` writes it.

    `where` names the declaring object in error messages; option names are read
    with the `codes` codemap (pydhcp's), a config's `options` by `codes.lease`.

    :raises ConfigError: the mapping cannot be used as given.
    """
    if not declared:
        return {}
    if not isinstance(declared, dict):
        raise ConfigError(
            f"dhcp_when on {where!r} must be a mapping of name -> condition, got "
            f"{type(declared).__name__}; each key is the policy/group/class name"
        )

    conditions: Dict[str, Condition] = {}
    for name, entry in declared.items():
        name = str(name)
        bad = set(name) - NAME_CHARS
        if not name or bad:
            raise ConfigError(
                f"dhcp_when name {name!r} on {where!r} may only contain letters, "
                f"digits, '-' and '_'"
                + (f" (found {''.join(sorted(bad))!r})" if bad else "")
            )
        if not isinstance(entry, dict):
            raise ConfigError(
                f"dhcp_when.{name} on {where!r} must be a mapping with `match` "
                f"and `options`, got {type(entry).__name__}"
            )
        unknown = set(entry) - {"match", "options", "lease"}
        if unknown or {"options", "lease"} <= set(entry):
            unknown = unknown or {"options", "lease"}
            raise ConfigError(
                f"dhcp_when.{name} on {where!r} has unknown key(s) "
                f"{', '.join(sorted(unknown))}; it takes `match` and `options` (or `lease`)"
            )
        match = entry.get("match") or {}
        if not isinstance(match, dict) or not match:
            raise ConfigError(
                f"dhcp_when.{name} on {where!r} needs a non-empty `match` "
                f"mapping; accepted keys: {', '.join(sorted(MATCH_KEYS))}"
            )
        for key in match:
            if key not in MATCH_KEYS:
                raise ConfigError(
                    f"dhcp_when.{name} on {where!r} cannot match on {key!r}; "
                    f"every backend must be able to test it, so the accepted "
                    f"keys are {', '.join(sorted(MATCH_KEYS))}"
                )
        try:
            if "lease" in entry:
                lease = (codes or PYDHCP_CODES).read_lease(entry["lease"])
            else:
                options = entry.get("options") or {}
                if not isinstance(options, dict):
                    raise ConfigError(
                        f"`options` must be a mapping, got {type(options).__name__}"
                    )
                lease = (codes or PYDHCP_CODES).lease(options)
        except ConfigError as exc:
            raise ConfigError(f"dhcp_when.{name} on {where!r}: {exc}") from exc
        if not serves(lease):
            raise ConfigError(
                f"dhcp_when.{name} on {where!r} needs a non-empty `options` "
                f"mapping -- a condition that serves nothing has no effect"
            )
        conditions[name] = Condition(name, match, lease)
    return conditions

dhcpctl.Completion(keep=False, lease=None, codes=None)

What to do with a reservation when provisioning finishes.

keep=False (the default) removes it. keep=True keeps it, with its boot fields and options replaced by lease's (none: the client falls through to its disk). lease is a DHCPLease, or a config mapping read with the codes codemap (codes.lease).

Source code in src/dhcpctl/_completion.py
41
42
43
44
45
46
47
48
def __init__(
    self,
    keep: bool = False,
    lease: "Union[DHCPLease, Mapping[Any, Any], None]" = None,
    codes: "Optional[type[OptionCodes]]" = None,
) -> None:
    self.keep = bool(keep)
    self.lease = (codes or PYDHCP_CODES).lease(lease)

__bool__()

True when completion does anything other than remove the entry.

Source code in src/dhcpctl/_completion.py
60
61
62
def __bool__(self) -> bool:
    """True when completion does anything other than remove the entry."""
    return self.keep

apply_to(lease)

The lease a kept entry should end up with.

BOOT_OPTIONS are dropped and this completion's options put in their place, and its siaddr, sname and file replace the lease's -- replaced, not merged. The address and every other option stay.

Source code in src/dhcpctl/_completion.py
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
def apply_to(self, lease: DHCPLease) -> DHCPLease:
    """The lease a kept entry should end up with.

    `BOOT_OPTIONS` are dropped and this completion's options put in their
    place, and its `siaddr`, `sname` and `file` replace the lease's --
    **replaced, not merged**. The address and every other option stay.
    """
    kept = lease.options.copy()
    kept.retain(code for code in kept if code not in BOOT_OPTIONS)
    kept.update(self.lease.options)
    return lease.replace(
        options=kept,
        siaddr=self.lease.siaddr,
        sname=self.lease.sname,
        file=self.lease.file,
    )

to_mapping()

{keep: false}, or {keep: true, lease: {...}} with lease as DHCPLease.to_mapping writes it.

Source code in src/dhcpctl/_completion.py
64
65
66
67
68
69
def to_mapping(self) -> Dict[str, Any]:
    """`{keep: false}`, or `{keep: true, lease: {...}}` with `lease` as
    `DHCPLease.to_mapping` writes it."""
    if not self.keep:
        return {"keep": False}
    return {"keep": True, "lease": self.lease.to_mapping()}

dhcpctl.parse_completion(declared, where='', codes=None)

Build a Completion from {keep: ..., options: ...}, or from {keep: ..., lease: ...} as Completion.to_mapping writes it.

None stays None (nothing declared). where names the declaring object in error messages; option names are read with the codes codemap (pydhcp's), a config's options by codes.lease.

:raises ConfigError: the mapping cannot be used as given.

Source code in src/dhcpctl/_completion.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
def parse_completion(
    declared: Any, where: str = "", codes: "Optional[type[OptionCodes]]" = None
) -> Optional[Completion]:
    """Build a `Completion` from `{keep: ..., options: ...}`, or from
    `{keep: ..., lease: ...}` as `Completion.to_mapping` writes it.

    `None` stays `None` (nothing declared). `where` names the declaring object
    in error messages; option names are read with the `codes` codemap
    (pydhcp's), a config's `options` by `codes.lease`.

    :raises ConfigError: the mapping cannot be used as given.
    """
    if declared is None:
        return None
    if not isinstance(declared, dict):
        raise ConfigError(
            f"dhcp_complete on {where!r} must be a mapping with `keep` and "
            f"`options`, got {type(declared).__name__}"
        )
    unknown = set(declared) - {"keep", "options", "lease"}
    if unknown or {"options", "lease"} <= set(declared):
        raise ConfigError(
            f"dhcp_complete on {where!r} has unknown key(s) "
            f"{', '.join(sorted(unknown or {'options', 'lease'}))}; it takes `keep` "
            f"and `options` (or `lease`)"
        )
    codes = codes or PYDHCP_CODES
    keep = declared.get("keep", False)
    if not isinstance(keep, bool):
        raise ConfigError(
            f"dhcp_complete.keep on {where!r} must be true or false, got {keep!r}"
        )
    try:
        if "lease" in declared:
            lease = codes.read_lease(declared["lease"])
        else:
            options = declared.get("options") or {}
            if not isinstance(options, dict):
                raise ConfigError(f"must be a mapping, got {type(options).__name__}")
            lease = codes.lease(options)
    except ConfigError as exc:
        raise ConfigError(f"dhcp_complete.options on {where!r}: {exc}") from exc
    if serves(lease) and not keep:
        raise ConfigError(
            f"dhcp_complete on {where!r} sets `options` but not `keep: true`; "
            f"options for a removed reservation have nowhere to go"
        )
    return Completion(keep=keep, lease=lease)

Errors

dhcpctl.exceptions

The errors dhcpctl raises. Every one derives from DHCPCtlError.

BackendUnavailableError

Bases: DHCPCtlError, ImportError

A backend needs a library that is not installed; the message names the extra.

ConditionMissingError(message, recipe='')

Bases: DHCPCtlError

The condition's construct is absent and this backend cannot create it.

recipe is the text a privileged operator applies once, in the server's own syntax; it is also appended to the message.

Source code in src/dhcpctl/exceptions.py
52
53
54
def __init__(self, message: str, recipe: str = "") -> None:
    super().__init__(message if not recipe else f"{message}\n\n{recipe}")
    self.recipe = recipe

ConditionUnsupportedError

Bases: DHCPCtlError

This backend cannot express a condition at all, so nothing is armed with it.

ConfigError

Bases: DHCPCtlError, ValueError

A connection URI, a setting or a value cannot be used as given.

DHCPCtlError

Bases: Exception

Base class of every error dhcpctl raises.

MissingMACError

Bases: DHCPCtlError, LookupError

The target has no MAC address, and the server identifies a reservation by one.

ServerError

Bases: DHCPCtlError

The server, or the program that talks to it, refused or failed.

UnknownBackendError

Bases: ConfigError

No backend is registered for a URI's scheme.