Merge "Add INFO.yaml for transportPCE"
[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
46 TYPE_APPLICATION_JSON = {'Content-Type': 'application/json', 'Accept': 'application/json'}
47 TYPE_APPLICATION_XML = {'Content-Type': 'application/xml', 'Accept': 'application/xml'}
48
49 CODE_SHOULD_BE_200 = 'Http status code should be 200'
50 CODE_SHOULD_BE_201 = 'Http status code should be 201'
51
52 LOG_DIRECTORY = os.path.dirname(os.path.realpath(__file__))
53
54 KARAF_LOG = os.path.join(
55     os.path.dirname(os.path.realpath(__file__)),
56     "..", "..", "..", "karaf", "target", "assembly", "data", "log", "karaf.log")
57
58 process_list = []
59
60 if "USE_LIGHTY" in os.environ and os.environ['USE_LIGHTY'] == 'True':
61     TPCE_LOG = 'odl.log'
62 else:
63     TPCE_LOG = KARAF_LOG
64
65
66 def start_sims(sims_list):
67     for sim in sims_list:
68         print("starting simulator for " + sim + "...")
69         log_file = os.path.join(LOG_DIRECTORY, SIMS[sim]['logfile'])
70         process = start_honeynode(log_file, SIMS[sim]['port'], SIMS[sim]['configfile'])
71         if wait_until_log_contains(log_file, HONEYNODE_OK_START_MSG, 100):
72             print("simulator for " + sim + " started")
73         else:
74             print("simulator for " + sim + " failed to start")
75             shutdown_process(process)
76             for pid in process_list:
77                 shutdown_process(pid)
78             sys.exit(3)
79         process_list.append(process)
80     return process_list
81
82
83 def start_tpce():
84     print("starting OpenDaylight...")
85     if "USE_LIGHTY" in os.environ and os.environ['USE_LIGHTY'] == 'True':
86         process = start_lighty()
87         # TODO: add some sort of health check similar to Karaf below
88     else:
89         process = start_karaf()
90         if wait_until_log_contains(KARAF_LOG, KARAF_OK_START_MSG, time_to_wait=60):
91             print("OpenDaylight started !")
92         else:
93             print("OpenDaylight failed to start !")
94             shutdown_process(process)
95             for pid in process_list:
96                 shutdown_process(pid)
97             sys.exit(1)
98     process_list.append(process)
99     return process_list
100
101
102 def start_karaf():
103     print("starting KARAF TransportPCE build...")
104     executable = os.path.join(
105         os.path.dirname(os.path.realpath(__file__)),
106         "..", "..", "..", "karaf", "target", "assembly", "bin", "karaf")
107     with open('odl.log', 'w') as outfile:
108         return subprocess.Popen(
109             ["sh", executable, "server"], stdout=outfile, stderr=outfile, stdin=None)
110
111
112 def start_lighty():
113     print("starting LIGHTY.IO TransportPCE build...")
114     executable = os.path.join(
115         os.path.dirname(os.path.realpath(__file__)),
116         "..", "..", "..", "lighty", "target", "tpce",
117         "clean-start-controller.sh")
118     with open('odl.log', 'w') as outfile:
119         return subprocess.Popen(
120             ["sh", executable], stdout=outfile, stderr=outfile, stdin=None)
121
122
123 def install_karaf_feature(feature_name: str):
124     print("installing feature " + feature_name)
125     executable = os.path.join(
126         os.path.dirname(os.path.realpath(__file__)),
127         "..", "..", "..", "karaf", "target", "assembly", "bin", "client")
128     return subprocess.run([executable],
129                           input='feature:install ' + feature_name + '\n feature:list | grep tapi \n logout \n',
130                           universal_newlines=True)
131
132
133 def get_request(url):
134     return requests.request(
135         "GET", url.format(RESTCONF_BASE_URL),
136         headers=TYPE_APPLICATION_JSON,
137         auth=(ODL_LOGIN, ODL_PWD))
138
139
140 def post_request(url, data):
141     if data:
142         return requests.request(
143             "POST", url.format(RESTCONF_BASE_URL),
144             data=json.dumps(data),
145             headers=TYPE_APPLICATION_JSON,
146             auth=(ODL_LOGIN, ODL_PWD))
147     else:
148         return requests.request(
149             "POST", url.format(RESTCONF_BASE_URL),
150             headers=TYPE_APPLICATION_JSON,
151             auth=(ODL_LOGIN, ODL_PWD))
152
153
154 def post_xmlrequest(url, data):
155     if data:
156         return requests.request(
157             "POST", url.format(RESTCONF_BASE_URL),
158             data=data,
159             headers=TYPE_APPLICATION_XML,
160             auth=(ODL_LOGIN, ODL_PWD))
161
162
163 def put_request(url, data):
164     return requests.request(
165         "PUT", url.format(RESTCONF_BASE_URL),
166         data=json.dumps(data),
167         headers=TYPE_APPLICATION_JSON,
168         auth=(ODL_LOGIN, ODL_PWD))
169
170
171 def put_xmlrequest(url, data):
172     return requests.request(
173         "PUT", url.format(RESTCONF_BASE_URL),
174         data=data,
175         headers=TYPE_APPLICATION_XML,
176         auth=(ODL_LOGIN, ODL_PWD))
177
178
179 def rawput_request(url, data):
180     return requests.request(
181         "PUT", url.format(RESTCONF_BASE_URL),
182         data=data,
183         headers=TYPE_APPLICATION_JSON,
184         auth=(ODL_LOGIN, ODL_PWD))
185
186
187 def delete_request(url):
188     return requests.request(
189         "DELETE", url.format(RESTCONF_BASE_URL),
190         headers=TYPE_APPLICATION_JSON,
191         auth=(ODL_LOGIN, ODL_PWD))
192
193
194 def mount_device(node_id, sim):
195     url = URL_CONFIG_NETCONF_TOPO+"node/"+node_id
196     body = {"node": [{
197         "node-id": node_id,
198         "netconf-node-topology:username": NODES_LOGIN,
199         "netconf-node-topology:password": NODES_PWD,
200         "netconf-node-topology:host": "127.0.0.1",
201         "netconf-node-topology:port": SIMS[sim]['port'],
202         "netconf-node-topology:tcp-only": "false",
203         "netconf-node-topology:pass-through": {}}]}
204     response = put_request(url, body)
205     if wait_until_log_contains(TPCE_LOG, re.escape("Triggering notification stream NETCONF for node "+node_id), 60):
206         print("Node "+node_id+" correctly added to tpce topology", end='... ', flush=True)
207     else:
208         print("Node "+node_id+" still not added to tpce topology", end='... ', flush=True)
209         if response.status_code == requests.codes.ok:
210             print("It was probably loaded at start-up", end='... ', flush=True)
211         # TODO an else-clause to abort test would probably be nice here
212     return response
213
214
215 def unmount_device(node_id):
216     url = URL_CONFIG_NETCONF_TOPO+"node/"+node_id
217     response = delete_request(url)
218     if wait_until_log_contains(TPCE_LOG, re.escape("onDeviceDisConnected: "+node_id), 60):
219         print("Node "+node_id+" correctly deleted from tpce topology", end='... ', flush=True)
220     else:
221         print("Node "+node_id+" still not deleted from tpce topology", end='... ', flush=True)
222     return response
223
224
225 def connect_xpdr_to_rdm_request(xpdr_node: str, xpdr_num: str, network_num: str,
226                                 rdm_node: str, srg_num: str, termination_num: str):
227     url = "{}/operations/transportpce-networkutils:init-xpdr-rdm-links"
228     data = {
229         "networkutils:input": {
230             "networkutils:links-input": {
231                 "networkutils:xpdr-node": xpdr_node,
232                 "networkutils:xpdr-num": xpdr_num,
233                 "networkutils:network-num": network_num,
234                 "networkutils:rdm-node": rdm_node,
235                 "networkutils:srg-num": srg_num,
236                 "networkutils:termination-point-num": termination_num
237             }
238         }
239     }
240     return post_request(url, data)
241
242
243 def connect_rdm_to_xpdr_request(xpdr_node: str, xpdr_num: str, network_num: str,
244                                 rdm_node: str, srg_num: str, termination_num: str):
245     url = "{}/operations/transportpce-networkutils:init-rdm-xpdr-links"
246     data = {
247         "networkutils:input": {
248             "networkutils:links-input": {
249                 "networkutils:xpdr-node": xpdr_node,
250                 "networkutils:xpdr-num": xpdr_num,
251                 "networkutils:network-num": network_num,
252                 "networkutils:rdm-node": rdm_node,
253                 "networkutils:srg-num": srg_num,
254                 "networkutils:termination-point-num": termination_num
255             }
256         }
257     }
258     return post_request(url, data)
259
260
261 def check_netconf_node_request(node: str, suffix: str):
262     url = URL_CONFIG_NETCONF_TOPO + (
263         "node/" + node + "/yang-ext:mount/org-openroadm-device:org-openroadm-device/" + suffix
264     )
265     return get_request(url)
266
267
268 def get_netconf_oper_request(node: str):
269     url = "{}/operational/network-topology:network-topology/topology/topology-netconf/node/" + node
270     return get_request(url)
271
272
273 def get_ordm_topo_request(suffix: str):
274     url = URL_CONFIG_ORDM_TOPO + suffix
275     return get_request(url)
276
277
278 def add_oms_attr_request(link: str, attr):
279     url = URL_CONFIG_ORDM_TOPO + (
280         "ietf-network-topology:link/" + link + "/org-openroadm-network-topology:OMS-attributes/span"
281     )
282     return put_request(url, attr)
283
284
285 def del_oms_attr_request(link: str):
286     url = URL_CONFIG_ORDM_TOPO + (
287         "ietf-network-topology:link/" + link + "/org-openroadm-network-topology:OMS-attributes/span"
288     )
289     return delete_request(url)
290
291
292 def get_clli_net_request():
293     return get_request(URL_CONFIG_CLLI_NET)
294
295
296 def get_ordm_net_request():
297     return get_request(URL_CONFIG_ORDM_NET)
298
299
300 def get_otn_topo_request():
301     return get_request(URL_CONFIG_OTN_TOPO)
302
303
304 def del_link_request(link: str):
305     url = URL_CONFIG_ORDM_TOPO + ("ietf-network-topology:link/" + link)
306     return delete_request(url)
307
308
309 def del_node_request(node: str):
310     url = URL_CONFIG_CLLI_NET + ("node/" + node)
311     return delete_request(url)
312
313
314 def portmapping_request(suffix: str):
315     url = URL_PORTMAPPING + suffix
316     return get_request(url)
317
318 def get_service_list_request(suffix: str):
319     url = URL_OPER_SERV_LIST + suffix
320     return get_request(url)
321
322 def service_create_request(attr):
323     return post_request(URL_SERV_CREATE, attr)
324
325
326 def shutdown_process(process):
327     if process is not None:
328         for child in psutil.Process(process.pid).children():
329             child.send_signal(signal.SIGINT)
330             child.wait()
331         process.send_signal(signal.SIGINT)
332
333
334 def start_honeynode(log_file: str, node_port: str, node_config_file_name: str):
335     if os.path.isfile(HONEYNODE_EXECUTABLE):
336         with open(log_file, 'w') as outfile:
337             return subprocess.Popen(
338                 [HONEYNODE_EXECUTABLE, node_port, os.path.join(SAMPLES_DIRECTORY, node_config_file_name)],
339                 stdout=outfile, stderr=outfile)
340
341
342 def wait_until_log_contains(log_file, regexp, time_to_wait=20):
343     stringfound = False
344     filefound = False
345     line = None
346     try:
347         with TimeOut(seconds=time_to_wait):
348             while not os.path.exists(log_file):
349                 time.sleep(0.2)
350             filelogs = open(log_file, 'r')
351             filelogs.seek(0, 2)
352             filefound = True
353             print("Searching for pattern '"+regexp+"' in "+os.path.basename(log_file), end='... ', flush=True)
354             compiled_regexp = re.compile(regexp)
355             while True:
356                 line = filelogs.readline()
357                 if compiled_regexp.search(line):
358                     print("Pattern found!", end=' ')
359                     stringfound = True
360                     break
361                 if not line:
362                     time.sleep(0.1)
363     except TimeoutError:
364         print("Pattern not found after "+str(time_to_wait), end=" seconds! ", flush=True)
365     except PermissionError:
366         print("Permission Error when trying to access the log file", end=" ... ", flush=True)
367     finally:
368         if filefound:
369             filelogs.close()
370         else:
371             print("log file does not exist or is not accessible... ", flush=True)
372         return stringfound
373
374
375 class TimeOut:
376     def __init__(self, seconds=1, error_message='Timeout'):
377         self.seconds = seconds
378         self.error_message = error_message
379
380     def handle_timeout(self, signum, frame):
381         raise TimeoutError(self.error_message)
382
383     def __enter__(self):
384         signal.signal(signal.SIGALRM, self.handle_timeout)
385         signal.alarm(self.seconds)
386
387     def __exit__(self, type, value, traceback):
388         signal.alarm(0)