Loading src/common/tools/rest_conf/server/restconf_server/Callbacks.py +60 −15 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -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): Loading @@ -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 ========================================================================================== Loading @@ -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: Loading @@ -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: Loading @@ -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() src/common/tools/rest_conf/server/restconf_server/DispatchData.py +4 −4 Original line number Diff line number Diff line Loading @@ -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 ) Loading Loading @@ -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 ) Loading Loading @@ -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 ) Loading Loading @@ -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 ) Loading src/common/tools/rest_conf/server/restconf_server/DispatchOperations.py 0 → 100644 +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 src/common/tools/rest_conf/server/restconf_server/RestConfServerApplication.py +7 −0 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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], Loading Loading
src/common/tools/rest_conf/server/restconf_server/Callbacks.py +60 −15 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -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): Loading @@ -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 ========================================================================================== Loading @@ -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: Loading @@ -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: Loading @@ -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()
src/common/tools/rest_conf/server/restconf_server/DispatchData.py +4 −4 Original line number Diff line number Diff line Loading @@ -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 ) Loading Loading @@ -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 ) Loading Loading @@ -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 ) Loading Loading @@ -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 ) Loading
src/common/tools/rest_conf/server/restconf_server/DispatchOperations.py 0 → 100644 +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
src/common/tools/rest_conf/server/restconf_server/RestConfServerApplication.py +7 −0 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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], Loading