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