Add support to programmably start a profile on the POWDER testbed, which can in turn...
[it/test.git] / XTesting / powder-control / powder / experiment.py
diff --git a/XTesting/powder-control/powder/experiment.py b/XTesting/powder-control/powder/experiment.py
new file mode 100644 (file)
index 0000000..2aa8f9d
--- /dev/null
@@ -0,0 +1,185 @@
+#!/usr/bin/env python3
+import json
+import logging
+import sys
+import time
+
+import xmltodict
+
+import powder.rpc as prpc
+import powder.ssh as pssh
+
+
+class PowderExperiment:
+    """Represents a single powder experiment. Can be used to start, interact with,
+    and terminate the experiment. After an experiment is ready, this object
+    holds references to the nodes in the experiment, which can be interacted
+    with via ssh.
+
+    Args:
+        experiment_name (str): A name for the experiment. Must be less than 16 characters.
+        project_name (str): The name of the Powder Project associated with the experiment.
+        profile_name (str): The name of an existing Powder profile you want to use for the experiment.
+
+    Attributes:
+        status (int): Represents the last known status of the experiment as
+            retrieved from the Powder RPC servqer.
+        nodes (dict of str: Node): A lookup table mapping node ids to Node instances
+            in the experiment.
+        experiment_name (str)
+        project_name (str)
+        profile_name (str)
+
+    """
+
+    EXPERIMENT_NOT_STARTED = 0
+    EXPERIMENT_PROVISIONING = 1
+    EXPERIMENT_PROVISIONED = 2
+    EXPERIMENT_READY = 3
+    EXPERIMENT_FAILED = 4
+    EXPERIMENT_NULL = 5
+
+    POLL_INTERVAL_S = 20
+    PROVISION_TIMEOUT_S = 1800
+    MAX_NAME_LENGTH = 16
+
+    def __init__(self, experiment_name, project_name, profile_name):
+        if len(experiment_name) > 16:
+            logging.error('Experiment name {} is too long (cannot exceed {} characters)'.format(experiment_name,
+                                                                                                self.MAX_NAME_LENGTH))
+            sys.exit(1)
+
+        self.experiment_name = experiment_name
+        self.project_name = project_name
+        self.profile_name = profile_name
+        self.status = self.EXPERIMENT_NOT_STARTED
+        self.nodes = dict()
+        self._manifests = None
+        self._poll_count_max = self.PROVISION_TIMEOUT_S // self.POLL_INTERVAL_S
+        logging.info('initialized experiment {} based on profile {} under project {}'.format(experiment_name,
+                                                                                             profile_name,
+                                                                                             project_name))
+
+    def start_and_wait(self):
+        """Start the experiment and wait for READY or FAILED status."""
+        logging.info('starting experiment {}'.format(self.experiment_name))
+        rval, response = prpc.start_experiment(self.experiment_name,
+                                               self.project_name,
+                                               self.profile_name)
+        if rval == prpc.RESPONSE_SUCCESS:
+            self._get_status()
+
+            poll_count = 0
+            logging.info('self.still_provisioning: {}'.format(self.still_provisioning))
+            while self.still_provisioning and poll_count < self._poll_count_max:
+                logging.info('waiting for provision process done')
+                self._get_status()
+                time.sleep(self.POLL_INTERVAL_S)
+        else:
+            self.status = self.EXPERIMENT_FAILED
+            logging.info(response)
+
+        return self.status
+
+    def terminate(self):
+        """Terminate the experiment. All allocated resources will be released."""
+        logging.info('terminating experiment {}'.format(self.experiment_name))
+        rval, response = prpc.terminate_experiment(self.project_name, self.experiment_name)
+        if rval == prpc.RESPONSE_SUCCESS:
+            self.status = self.EXPERIMENT_NULL
+        else:
+            logging.error('failed to terminate experiment')
+            logging.error('output {}'.format(response['output']))
+
+        return self.status
+
+    def _get_manifests(self):
+        """Get experiment manifests, translate to list of dicts."""
+        rval, response = prpc.get_experiment_manifests(self.project_name,
+                                                       self.experiment_name)
+        if rval == prpc.RESPONSE_SUCCESS:
+            response_json = json.loads(response['output'])
+            self._manifests = [xmltodict.parse(response_json[key]) for key in response_json.keys()]
+            logging.info('got manifests')
+        else:
+            logging.error('failed to get manifests')
+
+        return self
+
+    def _parse_manifests(self):
+        """Parse experiment manifests and add nodes to lookup table."""
+        for manifest in self._manifests:
+            logging.info('parsed manifest:{}'.format(manifest))
+            nodes = manifest['rspec']['node']
+            logging.info('parsed manifest nodes:{}'.format(nodes))
+            client_id = nodes['@client_id']
+            logging.info('parsed manifest client_id:{}'.format(client_id))
+            host = nodes['host']
+            logging.info('parsed manifest host:{}'.format(host))
+            hostname = host['@name']
+            logging.info('parsed manifest hostname:{}'.format(hostname))
+            ipv4 = host['@ipv4']
+            logging.info('parsed manifest ipv4:{}'.format(ipv4))
+            for node in nodes:
+                logging.info('parsed manifest node:{}'.format(node))
+                # only need to add nodes with public IP addresses for now
+#                try:
+#                    hostname = node['host']['@name']
+#                    ipv4 = node['host']['@ipv4']
+#                    client_id = nodes['@client_id']
+#                    self.nodes[client_id] = Node(client_id=client_id, ip_address=ipv4,
+#                                                 hostname=hostname)
+#                except KeyError:
+#                    pass
+#
+        return self
+
+    def _get_status(self):
+        """Get experiment status and update local state. If the experiment is ready, get
+        and parse the associated manifests.
+
+        """
+        rval, response = prpc.get_experiment_status(self.project_name,
+                                                    self.experiment_name)
+        if rval == prpc.RESPONSE_SUCCESS:
+            output = response['output']
+            #if output == 'Status: ready\n':
+            if "ready" in output:
+                self.status = self.EXPERIMENT_READY
+                self._get_manifests()._parse_manifests()
+            #elif output == 'Status: provisioning\n':
+            elif "provisioning" in output:
+                self.status = self.EXPERIMENT_PROVISIONING
+            #elif output == 'Status: provisioned\n':
+            elif "provisioned" in output:
+                self.status = self.EXPERIMENT_PROVISIONED
+            #elif output == 'Status: failed\n':
+            elif "failed" in output:
+                self.status = self.EXPERIMENT_FAILED
+
+            logging.info('status is {}'.format(self.status))
+            self.still_provisioning = self.status in [self.EXPERIMENT_PROVISIONING,
+                                                      self.EXPERIMENT_PROVISIONED]
+            logging.info('experiment status is {}'.format(output.strip()))
+        else:
+            logging.error('failed to get experiment status')
+
+        return self
+
+
+class Node:
+    """Represents a node on the Powder platform. Holds an SSHConnection instance for
+    interacting with the node.
+
+    Attributes:
+        client_id (str): Matches the id defined for the node in the Powder profile.
+        ip_address (str): The public IP address of the node.
+        hostname (str): The hostname of the node.
+        ssh (SSHConnection): For interacting with the node via ssh through pexpect.
+
+    """
+    def __init__(self, client_id, ip_address, hostname):
+        self.client_id = client_id
+        self.ip_address = ip_address
+        self.hostname = hostname
+        self.ssh = pssh.SSHConnection(ip_address=self.ip_address)