Auto-generated patch by python-black
[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(
57         "--odladdress",
58         default="127.0.0.1",
59         help="IP address of ODL Restconf to be used",
60     )
61     parser.add_argument(
62         "--restconfport", default="8181", help="Port on which ODL Restconf to be used"
63     )
64     parser.add_argument(
65         "--restconfuser",
66         default="admin",
67         help="Username for ODL Restconf authentication",
68     )
69     parser.add_argument(
70         "--restconfpassword",
71         default="admin",
72         help="Password for ODL Restconf authentication",
73     )
74     parser.add_argument(
75         "--scope", default="sdn", help="Scope for ODL Restconf authentication"
76     )
77     parser.add_argument(
78         "--deviceaddress",
79         default="127.0.0.1",
80         help="Common IP address for all available devices",
81     )
82     parser.add_argument(
83         "--devices",
84         default="1",
85         type=int,
86         help="Number of devices available for connecting",
87     )
88     parser.add_argument(
89         "--deviceuser",
90         default="admin",
91         help="Username for netconf device authentication",
92     )
93     parser.add_argument(
94         "--devicepassword",
95         default="admin",
96         help="Password for netconf device authentication",
97     )
98     parser.add_argument(
99         "--startport", default="17830", type=int, help="Port number of first device"
100     )
101     # FIXME: There has to be a better name, "delay" evokes seconds, not number of connections.
102     parser.add_argument(
103         "--disconndelay",
104         default="0",
105         type=int,
106         help="Deconfigure oldest device if more than this devices were configured",
107     )
108     parser.add_argument(
109         "--connsleep",
110         default="0.0",
111         type=float,
112         help="Sleep this many seconds after configuration to allow operational update.",
113     )
114     parser.add_argument(
115         "--basename",
116         default="sim-device",
117         help="Name of device without the generated suffixes",
118     )
119     parser.add_argument(
120         "--reuse",
121         default="True",
122         type=str2bool,
123         help="Should single requests session be re-used",
124     )
125     return parser.parse_args()  # arguments are read
126
127
128 DATA_TEMPLATE = string.Template(
129     """{
130     "network-topology:node": {
131         "node-id": "$DEVICE_NAME",
132         "netconf-node-topology:host": "$DEVICE_IP",
133         "netconf-node-topology:port": $DEVICE_PORT,
134         "netconf-node-topology:username": "$DEVICE_USER",
135         "netconf-node-topology:password": "$DEVICE_PASSWORD",
136         "netconf-node-topology:tcp-only": "false",
137         "netconf-node-topology:keepalive-delay": 0
138     }
139 }"""
140 )
141
142
143 def count_response(counter, response, method):
144     """Add counter item built from response data and given method."""
145     counter[(method, str(response.status_code), response.text)] += 1
146
147
148 def sorted_repr(counter):
149     """
150     Return sorted and inverted representation of Counter,
151     intended to make large output more readable.
152     Also, the shorter report part collapses items differing only in response text.
153     """
154     short_counter = collections.Counter()
155     for key_tuple in counter:
156         short_counter[(key_tuple[0], key_tuple[1])] += counter[key_tuple]
157     short_list = sorted(short_counter.keys())
158     short_text = ", ".join(
159         [
160             "(" + item[0] + ":" + item[1] + ")x" + str(short_counter[item])
161             for item in short_list
162         ]
163     )
164     long_text = "\n".join([item[2] for item in sorted(counter.keys(), reverse=True)])
165     return short_text + "\nresponses:\n" + long_text
166
167
168 def main():
169     """Top-level logic to execute."""
170     args = parse_arguments()
171     uri_part = (
172         "config/network-topology:network-topology/topology/topology-netconf/node/"
173     )
174     put_headers = {"Content-Type": "application/json", "Accept": "application/json"}
175     delete_headers = {"Accept": "application/json"}
176     counter = collections.Counter()
177
178     def handle_sigint(
179         received_signal, frame
180     ):  # This is a closure as it refers to the counter.
181         """Upon SIGINT, print counter contents and exit gracefully."""
182         signal.signal(signal.SIGINT, signal.SIG_DFL)
183         print(sorted_repr(counter))
184         sys.exit(0)
185
186     signal.signal(signal.SIGINT, handle_sigint)
187     session = AuthStandalone.Init_Session(
188         args.odladdress,
189         args.restconfuser,
190         args.restconfpassword,
191         args.scope,
192         args.reuse,
193     )
194     subst_dict = {}
195     subst_dict["DEVICE_IP"] = args.deviceaddress
196     subst_dict["DEVICE_USER"] = args.deviceuser
197     subst_dict["DEVICE_PASSWORD"] = args.devicepassword
198     iteration = 0
199     delayed = collections.deque()
200     wrap_port = args.startport + args.devices
201     while 1:
202         iteration += 1
203         port = args.startport
204         while port < wrap_port:
205             if len(delayed) > args.disconndelay:
206                 delete_name = delayed.popleft()
207                 response = AuthStandalone.Delete_Using_Session(
208                     session, uri_part + delete_name, headers=delete_headers
209                 )
210                 count_response(counter, response, "delete")
211             put_name = args.basename + "-" + str(port) + "-" + str(iteration)
212             subst_dict["DEVICE_NAME"] = put_name
213             subst_dict["DEVICE_PORT"] = str(port)
214             put_data = DATA_TEMPLATE.substitute(subst_dict)
215             uri = uri_part + put_name
216             response = AuthStandalone.Put_Using_Session(
217                 session, uri, data=put_data, headers=put_headers
218             )
219             count_response(counter, response, "put")
220             delayed.append(put_name)  # schedule for deconfiguration unconditionally
221             time.sleep(args.connsleep)
222             port += 1
223
224
225 if __name__ == "__main__":
226     main()