#!/usr/bin/python # -*- coding: utf-8 -*- # # (c) 2015, René Moser # # 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 . DOCUMENTATION = ''' --- module: cs_instance short_description: Manages instances and virtual machines on Apache CloudStack based clouds. description: - Deploy, start, restart, stop and destroy instances on Apache CloudStack, Citrix CloudPlatform and Exoscale. version_added: '2.0' author: '"René Moser (@resmo)" ' options: name: description: - Host name of the instance. C(name) can only contain ASCII letters. required: true display_name: description: - Custom display name of the instances. required: false default: null group: description: - Group in where the new instance should be in. required: false default: null state: description: - State of the instance. required: false default: 'present' choices: [ 'deployed', 'started', 'stopped', 'restarted', 'destroyed', 'expunged', 'present', 'absent' ] service_offering: description: - Name or id of the service offering of the new instance. If not set, first found service offering is used. required: false default: null template: description: - Name or id of the template to be used for creating the new instance. Required when using C(state=present). Mutually exclusive with C(ISO) option. required: false default: null iso: description: - Name or id of the ISO to be used for creating the new instance. Required when using C(state=present). Mutually exclusive with C(template) option. required: false default: null hypervisor: description: - Name the hypervisor to be used for creating the new instance. Relevant when using C(state=present) and option C(ISO) is used. If not set, first found hypervisor will be used. required: false default: null choices: [ 'KVM', 'VMware', 'BareMetal', 'XenServer', 'LXC', 'HyperV', 'UCS', 'OVM' ] keyboard: description: - Keyboard device type for the instance. required: false default: null choices: [ 'de', 'de-ch', 'es', 'fi', 'fr', 'fr-be', 'fr-ch', 'is', 'it', 'jp', 'nl-be', 'no', 'pt', 'uk', 'us' ] networks: description: - List of networks to use for the new instance. required: false default: [] aliases: [ 'network' ] ip_address: description: - IPv4 address for default instance's network during creation required: false default: null ip6_address: description: - IPv6 address for default instance's network. required: false default: null disk_offering: description: - Name of the disk offering to be used. required: false default: null disk_size: description: - Disk size in GByte required if deploying instance from ISO. required: false default: null security_groups: description: - List of security groups the instance to be applied to. required: false default: [] aliases: [ 'security_group' ] domain: description: - Domain the instance is related to. required: false default: null account: description: - Account the instance is related to. required: false default: null project: description: - Name of the project the instance to be deployed in. required: false default: null zone: description: - Name of the zone in which the instance shoud be deployed. If not set, default zone is used. required: false default: null ssh_key: description: - Name of the SSH key to be deployed on the new instance. required: false default: null affinity_groups: description: - Affinity groups names to be applied to the new instance. required: false default: [] aliases: [ 'affinity_group' ] user_data: description: - Optional data (ASCII) that can be sent to the instance upon a successful deployment. - The data will be automatically base64 encoded. - Consider switching to HTTP_POST by using C(CLOUDSTACK_METHOD=post) to increase the HTTP_GET size limit of 2KB to 32 KB. required: false default: null force: description: - Force stop/start the instance if required to apply changes, otherwise a running instance will not be changed. required: false default: true tags: description: - List of tags. Tags are a list of dictionaries having keys C(key) and C(value). - "If you want to delete all tags, set a empty list e.g. C(tags: [])." required: false default: null poll_async: description: - Poll async jobs until job has finished. required: false default: true extends_documentation_fragment: cloudstack ''' EXAMPLES = ''' --- # Create a instance on CloudStack from an ISO # NOTE: Names of offerings and ISOs depending on the CloudStack configuration. - local_action: module: cs_instance name: web-vm-1 iso: Linux Debian 7 64-bit hypervisor: VMware project: Integration zone: ch-zrh-ix-01 service_offering: 1cpu_1gb disk_offering: PerfPlus Storage disk_size: 20 networks: - Server Integration - Sync Integration - Storage Integration # For changing a running instance, use the 'force' parameter - local_action: module: cs_instance name: web-vm-1 display_name: web-vm-01.example.com iso: Linux Debian 7 64-bit service_offering: 2cpu_2gb force: yes # Create or update a instance on Exoscale's public cloud - local_action: module: cs_instance name: web-vm-1 template: Linux Debian 7 64-bit service_offering: Tiny ssh_key: john@example.com tags: - { key: admin, value: john } - { key: foo, value: bar } register: vm - debug: msg='default ip {{ vm.default_ip }} and is in state {{ vm.state }}' # Ensure a instance has stopped - local_action: cs_instance name=web-vm-1 state=stopped # Ensure a instance is running - local_action: cs_instance name=web-vm-1 state=started # Remove a instance - local_action: cs_instance name=web-vm-1 state=absent ''' RETURN = ''' --- id: description: ID of the instance. returned: success type: string sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6 name: description: Name of the instance. returned: success type: string sample: web-01 display_name: description: Display name of the instance. returned: success type: string sample: web-01 group: description: Group name of the instance is related. returned: success type: string sample: web created: description: Date of the instance was created. returned: success type: string sample: 2014-12-01T14:57:57+0100 password_enabled: description: True if password setting is enabled. returned: success type: boolean sample: true password: description: The password of the instance if exists. returned: success type: string sample: Ge2oe7Do ssh_key: description: Name of ssh key deployed to instance. returned: success type: string sample: key@work domain: description: Domain the instance is related to. returned: success type: string sample: example domain account: description: Account the instance is related to. returned: success type: string sample: example account project: description: Name of project the instance is related to. returned: success type: string sample: Production default_ip: description: Default IP address of the instance. returned: success type: string sample: 10.23.37.42 public_ip: description: Public IP address with instance via static nat rule. returned: success type: string sample: 1.2.3.4 iso: description: Name of ISO the instance was deployed with. returned: success type: string sample: Debian-8-64bit template: description: Name of template the instance was deployed with. returned: success type: string sample: Debian-8-64bit service_offering: description: Name of the service offering the instance has. returned: success type: string sample: 2cpu_2gb zone: description: Name of zone the instance is in. returned: success type: string sample: ch-gva-2 state: description: State of the instance. returned: success type: string sample: Running security_groups: description: Security groups the instance is in. returned: success type: list sample: '[ "default" ]' affinity_groups: description: Affinity groups the instance is in. returned: success type: list sample: '[ "webservers" ]' tags: description: List of resource tags associated with the instance. returned: success type: dict sample: '[ { "key": "foo", "value": "bar" } ]' ''' import base64 try: from cs import CloudStack, CloudStackException, read_config has_lib_cs = True except ImportError: has_lib_cs = False # import cloudstack common from ansible.module_utils.cloudstack import * class AnsibleCloudStackInstance(AnsibleCloudStack): def __init__(self, module): AnsibleCloudStack.__init__(self, module) self.instance = None def get_service_offering_id(self): service_offering = self.module.params.get('service_offering') service_offerings = self.cs.listServiceOfferings() if service_offerings: if not service_offering: return service_offerings['serviceoffering'][0]['id'] for s in service_offerings['serviceoffering']: if service_offering in [ s['name'], s['id'] ]: return s['id'] self.module.fail_json(msg="Service offering '%s' not found" % service_offering) def get_template_or_iso_id(self): template = self.module.params.get('template') iso = self.module.params.get('iso') if not template and not iso: self.module.fail_json(msg="Template or ISO is required.") if template and iso: self.module.fail_json(msg="Template are ISO are mutually exclusive.") args = {} args['account'] = self.get_account('name') args['domainid'] = self.get_domain('id') args['projectid'] = self.get_project('id') args['zoneid'] = self.get_zone('id') if template: args['templatefilter'] = 'executable' templates = self.cs.listTemplates(**args) if templates: for t in templates['template']: if template in [ t['displaytext'], t['name'], t['id'] ]: return t['id'] self.module.fail_json(msg="Template '%s' not found" % template) elif iso: args['isofilter'] = 'executable' isos = self.cs.listIsos(**args) if isos: for i in isos['iso']: if iso in [ i['displaytext'], i['name'], i['id'] ]: return i['id'] self.module.fail_json(msg="ISO '%s' not found" % iso) def get_disk_offering_id(self): disk_offering = self.module.params.get('disk_offering') if not disk_offering: return None args = {} args['domainid'] = self.get_domain('id') disk_offerings = self.cs.listDiskOfferings(**args) if disk_offerings: for d in disk_offerings['diskoffering']: if disk_offering in [ d['displaytext'], d['name'], d['id'] ]: return d['id'] self.module.fail_json(msg="Disk offering '%s' not found" % disk_offering) def get_instance(self): instance = self.instance if not instance: instance_name = self.module.params.get('name') args = {} args['account'] = self.get_account('name') args['domainid'] = self.get_domain('id') args['projectid'] = self.get_project('id') args['zoneid'] = self.get_zone('id') instances = self.cs.listVirtualMachines(**args) if instances: for v in instances['virtualmachine']: if instance_name in [ v['name'], v['displayname'], v['id'] ]: self.instance = v break return self.instance def get_network_ids(self): network_names = self.module.params.get('networks') if not network_names: return None args = {} args['account'] = self.get_account('name') args['domainid'] = self.get_domain('id') args['projectid'] = self.get_project('id') args['zoneid'] = self.get_zone('id') networks = self.cs.listNetworks(**args) if not networks: self.module.fail_json(msg="No networks available") network_ids = [] network_displaytexts = [] for network_name in network_names: for n in networks['network']: if network_name in [ n['displaytext'], n['name'], n['id'] ]: network_ids.append(n['id']) network_displaytexts.append(n['name']) break if len(network_ids) != len(network_names): self.module.fail_json(msg="Could not find all networks, networks list found: %s" % network_displaytexts) return ','.join(network_ids) def present_instance(self): instance = self.get_instance() if not instance: instance = self.deploy_instance() else: instance = self.update_instance(instance) instance = self.ensure_tags(resource=instance, resource_type='UserVm') return instance def get_user_data(self): user_data = self.module.params.get('user_data') if user_data: user_data = base64.b64encode(user_data) return user_data def get_display_name(self): display_name = self.module.params.get('display_name') if not display_name: display_name = self.module.params.get('name') return display_name def deploy_instance(self): self.result['changed'] = True args = {} args['templateid'] = self.get_template_or_iso_id() args['zoneid'] = self.get_zone('id') args['serviceofferingid'] = self.get_service_offering_id() args['account'] = self.get_account('name') args['domainid'] = self.get_domain('id') args['projectid'] = self.get_project('id') args['diskofferingid'] = self.get_disk_offering_id() args['networkids'] = self.get_network_ids() args['hypervisor'] = self.get_hypervisor() args['userdata'] = self.get_user_data() args['keyboard'] = self.module.params.get('keyboard') args['ipaddress'] = self.module.params.get('ip_address') args['ip6address'] = self.module.params.get('ip6_address') args['name'] = self.module.params.get('name') args['group'] = self.module.params.get('group') args['keypair'] = self.module.params.get('ssh_key') args['size'] = self.module.params.get('disk_size') args['securitygroupnames'] = ','.join(self.module.params.get('security_groups')) args['affinitygroupnames'] = ','.join(self.module.params.get('affinity_groups')) instance = None if not self.module.check_mode: instance = self.cs.deployVirtualMachine(**args) if 'errortext' in instance: self.module.fail_json(msg="Failed: '%s'" % instance['errortext']) poll_async = self.module.params.get('poll_async') if poll_async: instance = self._poll_job(instance, 'virtualmachine') return instance def update_instance(self, instance): args_service_offering = {} args_service_offering['id'] = instance['id'] args_service_offering['serviceofferingid'] = self.get_service_offering_id() args_instance_update = {} args_instance_update['id'] = instance['id'] args_instance_update['group'] = self.module.params.get('group') args_instance_update['displayname'] = self.get_display_name() args_instance_update['userdata'] = self.get_user_data() args_instance_update['ostypeid'] = self.get_os_type_id() args_ssh_key = {} args_ssh_key['id'] = instance['id'] args_ssh_key['keypair'] = self.module.params.get('ssh_key') args_ssh_key['projectid'] = self.get_project('id') if self._has_changed(args_service_offering, instance) or \ self._has_changed(args_instance_update, instance) or \ self._has_changed(args_ssh_key, instance): force = self.module.params.get('force') instance_state = instance['state'].lower() if instance_state == 'stopped' or force: self.result['changed'] = True if not self.module.check_mode: # Ensure VM has stopped instance = self.stop_instance() instance = self._poll_job(instance, 'virtualmachine') self.instance = instance # Change service offering if self._has_changed(args_service_offering, instance): res = self.cs.changeServiceForVirtualMachine(**args_service_offering) if 'errortext' in res: self.module.fail_json(msg="Failed: '%s'" % res['errortext']) instance = res['virtualmachine'] self.instance = instance # Update VM if self._has_changed(args_instance_update, instance): res = self.cs.updateVirtualMachine(**args_instance_update) if 'errortext' in res: self.module.fail_json(msg="Failed: '%s'" % res['errortext']) instance = res['virtualmachine'] self.instance = instance # Reset SSH key if self._has_changed(args_ssh_key, instance): instance = self.cs.resetSSHKeyForVirtualMachine(**args_ssh_key) if 'errortext' in instance: self.module.fail_json(msg="Failed: '%s'" % instance['errortext']) instance = self._poll_job(instance, 'virtualmachine') self.instance = instance # Start VM again if it was running before if instance_state == 'running': instance = self.start_instance() return instance def absent_instance(self): instance = self.get_instance() if instance: if instance['state'].lower() not in ['expunging', 'destroying', 'destroyed']: self.result['changed'] = True if not self.module.check_mode: res = self.cs.destroyVirtualMachine(id=instance['id']) if 'errortext' in res: self.module.fail_json(msg="Failed: '%s'" % res['errortext']) poll_async = self.module.params.get('poll_async') if poll_async: instance = self._poll_job(res, 'virtualmachine') return instance def expunge_instance(self): instance = self.get_instance() if instance: res = {} if instance['state'].lower() in [ 'destroying', 'destroyed' ]: self.result['changed'] = True if not self.module.check_mode: res = self.cs.expungeVirtualMachine(id=instance['id']) elif instance['state'].lower() not in [ 'expunging' ]: self.result['changed'] = True if not self.module.check_mode: res = self.cs.destroyVirtualMachine(id=instance['id'], expunge=True) if res and 'errortext' in res: self.module.fail_json(msg="Failed: '%s'" % res['errortext']) poll_async = self.module.params.get('poll_async') if poll_async: instance = self._poll_job(res, 'virtualmachine') return instance def stop_instance(self): instance = self.get_instance() if not instance: self.module.fail_json(msg="Instance named '%s' not found" % self.module.params.get('name')) if instance['state'].lower() in ['stopping', 'stopped']: return instance if instance['state'].lower() in ['starting', 'running']: self.result['changed'] = True if not self.module.check_mode: instance = self.cs.stopVirtualMachine(id=instance['id']) if 'errortext' in instance: self.module.fail_json(msg="Failed: '%s'" % instance['errortext']) poll_async = self.module.params.get('poll_async') if poll_async: instance = self._poll_job(instance, 'virtualmachine') return instance def start_instance(self): instance = self.get_instance() if not instance: self.module.fail_json(msg="Instance named '%s' not found" % module.params.get('name')) if instance['state'].lower() in ['starting', 'running']: return instance if instance['state'].lower() in ['stopped', 'stopping']: self.result['changed'] = True if not self.module.check_mode: instance = self.cs.startVirtualMachine(id=instance['id']) if 'errortext' in instance: self.module.fail_json(msg="Failed: '%s'" % instance['errortext']) poll_async = self.module.params.get('poll_async') if poll_async: instance = self._poll_job(instance, 'virtualmachine') return instance def restart_instance(self): instance = self.get_instance() if not instance: module.fail_json(msg="Instance named '%s' not found" % self.module.params.get('name')) if instance['state'].lower() in [ 'running', 'starting' ]: self.result['changed'] = True if not self.module.check_mode: instance = self.cs.rebootVirtualMachine(id=instance['id']) if 'errortext' in instance: self.module.fail_json(msg="Failed: '%s'" % instance['errortext']) poll_async = self.module.params.get('poll_async') if poll_async: instance = self._poll_job(instance, 'virtualmachine') elif instance['state'].lower() in [ 'stopping', 'stopped' ]: instance = self.start_instance() return instance def get_result(self, instance): if instance: if 'id' in instance: self.result['id'] = instance['id'] if 'name' in instance: self.result['name'] = instance['name'] if 'displayname' in instance: self.result['display_name'] = instance['displayname'] if 'group' in instance: self.result['group'] = instance['group'] if 'domain' in instance: self.result['domain'] = instance['domain'] if 'account' in instance: self.result['account'] = instance['account'] if 'project' in instance: self.result['project'] = instance['project'] if 'publicip' in instance: self.result['public_ip'] = instance['public_ip'] if 'passwordenabled' in instance: self.result['password_enabled'] = instance['passwordenabled'] if 'password' in instance: self.result['password'] = instance['password'] if 'serviceofferingname' in instance: self.result['service_offering'] = instance['serviceofferingname'] if 'zonename' in instance: self.result['zone'] = instance['zonename'] if 'templatename' in instance: self.result['template'] = instance['templatename'] if 'isoname' in instance: self.result['iso'] = instance['isoname'] if 'keypair' in instance: self.result['ssh_key'] = instance['keypair'] if 'created' in instance: self.result['created'] = instance['created'] if 'state' in instance: self.result['state'] = instance['state'] if 'tags' in instance: self.result['tags'] = [] for tag in instance['tags']: result_tag = {} result_tag['key'] = tag['key'] result_tag['value'] = tag['value'] self.result['tags'].append(result_tag) if 'securitygroup' in instance: security_groups = [] for securitygroup in instance['securitygroup']: security_groups.append(securitygroup['name']) self.result['security_groups'] = security_groups if 'affinitygroup' in instance: affinity_groups = [] for affinitygroup in instance['affinitygroup']: affinity_groups.append(affinitygroup['name']) self.result['affinity_groups'] = affinity_groups if 'nic' in instance: for nic in instance['nic']: if nic['isdefault']: self.result['default_ip'] = nic['ipaddress'] return self.result def main(): module = AnsibleModule( argument_spec = dict( name = dict(required=True), display_name = dict(default=None), group = dict(default=None), state = dict(choices=['present', 'deployed', 'started', 'stopped', 'restarted', 'absent', 'destroyed', 'expunged'], default='present'), service_offering = dict(default=None), template = dict(default=None), iso = dict(default=None), networks = dict(type='list', aliases=[ 'network' ], default=None), ip_address = dict(defaul=None), ip6_address = dict(defaul=None), disk_offering = dict(default=None), disk_size = dict(type='int', default=None), keyboard = dict(choices=['de', 'de-ch', 'es', 'fi', 'fr', 'fr-be', 'fr-ch', 'is', 'it', 'jp', 'nl-be', 'no', 'pt', 'uk', 'us'], default=None), hypervisor = dict(default=None), security_groups = dict(type='list', aliases=[ 'security_group' ], default=[]), affinity_groups = dict(type='list', aliases=[ 'affinity_group' ], default=[]), domain = dict(default=None), account = dict(default=None), project = dict(default=None), user_data = dict(default=None), zone = dict(default=None), ssh_key = dict(default=None), force = dict(choices=BOOLEANS, default=False), tags = dict(type='list', aliases=[ 'tag' ], default=None), poll_async = dict(choices=BOOLEANS, default=True), api_key = dict(default=None), api_secret = dict(default=None, no_log=True), api_url = dict(default=None), api_http_method = dict(default='get'), ), supports_check_mode=True ) if not has_lib_cs: module.fail_json(msg="python library cs required: pip install cs") try: acs_instance = AnsibleCloudStackInstance(module) state = module.params.get('state') if state in ['absent', 'destroyed']: instance = acs_instance.absent_instance() elif state in ['expunged']: instance = acs_instance.expunge_instance() elif state in ['present', 'deployed']: instance = acs_instance.present_instance() elif state in ['stopped']: instance = acs_instance.stop_instance() elif state in ['started']: instance = acs_instance.start_instance() elif state in ['restarted']: instance = acs_instance.restart_instance() if instance and 'state' in instance and instance['state'].lower() == 'error': module.fail_json(msg="Instance named '%s' in error state." % module.params.get('name')) result = acs_instance.get_result(instance) except CloudStackException, e: module.fail_json(msg='CloudStackException: %s' % str(e)) module.exit_json(**result) # import module snippets from ansible.module_utils.basic import * main()