Moving vpn-instance yang from VPNMgr > NeutronVPN
[integration/test.git] / tools / netconf_tools / configurer.py
1 """Singlethreaded utility for rapid Netconf device connection configuration via topology.
2
3 This utility is intended for stress testing netconf topology way
4 of configuring (and deconfiguring) connectors to netconf devices.
5
6 This utility does not stop by itself, ctrl+c is needed to stop activity and print results.
7 This utility counts responses of different status and text, summary is printed on break.
8 This utility can still fail early, for example if http connection is refused.
9
10 Only config datastore write is performed,
11 it is never verified whether a connection between ODL and device was even attempted.
12
13 To avoid resource starvation, both the number of available devices
14 and the number of configured devices have to be limited.
15 Thus this utility also deconfigures connectors added previously when a certain number is achieved.
16
17 Note that if ODL ignores some deconfiguration writes, it may end up leaking connections
18 and eventually run into the resource issues.
19 TODO: Is there a reasonable way to detect or prevent such a leak?
20
21 The set of devices to connect is assumed to have IP addresses the same
22 and ports in continuous segment (so that single testtool can emulate them all).
23 Each connector has unique name, devices are assigned in a cyclic fashion.
24 """
25
26 # Copyright (c) 2016 Cisco Systems, Inc. and others.  All rights reserved.
27 #
28 # This program and the accompanying materials are made available under the
29 # terms of the Eclipse Public License v1.0 which accompanies this distribution,
30 # and is available at http://www.eclipse.org/legal/epl-v10.html
31
32 import argparse
33 import collections
34 import signal
35 import string
36 import sys
37 import time
38
39 import AuthStandalone
40
41
42 __author__ = "Vratko Polak"
43 __copyright__ = "Copyright(c) 2016, Cisco Systems, Inc."
44 __license__ = "Eclipse Public License v1.0"
45 __email__ = "vrpolak@cisco.com"
46
47
48 def str2bool(text):
49     """Utility converter, based on http://stackoverflow.com/a/19227287"""
50     return text.lower() in ("yes", "true", "y", "t", "1")
51
52
53 def parse_arguments():
54     """Return parsed form of command-line arguments."""
55     parser = argparse.ArgumentParser()
56     parser.add_argument("--odladdress", default="127.0.0.1",
57                         help="IP address of ODL Restconf to be used")
58     parser.add_argument("--restconfport", default="8181",
59                         help="Port on which ODL Restconf to be used")
60     parser.add_argument("--restconfuser", default="admin",
61                         help="Username for ODL Restconf authentication")
62     parser.add_argument("--restconfpassword", default="admin",
63                         help="Password for ODL Restconf authentication")
64     parser.add_argument("--scope", default="sdn",
65                         help="Scope for ODL Restconf authentication")
66     parser.add_argument("--deviceaddress", default="127.0.0.1",
67                         help="Common IP address for all available devices")
68     parser.add_argument("--devices", default="1", type=int,
69                         help="Number of devices available for connecting")
70     parser.add_argument("--deviceuser", default="admin",
71                         help="Username for netconf device authentication")
72     parser.add_argument("--devicepassword", default="admin",
73                         help="Password for netconf device authentication")
74     parser.add_argument("--startport", default="17830", type=int,
75                         help="Port number of first device")
76     # FIXME: There has to be a better name, "delay" evokes seconds, not number of connections.
77     parser.add_argument("--disconndelay", default="0", type=int,
78                         help="Deconfigure oldest device if more than this devices were configured")
79     parser.add_argument("--connsleep", default="0.0", type=float,
80                         help="Sleep this many seconds after configuration to allow operational update.")
81     parser.add_argument("--basename", default="sim-device",
82                         help="Name of device without the generated suffixes")
83     parser.add_argument("--reuse", default="True", type=str2bool,
84                         help="Should single requests session be re-used")
85     return parser.parse_args()  # arguments are read
86
87
88 DATA_TEMPLATE = string.Template('''{
89     "network-topology:node": {
90         "node-id": "$DEVICE_NAME",
91         "netconf-node-topology:host": "$DEVICE_IP",
92         "netconf-node-topology:port": $DEVICE_PORT,
93         "netconf-node-topology:username": "$DEVICE_USER",
94         "netconf-node-topology:password": "$DEVICE_PASSWORD",
95         "netconf-node-topology:tcp-only": "false",
96         "netconf-node-topology:keepalive-delay": 0
97     }
98 }''')
99
100
101 def count_response(counter, response, method):
102     """Add counter item built from response data and given method."""
103     counter[(method, str(response.status_code), response.text)] += 1
104
105
106 def sorted_repr(counter):
107     """
108     Return sorted and inverted representation of Counter,
109     intended to make large output more readable.
110     Also, the shorter report part collapses items differing only in response text.
111     """
112     short_counter = collections.Counter()
113     for key_tuple in counter:
114         short_counter[(key_tuple[0], key_tuple[1])] += counter[key_tuple]
115     short_list = sorted(short_counter.keys())
116     short_text = ", ".join(["(" + item[0] + ":" + item[1] + ")x" + str(short_counter[item]) for item in short_list])
117     long_text = "\n".join([item[2] for item in sorted(counter.keys(), reverse=True)])
118     return short_text + "\nresponses:\n" + long_text
119
120
121 def main():
122     """Top-level logic to execute."""
123     args = parse_arguments()
124     uri_part = "config/network-topology:network-topology/topology/topology-netconf/node/"
125     put_headers = {"Content-Type": "application/json", "Accept": "application/json"}
126     delete_headers = {"Accept": "application/json"}
127     counter = collections.Counter()
128
129     def handle_sigint(received_signal, frame):  # This is a closure as it refers to the counter.
130         """Upon SIGINT, print counter contents and exit gracefully."""
131         signal.signal(signal.SIGINT, signal.SIG_DFL)
132         print sorted_repr(counter)
133         sys.exit(0)
134
135     signal.signal(signal.SIGINT, handle_sigint)
136     session = AuthStandalone.Init_Session(
137         args.odladdress, args.restconfuser, args.restconfpassword, args.scope, args.reuse)
138     subst_dict = {}
139     subst_dict["DEVICE_IP"] = args.deviceaddress
140     subst_dict["DEVICE_USER"] = args.deviceuser
141     subst_dict["DEVICE_PASSWORD"] = args.devicepassword
142     iteration = 0
143     delayed = collections.deque()
144     wrap_port = args.startport + args.devices
145     while 1:
146         iteration += 1
147         port = args.startport
148         while port < wrap_port:
149             if len(delayed) > args.disconndelay:
150                 delete_name = delayed.popleft()
151                 response = AuthStandalone.Delete_Using_Session(session, uri_part + delete_name, headers=delete_headers)
152                 count_response(counter, response, "delete")
153             put_name = args.basename + "-" + str(port) + "-" + str(iteration)
154             subst_dict["DEVICE_NAME"] = put_name
155             subst_dict["DEVICE_PORT"] = str(port)
156             put_data = DATA_TEMPLATE.substitute(subst_dict)
157             uri = uri_part + put_name
158             response = AuthStandalone.Put_Using_Session(session, uri, data=put_data, headers=put_headers)
159             count_response(counter, response, "put")
160             delayed.append(put_name)  # schedule for deconfiguration unconditionally
161             time.sleep(args.connsleep)
162             port += 1
163
164
165 if __name__ == "__main__":
166     main()