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 ...@@ -26,7 +26,7 @@ from .helpers import PathFormat, ErrorHelpers, LogHelpers, DataHelpers, JsonNode
from .nacm import NacmConfig, Permission, Action from .nacm import NacmConfig, Permission, Action
from .journal import ChangeType, UsrChangeJournal, RpcInfo, DataChange from .journal import ChangeType, UsrChangeJournal, RpcInfo, DataChange
from .handler_base import ConfDataObjectHandler, ConfDataListHandler, StateDataContainerHandler, StateDataListHandler 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 ( from .errors import (
StagingDataException, StagingDataException,
NoHandlerForStateDataError, NoHandlerForStateDataError,
...@@ -43,10 +43,11 @@ debug_data = LogHelpers.create_module_dbg_logger(__name__) ...@@ -43,10 +43,11 @@ debug_data = LogHelpers.create_module_dbg_logger(__name__)
class BackendHandlers: class BackendHandlers:
def __init__(self): def __init__(self, dm: DataModel):
self.conf = ConfDataHandlerList() self.conf = ConfDataHandlerList()
self.state = StateDataHandlerList() self.state = StateDataHandlerList()
self.op = OpHandlerList() self.op = OpHandlerList()
self.action = ActionHandlerList(dm)
def _blankfn(*args, **kwargs): def _blankfn(*args, **kwargs):
pass pass
...@@ -65,7 +66,7 @@ class BaseDatastore: ...@@ -65,7 +66,7 @@ class BaseDatastore:
self._lock_username = None # type: str self._lock_username = None # type: str
self._usr_journals = {} # type: Dict[str, UsrChangeJournal] self._usr_journals = {} # type: Dict[str, UsrChangeJournal]
self.nacm = None # type: NacmConfig self.nacm = None # type: NacmConfig
self.handlers = BackendHandlers() self.handlers = BackendHandlers(self._dm)
self.nacm = NacmConfig(self, self._dm) if with_nacm else None self.nacm = NacmConfig(self, self._dm) if with_nacm else None
# Returns DataModel object # Returns DataModel object
...@@ -660,6 +661,41 @@ class BaseDatastore: ...@@ -660,6 +661,41 @@ class BaseDatastore:
return ret_data 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): 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) usr_journal = self._usr_journals.get(rpc.username)
if usr_journal is not None: if usr_journal is not None:
......
...@@ -3,7 +3,7 @@ from typing import Callable, Union ...@@ -3,7 +3,7 @@ from typing import Callable, Union
from yangson.schemanode import SchemaNode from yangson.schemanode import SchemaNode
from yangson.instance import InstanceRoute from yangson.instance import InstanceRoute
from .journal import DataChange, RpcInfo from .journal import DataChange
from .helpers import JsonNodeT from .helpers import JsonNodeT
...@@ -77,4 +77,5 @@ class StateDataListHandler(StateDataHandlerBase): ...@@ -77,4 +77,5 @@ class StateDataListHandler(StateDataHandlerBase):
# ---------- Types ---------- # ---------- Types ----------
ConfDataHandler = Union[ConfDataObjectHandler, ConfDataListHandler] ConfDataHandler = Union[ConfDataObjectHandler, ConfDataListHandler]
StateDataHandler = Union[StateDataContainerHandler, StateDataListHandler] 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.schemadata import SchemaData
from yangson.typealiases import SchemaRoute 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 ---------- # ---------- Handler lists ----------
...@@ -52,8 +53,21 @@ class OpHandlerList: ...@@ -52,8 +53,21 @@ class OpHandlerList:
def __init__(self): def __init__(self):
self.handlers = {} # type: Dict[str, OpHandler] 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 self.handlers[op_name] = handler
def get_handler(self, op_name: str) -> OpHandler: def get_handler(self, op_name: str) -> OpHandler:
return self.handlers.get(op_name) 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 ...@@ -10,7 +10,7 @@ from typing import Dict, List, Tuple, Any, Optional, Callable
from yangson.exceptions import YangsonException, NonexistentSchemaNode, SchemaError, SemanticError from yangson.exceptions import YangsonException, NonexistentSchemaNode, SchemaError, SemanticError
from yangson.schemanode import ContainerNode, ListNode, GroupNode, LeafNode 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 yangson.instvalue import ArrayValue
from . import config from . import config
...@@ -469,58 +469,129 @@ class HttpHandlersImpl: ...@@ -469,58 +469,129 @@ class HttpHandlersImpl:
exception=e exception=e
) )
try: # Check if we are calling an action
self.ds.lock_data(username) 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: try:
staging_root = self.ds.get_data_root_staging(rpc1.username) input_args = json_data[ns + ":input"]
new_root = self.ds.create_node_rpc(staging_root, rpc1, json_data) except KeyError as e:
self.ds.add_to_journal_rpc(ChangeType.CREATE, rpc1, json_data, *new_root)
http_resp = HttpResponse.empty(HttpStatus.Created)
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 NoHandlerError as e:
http_resp = HttpResponse.error(
HttpStatus.BadRequest,
RestconfErrType.Protocol,
ERRTAG_OPNOTSUPPORTED,
exception=e
)
except (InstanceValueError, YangsonException, ValueError) as e:
http_resp = HttpResponse.error( http_resp = HttpResponse.error(
HttpStatus.BadRequest, HttpStatus.BadRequest,
RestconfErrType.Protocol, RestconfErrType.Protocol,
ERRTAG_INVVALUE, ERRTAG_INVVALUE,
exception=e exception=e
) )
except InstanceAlreadyPresent as 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)
try:
staging_root = self.ds.get_data_root_staging(rpc1.username)
new_root = self.ds.create_node_rpc(staging_root, rpc1, json_data)
self.ds.add_to_journal_rpc(ChangeType.CREATE, rpc1, json_data, *new_root)
http_resp = HttpResponse.empty(HttpStatus.Created)
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 NoHandlerError as e:
http_resp = HttpResponse.error(
HttpStatus.BadRequest,
RestconfErrType.Protocol,
ERRTAG_OPNOTSUPPORTED,
exception=e
)
except (InstanceValueError, YangsonException, ValueError) as e:
http_resp = HttpResponse.error(
HttpStatus.BadRequest,
RestconfErrType.Protocol,
ERRTAG_INVVALUE,
exception=e
)
except InstanceAlreadyPresent as e:
http_resp = HttpResponse.error(
HttpStatus.Conflict,
RestconfErrType.Protocol,
ERRTAG_EXISTS,
exception=e
)
except DataLockError as e:
http_resp = HttpResponse.error( http_resp = HttpResponse.error(
HttpStatus.Conflict, HttpStatus.Conflict,
RestconfErrType.Protocol, RestconfErrType.Protocol,
ERRTAG_EXISTS, ERRTAG_LOCKDENIED,
exception=e exception=e
) )
except DataLockError as e: finally:
http_resp = HttpResponse.error( self.ds.unlock_data()
HttpStatus.Conflict,
RestconfErrType.Protocol,
ERRTAG_LOCKDENIED,
exception=e
)
finally:
self.ds.unlock_data()
return http_resp return http_resp
......
...@@ -5,7 +5,7 @@ from importlib import import_module ...@@ -5,7 +5,7 @@ from importlib import import_module
from pkg_resources import resource_string from pkg_resources import resource_string
from yangson.enumerations import ContentType, ValidationScope 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.schemanode import SchemaError, SemanticError
from yangson.datamodel import DataModel from yangson.datamodel import DataModel
...@@ -45,6 +45,7 @@ class Jetconf: ...@@ -45,6 +45,7 @@ class Jetconf:
usr_state_data_handlers = import_module(backend_package + ".usr_state_data_handlers") 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_conf_data_handlers = import_module(backend_package + ".usr_conf_data_handlers")
usr_op_handlers = import_module(backend_package + ".usr_op_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") usr_datastore = import_module(backend_package + ".usr_datastore")
except ImportError as e: except ImportError as e:
raise JetconfInitError( raise JetconfInitError(
...@@ -59,7 +60,10 @@ class Jetconf: ...@@ -59,7 +60,10 @@ class Jetconf:
# Load data model # Load data model
yang_mod_dir = self.config.glob["YANG_LIB_DIR"] yang_mod_dir = self.config.glob["YANG_LIB_DIR"]
yang_lib_str = resource_string(backend_package, "yang-library-data.json").decode("utf-8") yang_lib_str = resource_string(backend_package, "yang-library-data.json").decode("utf-8")
datamodel = DataModel(yang_lib_str, [yang_mod_dir]) 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 init
datastore = usr_datastore.UserDatastore( datastore = usr_datastore.UserDatastore(
...@@ -93,6 +97,9 @@ class Jetconf: ...@@ -93,6 +97,9 @@ class Jetconf:
op_internal.register_op_handlers(datastore) op_internal.register_op_handlers(datastore)
usr_op_handlers.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 # Init backend package
if self.usr_init is not None: if self.usr_init is not None:
try: 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