Commit a495c490 authored by Lluis Gifre Renom's avatar Lluis Gifre Renom
Browse files

Common - Tools - RestConf - Server:

- Extended Callbacks for data and operations
- Implemented Operations dispatcher
parent bd7dacc1
Loading
Loading
Loading
Loading
+60 −15
Original line number Diff line number Diff line
@@ -38,12 +38,12 @@ class _Callback:
        '''
        return self._path_pattern.fullmatch(path)

    def execute(
    def execute_data(
        self, match : re.Match, path : str, old_data : Optional[Dict],
        new_data : Optional[Dict]
    ) -> bool:
        '''
        Execute the callback action for a matched path.
        Execute the callback action for a matched data path.
        This method should be implemented for each specific callback.
        @param match: `re.Match` object returned by `match()`.
        @param path: Original request path that was matched.
@@ -55,6 +55,21 @@ class _Callback:
        msg = MSG.format(match.groupdict(), path, old_data, new_data)
        raise NotImplementedError(msg)

    def execute_operation(
        self, match : re.Match, path : str, input_data : Optional[Dict]
    ) -> Optional[Dict]:
        '''
        Execute the callback action for a matched operation path.
        This method should be implemented for each specific callback.
        @param match: `re.Match` object returned by `match()`.
        @param path: Original request path that was matched.
        @param input_data: Input data, if applicable, otherwise `None`
        @returns Optional[Dict] containing output data, defaults to None
        '''
        MSG = 'match={:s}, path={:s}, input_data={:s}'
        msg = MSG.format(match.groupdict(), path, input_data)
        raise NotImplementedError(msg)


class CallbackDispatcher:
    def __init__(self):
@@ -63,16 +78,32 @@ class CallbackDispatcher:
    def register(self, callback : _Callback) -> None:
        self._callbacks.append(callback)

    def dispatch(
    def dispatch_data(
        self, path : str, old_data : Optional[Dict] = None, new_data : Optional[Dict] = None
    ) -> None:
        LOGGER.warning('Checking Callbacks for path={:s}'.format(str(path)))
        LOGGER.warning('[dispatch_data] Checking Callbacks for path={:s}'.format(str(path)))
        for callback in self._callbacks:
            match = callback.match(path)
            if match is None: continue
            keep_running_callbacks = callback.execute(match, path, old_data, new_data)
            keep_running_callbacks = callback.execute_data(match, path, old_data, new_data)
            if not keep_running_callbacks: break

    def dispatch_operation(
        self, path : str, input_data : Optional[Dict] = None
    ) -> Optional[Dict]:
        LOGGER.warning('[dispatch_operation] Checking Callbacks for path={:s}'.format(str(path)))

        # First matching callback is executed, and its output returned.
        for callback in self._callbacks:
            match = callback.match(path)
            if match is None: continue
            output_data = callback.execute_operation(match, path, input_data)
            return output_data

        # If no callback found, raise NotImplemented exception
        MSG = 'Callback for operation ({:s}) not defined'
        raise NotImplementedError(MSG.format(str(path)))


# ===== EXAMPLE ==========================================================================================

@@ -82,7 +113,7 @@ class CallbackOnNetwork(_Callback):
        pattern += r'/ietf-network:networks/network=(?P<network_id>[^/]+)'
        super().__init__(pattern)

    def execute(
    def execute_data(
        self, match : re.Match, path : str, old_data : Optional[Dict],
        new_data : Optional[Dict]
    ) -> bool:
@@ -96,7 +127,7 @@ class CallbackOnNode(_Callback):
        pattern += r'/node=(?P<node_id>[^/]+)'
        super().__init__(pattern)

    def execute(
    def execute_data(
        self, match : re.Match, path : str, old_data : Optional[Dict],
        new_data : Optional[Dict]
    ) -> bool:
@@ -110,25 +141,39 @@ class CallbackOnLink(_Callback):
        pattern += r'/ietf-network-topology:link=(?P<link_id>[^/]+)'
        super().__init__(pattern)

    def execute(
    def execute_data(
        self, match : re.Match, path : str, old_data : Optional[Dict],
        new_data : Optional[Dict]
    ) -> bool:
        print('[on_link]', match.groupdict(), path, old_data, new_data)
        return False

class CallbackShutdown(_Callback):
    def __init__(self) -> None:
        pattern = r'/restconf/operations'
        pattern += r'/shutdown'
        super().__init__(pattern)

    def execute_operation(
        self, match : re.Match, path : str, input_data : Optional[Dict]
    ) -> bool:
        print('[shutdown]', match.groupdict(), path, input_data)
        return {'state': 'processing'}

def main() -> None:
    callbacks = CallbackDispatcher()
    callbacks.register(CallbackOnNetwork())
    callbacks.register(CallbackOnNode())
    callbacks.register(CallbackOnLink())

    callbacks.dispatch('/restconf/data/ietf-network:networks/network=admin')
    callbacks.dispatch('/restconf/data/ietf-network:networks/network=admin/node=P-PE2')
    callbacks.dispatch('/restconf/data/ietf-network:networks/network=admin/ietf-network-topology:link=L6')
    callbacks.dispatch('/restconf/data/ietf-network:networks/network=admin/')
    callbacks.dispatch('/restconf/data/ietf-network:networks/network=admin/node=P-PE1/')
    callbacks.dispatch('/restconf/data/ietf-network:networks/network=admin/ietf-network-topology:link=L4/')
    callbacks.register(CallbackShutdown())

    callbacks.dispatch_data('/restconf/data/ietf-network:networks/network=admin')
    callbacks.dispatch_data('/restconf/data/ietf-network:networks/network=admin/node=P-PE2')
    callbacks.dispatch_data('/restconf/data/ietf-network:networks/network=admin/ietf-network-topology:link=L6')
    callbacks.dispatch_data('/restconf/data/ietf-network:networks/network=admin/')
    callbacks.dispatch_data('/restconf/data/ietf-network:networks/network=admin/node=P-PE1/')
    callbacks.dispatch_data('/restconf/data/ietf-network:networks/network=admin/ietf-network-topology:link=L4/')
    callbacks.dispatch_operation('/restconf/operations/shutdown/')

if __name__ == '__main__':
    main()
+4 −4
Original line number Diff line number Diff line
@@ -70,7 +70,7 @@ class RestConfDispatchData(Resource):

        LOGGER.info('[POST] {:s} {:s} => {:s}'.format(subpath, str(payload), str(json_data)))

        self._callback_dispatcher.dispatch(
        self._callback_dispatcher.dispatch_data(
            '/restconf/data/' + subpath, old_data=None, new_data=json_data
        )

@@ -102,7 +102,7 @@ class RestConfDispatchData(Resource):
        diff_data = deepdiff.DeepDiff(old_data, new_data)
        updated = len(diff_data) > 0

        self._callback_dispatcher.dispatch(
        self._callback_dispatcher.dispatch_data(
            '/restconf/data/' + subpath, old_data=old_data, new_data=new_data
        )

@@ -140,7 +140,7 @@ class RestConfDispatchData(Resource):
        #diff_data = deepdiff.DeepDiff(old_data, new_data)
        #updated = len(diff_data) > 0

        self._callback_dispatcher.dispatch(
        self._callback_dispatcher.dispatch_data(
            '/restconf/data/' + subpath, old_data=old_data, new_data=new_data
        )

@@ -170,7 +170,7 @@ class RestConfDispatchData(Resource):
                description='Path({:s}) not found'.format(str(subpath))
            )

        self._callback_dispatcher.dispatch(
        self._callback_dispatcher.dispatch_data(
            '/restconf/data/' + subpath, old_data=old_data, new_data=None
        )

+48 −0
Original line number Diff line number Diff line
# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


import logging
from flask import Response, abort, jsonify, request
from flask_restful import Resource
from .Callbacks import CallbackDispatcher
from .HttpStatusCodesEnum import HttpStatusCodesEnum
from .YangHandler import YangHandler

LOGGER = logging.getLogger(__name__)

class RestConfDispatchOperations(Resource):
    def __init__(
        self, yang_handler : YangHandler, callback_dispatcher : CallbackDispatcher
    ) -> None:
        super().__init__()
        self._yang_handler = yang_handler
        self._callback_dispatcher = callback_dispatcher

    def post(self, subpath : str) -> Response:
        try:
            payload = request.get_json(force=True)
        except Exception:
            LOGGER.exception('Invalid JSON')
            abort(HttpStatusCodesEnum.CLI_ERR_BAD_REQUEST.value, desctiption='Invalid JSON')

        output_data = self._callback_dispatcher.dispatch_operation(
            '/restconf/operations/' + subpath, input_data=payload
        )

        LOGGER.info('[POST] {:s} {:s} => {:s}'.format(subpath, str(payload), str(output_data)))

        response = jsonify(output_data)
        response.status_code = HttpStatusCodesEnum.SUCCESS_OK.value
        return response
+7 −0
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ from flask_restful import Api, Resource
from .Callbacks import CallbackDispatcher
from .Config import RESTCONF_PREFIX, SECRET_KEY, STARTUP_FILE, YANG_SEARCH_PATH
from .DispatchData import RestConfDispatchData
from .DispatchOperations import RestConfDispatchOperations
from .HostMeta import HostMeta
from .YangHandler import YangHandler
from .YangModelDiscoverer import YangModuleDiscoverer
@@ -82,6 +83,12 @@ class RestConfServerApplication:
            RESTCONF_PREFIX + '/data/<path:subpath>/',
            resource_class_args=(self._yang_handler, self._callback_dispatcher)
        )
        self._api.add_resource(
            RestConfDispatchOperations,
            RESTCONF_PREFIX + '/operations/<path:subpath>',
            RESTCONF_PREFIX + '/operations/<path:subpath>/',
            resource_class_args=(self._yang_handler, self._callback_dispatcher)
        )

    def register_custom(
        self, resource_class : Type[Resource],