Higher-level API for KNOT, support for basic zone editing

parent 6e84e0ba
This diff is collapsed.
This diff is collapsed.
......@@ -9,6 +9,13 @@
"conformance-type": "import",
"schema": "https://gitlab.labs.nic.cz/labs/dns-server-yang/raw/master/dns-parameters.yang"
},
{
"name": "iana-dns-parameters",
"revision": "2015-08-11",
"namespace": "http://www.nic.cz/ns/yang/dns-parameters",
"conformance-type": "import",
"schema": "https://gitlab.labs.nic.cz/llhotka/zone-data-yang/blob/master/iana-dns-parameters.yang"
},
{
"name": "dns-server",
"revision": "2016-01-14",
......@@ -21,6 +28,13 @@
"conformance-type": "implement",
"schema": "https://gitlab.labs.nic.cz/labs/dns-server-yang/raw/master/dns-server.yang"
},
{
"name": "dns-zones",
"revision": "2015-08-11",
"namespace": "http://www.nic.cz/ns/yang/dns-zones",
"conformance-type": "implement",
"schema": "https://gitlab.labs.nic.cz/llhotka/zone-data-yang/blob/master/dns-zones.yang"
},
{
"name": "dnssec-algorithms",
"revision": "2016-01-13",
......
This diff is collapsed.
......@@ -2,7 +2,7 @@ import json
from threading import Lock
from enum import Enum
from colorlog import error, warning as warn, info, debug
from typing import List, Any, Dict, TypeVar, Tuple, Set
from typing import List, Any, Dict, TypeVar, Tuple, Set, Callable
from pydispatch import dispatcher
from yangson.schema import SchemaRoute, SchemaNode, NonexistentSchemaNode, ListNode, LeafListNode
......@@ -73,16 +73,13 @@ class NoHandlerForStateDataError(NoHandlerError):
class BaseDataListener:
def __init__(self, ds: "BaseDatastore"):
def __init__(self, ds: "BaseDatastore", sch_pth: str):
self._ds = ds
self.schema_paths = []
self.schema_path = sch_pth # type: str
self.schema_node = ds.get_schema_node(sch_pth) # type: SchemaNode
dispatcher.connect(self.process, str(id(self.schema_node)))
def add_schema_node(self, sch_pth: str):
sn = self._ds.get_schema_node(sch_pth)
self.schema_paths.append(sch_pth)
dispatcher.connect(self.process, str(id(sn)))
def process(self, sn: SchemaNode, ii: InstancePath):
def process(self, sn: SchemaNode, ii: InstancePath, ch: "DataChange"):
raise NotImplementedError("Not implemented in base class")
def __str__(self):
......@@ -177,10 +174,11 @@ class UsrChangeJournal:
for cl in self.clists:
for change in cl.journal:
ii = ds.parse_ii(change.rpc_info.path, change.rpc_info.path_format)
if change.change_type != ChangeType.DELETE:
ds.notify_edit(ii)
else:
ds.notify_edit(ii[0:-1])
ds.notify_edit(ii, change)
# if change.change_type != ChangeType.DELETE:
# ds.notify_edit(ii)
# else:
# ds.notify_edit(ii[0:-1])
# Clear user changelists
self.clists.clear()
......@@ -198,6 +196,8 @@ class BaseDatastore:
self._data_lock = Lock()
self._lock_username = None # type: str
self._usr_journals = {} # type: Dict[str, UsrChangeJournal]
self.commit_begin_callback = None # type: Callable
self.commit_end_callback = None # type: Callable
# Register NACM module to datastore
def register_nacm(self, nacm_config: "NacmConfig"):
......@@ -207,6 +207,15 @@ class BaseDatastore:
def get_data_root(self) -> InstanceNode:
return self._data
# Returns the root node of data tree
def get_data_root_staging(self, username: str) -> InstanceNode:
usr_journal = self._usr_journals.get(username)
if usr_journal is not None:
root = usr_journal.get_root_head()
return root
else:
raise NoHandlerError("No active changelist for user \"{}\"".format(username))
# Set a new Instance node as data root
def set_data_root(self, new_root: InstanceNode):
self._data = new_root
......@@ -228,12 +237,18 @@ class BaseDatastore:
return ii
# Notify data observers about change in datastore
def notify_edit(self, ii: InstancePath):
n = self._data.goto(ii)
sn = n.schema_node
while sn is not None:
dispatcher.send(str(id(sn)), **{'sn': sn, 'ii': ii})
sn = sn.parent
def notify_edit(self, ii: InstancePath, ch: DataChange):
try:
# n = self._data.goto(ii)
# sn = n.schema_node
sch_pth = str(InstancePath(filter(lambda n: isinstance(n, MemberName), ii)))
sn = self.get_schema_node(sch_pth)
while sn is not None:
dispatcher.send(str(id(sn)), **{'sn': sn, 'ii': ii, 'ch': ch})
sn = sn.parent
except NonexistentInstance:
warn("Cannnot notify {}, parent container removed".format(ii))
# Just get the node, do not evaluate NACM (needed for NACM)
def get_node(self, root: InstanceNode, ii: InstancePath) -> InstanceNode:
......@@ -274,13 +289,8 @@ class BaseDatastore:
def get_node_staging_rpc(self, rpc: RpcInfo) -> InstanceNode:
ii = self.parse_ii(rpc.path, rpc.path_format)
usr_journal = self._usr_journals.get(rpc.username)
if usr_journal is not None:
root = usr_journal.get_root_head()
n = root.goto(ii)
else:
raise NoHandlerError("No active changelist for user \"{}\"".format(rpc.username))
root = self.get_data_root_staging(rpc.username)
n = root.goto(ii)
if self.nacm:
nrpc = self.nacm.get_user_nacm(rpc.username)
......@@ -462,7 +472,11 @@ class BaseDatastore:
elif rpc.op_name == "conf-commit":
usr_journal = self._usr_journals.get(rpc.username)
if usr_journal is not None:
if self.commit_begin_callback is not None:
self.commit_begin_callback()
usr_journal.commit(self)
if self.commit_end_callback is not None:
self.commit_end_callback()
del self._usr_journals[rpc.username]
else:
warn("Nothing to commit")
......
......@@ -117,6 +117,7 @@
"rpc-name": "zone-sign",
"comment": "Users can sign zones",
"action": "permit"
}
]
}
......
......@@ -332,5 +332,53 @@
}
}
]
},
"dns-zones:zones": {
"zone": [
{
"name": "pokus.cz",
"class": "IN",
"default-ttl": 100,
"SOA": {
"mname": "dns1.pokus.cz",
"rname": "hostmaster.pokus.cz",
"serial": 20160621,
"refresh": 200,
"retry": 300,
"expire": 400,
"minimum": 500
},
"rrset": [
{
"owner": "sub1.pokus.cz",
"type": "A",
"rdata": [
{
"A": {
"address": "192.168.100.100"
}
}
]
},
{
"owner": "pokus.cz",
"type": "MX",
"rdata": [
{
"MX": {
"preference": 0,
"exchange": "sub1.pokus.cz"
}
}
]
}
]
},
{
"name": "example.com",
"class": "IN",
"default-ttl": 100
}
]
}
}
......@@ -286,7 +286,7 @@ def _post(prot: "H2Protocol", data: bytes, stream_id: int, ds: BaseDatastore, pt
ds.lock_data(username)
ins_pos = (query_string.get("insert") or [None])[0]
point = (query_string.get("point") or [None])[0]
new_root = ds.create_node_rpc(ds.get_data_root(), rpc1, json_data, insert=ins_pos, point=point)
new_root = ds.create_node_rpc(ds.get_data_root_staging(rpc1.username), rpc1, json_data, insert=ins_pos, point=point)
ds.add_to_journal_rpc(ChangeType.CREATE, rpc1, json_data, new_root)
prot.send_empty(stream_id, "201", "Created")
except DataLockError as e:
......@@ -363,7 +363,7 @@ def _put(prot: "H2Protocol", data: bytes, stream_id: int, ds: BaseDatastore, pth
try:
ds.lock_data(username)
new_root = ds.update_node_rpc(ds.get_data_root(), rpc1, json_data)
new_root = ds.update_node_rpc(ds.get_data_root_staging(rpc1.username), rpc1, json_data)
ds.add_to_journal_rpc(ChangeType.REPLACE, rpc1, json_data, new_root)
prot.send_empty(stream_id, "204", "No Content", False)
except DataLockError as e:
......@@ -418,7 +418,7 @@ def _delete(prot: "H2Protocol", stream_id: int, ds: BaseDatastore, pth: str):
try:
ds.lock_data(username)
new_root = ds.delete_node_rpc(ds.get_data_root(), rpc1)
new_root = ds.delete_node_rpc(ds.get_data_root_staging(rpc1.username), rpc1)
ds.add_to_journal_rpc(ChangeType.DELETE, rpc1, None, new_root)
prot.send_empty(stream_id, "204", "No Content", False)
except DataLockError as e:
......
from enum import Enum
from typing import List
from threading import Lock
from .libknot.control import KnotCtl, KnotCtlType
KNOT = None # type: KnotConfig
class KnotConfState(Enum):
NONE = 0
CONF = 1
ZONE = 2
class KnotStateError(Exception):
def __init__(self, msg=""):
self.msg = msg
def __str__(self):
return self.msg
class RRecordBase:
def __init__(self, owner_name: str, res_type: str, ttl: int=3600):
self.owner = owner_name
self.type = res_type
self.ttl = ttl
def rrdata_format(self) -> str:
raise NotImplementedError("Not implemented in base class")
class SOARecord(RRecordBase):
def __init__(self, owner_name: str):
super().__init__(owner_name, "SOA")
self.mname = None # type: str
self.rname = None # type: str
self.serial = None # type: str
self.refresh = None # type: str
self.retry = None # type: str
self.expire = None # type: str
self.minimum = None # type: str
def rrdata_format(self) -> str:
return "{} {} {} {} {} {} {}".format(
self.mname, self.rname, self.serial, self.refresh, self.retry, self.expire, self.minimum
)
class ARecord(RRecordBase):
def __init__(self, owner_name: str):
super().__init__(owner_name, "A")
self.address = None # type: str
def rrdata_format(self) -> str:
return self.address
class MXRecord(RRecordBase):
def __init__(self, owner_name: str):
super().__init__(owner_name, "MX")
self.preference = None # type: str
self.exchange = None # type: str
def rrdata_format(self) -> str:
return self.exchange
class KnotConfig(KnotCtl):
def __init__(self, sock_path: str):
super().__init__()
self.sock_path = sock_path
self.connected = False
self.socket_lock = Lock()
self.conf_state = KnotConfState.NONE
def knot_connect(self):
if self.connected:
raise Exception("Knot socket already opened")
if not self.socket_lock.acquire(blocking=True, timeout=5):
raise Exception("Cannot acquire Knot socket lock")
self.connect(self.sock_path)
self.connected = True
def knot_disconnect(self):
self.send(KnotCtlType.END)
self.close()
self.connected = False
self.socket_lock.release()
def begin(self):
if self.conf_state == KnotConfState.NONE:
self.send_block("conf-begin")
self.conf_state = KnotConfState.CONF
def begin_zone(self):
if self.conf_state == KnotConfState.NONE:
self.send_block("zone-begin")
self.conf_state = KnotConfState.ZONE
def commit(self):
if self.conf_state == KnotConfState.CONF:
self.send_block("conf-commit")
self.conf_state = KnotConfState.NONE
else:
raise KnotStateError()
def commit_zone(self):
if self.conf_state == KnotConfState.ZONE:
self.send_block("zone-commit")
self.conf_state = KnotConfState.NONE
else:
raise KnotStateError()
def set_item(self, item=None, section=None, identifier=None, zone=None, data: str=None):
if not self.connected:
raise Exception("Knot socket is closed")
if data is not None:
self.send_block("conf-set", section=section, identifier=identifier, item=item, zone=zone, data=data)
else:
self.send_block("conf-unset", section=section, identifier=identifier, item=item, zone=zone)
def set_item_list(self, item=None, section=None, identifier=None, zone=None, data: List[str]=None):
if not self.connected:
raise Exception("Knot socket is closed")
self.send_block("conf-unset", section=section, identifier=identifier, item=item, zone=zone)
if data is None:
return
for data_item in data:
self.send_block("conf-set", section=section, identifier=identifier, item=item, zone=zone, data=data_item)
def set_zone_item(self, section=None, identifier=None, item=None, zone=None, owner=None, ttl=None, type=None, data=None):
if not self.connected:
raise Exception("Knot socket is closed")
if data is not None:
self.send_block("zone-add", section=section, identifier=identifier, item=item, zone=zone, owner=owner, ttl=ttl, type=type, data=data)
else:
self.send_block("zone-remove", section=section, identifier=identifier, item=item, zone=zone, owner=owner, ttl=ttl, type=type, data=data)
def zone_new(self, domain_name: str) -> str:
self.set_item(section="zone", item="domain", data=domain_name)
resp = self.receive_block()
return resp
def zone_add_record(self, domain_name: str, rr: RRecordBase) -> str:
self.set_zone_item(zone=domain_name, owner=rr.owner, ttl="3600", type=rr.type, data=rr.rrdata_format())
resp = self.receive_zone_block()
return resp
"""Libknot server control interface wrapper.
Example:
ctl = KnotCtl()
ctl.connect("/var/run/knot/knot.sock")
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()
"""
from ctypes import cdll, c_void_p, c_int, c_char_p, c_uint, byref
from enum import IntEnum
LIB = cdll.LoadLibrary('libknot.so.2')
CTL_ALLOC = LIB.knot_ctl_alloc
CTL_ALLOC.restype = c_void_p
CTL_FREE = LIB.knot_ctl_free
CTL_FREE.argtypes = [c_void_p]
CTL_SET_TIMEOUT = LIB.knot_ctl_set_timeout
CTL_SET_TIMEOUT.argtypes = [c_void_p, c_int]
CTL_CONNECT = LIB.knot_ctl_connect
CTL_CONNECT.restype = c_int
CTL_CONNECT.argtypes = [c_void_p, c_char_p]
CTL_CLOSE = LIB.knot_ctl_close
CTL_CLOSE.argtypes = [c_void_p]
CTL_SEND = LIB.knot_ctl_send
CTL_SEND.restype = c_int
CTL_SEND.argtypes = [c_void_p, c_uint, c_void_p]
CTL_RECEIVE = LIB.knot_ctl_receive
CTL_RECEIVE.restype = c_int
CTL_RECEIVE.argtypes = [c_void_p, c_void_p, c_void_p]
CTL_ERROR = LIB.knot_strerror
CTL_ERROR.restype = c_char_p
CTL_ERROR.argtypes = [c_int]
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
class KnotCtlData(object):
"""Libknot server control data unit."""
DataArray = c_char_p * len(KnotCtlDataIdx)
def __init__(self):
self.data = self.DataArray()
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
"""
self.data[index] = c_char_p(value.encode()) if value else c_char_p()
class KnotCtl(object):
"""Libknot server control interface."""
def __init__(self):
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)
raise Exception(err if isinstance(err, str) else err.decode())
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)
raise Exception(err if isinstance(err, str) else err.decode())
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)
raise Exception(err if isinstance(err, str) else err.decode())
return KnotCtlType(data_type.value)
def send_block(self, cmd, section=None, identifier=None, item=None, zone=None, owner=None, ttl=None, type=None, data=None):
"""Sends a control query block.
@type cmd: str
@type section: str
@type item: str
@type identifier: str
@type zone: str
@type data: str
"""
query = KnotCtlData()
query[KnotCtlDataIdx.COMMAND] = cmd
query[KnotCtlDataIdx.SECTION] = section
query[KnotCtlDataIdx.ITEM] = item
query[KnotCtlDataIdx.ID] = identifier
query[KnotCtlDataIdx.ZONE] = zone
query[KnotCtlDataIdx.OWNER] = owner
query[KnotCtlDataIdx.TTL] = ttl
query[KnotCtlDataIdx.TYPE] = type
query[KnotCtlDataIdx.DATA] = data
self.send(KnotCtlType.DATA, query)
self.send(KnotCtlType.BLOCK)
def receive_block(self):
"""Receives a control answer and returns it as a structured dictionary.
@rtype: dict
"""
out = dict()
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]:
raise Exception(reply[KnotCtlDataIdx.ERROR])
# Check for config data.
if reply[KnotCtlDataIdx.SECTION]:
ident = reply[KnotCtlDataIdx.ID]
key = reply[KnotCtlDataIdx.ITEM]
# Check for zone data.
elif reply[KnotCtlDataIdx.ZONE]:
ident = reply[KnotCtlDataIdx.ZONE]
key = reply[KnotCtlDataIdx.TYPE]
else:
continue
section = reply[KnotCtlDataIdx.SECTION]
data = reply[KnotCtlDataIdx.DATA]
# Add the section if not exists.
if section:
if section not in out:
out[section] = dict()
level1 = out[section] if section else out
# Add the identifier if not exists.
if ident:
if ident not in level1:
level1[ident] = dict()
level2 = level1[ident] if ident else level1
# Add the key/value.
if key:
# Treat alone identifier item differently.
if reply[KnotCtlDataIdx.SECTION] and key in ["id", "domain", "target"]:
level1[data] = dict()
else:
if key not in level2:
level2[key] = list()
if data:
level2[key].append(data)
return out
def receive_zone_block(self):
"""Receives a control answer and returns it as a structured dictionary.
@rtype: dict
"""
out = dict()
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]:
raise Exception(reply[KnotCtlDataIdx.ERROR])
# Check for zone data.
if reply[KnotCtlDataIdx.ZONE] and reply[KnotCtlDataIdx.OWNER]:
ident = reply[KnotCtlDataIdx.ZONE]
owner = reply[KnotCtlDataIdx.OWNER]
else:
continue
type = reply[KnotCtlDataIdx.TYPE]
ttl = reply[KnotCtlDataIdx.TTL]
data = reply[KnotCtlDataIdx.DATA]
# Add the owner if not exists.
if owner:
if owner not in out:
out[owner] = dict()
level1 = out[owner] if owner else out
# Add the identifier if not exists.
if type:
if type not in level1:
level1[type] = list()
if ttl:
level1[type].append(ttl)
level1[type].append(list())
level2 = level1[type][1]
#level2 = level1[type] if type else level1
# Add the key/value.
if type and data:
level2.append(data)
return out
......@@ -5,6 +5,7 @@ from yangson.datamodel import DataModel
from yangson.instance import InstanceIdentifier, InstanceNode