FIX: Topology reservation
[csit.git] / resources / tools / scripts / topo_reservation.py
index 4b5ed24..e7e1ff6 100755 (executable)
@@ -1,6 +1,6 @@
-#!/usr/bin/env python
+#!/usr/bin/env python2
 
-# Copyright (c) 2016 Cisco and/or its affiliates.
+# Copyright (c) 2019 Cisco and/or its affiliates.
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
 # You may obtain a copy of the License at:
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-"""This script provides simple reservation mechanism to avoid
-   simultaneous use of nodes listed in topology file.
-   As source of truth is used DUT1 node from the topology file."""
+"""Script managing reservation and un-reservation of testbeds.
+
+This script provides simple reservation mechanism to avoid
+simultaneous use of nodes listed in topology file.
+As source of truth, TG node from the topology file is used.
+"""
 
 import sys
 import argparse
-from resources.libraries.python.ssh import SSH
-from yaml import load
+import yaml
+
+from resources.libraries.python.ssh import exec_cmd
+
 
 RESERVATION_DIR = "/tmp/reservation_dir"
+RESERVATION_NODE = "TG"
+
+
+def diag_cmd(node, cmd):
+    """Execute cmd, print cmd and stdout, ignore stderr and rc; return None.
+
+    :param node: Node object as parsed from topology file to execute cmd on.
+    :param cmd: Command to execute.
+    :type ssh: dict
+    :type cmd: str
+    """
+    print('+ {cmd}'.format(cmd=cmd))
+    _, stdout, _ = exec_cmd(node, cmd)
+    print(stdout)
+
 
 def main():
+    """Parse arguments, perform the action, write useful output, propagate RC.
+
+    If the intended action is cancellation, reservation dir is deleted.
+
+    If the intended action is reservation, the list is longer:
+    1. List contents of reservation dir.
+    2. List contents of test.url file in the dir.
+    3. Create reservation dir.
+    4. Touch file according to -r option.
+    From these 4 steps, 1 and 2 are performed always, their RC ignored.
+    RC of step 3 gives the overall result.
+    If the result is success, step 4 is executed without any output,
+    their RC is ignored.
+
+    The "run tag" as a filename is useful for admins accessing the testbed
+    via a graphical terminal, which does not allow copying of text,
+    as they need less keypresses to identify the test run holding the testbed.
+    Also, the listing shows timestamps, which is useful for both audiences.
+
+    This all assumes the target system accepts ssh connections.
+    If it does not, the caller probably wants to stop trying
+    to reserve this system. Therefore this script can return 3 different codes.
+    Return code 0 means the reservation was successful.
+    Return code 1 means the system is inaccessible (or similarly unsuitable).
+    Return code 2 means the system is accessible, but already reserved.
+    The reason unsuitable systems return 1 is because that is also the value
+    Python returns on encountering and unexcepted exception.
+    """
     parser = argparse.ArgumentParser()
     parser.add_argument("-t", "--topo", required=True,
                         help="Topology file")
     parser.add_argument("-c", "--cancel", help="Cancel reservation",
                         action="store_true")
+    parser.add_argument("-r", "--runtag", required=False, default="Unknown",
+                        help="Identifier for test run suitable as filename")
     args = parser.parse_args()
-    topology_file = args.topo
-    cancel_reservation = args.cancel
 
-    work_file = open(topology_file)
-    topology = load(work_file.read())['nodes']
+    with open(args.topo, "r") as topo_file:
+        topology = yaml.load(topo_file.read())['nodes']
 
-    #we are using DUT1 node because we expect DUT1 to be a linux host
-    #we don't use TG because we don't expect TG to be linux only host
+    # Even if TG is not guaranteed to be a Linux host,
+    # we are using it, because testing shows SSH access to DUT
+    # during test affects its performance (bursts of lost packets).
     try:
-        tg_node = topology["DUT1"]
+        node = topology[RESERVATION_NODE]
     except KeyError:
-        print "Topology file does not contain 'DUT1' node"
+        print("Topology file does not contain '{node}' node".
+              format(node=RESERVATION_NODE))
         return 1
 
-    ssh = SSH()
-    ssh.connect(tg_node)
-
-    #For system reservation we use mkdir it is an atomic operation and we can
-    #store additional data (time, client_ID, ..) within reservation directory
-    if cancel_reservation:
-        ret, _, err = ssh.exec_command("rm -r {}".format(RESERVATION_DIR))
-    else:
-        ret, _, err = ssh.exec_command("mkdir {}".format(RESERVATION_DIR))
-
-    if ret != 0:
-        print("{} unsuccessful:\n{}".
-              format(("Cancellation " if cancel_reservation else "Reservation"),
-                     err))
-    return ret
+    # For system reservation we use mkdir it is an atomic operation and we can
+    # store additional data (time, client_ID, ..) within reservation directory.
+    if args.cancel:
+        ret, _, err = exec_cmd(node, "rm -r {dir}".format(dir=RESERVATION_DIR))
+        if ret:
+            print("Cancellation unsuccessful:\n{err}".format(err=err))
+        return ret
+    # Before critical section, output can be outdated already.
+    print("Diagnostic commands:")
+    # -d and * are to supress "total <size>", see https://askubuntu.com/a/61190
+    diag_cmd(node, "ls --full-time -cd '{dir}'/*".format(dir=RESERVATION_DIR))
+    print("Attempting testbed reservation.")
+    # Entering critical section.
+    ret, _, _ = exec_cmd(node, "mkdir '{dir}'".format(dir=RESERVATION_DIR))
+    # Critical section is over.
+    if ret:
+        _, stdo, _ = exec_cmd(node, "ls '{dir}'/*".format(dir=RESERVATION_DIR))
+        print("Testbed already reserved by:\n{stdo}".format(stdo=stdo))
+        return 2
+    # Here the script knows it is the only owner of the testbed.
+    print("Reservation success, writing additional info to reservation dir.")
+    ret, _, err = exec_cmd(
+        node, "touch '{dir}/{runtag}'"\
+        .format(dir=RESERVATION_DIR, runtag=args.runtag))
+    if ret:
+        print("Writing test run info failed, but continuing anyway:\n{err}".
+              format(err=err))
+    return 0
+
 
 if __name__ == "__main__":
     sys.exit(main())