Bump upstream dependencies to Ca
[transportpce.git] / tests / transportpce_tests / common / test_utils.py
1 #!/usr/bin/env python
2
3 ##############################################################################
4 # Copyright (c) 2021 Orange, Inc. and others.  All rights reserved.
5 #
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 ##############################################################################
11
12 # pylint: disable=no-member
13
14 import json
15 import os
16 # pylint: disable=wrong-import-order
17 import sys
18 import re
19 import signal
20 import subprocess
21 import time
22
23 import psutil
24 import requests
25 import urllib.parse
26
27 from dict2xml import dict2xml
28 from netconf_client.connect import connect_ssh
29 from netconf_client.ncclient import Manager
30
31
32 # pylint: disable=import-error
33 import simulators
34
35 SIMS = simulators.SIMS
36
37 HONEYNODE_OK_START_MSG = 'Netconf SSH endpoint started successfully at 0.0.0.0'
38 LIGHTYNODE_OK_START_MSG = 'Data tree change listeners registered'
39 KARAF_OK_START_MSG = "Transportpce controller started"
40 LIGHTY_OK_START_MSG = re.escape("lighty.io and RESTCONF-NETCONF started")
41
42 ODL_LOGIN = 'admin'
43 ODL_PWD = 'admin'
44 NODES_LOGIN = 'admin'
45 NODES_PWD = 'admin'
46
47 TYPE_APPLICATION_JSON = {'Content-Type': 'application/json', 'Accept': 'application/json'}
48 TYPE_APPLICATION_XML = {'Content-Type': 'application/xml', 'Accept': 'application/xml'}
49
50 REQUEST_TIMEOUT = 10
51
52 CODE_SHOULD_BE_200 = 'Http status code should be 200'
53 CODE_SHOULD_BE_201 = 'Http status code should be 201'
54 T100GE = 'Transponder 100GE'
55 T0_MULTILAYER_TOPO = 'T0 - Multi-layer topology'
56 T0_FULL_MULTILAYER_TOPO = 'T0 - Full Multi-layer topology'
57 T100GE_UUID = 'cf51c729-3699-308a-a7d0-594c6a62ebbb'
58 T0_MULTILAYER_TOPO_UUID = '747c670e-7a07-3dab-b379-5b1cd17402a3'
59 T0_FULL_MULTILAYER_TOPO_UUID = '393f09a4-0a0b-3d82-a4f6-1fbbc14ca1a7'
60
61 SIM_LOG_DIRECTORY = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'log')
62
63 process_list = []
64
65 if 'USE_ODL_ALT_RESTCONF_PORT' in os.environ:
66     RESTCONF_PORT = os.environ['USE_ODL_ALT_RESTCONF_PORT']
67 else:
68     RESTCONF_PORT = 8181
69
70 RESTCONF_PATH_PREFIX = {'rfc8040': '/rests',
71                         'draft-bierman02': '/restconf'}
72
73 if 'USE_ODL_RESTCONF_VERSION' in os.environ:
74     RESTCONF_VERSION = os.environ['USE_ODL_RESTCONF_VERSION']
75     if RESTCONF_VERSION not in RESTCONF_PATH_PREFIX:
76         print('unsupported RESTCONF version ' + RESTCONF_VERSION)
77         sys.exit(3)
78 else:
79     RESTCONF_VERSION = 'rfc8040'
80
81 RESTCONF_BASE_URL = 'http://localhost:' + str(RESTCONF_PORT) + RESTCONF_PATH_PREFIX[RESTCONF_VERSION]
82
83 if 'USE_ODL_ALT_KARAF_INSTALL_DIR' in os.environ:
84     KARAF_INSTALLDIR = os.environ['USE_ODL_ALT_KARAF_INSTALL_DIR']
85 else:
86     KARAF_INSTALLDIR = 'karaf'
87
88 KARAF_LOG = os.path.join(
89     os.path.dirname(os.path.realpath(__file__)),
90     '..', '..', '..', KARAF_INSTALLDIR, 'target', 'assembly', 'data', 'log', 'karaf.log')
91
92 if 'USE_LIGHTY' in os.environ and os.environ['USE_LIGHTY'] == 'True':
93     TPCE_LOG = 'odl-' + str(os.getpid()) + '.log'
94 else:
95     TPCE_LOG = KARAF_LOG
96
97 if 'USE_SIMS' not in os.environ:
98     SIMS_TO_USE = 'lightynode'
99     SIMS_TYPE = 'lightynode'
100 else:
101     SIMS_TO_USE = os.environ['USE_SIMS']
102     print("Forcing to use SIMS " + SIMS_TO_USE)
103     if SIMS_TO_USE != 'None' or 'SIMS_TYPE' not in os.environ:
104         SIMS_TYPE = SIMS_TO_USE
105     else:
106         SIMS_TYPE = os.environ['SIMS_TYPE']
107         print("Forcing to use SIMS type" + SIMS_TYPE)
108
109
110 #
111 # Basic HTTP operations
112 #
113
114
115 def get_request(url):
116     return requests.request(
117         'GET', url.format(RESTCONF_BASE_URL),
118         headers=TYPE_APPLICATION_JSON,
119         auth=(ODL_LOGIN, ODL_PWD),
120         timeout=REQUEST_TIMEOUT)
121
122
123 def put_request(url, data):
124     return requests.request(
125         'PUT', url.format(RESTCONF_BASE_URL),
126         data=json.dumps(data),
127         headers=TYPE_APPLICATION_JSON,
128         auth=(ODL_LOGIN, ODL_PWD),
129         timeout=REQUEST_TIMEOUT)
130
131
132 def delete_request(url):
133     return requests.request(
134         'DELETE', url.format(RESTCONF_BASE_URL),
135         headers=TYPE_APPLICATION_JSON,
136         auth=(ODL_LOGIN, ODL_PWD),
137         timeout=REQUEST_TIMEOUT)
138
139
140 def post_request(url, data):
141     if data:
142         return requests.request(
143             "POST", url.format(RESTCONF_BASE_URL),
144             data=json.dumps(data),
145             headers=TYPE_APPLICATION_JSON,
146             auth=(ODL_LOGIN, ODL_PWD),
147             timeout=REQUEST_TIMEOUT)
148     return requests.request(
149         "POST", url.format(RESTCONF_BASE_URL),
150         headers=TYPE_APPLICATION_JSON,
151         auth=(ODL_LOGIN, ODL_PWD),
152         timeout=REQUEST_TIMEOUT)
153
154 #
155 # Process management
156 #
157
158
159 def start_honeynode(log_file: str, sim):
160     executable = os.path.join(os.path.dirname(os.path.realpath(__file__)),
161                               '..', '..', 'honeynode', sim[1], 'honeynode-simulator', 'honeycomb-tpce')
162     sample_directory = os.path.join(os.path.dirname(os.path.realpath(__file__)),
163                                     '..', '..', 'sample_configs', 'openroadm', sim[1])
164     if os.path.isfile(executable):
165         with open(log_file, 'w', encoding='utf-8') as outfile:
166             return subprocess.Popen(
167                 [executable, SIMS[sim]['port'], os.path.join(sample_directory, SIMS[sim]['configfile'])],
168                 stdout=outfile, stderr=outfile)
169     return None
170
171
172 def start_lightynode(log_file: str, sim):
173     executable = os.path.join(os.path.dirname(os.path.realpath(__file__)),
174                               '..', '..', 'lightynode', 'lightynode-openroadm-device', 'start-device.sh')
175     sample_directory = os.path.join(os.path.dirname(os.path.realpath(__file__)),
176                                     '..', '..', 'sample_configs', 'openroadm', sim[1])
177     if os.path.isfile(executable):
178         with open(log_file, 'w', encoding='utf-8') as outfile:
179             return subprocess.Popen(
180                 [executable, "-v" + sim[1], "-p" + SIMS[sim]['port'], "-f" + os.path.join(sample_directory,
181                                                                                           SIMS[sim]['configfile'])],
182                 stdout=outfile, stderr=outfile)
183     return None
184
185
186 def start_sims(sims_list):
187     if SIMS_TO_USE == 'None':
188         return None
189     if SIMS_TO_USE == 'honeynode':
190         start_msg = HONEYNODE_OK_START_MSG
191         start_method = start_honeynode
192     else:
193         start_msg = LIGHTYNODE_OK_START_MSG
194         start_method = start_lightynode
195     for sim in sims_list:
196         print('starting simulator ' + sim[0] + ' in OpenROADM device version ' + sim[1] + '...')
197         log_file = os.path.join(SIM_LOG_DIRECTORY, SIMS[sim]['logfile'])
198         process = start_method(log_file, sim)
199         if wait_until_log_contains(log_file, start_msg, 100):
200             print('simulator for ' + sim[0] + ' started')
201         else:
202             print('simulator for ' + sim[0] + ' failed to start')
203             shutdown_process(process)
204             for pid in process_list:
205                 shutdown_process(pid)
206             sys.exit(3)
207         process_list.append(process)
208     return process_list
209
210
211 def start_tpce():
212     if 'NO_ODL_STARTUP' in os.environ:
213         print('No OpenDaylight instance to start!')
214         return []
215     print('starting OpenDaylight...')
216     if 'USE_LIGHTY' in os.environ and os.environ['USE_LIGHTY'] == 'True':
217         process = start_lighty()
218         start_msg = LIGHTY_OK_START_MSG
219     else:
220         process = start_karaf()
221         start_msg = KARAF_OK_START_MSG
222     if wait_until_log_contains(TPCE_LOG, start_msg, time_to_wait=100):
223         print('OpenDaylight started !')
224     else:
225         print('OpenDaylight failed to start !')
226         shutdown_process(process)
227         for pid in process_list:
228             shutdown_process(pid)
229         sys.exit(1)
230     process_list.append(process)
231     return process_list
232
233
234 def start_karaf():
235     print('starting KARAF TransportPCE build...')
236     executable = os.path.join(
237         os.path.dirname(os.path.realpath(__file__)),
238         '..', '..', '..', KARAF_INSTALLDIR, 'target', 'assembly', 'bin', 'karaf')
239     with open('odl.log', 'w', encoding='utf-8') as outfile:
240         return subprocess.Popen(
241             ['sh', executable, 'server'], stdout=outfile, stderr=outfile, stdin=None)
242
243
244 def start_lighty():
245     print('starting LIGHTY.IO TransportPCE build...')
246     executable = os.path.join(
247         os.path.dirname(os.path.realpath(__file__)),
248         '..', '..', '..', 'lighty', 'target', 'tpce',
249         'clean-start-controller.sh')
250     with open(TPCE_LOG, 'w', encoding='utf-8') as outfile:
251         return subprocess.Popen(
252             ['sh', executable], stdout=outfile, stderr=outfile, stdin=None)
253
254
255 def install_karaf_feature(feature_name: str):
256     print('installing feature ' + feature_name)
257     executable = os.path.join(
258         os.path.dirname(os.path.realpath(__file__)),
259         '..', '..', '..', KARAF_INSTALLDIR, 'target', 'assembly', 'bin', 'client')
260 # FIXME: https://jira.opendaylight.org/browse/TRNSPRTPCE-701
261 # -b option needed below because of Karaf client bug reporte in the JIRA ticket mentioned above
262     return subprocess.run([executable, '-b'],
263                           input='feature:install ' + feature_name + '\n feature:list | grep '
264                           + feature_name + ' \n logout \n',
265                           universal_newlines=True, check=False)
266
267
268 def shutdown_process(process):
269     if process is not None:
270         for child in psutil.Process(process.pid).children():
271             child.send_signal(signal.SIGINT)
272             child.wait()
273         process.send_signal(signal.SIGINT)
274
275
276 def wait_until_log_contains(log_file, regexp, time_to_wait=60):
277     # pylint: disable=lost-exception
278     # pylint: disable=consider-using-with
279     stringfound = False
280     line = None
281     try:
282         with TimeOut(seconds=time_to_wait):
283             while not os.path.exists(log_file):
284                 time.sleep(0.2)
285             with open(log_file, 'r', encoding='utf-8') as filelogs:
286                 filelogs.seek(0, 2)
287                 print("Searching for pattern '" + regexp + "' in " + os.path.basename(log_file), end='... ', flush=True)
288                 compiled_regexp = re.compile(regexp)
289                 while True:
290                     line = filelogs.readline()
291                     if compiled_regexp.search(line):
292                         print('Pattern found!', end=' ')
293                         stringfound = True
294                         break
295                     if not line:
296                         time.sleep(0.1)
297         return stringfound
298     except TimeoutError:
299         print('Pattern not found after ' + str(time_to_wait), end=' seconds! ', flush=True)
300         return stringfound
301     except PermissionError:
302         print('Permission Error when trying to access the log file', end=' ... ', flush=True)
303         return stringfound
304
305
306 class TimeOut:
307     def __init__(self, seconds=1, error_message='Timeout'):
308         self.seconds = seconds
309         self.error_message = error_message
310
311     def handle_timeout(self, signum, frame):
312         raise TimeoutError(self.error_message)
313
314     def __enter__(self):
315         signal.signal(signal.SIGALRM, self.handle_timeout)
316         signal.alarm(self.seconds)
317
318     def __exit__(self, type, value, traceback):
319         # pylint: disable=W0622
320         signal.alarm(0)
321
322 #
323 # Basic NetCONF device operations
324 #
325
326
327 def mount_device(node: str, sim: str):
328     url = {'rfc8040': '{}/data/network-topology:network-topology/topology=topology-netconf/node={}',
329            'draft-bierman02': '{}/config/network-topology:network-topology/topology/topology-netconf/node/{}'}
330     body = {'node': [{
331             "node-id": node,
332             "netconf-node-topology:host": "127.0.0.1",
333             "netconf-node-topology:port": SIMS[sim]['port'],
334             "netconf-node-topology:login-password-unencrypted": {
335                 "netconf-node-topology:username": NODES_LOGIN,
336                 "netconf-node-topology:password": NODES_PWD
337             },
338             "netconf-node-topology:tcp-only": "false",
339             "netconf-node-topology:reconnect-on-changed-schema": "false",
340             "netconf-node-topology:connection-timeout-millis": "20000",
341             "netconf-node-topology:default-request-timeout-millis": "60000",
342             "netconf-node-topology:max-connection-attempts": "0",
343             "netconf-node-topology:keepalive-delay": "120"}]}
344     response = put_request(url[RESTCONF_VERSION].format('{}', node), body)
345     if wait_until_log_contains(TPCE_LOG, 'Triggering notification stream NETCONF for node ' + node, 180):
346         print('Node ' + node + ' correctly added to tpce topology', end='... ', flush=True)
347     else:
348         print('Node ' + node + ' still not added to tpce topology', end='... ', flush=True)
349         if response.status_code == requests.codes.ok:
350             print('It was probably loaded at start-up', end='... ', flush=True)
351         # TODO an else-clause to abort test would probably be nice here
352     return response
353
354
355 def unmount_device(node: str):
356     url = {'rfc8040': '{}/data/network-topology:network-topology/topology=topology-netconf/node={}',
357            'draft-bierman02': '{}/config/network-topology:network-topology/topology/topology-netconf/node/{}'}
358     response = delete_request(url[RESTCONF_VERSION].format('{}', node))
359     if wait_until_log_contains(TPCE_LOG, re.escape("onDeviceDisConnected: " + node), 180):
360         print('Node ' + node + ' correctly deleted from tpce topology', end='... ', flush=True)
361     else:
362         print('Node ' + node + ' still not deleted from tpce topology', end='... ', flush=True)
363     return response
364
365
366 def check_device_connection(node: str):
367     url = {'rfc8040': '{}/data/network-topology:network-topology/topology=topology-netconf/node={}?content=nonconfig',
368            'draft-bierman02': '{}/operational/network-topology:network-topology/topology/topology-netconf/node/{}'}
369     response = get_request(url[RESTCONF_VERSION].format('{}', node))
370     res = response.json()
371     return_key = {'rfc8040': 'network-topology:node',
372                   'draft-bierman02': 'node'}
373     if return_key[RESTCONF_VERSION] in res.keys():
374         connection_status = res[return_key[RESTCONF_VERSION]][0]['netconf-node-topology:connection-status']
375     else:
376         connection_status = res['errors']['error'][0]
377     return {'status_code': response.status_code,
378             'connection-status': connection_status}
379
380
381 def check_node_request(node: 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?content=config',  # 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 = get_request(url[RESTCONF_VERSION].format('{}', node))
386     res = response.json()
387     return_key = {'rfc8040': 'org-openroadm-device:org-openroadm-device',
388                   'draft-bierman02': 'org-openroadm-device'}
389     if return_key[RESTCONF_VERSION] in res.keys():
390         response_attribute = res[return_key[RESTCONF_VERSION]]
391     else:
392         response_attribute = res['errors']['error'][0]
393     return {'status_code': response.status_code,
394             'org-openroadm-device': response_attribute}
395
396
397 def check_node_attribute_request(node: str, attribute: str, attribute_value: str):
398     # pylint: disable=line-too-long
399     url = {'rfc8040': '{}/data/network-topology:network-topology/topology=topology-netconf/node={}/yang-ext:mount/org-openroadm-device:org-openroadm-device/{}={}?content=nonconfig',  # nopep8
400            'draft-bierman02': '{}/operational/network-topology:network-topology/topology/topology-netconf/node/{}/yang-ext:mount/org-openroadm-device:org-openroadm-device/{}/{}'}  # nopep8
401     response = get_request(url[RESTCONF_VERSION].format('{}', node, attribute, attribute_value))
402     res = response.json()
403     return_key = {'rfc8040': 'org-openroadm-device:' + attribute,
404                   'draft-bierman02': attribute}
405     if return_key[RESTCONF_VERSION] in res.keys():
406         response_attribute = res[return_key[RESTCONF_VERSION]]
407     elif 'errors' in res.keys():
408         response_attribute = res['errors']['error'][0]
409     else:
410         # status code 400 invalid request
411         response_attribute = res['message'] + ' ' + res['url']
412         print(response_attribute)
413     return {'status_code': response.status_code,
414             attribute: response_attribute}
415
416
417 def check_node_attribute2_request(node: str, attribute: str, attribute_value: str, attribute2: str):
418     # pylint: disable=line-too-long
419     url = {'rfc8040': '{}/data/network-topology:network-topology/topology=topology-netconf/node={}/yang-ext:mount/org-openroadm-device:org-openroadm-device/{}={}/{}?content=config',  # nopep8
420            'draft-bierman02': '{}/config/network-topology:network-topology/topology/topology-netconf/node/{}/yang-ext:mount/org-openroadm-device:org-openroadm-device/{}/{}/{}'}  # nopep8
421     response = get_request(url[RESTCONF_VERSION].format('{}', node, attribute, attribute_value, attribute2))
422     res = response.json()
423     if attribute2 in res.keys():
424         response_attribute = res[attribute2]
425     else:
426         response_attribute = res['errors']['error'][0]
427     return {'status_code': response.status_code,
428             attribute2: response_attribute}
429
430
431 def del_node_attribute_request(node: str, attribute: str, attribute_value: str):
432     # pylint: disable=line-too-long
433     url = {'rfc8040': '{}/data/network-topology:network-topology/topology=topology-netconf/node={}/yang-ext:mount/org-openroadm-device:org-openroadm-device/{}={}',  # nopep8
434            'draft-bierman02': '{}/config/network-topology:network-topology/topology/topology-netconf/node/{}/yang-ext:mount/org-openroadm-device:org-openroadm-device/{}/{}'}  # nopep8
435     response = delete_request(url[RESTCONF_VERSION].format('{}', node, attribute, attribute_value))
436     return response
437
438 #
439 # Portmapping operations
440 #
441
442
443 def post_portmapping(payload: str):
444     url = {'rfc8040': '{}/data/transportpce-portmapping:network',
445            'draft-bierman02': '{}/config/transportpce-portmapping:network'}
446     json_payload = json.loads(payload)
447     response = post_request(url[RESTCONF_VERSION].format('{}'), json_payload)
448     return {'status_code': response.status_code}
449
450
451 def del_portmapping():
452     url = {'rfc8040': '{}/data/transportpce-portmapping:network',
453            'draft-bierman02': '{}/config/transportpce-portmapping:network'}
454     response = delete_request(url[RESTCONF_VERSION].format('{}'))
455     return {'status_code': response.status_code}
456
457
458 def get_portmapping_node_attr(node: str, attr: str, value: str):
459     # pylint: disable=consider-using-f-string
460     url = {'rfc8040': '{}/data/transportpce-portmapping:network/nodes={}',
461            'draft-bierman02': '{}/config/transportpce-portmapping:network/nodes/{}'}
462     target_url = url[RESTCONF_VERSION].format('{}', node)
463     if attr is not None:
464         target_url = (target_url + '/{}').format('{}', attr)
465         if value is not None:
466             suffix = {'rfc8040': '={}', 'draft-bierman02': '/{}'}
467             target_url = (target_url + suffix[RESTCONF_VERSION]).format('{}', value)
468     else:
469         attr = 'nodes'
470     response = get_request(target_url)
471     res = response.json()
472     return_key = {'rfc8040': 'transportpce-portmapping:' + attr,
473                   'draft-bierman02': attr}
474     if return_key[RESTCONF_VERSION] in res.keys():
475         return_output = res[return_key[RESTCONF_VERSION]]
476     else:
477         return_output = res['errors']['error'][0]
478     return {'status_code': response.status_code,
479             attr: return_output}
480
481 #
482 # Topology operations
483 #
484
485
486 def get_ietf_network_request(network: str, content: str):
487     url = {'rfc8040': '{}/data/ietf-network:networks/network={}?content={}',
488            'draft-bierman02': '{}/{}/ietf-network:networks/network/{}'}
489     if RESTCONF_VERSION in ('rfc8040'):
490         format_args = ('{}', network, content)
491     elif content == 'config':
492         format_args = ('{}', content, network)
493     else:
494         format_args = ('{}', 'operational', network)
495     response = get_request(url[RESTCONF_VERSION].format(*format_args))
496     if bool(response):
497         res = response.json()
498         return_key = {'rfc8040': 'ietf-network:network',
499                       'draft-bierman02': 'network'}
500         networks = res[return_key[RESTCONF_VERSION]]
501     else:
502         networks = None
503     return {'status_code': response.status_code,
504             'network': networks}
505
506
507 def put_ietf_network(network: str, payload: str):
508     url = {'rfc8040': '{}/data/ietf-network:networks/network={}',
509            'draft-bierman02': '{}/config/ietf-network:networks/network/{}'}
510     json_payload = json.loads(payload)
511     response = put_request(url[RESTCONF_VERSION].format('{}', network), json_payload)
512     return {'status_code': response.status_code}
513
514
515 def del_ietf_network(network: str):
516     url = {'rfc8040': '{}/data/ietf-network:networks/network={}',
517            'draft-bierman02': '{}/config/ietf-network:networks/network/{}'}
518     response = delete_request(url[RESTCONF_VERSION].format('{}', network))
519     return {'status_code': response.status_code}
520
521
522 def get_ietf_network_link_request(network: str, link: str, content: str):
523     url = {'rfc8040': '{}/data/ietf-network:networks/network={}/ietf-network-topology:link={}?content={}',
524            'draft-bierman02': '{}/{}/ietf-network:networks/network/{}/ietf-network-topology:link/{}'}
525     if RESTCONF_VERSION in ('rfc8040'):
526         format_args = ('{}', network, link, content)
527     elif content == 'config':
528         format_args = ('{}', content, network, link)
529     else:
530         format_args = ('{}', 'operational', network, link)
531     response = get_request(url[RESTCONF_VERSION].format(*format_args))
532     res = response.json()
533     return_key = {'rfc8040': 'ietf-network-topology:link',
534                   'draft-bierman02': 'ietf-network-topology:link'}
535     link = res[return_key[RESTCONF_VERSION]][0]
536     return {'status_code': response.status_code,
537             'link': link}
538
539
540 def del_ietf_network_link_request(network: str, link: str, content: str):
541     url = {'rfc8040': '{}/data/ietf-network:networks/network={}/ietf-network-topology:link={}?content={}',
542            'draft-bierman02': '{}/{}/ietf-network:networks/network/{}/ietf-network-topology:link/{}'}
543     if RESTCONF_VERSION in ('rfc8040'):
544         format_args = ('{}', network, link, content)
545     elif content == 'config':
546         format_args = ('{}', content, network, link)
547     else:
548         format_args = ('{}', 'operational', network, link)
549     response = delete_request(url[RESTCONF_VERSION].format(*format_args))
550     return response
551
552
553 def add_oms_attr_request(link: str, oms_attr: str):
554     url = {'rfc8040': '{}/data/ietf-network:networks/network={}/ietf-network-topology:link={}',
555            'draft-bierman02': '{}/config/ietf-network:networks/network/{}/ietf-network-topology:link/{}'}
556     url2 = url[RESTCONF_VERSION] + '/org-openroadm-network-topology:OMS-attributes/span'
557     network = 'openroadm-topology'
558     response = put_request(url2.format('{}', network, link), oms_attr)
559     return response
560
561
562 def del_oms_attr_request(link: str,):
563     url = {'rfc8040': '{}/data/ietf-network:networks/network={}/ietf-network-topology:link={}',
564            'draft-bierman02': '{}/config/ietf-network:networks/network/{}/ietf-network-topology:link/{}'}
565     url2 = url[RESTCONF_VERSION] + '/org-openroadm-network-topology:OMS-attributes/span'
566     network = 'openroadm-topology'
567     response = delete_request(url2.format('{}', network, link))
568     return response
569
570
571 def get_ietf_network_node_request(network: str, node: str, content: str):
572     url = {'rfc8040': '{}/data/ietf-network:networks/network={}/node={}?content={}',
573            'draft-bierman02': '{}/{}/ietf-network:networks/network/{}/node/{}'}
574     if RESTCONF_VERSION in ('rfc8040'):
575         format_args = ('{}', network, node, content)
576     elif content == 'config':
577         format_args = ('{}', content, network, node)
578     else:
579         format_args = ('{}', 'operational', network, node)
580     response = get_request(url[RESTCONF_VERSION].format(*format_args))
581     if bool(response):
582         res = response.json()
583         return_key = {'rfc8040': 'ietf-network:node',
584                       'draft-bierman02': 'node'}
585         node = res[return_key[RESTCONF_VERSION]][0]
586     else:
587         node = None
588     return {'status_code': response.status_code,
589             'node': node}
590
591
592 def del_ietf_network_node_request(network: str, node: str, content: str):
593     url = {'rfc8040': '{}/data/ietf-network:networks/network={}/node={}?content={}',
594            'draft-bierman02': '{}/{}/ietf-network:networks/network/{}/node/{}'}
595     if RESTCONF_VERSION in ('rfc8040'):
596         format_args = ('{}', network, node, content)
597     elif content == 'config':
598         format_args = ('{}', content, network, node)
599     else:
600         format_args = ('{}', 'operational', network, node)
601     response = delete_request(url[RESTCONF_VERSION].format(*format_args))
602     return response
603
604
605 #
606 # Service list operations
607 #
608
609
610 def get_ordm_serv_list_request():
611     url = {'rfc8040': '{}/data/org-openroadm-service:service-list?content=nonconfig',
612            'draft-bierman02': '{}/operational/org-openroadm-service:service-list/'}
613     response = get_request(url[RESTCONF_VERSION])
614     res = response.json()
615     return_key = {'rfc8040': 'org-openroadm-service:service-list',
616                   'draft-bierman02': 'service-list'}
617     if return_key[RESTCONF_VERSION] in res.keys():
618         response_attribute = res[return_key[RESTCONF_VERSION]]
619     else:
620         response_attribute = res['errors']['error'][0]
621     return {'status_code': response.status_code,
622             'service-list': response_attribute}
623
624
625 def get_ordm_serv_list_attr_request(attribute: str, value: str):
626     url = {'rfc8040': '{}/data/org-openroadm-service:service-list/{}={}?content=nonconfig',
627            'draft-bierman02': '{}/operational/org-openroadm-service:service-list/{}/{}'}
628     format_args = ('{}', attribute, value)
629     response = get_request(url[RESTCONF_VERSION].format(*format_args))
630     res = response.json()
631     return_key = {'rfc8040': 'org-openroadm-service:' + attribute,
632                   'draft-bierman02': attribute}
633     if return_key[RESTCONF_VERSION] in res.keys():
634         response_attribute = res[return_key[RESTCONF_VERSION]]
635     else:
636         response_attribute = res['errors']['error'][0]
637     return {'status_code': response.status_code,
638             attribute: response_attribute}
639
640
641 def get_serv_path_list_attr(attribute: str, value: str):
642     url = {'rfc8040': '{}/data/transportpce-service-path:service-path-list/{}={}?content=nonconfig',
643            'draft-bierman02': '{}/operational/transportpce-service-path:service-path-list/{}/{}'}
644     response = get_request(url[RESTCONF_VERSION].format('{}', attribute, value))
645     res = response.json()
646     return_key = {'rfc8040': 'transportpce-service-path:' + attribute,
647                   'draft-bierman02': attribute}
648     if return_key[RESTCONF_VERSION] in res.keys():
649         response_attribute = res[return_key[RESTCONF_VERSION]]
650     else:
651         response_attribute = res['errors']['error'][0]
652     return {'status_code': response.status_code,
653             attribute: response_attribute}
654
655
656 #
657 # TransportPCE internal API RPCs
658 #
659
660
661 def prepend_dict_keys(input_dict: dict, prefix: str):
662     return_dict = {}
663     for key, value in input_dict.items():
664         newkey = prefix + key
665         if isinstance(value, dict):
666             return_dict[newkey] = prepend_dict_keys(value, prefix)
667             # TODO: perhaps some recursion depth limit or another solution has to be considered here
668             # even if recursion depth is given by the input_dict argument
669             # direct (self-)recursive functions may carry unwanted side-effects such as ressource consumptions
670         else:
671             return_dict[newkey] = value
672     return return_dict
673
674
675 def transportpce_api_rpc_request(api_module: str, rpc: str, payload: dict):
676     # pylint: disable=consider-using-f-string
677     url = "{}/operations/{}:{}".format('{}', api_module, rpc)
678     if payload is None:
679         data = None
680     elif RESTCONF_VERSION == 'draft-bierman02':
681         data = prepend_dict_keys({'input': payload}, api_module + ':')
682     else:
683         data = {'input': payload}
684     response = post_request(url, data)
685     if response.status_code == requests.codes.no_content:
686         return_output = None
687     else:
688         res = response.json()
689         return_key = {'rfc8040': api_module + ':output',
690                       'draft-bierman02': 'output'}
691         if response.status_code == requests.codes.internal_server_error:
692             return_output = res
693         else:
694             return_output = res[return_key[RESTCONF_VERSION]]
695     return {'status_code': response.status_code,
696             'output': return_output}
697
698 #
699 # simulators datastore operations
700 #
701
702
703 def sims_update_cp_port(sim: tuple, circuitpack: str, port: str, payload: dict):
704     if SIMS_TYPE == 'lightynode':
705         return sims_update_cp_port_ntcf(sim, circuitpack, payload)
706     if SIMS_TYPE == 'honeynode':
707         return sims_update_cp_port_rest(sim, circuitpack, port, payload)
708     return False
709
710
711 def sims_update_cp_port_rest(sim: tuple, circuitpack: str, port: str, payload: dict):
712     # pylint: disable=consider-using-f-string
713     url = "{}/config/org-openroadm-device:org-openroadm-device/circuit-packs/{}/ports/{}".format(
714         SIMS[sim]['restconf_baseurl'],
715         urllib.parse.quote(circuitpack, safe=''),
716         urllib.parse.quote(port, safe=''))
717     body = {"ports": [payload]}
718     response = requests.request("PUT",
719                                 url,
720                                 data=json.dumps(body),
721                                 headers=TYPE_APPLICATION_JSON,
722                                 auth=(ODL_LOGIN, ODL_PWD),
723                                 timeout=REQUEST_TIMEOUT)
724     return response.status_code == requests.codes.ok
725
726
727 def sims_update_cp_port_ntcf(sim: tuple, circuitpack: str, payload: dict):
728     body = {"circuit-packs": {"circuit-pack-name": circuitpack, "ports": payload}}
729     xml_body = '<config><org-openroadm-device xmlns="http://org/openroadm/device">'
730     xml_body += dict2xml(body, indent="  ")
731     xml_body += '</org-openroadm-device></config>'
732     with connect_ssh(host='127.0.0.1',
733                      port=int(SIMS[sim]['port']),
734                      username=NODES_LOGIN,
735                      password=NODES_PWD) as session:
736         mgr = Manager(session, timeout=120)
737         reply = mgr.edit_config(xml_body, target="candidate", default_operation="merge")
738     if "None" in str(reply):
739         return True
740     return False
741
742
743 def sims_update_pm_interact(sim: tuple, payload: dict):
744     if SIMS_TYPE == 'lightynode':
745         return sims_update_pm_interact_ntcf(sim, payload)
746     if SIMS_TYPE == 'honeynode':
747         return sims_update_pm_interact_rest(sim, payload)
748     return False
749
750
751 def sims_update_pm_interact_rest(sim: tuple, payload: dict):
752     # pylint: disable=consider-using-f-string
753     url = "{}/operations/pm-handling:pm-interact".format(SIMS[sim]['restconf_baseurl'])
754     body = {"input": payload}
755     response = requests.request("POST",
756                                 url,
757                                 data=json.dumps(body),
758                                 headers=TYPE_APPLICATION_JSON,
759                                 auth=(ODL_LOGIN, ODL_PWD),
760                                 timeout=REQUEST_TIMEOUT)
761     return response.status_code == requests.codes.ok
762
763
764 def sims_update_pm_interact_ntcf(sim: tuple, payload: dict):
765     # pylint: disable=line-too-long
766     xml_body = '<pm-interact xmlns="http://honeynode-simulator/pm-handling">'
767     xml_body += dict2xml(payload, indent="  ")
768     xml_body += '</pm-interact>'
769     new_xml = xml_body.replace("<pm-resource-instance>/org-openroadm-device:org-openroadm-device/org-openroadm-device:interface[org-openroadm-device:name='OTS-DEG2-TTP-TXRX']</pm-resource-instance>",
770                                "<pm-resource-instance xmlns:a=\"http://org/openroadm/device\">/a:org-openroadm-device/a:interface[a:name='OTS-DEG2-TTP-TXRX']</pm-resource-instance>")
771     with connect_ssh(host='127.0.0.1',
772                      port=int(SIMS[sim]['port']),
773                      username=NODES_LOGIN,
774                      password=NODES_PWD) as session:
775         mgr = Manager(session, timeout=120)
776         reply = mgr.dispatch(new_xml)
777     if "netconf_client.ncclient.RPCReply" in str(reply):
778         return True
779     return False