Mercurial > repos > shellac > guppy_basecaller
diff env/lib/python3.7/site-packages/bioblend/cloudman/launch.py @ 5:9b1c78e6ba9c draft default tip
"planemo upload commit 6c0a8142489327ece472c84e558c47da711a9142"
author | shellac |
---|---|
date | Mon, 01 Jun 2020 08:59:25 -0400 |
parents | 79f47841a781 |
children |
line wrap: on
line diff
--- a/env/lib/python3.7/site-packages/bioblend/cloudman/launch.py Thu May 14 16:47:39 2020 -0400 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,878 +0,0 @@ -""" -Setup and launch a CloudMan instance. -""" -import datetime -import socket - -import boto -import six -import yaml -from boto.compat import http_client -from boto.ec2.regioninfo import RegionInfo -from boto.exception import EC2ResponseError, S3ResponseError -from boto.s3.connection import OrdinaryCallingFormat, S3Connection, SubdomainCallingFormat -from six.moves.http_client import HTTPConnection -from six.moves.urllib.parse import urlparse - -import bioblend -from bioblend.util import Bunch - - -# Uncomment the following line if no logging from boto is desired -# bioblend.logging.getLogger('boto').setLevel(bioblend.logging.CRITICAL) -# Uncomment the following line if logging at the prompt is desired -# bioblend.set_stream_logger(__name__) -def instance_types(cloud_name='generic'): - """ - Return a list of dictionaries containing details about the available - instance types for the given `cloud_name`. - - :type cloud_name: str - :param cloud_name: A name of the cloud for which the list of instance - types will be returned. Valid values are: `aws`, - `nectar`, `generic`. - - :rtype: list - :return: A list of dictionaries describing instance types. Each dict will - contain the following keys: `name`, `model`, and `description`. - """ - instance_list = [] - if cloud_name.lower() == 'aws': - instance_list.append({"model": "c3.large", - "name": "Compute optimized Large", - "description": "2 vCPU/4GB RAM"}) - instance_list.append({"model": "c3.2xlarge", - "name": "Compute optimized 2xLarge", - "description": "8 vCPU/15GB RAM"}) - instance_list.append({"model": "c3.8xlarge", - "name": "Compute optimized 8xLarge", - "description": "32 vCPU/60GB RAM"}) - elif cloud_name.lower() in ['nectar', 'generic']: - instance_list.append({"model": "m1.small", - "name": "Small", - "description": "1 vCPU / 4GB RAM"}) - instance_list.append({"model": "m1.medium", - "name": "Medium", - "description": "2 vCPU / 8GB RAM"}) - instance_list.append({"model": "m1.large", - "name": "Large", - "description": "4 vCPU / 16GB RAM"}) - instance_list.append({"model": "m1.xlarge", - "name": "Extra Large", - "description": "8 vCPU / 32GB RAM"}) - instance_list.append({"model": "m1.xxlarge", - "name": "Extra-extra Large", - "description": "16 vCPU / 64GB RAM"}) - return instance_list - - -class CloudManLauncher(object): - - def __init__(self, access_key, secret_key, cloud=None): - """ - Define the environment in which this instance of CloudMan will be launched. - - Besides providing the credentials, optionally provide the ``cloud`` - object. This object must define the properties required to establish a - `boto <https://github.com/boto/boto/>`_ connection to that cloud. See - this method's implementation for an example of the required fields. - Note that as long the as provided object defines the required fields, - it can really by implemented as anything (e.g., a Bunch, a database - object, a custom class). If no value for the ``cloud`` argument is - provided, the default is to use the Amazon cloud. - """ - self.access_key = access_key - self.secret_key = secret_key - if cloud is None: - # Default to an EC2-compatible object - self.cloud = Bunch(id='1', # for compatibility w/ DB representation - name="Amazon", - cloud_type="ec2", - bucket_default="cloudman", - region_name="us-east-1", - region_endpoint="ec2.amazonaws.com", - ec2_port="", - ec2_conn_path="/", - cidr_range="", - is_secure=True, - s3_host="s3.amazonaws.com", - s3_port="", - s3_conn_path='/') - else: - self.cloud = cloud - self.ec2_conn = self.connect_ec2( - self.access_key, - self.secret_key, - self.cloud) - self.vpc_conn = self.connect_vpc( - self.access_key, - self.secret_key, - self.cloud) - # Define exceptions from http_client that we want to catch and retry - self.http_exceptions = (http_client.HTTPException, socket.error, - socket.gaierror, http_client.BadStatusLine) - - def __repr__(self): - return "Cloud: {0}; acct ID: {1}".format( - self.cloud.name, self.access_key) - - def launch(self, cluster_name, image_id, instance_type, password, - kernel_id=None, ramdisk_id=None, key_name='cloudman_key_pair', - security_groups=['CloudMan'], placement='', subnet_id=None, - ebs_optimized=False, **kwargs): - """ - Check all the prerequisites (key pair and security groups) for - launching a CloudMan instance, compose the user data based on the - parameters specified in the arguments and the cloud properties as - defined in the object's ``cloud`` field. - - For the current list of user data fields that can be provided via - ``kwargs``, see `<https://galaxyproject.org/cloudman/userdata/>`_ - - Return a dict containing the properties and info with which an instance - was launched, namely: ``sg_names`` containing the names of the security - groups, ``kp_name`` containing the name of the key pair, ``kp_material`` - containing the private portion of the key pair (*note* that this portion - of the key is available and can be retrieved *only* at the time the key - is created, which will happen only if no key with the name provided in - the ``key_name`` argument exists), ``rs`` containing the - `boto <https://github.com/boto/boto/>`_ ``ResultSet`` object, - ``instance_id`` containing the ID of a started instance, and - ``error`` containing an error message if there was one. - """ - ret = {'sg_names': [], - 'sg_ids': [], - 'kp_name': '', - 'kp_material': '', - 'rs': None, - 'instance_id': '', - 'error': None} - # First satisfy the prerequisites - for sg in security_groups: - # Get VPC ID in case we're launching into a VPC - vpc_id = None - if subnet_id: - try: - sn = self.vpc_conn.get_all_subnets(subnet_id)[0] - vpc_id = sn.vpc_id - except (EC2ResponseError, IndexError) as e: - bioblend.log.exception("Trouble fetching subnet %s: %s" % - (subnet_id, e)) - cmsg = self.create_cm_security_group(sg, vpc_id=vpc_id) - ret['error'] = cmsg['error'] - if ret['error']: - return ret - if cmsg['name']: - ret['sg_names'].append(cmsg['name']) - ret['sg_ids'].append(cmsg['sg_id']) - if subnet_id: - # Must setup a network interface if launching into VPC - security_groups = None - interface = boto.ec2.networkinterface.NetworkInterfaceSpecification( - subnet_id=subnet_id, groups=[cmsg['sg_id']], - associate_public_ip_address=True) - network_interfaces = (boto.ec2.networkinterface. - NetworkInterfaceCollection(interface)) - else: - network_interfaces = None - kp_info = self.create_key_pair(key_name) - ret['kp_name'] = kp_info['name'] - ret['kp_material'] = kp_info['material'] - ret['error'] = kp_info['error'] - if ret['error']: - return ret - # If not provided, try to find a placement - # TODO: Should placement always be checked? To make sure it's correct - # for existing clusters. - if not placement: - placement = self._find_placement( - cluster_name).get('placement', None) - # Compose user data for launching an instance, ensuring we have the - # required fields - kwargs['access_key'] = self.access_key - kwargs['secret_key'] = self.secret_key - kwargs['cluster_name'] = cluster_name - kwargs['password'] = password - kwargs['cloud_name'] = self.cloud.name - ud = self._compose_user_data(kwargs) - # Now launch an instance - try: - - rs = None - rs = self.ec2_conn.run_instances(image_id=image_id, - instance_type=instance_type, - key_name=key_name, - security_groups=security_groups, - # The following two arguments are - # provided in the network_interface - # instead of arguments: - # security_group_ids=security_group_ids, - # subnet_id=subnet_id, - network_interfaces=network_interfaces, - user_data=ud, - kernel_id=kernel_id, - ramdisk_id=ramdisk_id, - placement=placement, - ebs_optimized=ebs_optimized) - ret['rs'] = rs - except EC2ResponseError as e: - err_msg = "Problem launching an instance: {0} (code {1}; status {2})" \ - .format(e.message, e.error_code, e.status) - bioblend.log.exception(err_msg) - ret['error'] = err_msg - return ret - else: - if rs: - try: - bioblend.log.info( - "Launched an instance with ID %s" % - rs.instances[0].id) - ret['instance_id'] = rs.instances[0].id - ret['instance_ip'] = rs.instances[0].ip_address - except EC2ResponseError as e: - err_msg = "Problem with the launched instance object: {0} " \ - "(code {1}; status {2})" \ - .format(e.message, e.error_code, e.status) - bioblend.log.exception(err_msg) - ret['error'] = err_msg - else: - ret['error'] = ("No response after launching an instance. Check " - "your account permissions and try again.") - return ret - - def create_cm_security_group(self, sg_name='CloudMan', vpc_id=None): - """ - Create a security group with all authorizations required to run CloudMan. - - If the group already exists, check its rules and add the missing ones. - - :type sg_name: str - :param sg_name: A name for the security group to be created. - - :type vpc_id: str - :param vpc_id: VPC ID under which to create the security group. - - :rtype: dict - :return: A dictionary containing keys ``name`` (with the value being the - name of the security group that was created), ``error`` - (with the value being the error message if there was an error - or ``None`` if no error was encountered), and ``ports`` - (containing the list of tuples with port ranges that were - opened or attempted to be opened). - - .. versionchanged:: 0.6.1 - The return value changed from a string to a dict - """ - ports = (('20', '21'), # FTP - ('22', '22'), # SSH - ('80', '80'), # Web UI - ('443', '443'), # SSL Web UI - ('8800', '8800'), # NodeJS Proxy for Galaxy IPython IE - ('9600', '9700'), # HTCondor - ('30000', '30100')) # FTP transfer - progress = {'name': None, - 'sg_id': None, - 'error': None, - 'ports': ports} - cmsg = None - filters = None - if vpc_id: - filters = {'vpc-id': vpc_id} - # Check if this security group already exists - try: - sgs = self.ec2_conn.get_all_security_groups(filters=filters) - except EC2ResponseError as e: - err_msg = ("Problem getting security groups. This could indicate a " - "problem with your account credentials or permissions: " - "{0} (code {1}; status {2})" - .format(e.message, e.error_code, e.status)) - bioblend.log.exception(err_msg) - progress['error'] = err_msg - return progress - for sg in sgs: - if sg.name == sg_name: - cmsg = sg - bioblend.log.debug("Security group '%s' already exists; will " - "add authorizations next." % sg_name) - break - # If it does not exist, create security group - if cmsg is None: - bioblend.log.debug("Creating Security Group %s" % sg_name) - try: - cmsg = self.ec2_conn.create_security_group(sg_name, 'A security ' - 'group for CloudMan', - vpc_id=vpc_id) - except EC2ResponseError as e: - err_msg = "Problem creating security group '{0}': {1} (code {2}; " \ - "status {3})" \ - .format(sg_name, e.message, e.error_code, e.status) - bioblend.log.exception(err_msg) - progress['error'] = err_msg - if cmsg: - progress['name'] = cmsg.name - progress['sg_id'] = cmsg.id - # Add appropriate authorization rules - # If these rules already exist, nothing will be changed in the SG - for port in ports: - try: - if not self.rule_exists( - cmsg.rules, from_port=port[0], to_port=port[1]): - cmsg.authorize( - ip_protocol='tcp', - from_port=port[0], - to_port=port[1], - cidr_ip='0.0.0.0/0') - else: - bioblend.log.debug( - "Rule (%s:%s) already exists in the SG" % - (port[0], port[1])) - except EC2ResponseError as e: - err_msg = "A problem adding security group authorizations: {0} " \ - "(code {1}; status {2})" \ - .format(e.message, e.error_code, e.status) - bioblend.log.exception(err_msg) - progress['error'] = err_msg - # Add ICMP (i.e., ping) rule required by HTCondor - try: - if not self.rule_exists( - cmsg.rules, from_port='-1', to_port='-1', ip_protocol='icmp'): - cmsg.authorize( - ip_protocol='icmp', - from_port=-1, - to_port=-1, - cidr_ip='0.0.0.0/0') - else: - bioblend.log.debug( - "ICMP rule already exists in {0} SG.".format(sg_name)) - except EC2ResponseError as e: - err_msg = "A problem with security ICMP rule authorization: {0} " \ - "(code {1}; status {2})" \ - .format(e.message, e.error_code, e.status) - bioblend.log.exception(err_msg) - progress['err_msg'] = err_msg - # Add rule that allows communication between instances in the same - # SG - # A flag to indicate if group rule already exists - g_rule_exists = False - for rule in cmsg.rules: - for grant in rule.grants: - if grant.name == cmsg.name: - g_rule_exists = True - bioblend.log.debug( - "Group rule already exists in the SG.") - if g_rule_exists: - break - if not g_rule_exists: - try: - cmsg.authorize( - src_group=cmsg, - ip_protocol='tcp', - from_port=0, - to_port=65535) - except EC2ResponseError as e: - err_msg = "A problem with security group group " \ - "authorization: {0} (code {1}; status {2})" \ - .format(e.message, e.error_code, e.status) - bioblend.log.exception(err_msg) - progress['err_msg'] = err_msg - bioblend.log.info( - "Done configuring '%s' security group" % - cmsg.name) - else: - bioblend.log.warning( - "Did not create security group '{0}'".format(sg_name)) - return progress - - def rule_exists( - self, rules, from_port, to_port, ip_protocol='tcp', cidr_ip='0.0.0.0/0'): - """ - A convenience method to check if an authorization rule in a security group - already exists. - """ - for rule in rules: - if rule.ip_protocol == ip_protocol and rule.from_port == from_port and \ - rule.to_port == to_port and cidr_ip in [ip.cidr_ip for ip in rule.grants]: - return True - return False - - def create_key_pair(self, key_name='cloudman_key_pair'): - """ - If a key pair with the provided ``key_name`` does not exist, create it. - - :type sg_name: str - :param sg_name: A name for the key pair to be created. - - :rtype: dict - :return: A dictionary containing keys ``name`` (with the value being the - name of the key pair that was created), ``error`` - (with the value being the error message if there was an error - or ``None`` if no error was encountered), and ``material`` - (containing the unencrypted PEM encoded RSA private key if the - key was created or ``None`` if the key already eixsted). - - .. versionchanged:: 0.6.1 - The return value changed from a tuple to a dict - """ - progress = {'name': None, - 'material': None, - 'error': None} - kp = None - # Check if a key pair under the given name already exists. If it does not, - # create it, else return. - try: - kps = self.ec2_conn.get_all_key_pairs() - except EC2ResponseError as e: - err_msg = "Problem getting key pairs: {0} (code {1}; status {2})" \ - .format(e.message, e.error_code, e.status) - bioblend.log.exception(err_msg) - progress['error'] = err_msg - return progress - for akp in kps: - if akp.name == key_name: - bioblend.log.info( - "Key pair '%s' already exists; reusing it." % - key_name) - progress['name'] = akp.name - return progress - try: - kp = self.ec2_conn.create_key_pair(key_name) - except EC2ResponseError as e: - err_msg = "Problem creating key pair '{0}': {1} (code {2}; status {3})" \ - .format(key_name, e.message, e.error_code, e.status) - bioblend.log.exception(err_msg) - progress['error'] = err_msg - return progress - bioblend.log.info("Created key pair '%s'" % kp.name) - progress['name'] = kp.name - progress['material'] = kp.material - return progress - - def assign_floating_ip(self, ec2_conn, instance): - try: - bioblend.log.debug("Allocating a new floating IP address.") - address = ec2_conn.allocate_address() - except EC2ResponseError as e: - bioblend.log.exception("Exception allocating a new floating IP " - "address: %s" % e) - bioblend.log.info("Associating floating IP %s to instance %s" % - (address.public_ip, instance.id)) - ec2_conn.associate_address(instance_id=instance.id, - public_ip=address.public_ip) - - def get_status(self, instance_id): - """ - Check on the status of an instance. ``instance_id`` needs to be a - ``boto``-library copatible instance ID (e.g., ``i-8fehrdss``).If - ``instance_id`` is not provided, the ID obtained when launching - *the most recent* instance is used. Note that this assumes the instance - being checked on was launched using this class. Also note that the same - class may be used to launch multiple instances but only the most recent - ``instance_id`` is kept while any others will to be explicitly specified. - - This method also allows the required ``ec2_conn`` connection object to be - provided at invocation time. If the object is not provided, credentials - defined for the class are used (ability to specify a custom ``ec2_conn`` - helps in case of stateless method invocations). - - Return a ``state`` dict containing the following keys: ``instance_state``, - ``public_ip``, ``placement``, and ``error``, which capture CloudMan's - current state. For ``instance_state``, expected values are: ``pending``, - ``booting``, ``running``, or ``error`` and represent the state of the - underlying instance. Other keys will return an empty value until the - ``instance_state`` enters ``running`` state. - """ - ec2_conn = self.ec2_conn - rs = None - state = {'instance_state': "", - 'public_ip': "", - 'placement': "", - 'error': ""} - - # Make sure we have an instance ID - if instance_id is None: - err = "Missing instance ID, cannot check the state." - bioblend.log.error(err) - state['error'] = err - return state - try: - rs = ec2_conn.get_all_instances([instance_id]) - if rs is not None: - inst_state = rs[0].instances[0].update() - public_ip = rs[0].instances[0].ip_address - state['public_ip'] = public_ip - if inst_state == 'running': - # if there's a private ip, but no public ip - # attempt auto allocation of floating IP - if rs[0].instances[0].private_ip_address and not public_ip: - self.assign_floating_ip(ec2_conn, rs[0].instances[0]) - cm_url = "http://{dns}/cloud".format(dns=public_ip) - # Wait until the CloudMan URL is accessible to return the - # data - if self._checkURL(cm_url) is True: - state['instance_state'] = inst_state - state['placement'] = rs[0].instances[0].placement - else: - state['instance_state'] = 'booting' - else: - state['instance_state'] = inst_state - except Exception as e: - err = "Problem updating instance '%s' state: %s" % (instance_id, e) - bioblend.log.error(err) - state['error'] = err - return state - - def get_clusters_pd(self, include_placement=True): - """ - Return *persistent data* of all existing clusters for this account. - - :type include_placement: bool - :param include_placement: Whether or not to include region placement for - the clusters. Setting this option will lead - to a longer function runtime. - - :rtype: dict - :return: A dictionary containing keys ``clusters`` and ``error``. The - value of ``clusters`` will be a dictionary with the following keys - ``cluster_name``, ``persistent_data``, ``bucket_name`` and optionally - ``placement`` or an empty list if no clusters were found or an - error was encountered. ``persistent_data`` key value is yet - another dictionary containing given cluster's persistent data. - The value for the ``error`` key will contain a string with the - error message. - - .. versionadded:: 0.3 - .. versionchanged:: 0.7.0 - The return value changed from a list to a dictionary. - """ - clusters = [] - response = {'clusters': clusters, 'error': None} - s3_conn = self.connect_s3(self.access_key, self.secret_key, self.cloud) - try: - buckets = s3_conn.get_all_buckets() - except S3ResponseError as e: - response['error'] = "S3ResponseError getting buckets: %s" % e - except self.http_exceptions as ex: - response['error'] = "Exception getting buckets: %s" % ex - if response['error']: - bioblend.log.exception(response['error']) - return response - for bucket in [b for b in buckets if b.name.startswith('cm-')]: - try: - # TODO: first lookup if persistent_data.yaml key exists - pd = bucket.get_key('persistent_data.yaml') - except S3ResponseError: - # This can fail for a number of reasons for non-us and/or - # CNAME'd buckets but it is not a terminal error - bioblend.log.warning("Problem fetching persistent_data.yaml " - "from bucket %s" % bucket) - continue - if pd: - # We are dealing with a CloudMan bucket - pd_contents = pd.get_contents_as_string() - pd = yaml.load(pd_contents) - if 'cluster_name' in pd: - cluster_name = pd['cluster_name'] - else: - for key in bucket.list(): - if key.name.endswith('.clusterName'): - cluster_name = key.name.split('.clusterName')[0] - cluster = {'cluster_name': cluster_name, - 'persistent_data': pd, - 'bucket_name': bucket.name} - # Look for cluster's placement too - if include_placement: - placement = self._find_placement(cluster_name, cluster) - cluster['placement'] = placement - clusters.append(cluster) - response['clusters'] = clusters - return response - - def get_cluster_pd(self, cluster_name): - """ - Return *persistent data* (as a dict) associated with a cluster with the - given ``cluster_name``. If a cluster with the given name is not found, - return an empty dict. - - .. versionadded:: 0.3 - """ - cluster = {} - clusters = self.get_clusters_pd().get('clusters', []) - for c in clusters: - if c['cluster_name'] == cluster_name: - cluster = c - break - return cluster - - def connect_ec2(self, a_key, s_key, cloud=None): - """ - Create and return an EC2-compatible connection object for the given cloud. - - See ``_get_cloud_info`` method for more details on the requirements for - the ``cloud`` parameter. If no value is provided, the class field is used. - """ - if cloud is None: - cloud = self.cloud - ci = self._get_cloud_info(cloud) - r = RegionInfo(name=ci['region_name'], endpoint=ci['region_endpoint']) - ec2_conn = boto.connect_ec2(aws_access_key_id=a_key, - aws_secret_access_key=s_key, - is_secure=ci['is_secure'], - region=r, - port=ci['ec2_port'], - path=ci['ec2_conn_path'], - validate_certs=False) - return ec2_conn - - def connect_s3(self, a_key, s_key, cloud=None): - """ - Create and return an S3-compatible connection object for the given cloud. - - See ``_get_cloud_info`` method for more details on the requirements for - the ``cloud`` parameter. If no value is provided, the class field is used. - """ - if cloud is None: - cloud = self.cloud - ci = self._get_cloud_info(cloud) - if ci['cloud_type'] == 'amazon': - calling_format = SubdomainCallingFormat() - else: - calling_format = OrdinaryCallingFormat() - s3_conn = S3Connection( - aws_access_key_id=a_key, aws_secret_access_key=s_key, - is_secure=ci['is_secure'], port=ci['s3_port'], host=ci['s3_host'], - path=ci['s3_conn_path'], calling_format=calling_format) - return s3_conn - - def connect_vpc(self, a_key, s_key, cloud=None): - """ - Establish a connection to the VPC service. - - TODO: Make this work with non-default clouds as well. - """ - if cloud is None: - cloud = self.cloud - ci = self._get_cloud_info(cloud) - r = RegionInfo(name=ci['region_name'], endpoint=ci['region_endpoint']) - vpc_conn = boto.connect_vpc( - aws_access_key_id=a_key, - aws_secret_access_key=s_key, - is_secure=ci['is_secure'], - region=r, - port=ci['ec2_port'], - path=ci['ec2_conn_path'], - validate_certs=False) - return vpc_conn - - def _compose_user_data(self, user_provided_data): - """ - A convenience method used to compose and properly format the user data - required when requesting an instance. - - ``user_provided_data`` is the data provided by a user required to identify - a cluster and user other user requirements. - """ - form_data = {} - # Do not include the following fields in the user data but do include - # any 'advanced startup fields' that might be added in the future - excluded_fields = ['sg_name', 'image_id', 'instance_id', 'kp_name', - 'cloud', 'cloud_type', 'public_dns', 'cidr_range', - 'kp_material', 'placement', 'flavor_id'] - for key, value in six.iteritems(user_provided_data): - if key not in excluded_fields: - form_data[key] = value - # If the following user data keys are empty, do not include them in the - # request user data - udkeys = [ - 'post_start_script_url', - 'worker_post_start_script_url', - 'bucket_default', - 'share_string'] - for udkey in udkeys: - if udkey in form_data and form_data[udkey] == '': - del form_data[udkey] - # If bucket_default was not provided, add a default value to the user data - # (missing value does not play nicely with CloudMan's ec2autorun.py) - if not form_data.get( - 'bucket_default', None) and self.cloud.bucket_default: - form_data['bucket_default'] = self.cloud.bucket_default - # Reuse the ``password`` for the ``freenxpass`` user data option - if 'freenxpass' not in form_data and 'password' in form_data: - form_data['freenxpass'] = form_data['password'] - # Convert form_data into the YAML format - ud = yaml.dump(form_data, default_flow_style=False, allow_unicode=False) - # Also include connection info about the selected cloud - ci = self._get_cloud_info(self.cloud, as_str=True) - return ud + "\n" + ci - - def _get_cloud_info(self, cloud, as_str=False): - """ - Get connection information about a given cloud - """ - ci = {} - ci['cloud_type'] = cloud.cloud_type - ci['region_name'] = cloud.region_name - ci['region_endpoint'] = cloud.region_endpoint - ci['is_secure'] = cloud.is_secure - ci['ec2_port'] = cloud.ec2_port if cloud.ec2_port != '' else None - ci['ec2_conn_path'] = cloud.ec2_conn_path - # Include cidr_range only if not empty - if cloud.cidr_range != '': - ci['cidr_range'] = cloud.cidr_range - ci['s3_host'] = cloud.s3_host - ci['s3_port'] = cloud.s3_port if cloud.s3_port != '' else None - ci['s3_conn_path'] = cloud.s3_conn_path - if as_str: - ci = yaml.dump(ci, default_flow_style=False, allow_unicode=False) - return ci - - def _get_volume_placement(self, vol_id): - """ - Returns the placement of a volume (or None, if it cannot be determined) - """ - try: - vol = self.ec2_conn.get_all_volumes(volume_ids=[vol_id]) - except EC2ResponseError as ec2e: - bioblend.log.error("EC2ResponseError querying for volume {0}: {1}" - .format(vol_id, ec2e)) - vol = None - if vol: - return vol[0].zone - else: - bioblend.log.error( - "Requested placement of a volume '%s' that does not exist." % - vol_id) - return None - - def _find_placement(self, cluster_name, cluster=None): - """ - Find a placement zone for a cluster with the name ``cluster_name``. - - By default, this method will search for and fetch given cluster's - *persistent data*; alternatively, *persistent data* can be provided via - the ``cluster`` parameter. This dict needs to have ``persistent_data`` - key with the contents of cluster's *persistent data*. - If the cluster or the volume associated with the cluster cannot be found, - cluster placement is set to ``None``. - - :rtype: dict - :return: A dictionary with ``placement`` and ``error`` keywords. - - .. versionchanged:: 0.7.0 - The return value changed from a list to a dictionary. - """ - placement = None - response = {'placement': placement, 'error': None} - cluster = cluster or self.get_cluster_pd(cluster_name) - if cluster and 'persistent_data' in cluster: - pd = cluster['persistent_data'] - try: - if 'placement' in pd: - response['placement'] = pd['placement'] - elif 'data_filesystems' in pd: - # We have v1 format persistent data so get the volume first and - # then the placement zone - vol_id = pd['data_filesystems']['galaxyData'][0]['vol_id'] - response['placement'] = self._get_volume_placement(vol_id) - elif 'filesystems' in pd: - # V2 format. - for fs in [fs for fs in pd['filesystems'] if fs.get( - 'kind', None) == 'volume' and 'ids' in fs]: - # All volumes must be in the same zone - vol_id = fs['ids'][0] - response['placement'] = self._get_volume_placement( - vol_id) - # No need to continue to iterate through - # filesystems, if we found one with a volume. - break - except Exception as exc: - response['error'] = ("Exception while finding placement for " - "cluster '{0}'. This can indicate malformed " - "instance data. Or that this method is " - "broken: {1}".format(cluster_name, exc)) - bioblend.log.error(response['error']) - response['placement'] = None - else: - bioblend.log.debug("Insufficient info about cluster {0} to get placement." - .format(cluster_name)) - return response - - def find_placements( - self, ec2_conn, instance_type, cloud_type, cluster_name=None): - """ - Find a list of placement zones that support the specified instance type. - - If ``cluster_name`` is given and a cluster with the given name exist, - return a list with only one entry where the given cluster lives. - - Searching for available zones for a given instance type is done by - checking the spot prices in the potential availability zones for - support before deciding on a region: - http://blog.piefox.com/2011/07/ec2-availability-zones-and-instance.html - - Note that, currently, instance-type based zone selection applies only to - AWS. For other clouds, all the available zones are returned (unless a - cluster is being recreated, in which case the cluster's placement zone is - returned sa stored in its persistent data. - - :rtype: dict - :return: A dictionary with ``zones`` and ``error`` keywords. - - .. versionchanged:: 0.3 - Changed method name from ``_find_placements`` to ``find_placements``. - Also added ``cluster_name`` parameter. - - .. versionchanged:: 0.7.0 - The return value changed from a list to a dictionary. - """ - # First look for a specific zone a given cluster is bound to - zones = [] - response = {'zones': zones, 'error': None} - if cluster_name: - placement = self._find_placement(cluster_name) - if placement.get('error'): - response['error'] = placement['error'] - return response - response['zones'] = placement.get('placement', []) - # If placement is not found, look for a list of available zones - if not response['zones']: - in_the_past = datetime.datetime.now() - datetime.timedelta(hours=1) - back_compatible_zone = "us-east-1e" - for zone in [ - z for z in ec2_conn.get_all_zones() if z.state == 'available']: - # Non EC2 clouds may not support get_spot_price_history - if instance_type is None or cloud_type != 'ec2': - zones.append(zone.name) - elif ec2_conn.get_spot_price_history(instance_type=instance_type, - end_time=in_the_past.isoformat(), - availability_zone=zone.name): - zones.append(zone.name) - # Higher-lettered zones seem to have more availability currently - zones.sort(reverse=True) - if back_compatible_zone in zones: - zones = [back_compatible_zone] + \ - [z for z in zones if z != back_compatible_zone] - if len(zones) == 0: - response['error'] = ("Did not find availabilty zone for {0}" - .format(instance_type)) - bioblend.log.error(response['error']) - zones.append(back_compatible_zone) - return response - - def _checkURL(self, url): - """ - Check if the ``url`` is *alive* (i.e., remote server returns code 200(OK) - or 401 (unauthorized)). - """ - try: - p = urlparse(url) - h = HTTPConnection(p[1]) - h.putrequest('HEAD', p[2]) - h.endheaders() - r = h.getresponse() - # CloudMan UI is pwd protected so include 401 - if r.status in (200, 401): - return True - except Exception: - # No response or no good response - pass - return False