2 Utility library for configuring NETCONF topology node to connect to network elements. This operates
3 on the legacy network-topology model, with topology-id="topology-netconf".
6 from logging import debug, info
7 from requests import get, patch
9 from time import sleep, time
10 from uuid import uuid4
13 def configure_device_range(
21 """Generate device_count names in format "$device_name_prefix-$i" and configure them into NETCONF topology at specified RESTCONF URL.
24 configure_device_range("http://127.0.0.1:8181/rests", "example", "127.0.0.1", 1730, 5)
26 would configure devices "example-0" through "example-4" to connect to 127.0.0.0:1730.
28 configure_device_range("http://127.0.0.1:8181/rests", "example", "127.0.0.1", 1720, 5, 5)
30 would configure devices "example-5" through "example-9" to connect to 127.0.0.0:1720.
32 This method assumes RFC8040 with RFC7952 encoding and support for RFC8072 (YANG patch). Payload it generates looks roughly like this:
34 "ietf-yang-patch:yang-patch" : {
38 "edit-id" : "test-edit",
39 "operation" : "replace",
40 "target" : "/node=test-node",
44 "node-id" : "test-node"
45 "netconf-node-topology:host" : "127.0.0.1",
46 "netconf-node-topology:port" : 17830,
47 "netconf-node-topology:username" : "admin",
48 "netconf-node-topology:password" : "topsecret",
49 "netconf-node-topology:keepalive-delay" : 0,
60 "Configure %s devices starting from %s (at %s:%s)",
70 for i in range(first_device_id, first_device_id + device_count):
71 name = "{}-{}".format(device_name_prefix, i)
72 device_names.append(name)
76 "edit-id" : "node-%s",
77 "operation" : "replace",
78 "target" : "/network-topology:node[network-topology:node-id='%s']",
83 "netconf-node-topology:host" : "%s",
84 "netconf-node-topology:port" : %s,
85 "netconf-node-topology:username" : "admin",
86 "netconf-node-topology:password" : "topsecret",
87 "netconf-node-topology:tcp-only" : false,
88 "netconf-node-topology:keepalive-delay" : 0
94 % (name, name, name, device_ipaddress, device_port)
99 "ietf-yang-patch:yang-patch" : {
100 "patch-id" : "csit-%s",
106 # TODO: I bet there is a fancier way to write this
123 + """/data/network-topology:network-topology/topology=topology-netconf""",
125 "Content-Type": "application/yang-patch+json",
126 "Accept": "application/yang-data+json",
127 "User-Agent": "csit agent",
130 # FIXME: do not hard-code credentials here
131 auth=("admin", "admin"),
134 resp.raise_for_status()
136 # FIXME: validate response
138 # "ietf-yang-patch:yang-patch-status" : {
139 # "patch-id" : "add-songs-patch-2",
145 # "ietf-yang-patch:yang-patch-status" : {
146 # "patch-id" : "add-songs-patch",
150 # "edit-id" : "edit1",
154 # "error-type": "application",
155 # "error-tag": "data-exists",
156 # "error-path": "/example-jukebox:jukebox/library\
157 # /artist[name='Foo Fighters']\
158 # /album[name='Wasting Light']\
159 # /song[name='Bridge Burning']",
161 # "Data already exists; cannot be created"
174 def await_devices_connected(restconf_url, device_names, deadline_seconds):
175 """Await all specified devices to become connected in NETCONF topology at specified RESTCONF URL."""
177 info("Awaiting connection of %s", device_names)
178 deadline = time() + deadline_seconds
179 names = set(device_names)
182 while time() < deadline:
185 + """/data/network-topology:network-topology/topology=topology-netconf?content=nonconfig""",
186 headers={"Accept": "application/yang-data+json"},
187 # FIXME: do not hard-code credentials here
188 auth=("admin", "admin"),
191 # FIXME: also check for 409 might be okay?
192 resp.raise_for_status()
194 if "node" not in resp.json()["network-topology:topology"][0]:
198 # Check all reported nodes
199 for node in resp.json()["network-topology:topology"][0]["node"]:
200 name = node["node-id"]
201 status = node["netconf-node-topology:connection-status"]
202 debug("Evaluating %s status %s", name, status)
205 if status == "connected":
206 if name not in connected:
207 debug("Device %s connected", name)
209 elif name in connected:
210 # also remove from connected in case we switched from
211 # connected on a device from previous iteration
212 connected.remove(name)
214 if len(connected) == len(names):
219 raise Exception("Timed out waiting for %s to connect" % names.difference(connected))
223 # FIXME: add proper option parsing
224 if args[0] == "configure":
225 names = configure_device_range(
226 restconf_url="http://127.0.0.1:8181/rests",
227 device_name_prefix="example",
228 device_ipaddress="127.0.0.1",
230 device_count=int(args[1]),
233 elif args[0] == "await":
234 await_devices_connected(
235 restconf_url="http://127.0.0.1:8181/rests",
237 device_names=args[1:],
240 raise Exception("Unhandled argument %s" % args[0])
243 if __name__ == "__main__":
244 # i.e. main does not depend on name of the binary