Remove code for capwap
[integration/test.git] / csit / libraries / TopologyNetconfNodes.py
1 """
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".
4 """
5
6 from logging import debug, info
7 from requests import get, patch
8 from sys import argv
9 from time import sleep, time
10 from uuid import uuid4
11
12
13 def configure_device_range(
14     restconf_url,
15     device_name_prefix,
16     device_ipaddress,
17     device_port,
18     device_count,
19     first_device_id=1,
20 ):
21     """Generate device_count names in format "$device_name_prefix-$i" and configure them into NETCONF topology at specified RESTCONF URL.
22     For example:
23
24        configure_device_range("http://127.0.0.1:8181/rests", "example", "127.0.0.1", 1730, 5)
25
26     would configure devices "example-0" through "example-4" to connect to 127.0.0.0:1730.
27
28        configure_device_range("http://127.0.0.1:8181/rests", "example", "127.0.0.1", 1720, 5, 5)
29
30     would configure devices "example-5" through "example-9" to connect to 127.0.0.0:1720.
31
32     This method assumes RFC8040 with RFC7952 encoding and support for RFC8072 (YANG patch). Payload it generates looks roughly like this:
33     {
34        "ietf-yang-patch:yang-patch" : {
35           "patch-id" : "test"
36           "edit" : [
37              {
38                 "edit-id" : "test-edit",
39                 "operation" : "replace",
40                 "target" : "/node=test-node",
41                 "value" : {
42                    "node" : [
43                       {
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,
50                       }
51                    ]
52                 }
53              }
54           ],
55        }
56     }
57     """
58
59     info(
60         "Configure %s devices starting from %s (at %s:%s)",
61         device_count,
62         first_device_id,
63         device_ipaddress,
64         device_port,
65     )
66
67     device_names = []
68     edits = []
69
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)
73         edits.append(
74             """
75             {
76               "edit-id" : "node-%s",
77               "operation" : "replace",
78               "target" : "/network-topology:node[network-topology:node-id='%s']",
79               "value" : {
80                 "node" : [
81                   {
82                     "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
89                   }
90                 ]
91               }
92             }
93         """
94             % (name, name, name, device_ipaddress, device_port)
95         )
96
97     data = """
98     {
99       "ietf-yang-patch:yang-patch" : {
100         "patch-id" : "csit-%s",
101         "edit" : [
102     """ % str(
103         uuid4()
104     )
105
106     # TODO: I bet there is a fancier way to write this
107     it = iter(edits)
108     cur = next(it)
109     while True:
110         data = data + cur
111         nxt = next(it, None)
112         if nxt is None:
113             break
114         data += ", "
115         cur = nxt
116
117     data += """]
118       }
119     }"""
120
121     resp = patch(
122         url=restconf_url
123         + """/data/network-topology:network-topology/topology=topology-netconf""",
124         headers={
125             "Content-Type": "application/yang-patch+json",
126             "Accept": "application/yang-data+json",
127             "User-Agent": "csit agent",
128         },
129         data=data,
130         # FIXME: do not hard-code credentials here
131         auth=("admin", "admin"),
132     )
133
134     resp.raise_for_status()
135     status = resp.json()
136     # FIXME: validate response
137     #  {
138     #    "ietf-yang-patch:yang-patch-status" : {
139     #      "patch-id" : "add-songs-patch-2",
140     #      "ok" : [null]
141     #    }
142     #  }
143
144     #  {
145     #    "ietf-yang-patch:yang-patch-status" : {
146     #      "patch-id" : "add-songs-patch",
147     #      "edit-status" : {
148     #        "edit" : [
149     #          {
150     #            "edit-id" : "edit1",
151     #            "errors" : {
152     #              "error" : [
153     #                {
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']",
160     #                  "error-message":
161     #                    "Data already exists; cannot be created"
162     #                }
163     #              ]
164     #            }
165     #          }
166     #        ]
167     #      }
168     #    }
169     #  }
170
171     return device_names
172
173
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."""
176
177     info("Awaiting connection of %s", device_names)
178     deadline = time() + deadline_seconds
179     names = set(device_names)
180     connected = set()
181
182     while time() < deadline:
183         resp = get(
184             url=restconf_url
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"),
189         )
190
191         # FIXME: also check for 409 might be okay?
192         resp.raise_for_status()
193
194         if "node" not in resp.json()["network-topology:topology"][0]:
195             sleep(1)
196             continue
197
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)
203
204             if name in names:
205                 if status == "connected":
206                     if name not in connected:
207                         debug("Device %s connected", name)
208                         connected.add(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)
213
214         if len(connected) == len(names):
215             return
216
217         sleep(1)
218
219     raise Exception("Timed out waiting for %s to connect" % names.difference(connected))
220
221
222 def main(args):
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",
229             device_port=17830,
230             device_count=int(args[1]),
231         )
232         print(names)
233     elif args[0] == "await":
234         await_devices_connected(
235             restconf_url="http://127.0.0.1:8181/rests",
236             deadline_seconds=5,
237             device_names=args[1:],
238         )
239     else:
240         raise Exception("Unhandled argument %s" % args[0])
241
242
243 if __name__ == "__main__":
244     # i.e. main does not depend on name of the binary
245     main(argv[1:])