2017-11-01 19:16:59 +00:00
|
|
|
# -*- 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
|
2018-01-14 15:31:28 +00:00
|
|
|
from ansible.module_utils._text import to_text, to_native
|
2017-11-01 19:16:59 +00:00
|
|
|
from ansible.module_utils.urls import fetch_url
|
|
|
|
|
|
|
|
|
|
|
|
VULTR_API_ENDPOINT = "https://api.vultr.com"
|
2018-08-23 21:14:26 +00:00
|
|
|
VULTR_USER_AGENT = 'Ansible Vultr'
|
2017-11-01 19:16:59 +00:00
|
|
|
|
|
|
|
|
|
|
|
def vultr_argument_spec():
|
|
|
|
return dict(
|
|
|
|
api_key=dict(default=os.environ.get('VULTR_API_KEY'), no_log=True),
|
2018-01-14 13:20:26 +00:00
|
|
|
api_timeout=dict(type='int', default=os.environ.get('VULTR_API_TIMEOUT')),
|
|
|
|
api_retries=dict(type='int', default=os.environ.get('VULTR_API_RETRIES')),
|
2017-11-01 19:16:59 +00:00
|
|
|
api_account=dict(default=os.environ.get('VULTR_API_ACCOUNT') or 'default'),
|
2018-01-14 15:31:28 +00:00
|
|
|
api_endpoint=dict(default=os.environ.get('VULTR_API_ENDPOINT')),
|
2017-11-01 19:16:59 +00:00
|
|
|
validate_certs=dict(default=True, type='bool'),
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
class Vultr:
|
|
|
|
|
|
|
|
def __init__(self, module, namespace):
|
2018-08-13 08:21:50 +00:00
|
|
|
|
|
|
|
if module._name.startswith('vr_'):
|
|
|
|
module.deprecate("The Vultr modules were renamed. The prefix of the modules changed from vr_ to vultr_", version='2.11')
|
|
|
|
|
2017-11-01 19:16:59 +00:00
|
|
|
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()
|
|
|
|
|
|
|
|
try:
|
2018-07-16 13:00:08 +00:00
|
|
|
config = self.read_env_variables()
|
2018-08-23 21:14:26 +00:00
|
|
|
config.update(Vultr.read_ini_config(self.module.params.get('api_account')))
|
2017-11-01 19:16:59 +00:00
|
|
|
except KeyError:
|
|
|
|
config = {}
|
|
|
|
|
2018-01-14 15:31:28 +00:00
|
|
|
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)))
|
2017-11-01 19:16:59 +00:00
|
|
|
|
2018-07-16 13:00:08 +00:00
|
|
|
if not self.api_config.get('api_key'):
|
|
|
|
self.module.fail_json(msg="The API key is not speicied. Please refer to the documentation.")
|
|
|
|
|
2017-11-01 19:16:59 +00:00
|
|
|
# Common vultr returns
|
|
|
|
self.result['vultr_api'] = {
|
|
|
|
'api_account': self.module.params.get('api_account'),
|
|
|
|
'api_timeout': self.api_config['api_timeout'],
|
2018-01-14 15:31:28 +00:00
|
|
|
'api_retries': self.api_config['api_retries'],
|
|
|
|
'api_endpoint': self.api_config['api_endpoint'],
|
2017-11-01 19:16:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
# Headers to be passed to the API
|
|
|
|
self.headers = {
|
|
|
|
'API-Key': "%s" % self.api_config['api_key'],
|
2018-08-23 21:14:26 +00:00
|
|
|
'User-Agent': VULTR_USER_AGENT,
|
2017-11-01 19:16:59 +00:00
|
|
|
'Accept': 'application/json',
|
|
|
|
}
|
|
|
|
|
2018-07-16 13:00:08 +00:00
|
|
|
def read_env_variables(self):
|
2018-01-14 15:31:28 +00:00
|
|
|
keys = ['key', 'timeout', 'retries', 'endpoint']
|
2017-11-01 19:16:59 +00:00
|
|
|
env_conf = {}
|
|
|
|
for key in keys:
|
|
|
|
if 'VULTR_API_%s' % key.upper() not in os.environ:
|
2018-07-16 13:00:08 +00:00
|
|
|
continue
|
|
|
|
env_conf[key] = os.environ['VULTR_API_%s' % key.upper()]
|
|
|
|
|
|
|
|
return env_conf
|
|
|
|
|
2018-08-23 21:14:26 +00:00
|
|
|
@staticmethod
|
|
|
|
def read_ini_config(ini_group):
|
2017-11-01 19:16:59 +00:00
|
|
|
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']),)
|
|
|
|
|
|
|
|
conf = configparser.ConfigParser()
|
|
|
|
conf.read(paths)
|
2018-07-16 13:00:08 +00:00
|
|
|
|
|
|
|
if not conf._sections.get(ini_group):
|
|
|
|
return dict()
|
|
|
|
|
2017-11-01 19:16:59 +00:00
|
|
|
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):
|
2018-01-14 15:31:28 +00:00
|
|
|
url = self.api_config['api_endpoint'] + path
|
2017-11-01 19:16:59 +00:00
|
|
|
|
|
|
|
if data:
|
|
|
|
data_encoded = dict()
|
2018-01-11 17:58:02 +00:00
|
|
|
data_list = ""
|
2017-11-01 19:16:59 +00:00
|
|
|
for k, v in data.items():
|
2018-01-11 17:58:02 +00:00
|
|
|
if isinstance(v, list):
|
|
|
|
for s in v:
|
2018-03-08 15:46:06 +00:00
|
|
|
try:
|
|
|
|
data_list += '&%s[]=%s' % (k, urllib.quote(s))
|
|
|
|
except AttributeError:
|
|
|
|
data_list += '&%s[]=%s' % (k, urllib.parse.quote(s))
|
2018-01-11 17:58:02 +00:00
|
|
|
elif v is not None:
|
2017-11-01 19:16:59 +00:00
|
|
|
data_encoded[k] = v
|
2018-03-08 15:46:06 +00:00
|
|
|
try:
|
|
|
|
data = urllib.urlencode(data_encoded) + data_list
|
|
|
|
except AttributeError:
|
|
|
|
data = urllib.parse.urlencode(data_encoded) + data_list
|
2017-11-01 19:16:59 +00:00
|
|
|
|
|
|
|
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'],
|
|
|
|
)
|
|
|
|
|
2018-09-18 22:01:53 +00:00
|
|
|
if info.get('status') == 200:
|
2017-11-01 19:16:59 +00:00
|
|
|
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:
|
2018-11-04 10:23:36 +00:00
|
|
|
return self.module.from_json(to_native(res)) or {}
|
2017-11-01 19:16:59 +00:00
|
|
|
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 {}
|
2018-08-13 14:59:27 +00:00
|
|
|
elif isinstance(r_list, list):
|
|
|
|
for r_data in r_list:
|
|
|
|
if str(r_data[key]) == str(value):
|
|
|
|
self.api_cache.update({
|
|
|
|
resource: r_data
|
|
|
|
})
|
|
|
|
return r_data
|
|
|
|
elif isinstance(r_list, dict):
|
|
|
|
for r_id, r_data in r_list.items():
|
|
|
|
if str(r_data[key]) == str(value):
|
|
|
|
self.api_cache.update({
|
|
|
|
resource: r_data
|
|
|
|
})
|
|
|
|
return r_data
|
2017-11-01 19:16:59 +00:00
|
|
|
|
2018-01-11 21:26:39 +00:00
|
|
|
self.module.fail_json(msg="Could not find %s with %s: %s" % (resource, key, value))
|
2017-11-01 19:16:59 +00:00
|
|
|
|
2018-08-23 21:14:26 +00:00
|
|
|
@staticmethod
|
|
|
|
def normalize_result(resource, schema, remove_missing_keys=True):
|
|
|
|
if remove_missing_keys:
|
|
|
|
fields_to_remove = set(resource.keys()) - set(schema.keys())
|
|
|
|
for field in fields_to_remove:
|
|
|
|
resource.pop(field)
|
2018-08-14 13:43:15 +00:00
|
|
|
|
2018-08-23 21:14:26 +00:00
|
|
|
for search_key, config in schema.items():
|
2018-07-17 14:24:21 +00:00
|
|
|
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
|
|
|
|
|
Vultr: Introducing vultr_server_facts module (#43001)
This commit introduces a new module called vultr_server_facts.
This module aims to return the list of servers avaiable avaiable in
Vultr.
Sample available here:
```
"vultr_server_facts": [
{
"allowed_bandwidth_gb": 1000,
"application": null,
"auto_backup_enabled": false,
"cost_per_month": 5.00,
"current_bandwidth_gb": 0,
"date_created": "2018-07-19 08:23:03",
"default_password": "p4ssw0rd!",
"disk": "Virtual 25 GB",
"firewallgroup": null,
"id": 17241096,
"internal_ip": "",
"kvm_url": "https://my.vultr.com/subs/vps/novnc/api.php?data=OFB...",
"name": "ansibletest",
"os": "CentOS 7 x64",
"pending_charges": 0.01,
"plan": "1024 MB RAM,25 GB SSD,1.00 TB BW",
"power_status": "running",
"ram": "1024 MB",
"region": "Amsterdam",
"server_state": "ok",
"status": "active",
"tag": "",
"v4_gateway": "105.178.158.1",
"v4_main_ip": "105.178.158.181",
"v4_netmask": "255.255.254.0",
"v6_main_ip": "",
"v6_network": "",
"v6_network_size": "",
"v6_networks": [],
"vcpu_count": 1
}
]
2018-08-16 18:29:48 +00:00
|
|
|
if 'transform' in config:
|
|
|
|
resource[search_key] = config['transform'](resource[search_key])
|
|
|
|
|
2018-07-17 14:24:21 +00:00
|
|
|
if 'key' in config:
|
|
|
|
resource[config['key']] = resource[search_key]
|
2018-07-18 07:53:58 +00:00
|
|
|
del resource[search_key]
|
2018-07-17 14:24:21 +00:00
|
|
|
|
|
|
|
return resource
|
|
|
|
|
2017-11-01 19:16:59 +00:00
|
|
|
def get_result(self, resource):
|
|
|
|
if resource:
|
2018-07-17 14:24:21 +00:00
|
|
|
if isinstance(resource, list):
|
2018-08-23 21:14:26 +00:00
|
|
|
self.result[self.namespace] = [Vultr.normalize_result(item, self.returns) for item in resource]
|
2018-07-17 14:24:21 +00:00
|
|
|
else:
|
2018-08-23 21:14:26 +00:00
|
|
|
self.result[self.namespace] = Vultr.normalize_result(resource, self.returns)
|
2018-07-17 14:24:21 +00:00
|
|
|
|
2017-11-01 19:16:59 +00:00
|
|
|
return self.result
|
Vultr: Introducing vultr_server_facts module (#43001)
This commit introduces a new module called vultr_server_facts.
This module aims to return the list of servers avaiable avaiable in
Vultr.
Sample available here:
```
"vultr_server_facts": [
{
"allowed_bandwidth_gb": 1000,
"application": null,
"auto_backup_enabled": false,
"cost_per_month": 5.00,
"current_bandwidth_gb": 0,
"date_created": "2018-07-19 08:23:03",
"default_password": "p4ssw0rd!",
"disk": "Virtual 25 GB",
"firewallgroup": null,
"id": 17241096,
"internal_ip": "",
"kvm_url": "https://my.vultr.com/subs/vps/novnc/api.php?data=OFB...",
"name": "ansibletest",
"os": "CentOS 7 x64",
"pending_charges": 0.01,
"plan": "1024 MB RAM,25 GB SSD,1.00 TB BW",
"power_status": "running",
"ram": "1024 MB",
"region": "Amsterdam",
"server_state": "ok",
"status": "active",
"tag": "",
"v4_gateway": "105.178.158.1",
"v4_main_ip": "105.178.158.181",
"v4_netmask": "255.255.254.0",
"v6_main_ip": "",
"v6_network": "",
"v6_network_size": "",
"v6_networks": [],
"vcpu_count": 1
}
]
2018-08-16 18:29:48 +00:00
|
|
|
|
|
|
|
def get_plan(self, plan=None, key='name'):
|
|
|
|
value = plan or self.module.params.get('plan')
|
|
|
|
|
|
|
|
return self.query_resource_by_key(
|
|
|
|
key=key,
|
|
|
|
value=value,
|
|
|
|
resource='plans',
|
|
|
|
use_cache=True
|
|
|
|
)
|
|
|
|
|
|
|
|
def get_firewallgroup(self, firewallgroup=None, key='description'):
|
|
|
|
value = firewallgroup or self.module.params.get('firewallgroup')
|
|
|
|
|
|
|
|
return self.query_resource_by_key(
|
|
|
|
key=key,
|
|
|
|
value=value,
|
|
|
|
resource='firewall',
|
|
|
|
query_by='group_list',
|
|
|
|
use_cache=True
|
|
|
|
)
|
|
|
|
|
|
|
|
def get_application(self, application=None, key='name'):
|
|
|
|
value = application or self.module.params.get('application')
|
|
|
|
|
|
|
|
return self.query_resource_by_key(
|
|
|
|
key=key,
|
|
|
|
value=value,
|
|
|
|
resource='app',
|
|
|
|
use_cache=True
|
|
|
|
)
|
2018-08-17 08:30:57 +00:00
|
|
|
|
|
|
|
def get_region(self, region=None, key='name'):
|
|
|
|
value = region or self.module.params.get('region')
|
|
|
|
|
|
|
|
return self.query_resource_by_key(
|
|
|
|
key=key,
|
|
|
|
value=value,
|
|
|
|
resource='regions',
|
|
|
|
use_cache=True
|
|
|
|
)
|