fix some pylint issues
[transportpce.git] / tests / transportpce_tests / common / test_utils.py
1 #!/usr/bin/env python
2
3 ##############################################################################
4 # Copyright (c) 2020 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 import sys
17 import re
18 import signal
19 import subprocess
20 import time
21
22 import psutil
23 import requests
24
25 import simulators
26
27 SIMS = simulators.SIMS
28 HONEYNODE_EXECUTABLE = simulators.HONEYNODE_EXECUTABLE
29 SAMPLES_DIRECTORY = simulators.SAMPLES_DIRECTORY
30
31 HONEYNODE_OK_START_MSG = "Netconf SSH endpoint started successfully at 0.0.0.0"
32 KARAF_OK_START_MSG = re.escape(
33     "Blueprint container for bundle org.opendaylight.netconf.restconf")+".* was successfully created"
34
35
36 RESTCONF_BASE_URL = "http://localhost:8181/restconf"
37 ODL_LOGIN = "admin"
38 ODL_PWD = "admin"
39 NODES_LOGIN = "admin"
40 NODES_PWD = "admin"
41 URL_CONFIG_NETCONF_TOPO = "{}/config/network-topology:network-topology/topology/topology-netconf/"
42 URL_CONFIG_ORDM_TOPO = "{}/config/ietf-network:networks/network/openroadm-topology/"
43 URL_CONFIG_OTN_TOPO = "{}/config/ietf-network:networks/network/otn-topology/"
44 URL_CONFIG_CLLI_NET = "{}/config/ietf-network:networks/network/clli-network/"
45 URL_CONFIG_ORDM_NET = "{}/config/ietf-network:networks/network/openroadm-network/"
46 URL_PORTMAPPING = "{}/config/transportpce-portmapping:network/nodes/"
47 URL_OPER_SERV_LIST = "{}/operational/org-openroadm-service:service-list/"
48 URL_SERV_CREATE = "{}/operations/org-openroadm-service:service-create"
49 URL_SERV_DELETE = "{}/operations/org-openroadm-service:service-delete"
50 URL_SERVICE_PATH = "{}/operations/transportpce-device-renderer:service-path"
51 URL_OTN_SERVICE_PATH = "{}/operations/transportpce-device-renderer:otn-service-path"
52 URL_CREATE_OTS_OMS = "{}/operations/transportpce-device-renderer:create-ots-oms"
53 URL_PATH_COMPUTATION_REQUEST = "{}/operations/transportpce-pce:path-computation-request"
54
55 TYPE_APPLICATION_JSON = {'Content-Type': 'application/json', 'Accept': 'application/json'}
56 TYPE_APPLICATION_XML = {'Content-Type': 'application/xml', 'Accept': 'application/xml'}
57
58 CODE_SHOULD_BE_200 = 'Http status code should be 200'
59 CODE_SHOULD_BE_201 = 'Http status code should be 201'
60
61 LOG_DIRECTORY = os.path.dirname(os.path.realpath(__file__))
62
63 KARAF_LOG = os.path.join(
64     os.path.dirname(os.path.realpath(__file__)),
65     "..", "..", "..", "karaf", "target", "assembly", "data", "log", "karaf.log")
66
67 process_list = []
68
69 if "USE_LIGHTY" in os.environ and os.environ['USE_LIGHTY'] == 'True':
70     TPCE_LOG = 'odl.log'
71 else:
72     TPCE_LOG = KARAF_LOG
73
74
75 def start_sims(sims_list):
76     for sim in sims_list:
77         print("starting simulator for " + sim + "...")
78         log_file = os.path.join(LOG_DIRECTORY, SIMS[sim]['logfile'])
79         process = start_honeynode(log_file, SIMS[sim]['port'], SIMS[sim]['configfile'])
80         if wait_until_log_contains(log_file, HONEYNODE_OK_START_MSG, 100):
81             print("simulator for " + sim + " started")
82         else:
83             print("simulator for " + sim + " failed to start")
84             shutdown_process(process)
85             for pid in process_list:
86                 shutdown_process(pid)
87             sys.exit(3)
88         process_list.append(process)
89     return process_list
90
91
92 def start_tpce():
93     print("starting OpenDaylight...")
94     if "USE_LIGHTY" in os.environ and os.environ['USE_LIGHTY'] == 'True':
95         process = start_lighty()
96         # TODO: add some sort of health check similar to Karaf below
97     else:
98         process = start_karaf()
99         if wait_until_log_contains(KARAF_LOG, KARAF_OK_START_MSG, time_to_wait=60):
100             print("OpenDaylight started !")
101         else:
102             print("OpenDaylight failed to start !")
103             shutdown_process(process)
104             for pid in process_list:
105                 shutdown_process(pid)
106             sys.exit(1)
107     process_list.append(process)
108     return process_list
109
110
111 def start_karaf():
112     print("starting KARAF TransportPCE build...")
113     executable = os.path.join(
114         os.path.dirname(os.path.realpath(__file__)),
115         "..", "..", "..", "karaf", "target", "assembly", "bin", "karaf")
116     with open('odl.log', 'w') as outfile:
117         return subprocess.Popen(
118             ["sh", executable, "server"], stdout=outfile, stderr=outfile, stdin=None)
119
120
121 def start_lighty():
122     print("starting LIGHTY.IO TransportPCE build...")
123     executable = os.path.join(
124         os.path.dirname(os.path.realpath(__file__)),
125         "..", "..", "..", "lighty", "target", "tpce",
126         "clean-start-controller.sh")
127     with open('odl.log', 'w') as outfile:
128         return subprocess.Popen(
129             ["sh", executable], stdout=outfile, stderr=outfile, stdin=None)
130
131
132 def install_karaf_feature(feature_name: str):
133     print("installing feature " + feature_name)
134     executable = os.path.join(
135         os.path.dirname(os.path.realpath(__file__)),
136         "..", "..", "..", "karaf", "target", "assembly", "bin", "client")
137     return subprocess.run([executable],
138                           input='feature:install ' + feature_name + '\n feature:list | grep tapi \n logout \n',
139                           universal_newlines=True, check=False)
140
141
142 def get_request(url):
143     return requests.request(
144         "GET", url.format(RESTCONF_BASE_URL),
145         headers=TYPE_APPLICATION_JSON,
146         auth=(ODL_LOGIN, ODL_PWD))
147
148
149 def post_request(url, data):
150     if data:
151         return requests.request(
152             "POST", url.format(RESTCONF_BASE_URL),
153             data=json.dumps(data),
154             headers=TYPE_APPLICATION_JSON,
155             auth=(ODL_LOGIN, ODL_PWD))
156
157     return requests.request(
158         "POST", url.format(RESTCONF_BASE_URL),
159         headers=TYPE_APPLICATION_JSON,
160         auth=(ODL_LOGIN, ODL_PWD))
161
162
163 def post_xmlrequest(url, data):
164     if data:
165         return requests.request(
166             "POST", url.format(RESTCONF_BASE_URL),
167             data=data,
168             headers=TYPE_APPLICATION_XML,
169             auth=(ODL_LOGIN, ODL_PWD))
170     return None
171
172
173 def put_request(url, data):
174     return requests.request(
175         "PUT", url.format(RESTCONF_BASE_URL),
176         data=json.dumps(data),
177         headers=TYPE_APPLICATION_JSON,
178         auth=(ODL_LOGIN, ODL_PWD))
179
180
181 def put_xmlrequest(url, data):
182     return requests.request(
183         "PUT", url.format(RESTCONF_BASE_URL),
184         data=data,
185         headers=TYPE_APPLICATION_XML,
186         auth=(ODL_LOGIN, ODL_PWD))
187
188
189 def rawput_request(url, data):
190     return requests.request(
191         "PUT", url.format(RESTCONF_BASE_URL),
192         data=data,
193         headers=TYPE_APPLICATION_JSON,
194         auth=(ODL_LOGIN, ODL_PWD))
195
196
197 def delete_request(url):
198     return requests.request(
199         "DELETE", url.format(RESTCONF_BASE_URL),
200         headers=TYPE_APPLICATION_JSON,
201         auth=(ODL_LOGIN, ODL_PWD))
202
203
204 def mount_device(node_id, sim):
205     url = URL_CONFIG_NETCONF_TOPO+"node/"+node_id
206     body = {"node": [{
207         "node-id": node_id,
208         "netconf-node-topology:username": NODES_LOGIN,
209         "netconf-node-topology:password": NODES_PWD,
210         "netconf-node-topology:host": "127.0.0.1",
211         "netconf-node-topology:port": SIMS[sim]['port'],
212         "netconf-node-topology:tcp-only": "false",
213         "netconf-node-topology:pass-through": {}}]}
214     response = put_request(url, body)
215     if wait_until_log_contains(TPCE_LOG, re.escape("Triggering notification stream NETCONF for node "+node_id), 60):
216         print("Node "+node_id+" correctly added to tpce topology", end='... ', flush=True)
217     else:
218         print("Node "+node_id+" still not added to tpce topology", end='... ', flush=True)
219         if response.status_code == requests.codes.ok:
220             print("It was probably loaded at start-up", end='... ', flush=True)
221         # TODO an else-clause to abort test would probably be nice here
222     return response
223
224
225 def unmount_device(node_id):
226     url = URL_CONFIG_NETCONF_TOPO+"node/"+node_id
227     response = delete_request(url)
228     if wait_until_log_contains(TPCE_LOG, re.escape("onDeviceDisConnected: "+node_id), 60):
229         print("Node "+node_id+" correctly deleted from tpce topology", end='... ', flush=True)
230     else:
231         print("Node "+node_id+" still not deleted from tpce topology", end='... ', flush=True)
232     return response
233
234
235 def connect_xpdr_to_rdm_request(xpdr_node: str, xpdr_num: str, network_num: str,
236                                 rdm_node: str, srg_num: str, termination_num: str):
237     url = "{}/operations/transportpce-networkutils:init-xpdr-rdm-links"
238     data = {
239         "networkutils:input": {
240             "networkutils:links-input": {
241                 "networkutils:xpdr-node": xpdr_node,
242                 "networkutils:xpdr-num": xpdr_num,
243                 "networkutils:network-num": network_num,
244                 "networkutils:rdm-node": rdm_node,
245                 "networkutils:srg-num": srg_num,
246                 "networkutils:termination-point-num": termination_num
247             }
248         }
249     }
250     return post_request(url, data)
251
252
253 def connect_rdm_to_xpdr_request(xpdr_node: str, xpdr_num: str, network_num: str,
254                                 rdm_node: str, srg_num: str, termination_num: str):
255     url = "{}/operations/transportpce-networkutils:init-rdm-xpdr-links"
256     data = {
257         "networkutils:input": {
258             "networkutils:links-input": {
259                 "networkutils:xpdr-node": xpdr_node,
260                 "networkutils:xpdr-num": xpdr_num,
261                 "networkutils:network-num": network_num,
262                 "networkutils:rdm-node": rdm_node,
263                 "networkutils:srg-num": srg_num,
264                 "networkutils:termination-point-num": termination_num
265             }
266         }
267     }
268     return post_request(url, data)
269
270
271 def check_netconf_node_request(node: str, suffix: str):
272     url = URL_CONFIG_NETCONF_TOPO + (
273         "node/" + node + "/yang-ext:mount/org-openroadm-device:org-openroadm-device/" + suffix
274     )
275     return get_request(url)
276
277
278 def get_netconf_oper_request(node: str):
279     url = "{}/operational/network-topology:network-topology/topology/topology-netconf/node/" + node
280     return get_request(url)
281
282
283 def get_ordm_topo_request(suffix: str):
284     url = URL_CONFIG_ORDM_TOPO + suffix
285     return get_request(url)
286
287
288 def add_oms_attr_request(link: str, attr):
289     url = URL_CONFIG_ORDM_TOPO + (
290         "ietf-network-topology:link/" + link + "/org-openroadm-network-topology:OMS-attributes/span"
291     )
292     return put_request(url, attr)
293
294
295 def del_oms_attr_request(link: str):
296     url = URL_CONFIG_ORDM_TOPO + (
297         "ietf-network-topology:link/" + link + "/org-openroadm-network-topology:OMS-attributes/span"
298     )
299     return delete_request(url)
300
301
302 def get_clli_net_request():
303     return get_request(URL_CONFIG_CLLI_NET)
304
305
306 def get_ordm_net_request():
307     return get_request(URL_CONFIG_ORDM_NET)
308
309
310 def get_otn_topo_request():
311     return get_request(URL_CONFIG_OTN_TOPO)
312
313
314 def del_link_request(link: str):
315     url = URL_CONFIG_ORDM_TOPO + ("ietf-network-topology:link/" + link)
316     return delete_request(url)
317
318
319 def del_node_request(node: str):
320     url = URL_CONFIG_CLLI_NET + ("node/" + node)
321     return delete_request(url)
322
323
324 def portmapping_request(suffix: str):
325     url = URL_PORTMAPPING + suffix
326     return get_request(url)
327
328
329 def get_service_list_request(suffix: str):
330     url = URL_OPER_SERV_LIST + suffix
331     return get_request(url)
332
333
334 def service_create_request(attr):
335     return post_request(URL_SERV_CREATE, attr)
336
337
338 def service_delete_request(servicename: str,
339                            requestid="e3028bae-a90f-4ddd-a83f-cf224eba0e58",
340                            notificationurl="http://localhost:8585/NotificationServer/notify"):
341     attr = {"input": {
342         "sdnc-request-header": {
343             "request-id": requestid,
344             "rpc-action": "service-delete",
345             "request-system-id": "appname",
346             "notification-url": notificationurl},
347         "service-delete-req-info": {
348             "service-name": servicename,
349             "tail-retention": "no"}}}
350     return post_request(URL_SERV_DELETE, attr)
351
352
353 def service_path_request(operation: str, servicename: str, wavenumber: str, nodes):
354     attr = {"renderer:input": {
355         "renderer:service-name": servicename,
356         "renderer:wave-number": wavenumber,
357         "renderer:modulation-format": "qpsk",
358         "renderer:operation": operation,
359         "renderer:nodes": nodes}}
360     return post_request(URL_SERVICE_PATH, attr)
361
362
363 def otn_service_path_request(operation: str, servicename: str, servicerate: str, servicetype: str, nodes,
364                              eth_attr=None):
365     attr = {"service-name": servicename,
366             "operation": operation,
367             "service-rate": servicerate,
368             "service-type": servicetype,
369             "nodes": nodes}
370     if eth_attr:
371         attr.update(eth_attr)
372     return post_request(URL_OTN_SERVICE_PATH, {"renderer:input": attr})
373
374
375 def create_ots_oms_request(nodeid: str, lcp: str):
376     attr = {"input": {
377         "node-id": nodeid,
378         "logical-connection-point": lcp}}
379     return post_request(URL_CREATE_OTS_OMS, attr)
380
381
382 def path_computation_request(requestid: str, servicename: str, serviceaend, servicezend,
383                              hardconstraints=None, softconstraints=None, metric="hop-count", other_attr=None):
384     attr = {"service-name": servicename,
385             "resource-reserve": "true",
386             "service-handler-header": {"request-id": requestid},
387             "service-a-end": serviceaend,
388             "service-z-end": servicezend,
389             "pce-metric": metric}
390     if hardconstraints:
391         attr.update({"hard-constraints": hardconstraints})
392     if softconstraints:
393         attr.update({"soft-constraints": softconstraints})
394     if other_attr:
395         attr.update(other_attr)
396     return post_request(URL_PATH_COMPUTATION_REQUEST, {"input": attr})
397
398
399 def shutdown_process(process):
400     if process is not None:
401         for child in psutil.Process(process.pid).children():
402             child.send_signal(signal.SIGINT)
403             child.wait()
404         process.send_signal(signal.SIGINT)
405
406
407 def start_honeynode(log_file: str, node_port: str, node_config_file_name: str):
408     if os.path.isfile(HONEYNODE_EXECUTABLE):
409         with open(log_file, 'w') as outfile:
410             return subprocess.Popen(
411                 [HONEYNODE_EXECUTABLE, node_port, os.path.join(SAMPLES_DIRECTORY, node_config_file_name)],
412                 stdout=outfile, stderr=outfile)
413     return None
414
415
416 def wait_until_log_contains(log_file, regexp, time_to_wait=20):
417     # pylint: disable=lost-exception
418     stringfound = False
419     filefound = False
420     line = None
421     try:
422         with TimeOut(seconds=time_to_wait):
423             while not os.path.exists(log_file):
424                 time.sleep(0.2)
425             filelogs = open(log_file, 'r')
426             filelogs.seek(0, 2)
427             filefound = True
428             print("Searching for pattern '"+regexp+"' in "+os.path.basename(log_file), end='... ', flush=True)
429             compiled_regexp = re.compile(regexp)
430             while True:
431                 line = filelogs.readline()
432                 if compiled_regexp.search(line):
433                     print("Pattern found!", end=' ')
434                     stringfound = True
435                     break
436                 if not line:
437                     time.sleep(0.1)
438     except TimeoutError:
439         print("Pattern not found after "+str(time_to_wait), end=" seconds! ", flush=True)
440     except PermissionError:
441         print("Permission Error when trying to access the log file", end=" ... ", flush=True)
442     finally:
443         if filefound:
444             filelogs.close()
445         else:
446             print("log file does not exist or is not accessible... ", flush=True)
447         return stringfound
448
449
450 class TimeOut:
451     def __init__(self, seconds=1, error_message='Timeout'):
452         self.seconds = seconds
453         self.error_message = error_message
454
455     def handle_timeout(self, signum, frame):
456         raise TimeoutError(self.error_message)
457
458     def __enter__(self):
459         signal.signal(signal.SIGALRM, self.handle_timeout)
460         signal.alarm(self.seconds)
461
462     def __exit__(self, type, value, traceback):
463         # pylint: disable=W0622
464         signal.alarm(0)