Much improved error reporting (compliant with RESTCONF RFC) (for GET and operations yet)

parent 71e3d6bf
......@@ -155,24 +155,23 @@ def main():
try:
datastore.get_data_root().validate(ValidationScope.all, ContentType.config)
except (SchemaError, SemanticError) as e:
error("Validation of datastore failed")
error("Initial validation of datastore failed")
error(ErrorHelpers.epretty(e))
# print(e.__dict__)
sig_exit_handler(0, None)
# Register configuration data node listeners
CONF_DATA_HANDLES.register_handler(KnotConfServerListener(datastore, "/dns-server:dns-server/server-options"))
CONF_DATA_HANDLES.register_handler(KnotConfLogListener(datastore, "/dns-server:dns-server/knot-dns:log"))
CONF_DATA_HANDLES.register_handler(KnotConfZoneListener(datastore, "/dns-server:dns-server/zones/zone"))
CONF_DATA_HANDLES.register_handler(KnotConfControlListener(datastore, "/dns-server:dns-server/knot-dns:control-socket"))
CONF_DATA_HANDLES.register_handler(KnotConfAclListener(datastore, "/dns-server:dns-server/access-control-list"))
CONF_DATA_HANDLES.register(KnotConfServerListener(datastore, "/dns-server:dns-server/server-options"))
CONF_DATA_HANDLES.register(KnotConfLogListener(datastore, "/dns-server:dns-server/knot-dns:log"))
CONF_DATA_HANDLES.register(KnotConfZoneListener(datastore, "/dns-server:dns-server/zones/zone"))
CONF_DATA_HANDLES.register(KnotConfControlListener(datastore, "/dns-server:dns-server/knot-dns:control-socket"))
CONF_DATA_HANDLES.register(KnotConfAclListener(datastore, "/dns-server:dns-server/access-control-list"))
# Register op handlers
OP_HANDLERS.register_handler("dns-zone-rpcs:begin-transaction", OP_HANDLERS_IMPL.zone_begin_transaction)
OP_HANDLERS.register_handler("dns-zone-rpcs:commit-transaction", OP_HANDLERS_IMPL.zone_commit_transaction)
OP_HANDLERS.register_handler("dns-zone-rpcs:abort-transaction", OP_HANDLERS_IMPL.zone_abort_transaction)
OP_HANDLERS.register_handler("dns-zone-rpcs:zone-set", OP_HANDLERS_IMPL.zone_set)
OP_HANDLERS.register_handler("dns-zone-rpcs:zone-unset", OP_HANDLERS_IMPL.zone_unset)
OP_HANDLERS.register("dns-zone-rpcs:begin-transaction", OP_HANDLERS_IMPL.zone_begin_transaction)
OP_HANDLERS.register("dns-zone-rpcs:commit-transaction", OP_HANDLERS_IMPL.zone_commit_transaction)
OP_HANDLERS.register("dns-zone-rpcs:abort-transaction", OP_HANDLERS_IMPL.zone_abort_transaction)
OP_HANDLERS.register("dns-zone-rpcs:zone-set", OP_HANDLERS_IMPL.zone_set)
OP_HANDLERS.register("dns-zone-rpcs:zone-unset", OP_HANDLERS_IMPL.zone_unset)
# Create and register state data node listeners
usr_state_data_handlers.create_zone_state_handlers(STATE_DATA_HANDLES, datamodel)
......
......@@ -58,10 +58,10 @@ class BaseHandlerList:
self.handlers = [] # type: List[Tuple[HandlerSelectorT, Callable]]
self.default_handler = None # type: Callable
def register_handler(self, identifier: str, handler: Callable):
def register(self, identifier: str, handler: Callable):
raise NotImplementedError("Not implemented in base class")
def register_default_handler(self, handler: Callable):
def register_default(self, handler: Callable):
self.default_handler = handler
def get_handler(self, identifier: str) -> Any:
......@@ -69,7 +69,7 @@ class BaseHandlerList:
class OpHandlerList(BaseHandlerList):
def register_handler(self, op_name: str, handler: Callable):
def register(self, op_name: str, handler: Callable):
self.handlers.append((op_name, handler))
def get_handler(self, op_name: str) -> Callable:
......@@ -84,7 +84,7 @@ class ConfDataHandlerList:
def __init__(self):
self.handlers = [] # type: List[Tuple[HandlerSelectorT, BaseDataListener]]
def register_handler(self, handler: "BaseDataListener"):
def register(self, handler: "BaseDataListener"):
schema_node = handler.schema_node # type: SchemaNode
sch_node_id = str(id(schema_node))
self.handlers.append((sch_node_id, handler))
......@@ -101,7 +101,7 @@ class StateDataHandlerList:
def __init__(self):
self.handlers = []
def register_handler(self, handler: "StateNodeHandlerBase"):
def register(self, handler: "StateNodeHandlerBase"):
saddr = SchemaData.path2route(handler.sch_pth)
self.handlers.append((saddr, handler))
......
......@@ -46,9 +46,13 @@ class DataHelpers:
n = [n]
return n
# Get the namespace of the first segment in path
# Raises ValueError if the first segment is not in fully-qualified format
@staticmethod
def path_first_ns(api_pth: str) -> str:
return api_pth[1:].split("/", maxsplit=1)[0].split(":", maxsplit=1)[0]
first_seg = api_pth[1:].split("/", maxsplit=1)[0]
ns1, sel1 = first_seg.split(":", maxsplit=1)
return ns1
@staticmethod
def load_data_model(module_dir: str, yang_library_file: str) -> DataModel:
......@@ -115,6 +119,15 @@ class ErrorHelpers:
else:
return err_str
@staticmethod
def errtag(e: BaseException) -> str:
try:
tag = e.tag
except AttributeError:
tag = None
return tag
class LogHelpers:
@staticmethod
......
......@@ -29,6 +29,7 @@ from .data import (
QueryStrT = Dict[str, List[str]]
epretty = ErrorHelpers.epretty
errtag = ErrorHelpers.errtag
debug_httph = LogHelpers.create_module_dbg_logger(__name__)
......@@ -62,6 +63,23 @@ class HttpStatus(Enum):
return self.value[1]
ERRTAG_MALFORMED = "malformed-message"
ERRTAG_REQLARGE = "request-too-large"
ERRTAG_OPNOTSUPPORTED = "operation-not-supported"
ERRTAG_OPFAILED = "operation-failed"
ERRTAG_ACCDENIED = "access-denied"
ERRTAG_LOCKDENIED = "lock-denied"
ERRTAG_INVVALUE = "invalid-value"
ERRTAG_EXISTS = "data-exists"
class RestconfErrType(Enum):
Transport = "transport"
Rpc = "rpc"
Protocol = "protocol"
Application = "application"
class HttpResponse:
def __init__(self, status: HttpStatus, data: bytes, content_type: str, extra_headers: OrderedDict=None):
self.status_code = status.code
......@@ -79,9 +97,56 @@ class HttpResponse:
return cls(status, response.encode(), CT_PLAIN)
@classmethod
def error(cls, status: HttpStatus, err_type: RestconfErrType, err_tag: str, err_apptag: str=None,
err_path: str=None, err_msg: str=None, exception: Exception=None) -> "HttpResponse":
err_body = {
"error-type": err_type.value,
"error-tag": err_tag
}
# Auto-fill app-tag, path and mesage fields from Python's Exception attributes
if exception is not None:
try:
err_body["error-app-tag"] = exception.tag
except AttributeError:
pass
try:
err_body["error-path"] = exception.path
except AttributeError:
pass
err_body["error-message"] = epretty(exception)
if err_apptag is not None:
err_body["error-app-tag"] = err_apptag
if err_path is not None:
err_body["error-path"] = err_path
if err_msg is not None:
err_body["error-message"] = err_msg
err_template = {
"ietf-restconf:errors": {
"error": [
err_body
]
}
}
response = json.dumps(err_template, indent=4)
return cls(status, response.encode(), CT_YANG_JSON)
def unknown_req_handler(headers: OrderedDict, data: Optional[str], client_cert: SSLCertT) -> HttpResponse:
return HttpResponse.empty(HttpStatus.BadRequest)
return HttpResponse.error(
HttpStatus.BadRequest,
RestconfErrType.Transport,
ERRTAG_MALFORMED,
"unknown_req_handler"
)
def api_root_handler(headers: OrderedDict, data: Optional[str], client_cert: SSLCertT):
......@@ -119,17 +184,33 @@ def _get(ds: BaseDatastore, pth: str, username: str, yl_data: bool=False, stagin
try:
n = ds.get_node_rpc(rpc1, yl_data, staging)
except NacmForbiddenError as e:
warn(epretty(e))
http_resp = HttpResponse.empty(HttpStatus.Forbidden)
http_resp = HttpResponse.error(
HttpStatus.Forbidden,
RestconfErrType.Protocol,
ERRTAG_ACCDENIED,
exception=e
)
except (NonexistentSchemaNode, NonexistentInstance) as e:
warn(epretty(e))
http_resp = HttpResponse.empty(HttpStatus.NotFound)
http_resp = HttpResponse.error(
HttpStatus.NotFound,
RestconfErrType.Protocol,
ERRTAG_INVVALUE,
exception=e
)
except (InstanceValueError, ValueError) as e:
warn(epretty(e))
http_resp = HttpResponse.empty(HttpStatus.BadRequest)
http_resp = HttpResponse.error(
HttpStatus.BadRequest,
RestconfErrType.Protocol,
ERRTAG_INVVALUE,
exception=e
)
except (ConfHandlerFailedError, NoHandlerError, KnotError, YangsonException) as e:
error(epretty(e))
http_resp = HttpResponse.empty(HttpStatus.InternalServerError)
http_resp = HttpResponse.error(
HttpStatus.InternalServerError,
RestconfErrType.Protocol,
ERRTAG_OPFAILED,
exception=e
)
finally:
ds.unlock_data()
......@@ -169,11 +250,19 @@ def _get(ds: BaseDatastore, pth: str, username: str, yl_data: bool=False, stagin
http_resp = HttpResponse(HttpStatus.Ok, response.encode(), CT_YANG_JSON, extra_headers=add_headers)
except DataLockError as e:
warn(epretty(e))
http_resp = HttpResponse.empty(HttpStatus.InternalServerError)
http_resp = HttpResponse.error(
HttpStatus.Conflict,
RestconfErrType.Protocol,
ERRTAG_LOCKDENIED,
exception=e
)
except HttpRequestError as e:
warn(epretty(e))
http_resp = HttpResponse.empty(HttpStatus.BadRequest)
http_resp = HttpResponse.error(
HttpStatus.BadRequest,
RestconfErrType.Protocol,
ERRTAG_INVVALUE,
exception=e
)
return http_resp
......@@ -442,21 +531,27 @@ def create_api_op(ds: BaseDatastore):
info("[{}] invoke_op: {}".format(username, headers[":path"]))
api_pth = headers[":path"][len(API_ROOT_ops):]
op_name_fq = api_pth[1:]
op_name_splitted = op_name_fq.split(":", maxsplit=1)
op_name_fq = api_pth[1:].split("/", maxsplit=1)[0]
try:
ns = op_name_splitted[0]
op_name = op_name_splitted[1]
except IndexError:
warn("Operation name must be in fully-qualified format")
return HttpResponse.empty(HttpStatus.BadRequest)
ns, sel1 = op_name_fq.split(":", maxsplit=1)
except ValueError:
return HttpResponse.error(
HttpStatus.BadRequest,
RestconfErrType.Protocol,
ERRTAG_MALFORMED,
"Operation name must be in fully-qualified format"
)
try:
json_data = json.loads(data) if len(data) > 0 else {}
except ValueError as e:
error("Failed to parse POST data: " + epretty(e))
return HttpResponse.empty(HttpStatus.BadRequest)
return HttpResponse.error(
HttpStatus.BadRequest,
RestconfErrType.Protocol,
ERRTAG_MALFORMED,
"Failed to parse POST data: " + epretty(e)
)
input_args = json_data.get(ns + ":input")
......@@ -481,17 +576,47 @@ def create_api_op(ds: BaseDatastore):
response = ret_data
http_resp = HttpResponse(HttpStatus.Ok, response.encode(), CT_YANG_JSON)
except NacmForbiddenError as e:
warn(epretty(e))
http_resp = HttpResponse.empty(HttpStatus.Forbidden)
http_resp = HttpResponse.error(
HttpStatus.Forbidden,
RestconfErrType.Protocol,
ERRTAG_ACCDENIED,
exception=e
)
except NonexistentSchemaNode as e:
warn(epretty(e))
http_resp = HttpResponse.empty(HttpStatus.NotFound)
except (InstanceAlreadyPresent, NoHandlerForOpError, ValueError) as e:
warn(epretty(e))
http_resp = HttpResponse.empty(HttpStatus.BadRequest)
http_resp = HttpResponse.error(
HttpStatus.NotFound,
RestconfErrType.Protocol,
ERRTAG_INVVALUE,
exception=e
)
except InstanceAlreadyPresent as e:
http_resp = HttpResponse.error(
HttpStatus.Conflict,
RestconfErrType.Protocol,
ERRTAG_EXISTS,
exception=e
)
except NoHandlerForOpError as e:
http_resp = HttpResponse.error(
HttpStatus.BadRequest,
RestconfErrType.Protocol,
ERRTAG_OPNOTSUPPORTED,
exception=e
)
except ConfHandlerFailedError as e:
error(epretty(e))
http_resp = HttpResponse.empty(HttpStatus.InternalServerError)
http_resp = HttpResponse.error(
HttpStatus.InternalServerError,
RestconfErrType.Protocol,
ERRTAG_OPFAILED,
exception=e
)
except ValueError as e:
http_resp = HttpResponse.error(
HttpStatus.BadRequest,
RestconfErrType.Protocol,
ERRTAG_INVVALUE,
exception=e
)
return http_resp
......
import json
from enum import Enum
from typing import List, Union, Dict, Any, Optional
from threading import Lock
from .libknot.control import KnotCtl, KnotCtlType
from .config import CONFIG
from .helpers import LogHelpers
JsonNodeT = Union[Dict[str, Any], List[Any], str, int]
......
import asyncio
import ssl
from io import BytesIO
from collections import OrderedDict
from colorlog import error, warning as warn, info
from typing import List, Tuple, Dict, Any, Callable, Optional
from typing import List, Tuple, Dict, Callable, Optional
from h2.connection import H2Connection
from h2.errors import PROTOCOL_ERROR, ENHANCE_YOUR_CALM
......@@ -12,7 +12,7 @@ from h2.exceptions import ProtocolError
from h2.events import DataReceived, RequestReceived, RemoteSettingsChanged, StreamEnded, WindowUpdated
from . import http_handlers as handlers
from .http_handlers import HttpResponse, HttpStatus
from .http_handlers import HttpResponse, HttpStatus, RestconfErrType, ERRTAG_MALFORMED, ERRTAG_OPNOTSUPPORTED, ERRTAG_REQLARGE
from .config import CONFIG_HTTP, API_ROOT_data, API_ROOT_STAGING_data, API_ROOT_ops
from .data import BaseDatastore
from .helpers import SSLCertT, LogHelpers
......@@ -31,6 +31,7 @@ class RequestData:
self.data = data
self.data_overflow = False
class ResponseData:
def __init__(self, data: bytes):
self.data = data
......@@ -42,10 +43,10 @@ class HttpHandlerList:
self.handlers = [] # type: List[Tuple[HandlerConditionT, HttpHandlerT]]
self.default_handler = None # type: HttpHandlerT
def register_handler(self, condition: HandlerConditionT, handler: HttpHandlerT):
def register(self, condition: HandlerConditionT, handler: HttpHandlerT):
self.handlers.append((condition, handler))
def register_default_handler(self, handler: HttpHandlerT):
def register_default(self, handler: HttpHandlerT):
self.default_handler = handler
def get_handler(self, method: str, path: str) -> HttpHandlerT:
......@@ -96,10 +97,16 @@ class H2Protocol(asyncio.Protocol):
try:
request_data = self.stream_data.pop(event.stream_id)
except KeyError:
self.send_response(HttpResponse.empty(HttpStatus.BadRequest), event.stream_id)
self.send_response(
HttpResponse.error(HttpStatus.BadRequest, RestconfErrType.Transport, ERRTAG_MALFORMED),
event.stream_id
)
else:
if request_data.data_overflow:
self.send_response(HttpResponse.empty(HttpStatus.ReqTooLarge), event.stream_id)
self.send_response(
HttpResponse.error(HttpStatus.ReqTooLarge, RestconfErrType.Transport, ERRTAG_REQLARGE),
event.stream_id
)
else:
headers = request_data.headers
http_method = headers[":method"]
......@@ -111,7 +118,14 @@ class H2Protocol(asyncio.Protocol):
self.run_request_handler(headers, event.stream_id, body)
else:
warn("Unknown http method \"{}\"".format(headers[":method"]))
self.send_response(HttpResponse.empty(HttpStatus.MethodNotAllowed), event.stream_id)
self.send_response(
HttpResponse.error(
HttpStatus.MethodNotAllowed,
RestconfErrType.Transport,
ERRTAG_OPNOTSUPPORTED
),
event.stream_id
)
elif isinstance(event, RemoteSettingsChanged):
changed_settings = {}
for s in event.changed_settings.items():
......@@ -119,7 +133,9 @@ class H2Protocol(asyncio.Protocol):
self.conn.update_settings(changed_settings)
elif isinstance(event, WindowUpdated):
try:
debug_srv("str {} nw={}".format(event.stream_id, self.conn.local_flow_control_window(event.stream_id)))
debug_srv(
"str {} nw={}".format(event.stream_id, self.conn.local_flow_control_window(event.stream_id))
)
self.send_response_continue(event.stream_id)
except (ProtocolError, KeyError) as e:
# debug_srv("wupdexception strid={}: {}".format(event.stream_id, str(e)))
......@@ -145,7 +161,10 @@ class H2Protocol(asyncio.Protocol):
h = h2_handlers.get_handler(method, url_path)
if not h:
self.send_response(HttpResponse.empty(HttpStatus.BadRequest), stream_id)
self.send_response(
HttpResponse.error(HttpStatus.BadRequest, RestconfErrType.Transport, ERRTAG_MALFORMED),
stream_id
)
else:
# Run handler and send HTTP response
resp = h(headers, data, self.client_cert)
......@@ -238,28 +257,28 @@ class RestServer:
# Register HTTP handlers
api_get_root = handlers.api_root_handler
api_get = handlers.create_get_api(datastore)
api_get_staging = handlers.create_get_staging_api(datastore)
api_get_st = handlers.create_get_staging_api(datastore)
api_post = handlers.create_post_api(datastore)
api_put = handlers.create_put_api(datastore)
api_delete = handlers.create_api_delete(datastore)
api_op = handlers.create_api_op(datastore)
self.http_handlers.register_handler(lambda m, p: (m == "GET") and (p == CONFIG_HTTP["API_ROOT"]), api_get_root)
self.http_handlers.register_handler(lambda m, p: (m == "GET") and (p.startswith(API_ROOT_data)), api_get)
self.http_handlers.register_handler(lambda m, p: (m == "GET") and (p.startswith(API_ROOT_STAGING_data)), api_get_staging)
self.http_handlers.register_handler(lambda m, p: (m == "POST") and (p.startswith(API_ROOT_data)), api_post)
self.http_handlers.register_handler(lambda m, p: (m == "PUT") and (p.startswith(API_ROOT_data)), api_put)
self.http_handlers.register_handler(lambda m, p: (m == "DELETE") and (p.startswith(API_ROOT_data)), api_delete)
self.http_handlers.register_handler(lambda m, p: (m == "POST") and (p.startswith(API_ROOT_ops)), api_op)
self.http_handlers.register_handler(lambda m, p: m == "OPTIONS", handlers.options_api)
self.http_handlers.register(lambda m, p: (m == "GET") and (p == CONFIG_HTTP["API_ROOT"]), api_get_root)
self.http_handlers.register(lambda m, p: (m == "GET") and (p.startswith(API_ROOT_data)), api_get)
self.http_handlers.register(lambda m, p: (m == "GET") and (p.startswith(API_ROOT_STAGING_data)), api_get_st)
self.http_handlers.register(lambda m, p: (m == "POST") and (p.startswith(API_ROOT_data)), api_post)
self.http_handlers.register(lambda m, p: (m == "PUT") and (p.startswith(API_ROOT_data)), api_put)
self.http_handlers.register(lambda m, p: (m == "DELETE") and (p.startswith(API_ROOT_data)), api_delete)
self.http_handlers.register(lambda m, p: (m == "POST") and (p.startswith(API_ROOT_ops)), api_op)
self.http_handlers.register(lambda m, p: m == "OPTIONS", handlers.options_api)
h2_handlers = self.http_handlers
def register_static_handlers(self):
global h2_handlers
self.http_handlers.register_handler(lambda m, p: m == "GET", handlers.get_file)
self.http_handlers.register_default_handler(handlers.unknown_req_handler)
self.http_handlers.register(lambda m, p: m == "GET", handlers.get_file)
self.http_handlers.register_default(handlers.unknown_req_handler)
h2_handlers = self.http_handlers
......
......@@ -411,6 +411,6 @@ def create_zone_state_handlers(handler_list: "StateDataHandlerList", dm: DataMod
zsh = ZoneStateHandler(dm)
zdsh = ZoneDataStateHandler(dm)
psh = PokusStateHandler(dm)
handler_list.register_handler(zsh)
handler_list.register_handler(zdsh)
handler_list.register_handler(psh)
handler_list.register(zsh)
handler_list.register(zdsh)
handler_list.register(psh)
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