Test suite for flex-grid support
[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 def service_delete_request(servicename : str,
333                            requestid = "e3028bae-a90f-4ddd-a83f-cf224eba0e58",
334                            notificationurl="http://localhost:8585/NotificationServer/notify"):
335     attr = {"input": {
336                 "sdnc-request-header": {
337                      "request-id": requestid,
338                      "rpc-action": "service-delete",
339                      "request-system-id": "appname",
340                      "notification-url": notificationurl},
341                 "service-delete-req-info": {
342                      "service-name": servicename,
343                      "tail-retention": "no"}}}
344     return post_request(URL_SERV_DELETE, attr)
345
346
347 def service_path_request(operation: str, servicename: str, wavenumber: str, nodes):
348     attr = {"renderer:input": {
349             "renderer:service-name": servicename,
350             "renderer:wave-number": wavenumber,
351             "renderer:modulation-format": "qpsk",
352             "renderer:operation": operation,
353             "renderer:nodes": nodes}}
354     return post_request(URL_SERVICE_PATH, attr)
355
356
357 def otn_service_path_request(operation: str, servicename: str, servicerate: str, servicetype: str, nodes, eth_attr=None):
358     attr = {"service-name": servicename,
359             "operation": operation,
360             "service-rate": servicerate,
361             "service-type": servicetype,
362             "nodes": nodes}
363     if eth_attr:
364         attr.update(eth_attr)
365     return post_request(URL_OTN_SERVICE_PATH, {"renderer:input": attr})
366
367
368 def create_ots_oms_request(nodeid: str, lcp: str):
369     attr = {"input": {
370             "node-id": nodeid,
371             "logical-connection-point": lcp}}
372     return post_request(URL_CREATE_OTS_OMS, attr)
373
374 def path_computation_request(requestid: str, servicename: str, serviceaend, servicezend,
375                              hardconstraints=None, softconstraints=None, metric="hop-count", other_attr=None):
376     attr =  {"service-name": servicename,
377              "resource-reserve": "true",
378              "service-handler-header": { "request-id": requestid },
379              "service-a-end": serviceaend,
380              "service-z-end": servicezend,
381              "pce-metric": metric}
382     if hardconstraints:
383         attr.update({ "hard-constraints": hardconstraints})
384     if softconstraints:
385         attr.update({ "soft-constraints": softconstraints})
386     if other_attr:
387         attr.update(other_attr)
388     return post_request(URL_PATH_COMPUTATION_REQUEST, {"input": attr })
389
390
391 def shutdown_process(process):
392     if process is not None:
393         for child in psutil.Process(process.pid).children():
394             child.send_signal(signal.SIGINT)
395             child.wait()
396         process.send_signal(signal.SIGINT)
397
398
399 def start_honeynode(log_file: str, node_port: str, node_config_file_name: str):
400     if os.path.isfile(HONEYNODE_EXECUTABLE):
401         with open(log_file, 'w') as outfile:
402             return subprocess.Popen(
403                 [HONEYNODE_EXECUTABLE, node_port, os.path.join(SAMPLES_DIRECTORY, node_config_file_name)],
404                 stdout=outfile, stderr=outfile)
405
406
407 def wait_until_log_contains(log_file, regexp, time_to_wait=20):
408     stringfound = False
409     filefound = False
410     line = None
411     try:
412         with TimeOut(seconds=time_to_wait):
413             while not os.path.exists(log_file):
414                 time.sleep(0.2)
415             filelogs = open(log_file, 'r')
416             filelogs.seek(0, 2)
417             filefound = True
418             print("Searching for pattern '"+regexp+"' in "+os.path.basename(log_file), end='... ', flush=True)
419             compiled_regexp = re.compile(regexp)
420             while True:
421                 line = filelogs.readline()
422                 if compiled_regexp.search(line):
423                     print("Pattern found!", end=' ')
424                     stringfound = True
425                     break
426                 if not line:
427                     time.sleep(0.1)
428     except TimeoutError:
429         print("Pattern not found after "+str(time_to_wait), end=" seconds! ", flush=True)
430     except PermissionError:
431         print("Permission Error when trying to access the log file", end=" ... ", flush=True)
432     finally:
433         if filefound:
434             filelogs.close()
435         else:
436             print("log file does not exist or is not accessible... ", flush=True)
437         return stringfound
438
439
440 class TimeOut:
441     def __init__(self, seconds=1, error_message='Timeout'):
442         self.seconds = seconds
443         self.error_message = error_message
444
445     def handle_timeout(self, signum, frame):
446         raise TimeoutError(self.error_message)
447
448     def __enter__(self):
449         signal.signal(signal.SIGALRM, self.handle_timeout)
450         signal.alarm(self.seconds)
451
452     def __exit__(self, type, value, traceback):
453         signal.alarm(0)