From 7bedacf012eb4d5dfb0f7ef1cd6a4e9994277f2e Mon Sep 17 00:00:00 2001 From: tanganellilore Date: Thu, 19 Sep 2024 08:43:29 +0200 Subject: [PATCH 1/6] add api lookup --- plugins/lookup/api.py | 87 +++++++++++++++++ plugins/module_utils/cloudstack_api.py | 127 +++++++++++++++++++++++++ 2 files changed, 214 insertions(+) create mode 100644 plugins/lookup/api.py create mode 100644 plugins/module_utils/cloudstack_api.py diff --git a/plugins/lookup/api.py b/plugins/lookup/api.py new file mode 100644 index 00000000..fe28cd52 --- /dev/null +++ b/plugins/lookup/api.py @@ -0,0 +1,87 @@ +# (c) 2020 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = """ +name: api +author: Lorenzo Tanganelli (@tanganellilore) +short_description: Iteract with the Cloudstack API via lookup +requirements: + - None +description: + - Returns GET requests from the Cloudstack API. +options: + _terms: + description: + - The endpoint to query, i.e. listUserData, listVirtualMachines, etc. + required: True + query_params: + description: + - The query parameters to search for in the form of key/value pairs. + type: dict + required: False +extends_documentation_fragment: +- ngine_io.cloudstack.cloudstack +notes: + - If the query is not filtered properly this can cause a performance impact. +""" + +EXAMPLES = """ +- name: List all UserData from the API + set_fact: + controller_settings: "{{ lookup('ngine_io.cloudstack.api', 'listUserData', query_params={ 'listall': true }) }}" + +- name: List all Virtual Machines from the API + set_fact: + virtual_machines: "{{ lookup('ngine_io.cloudstack.api', 'listVirtualMachines') }}" + +- name: List specific Virtual Machines from the API + set_fact: + virtual_machines: "{{ lookup('ngine_io.cloudstack.api', 'listVirtualMachines', query_params={ 'name': 'myvmname' }) }}" +""" + +RETURN = """ +_raw: + description: + - Response from the API + type: dict + returned: on successful request +""" + +from ansible.plugins.lookup import LookupBase +from ansible.errors import AnsibleError +from ansible.module_utils._text import to_native +from ansible.utils.display import Display + +from ..module_utils.cloudstack_api import AnsibleCloudStackAPI + +class LookupModule(LookupBase): + display = Display() + + def handle_error(self, **kwargs): + raise AnsibleError(to_native(kwargs.get('msg'))) + + def warn_callback(self, warning): + self.display.warning(warning) + + def run(self, terms, variables=None, **kwargs): + if len(terms) != 1: + raise AnsibleError('You must pass exactly one endpoint to query') + + self.set_options(direct=kwargs) + + module = AnsibleCloudStackAPI(argument_spec={}, direct_params=kwargs, error_callback=self.handle_error, warn_callback=self.warn_callback) + + args = {} + if self.get_option('query_params'): + args.update(self.get_option('query_params', {})) + + res = module.query_api(terms[0], **args) + + if res is None: + return [] + if type(res) is list: + return res + return [res] diff --git a/plugins/module_utils/cloudstack_api.py b/plugins/module_utils/cloudstack_api.py new file mode 100644 index 00000000..eac94536 --- /dev/null +++ b/plugins/module_utils/cloudstack_api.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024, Lorenzo Tanganelli +# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +import os +import re +import sys +import traceback + +from ansible.module_utils._text import to_native +from ansible.module_utils.basic import missing_required_lib, AnsibleModule + +CS_IMP_ERR = None +try: + from cs import CloudStack, CloudStackException + HAS_LIB_CS = True +except ImportError: + CS_IMP_ERR = traceback.format_exc() + HAS_LIB_CS = False + + +if sys.version_info > (3,): + long = int + + +class AnsibleCloudStackAPI(AnsibleModule): + + error_callback = None + warn_callback = None + AUTH_ARGSPEC = dict( + api_key=os.getenv('CLOUDSTACK_KEY'), + api_secret=os.getenv('CLOUDSTACK_SECRET'), + api_url=os.getenv('CLOUDSTACK_ENDPOINT'), + api_http_method=os.getenv('CLOUDSTACK_METHOD', 'get'), + api_timeout=os.getenv('CLOUDSTACK_TIMEOUT', 10), + api_verify_ssl_cert=os.getenv('CLOUDSTACK_VERIFY'), + validate_certs=os.getenv('CLOUDSTACK_DANGEROUS_NO_TLS_VERIFY', True), + ) + + def __init__(self, argument_spec=None, direct_params=None, error_callback=None, warn_callback=None, **kwargs): + + if not HAS_LIB_CS: + self.fail_json(msg=missing_required_lib('cs'), exception=CS_IMP_ERR) + + full_argspec = {} + full_argspec.update(AnsibleCloudStackAPI.AUTH_ARGSPEC) + full_argspec.update(argument_spec) + kwargs['supports_check_mode'] = True + + self.error_callback = error_callback + self.warn_callback = warn_callback + + self._cs = None + self.result = {} + + if direct_params is not None: + for param, value in full_argspec.items(): + if param in direct_params: + setattr(self, param, direct_params[param]) + else: + setattr(self, param, value) + else: + super(AnsibleCloudStackAPI, self).__init__(argument_spec=full_argspec, **kwargs) + + # Perform some basic validation + if not re.match('^https{0,1}://', self.api_url): + self.api_url = "https://{0}".format(self.api_url) + + + def fail_json(self, **kwargs): + if self.error_callback: + self.error_callback(**kwargs) + else: + super().fail_json(**kwargs) + + def exit_json(self, **kwargs): + super().exit_json(**kwargs) + + @property + def cs(self): + if self._cs is None: + api_config = self.get_api_config() + self._cs = CloudStack(**api_config) + return self._cs + + def get_api_config(self): + api_config = { + 'endpoint': self.api_url, + 'key': self.api_key, + 'secret': self.api_secret, + 'timeout': self.api_timeout, + 'method': self.api_http_method, + 'verify': self.api_verify_ssl_cert, + 'dangerous_no_tls_verify': not self.validate_certs, + } + + # self.result.update({ + # 'api_url': api_config['endpoint'], + # 'api_key': api_config['key'], + # 'api_timeout': int(api_config['timeout']), + # 'api_http_method': api_config['method'], + # 'api_verify_ssl_cert': api_config['verify'], + # 'validate_certs': not api_config['dangerous_no_tls_verify'], + # }) + return api_config + + def query_api(self, command, **args): + + try: + res = getattr(self.cs, command)(**args) + + if 'errortext' in res: + self.fail_json(msg="Failed: '%s'" % res['errortext']) + + except CloudStackException as e: + self.fail_json(msg='CloudStackException: %s' % to_native(e)) + + except Exception as e: + self.fail_json(msg=to_native(e)) + + # res.update({'params': self.result, 'query_params': args}) + return res From d5d03c0e6a791b5b1bf08243e14779ade097fe3a Mon Sep 17 00:00:00 2001 From: tanganellilore Date: Thu, 19 Sep 2024 08:55:48 +0200 Subject: [PATCH 2/6] sanity fix --- plugins/lookup/api.py | 10 ++++++---- plugins/module_utils/cloudstack_api.py | 9 ++++----- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/plugins/lookup/api.py b/plugins/lookup/api.py index fe28cd52..900d232b 100644 --- a/plugins/lookup/api.py +++ b/plugins/lookup/api.py @@ -36,7 +36,7 @@ - name: List all Virtual Machines from the API set_fact: virtual_machines: "{{ lookup('ngine_io.cloudstack.api', 'listVirtualMachines') }}" - + - name: List specific Virtual Machines from the API set_fact: virtual_machines: "{{ lookup('ngine_io.cloudstack.api', 'listVirtualMachines', query_params={ 'name': 'myvmname' }) }}" @@ -57,6 +57,7 @@ from ..module_utils.cloudstack_api import AnsibleCloudStackAPI + class LookupModule(LookupBase): display = Display() @@ -69,19 +70,20 @@ def warn_callback(self, warning): def run(self, terms, variables=None, **kwargs): if len(terms) != 1: raise AnsibleError('You must pass exactly one endpoint to query') - + self.set_options(direct=kwargs) - + module = AnsibleCloudStackAPI(argument_spec={}, direct_params=kwargs, error_callback=self.handle_error, warn_callback=self.warn_callback) args = {} if self.get_option('query_params'): args.update(self.get_option('query_params', {})) - + res = module.query_api(terms[0], **args) if res is None: return [] if type(res) is list: return res + return [res] diff --git a/plugins/module_utils/cloudstack_api.py b/plugins/module_utils/cloudstack_api.py index eac94536..37a1e04a 100644 --- a/plugins/module_utils/cloudstack_api.py +++ b/plugins/module_utils/cloudstack_api.py @@ -43,7 +43,7 @@ class AnsibleCloudStackAPI(AnsibleModule): ) def __init__(self, argument_spec=None, direct_params=None, error_callback=None, warn_callback=None, **kwargs): - + if not HAS_LIB_CS: self.fail_json(msg=missing_required_lib('cs'), exception=CS_IMP_ERR) @@ -54,7 +54,7 @@ def __init__(self, argument_spec=None, direct_params=None, error_callback=None, self.error_callback = error_callback self.warn_callback = warn_callback - + self._cs = None self.result = {} @@ -63,7 +63,7 @@ def __init__(self, argument_spec=None, direct_params=None, error_callback=None, if param in direct_params: setattr(self, param, direct_params[param]) else: - setattr(self, param, value) + setattr(self, param, value) else: super(AnsibleCloudStackAPI, self).__init__(argument_spec=full_argspec, **kwargs) @@ -71,7 +71,6 @@ def __init__(self, argument_spec=None, direct_params=None, error_callback=None, if not re.match('^https{0,1}://', self.api_url): self.api_url = "https://{0}".format(self.api_url) - def fail_json(self, **kwargs): if self.error_callback: self.error_callback(**kwargs) @@ -110,7 +109,7 @@ def get_api_config(self): return api_config def query_api(self, command, **args): - + try: res = getattr(self.cs, command)(**args) From 2e1f31b4b60c118fcc91895c467356465ffb8df4 Mon Sep 17 00:00:00 2001 From: Rene Moser Date: Mon, 2 Dec 2024 08:25:44 +0100 Subject: [PATCH 3/6] add changelog, bump version --- changelogs/fragments/v3.yml | 3 +++ galaxy.yml | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 changelogs/fragments/v3.yml diff --git a/changelogs/fragments/v3.yml b/changelogs/fragments/v3.yml new file mode 100644 index 00000000..ff8fc13e --- /dev/null +++ b/changelogs/fragments/v3.yml @@ -0,0 +1,3 @@ +major_changes: + - General overhaul (black code style) and renaming of all modules (droping cs_ prefix) + ensuring backwards compatibility and integration tests with CloudStack 4.17 and 4.18 (https://github.com/ngine-io/ansible-collection-cloudstack/pull/141). diff --git a/galaxy.yml b/galaxy.yml index 118d65ad..fe00a270 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -14,7 +14,8 @@ description: "Ansible Collection for Apache CloudStack based clouds" documentation: "" homepage: "https://github.com/ngine-io/ansible-collection-cloudstack" issues: "https://github.com/ngine-io/ansible-collection-cloudstack/issues" -license: GPL-3.0-or-later +license: + - GPL-3.0-or-later name: cloudstack namespace: ngine_io readme: README.md @@ -23,4 +24,4 @@ tags: - cloud - cloudstack - ngine_io -version: 2.5.0 +version: 3.0.0 From 7614a8642c9e26ba20315afaec3fe6f3d85d810e Mon Sep 17 00:00:00 2001 From: Lorenzo Tanganelli Date: Mon, 2 Dec 2024 07:49:06 +0000 Subject: [PATCH 4/6] add copyright lookup api --- plugins/lookup/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lookup/api.py b/plugins/lookup/api.py index 900d232b..e0faf7e4 100644 --- a/plugins/lookup/api.py +++ b/plugins/lookup/api.py @@ -1,4 +1,4 @@ -# (c) 2020 Ansible Project +# Copyright (c) 2024, Lorenzo Tanganelli # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function From d1b7c048234f7e638463d7d5f54a23fad5ee1180 Mon Sep 17 00:00:00 2001 From: Lorenzo Tanganelli Date: Mon, 2 Dec 2024 07:52:13 +0000 Subject: [PATCH 5/6] reformat --- plugins/lookup/api.py | 8 ++--- plugins/module_utils/cloudstack_api.py | 49 +++++++++++--------------- 2 files changed, 25 insertions(+), 32 deletions(-) diff --git a/plugins/lookup/api.py b/plugins/lookup/api.py index e0faf7e4..ae4d8920 100644 --- a/plugins/lookup/api.py +++ b/plugins/lookup/api.py @@ -62,22 +62,22 @@ class LookupModule(LookupBase): display = Display() def handle_error(self, **kwargs): - raise AnsibleError(to_native(kwargs.get('msg'))) + raise AnsibleError(to_native(kwargs.get("msg"))) def warn_callback(self, warning): self.display.warning(warning) def run(self, terms, variables=None, **kwargs): if len(terms) != 1: - raise AnsibleError('You must pass exactly one endpoint to query') + raise AnsibleError("You must pass exactly one endpoint to query") self.set_options(direct=kwargs) module = AnsibleCloudStackAPI(argument_spec={}, direct_params=kwargs, error_callback=self.handle_error, warn_callback=self.warn_callback) args = {} - if self.get_option('query_params'): - args.update(self.get_option('query_params', {})) + if self.get_option("query_params"): + args.update(self.get_option("query_params", {})) res = module.query_api(terms[0], **args) diff --git a/plugins/module_utils/cloudstack_api.py b/plugins/module_utils/cloudstack_api.py index 37a1e04a..8911383e 100644 --- a/plugins/module_utils/cloudstack_api.py +++ b/plugins/module_utils/cloudstack_api.py @@ -18,6 +18,7 @@ CS_IMP_ERR = None try: from cs import CloudStack, CloudStackException + HAS_LIB_CS = True except ImportError: CS_IMP_ERR = traceback.format_exc() @@ -33,24 +34,24 @@ class AnsibleCloudStackAPI(AnsibleModule): error_callback = None warn_callback = None AUTH_ARGSPEC = dict( - api_key=os.getenv('CLOUDSTACK_KEY'), - api_secret=os.getenv('CLOUDSTACK_SECRET'), - api_url=os.getenv('CLOUDSTACK_ENDPOINT'), - api_http_method=os.getenv('CLOUDSTACK_METHOD', 'get'), - api_timeout=os.getenv('CLOUDSTACK_TIMEOUT', 10), - api_verify_ssl_cert=os.getenv('CLOUDSTACK_VERIFY'), - validate_certs=os.getenv('CLOUDSTACK_DANGEROUS_NO_TLS_VERIFY', True), + api_key=os.getenv("CLOUDSTACK_KEY"), + api_secret=os.getenv("CLOUDSTACK_SECRET"), + api_url=os.getenv("CLOUDSTACK_ENDPOINT"), + api_http_method=os.getenv("CLOUDSTACK_METHOD", "get"), + api_timeout=os.getenv("CLOUDSTACK_TIMEOUT", 10), + api_verify_ssl_cert=os.getenv("CLOUDSTACK_VERIFY"), + validate_certs=os.getenv("CLOUDSTACK_DANGEROUS_NO_TLS_VERIFY", True), ) def __init__(self, argument_spec=None, direct_params=None, error_callback=None, warn_callback=None, **kwargs): if not HAS_LIB_CS: - self.fail_json(msg=missing_required_lib('cs'), exception=CS_IMP_ERR) + self.fail_json(msg=missing_required_lib("cs"), exception=CS_IMP_ERR) full_argspec = {} full_argspec.update(AnsibleCloudStackAPI.AUTH_ARGSPEC) full_argspec.update(argument_spec) - kwargs['supports_check_mode'] = True + kwargs["supports_check_mode"] = True self.error_callback = error_callback self.warn_callback = warn_callback @@ -68,7 +69,7 @@ def __init__(self, argument_spec=None, direct_params=None, error_callback=None, super(AnsibleCloudStackAPI, self).__init__(argument_spec=full_argspec, **kwargs) # Perform some basic validation - if not re.match('^https{0,1}://', self.api_url): + if not re.match("^https{0,1}://", self.api_url): self.api_url = "https://{0}".format(self.api_url) def fail_json(self, **kwargs): @@ -89,23 +90,15 @@ def cs(self): def get_api_config(self): api_config = { - 'endpoint': self.api_url, - 'key': self.api_key, - 'secret': self.api_secret, - 'timeout': self.api_timeout, - 'method': self.api_http_method, - 'verify': self.api_verify_ssl_cert, - 'dangerous_no_tls_verify': not self.validate_certs, + "endpoint": self.api_url, + "key": self.api_key, + "secret": self.api_secret, + "timeout": self.api_timeout, + "method": self.api_http_method, + "verify": self.api_verify_ssl_cert, + "dangerous_no_tls_verify": not self.validate_certs, } - # self.result.update({ - # 'api_url': api_config['endpoint'], - # 'api_key': api_config['key'], - # 'api_timeout': int(api_config['timeout']), - # 'api_http_method': api_config['method'], - # 'api_verify_ssl_cert': api_config['verify'], - # 'validate_certs': not api_config['dangerous_no_tls_verify'], - # }) return api_config def query_api(self, command, **args): @@ -113,11 +106,11 @@ def query_api(self, command, **args): try: res = getattr(self.cs, command)(**args) - if 'errortext' in res: - self.fail_json(msg="Failed: '%s'" % res['errortext']) + if "errortext" in res: + self.fail_json(msg="Failed: '%s'" % res["errortext"]) except CloudStackException as e: - self.fail_json(msg='CloudStackException: %s' % to_native(e)) + self.fail_json(msg="CloudStackException: %s" % to_native(e)) except Exception as e: self.fail_json(msg=to_native(e)) From 3de47c9762034bf5413dcdec4b023e21385a2d9d Mon Sep 17 00:00:00 2001 From: Lorenzo Tanganelli Date: Mon, 2 Dec 2024 09:33:14 +0000 Subject: [PATCH 6/6] fix sanity --- plugins/lookup/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lookup/api.py b/plugins/lookup/api.py index ae4d8920..d04c83e5 100644 --- a/plugins/lookup/api.py +++ b/plugins/lookup/api.py @@ -83,7 +83,7 @@ def run(self, terms, variables=None, **kwargs): if res is None: return [] - if type(res) is list: + if isinstance(res, list): return res return [res]