Skip to content

Commit

Permalink
Improve documentation and add new generic lookup functions
Browse files Browse the repository at this point in the history
  • Loading branch information
cygnusb committed Feb 11, 2019
1 parent b0b0fb1 commit be95a44
Showing 1 changed file with 86 additions and 173 deletions.
259 changes: 86 additions & 173 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,19 @@
pypureomapi
===========

pypureomapi is a Python implementation of the DHCP OMAPI protocol used in the most popular Linux DHCP server from ISC. It can be used to query and modify leases and other objects exported by an ISC DHCP server. The interaction can be authenticated using HMAC-MD5. Besides basic ready to use operations, custom interaction can be implemented with limited effort. It can be used as a drop-in replacement for pyomapic, but provides error checking and extensibility beyond pyomapic.
pypureomapi is a Python implementation of the DHCP OMAPI protocol used in the most popular Linux DHCP server from ISC.
It can be used to query and modify leases and other objects exported by an ISC DHCP server.
The interaction can be authenticated using HMAC-MD5. Besides basic ready to use operations, custom interaction can be implemented with limited effort.
It can be used as a drop-in replacement for pyomapic, but provides error checking and extensibility beyond pyomapic.

# Example omapi lookup

```
from __future__ import print_function
import pypureomapi
KEYNAME="defomapi"
BASE64_ENCODED_KEY="+bFQtBCta6j2vWkjPkNFtgA=="
lease_ip = "192.168.0.250" # ip of some host with a dhcp lease on your dhcp server
dhcp_server_ip="127.0.0.1"
port = 7911 # Port of the omapi service
try:
o = pypureomapi.Omapi(dhcp_server_ip,port, KEYNAME, BASE64_ENCODED_KEY)
mac = o.lookup_mac(lease_ip)
print("%s is currently assigned to mac %s" % (lease_ip, mac))
except pypureomapi.OmapiErrorNotFound:
print("%s is currently not assigned" % (lease_ip,))
except pypureomapi.OmapiError, err:
print("an error occurred: %r" % (err,))
```

# Server side configugration for ISC DHCP3
## Server side configugration for ISC DHCP3

To allow a OMAPI access to your ISC DHCP3 DHCP Server you should define the following in your dhcpd.conf config file:

```
key defomapi {
algorithm hmac-md5;
secret +bFQtBCta6j2vWkjPkNFtgA==; # FIXME: replace by your own dnssec key (see below)
secret +bFQtBCta6j2vWkjPkNFtgA==; # FIXME: replace by your own dnssec key (see below)!!!
};
omapi-key defomapi;
Expand All @@ -59,181 +39,114 @@ is possible to generate the key value for the config file directly:
dd if=/dev/urandom bs=16 count=1 2>/dev/null | openssl enc -e -base64
```

# Create Group
## Example omapi lookup

This is a short example, of how to use basic lookup functions **lookup_mac** and **lookup_ip** to quickly query a DHCP lease on a ISC DHCP Server.

A group needs at least one statement. See UseCaseSupersedeHostname for example statements.
```
def add_group(omapi, groupname, statements):
"""
@type omapi: Omapi
@type groupname: bytes
@type statements: str
"""
msg = OmapiMessage.open("group")
msg.message.append(("create", struct.pack("!I", 1)))
msg.obj.append(("name", groupname))
msg.obj.append(("statements", statements))
response = self.query_server(msg)
if response.opcode != OMAPI_OP_UPDATE:
raise OmapiError("add group failed")
```

And with that, to attach a new host to a group:
```
def add_host_with_group(omapi, ip, mac, groupname):
msg = OmapiMessage.open("host")
msg.message.append(("create", struct.pack("!I", 1)))
msg.message.append(("exclusive", struct.pack("!I", 1)))
msg.obj.append(("hardware-address", pack_mac(mac)))
msg.obj.append(("hardware-type", struct.pack("!I", 1)))
msg.obj.append(("ip-address", pack_ip(ip)))
msg.obj.append(("group", groupname))
response = omapi.query_server(msg)
if response.opcode != OMAPI_OP_UPDATE:
raise OmapiError("add failed")
```
from __future__ import print_function
import pypureomapi
# Supersede Hostname
KEYNAME="defomapi"
BASE64_ENCODED_KEY="+bFQtBCta6j2vWkjPkNFtgA==" # FIXME: be sure to replace this by your own key!!!
See http://jpmens.net/2011/07/20/dynamically-add-static-leases-to-dhcpd/ for the original idea.
dhcp_server_ip="127.0.0.1"
port = 7911 # Port of the omapi service
```
def add_host_supersede_name(omapi, ip, mac, name):
"""Add a host with a fixed-address and override its hostname with the given name.
@type omapi: Omapi
@type ip: str
@type mac: str
@type name: str
@raises ValueError:
@raises OmapiError:
@raises socket.error:
"""
msg = OmapiMessage.open("host")
msg.message.append(("create", struct.pack("!I", 1)))
msg.message.append(("exclusive", struct.pack("!I", 1)))
msg.obj.append(("hardware-address", pack_mac(mac)))
msg.obj.append(("hardware-type", struct.pack("!I", 1)))
msg.obj.append(("ip-address", pack_ip(ip)))
msg.obj.append(("name", name))
msg.obj.append(("statement", "supersede host-name %s;" % name))
response = omapi.query_server(msg)
if response.opcode != OMAPI_OP_UPDATE:
raise OmapiError("add failed")
omapi = pypureomapi.Omapi(dhcp_server_ip, port, KEYNAME, BASE64_ENCODED_KEY)
mac = omapi.lookup_mac("192.168.0.250")
print("%s is currently assigned to mac %s" % (lease_ip, mac))
ip = omapi.lookup_ip(mac)
print("%s mac currently has ip %s assigned" % (mac, ip))
```

Similarly the router can be superseded.
If you need full lease information, you can also query the full lease directly by using **lookup_by_lease**, which gives you the full lease details as output:

# add host declaration without static ip
```
def add_host_without_ip(self, mac):
"""Create a host object with given mac address without assigning a static ip address.
@type ip: str
@type mac: str
@raises ValueError:
@raises OmapiError:
@raises socket.error:
"""
msg = OmapiMessage.open(b"host")
msg.message.append((b"create", struct.pack("!I", 1)))
msg.message.append((b"exclusive", struct.pack("!I", 1)))
msg.obj.append((b"hardware-address", pack_mac(mac)))
msg.obj.append((b"hardware-type", struct.pack("!I", 1)))
response = self.query_server(msg)
if response.opcode != OMAPI_OP_UPDATE:
raise OmapiError("add failed")
lease = omapi.lookup_by_lease(mac="24:79:2a:0a:13:c0")
for k, v in res.items():
print("%s: %s" % (k, v))
```

# lookup hostname based on ip address
Output:
```
def lookup_hostname(self, ip):
"""Look up a lease object with given ip address and return the associated client hostname.
@type ip: str
@rtype: str or None
@raises ValueError:
@raises OmapiError:
@raises OmapiErrorNotFound: if no lease object with the given ip
address could be found or the object lacks a hostname
@raises socket.error:
"""
msg = OmapiMessage.open(b"lease")
msg.obj.append((b"ip-address", pack_ip(ip)))
response = self.query_server(msg)
if response.opcode != OMAPI_OP_UPDATE:
raise OmapiErrorNotFound()
try:
return (dict(response.obj)[b"client-hostname"])
except KeyError: # client hostname
raise OmapiErrorNotFound()
state: 2
ip-address: 192.168.10.167
dhcp-client-identifier: b'\x01$y*\x06U\xc0'
subnet: 6126
pool: 6127
hardware-address: 24:79:2a:0a:13:c0
hardware-type: 1
ends: 1549885690
starts: 1549885390
tstp: 1549885840
tsfp: 1549885840
atsfp: 1549885840
cltt: 1549885390
flags: 0
clientip: b'192.168.10.167'
clientmac: b'24:79:2a:0a:13:c0'
clientmac_hostname: b'24792a0a13c0'
vendor-class-identifier: b'Ruckus CPE'
agent.circuit-id: b'\x00\x04\x00\x12\x00-'
agent.remote-id: b'\x00\x06\x00\x12\xf2\x8e!\x00'
agent.subscriber-id: b'wifi-basement'
```

# Get a lease

Original idea from Josh West.
To check if a lease is still valid, you should check ends and state:

```
def get_lease(omapi, ip):
"""
@type omapi: Omapi
@type ip: str
@rtype: OmapiMessage
@raises OmapiErrorNotFound:
@raises socket.error:
"""
msg = OmapiMessage.open("lease")
msg.obj.append(("ip-address", pack_ip(ip)))
response = omapi.query_server(msg)
if response.opcode != OMAPI_OP_UPDATE:
raise OmapiErrorNotFound()
return response
if lease["ends"] < time.time() or lease["state"] != 2:
print("Lease is not valid")
```

# Get an IP from a host MAC address
Most attributes will be decoded directly into the corresponding human readable values.
Converted attributes are ip-address, hardware-address and all 32 bit and 8 bit integer values. If you need raw values, you can add a raw option to the lookup:

```
def lookup_ip_host(self, mac):
"""Lookup a host object with with given mac address.
@type mac: str
@raises ValueError:
@raises OmapiError:
@raises socket.error:
"""
msg = OmapiMessage.open(b"host")
msg.obj.append((b"hardware-address", pack_mac(mac)))
msg.obj.append((b"hardware-type", struct.pack("!I", 1)))
response = self.query_server(msg)
if response.opcode != OMAPI_OP_UPDATE:
raise OmapiErrorNotFound()
try:
return unpack_ip(dict(response.obj)[b"ip-address"])
except KeyError: # ip-address
raise OmapiErrorNotFound()
lease = omapi.lookup_by_lease(mac="24:79:2a:0a:13:c0", raw=True)
for k, v in res.items():
print("%s: %s" % (k, v))
```

# Change Group
Output:

```
def change_group(omapi, name, group):
"""Change the group of a host given the name of the host.
@type omapi: Omapi
@type name: str
@type group: str
"""
m1 = OmapiMessage.open("host")
m1.update_object(dict(name=name))
r1 = omapi.query_server(m1)
if r1.opcode != OMAPI_OP_UPDATE:
raise OmapiError("opening host %s failed" % name)
m2 = OmapiMessage.update(r.handle)
m2.update_object(dict(group=group))
r2 = omapi.query_server(m2)
if r2.opcode != OMAPI_OP_UPDATE:
raise OmapiError("changing group of host %s to %s failed" % (name, group))
b'state': b'\x00\x00\x00\x02'
b'ip-address': b'\xc0\xa8\n\xa7'
...
```

The following lookup functions are implemented, allowing directly querying the different types:

* lookup_ip_host(mac) - lookups up a host object (static defined host) by mac
* lookup_ip(mac) - lookups a lease object by mac and returns the ip
* lookup_host(name) - lookups a host object by name and returns the ip, mac and hostname
* lookup_host_host(mac) - lookups a host object by mac and returns the ip, mac and name
* lookup_hostname(ip) - lookups a lease object by ip and returns the client-hostname

These special functions use:

* lookup_by_host - generic lookup function for host objects
* lookup_by_lease - generic lookup function for lease objects

which provide full access to complete lease data.

## Add and delete host objects

For adding and deleting host objects (static DHCP leases), there are multiple functions:

* add_host(ip, mac)
* add_host_supersede_name(ip, mac, name)
* add_host_without_ip(mac)
* add_host_supersede(ip, mac, name, hostname=None, router=None, domain=None)
* add_group(groupname, statements)
* add_host_with_group(ip, mac, groupname))

See http://jpmens.net/2011/07/20/dynamically-add-static-leases-to-dhcpd/ for original idea (which is now merged) and detailed explanation.

# Custom Integration

Assuming there already is a connection named `o` (i.e. a `Omapi` instance, see [Example]).
Expand Down

0 comments on commit be95a44

Please sign in to comment.