#!/usr/bin/python # Copyright (c) 2017 Alibaba Group Holding Limited. He Guimin # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Ansible is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see http://www.gnu.org/licenses/. from __future__ import absolute_import, division, print_function __metaclass__ = type ANSIBLE_METADATA = { 'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community' } DOCUMENTATION = r''' --- module: ali_instance short_description: Create, start, stop, restart or terminate an instance in ECS, add or remove an instance to/from a security group description: - Create, start, stop, restart, modify or terminate ecs instances. - Add or remove ecs instances to/from security group. options: state: description: - The state of the instance after operating. type: str default: 'present' choices: ['absent', 'present', 'restarted', 'running', 'stopped'] availability_zone: description: - Aliyun availability zone ID in which to launch the instance. - If it is not specified, it will be allocated by system automatically. aliases: ['alicloud_zone'] type: str image_id: description: - Image ID used to launch instances. - Required when I(state=present) and creating new ECS instances. aliases: ['image'] type: str instance_type: description: - Instance type used to launch instances. - Required when I(state=present) and creating new ECS instances. aliases: ['type'] type: str security_groups: description: - A list of security group IDs. type: list vswitch_id: description: - The subnet ID in which to launch the instances (VPC). aliases: ['subnet_id'] type: str instance_name: description: - The name of ECS instance, which is a string of 2 to 128 Chinese or English characters. - It must begin with an uppercase/lowercase letter or a Chinese character and can contain numerals, ".", "_" or "-". It cannot begin with http:// or https://. aliases: ['name'] type: str description: description: - The description of ECS instance, which is a string of 2 to 256 characters. - It cannot begin with http:// or https://. type: str internet_charge_type: description: - Internet charge type of ECS instance. type: str default: 'PayByBandwidth' choices: ['PayByBandwidth', 'PayByTraffic'] max_bandwidth_in: description: - Maximum incoming bandwidth from the public network, measured in Mbps (Megabits per second). default: 200 type: int max_bandwidth_out: description: - Maximum outgoing bandwidth to the public network, measured in Mbps (Megabits per second). type: int default: 0 host_name: description: - Instance host name. type: str password: description: - The password to login instance. - After rebooting instances, modified password will take effect. type: str system_disk_category: description: - Category of the system disk. type: str default: 'cloud_efficiency' choices: ['cloud_efficiency', 'cloud_ssd'] system_disk_size: description: - Size of the system disk, in GB. The valid values are 40~500. type: int default: 40 system_disk_name: description: - Name of the system disk. type: str system_disk_description: description: - Description of the system disk. type: str count: description: - The number of the new instance. - Indicates how many instances that match I(count_tag) should be running. - Instances are either created or terminated based on this value. type: int default: 1 count_tag: description: - Determines how many instances based on a specific tag criteria should be present. - This can be expressed in multiple ways and is shown in the EXAMPLES section. - The specified count_tag must already exist or be passed in as the I(instance_tags) option. - If it is not specified, it will be replaced by I(instance_name). type: str allocate_public_ip: description: - Whether allocate a public ip for the new instance. default: False aliases: ['assign_public_ip'] type: bool instance_charge_type: description: - The charge type of the instance. type: str choices: ['PrePaid', 'PostPaid'] default: 'PostPaid' period: description: - The charge duration of the instance, in month. - Required when I(instance_charge_type=PrePaid). - The valid value are [1-9, 12, 24, 36]. type: int default: 1 auto_renew: description: - Whether automate renew the charge of the instance. type: bool default: False auto_renew_period: description: - The duration of the automatic renew the charge of the instance. - Required when I(auto_renew=True). type: int choices: [1, 2, 3, 6, 12] instance_ids: description: - A list of instance ids. It is required when need to operate existing instances. - If it is specified, I(count) will lose efficacy. type: list force: description: - Whether the current operation needs to be execute forcibly. default: False type: bool instance_tags: description: - A hash/dictionaries of instance tags, to add to the new instance or for starting/stopping instance by tag (C({"key":"value"})). aliases: ['tags'] type: dict key_name: description: - The name of key pair which is used to access ECS instance in SSH. type: str required: false aliases: ['keypair'] user_data: description: - User-defined data to customize the startup behaviors of an ECS instance and to pass data into an ECS instance. It only will take effect when launching the new ECS instances. required: false type: str author: - "He Guimin (@xiaozhu36)" requirements: - "python >= 2.6" - "footmark >= 1.1.16" extends_documentation_fragment: - community.general.alicloud ''' EXAMPLES = r''' # basic provisioning example vpc network - name: basic provisioning example hosts: localhost vars: alicloud_access_key: alicloud_secret_key: alicloud_region: cn-beijing image: ubuntu1404_64_40G_cloudinit_20160727.raw instance_type: ecs.n4.small vswitch_id: vsw-abcd1234 assign_public_ip: True max_bandwidth_out: 10 host_name: myhost password: mypassword system_disk_category: cloud_efficiency system_disk_size: 100 internet_charge_type: PayByBandwidth security_groups: ["sg-f2rwnfh23r"] instance_ids: ["i-abcd12346", "i-abcd12345"] force: True tasks: - name: launch ECS instance in VPC network ali_instance: alicloud_access_key: '{{ alicloud_access_key }}' alicloud_secret_key: '{{ alicloud_secret_key }}' alicloud_region: '{{ alicloud_region }}' image: '{{ image }}' system_disk_category: '{{ system_disk_category }}' system_disk_size: '{{ system_disk_size }}' instance_type: '{{ instance_type }}' vswitch_id: '{{ vswitch_id }}' assign_public_ip: '{{ assign_public_ip }}' internet_charge_type: '{{ internet_charge_type }}' max_bandwidth_out: '{{ max_bandwidth_out }}' instance_tags: Name: created_one host_name: '{{ host_name }}' password: '{{ password }}' - name: with count and count_tag to create a number of instances ali_instance: alicloud_access_key: '{{ alicloud_access_key }}' alicloud_secret_key: '{{ alicloud_secret_key }}' alicloud_region: '{{ alicloud_region }}' image: '{{ image }}' system_disk_category: '{{ system_disk_category }}' system_disk_size: '{{ system_disk_size }}' instance_type: '{{ instance_type }}' assign_public_ip: '{{ assign_public_ip }}' security_groups: '{{ security_groups }}' internet_charge_type: '{{ internet_charge_type }}' max_bandwidth_out: '{{ max_bandwidth_out }}' instance_tags: Name: created_one Version: 0.1 count: 2 count_tag: Name: created_one host_name: '{{ host_name }}' password: '{{ password }}' - name: start instance ali_instance: alicloud_access_key: '{{ alicloud_access_key }}' alicloud_secret_key: '{{ alicloud_secret_key }}' alicloud_region: '{{ alicloud_region }}' instance_ids: '{{ instance_ids }}' state: 'running' - name: reboot instance forcibly ecs: alicloud_access_key: '{{ alicloud_access_key }}' alicloud_secret_key: '{{ alicloud_secret_key }}' alicloud_region: '{{ alicloud_region }}' instance_ids: '{{ instance_ids }}' state: 'restarted' force: '{{ force }}' - name: Add instances to an security group ecs: alicloud_access_key: '{{ alicloud_access_key }}' alicloud_secret_key: '{{ alicloud_secret_key }}' alicloud_region: '{{ alicloud_region }}' instance_ids: '{{ instance_ids }}' security_groups: '{{ security_groups }}' ''' RETURN = r''' instances: description: List of ECS instances. returned: always type: complex contains: availability_zone: description: The availability zone of the instance is in. returned: always type: str sample: cn-beijing-a block_device_mappings: description: Any block device mapping entries for the instance. returned: always type: complex contains: device_name: description: The device name exposed to the instance (for example, /dev/xvda). returned: always type: str sample: /dev/xvda attach_time: description: The time stamp when the attachment initiated. returned: always type: str sample: "2018-06-25T04:08:26Z" delete_on_termination: description: Indicates whether the volume is deleted on instance termination. returned: always type: bool sample: true status: description: The attachment state. returned: always type: str sample: in_use volume_id: description: The ID of the cloud disk. returned: always type: str sample: d-2zei53pjsi117y6gf9t6 cpu: description: The CPU core count of the instance. returned: always type: int sample: 4 creation_time: description: The time the instance was created. returned: always type: str sample: "2018-06-25T04:08Z" description: description: The instance description. returned: always type: str sample: "my ansible instance" eip: description: The attribution of EIP associated with the instance. returned: always type: complex contains: allocation_id: description: The ID of the EIP. returned: always type: str sample: eip-12345 internet_charge_type: description: The internet charge type of the EIP. returned: always type: str sample: "paybybandwidth" ip_address: description: EIP address. returned: always type: str sample: 42.10.2.2 expired_time: description: The time the instance will expire. returned: always type: str sample: "2099-12-31T15:59Z" gpu: description: The attribution of instance GPU. returned: always type: complex contains: amount: description: The count of the GPU. returned: always type: int sample: 0 spec: description: The specification of the GPU. returned: always type: str sample: "" host_name: description: The host name of the instance. returned: always type: str sample: iZ2zewaoZ id: description: Alias of instance_id. returned: always type: str sample: i-abc12345 instance_id: description: ECS instance resource ID. returned: always type: str sample: i-abc12345 image_id: description: The ID of the image used to launch the instance. returned: always type: str sample: m-0011223344 inner_ip_address: description: The inner IPv4 address of the classic instance. returned: always type: str sample: 10.0.0.2 instance_charge_type: description: The instance charge type. returned: always type: str sample: PostPaid instance_name: description: The name of the instance. returned: always type: str sample: my-ecs instance_type: description: The instance type of the running instance. returned: always type: str sample: ecs.sn1ne.xlarge internet_charge_type: description: The billing method of the network bandwidth. returned: always type: str sample: PayByBandwidth internet_max_bandwidth_in: description: Maximum incoming bandwidth from the internet network. returned: always type: int sample: 200 internet_max_bandwidth_out: description: Maximum incoming bandwidth from the internet network. returned: always type: int sample: 20 io_optimized: description: Indicates whether the instance is optimized for EBS I/O. returned: always type: bool sample: false memory: description: Memory size of the instance. returned: always type: int sample: 8192 network_interfaces: description: One or more network interfaces for the instance. returned: always type: complex contains: mac_address: description: The MAC address. returned: always type: str sample: "00:11:22:33:44:55" network_interface_id: description: The ID of the network interface. returned: always type: str sample: eni-01234567 primary_ip_address: description: The primary IPv4 address of the network interface within the vswitch. returned: always type: str sample: 10.0.0.1 osname: description: The operation system name of the instance owned. returned: always type: str sample: CentOS ostype: description: The operation system type of the instance owned. returned: always type: str sample: linux private_ip_address: description: The IPv4 address of the network interface within the subnet. returned: always type: str sample: 10.0.0.1 public_ip_address: description: The public IPv4 address assigned to the instance. returned: always type: str sample: 43.0.0.1 resource_group_id: description: The id of the resource group to which the instance belongs. returned: always type: str sample: my-ecs-group security_groups: description: One or more security groups for the instance. returned: always type: list elements: dict contains: group_id: description: The ID of the security group. returned: always type: str sample: sg-0123456 group_name: description: The name of the security group. returned: always type: str sample: my-security-group status: description: The current status of the instance. returned: always type: str sample: running tags: description: Any tags assigned to the instance. returned: always type: dict sample: vswitch_id: description: The ID of the vswitch in which the instance is running. returned: always type: str sample: vsw-dew00abcdef vpc_id: description: The ID of the VPC the instance is in. returned: always type: dict sample: vpc-0011223344 ids: description: List of ECS instance IDs. returned: always type: list sample: [i-12345er, i-3245fs] ''' import time import traceback from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible_collections.community.general.plugins.module_utils.alicloud_ecs import ecs_argument_spec, ecs_connect HAS_FOOTMARK = False FOOTMARK_IMP_ERR = None try: from footmark.exception import ECSResponseError HAS_FOOTMARK = True except ImportError: FOOTMARK_IMP_ERR = traceback.format_exc() HAS_FOOTMARK = False def get_instances_info(connection, ids): result = [] instances = connection.get_all_instances(instance_ids=ids) if len(instances) > 0: for inst in instances: result.append(inst.read()) return result def create_instance(module, ecs, exact_count): if exact_count <= 0: return None zone_id = module.params['availability_zone'] image_id = module.params['image_id'] instance_type = module.params['instance_type'] security_groups = module.params['security_groups'] vswitch_id = module.params['vswitch_id'] instance_name = module.params['instance_name'] description = module.params['description'] internet_charge_type = module.params['internet_charge_type'] max_bandwidth_out = module.params['max_bandwidth_out'] max_bandwidth_in = module.params['max_bandwidth_out'] host_name = module.params['host_name'] password = module.params['password'] system_disk_category = module.params['system_disk_category'] system_disk_size = module.params['system_disk_size'] system_disk_name = module.params['system_disk_name'] system_disk_description = module.params['system_disk_description'] allocate_public_ip = module.params['allocate_public_ip'] instance_tags = module.params['instance_tags'] period = module.params['period'] auto_renew = module.params['auto_renew'] instance_charge_type = module.params['instance_charge_type'] auto_renew_period = module.params['auto_renew_period'] user_data = module.params['user_data'] key_name = module.params['key_name'] # check whether the required parameter passed or not if not image_id: module.fail_json(msg='image_id is required for new instance') if not instance_type: module.fail_json(msg='instance_type is required for new instance') if not isinstance(security_groups, list): module.fail_json(msg='The parameter security_groups should be a list, aborting') if len(security_groups) <= 0: module.fail_json(msg='Expected the parameter security_groups is non-empty when create new ECS instances, aborting') client_token = "Ansible-Alicloud-{0}-{1}".format(hash(str(module.params)), str(time.time())) try: # call to create_instance method from footmark instances = ecs.create_instance(image_id=image_id, instance_type=instance_type, security_group_id=security_groups[0], zone_id=zone_id, instance_name=instance_name, description=description, internet_charge_type=internet_charge_type, max_bandwidth_out=max_bandwidth_out, max_bandwidth_in=max_bandwidth_in, host_name=host_name, password=password, io_optimized='optimized', system_disk_category=system_disk_category, system_disk_size=system_disk_size, system_disk_name=system_disk_name, system_disk_description=system_disk_description, vswitch_id=vswitch_id, count=exact_count, allocate_public_ip=allocate_public_ip, instance_charge_type=instance_charge_type, period=period, auto_renew=auto_renew, auto_renew_period=auto_renew_period, instance_tags=instance_tags, key_pair_name=key_name, user_data=user_data, client_token=client_token) except Exception as e: module.fail_json(msg='Unable to create instance, error: {0}'.format(e)) return instances def main(): argument_spec = ecs_argument_spec() argument_spec.update(dict( security_groups=dict(type='list'), availability_zone=dict(type='str', aliases=['alicloud_zone']), instance_type=dict(type='str', aliases=['type']), image_id=dict(type='str', aliases=['image']), count=dict(type='int', default=1), count_tag=dict(type='str'), vswitch_id=dict(type='str', aliases=['subnet_id']), instance_name=dict(type='str', aliases=['name']), host_name=dict(type='str'), password=dict(type='str', no_log=True), internet_charge_type=dict(type='str', default='PayByBandwidth', choices=['PayByBandwidth', 'PayByTraffic']), max_bandwidth_in=dict(type='int', default=200), max_bandwidth_out=dict(type='int', default=0), system_disk_category=dict(type='str', default='cloud_efficiency', choices=['cloud_efficiency', 'cloud_ssd']), system_disk_size=dict(type='int', default=40), system_disk_name=dict(type='str'), system_disk_description=dict(type='str'), force=dict(type='bool', default=False), instance_tags=dict(type='dict', aliases=['tags']), state=dict(default='present', choices=['present', 'running', 'stopped', 'restarted', 'absent']), description=dict(type='str'), allocate_public_ip=dict(type='bool', aliases=['assign_public_ip'], default=False), instance_charge_type=dict(type='str', default='PostPaid', choices=['PrePaid', 'PostPaid']), period=dict(type='int', default=1), auto_renew=dict(type='bool', default=False), instance_ids=dict(type='list'), auto_renew_period=dict(type='int', choices=[1, 2, 3, 6, 12]), key_name=dict(type='str', aliases=['keypair']), user_data=dict(type='str') ) ) module = AnsibleModule(argument_spec=argument_spec) if HAS_FOOTMARK is False: module.fail_json(msg=missing_required_lib('footmark'), exception=FOOTMARK_IMP_ERR) ecs = ecs_connect(module) state = module.params['state'] instance_ids = module.params['instance_ids'] count_tag = module.params['count_tag'] count = module.params['count'] instance_name = module.params['instance_name'] force = module.params['force'] zone_id = module.params['availability_zone'] key_name = module.params['key_name'] changed = False instances = [] if instance_ids: if not isinstance(instance_ids, list): module.fail_json(msg='The parameter instance_ids should be a list, aborting') instances = ecs.get_all_instances(zone_id=zone_id, instance_ids=instance_ids) if not instances: module.fail_json(msg="There are no instances in our record based on instance_ids {0}. " "Please check it and try again.".format(instance_ids)) elif count_tag: instances = ecs.get_all_instances(zone_id=zone_id, instance_tags=eval(count_tag)) elif instance_name: instances = ecs.get_all_instances(zone_id=zone_id, instance_name=instance_name) ids = [] if state == 'present': if not instance_ids: if len(instances) > count: for i in range(0, len(instances) - count): inst = instances[len(instances) - 1] if inst.status != 'stopped' and not force: module.fail_json(msg="That to delete instance {0} is failed results from it is running, " "and please stop it or set 'force' as True.".format(inst.id)) try: changed = inst.terminate(force=force) except Exception as e: module.fail_json(msg="Delete instance {0} got an error: {1}".format(inst.id, e)) instances.pop(len(instances) - 1) else: try: new_instances = create_instance(module, ecs, count - len(instances)) if new_instances: changed = True instances.extend(new_instances) except Exception as e: module.fail_json(msg="Create new instances got an error: {0}".format(e)) # Security Group join/leave begin security_groups = module.params['security_groups'] if not isinstance(security_groups, list): module.fail_json(msg='The parameter security_groups should be a list, aborting') if len(security_groups) > 0: for inst in instances: existing = inst.security_group_ids['security_group_id'] remove = list(set(existing).difference(set(security_groups))) add = list(set(security_groups).difference(set(existing))) for sg in remove: if inst.leave_security_group(sg): changed = True for sg in add: if inst.join_security_group(sg): changed = True # Security Group join/leave ends here # Attach/Detach key pair inst_ids = [] for inst in instances: if key_name is not None and key_name != inst.key_name: if key_name == "": changed = inst.detach_key_pair() else: inst_ids.append(inst.id) if inst_ids: changed = ecs.attach_key_pair(instance_ids=inst_ids, key_pair_name=key_name) # Modify instance attribute description = module.params['description'] host_name = module.params['host_name'] password = module.params['password'] for inst in instances: if not instance_name: instance_name = inst.name if not description: description = inst.description if not host_name: host_name = inst.host_name try: if inst.modify(name=instance_name, description=description, host_name=host_name, password=password): changed = True except Exception as e: module.fail_json(msg="Modify instance attribute {0} got an error: {1}".format(inst.id, e)) if inst.id not in ids: ids.append(inst.id) module.exit_json(changed=changed, ids=ids, instances=get_instances_info(ecs, ids)) else: if len(instances) < 1: module.fail_json(msg='Please specify ECS instances that you want to operate by using ' 'parameters instance_ids, instance_tags or instance_name, aborting') force = module.params['force'] if state == 'running': try: for inst in instances: if inst.start(): changed = True ids.append(inst.id) module.exit_json(changed=changed, ids=ids, instances=get_instances_info(ecs, ids)) except Exception as e: module.fail_json(msg='Start instances got an error: {0}'.format(e)) elif state == 'stopped': try: for inst in instances: if inst.stop(force=force): changed = True ids.append(inst.id) module.exit_json(changed=changed, ids=ids, instances=get_instances_info(ecs, ids)) except Exception as e: module.fail_json(msg='Stop instances got an error: {0}'.format(e)) elif state == 'restarted': try: for inst in instances: if inst.reboot(force=module.params['force']): changed = True ids.append(inst.id) module.exit_json(changed=changed, ids=ids, instances=get_instances_info(ecs, ids)) except Exception as e: module.fail_json(msg='Reboot instances got an error: {0}'.format(e)) else: try: for inst in instances: if inst.status != 'stopped' and not force: module.fail_json(msg="Instance is running, and please stop it or set 'force' as True.") if inst.terminate(force=module.params['force']): changed = True module.exit_json(changed=changed, ids=[], instances=[]) except Exception as e: module.fail_json(msg='Delete instance got an error: {0}'.format(e)) if __name__ == '__main__': main()