Commit 863d6bb1 authored by Pavel Spirek's avatar Pavel Spirek

Initial support for RPC Actions

parent 7bb9414d
......@@ -26,7 +26,7 @@ from .helpers import PathFormat, ErrorHelpers, LogHelpers, DataHelpers, JsonNode
from .nacm import NacmConfig, Permission, Action
from .journal import ChangeType, UsrChangeJournal, RpcInfo, DataChange
from .handler_base import ConfDataObjectHandler, ConfDataListHandler, StateDataContainerHandler, StateDataListHandler
from .handler_list import ConfDataHandlerList, StateDataHandlerList, OpHandlerList
from .handler_list import ConfDataHandlerList, StateDataHandlerList, OpHandlerList, ActionHandlerList
from .errors import (
StagingDataException,
NoHandlerForStateDataError,
......@@ -43,10 +43,11 @@ debug_data = LogHelpers.create_module_dbg_logger(__name__)
class BackendHandlers:
def __init__(self):
def __init__(self, dm: DataModel):
self.conf = ConfDataHandlerList()
self.state = StateDataHandlerList()
self.op = OpHandlerList()
self.action = ActionHandlerList(dm)
def _blankfn(*args, **kwargs):
pass
......@@ -65,7 +66,7 @@ class BaseDatastore:
self._lock_username = None # type: str
self._usr_journals = {} # type: Dict[str, UsrChangeJournal]
self.nacm = None # type: NacmConfig
self.handlers = BackendHandlers()
self.handlers = BackendHandlers(self._dm)
self.nacm = NacmConfig(self, self._dm) if with_nacm else None
# Returns DataModel object
......@@ -660,6 +661,41 @@ class BaseDatastore:
return ret_data
# Invoke a node action
def invoke_action_rpc(self, root: InstanceNode, rpc: RpcInfo) -> JsonNodeT:
ii = self.parse_ii(rpc.path, rpc.path_format)
node_ii = ii[0:-1]
n = root.goto(node_ii)
# Evaluate NACM
if self.nacm and not rpc.skip_nacm_check:
nrpc = self.nacm.get_user_rules(rpc.username)
if nrpc.check_data_node_permission(root, node_ii, Permission.NACM_ACCESS_EXEC) == Action.DENY:
raise NacmForbiddenError(
"Invocation of \"{}\" operation denied for user \"{}\"".format(rpc.op_name, rpc.username)
)
ii_an = ii[-1]
node_sn = n.schema_node
sn = node_sn.get_child(ii_an.name, ii_an.namespace)
action_handler = self.handlers.action.get_handler(id(sn))
if action_handler is None:
raise NoHandlerForOpError(rpc.path)
# Get operation input schema
sn_input = sn.get_child("input")
# Input arguments are expected, this will validate them
op_input_args = sn_input.from_raw(rpc.op_input_args) if sn_input.children else None
try:
ret_data = action_handler(ii, op_input_args, rpc.username)
except Exception as e:
raise OpHandlerFailedError(epretty(e))
return ret_data
def add_to_journal_rpc(self, ch_type: ChangeType, rpc: RpcInfo, value: Optional[JsonNodeT], new_root: InstanceNode, nacm_modified: bool):
usr_journal = self._usr_journals.get(rpc.username)
if usr_journal is not None:
......
......@@ -3,7 +3,7 @@ from typing import Callable, Union
from yangson.schemanode import SchemaNode
from yangson.instance import InstanceRoute
from .journal import DataChange, RpcInfo
from .journal import DataChange
from .helpers import JsonNodeT
......@@ -77,4 +77,5 @@ class StateDataListHandler(StateDataHandlerBase):
# ---------- Types ----------
ConfDataHandler = Union[ConfDataObjectHandler, ConfDataListHandler]
StateDataHandler = Union[StateDataContainerHandler, StateDataListHandler]
OpHandler = Callable[[RpcInfo], JsonNodeT]
OpHandler = Callable[[JsonNodeT, str], JsonNodeT]
ActionHandler = Callable[[InstanceRoute, JsonNodeT, str], JsonNodeT]
from typing import List, Dict, Tuple, Callable
from typing import List, Dict, Tuple
from yangson.datamodel import DataModel
from yangson.schemadata import SchemaData
from yangson.typealiases import SchemaRoute
from .handler_base import ConfDataHandlerBase, StateDataHandlerBase, ConfDataHandler, StateDataHandler, OpHandler
from .handler_base import ConfDataHandlerBase, StateDataHandlerBase, ConfDataHandler, StateDataHandler, OpHandler, ActionHandler
# ---------- Handler lists ----------
......@@ -52,8 +53,21 @@ class OpHandlerList:
def __init__(self):
self.handlers = {} # type: Dict[str, OpHandler]
def register(self, handler: Callable, op_name: str):
def register(self, handler: OpHandler, op_name: str):
self.handlers[op_name] = handler
def get_handler(self, op_name: str) -> OpHandler:
return self.handlers.get(op_name)
class ActionHandlerList:
def __init__(self, dm: DataModel):
self.handlers = {} # type: Dict[int, ActionHandler]
self._dm = dm
def register(self, handler: ActionHandler, sch_pth: str):
sn = self._dm.get_schema_node(sch_pth)
self.handlers[id(sn)] = handler
def get_handler(self, sch_node_id: int) -> ActionHandler:
return self.handlers.get(sch_node_id)
......@@ -10,7 +10,7 @@ from typing import Dict, List, Tuple, Any, Optional, Callable
from yangson.exceptions import YangsonException, NonexistentSchemaNode, SchemaError, SemanticError
from yangson.schemanode import ContainerNode, ListNode, GroupNode, LeafNode
from yangson.instance import NonexistentInstance, InstanceValueError, RootNode
from yangson.instance import NonexistentInstance, InstanceValueError, RootNode, ActionName
from yangson.instvalue import ArrayValue
from . import config
......@@ -469,6 +469,77 @@ class HttpHandlersImpl:
exception=e
)
# Check if we are calling an action
ii = self.ds.parse_ii(rpc1.path, rpc1.path_format)
if isinstance(ii[-1], ActionName):
# Calling action on a node
ns = tuple(filter(lambda seg: hasattr(seg, "namespace") and (seg.namespace is not None), ii))[-1].namespace
try:
input_args = json_data[ns + ":input"]
except KeyError as e:
http_resp = HttpResponse.error(
HttpStatus.BadRequest,
RestconfErrType.Protocol,
ERRTAG_INVVALUE,
exception=e
)
else:
rpc1.op_input_args = input_args
try:
root_running = self.ds.get_data_root()
ret_data = self.ds.invoke_action_rpc(root_running, rpc1)
if ret_data is None:
http_resp = HttpResponse.empty(HttpStatus.NoContent, status_in_body=False)
else:
if not isinstance(ret_data, str):
response = json.dumps(ret_data, indent=4)
else:
response = ret_data
http_resp = HttpResponse(HttpStatus.Ok, response.encode(), CTYPE_YANG_JSON)
except NacmForbiddenError as e:
http_resp = HttpResponse.error(
HttpStatus.Forbidden,
RestconfErrType.Protocol,
ERRTAG_ACCDENIED,
exception=e
)
except (NonexistentSchemaNode, NonexistentInstance) as e:
http_resp = HttpResponse.error(
HttpStatus.NotFound,
RestconfErrType.Protocol,
ERRTAG_INVVALUE,
exception=e
)
except NoHandlerForOpError as e:
http_resp = HttpResponse.error(
HttpStatus.BadRequest,
RestconfErrType.Protocol,
ERRTAG_OPNOTSUPPORTED,
exception=e
)
except (SchemaError, SemanticError) as e:
http_resp = HttpResponse.error(
HttpStatus.BadRequest,
RestconfErrType.Protocol,
ERRTAG_INVVALUE,
exception=e
)
except (OpHandlerFailedError, StagingDataException, YangsonException) as e:
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
)
else:
# Creating new node
try:
self.ds.lock_data(username)
......
......@@ -5,7 +5,7 @@ from importlib import import_module
from pkg_resources import resource_string
from yangson.enumerations import ContentType, ValidationScope
from yangson.exceptions import YangsonException
from yangson.exceptions import YangsonException, ModuleNotFound
from yangson.schemanode import SchemaError, SemanticError
from yangson.datamodel import DataModel
......@@ -45,6 +45,7 @@ class Jetconf:
usr_state_data_handlers = import_module(backend_package + ".usr_state_data_handlers")
usr_conf_data_handlers = import_module(backend_package + ".usr_conf_data_handlers")
usr_op_handlers = import_module(backend_package + ".usr_op_handlers")
usr_action_handlers = import_module(backend_package + ".usr_action_handlers")
usr_datastore = import_module(backend_package + ".usr_datastore")
except ImportError as e:
raise JetconfInitError(
......@@ -59,7 +60,10 @@ class Jetconf:
# Load data model
yang_mod_dir = self.config.glob["YANG_LIB_DIR"]
yang_lib_str = resource_string(backend_package, "yang-library-data.json").decode("utf-8")
try:
datamodel = DataModel(yang_lib_str, [yang_mod_dir])
except ModuleNotFound as e:
raise JetconfInitError("Cannot find YANG module \"{} ({})\" in YANG library".format(e.name, e.rev))
# Datastore init
datastore = usr_datastore.UserDatastore(
......@@ -93,6 +97,9 @@ class Jetconf:
op_internal.register_op_handlers(datastore)
usr_op_handlers.register_op_handlers(datastore)
# Register handlers for actions
usr_action_handlers.register_action_handlers(datastore)
# Init backend package
if self.usr_init is not None:
try:
......
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