pax_global_header00006660000000000000000000000064125452672670014532gustar00rootroot0000000000000052 comment=7a59bef0fb49254a0020958a364510fe388aa395 pypureomapi-0.4/000077500000000000000000000000001254526726700137475ustar00rootroot00000000000000pypureomapi-0.4/.gitignore000066400000000000000000000000671254526726700157420ustar00rootroot00000000000000.idea *.pyc atlassian-ide-plugin.xml __pycache__ build pypureomapi-0.4/.travis.yml000066400000000000000000000001431254526726700160560ustar00rootroot00000000000000language: python python: - 2.6 - 2.7 - 3.3 - 3.4 install: script: - python pypureomapi.py -v pypureomapi-0.4/LICENSE.md000066400000000000000000000250221254526726700153540ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS Copyright 2010-2015 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. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. pypureomapi-0.4/MANIFEST.in000066400000000000000000000000221254526726700154770ustar00rootroot00000000000000include LICENSE.mdpypureomapi-0.4/README.md000066400000000000000000000163321254526726700152330ustar00rootroot00000000000000[![Build Status](https://travis-ci.org/CygnusNetworks/pypureomapi.svg?branch=master)](https://travis-ci.org/CygnusNetworks/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. #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 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==; }; omapi-key defomapi; omapi-port 7911; ``` Replace the given secret by a key created on your own! To generate a key use the following command: ``` /usr/sbin/dnssec-keygen -a HMAC-MD5 -b 128 -n USER defomapi ``` which will create two files containing a HMAC MD5 key. Alternatively, it 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 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") ``` #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)) ``` #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. ``` m1 = OmapiMessage.open("host") ``` We are using a named constructor (`OmapiMessage.open`). It fills in the opcode (as `OMAPI_OP_OPEN`), generates a random transaction id, and uses the parameter for the type field. This is the thing you want almost all the time. In this case we are going to open a host object, but we did not specify which host to open. For example we can select a host by its name. ``` m1.update_object(dict(name="foo")) ``` The next step is to interact with the DHCP server. The easiest way to do so is using the `query_server` method. It takes an `OmapiMessage`and returns another. ``` r1 = o.query_server(m1) ``` The returned `OmapiMessage` contains the parsed response from the server. Since opening can fail, we need to check the `opcode` attribute. In case of success its value is `OMAPI_OP_UPDATE`. As with files on unix we now have a descriptor called `r1.handle`. So now we are to modify some attribute about this host. Say we want to set its group. To do so we construct a new message and reference the opened host object via its handle. ``` m2 = OmapiMessage.update(r1.handle) ``` Again `OmapiMessage.update` is a named constructor. It fills in the opcode (as `OMAPI_OP_UPDATE`), generates a random transaction id and fills in the handle. So now we need to add the actual modification to the message and send the message to the server. ``` m2.update_object(dict(group="bar")) r2 = o.query_server(m2) ``` We receive a new message and need to check the returned `opcode` which should be `OMAPI_OP_UPDATE` again. Now we have a complete sequence. As can be seen, the OMAPI protocol permits flexible interaction and it would be unreasonable to include every possibility as library functions. Instead you are encouraged to subclass the `Omapi` class and define your own methods. If they prove useful in multiple locations, please submit them to the issue tracker. pypureomapi-0.4/debian/000077500000000000000000000000001254526726700151715ustar00rootroot00000000000000pypureomapi-0.4/debian/changelog000066400000000000000000000031051254526726700170420ustar00rootroot00000000000000pypureomapi (0.4-1) unstable; urgency=low [ Dr. Torge Szczepanek ] * Switch License to Apache 2.0 * PEP-8 cleanups * Use new-style classes. * Change maintainer to Torge Szczepanek and remove Helmut Grohne from uploaders [ Helmut Grohne ] * Bump python dependency to 2.6 since we need it. * Use logging module for debugging. * Switch to native versioning to not confuse tools. * Switch from pysupport to dh_python2. * Update packaging -- Dr. Torge Szczepanek Thu, 02 Jul 2015 18:29:03 +0200 pypureomapi (0.3-1) unstable; urgency=low * New upstream release. + Update debian/copyright years. + Minimum Python version bumped to 2.6. * Bump Standards-Version from 3.9.2 to 3.9.4: No changes needed. * Move required Python version to X-Python-Version to comply with python policy 2.3. * Update debian/copyright from dep5 draft to final version. * Added OmapiMessage.update. * Export {,un}pack_{ip,mac}. * Forward compatibility with python3. -- Helmut Grohne Wed, 26 Jun 2013 22:03:20 +0200 pypureomapi (0.2-1) unstable; urgency=low * Initial release. (Closes: #602921: RFP: pypureomapi -- ISC DHCP OMAPI protocol implementation in Python) * Set debian/source/format to 1.0. * Added missing (empty) ${misc:Depends} for debhelper. * Invoke test suite in override_dh_auto_test. -- Helmut Grohne Mon, 05 Dec 2011 11:24:34 +0100 pypureomapi (0.1-1) unstable; urgency=low * Initial release. -- Helmut Grohne Thu, 08 Dec 2011 08:57:55 +0100 pypureomapi-0.4/debian/compat000066400000000000000000000000021254526726700163670ustar00rootroot000000000000009 pypureomapi-0.4/debian/control000066400000000000000000000033051254526726700165750ustar00rootroot00000000000000Source: pypureomapi Maintainer: Dr. Torge Szczepanek Standards-Version: 3.9.6 Section: python Priority: extra Homepage: https://github.com/CygnusNetworks/pypureomapi Build-Depends: debhelper (>= 9), python (>= 2.6), python-all, python3, python3-all X-Python-Version: >= 2.6 X-Python3-Version: >= 3.3 Package: python-pypureomapi Architecture: all Depends: ${python:Depends}, ${misc:Depends} Description: ISC DHCP OMAPI protocol implementation in Python2 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 py2 version of the module. 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-0.4/debian/copyright000066400000000000000000000017311254526726700171260ustar00rootroot00000000000000Format: http://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 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. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. On Debian systems, the complete text of the Apache License Version 2.0 can be found in `/usr/share/common-licenses/Apache-2.0'. pypureomapi-0.4/debian/rules000077500000000000000000000001541254526726700162510ustar00rootroot00000000000000#!/usr/bin/make -f export PYBUILD_NAME=pypureomapi %: dh $@ --with python2,python3 --buildsystem=pybuild pypureomapi-0.4/debian/source/000077500000000000000000000000001254526726700164715ustar00rootroot00000000000000pypureomapi-0.4/debian/source/format000066400000000000000000000000131254526726700176760ustar00rootroot000000000000003.0 (quilt)pypureomapi-0.4/debian/watch000066400000000000000000000002331254526726700162200ustar00rootroot00000000000000version=3 opts=filenamemangle=s/.+\/v?(\d\S*)\.tar\.gz/-$1\.tar\.gz/ \ https://github.com/CygnusNetworks/pypureomapi/tags .*/v?(\d\S*)\.tar\.gz pypureomapi-0.4/pypureomapi.py000066400000000000000000000760321254526726700167030ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf8 -*- # # library for communicating with an isc dhcp server over the omapi protocol # # Copyright 2010-2015 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. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # Message format: # authid (netint32) # authlen (netint32) # opcode (netint32) # handle (netint32) # tid (netint32) # rid (netint32) # message (dictionary) # object (dictionary) # signature (length is authlen) # 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 hmac import io import logging import socket import random import operator try: basestring except NameError: basestring = str # pylint:disable=W0622 logger = logging.getLogger("pypureomapi") sysrand = random.SystemRandom() __all__.extend("OMAPI_OP_OPEN OMAPI_OP_REFRESH OMAPI_OP_UPDATE".split()) __all__.extend("OMAPI_OP_NOTIFY OMAPI_OP_STATUS OMAPI_OP_DELETE".split()) OMAPI_OP_OPEN = 1 OMAPI_OP_REFRESH = 2 OMAPI_OP_UPDATE = 3 OMAPI_OP_NOTIFY = 4 OMAPI_OP_STATUS = 5 OMAPI_OP_DELETE = 6 def repr_opcode(opcode): """Returns a textual representation for the given opcode. @type opcode: int @rtype: str """ 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") class OmapiSizeLimitError(OmapiError): """Packet size limit reached.""" def __init__(self): OmapiError.__init__(self, "Packet size limit reached.") __all__.append("OmapiErrorNotFound") class OmapiErrorNotFound(OmapiError): """Not found.""" def __init__(self): OmapiError.__init__(self, "not found") class OutBuffer(object): """Helper class for constructing network packets.""" sizelimit = 65536 def __init__(self): self.buff = io.BytesIO() def __len__(self): """Return the number of bytes in the buffer. @rtype: int """ # On Py2.7 tell returns long, but __len__ is required to return int. return int(self.buff.tell()) def add(self, data): """ >>> ob = OutBuffer().add(OutBuffer.sizelimit * b"x") >>> ob.add(b"y") # doctest: +ELLIPSIS Traceback (most recent call last): ... OmapiSizeLimitError: ... @type data: bytes @returns: self @raises OmapiSizeLimitError: """ if len(self) + len(data) > self.sizelimit: raise OmapiSizeLimitError() self.buff.write(data) return self def add_net32int(self, integer): """ @type integer: int @param integer: a 32bit unsigned integer @returns: self @raises OmapiSizeLimitError: """ if integer < 0 or integer >= (1 << 32): raise ValueError("not a 32bit unsigned integer") return self.add(struct.pack("!L", integer)) def add_net16int(self, integer): """ @type integer: int @param integer: a 16bit unsigned integer @returns: self @raises OmapiSizeLimitError: """ if integer < 0 or integer >= (1 << 16): raise ValueError("not a 16bit unsigned integer") return self.add(struct.pack("!H", integer)) def add_net32string(self, string): """ >>> r = b'\\x00\\x00\\x00\\x01x' >>> OutBuffer().add_net32string(b"x").getvalue() == r True @type string: bytes @param string: maximum length must fit in a 32bit integer @returns: self @raises OmapiSizeLimitError: """ if len(string) >= (1 << 32): raise ValueError("string too long") return self.add_net32int(len(string)).add(string) def add_net16string(self, string): """ >>> OutBuffer().add_net16string(b"x").getvalue() == b'\\x00\\x01x' True @type string: bytes @param string: maximum length must fit in a 16bit integer @returns: self @raises OmapiSizeLimitError: """ if len(string) >= (1 << 16): raise ValueError("string too long") return self.add_net16int(len(string)).add(string) def add_bindict(self, items): """ >>> r = b'\\x00\\x03foo\\x00\\x00\\x00\\x03bar\\x00\\x00' >>> OutBuffer().add_bindict({b"foo": b"bar"}).getvalue() == r True @type items: [(bytes, bytes)] or {bytes: bytes} @returns: self @raises OmapiSizeLimitError: """ if not isinstance(items, list): items = items.items() for key, value in items: self.add_net16string(key).add_net32string(value) return self.add(b"\x00\x00") # end marker def getvalue(self): """ >>> OutBuffer().add(b"sp").add(b"am").getvalue() == b"spam" True @rtype: bytes """ return self.buff.getvalue() def consume(self, length): """ >>> OutBuffer().add(b"spam").consume(2).getvalue() == b"am" True @type length: int @returns: self """ self.buff = io.BytesIO(self.getvalue()[length:]) return self class OmapiStartupMessage(object): """Class describing the protocol negotiation messages. >>> s = OmapiStartupMessage().as_string() >>> s == b"\\0\\0\\0\\x64\\0\\0\\0\\x18" True >>> next(InBuffer(s).parse_startup_message()).validate() >>> OmapiStartupMessage(42).validate() Traceback (most recent call last): ... OmapiError: protocol mismatch """ implemented_protocol_version = 100 implemented_header_size = 4 * 6 def __init__(self, protocol_version=None, header_size=None): """ @type protocol_version: int or None @type header_size: int or None """ if protocol_version is None: protocol_version = self.implemented_protocol_version if header_size is None: header_size = self.implemented_header_size self.protocol_version = protocol_version self.header_size = header_size def validate(self): """Checks whether this OmapiStartupMessage matches the implementation. @raises OmapiError: """ if self.implemented_protocol_version != self.protocol_version: raise OmapiError("protocol mismatch") if self.implemented_header_size != self.header_size: raise OmapiError("header size mismatch") def as_string(self): """ @rtype: bytes """ ret = OutBuffer() self.serialize(ret) return ret.getvalue() def serialize(self, outbuffer): """Serialize this OmapiStartupMessage to the given outbuffer. @type outbuffer: OutBuffer """ outbuffer.add_net32int(self.protocol_version) outbuffer.add_net32int(self.header_size) def dump_oneline(self): """ @rtype: str @returns: a human readable representation in one line """ return "protocol_version=%d header_size=%d" % (self.protocol_version, self.header_size) class OmapiAuthenticatorBase(object): """Base class for OMAPI authenticators. @cvar authlen: is the length of a signature as returned by the sign method @type authlen: int @cvar algorithm: is a textual name for the algorithm @type algorithm: str or None @ivar authid: is the authenticator id as assigned during the handshake @type authid: int """ authlen = -1 # must be overwritten algorithm = None authid = -1 # will be an instance attribute def __init__(self): pass def auth_object(self): """ @rtype: {bytes: bytes} @returns: object part of an omapi authentication message """ raise NotImplementedError def sign(self, message): """ @type message: bytes @rtype: bytes @returns: a signature of length self.authlen """ raise NotImplementedError() class OmapiNullAuthenticator(OmapiAuthenticatorBase): authlen = 0 authid = 0 # always 0 def __init__(self): OmapiAuthenticatorBase.__init__(self) def auth_object(self): return {} def sign(self, _): return b"" class OmapiHMACMD5Authenticator(OmapiAuthenticatorBase): authlen = 16 algorithm = b"hmac-md5.SIG-ALG.REG.INT." def __init__(self, user, key): """ @type user: bytes @type key: bytes @param key: base64 encoded key @raises binascii.Error: for bad base64 encoding """ OmapiAuthenticatorBase.__init__(self) self.user = user self.key = binascii.a2b_base64(key) def auth_object(self): return {b"name": self.user, b"algorithm": self.algorithm} def sign(self, message): """ >>> authlen = OmapiHMACMD5Authenticator.authlen >>> len(OmapiHMACMD5Authenticator(b"foo", 16*b"x").sign(b"baz")) == authlen True @type message: bytes @rtype: bytes @returns: a signature of length self.authlen """ return hmac.HMAC(self.key, message).digest() __all__.append("OmapiMessage") class OmapiMessage(object): """ @type authid: int @ivar authid: The id of the message authenticator. @type opcode: int @ivar opcode: One out of OMAPI_OP_{OPEN,REFRESH,UPDATE,NOTIFY,STATUS,DELETE}. @type handle: int @ivar handle: The id of a handle acquired from a previous request or 0. @type tid: int @ivar tid: Transmission identifier. @type rid: int @ivar rid: Receive identifier (of a response is the tid of the request). @type message: [(bytes, bytes)] @ivar message: A list of (key, value) pairs. @type obj: [(bytes, bytes)] @ivar obj: A list of (key, value) pairs. @type signature: bytes @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""): """ Construct an OmapiMessage from the given fields. No error checking is performed. @type authid: int @type opcode: int @type handle: int @type tid: int @param tid: The special value -1 causes a tid to be generated randomly. @type rid: int @type message: [(bytes, bytes)] @type obj: [(bytes, bytes)] @type signature: str @rtype: OmapiMessage """ self.authid, self.opcode, self.handle = authid, opcode, handle self.handle, self.tid, self.rid = handle, tid, rid self.message = message or [] self.obj = obj or [] self.signature = signature if self.tid == -1: self.generate_tid() def generate_tid(self): """Generate a random transmission id for this OMAPI message. >>> OmapiMessage(tid=-1).tid != OmapiMessage(tid=-1).tid True """ self.tid = sysrand.randrange(0, 1 << 32) def serialize(self, outbuffer, forsigning=False): """ @type outbuffer: OutBuffer @type forsigning: bool @raises OmapiSizeLimitError: """ if not forsigning: outbuffer.add_net32int(self.authid) outbuffer.add_net32int(len(self.signature)) outbuffer.add_net32int(self.opcode) outbuffer.add_net32int(self.handle) outbuffer.add_net32int(self.tid) outbuffer.add_net32int(self.rid) outbuffer.add_bindict(self.message) outbuffer.add_bindict(self.obj) if not forsigning: outbuffer.add(self.signature) def as_string(self, forsigning=False): """ >>> len(OmapiMessage().as_string(True)) >= 24 True @type forsigning: bool @rtype: bytes @raises OmapiSizeLimitError: """ ret = OutBuffer() self.serialize(ret, forsigning) return ret.getvalue() def sign(self, authenticator): """Sign this OMAPI message. @type authenticator: OmapiAuthenticatorBase """ self.authid = authenticator.authid self.signature = b"\0" * authenticator.authlen # provide authlen self.signature = authenticator.sign(self.as_string(forsigning=True)) assert len(self.signature) == authenticator.authlen def verify(self, authenticators): """Verify this OMAPI message. >>> a1 = OmapiHMACMD5Authenticator(b"egg", b"spam") >>> a2 = OmapiHMACMD5Authenticator(b"egg", b"tomatoes") >>> a1.authid = a2.authid = 5 >>> m = OmapiMessage.open(b"host") >>> m.verify({a1.authid: a1}) False >>> m.sign(a1) >>> m.verify({a1.authid: a1}) True >>> m.sign(a2) >>> m.verify({a1.authid: a1}) False @type authenticators: {int: OmapiAuthenticatorBase} @rtype: bool """ try: return authenticators[self.authid]. sign(self.as_string(forsigning=True)) == self.signature except KeyError: return False @classmethod def open(cls, typename): """Create an OMAPI open message with given typename. @type typename: bytes @rtype: OmapiMessage """ return cls(opcode=OMAPI_OP_OPEN, message=[(b"type", typename)], tid=-1) @classmethod def update(cls, handle): """Create an OMAPI update message for the given handle. @type handle: int @rtype: OmapiMessage """ return cls(opcode=OMAPI_OP_UPDATE, handle=handle, tid=-1) @classmethod def delete(cls, handle): """Create an OMAPI delete message for given handle. @type handle: int @rtype: OmapiMessage """ return cls(opcode=OMAPI_OP_DELETE, handle=handle, tid=-1) def is_response(self, other): """Check whether this OMAPI message is a response to the given OMAPI message. @rtype: bool """ return self.rid == other.tid def update_object(self, update): """ @type update: {bytes: bytes} """ self.obj = [(key, value) for key, value in self.obj if key not in update] self.obj.extend(update.items()) def dump(self): """ @rtype: str @returns: a human readable representation of the message """ return "".join(("Omapi message attributes:\n", "authid:\t\t%d\n" % self.authid, "authlen:\t%d\n" % len(self.signature), "opcode:\t\t%s\n" % repr_opcode(self.opcode), "handle:\t\t%d\n" % self.handle, "tid:\t\t%d\n" % self.tid, "rid:\t\t%d\n" % self.rid, "message:\t%r\n" % self.message, "obj:\t\t%r\n" % self.obj, "signature:\t%r\n" % self.signature)) def dump_oneline(self): """ @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) def parse_map(filterfun, parser): """Creates a new parser that passes the result of the given parser through the given filterfun. >>> list(parse_map(int, (None, "42"))) [None, 42] @type filterfun: obj -> obj @param parser: parser @returns: parser """ for element in parser: if element is None: yield None else: yield filterfun(element) break def parse_chain(*args): """Creates a new parser that executes the passed parsers (args) with the previous results and yields a tuple of the results. >>> list(parse_chain(lambda: (None, 1), lambda one: (None, 2))) [None, None, (1, 2)] @param args: parsers @returns: parser """ items = [] for parser in args: for element in parser(*items): if element is None: yield None else: items.append(element) break yield tuple(items) class InBuffer(object): sizelimit = 65536 def __init__(self, initial=b""): """ @type initial: bytes @param initial: initial value of the buffer @raises OmapiSizeLimitError: """ self.buff = b"" self.totalsize = 0 if initial: self.feed(initial) def feed(self, data): """ @type data: bytes @returns: self @raises OmapiSizeLimitError: """ if self.totalsize + len(data) > self.sizelimit: raise OmapiSizeLimitError() self.buff += data self.totalsize += len(data) return self def resetsize(self): """This method is to be called after handling a packet to reset the total size to be parsed at once and that way not overflow the size limit. """ self.totalsize = len(self.buff) def parse_fixedbuffer(self, length): """ @type length: int """ while len(self.buff) < length: yield None result = self.buff[:length] self.buff = self.buff[length:] yield result def parse_net16int(self): """ >>> hex(next(InBuffer(b"\\x01\\x02").parse_net16int())) '0x102' """ return parse_map(lambda data: struct.unpack("!H", data)[0], self.parse_fixedbuffer(2)) def parse_net32int(self): """ >>> hex(int(next(InBuffer(b"\\x01\\0\\0\\x02").parse_net32int()))) '0x1000002' """ return parse_map(lambda data: struct.unpack("!L", data)[0], self.parse_fixedbuffer(4)) def parse_net16string(self): """ >>> next(InBuffer(b"\\0\\x03eggs").parse_net16string()) == b'egg' True """ return parse_map(operator.itemgetter(1), parse_chain(self.parse_net16int, self.parse_fixedbuffer)) def parse_net32string(self): """ >>> next(InBuffer(b"\\0\\0\\0\\x03eggs").parse_net32string()) == b'egg' True """ return parse_map(operator.itemgetter(1), parse_chain(self.parse_net32int, self.parse_fixedbuffer)) def parse_bindict(self): """ >>> d = b"\\0\\x01a\\0\\0\\0\\x01b\\0\\0spam" >>> next(InBuffer(d).parse_bindict()) == [(b'a', b'b')] True """ entries = [] try: while True: for key in self.parse_net16string(): if key is None: yield None elif not key: raise StopIteration() else: for value in self.parse_net32string(): if value is None: yield None else: entries.append((key, value)) break break # Abusing StopIteration here, since nothing should be throwing # it at us. except StopIteration: yield entries def parse_startup_message(self): """results in an OmapiStartupMessage >>> 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())) def parse_message(self): """results in an OmapiMessage""" parser = parse_chain(self.parse_net32int, # authid lambda *_: self.parse_net32int(), # authlen lambda *_: self.parse_net32int(), # opcode lambda *_: self.parse_net32int(), # handle lambda *_: self.parse_net32int(), # tid lambda *_: self.parse_net32int(), # rid lambda *_: self.parse_bindict(), # message 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) if isinstance(bytes(b"x")[0], int): def bytes_to_int_seq(b): return b int_seq_to_bytes = bytes # raises ValueError else: def bytes_to_int_seq(b): return [ord(x) for x in b] def int_seq_to_bytes(s): return "".join([chr(x) for x in s]) # raises ValueError __all__.append("pack_ip") def pack_ip(ipstr): """Converts an ip address given in dotted notation to a four byte string in network byte order. >>> len(pack_ip("127.0.0.1")) 4 >>> pack_ip("foo") Traceback (most recent call last): ... ValueError: given ip address has an invalid number of dots @type ipstr: str @rtype: bytes @raises ValueError: for badly formatted ip addresses """ if not isinstance(ipstr, basestring): raise ValueError("given ip address is not a string") parts = ipstr.split('.') if len(parts) != 4: raise ValueError("given ip address has an invalid number of dots") parts = [int(x) for x in parts] # raises ValueError return int_seq_to_bytes(parts) # raises ValueError __all__.append("unpack_ip") def unpack_ip(fourbytes): """Converts an ip address given in a four byte string in network byte order to a string in dotted notation. >>> unpack_ip(b"dead") '100.101.97.100' >>> unpack_ip(b"alive") Traceback (most recent call last): ... ValueError: given buffer is not exactly four bytes long @type fourbytes: bytes @rtype: str @raises ValueError: for bad input """ if not isinstance(fourbytes, bytes): raise ValueError("given buffer is not a string") if len(fourbytes) != 4: 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") def pack_mac(macstr): """Converts a mac address given in colon delimited notation to a six byte string in network byte order. >>> pack_mac("30:31:32:33:34:35") == b'012345' True >>> pack_mac("bad") Traceback (most recent call last): ... ValueError: given mac addresses has an invalid number of colons @type macstr: str @rtype: bytes @raises ValueError: for badly formatted mac addresses """ if not isinstance(macstr, basestring): raise ValueError("given mac addresses is not a string") parts = macstr.split(":") if len(parts) != 6: raise ValueError("given mac addresses has an invalid number of colons") parts = [int(part, 16) for part in parts] # raises ValueError return int_seq_to_bytes(parts) # raises ValueError __all__.append("unpack_mac") def unpack_mac(sixbytes): """Converts a mac address given in a six byte string in network byte order to a string in colon delimited notation. >>> unpack_mac(b"012345") '30:31:32:33:34:35' >>> unpack_mac(b"bad") Traceback (most recent call last): ... ValueError: given buffer is not exactly six bytes long @type sixbytes: bytes @rtype: str @raises ValueError: for bad input """ if not isinstance(sixbytes, bytes): raise ValueError("given buffer is not a string") if len(sixbytes) != 6: raise ValueError("given buffer is not exactly six bytes long") return ":".join(["%2.2x".__mod__(x) for x in bytes_to_int_seq(sixbytes)]) class LazyStr(object): def __init__(self, function): self.function = function def __str__(self): return self.function() class TCPClientTransport(object): """PEP 3156 dummy transport class to support OmapiProtocol class.""" def __init__(self, protocol, host, port): self.protocol = protocol self.connection = socket.socket() self.connection.connect((host, port)) self.protocol.connection_made(self) def close(self): """Close the omapi connection if it is open.""" if self.connection: self.connection.close() self.connection = None def fill_inbuffer(self): """Read bytes from the connection and hand them to the protocol. @raises OmapiError: @raises socket.error: """ if not self.connection: raise OmapiError("not connected") try: data = self.connection.recv(2048) except socket.error: self.close() raise if not data: self.close() raise OmapiError("connection closed") try: self.protocol.data_received(data) except OmapiSizeLimitError: self.close() raise def write(self, data): """Send all of data to the connection. @type data: bytes @raises socket.error: """ try: self.connection.sendall(data) except socket.error: self.close() raise class OmapiProtocol(object): """PEP 3156 like protocol class for Omapi. This interface is not yet to be relied upon. """ def __init__(self): self.transport = None self.authenticators = {0: OmapiNullAuthenticator()} self.defauth = 0 self.inbuffer = InBuffer() self.current_parser = self.inbuffer.parse_startup_message() def connection_made(self, transport): self.transport = transport message = OmapiStartupMessage() logger.debug("sending omapi startup message %s", LazyStr(message.dump_oneline)) self.transport.write(message.as_string()) def data_received(self, data): """ @type data: bytes """ self.inbuffer.feed(data) while True: if self.current_parser is None: self.current_parser = self.inbuffer.parse_message() result = next(self.current_parser) if result is None: return self.current_parser = None self.inbuffer.resetsize() if isinstance(result, OmapiStartupMessage): logger.debug("received omapi startup message %s", LazyStr(result.dump_oneline)) self.startup_received(result) else: assert isinstance(result, OmapiMessage) logger.debug("received %s", LazyStr(result.dump_oneline)) self.message_received(result) def startup_received(self, startup_message): try: startup_message.validate() except OmapiError: self.transport.close() raise self.startup_completed() @staticmethod def startup_completed(): logger.debug("omapi connection initialized") def message_received(self, message): pass def send_message(self, message, sign=True): """Send the given message to the connection. @type message: OmapiMessage @param sign: whether the message needs to be signed @raises OmapiError: @raises socket.error: """ if sign: message.sign(self.authenticators[self.defauth]) 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): """ @type hostname: str @type port: int @type username: bytes or None @type key: bytes or None @param key: if given, it must be base64 encoded @raises binascii.Error: for bad base64 encoding @raises socket.error: @raises OmapiError: """ self.hostname = hostname self.port = port self.protocol = OmapiProtocol() self.recv_message_queue = [] self.protocol.startup_completed = lambda: self.recv_message_queue.append(None) self.protocol.message_received = self.recv_message_queue.append newauth = None if username is not None and key is not None: newauth = OmapiHMACMD5Authenticator(username, key) self.transport = TCPClientTransport(self.protocol, hostname, port) self.recv_protocol_initialization() if newauth: self.initialize_authenticator(newauth) def close(self): """Close the omapi connection if it is open.""" self.transport.close() def check_connected(self): """Raise an OmapiError unless connected. @raises OmapiError: """ if not self.transport.connection: raise OmapiError("not connected") def recv_protocol_initialization(self): """ @raises OmapiError: @raises socket.error: """ while not self.recv_message_queue: self.transport.fill_inbuffer() message = self.recv_message_queue.pop(0) assert message is None def receive_message(self): """Read the next message from the connection. @rtype: OmapiMessage @raises OmapiError: @raises socket.error: """ while not self.recv_message_queue: self.transport.fill_inbuffer() message = self.recv_message_queue.pop(0) assert message is not None if not message.verify(self.protocol.authenticators): self.close() raise OmapiError("bad omapi message signature") return message def receive_response(self, message, insecure=False): """Read the response for the given message. @type message: OmapiMessage @type insecure: bool @param insecure: avoid an OmapiError about a wrong authenticator @rtype: OmapiMessage @raises OmapiError: @raises socket.error: """ response = self.receive_message() if not response.is_response(message): raise OmapiError("received message is not the desired response") # signature already verified if response.authid != self.protocol.defauth and not insecure: raise OmapiError("received message is signed with wrong authenticator") return response def send_message(self, message, sign=True): """Sends the given message to the connection. @type message: OmapiMessage @type sign: bool @param sign: whether the message needs to be signed @raises OmapiError: @raises socket.error: """ self.check_connected() self.protocol.send_message(message, sign) def query_server(self, message): """Send the message and receive a response for it. @type message: OmapiMessage @rtype: OmapiMessage @raises OmapiError: @raises socket.error: """ self.send_message(message) return self.receive_response(message) def initialize_authenticator(self, authenticator): """ @type authenticator: OmapiAuthenticatorBase @raises OmapiError: @raises socket.error: """ msg = OmapiMessage.open(b"authenticator") msg.update_object(authenticator.auth_object()) response = self.query_server(msg) if response.opcode != OMAPI_OP_UPDATE: raise OmapiError("received non-update response for open") authid = response.handle if authid == 0: raise OmapiError("received invalid authid from server") self.protocol.authenticators[authid] = authenticator authenticator.authid = authid self.protocol.defauth = authid logger.debug("successfully initialized default authid %d", authid) def add_host(self, ip, mac): """Create a host object with given ip address and and mac 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))) msg.obj.append((b"ip-address", pack_ip(ip))) response = self.query_server(msg) if response.opcode != OMAPI_OP_UPDATE: raise OmapiError("add failed") def del_host(self, mac): """Delete 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() 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. @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.obj.append((b"hardware-address", pack_mac(mac))) 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() 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 address could be found or the object lacks a mac address @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 unpack_mac(dict(response.obj)[b"hardware-address"]) except KeyError: # hardware-address raise OmapiErrorNotFound() 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 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) if response.opcode != OMAPI_OP_UPDATE: raise OmapiError("add failed") if __name__ == '__main__': import doctest doctest.testmod() pypureomapi-0.4/setup.py000077500000000000000000000042061254526726700154660ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf8 -*- # library for communicating with an isc dhcp server over the omapi protocol # # Copyright 2010-2015 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. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import distutils.core distutils.core.setup(name='pypureomapi', version='0.4', 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.", author='Helmut Grohne', author_email='h.grohne@cygnusnetworks.de', maintainer='Dr. Torge Szczepanek', maintainer_email='debian@cygnusnetworks.de', license='Apache-2.0', url='https://github.com/CygnusNetworks/pypureomapi', py_modules=['pypureomapi'], classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: System Administrators", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Topic :: Internet", "Topic :: System :: Networking", "Topic :: Software Development :: Libraries :: Python Modules", ] )