mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-04-23 10:51:24 -07:00
In some rare situations, the CloudStack API returns string for numbers when we expected int. With this fix, we ensure we compare the types expected.
503 lines
18 KiB
Python
503 lines
18 KiB
Python
# -*- coding: utf-8 -*-
|
|
#
|
|
# (c) 2015, René Moser <mail@renemoser.net>
|
|
#
|
|
# This code is part of Ansible, but is an independent component.
|
|
# This particular file snippet, and this file snippet only, is BSD licensed.
|
|
# Modules you write using this snippet, which is embedded dynamically by Ansible
|
|
# still belong to the author of the module, and may assign their own license
|
|
# to the complete work.
|
|
#
|
|
# Redistribution and use in source and binary forms, with or without modification,
|
|
# are permitted provided that the following conditions are met:
|
|
#
|
|
# * Redistributions of source code must retain the above copyright
|
|
# notice, this list of conditions and the following disclaimer.
|
|
# * Redistributions in binary form must reproduce the above copyright notice,
|
|
# this list of conditions and the following disclaimer in the documentation
|
|
# and/or other materials provided with the distribution.
|
|
#
|
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
|
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
|
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
import time
|
|
|
|
try:
|
|
from cs import CloudStack, CloudStackException, read_config
|
|
has_lib_cs = True
|
|
except ImportError:
|
|
has_lib_cs = False
|
|
|
|
CS_HYPERVISORS = [
|
|
"KVM", "kvm",
|
|
"VMware", "vmware",
|
|
"BareMetal", "baremetal",
|
|
"XenServer", "xenserver",
|
|
"LXC", "lxc",
|
|
"HyperV", "hyperv",
|
|
"UCS", "ucs",
|
|
"OVM", "ovm",
|
|
"Simulator", "simulator",
|
|
]
|
|
|
|
def cs_argument_spec():
|
|
return dict(
|
|
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),
|
|
api_region = dict(default='cloudstack'),
|
|
)
|
|
|
|
def cs_required_together():
|
|
return [['api_key', 'api_secret', 'api_url']]
|
|
|
|
class AnsibleCloudStack(object):
|
|
|
|
def __init__(self, module):
|
|
if not has_lib_cs:
|
|
module.fail_json(msg="python library cs required: pip install cs")
|
|
|
|
self.result = {
|
|
'changed': False,
|
|
}
|
|
|
|
# Common returns, will be merged with self.returns
|
|
# search_for_key: replace_with_key
|
|
self.common_returns = {
|
|
'id': 'id',
|
|
'name': 'name',
|
|
'created': 'created',
|
|
'zonename': 'zone',
|
|
'state': 'state',
|
|
'project': 'project',
|
|
'account': 'account',
|
|
'domain': 'domain',
|
|
'displaytext': 'display_text',
|
|
'displayname': 'display_name',
|
|
'description': 'description',
|
|
}
|
|
|
|
# Init returns dict for use in subclasses
|
|
self.returns = {}
|
|
# these values will be casted to int
|
|
self.returns_to_int = {}
|
|
# these keys will be compared case sensitive in self.has_changed()
|
|
self.case_sensitive_keys = [
|
|
'id',
|
|
'displaytext',
|
|
'displayname',
|
|
'description',
|
|
]
|
|
|
|
self.module = module
|
|
self._connect()
|
|
|
|
self.domain = None
|
|
self.account = None
|
|
self.project = None
|
|
self.ip_address = None
|
|
self.network = None
|
|
self.vpc = 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('api_secret')
|
|
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:
|
|
api_region = self.module.params.get('api_region', 'cloudstack')
|
|
self.cs = CloudStack(**read_config(api_region))
|
|
|
|
|
|
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:
|
|
if isinstance(value, (int, float, long, complex)):
|
|
# ensure we compare the same type
|
|
if isinstance(value, int):
|
|
current_dict[key] = int(current_dict[key])
|
|
elif isinstance(value, float):
|
|
current_dict[key] = float(current_dict[key])
|
|
elif isinstance(value, long):
|
|
current_dict[key] = long(current_dict[key])
|
|
elif isinstance(value, complex):
|
|
current_dict[key] = complex(current_dict[key])
|
|
|
|
if value != current_dict[key]:
|
|
return True
|
|
else:
|
|
if self.case_sensitive_keys and key in self.case_sensitive_keys:
|
|
if value != current_dict[key].encode('utf-8'):
|
|
return True
|
|
|
|
# Test for diff in case insensitive way
|
|
elif value.lower() != current_dict[key].encode('utf-8').lower():
|
|
return True
|
|
else:
|
|
return True
|
|
return False
|
|
|
|
|
|
def _get_by_key(self, key=None, my_dict=None):
|
|
if my_dict is 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_vpc(self, key=None):
|
|
"""Return a VPC dictionary or the value of given key of."""
|
|
if self.vpc:
|
|
return self._get_by_key(key, self.vpc)
|
|
|
|
vpc = self.module.params.get('vpc')
|
|
if not vpc:
|
|
return None
|
|
|
|
args = {
|
|
'account': self.get_account(key='name'),
|
|
'domainid': self.get_domain(key='id'),
|
|
'projectid': self.get_project(key='id'),
|
|
'zoneid': self.get_zone(key='id'),
|
|
}
|
|
vpcs = self.cs.listVPCs(**args)
|
|
if not vpcs:
|
|
self.module.fail_json(msg="No VPCs available.")
|
|
|
|
for v in vpcs['vpc']:
|
|
if vpc in [v['displaytext'], v['name'], v['id']]:
|
|
self.vpc = v
|
|
return self._get_by_key(key, self.vpc)
|
|
self.module.fail_json(msg="VPC '%s' not found" % vpc)
|
|
|
|
|
|
def get_network(self, key=None):
|
|
"""Return a network dictionary or the value of given key of."""
|
|
if self.network:
|
|
return self._get_by_key(key, self.network)
|
|
|
|
network = self.module.params.get('network')
|
|
if not network:
|
|
return None
|
|
|
|
args = {
|
|
'account': self.get_account('name'),
|
|
'domainid': self.get_domain('id'),
|
|
'projectid': self.get_project('id'),
|
|
'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']]:
|
|
self.network = n
|
|
return self._get_by_key(key, self.network)
|
|
self.module.fail_json(msg="Network '%s' not found" % network)
|
|
|
|
|
|
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_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')
|
|
ip_addresses = self.cs.listPublicIpAddresses(**args)
|
|
|
|
if not ip_addresses:
|
|
self.module.fail_json(msg="IP address '%s' not found" % args['ipaddress'])
|
|
|
|
self.ip_address = ip_addresses['publicipaddress'][0]
|
|
return self._get_by_key(key, self.ip_address)
|
|
|
|
|
|
def get_vm(self, key=None):
|
|
if self.vm:
|
|
return self._get_by_key(key, self.vm)
|
|
|
|
vm = self.module.params.get('vm')
|
|
if not vm:
|
|
self.module.fail_json(msg="Virtual machine param 'vm' is required")
|
|
|
|
args = {}
|
|
args['account'] = self.get_account(key='name')
|
|
args['domainid'] = self.get_domain(key='id')
|
|
args['projectid'] = self.get_project(key='id')
|
|
args['zoneid'] = self.get_zone(key='id')
|
|
vms = self.cs.listVirtualMachines(**args)
|
|
if vms:
|
|
for v in vms['virtualmachine']:
|
|
if vm.lower() in [ v['name'].lower(), v['displayname'].lower(), v['id'] ]:
|
|
self.vm = v
|
|
return self._get_by_key(key, self.vm)
|
|
self.module.fail_json(msg="Virtual machine '%s' not found" % vm)
|
|
|
|
|
|
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.lower() in [ z['name'].lower(), 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_os_type(self, key=None):
|
|
if self.os_type:
|
|
return self._get_by_key(key, self.zone)
|
|
|
|
os_type = self.module.params.get('os_type')
|
|
if not os_type:
|
|
return None
|
|
|
|
os_types = self.cs.listOsTypes()
|
|
if os_types:
|
|
for o in os_types['ostype']:
|
|
if os_type in [ o['description'], o['id'] ]:
|
|
self.os_type = o
|
|
return self._get_by_key(key, self.os_type)
|
|
self.module.fail_json(msg="OS type '%s' not found" % os_type)
|
|
|
|
|
|
def get_hypervisor(self):
|
|
if self.hypervisor:
|
|
return self.hypervisor
|
|
|
|
hypervisor = self.module.params.get('hypervisor')
|
|
hypervisors = self.cs.listHypervisors()
|
|
|
|
# use the first hypervisor if no hypervisor param given
|
|
if not hypervisor:
|
|
self.hypervisor = hypervisors['hypervisor'][0]['name']
|
|
return self.hypervisor
|
|
|
|
for h in hypervisors['hypervisor']:
|
|
if hypervisor.lower() == h['name'].lower():
|
|
self.hypervisor = h['name']
|
|
return self.hypervisor
|
|
self.module.fail_json(msg="Hypervisor '%s' not found" % hypervisor)
|
|
|
|
|
|
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)
|
|
|
|
|
|
def get_tags(self, resource=None):
|
|
existing_tags = []
|
|
for tag in resource.get('tags',[]):
|
|
existing_tags.append({'key': tag['key'], 'value': tag['value']})
|
|
return existing_tags
|
|
|
|
|
|
def _process_tags(self, resource, resource_type, tags, operation="create"):
|
|
if tags:
|
|
self.result['changed'] = True
|
|
if not self.module.check_mode:
|
|
args = {}
|
|
args['resourceids'] = resource['id']
|
|
args['resourcetype'] = resource_type
|
|
args['tags'] = tags
|
|
if operation == "create":
|
|
response = self.cs.createTags(**args)
|
|
else:
|
|
response = self.cs.deleteTags(**args)
|
|
self.poll_job(response)
|
|
|
|
|
|
def _tags_that_should_exist_or_be_updated(self, resource, tags):
|
|
existing_tags = self.get_tags(resource)
|
|
return [tag for tag in tags if tag not in existing_tags]
|
|
|
|
|
|
def _tags_that_should_not_exist(self, resource, tags):
|
|
existing_tags = self.get_tags(resource)
|
|
return [tag for tag in existing_tags if tag not in tags]
|
|
|
|
|
|
def ensure_tags(self, resource, resource_type=None):
|
|
if not resource_type or not resource:
|
|
self.module.fail_json(msg="Error: Missing resource or resource_type for tags.")
|
|
|
|
if 'tags' in resource:
|
|
tags = self.module.params.get('tags')
|
|
if tags is not None:
|
|
self._process_tags(resource, resource_type, self._tags_that_should_not_exist(resource, tags), operation="delete")
|
|
self._process_tags(resource, resource_type, self._tags_that_should_exist_or_be_updated(resource, tags))
|
|
resource['tags'] = tags
|
|
return resource
|
|
|
|
|
|
def get_capabilities(self, key=None):
|
|
if self.capabilities:
|
|
return self._get_by_key(key, self.capabilities)
|
|
capabilities = self.cs.listCapabilities()
|
|
self.capabilities = capabilities['capability']
|
|
return self._get_by_key(key, self.capabilities)
|
|
|
|
|
|
# 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
|
|
|
|
|
|
def get_result(self, resource):
|
|
if resource:
|
|
returns = self.common_returns.copy()
|
|
returns.update(self.returns)
|
|
for search_key, return_key in returns.iteritems():
|
|
if search_key in resource:
|
|
self.result[return_key] = resource[search_key]
|
|
|
|
# Bad bad API does not always return int when it should.
|
|
for search_key, return_key in self.returns_to_int.iteritems():
|
|
if search_key in resource:
|
|
self.result[return_key] = int(resource[search_key])
|
|
|
|
# Special handling for tags
|
|
if 'tags' in resource:
|
|
self.result['tags'] = []
|
|
for tag in resource['tags']:
|
|
result_tag = {}
|
|
result_tag['key'] = tag['key']
|
|
result_tag['value'] = tag['value']
|
|
self.result['tags'].append(result_tag)
|
|
return self.result
|