Scripting guide¶
In addition to static parameters to configure staticDHCPd to work in your environment, extensive scripting is possible, allowing it to perform dynamic allocations, send events to other systems, and give you full control over every last detail of how DHCP works in your domain.
As with the static parameters, scripting logic is intended to be as forwards-compatible as possible, so newer versions of the server should work with any extensions you’ve developed, with no issues. (Check the changelog, though)
Organising your setup¶
To avoid potential conflicts, any non-standard functions you may define should be named with a leading underscore. Likewise, any modules you define should also be named with a leading underscore.
If you wish to encapsulate code inside of a module, place it in the
extensions/
subdirectory next to conf.py
or in a subpackage thereof. The
extensions/
subdirectory is added to sys.path
, so imports from a
relative root should work just like they usually do.
In particular, if you expect to have a lot of custom code, you should create a
file in the extensions/
subdirectory named _handlers.py
and import
the main callback functions from there to keep conf.py
clean. At the same
level as the config directives, do something like the following:
from _handlers import (handleUnknownMAC, loadDHCPPacket)
#Where the things imported are the handlers you've defined
Note that, although _handlers
is in extensions/
, it is imported as a
root-level module.
Note also that your handlers module will not have access to functions and
namespace elements defined within conf.py
. For this reason, it is a good
idea to define init()
in conf.py()
to do the run-once work (which is
what most of the built-ins are for), but export the runtime operations to the
new namespace. You can inject needed elements into it as part of init()
,
too:
def init():
import _handlers
_handlers.rfc1035_plus = rfc1035_plus
Important data-types¶
staticDHCPd has some local data-types that are used throughout the framework:
databases.generic.Definition
statistics.Statistics
Customising DHCP behaviour¶
Several functions are provisioned as hooks from staticDHCPd’s core. To
make use of one, just create a function in conf.py
with the corresponding
signature.
All of these functions can share packet-specific data using libpydhcpserver’s
meta
attribute on the packet object. This is a dictionary that neither
staticDHCPd nor libpydhcpserver ever touch, letting you use it for
whatever you want.
init()¶
-
init
() Called immediately after all of staticDHCPd’s other subsystems have been configured.
Example¶
def _tick_logger():
#A trivial function that writes to the log whenever a tick occurs
logger.debug("Ticked!")
def init():
callbacks.systemAddTickHandler(_tick_logger)
When init() is called, the _tick_logger() function, also defined in
conf.py
, will be registered to be invoked every time the system generates
a “tick” event.
To do something before staticDHCPd has finished bringing up its own subsystems, write your logic at the same level as the parameter definitions, where _tick_logger() is defined.
filterPacket()¶
-
filterPacket
(packet, method, mac, client_ip, relay_ip, port)¶ Provides a means of writing your own blacklist logic, excluding packets from sources you don’t trust, that have done weird things, or under any other conceivable circumstance.
It is called before the MAC is looked up in the database or by
handleUnknownMAC()
. ReturningTrue
will cause the MAC to proceed through the chain as normal, while returningFalse
will cause it to be discarded. ReturningNone
will put it into the temporary blacklist, like other MACs that trip staticDHCPd’s configurable thresholds.Parameters: - packet –
The packet received from the client, an instance of
libpydhcpserver.dhcp_types.packet.DHCPPacket
.Any changes made to this packet will persist.
- method (str) – The type of DHCP request the packet represents, one of
DECLINE
,DISCOVER
,INFORM
,RELEASE
,REQUEST:INIT-REBOOT
,REQUEST:REBIND
,REQUEST:RENEW
,REQUEST:SELECTING
. - mac – The MAC of the client, an instance of
libpydhcpserver.dhcp_types.mac.MAC
. - client_ip – The client’s requested IP address (may be
None
), an instance oflibpydhcpserver.dhcp_types.ipv4.IPv4
. - relay_ip – The relay used by the client (may be
None
), an instance oflibpydhcpserver.dhcp_types.ipv4.IPv4
. - port (int) – The port on which the packet was received.
Returns: False
if the packet should be rejected;True
if it should be accepted;None
if the source should be ignored temporarily.- packet –
Example¶
import random
def filterPacket(packet, method, mac, client_ip, relay_ip, port):
return random.random() > 0.2
This will fake a lossy network, dropping 20% of all packets received.
handleUnknownMAC()¶
-
handleUnknownMAC
(packet, method, mac, client_ip, relay_ip, port)¶ If staticDHCPd gets a request to serve a MAC that it does not recognise, this function will be invoked, allowing you to query databases of your own to fill in the blanks.
Parameters: - packet –
The packet received from the client, an instance of
libpydhcpserver.dhcp_types.packet.DHCPPacket
.Any changes made to this packet will persist.
- method (str) – The type of DHCP request the packet represents, one of
DECLINE
,DISCOVER
,INFORM
,RELEASE
,REQUEST:INIT-REBOOT
,REQUEST:REBIND
,REQUEST:RENEW
,REQUEST:SELECTING
. - mac – The MAC of the client, an instance of
libpydhcpserver.dhcp_types.mac.MAC
. - client_ip – The client’s requested IP address (may be
None
), an instance oflibpydhcpserver.dhcp_types.ipv4.IPv4
. - relay_ip – The relay used by the client (may be
None
), an instance oflibpydhcpserver.dhcp_types.ipv4.IPv4
. - port (int) – The port on which the packet was received.
Returns: An instance of
databases.generic.Definition
orNone
, if the MAC could not be handled.- packet –
Example¶
import databases.generic.Definition
def handleUnknownMAC(packet, method, mac, client_ip, relay_ip, port):
if mac == 'aa:bb:cc:dd:ee:ff':
return databases.generic.Definition(
ip='192.168.0.100', lease_time=600,
subnet='192.168.0.0/24', serial=0,
hostname='guestbox',
#gateways=None, #The old format didn't support per-definition gateways
subnet_mask='255.255.255.0',
broadcast_address='192.168.0.255',
domain_name='guestbox.example.org.',
domain_name_servers=['192.168.0.5', '192.168.0.6', '192.168.0.7'],
ntp_servers=['192.168.0.8', '192.168.0.9'],
)
return None
It is difficult to provide a general example of how to use this function, since its role is basically that of a code-driven database. When you need to use it, you will know.
filterRetrievedDefinitions()¶
-
filterRetrievedDefinitions
(definitions, packet, method, mac, client_ip, relay_ip, port)¶ Some databases produce collections of
databases.generic.Definition
objects, rather than simply returning one orNone
. This function allows you to use runtime information, not necessarily passed back to the database, to make a decision about whichdatabases.generic.Definition
to use for processing the request.Parameters: - definitions – A collection of :class:`databases.generic.Definition`s.
- packet –
The packet received from the client, an instance of
libpydhcpserver.dhcp_types.packet.DHCPPacket
.Any changes made to this packet will persist.
- method (str) – The type of DHCP request the packet represents, one of
DECLINE
,DISCOVER
,INFORM
,RELEASE
,REQUEST:INIT-REBOOT
,REQUEST:REBIND
,REQUEST:RENEW
,REQUEST:SELECTING
. - mac – The MAC of the client, an instance of
libpydhcpserver.dhcp_types.mac.MAC
. - client_ip – The client’s requested IP address (may be
None
), an instance oflibpydhcpserver.dhcp_types.ipv4.IPv4
. - relay_ip – The relay used by the client (may be
None
), an instance oflibpydhcpserver.dhcp_types.ipv4.IPv4
. - port (int) – The port on which the packet was received.
Returns: An instance of
databases.generic.Definition
orNone
, if the definitons could not be processed.
Example¶
This is a very site-specific feature, since no built-in database modules support cases with MAC-collisions.
Likely users of this feature will be heavy users of VM environments, where images may be loaded on multiple systems in various subnets, without the MAC being redefined.
Before resorting to this approach for resolving such conflicts, consider using handleUnknownMAC() and passing the parameters it receives to your database engine. filterRetrievedDefinitions() is appropriate only in the case where the database layer cannot do additional processing on its own or runtime context is only available on the DHCP server for technical reasons.
loadDHCPPacket()¶
-
loadDHCPPacket
(packet, method, mac, definition, relay_ip, port, source_packet)¶ Before any response is sent to a client, an opportunity is presented to allow you to modify the packet, adding or removing options and setting values as needed for your environment’s specific requirements. Or even allowing you to define your own blacklist rules and behaviour.
Parameters: - packet – The packet to be sent to the client, an instance of
libpydhcpserver.dhcp_types.packet.DHCPPacket
. - method (str) – The type of DHCP request the packet represents, one of
DECLINE
,DISCOVER
,INFORM
,RELEASE
,REQUEST:INIT-REBOOT
,REQUEST:REBIND
,REQUEST:RENEW
,REQUEST:SELECTING
. - mac – The MAC of the client, an instance of
libpydhcpserver.dhcp_types.mac.MAC
. - definition – The lease-definition provided via MAC-lookup, an instance
of
databases.generic.Definition
. - relay_ip – The relay used by the client (may be
None
), an instance oflibpydhcpserver.dhcp_types.ipv4.IPv4
. - port (int) – The port on which the packet was received.
- source_packet –
The packet received from the client, an instance of
libpydhcpserver.dhcp_types.packet.DHCPPacket
.This is a pristine copy of the original packet, unaffected by any previous modifications.
Returns: True
if processing can proceed;False
if the packet should be rejected.- packet – The packet to be sent to the client, an instance of
Example¶
import random
def loadDHCPPacket(packet, method, mac, definition, relay_ip, port, source_packet):
if not definition.ip[3] % 3: #The client's IP's fourth octet is a multiple of 3
packet.setOption('renewal_time_value', 60)
elif method.startswith('REQUEST:') and random.random() < 0.5:
packet.transformToDHCPNakPacket()
elif random.random() < 0.1:
return False
return True
This will set the renewal-time (T1) for clients to one minute if they have an IP that ends in a multiple of 3.
If the first qualifier isn’t satisfied and it’s a REQUEST-type packet, there’s a 50% chance that it will be changed into a NAK response.
Lastly, if neither of the previous conditions were met, there’s a 10% chance the packet will simply be dropped.
Using system callbacks¶
A number of callbacks exist that let you hook your code into staticDHCPd’s core functions and modules. All of these are accessible from anywhere within conf.py.
-
callbacks.
systemAddReinitHandler
(callback)¶ Registers a reinitialisation callback.
Parameters: callback (callable) – A callable that takes no arguments; if already present, it will not be registered a second time.
-
callbacks.
systemRemoveReinitHandler
(callback)¶ Unregisters a reinitialisation callback.
Parameters: callback (callable) – The callback to remove. Returns: True if a callback was removed.
-
callbacks.
systemAddTickHandler
(callback)¶ Registers a tick callback. Tick callbacks are invoked approximately once per second, but should treat this as a wake-up, not a metronome, and query the system-clock if performing any time-sensitive operations.
Parameters: callback (callable) – A callable that takes no arguments; if already present, it will not be registered a second time. The given callable must not block for any significant amount of time.
-
callbacks.
systemRemoveTickHandler
(callback)¶ Unregisters a tick callback.
Parameters: callback (callable) – The callback to remove. Return bool: True if a callback was removed.
-
callbacks.
statsAddHandler
(callback)¶ Registers a statistics callback.
Parameters: callback (callable) – A callable that takes statistics.Statistics
as its argument; if already present, it will not be registered a second time. This function must never block for any significant amount of time.
-
callbacks.
statsRemoveHandler
(callback)¶ Unregisters a statistics callback.
Parameters: callback (callable) – The callable to be removed. Return bool: True if a callback was removed.
-
callbacks.
WEB_METHOD_DASHBOARD
¶ The content is rendered before the dashboard.
-
callbacks.
WEB_METHOD_TEMPLATE
¶ The content is rendered in the same container that would normally show the dashboard, but no dashboard elements are present.
-
callbacks.
WEB_METHOD_RAW
¶ The content is presented exactly as returned, identified by the given MIME-type.
-
callbacks.
webAddHeader
(callback)¶ Installs an element in the headers; at most one instance of any given
callback
will be accepted.Parameters: callback (callable) – Must accept the parameters path, queryargs, mimetype, data, and headers, with the possibility that mimetype and data may be None; queryargs is a dictionary of parsed query-string items, with values expressed as lists of strings; headers is a dictionary-like object.
It must return data as a string, formatted as XHTML, to be embedded inside of <head/>, or None to suppress inclusion.
-
callbacks.
webRemoveHeader
(callback)¶ Removes a header element.
Parameters: callback (callable) – The element to be removed. Return bool: True if an element was removed.
-
callbacks.
webAddDashboard
(module, name, callback, ordering=None)¶ Installs an element in the dashboard; at most one instance of any given
callback
will be accepted.Parameters: - module (basestring) – The name of the module to which this element belongs.
- name (basestring) – The name under which to display the element.
- callback (callable) –
Must accept the parameters path, queryargs, mimetype, data, and headers, with the possibility that mimetype and data may be None; queryargs is a dictionary of parsed query-string items, with values expressed as lists of strings; headers is a dictionary-like object.
It must return data as a string, formatted as XHTML, to be embedded inside of a <div/>, or None to suppress inclusion.
- ordering (int) – A number that controls where this element will appear in relation to others. If not specified, the value will be that of the highest number plus one, placing it at the end; negatives are valid.
-
callbacks.
webRemoveDashboard
(callback)¶ Removes a dashboard element.
Parameters: callback (callable) – The element to be removed. Return bool: True if an element was removed.
-
callbacks.
webAddMethod
(path, callback, cacheable=False, hidden=True, secure=False, module=None, name=None, confirm=False, display_mode=WEB_METHOD_RAW)¶ Installs a webservice method; at most one instance of
path
will be accepted.Parameters: - path (basestring) – The location at which the service may be called, like “/ca/uguu/puukusoft/staticDHCPd/extension/stats/histograph.csv”.
- callback (callable) –
Must accept the parameters path, queryargs, mimetype, data, and headers, with the possibility that mimetype and data may be None; queryargs is a dictionary of parsed query-string items, with values expressed as lists of strings; headers is a dictionary-like object.
It must return a tuple of (mimetype, data, headers), with data being a string or bytes-like object.
- cacheable (bool) – Whether the client is allowed to cache the method’s content.
- hidden (bool) – Whether to render a link in the side-bar.
- secure (bool) – Whether authentication will be required before this method can be called.
- module (basestring) – The name of the module to which this element belongs.
- name (basestring) – The name under which to display the element.
- confirm (bool) – Adds JavaScript validation to ask the user if they’re sure they know what they’re doing before the method will be invoked, if not hidden.
- display_mode – One of the WEB_METHOD_* constants.
-
callbacks.
webRemoveMethod
(path)¶ Removes a method element.
Parameters: path (basestring) – The element to be removed. Return bool: True if an element was removed.
Logging facilities¶
staticDHCPd uses Python’s native logging framework:
logger.debug("The value of some parameter is %(param)r" % {
'param': my_variable,
})
logger.info("Some step finished")
logger.warn("The client is supposed to have been decommissioned")
logger.error("The client provided invalid data")
logger.critical("The database is offline")
In any modules you create, do the following at the start to hook into it:
import logging
logger = logging.getLogger('your-extension')
For backwards-compatibility reasons, an alias for the warning level is provided; please do not use this and be sure to change any existing code:
writeLog("Something happened")
conf.py
Environment¶
A number of convenience resources are present in conf.py
’s namespace by
default; these are enumerated here so you know what’s provided out-of-the-box.
Conversion functions¶
Various functions from libpydhcpserver. It is very rare that you will need to make use of these directly from 2.0.0 onwards, but they exist for backwards-compatibility and special cases.
- listToIP(
[127, 0, 0, 1]
) ->IPv4
- listToIPs(
[127, 0, 0, 1, 127, 0, 0, 2]
) ->[IPv4, IPv4]
- ipToList(
IPv4
) ->[127, 0, 0, 1]
- ipsToList(
[IPv4, IPv4]
) ->[127, 0, 0, 1, 127, 0, 0, 2]
- listToInt(
[127, 10]
) ->32522
- listToInts(
[127, 10, 127, 9]
) ->[32522, 32521]
- listToLong(
[16, 23, 127, 10]
) ->269975306
- listToLongs(
[16, 23, 127, 10, 16, 23, 127, 9]
) ->[269975306, 269975305]
- intToList(
32522
) ->[127, 10]
- intsToList(
[32522, 32521]
) ->[127, 10, 127, 9]
- longToList(
269975306
) ->[16, 23, 127, 10]
- longsToList(
[269975306, 269975305]
) ->[16, 23, 127, 10, 16, 23, 127, 9]
- strToList(
'hello'
) ->[104, 101, 108, 108, 111]
- strToPaddedList(
'hello', 7
) ->[104, 101, 108, 108, 111, 0, 0]
- listToStr(
[104, 101, 108, 108, 111]
) ->'hello'
RFC interfaces¶
Also from libpydhcpserver is the RFC utility-set. You may need to use these at some point, so it is worth reading libpydhcpserver’s documentation for more information.
- rfc3046_decode
- rfc3925_decode
- rfc3925_125_decode
- rfc1035_plus
- rfc2610_78
- rfc2610_79
- rfc3361_120
- rfc3397_119
- rfc3442_121
- rfc3925_124
- rfc3925_125
- rfc4174_83
- rfc4280_88
- rfc5223_137
- rfc5678_139
- rfc5678_140