comparison venv/lib/python2.7/site-packages/bioblend/cloudman/launch.py @ 0:d67268158946 draft

planemo upload commit a3f181f5f126803c654b3a66dd4e83a48f7e203b
author bcclaywell
date Mon, 12 Oct 2015 17:43:33 -0400
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:d67268158946
1 """
2 Setup and launch a CloudMan instance.
3 """
4 import datetime
5 import yaml
6
7 import boto
8 from boto.ec2.regioninfo import RegionInfo
9 from boto.exception import EC2ResponseError, S3ResponseError
10 from boto.s3.connection import OrdinaryCallingFormat, S3Connection, SubdomainCallingFormat
11 import six
12 from six.moves.http_client import HTTPConnection
13 from six.moves.urllib.parse import urlparse
14
15 import bioblend
16 from bioblend.util import Bunch
17
18 # Uncomment the following line if no logging from boto is desired
19 # bioblend.logging.getLogger('boto').setLevel(bioblend.logging.CRITICAL)
20 # Uncomment the following line if logging at the prompt is desired
21 # bioblend.set_stream_logger(__name__)
22
23
24 def instance_types(cloud_name='generic'):
25 """
26 Return a list of dictionaries containing details about the available
27 instance types for the given `cloud_name`.
28
29 :type cloud_name: str
30 :param cloud_name: A name of the cloud for which the list of instance
31 types will be returned. Valid values are: `aws`,
32 `nectar`, `generic`.
33
34 :rtype: list
35 :return: A list of dictionaries describing instance types. Each dict will
36 contain the following keys: `name`, `model`, and `description`.
37 """
38 instance_list = []
39 if cloud_name.lower() == 'aws':
40 instance_list.append({"model": "c3.large",
41 "name": "Compute optimized Large",
42 "description": "2 vCPU/4GB RAM"})
43 instance_list.append({"model": "c3.2xlarge",
44 "name": "Compute optimized 2xLarge",
45 "description": "8 vCPU/15GB RAM"})
46 instance_list.append({"model": "c3.8xlarge",
47 "name": "Compute optimized 8xLarge",
48 "description": "32 vCPU/60GB RAM"})
49 elif cloud_name.lower() in ['nectar', 'generic']:
50 instance_list.append({"model": "m1.small",
51 "name": "Small",
52 "description": "1 vCPU / 4GB RAM"})
53 instance_list.append({"model": "m1.medium",
54 "name": "Medium",
55 "description": "2 vCPU / 8GB RAM"})
56 instance_list.append({"model": "m1.large",
57 "name": "Large",
58 "description": "4 vCPU / 16GB RAM"})
59 instance_list.append({"model": "m1.xlarge",
60 "name": "Extra Large",
61 "description": "8 vCPU / 32GB RAM"})
62 instance_list.append({"model": "m1.xxlarge",
63 "name": "Extra-extra Large",
64 "description": "16 vCPU / 64GB RAM"})
65 return instance_list
66
67
68 class CloudManLauncher(object):
69 def __init__(self, access_key, secret_key, cloud=None):
70 """
71 Define the environment in which this instance of CloudMan will be launched.
72
73 Besides providing the credentials, optionally provide the ``cloud``
74 object. This object must define the properties required to establish a
75 `boto <https://github.com/boto/boto/>`_ connection to that cloud. See
76 this method's implementation for an example of the required fields.
77 Note that as long the as provided object defines the required fields,
78 it can really by implemented as anything (e.g., a Bunch, a database
79 object, a custom class). If no value for the ``cloud`` argument is
80 provided, the default is to use the Amazon cloud.
81 """
82 self.access_key = access_key
83 self.secret_key = secret_key
84 if cloud is None:
85 # Default to an EC2-compatible object
86 self.cloud = Bunch(id='1', # for compatibility w/ DB representation
87 name="Amazon",
88 cloud_type="ec2",
89 bucket_default="cloudman",
90 region_name="us-east-1",
91 region_endpoint="ec2.amazonaws.com",
92 ec2_port="",
93 ec2_conn_path="/",
94 cidr_range="",
95 is_secure=True,
96 s3_host="s3.amazonaws.com",
97 s3_port="",
98 s3_conn_path='/')
99 else:
100 self.cloud = cloud
101 self.ec2_conn = self.connect_ec2(self.access_key, self.secret_key, self.cloud)
102
103 def __repr__(self):
104 return "Cloud: {0}; acct ID: {1}".format(self.cloud.name, self.access_key)
105
106 def launch(self, cluster_name, image_id, instance_type, password,
107 kernel_id=None, ramdisk_id=None, key_name='cloudman_key_pair',
108 security_groups=['CloudMan'], placement='', **kwargs):
109 """
110 Check all the prerequisites (key pair and security groups) for
111 launching a CloudMan instance, compose the user data based on the
112 parameters specified in the arguments and the cloud properties as
113 defined in the object's ``cloud`` field.
114
115 For the current list of user data fields that can be provided via
116 ``kwargs``, see `<http://wiki.g2.bx.psu.edu/CloudMan/UserData>`_
117
118 Return a dict containing the properties and info with which an instance
119 was launched, namely: ``sg_names`` containing the names of the security
120 groups, ``kp_name`` containing the name of the key pair, ``kp_material``
121 containing the private portion of the key pair (*note* that this portion
122 of the key is available and can be retrieved *only* at the time the key
123 is created, which will happen only if no key with the name provided in
124 the ``key_name`` argument exists), ``rs`` containing the
125 `boto <https://github.com/boto/boto/>`_ ``ResultSet`` object,
126 ``instance_id`` containing the ID of a started instance, and
127 ``error`` containing an error message if there was one.
128 """
129 ret = {'sg_names': [],
130 'kp_name': '',
131 'kp_material': '',
132 'rs': None,
133 'instance_id': '',
134 'error': None}
135 # First satisfy the prerequisites
136 for sg in security_groups:
137 cmsg = self.create_cm_security_group(sg)
138 if cmsg['name']:
139 ret['sg_names'].append(cmsg['name'])
140 ret['error'] = cmsg['error']
141 if ret['error']:
142 return ret
143 kp_info = self.create_key_pair(key_name)
144 ret['error'] = kp_info['error']
145 if ret['error']:
146 return ret
147 ret['kp_name'] = kp_info['name']
148 ret['kp_material'] = kp_info['material']
149 # If not provided, try to find a placement
150 # TODO: Should placement always be checked? To make sure it's correct
151 # for existing clusters.
152 if not placement:
153 placement = self._find_placement(cluster_name)
154 if not placement:
155 # Let the cloud middleware assign a zone
156 placement = None
157 # Compose user data for launching an instance, ensuring we have the required fields
158 kwargs['access_key'] = self.access_key
159 kwargs['secret_key'] = self.secret_key
160 kwargs['cluster_name'] = cluster_name
161 kwargs['password'] = password
162 kwargs['cloud_name'] = self.cloud.name
163 ud = self._compose_user_data(kwargs)
164 # Now launch an instance
165 try:
166 rs = None
167 rs = self.ec2_conn.run_instances(image_id=image_id,
168 instance_type=instance_type,
169 key_name=key_name,
170 security_groups=security_groups,
171 user_data=ud,
172 kernel_id=kernel_id,
173 ramdisk_id=ramdisk_id,
174 placement=placement)
175 ret['rs'] = rs
176 except EC2ResponseError as e:
177 err_msg = "Problem launching an instance: {0} (code {1}; status {2})" \
178 .format(e.message, e.error_code, e.status)
179 bioblend.log.exception(err_msg)
180 ret['error'] = err_msg
181 return ret
182 else:
183 try:
184 bioblend.log.info("Launched an instance with ID %s" % rs.instances[0].id)
185 ret['instance_id'] = rs.instances[0].id
186 ret['instance_ip'] = rs.instances[0].ip_address
187 except Exception as e:
188 err_msg = "Problem with the launched instance object: {0} " \
189 "(code {1}; status {2})" \
190 .format(e.message, e.error_code, e.status)
191 bioblend.log.exception(err_msg)
192 ret['error'] = err_msg
193 return ret
194
195 def create_cm_security_group(self, sg_name='CloudMan'):
196 """
197 Create a security group with all authorizations required to run CloudMan.
198
199 If the group already exists, check its rules and add the missing ones.
200
201 :type sg_name: str
202 :param sg_name: A name for the security group to be created.
203
204 :rtype: dict
205 :return: A dictionary containing keys ``name`` (with the value being the
206 name of the security group that was created), ``error``
207 (with the value being the error message if there was an error
208 or ``None`` if no error was encountered), and ``ports``
209 (containing the list of tuples with port ranges that were
210 opened or attempted to be opened).
211
212 .. versionchanged:: 0.6.1
213 The return value changed from a string to a dict
214 """
215 ports = (('20', '21'), # FTP
216 ('22', '22'), # SSH
217 ('80', '80'), # Web UI
218 ('443', '443'), # SSL Web UI
219 ('8800', '8800'), # NodeJS Proxy for Galaxy IPython IE
220 ('9600', '9700'), # HTCondor
221 ('30000', '30100')) # FTP transfer
222 progress = {'name': None,
223 'error': None,
224 'ports': ports}
225 cmsg = None
226 # Check if this security group already exists
227 try:
228 sgs = self.ec2_conn.get_all_security_groups()
229 except EC2ResponseError as e:
230 err_msg = "Problem getting security groups: {0} (code {1}; status {2})" \
231 .format(e.message, e.error_code, e.status)
232 bioblend.log.exception(err_msg)
233 progress['error'] = err_msg
234 return progress
235 for sg in sgs:
236 if sg.name == sg_name:
237 cmsg = sg
238 bioblend.log.debug("Security group '%s' already exists; will "
239 "add authorizations next." % sg_name)
240 break
241 # If it does not exist, create security group
242 if cmsg is None:
243 bioblend.log.debug("Creating Security Group %s" % sg_name)
244 try:
245 cmsg = self.ec2_conn.create_security_group(sg_name, 'A security '
246 'group for CloudMan')
247 except EC2ResponseError as e:
248 err_msg = "Problem creating security group {0}: {1} (code {2}; " \
249 "status {3})" \
250 .format(sg_name, e.message, e.error_code, e.status)
251 bioblend.log.exception(err_msg)
252 progress['error'] = err_msg
253 if cmsg:
254 progress['name'] = cmsg.name
255 # Add appropriate authorization rules
256 # If these rules already exist, nothing will be changed in the SG
257 for port in ports:
258 try:
259 if not self.rule_exists(cmsg.rules, from_port=port[0], to_port=port[1]):
260 cmsg.authorize(ip_protocol='tcp', from_port=port[0], to_port=port[1], cidr_ip='0.0.0.0/0')
261 else:
262 bioblend.log.debug("Rule (%s:%s) already exists in the SG" % (port[0], port[1]))
263 except EC2ResponseError as e:
264 err_msg = "A problem adding security group authorizations: {0} " \
265 "(code {1}; status {2})" \
266 .format(e.message, e.error_code, e.status)
267 bioblend.log.exception(err_msg)
268 progress['error'] = err_msg
269 # Add ICMP (i.e., ping) rule required by HTCondor
270 try:
271 if not self.rule_exists(cmsg.rules, from_port='-1', to_port='-1', ip_protocol='icmp'):
272 cmsg.authorize(ip_protocol='icmp', from_port=-1, to_port=-1, cidr_ip='0.0.0.0/0')
273 else:
274 bioblend.log.debug("ICMP rule already exists in {0} SG.".format(sg_name))
275 except EC2ResponseError as e:
276 err_msg = "A problem with security ICMP rule authorization: {0} " \
277 "(code {1}; status {2})" \
278 .format(e.message, e.error_code, e.status)
279 bioblend.log.exception(err_msg)
280 progress['err_msg'] = err_msg
281 # Add rule that allows communication between instances in the same SG
282 g_rule_exists = False # A flag to indicate if group rule already exists
283 for rule in cmsg.rules:
284 for grant in rule.grants:
285 if grant.name == cmsg.name:
286 g_rule_exists = True
287 bioblend.log.debug("Group rule already exists in the SG.")
288 if g_rule_exists:
289 break
290 if not g_rule_exists:
291 try:
292 cmsg.authorize(src_group=cmsg)
293 except EC2ResponseError as e:
294 err_msg = "A problem with security group authorization: {0} " \
295 "(code {1}; status {2})" \
296 .format(e.message, e.error_code, e.status)
297 bioblend.log.exception(err_msg)
298 progress['err_msg'] = err_msg
299 bioblend.log.info("Done configuring '%s' security group" % cmsg.name)
300 return progress
301
302 def rule_exists(self, rules, from_port, to_port, ip_protocol='tcp', cidr_ip='0.0.0.0/0'):
303 """
304 A convenience method to check if an authorization rule in a security group
305 already exists.
306 """
307 for rule in rules:
308 if rule.ip_protocol == ip_protocol and rule.from_port == from_port and \
309 rule.to_port == to_port and cidr_ip in [ip.cidr_ip for ip in rule.grants]:
310 return True
311 return False
312
313 def create_key_pair(self, key_name='cloudman_key_pair'):
314 """
315 If a key pair with the provided ``key_name`` does not exist, create it.
316
317 :type sg_name: str
318 :param sg_name: A name for the key pair to be created.
319
320 :rtype: dict
321 :return: A dictionary containing keys ``name`` (with the value being the
322 name of the key pair that was created), ``error``
323 (with the value being the error message if there was an error
324 or ``None`` if no error was encountered), and ``material``
325 (containing the unencrypted PEM encoded RSA private key if the
326 key was created or ``None`` if the key already eixsted).
327
328 .. versionchanged:: 0.6.1
329 The return value changed from a tuple to a dict
330 """
331 progress = {'name': None,
332 'material': None,
333 'error': None}
334 kp = None
335 # Check if a key pair under the given name already exists. If it does not,
336 # create it, else return.
337 try:
338 kps = self.ec2_conn.get_all_key_pairs()
339 except EC2ResponseError as e:
340 err_msg = "Problem getting key pairs: {0} (code {1}; status {2})" \
341 .format(e.message, e.error_code, e.status)
342 bioblend.log.exception(err_msg)
343 progress['error'] = err_msg
344 return progress
345 for akp in kps:
346 if akp.name == key_name:
347 bioblend.log.info("Key pair '%s' already exists; reusing it." % key_name)
348 progress['name'] = akp.name
349 return progress
350 try:
351 kp = self.ec2_conn.create_key_pair(key_name)
352 except EC2ResponseError as e:
353 err_msg = "Problem creating key pair '{0}': {1} (code {2}; status {3})" \
354 .format(key_name, e.message, e.error_code, e.status)
355 bioblend.log.exception(err_msg)
356 progress['error'] = err_msg
357 return progress
358 bioblend.log.info("Created key pair '%s'" % kp.name)
359 progress['name'] = kp.name
360 progress['material'] = kp.material
361 return progress
362
363 def get_status(self, instance_id):
364 """
365 Check on the status of an instance. ``instance_id`` needs to be a
366 ``boto``-library copatible instance ID (e.g., ``i-8fehrdss``).If
367 ``instance_id`` is not provided, the ID obtained when launching
368 *the most recent* instance is used. Note that this assumes the instance
369 being checked on was launched using this class. Also note that the same
370 class may be used to launch multiple instances but only the most recent
371 ``instance_id`` is kept while any others will to be explicitly specified.
372
373 This method also allows the required ``ec2_conn`` connection object to be
374 provided at invocation time. If the object is not provided, credentials
375 defined for the class are used (ability to specify a custom ``ec2_conn``
376 helps in case of stateless method invocations).
377
378 Return a ``state`` dict containing the following keys: ``instance_state``,
379 ``public_ip``, ``placement``, and ``error``, which capture CloudMan's
380 current state. For ``instance_state``, expected values are: ``pending``,
381 ``booting``, ``running``, or ``error`` and represent the state of the
382 underlying instance. Other keys will return an empty value until the
383 ``instance_state`` enters ``running`` state.
384 """
385 ec2_conn = self.ec2_conn
386 rs = None
387 state = {'instance_state': "",
388 'public_ip': "",
389 'placement': "",
390 'error': ""}
391
392 # Make sure we have an instance ID
393 if instance_id is None:
394 err = "Missing instance ID, cannot check the state."
395 bioblend.log.error(err)
396 state['error'] = err
397 return state
398 try:
399 rs = ec2_conn.get_all_instances([instance_id])
400 if rs is not None:
401 inst_state = rs[0].instances[0].update()
402 public_ip = rs[0].instances[0].ip_address
403 state['public_ip'] = public_ip
404 if inst_state == 'running':
405 cm_url = "http://{dns}/cloud".format(dns=public_ip)
406 # Wait until the CloudMan URL is accessible to return the data
407 if self._checkURL(cm_url) is True:
408 state['instance_state'] = inst_state
409 state['placement'] = rs[0].instances[0].placement
410 else:
411 state['instance_state'] = 'booting'
412 else:
413 state['instance_state'] = inst_state
414 except Exception as e:
415 err = "Problem updating instance '%s' state: %s" % (instance_id, e)
416 bioblend.log.error(err)
417 state['error'] = err
418 return state
419
420 def get_clusters_pd(self, include_placement=True):
421 """
422 Return a list containing the *persistent data* of all existing clusters
423 associated with this account. If no clusters are found, return an empty
424 list. Each list element is a dictionary with the following keys:
425 ``cluster_name``, ``persistent_data``, ``bucket_name`` and, optionally,
426 ``placement``. ``persistent_data`` value is yet another dictionary
427 containing given cluster's persistent data.
428
429 .. versionadded:: 0.3
430 """
431 s3_conn = self.connect_s3(self.access_key, self.secret_key, self.cloud)
432 buckets = s3_conn.get_all_buckets()
433 clusters = []
434 for bucket in [b for b in buckets if b.name.startswith('cm-')]:
435 try:
436 # TODO: first lookup if persistent_data.yaml key exists
437 pd = bucket.get_key('persistent_data.yaml')
438 except S3ResponseError:
439 # This can fail for a number of reasons for non-us and/or CNAME'd buckets.
440 bioblend.log.exception("Problem fetching persistent_data.yaml from bucket %s" % bucket)
441 continue
442 if pd:
443 # We are dealing with a CloudMan bucket
444 pd_contents = pd.get_contents_as_string()
445 pd = yaml.load(pd_contents)
446 if 'cluster_name' in pd:
447 cluster_name = pd['cluster_name']
448 else:
449 for key in bucket.list():
450 if key.name.endswith('.clusterName'):
451 cluster_name = key.name.split('.clusterName')[0]
452 cluster = {'cluster_name': cluster_name,
453 'persistent_data': pd,
454 'bucket_name': bucket.name}
455 # Look for cluster's placement too
456 if include_placement:
457 placement = self._find_placement(cluster_name, cluster)
458 cluster['placement'] = placement
459 clusters.append(cluster)
460 return clusters
461
462 def get_cluster_pd(self, cluster_name):
463 """
464 Return *persistent data* (as a dict) associated with a cluster with the
465 given ``cluster_name``. If a cluster with the given name is not found,
466 return an empty dict.
467
468 .. versionadded:: 0.3
469 """
470 cluster = {}
471 clusters = self.get_clusters_pd()
472 for c in clusters:
473 if c['cluster_name'] == cluster_name:
474 cluster = c
475 break
476 return cluster
477
478 def connect_ec2(self, a_key, s_key, cloud=None):
479 """
480 Create and return an EC2-compatible connection object for the given cloud.
481
482 See ``_get_cloud_info`` method for more details on the requirements for
483 the ``cloud`` parameter. If no value is provided, the class field is used.
484 """
485 if cloud is None:
486 cloud = self.cloud
487 ci = self._get_cloud_info(cloud)
488 r = RegionInfo(name=ci['region_name'], endpoint=ci['region_endpoint'])
489 ec2_conn = boto.connect_ec2(aws_access_key_id=a_key,
490 aws_secret_access_key=s_key,
491 # api_version is needed for availability zone support for EC2
492 api_version='2012-06-01' if ci['cloud_type'] == 'ec2' else None,
493 is_secure=ci['is_secure'],
494 region=r,
495 port=ci['ec2_port'],
496 path=ci['ec2_conn_path'],
497 validate_certs=False)
498 return ec2_conn
499
500 def connect_s3(self, a_key, s_key, cloud=None):
501 """
502 Create and return an S3-compatible connection object for the given cloud.
503
504 See ``_get_cloud_info`` method for more details on the requirements for
505 the ``cloud`` parameter. If no value is provided, the class field is used.
506 """
507 if cloud is None:
508 cloud = self.cloud
509 ci = self._get_cloud_info(cloud)
510 if ci['cloud_type'] == 'amazon':
511 calling_format = SubdomainCallingFormat()
512 else:
513 calling_format = OrdinaryCallingFormat()
514 s3_conn = S3Connection(
515 aws_access_key_id=a_key, aws_secret_access_key=s_key,
516 is_secure=ci['is_secure'], port=ci['s3_port'], host=ci['s3_host'],
517 path=ci['s3_conn_path'], calling_format=calling_format)
518 return s3_conn
519
520 def _compose_user_data(self, user_provided_data):
521 """
522 A convenience method used to compose and properly format the user data
523 required when requesting an instance.
524
525 ``user_provided_data`` is the data provided by a user required to identify
526 a cluster and user other user requirements.
527 """
528 form_data = {}
529 # Do not include the following fields in the user data but do include
530 # any 'advanced startup fields' that might be added in the future
531 excluded_fields = ['sg_name', 'image_id', 'instance_id', 'kp_name',
532 'cloud', 'cloud_type', 'public_dns', 'cidr_range',
533 'kp_material', 'placement', 'flavor_id']
534 for key, value in six.iteritems(user_provided_data):
535 if key not in excluded_fields:
536 form_data[key] = value
537 # If the following user data keys are empty, do not include them in the request user data
538 udkeys = ['post_start_script_url', 'worker_post_start_script_url', 'bucket_default', 'share_string']
539 for udkey in udkeys:
540 if udkey in form_data and form_data[udkey] == '':
541 del form_data[udkey]
542 # If bucket_default was not provided, add a default value to the user data
543 # (missing value does not play nicely with CloudMan's ec2autorun.py)
544 if not form_data.get('bucket_default', None) and self.cloud.bucket_default:
545 form_data['bucket_default'] = self.cloud.bucket_default
546 # Reuse the ``password`` for the ``freenxpass`` user data option
547 if 'freenxpass' not in form_data and 'password' in form_data:
548 form_data['freenxpass'] = form_data['password']
549 # Convert form_data into the YAML format
550 ud = yaml.dump(form_data, default_flow_style=False, allow_unicode=False)
551 # Also include connection info about the selected cloud
552 ci = self._get_cloud_info(self.cloud, as_str=True)
553 return ud + "\n" + ci
554
555 def _get_cloud_info(self, cloud, as_str=False):
556 """
557 Get connection information about a given cloud
558 """
559 ci = {}
560 ci['cloud_type'] = cloud.cloud_type
561 ci['region_name'] = cloud.region_name
562 ci['region_endpoint'] = cloud.region_endpoint
563 ci['is_secure'] = cloud.is_secure
564 ci['ec2_port'] = cloud.ec2_port if cloud.ec2_port != '' else None
565 ci['ec2_conn_path'] = cloud.ec2_conn_path
566 # Include cidr_range only if not empty
567 if cloud.cidr_range != '':
568 ci['cidr_range'] = cloud.cidr_range
569 ci['s3_host'] = cloud.s3_host
570 ci['s3_port'] = cloud.s3_port if cloud.s3_port != '' else None
571 ci['s3_conn_path'] = cloud.s3_conn_path
572 if as_str:
573 ci = yaml.dump(ci, default_flow_style=False, allow_unicode=False)
574 return ci
575
576 def _get_volume_placement(self, vol_id):
577 """
578 Returns the placement of a volume (or None, if it cannot be determined)
579 """
580 try:
581 vol = self.ec2_conn.get_all_volumes(volume_ids=[vol_id])
582 except EC2ResponseError as ec2e:
583 bioblend.log.error("EC2ResponseError querying for volume {0}: {1}"
584 .format(vol_id, ec2e))
585 vol = None
586 if vol:
587 return vol[0].zone
588 else:
589 bioblend.log.error("Requested placement of a volume '%s' that does not exist." % vol_id)
590 return None
591
592 def _find_placement(self, cluster_name, cluster=None):
593 """
594 Find a placement zone for a cluster with the name ``cluster_name`` and
595 return the zone name as a string. By default, this method will search
596 for and fetch given cluster's persistent data; alternatively, persistent
597 data can be provided via ``cluster``. This dict needs to have
598 ``persistent_data`` key with the contents of cluster's persistent data.
599 If the cluster or the volume associated with the cluster cannot be found,
600 return ``None``.
601 """
602 placement = None
603 cluster = cluster or self.get_cluster_pd(cluster_name)
604 if cluster and 'persistent_data' in cluster:
605 pd = cluster['persistent_data']
606 try:
607 if 'placement' in pd:
608 placement = pd['placement']
609 elif 'data_filesystems' in pd:
610 # We have v1 format persistent data so get the volume first and
611 # then the placement zone
612 vol_id = pd['data_filesystems']['galaxyData'][0]['vol_id']
613 placement = self._get_volume_placement(vol_id)
614 elif 'filesystems' in pd:
615 # V2 format.
616 for fs in [fs for fs in pd['filesystems'] if fs.get('kind', None) == 'volume' and 'ids' in fs]:
617 vol_id = fs['ids'][0] # All volumes must be in the same zone
618 placement = self._get_volume_placement(vol_id)
619 # No need to continue to iterate through
620 # filesystems, if we found one with a volume.
621 break
622 except Exception as exc:
623 bioblend.log.exception("Exception while finding placement for "
624 "cluster '{0}'. This can indicate malformed "
625 "instance data. Or that this method is "
626 "broken: {1}".format(cluster_name, exc))
627 placement = None
628 return placement
629
630 def find_placements(self, ec2_conn, instance_type, cloud_type, cluster_name=None):
631 """
632 Find a list of placement zones that support the requested instance type.
633 If ``cluster_name`` is given and a cluster with the given name exist,
634 return a list with only one entry where the given cluster lives.
635
636 Searching for available zones for a given instance type is done by
637 checking the spot prices in the potential availability zones for
638 support before deciding on a region:
639 http://blog.piefox.com/2011/07/ec2-availability-zones-and-instance.html
640
641 Note that, currently, instance-type based zone selection applies only to
642 AWS. For other clouds, all the available zones are returned (unless a
643 cluster is being recreated, in which case the cluster's placement zone is
644 returned sa stored in its persistent data.
645
646 .. versionchanged:: 0.3
647 Changed method name from ``_find_placements`` to ``find_placements``.
648 Also added ``cluster_name`` parameter.
649 """
650 # First look for a specific zone a given cluster is bound to
651 zones = []
652 if cluster_name:
653 zones = self._find_placement(cluster_name) or []
654 # If placement is not found, look for a list of available zones
655 if not zones:
656 in_the_past = datetime.datetime.now() - datetime.timedelta(hours=1)
657 back_compatible_zone = "us-east-1e"
658 for zone in [z for z in ec2_conn.get_all_zones() if z.state == 'available']:
659 # Non EC2 clouds may not support get_spot_price_history
660 if instance_type is None or cloud_type != 'ec2':
661 zones.append(zone.name)
662 elif ec2_conn.get_spot_price_history(instance_type=instance_type,
663 end_time=in_the_past.isoformat(),
664 availability_zone=zone.name):
665 zones.append(zone.name)
666 zones.sort(reverse=True) # Higher-lettered zones seem to have more availability currently
667 if back_compatible_zone in zones:
668 zones = [back_compatible_zone] + [z for z in zones if z != back_compatible_zone]
669 if len(zones) == 0:
670 bioblend.log.error("Did not find availabilty zone for {1}".format(instance_type))
671 zones.append(back_compatible_zone)
672 return zones
673
674 def _checkURL(self, url):
675 """
676 Check if the ``url`` is *alive* (i.e., remote server returns code 200(OK)
677 or 401 (unauthorized)).
678 """
679 try:
680 p = urlparse(url)
681 h = HTTPConnection(p[1])
682 h.putrequest('HEAD', p[2])
683 h.endheaders()
684 r = h.getresponse()
685 if r.status in (200, 401): # CloudMan UI is pwd protected so include 401
686 return True
687 except Exception:
688 # No response or no good response
689 pass
690 return False