Add support to programmably start a profile on the POWDER testbed, which can in turn...
[it/test.git] / XTesting / powder-control / powder / experiment.py
1 #!/usr/bin/env python3
2 import json
3 import logging
4 import sys
5 import time
6
7 import xmltodict
8
9 import powder.rpc as prpc
10 import powder.ssh as pssh
11
12
13 class PowderExperiment:
14     """Represents a single powder experiment. Can be used to start, interact with,
15     and terminate the experiment. After an experiment is ready, this object
16     holds references to the nodes in the experiment, which can be interacted
17     with via ssh.
18
19     Args:
20         experiment_name (str): A name for the experiment. Must be less than 16 characters.
21         project_name (str): The name of the Powder Project associated with the experiment.
22         profile_name (str): The name of an existing Powder profile you want to use for the experiment.
23
24     Attributes:
25         status (int): Represents the last known status of the experiment as
26             retrieved from the Powder RPC servqer.
27         nodes (dict of str: Node): A lookup table mapping node ids to Node instances
28             in the experiment.
29         experiment_name (str)
30         project_name (str)
31         profile_name (str)
32
33     """
34
35     EXPERIMENT_NOT_STARTED = 0
36     EXPERIMENT_PROVISIONING = 1
37     EXPERIMENT_PROVISIONED = 2
38     EXPERIMENT_READY = 3
39     EXPERIMENT_FAILED = 4
40     EXPERIMENT_NULL = 5
41
42     POLL_INTERVAL_S = 20
43     PROVISION_TIMEOUT_S = 1800
44     MAX_NAME_LENGTH = 16
45
46     def __init__(self, experiment_name, project_name, profile_name):
47         if len(experiment_name) > 16:
48             logging.error('Experiment name {} is too long (cannot exceed {} characters)'.format(experiment_name,
49                                                                                                 self.MAX_NAME_LENGTH))
50             sys.exit(1)
51
52         self.experiment_name = experiment_name
53         self.project_name = project_name
54         self.profile_name = profile_name
55         self.status = self.EXPERIMENT_NOT_STARTED
56         self.nodes = dict()
57         self._manifests = None
58         self._poll_count_max = self.PROVISION_TIMEOUT_S // self.POLL_INTERVAL_S
59         logging.info('initialized experiment {} based on profile {} under project {}'.format(experiment_name,
60                                                                                              profile_name,
61                                                                                              project_name))
62
63     def start_and_wait(self):
64         """Start the experiment and wait for READY or FAILED status."""
65         logging.info('starting experiment {}'.format(self.experiment_name))
66         rval, response = prpc.start_experiment(self.experiment_name,
67                                                self.project_name,
68                                                self.profile_name)
69         if rval == prpc.RESPONSE_SUCCESS:
70             self._get_status()
71
72             poll_count = 0
73             logging.info('self.still_provisioning: {}'.format(self.still_provisioning))
74             while self.still_provisioning and poll_count < self._poll_count_max:
75                 logging.info('waiting for provision process done')
76                 self._get_status()
77                 time.sleep(self.POLL_INTERVAL_S)
78         else:
79             self.status = self.EXPERIMENT_FAILED
80             logging.info(response)
81
82         return self.status
83
84     def terminate(self):
85         """Terminate the experiment. All allocated resources will be released."""
86         logging.info('terminating experiment {}'.format(self.experiment_name))
87         rval, response = prpc.terminate_experiment(self.project_name, self.experiment_name)
88         if rval == prpc.RESPONSE_SUCCESS:
89             self.status = self.EXPERIMENT_NULL
90         else:
91             logging.error('failed to terminate experiment')
92             logging.error('output {}'.format(response['output']))
93
94         return self.status
95
96     def _get_manifests(self):
97         """Get experiment manifests, translate to list of dicts."""
98         rval, response = prpc.get_experiment_manifests(self.project_name,
99                                                        self.experiment_name)
100         if rval == prpc.RESPONSE_SUCCESS:
101             response_json = json.loads(response['output'])
102             self._manifests = [xmltodict.parse(response_json[key]) for key in response_json.keys()]
103             logging.info('got manifests')
104         else:
105             logging.error('failed to get manifests')
106
107         return self
108
109     def _parse_manifests(self):
110         """Parse experiment manifests and add nodes to lookup table."""
111         for manifest in self._manifests:
112             logging.info('parsed manifest:{}'.format(manifest))
113             nodes = manifest['rspec']['node']
114             logging.info('parsed manifest nodes:{}'.format(nodes))
115             client_id = nodes['@client_id']
116             logging.info('parsed manifest client_id:{}'.format(client_id))
117             host = nodes['host']
118             logging.info('parsed manifest host:{}'.format(host))
119             hostname = host['@name']
120             logging.info('parsed manifest hostname:{}'.format(hostname))
121             ipv4 = host['@ipv4']
122             logging.info('parsed manifest ipv4:{}'.format(ipv4))
123             for node in nodes:
124                 logging.info('parsed manifest node:{}'.format(node))
125                 # only need to add nodes with public IP addresses for now
126 #                try:
127 #                    hostname = node['host']['@name']
128 #                    ipv4 = node['host']['@ipv4']
129 #                    client_id = nodes['@client_id']
130 #                    self.nodes[client_id] = Node(client_id=client_id, ip_address=ipv4,
131 #                                                 hostname=hostname)
132 #                except KeyError:
133 #                    pass
134 #
135         return self
136
137     def _get_status(self):
138         """Get experiment status and update local state. If the experiment is ready, get
139         and parse the associated manifests.
140
141         """
142         rval, response = prpc.get_experiment_status(self.project_name,
143                                                     self.experiment_name)
144         if rval == prpc.RESPONSE_SUCCESS:
145             output = response['output']
146             #if output == 'Status: ready\n':
147             if "ready" in output:
148                 self.status = self.EXPERIMENT_READY
149                 self._get_manifests()._parse_manifests()
150             #elif output == 'Status: provisioning\n':
151             elif "provisioning" in output:
152                 self.status = self.EXPERIMENT_PROVISIONING
153             #elif output == 'Status: provisioned\n':
154             elif "provisioned" in output:
155                 self.status = self.EXPERIMENT_PROVISIONED
156             #elif output == 'Status: failed\n':
157             elif "failed" in output:
158                 self.status = self.EXPERIMENT_FAILED
159
160             logging.info('status is {}'.format(self.status))
161             self.still_provisioning = self.status in [self.EXPERIMENT_PROVISIONING,
162                                                       self.EXPERIMENT_PROVISIONED]
163             logging.info('experiment status is {}'.format(output.strip()))
164         else:
165             logging.error('failed to get experiment status')
166
167         return self
168
169
170 class Node:
171     """Represents a node on the Powder platform. Holds an SSHConnection instance for
172     interacting with the node.
173
174     Attributes:
175         client_id (str): Matches the id defined for the node in the Powder profile.
176         ip_address (str): The public IP address of the node.
177         hostname (str): The hostname of the node.
178         ssh (SSHConnection): For interacting with the node via ssh through pexpect.
179
180     """
181     def __init__(self, client_id, ip_address, hostname):
182         self.client_id = client_id
183         self.ip_address = ip_address
184         self.hostname = hostname
185         self.ssh = pssh.SSHConnection(ip_address=self.ip_address)