9 import powder.rpc as prpc
10 import powder.ssh as pssh
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
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.
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
35 EXPERIMENT_NOT_STARTED = 0
36 EXPERIMENT_PROVISIONING = 1
37 EXPERIMENT_PROVISIONED = 2
43 PROVISION_TIMEOUT_S = 1800
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))
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
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,
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,
69 if rval == prpc.RESPONSE_SUCCESS:
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')
77 time.sleep(self.POLL_INTERVAL_S)
79 self.status = self.EXPERIMENT_FAILED
80 logging.info(response)
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
91 logging.error('failed to terminate experiment')
92 logging.error('output {}'.format(response['output']))
96 def _get_manifests(self):
97 """Get experiment manifests, translate to list of dicts."""
98 rval, response = prpc.get_experiment_manifests(self.project_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')
105 logging.error('failed to get manifests')
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))
118 logging.info('parsed manifest host:{}'.format(host))
119 hostname = host['@name']
120 logging.info('parsed manifest hostname:{}'.format(hostname))
122 logging.info('parsed manifest ipv4:{}'.format(ipv4))
124 logging.info('parsed manifest node:{}'.format(node))
125 # only need to add nodes with public IP addresses for now
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,
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.
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
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()))
165 logging.error('failed to get experiment status')
171 """Represents a node on the Powder platform. Holds an SSHConnection instance for
172 interacting with the node.
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.
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)