From 3a1720d6f087082d4d95ca605c94064ab46f7ae0 Mon Sep 17 00:00:00 2001 From: Darren Worrall Date: Mon, 10 Aug 2015 15:53:46 +0100 Subject: [PATCH] Add cs_ip_address module --- .../extras/cloud/cloudstack/cs_ip_address.py | 459 ++++++++++++++++++ 1 file changed, 459 insertions(+) create mode 100644 lib/ansible/modules/extras/cloud/cloudstack/cs_ip_address.py diff --git a/lib/ansible/modules/extras/cloud/cloudstack/cs_ip_address.py b/lib/ansible/modules/extras/cloud/cloudstack/cs_ip_address.py new file mode 100644 index 0000000000..e63ae0b7ae --- /dev/null +++ b/lib/ansible/modules/extras/cloud/cloudstack/cs_ip_address.py @@ -0,0 +1,459 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2015, Darren Worrall +# +# 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_ip_address +short_description: Manages Public/Secondary IP address associations +description: + - Acquires and associates a public IP to an account. Due to API limitations, + - this is not an idempotent call, so be sure to only conditionally call this + - when C(state=present) +version_added: '2.0' +author: "Darren Worrall @dazworrall" +options: + ip_address: + description: + - Public IP address. + - Required if C(state=absent) + required: true + domain: + description: + - Domain the IP address is related to. + required: false + default: null + network: + description: + - Network the IP address is related to. + required: false + default: null + account: + description: + - Account the IP address is related to. + required: false + default: null + project: + description: + - Name of the project the IP address is related to. + required: false + default: null + zone: + description: + - Name of the zone in which the virtual machine is in. + - If not set, default zone is used. + required: false + default: null + poll_async: + description: + - Poll async jobs until job has finished. + required: false + default: true +extends_documentation_fragment: cloudstack +''' + +EXAMPLES = ''' +# Associate an IP address +- local_action: + module: cs_ip_address + account: My Account + register: ip_address + when: create_instance|changed + +# Disassociate an IP address +- local_action: + module: cs_ip_address + ip_address: 1.2.3.4 + state: absent +''' + +RETURN = ''' +--- +ip_address: + description: Public IP address. + returned: success + type: string + sample: 1.2.3.4 +zone: + description: Name of zone the IP address is related to. + returned: success + type: string + sample: ch-gva-2 +project: + description: Name of project the IP address is related to. + returned: success + type: string + sample: Production +account: + description: Account the IP address is related to. + returned: success + type: string + sample: example account +domain: + description: Domain the IP address is related to. + returned: success + type: string + sample: example domain +''' + + +try: + from cs import CloudStack, CloudStackException, read_config + has_lib_cs = True +except ImportError: + has_lib_cs = False + +# import cloudstack common +class AnsibleCloudStack: + + def __init__(self, module): + if not has_lib_cs: + module.fail_json(msg="python library cs required: pip install cs") + + self.result = { + 'changed': False, + } + + self.module = module + self._connect() + + self.domain = None + self.account = None + self.project = None + self.ip_address = None + self.zone = None + self.vm = None + self.os_type = None + self.hypervisor = None + self.capabilities = None + + + def _connect(self): + api_key = self.module.params.get('api_key') + api_secret = self.module.params.get('secret_key') + api_url = self.module.params.get('api_url') + api_http_method = self.module.params.get('api_http_method') + api_timeout = self.module.params.get('api_timeout') + + if api_key and api_secret and api_url: + self.cs = CloudStack( + endpoint=api_url, + key=api_key, + secret=api_secret, + timeout=api_timeout, + method=api_http_method + ) + else: + self.cs = CloudStack(**read_config()) + + + def get_or_fallback(self, key=None, fallback_key=None): + value = self.module.params.get(key) + if not value: + value = self.module.params.get(fallback_key) + return value + + + # TODO: for backward compatibility only, remove if not used anymore + def _has_changed(self, want_dict, current_dict, only_keys=None): + return self.has_changed(want_dict=want_dict, current_dict=current_dict, only_keys=only_keys) + + + def has_changed(self, want_dict, current_dict, only_keys=None): + for key, value in want_dict.iteritems(): + + # Optionally limit by a list of keys + if only_keys and key not in only_keys: + continue; + + # Skip None values + if value is None: + continue; + + if key in current_dict: + + # API returns string for int in some cases, just to make sure + if isinstance(value, int): + current_dict[key] = int(current_dict[key]) + elif isinstance(value, str): + current_dict[key] = str(current_dict[key]) + + # Only need to detect a singe change, not every item + if value != current_dict[key]: + return True + return False + + + def _get_by_key(self, key=None, my_dict={}): + if key: + if key in my_dict: + return my_dict[key] + self.module.fail_json(msg="Something went wrong: %s not found" % key) + return my_dict + + + def get_project(self, key=None): + if self.project: + return self._get_by_key(key, self.project) + + project = self.module.params.get('project') + if not project: + return None + args = {} + args['account'] = self.get_account(key='name') + args['domainid'] = self.get_domain(key='id') + projects = self.cs.listProjects(**args) + if projects: + for p in projects['project']: + if project.lower() in [ p['name'].lower(), p['id'] ]: + self.project = p + return self._get_by_key(key, self.project) + self.module.fail_json(msg="project '%s' not found" % project) + + + def get_network(self, key=None, network=None): + if not network: + network = self.module.params.get('network') + + if not network: + 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") + + for n in networks['network']: + if network in [ n['displaytext'], n['name'], n['id'] ]: + return self._get_by_key(key, n) + break + self.module.fail_json(msg="Network '%s' not found" % network) + + + def get_ip_address(self, key=None): + if self.ip_address: + return self._get_by_key(key, self.ip_address) + + ip_address = self.module.params.get('ip_address') + if not ip_address: + self.module.fail_json(msg="IP address param 'ip_address' is required") + + args = {} + args['ipaddress'] = ip_address + args['account'] = self.get_account(key='name') + args['domainid'] = self.get_domain(key='id') + args['projectid'] = self.get_project(key='id') + args['listall'] = True + ip_addresses = self.cs.listPublicIpAddresses(**args) + + if ip_addresses: + self.ip_address = ip_addresses['publicipaddress'][0] + return self._get_by_key(key, self.ip_address) + + + def get_zone(self, key=None): + if self.zone: + return self._get_by_key(key, self.zone) + + zone = self.module.params.get('zone') + zones = self.cs.listZones() + + # use the first zone if no zone param given + if not zone: + self.zone = zones['zone'][0] + return self._get_by_key(key, self.zone) + + if zones: + for z in zones['zone']: + if zone in [ z['name'], z['id'] ]: + self.zone = z + return self._get_by_key(key, self.zone) + self.module.fail_json(msg="zone '%s' not found" % zone) + + + def get_account(self, key=None): + if self.account: + return self._get_by_key(key, self.account) + + account = self.module.params.get('account') + if not account: + return None + + domain = self.module.params.get('domain') + if not domain: + self.module.fail_json(msg="Account must be specified with Domain") + + args = {} + args['name'] = account + args['domainid'] = self.get_domain(key='id') + args['listall'] = True + accounts = self.cs.listAccounts(**args) + if accounts: + self.account = accounts['account'][0] + return self._get_by_key(key, self.account) + self.module.fail_json(msg="Account '%s' not found" % account) + + + def get_domain(self, key=None): + if self.domain: + return self._get_by_key(key, self.domain) + + domain = self.module.params.get('domain') + if not domain: + return None + + args = {} + args['listall'] = True + domains = self.cs.listDomains(**args) + if domains: + for d in domains['domain']: + if d['path'].lower() in [ domain.lower(), "root/" + domain.lower(), "root" + domain.lower() ] : + self.domain = d + return self._get_by_key(key, self.domain) + self.module.fail_json(msg="Domain '%s' not found" % domain) + + + # TODO: for backward compatibility only, remove if not used anymore + def _poll_job(self, job=None, key=None): + return self.poll_job(job=job, key=key) + + + def poll_job(self, job=None, key=None): + if 'jobid' in job: + while True: + res = self.cs.queryAsyncJobResult(jobid=job['jobid']) + if res['jobstatus'] != 0 and 'jobresult' in res: + if 'errortext' in res['jobresult']: + self.module.fail_json(msg="Failed: '%s'" % res['jobresult']['errortext']) + if key and key in res['jobresult']: + job = res['jobresult'][key] + break + time.sleep(2) + return job + + +class AnsibleCloudStackIPAddress(AnsibleCloudStack): + + def __init__(self, module): + AnsibleCloudStack.__init__(self, module) + self.vm_default_nic = None + + def associate_ip_address(self): + self.result['changed'] = True + args = {} + args['account'] = self.get_account(key='id') + args['networkid'] = self.get_network(key='id') + args['zoneid'] = self.get_zone(key='id') + ip_address = {} + if not self.module.check_mode: + res = self.cs.associateIpAddress(**args) + if 'errortext' in res: + self.module.fail_json(msg="Failed: '%s'" % res['errortext']) + + poll_async = self.module.params.get('poll_async') + if poll_async: + res = self._poll_job(res, 'ipaddress') + ip_address = res + return ip_address + + def disassociate_ip_address(self): + ip_address = self.get_ip_address() + if ip_address is None: + return ip_address + if ip_address['isstaticnat']: + self.module.fail_json(msg="IP address is allocated via static nat") + + self.result['changed'] = True + if not self.module.check_mode: + res = self.cs.disassociateIpAddress(id=ip_address['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: + res = self._poll_job(res, 'ipaddress') + return ip_address + + def get_result(self, ip_address): + if ip_address: + if 'zonename' in ip_address: + self.result['zone'] = ip_address['zonename'] + if 'domain' in ip_address: + self.result['domain'] = ip_address['domain'] + if 'account' in ip_address: + self.result['account'] = ip_address['account'] + if 'project' in ip_address: + self.result['project'] = ip_address['project'] + if 'ipaddress' in ip_address: + self.result['ip_address'] = ip_address['ipaddress'] + if 'id' in ip_address: + self.result['id'] = ip_address['id'] + return self.result + + +def main(): + module = AnsibleModule( + argument_spec = dict( + ip_address = dict(required=False), + state = dict(choices=['present', 'absent'], default='present'), + zone = dict(default=None), + domain = dict(default=None), + account = dict(default=None), + network = dict(default=None), + project = dict(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(choices=['get', 'post'], default='get'), + api_timeout = dict(type='int', default=10), + ), + required_together = ( + ['api_key', 'api_secret', 'api_url'], + ), + supports_check_mode=True + ) + + if not has_lib_cs: + module.fail_json(msg="python library cs required: pip install cs") + + try: + acs_ip_address = AnsibleCloudStackIPAddress(module) + + state = module.params.get('state') + if state in ['absent']: + ip_address = acs_ip_address.disassociate_ip_address() + else: + ip_address = acs_ip_address.associate_ip_address() + + result = acs_ip_address.get_result(ip_address) + + except CloudStackException, e: + module.fail_json(msg='CloudStackException: %s' % str(e)) + + module.exit_json(**result) + +# import module snippets +from ansible.module_utils.basic import * +if __name__ == '__main__': + main()