Skip to content

Commit ca0a634

Browse files
Add lookup plugin to query CloudStack API (#136)
1 parent 52fcebc commit ca0a634

File tree

2 files changed

+208
-0
lines changed

2 files changed

+208
-0
lines changed

plugins/lookup/api.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Copyright (c) 2024, Lorenzo Tanganelli
2+
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
3+
from __future__ import absolute_import, division, print_function
4+
5+
__metaclass__ = type
6+
7+
DOCUMENTATION = """
8+
name: api
9+
author: Lorenzo Tanganelli (@tanganellilore)
10+
short_description: Iteract with the Cloudstack API via lookup
11+
requirements:
12+
- None
13+
description:
14+
- Returns GET requests from the Cloudstack API.
15+
options:
16+
_terms:
17+
description:
18+
- The endpoint to query, i.e. listUserData, listVirtualMachines, etc.
19+
required: True
20+
query_params:
21+
description:
22+
- The query parameters to search for in the form of key/value pairs.
23+
type: dict
24+
required: False
25+
extends_documentation_fragment:
26+
- ngine_io.cloudstack.cloudstack
27+
notes:
28+
- If the query is not filtered properly this can cause a performance impact.
29+
"""
30+
31+
EXAMPLES = """
32+
- name: List all UserData from the API
33+
set_fact:
34+
controller_settings: "{{ lookup('ngine_io.cloudstack.api', 'listUserData', query_params={ 'listall': true }) }}"
35+
36+
- name: List all Virtual Machines from the API
37+
set_fact:
38+
virtual_machines: "{{ lookup('ngine_io.cloudstack.api', 'listVirtualMachines') }}"
39+
40+
- name: List specific Virtual Machines from the API
41+
set_fact:
42+
virtual_machines: "{{ lookup('ngine_io.cloudstack.api', 'listVirtualMachines', query_params={ 'name': 'myvmname' }) }}"
43+
"""
44+
45+
RETURN = """
46+
_raw:
47+
description:
48+
- Response from the API
49+
type: dict
50+
returned: on successful request
51+
"""
52+
53+
from ansible.plugins.lookup import LookupBase
54+
from ansible.errors import AnsibleError
55+
from ansible.module_utils._text import to_native
56+
from ansible.utils.display import Display
57+
58+
from ..module_utils.cloudstack_api import AnsibleCloudStackAPI
59+
60+
61+
class LookupModule(LookupBase):
62+
display = Display()
63+
64+
def handle_error(self, **kwargs):
65+
raise AnsibleError(to_native(kwargs.get("msg")))
66+
67+
def warn_callback(self, warning):
68+
self.display.warning(warning)
69+
70+
def run(self, terms, variables=None, **kwargs):
71+
if len(terms) != 1:
72+
raise AnsibleError("You must pass exactly one endpoint to query")
73+
74+
self.set_options(direct=kwargs)
75+
76+
module = AnsibleCloudStackAPI(argument_spec={}, direct_params=kwargs, error_callback=self.handle_error, warn_callback=self.warn_callback)
77+
78+
args = {}
79+
if self.get_option("query_params"):
80+
args.update(self.get_option("query_params", {}))
81+
82+
res = module.query_api(terms[0], **args)
83+
84+
if res is None:
85+
return []
86+
if isinstance(res, list):
87+
return res
88+
89+
return [res]
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright (c) 2024, Lorenzo Tanganelli
3+
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
4+
5+
from __future__ import absolute_import, division, print_function
6+
7+
__metaclass__ = type
8+
9+
10+
import os
11+
import re
12+
import sys
13+
import traceback
14+
15+
from ansible.module_utils._text import to_native
16+
from ansible.module_utils.basic import missing_required_lib, AnsibleModule
17+
18+
CS_IMP_ERR = None
19+
try:
20+
from cs import CloudStack, CloudStackException
21+
22+
HAS_LIB_CS = True
23+
except ImportError:
24+
CS_IMP_ERR = traceback.format_exc()
25+
HAS_LIB_CS = False
26+
27+
28+
if sys.version_info > (3,):
29+
long = int
30+
31+
32+
class AnsibleCloudStackAPI(AnsibleModule):
33+
34+
error_callback = None
35+
warn_callback = None
36+
AUTH_ARGSPEC = dict(
37+
api_key=os.getenv("CLOUDSTACK_KEY"),
38+
api_secret=os.getenv("CLOUDSTACK_SECRET"),
39+
api_url=os.getenv("CLOUDSTACK_ENDPOINT"),
40+
api_http_method=os.getenv("CLOUDSTACK_METHOD", "get"),
41+
api_timeout=os.getenv("CLOUDSTACK_TIMEOUT", 10),
42+
api_verify_ssl_cert=os.getenv("CLOUDSTACK_VERIFY"),
43+
validate_certs=os.getenv("CLOUDSTACK_DANGEROUS_NO_TLS_VERIFY", True),
44+
)
45+
46+
def __init__(self, argument_spec=None, direct_params=None, error_callback=None, warn_callback=None, **kwargs):
47+
48+
if not HAS_LIB_CS:
49+
self.fail_json(msg=missing_required_lib("cs"), exception=CS_IMP_ERR)
50+
51+
full_argspec = {}
52+
full_argspec.update(AnsibleCloudStackAPI.AUTH_ARGSPEC)
53+
full_argspec.update(argument_spec)
54+
kwargs["supports_check_mode"] = True
55+
56+
self.error_callback = error_callback
57+
self.warn_callback = warn_callback
58+
59+
self._cs = None
60+
self.result = {}
61+
62+
if direct_params is not None:
63+
for param, value in full_argspec.items():
64+
if param in direct_params:
65+
setattr(self, param, direct_params[param])
66+
else:
67+
setattr(self, param, value)
68+
else:
69+
super(AnsibleCloudStackAPI, self).__init__(argument_spec=full_argspec, **kwargs)
70+
71+
# Perform some basic validation
72+
if not re.match("^https{0,1}://", self.api_url):
73+
self.api_url = "https://{0}".format(self.api_url)
74+
75+
def fail_json(self, **kwargs):
76+
if self.error_callback:
77+
self.error_callback(**kwargs)
78+
else:
79+
super().fail_json(**kwargs)
80+
81+
def exit_json(self, **kwargs):
82+
super().exit_json(**kwargs)
83+
84+
@property
85+
def cs(self):
86+
if self._cs is None:
87+
api_config = self.get_api_config()
88+
self._cs = CloudStack(**api_config)
89+
return self._cs
90+
91+
def get_api_config(self):
92+
api_config = {
93+
"endpoint": self.api_url,
94+
"key": self.api_key,
95+
"secret": self.api_secret,
96+
"timeout": self.api_timeout,
97+
"method": self.api_http_method,
98+
"verify": self.api_verify_ssl_cert,
99+
"dangerous_no_tls_verify": not self.validate_certs,
100+
}
101+
102+
return api_config
103+
104+
def query_api(self, command, **args):
105+
106+
try:
107+
res = getattr(self.cs, command)(**args)
108+
109+
if "errortext" in res:
110+
self.fail_json(msg="Failed: '%s'" % res["errortext"])
111+
112+
except CloudStackException as e:
113+
self.fail_json(msg="CloudStackException: %s" % to_native(e))
114+
115+
except Exception as e:
116+
self.fail_json(msg=to_native(e))
117+
118+
# res.update({'params': self.result, 'query_params': args})
119+
return res

0 commit comments

Comments
 (0)