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