add ssh capability and an example on how to execute ssh commands on a newly started...
[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         self.ipv4 = None
60         logging.info('initialized experiment {} based on profile {} under project {}'.format(experiment_name,
61                                                                                              profile_name,
62                                                                                              project_name))
63
64     def start_and_wait(self):
65         """Start the experiment and wait for READY or FAILED status."""
66         logging.info('starting experiment {}'.format(self.experiment_name))
67         rval, response = prpc.start_experiment(self.experiment_name,
68                                                self.project_name,
69                                                self.profile_name)
70         if rval == prpc.RESPONSE_SUCCESS:
71             self._get_status()
72
73             poll_count = 0
74             logging.info('self.still_provisioning: {}'.format(self.still_provisioning))
75             while self.still_provisioning and poll_count < self._poll_count_max:
76                 logging.info('waiting for provision process done')
77                 self._get_status()
78                 time.sleep(self.POLL_INTERVAL_S)
79         else:
80             self.status = self.EXPERIMENT_FAILED
81             logging.info(response)
82
83         return self.status
84
85     def terminate(self):
86         """Terminate the experiment. All allocated resources will be released."""
87         logging.info('terminating experiment {}'.format(self.experiment_name))
88         rval, response = prpc.terminate_experiment(self.project_name, self.experiment_name)
89         if rval == prpc.RESPONSE_SUCCESS:
90             self.status = self.EXPERIMENT_NULL
91         else:
92             logging.error('failed to terminate experiment')
93             logging.error('output {}'.format(response['output']))
94
95         return self.status
96
97     def _get_manifests(self):
98         """Get experiment manifests, translate to list of dicts."""
99         rval, response = prpc.get_experiment_manifests(self.project_name,
100                                                        self.experiment_name)
101         if rval == prpc.RESPONSE_SUCCESS:
102             response_json = json.loads(response['output'])
103             self._manifests = [xmltodict.parse(response_json[key]) for key in response_json.keys()]
104             logging.info('got manifests')
105         else:
106             logging.error('failed to get manifests')
107
108         return self
109
110     def _parse_manifests(self):
111         """Parse experiment manifests and add nodes to lookup table."""
112         for manifest in self._manifests:
113             logging.info('parsed manifest:{}'.format(manifest))
114             nodes = manifest['rspec']['node']
115             logging.info('parsed manifest nodes:{}'.format(nodes))
116             client_id = nodes['@client_id']
117             logging.info('parsed manifest client_id:{}'.format(client_id))
118             host = nodes['host']
119             logging.info('parsed manifest host:{}'.format(host))
120             hostname = host['@name']
121             logging.info('parsed manifest hostname:{}'.format(hostname))
122             self.ipv4 = host['@ipv4']
123             logging.info('parsed manifest ipv4:{}'.format(self.ipv4))
124             for node in nodes:
125                 logging.info('parsed manifest node:{}'.format(node))
126                 # only need to add nodes with public IP addresses for now
127 #                try:
128 #                    hostname = node['host']['@name']
129 #                    ipv4 = node['host']['@ipv4']
130 #                    client_id = nodes['@client_id']
131 #                    self.nodes[client_id] = Node(client_id=client_id, ip_address=ipv4,
132 #                                                 hostname=hostname)
133 #                except KeyError:
134 #                    pass
135 #
136         return self
137
138     def _get_status(self):
139         """Get experiment status and update local state. If the experiment is ready, get
140         and parse the associated manifests.
141
142         """
143         rval, response = prpc.get_experiment_status(self.project_name,
144                                                     self.experiment_name)
145         if rval == prpc.RESPONSE_SUCCESS:
146             output = response['output']
147             #if output == 'Status: ready\n':
148             if "ready" in output:
149                 self.status = self.EXPERIMENT_READY
150                 self._get_manifests()._parse_manifests()
151             #elif output == 'Status: provisioning\n':
152             elif "provisioning" in output:
153                 self.status = self.EXPERIMENT_PROVISIONING
154             #elif output == 'Status: provisioned\n':
155             elif "provisioned" in output:
156                 self.status = self.EXPERIMENT_PROVISIONED
157             #elif output == 'Status: failed\n':
158             elif "failed" in output:
159                 self.status = self.EXPERIMENT_FAILED
160
161             logging.info('status is {}'.format(self.status))
162             self.still_provisioning = self.status in [self.EXPERIMENT_PROVISIONING,
163                                                       self.EXPERIMENT_PROVISIONED]
164             logging.info('experiment status is {}'.format(output.strip()))
165         else:
166             logging.error('failed to get experiment status')
167
168         return self
169
170
171 class Node:
172     """Represents a node on the Powder platform. Holds an SSHConnection instance for
173     interacting with the node.
174
175     Attributes:
176         client_id (str): Matches the id defined for the node in the Powder profile.
177         ip_address (str): The public IP address of the node.
178         hostname (str): The hostname of the node.
179         ssh (SSHConnection): For interacting with the node via ssh through pexpect.
180
181     """
182     #def __init__(self, client_id, ip_address, hostname):
183     def __init__(self, ip_address):    
184         #self.client_id = client_id
185         self.ip_address = ip_address
186         #self.hostname = hostname
187         self.ssh = pssh.SSHConnection(ip_address=self.ip_address)