diff -Nru pypureomapi-0.4/debian/changelog pypureomapi-0.8/debian/changelog --- pypureomapi-0.4/debian/changelog 2019-10-05 02:02:53.000000000 +0000 +++ pypureomapi-0.8/debian/changelog 2020-01-29 08:58:32.000000000 +0000 @@ -1,3 +1,12 @@ +pypureomapi (0.8-1) unstable; urgency=medium + + * New upstream release + * Add dh-python to build-dependency; Closes: #950061 + * Use updated description from upstream + * Update Debian policy + + -- Dr. Torge Szczepanek Wed, 29 Jan 2020 09:58:32 +0100 + pypureomapi (0.4-1.1) unstable; urgency=medium * Non-maintainer upload. @@ -13,7 +22,7 @@ * Use new-style classes * Disabled doctests - to be included again later in next upstream release * Change project source from Google Code to Github after upstream migration - * Change maintainer to Torge Szczepanek and remove + * Change maintainer to Torge Szczepanek and remove Helmut Grohne from uploaders [ Helmut Grohne ] * Bump python dependency to 2.6 since we need it. diff -Nru pypureomapi-0.4/debian/control pypureomapi-0.8/debian/control --- pypureomapi-0.4/debian/control 2019-10-05 02:02:38.000000000 +0000 +++ pypureomapi-0.8/debian/control 2020-01-29 08:58:32.000000000 +0000 @@ -1,22 +1,21 @@ Source: pypureomapi Maintainer: Dr. Torge Szczepanek -Standards-Version: 3.9.6 +Standards-Version: 4.4.1 Section: python -Priority: extra +Priority: optional Homepage: https://github.com/CygnusNetworks/pypureomapi -Build-Depends: debhelper (>= 9), python3, python3-all -X-Python-Version: >= 2.6 -X-Python3-Version: >= 3.3 +Build-Depends: debhelper (>= 9), python3, python3-all, dh-python +X-Python3-Version: >= 3.7 Package: python3-pypureomapi Architecture: all Depends: ${python3:Depends}, ${misc:Depends} Description: ISC DHCP OMAPI protocol implementation in Python3 - This module grew out of frustration about pyomapi and later pyomapic. The - extension modules mentioned can be used to query the ISC DHCP server for - information about leases. pyomapic does this job using swig and the static - library provided with ISC DHCP. It leaks and has basically no error checking. - Adding error checking later turned out to be a maintenance hell with swig, so - a pure Python implementation for omapi, pypureomapi was born. It can mostly - be used as a drop-in replacement for pyomapic. - This is the py3 version of the module. + 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. diff -Nru pypureomapi-0.4/debian/copyright pypureomapi-0.8/debian/copyright --- pypureomapi-0.4/debian/copyright 2015-07-13 18:23:42.000000000 +0000 +++ pypureomapi-0.8/debian/copyright 2020-01-29 08:58:32.000000000 +0000 @@ -1,10 +1,10 @@ -Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: pypureomapi Upstream-Contact: Dr. Torge Szczepanek Source: https://github.com/CygnusNetworks/pypureomapi Files: * -Copyright: 2010-2015, Cygnus Networks GmbH +Copyright: 2010-2020, Cygnus Networks GmbH License: Apache-2.0 Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff -Nru pypureomapi-0.4/.gitignore pypureomapi-0.8/.gitignore --- pypureomapi-0.4/.gitignore 2015-07-02 17:02:47.000000000 +0000 +++ pypureomapi-0.8/.gitignore 2019-03-26 11:29:38.000000000 +0000 @@ -1,5 +1,5 @@ .idea *.pyc -atlassian-ide-plugin.xml __pycache__ build +test_live_omapi.py diff -Nru pypureomapi-0.4/LICENSE.md pypureomapi-0.8/LICENSE.md --- pypureomapi-0.4/LICENSE.md 2015-07-02 17:02:47.000000000 +0000 +++ pypureomapi-0.8/LICENSE.md 2019-03-26 11:29:38.000000000 +0000 @@ -175,7 +175,7 @@ END OF TERMS AND CONDITIONS - Copyright 2010-2015 Cygnus Networks GmbH + Copyright 2010-2017 Cygnus Networks GmbH Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff -Nru pypureomapi-0.4/pypureomapi.py pypureomapi-0.8/pypureomapi.py --- pypureomapi-0.4/pypureomapi.py 2015-07-02 17:02:47.000000000 +0000 +++ pypureomapi-0.8/pypureomapi.py 2019-03-26 11:29:38.000000000 +0000 @@ -1,9 +1,10 @@ #!/usr/bin/env python # -*- coding: utf8 -*- -# +# pylint:disable=too-many-lines + # library for communicating with an isc dhcp server over the omapi protocol # -# Copyright 2010-2015 Cygnus Networks GmbH +# Copyright 2010-2017 Cygnus Networks GmbH # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -33,18 +34,9 @@ # dictionary = entry* 0x00 0x00 # entry = key (net16str) value (net32str) -__author__ = "Helmut Grohne, Dr. Torge Szczepanek" -__copyright__ = "Cygnus Networks GmbH" -__license__ = "Apache-2.0" -__version__ = "0.4" -__maintainer__ = "Dr. Torge Szczepanek" -__email__ = "debian@cygnusnetworks.de" - - -__all__ = [] - import binascii import struct +import hashlib import hmac import io import logging @@ -56,6 +48,16 @@ except NameError: basestring = str # pylint:disable=W0622 +__author__ = "Helmut Grohne, Dr. Torge Szczepanek" +__copyright__ = "Cygnus Networks GmbH" +__license__ = "Apache-2.0" +__version__ = "0.8" +__maintainer__ = "Dr. Torge Szczepanek" +__email__ = "debian@cygnusnetworks.de" + + +__all__ = [] + logger = logging.getLogger("pypureomapi") sysrand = random.SystemRandom() @@ -77,12 +79,14 @@ opmap = {1: "open", 2: "refresh", 3: "update", 4: "notify", 5: "status", 6: "delete"} return opmap.get(opcode, "unknown (%d)" % opcode) + __all__.append("OmapiError") class OmapiError(Exception): """OMAPI exception base class.""" + __all__.append("OmapiSizeLimitError") @@ -91,6 +95,7 @@ def __init__(self): OmapiError.__init__(self, "Packet size limit reached.") + __all__.append("OmapiErrorNotFound") @@ -100,6 +105,15 @@ OmapiError.__init__(self, "not found") +__all__.append("OmapiErrorAttributeNotFound") + + +class OmapiErrorAttributeNotFound(OmapiErrorNotFound): + """Attribute not found.""" + def __init__(self): # pylint:disable=super-init-not-called + OmapiError.__init__(self, "attribute not found") # pylint:disable=non-parent-init-called + + class OutBuffer(object): """Helper class for constructing network packets.""" sizelimit = 65536 @@ -352,12 +366,13 @@ @rtype: bytes @returns: a signature of length self.authlen """ - return hmac.HMAC(self.key, message).digest() + return hmac.HMAC(self.key, message, digestmod=hashlib.md5).digest() + __all__.append("OmapiMessage") -class OmapiMessage(object): +class OmapiMessage(object): # pylint:disable=too-many-instance-attributes """ @type authid: int @ivar authid: The id of the message authenticator. @@ -378,7 +393,7 @@ @ivar signature: A signature on this message as generated by an authenticator. """ - def __init__(self, authid=0, opcode=0, handle=0, tid=0, rid=0, message=None, obj=None, signature=b""): + def __init__(self, authid=0, opcode=0, handle=0, tid=0, rid=0, message=None, obj=None, signature=b""): # pylint:disable=too-many-arguments """ Construct an OmapiMessage from the given fields. No error checking is performed. @@ -525,7 +540,7 @@ @rtype: str @returns: a barely human readable representation in one line """ - return ("authid=%d authlen=%d opcode=%s handle=%d tid=%d rid=%d message=%r obj=%r signature=%r") % (self.authid, len(self.signature), repr_opcode(self.opcode), self.handle, self.tid, self.rid, self.message, self.obj, self.signature) + return "authid=%d authlen=%d opcode=%s handle=%d tid=%d rid=%d message=%r obj=%r signature=%r" % (self.authid, len(self.signature), repr_opcode(self.opcode), self.handle, self.tid, self.rid, self.message, self.obj, self.signature) def parse_map(filterfun, parser): @@ -559,7 +574,7 @@ """ items = [] for parser in args: - for element in parser(*items): + for element in parser(*items): # pylint:disable=star-args if element is None: yield None else: @@ -672,7 +687,7 @@ >>> d = b"\\0\\0\\0\\x64\\0\\0\\0\\x18" >>> next(InBuffer(d).parse_startup_message()).validate() """ - return parse_map(lambda args: OmapiStartupMessage(*args), parse_chain(self.parse_net32int, lambda _: self.parse_net32int())) + return parse_map(lambda args: OmapiStartupMessage(*args), parse_chain(self.parse_net32int, lambda _: self.parse_net32int())) # pylint:disable=star-args def parse_message(self): """results in an OmapiMessage""" @@ -686,7 +701,8 @@ lambda *_: self.parse_bindict(), # object lambda *args: self.parse_fixedbuffer(args[1])) # signature return parse_map(lambda args: # skip authlen in args: - OmapiMessage(*(args[0:1] + args[2:])), parser) + OmapiMessage(*(args[0:1] + args[2:])), parser) # pylint:disable=star-args + if isinstance(bytes(b"x")[0], int): def bytes_to_int_seq(b): @@ -699,6 +715,7 @@ def int_seq_to_bytes(s): return "".join([chr(x) for x in s]) # raises ValueError + __all__.append("pack_ip") @@ -725,6 +742,7 @@ parts = [int(x) for x in parts] # raises ValueError return int_seq_to_bytes(parts) # raises ValueError + __all__.append("unpack_ip") @@ -749,6 +767,7 @@ raise ValueError("given buffer is not exactly four bytes long") return ".".join([str(x) for x in bytes_to_int_seq(fourbytes)]) + __all__.append("pack_mac") @@ -776,6 +795,7 @@ parts = [int(part, 16) for part in parts] # raises ValueError return int_seq_to_bytes(parts) # raises ValueError + __all__.append("unpack_mac") @@ -801,10 +821,9 @@ return ":".join(["%2.2x".__mod__(x) for x in bytes_to_int_seq(sixbytes)]) -class LazyStr(object): - - def __init__(self, function): - self.function = function +class LazyStr(object): # pylint:disable=too-few-public-methods + def __init__(self, fnc): + self.function = fnc def __str__(self): return self.function() @@ -812,9 +831,10 @@ class TCPClientTransport(object): """PEP 3156 dummy transport class to support OmapiProtocol class.""" - def __init__(self, protocol, host, port): + def __init__(self, protocol, host, port, timeout=None): self.protocol = protocol self.connection = socket.socket() + self.connection.settimeout(timeout) self.connection.connect((host, port)) self.protocol.connection_made(self) @@ -925,11 +945,12 @@ logger.debug("sending %s", LazyStr(message.dump_oneline)) self.transport.write(message.as_string()) + __all__.append("Omapi") -class Omapi(object): - def __init__(self, hostname, port, username=None, key=None): +class Omapi(object): # pylint:disable=too-many-public-methods + def __init__(self, hostname, port, username=None, key=None, timeout=None): # pylint:disable=too-many-arguments """ @type hostname: str @type port: int @@ -951,7 +972,7 @@ if username is not None and key is not None: newauth = OmapiHMACMD5Authenticator(username, key) - self.transport = TCPClientTransport(self.protocol, hostname, port) + self.transport = TCPClientTransport(self.protocol, hostname, port, timeout=timeout) self.recv_protocol_initialization() @@ -1051,6 +1072,169 @@ self.protocol.defauth = authid logger.debug("successfully initialized default authid %d", authid) + def lookup_ip_host(self, mac): + """Lookup a host object with with given mac address. + + @type mac: str + @raises ValueError: + @raises OmapiError: + @raises OmapiErrorNotFound: if no lease object with the given mac could be found + @raises OmapiErrorAttributeNotFound: if lease could be found, but objects lacks a ip + @raises socket.error: + """ + res = self.lookup_by_host(mac=mac) + try: + return res["ip-address"] + except KeyError: + raise OmapiErrorAttributeNotFound() + + def lookup_ip(self, mac): + """Look for a lease object with given mac address and return the + assigned ip address. + + @type mac: str + @rtype: str or None + @raises ValueError: + @raises OmapiError: + @raises OmapiErrorNotFound: if no lease object with the given mac could be found + @raises OmapiErrorAttributeNotFound: if lease could be found, but objects lacks a ip + @raises socket.error: + """ + res = self.lookup_by_lease(mac=mac) + try: + return res["ip-address"] + except KeyError: + raise OmapiErrorAttributeNotFound() + + def lookup_mac(self, ip): + """Look up a lease object with given ip address and return the + associated mac address. + + @type ip: str + @rtype: str or None + @raises ValueError: + @raises OmapiError: + @raises OmapiErrorNotFound: if no lease object with the given ip could be found + @raises OmapiErrorAttributeNotFound: if lease could be found, but objects lacks a mac + @raises socket.error: + """ + res = self.lookup_by_lease(ip=ip) + try: + return res["hardware-address"] + except KeyError: + raise OmapiErrorAttributeNotFound() + + def lookup_host(self, name): + """Look for a host object with given name and return the + name, mac, and ip address + + @type name: str + @rtype: dict or None + @raises ValueError: + @raises OmapiError: + @raises OmapiErrorNotFound: if no host object with the given name could be found + @raises OmapiErrorAttributeNotFound: if lease could be found, but objects lacks ip, mac or name + @raises socket.error: + """ + res = self.lookup_by_host(name=name) + try: + return dict(ip=res["ip-address"], mac=res["hardware-address"], hostname=res["name"].decode('utf-8')) + except KeyError: + raise OmapiErrorAttributeNotFound() + + def lookup_host_host(self, mac): + """Look for a host object with given mac address and return the + name, mac, and ip address + + @type mac: str + @rtype: dict or None + @raises ValueError: + @raises OmapiError: + @raises OmapiErrorNotFound: if no host object with the given mac address could be found + @raises OmapiErrorAttributeNotFound: if lease could be found, but objects lacks ip, mac or name + @raises socket.error: + """ + res = self.lookup_by_host(mac=mac) + try: + return dict(ip=res["ip-address"], mac=res["hardware-address"], name=res["name"].decode('utf-8')) + except KeyError: + raise OmapiErrorAttributeNotFound() + + 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 + @raises OmapiErrorAttributeNotFound: if lease could be found, but objects lacks a hostname + @raises socket.error: + """ + res = self.lookup_by_lease(ip=ip) + if "client-hostname" not in res: + raise OmapiErrorAttributeNotFound() + return res["client-hostname"].decode('utf-8') + + def lookup_by_host(self, **kwargs): + return self.__lookup("host", **kwargs) + + def lookup_by_lease(self, **kwargs): + return self.__lookup("lease", **kwargs) + + def __lookup(self, ltype, **kwargs): + """Generic Lookup function + + @type ltype: str + @type rvalues: list + @type ip: str + @type mac: str + @type name: str + @rtype: dict or str (if len(rvalues) == 1) or None + @raises ValueError: + @raises OmapiError: + @raises OmapiErrorNotFound: if no host object with the given name + could be found or the object lacks an ip address or mac + @raises socket.error: + """ + ltype_utf = ltype.encode("utf-8") + assert ltype_utf in [b"host", b"lease"] + msg = OmapiMessage.open(ltype_utf) + for k in kwargs: + if k == "raw": + continue + _k = k.replace("_", "-") + if _k in ["ip", "ip-address"]: + msg.obj.append((b"ip-address", pack_ip(kwargs[k]))) + elif _k in ["mac", "hardware-address"]: + msg.obj.append((b"hardware-address", pack_mac(kwargs[k]))) + msg.obj.append((b"hardware-type", struct.pack("!I", 1))) + elif _k == "name": + msg.obj.append((b"name", kwargs[k].encode('utf-8'))) + else: + msg.obj.append((str(k).encode(), kwargs[k].encode('utf-8'))) + response = self.query_server(msg) + if response.opcode != OMAPI_OP_UPDATE: + raise OmapiErrorNotFound() + if "raw" in kwargs and kwargs["raw"]: + return dict(response.obj) + res = dict() + for k, v in dict(response.obj).items(): + _k = k.decode('utf-8') + try: + if _k == "ip-address": + v = unpack_ip(v) + elif _k in ["hardware-address"]: + v = unpack_mac(v) + elif _k in ["starts", "ends", "tstp", "tsfp", "atsfp", "cltt", "subnet", "pool", "state", "hardware-type"]: + v = struct.unpack(">I", v)[0] + elif _k in ["flags"]: + v = struct.unpack(">I", v)[0] + except struct.error: + pass + res[_k] = v + return res + def add_host(self, ip, mac): """Create a host object with given ip address and and mac address. @@ -1070,92 +1254,150 @@ if response.opcode != OMAPI_OP_UPDATE: raise OmapiError("add failed") - def del_host(self, mac): - """Delete a host object with with given mac address. - + def add_host_supersede_name(self, ip, mac, name): # pylint:disable=E0213 + """Add a host with a fixed-address and override its hostname with the given name. + @type self: Omapi + @type ip: str @type mac: str + @type name: 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))) + msg.obj.append((b"ip-address", pack_ip(ip))) + msg.obj.append((b"name", name.encode('utf-8'))) + msg.obj.append((b"statements", 'supersede host-name "{0}";'.format(name).encode('utf-8'))) response = self.query_server(msg) if response.opcode != OMAPI_OP_UPDATE: - raise OmapiErrorNotFound() - if response.handle == 0: - raise OmapiError("received invalid handle from server") - response = self.query_server(OmapiMessage.delete(response.handle)) - if response.opcode != OMAPI_OP_STATUS: - raise OmapiError("delete failed") - - def lookup_ip(self, mac): - """Look for a lease object with given mac address and return the - assigned ip address. + raise OmapiError("add failed") + def add_host_without_ip(self, mac): + """Create a host object with given mac address without assigning a static ip address. @type mac: str - @rtype: str or None @raises ValueError: @raises OmapiError: - @raises OmapiErrorNotFound: if no lease object with the given mac - address could be found or the object lacks an ip address @raises socket.error: """ - msg = OmapiMessage.open(b"lease") + 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 OmapiErrorNotFound() - try: - return unpack_ip(dict(response.obj)[b"ip-address"]) - except KeyError: # ip-address - raise OmapiErrorNotFound() + raise OmapiError("add failed") - def lookup_mac(self, ip): - """Look up a lease object with given ip address and return the - associated mac address. + def add_host_supersede(self, ip, mac, name, hostname=None, router=None, domain=None): # pylint:disable=too-many-arguments + """Create a host object with given ip, mac, name, hostname, router and + domain. hostname, router and domain are optional arguments. @type ip: str - @rtype: str or None - @raises ValueError: + @type mac: str + @type name: str + @type hostname: str + @type router: str + @type domain: str @raises OmapiError: - @raises OmapiErrorNotFound: if no lease object with the given ip - address could be found or the object lacks a mac address @raises socket.error: """ - msg = OmapiMessage.open(b"lease") + stmts = [] + + msg = OmapiMessage.open(b"host") + msg.message.append((b"create", struct.pack("!I", 1))) + msg.obj.append((b"name", name)) + msg.obj.append((b"hardware-address", pack_mac(mac))) + msg.obj.append((b"hardware-type", struct.pack("!I", 1))) msg.obj.append((b"ip-address", pack_ip(ip))) + if hostname: + stmts.append('supersede host-name "{0}";\n '.format(hostname)) + if router: + stmts.append('supersede routers {0};\n '.format(router)) + if domain: + stmts.append('supersede domain-name "{0}";'.format(domain)) + if stmts: + encoded_stmts = "".join(stmts).encode("utf-8") + msg.obj.append((b"statements", encoded_stmts)) + response = self.query_server(msg) if response.opcode != OMAPI_OP_UPDATE: - raise OmapiErrorNotFound() - try: - return unpack_mac(dict(response.obj)[b"hardware-address"]) - except KeyError: # hardware-address - raise OmapiErrorNotFound() + raise OmapiError("add failed") + + def del_host(self, mac): + """Delete a host object with with given mac address. - def add_host_supersede_name(omapi, ip, mac, name): # pylint:disable=E0213 - """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 OmapiErrorNotFound: if no lease object with the given + mac address could be found @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))) - msg.obj.append((b"ip-address", pack_ip(ip))) - msg.obj.append((b"name", name)) - msg.obj.append((b"statements", str.encode('supersede host-name "%s";' % name))) - response = omapi.query_server(msg) + response = self.query_server(msg) + if response.opcode != OMAPI_OP_UPDATE: + raise OmapiErrorNotFound() + if response.handle == 0: + raise OmapiError("received invalid handle from server") + response = self.query_server(OmapiMessage.delete(response.handle)) + if response.opcode != OMAPI_OP_STATUS: + raise OmapiError("delete failed") + + def add_group(self, groupname, statements): + """ + Adds a group + @type groupname: bytes + @type statements: str + """ + msg = OmapiMessage.open(b"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") + + def add_host_with_group(self, ip, mac, groupname): + """ + Adds a host with given ip and mac in a group named groupname + @type ip: str + @type mac: str + @type groupname: str + """ + msg = OmapiMessage.open(b"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 = self.query_server(msg) if response.opcode != OMAPI_OP_UPDATE: raise OmapiError("add failed") -if __name__ == '__main__': + def change_group(self, name, group): + """Change the group of a host given the name of the host. + @type name: str + @type group: str + """ + m1 = OmapiMessage.open(b"host") + m1.update_object(dict(name=name)) + r1 = self.query_server(m1) + if r1.opcode != OMAPI_OP_UPDATE: + raise OmapiError("opening host %s failed" % name) + m2 = OmapiMessage.update(r1.handle) + m2.update_object(dict(group=group)) + r2 = self.query_server(m2) + if r2.opcode != OMAPI_OP_UPDATE: + raise OmapiError("changing group of host %s to %s failed" % (name, group)) + + +if __name__ == "__main__": import doctest doctest.testmod() diff -Nru pypureomapi-0.4/pypureomapi.spec pypureomapi-0.8/pypureomapi.spec --- pypureomapi-0.4/pypureomapi.spec 1970-01-01 00:00:00.000000000 +0000 +++ pypureomapi-0.8/pypureomapi.spec 2019-03-26 11:29:38.000000000 +0000 @@ -0,0 +1,89 @@ +%if 0%{?rhel} && 0%{?rhel} <= 7 +%{!?py2_build: %global py2_build %{__python2} setup.py build} +%{!?py2_install: %global py2_install %{__python2} setup.py install --skip-build --root %{buildroot}} +%endif + +%if (0%{?fedora} >= 21 || 0%{?rhel} >= 8) +%global with_python3 1 +%endif + +%define srcname pypureomapi +%define version 0.8 +%define release 1 +%define sum Cygnus Networks GmbH %{srcname} package + +Name: python-%{srcname} +Version: %{version} +Release: %{release}%{?dist} +Summary: %{sum} +License: proprietary +Source0: python-%{srcname}-%{version}.tar.gz + +BuildArch: noarch +BuildRequires: python2-devel, python-setuptools +%if 0%{?with_check} +BuildRequires: pytest +%endif # with_check +Requires: python-setuptools + +%{?python_provide:%python_provide python-%{project}} + +%if 0%{?with_python3} +BuildRequires: python3-devel +BuildRequires: python3-setuptools +%if 0%{?with_check} +BuildRequires: python3-pytest +%endif # with_check +%endif # with_python3 + +%description +%{sum} + +%if 0%{?with_python3} +%package -n python3-%{project} +Summary: %{sum} +%{?python_provide:%python_provide python3-%{project}} +Requires: python3-setuptools + +%description -n python3-%{project} +%{sum} +%endif # with_python3 + +%prep +%setup -q -n python-%{srcname}-%{version} + +%build +%py2_build + +%if 0%{?with_python3} +%py3_build +%endif # with_python3 + + +%install +%py2_install + +%if 0%{?with_python3} +%py3_install +%endif # with_python3 + +%if 0%{?with_check} +%check +LANG=en_US.utf8 py.test-%{python2_version} -vv tests + +%if 0%{?with_python3} +LANG=en_US.utf8 py.test-%{python3_version} -vv tests +%endif # with_python3 +%endif # with_check + +%files +%{python2_sitelib}/%{srcname}.py* +%{python2_sitelib}/%{srcname}-%{version}-py2.*.egg-info + +%if 0%{?with_python3} +%files -n python3-%{project} +%dir %{python3_sitelib}/%{srcname}/__pycache__ +%{python2_sitelib}/%{srcname}.py* +%{python3_sitelib}/%{srcname}-%{version}-py3.*.egg-info +%{python3_sitelib}/%{srcname}/__pycache__/*.py* +%endif # with_python3 diff -Nru pypureomapi-0.4/README.md pypureomapi-0.8/README.md --- pypureomapi-0.4/README.md 2015-07-02 17:02:47.000000000 +0000 +++ pypureomapi-0.8/README.md 2019-03-26 11:29:38.000000000 +0000 @@ -1,47 +1,30 @@ -[![Build Status](https://travis-ci.org/CygnusNetworks/pypureomapi.svg?branch=master)](https://travis-ci.org/CygnusNetworks/pypureomapi) +[![Build Status](https://travis-ci.org/CygnusNetworks/pypureomapi.svg?branch=master)](https://travis-ci.org/CygnusNetworks/pypureomapi) +[![Latest Version](https://img.shields.io/pypi/v/pypureomapi.svg)](https://pypi.python.org/pypi/pypureomapi) +[![PyPi Status](https://img.shields.io/pypi/status/pypureomapi.svg)](https://pypi.python.org/pypi/pypureomapi) [![PyPi Versions](https://img.shields.io/pypi/pyversions/pypureomapi.svg)](https://pypi.python.org/pypi/pypureomapi) 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 - -``` -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 occured: %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==; + secret +bFQtBCta6j2vWkjPkNFtgA==; # FIXME: replace by your own dnssec key (see below)!!! }; omapi-key defomapi; omapi-port 7911; ``` -Replace the given secret by a key created on your own! +**Replace the given secret by a key created on your own!** To generate a key use the following command: @@ -56,114 +39,115 @@ 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. + + +``` +from __future__ import print_function +import pypureomapi + +KEYNAME="defomapi" +BASE64_ENCODED_KEY="+bFQtBCta6j2vWkjPkNFtgA==" # FIXME: be sure to replace this by your own key!!! + +dhcp_server_ip="127.0.0.1" +port = 7911 # Port of the omapi service + +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)) -A group needs at least one statement. See UseCaseSupersedeHostname for example statements. +ip = omapi.lookup_ip(mac) +print("%s mac currently has ip %s assigned" % (mac, ip)) ``` -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") -``` - -#Supersede Hostname - -See http://jpmens.net/2011/07/20/dynamically-add-static-leases-to-dhcpd/ for the original idea. - -``` -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") - -``` - -Similarly the router can be superseded. - -#Get a lease - -Original idea from Josh West. - -``` -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 -``` - -#Change Group - -``` -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)) + +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: + ``` +lease = omapi.lookup_by_lease(mac="24:79:2a:0a:13:c0") +for k, v in res.items(): + print("%s: %s" % (k, v)) +``` + +Output: +``` +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' +``` + +To check if a lease is still valid, you should check ends and state: + +``` +if lease["ends"] < time.time() or lease["state"] != 2: + print("Lease is not valid") +``` + +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: + +``` +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)) +``` + +Output: + +``` +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 +# Custom Integration Assuming there already is a connection named `o` (i.e. a `Omapi` instance, see [Example]). To craft your own communication with the server you need to create an `OmapiMessage`, send it, receive a response and evaluate that response being an `OmapiMessage` as well. So here we go and create our first message. diff -Nru pypureomapi-0.4/setup.cfg pypureomapi-0.8/setup.cfg --- pypureomapi-0.4/setup.cfg 1970-01-01 00:00:00.000000000 +0000 +++ pypureomapi-0.8/setup.cfg 2019-03-26 11:29:38.000000000 +0000 @@ -0,0 +1,2 @@ +[metadata] +description-file = README.md diff -Nru pypureomapi-0.4/setup.py pypureomapi-0.8/setup.py --- pypureomapi-0.4/setup.py 2015-07-02 17:02:47.000000000 +0000 +++ pypureomapi-0.8/setup.py 2019-03-26 11:29:38.000000000 +0000 @@ -2,7 +2,7 @@ # -*- coding: utf8 -*- # library for communicating with an isc dhcp server over the omapi protocol # -# Copyright 2010-2015 Cygnus Networks GmbH +# Copyright 2010-2017 Cygnus Networks GmbH # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,9 +19,9 @@ import distutils.core distutils.core.setup(name='pypureomapi', - version='0.4', + version='0.8', description="ISC DHCP OMAPI protocol implementation in Python", - long_description="This module grew out of frustration about pyomapi and later pyomapic. The extension modules mentioned can be used to query the ISC DHCP server for information about leases. pyomapic does this job using swig and the static library provided with ISC DHCP. It leaks and has basically no error checking. Adding error checking later turned out to be a maintenance hell with swig, so a pure python implementation for omapi, pypureomapi was born. It can mostly be used as a drop-in replacement for pyomapic.", + long_description="This module provides a OMAPI implementation for managing ISC DHCP server by OMAPI protocol purely in Python code. You can query, create or modify ISC DHCP leases with this module. This module grew out of frustration about pyomapi and later pyomapic, which use swig and the static library provided with ISC DHCP without proper error handling. pypureomapi fixes these issues and can be used more or less as a drop-in replacement for pyomapic.", author='Helmut Grohne', author_email='h.grohne@cygnusnetworks.de', maintainer='Dr. Torge Szczepanek', @@ -39,8 +39,10 @@ "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", "Topic :: Internet", "Topic :: System :: Networking", "Topic :: Software Development :: Libraries :: Python Modules", - ] - ) \ No newline at end of file + ] +) diff -Nru pypureomapi-0.4/.tito/packages/python-pypureomapi pypureomapi-0.8/.tito/packages/python-pypureomapi --- pypureomapi-0.4/.tito/packages/python-pypureomapi 1970-01-01 00:00:00.000000000 +0000 +++ pypureomapi-0.8/.tito/packages/python-pypureomapi 2019-03-26 11:29:38.000000000 +0000 @@ -0,0 +1 @@ +0.4-1 ./ diff -Nru pypureomapi-0.4/.tito/packages/.readme pypureomapi-0.8/.tito/packages/.readme --- pypureomapi-0.4/.tito/packages/.readme 1970-01-01 00:00:00.000000000 +0000 +++ pypureomapi-0.8/.tito/packages/.readme 2019-03-26 11:29:38.000000000 +0000 @@ -0,0 +1,3 @@ +the .tito/packages directory contains metadata files +named after their packages. Each file has the latest tagged +version and the project's relative directory. diff -Nru pypureomapi-0.4/.tito/tito.props pypureomapi-0.8/.tito/tito.props --- pypureomapi-0.4/.tito/tito.props 1970-01-01 00:00:00.000000000 +0000 +++ pypureomapi-0.8/.tito/tito.props 2019-03-26 11:29:38.000000000 +0000 @@ -0,0 +1,5 @@ +[buildconfig] +builder = tito.builder.Builder +tagger = tito.tagger.VersionTagger +changelog_do_not_remove_cherrypick = 0 +changelog_format = %s (%ae) diff -Nru pypureomapi-0.4/.travis.yml pypureomapi-0.8/.travis.yml --- pypureomapi-0.4/.travis.yml 2015-07-02 17:02:47.000000000 +0000 +++ pypureomapi-0.8/.travis.yml 2019-03-26 11:29:38.000000000 +0000 @@ -4,6 +4,8 @@ - 2.7 - 3.3 - 3.4 + - 3.5 + - 3.6 install: script: - python pypureomapi.py -v