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