From c098c42ab93cda68d18914fa0918272787d3e661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Wed, 1 Nov 2017 20:16:59 +0100 Subject: [PATCH] vultr: new module utils and common docs (#30868) * vultr: new module utils and common docs * vultr: minor fixes after review --- lib/ansible/module_utils/vultr.py | 220 ++++++++++++++++++ lib/ansible/modules/cloud/vultr/__init__.py | 0 .../utils/module_docs_fragments/vultr.py | 39 ++++ 3 files changed, 259 insertions(+) create mode 100644 lib/ansible/module_utils/vultr.py create mode 100644 lib/ansible/modules/cloud/vultr/__init__.py create mode 100644 lib/ansible/utils/module_docs_fragments/vultr.py diff --git a/lib/ansible/module_utils/vultr.py b/lib/ansible/module_utils/vultr.py new file mode 100644 index 00000000000..4842992e891 --- /dev/null +++ b/lib/ansible/module_utils/vultr.py @@ -0,0 +1,220 @@ +# -*- coding: utf-8 -*- +# (c) 2017, René Moser +# 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 time +import urllib +from ansible.module_utils.six.moves import configparser +from ansible.module_utils._text import to_text +from ansible.module_utils.urls import fetch_url + + +VULTR_API_ENDPOINT = "https://api.vultr.com" + + +def vultr_argument_spec(): + return dict( + api_key=dict(default=os.environ.get('VULTR_API_KEY'), no_log=True), + api_timeout=dict(type='int', default=os.environ.get('VULTR_API_TIMEOUT') or 60), + api_retries=dict(type='int', default=os.environ.get('VULTR_API_RETRIES') or 5), + api_account=dict(default=os.environ.get('VULTR_API_ACCOUNT') or 'default'), + validate_certs=dict(default=True, type='bool'), + ) + + +class Vultr: + + def __init__(self, module, namespace): + self.module = module + + # Namespace use for returns + self.namespace = namespace + self.result = { + 'changed': False, + namespace: dict(), + 'diff': dict(before=dict(), after=dict()) + } + + # For caching HTTP API responses + self.api_cache = dict() + + # Reads the config from vultr.ini + try: + config = self.read_ini_config() + except KeyError: + config = {} + + self.api_config = { + 'api_key': self.module.params.get('api_key') or config.get('key'), + 'api_timeout': self.module.params.get('api_timeout') or config.get('timeout'), + 'api_retries': self.module.params.get('api_retries') or config.get('retries'), + } + + # Common vultr returns + self.result['vultr_api'] = { + 'api_account': self.module.params.get('api_account'), + 'api_timeout': self.api_config['api_timeout'], + } + + # Headers to be passed to the API + self.headers = { + 'API-Key': "%s" % self.api_config['api_key'], + 'User-Agent': "Ansible Vultr", + 'Accept': 'application/json', + } + + def read_ini_config(self): + ini_group = self.module.params.get('api_account') + + keys = ['key', 'timeout', 'retries'] + env_conf = {} + for key in keys: + if 'VULTR_API_%s' % key.upper() not in os.environ: + break + else: + env_conf[key] = os.environ['VULTR_API_%s' % key.upper()] + else: + return env_conf + + paths = ( + os.path.join(os.path.expanduser('~'), '.vultr.ini'), + os.path.join(os.getcwd(), 'vultr.ini'), + ) + if 'VULTR_API_CONFIG' in os.environ: + paths += (os.path.expanduser(os.environ['VULTR_API_CONFIG']),) + if not any((os.path.exists(c) for c in paths)): + self.module.fail_json(msg="Config file not found. Tried : %s" % ", ".join(paths)) + + conf = configparser.ConfigParser() + conf.read(paths) + return dict(conf.items(ini_group)) + + def fail_json(self, **kwargs): + self.result.update(kwargs) + self.module.fail_json(**self.result) + + def get_yes_or_no(self, key): + if self.module.params.get(key) is not None: + return 'yes' if self.module.params.get(key) is True else 'no' + + def switch_enable_disable(self, resource, param_key, resource_key=None): + if resource_key is None: + resource_key = param_key + + param = self.module.params.get(param_key) + if param is None: + return + + r_value = resource.get(resource_key) + if isinstance(param, bool): + if param is True and r_value not in ['yes', 'enable']: + return "enable" + elif param is False and r_value not in ['no', 'disable']: + return "disable" + else: + if r_value is None: + return "enable" + else: + return "disable" + + def api_query(self, path="/", method="GET", data=None): + url = VULTR_API_ENDPOINT + path + + if data: + data_encoded = dict() + for k, v in data.items(): + if v is not None: + data_encoded[k] = v + data = urllib.urlencode(data_encoded) + + for s in range(0, self.api_config['api_retries']): + response, info = fetch_url( + module=self.module, + url=url, + data=data, + method=method, + headers=self.headers, + timeout=self.api_config['api_timeout'], + ) + + # Did we hit the rate limit? + if info.get('status') and info.get('status') != 503: + break + + # Vultr has a rate limiting requests per second, try to be polite + time.sleep(1) + + else: + self.fail_json(msg="Reached API retries limit %s for URL %s, method %s with data %s. Returned %s, with body: %s %s" % ( + self.api_config['api_retries'], + url, + method, + data, + info['status'], + info['msg'], + info.get('body') + )) + + if info.get('status') != 200: + self.fail_json(msg="URL %s, method %s with data %s. Returned %s, with body: %s %s" % ( + url, + method, + data, + info['status'], + info['msg'], + info.get('body') + )) + + res = response.read() + if not res: + return {} + + try: + return self.module.from_json(to_text(res)) + except ValueError as e: + self.module.fail_json(msg="Could not process response into json: %s" % e) + + def query_resource_by_key(self, key, value, resource='regions', query_by='list', params=None, use_cache=False): + if not value: + return {} + + if use_cache: + if resource in self.api_cache: + if self.api_cache[resource] and self.api_cache[resource].get(key) == value: + return self.api_cache[resource] + + r_list = self.api_query(path="/v1/%s/%s" % (resource, query_by), data=params) + + if not r_list: + return {} + + for r_id, r_data in r_list.items(): + if r_data[key] == value: + self.api_cache.update({ + resource: r_data + }) + return r_data + + self.module.fail_json(msg="Could not find %s %s: %s" % (resource, key, value)) + + def get_result(self, resource): + if resource: + for search_key, config in self.returns.items(): + if search_key in resource: + if 'convert_to' in config: + if config['convert_to'] == 'int': + resource[search_key] = int(resource[search_key]) + elif config['convert_to'] == 'float': + resource[search_key] = float(resource[search_key]) + elif config['convert_to'] == 'bool': + resource[search_key] = True if resource[search_key] == 'yes' else False + + if 'key' in config: + self.result[self.namespace][config['key']] = resource[search_key] + else: + self.result[self.namespace][search_key] = resource[search_key] + return self.result diff --git a/lib/ansible/modules/cloud/vultr/__init__.py b/lib/ansible/modules/cloud/vultr/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/ansible/utils/module_docs_fragments/vultr.py b/lib/ansible/utils/module_docs_fragments/vultr.py new file mode 100644 index 00000000000..c544a8881ab --- /dev/null +++ b/lib/ansible/utils/module_docs_fragments/vultr.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017 René Moser +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +class ModuleDocFragment(object): + + # Standard documentation fragment + DOCUMENTATION = ''' +options: + api_key: + description: + - API key of the Vultr API. + - The ENV variable C(VULTR_API_KEY) is used as default, when defined. + api_timeout: + description: + - HTTP timeout to Vultr API. + - The ENV variable C(VULTR_API_TIMEOUT) is used as default, when defined. + default: 10 + api_retries: + description: + - Amount of retries in case of the Vultr API retuns an HTTP 503 code. + - The ENV variable C(VULTR_API_RETRIES) is used as default, when defined. + default: 10 + api_account: + description: + - Name of the ini section in the C(vultr.ini) file. + - The ENV variable C(VULTR_API_ACCOUNT) is used as default, when defined. + default: default + validate_certs: + description: + - Validate SSL certs of the Vultr API. + default: true + type: bool +requirements: + - "python >= 2.6" +notes: + - Also see the API documentation on https://www.vultr.com/api/. +'''