diff --git a/README.md b/README.md index 4820960..8dc06a0 100644 --- a/README.md +++ b/README.md @@ -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; @@ -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]).