diff --git a/src/tests/tools/mock_ipm_sdn_ctrl/MockIPMSdnCtrl.py b/src/tests/tools/mock_ipm_sdn_ctrl/MockIPMSdnCtrl.py
new file mode 100644
index 0000000000000000000000000000000000000000..52a85a00da442a2684877c9f753571db124eee79
--- /dev/null
+++ b/src/tests/tools/mock_ipm_sdn_ctrl/MockIPMSdnCtrl.py
@@ -0,0 +1,131 @@
+# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (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.
+
+# Mock IPM controller (implements minimal support)
+
+import functools, json, logging, sys, time, uuid
+from flask import Flask, jsonify, make_response, request
+from flask_restful import Api, Resource
+
+BIND_ADDRESS = '0.0.0.0'
+BIND_PORT    = 8444
+IPM_USERNAME = 'xr-user-1'
+IPM_PASSWORD = 'xr-user-1'
+STR_ENDPOINT = 'https://{:s}:{:s}'.format(str(BIND_ADDRESS), str(BIND_PORT))
+LOG_LEVEL    = logging.DEBUG
+
+CONSTELLATION = {
+    'id': 'ofc-constellation',
+    'hubModule': {'state': {
+        'module': {'moduleName': 'OFC HUB 1', 'trafficMode': 'L1Mode'},
+        'endpoints': [{'moduleIf': {'clientIfAid': 'XR-T1'}}, {'moduleIf': {'clientIfAid': 'XR-T4'}}]
+    }},
+    'leafModules': [
+        {'state': {
+            'module': {'moduleName': 'OFC LEAF 1', 'trafficMode': 'L1Mode'},
+            'endpoints': [{'moduleIf': {'clientIfAid': 'XR-T1'}}]
+        }},
+        {'state': {
+            'module': {'moduleName': 'OFC LEAF 2', 'trafficMode': 'L1Mode'},
+            'endpoints': [{'moduleIf': {'clientIfAid': 'XR-T1'}}]
+        }}
+    ]
+}
+
+logging.basicConfig(level=LOG_LEVEL, format="[%(asctime)s] %(levelname)s:%(name)s:%(message)s")
+LOGGER = logging.getLogger(__name__)
+
+logging.getLogger('werkzeug').setLevel(logging.WARNING)
+
+def log_request(logger : logging.Logger, response):
+    timestamp = time.strftime('[%Y-%b-%d %H:%M]')
+    logger.info('%s %s %s %s %s', timestamp, request.remote_addr, request.method, request.full_path, response.status)
+    return response
+
+#class Health(Resource):
+#    def get(self):
+#        return make_response(jsonify({}), 200)
+
+class OpenIdConnect(Resource):
+    ACCESS_TOKENS = {}
+
+    def post(self):
+        if request.content_type != 'application/x-www-form-urlencoded': return make_response('bad content type', 400)
+        if request.content_length == 0: return make_response('bad content length', 400)
+        form_request = request.form
+        if form_request.get('client_id') != 'xr-web-client': return make_response('bad client_id', 403)
+        if form_request.get('client_secret') != 'xr-web-client': return make_response('bad client_secret', 403)
+        if form_request.get('grant_type') != 'password': return make_response('bad grant_type', 403)
+        if form_request.get('username') != IPM_USERNAME: return make_response('bad username', 403)
+        if form_request.get('password') != IPM_PASSWORD: return make_response('bad password', 403)
+        access_token = OpenIdConnect.ACCESS_TOKENS.setdefault(IPM_USERNAME, uuid.uuid4())
+        reply = {'access_token': access_token, 'expires_in': 86400}
+        return make_response(jsonify(reply), 200)
+
+class XrNetworks(Resource):
+    def get(self):
+        print(str(request.args))
+        content = request.args.get('content')
+        print('content', content)
+        query = json.loads(request.args.get('q'))
+        hub_module_name = query.get('hubModule.state.module.moduleName')
+        if hub_module_name != 'OFC HUB 1': return make_response('unexpected hub module', 404)
+        print('query', query)
+        return make_response(jsonify([CONSTELLATION]), 200)
+
+#class Services(Resource):
+#    def get(self):
+#        services = [service for service in NETWORK_SERVICES.values()]
+#        return make_response(jsonify({'ietf-eth-tran-service:etht-svc': {'etht-svc-instances': services}}), 200)
+#
+#    def post(self):
+#        json_request = request.get_json()
+#        if not json_request: abort(400)
+#        if not isinstance(json_request, dict): abort(400)
+#        if 'etht-svc-instances' not in json_request: abort(400)
+#        json_services = json_request['etht-svc-instances']
+#        if not isinstance(json_services, list): abort(400)
+#        if len(json_services) != 1: abort(400)
+#        svc_data = json_services[0]
+#        etht_svc_name = svc_data['etht-svc-name']
+#        NETWORK_SERVICES[etht_svc_name] = svc_data
+#        return make_response(jsonify({}), 201)
+
+#class DelServices(Resource):
+#    def delete(self, service_uuid : str):
+#        NETWORK_SERVICES.pop(service_uuid, None)
+#        return make_response(jsonify({}), 204)
+
+def main():
+    LOGGER.info('Starting...')
+    
+    app = Flask(__name__)
+    app.after_request(functools.partial(log_request, LOGGER))
+
+    api = Api(app)
+    #api.add_resource(Health,      '/ietf-network:networks')
+    api.add_resource(OpenIdConnect, '/realms/xr-cm/protocol/openid-connect/token')
+    api.add_resource(XrNetworks,    '/api/v1/xr-networks')
+    #api.add_resource(Network,     '/ietf-network:networks/network=<string:network_uuid>')
+    #api.add_resource(Services,    '/ietf-eth-tran-service:etht-svc')
+    #api.add_resource(DelServices, '/ietf-eth-tran-service:etht-svc/etht-svc-instances=<string:service_uuid>')
+
+    LOGGER.info('Listening on {:s}...'.format(str(STR_ENDPOINT)))
+    app.run(debug=True, host=BIND_ADDRESS, port=BIND_PORT, ssl_context='adhoc')
+
+    LOGGER.info('Bye')
+    return 0
+
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/src/tests/tools/mock_ipm_sdn_ctrl/run.sh b/src/tests/tools/mock_ipm_sdn_ctrl/run.sh
new file mode 100755
index 0000000000000000000000000000000000000000..2aa78712c58d8cc255b60202d1576de683798d2e
--- /dev/null
+++ b/src/tests/tools/mock_ipm_sdn_ctrl/run.sh
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (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.
+
+python MockIPMSdnCtrl.py