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 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.
@ -21,11 +21,17 @@ You'll need this Python module installed on the execution host, usually your wor
$ 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``.
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
````````````````
@ -46,9 +52,36 @@ The structure of the ini file must look like this:
endpoint = https://cloud.example.com/client/api
key = api key
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.
.. 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
```````
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,48 +29,49 @@
import os
import time
from ansible.module_utils.six import iteritems
try:
from cs import CloudStack, CloudStackException, read_config
has_lib_cs = True
HAS_LIB_CS = True
except ImportError:
has_lib_cs = False
HAS_LIB_CS = False
CS_HYPERVISORS = [
"KVM", "kvm",
"VMware", "vmware",
"BareMetal", "baremetal",
"XenServer", "xenserver",
"LXC", "lxc",
"HyperV", "hyperv",
"UCS", "ucs",
"OVM", "ovm",
"Simulator", "simulator",
]
'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'),
api_key=dict(default=os.environ.get('CLOUDSTACK_KEY')),
api_secret=dict(default=os.environ.get('CLOUDSTACK_SECRET'), no_log=True),
api_url=dict(default=os.environ.get('CLOUDSTACK_ENDPOINT')),
api_http_method=dict(choices=['get', 'post'], default=os.environ.get('CLOUDSTACK_METHOD') or 'get'),
api_timeout=dict(type='int', default=os.environ.get('CLOUDSTACK_TIMEOUT') or 10),
api_region=dict(default=os.environ.get('CLOUDSTACK_REGION') or 'cloudstack'),
)
def cs_required_together():
return [['api_key', 'api_secret', 'api_url']]
return [['api_key', 'api_secret']]
class AnsibleCloudStack(object):
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")
self.result = {
'changed': False,
'diff' : {
'diff': {
'before': dict(),
'after': dict()
}
@ -79,17 +80,17 @@ class AnsibleCloudStack(object):
# 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',
'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
@ -123,26 +124,30 @@ class AnsibleCloudStack(object):
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))
api_region = self.module.params.get('api_region') or os.environ.get('CLOUDSTACK_REGION')
try:
config = read_config(api_region)
except KeyError:
config = {}
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):
value = self.module.params.get(key)
@ -150,12 +155,6 @@ class AnsibleCloudStack(object):
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):
result = False
for key, value in want_dict.items():
@ -202,7 +201,6 @@ class AnsibleCloudStack(object):
result = True
return result
def _get_by_key(self, key=None, my_dict=None):
if my_dict is None:
my_dict = {}
@ -212,7 +210,6 @@ class AnsibleCloudStack(object):
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:
@ -259,11 +256,10 @@ class AnsibleCloudStack(object):
self._vpc_networks_ids = []
if vpcs:
for vpc in vpcs['vpc']:
for n in vpc.get('network',[]):
for n in vpc.get('network', []):
self._vpc_networks_ids.append(n['id'])
return network_id in self._vpc_networks_ids
def get_network(self, key=None):
"""Return a network dictionary or the value of given key of."""
if self.network:
@ -296,7 +292,6 @@ class AnsibleCloudStack(object):
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)
@ -306,18 +301,18 @@ class AnsibleCloudStack(object):
project = os.environ.get('CLOUDSTACK_PROJECT')
if not project:
return None
args = {}
args['account'] = self.get_account(key='name')
args['domainid'] = self.get_domain(key='id')
args = {
'account': self.get_account(key='name'),
'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'] ]:
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)
@ -341,7 +336,6 @@ class AnsibleCloudStack(object):
self.ip_address = ip_addresses['publicipaddress'][0]
return self._get_by_key(key, self.ip_address)
def get_vm_guest_ip(self):
vm_guest_ip = self.module.params.get('vm_guest_ip')
default_nic = self.get_vm_default_nic()
@ -354,7 +348,6 @@ class AnsibleCloudStack(object):
return vm_guest_ip
self.module.fail_json(msg="Secondary IP '%s' not assigned to VM" % vm_guest_ip)
def get_vm_default_nic(self):
if self.vm_default_nic:
return self.vm_default_nic
@ -367,7 +360,6 @@ class AnsibleCloudStack(object):
return self.vm_default_nic
self.module.fail_json(msg="No default IP address of VM '%s' found" % self.module.params.get('vm'))
def get_vm(self, key=None):
if self.vm:
return self._get_by_key(key, self.vm)
@ -386,12 +378,11 @@ class AnsibleCloudStack(object):
vms = self.cs.listVirtualMachines(**args)
if vms:
for v in vms['virtualmachine']:
if vm.lower() in [ v['name'].lower(), v['displayname'].lower(), v['id'] ]:
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)
@ -411,12 +402,11 @@ class AnsibleCloudStack(object):
if zones:
for z in zones['zone']:
if zone.lower() in [ z['name'].lower(), z['id'] ]:
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)
@ -428,12 +418,11 @@ class AnsibleCloudStack(object):
os_types = self.cs.listOsTypes()
if os_types:
for o in os_types['ostype']:
if os_type in [ o['description'], o['id'] ]:
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
@ -452,7 +441,6 @@ class AnsibleCloudStack(object):
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)
@ -467,17 +455,17 @@ class AnsibleCloudStack(object):
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
args = {
'name': account,
'domainid': self.get_domain(key='id'),
'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)
@ -488,49 +476,46 @@ class AnsibleCloudStack(object):
if not domain:
return None
args = {}
args['listall'] = True
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() ]:
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',[]):
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
args = {
'resourceids': resource['id'],
'resourcetype': resource_type,
'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.")
@ -543,7 +528,6 @@ class AnsibleCloudStack(object):
resource['tags'] = tags
return resource
def get_capabilities(self, key=None):
if self.capabilities:
return self._get_by_key(key, self.capabilities)
@ -551,12 +535,6 @@ class AnsibleCloudStack(object):
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:
@ -570,7 +548,6 @@ class AnsibleCloudStack(object):
time.sleep(2)
return job
def get_result(self, resource):
if resource:
returns = self.common_returns.copy()
@ -584,12 +561,6 @@ class AnsibleCloudStack(object):
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)
self.result['tags'] = resource['tags']
return self.result