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