822aab06f8f6d8b8e7af4ee38361951221b4ce33
[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, eth_attr=None):
363     attr = {"service-name": servicename,
364             "operation": operation,
365             "service-rate": servicerate,
366             "service-type": servicetype,
367             "nodes": nodes}
368     if eth_attr:
369         attr.update(eth_attr)
370     return post_request(URL_OTN_SERVICE_PATH, {"renderer:input": attr})
371
372
373 def create_ots_oms_request(nodeid: str, lcp: str):
374     attr = {"input": {
375             "node-id": nodeid,
376             "logical-connection-point": lcp}}
377     return post_request(URL_CREATE_OTS_OMS, attr)
378
379
380 def path_computation_request(requestid: str, servicename: str, serviceaend, servicezend,
381                              hardconstraints=None, softconstraints=None, metric="hop-count", other_attr=None):
382     attr = {"service-name": servicename,
383             "resource-reserve": "true",
384             "service-handler-header": {"request-id": requestid},
385             "service-a-end": serviceaend,
386             "service-z-end": servicezend,
387             "pce-metric": metric}
388     if hardconstraints:
389         attr.update({"hard-constraints": hardconstraints})
390     if softconstraints:
391         attr.update({"soft-constraints": softconstraints})
392     if other_attr:
393         attr.update(other_attr)
394     return post_request(URL_PATH_COMPUTATION_REQUEST, {"input": attr})
395
396
397 def shutdown_process(process):
398     if process is not None:
399         for child in psutil.Process(process.pid).children():
400             child.send_signal(signal.SIGINT)
401             child.wait()
402         process.send_signal(signal.SIGINT)
403
404
405 def start_honeynode(log_file: str, node_port: str, node_config_file_name: str):
406     if os.path.isfile(HONEYNODE_EXECUTABLE):
407         with open(log_file, 'w') as outfile:
408             return subprocess.Popen(
409                 [HONEYNODE_EXECUTABLE, node_port, os.path.join(SAMPLES_DIRECTORY, node_config_file_name)],
410                 stdout=outfile, stderr=outfile)
411
412
413 def wait_until_log_contains(log_file, regexp, time_to_wait=20):
414     stringfound = False
415     filefound = False
416     line = None
417     try:
418         with TimeOut(seconds=time_to_wait):
419             while not os.path.exists(log_file):
420                 time.sleep(0.2)
421             filelogs = open(log_file, 'r')
422             filelogs.seek(0, 2)
423             filefound = True
424             print("Searching for pattern '"+regexp+"' in "+os.path.basename(log_file), end='... ', flush=True)
425             compiled_regexp = re.compile(regexp)
426             while True:
427                 line = filelogs.readline()
428                 if compiled_regexp.search(line):
429                     print("Pattern found!", end=' ')
430                     stringfound = True
431                     break
432                 if not line:
433                     time.sleep(0.1)
434     except TimeoutError:
435         print("Pattern not found after "+str(time_to_wait), end=" seconds! ", flush=True)
436     except PermissionError:
437         print("Permission Error when trying to access the log file", end=" ... ", flush=True)
438     finally:
439         if filefound:
440             filelogs.close()
441         else:
442             print("log file does not exist or is not accessible... ", flush=True)
443         return stringfound
444
445
446 class TimeOut:
447     def __init__(self, seconds=1, error_message='Timeout'):
448         self.seconds = seconds
449         self.error_message = error_message
450
451     def handle_timeout(self, signum, frame):
452         raise TimeoutError(self.error_message)
453
454     def __enter__(self):
455         signal.signal(signal.SIGALRM, self.handle_timeout)
456         signal.alarm(self.seconds)
457
458     def __exit__(self, type, value, traceback):
459         signal.alarm(0)