+#!/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)