Major code refactor

parent d510df97
......@@ -34,8 +34,8 @@ def main():
tm.test()
except ImportError as e:
print(e.msg)
#except AttributeError:
# print("Module \"{}\" has no test() function".format(test_module))
# except AttributeError:
# print("Module \"{}\" has no test() function".format(test_module))
else:
rest_server.run()
......
import os
import yaml
from colorlog import warning as warn, info
CONFIG_HTTP = {
"DOC_ROOT": "doc-root",
"DOC_DEFAULT_NAME": "index.html",
"RESTCONF_API_ROOT": "/restconf",
"RESTCONF_NACM_API_ROOT": "/restconf_nacm",
"SERVER_NAME": "hyper-h2",
"PORT": 8443,
"SERVER_SSL_CERT": "server.crt",
"SERVER_SSL_PRIVKEY": "server.key",
"CA_CERT": "ca.pem"
}
CONFIG_NACM = {
"ALLOWED_USERS": "lojza@mail.cz"
}
CONFIG = {
"HTTP_SERVER": CONFIG_HTTP,
"NACM": CONFIG_NACM
}
NACM_ADMINS = CONFIG["NACM"]["ALLOWED_USERS"]
RESTCONF_NACM_API_ROOT_data = os.path.join(CONFIG_HTTP["RESTCONF_NACM_API_ROOT"], "data")
RESTCONF_API_ROOT_data = os.path.join(CONFIG_HTTP["RESTCONF_API_ROOT"], "data")
def load_config(filename: str):
global NACM_ADMINS
global RESTCONF_NACM_API_ROOT_data
global RESTCONF_API_ROOT_data
try:
with open(filename) as conf_fd:
conf_yaml = yaml.load(conf_fd)
for conf_key in CONFIG.keys():
try:
CONFIG[conf_key].update(conf_yaml[conf_key])
except KeyError:
pass
except FileNotFoundError:
warn("Configuration file does not exist")
# Shortcuts
NACM_ADMINS = CONFIG["NACM"]["ALLOWED_USERS"]
RESTCONF_NACM_API_ROOT_data = os.path.join(CONFIG_HTTP["RESTCONF_NACM_API_ROOT"], "data")
RESTCONF_API_ROOT_data = os.path.join(CONFIG_HTTP["RESTCONF_API_ROOT"], "data")
def print_config():
info("Using config:\n" + yaml.dump(CONFIG, default_flow_style=False))
......@@ -9,9 +9,10 @@ from colorlog import error, warning as warn, info, debug
from typing import List, Any, Dict, TypeVar, Tuple, Set
import copy
import yangson.instance
from yangson.instance import Instance, NonexistentInstance, ArrayValue, ObjectValue
from yangson.instance import Instance, NonexistentInstance, InstanceError, ArrayValue, ObjectValue, MemberName, EntryKeys
from yangson import DataModel
from yangson.datamodel import InstanceIdentifier
from .helpers import DataHelpers
class PathFormat(Enum):
......@@ -29,6 +30,10 @@ class DataLockError(Exception):
self.msg = msg
class InstanceAlreadyPresent(InstanceError):
pass
class Rpc:
def __init__(self):
self.username = None # type: str
......@@ -92,6 +97,60 @@ class BaseDatastore:
return n
def create_node_rpc(self, rpc: Rpc, value: Any, insert=None, point=None):
ii = self.parse_ii(rpc.path, rpc.path_format)
n = self._data.goto(ii)
# if self.nacm:
# nrpc = NacmRpc(self.nacm, self, rpc.username)
# if nrpc.check_data_node_path(ii, Permission.NACM_ACCESS_READ) == Action.DENY:
# raise NacmForbiddenError()
# else:
# # Prun subtree data
# n = nrpc.check_data_read_path(ii)
value_keys = value.keys()
if len(value_keys) > 1:
raise ValueError("Received data containing more than one instance")
val_key = tuple(value_keys)[0]
val_data = value[val_key]
existing_member = None
try:
existing_member = n.member(val_key)
except NonexistentInstance:
pass
if existing_member is None:
# Create new data node
data_doc = DataHelpers.node2doc(ii + [MemberName(val_key)], val_data)
data_doc_inst = self._dm.from_raw(data_doc)
new_value = data_doc_inst.goto(ii).value
new_value_data = new_value[val_key]
new_n = n.new_member(val_key, new_value_data)
self._data = new_n.top()
elif isinstance(existing_member.value, ArrayValue):
# Append received node to list
data_doc = DataHelpers.node2doc(ii + [MemberName(val_key)], [val_data])
data_doc_inst = self._dm.from_raw(data_doc)
new_value = data_doc_inst.goto(ii).value
new_value_data = new_value[val_key][0]
if insert == "first":
new_n = existing_member.update(ArrayValue(val=[new_value_data] + existing_member.value))
else:
new_n = existing_member.update(ArrayValue(val=existing_member.value + [new_value_data]))
self._data = new_n.top()
else:
raise InstanceAlreadyPresent("InstanceAlreadyPresent")
if not isinstance(n.value, ObjectValue):
error("create_node: target resource not an object")
def put_node_rpc(self, rpc: Rpc, value: Any):
ii = self.parse_ii(rpc.path, rpc.path_format)
n = self._data.goto(ii)
......@@ -104,7 +163,13 @@ class BaseDatastore:
# # Prun subtree data
# n = nrpc.check_data_read_path(ii)
new_n = n.update(value)
value_keys = value.keys()
if len(value_keys) > 1:
raise ValueError("Received data containing more than one instance")
inst_val = tuple(value_keys)[0]
new_n = n.update(inst_val)
self._data = new_n.top()
# Locks datastore data
......
......@@ -76,20 +76,6 @@
"comment": "... but no query-module",
"action": "deny"
},
{
"name": "povol pristup ke groups",
"path": "/ietf-netconf-acm:nacm/groups",
"access-operations": "read",
"comment": "komentar",
"action": "permit"
},
{
"name": "zakaz pristup k admin group",
"path": "/ietf-netconf-acm:nacm/groups/group[name='admin']",
"access-operations": "*",
"comment": "komentar",
"action": "deny"
},
{
"name": "permit-zone-reload",
"module-name": "dns-server",
......
from typing import Dict, Any
from yangson.instance import InstanceIdentifier, MemberName, EntryKeys
class CertHelpers:
@staticmethod
def get_field(cert: Dict[str, Any], key: str) -> str:
return ([x[0][1] for x in cert["subject"] if x[0][0] == key] or [None])[0]
class DataHelpers:
# Create parent data nodes to JSON subtree up to top level
@staticmethod
def node2doc(id: InstanceIdentifier, val: Any) -> Dict[str, Any]:
n = val
for isel in reversed(id):
if isinstance(isel, MemberName):
new_node = {}
new_node[isel.name] = n
n = new_node
if isinstance(isel, EntryKeys):
new_node = []
for k in isel.keys:
n[k] = isel.keys[k]
new_node.append(n)
n = new_node
return n
import json
import os
import mimetypes
from collections import OrderedDict
from colorlog import error, warning as warn, info, debug
from urllib.parse import parse_qs
from yangson.schema import NonexistentSchemaNode
from yangson.instance import NonexistentInstance
from .config import CONFIG_HTTP, NACM_ADMINS, RESTCONF_API_ROOT_data, RESTCONF_NACM_API_ROOT_data
from .helpers import CertHelpers
import jetconf.rest_server
from .data import Rpc, DataLockError, NacmForbiddenError, InstanceAlreadyPresent
def api_root_handler(prot: "H2Protocol", headers: OrderedDict, stream_id: int):
# Top level api resource (appendix D.1.1)
response = (
"{\n"
" \"ietf-restconf:restconf\": {\n"
" \"data\" : [ null ],\n"
" \"operations\" : [ null ]\n"
" }\n"
"}"
)
response_headers = (
(':status', '200'),
('content-type', 'application/yang.api+json'),
('content-length', len(response)),
('server', CONFIG_HTTP["SERVER_NAME"]),
)
prot.conn.send_headers(stream_id, response_headers)
prot.conn.send_data(stream_id, response.encode(), end_stream=True)
def get(prot: "H2Protocol", headers: OrderedDict, stream_id: int, ds, pth):
username = CertHelpers.get_field(prot.client_cert, "emailAddress")
rpc1 = Rpc()
rpc1.username = username
rpc1.path = pth
try:
ds.lock_data(username)
n = ds.get_node_rpc(rpc1)
response = json.dumps(n.value, indent=4)
http_status = "200"
except DataLockError as e:
warn(e.msg)
response = "Internal Server Error"
http_status = "500"
except NacmForbiddenError as e:
warn(e.msg)
response = "Forbidden"
http_status = "403"
except NonexistentSchemaNode:
warn("NonexistentSchemaNode: " + pth)
response = "NonexistentSchemaNode"
http_status = "404"
except NonexistentInstance:
warn("NonexistentInstance: " + pth)
response = "NonexistentInstance"
http_status = "404"
finally:
ds.unlock_data()
response += "\n"
response_headers = (
(':status', http_status),
('content-type', 'application/yang.api+json'),
('content-length', len(response)),
('server', CONFIG_HTTP["SERVER_NAME"]),
)
prot.conn.send_headers(stream_id, response_headers)
prot.conn.send_data(stream_id, response.encode(), end_stream=True)
def get_nacm_api(prot: "H2Protocol", headers: OrderedDict, stream_id: int):
# NACM api request
info(("nacm_api_get: " + headers[":path"]))
url_split = headers[":path"].split("?")
url_path = url_split[0]
if len(url_split) > 1:
query_string = parse_qs(url_split[1])
else:
query_string = {}
username = CertHelpers.get_field(prot.client_cert, "emailAddress")
if username not in NACM_ADMINS:
warn(username + " not allowed to access NACM data")
response = "Forbidden"
http_status = "403"
response += "\n"
response_headers = (
(':status', http_status),
('content-type', 'application/yang.api+json'),
('content-length', len(response)),
('server', CONFIG_HTTP["SERVER_NAME"]),
)
prot.conn.send_headers(stream_id, response_headers)
prot.conn.send_data(stream_id, response.encode(), end_stream=True)
else:
pth = url_path[len(jetconf.rest_server.RESTCONF_NACM_API_ROOT_data):] or "/"
get(prot, headers, stream_id, jetconf.rest_server.ex_datastore.nacm.nacm_ds, pth)
def get_api(prot: "H2Protocol", headers: OrderedDict, stream_id: int):
# api request
info(("api_get: " + headers[":path"]))
url_split = headers[":path"].split("?")
url_path = url_split[0]
if len(url_split) > 1:
query_string = parse_qs(url_split[1])
else:
query_string = {}
pth = url_path[len(jetconf.rest_server.RESTCONF_API_ROOT_data):] or "/"
get(prot, headers, stream_id, jetconf.rest_server.ex_datastore, pth)
def get_file(prot: "H2Protocol", headers: OrderedDict, stream_id: int):
# Ordinary file on filesystem
url_split = headers[":path"].split("?")
url_path = url_split[0]
file_path = os.path.join(CONFIG_HTTP["DOC_ROOT"], url_path[1:].replace("..", "").replace("&", ""))
if os.path.isdir(file_path):
file_path = os.path.join(file_path, CONFIG_HTTP["DOC_DEFAULT_NAME"])
(ctype, encoding) = mimetypes.guess_type(file_path)
if ctype is None:
ctype = "application/octet-stream"
subtype = ctype.split('/', 1)[1]
try:
fd = open(file_path, 'rb')
response = fd.read()
fd.close()
except FileNotFoundError:
warn("Cannot open requested file \"{}\"".format(file_path))
response_headers = (
(':status', '404'),
('content-length', 0),
('server', CONFIG_HTTP["SERVER_NAME"]),
)
prot.conn.send_headers(stream_id, response_headers, end_stream=True)
return
info("Serving ordinary file {} of type \"{}\"".format(file_path, ctype))
response_headers = (
(':status', '200'),
('content-type', ctype),
('content-length', len(response)),
('server', CONFIG_HTTP["SERVER_NAME"]),
)
prot.conn.send_headers(stream_id, response_headers)
def split_arr(arr, chunk_size):
for i in range(0, len(arr), chunk_size):
yield arr[i:i + chunk_size]
for data_chunk in split_arr(response, prot.conn.max_outbound_frame_size):
prot.conn.send_data(stream_id, data_chunk, end_stream=False)
prot.conn.send_data(stream_id, bytes(), end_stream=True)
def put_post_nacm_api(prot: "H2Protocol", headers: OrderedDict, data: bytes, stream_id: int):
data_str = data.decode("utf-8")
info("prijato: " + data_str)
url_split = headers[":path"].split("?")
path = url_split[0]
if len(url_split) > 1:
query_string = parse_qs(url_split[1])
else:
query_string = {}
info(("nacm_api_put: " + path))
info("qs = {}".format(query_string))
username = CertHelpers.get_field(prot.client_cert, "emailAddress")
pth = path[len(jetconf.rest_server.RESTCONF_NACM_API_ROOT_data):]
rpc1 = Rpc()
rpc1.username = username
rpc1.path = pth
json_data = json.loads(data_str)
try:
jetconf.rest_server.ex_datastore.nacm.nacm_ds.lock_data(username)
if headers[":method"] == "PUT":
jetconf.rest_server.ex_datastore.nacm.nacm_ds.put_node_rpc(rpc1, json_data)
else:
ins_pos = (query_string.get("insert") or [None])[0]
jetconf.rest_server.ex_datastore.nacm.nacm_ds.create_node_rpc(rpc1, json_data, insert=ins_pos)
response = "Done\n"
http_status = "200"
except DataLockError as e:
warn(e.msg)
response = "Internal Server Error"
http_status = "500"
except NacmForbiddenError as e:
warn(e.msg)
response = "Forbidden"
http_status = "403"
except NonexistentSchemaNode:
warn("NonexistentSchemaNode: " + pth)
response = "NonexistentSchemaNode"
http_status = "404"
except NonexistentInstance:
warn("NonexistentInstance: " + pth)
response = "NonexistentInstance"
http_status = "404"
except InstanceAlreadyPresent:
warn("InstanceAlreadyPresent: " + pth)
response = "Conflict"
http_status = "409"
finally:
jetconf.rest_server.ex_datastore.nacm.nacm_ds.unlock_data()
jetconf.rest_server.ex_datastore.nacm.update(jetconf.rest_server.ex_datastore.nacm.nacm_ds.get_data_root().member("ietf-netconf-acm:nacm"))
response += "\n"
response = response.encode()
response_headers = (
(':status', http_status),
('content-type', 'application/yang.api+json'),
('content-length', len(response)),
('server', CONFIG_HTTP["SERVER_NAME"]),
)
prot.conn.send_headers(stream_id, response_headers)
prot.conn.send_data(stream_id, response, end_stream=True)
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