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