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