3 ##############################################################################
4 # Copyright (c) 2021 Orange, Inc. and others. All rights reserved.
6 # All rights reserved. This program and the accompanying materials
7 # are made available under the terms of the Apache License, Version 2.0
8 # which accompanies this distribution, and is available at
9 # http://www.apache.org/licenses/LICENSE-2.0
10 ##############################################################################
12 # pylint: disable=no-member
16 # pylint: disable=wrong-import-order
26 # pylint: disable=import-error
29 SIMS = simulators.SIMS
31 HONEYNODE_OK_START_MSG = 'Netconf SSH endpoint started successfully at 0.0.0.0'
32 KARAF_OK_START_MSG = "Transportpce controller started"
33 LIGHTY_OK_START_MSG = re.escape("lighty.io and RESTCONF-NETCONF started")
40 TYPE_APPLICATION_JSON = {'Content-Type': 'application/json', 'Accept': 'application/json'}
41 TYPE_APPLICATION_XML = {'Content-Type': 'application/xml', 'Accept': 'application/xml'}
45 CODE_SHOULD_BE_200 = 'Http status code should be 200'
46 CODE_SHOULD_BE_201 = 'Http status code should be 201'
47 T100GE = 'Transponder 100GE'
48 T0_MULTILAYER_TOPO = 'T0 - Multi-layer topology'
49 T0_FULL_MULTILAYER_TOPO = 'T0 - Full Multi-layer topology'
51 SIM_LOG_DIRECTORY = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'log')
55 if 'USE_ODL_ALT_RESTCONF_PORT' in os.environ:
56 RESTCONF_PORT = os.environ['USE_ODL_ALT_RESTCONF_PORT']
60 RESTCONF_PATH_PREFIX = {'rfc8040': '/rests',
61 'draft-bierman02': '/restconf'}
63 if 'USE_ODL_RESTCONF_VERSION' in os.environ:
64 RESTCONF_VERSION = os.environ['USE_ODL_RESTCONF_VERSION']
65 if RESTCONF_VERSION not in RESTCONF_PATH_PREFIX:
66 print('unsupported RESTCONF version ' + RESTCONF_VERSION)
69 RESTCONF_VERSION = 'rfc8040'
71 RESTCONF_BASE_URL = 'http://localhost:' + str(RESTCONF_PORT) + RESTCONF_PATH_PREFIX[RESTCONF_VERSION]
73 if 'USE_ODL_ALT_KARAF_INSTALL_DIR' in os.environ:
74 KARAF_INSTALLDIR = os.environ['USE_ODL_ALT_KARAF_INSTALL_DIR']
76 KARAF_INSTALLDIR = 'karaf'
78 KARAF_LOG = os.path.join(
79 os.path.dirname(os.path.realpath(__file__)),
80 '..', '..', '..', KARAF_INSTALLDIR, 'target', 'assembly', 'data', 'log', 'karaf.log')
82 if 'USE_LIGHTY' in os.environ and os.environ['USE_LIGHTY'] == 'True':
83 TPCE_LOG = 'odl-' + str(os.getpid()) + '.log'
88 # Basic HTTP operations
93 return requests.request(
94 'GET', url.format(RESTCONF_BASE_URL),
95 headers=TYPE_APPLICATION_JSON,
96 auth=(ODL_LOGIN, ODL_PWD),
97 timeout=REQUEST_TIMEOUT)
100 def put_request(url, data):
101 return requests.request(
102 'PUT', url.format(RESTCONF_BASE_URL),
103 data=json.dumps(data),
104 headers=TYPE_APPLICATION_JSON,
105 auth=(ODL_LOGIN, ODL_PWD),
106 timeout=REQUEST_TIMEOUT)
109 def delete_request(url):
110 return requests.request(
111 'DELETE', url.format(RESTCONF_BASE_URL),
112 headers=TYPE_APPLICATION_JSON,
113 auth=(ODL_LOGIN, ODL_PWD),
114 timeout=REQUEST_TIMEOUT)
117 def post_request(url, data):
119 return requests.request(
120 "POST", url.format(RESTCONF_BASE_URL),
121 data=json.dumps(data),
122 headers=TYPE_APPLICATION_JSON,
123 auth=(ODL_LOGIN, ODL_PWD),
124 timeout=REQUEST_TIMEOUT)
125 return requests.request(
126 "POST", url.format(RESTCONF_BASE_URL),
127 headers=TYPE_APPLICATION_JSON,
128 auth=(ODL_LOGIN, ODL_PWD),
129 timeout=REQUEST_TIMEOUT)
136 def start_sims(sims_list):
137 for sim in sims_list:
138 print('starting simulator ' + sim[0] + ' in OpenROADM device version ' + sim[1] + '...')
139 log_file = os.path.join(SIM_LOG_DIRECTORY, SIMS[sim]['logfile'])
140 process = start_honeynode(log_file, sim)
141 if wait_until_log_contains(log_file, HONEYNODE_OK_START_MSG, 100):
142 print('simulator for ' + sim[0] + ' started')
144 print('simulator for ' + sim[0] + ' failed to start')
145 shutdown_process(process)
146 for pid in process_list:
147 shutdown_process(pid)
149 process_list.append(process)
154 if 'NO_ODL_STARTUP' in os.environ:
155 print('No OpenDaylight instance to start!')
157 print('starting OpenDaylight...')
158 if 'USE_LIGHTY' in os.environ and os.environ['USE_LIGHTY'] == 'True':
159 process = start_lighty()
160 start_msg = LIGHTY_OK_START_MSG
162 process = start_karaf()
163 start_msg = KARAF_OK_START_MSG
164 if wait_until_log_contains(TPCE_LOG, start_msg, time_to_wait=100):
165 print('OpenDaylight started !')
167 print('OpenDaylight failed to start !')
168 shutdown_process(process)
169 for pid in process_list:
170 shutdown_process(pid)
172 process_list.append(process)
177 print('starting KARAF TransportPCE build...')
178 executable = os.path.join(
179 os.path.dirname(os.path.realpath(__file__)),
180 '..', '..', '..', KARAF_INSTALLDIR, 'target', 'assembly', 'bin', 'karaf')
181 with open('odl.log', 'w', encoding='utf-8') as outfile:
182 return subprocess.Popen(
183 ['sh', executable, 'server'], stdout=outfile, stderr=outfile, stdin=None)
187 print('starting LIGHTY.IO TransportPCE build...')
188 executable = os.path.join(
189 os.path.dirname(os.path.realpath(__file__)),
190 '..', '..', '..', 'lighty', 'target', 'tpce',
191 'clean-start-controller.sh')
192 with open(TPCE_LOG, 'w', encoding='utf-8') as outfile:
193 return subprocess.Popen(
194 ['sh', executable], stdout=outfile, stderr=outfile, stdin=None)
197 def install_karaf_feature(feature_name: str):
198 print('installing feature ' + feature_name)
199 executable = os.path.join(
200 os.path.dirname(os.path.realpath(__file__)),
201 '..', '..', '..', KARAF_INSTALLDIR, 'target', 'assembly', 'bin', 'client')
202 # FIXME: https://jira.opendaylight.org/browse/TRNSPRTPCE-701
203 # -b option needed below because of Karaf client bug reporte in the JIRA ticket mentioned above
204 return subprocess.run([executable, '-b'],
205 input='feature:install ' + feature_name + '\n feature:list | grep '
206 + feature_name + ' \n logout \n',
207 universal_newlines=True, check=False)
210 def shutdown_process(process):
211 if process is not None:
212 for child in psutil.Process(process.pid).children():
213 child.send_signal(signal.SIGINT)
215 process.send_signal(signal.SIGINT)
218 def start_honeynode(log_file: str, sim):
219 executable = os.path.join(os.path.dirname(os.path.realpath(__file__)),
220 '..', '..', 'honeynode', sim[1], 'honeynode-simulator', 'honeycomb-tpce')
221 sample_directory = os.path.join(os.path.dirname(os.path.realpath(__file__)),
222 '..', '..', 'sample_configs', 'openroadm', sim[1])
223 if os.path.isfile(executable):
224 with open(log_file, 'w', encoding='utf-8') as outfile:
225 return subprocess.Popen(
226 [executable, SIMS[sim]['port'], os.path.join(sample_directory, SIMS[sim]['configfile'])],
227 stdout=outfile, stderr=outfile)
231 def wait_until_log_contains(log_file, regexp, time_to_wait=60):
232 # pylint: disable=lost-exception
233 # pylint: disable=consider-using-with
238 with TimeOut(seconds=time_to_wait):
239 while not os.path.exists(log_file):
241 filelogs = open(log_file, 'r', encoding='utf-8')
244 print("Searching for pattern '" + regexp + "' in " + os.path.basename(log_file), end='... ', flush=True)
245 compiled_regexp = re.compile(regexp)
247 line = filelogs.readline()
248 if compiled_regexp.search(line):
249 print('Pattern found!', end=' ')
255 print('Pattern not found after ' + str(time_to_wait), end=' seconds! ', flush=True)
256 except PermissionError:
257 print('Permission Error when trying to access the log file', end=' ... ', flush=True)
262 print('log file does not exist or is not accessible... ', flush=True)
267 def __init__(self, seconds=1, error_message='Timeout'):
268 self.seconds = seconds
269 self.error_message = error_message
271 def handle_timeout(self, signum, frame):
272 raise TimeoutError(self.error_message)
275 signal.signal(signal.SIGALRM, self.handle_timeout)
276 signal.alarm(self.seconds)
278 def __exit__(self, type, value, traceback):
279 # pylint: disable=W0622
283 # Basic NetCONF device operations
287 def mount_device(node: str, sim: str):
288 url = {'rfc8040': '{}/data/network-topology:network-topology/topology=topology-netconf/node={}',
289 'draft-bierman02': '{}/config/network-topology:network-topology/topology/topology-netconf/node/{}'}
292 'netconf-node-topology:username': NODES_LOGIN,
293 'netconf-node-topology:password': NODES_PWD,
294 'netconf-node-topology:host': '127.0.0.1',
295 'netconf-node-topology:port': SIMS[sim]['port'],
296 'netconf-node-topology:tcp-only': 'false',
297 'netconf-node-topology:pass-through': {}}]}
298 response = put_request(url[RESTCONF_VERSION].format('{}', node), body)
299 if wait_until_log_contains(TPCE_LOG, 'Triggering notification stream NETCONF for node ' + node, 180):
300 print('Node ' + node + ' correctly added to tpce topology', end='... ', flush=True)
302 print('Node ' + node + ' still not added to tpce topology', end='... ', flush=True)
303 if response.status_code == requests.codes.ok:
304 print('It was probably loaded at start-up', end='... ', flush=True)
305 # TODO an else-clause to abort test would probably be nice here
309 def unmount_device(node: str):
310 url = {'rfc8040': '{}/data/network-topology:network-topology/topology=topology-netconf/node={}',
311 'draft-bierman02': '{}/config/network-topology:network-topology/topology/topology-netconf/node/{}'}
312 response = delete_request(url[RESTCONF_VERSION].format('{}', node))
313 if wait_until_log_contains(TPCE_LOG, re.escape("onDeviceDisConnected: " + node), 180):
314 print('Node ' + node + ' correctly deleted from tpce topology', end='... ', flush=True)
316 print('Node ' + node + ' still not deleted from tpce topology', end='... ', flush=True)
320 def check_device_connection(node: str):
321 url = {'rfc8040': '{}/data/network-topology:network-topology/topology=topology-netconf/node={}?content=nonconfig',
322 'draft-bierman02': '{}/operational/network-topology:network-topology/topology/topology-netconf/node/{}'}
323 response = get_request(url[RESTCONF_VERSION].format('{}', node))
324 res = response.json()
325 return_key = {'rfc8040': 'network-topology:node',
326 'draft-bierman02': 'node'}
327 if return_key[RESTCONF_VERSION] in res.keys():
328 connection_status = res[return_key[RESTCONF_VERSION]][0]['netconf-node-topology:connection-status']
330 connection_status = res['errors']['error'][0]
331 return {'status_code': response.status_code,
332 'connection-status': connection_status}
335 def check_node_request(node: str):
336 # pylint: disable=line-too-long
337 url = {'rfc8040': '{}/data/network-topology:network-topology/topology=topology-netconf/node={}/yang-ext:mount/org-openroadm-device:org-openroadm-device?content=config', # nopep8
338 'draft-bierman02': '{}/config/network-topology:network-topology/topology/topology-netconf/node/{}/yang-ext:mount/org-openroadm-device:org-openroadm-device'} # nopep8
339 response = get_request(url[RESTCONF_VERSION].format('{}', node))
340 res = response.json()
341 return_key = {'rfc8040': 'org-openroadm-device:org-openroadm-device',
342 'draft-bierman02': 'org-openroadm-device'}
343 if return_key[RESTCONF_VERSION] in res.keys():
344 response_attribute = res[return_key[RESTCONF_VERSION]]
346 response_attribute = res['errors']['error'][0]
347 return {'status_code': response.status_code,
348 'org-openroadm-device': response_attribute}
351 def check_node_attribute_request(node: str, attribute: str, attribute_value: str):
352 # pylint: disable=line-too-long
353 url = {'rfc8040': '{}/data/network-topology:network-topology/topology=topology-netconf/node={}/yang-ext:mount/org-openroadm-device:org-openroadm-device/{}={}?content=nonconfig', # nopep8
354 'draft-bierman02': '{}/operational/network-topology:network-topology/topology/topology-netconf/node/{}/yang-ext:mount/org-openroadm-device:org-openroadm-device/{}/{}'} # nopep8
355 response = get_request(url[RESTCONF_VERSION].format('{}', node, attribute, attribute_value))
356 res = response.json()
357 return_key = {'rfc8040': 'org-openroadm-device:' + attribute,
358 'draft-bierman02': attribute}
359 if return_key[RESTCONF_VERSION] in res.keys():
360 response_attribute = res[return_key[RESTCONF_VERSION]]
362 response_attribute = res['errors']['error'][0]
363 return {'status_code': response.status_code,
364 attribute: response_attribute}
367 def check_node_attribute2_request(node: str, attribute: str, attribute_value: str, attribute2: str):
368 # pylint: disable=line-too-long
369 url = {'rfc8040': '{}/data/network-topology:network-topology/topology=topology-netconf/node={}/yang-ext:mount/org-openroadm-device:org-openroadm-device/{}={}/{}?content=config', # nopep8
370 'draft-bierman02': '{}/config/network-topology:network-topology/topology/topology-netconf/node/{}/yang-ext:mount/org-openroadm-device:org-openroadm-device/{}/{}/{}'} # nopep8
371 response = get_request(url[RESTCONF_VERSION].format('{}', node, attribute, attribute_value, attribute2))
372 res = response.json()
373 if attribute2 in res.keys():
374 response_attribute = res[attribute2]
376 response_attribute = res['errors']['error'][0]
377 return {'status_code': response.status_code,
378 attribute2: response_attribute}
381 def del_node_attribute_request(node: str, attribute: str, attribute_value: str):
382 # pylint: disable=line-too-long
383 url = {'rfc8040': '{}/data/network-topology:network-topology/topology=topology-netconf/node={}/yang-ext:mount/org-openroadm-device:org-openroadm-device/{}={}', # nopep8
384 'draft-bierman02': '{}/config/network-topology:network-topology/topology/topology-netconf/node/{}/yang-ext:mount/org-openroadm-device:org-openroadm-device/{}/{}'} # nopep8
385 response = delete_request(url[RESTCONF_VERSION].format('{}', node, attribute, attribute_value))
389 # Portmapping operations
393 def post_portmapping(payload: str):
394 url = {'rfc8040': '{}/data/transportpce-portmapping:network',
395 'draft-bierman02': '{}/config/transportpce-portmapping:network'}
396 json_payload = json.loads(payload)
397 response = post_request(url[RESTCONF_VERSION].format('{}'), json_payload)
398 return {'status_code': response.status_code}
401 def del_portmapping():
402 url = {'rfc8040': '{}/data/transportpce-portmapping:network',
403 'draft-bierman02': '{}/config/transportpce-portmapping:network'}
404 response = delete_request(url[RESTCONF_VERSION].format('{}'))
405 return {'status_code': response.status_code}
408 def get_portmapping_node_attr(node: str, attr: str, value: str):
409 # pylint: disable=consider-using-f-string
410 url = {'rfc8040': '{}/data/transportpce-portmapping:network/nodes={}',
411 'draft-bierman02': '{}/config/transportpce-portmapping:network/nodes/{}'}
412 target_url = url[RESTCONF_VERSION].format('{}', node)
414 target_url = (target_url + '/{}').format('{}', attr)
415 if value is not None:
416 suffix = {'rfc8040': '={}', 'draft-bierman02': '/{}'}
417 target_url = (target_url + suffix[RESTCONF_VERSION]).format('{}', value)
420 response = get_request(target_url)
421 res = response.json()
422 return_key = {'rfc8040': 'transportpce-portmapping:' + attr,
423 'draft-bierman02': attr}
424 if return_key[RESTCONF_VERSION] in res.keys():
425 return_output = res[return_key[RESTCONF_VERSION]]
427 return_output = res['errors']['error'][0]
428 return {'status_code': response.status_code,
432 # Topology operations
436 def get_ietf_network_request(network: str, content: str):
437 url = {'rfc8040': '{}/data/ietf-network:networks/network={}?content={}',
438 'draft-bierman02': '{}/{}/ietf-network:networks/network/{}'}
439 if RESTCONF_VERSION in ('rfc8040'):
440 format_args = ('{}', network, content)
441 elif content == 'config':
442 format_args = ('{}', content, network)
444 format_args = ('{}', 'operational', network)
445 response = get_request(url[RESTCONF_VERSION].format(*format_args))
447 res = response.json()
448 return_key = {'rfc8040': 'ietf-network:network',
449 'draft-bierman02': 'network'}
450 networks = res[return_key[RESTCONF_VERSION]]
453 return {'status_code': response.status_code,
457 def put_ietf_network(network: str, payload: str):
458 url = {'rfc8040': '{}/data/ietf-network:networks/network={}',
459 'draft-bierman02': '{}/config/ietf-network:networks/network/{}'}
460 json_payload = json.loads(payload)
461 response = put_request(url[RESTCONF_VERSION].format('{}', network), json_payload)
462 return {'status_code': response.status_code}
465 def del_ietf_network(network: str):
466 url = {'rfc8040': '{}/data/ietf-network:networks/network={}',
467 'draft-bierman02': '{}/config/ietf-network:networks/network/{}'}
468 response = delete_request(url[RESTCONF_VERSION].format('{}', network))
469 return {'status_code': response.status_code}
472 def get_ietf_network_link_request(network: str, link: str, content: str):
473 url = {'rfc8040': '{}/data/ietf-network:networks/network={}/ietf-network-topology:link={}?content={}',
474 'draft-bierman02': '{}/{}/ietf-network:networks/network/{}/ietf-network-topology:link/{}'}
475 if RESTCONF_VERSION in ('rfc8040'):
476 format_args = ('{}', network, link, content)
477 elif content == 'config':
478 format_args = ('{}', content, network, link)
480 format_args = ('{}', 'operational', network, link)
481 response = get_request(url[RESTCONF_VERSION].format(*format_args))
482 res = response.json()
483 return_key = {'rfc8040': 'ietf-network-topology:link',
484 'draft-bierman02': 'ietf-network-topology:link'}
485 link = res[return_key[RESTCONF_VERSION]][0]
486 return {'status_code': response.status_code,
490 def del_ietf_network_link_request(network: str, link: str, content: str):
491 url = {'rfc8040': '{}/data/ietf-network:networks/network={}/ietf-network-topology:link={}?content={}',
492 'draft-bierman02': '{}/{}/ietf-network:networks/network/{}/ietf-network-topology:link/{}'}
493 if RESTCONF_VERSION in ('rfc8040'):
494 format_args = ('{}', network, link, content)
495 elif content == 'config':
496 format_args = ('{}', content, network, link)
498 format_args = ('{}', 'operational', network, link)
499 response = delete_request(url[RESTCONF_VERSION].format(*format_args))
503 def add_oms_attr_request(link: str, oms_attr: str):
504 url = {'rfc8040': '{}/data/ietf-network:networks/network={}/ietf-network-topology:link={}',
505 'draft-bierman02': '{}/config/ietf-network:networks/network/{}/ietf-network-topology:link/{}'}
506 url2 = url[RESTCONF_VERSION] + '/org-openroadm-network-topology:OMS-attributes/span'
507 network = 'openroadm-topology'
508 response = put_request(url2.format('{}', network, link), oms_attr)
512 def del_oms_attr_request(link: str,):
513 url = {'rfc8040': '{}/data/ietf-network:networks/network={}/ietf-network-topology:link={}',
514 'draft-bierman02': '{}/config/ietf-network:networks/network/{}/ietf-network-topology:link/{}'}
515 url2 = url[RESTCONF_VERSION] + '/org-openroadm-network-topology:OMS-attributes/span'
516 network = 'openroadm-topology'
517 response = delete_request(url2.format('{}', network, link))
521 def get_ietf_network_node_request(network: str, node: str, content: str):
522 url = {'rfc8040': '{}/data/ietf-network:networks/network={}/node={}?content={}',
523 'draft-bierman02': '{}/{}/ietf-network:networks/network/{}/node/{}'}
524 if RESTCONF_VERSION in ('rfc8040'):
525 format_args = ('{}', network, node, content)
526 elif content == 'config':
527 format_args = ('{}', content, network, node)
529 format_args = ('{}', 'operational', network, node)
530 response = get_request(url[RESTCONF_VERSION].format(*format_args))
532 res = response.json()
533 return_key = {'rfc8040': 'ietf-network:node',
534 'draft-bierman02': 'node'}
535 node = res[return_key[RESTCONF_VERSION]][0]
538 return {'status_code': response.status_code,
542 def del_ietf_network_node_request(network: str, node: str, content: str):
543 url = {'rfc8040': '{}/data/ietf-network:networks/network={}/node={}?content={}',
544 'draft-bierman02': '{}/{}/ietf-network:networks/network/{}/node/{}'}
545 if RESTCONF_VERSION in ('rfc8040'):
546 format_args = ('{}', network, node, content)
547 elif content == 'config':
548 format_args = ('{}', content, network, node)
550 format_args = ('{}', 'operational', network, node)
551 response = delete_request(url[RESTCONF_VERSION].format(*format_args))
556 # Service list operations
560 def get_ordm_serv_list_request():
561 url = {'rfc8040': '{}/data/org-openroadm-service:service-list?content=nonconfig',
562 'draft-bierman02': '{}/operational/org-openroadm-service:service-list/'}
563 response = get_request(url[RESTCONF_VERSION])
564 res = response.json()
565 return_key = {'rfc8040': 'org-openroadm-service:service-list',
566 'draft-bierman02': 'service-list'}
567 if return_key[RESTCONF_VERSION] in res.keys():
568 response_attribute = res[return_key[RESTCONF_VERSION]]
570 response_attribute = res['errors']['error'][0]
571 return {'status_code': response.status_code,
572 'service-list': response_attribute}
575 def get_ordm_serv_list_attr_request(attribute: str, value: str):
576 url = {'rfc8040': '{}/data/org-openroadm-service:service-list/{}={}?content=nonconfig',
577 'draft-bierman02': '{}/operational/org-openroadm-service:service-list/{}/{}'}
578 format_args = ('{}', attribute, value)
579 response = get_request(url[RESTCONF_VERSION].format(*format_args))
580 res = response.json()
581 return_key = {'rfc8040': 'org-openroadm-service:' + attribute,
582 'draft-bierman02': attribute}
583 if return_key[RESTCONF_VERSION] in res.keys():
584 response_attribute = res[return_key[RESTCONF_VERSION]]
586 response_attribute = res['errors']['error'][0]
587 return {'status_code': response.status_code,
588 attribute: response_attribute}
591 def get_serv_path_list_attr(attribute: str, value: str):
592 url = {'rfc8040': '{}/data/transportpce-service-path:service-path-list/{}={}?content=nonconfig',
593 'draft-bierman02': '{}/operational/transportpce-service-path:service-path-list/{}/{}'}
594 response = get_request(url[RESTCONF_VERSION].format('{}', attribute, value))
595 res = response.json()
596 return_key = {'rfc8040': 'transportpce-service-path:' + attribute,
597 'draft-bierman02': attribute}
598 if return_key[RESTCONF_VERSION] in res.keys():
599 response_attribute = res[return_key[RESTCONF_VERSION]]
601 response_attribute = res['errors']['error'][0]
602 return {'status_code': response.status_code,
603 attribute: response_attribute}
607 # TransportPCE internal API RPCs
611 def prepend_dict_keys(input_dict: dict, prefix: str):
613 for key, value in input_dict.items():
614 newkey = prefix + key
615 if isinstance(value, dict):
616 return_dict[newkey] = prepend_dict_keys(value, prefix)
617 # TODO: perhaps some recursion depth limit or another solution has to be considered here
618 # even if recursion depth is given by the input_dict argument
619 # direct (self-)recursive functions may carry unwanted side-effects such as ressource consumptions
621 return_dict[newkey] = value
625 def transportpce_api_rpc_request(api_module: str, rpc: str, payload: dict):
626 # pylint: disable=consider-using-f-string
627 url = "{}/operations/{}:{}".format('{}', api_module, rpc)
630 elif RESTCONF_VERSION == 'draft-bierman02':
631 data = prepend_dict_keys({'input': payload}, api_module + ':')
633 data = {'input': payload}
634 response = post_request(url, data)
635 if response.status_code == requests.codes.no_content:
638 res = response.json()
639 return_key = {'rfc8040': api_module + ':output',
640 'draft-bierman02': 'output'}
641 if response.status_code == requests.codes.internal_server_error:
644 return_output = res[return_key[RESTCONF_VERSION]]
645 return {'status_code': response.status_code,
646 'output': return_output}