Add graceful-restart test suite
[integration/test.git] / tools / fastbgp / bgp_app_peer.py
1 """This program performs required BGP application peer operations."""
2
3 # Copyright (c) 2015 Cisco Systems, Inc. and others.  All rights reserved.
4 #
5 # This program and the accompanying materials are made available under the
6 # terms of the Eclipse Public License v1.0 which accompanies this distribution,
7 # and is available at http://www.eclipse.org/legal/epl-v10.html
8
9 import requests
10 import ipaddr
11 import argparse
12 import logging
13 import time
14 import xml.dom.minidom as md
15 import os.path
16
17
18 __author__ = "Radovan Sajben"
19 __copyright__ = "Copyright(c) 2015, Cisco Systems, Inc."
20 __license__ = "Eclipse Public License v1.0"
21 __email__ = "rsajben@cisco.com"
22
23
24 def _build_url(odl_ip, port, uri):
25     """Compose URL from generic IP, port and URI fragment.
26
27     Args:
28         :param odl_ip: controller's ip address or hostname
29
30         :param port: controller's restconf port
31
32         :param uri: URI without /restconf/ to complete URL
33
34     Returns:
35         :returns url: full restconf url corresponding to params
36     """
37
38     url = "http://" + str(odl_ip) + ":" + port + "/restconf/" + uri
39     return url
40
41
42 def _stream_data(xml_template, prefix_base, prefix_len, count, route_key=False, element="ipv4-routes"):
43     """Stream list of routes based on xml template. Memory non-consumable
44     data generation (on the fly).
45
46     Args:
47         :xml_template: xml template for routes
48
49         :prefix_base: first prefix IP address
50
51         :prefix_len: prefix length in bits
52
53         :count: number of routes to be generated
54
55         :route_key: bool deciding route-key tag existence
56
57         :element: element to be returned
58
59     Returns:
60         :yield xml_data: requested data by elements as xml data
61     """
62     global total_build_data_time_counter
63     if os.path.isfile(xml_template + "." + stream):
64         routes = md.parse(xml_template + "." + stream)
65     elif os.path.isfile(xml_template):
66         routes = md.parse(xml_template)
67     else:
68         logger.error("Template '{}' does not exist.".format(xml_template))
69
70     routes_node = routes.getElementsByTagName("ipv4-routes")[0]
71     route_node = routes.getElementsByTagName("ipv4-route")[0]
72     routes_node.removeChild(route_node)
73     route_prefix = route_node.getElementsByTagName("prefix")[0]
74     if route_key:
75         key = route_node.getElementsByTagName("route-key")[0]
76     prefix_gap = 2 ** (32 - prefix_len)
77     prefix_index_list = range(count)
78     if element == routes_node.tagName:
79         lines = routes_node.toxml().splitlines()
80         xml_head = lines[0] + "\n"
81         xml_tail = "\n".join(lines[1:])
82     elif element == route_node.tagName:
83         xml_head = ""
84         xml_tail = ""
85         route_node.setAttribute("xmlns", route_node.namespaceURI)
86     else:
87         prefix_index_list = range(0)
88
89     for prefix_index in prefix_index_list:
90         build_data_timestamp = time.time()
91         prefix = prefix_base + prefix_index * prefix_gap
92         prefix_str = str(prefix) + "/" + str(prefix_len)
93         route_prefix.childNodes[0].nodeValue = prefix_str
94         if route_key:
95             key.childNodes[0].nodeValue = prefix_str
96         xml_data = route_node.toxml()
97         if prefix_index == 0:
98             xml_data = xml_head + xml_data
99         if prefix_index == len(prefix_index_list) - 1:
100             xml_data = xml_data + xml_tail
101         chunk = prefix_index + 1
102         if not (chunk % 1000):
103             logger.info("... streaming chunk %s (prefix: %s)", chunk, prefix_str)
104         else:
105             logger.debug("...streaming chunk %s (prefix: %s)", chunk, prefix_str)
106         logger.debug("xml data\n%s", xml_data)
107         total_build_data_time_counter += time.time() - build_data_timestamp
108         yield xml_data
109
110
111 def send_request(operation, odl_ip, port, uri, auth, xml_data=None, expect_status_code=200):
112     """Send a http request.
113
114     Args:
115         :operation: GET, POST, PUT, DELETE
116
117         :param odl_ip: controller's ip address or hostname
118
119         :param port: controller's restconf port
120
121         :param uri: URI without /restconf/ to complete URL
122
123         :param auth: authentication credentials
124
125         :param xml_data: list of routes as xml data
126
127     Returns:
128         :returns http response object
129     """
130     global total_response_time_counter
131     global total_number_of_responses_counter
132
133     ses = requests.Session()
134
135     url = _build_url(odl_ip, port, uri)
136     header = {"Content-Type": "application/xml"}
137     req = requests.Request(operation, url, headers=header, data=xml_data, auth=auth)
138     prep = req.prepare()
139     try:
140         send_request_timestamp = time.time()
141         rsp = ses.send(prep, timeout=60)
142         total_response_time_counter += time.time() - send_request_timestamp
143         total_number_of_responses_counter += 1
144     except requests.exceptions.Timeout:
145         logger.error("No response from %s", odl_ip)
146     else:
147         if rsp.status_code == expect_status_code:
148             logger.debug("%s %s", rsp.request, rsp.request.url)
149             logger.debug("Request headers: %s:", rsp.request.headers)
150             logger.debug("Response: %s", rsp.text)
151             logger.debug("%s %s", rsp, rsp.reason)
152         else:
153             logger.error("%s %s", rsp.request, rsp.request.url)
154             logger.error("Request headers: %s:", rsp.request.headers)
155             logger.error("Response: %s", rsp.text)
156             logger.error("%s %s", rsp, rsp.reason)
157         return rsp
158
159
160 def get_prefixes(odl_ip, port, uri, auth, prefix_base=None, prefix_len=None,
161                  count=None, xml_template=None):
162     """Send a http GET request for getting all prefixes.
163
164     Args:
165         :param odl_ip: controller's ip address or hostname
166
167         :param port: controller's restconf port
168
169         :param uri: URI without /restconf/ to complete URL
170
171         :param auth: authentication tupple as (user, password)
172
173         :param prefix_base: IP address of the first prefix
174
175         :prefix_len: length of the prefix in bites (specifies the increment as well)
176
177         :param count: number of prefixes to be processed
178
179         :param xml_template: xml template for building the xml data
180
181     Returns:
182         :returns None
183     """
184
185     logger.info("Get all prefixes from %s:%s/restconf/%s", odl_ip, port, uri)
186     rsp = send_request("GET", odl_ip, port, uri, auth)
187     if rsp is not None:
188         s = rsp.text
189         s = s.replace("{", "")
190         s = s.replace("}", "")
191         s = s.replace("[", "")
192         s = s.replace("]", "")
193         prefixes = ''
194         prefix_count = 0
195         for item in s.split(","):
196             if "prefix" in item:
197                 prefixes += item + ","
198                 prefix_count += 1
199         prefixes = prefixes[:len(prefixes) - 1]
200         logger.debug("prefix_list=%s", prefixes)
201         logger.info("prefix_count=%s", prefix_count)
202
203
204 def post_prefixes(odl_ip, port, uri, auth, prefix_base=None, prefix_len=None,
205                   count=0, route_key=False, xml_template=None):
206     """Send a http POST request for creating a prefix list.
207
208     Args:
209         :param odl_ip: controller's ip address or hostname
210
211         :param port: controller's restconf port
212
213         :param uri: URI without /restconf/ to complete URL
214
215         :param auth: authentication tupple as (user, password)
216
217         :param prefix_base: IP address of the first prefix
218
219         :prefix_len: length of the prefix in bites (specifies the increment as well)
220
221         :param count: number of prefixes to be processed
222
223         :route_key: bool deciding route-key tag existence
224
225         :param xml_template: xml template for building the xml data (not used)
226
227     Returns:
228         :returns None
229     """
230     logger.info("Post %s prefix(es) in a single request (starting from %s/%s) into %s:%s/restconf/%s",
231                 count, prefix_base, prefix_len, odl_ip, port, uri)
232     xml_stream = _stream_data(xml_template, prefix_base, prefix_len, count, route_key)
233     send_request("POST", odl_ip, port, uri, auth, xml_data=xml_stream, expect_status_code=204)
234
235
236 def put_prefixes(odl_ip, port, uri, auth, prefix_base, prefix_len, count,
237                  route_key, xml_template=None):
238     """Send a http PUT request for updating the prefix list.
239
240     Args:
241         :param odl_ip: controller's ip address or hostname
242
243         :param port: controller's restconf port
244
245         :param uri: URI without /restconf/ to complete URL
246
247         :param auth: authentication tupple as (user, password)
248
249         :param prefix_base: IP address of the first prefix
250
251         :prefix_len: length of the prefix in bites (specifies the increment as well)
252
253         :param count: number of prefixes to be processed
254
255         :param xml_template: xml template for building the xml data (not used)
256
257     Returns:
258         :returns None
259     """
260     uri_add_prefix = uri + _uri_suffix_ipv4_routes
261     logger.info("Put %s prefix(es) in a single request (starting from %s/%s) into %s:%s/restconf/%s",
262                 count, prefix_base, prefix_len, odl_ip, port, uri_add_prefix)
263     xml_stream = _stream_data(xml_template, prefix_base, prefix_len, count, route_key)
264     send_request("PUT", odl_ip, port, uri_add_prefix, auth, xml_data=xml_stream)
265
266
267 def add_prefixes(odl_ip, port, uri, auth, prefix_base, prefix_len, count,
268                  route_key, xml_template=None):
269     """Send a consequent http POST request for adding prefixes.
270
271     Args:
272         :param odl_ip: controller's ip address or hostname
273
274         :param port: controller's restconf port
275
276         :param uri: URI without /restconf/ to complete URL
277
278         :param auth: authentication tupple as (user, password)
279
280         :param prefix_base: IP address of the first prefix
281
282         :prefix_len: length of the prefix in bites (specifies the increment as well)
283
284         :param count: number of prefixes to be processed
285
286         :param xml_template: xml template for building the xml data (not used)
287
288     Returns:
289         :returns None
290     """
291     logger.info("Add %s prefixes (starting from %s/%s) into %s:%s/restconf/%s",
292                 count, prefix_base, prefix_len, odl_ip, port, uri)
293     uri_add_prefix = uri + _uri_suffix_ipv4_routes
294     prefix_gap = 2 ** (32 - prefix_len)
295     for prefix_index in range(count):
296         prefix = prefix_base + prefix_index * prefix_gap
297         logger.info("Adding prefix %s/%s to %s:%s/restconf/%s",
298                     prefix, prefix_len, odl_ip, port, uri)
299         xml_stream = _stream_data(xml_template, prefix, prefix_len, 1, route_key,
300                                   element="ipv4-route")
301         send_request("POST", odl_ip, port, uri_add_prefix, auth,
302                      xml_data=xml_stream, expect_status_code=204)
303
304
305 def delete_prefixes(odl_ip, port, uri, auth, prefix_base, prefix_len, count,
306                     xml_template=None):
307     """Send a http DELETE requests for deleting prefixes.
308
309     Args:
310         :param odl_ip: controller's ip address or hostname
311
312         :param port: controller's restconf port
313
314         :param uri: URI without /restconf/ to complete URL
315
316         :param auth: authentication tupple as (user, password)
317
318         :param prefix_base: IP address of the first prefix
319
320         :prefix_len: length of the prefix in bites (specifies the increment as well)
321
322         :param count: number of prefixes to be processed
323
324         :param xml_template: xml template for building the xml data (not used)
325
326     Returns:
327         :returns None
328     """
329     logger.info("Delete %s prefix(es) (starting from %s/%s) from %s:%s/restconf/%s",
330                 count, prefix_base, prefix_len, odl_ip, port, uri)
331     partkey = "/0"
332     uri_del_prefix = uri + _uri_suffix_ipv4_routes + _uri_suffix_ipv4_route
333     prefix_gap = 2 ** (32 - prefix_len)
334     for prefix_index in range(count):
335         prefix = prefix_base + prefix_index * prefix_gap
336         logger.info("Deleting prefix %s/%s/%s from %s:%s/restconf/%s",
337                     prefix, prefix_len, partkey, odl_ip, port, uri)
338         send_request("DELETE", odl_ip, port,
339                      uri_del_prefix + str(prefix) + "%2F" + str(prefix_len) + partkey, auth)
340
341
342 def delete_all_prefixes(odl_ip, port, uri, auth, prefix_base=None,
343                         prefix_len=None, count=None, xml_template=None):
344     """Send a http DELETE request for deleting all prefixes.
345
346     Args:
347         :param odl_ip: controller's ip address or hostname
348
349         :param port: controller's restconf port
350
351         :param uri: URI without /restconf/ to complete URL
352
353         :param auth: authentication tupple as (user, password)
354
355         :param prefix_base: IP address of the first prefix (not used)
356
357         :prefix_len: length of the prefix in bites (not used)
358
359         :param count: number of prefixes to be processed (not used)
360
361         :param xml_template: xml template for building the xml data (not used)
362
363     Returns:
364         :returns None
365     """
366     logger.info("Delete all prefixes from %s:%s/restconf/%s", odl_ip, port, uri)
367     uri_del_all_prefixes = uri + _uri_suffix_ipv4_routes
368     send_request("DELETE", odl_ip, port, uri_del_all_prefixes, auth)
369
370
371 _commands = ["post", "put", "add", "delete", "delete-all", "get"]
372 _uri_suffix_ipv4_routes = "bgp-inet:ipv4-routes/"
373 _uri_suffix_ipv4_route = "bgp-inet:ipv4-route/"   # followed by IP address like 1.1.1.1%2F32
374
375 if __name__ == "__main__":
376     parser = argparse.ArgumentParser(description="BGP application peer script")
377     parser.add_argument("--host", type=ipaddr.IPv4Address, default="127.0.0.1",
378                         help="ODL controller IP address")
379     parser.add_argument("--port", default="8181",
380                         help="ODL RESTCONF port")
381     parser.add_argument("--command", choices=_commands, metavar="command",
382                         help="Command to be performed."
383                         "post, put, add, delete, delete-all, get")
384     parser.add_argument("--prefix", type=ipaddr.IPv4Address, default="8.0.1.0",
385                         help="First prefix IP address")
386     parser.add_argument("--prefixlen", type=int, help="Prefix length in bites",
387                         default=28)
388     parser.add_argument("--count", type=int, help="Number of prefixes",
389                         default=1)
390     parser.add_argument("--user", help="Restconf user name", default="admin")
391     parser.add_argument("--password", help="Restconf password", default="admin")
392     parser.add_argument("--uri", help="The uri part of requests",
393                         default="config/bgp-rib:application-rib/example-app-rib/"
394                                 "tables/bgp-types:ipv4-address-family/"
395                                 "bgp-types:unicast-subsequent-address-family/")
396     parser.add_argument("--xml", help="File name of the xml data template",
397                         default="ipv4-routes-template.xml")
398     parser.add_argument("--error", dest="loglevel", action="store_const",
399                         const=logging.ERROR, default=logging.INFO,
400                         help="Set log level to error (default is info)")
401     parser.add_argument("--warning", dest="loglevel", action="store_const",
402                         const=logging.WARNING, default=logging.INFO,
403                         help="Set log level to warning (default is info)")
404     parser.add_argument("--info", dest="loglevel", action="store_const",
405                         const=logging.INFO, default=logging.INFO,
406                         help="Set log level to info (default is info)")
407     parser.add_argument("--debug", dest="loglevel", action="store_const",
408                         const=logging.DEBUG, default=logging.INFO,
409                         help="Set log level to debug (default is info)")
410     parser.add_argument("--logfile", default="bgp_app_peer.log", help="Log file name")
411     parser.add_argument("--stream", default="", help="ODL Stream - oxygen, fluorine ...")
412
413     args = parser.parse_args()
414
415     logger = logging.getLogger("logger")
416     log_formatter = logging.Formatter("%(asctime)s %(levelname)s: %(message)s")
417     console_handler = logging.StreamHandler()
418     file_handler = logging.FileHandler(args.logfile, mode="w")
419     console_handler.setFormatter(log_formatter)
420     file_handler.setFormatter(log_formatter)
421     logger.addHandler(console_handler)
422     logger.addHandler(file_handler)
423     logger.setLevel(args.loglevel)
424
425     auth = (args.user, args.password)
426
427     odl_ip = args.host
428     port = args.port
429     command = args.command
430     prefix_base = args.prefix
431     prefix_len = args.prefixlen
432     count = args.count
433     uri = args.uri
434     stream = args.stream
435     xml_template = args.xml
436
437     # From Fluorine onward route-key argument is mandatory for identification.
438     route_key_stream = ["oxygen"]
439     route_key = True if args.stream not in route_key_stream else False
440
441     test_start_time = time.time()
442     total_build_data_time_counter = 0
443     total_response_time_counter = 0
444     total_number_of_responses_counter = 0
445
446     if command == "post":
447         post_prefixes(odl_ip, port, uri, auth, prefix_base, prefix_len, count,
448                       route_key, xml_template)
449     if command == "put":
450         put_prefixes(odl_ip, port, uri, auth, prefix_base, prefix_len, count,
451                      route_key, xml_template)
452     if command == "add":
453         add_prefixes(odl_ip, port, uri, auth, prefix_base, prefix_len, count,
454                      route_key, xml_template)
455     elif command == "delete":
456         delete_prefixes(odl_ip, port, uri, auth, prefix_base, prefix_len, count)
457     elif command == "delete-all":
458         delete_all_prefixes(odl_ip, port, uri, auth)
459     elif command == "get":
460         get_prefixes(odl_ip, port, uri, auth)
461
462     total_test_execution_time = time.time() - test_start_time
463
464     logger.info("Total test execution time: %.3fs", total_test_execution_time)
465     logger.info("Total build data time: %.3fs", total_build_data_time_counter)
466     logger.info("Total response time: %.3fs", total_response_time_counter)
467     logger.info("Total number of response(s): %s", total_number_of_responses_counter)
468     file_handler.close()