Practical recipies that everyone can use!

While there’s a lot of stuff that you can do with the scripting toolset, figuring out how to get started, especially if you’re not already familiar with Python, can be a bit overwhelming. That’s what this document is for: loaded with examples, it serves as a crash-course for tweaking your environment to do special things that will really help to make your life easier.

Pre-requisites

There are a few things that you will need to understand before diving into these examples. Nothing difficult or long, but things that are essential nonetheless.

libpydhcpserver

staticDHCPd unapologetically uses resources from libpydhcpserver. Reading the examples section of its documentation, which is always distributed alongside this one, should be considered necessary.

Python

staticDHCPd’s configuration, conf.py, is a living, breathing chunk of Python source code. As such, when working with it, Python coding conventions must be followed.

If anything mentioned here doesn’t make sense, search the Internet for a “hello, world!” Python script and do a bit of exploratory hacking.

Whitespace

Python is whitespace-sensitive. All this means, really, is that putting spaces before every line you write is important and that the number of spaces must be consistent. (And it’s something you should do anyway, since indented code is much easier to read)

When adding code to a scripting method, the standard convention is to indent it with four spaces, like this:

def loadDHCPPacket(...):
    packet.setOption('renewal_time_value', 60)
    if packet.isOption('router'):
        packet.setOption('domain_name', 'uguu.ca')
        logger.info("domain name set to 'uguu.ca'")

    #blank lines, like the one above, are optional; your code should be readable
    logger.info("processing done")
    return True

Strings

A string is a sequence of bytes, usually text, like 'hello'. It may be single- or double-quoted, and, if you need to put the same type of quotation you used to start the string somewhere in the middle, it can be “escaped” by prefixing it with a backslash: "Static assignments aren't really \"leases\".".

Numbers

Integers, referred to as “ints”, should be familiar, and “floating-point” values, also known as “floats”, are just numbers with a decimal component, like 64.53.

Conditionals

You probably won’t want logic to execute in all cases and that’s what the if statement is for. Rather than trying to learn from an explanation, just read the examples below and its use will become apparent quickly.

Comparators like >, <=, and != should be pretty obvious, but you will need to use == to test equality, since a single = is used for assignment.

Comments

Anything prefixed with a hash-mark (#) is a comment and will not be processed.

Sequences

Lists, tuples, arrays, strings… Whatever they are, they are indexible, meaning that you can access any individual element if you know its position. The only real catch here is that everything starts at 0, not 1:

x = [1, 2, 8, 'hello']
x[0] #This is the value 1
x[2] #This is the value 8

Evaluation

In Python, it is common to see things like the following:

clients = some_function_that_returns_a_number_or_a_sequence()
if not clients:
    #do something

When x is evaluated, it is asked if it holds a meaningful value and this is used to determine whether it is equivalent to True or False for the comparison. Numbers are False if equal to 0, sequences are False when empty, and None is always False. The not keyword is a more readable variant of !, meaning that True/False should be flipped.

Returns

A return statement may be placed anywhere inside of a function. Its purpose is to end execution and report a result.

The convention within staticDHCPd is to have return True indicate that everything is good and processing should continue, while return False means that the packet should be rejected. For your own sanity, when rejecting a packet, you should log the reason why.

Examples

This section will grow as new examples are created; if you let us know how to do something cool or you ask a question and the result of the exchange boils down to a handy snippet, it will probably show up here.

Gateway configuration

Tell all clients with an IP address ending in a multiple of 3 to use 192.168.1.254 as their default gateway:

def loadDHCPPacket(...):
    #...
    if definition.ip[3] % 3 == 0:
        logger.info("I'm a log message. Please use me!")
        packet.setOption('router', '192.168.1.254')
    #...
    return True

Here, the modulus-by-3 of the last octet (zero-based array) of the IP address to associate with the client is checked to see if it is zero. If so, the “router” option (DHCP option 3) is set to 192.168.1.254

Prevent clients in all "192.168.0.0/24" subnets from having a default gateway:

def loadDHCPPacket(...):
    #...
    if definition.subnet == '192.168.0.0/24':
        packet.deleteOption('router')
    #...
    return True

“subnet”, which is the database’s “subnet” field, not that of the client’s IP/netmask, is checked to see if it matches. If so, then the “router” option is discarded.

Override renewal times

Set T1 to 60 seconds:

def loadDHCPPacket(...):
    #...
    packet.setOption('renewal_time_value', 60)
    #...
    return True

Adjust domain names

Set the client’s domain name to “example.com” if the request was relayed, but refuse to respond if it was relayed from 10.0.0.1:

def loadDHCPPacket(...):
    #...
    if relay_ip: #The request was relayed
        if relay_ip == "10.0.0.1":
            return False #Abort processing
        packet.setOption('domain_name', 'example.com')
    #...
    return True

Here, relay_ip (DHCP field “giaddr”), is checked to see if it was set, indicating that this request was relayed. The IP of the relay server is then compared and, if it matches, “domain_name” is set to “example.com”.

Working with option 82

Refuse relays without “relay_agent” (DHCP option 82)’s agent-ID set to [1, 2, 3]:

def loadDHCPPacket(...):
    #...
    if relay_ip: #The request was relayed
        relay_agent = packet.getOption('relay_agent')
        if relay_agent and not rfc3046_decode(relay_agent)[1] == [1, 2, 3]:
            logger.warn("Rejecting relayed request from [1, 2, 3]")
            return False
    #...
    return True

This allows any non-relayed requests to pass through. Any relayed requests missing option 82 will be allowed (more on this below); any instances of option 82 with an invalid agent-ID (sub-option 1) will be ignored. Any instances of option 82 missing sub-option 1 will generate an error (described in the next example).

Even relay agents configured to set option 82 will omit it if the resulting DHCP packet would be too large. For this reason, it’s important to limit the relay IPs allowed in the config settings.

Managing errors

Do something to generate an error for testing purposes:

def loadDHCPPacket(...):
    #...
    if not packet.setOption('router', [192])):
        raise Exception("192 is not a valid IP")
    #...
    return True

The reason why this fails should be obvious, though it is worth noting that setOption() returns False on failure, rather than raising an exception of its own. This was done because it seemed easier for scripting novices to work with while staticDHCPd was still in its infancy.

What’s important here is that raising any sort of exception in loadDHCPPacket() prevents the DHCP response from being sent, but it will help to debug problems by printing or e-mailing a thorough description of the exception that occurred.