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