summaryrefslogtreecommitdiff
path: root/lib/ansible/module_utils/vultr.py
blob: 7beb4fa3c71fc1b4ea809deb64852eadaa0255a9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# -*- 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, to_native
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')),
        api_retries=dict(type='int', default=os.environ.get('VULTR_API_RETRIES')),
        api_account=dict(default=os.environ.get('VULTR_API_ACCOUNT') or 'default'),
        api_endpoint=dict(default=os.environ.get('VULTR_API_ENDPOINT')),
        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 = {}

        try:
            self.api_config = {
                'api_key': self.module.params.get('api_key') or config.get('key'),
                'api_timeout': self.module.params.get('api_timeout') or int(config.get('timeout') or 60),
                'api_retries': self.module.params.get('api_retries') or int(config.get('retries') or 5),
                'api_endpoint': self.module.params.get('api_endpoint') or config.get('endpoint') or VULTR_API_ENDPOINT,
            }
        except ValueError as e:
            self.fail_json(msg="One of the following settings, "
                               "in section '%s' in the ini config file has not an int value: timeout, retries. "
                               "Error was %s" % (self.module.params.get('api_account'), to_native(e)))

        # Common vultr returns
        self.result['vultr_api'] = {
            'api_account': self.module.params.get('api_account'),
            'api_timeout': self.api_config['api_timeout'],
            'api_retries': self.api_config['api_retries'],
            'api_endpoint': self.api_config['api_endpoint'],
        }

        # 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', 'endpoint']
        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 = self.api_config['api_endpoint'] + path

        if data:
            data_encoded = dict()
            data_list = ""
            for k, v in data.items():
                if isinstance(v, list):
                    for s in v:
                        data_list += '&%s[]=%s' % (k, urllib.quote(s))
                elif v is not None:
                    data_encoded[k] = v
            data = urllib.urlencode(data_encoded) + data_list

        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 with %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