cloudstack: implement config overloading and ENV vars for API auth (#22724)

* cloudstack: fix connection by ENV vars and configs overloading

* cloudstack: pep8 cloudstack module_utils

* cloudstack: allow api_url to be set in ini config

* docsite: explain ENV vars support as written in python-cs for ansible
This commit is contained in:
René Moser 2017-03-17 11:01:43 +01:00 committed by GitHub
parent 8de05d3752
commit b90517caf9
2 changed files with 117 additions and 113 deletions

View file

@ -13,7 +13,7 @@ Ansible contains a number of extra modules for interacting with CloudStack based
Prerequisites Prerequisites
````````````` `````````````
Prerequisites for using the CloudStack modules are minimal. In addition to ansible itself, all of the modules require the python library ``cs`` https://pypi.python.org/pypi/cs. Prerequisites for using the CloudStack modules are minimal. In addition to Ansible itself, all of the modules require the python library ``cs`` https://pypi.python.org/pypi/cs.
You'll need this Python module installed on the execution host, usually your workstation. You'll need this Python module installed on the execution host, usually your workstation.
@ -21,11 +21,17 @@ You'll need this Python module installed on the execution host, usually your wor
$ pip install cs $ pip install cs
Or alternatively starting with Debian 9 and Ubuntu 16.04:
.. code-block:: bash
$ sudo apt install python-cs
.. note:: cs also includes a command line interface for ad-hoc interaction with the CloudStack API e.g. ``$ cs listVirtualMachines state=Running``. .. note:: cs also includes a command line interface for ad-hoc interaction with the CloudStack API e.g. ``$ cs listVirtualMachines state=Running``.
Limitations and Known Issues Limitations and Known Issues
```````````````````````````` ````````````````````````````
VPC support is not yet fully implemented and tested. The community is working on the VPC integration. VPC support has been improved since Ansible 2.3 but is still not yet fully implemented. The community is working on the VPC integration.
Credentials File Credentials File
```````````````` ````````````````
@ -46,9 +52,36 @@ The structure of the ini file must look like this:
endpoint = https://cloud.example.com/client/api endpoint = https://cloud.example.com/client/api
key = api key key = api key
secret = api secret secret = api secret
timeout = 30
.. Note:: The section ``[cloudstack]`` is the default section. ``CLOUDSTACK_REGION`` environment variable can be used to define the default section. .. Note:: The section ``[cloudstack]`` is the default section. ``CLOUDSTACK_REGION`` environment variable can be used to define the default section.
.. versionadded:: 2.4
The ENV variables support ``CLOUDSTACK_*`` as written in the documentation of the library ``cs``, like e.g ``CLOUDSTACK_TIMEOUT``, ``CLOUDSTACK_METHOD``, etc. has been implemented into Ansible. It is even possible to have some incomplete config in your cloudstack.ini:
.. code-block:: bash
$ cat $HOME/.cloudstack.ini
[cloudstack]
endpoint = https://cloud.example.com/client/api
timeout = 30
and fulfill the missing data by either setting ENV variables or tasks params:
.. code-block:: yaml
---
- name: provision our VMs
hosts: cloud-vm
connection: local
tasks:
- name: ensure VMs are created and running
cs_instance:
api_key: your api key
api_secret: your api secret
...
Regions Regions
``````` ```````
If you use more than one CloudStack region, you can define as many sections as you want and name them as you like, e.g.: If you use more than one CloudStack region, you can define as many sections as you want and name them as you like, e.g.:

View file

@ -29,43 +29,44 @@
import os import os
import time import time
from ansible.module_utils.six import iteritems
try: try:
from cs import CloudStack, CloudStackException, read_config from cs import CloudStack, CloudStackException, read_config
has_lib_cs = True HAS_LIB_CS = True
except ImportError: except ImportError:
has_lib_cs = False HAS_LIB_CS = False
CS_HYPERVISORS = [ CS_HYPERVISORS = [
"KVM", "kvm", 'KVM', 'kvm',
"VMware", "vmware", 'VMware', 'vmware',
"BareMetal", "baremetal", 'BareMetal', 'baremetal',
"XenServer", "xenserver", 'XenServer', 'xenserver',
"LXC", "lxc", 'LXC', 'lxc',
"HyperV", "hyperv", 'HyperV', 'hyperv',
"UCS", "ucs", 'UCS', 'ucs',
"OVM", "ovm", 'OVM', 'ovm',
"Simulator", "simulator", 'Simulator', 'simulator',
] ]
def cs_argument_spec(): def cs_argument_spec():
return dict( return dict(
api_key = dict(default=None), api_key=dict(default=os.environ.get('CLOUDSTACK_KEY')),
api_secret = dict(default=None, no_log=True), api_secret=dict(default=os.environ.get('CLOUDSTACK_SECRET'), no_log=True),
api_url = dict(default=None), api_url=dict(default=os.environ.get('CLOUDSTACK_ENDPOINT')),
api_http_method = dict(choices=['get', 'post'], default='get'), api_http_method=dict(choices=['get', 'post'], default=os.environ.get('CLOUDSTACK_METHOD') or 'get'),
api_timeout = dict(type='int', default=10), api_timeout=dict(type='int', default=os.environ.get('CLOUDSTACK_TIMEOUT') or 10),
api_region = dict(default='cloudstack'), api_region=dict(default=os.environ.get('CLOUDSTACK_REGION') or 'cloudstack'),
) )
def cs_required_together(): def cs_required_together():
return [['api_key', 'api_secret', 'api_url']] return [['api_key', 'api_secret']]
class AnsibleCloudStack(object): class AnsibleCloudStack(object):
def __init__(self, module): def __init__(self, module):
if not has_lib_cs: if not HAS_LIB_CS:
module.fail_json(msg="python library cs required: pip install cs") module.fail_json(msg="python library cs required: pip install cs")
self.result = { self.result = {
@ -123,26 +124,30 @@ class AnsibleCloudStack(object):
self.hypervisor = None self.hypervisor = None
self.capabilities = None self.capabilities = None
def _connect(self): def _connect(self):
api_key = self.module.params.get('api_key') api_region = self.module.params.get('api_region') or os.environ.get('CLOUDSTACK_REGION')
api_secret = self.module.params.get('api_secret') try:
api_url = self.module.params.get('api_url') config = read_config(api_region)
api_http_method = self.module.params.get('api_http_method') except KeyError:
api_timeout = self.module.params.get('api_timeout') config = {}
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))
api_config = {
'endpoint': self.module.params.get('api_url') or config.get('endpoint'),
'key': self.module.params.get('api_key') or config.get('key'),
'secret': self.module.params.get('api_secret') or config.get('secret'),
'timeout': self.module.params.get('api_timeout') or config.get('timeout'),
'method': self.module.params.get('api_http_method') or config.get('method'),
}
self.result.update({
'api_region': api_region,
'api_url': api_config['endpoint'],
'api_key': api_config['key'],
'api_timeout': api_config['timeout'],
'api_http_method': api_config['method'],
})
if not all([api_config['endpoint'], api_config['key'], api_config['secret']]):
self.module.fail_json(msg="Missing api credentials: can not authenticate", result=self.result)
self.cs = CloudStack(**api_config)
def get_or_fallback(self, key=None, fallback_key=None): def get_or_fallback(self, key=None, fallback_key=None):
value = self.module.params.get(key) value = self.module.params.get(key)
@ -150,12 +155,6 @@ class AnsibleCloudStack(object):
value = self.module.params.get(fallback_key) value = self.module.params.get(fallback_key)
return value 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): def has_changed(self, want_dict, current_dict, only_keys=None):
result = False result = False
for key, value in want_dict.items(): for key, value in want_dict.items():
@ -202,7 +201,6 @@ class AnsibleCloudStack(object):
result = True result = True
return result return result
def _get_by_key(self, key=None, my_dict=None): def _get_by_key(self, key=None, my_dict=None):
if my_dict is None: if my_dict is None:
my_dict = {} my_dict = {}
@ -212,7 +210,6 @@ class AnsibleCloudStack(object):
self.module.fail_json(msg="Something went wrong: %s not found" % key) self.module.fail_json(msg="Something went wrong: %s not found" % key)
return my_dict return my_dict
def get_vpc(self, key=None): def get_vpc(self, key=None):
"""Return a VPC dictionary or the value of given key of.""" """Return a VPC dictionary or the value of given key of."""
if self.vpc: if self.vpc:
@ -263,7 +260,6 @@ class AnsibleCloudStack(object):
self._vpc_networks_ids.append(n['id']) self._vpc_networks_ids.append(n['id'])
return network_id in self._vpc_networks_ids return network_id in self._vpc_networks_ids
def get_network(self, key=None): def get_network(self, key=None):
"""Return a network dictionary or the value of given key of.""" """Return a network dictionary or the value of given key of."""
if self.network: if self.network:
@ -296,7 +292,6 @@ class AnsibleCloudStack(object):
return self._get_by_key(key, self.network) return self._get_by_key(key, self.network)
self.module.fail_json(msg="Network '%s' not found" % network) self.module.fail_json(msg="Network '%s' not found" % network)
def get_project(self, key=None): def get_project(self, key=None):
if self.project: if self.project:
return self._get_by_key(key, self.project) return self._get_by_key(key, self.project)
@ -306,9 +301,10 @@ class AnsibleCloudStack(object):
project = os.environ.get('CLOUDSTACK_PROJECT') project = os.environ.get('CLOUDSTACK_PROJECT')
if not project: if not project:
return None return None
args = {} args = {
args['account'] = self.get_account(key='name') 'account': self.get_account(key='name'),
args['domainid'] = self.get_domain(key='id') 'domainid': self.get_domain(key='id')
}
projects = self.cs.listProjects(**args) projects = self.cs.listProjects(**args)
if projects: if projects:
for p in projects['project']: for p in projects['project']:
@ -317,7 +313,6 @@ class AnsibleCloudStack(object):
return self._get_by_key(key, self.project) return self._get_by_key(key, self.project)
self.module.fail_json(msg="project '%s' not found" % project) self.module.fail_json(msg="project '%s' not found" % project)
def get_ip_address(self, key=None): def get_ip_address(self, key=None):
if self.ip_address: if self.ip_address:
return self._get_by_key(key, self.ip_address) return self._get_by_key(key, self.ip_address)
@ -341,7 +336,6 @@ class AnsibleCloudStack(object):
self.ip_address = ip_addresses['publicipaddress'][0] self.ip_address = ip_addresses['publicipaddress'][0]
return self._get_by_key(key, self.ip_address) return self._get_by_key(key, self.ip_address)
def get_vm_guest_ip(self): def get_vm_guest_ip(self):
vm_guest_ip = self.module.params.get('vm_guest_ip') vm_guest_ip = self.module.params.get('vm_guest_ip')
default_nic = self.get_vm_default_nic() default_nic = self.get_vm_default_nic()
@ -354,7 +348,6 @@ class AnsibleCloudStack(object):
return vm_guest_ip return vm_guest_ip
self.module.fail_json(msg="Secondary IP '%s' not assigned to VM" % vm_guest_ip) self.module.fail_json(msg="Secondary IP '%s' not assigned to VM" % vm_guest_ip)
def get_vm_default_nic(self): def get_vm_default_nic(self):
if self.vm_default_nic: if self.vm_default_nic:
return self.vm_default_nic return self.vm_default_nic
@ -367,7 +360,6 @@ class AnsibleCloudStack(object):
return self.vm_default_nic return self.vm_default_nic
self.module.fail_json(msg="No default IP address of VM '%s' found" % self.module.params.get('vm')) self.module.fail_json(msg="No default IP address of VM '%s' found" % self.module.params.get('vm'))
def get_vm(self, key=None): def get_vm(self, key=None):
if self.vm: if self.vm:
return self._get_by_key(key, self.vm) return self._get_by_key(key, self.vm)
@ -391,7 +383,6 @@ class AnsibleCloudStack(object):
return self._get_by_key(key, self.vm) return self._get_by_key(key, self.vm)
self.module.fail_json(msg="Virtual machine '%s' not found" % vm) self.module.fail_json(msg="Virtual machine '%s' not found" % vm)
def get_zone(self, key=None): def get_zone(self, key=None):
if self.zone: if self.zone:
return self._get_by_key(key, self.zone) return self._get_by_key(key, self.zone)
@ -416,7 +407,6 @@ class AnsibleCloudStack(object):
return self._get_by_key(key, self.zone) return self._get_by_key(key, self.zone)
self.module.fail_json(msg="zone '%s' not found" % zone) self.module.fail_json(msg="zone '%s' not found" % zone)
def get_os_type(self, key=None): def get_os_type(self, key=None):
if self.os_type: if self.os_type:
return self._get_by_key(key, self.zone) return self._get_by_key(key, self.zone)
@ -433,7 +423,6 @@ class AnsibleCloudStack(object):
return self._get_by_key(key, self.os_type) return self._get_by_key(key, self.os_type)
self.module.fail_json(msg="OS type '%s' not found" % os_type) self.module.fail_json(msg="OS type '%s' not found" % os_type)
def get_hypervisor(self): def get_hypervisor(self):
if self.hypervisor: if self.hypervisor:
return self.hypervisor return self.hypervisor
@ -452,7 +441,6 @@ class AnsibleCloudStack(object):
return self.hypervisor return self.hypervisor
self.module.fail_json(msg="Hypervisor '%s' not found" % hypervisor) self.module.fail_json(msg="Hypervisor '%s' not found" % hypervisor)
def get_account(self, key=None): def get_account(self, key=None):
if self.account: if self.account:
return self._get_by_key(key, self.account) return self._get_by_key(key, self.account)
@ -467,17 +455,17 @@ class AnsibleCloudStack(object):
if not domain: if not domain:
self.module.fail_json(msg="Account must be specified with Domain") self.module.fail_json(msg="Account must be specified with Domain")
args = {} args = {
args['name'] = account 'name': account,
args['domainid'] = self.get_domain(key='id') 'domainid': self.get_domain(key='id'),
args['listall'] = True 'listall': True
}
accounts = self.cs.listAccounts(**args) accounts = self.cs.listAccounts(**args)
if accounts: if accounts:
self.account = accounts['account'][0] self.account = accounts['account'][0]
return self._get_by_key(key, self.account) return self._get_by_key(key, self.account)
self.module.fail_json(msg="Account '%s' not found" % account) self.module.fail_json(msg="Account '%s' not found" % account)
def get_domain(self, key=None): def get_domain(self, key=None):
if self.domain: if self.domain:
return self._get_by_key(key, self.domain) return self._get_by_key(key, self.domain)
@ -488,8 +476,9 @@ class AnsibleCloudStack(object):
if not domain: if not domain:
return None return None
args = {} args = {
args['listall'] = True 'listall': True,
}
domains = self.cs.listDomains(**args) domains = self.cs.listDomains(**args)
if domains: if domains:
for d in domains['domain']: for d in domains['domain']:
@ -498,39 +487,35 @@ class AnsibleCloudStack(object):
return self._get_by_key(key, self.domain) return self._get_by_key(key, self.domain)
self.module.fail_json(msg="Domain '%s' not found" % domain) self.module.fail_json(msg="Domain '%s' not found" % domain)
def get_tags(self, resource=None): def get_tags(self, resource=None):
existing_tags = [] existing_tags = []
for tag in resource.get('tags', []): for tag in resource.get('tags', []):
existing_tags.append({'key': tag['key'], 'value': tag['value']}) existing_tags.append({'key': tag['key'], 'value': tag['value']})
return existing_tags return existing_tags
def _process_tags(self, resource, resource_type, tags, operation="create"): def _process_tags(self, resource, resource_type, tags, operation="create"):
if tags: if tags:
self.result['changed'] = True self.result['changed'] = True
if not self.module.check_mode: if not self.module.check_mode:
args = {} args = {
args['resourceids'] = resource['id'] 'resourceids': resource['id'],
args['resourcetype'] = resource_type 'resourcetype': resource_type,
args['tags'] = tags 'tags': tags,
}
if operation == "create": if operation == "create":
response = self.cs.createTags(**args) response = self.cs.createTags(**args)
else: else:
response = self.cs.deleteTags(**args) response = self.cs.deleteTags(**args)
self.poll_job(response) self.poll_job(response)
def _tags_that_should_exist_or_be_updated(self, resource, tags): def _tags_that_should_exist_or_be_updated(self, resource, tags):
existing_tags = self.get_tags(resource) existing_tags = self.get_tags(resource)
return [tag for tag in tags if tag not in existing_tags] return [tag for tag in tags if tag not in existing_tags]
def _tags_that_should_not_exist(self, resource, tags): def _tags_that_should_not_exist(self, resource, tags):
existing_tags = self.get_tags(resource) existing_tags = self.get_tags(resource)
return [tag for tag in existing_tags if tag not in tags] return [tag for tag in existing_tags if tag not in tags]
def ensure_tags(self, resource, resource_type=None): def ensure_tags(self, resource, resource_type=None):
if not resource_type or not resource: if not resource_type or not resource:
self.module.fail_json(msg="Error: Missing resource or resource_type for tags.") self.module.fail_json(msg="Error: Missing resource or resource_type for tags.")
@ -543,7 +528,6 @@ class AnsibleCloudStack(object):
resource['tags'] = tags resource['tags'] = tags
return resource return resource
def get_capabilities(self, key=None): def get_capabilities(self, key=None):
if self.capabilities: if self.capabilities:
return self._get_by_key(key, self.capabilities) return self._get_by_key(key, self.capabilities)
@ -551,12 +535,6 @@ class AnsibleCloudStack(object):
self.capabilities = capabilities['capability'] self.capabilities = capabilities['capability']
return self._get_by_key(key, self.capabilities) 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): def poll_job(self, job=None, key=None):
if 'jobid' in job: if 'jobid' in job:
while True: while True:
@ -570,7 +548,6 @@ class AnsibleCloudStack(object):
time.sleep(2) time.sleep(2)
return job return job
def get_result(self, resource): def get_result(self, resource):
if resource: if resource:
returns = self.common_returns.copy() returns = self.common_returns.copy()
@ -584,12 +561,6 @@ class AnsibleCloudStack(object):
if search_key in resource: if search_key in resource:
self.result[return_key] = int(resource[search_key]) self.result[return_key] = int(resource[search_key])
# Special handling for tags
if 'tags' in resource: if 'tags' in resource:
self.result['tags'] = [] self.result['tags'] = resource['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 return self.result