#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright: Ansible Project # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function __metaclass__ = type ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community'} DOCUMENTATION = ''' --- module: lightsail short_description: Create or delete a virtual machine instance in AWS Lightsail description: - Creates or instances in AWS Lightsail and optionally wait for it to be 'running'. version_added: "2.4" author: "Nick Ball (@nickball)" options: state: description: - Indicate desired state of the target. default: present choices: ['present', 'absent', 'running', 'restarted', 'stopped'] name: description: - Name of the instance required: true default : null zone: description: - AWS availability zone in which to launch the instance. Required when state='present' required: false default: null blueprint_id: description: - ID of the instance blueprint image. Required when state='present' required: false default: null bundle_id: description: - Bundle of specification info for the instance. Required when state='present' required: false default: null user_data: description: - Launch script that can configure the instance with additional data required: false default: null key_pair_name: description: - Name of the key pair to use with the instance required: false default: null wait: description: - Wait for the instance to be in state 'running' before returning. If wait is "no" an ip_address may not be returned default: "yes" choices: [ "yes", "no" ] wait_timeout: description: - How long before wait gives up, in seconds. default: 300 open_ports: description: - Adds public ports to an Amazon Lightsail instance. default: null suboptions: from_port: description: Begin of the range required: true default: null to_port: description: End of the range required: true default: null protocol: description: Accepted traffic protocol. required: true choices: - udp - tcp - all default: null requirements: - "python >= 2.6" - boto3 extends_documentation_fragment: - aws - ec2 ''' EXAMPLES = ''' # Create a new Lightsail instance, register the instance details - lightsail: state: present name: myinstance region: us-east-1 zone: us-east-1a blueprint_id: ubuntu_16_04 bundle_id: nano_1_0 key_pair_name: id_rsa user_data: " echo 'hello world' > /home/ubuntu/test.txt" wait_timeout: 500 open_ports: - from_port: 4500 to_port: 4500 protocol: udp - from_port: 500 to_port: 500 protocol: udp register: my_instance - debug: msg: "Name is {{ my_instance.instance.name }}" - debug: msg: "IP is {{ my_instance.instance.publicIpAddress }}" # Delete an instance if present - lightsail: state: absent region: us-east-1 name: myinstance ''' RETURN = ''' changed: description: if a snapshot has been modified/created returned: always type: bool sample: changed: true instance: description: instance data returned: always type: dict sample: arn: "arn:aws:lightsail:us-east-1:448830907657:Instance/1fef0175-d6c8-480e-84fa-214f969cda87" blueprint_id: "ubuntu_16_04" blueprint_name: "Ubuntu" bundle_id: "nano_1_0" created_at: "2017-03-27T08:38:59.714000-04:00" hardware: cpu_count: 1 ram_size_in_gb: 0.5 is_static_ip: false location: availability_zone: "us-east-1a" region_name: "us-east-1" name: "my_instance" networking: monthly_transfer: gb_per_month_allocated: 1024 ports: - access_direction: "inbound" access_from: "Anywhere (0.0.0.0/0)" access_type: "public" common_name: "" from_port: 80 protocol: tcp to_port: 80 - access_direction: "inbound" access_from: "Anywhere (0.0.0.0/0)" access_type: "public" common_name: "" from_port: 22 protocol: tcp to_port: 22 private_ip_address: "172.26.8.14" public_ip_address: "34.207.152.202" resource_type: "Instance" ssh_key_name: "keypair" state: code: 16 name: running support_code: "588307843083/i-0997c97831ee21e33" username: "ubuntu" ''' import time import traceback try: import botocore HAS_BOTOCORE = True except ImportError: HAS_BOTOCORE = False try: import boto3 except ImportError: # will be caught by imported HAS_BOTO3 pass from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.ec2 import (ec2_argument_spec, get_aws_connection_info, boto3_conn, HAS_BOTO3, camel_dict_to_snake_dict) def create_instance(module, client, instance_name): """ Create an instance module: Ansible module object client: authenticated lightsail connection object instance_name: name of instance to delete Returns a dictionary of instance information about the new instance. """ changed = False # Check if instance already exists inst = None try: inst = _find_instance_info(client, instance_name) except botocore.exceptions.ClientError as e: if e.response['Error']['Code'] != 'NotFoundException': module.fail_json(msg='Error finding instance {0}, error: {1}'.format(instance_name, e)) zone = module.params.get('zone') blueprint_id = module.params.get('blueprint_id') bundle_id = module.params.get('bundle_id') user_data = module.params.get('user_data') user_data = '' if user_data is None else user_data wait = module.params.get('wait') wait_timeout = int(module.params.get('wait_timeout')) wait_max = time.time() + wait_timeout if module.params.get('key_pair_name'): key_pair_name = module.params.get('key_pair_name') else: key_pair_name = '' if module.params.get('open_ports'): open_ports = module.params.get('open_ports') else: open_ports = '[]' resp = None if inst is None: try: resp = client.create_instances( instanceNames=[ instance_name ], availabilityZone=zone, blueprintId=blueprint_id, bundleId=bundle_id, userData=user_data, keyPairName=key_pair_name, ) resp = resp['operations'][0] except botocore.exceptions.ClientError as e: module.fail_json(msg='Unable to create instance {0}, error: {1}'.format(instance_name, e)) inst = _find_instance_info(client, instance_name) # Wait for instance to become running if wait: while (wait_max > time.time()) and (inst is not None and inst['state']['name'] != "running"): try: time.sleep(2) inst = _find_instance_info(client, instance_name) except botocore.exceptions.ClientError as e: if e.response['ResponseMetadata']['HTTPStatusCode'] == "403": module.fail_json(msg="Failed to start/stop instance {0}. Check that you have permissions to perform the operation".format(instance_name), exception=traceback.format_exc()) elif e.response['Error']['Code'] == "RequestExpired": module.fail_json(msg="RequestExpired: Failed to start instance {0}.".format(instance_name), exception=traceback.format_exc()) time.sleep(1) # Timed out if wait and not changed and wait_max <= time.time(): module.fail_json(msg="Wait for instance start timeout at %s" % time.asctime()) # Attempt to open ports if open_ports: if inst is not None: try: for o in open_ports: resp = client.open_instance_public_ports( instanceName=instance_name, portInfo={ 'fromPort': o['from_port'], 'toPort': o['to_port'], 'protocol': o['protocol'] } ) except botocore.exceptions.ClientError as e: module.fail_json(msg='Error opening ports for instance {0}, error: {1}'.format(instance_name, e)) changed = True return (changed, inst) def delete_instance(module, client, instance_name): """ Terminates an instance module: Ansible module object client: authenticated lightsail connection object instance_name: name of instance to delete Returns a dictionary of instance information about the instance deleted (pre-deletion). If the instance to be deleted is running "changed" will be set to False. """ # It looks like deleting removes the instance immediately, nothing to wait for wait = module.params.get('wait') wait_timeout = int(module.params.get('wait_timeout')) wait_max = time.time() + wait_timeout changed = False inst = None try: inst = _find_instance_info(client, instance_name) except botocore.exceptions.ClientError as e: if e.response['Error']['Code'] != 'NotFoundException': module.fail_json(msg='Error finding instance {0}, error: {1}'.format(instance_name, e)) # Wait for instance to exit transition state before deleting if wait: while wait_max > time.time() and inst is not None and inst['state']['name'] in ('pending', 'stopping'): try: time.sleep(5) inst = _find_instance_info(client, instance_name) except botocore.exceptions.ClientError as e: if e.response['ResponseMetadata']['HTTPStatusCode'] == "403": module.fail_json(msg="Failed to delete instance {0}. Check that you have permissions to perform the operation.".format(instance_name), exception=traceback.format_exc()) elif e.response['Error']['Code'] == "RequestExpired": module.fail_json(msg="RequestExpired: Failed to delete instance {0}.".format(instance_name), exception=traceback.format_exc()) # sleep and retry time.sleep(10) # Attempt to delete if inst is not None: while not changed and ((wait and wait_max > time.time()) or (not wait)): try: client.delete_instance(instanceName=instance_name) changed = True except botocore.exceptions.ClientError as e: module.fail_json(msg='Error deleting instance {0}, error: {1}'.format(instance_name, e)) # Timed out if wait and not changed and wait_max <= time.time(): module.fail_json(msg="wait for instance delete timeout at %s" % time.asctime()) return (changed, inst) def restart_instance(module, client, instance_name): """ Reboot an existing instance module: Ansible module object client: authenticated lightsail connection object instance_name: name of instance to reboot Returns a dictionary of instance information about the restarted instance If the instance was not able to reboot, "changed" will be set to False. Wait will not apply here as this is an OS-level operation """ wait = module.params.get('wait') wait_timeout = int(module.params.get('wait_timeout')) wait_max = time.time() + wait_timeout changed = False inst = None try: inst = _find_instance_info(client, instance_name) except botocore.exceptions.ClientError as e: if e.response['Error']['Code'] != 'NotFoundException': module.fail_json(msg='Error finding instance {0}, error: {1}'.format(instance_name, e)) # Wait for instance to exit transition state before state change if wait: while wait_max > time.time() and inst is not None and inst['state']['name'] in ('pending', 'stopping'): try: time.sleep(5) inst = _find_instance_info(client, instance_name) except botocore.exceptions.ClientError as e: if e.response['ResponseMetadata']['HTTPStatusCode'] == "403": module.fail_json(msg="Failed to restart instance {0}. Check that you have permissions to perform the operation.".format(instance_name), exception=traceback.format_exc()) elif e.response['Error']['Code'] == "RequestExpired": module.fail_json(msg="RequestExpired: Failed to restart instance {0}.".format(instance_name), exception=traceback.format_exc()) time.sleep(3) # send reboot if inst is not None: try: client.reboot_instance(instanceName=instance_name) except botocore.exceptions.ClientError as e: if e.response['Error']['Code'] != 'NotFoundException': module.fail_json(msg='Unable to reboot instance {0}, error: {1}'.format(instance_name, e)) changed = True return (changed, inst) def startstop_instance(module, client, instance_name, state): """ Starts or stops an existing instance module: Ansible module object client: authenticated lightsail connection object instance_name: name of instance to start/stop state: Target state ("running" or "stopped") Returns a dictionary of instance information about the instance started/stopped If the instance was not able to state change, "changed" will be set to False. """ wait = module.params.get('wait') wait_timeout = int(module.params.get('wait_timeout')) wait_max = time.time() + wait_timeout changed = False inst = None try: inst = _find_instance_info(client, instance_name) except botocore.exceptions.ClientError as e: if e.response['Error']['Code'] != 'NotFoundException': module.fail_json(msg='Error finding instance {0}, error: {1}'.format(instance_name, e)) # Wait for instance to exit transition state before state change if wait: while wait_max > time.time() and inst is not None and inst['state']['name'] in ('pending', 'stopping'): try: time.sleep(5) inst = _find_instance_info(client, instance_name) except botocore.exceptions.ClientError as e: if e.response['ResponseMetadata']['HTTPStatusCode'] == "403": module.fail_json(msg="Failed to start/stop instance {0}. Check that you have permissions to perform the operation".format(instance_name), exception=traceback.format_exc()) elif e.response['Error']['Code'] == "RequestExpired": module.fail_json(msg="RequestExpired: Failed to start/stop instance {0}.".format(instance_name), exception=traceback.format_exc()) time.sleep(1) # Try state change if inst is not None and inst['state']['name'] != state: try: if state == 'running': client.start_instance(instanceName=instance_name) else: client.stop_instance(instanceName=instance_name) except botocore.exceptions.ClientError as e: module.fail_json(msg='Unable to change state for instance {0}, error: {1}'.format(instance_name, e)) changed = True # Grab current instance info inst = _find_instance_info(client, instance_name) return (changed, inst) def core(module): region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) if not region: module.fail_json(msg='region must be specified') client = None try: client = boto3_conn(module, conn_type='client', resource='lightsail', region=region, endpoint=ec2_url, **aws_connect_kwargs) except (botocore.exceptions.ClientError, botocore.exceptions.ValidationError) as e: module.fail_json(msg='Failed while connecting to the lightsail service: %s' % e, exception=traceback.format_exc()) changed = False state = module.params['state'] name = module.params['name'] if state == 'absent': changed, instance_dict = delete_instance(module, client, name) elif state in ('running', 'stopped'): changed, instance_dict = startstop_instance(module, client, name, state) elif state == 'restarted': changed, instance_dict = restart_instance(module, client, name) elif state == 'present': changed, instance_dict = create_instance(module, client, name) module.exit_json(changed=changed, instance=camel_dict_to_snake_dict(instance_dict)) def _find_instance_info(client, instance_name): ''' handle exceptions where this function is called ''' inst = None try: inst = client.get_instance(instanceName=instance_name) except botocore.exceptions.ClientError as e: raise return inst['instance'] def main(): argument_spec = ec2_argument_spec() argument_spec.update(dict( name=dict(type='str', required=True), state=dict(type='str', default='present', choices=['present', 'absent', 'stopped', 'running', 'restarted']), zone=dict(type='str'), blueprint_id=dict(type='str'), bundle_id=dict(type='str'), key_pair_name=dict(type='str'), user_data=dict(type='str'), wait=dict(type='bool', default=True), wait_timeout=dict(default=300), open_ports=dict(type='list') )) module = AnsibleModule(argument_spec=argument_spec) if not HAS_BOTO3: module.fail_json(msg='Python module "boto3" is missing, please install it') if not HAS_BOTOCORE: module.fail_json(msg='Python module "botocore" is missing, please install it') try: core(module) except (botocore.exceptions.ClientError, Exception) as e: module.fail_json(msg=str(e), exception=traceback.format_exc()) if __name__ == '__main__': main()