Improved GET method to return data in correct RESTCONF format

parent 708cf95c
......@@ -9,13 +9,12 @@ from yangson.datamodel import DataModel
from yangson.enumerations import ContentType, ValidationScope
from yangson.schema import (
SchemaNode,
NonexistentSchemaNode,
ListNode,
LeafListNode,
SchemaError,
SemanticError,
InternalNode
)
InternalNode,
ContainerNode)
from yangson.instance import (
InstanceNode,
NonexistentInstance,
......@@ -33,7 +32,7 @@ from yangson.instance import (
from .helpers import PathFormat, ErrorHelpers, LogHelpers, DataHelpers, JsonNodeT
from .config import CONFIG
from .nacm import NacmConfig, Permission, Action
from .handler_list import OP_HANDLERS, STATE_DATA_HANDLES, CONF_DATA_HANDLES
from .handler_list import OP_HANDLERS, STATE_DATA_HANDLES, CONF_DATA_HANDLES, ConfDataObjectHandler, ConfDataListHandler
epretty = ErrorHelpers.epretty
debug_data = LogHelpers.create_module_dbg_logger(__name__)
......@@ -98,19 +97,6 @@ class NoHandlerForStateDataError(NoHandlerError):
pass
class BaseDataListener:
def __init__(self, ds: "BaseDatastore", sch_pth: str):
self.ds = ds
self.schema_path = sch_pth # type: str
self.schema_node = ds.get_schema_node(sch_pth) # type: SchemaNode
def process(self, sn: SchemaNode, ii: InstanceRoute, ch: "DataChange"):
raise NotImplementedError("Not implemented in base class")
def __str__(self):
return self.__class__.__name__ + ": listening at " + self.schema_path
class RpcInfo:
def __init__(self):
self.username = None # type: str
......@@ -305,7 +291,7 @@ class BaseDatastore:
# Get schema node with particular schema address
def get_schema_node(self, sch_pth: str) -> SchemaNode:
sn = self._dm.get_schema_node(sch_pth)
sn = self._dm.get_data_node(sch_pth)
if sn is None:
# raise NonexistentSchemaNode(sch_pth)
debug_data("Cannot find schema node for " + sch_pth)
......@@ -314,21 +300,50 @@ class BaseDatastore:
# Notify data observers about change in datastore
def run_conf_edit_handler(self, ii: InstanceRoute, ch: DataChange):
try:
sch_pth_list = filter(lambda n: isinstance(n, MemberName), ii)
sch_pth_list = list(filter(lambda n: isinstance(n, MemberName), ii))
if ch.change_type == ChangeType.CREATE:
# Get target member name
input_member_name = tuple(ch.data.keys())[0]
# Append it to ii
sch_pth_list.append(MemberName(input_member_name))
sch_pth = DataHelpers.ii2str(sch_pth_list)
sn = self.get_schema_node(sch_pth)
while sn is not None:
h = CONF_DATA_HANDLES.get_handler(str(id(sn)))
if h is not None:
h.process(sn, ii, ch)
if sn is None:
return
h = CONF_DATA_HANDLES.get_handler(str(id(sn)))
if h is not None:
info("handler for actual data node triggered")
if ch.change_type == ChangeType.CREATE:
h.create(ii, ch)
elif ch.change_type == ChangeType.REPLACE:
h.replace(ii, ch)
elif ch.change_type == ChangeType.DELETE:
h.delete(ii, ch)
else:
sn = sn.parent
while sn is not None:
h = CONF_DATA_HANDLES.get_handler(str(id(sn)))
if h is not None and isinstance(h, ConfDataObjectHandler):
info("handler for superior data node triggered, replace")
h.replace(ii, ch)
if h is not None and isinstance(h, ConfDataListHandler):
info("handler for superior data node triggered, replace_item")
h.replace_item(ii, ch)
sn = sn.parent
except NonexistentInstance:
warn("Cannnot notify {}, parent container removed".format(ii))
# Get data node, evaluate NACM if required
def get_node_rpc(self, rpc: RpcInfo, yl_data=False, staging=False) -> InstanceNode:
ii = DataHelpers.parse_ii(rpc.path, rpc.path_format)
if rpc.path == "":
ii = []
else:
ii = DataHelpers.parse_ii(rpc.path, rpc.path_format)
if yl_data:
root = self._yang_lib_data
else:
......
from typing import List, Tuple, Callable, Any
from yangson.context import Context
from yangson.schema import SchemaNode
from yangson.instance import InstanceRoute
HandlerSelectorT = Any
class ConfDataObjectHandler:
def __init__(self, ds: "BaseDatastore", sch_pth: str):
self.ds = ds
self.schema_path = sch_pth # type: str
self.schema_node = ds.get_schema_node(sch_pth) # type: SchemaNode
def create(self, ii: InstanceRoute, ch: "DataChange"):
pass
def replace(self, ii: InstanceRoute, ch: "DataChange"):
pass
def delete(self, ii: InstanceRoute, ch: "DataChange"):
pass
def __str__(self):
return self.__class__.__name__ + ": listening at " + self.schema_path
class ConfDataListHandler:
def __init__(self, ds: "BaseDatastore", sch_pth: str):
self.ds = ds
self.schema_path = sch_pth # type: str
self.schema_node = ds.get_schema_node(sch_pth) # type: SchemaNode
def create_item(self, ii: InstanceRoute, ch: "DataChange"):
pass
def replace_item(self, ii: InstanceRoute, ch: "DataChange"):
pass
def delete_item(self, ii: InstanceRoute, ch: "DataChange"):
pass
def create_list(self, ii: InstanceRoute, ch: "DataChange"):
pass
def replace_list(self, ii: InstanceRoute, ch: "DataChange"):
pass
def delete_list(self, ii: InstanceRoute, ch: "DataChange"):
pass
def __str__(self):
return self.__class__.__name__ + ": listening at " + self.schema_path
class BaseHandlerList:
def __init__(self):
self.handlers = [] # type: List[Tuple[HandlerSelectorT, Callable]]
......@@ -16,7 +64,7 @@ class BaseHandlerList:
def register_default_handler(self, handler: Callable):
self.default_handler = handler
def get_handler(self, identifier: str) -> Callable:
def get_handler(self, identifier: str) -> Any:
raise NotImplementedError("Not implemented in base class")
......@@ -41,7 +89,7 @@ class ConfDataHandlerList:
sch_node_id = str(id(schema_node))
self.handlers.append((sch_node_id, handler))
def get_handler(self, sch_node_id: str) -> Callable:
def get_handler(self, sch_node_id: str) -> "BaseDataListener":
for h in self.handlers:
if h[0] == sch_node_id:
return h[1]
......
......@@ -8,7 +8,7 @@ from colorlog import error, warning as warn, info
from urllib.parse import parse_qs
from typing import Dict, List, Optional
from yangson.schema import NonexistentSchemaNode
from yangson.schema import NonexistentSchemaNode, ContainerNode, ListNode, GroupNode
from yangson.instance import NonexistentInstance, InstanceValueError
from yangson.datatype import YangTypeError
......@@ -34,6 +34,10 @@ CT_PLAIN = "text/plain"
CT_YANG_JSON = "application/yang.api+json"
class HttpRequestError(Exception):
pass
class HttpStatus(Enum):
Ok = ("200", "OK")
Created = ("201", "Created")
......@@ -126,7 +130,33 @@ def _get(ds: BaseDatastore, pth: str, username: str, yl_data: bool=False, stagin
ds.unlock_data()
if n is not None:
response = json.dumps(n.raw_value(), indent=4)
n_value = n.raw_value()
try:
env_n = n.up()
env_sn = env_n.schema_node
print(n.schema_node)
print(env_sn)
if isinstance(env_sn, (ContainerNode, GroupNode)):
sn = n.schema_node
restconf_env = "{}:{}".format(sn.qual_name[1], sn.qual_name[0])
restconf_n_value = {restconf_env: n_value}
elif isinstance(env_sn, ListNode):
sn = n.schema_node
restconf_env = "{}:{}".format(sn.qual_name[1], sn.qual_name[0])
if isinstance(n_value, list):
# List and list item points to the same schema node
restconf_n_value = {restconf_env: n_value}
else:
restconf_n_value = {restconf_env: [n_value]}
else:
raise HttpRequestError()
except NonexistentInstance:
# Getting root node (cannot go up)
restconf_env = "ietf-restconf:data"
restconf_n_value = {restconf_env: n_value}
response = json.dumps(restconf_n_value, indent=4)
add_headers = OrderedDict()
add_headers["ETag"] = str(hash(n.value))
......@@ -142,6 +172,9 @@ def _get(ds: BaseDatastore, pth: str, username: str, yl_data: bool=False, stagin
except DataLockError as e:
warn(epretty(e))
http_resp = HttpResponse.empty(HttpStatus.InternalServerError)
except HttpRequestError as e:
warn(epretty(e))
http_resp = HttpResponse.empty(HttpStatus.BadRequest)
return http_resp
......
......@@ -266,14 +266,29 @@ class KnotConfig(KnotCtl):
raise KnotInternalError(str(e))
return resp
# Purges all zone data
def zone_purge(self, domain_name: str = None) -> JsonNodeT:
if not self.connected:
raise KnotApiError("Knot socket is closed")
self.send_block("zone-purge", zone=domain_name)
try:
resp = self.receive_block()
except Exception as e:
raise KnotInternalError(str(e))
return resp
# Adds a new DNS zone to configuration section
def zone_new(self, domain_name: str) -> JsonNodeT:
resp = self.set_item(section="zone", identifier=None, item="domain", value=domain_name)
return resp
# Removes a DNS zone from configuration section
def zone_remove(self, domain_name: str) -> JsonNodeT:
def zone_remove(self, domain_name: str, purge_data: bool) -> JsonNodeT:
resp = self.unset_item(section="zone", identifier=domain_name, item="domain")
if purge_data:
self.zone_purge(domain_name)
return resp
# Adds a resource record to DNS zone
......
"""Libknot server control interface wrapper.
Example:
import json
from libknot.control import *
ctl = KnotCtl()
ctl.connect("/var/run/knot/knot.sock")
......@@ -25,35 +28,72 @@ Example:
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 = None
CTL_FREE = None
CTL_SET_TIMEOUT = None
CTL_CONNECT = None
CTL_CLOSE = None
CTL_SEND = None
CTL_RECEIVE = None
CTL_ERROR = None
def load_lib(path="libknot.so"):
"""Loads the libknot library."""
LIB = cdll.LoadLibrary(path)
global CTL_ALLOC
CTL_ALLOC = LIB.knot_ctl_alloc
CTL_ALLOC.restype = c_void_p
global CTL_FREE
CTL_FREE = LIB.knot_ctl_free
CTL_FREE.argtypes = [c_void_p]
global CTL_SET_TIMEOUT
CTL_SET_TIMEOUT = LIB.knot_ctl_set_timeout
CTL_SET_TIMEOUT.argtypes = [c_void_p, c_int]
CTL_ALLOC = LIB.knot_ctl_alloc
CTL_ALLOC.restype = c_void_p
global CTL_CONNECT
CTL_CONNECT = LIB.knot_ctl_connect
CTL_CONNECT.restype = c_int
CTL_CONNECT.argtypes = [c_void_p, c_char_p]
CTL_FREE = LIB.knot_ctl_free
CTL_FREE.argtypes = [c_void_p]
global CTL_CLOSE
CTL_CLOSE = LIB.knot_ctl_close
CTL_CLOSE.argtypes = [c_void_p]
CTL_SET_TIMEOUT = LIB.knot_ctl_set_timeout
CTL_SET_TIMEOUT.argtypes = [c_void_p, c_int]
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]
CTL_CONNECT = LIB.knot_ctl_connect
CTL_CONNECT.restype = c_int
CTL_CONNECT.argtypes = [c_void_p, c_char_p]
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]
CTL_CLOSE = LIB.knot_ctl_close
CTL_CLOSE.argtypes = [c_void_p]
global CTL_ERROR
CTL_ERROR = LIB.knot_strerror
CTL_ERROR.restype = c_char_p
CTL_ERROR.argtypes = [c_int]
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]
class KnotCtlError(Exception):
"""Libknot server control error."""
CTL_ERROR = LIB.knot_strerror
CTL_ERROR.restype = c_char_p
CTL_ERROR.argtypes = [c_int]
def __init__(self, message, data=None):
"""
@type message: str
@type data: KnotCtlData
"""
self.message = message
self.data = data
def __str__(self):
return "message: %s\ndata: %s" % (self.message, self.data)
class KnotCtlType(IntEnum):
......@@ -89,6 +129,17 @@ class KnotCtlData(object):
def __init__(self):
self.data = self.DataArray()
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
def __getitem__(self, index):
"""Data unit item getter.
......@@ -114,6 +165,8 @@ class KnotCtl(object):
"""Libknot server control interface."""
def __init__(self):
if not CTL_ALLOC:
load_lib()
self.obj = CTL_ALLOC()
def __del__(self):
......@@ -136,7 +189,7 @@ class KnotCtl(object):
ret = CTL_CONNECT(self.obj, path.encode())
if ret != 0:
err = CTL_ERROR(ret)
raise Exception(err if isinstance(err, str) else err.decode())
raise KnotCtlError(err if isinstance(err, str) else err.decode())
def close(self):
"""Disconnects from the current control socket."""
......@@ -154,7 +207,7 @@ class KnotCtl(object):
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())
raise KnotCtlError(err if isinstance(err, str) else err.decode())
def receive(self, data=None):
"""Receives a data unit from the connected control socket.
......@@ -168,11 +221,11 @@ class KnotCtl(object):
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())
raise KnotCtlError(err if isinstance(err, str) else err.decode())
return KnotCtlType(data_type.value)
def send_block(self, cmd, section=None, item=None, identifier=None, zone=None,
owner=None, ttl=None, rtype=None, data=None):
owner=None, ttl=None, rtype=None, data=None, flags=None):
"""Sends a control query block.
@type cmd: str
......@@ -196,6 +249,7 @@ class KnotCtl(object):
query[KnotCtlDataIdx.TTL] = ttl
query[KnotCtlDataIdx.TYPE] = rtype
query[KnotCtlDataIdx.DATA] = data
query[KnotCtlDataIdx.FLAGS] = flags
self.send(KnotCtlType.DATA, query)
self.send(KnotCtlType.BLOCK)
......@@ -212,18 +266,19 @@ class KnotCtl(object):
out[section] = dict()
# Add the identifier if not exists.
if ident and ident not in section:
section[ident] = dict()
if ident and ident not in out[section]:
out[section][ident] = dict()
# Return if no item/value.
if not item:
return
item_level = section[ident] if ident else section
item_level = out[section][ident] if ident else out[section]
# Treat alone identifier item differently.
if item in ["id", "domain", "target"]:
section[data] = dict()
if data not in out[section]:
out[section][data] = dict()
else:
if item not in item_level:
item_level[item] = list()
......@@ -269,6 +324,64 @@ class KnotCtl(object):
else:
out[zone][owner][rtype]["data"].append(data)
def _receive_stats(self, out, reply):
zone = reply[KnotCtlDataIdx.ZONE]
section = reply[KnotCtlDataIdx.SECTION]
item = reply[KnotCtlDataIdx.ITEM]
idx = reply[KnotCtlDataIdx.ID]
data = reply[KnotCtlDataIdx.DATA]
# 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()
err_reply = None
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]:
err_reply = reply
continue
self._receive_stats(out, reply)
if err_reply:
raise KnotCtlError(err_reply[KnotCtlDataIdx.ERROR], err_reply)
return out
def receive_block(self):
"""Receives a control answer and returns it as a structured dictionary.
......@@ -276,6 +389,7 @@ class KnotCtl(object):
"""
out = dict()
err_reply = None
while True:
reply = KnotCtlData()
......@@ -287,7 +401,8 @@ class KnotCtl(object):
# Check for an error.
if reply[KnotCtlDataIdx.ERROR]:
raise Exception(reply[KnotCtlDataIdx.ERROR])
err_reply = reply
continue
# Check for config data.
if reply[KnotCtlDataIdx.SECTION]:
......@@ -301,4 +416,7 @@ class KnotCtl(object):
else:
continue
if err_reply:
raise KnotCtlError(err_reply[KnotCtlDataIdx.ERROR], err_reply)
return out
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment