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()
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
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
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
106 def sorted_repr(counter):
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.
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
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()
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)
135 signal.signal(signal.SIGINT, handle_sigint)
136 session = AuthStandalone.Init_Session(
137 args.odladdress, args.restconfuser, args.restconfpassword, args.scope, args.reuse)
139 subst_dict["DEVICE_IP"] = args.deviceaddress
140 subst_dict["DEVICE_USER"] = args.deviceuser
141 subst_dict["DEVICE_PASSWORD"] = args.devicepassword
143 delayed = collections.deque()
144 wrap_port = args.startport + args.devices
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)
165 if __name__ == "__main__":