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