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