Migrate portmapping functional tests to RFC8040
[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 import sys
17 import re
18 import signal
19 import subprocess
20 import time
21
22 import psutil
23 import requests
24
25 import simulators
26
27 SIMS = simulators.SIMS
28
29 HONEYNODE_OK_START_MSG = "Netconf SSH endpoint started successfully at 0.0.0.0"
30 KARAF_OK_START_MSG = re.escape(
31     "Blueprint container for bundle org.opendaylight.netconf.restconf")+".* was successfully created"
32 LIGHTY_OK_START_MSG = re.escape("lighty.io and RESTCONF-NETCONF started")
33
34 ODL_LOGIN = "admin"
35 ODL_PWD = "admin"
36 NODES_LOGIN = "admin"
37 NODES_PWD = "admin"
38 URL_CONFIG_ORDM_TOPO = "{}/data/ietf-network:networks/network=openroadm-topology"
39 URL_PORTMAPPING = "{}/data/transportpce-portmapping:network/nodes="
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') 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') 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') 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     stringfound = False
195     filefound = False
196     line = None
197     try:
198         with TimeOut(seconds=time_to_wait):
199             while not os.path.exists(log_file):
200                 time.sleep(0.2)
201             filelogs = open(log_file, 'r')
202             filelogs.seek(0, 2)
203             filefound = True
204             print("Searching for pattern '" + regexp + "' in " + os.path.basename(log_file), end='... ', flush=True)
205             compiled_regexp = re.compile(regexp)
206             while True:
207                 line = filelogs.readline()
208                 if compiled_regexp.search(line):
209                     print("Pattern found!", end=' ')
210                     stringfound = True
211                     break
212                 if not line:
213                     time.sleep(0.1)
214     except TimeoutError:
215         print("Pattern not found after " + str(time_to_wait), end=" seconds! ", flush=True)
216     except PermissionError:
217         print("Permission Error when trying to access the log file", end=" ... ", flush=True)
218     finally:
219         if filefound:
220             filelogs.close()
221         else:
222             print("log file does not exist or is not accessible... ", flush=True)
223         return stringfound
224
225
226 class TimeOut:
227     def __init__(self, seconds=1, error_message='Timeout'):
228         self.seconds = seconds
229         self.error_message = error_message
230
231     def handle_timeout(self, signum, frame):
232         raise TimeoutError(self.error_message)
233
234     def __enter__(self):
235         signal.signal(signal.SIGALRM, self.handle_timeout)
236         signal.alarm(self.seconds)
237
238     def __exit__(self, type, value, traceback):
239         # pylint: disable=W0622
240         signal.alarm(0)
241
242 #
243 # Basic NetCONF device operations
244 #
245
246
247 def mount_device(node_id, sim):
248     url = "{}/data/network-topology:network-topology/topology=topology-netconf/node={}"
249     body = {"node": [{
250         "node-id": node_id,
251         "netconf-node-topology:username": NODES_LOGIN,
252         "netconf-node-topology:password": NODES_PWD,
253         "netconf-node-topology:host": "127.0.0.1",
254         "netconf-node-topology:port": SIMS[sim]['port'],
255         "netconf-node-topology:tcp-only": "false",
256         "netconf-node-topology:pass-through": {}}]}
257     response = put_request(url.format('{}', node_id), body)
258     if wait_until_log_contains(TPCE_LOG, re.escape("Triggering notification stream NETCONF for node " + node_id), 180):
259         print("Node " + node_id + " correctly added to tpce topology", end='... ', flush=True)
260     else:
261         print("Node " + node_id + " still not added to tpce topology", end='... ', flush=True)
262         if response.status_code == requests.codes.ok:
263             print("It was probably loaded at start-up", end='... ', flush=True)
264         # TODO an else-clause to abort test would probably be nice here
265     return response
266
267
268 def unmount_device(node_id):
269     url = "{}/data/network-topology:network-topology/topology=topology-netconf/node={}"
270     response = delete_request(url.format('{}', node_id))
271     if wait_until_log_contains(TPCE_LOG, re.escape("onDeviceDisConnected: " + node_id), 180):
272         print("Node " + node_id + " correctly deleted from tpce topology", end='... ', flush=True)
273     else:
274         print("Node " + node_id + " still not deleted from tpce topology", end='... ', flush=True)
275     return response
276
277
278 def check_device_connection(node: str):
279     url = "{}/data/network-topology:network-topology/topology=topology-netconf/node={}"
280     response = get_request(url.format('{}', node))
281     res = response.json()
282     key = 'network-topology:node'
283     if key in res.keys():
284         connection_status = res[key][0]['netconf-node-topology:connection-status']
285     else:
286         connection_status = res['errors']['error']
287     return {'status_code': response.status_code,
288             'connection-status': connection_status}
289
290 #
291 # Portmapping operations
292 #
293
294
295 def get_portmapping(node: str):
296     url = "{}/data/transportpce-portmapping:network/nodes={}"
297     response = get_request(url.format('{}', node))
298     res = response.json()
299     print(res)
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 = u'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}