2016-03-18 04:05:55 -04:00
|
|
|
"""Libknot server control interface wrapper.
|
|
|
|
|
|
|
|
|
|
Example:
|
2016-09-22 10:08:08 -04:00
|
|
|
import json
|
|
|
|
|
from libknot.control import *
|
|
|
|
|
|
2018-10-24 06:50:20 -04:00
|
|
|
#load_lib("/usr/lib/libknot.so")
|
|
|
|
|
|
2016-03-18 04:05:55 -04:00
|
|
|
ctl = KnotCtl()
|
|
|
|
|
ctl.connect("/var/run/knot/knot.sock")
|
|
|
|
|
|
2016-05-06 03:39:25 -04:00
|
|
|
try:
|
|
|
|
|
ctl.send_block(cmd="conf-begin")
|
|
|
|
|
resp = ctl.receive_block()
|
|
|
|
|
|
|
|
|
|
ctl.send_block(cmd="conf-set", section="zone", item="domain", data="test")
|
|
|
|
|
resp = ctl.receive_block()
|
|
|
|
|
|
|
|
|
|
ctl.send_block(cmd="conf-commit")
|
|
|
|
|
resp = ctl.receive_block()
|
|
|
|
|
|
|
|
|
|
ctl.send_block(cmd="conf-read", section="zone", item="domain")
|
|
|
|
|
resp = ctl.receive_block()
|
|
|
|
|
print(json.dumps(resp, indent=4))
|
|
|
|
|
finally:
|
|
|
|
|
ctl.send(KnotCtlType.END)
|
|
|
|
|
ctl.close()
|
2016-03-18 04:05:55 -04:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from ctypes import cdll, c_void_p, c_int, c_char_p, c_uint, byref
|
|
|
|
|
from enum import IntEnum
|
2018-10-19 09:08:53 -04:00
|
|
|
import sys
|
2016-03-18 04:05:55 -04:00
|
|
|
|
2016-09-22 10:08:08 -04:00
|
|
|
CTL_ALLOC = None
|
|
|
|
|
CTL_FREE = None
|
|
|
|
|
CTL_SET_TIMEOUT = None
|
|
|
|
|
CTL_CONNECT = None
|
|
|
|
|
CTL_CLOSE = None
|
|
|
|
|
CTL_SEND = None
|
|
|
|
|
CTL_RECEIVE = None
|
|
|
|
|
CTL_ERROR = None
|
|
|
|
|
|
|
|
|
|
|
2017-07-24 03:23:04 -04:00
|
|
|
def load_lib(path=None):
|
2016-09-22 10:08:08 -04:00
|
|
|
"""Loads the libknot library."""
|
|
|
|
|
|
2017-07-24 03:23:04 -04:00
|
|
|
if path is None:
|
2018-11-20 10:33:07 -05:00
|
|
|
version = ""
|
|
|
|
|
try:
|
|
|
|
|
from libknot import LIBKNOT_VERSION
|
|
|
|
|
version = ".%u" % int(LIBKNOT_VERSION)
|
|
|
|
|
except:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
if sys.platform == "darwin":
|
|
|
|
|
path = "libknot%s.dylib" % version
|
|
|
|
|
else:
|
|
|
|
|
path = "libknot.so%s" % version
|
2016-09-22 10:08:08 -04:00
|
|
|
LIB = cdll.LoadLibrary(path)
|
2016-03-18 04:05:55 -04:00
|
|
|
|
2016-09-22 10:08:08 -04:00
|
|
|
global CTL_ALLOC
|
|
|
|
|
CTL_ALLOC = LIB.knot_ctl_alloc
|
|
|
|
|
CTL_ALLOC.restype = c_void_p
|
2016-03-18 04:05:55 -04:00
|
|
|
|
2016-09-22 10:08:08 -04:00
|
|
|
global CTL_FREE
|
|
|
|
|
CTL_FREE = LIB.knot_ctl_free
|
|
|
|
|
CTL_FREE.argtypes = [c_void_p]
|
2016-03-18 04:05:55 -04:00
|
|
|
|
2016-09-22 10:08:08 -04:00
|
|
|
global CTL_SET_TIMEOUT
|
|
|
|
|
CTL_SET_TIMEOUT = LIB.knot_ctl_set_timeout
|
|
|
|
|
CTL_SET_TIMEOUT.argtypes = [c_void_p, c_int]
|
2016-03-18 04:05:55 -04:00
|
|
|
|
2016-09-22 10:08:08 -04:00
|
|
|
global CTL_CONNECT
|
|
|
|
|
CTL_CONNECT = LIB.knot_ctl_connect
|
|
|
|
|
CTL_CONNECT.restype = c_int
|
|
|
|
|
CTL_CONNECT.argtypes = [c_void_p, c_char_p]
|
2016-03-18 04:05:55 -04:00
|
|
|
|
2016-09-22 10:08:08 -04:00
|
|
|
global CTL_CLOSE
|
|
|
|
|
CTL_CLOSE = LIB.knot_ctl_close
|
|
|
|
|
CTL_CLOSE.argtypes = [c_void_p]
|
2016-03-18 04:05:55 -04:00
|
|
|
|
2016-09-22 10:08:08 -04:00
|
|
|
global CTL_SEND
|
|
|
|
|
CTL_SEND = LIB.knot_ctl_send
|
|
|
|
|
CTL_SEND.restype = c_int
|
|
|
|
|
CTL_SEND.argtypes = [c_void_p, c_uint, c_void_p]
|
2016-03-18 04:05:55 -04:00
|
|
|
|
2016-09-22 10:08:08 -04:00
|
|
|
global CTL_RECEIVE
|
|
|
|
|
CTL_RECEIVE = LIB.knot_ctl_receive
|
|
|
|
|
CTL_RECEIVE.restype = c_int
|
|
|
|
|
CTL_RECEIVE.argtypes = [c_void_p, c_void_p, c_void_p]
|
2016-03-18 04:05:55 -04:00
|
|
|
|
2016-09-22 10:08:08 -04:00
|
|
|
global CTL_ERROR
|
|
|
|
|
CTL_ERROR = LIB.knot_strerror
|
|
|
|
|
CTL_ERROR.restype = c_char_p
|
|
|
|
|
CTL_ERROR.argtypes = [c_int]
|
2016-03-18 04:05:55 -04:00
|
|
|
|
|
|
|
|
|
2017-01-09 06:55:23 -05:00
|
|
|
class KnotCtlError(Exception):
|
|
|
|
|
"""Libknot server control error."""
|
|
|
|
|
|
|
|
|
|
def __init__(self, message, data=None):
|
|
|
|
|
"""
|
|
|
|
|
@type message: str
|
|
|
|
|
@type data: KnotCtlData
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
self.message = message
|
|
|
|
|
self.data = data
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
2017-01-15 04:02:37 -05:00
|
|
|
return "%s (data: %s)" % (self.message, self.data)
|
2017-01-09 06:55:23 -05:00
|
|
|
|
|
|
|
|
|
2016-03-18 04:05:55 -04:00
|
|
|
class KnotCtlType(IntEnum):
|
|
|
|
|
"""Libknot server control data unit types."""
|
|
|
|
|
|
|
|
|
|
END = 0
|
|
|
|
|
DATA = 1
|
|
|
|
|
EXTRA = 2
|
|
|
|
|
BLOCK = 3
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class KnotCtlDataIdx(IntEnum):
|
|
|
|
|
"""Libknot server control data unit indices."""
|
|
|
|
|
|
|
|
|
|
COMMAND = 0
|
|
|
|
|
FLAGS = 1
|
|
|
|
|
ERROR = 2
|
|
|
|
|
SECTION = 3
|
|
|
|
|
ITEM = 4
|
|
|
|
|
ID = 5
|
|
|
|
|
ZONE = 6
|
|
|
|
|
OWNER = 7
|
|
|
|
|
TTL = 8
|
|
|
|
|
TYPE = 9
|
|
|
|
|
DATA = 10
|
2017-05-25 03:43:17 -04:00
|
|
|
FILTER = 11
|
2016-03-18 04:05:55 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class KnotCtlData(object):
|
|
|
|
|
"""Libknot server control data unit."""
|
|
|
|
|
|
|
|
|
|
DataArray = c_char_p * len(KnotCtlDataIdx)
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
self.data = self.DataArray()
|
|
|
|
|
|
2017-01-09 06:55:23 -05:00
|
|
|
def __str__(self):
|
|
|
|
|
string = str()
|
|
|
|
|
|
|
|
|
|
for idx in KnotCtlDataIdx:
|
|
|
|
|
if self.data[idx]:
|
|
|
|
|
if string:
|
|
|
|
|
string += ", "
|
|
|
|
|
string += "%s = %s" % (idx.name, self.data[idx])
|
|
|
|
|
|
|
|
|
|
return string
|
|
|
|
|
|
2016-03-18 04:05:55 -04:00
|
|
|
def __getitem__(self, index):
|
|
|
|
|
"""Data unit item getter.
|
|
|
|
|
|
|
|
|
|
@type index: KnotCtlDataIdx
|
|
|
|
|
@rtype: str
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
value = self.data[index]
|
|
|
|
|
if not value:
|
|
|
|
|
value = str()
|
|
|
|
|
return value if isinstance(value, str) else value.decode()
|
|
|
|
|
|
|
|
|
|
def __setitem__(self, index, value):
|
|
|
|
|
"""Data unit item setter.
|
|
|
|
|
|
|
|
|
|
@type index: KnotCtlDataIdx
|
|
|
|
|
@type value: str
|
|
|
|
|
"""
|
|
|
|
|
|
2016-05-06 03:39:25 -04:00
|
|
|
self.data[index] = c_char_p(value.encode()) if value else c_char_p()
|
2016-03-18 04:05:55 -04:00
|
|
|
|
|
|
|
|
class KnotCtl(object):
|
|
|
|
|
"""Libknot server control interface."""
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
2016-09-22 10:08:08 -04:00
|
|
|
if not CTL_ALLOC:
|
|
|
|
|
load_lib()
|
2016-03-18 04:05:55 -04:00
|
|
|
self.obj = CTL_ALLOC()
|
|
|
|
|
|
|
|
|
|
def __del__(self):
|
|
|
|
|
CTL_FREE(self.obj)
|
|
|
|
|
|
|
|
|
|
def set_timeout(self, timeout):
|
|
|
|
|
"""Sets control socket operations timeout in seconds.
|
|
|
|
|
|
|
|
|
|
@type timeout: int
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
CTL_SET_TIMEOUT(self.obj, timeout * 1000)
|
|
|
|
|
|
|
|
|
|
def connect(self, path):
|
|
|
|
|
"""Connect to a specified control UNIX socket.
|
|
|
|
|
|
|
|
|
|
@type path: str
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
ret = CTL_CONNECT(self.obj, path.encode())
|
|
|
|
|
if ret != 0:
|
|
|
|
|
err = CTL_ERROR(ret)
|
2017-01-09 06:55:23 -05:00
|
|
|
raise KnotCtlError(err if isinstance(err, str) else err.decode())
|
2016-03-18 04:05:55 -04:00
|
|
|
|
|
|
|
|
def close(self):
|
|
|
|
|
"""Disconnects from the current control socket."""
|
|
|
|
|
|
|
|
|
|
CTL_CLOSE(self.obj)
|
|
|
|
|
|
|
|
|
|
def send(self, data_type, data=None):
|
|
|
|
|
"""Sends a data unit to the connected control socket.
|
|
|
|
|
|
|
|
|
|
@type data_type: KnotCtlType
|
|
|
|
|
@type data: KnotCtlData
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
ret = CTL_SEND(self.obj, data_type,
|
|
|
|
|
data.data if data else c_char_p())
|
|
|
|
|
if ret != 0:
|
|
|
|
|
err = CTL_ERROR(ret)
|
2017-01-09 06:55:23 -05:00
|
|
|
raise KnotCtlError(err if isinstance(err, str) else err.decode())
|
2016-03-18 04:05:55 -04:00
|
|
|
|
|
|
|
|
def receive(self, data=None):
|
|
|
|
|
"""Receives a data unit from the connected control socket.
|
|
|
|
|
|
|
|
|
|
@type data: KnotCtlData
|
|
|
|
|
@rtype: KnotCtlType
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
data_type = c_uint()
|
|
|
|
|
ret = CTL_RECEIVE(self.obj, byref(data_type),
|
|
|
|
|
data.data if data else c_char_p())
|
|
|
|
|
if ret != 0:
|
|
|
|
|
err = CTL_ERROR(ret)
|
2017-01-09 06:55:23 -05:00
|
|
|
raise KnotCtlError(err if isinstance(err, str) else err.decode())
|
2016-03-18 04:05:55 -04:00
|
|
|
return KnotCtlType(data_type.value)
|
2016-05-06 03:39:25 -04:00
|
|
|
|
2016-07-15 06:24:00 -04:00
|
|
|
def send_block(self, cmd, section=None, item=None, identifier=None, zone=None,
|
2017-05-25 03:43:17 -04:00
|
|
|
owner=None, ttl=None, rtype=None, data=None, flags=None,
|
|
|
|
|
filter=None):
|
2016-05-06 03:39:25 -04:00
|
|
|
"""Sends a control query block.
|
|
|
|
|
|
|
|
|
|
@type cmd: str
|
|
|
|
|
@type section: str
|
|
|
|
|
@type item: str
|
|
|
|
|
@type identifier: str
|
|
|
|
|
@type zone: str
|
2016-07-15 06:24:00 -04:00
|
|
|
@type owner: str
|
|
|
|
|
@type ttl: str
|
|
|
|
|
@type rtype: str
|
2016-05-06 03:39:25 -04:00
|
|
|
@type data: str
|
2017-05-25 03:43:17 -04:00
|
|
|
@type filter: str
|
2016-05-06 03:39:25 -04:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
query = KnotCtlData()
|
|
|
|
|
query[KnotCtlDataIdx.COMMAND] = cmd
|
|
|
|
|
query[KnotCtlDataIdx.SECTION] = section
|
|
|
|
|
query[KnotCtlDataIdx.ITEM] = item
|
|
|
|
|
query[KnotCtlDataIdx.ID] = identifier
|
|
|
|
|
query[KnotCtlDataIdx.ZONE] = zone
|
2016-07-15 06:24:00 -04:00
|
|
|
query[KnotCtlDataIdx.OWNER] = owner
|
|
|
|
|
query[KnotCtlDataIdx.TTL] = ttl
|
|
|
|
|
query[KnotCtlDataIdx.TYPE] = rtype
|
2016-05-06 03:39:25 -04:00
|
|
|
query[KnotCtlDataIdx.DATA] = data
|
2016-11-07 03:34:26 -05:00
|
|
|
query[KnotCtlDataIdx.FLAGS] = flags
|
2017-05-25 03:43:17 -04:00
|
|
|
query[KnotCtlDataIdx.FILTER] = filter
|
2016-05-06 03:39:25 -04:00
|
|
|
|
|
|
|
|
self.send(KnotCtlType.DATA, query)
|
|
|
|
|
self.send(KnotCtlType.BLOCK)
|
|
|
|
|
|
2016-07-15 06:24:00 -04:00
|
|
|
def _receive_conf(self, out, reply):
|
|
|
|
|
|
|
|
|
|
section = reply[KnotCtlDataIdx.SECTION]
|
|
|
|
|
ident = reply[KnotCtlDataIdx.ID]
|
|
|
|
|
item = reply[KnotCtlDataIdx.ITEM]
|
|
|
|
|
data = reply[KnotCtlDataIdx.DATA]
|
|
|
|
|
|
|
|
|
|
# Add the section if not exists.
|
|
|
|
|
if section not in out:
|
|
|
|
|
out[section] = dict()
|
|
|
|
|
|
|
|
|
|
# Add the identifier if not exists.
|
2016-09-23 03:49:11 -04:00
|
|
|
if ident and ident not in out[section]:
|
|
|
|
|
out[section][ident] = dict()
|
2016-07-15 06:24:00 -04:00
|
|
|
|
|
|
|
|
# Return if no item/value.
|
|
|
|
|
if not item:
|
|
|
|
|
return
|
|
|
|
|
|
2016-09-23 03:49:11 -04:00
|
|
|
item_level = out[section][ident] if ident else out[section]
|
2016-07-15 06:24:00 -04:00
|
|
|
|
|
|
|
|
# Treat alone identifier item differently.
|
|
|
|
|
if item in ["id", "domain", "target"]:
|
2016-09-23 03:49:11 -04:00
|
|
|
if data not in out[section]:
|
|
|
|
|
out[section][data] = dict()
|
2016-07-15 06:24:00 -04:00
|
|
|
else:
|
|
|
|
|
if item not in item_level:
|
|
|
|
|
item_level[item] = list()
|
|
|
|
|
|
|
|
|
|
if data:
|
|
|
|
|
item_level[item].append(data)
|
|
|
|
|
|
|
|
|
|
def _receive_zone_status(self, out, reply):
|
|
|
|
|
|
|
|
|
|
zone = reply[KnotCtlDataIdx.ZONE]
|
|
|
|
|
rtype = reply[KnotCtlDataIdx.TYPE]
|
|
|
|
|
data = reply[KnotCtlDataIdx.DATA]
|
|
|
|
|
|
|
|
|
|
# Add the zone if not exists.
|
|
|
|
|
if zone not in out:
|
|
|
|
|
out[zone] = dict()
|
|
|
|
|
|
|
|
|
|
out[zone][rtype] = data
|
|
|
|
|
|
|
|
|
|
def _receive_zone(self, out, reply):
|
|
|
|
|
|
|
|
|
|
zone = reply[KnotCtlDataIdx.ZONE]
|
|
|
|
|
owner = reply[KnotCtlDataIdx.OWNER]
|
|
|
|
|
ttl = reply[KnotCtlDataIdx.TTL]
|
|
|
|
|
rtype = reply[KnotCtlDataIdx.TYPE]
|
|
|
|
|
data = reply[KnotCtlDataIdx.DATA]
|
|
|
|
|
|
|
|
|
|
# Add the zone if not exists.
|
|
|
|
|
if zone not in out:
|
|
|
|
|
out[zone] = dict()
|
|
|
|
|
|
|
|
|
|
if owner not in out[zone]:
|
|
|
|
|
out[zone][owner] = dict()
|
|
|
|
|
|
|
|
|
|
if rtype not in out[zone][owner]:
|
|
|
|
|
out[zone][owner][rtype] = dict()
|
|
|
|
|
|
|
|
|
|
# Add the key/value.
|
|
|
|
|
out[zone][owner][rtype]["ttl"] = ttl
|
|
|
|
|
|
|
|
|
|
if not "data" in out[zone][owner][rtype]:
|
|
|
|
|
out[zone][owner][rtype]["data"] = [data]
|
|
|
|
|
else:
|
|
|
|
|
out[zone][owner][rtype]["data"].append(data)
|
|
|
|
|
|
2016-11-07 03:34:26 -05:00
|
|
|
def _receive_stats(self, out, reply):
|
|
|
|
|
|
|
|
|
|
zone = reply[KnotCtlDataIdx.ZONE]
|
|
|
|
|
section = reply[KnotCtlDataIdx.SECTION]
|
|
|
|
|
item = reply[KnotCtlDataIdx.ITEM]
|
|
|
|
|
idx = reply[KnotCtlDataIdx.ID]
|
2017-02-10 03:10:59 -05:00
|
|
|
data = int(reply[KnotCtlDataIdx.DATA])
|
2016-11-07 03:34:26 -05:00
|
|
|
|
|
|
|
|
# Add the zone if not exists.
|
|
|
|
|
if zone:
|
|
|
|
|
if "zone" not in out:
|
|
|
|
|
out["zone"] = dict()
|
|
|
|
|
|
|
|
|
|
if zone not in out["zone"]:
|
|
|
|
|
out["zone"][zone] = dict()
|
|
|
|
|
|
|
|
|
|
section_level = out["zone"][zone] if zone else out
|
|
|
|
|
|
|
|
|
|
if section not in section_level:
|
|
|
|
|
section_level[section] = dict()
|
|
|
|
|
|
|
|
|
|
if idx:
|
|
|
|
|
if item not in section_level[section]:
|
|
|
|
|
section_level[section][item] = dict()
|
|
|
|
|
|
|
|
|
|
section_level[section][item][idx] = data
|
|
|
|
|
else:
|
|
|
|
|
section_level[section][item] = data
|
|
|
|
|
|
|
|
|
|
def receive_stats(self):
|
|
|
|
|
"""Receives statistics answer and returns it as a structured dictionary.
|
|
|
|
|
|
|
|
|
|
@rtype: dict
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
out = dict()
|
2017-01-09 07:46:00 -05:00
|
|
|
err_reply = None
|
2016-11-07 03:34:26 -05:00
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
reply = KnotCtlData()
|
|
|
|
|
reply_type = self.receive(reply)
|
|
|
|
|
|
|
|
|
|
# Stop if not data type.
|
|
|
|
|
if reply_type not in [KnotCtlType.DATA, KnotCtlType.EXTRA]:
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
# Check for an error.
|
|
|
|
|
if reply[KnotCtlDataIdx.ERROR]:
|
2017-01-09 07:46:00 -05:00
|
|
|
err_reply = reply
|
|
|
|
|
continue
|
2016-11-07 03:34:26 -05:00
|
|
|
|
|
|
|
|
self._receive_stats(out, reply)
|
|
|
|
|
|
2017-01-09 07:46:00 -05:00
|
|
|
if err_reply:
|
|
|
|
|
raise KnotCtlError(err_reply[KnotCtlDataIdx.ERROR], err_reply)
|
|
|
|
|
|
2016-11-07 03:34:26 -05:00
|
|
|
return out
|
|
|
|
|
|
2016-05-06 03:39:25 -04:00
|
|
|
def receive_block(self):
|
|
|
|
|
"""Receives a control answer and returns it as a structured dictionary.
|
|
|
|
|
|
|
|
|
|
@rtype: dict
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
out = dict()
|
2017-01-09 07:46:00 -05:00
|
|
|
err_reply = None
|
2016-05-06 03:39:25 -04:00
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
reply = KnotCtlData()
|
|
|
|
|
reply_type = self.receive(reply)
|
2017-01-09 07:46:00 -05:00
|
|
|
|
2016-05-06 03:39:25 -04:00
|
|
|
# Stop if not data type.
|
|
|
|
|
if reply_type not in [KnotCtlType.DATA, KnotCtlType.EXTRA]:
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
# Check for an error.
|
|
|
|
|
if reply[KnotCtlDataIdx.ERROR]:
|
2017-01-09 07:46:00 -05:00
|
|
|
err_reply = reply
|
|
|
|
|
continue
|
2016-05-06 03:39:25 -04:00
|
|
|
|
|
|
|
|
# Check for config data.
|
|
|
|
|
if reply[KnotCtlDataIdx.SECTION]:
|
2016-07-15 06:24:00 -04:00
|
|
|
self._receive_conf(out, reply)
|
2016-05-06 03:39:25 -04:00
|
|
|
# Check for zone data.
|
|
|
|
|
elif reply[KnotCtlDataIdx.ZONE]:
|
2016-07-15 06:24:00 -04:00
|
|
|
if reply[KnotCtlDataIdx.OWNER]:
|
|
|
|
|
self._receive_zone(out, reply)
|
|
|
|
|
else:
|
|
|
|
|
self._receive_zone_status(out, reply)
|
2016-05-06 03:39:25 -04:00
|
|
|
else:
|
|
|
|
|
continue
|
|
|
|
|
|
2017-01-09 07:46:00 -05:00
|
|
|
if err_reply:
|
|
|
|
|
raise KnotCtlError(err_reply[KnotCtlDataIdx.ERROR], err_reply)
|
|
|
|
|
|
2016-05-06 03:39:25 -04:00
|
|
|
return out
|