class OmapiClient: """Client for the DHCP OMAPI.""" def __init__(self, omapi_key: str, ipv6: bool = False): self._omapi = Omapi( "127.0.0.1", 7912 if ipv6 else 7911, b"omapi_key", omapi_key.encode("ascii"), ) def add_host(self, mac: str, ip: str): """Add a host mapping for a MAC.""" name = self._name_from_mac(mac) self._omapi.add_host_supersede(ip, mac, name) def del_host(self, mac: str): """Remove a host mapping for a MAC.""" self._omapi.del_host(mac) def update_host(self, mac: str, ip: str): """Update a host mapping for a MAC.""" name = self._name_from_mac(mac) msg = OmapiMessage.open(b"host") msg.update_object({b"name": name}) resp = self._omapi.query_server(msg) if resp.opcode != OMAPI_OP_UPDATE: raise OmapiError(f"Host not found: {name.decode('ascii')}") msg = OmapiMessage.update(resp.handle) msg.update_object({b"ip-address": pack_ip(ip)}) resp = self._omapi.query_server(msg) if resp.opcode != OMAPI_OP_STATUS: raise OmapiError( f"Updating IP for host {name.decode('ascii')} to {ip} failed" ) def _name_from_mac(self, mac: str) -> bytes: return mac.replace(":", "-").encode("ascii")
class OmapiHostManager: def __init__(self, module): self.module = module self.omapi = None self.connect() def connect(self): try: self.omapi = Omapi(self.module.params['host'], self.module.params['port'], self.module.params['key_name'], self.module.params['key']) except socket.error: e = get_exception() self.module.fail_json(msg="Unable to connect to OMAPI server: %s" % e) def get_host(self, macaddr): msg = OmapiMessage.open(to_bytes("host", errors='surrogate_or_strict')) msg.obj.append((to_bytes("hardware-address", errors='surrogate_or_strict'), pack_mac(macaddr))) msg.obj.append((to_bytes("hardware-type", errors='surrogate_or_strict'), struct.pack("!I", 1))) response = self.omapi.query_server(msg) if response.opcode != OMAPI_OP_UPDATE: return None return response @staticmethod def unpack_facts(obj): result = dict(obj) if 'hardware-address' in result: result['hardware-address'] = unpack_mac(result['hardware-address']) if 'ip-address' in result: result['ip-address'] = unpack_ip(result['ip-address']) if 'hardware-type' in result: result['hardware-type'] = struct.unpack("!I", result['hardware-type']) return result def setup_host(self): if self.module.params['hostname'] is None or len(self.module.params['hostname']) == 0: self.module.fail_json(msg="name attribute could not be empty when adding or modifying host.") msg = None host_response = self.get_host(self.module.params['macaddr']) # If host was not found using macaddr, add create message if host_response is None: msg = OmapiMessage.open(to_bytes('host', errors='surrogate_or_strict')) msg.message.append(('create', struct.pack('!I', 1))) msg.message.append(('exclusive', struct.pack('!I', 1))) msg.obj.append(('hardware-address', pack_mac(self.module.params['macaddr']))) msg.obj.append(('hardware-type', struct.pack('!I', 1))) msg.obj.append(('name', self.module.params['hostname'])) if self.module.params['ip'] is not None: msg.obj.append((to_bytes("ip-address", errors='surrogate_or_strict'), pack_ip(self.module.params['ip']))) stmt_join = "" if self.module.params['ddns']: stmt_join += 'ddns-hostname "{0}"; '.format(self.module.params['hostname']) try: if len(self.module.params['statements']) > 0: stmt_join += "; ".join(self.module.params['statements']) stmt_join += "; " except TypeError: e = get_exception() self.module.fail_json(msg="Invalid statements found: %s" % e) if len(stmt_join) > 0: msg.obj.append(('statements', stmt_join)) try: response = self.omapi.query_server(msg) if response.opcode != OMAPI_OP_UPDATE: self.module.fail_json(msg="Failed to add host, ensure authentication and host parameters " "are valid.") self.module.exit_json(changed=True, lease=self.unpack_facts(response.obj)) except OmapiError: e = get_exception() self.module.fail_json(msg="OMAPI error: %s" % e) # Forge update message else: response_obj = self.unpack_facts(host_response.obj) fields_to_update = {} if to_bytes('ip-address', errors='surrogate_or_strict') not in response_obj or \ unpack_ip(response_obj[to_bytes('ip-address', errors='surrogate_or_strict')]) != self.module.params['ip']: fields_to_update['ip-address'] = pack_ip(self.module.params['ip']) # Name cannot be changed if 'name' not in response_obj or response_obj['name'] != self.module.params['hostname']: self.module.fail_json(msg="Changing hostname is not supported. Old was %s, new is %s. " "Please delete host and add new." % (response_obj['name'], self.module.params['hostname'])) """ # It seems statements are not returned by OMAPI, then we cannot modify them at this moment. if 'statements' not in response_obj and len(self.module.params['statements']) > 0 or \ response_obj['statements'] != self.module.params['statements']: with open('/tmp/omapi', 'w') as fb: for (k,v) in iteritems(response_obj): fb.writelines('statements: %s %s\n' % (k, v)) """ if len(fields_to_update) == 0: self.module.exit_json(changed=False, lease=response_obj) else: msg = OmapiMessage.update(host_response.handle) msg.update_object(fields_to_update) try: response = self.omapi.query_server(msg) if response.opcode != OMAPI_OP_STATUS: self.module.fail_json(msg="Failed to modify host, ensure authentication and host parameters " "are valid.") self.module.exit_json(changed=True) except OmapiError: e = get_exception() self.module.fail_json(msg="OMAPI error: %s" % e) def remove_host(self): try: self.omapi.del_host(self.module.params['macaddr']) self.module.exit_json(changed=True) except OmapiErrorNotFound: self.module.exit_json() except OmapiError: e = get_exception() self.module.fail_json(msg="OMAPI error: %s" % e)
class OmapiHostManager: def __init__(self, module): self.module = module self.omapi = None self.connect() def connect(self): try: self.omapi = Omapi(self.module.params['host'], self.module.params['port'], self.module.params['key_name'], self.module.params['key']) except binascii.Error: self.module.fail_json(msg="Unable to open OMAPI connection. 'key' is not a valid base64 key.") except OmapiError as e: self.module.fail_json(msg="Unable to open OMAPI connection. Ensure 'host', 'port', 'key' and 'key_name' " "are valid. Exception was: %s" % to_native(e)) except socket.error as e: self.module.fail_json(msg="Unable to connect to OMAPI server: %s" % to_native(e)) def get_host(self, macaddr): msg = OmapiMessage.open(to_bytes("host", errors='surrogate_or_strict')) msg.obj.append((to_bytes("hardware-address", errors='surrogate_or_strict'), pack_mac(macaddr))) msg.obj.append((to_bytes("hardware-type", errors='surrogate_or_strict'), struct.pack("!I", 1))) response = self.omapi.query_server(msg) if response.opcode != OMAPI_OP_UPDATE: return None return response @staticmethod def unpack_facts(obj): result = dict(obj) if 'hardware-address' in result: result['hardware-address'] = unpack_mac(result['hardware-address']) if 'ip-address' in result: result['ip-address'] = unpack_ip(result['ip-address']) if 'hardware-type' in result: result['hardware-type'] = struct.unpack("!I", result['hardware-type']) return result def setup_host(self): if self.module.params['hostname'] is None or len(self.module.params['hostname']) == 0: self.module.fail_json(msg="name attribute could not be empty when adding or modifying host.") msg = None host_response = self.get_host(self.module.params['macaddr']) # If host was not found using macaddr, add create message if host_response is None: msg = OmapiMessage.open(to_bytes('host', errors='surrogate_or_strict')) msg.message.append(('create', struct.pack('!I', 1))) msg.message.append(('exclusive', struct.pack('!I', 1))) msg.obj.append(('hardware-address', pack_mac(self.module.params['macaddr']))) msg.obj.append(('hardware-type', struct.pack('!I', 1))) msg.obj.append(('name', self.module.params['hostname'])) if self.module.params['ip'] is not None: msg.obj.append((to_bytes("ip-address", errors='surrogate_or_strict'), pack_ip(self.module.params['ip']))) stmt_join = "" if self.module.params['ddns']: stmt_join += 'ddns-hostname "{0}"; '.format(self.module.params['hostname']) try: if len(self.module.params['statements']) > 0: stmt_join += "; ".join(self.module.params['statements']) stmt_join += "; " except TypeError as e: self.module.fail_json(msg="Invalid statements found: %s" % to_native(e), exception=traceback.format_exc()) if len(stmt_join) > 0: msg.obj.append(('statements', stmt_join)) try: response = self.omapi.query_server(msg) if response.opcode != OMAPI_OP_UPDATE: self.module.fail_json(msg="Failed to add host, ensure authentication and host parameters " "are valid.") self.module.exit_json(changed=True, lease=self.unpack_facts(response.obj)) except OmapiError as e: self.module.fail_json(msg="OMAPI error: %s" % to_native(e), exception=traceback.format_exc()) # Forge update message else: response_obj = self.unpack_facts(host_response.obj) fields_to_update = {} if to_bytes('ip-address', errors='surrogate_or_strict') not in response_obj or \ unpack_ip(response_obj[to_bytes('ip-address', errors='surrogate_or_strict')]) != self.module.params['ip']: fields_to_update['ip-address'] = pack_ip(self.module.params['ip']) # Name cannot be changed if 'name' not in response_obj or response_obj['name'] != self.module.params['hostname']: self.module.fail_json(msg="Changing hostname is not supported. Old was %s, new is %s. " "Please delete host and add new." % (response_obj['name'], self.module.params['hostname'])) """ # It seems statements are not returned by OMAPI, then we cannot modify them at this moment. if 'statements' not in response_obj and len(self.module.params['statements']) > 0 or \ response_obj['statements'] != self.module.params['statements']: with open('/tmp/omapi', 'w') as fb: for (k,v) in iteritems(response_obj): fb.writelines('statements: %s %s\n' % (k, v)) """ if len(fields_to_update) == 0: self.module.exit_json(changed=False, lease=response_obj) else: msg = OmapiMessage.update(host_response.handle) msg.update_object(fields_to_update) try: response = self.omapi.query_server(msg) if response.opcode != OMAPI_OP_STATUS: self.module.fail_json(msg="Failed to modify host, ensure authentication and host parameters " "are valid.") self.module.exit_json(changed=True) except OmapiError as e: self.module.fail_json(msg="OMAPI error: %s" % to_native(e), exception=traceback.format_exc()) def remove_host(self): try: self.omapi.del_host(self.module.params['macaddr']) self.module.exit_json(changed=True) except OmapiErrorNotFound: self.module.exit_json() except OmapiError as e: self.module.fail_json(msg="OMAPI error: %s" % to_native(e), exception=traceback.format_exc())