pydhcpctl 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pydhcpctl-0.1.0/.gitignore +53 -0
- pydhcpctl-0.1.0/CHANGELOG.md +143 -0
- pydhcpctl-0.1.0/LICENSE +21 -0
- pydhcpctl-0.1.0/PKG-INFO +227 -0
- pydhcpctl-0.1.0/README.md +165 -0
- pydhcpctl-0.1.0/docs/api/reference.md +50 -0
- pydhcpctl-0.1.0/docs/backends.md +65 -0
- pydhcpctl-0.1.0/docs/changelog.md +1 -0
- pydhcpctl-0.1.0/docs/index.md +1 -0
- pydhcpctl-0.1.0/mkdocs.yml +52 -0
- pydhcpctl-0.1.0/pyproject.toml +100 -0
- pydhcpctl-0.1.0/src/dhcpctl/AGENTS.md +524 -0
- pydhcpctl-0.1.0/src/dhcpctl/__init__.py +109 -0
- pydhcpctl-0.1.0/src/dhcpctl/__main__.py +4 -0
- pydhcpctl-0.1.0/src/dhcpctl/_completion.py +136 -0
- pydhcpctl-0.1.0/src/dhcpctl/_conditions.py +156 -0
- pydhcpctl-0.1.0/src/dhcpctl/_model.py +231 -0
- pydhcpctl-0.1.0/src/dhcpctl/_options.py +343 -0
- pydhcpctl-0.1.0/src/dhcpctl/_paths.py +26 -0
- pydhcpctl-0.1.0/src/dhcpctl/_registry.py +144 -0
- pydhcpctl-0.1.0/src/dhcpctl/_server.py +254 -0
- pydhcpctl-0.1.0/src/dhcpctl/backends/__init__.py +30 -0
- pydhcpctl-0.1.0/src/dhcpctl/backends/_dhcpd.py +655 -0
- pydhcpctl-0.1.0/src/dhcpctl/backends/_dnsmasq.py +555 -0
- pydhcpctl-0.1.0/src/dhcpctl/backends/_kea.py +551 -0
- pydhcpctl-0.1.0/src/dhcpctl/backends/_windhcp.py +1107 -0
- pydhcpctl-0.1.0/src/dhcpctl/cli/__init__.py +36 -0
- pydhcpctl-0.1.0/src/dhcpctl/cli/_common.py +160 -0
- pydhcpctl-0.1.0/src/dhcpctl/cli/_root.py +23 -0
- pydhcpctl-0.1.0/src/dhcpctl/cli/backends.py +39 -0
- pydhcpctl-0.1.0/src/dhcpctl/cli/operations.py +58 -0
- pydhcpctl-0.1.0/src/dhcpctl/cli/recipe.py +27 -0
- pydhcpctl-0.1.0/src/dhcpctl/cli/render.py +33 -0
- pydhcpctl-0.1.0/src/dhcpctl/exceptions.py +54 -0
- pydhcpctl-0.1.0/src/dhcpctl/py.typed +0 -0
- pydhcpctl-0.1.0/src/dhcpctl/testing.py +156 -0
- pydhcpctl-0.1.0/tests/conftest.py +177 -0
- pydhcpctl-0.1.0/tests/peers/dhcpd_session.sh +89 -0
- pydhcpctl-0.1.0/tests/peers/dnsmasq_exchange.sh +50 -0
- pydhcpctl-0.1.0/tests/peers/dnsmasq_twice.sh +59 -0
- pydhcpctl-0.1.0/tests/peers/kea_session.sh +103 -0
- pydhcpctl-0.1.0/tests/test_cli.py +121 -0
- pydhcpctl-0.1.0/tests/test_completion.py +317 -0
- pydhcpctl-0.1.0/tests/test_conditions.py +795 -0
- pydhcpctl-0.1.0/tests/test_contract.py +31 -0
- pydhcpctl-0.1.0/tests/test_dhcpd.py +363 -0
- pydhcpctl-0.1.0/tests/test_dhcpd_real.py +198 -0
- pydhcpctl-0.1.0/tests/test_dnsmasq.py +544 -0
- pydhcpctl-0.1.0/tests/test_dnsmasq_real.py +217 -0
- pydhcpctl-0.1.0/tests/test_kea.py +405 -0
- pydhcpctl-0.1.0/tests/test_kea_real.py +133 -0
- pydhcpctl-0.1.0/tests/test_mapping.py +148 -0
- pydhcpctl-0.1.0/tests/test_options.py +522 -0
- pydhcpctl-0.1.0/tests/test_registry.py +407 -0
- pydhcpctl-0.1.0/tests/test_render.py +106 -0
- pydhcpctl-0.1.0/tests/test_secrets.py +172 -0
- pydhcpctl-0.1.0/tests/test_server.py +233 -0
- pydhcpctl-0.1.0/tests/test_windhcp.py +746 -0
- pydhcpctl-0.1.0/tests/test_windhcp_delivery.py +62 -0
- pydhcpctl-0.1.0/tests/test_windhcp_live.py +115 -0
- pydhcpctl-0.1.0/tests/test_windhcp_powershell.py +317 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Dotfiles are opt-in, as a category: a new secret-bearing dotfile (.env,
|
|
2
|
+
# .envrc, .pypirc, .netrc) is ignored before anyone has to remember it.
|
|
3
|
+
/.*
|
|
4
|
+
!/.gitignore
|
|
5
|
+
!/.gitattributes
|
|
6
|
+
!/.github/
|
|
7
|
+
|
|
8
|
+
# Personal, per-machine overrides: never committed, never packaged (the
|
|
9
|
+
# manifest excludes them too -- a dotfile pattern does not match these names).
|
|
10
|
+
*.local.*
|
|
11
|
+
|
|
12
|
+
# Agent configs / private notes, for nested occurrences the rule above cannot
|
|
13
|
+
# reach. `.agents` is slashless on purpose: it is usually a symlink, which a
|
|
14
|
+
# directory-only `.agents/` would not match.
|
|
15
|
+
.agents
|
|
16
|
+
CLAUDE*
|
|
17
|
+
.claude
|
|
18
|
+
|
|
19
|
+
# Virtualenvs
|
|
20
|
+
.venv*/
|
|
21
|
+
.pyvenv/
|
|
22
|
+
venv/
|
|
23
|
+
env/
|
|
24
|
+
|
|
25
|
+
# Byte-compiled / optimized
|
|
26
|
+
__pycache__/
|
|
27
|
+
*.py[cod]
|
|
28
|
+
*$py.class
|
|
29
|
+
|
|
30
|
+
# Distribution / packaging
|
|
31
|
+
build/
|
|
32
|
+
dist/
|
|
33
|
+
*.egg-info/
|
|
34
|
+
*.egg
|
|
35
|
+
.eggs/
|
|
36
|
+
|
|
37
|
+
# Test / coverage / type-check caches
|
|
38
|
+
.pytest_cache/
|
|
39
|
+
.hypothesis/
|
|
40
|
+
.mypy_cache/
|
|
41
|
+
.ruff_cache/
|
|
42
|
+
.coverage*
|
|
43
|
+
htmlcov/
|
|
44
|
+
.tox/
|
|
45
|
+
|
|
46
|
+
# Docs site build output
|
|
47
|
+
/site/
|
|
48
|
+
|
|
49
|
+
# Editors / OS
|
|
50
|
+
.vscode/
|
|
51
|
+
.idea/
|
|
52
|
+
*.swp
|
|
53
|
+
.DS_Store
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-10-19
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `DHCPServer.render(reservation, action="add")`: what an operation would
|
|
15
|
+
send, contacting nothing, on every shipped backend.
|
|
16
|
+
- `dhcpctl.testing.BackendContract`: the suite every shipped backend passes,
|
|
17
|
+
for a third-party backend to run unmodified; it talks to no server.
|
|
18
|
+
- The `dhcpctl` command line (`pydhcpctl[cli]`): `render`, `add`,
|
|
19
|
+
`complete`, `remove`, `recipe` and `backends`, a reservation given by flags
|
|
20
|
+
or a document, secrets from `DHCPCTL_*` variables.
|
|
21
|
+
- Published as `pydhcpctl`; the import package is `dhcpctl`.
|
|
22
|
+
- `connect(uri, *, secrets=None, owner="dhcpctl")`, `register_backend`,
|
|
23
|
+
`unregister_backend`, `backend_names`, `get_backend` and the
|
|
24
|
+
`dhcpctl.backends` entry-point group.
|
|
25
|
+
- `DHCPServer`: `add`, `remove`, `keep`, `complete`, `ensure_condition`,
|
|
26
|
+
`condition_recipe`, `remove_condition_member`, `extras`,
|
|
27
|
+
`secret`, `redacted_uri`. A secret is the caller's when it gives one, else
|
|
28
|
+
the URI's user or password, so one URI serves several logins; a password is
|
|
29
|
+
masked in `str`, `repr` and messages.
|
|
30
|
+
- `Reservation`, `Target`, `Scope`, `Condition`, `Completion`,
|
|
31
|
+
`parse_conditions`, `parse_completion`. What each serves is a pydhcp
|
|
32
|
+
`DHCPLease` (re-exported): the address (`0.0.0.0` for a MAC-only
|
|
33
|
+
reservation), the options, and the BOOTP header fields `siaddr`, `sname` and
|
|
34
|
+
`file`, which are not options 66 and 67; each backend writes them as its
|
|
35
|
+
server spells the fields. `Reservation.to_mapping` / `from_mapping` and
|
|
36
|
+
`to_text` / `from_text` write and read a reservation as plain data or a
|
|
37
|
+
JSON, YAML, TOML or INI document (all but JSON with the `formats` extra),
|
|
38
|
+
each lease in pydhcp's `DHCPLease.to_mapping` form.
|
|
39
|
+
- Options are pydhcp's `DHCPOptions` and `DHCPOptionCode`, re-exported, and
|
|
40
|
+
reach a backend as pydhcp objects for it to write as its server needs.
|
|
41
|
+
A codemap (`option_codes`, an `OptionCodes` enum, pydhcp's
|
|
42
|
+
`OptionCodeEnum`, its names read by pydhcp's `resolve`) reads a config's option names: declared options with a
|
|
43
|
+
code and a pydhcp type of their own, aliases of names or codes, over pydhcp's
|
|
44
|
+
names, values read by pydhcp's `DHCPOptions.from_mapping` (an enum member
|
|
45
|
+
by name, octets as hex); the Kea and dhcpd backends add their own spellings (`OPTION_NAMES`),
|
|
46
|
+
and `connect(uri, codes=...)` the caller's; `codes.lease(mapping)` takes
|
|
47
|
+
the header fields (`next-server`, `file`, an alias) out of a config's options. Declared types belong to the
|
|
48
|
+
codemap, which starts from the codecs its base has when it is made. netsh's
|
|
49
|
+
data types come from the codecs. An unset address is `UNSPECIFIED`
|
|
50
|
+
(pydhcp's `UNSPECIFIED_ADDRESS`, `0.0.0.0`), as in BOOTP.
|
|
51
|
+
- The dnsmasq backend writes its hosts and options files as dnsmasq reads
|
|
52
|
+
them: each line the value of `--dhcp-host` / `--dhcp-option` without the
|
|
53
|
+
key (netboot wrote `dhcp-host=` and `dhcp-option=` prefixes, which dnsmasq
|
|
54
|
+
rejects as "bad hex constant", so no reservation took effect), and the boot
|
|
55
|
+
file and next server as `option:bootfile-name` / `option:server-ip-address`
|
|
56
|
+
in place of a `dhcp-boot` line. An extras line for the options file is
|
|
57
|
+
written the same way. Checked against a real dnsmasq and DHCP client
|
|
58
|
+
(`tests/test_dnsmasq_real.py`).
|
|
59
|
+
- A dnsmasq directory (`--dhcp-hostsdir`) is SIGHUPed when an entry it may have
|
|
60
|
+
read is removed or changed: dnsmasq only ever adds what such a directory
|
|
61
|
+
holds, so a completed client kept its boot file. A new entry still needs no
|
|
62
|
+
signal. A reservation with options and no `optsfile=` is refused instead of
|
|
63
|
+
arming the client without them; a trailing `/` on a path that does not exist
|
|
64
|
+
yet means a directory; names that reduce to one tag get distinct tags; a
|
|
65
|
+
missing or hung reload command, an unreadable pidfile and a platform without
|
|
66
|
+
SIGHUP are errors instead of a stray exception or SIGTERM. A local hosts or
|
|
67
|
+
options file is rewritten under a lock on `.<file>.lock`, so two runs at once
|
|
68
|
+
no longer drop each other's entries.
|
|
69
|
+
- Every option reaches each server in a form it reads. dhcpd: an option it
|
|
70
|
+
defines is written under its name in its own syntax (119 codes, taken from
|
|
71
|
+
dhcpd 4.4 itself); netboot wrote `unknown-<n>` and octets for all but a
|
|
72
|
+
dozen, which dhcpd parses as the option's own type, so it refused the
|
|
73
|
+
host's statements and served none of them (70 of 134 options). Negative
|
|
74
|
+
numbers and domain names are written bare, and octets for an option dhcpd
|
|
75
|
+
defines with a structure of its own are a `ConfigError`. dnsmasq: a flag,
|
|
76
|
+
a number list and octets are written as octets (`True`, `576,1500` and
|
|
77
|
+
`0a0b` were text or refused). Kea: octets go with `csv-format` off, a flag
|
|
78
|
+
as `true`. Windows: a flag is 1 or 0, and a negative number the DWORD it
|
|
79
|
+
stores (it refused `-3600` for option 2). Checked by sending every option of a
|
|
80
|
+
plain codec through a real dhcpd (`dhcpd -t`) and dnsmasq, and one of each
|
|
81
|
+
codec to a real client (`tests/test_dhcpd_real.py`,
|
|
82
|
+
`tests/test_dnsmasq_real.py`).
|
|
83
|
+
- Re-arming a client replaces what the server holds for it, on every
|
|
84
|
+
backend: dhcpd and Kea refused, and the Windows cmdlet threw.
|
|
85
|
+
- Kea conditions work: a condition is a client class of the client's own,
|
|
86
|
+
tested on its MAC and `option[n].hex` (iPXE's flat option 77 never matched
|
|
87
|
+
`.text`), and what it overrides is served by a second class of the
|
|
88
|
+
client's when it does not match, since Kea serves a reservation's values
|
|
89
|
+
over any class's. The reservation names no `client-classes`, which put the
|
|
90
|
+
client in the class whatever its test said: on a real Kea every client was
|
|
91
|
+
handed the reservation's boot file, iPXE or not. `?target=memory` (host_cmds'
|
|
92
|
+
`operation-target`) arms a Kea with no hosts database. Checked against a
|
|
93
|
+
real Kea 3.0.4 (`tests/test_kea_real.py`).
|
|
94
|
+
- Commands run through hostctl hosts: the Windows backend's local, ssh and
|
|
95
|
+
WinRM transports (`hostctl.LocalConfig`, `SSHConfig`, `WinRMConfig`) and a
|
|
96
|
+
dnsmasq `reload=`, locally or over ssh. hostctl delivers a PowerShell script
|
|
97
|
+
whole; netboot piped it into `powershell -Command -`, which runs only the
|
|
98
|
+
first line of a multi-line block and exits 0 even when a cmdlet fails, so
|
|
99
|
+
reservations were reported armed and were not. A script that ends without
|
|
100
|
+
printing its closing `{"ok": true}` is a `ServerError`, as are an
|
|
101
|
+
unreachable host, a refused login and a timeout (`timeout=`, default 120 s).
|
|
102
|
+
ssh is asyncssh, through `hostctl[ssh]`, rather than the system `ssh`
|
|
103
|
+
client; it reads `~/.ssh/config` as `ssh` does, and the `ssh` extra now
|
|
104
|
+
serves windhcp over ssh too. Checked against real Windows PowerShell 5.1
|
|
105
|
+
(`tests/test_windhcp_delivery.py`), and against a live Windows Server 2025
|
|
106
|
+
DHCP server over ssh with both methods: arm, re-arm, keep and remove, the
|
|
107
|
+
policies included (`tests/test_windhcp_live.py`, opt-in).
|
|
108
|
+
- The Windows backend removes a target's own policies when it removes the
|
|
109
|
+
target (they kept answering its MAC); leaves an existing shared policy as it
|
|
110
|
+
is, and trusts it on a host without the DhcpServer module; prints, in target
|
|
111
|
+
mode, the MAC-matched policies it would create rather than a scope-wide one,
|
|
112
|
+
with every value quoted against injection (typographic quotes included);
|
|
113
|
+
reads back under `method=netsh` the options it actually set; keeps a
|
|
114
|
+
reservation through netsh when `method=netsh`; and warns about option 119 and
|
|
115
|
+
a next-server it cannot set.
|
|
116
|
+
- The Kea backend keeps a reservation and detaches its classes with
|
|
117
|
+
`reservation-update` (or `reservation-del` then `reservation-add` where Kea
|
|
118
|
+
lacks it), checking every result: `reservation-add` on an existing
|
|
119
|
+
reservation was refused, and the detach's answer was ignored. Kea's keep
|
|
120
|
+
writes the `remove`-phase extras, as the other backends' do; a subnet inside
|
|
121
|
+
a `shared-networks` entry is found; `service=` other than `dhcp4` is
|
|
122
|
+
refused.
|
|
123
|
+
- The dhcpd backend keeps a host by deleting and creating it again (dhcpd
|
|
124
|
+
refuses to create a host that exists, so keeping and leaving a group failed);
|
|
125
|
+
creates a host in its group in one create, after finding the group by lookup
|
|
126
|
+
or creating it, and arms nothing when the group cannot be made; in group mode
|
|
127
|
+
puts each value a condition overrides in the `else` of its test, since a
|
|
128
|
+
host's statements outrank its group's; writes an option dhcpd has no name
|
|
129
|
+
for as `unknown-<n>` (`dhcp-option-<n>` does not parse); and refuses to arm a
|
|
130
|
+
host that exists rather than failing deep in pypureomapi. Checked against a
|
|
131
|
+
real ISC dhcpd 4.4 and DHCP client (`tests/test_dhcpd_real.py`).
|
|
132
|
+
- Backends for dnsmasq, Kea (`kea`/`keas`), ISC dhcpd over OMAPI and Windows
|
|
133
|
+
DHCP (PowerShell or netsh; local, ssh or WinRM), taken from netboot 0.4,
|
|
134
|
+
sending what netboot sent, except that Kea's `option-data` names options by
|
|
135
|
+
code (netboot sent names Kea does not define: `router`, `lease-time`), and a
|
|
136
|
+
Kea client class carries the boot file as its `boot-file-name` field, as a
|
|
137
|
+
reservation does, rather than as option 67.
|
|
138
|
+
- Errors under `DHCPCtlError`: `ConfigError`, `UnknownBackendError`,
|
|
139
|
+
`BackendUnavailableError`, `MissingMACError`, `ServerError`,
|
|
140
|
+
`ConditionUnsupportedError`, `ConditionMissingError`.
|
|
141
|
+
|
|
142
|
+
[Unreleased]: https://github.com/jose-pr/dhcpctl/compare/v0.1.0...HEAD
|
|
143
|
+
[0.1.0]: https://github.com/jose-pr/dhcpctl/releases/tag/v0.1.0
|
pydhcpctl-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jose A.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pydhcpctl-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pydhcpctl
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: One interface to DHCP servers: arm a reservation on dnsmasq, Kea, ISC dhcpd or Windows DHCP, then remove or keep it.
|
|
5
|
+
Project-URL: Homepage, https://github.com/jose-pr/dhcpctl/
|
|
6
|
+
Project-URL: Documentation, https://jose-pr.github.io/dhcpctl/
|
|
7
|
+
Project-URL: Issues, https://github.com/jose-pr/dhcpctl/issues
|
|
8
|
+
Author: jose-pr
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: System Administrators
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: System :: Networking
|
|
22
|
+
Classifier: Topic :: System :: Systems Administration
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Requires-Dist: hostctl<0.5,>=0.4.0
|
|
26
|
+
Requires-Dist: netimps<0.5,>=0.4.1
|
|
27
|
+
Requires-Dist: pathapi-uri<0.2,>=0.1.0
|
|
28
|
+
Requires-Dist: pathapi<0.2,>=0.1.2
|
|
29
|
+
Requires-Dist: pydhcp<0.10,>=0.9.0
|
|
30
|
+
Provides-Extra: cli
|
|
31
|
+
Requires-Dist: duho<0.8,>=0.7.6; extra == 'cli'
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: black; extra == 'dev'
|
|
34
|
+
Requires-Dist: build; extra == 'dev'
|
|
35
|
+
Requires-Dist: duho<0.8,>=0.7.6; extra == 'dev'
|
|
36
|
+
Requires-Dist: hostctl[ssh]<0.5,>=0.4.0; extra == 'dev'
|
|
37
|
+
Requires-Dist: hostctl[winrm]<0.5,>=0.4.0; extra == 'dev'
|
|
38
|
+
Requires-Dist: pktcap[toml,yaml]<0.2,>=0.1.1; extra == 'dev'
|
|
39
|
+
Requires-Dist: pydhcp[pktcap]; extra == 'dev'
|
|
40
|
+
Requires-Dist: pypureomapi<2,>=0.8; extra == 'dev'
|
|
41
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
42
|
+
Requires-Dist: requests<3,>=2.32.3; extra == 'dev'
|
|
43
|
+
Requires-Dist: sftppath[uri]<0.2,>=0.1.3; extra == 'dev'
|
|
44
|
+
Provides-Extra: dhcpd
|
|
45
|
+
Requires-Dist: pypureomapi<2,>=0.8; extra == 'dhcpd'
|
|
46
|
+
Provides-Extra: docs
|
|
47
|
+
Requires-Dist: mkdocs-material; extra == 'docs'
|
|
48
|
+
Requires-Dist: mkdocs<2; extra == 'docs'
|
|
49
|
+
Requires-Dist: mkdocstrings[python]; extra == 'docs'
|
|
50
|
+
Requires-Dist: pymdown-extensions<12.2; (python_version < '3.11') and extra == 'docs'
|
|
51
|
+
Provides-Extra: formats
|
|
52
|
+
Requires-Dist: pktcap[toml,yaml]<0.2,>=0.1.1; extra == 'formats'
|
|
53
|
+
Requires-Dist: pydhcp[pktcap]; extra == 'formats'
|
|
54
|
+
Provides-Extra: kea
|
|
55
|
+
Requires-Dist: requests<3,>=2.32.3; extra == 'kea'
|
|
56
|
+
Provides-Extra: ssh
|
|
57
|
+
Requires-Dist: hostctl[ssh]<0.5,>=0.4.0; extra == 'ssh'
|
|
58
|
+
Requires-Dist: sftppath[uri]<0.2,>=0.1.3; extra == 'ssh'
|
|
59
|
+
Provides-Extra: winrm
|
|
60
|
+
Requires-Dist: hostctl[winrm]<0.5,>=0.4.0; extra == 'winrm'
|
|
61
|
+
Description-Content-Type: text/markdown
|
|
62
|
+
|
|
63
|
+
# dhcpctl
|
|
64
|
+
|
|
65
|
+
[](https://pypi.org/project/pydhcpctl/)
|
|
66
|
+
[](https://pypi.org/project/pydhcpctl/)
|
|
67
|
+
[](https://github.com/jose-pr/dhcpctl/blob/main/LICENSE)
|
|
68
|
+
[](https://jose-pr.github.io/dhcpctl/)
|
|
69
|
+
[](https://github.com/jose-pr/dhcpctl/actions/workflows/test.yml)
|
|
70
|
+
|
|
71
|
+
One interface to DHCP servers: **arm a reservation** for a client on dnsmasq,
|
|
72
|
+
ISC Kea, ISC dhcpd or Windows DHCP, then **remove it or keep it** without its
|
|
73
|
+
boot options. A server is a URI: its scheme picks the backend and its query says how to
|
|
74
|
+
reach it. The options to serve belong to each reservation.
|
|
75
|
+
Documentation: <https://jose-pr.github.io/dhcpctl/>.
|
|
76
|
+
|
|
77
|
+
## Features
|
|
78
|
+
|
|
79
|
+
- **Four servers, one call** — `add`, `remove`, `complete` on dnsmasq (files,
|
|
80
|
+
local or over `sftp://`), Kea (control agent), dhcpd (OMAPI) and Windows DHCP
|
|
81
|
+
(PowerShell cmdlets or netsh, locally, over ssh or WinRM).
|
|
82
|
+
- **pydhcp's lease** — what a client is served is a [pydhcp](https://github.com/jose-pr/pydhcp)
|
|
83
|
+
`DHCPLease`: its address, its `DHCPOptions` named by pydhcp's registry
|
|
84
|
+
(`router`, `dns`, `option-<n>`) or an alias, each value read by its pydhcp
|
|
85
|
+
codec before any server sees it, and the BOOTP header fields (`next-server`,
|
|
86
|
+
`file`); each backend translates them, and backend-native text in a
|
|
87
|
+
reservation's `extras` rides through untouched.
|
|
88
|
+
- **Conditions** — options served only to clients that match a test (the iPXE
|
|
89
|
+
chainload), as a Windows policy, a dhcpd `if` or group, or a Kea client class;
|
|
90
|
+
a backend that cannot express one refuses rather than arming without it, and
|
|
91
|
+
hands over the server-side recipe when it lacks the rights to create it.
|
|
92
|
+
- **Completion that does not reinstall** — keep a finished client's reservation
|
|
93
|
+
and replace only its boot options.
|
|
94
|
+
- **Values are data, never source** — dhcpd statements are escaped, Windows
|
|
95
|
+
scripts get their parameters as a JSON payload, netsh arguments stay array
|
|
96
|
+
elements.
|
|
97
|
+
- **Nothing required** — each backend's library is an extra, imported only when
|
|
98
|
+
its scheme is used; secrets come from the URI or the caller, and a password
|
|
99
|
+
is masked wherever a server is shown.
|
|
100
|
+
- **Reservations as documents** — `Reservation.to_mapping` / `from_mapping`
|
|
101
|
+
and `to_text` / `from_text` (JSON, YAML, TOML, INI) round-trip a reservation
|
|
102
|
+
through pydhcp's own option form.
|
|
103
|
+
- **Pluggable** — a backend is a `DHCPServer` subclass registered for a scheme,
|
|
104
|
+
directly or through the `dhcpctl.backends` entry point.
|
|
105
|
+
|
|
106
|
+
## Installation
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
pip install pydhcpctl
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The distribution is `pydhcpctl`; the import package is `dhcpctl`.
|
|
113
|
+
|
|
114
|
+
| Scheme | Server | Install |
|
|
115
|
+
| --- | --- | --- |
|
|
116
|
+
| `dnsmasq://` | dnsmasq | nothing locally; `pydhcpctl[ssh]` for `sftp://` paths |
|
|
117
|
+
| `kea://`, `keas://` | ISC Kea control agent | `pydhcpctl[kea]` (requests) |
|
|
118
|
+
| `dhcpd://` | ISC dhcpd over OMAPI | `pydhcpctl[dhcpd]` (pypureomapi) |
|
|
119
|
+
| `windhcp://` | Windows DHCP | nothing locally; `pydhcpctl[ssh]` over ssh, `pydhcpctl[winrm]` over WinRM |
|
|
120
|
+
|
|
121
|
+
`pydhcpctl[formats]` adds YAML, TOML and INI to `Reservation.to_text` /
|
|
122
|
+
`from_text` (JSON needs nothing), and `pydhcpctl[cli]` the `dhcpctl` command.
|
|
123
|
+
|
|
124
|
+
Python 3.9 or later.
|
|
125
|
+
|
|
126
|
+
## Usage
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
import dhcpctl
|
|
130
|
+
from ipaddress import ip_network
|
|
131
|
+
|
|
132
|
+
from netimps import MACAddress
|
|
133
|
+
|
|
134
|
+
server = dhcpctl.connect("kea://kea01:8000/?service=dhcp4") # how to reach it, nothing else
|
|
135
|
+
|
|
136
|
+
# Options by name, and the BOOTP header fields beside them (not options).
|
|
137
|
+
lease = server.codes.lease(
|
|
138
|
+
{"router": "10.0.0.1", "file": "undionly.kpxe", "next-server": "10.0.0.2"}
|
|
139
|
+
)
|
|
140
|
+
reservation = dhcpctl.Reservation(
|
|
141
|
+
target=dhcpctl.Target("web01", mac=MACAddress("aa:bb:cc:dd:ee:ff")),
|
|
142
|
+
scope=dhcpctl.Scope("lan", network=ip_network("10.0.0.0/24")),
|
|
143
|
+
lease=lease.replace(ip="10.0.0.10"),
|
|
144
|
+
completion=dhcpctl.Completion(keep=True), # keep the address when done
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
server.add(reservation) # arm
|
|
148
|
+
server.complete(reservation) # finished: keep the entry, drop its boot options
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
A condition serves different options to clients that match a test:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
import dataclasses
|
|
155
|
+
|
|
156
|
+
conditions = dhcpctl.parse_conditions(
|
|
157
|
+
{"ipxe": {"match": {"user-class": "iPXE"}, "options": {"file": "boot.ipxe"}}}
|
|
158
|
+
)
|
|
159
|
+
server.add(dataclasses.replace(reservation, conditions=conditions))
|
|
160
|
+
print(server.condition_recipe(reservation, conditions["ipxe"])) # what an admin applies
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
A secret is the URI's when it carries one, else the caller's, asked for only
|
|
164
|
+
when needed — so it can stay out of a URI that ends up in config or logs:
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
server = dhcpctl.connect(
|
|
168
|
+
"windhcp://dhcp01/?transport=winrm", secrets={"user": "svc", "password": "..."}
|
|
169
|
+
)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Command line
|
|
173
|
+
|
|
174
|
+
`pip install pydhcpctl[cli]` adds the `dhcpctl` command (also `python -m
|
|
175
|
+
dhcpctl`). A reservation is given by flags, or as a document with `--from`
|
|
176
|
+
(`.json`, `.yaml`, `.toml`, `.ini`, or `-` for JSON on standard input), which
|
|
177
|
+
can also carry conditions and a completion:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
# What arming web01 would send, contacting nothing:
|
|
181
|
+
dhcpctl render 'dhcpd://dhcp01/' -n web01 -m aa:bb:cc:dd:ee:ff --ip 10.0.0.10 \
|
|
182
|
+
-o router=10.0.0.1 -o file=undionly.kpxe
|
|
183
|
+
dhcpctl add 'kea://kea01/?target=memory' --from web01.json # arm (or re-arm)
|
|
184
|
+
dhcpctl complete 'kea://kea01/?target=memory' --from web01.json --keep
|
|
185
|
+
dhcpctl remove 'windhcp://dhcp01/?transport=winrm' -n web01 -m aa:bb:cc:dd:ee:ff
|
|
186
|
+
dhcpctl recipe 'windhcp://dhcp01/' --from web01.json # what an admin sets up once
|
|
187
|
+
dhcpctl backends --json
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`-o name=value` is an option or header field; a comma makes a list
|
|
191
|
+
(`dns=10.0.0.53,10.0.0.54`) and `json:` a JSON value (`lease-time=json:600`).
|
|
192
|
+
Secrets come from the environment, never a flag: `DHCPCTL_USER`,
|
|
193
|
+
`DHCPCTL_PASSWORD`, `DHCPCTL_KEYNAME`, `DHCPCTL_OMAPI_KEY`. Exit status 0 is
|
|
194
|
+
done, 1 the server refused or could not be reached, 2 the invocation or the
|
|
195
|
+
reservation was wrong.
|
|
196
|
+
|
|
197
|
+
## Writing a backend
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
class Recorder(dhcpctl.DHCPServer):
|
|
201
|
+
def add(self, reservation): ...
|
|
202
|
+
def remove(self, reservation): ...
|
|
203
|
+
|
|
204
|
+
dhcpctl.register_backend("recorder", Recorder)
|
|
205
|
+
dhcpctl.connect("recorder://anything")
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
or declare it in your package's metadata:
|
|
209
|
+
|
|
210
|
+
```toml
|
|
211
|
+
[project.entry-points."dhcpctl.backends"]
|
|
212
|
+
recorder = "mypkg.dhcp:Recorder"
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
A backend is done when it passes the contract the shipped ones pass, which
|
|
216
|
+
talks to no server (it needs pytest, and the backend to implement `render`):
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
from dhcpctl.testing import BackendContract
|
|
220
|
+
|
|
221
|
+
class TestRecorder(BackendContract):
|
|
222
|
+
uri = "recorder://anything"
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## License
|
|
226
|
+
|
|
227
|
+
MIT
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# dhcpctl
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/pydhcpctl/)
|
|
4
|
+
[](https://pypi.org/project/pydhcpctl/)
|
|
5
|
+
[](https://github.com/jose-pr/dhcpctl/blob/main/LICENSE)
|
|
6
|
+
[](https://jose-pr.github.io/dhcpctl/)
|
|
7
|
+
[](https://github.com/jose-pr/dhcpctl/actions/workflows/test.yml)
|
|
8
|
+
|
|
9
|
+
One interface to DHCP servers: **arm a reservation** for a client on dnsmasq,
|
|
10
|
+
ISC Kea, ISC dhcpd or Windows DHCP, then **remove it or keep it** without its
|
|
11
|
+
boot options. A server is a URI: its scheme picks the backend and its query says how to
|
|
12
|
+
reach it. The options to serve belong to each reservation.
|
|
13
|
+
Documentation: <https://jose-pr.github.io/dhcpctl/>.
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- **Four servers, one call** — `add`, `remove`, `complete` on dnsmasq (files,
|
|
18
|
+
local or over `sftp://`), Kea (control agent), dhcpd (OMAPI) and Windows DHCP
|
|
19
|
+
(PowerShell cmdlets or netsh, locally, over ssh or WinRM).
|
|
20
|
+
- **pydhcp's lease** — what a client is served is a [pydhcp](https://github.com/jose-pr/pydhcp)
|
|
21
|
+
`DHCPLease`: its address, its `DHCPOptions` named by pydhcp's registry
|
|
22
|
+
(`router`, `dns`, `option-<n>`) or an alias, each value read by its pydhcp
|
|
23
|
+
codec before any server sees it, and the BOOTP header fields (`next-server`,
|
|
24
|
+
`file`); each backend translates them, and backend-native text in a
|
|
25
|
+
reservation's `extras` rides through untouched.
|
|
26
|
+
- **Conditions** — options served only to clients that match a test (the iPXE
|
|
27
|
+
chainload), as a Windows policy, a dhcpd `if` or group, or a Kea client class;
|
|
28
|
+
a backend that cannot express one refuses rather than arming without it, and
|
|
29
|
+
hands over the server-side recipe when it lacks the rights to create it.
|
|
30
|
+
- **Completion that does not reinstall** — keep a finished client's reservation
|
|
31
|
+
and replace only its boot options.
|
|
32
|
+
- **Values are data, never source** — dhcpd statements are escaped, Windows
|
|
33
|
+
scripts get their parameters as a JSON payload, netsh arguments stay array
|
|
34
|
+
elements.
|
|
35
|
+
- **Nothing required** — each backend's library is an extra, imported only when
|
|
36
|
+
its scheme is used; secrets come from the URI or the caller, and a password
|
|
37
|
+
is masked wherever a server is shown.
|
|
38
|
+
- **Reservations as documents** — `Reservation.to_mapping` / `from_mapping`
|
|
39
|
+
and `to_text` / `from_text` (JSON, YAML, TOML, INI) round-trip a reservation
|
|
40
|
+
through pydhcp's own option form.
|
|
41
|
+
- **Pluggable** — a backend is a `DHCPServer` subclass registered for a scheme,
|
|
42
|
+
directly or through the `dhcpctl.backends` entry point.
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install pydhcpctl
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The distribution is `pydhcpctl`; the import package is `dhcpctl`.
|
|
51
|
+
|
|
52
|
+
| Scheme | Server | Install |
|
|
53
|
+
| --- | --- | --- |
|
|
54
|
+
| `dnsmasq://` | dnsmasq | nothing locally; `pydhcpctl[ssh]` for `sftp://` paths |
|
|
55
|
+
| `kea://`, `keas://` | ISC Kea control agent | `pydhcpctl[kea]` (requests) |
|
|
56
|
+
| `dhcpd://` | ISC dhcpd over OMAPI | `pydhcpctl[dhcpd]` (pypureomapi) |
|
|
57
|
+
| `windhcp://` | Windows DHCP | nothing locally; `pydhcpctl[ssh]` over ssh, `pydhcpctl[winrm]` over WinRM |
|
|
58
|
+
|
|
59
|
+
`pydhcpctl[formats]` adds YAML, TOML and INI to `Reservation.to_text` /
|
|
60
|
+
`from_text` (JSON needs nothing), and `pydhcpctl[cli]` the `dhcpctl` command.
|
|
61
|
+
|
|
62
|
+
Python 3.9 or later.
|
|
63
|
+
|
|
64
|
+
## Usage
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
import dhcpctl
|
|
68
|
+
from ipaddress import ip_network
|
|
69
|
+
|
|
70
|
+
from netimps import MACAddress
|
|
71
|
+
|
|
72
|
+
server = dhcpctl.connect("kea://kea01:8000/?service=dhcp4") # how to reach it, nothing else
|
|
73
|
+
|
|
74
|
+
# Options by name, and the BOOTP header fields beside them (not options).
|
|
75
|
+
lease = server.codes.lease(
|
|
76
|
+
{"router": "10.0.0.1", "file": "undionly.kpxe", "next-server": "10.0.0.2"}
|
|
77
|
+
)
|
|
78
|
+
reservation = dhcpctl.Reservation(
|
|
79
|
+
target=dhcpctl.Target("web01", mac=MACAddress("aa:bb:cc:dd:ee:ff")),
|
|
80
|
+
scope=dhcpctl.Scope("lan", network=ip_network("10.0.0.0/24")),
|
|
81
|
+
lease=lease.replace(ip="10.0.0.10"),
|
|
82
|
+
completion=dhcpctl.Completion(keep=True), # keep the address when done
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
server.add(reservation) # arm
|
|
86
|
+
server.complete(reservation) # finished: keep the entry, drop its boot options
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
A condition serves different options to clients that match a test:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
import dataclasses
|
|
93
|
+
|
|
94
|
+
conditions = dhcpctl.parse_conditions(
|
|
95
|
+
{"ipxe": {"match": {"user-class": "iPXE"}, "options": {"file": "boot.ipxe"}}}
|
|
96
|
+
)
|
|
97
|
+
server.add(dataclasses.replace(reservation, conditions=conditions))
|
|
98
|
+
print(server.condition_recipe(reservation, conditions["ipxe"])) # what an admin applies
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
A secret is the URI's when it carries one, else the caller's, asked for only
|
|
102
|
+
when needed — so it can stay out of a URI that ends up in config or logs:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
server = dhcpctl.connect(
|
|
106
|
+
"windhcp://dhcp01/?transport=winrm", secrets={"user": "svc", "password": "..."}
|
|
107
|
+
)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Command line
|
|
111
|
+
|
|
112
|
+
`pip install pydhcpctl[cli]` adds the `dhcpctl` command (also `python -m
|
|
113
|
+
dhcpctl`). A reservation is given by flags, or as a document with `--from`
|
|
114
|
+
(`.json`, `.yaml`, `.toml`, `.ini`, or `-` for JSON on standard input), which
|
|
115
|
+
can also carry conditions and a completion:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
# What arming web01 would send, contacting nothing:
|
|
119
|
+
dhcpctl render 'dhcpd://dhcp01/' -n web01 -m aa:bb:cc:dd:ee:ff --ip 10.0.0.10 \
|
|
120
|
+
-o router=10.0.0.1 -o file=undionly.kpxe
|
|
121
|
+
dhcpctl add 'kea://kea01/?target=memory' --from web01.json # arm (or re-arm)
|
|
122
|
+
dhcpctl complete 'kea://kea01/?target=memory' --from web01.json --keep
|
|
123
|
+
dhcpctl remove 'windhcp://dhcp01/?transport=winrm' -n web01 -m aa:bb:cc:dd:ee:ff
|
|
124
|
+
dhcpctl recipe 'windhcp://dhcp01/' --from web01.json # what an admin sets up once
|
|
125
|
+
dhcpctl backends --json
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`-o name=value` is an option or header field; a comma makes a list
|
|
129
|
+
(`dns=10.0.0.53,10.0.0.54`) and `json:` a JSON value (`lease-time=json:600`).
|
|
130
|
+
Secrets come from the environment, never a flag: `DHCPCTL_USER`,
|
|
131
|
+
`DHCPCTL_PASSWORD`, `DHCPCTL_KEYNAME`, `DHCPCTL_OMAPI_KEY`. Exit status 0 is
|
|
132
|
+
done, 1 the server refused or could not be reached, 2 the invocation or the
|
|
133
|
+
reservation was wrong.
|
|
134
|
+
|
|
135
|
+
## Writing a backend
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
class Recorder(dhcpctl.DHCPServer):
|
|
139
|
+
def add(self, reservation): ...
|
|
140
|
+
def remove(self, reservation): ...
|
|
141
|
+
|
|
142
|
+
dhcpctl.register_backend("recorder", Recorder)
|
|
143
|
+
dhcpctl.connect("recorder://anything")
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
or declare it in your package's metadata:
|
|
147
|
+
|
|
148
|
+
```toml
|
|
149
|
+
[project.entry-points."dhcpctl.backends"]
|
|
150
|
+
recorder = "mypkg.dhcp:Recorder"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
A backend is done when it passes the contract the shipped ones pass, which
|
|
154
|
+
talks to no server (it needs pytest, and the backend to implement `render`):
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from dhcpctl.testing import BackendContract
|
|
158
|
+
|
|
159
|
+
class TestRecorder(BackendContract):
|
|
160
|
+
uri = "recorder://anything"
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## License
|
|
164
|
+
|
|
165
|
+
MIT
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# API reference
|
|
2
|
+
|
|
3
|
+
## Connecting
|
|
4
|
+
|
|
5
|
+
::: dhcpctl.connect
|
|
6
|
+
|
|
7
|
+
::: dhcpctl.register_backend
|
|
8
|
+
|
|
9
|
+
::: dhcpctl.unregister_backend
|
|
10
|
+
|
|
11
|
+
::: dhcpctl.backend_names
|
|
12
|
+
|
|
13
|
+
::: dhcpctl.get_backend
|
|
14
|
+
|
|
15
|
+
## The contract
|
|
16
|
+
|
|
17
|
+
::: dhcpctl.DHCPServer
|
|
18
|
+
|
|
19
|
+
## Reservations
|
|
20
|
+
|
|
21
|
+
::: dhcpctl.Reservation
|
|
22
|
+
|
|
23
|
+
::: dhcpctl.Target
|
|
24
|
+
|
|
25
|
+
::: dhcpctl.Scope
|
|
26
|
+
|
|
27
|
+
## What is served
|
|
28
|
+
|
|
29
|
+
`dhcpctl.DHCPLease`, `dhcpctl.DHCPOptions` and `dhcpctl.DHCPOptionCode` are
|
|
30
|
+
pydhcp's, re-exported: see [pydhcp's reference](https://jose-pr.github.io/pydhcp/).
|
|
31
|
+
|
|
32
|
+
::: dhcpctl.OptionCodes
|
|
33
|
+
|
|
34
|
+
::: dhcpctl.option_codes
|
|
35
|
+
|
|
36
|
+
::: dhcpctl.plain_items
|
|
37
|
+
|
|
38
|
+
## Conditions and completion
|
|
39
|
+
|
|
40
|
+
::: dhcpctl.Condition
|
|
41
|
+
|
|
42
|
+
::: dhcpctl.parse_conditions
|
|
43
|
+
|
|
44
|
+
::: dhcpctl.Completion
|
|
45
|
+
|
|
46
|
+
::: dhcpctl.parse_completion
|
|
47
|
+
|
|
48
|
+
## Errors
|
|
49
|
+
|
|
50
|
+
::: dhcpctl.exceptions
|