X-Git-Url: https://gerrit.o-ran-sc.org/r/gitweb?a=blobdiff_plain;f=XTesting%2Fpowder-control%2Fpowder%2Fexperiment.py;fp=XTesting%2Fpowder-control%2Fpowder%2Fexperiment.py;h=2aa8f9d6e42536df46362139afac0df73779a31e;hb=34a7a72a85ba9cf867a14ad353d15fbb28562e7b;hp=0000000000000000000000000000000000000000;hpb=5c5b9d9c60467d6f2d2c6a99d111045b8aea94f1;p=it%2Ftest.git diff --git a/XTesting/powder-control/powder/experiment.py b/XTesting/powder-control/powder/experiment.py new file mode 100644 index 0000000..2aa8f9d --- /dev/null +++ b/XTesting/powder-control/powder/experiment.py @@ -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)