Add TopologyNetconfNodes.py library 96/97196/32
authorRobert Varga <robert.varga@pantheon.tech>
Thu, 12 Aug 2021 20:21:56 +0000 (22:21 +0200)
committerLuis Gomez <ecelgp@gmail.com>
Sat, 28 Aug 2021 00:51:04 +0000 (00:51 +0000)
Our current method of configuring netconf topology devices does not
really scale in terms of speed and memory footprint.

Add TopologyNetconfNodes.py, which provides a simple library to
configure and ascertain connection establishment in a scalable way --
from the perspective of RF, both actions are single keywords which do
not produce much in terms of output.

The configuration part relies on RFC8072 YANG Patch to introduce any
number of devices through a single request.

The keyword to await connection polls NETCONF topology for device status
and loops until the deadline passes or all requested devices are found
to have been connected.

// FIXME: pass down authentication

JIRA: INTTEST-125
Change-Id: I1661d84a6535abff45cbd346f54e996a4d757b33
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
Signed-off-by: Tomas Cere <tomas.cere@pantheon.tech>
csit/libraries/TopologyNetconfNodes.py [new file with mode: 0644]
csit/suites/netconf/scale/max_devices.robot

diff --git a/csit/libraries/TopologyNetconfNodes.py b/csit/libraries/TopologyNetconfNodes.py
new file mode 100644 (file)
index 0000000..0dd5fb7
--- /dev/null
@@ -0,0 +1,245 @@
+"""
+   Utility library for configuring NETCONF topology node to connect to network elements. This operates
+   on the legacy network-topology model, with topology-id="topology-netconf".
+"""
+
+from logging import debug, info
+from requests import get, patch
+from sys import argv
+from time import sleep, time
+from uuid import uuid4
+
+
+def configure_device_range(
+    restconf_url,
+    device_name_prefix,
+    device_ipaddress,
+    device_port,
+    device_count,
+    first_device_id=1,
+):
+    """Generate device_count names in format "$device_name_prefix-$i" and configure them into NETCONF topology at specified RESTCONF URL.
+    For example:
+
+       configure_device_range("http://127.0.0.1:8181/rests", "example", "127.0.0.1", 1730, 5)
+
+    would configure devices "example-0" through "example-4" to connect to 127.0.0.0:1730.
+
+       configure_device_range("http://127.0.0.1:8181/rests", "example", "127.0.0.1", 1720, 5, 5)
+
+    would configure devices "example-5" through "example-9" to connect to 127.0.0.0:1720.
+
+    This method assumes RFC8040 with RFC7952 encoding and support for RFC8072 (YANG patch). Payload it generates looks roughly like this:
+    {
+       "ietf-yang-patch:yang-patch" : {
+          "patch-id" : "test"
+          "edit" : [
+             {
+                "edit-id" : "test-edit",
+                "operation" : "replace",
+                "target" : "/node=test-node",
+                "value" : {
+                   "node" : [
+                      {
+                         "node-id" : "test-node"
+                         "netconf-node-topology:host" : "127.0.0.1",
+                         "netconf-node-topology:port" : 17830,
+                         "netconf-node-topology:username" : "admin",
+                         "netconf-node-topology:password" : "topsecret",
+                         "netconf-node-topology:keepalive-delay" : 0,
+                      }
+                   ]
+                }
+             }
+          ],
+       }
+    }
+    """
+
+    info(
+        "Configure %s devices starting from %s (at %s:%s)",
+        device_count,
+        first_device_id,
+        device_ipaddress,
+        device_port,
+    )
+
+    device_names = []
+    edits = []
+
+    for i in range(first_device_id, first_device_id + device_count):
+        name = "{}-{}".format(device_name_prefix, i)
+        device_names.append(name)
+        edits.append(
+            """
+            {
+              "edit-id" : "node-%s",
+              "operation" : "replace",
+              "target" : "/network-topology:node[network-topology:node-id='%s']",
+              "value" : {
+                "node" : [
+                  {
+                    "node-id" : "%s",
+                    "netconf-node-topology:host" : "%s",
+                    "netconf-node-topology:port" : %s,
+                    "netconf-node-topology:username" : "admin",
+                    "netconf-node-topology:password" : "topsecret",
+                    "netconf-node-topology:tcp-only" : false,
+                    "netconf-node-topology:keepalive-delay" : 0
+                  }
+                ]
+              }
+            }
+        """
+            % (name, name, name, device_ipaddress, device_port)
+        )
+
+    data = """
+    {
+      "ietf-yang-patch:yang-patch" : {
+        "patch-id" : "csit-%s",
+        "edit" : [
+    """ % str(
+        uuid4()
+    )
+
+    # TODO: I bet there is a fancier way to write this
+    it = iter(edits)
+    cur = next(it)
+    while True:
+        data = data + cur
+        nxt = next(it, None)
+        if nxt is None:
+            break
+        data += ", "
+        cur = nxt
+
+    data += """]
+      }
+    }"""
+
+    resp = patch(
+        url=restconf_url
+        + """/data/network-topology:network-topology/topology=topology-netconf""",
+        headers={
+            "Content-Type": "application/yang-patch+json",
+            "Accept": "application/yang-data+json",
+            "User-Agent": "csit agent",
+        },
+        data=data,
+        # FIXME: do not hard-code credentials here
+        auth=("admin", "admin"),
+    )
+
+    resp.raise_for_status()
+    status = resp.json()
+    # FIXME: validate response
+    #  {
+    #    "ietf-yang-patch:yang-patch-status" : {
+    #      "patch-id" : "add-songs-patch-2",
+    #      "ok" : [null]
+    #    }
+    #  }
+
+    #  {
+    #    "ietf-yang-patch:yang-patch-status" : {
+    #      "patch-id" : "add-songs-patch",
+    #      "edit-status" : {
+    #        "edit" : [
+    #          {
+    #            "edit-id" : "edit1",
+    #            "errors" : {
+    #              "error" : [
+    #                {
+    #                  "error-type": "application",
+    #                  "error-tag": "data-exists",
+    #                  "error-path": "/example-jukebox:jukebox/library\
+    #                     /artist[name='Foo Fighters']\
+    #                     /album[name='Wasting Light']\
+    #                     /song[name='Bridge Burning']",
+    #                  "error-message":
+    #                    "Data already exists; cannot be created"
+    #                }
+    #              ]
+    #            }
+    #          }
+    #        ]
+    #      }
+    #    }
+    #  }
+
+    return device_names
+
+
+def await_devices_connected(restconf_url, device_names, deadline_seconds):
+    """Await all specified devices to become connected in NETCONF topology at specified RESTCONF URL."""
+
+    info("Awaiting connection of %s", device_names)
+    deadline = time() + deadline_seconds
+    names = set(device_names)
+    connected = set()
+
+    while time() < deadline:
+        resp = get(
+            url=restconf_url
+            + """/data/network-topology:network-topology/topology=topology-netconf?content=nonconfig""",
+            headers={"Accept": "application/yang-data+json"},
+            # FIXME: do not hard-code credentials here
+            auth=("admin", "admin"),
+        )
+
+        # FIXME: also check for 409 might be okay?
+        resp.raise_for_status()
+
+        if "node" not in resp.json()["network-topology:topology"][0]:
+            sleep(1)
+            continue
+
+        # Check all reported nodes
+        for node in resp.json()["network-topology:topology"][0]["node"]:
+            name = node["node-id"]
+            status = node["netconf-node-topology:connection-status"]
+            debug("Evaluating %s status %s", name, status)
+
+            if name in names:
+                if status == "connected":
+                    if name not in connected:
+                        debug("Device %s connected", name)
+                        connected.add(name)
+                elif name in connected:
+                    # also remove from connected in case we switched from
+                    # connected on a device from previous iteration
+                    connected.remove(name)
+
+        if len(connected) == len(names):
+            return
+
+        sleep(1)
+
+    raise Exception("Timed out waiting for %s to connect" % names.difference(connected))
+
+
+def main(args):
+    # FIXME: add proper option parsing
+    if args[0] == "configure":
+        names = configure_device_range(
+            restconf_url="http://127.0.0.1:8181/rests",
+            device_name_prefix="example",
+            device_ipaddress="127.0.0.1",
+            device_port=17830,
+            device_count=int(args[1]),
+        )
+        print(names)
+    elif args[0] == "await":
+        await_devices_connected(
+            restconf_url="http://127.0.0.1:8181/rests",
+            deadline_seconds=5,
+            device_names=args[1:],
+        )
+    else:
+        raise Exception("Unhandled argument %s" % args[0])
+
+
+if __name__ == "__main__":
+    # i.e. main does not depend on name of the binary
+    main(argv[1:])
index 8888c24f646a217d291e96c6d16a6f809d4a03ae..d2853ef6fe2911b91ad1edc6ed21f51185634041 100644 (file)
@@ -16,6 +16,7 @@ Test Setup        SetupUtils.Setup_Test_With_Logging_And_Without_Fast_Failing
 Library           Collections
 Library           String
 Library           SSHLibrary    timeout=1000s
+Library           ../../../libraries/TopologyNetconfNodes.py
 Resource          ../../../libraries/KarafKeywords.robot
 Resource          ../../../libraries/NetconfKeywords.robot
 Resource          ../../../libraries/SetupUtils.robot
@@ -50,24 +51,21 @@ Find Max Netconf Devices
     ${INSTALL_TESTTOOL} =    Set Variable If    '${IS_KARAF_APPL}' == 'False'    False    True
     ${TESTTOOL_EXECUTABLE} =    Set Variable If    '${IS_KARAF_APPL}' == 'False'    ${NETCONF_FILENAME}    ${EMPTY}
     ${SCHEMAS} =    Set Variable If    '${IS_KARAF_APPL}' == 'False'    ${CURDIR}/../../../variables/netconf/CRUD/schemas    ${schema_dir}
+    ${restconf_url} =    BuiltIn.Set_Variable    http://${ODL_SYSTEM_IP}:${RESTCONFPORT}/rests
+    ${device_names} =    BuiltIn.Set_Variable    []
     FOR    ${devices}    IN RANGE    ${start}    ${stop+1}    ${increment}
         ${timeout} =    BuiltIn.Evaluate    ${devices}*${TIMEOUT_FACTOR}
         ${timeout} =    Set Variable If    ${timeout} > ${MIN_CONNECT_TIMEOUT}    ${timeout}    ${MIN_CONNECT_TIMEOUT}
         Log To Console    Starting Iteration with ${devices} devices
         Run Keyword If    "${INSTALL_TESTTOOL}"=="True"    NetconfKeywords.Install_And_Start_Testtool    debug=false    schemas=${schema_dir}    device-count=${devices}    log_response=False
         ...    ELSE    NetconfKeywords.Start_Testtool    ${TESTTOOL_EXECUTABLE}    debug=false    schemas=${SCHEMAS}    device-count=${devices}    log_response=False
-        ${status}    ${result} =    Run Keyword And Ignore Error    NetconfKeywords.Perform_Operation_On_Each_Device    NetconfKeywords.Configure_Device    timeout=${timeout}
-        Exit For Loop If    '${status}' == 'FAIL'
-        ${status}    ${result} =    Run Keyword And Ignore Error    NetconfKeywords.Perform_Operation_On_Each_Device    NetconfKeywords.Wait_Connected    timeout=${timeout}    log_response=False
-        Exit For Loop If    '${status}' == 'FAIL'
+        ${devices_to_configure} =    BuiltIn.Evaluate    ${devices} - len(${device_names})
+        ${first_id} =    BuiltIn.Evaluate    len(${device_names}) + 1
+        ${device_names} =    TopologyNetconfNodes.Configure Device Range    restconf_url=${restconf_url}    device_name_prefix=${DEVICE_NAME_BASE}
+        ...    device_ipaddress=${TOOLS_SYSTEM_IP}    device_port=17830    device_count=${devices_to_configure}    first_device_id=${first_id}
+        TopologyNetconfNodes.Await Devices Connected    restconf_url=${restconf_url}    device_names=${device_names}    deadline_seconds=${timeout}
         ${status}    ${result} =    Run Keyword And Ignore Error    Issue_Requests_On_Devices    ${TOOLS_SYSTEM_IP}    ${devices}    ${NUM_WORKERS}
         Exit For Loop If    '${status}' == 'FAIL'
-        ${status}    ${result} =    Run Keyword And Ignore Error    NetconfKeywords.Perform_Operation_On_Each_Device    NetconfKeywords.Wait_Connected    timeout=${timeout}    log_response=False
-        Exit For Loop If    '${status}' == 'FAIL'
-        ${status}    ${result} =    Run Keyword And Ignore Error    NetconfKeywords.Perform_Operation_On_Each_Device    NetconfKeywords.Deconfigure_Device    timeout=${timeout}    log_response=False
-        Exit For Loop If    '${status}' == 'FAIL'
-        ${status}    ${result} =    Run Keyword And Ignore Error    NetconfKeywords.Perform_Operation_On_Each_Device    Check_Device_Deconfigured    timeout=${timeout}    log_response=False
-        Exit For Loop If    '${status}' == 'FAIL'
         ${maximum_devices} =    Set Variable    ${devices}
         NetconfKeywords.Stop_Testtool
     END