1 """Singlethreaded utility for rapid Netconf device connection configuration via topology.
3 This utility is intended for stress testing netconf topology way
4 of configuring (and deconfiguring) connectors to netconf devices.
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.
10 Only config datastore write is performed,
11 it is never verified whether a connection between ODL and device was even attempted.
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.
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?
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.
26 # Copyright (c) 2016 Cisco Systems, Inc. and others. All rights reserved.
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
42 __author__ = "Vratko Polak"
43 __copyright__ = "Copyright(c) 2016, Cisco Systems, Inc."
44 __license__ = "Eclipse Public License v1.0"
45 __email__ = "vrpolak@cisco.com"
49 """Utility converter, based on http://stackoverflow.com/a/19227287"""
50 return text.lower() in ("yes", "true", "y", "t", "1")
53 def parse_arguments():
54 """Return parsed form of command-line arguments."""
55 parser = argparse.ArgumentParser()
59 help="IP address of ODL Restconf to be used",
62 "--restconfport", default="8181", help="Port on which ODL Restconf to be used"
67 help="Username for ODL Restconf authentication",
72 help="Password for ODL Restconf authentication",
75 "--scope", default="sdn", help="Scope for ODL Restconf authentication"
80 help="Common IP address for all available devices",
86 help="Number of devices available for connecting",
91 help="Username for netconf device authentication",
96 help="Password for netconf device authentication",
99 "--startport", default="17830", type=int, help="Port number of first device"
101 # FIXME: There has to be a better name, "delay" evokes seconds, not number of connections.
106 help="Deconfigure oldest device if more than this devices were configured",
112 help="Sleep this many seconds after configuration to allow operational update.",
116 default="sim-device",
117 help="Name of device without the generated suffixes",
123 help="Should single requests session be re-used",
125 return parser.parse_args() # arguments are read
128 DATA_TEMPLATE = string.Template(
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
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
148 def sorted_repr(counter):
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.
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(
160 "(" + item[0] + ":" + item[1] + ")x" + str(short_counter[item])
161 for item in short_list
164 long_text = "\n".join([item[2] for item in sorted(counter.keys(), reverse=True)])
165 return short_text + "\nresponses:\n" + long_text
169 """Top-level logic to execute."""
170 args = parse_arguments()
172 "config/network-topology:network-topology/topology/topology-netconf/node/"
174 put_headers = {"Content-Type": "application/json", "Accept": "application/json"}
175 delete_headers = {"Accept": "application/json"}
176 counter = collections.Counter()
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))
186 signal.signal(signal.SIGINT, handle_sigint)
187 session = AuthStandalone.Init_Session(
190 args.restconfpassword,
195 subst_dict["DEVICE_IP"] = args.deviceaddress
196 subst_dict["DEVICE_USER"] = args.deviceuser
197 subst_dict["DEVICE_PASSWORD"] = args.devicepassword
199 delayed = collections.deque()
200 wrap_port = args.startport + args.devices
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
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
219 count_response(counter, response, "put")
220 delayed.append(put_name) # schedule for deconfiguration unconditionally
221 time.sleep(args.connsleep)
225 if __name__ == "__main__":