Commit 1a41483a authored by Pavel Spirek's avatar Pavel Spirek

Another code refactor, version bump

parent 24596fb2
PROJECT = jetconf PROJECT = jetconf
VERSION = 0.3.1 VERSION = 0.3.2
.PHONY = tags deps install-deps test .PHONY = tags deps install-deps test
tags: tags:
......
This diff is collapsed.
from typing import Callable, Union
from yangson.schemanode import SchemaNode
from yangson.instance import InstanceRoute
from .journal import DataChange, RpcInfo
from .helpers import JsonNodeT
# ---------- Base classes for conf data handlers ----------
class ConfDataHandlerBase:
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
class ConfDataObjectHandler(ConfDataHandlerBase):
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(ConfDataHandlerBase):
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
# ---------- Base classes for state data handlers ----------
class StateDataHandlerBase:
def __init__(self, datastore: "BaseDatastore", schema_path: str):
self.ds = datastore
self.data_model = datastore.get_dm()
self.sch_pth = schema_path
self.schema_node = self.data_model.get_data_node(self.sch_pth)
class StateDataContainerHandler(StateDataHandlerBase):
def generate_node(self, node_ii: InstanceRoute, username: str, staging: bool) -> JsonNodeT:
pass
class StateDataListHandler(StateDataHandlerBase):
def generate_list(self, node_ii: InstanceRoute, username: str, staging: bool) -> JsonNodeT:
pass
def generate_item(self, node_ii: InstanceRoute, username: str, staging: bool) -> JsonNodeT:
pass
# ---------- Types ----------
ConfDataHandler = Union[ConfDataObjectHandler, ConfDataListHandler]
StateDataHandler = Union[StateDataContainerHandler, StateDataListHandler]
OpHandler = Callable[[RpcInfo], JsonNodeT]
from typing import Dict, Callable from typing import List, Dict, Tuple, Callable
from yangson.schemanode import SchemaNode
from yangson.schemadata import SchemaData from yangson.schemadata import SchemaData
from yangson.instance import InstanceRoute from yangson.typealiases import SchemaRoute
from .helpers import JsonNodeT from .handler_base import ConfDataHandlerBase, StateDataHandlerBase, ConfDataHandler, StateDataHandler, OpHandler
# ---------- Base classes for conf data handlers ----------
class ConfDataHandlerBase:
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
class ConfDataObjectHandler(ConfDataHandlerBase):
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(ConfDataHandlerBase):
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
# ---------- Base classes for state data handlers ----------
class StateDataHandlerBase:
def __init__(self, datastore: "BaseDatastore", schema_path: str):
self.ds = datastore
self.data_model = datastore.get_dm()
self.sch_pth = schema_path
self.schema_node = self.data_model.get_data_node(self.sch_pth)
class StateDataContainerHandler(StateDataHandlerBase):
def generate_node(self, node_ii: InstanceRoute, username: str, staging: bool) -> JsonNodeT:
pass
class StateDataListHandler(StateDataHandlerBase):
def generate_list(self, node_ii: InstanceRoute, username: str, staging: bool) -> JsonNodeT:
pass
def generate_item(self, node_ii: InstanceRoute, username: str, staging: bool) -> JsonNodeT:
pass
# ---------- Handler lists ---------- # ---------- Handler lists ----------
...@@ -85,22 +17,22 @@ class ConfDataHandlerList: ...@@ -85,22 +17,22 @@ class ConfDataHandlerList:
self.handlers[sch_node_id] = handler self.handlers[sch_node_id] = handler
self.handlers_pth[handler.schema_path] = handler self.handlers_pth[handler.schema_path] = handler
def get_handler(self, sch_node_id: int) -> ConfDataHandlerBase: def get_handler(self, sch_node_id: int) -> ConfDataHandler:
return self.handlers.get(sch_node_id) return self.handlers.get(sch_node_id)
def get_handler_by_pth(self, sch_pth: str) -> ConfDataHandlerBase: def get_handler_by_pth(self, sch_pth: str) -> ConfDataHandler:
return self.handlers_pth.get(sch_pth) return self.handlers_pth.get(sch_pth)
class StateDataHandlerList: class StateDataHandlerList:
def __init__(self): def __init__(self):
self.handlers = [] self.handlers = [] # type: List[Tuple[SchemaRoute, StateDataHandlerBase]]
def register(self, handler: "StateDataHandlerBase"): def register(self, handler: StateDataHandlerBase):
saddr = SchemaData.path2route(handler.sch_pth) saddr = SchemaData.path2route(handler.sch_pth)
self.handlers.append((saddr, handler)) self.handlers.append((saddr, handler))
def get_handler(self, sch_pth: str, allow_superior: bool = True) -> Callable: def get_handler(self, sch_pth: str, allow_superior: bool = True) -> StateDataHandler:
saddr = SchemaData.path2route(sch_pth) saddr = SchemaData.path2route(sch_pth)
if allow_superior: if allow_superior:
while saddr: while saddr:
...@@ -118,20 +50,10 @@ class StateDataHandlerList: ...@@ -118,20 +50,10 @@ class StateDataHandlerList:
class OpHandlerList: class OpHandlerList:
def __init__(self): def __init__(self):
self.handlers = {} # type: Dict[str, Callable] self.handlers = {} # type: Dict[str, OpHandler]
self.default_handler = None # type: Callable
def register(self, handler: Callable, op_name: str): def register(self, handler: Callable, op_name: str):
self.handlers[op_name] = handler self.handlers[op_name] = handler
def register_default(self, handler: Callable): def get_handler(self, op_name: str) -> OpHandler:
self.default_handler = handler return self.handlers.get(op_name)
def get_handler(self, op_name: str) -> Callable:
return self.handlers.get(op_name, self.default_handler)
# ---------- Handler list globals ----------
OP_HANDLERS = OpHandlerList()
STATE_DATA_HANDLES = StateDataHandlerList()
CONF_DATA_HANDLES = ConfDataHandlerList()
This diff is collapsed.
...@@ -111,7 +111,6 @@ class Jetconf: ...@@ -111,7 +111,6 @@ class Jetconf:
# Create HTTP server # Create HTTP server
self.rest_srv = RestServer() self.rest_srv = RestServer()
self.rest_srv.register_api_handlers(datastore) self.rest_srv.register_api_handlers(datastore)
self.rest_srv.register_static_handlers()
def run(self): def run(self):
# Set signal handlers # Set signal handlers
......
from enum import Enum
from colorlog import error, info
from typing import List, Dict
from yangson.enumerations import ContentType, ValidationScope
from yangson.schemanode import SchemaError, SemanticError
from yangson.instvalue import ObjectValue
from yangson.instance import InstanceNode
from . import config
from .helpers import ErrorHelpers, LogHelpers, PathFormat, JsonNodeT
from .errors import ConfHandlerFailedError
epretty = ErrorHelpers.epretty
debug_journal = LogHelpers.create_module_dbg_logger(__name__)
class ChangeType(Enum):
CREATE = 0,
REPLACE = 1,
DELETE = 2
class RpcInfo:
def __init__(self):
self.username = None # type: str
self.path = None # type: str
self.qs = None # type: Dict[str, List[str]]
self.path_format = PathFormat.URL # type: PathFormat
self.skip_nacm_check = False # type: bool
self.op_name = None # type: str
self.op_input_args = None # type: ObjectValue
class DataChange:
def __init__(self, change_type: ChangeType, rpc_info: RpcInfo, input_data: JsonNodeT, root_after_change: InstanceNode, nacm_modified: bool):
self.change_type = change_type
self.rpc_info = rpc_info
self.input_data = input_data
self.root_after_change = root_after_change
self.nacm_modified = nacm_modified
class UsrChangeJournal:
def __init__(self, root_origin: InstanceNode):
self._root_origin = root_origin
self._journal = [] # type: List[DataChange]
def get_root_head(self) -> InstanceNode:
if len(self._journal) > 0:
return self._journal[-1].root_after_change
else:
return self._root_origin
def get_root_origin(self) -> InstanceNode:
return self._root_origin
def add(self, change: DataChange):
self._journal.append(change)
def list(self) -> JsonNodeT:
changes_info = []
for ch in self._journal:
changes_info.append([ch.change_type.name, ch.rpc_info.path])
return changes_info
def commit(self, ds: "BaseDatastore") -> bool:
nacm_modified = False
if len(self._journal) == 0:
return False
if hash(ds.get_data_root()) == hash(self._root_origin):
info("Commiting new configuration (swapping roots)")
# Set new root
nr = self.get_root_head()
for change in self._journal:
nacm_modified = nacm_modified or change.nacm_modified
else:
info("Commiting new configuration (re-applying changes)")
nr = ds.get_data_root()
for change in self._journal:
nacm_modified = nacm_modified or change.nacm_modified
if change.change_type == ChangeType.CREATE:
nr = ds.create_node_rpc(nr, change.rpc_info, change.input_data)[0]
elif change.change_type == ChangeType.REPLACE:
nr = ds.update_node_rpc(nr, change.rpc_info, change.input_data)[0]
elif change.change_type == ChangeType.DELETE:
nr = ds.delete_node_rpc(nr, change.rpc_info)[0]
try:
# Validate syntax and semantics of new data
if config.CFG.glob["VALIDATE_TRANSACTIONS"] is True:
nr.validate(ValidationScope.all, ContentType.config)
except (SchemaError, SemanticError) as e:
error("Data validation error:")
error(epretty(e))
raise e
# Set new data root
ds.set_data_root(nr)
# Update NACM if NACM data has been affected by any edit
if nacm_modified and ds.nacm is not None:
ds.nacm.update()
# Call commit begin hook
begin_hook_failed = False
try:
ds.commit_begin_callback()
except Exception as e:
error("Exception occured in commit_begin handler: {}".format(epretty(e)))
begin_hook_failed = True
# Run schema node handlers
conf_handler_failed = False
if not begin_hook_failed:
try:
for change in self._journal:
ii = ds.parse_ii(change.rpc_info.path, change.rpc_info.path_format)
ds.run_conf_edit_handler(ii, change)
except Exception as e:
error("Exception occured in edit handler: {}".format(epretty(e)))
conf_handler_failed = True
# Call commit end hook
end_hook_failed = False
end_hook_abort_failed = False
if not (begin_hook_failed or conf_handler_failed):
try:
ds.commit_end_callback(failed=False)
except Exception as e:
error("Exception occured in commit_end handler: {}".format(epretty(e)))
end_hook_failed = True
if begin_hook_failed or conf_handler_failed or end_hook_failed:
try:
# Call commit_end callback again with "failed" argument set to True
ds.commit_end_callback(failed=True)
except Exception as e:
error("Exception occured in commit_end handler (abort): {}".format(epretty(e)))
end_hook_abort_failed = True
# Return to previous version of data and raise an exception if something went wrong
if begin_hook_failed or conf_handler_failed or end_hook_failed or end_hook_abort_failed:
ds.data_root_rollback(history_steps=1, store_current=False)
# Update NACM again after rollback
if nacm_modified and ds.nacm is not None:
ds.nacm.update()
raise ConfHandlerFailedError("(see logged)")
return True
from . import config from . import config
from .helpers import JsonNodeT from .helpers import JsonNodeT
from .handler_list import OP_HANDLERS
from .data import BaseDatastore, RpcInfo, StagingDataException from .data import BaseDatastore, RpcInfo, StagingDataException
...@@ -85,9 +84,9 @@ class OpHandlersContainer: ...@@ -85,9 +84,9 @@ class OpHandlersContainer:
def register_op_handlers(ds: BaseDatastore): def register_op_handlers(ds: BaseDatastore):
op_handlers_obj = OpHandlersContainer(ds) ds.handlers.op_obj = OpHandlersContainer(ds)
OP_HANDLERS.register(op_handlers_obj.jetconf_conf_status, "jetconf:conf-status") ds.handlers.op.register(ds.handlers.op_obj.jetconf_conf_status, "jetconf:conf-status")
OP_HANDLERS.register(op_handlers_obj.jetconf_conf_reset, "jetconf:conf-reset") ds.handlers.op.register(ds.handlers.op_obj.jetconf_conf_reset, "jetconf:conf-reset")
OP_HANDLERS.register(op_handlers_obj.jetconf_conf_commit, "jetconf:conf-commit") ds.handlers.op.register(ds.handlers.op_obj.jetconf_conf_commit, "jetconf:conf-commit")
OP_HANDLERS.register(op_handlers_obj.jetconf_get_schema_digest, "jetconf:get-schema-digest") ds.handlers.op.register(ds.handlers.op_obj.jetconf_get_schema_digest, "jetconf:get-schema-digest")
OP_HANDLERS.register(op_handlers_obj.jetconf_get_list_length, "jetconf:get-list-length") ds.handlers.op.register(ds.handlers.op_obj.jetconf_get_list_length, "jetconf:get-list-length")
...@@ -16,6 +16,7 @@ from . import config, http_handlers as handlers ...@@ -16,6 +16,7 @@ from . import config, http_handlers as handlers
from .helpers import SSLCertT, LogHelpers from .helpers import SSLCertT, LogHelpers
from .data import BaseDatastore from .data import BaseDatastore
from .http_handlers import ( from .http_handlers import (
HttpHandlersImpl,
HttpResponse, HttpResponse,
HttpStatus, HttpStatus,
RestconfErrType, RestconfErrType,
...@@ -25,8 +26,6 @@ from .http_handlers import ( ...@@ -25,8 +26,6 @@ from .http_handlers import (
) )
HandlerConditionT = Callable[[str, str], bool] # Function(method, path) -> bool
HttpHandlerT = Callable[[OrderedDict, Optional[str], SSLCertT], handlers.HttpResponse]
debug_srv = LogHelpers.create_module_dbg_logger(__name__) debug_srv = LogHelpers.create_module_dbg_logger(__name__)
...@@ -43,27 +42,8 @@ class ResponseData: ...@@ -43,27 +42,8 @@ class ResponseData:
self.bytes_sent = 0 self.bytes_sent = 0
class HttpHandlerList:
def __init__(self):
self.handlers = [] # type: List[Tuple[HandlerConditionT, HttpHandlerT]]
self.default_handler = None # type: HttpHandlerT
def register(self, condition: HandlerConditionT, handler: HttpHandlerT):
self.handlers.append((condition, handler))
def register_default(self, handler: HttpHandlerT):
self.default_handler = handler
def get_handler(self, method: str, path: str) -> HttpHandlerT:
for h in self.handlers:
if h[0](method, path):
return h[1]
return self.default_handler
class H2Protocol(asyncio.Protocol): class H2Protocol(asyncio.Protocol):
HTTP_HANDLERS = None # type: HttpHandlerList HTTP_HANDLERS = None # type: HttpHandlersImpl
def __init__(self): def __init__(self):
self.conn = H2Connection(H2Configuration(client_side=False, header_encoding="utf-8")) self.conn = H2Connection(H2Configuration(client_side=False, header_encoding="utf-8"))
...@@ -174,9 +154,9 @@ class H2Protocol(asyncio.Protocol): ...@@ -174,9 +154,9 @@ class H2Protocol(asyncio.Protocol):
method = headers[":method"] method = headers[":method"]
if method == "HEAD": if method == "HEAD":
h = self.HTTP_HANDLERS.get_handler("GET", url_path) h = self.HTTP_HANDLERS.list.get("GET", url_path)
else: else:
h = self.HTTP_HANDLERS.get_handler(method, url_path) h = self.HTTP_HANDLERS.list.get(method, url_path)
if not h: if not h:
self.send_response( self.send_response(
...@@ -272,47 +252,11 @@ class RestServer: ...@@ -272,47 +252,11 @@ class RestServer:
ssl=ssl_context ssl=ssl_context
) )
self.server = self.loop.run_until_complete(listener) self.server = self.loop.run_until_complete(listener)
# Set H2Protocol class variables
H2Protocol.HTTP_HANDLERS = HttpHandlerList()
# Register HTTP handlers # Register HTTP handlers
@staticmethod @staticmethod
def register_api_handlers(datastore: BaseDatastore): def register_api_handlers(datastore: BaseDatastore):
api_get_root = handlers.api_root_handler H2Protocol.HTTP_HANDLERS = HttpHandlersImpl(datastore)
api_get_ylv = handlers.api_ylv_handler
api_get = handlers.create_get_api(datastore)
api_get_run = handlers.create_get_running_api(datastore)
api_post = handlers.create_post_api(datastore)
api_put = handlers.create_put_api(datastore)
api_delete = handlers.create_api_delete(datastore)
api_get_op = handlers.create_api_op(datastore)
api_op = handlers.create_api_op(datastore)
api_root = config.CFG.http["API_ROOT"]
api_root_data = config.CFG.api_root_data
api_root_running_data = config.CFG.api_root_running_data
api_root_ylv = config.CFG.api_root_ylv
api_root_ops = config.CFG.api_root_ops
H2Protocol.HTTP_HANDLERS.register(lambda m, p: (m == "GET") and (p.startswith(api_root_data)), api_get)
H2Protocol.HTTP_HANDLERS.register(lambda m, p: (m == "GET") and (p.startswith(api_root_running_data)), api_get_run)
H2Protocol.HTTP_HANDLERS.register(lambda m, p: (m == "GET") and (p == api_root_ylv), api_get_ylv)
H2Protocol.HTTP_HANDLERS.register(lambda m, p: (m == "GET") and (p == api_root), api_get_root)
H2Protocol.HTTP_HANDLERS.register(lambda m, p: (m == "POST") and (p.startswith(api_root_data)), api_post)
H2Protocol.HTTP_HANDLERS.register(lambda m, p: (m == "PUT") and (p.startswith(api_root_data)), api_put)
H2Protocol.HTTP_HANDLERS.register(lambda m, p: (m == "DELETE") and (p.startswith(api_root_data)), api_delete)
H2Protocol.HTTP_HANDLERS.register(lambda m, p: (m == "GET") and (p.startswith(api_root_ops)), api_get_op)
H2Protocol.HTTP_HANDLERS.register(lambda m, p: (m == "POST") and (p.startswith(api_root_ops)), api_op)
H2Protocol.HTTP_HANDLERS.register(lambda m, p: m == "OPTIONS", handlers.options_api)
# Register static HTTP handlers
@staticmethod
def register_static_handlers():
api_root = config.CFG.http["API_ROOT"]
H2Protocol.HTTP_HANDLERS.register(lambda m, p: (m == "GET") and not (p.startswith(api_root)), handlers.get_file)
H2Protocol.HTTP_HANDLERS.register_default(handlers.unknown_req_handler)
# Start server event loop (this will block until shutdown) # Start server event loop (this will block until shutdown)
def run(self): def run(self):
......
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