Repository: ansible/ansible

Issue:
vultr integration
##### SUMMARY
Vultr Ansible integration containing multiple modules, tests, and module_utils.

##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
 - New Module Pull Request

##### COMPONENT NAME
vutlr modules

##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --version" between quotes below -->
```
2.5
```


##### ADDITIONAL INFORMATION

Old PR: #28696


Discussion:
@resmo this PR contains more than one new module. 

Please submit only one new module per pullrequest. For further explanation, please read [grouped module documentation](http://docs.ansible.com/ansible/dev_guide/developing_modules_in_groups.html)

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: multiple_module_notify --->
@resmo can you remake PR with module_utils and single module? its hard to review soo many new files in one sitting.

Resolving patch:
diff --git a/lib/ansible/module_utils/vultr.py b/lib/ansible/module_utils/vultr.py
new file mode 100644
--- /dev/null
+++ b/lib/ansible/module_utils/vultr.py
@@ -0,0 +1,220 @@
+# -*- coding: utf-8 -*-
+# (c) 2017, René Moser <mail@renemoser.net>
+# 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
diff --git a/lib/ansible/utils/module_docs_fragments/vultr.py b/lib/ansible/utils/module_docs_fragments/vultr.py
new file mode 100644
--- /dev/null
+++ b/lib/ansible/utils/module_docs_fragments/vultr.py
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2017 René Moser <mail@renemoser.net>
+# 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/.
+'''


Test patch: