e1323f2172f539e4816c409ad2dc15212d81c0c3
[transportpce.git] / tests / transportpce_tests / common / test_utils_rfc8040.py
1 #!/usr/bin/env python
2
3 ##############################################################################
4 # Copyright (c) 2021 Orange, Inc. and others.  All rights reserved.
5 #
6 # All rights reserved. This program and the accompanying materials
7 # are made available under the terms of the Apache License, Version 2.0
8 # which accompanies this distribution, and is available at
9 # http://www.apache.org/licenses/LICENSE-2.0
10 ##############################################################################
11
12 # pylint: disable=no-member
13
14 import json
15 import os
16 # pylint: disable=wrong-import-order
17 import sys
18 import re
19 import signal
20 import subprocess
21 import time
22
23 import psutil
24 import requests
25
26 # pylint: disable=import-error
27 import simulators
28
29 SIMS = simulators.SIMS
30
31 HONEYNODE_OK_START_MSG = "Netconf SSH endpoint started successfully at 0.0.0.0"
32 KARAF_OK_START_MSG = re.escape(
33     "Blueprint container for bundle org.opendaylight.netconf.restconf")+".* was successfully created"
34 LIGHTY_OK_START_MSG = re.escape("lighty.io and RESTCONF-NETCONF started")
35
36 ODL_LOGIN = "admin"
37 ODL_PWD = "admin"
38 NODES_LOGIN = "admin"
39 NODES_PWD = "admin"
40 URL_CONFIG_ORDM_TOPO = "{}/data/ietf-network:networks/network=openroadm-topology"
41 URL_PORTMAPPING = "{}/data/transportpce-portmapping:network/nodes="
42
43 TYPE_APPLICATION_JSON = {'Content-Type': 'application/json', 'Accept': 'application/json'}
44 TYPE_APPLICATION_XML = {'Content-Type': 'application/xml', 'Accept': 'application/xml'}
45
46 CODE_SHOULD_BE_200 = 'Http status code should be 200'
47 CODE_SHOULD_BE_201 = 'Http status code should be 201'
48
49 SIM_LOG_DIRECTORY = os.path.join(os.path.dirname(os.path.realpath(__file__)), "log")
50
51 process_list = []
52
53
54 if "USE_ODL_ALT_RESTCONF_PORT" in os.environ:
55     RESTCONF_BASE_URL = "http://localhost:" + os.environ['USE_ODL_ALT_RESTCONF_PORT'] + "/rests"
56 else:
57     RESTCONF_BASE_URL = "http://localhost:8181/rests"
58
59 if "USE_ODL_ALT_KARAF_INSTALL_DIR" in os.environ:
60     KARAF_INSTALLDIR = os.environ['USE_ODL_ALT_KARAF_INSTALL_DIR']
61 else:
62     KARAF_INSTALLDIR = "karaf"
63
64 KARAF_LOG = os.path.join(
65     os.path.dirname(os.path.realpath(__file__)),
66     "..", "..", "..", KARAF_INSTALLDIR, "target", "assembly", "data", "log", "karaf.log")
67
68 if "USE_LIGHTY" in os.environ and os.environ['USE_LIGHTY'] == 'True':
69     TPCE_LOG = 'odl-' + str(os.getpid()) + '.log'
70 else:
71     TPCE_LOG = KARAF_LOG
72
73 #
74 # Basic HTTP operations
75 #
76
77
78 def get_request(url):
79     return requests.request(
80         "GET", url.format(RESTCONF_BASE_URL),
81         headers=TYPE_APPLICATION_JSON,
82         auth=(ODL_LOGIN, ODL_PWD))
83
84
85 def put_request(url, data):
86     return requests.request(
87         "PUT", url.format(RESTCONF_BASE_URL),
88         data=json.dumps(data),
89         headers=TYPE_APPLICATION_JSON,
90         auth=(ODL_LOGIN, ODL_PWD))
91
92
93 def delete_request(url):
94     return requests.request(
95         "DELETE", url.format(RESTCONF_BASE_URL),
96         headers=TYPE_APPLICATION_JSON,
97         auth=(ODL_LOGIN, ODL_PWD))
98
99 #
100 # Process management
101 #
102
103
104 def start_sims(sims_list):
105     for sim in sims_list:
106         print("starting simulator " + sim[0] + " in OpenROADM device version " + sim[1] + "...")
107         log_file = os.path.join(SIM_LOG_DIRECTORY, SIMS[sim]['logfile'])
108         process = start_honeynode(log_file, sim)
109         if wait_until_log_contains(log_file, HONEYNODE_OK_START_MSG, 100):
110             print("simulator for " + sim[0] + " started")
111         else:
112             print("simulator for " + sim[0] + " failed to start")
113             shutdown_process(process)
114             for pid in process_list:
115                 shutdown_process(pid)
116             sys.exit(3)
117         process_list.append(process)
118     return process_list
119
120
121 def start_tpce():
122     print("starting OpenDaylight...")
123     if "USE_LIGHTY" in os.environ and os.environ['USE_LIGHTY'] == 'True':
124         process = start_lighty()
125         start_msg = LIGHTY_OK_START_MSG
126     else:
127         process = start_karaf()
128         start_msg = KARAF_OK_START_MSG
129     if wait_until_log_contains(TPCE_LOG, start_msg, time_to_wait=300):
130         print("OpenDaylight started !")
131     else:
132         print("OpenDaylight failed to start !")
133         shutdown_process(process)
134         for pid in process_list:
135             shutdown_process(pid)
136         sys.exit(1)
137     process_list.append(process)
138     return process_list
139
140
141 def start_karaf():
142     print("starting KARAF TransportPCE build...")
143     executable = os.path.join(
144         os.path.dirname(os.path.realpath(__file__)),
145         "..", "..", "..", KARAF_INSTALLDIR, "target", "assembly", "bin", "karaf")
146     with open('odl.log', 'w', encoding='utf-8') as outfile:
147         return subprocess.Popen(
148             ["sh", executable, "server"], stdout=outfile, stderr=outfile, stdin=None)
149
150
151 def start_lighty():
152     print("starting LIGHTY.IO TransportPCE build...")
153     executable = os.path.join(
154         os.path.dirname(os.path.realpath(__file__)),
155         "..", "..", "..", "lighty", "target", "tpce",
156         "clean-start-controller.sh")
157     with open(TPCE_LOG, 'w', encoding='utf-8') as outfile:
158         return subprocess.Popen(
159             ["sh", executable], stdout=outfile, stderr=outfile, stdin=None)
160
161
162 def install_karaf_feature(feature_name: str):
163     print("installing feature " + feature_name)
164     executable = os.path.join(
165         os.path.dirname(os.path.realpath(__file__)),
166         "..", "..", "..", KARAF_INSTALLDIR, "target", "assembly", "bin", "client")
167     return subprocess.run([executable],
168                           input='feature:install ' + feature_name + '\n feature:list | grep '
169                           + feature_name + ' \n logout \n',
170                           universal_newlines=True, check=False)
171
172
173 def shutdown_process(process):
174     if process is not None:
175         for child in psutil.Process(process.pid).children():
176             child.send_signal(signal.SIGINT)
177             child.wait()
178         process.send_signal(signal.SIGINT)
179
180
181 def start_honeynode(log_file: str, sim):
182     executable = os.path.join(os.path.dirname(os.path.realpath(__file__)),
183                               "..", "..", "honeynode", sim[1], "honeynode-simulator", "honeycomb-tpce")
184     sample_directory = os.path.join(os.path.dirname(os.path.realpath(__file__)),
185                                     "..", "..", "sample_configs", "openroadm", sim[1])
186     if os.path.isfile(executable):
187         with open(log_file, 'w', encoding='utf-8') as outfile:
188             return subprocess.Popen(
189                 [executable, SIMS[sim]['port'], os.path.join(sample_directory, SIMS[sim]['configfile'])],
190                 stdout=outfile, stderr=outfile)
191     return None
192
193
194 def wait_until_log_contains(log_file, regexp, time_to_wait=60):
195     # pylint: disable=lost-exception
196     stringfound = False
197     filefound = False
198     line = None
199     try:
200         with TimeOut(seconds=time_to_wait):
201             while not os.path.exists(log_file):
202                 time.sleep(0.2)
203             filelogs = open(log_file, 'r', encoding='utf-8')
204             filelogs.seek(0, 2)
205             filefound = True
206             print("Searching for pattern '" + regexp + "' in " + os.path.basename(log_file), end='... ', flush=True)
207             compiled_regexp = re.compile(regexp)
208             while True:
209                 line = filelogs.readline()
210                 if compiled_regexp.search(line):
211                     print("Pattern found!", end=' ')
212                     stringfound = True
213                     break
214                 if not line:
215                     time.sleep(0.1)
216     except TimeoutError:
217         print("Pattern not found after " + str(time_to_wait), end=" seconds! ", flush=True)
218     except PermissionError:
219         print("Permission Error when trying to access the log file", end=" ... ", flush=True)
220     finally:
221         if filefound:
222             filelogs.close()
223         else:
224             print("log file does not exist or is not accessible... ", flush=True)
225         return stringfound
226
227
228 class TimeOut:
229     def __init__(self, seconds=1, error_message='Timeout'):
230         self.seconds = seconds
231         self.error_message = error_message
232
233     def handle_timeout(self, signum, frame):
234         raise TimeoutError(self.error_message)
235
236     def __enter__(self):
237         signal.signal(signal.SIGALRM, self.handle_timeout)
238         signal.alarm(self.seconds)
239
240     def __exit__(self, type, value, traceback):
241         # pylint: disable=W0622
242         signal.alarm(0)
243
244 #
245 # Basic NetCONF device operations
246 #
247
248
249 def mount_device(node_id, sim):
250     url = "{}/data/network-topology:network-topology/topology=topology-netconf/node={}"
251     body = {"node": [{
252         "node-id": node_id,
253         "netconf-node-topology:username": NODES_LOGIN,
254         "netconf-node-topology:password": NODES_PWD,
255         "netconf-node-topology:host": "127.0.0.1",
256         "netconf-node-topology:port": SIMS[sim]['port'],
257         "netconf-node-topology:tcp-only": "false",
258         "netconf-node-topology:pass-through": {}}]}
259     response = put_request(url.format('{}', node_id), body)
260     if wait_until_log_contains(TPCE_LOG, re.escape("Triggering notification stream NETCONF for node " + node_id), 180):
261         print("Node " + node_id + " correctly added to tpce topology", end='... ', flush=True)
262     else:
263         print("Node " + node_id + " still not added to tpce topology", end='... ', flush=True)
264         if response.status_code == requests.codes.ok:
265             print("It was probably loaded at start-up", end='... ', flush=True)
266         # TODO an else-clause to abort test would probably be nice here
267     return response
268
269
270 def unmount_device(node_id):
271     url = "{}/data/network-topology:network-topology/topology=topology-netconf/node={}"
272     response = delete_request(url.format('{}', node_id))
273     if wait_until_log_contains(TPCE_LOG, re.escape("onDeviceDisConnected: " + node_id), 180):
274         print("Node " + node_id + " correctly deleted from tpce topology", end='... ', flush=True)
275     else:
276         print("Node " + node_id + " still not deleted from tpce topology", end='... ', flush=True)
277     return response
278
279
280 def check_device_connection(node: str):
281     url = "{}/data/network-topology:network-topology/topology=topology-netconf/node={}"
282     response = get_request(url.format('{}', node))
283     res = response.json()
284     key = 'network-topology:node'
285     if key in res.keys():
286         connection_status = res[key][0]['netconf-node-topology:connection-status']
287     else:
288         connection_status = res['errors']['error']
289     return {'status_code': response.status_code,
290             'connection-status': connection_status}
291
292 #
293 # Portmapping operations
294 #
295
296
297 def get_portmapping(node: str):
298     url = "{}/data/transportpce-portmapping:network/nodes={}"
299     response = get_request(url.format('{}', node))
300     res = response.json()
301     print(res)
302     nodes = res['transportpce-portmapping:nodes']
303     return {'status_code': response.status_code,
304             'nodes': nodes}
305
306
307 def get_portmapping_node_info(node: str):
308     url = "{}/data/transportpce-portmapping:network/nodes={}/node-info"
309     response = get_request(url.format('{}', node))
310     res = response.json()
311     key = 'transportpce-portmapping:node-info'
312     if key in res.keys():
313         node_info = res[key]
314     else:
315         node_info = res['errors']['error']
316     return {'status_code': response.status_code,
317             'node-info': node_info}
318
319
320 def portmapping_request(node: str, mapping: str):
321     url = "{}/data/transportpce-portmapping:network/nodes={}/mapping={}"
322     response = get_request(url.format('{}', node, mapping))
323     res = response.json()
324     mapping = res['transportpce-portmapping:mapping']
325     return {'status_code': response.status_code,
326             'mapping': mapping}
327
328
329 def portmapping_mc_capa_request(node: str, mc_capa: str):
330     url = "{}/data/transportpce-portmapping:network/nodes={}/mc-capabilities={}"
331     response = get_request(url.format('{}', node, mc_capa))
332     res = response.json()
333     capabilities = res['transportpce-portmapping:mc-capabilities']
334     return {'status_code': response.status_code,
335             'mc-capabilities': capabilities}