From 975f66f2eebe9dadba04f275774d4ab83f74cf25 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 13 Apr 2024 14:04:41 +0200 Subject: Adding upstream version 7.7.0+dfsg. Signed-off-by: Daniel Baumann --- .../vultr/plugins/doc_fragments/__init__.py | 0 .../ngine_io/vultr/plugins/doc_fragments/vultr.py | 58 ++ .../ngine_io/vultr/plugins/inventory/__init__.py | 0 .../ngine_io/vultr/plugins/inventory/vultr.py | 200 +++++ .../ngine_io/vultr/plugins/module_utils/vultr.py | 336 ++++++++ .../ngine_io/vultr/plugins/modules/__init__.py | 0 .../vultr/plugins/modules/vultr_account_info.py | 130 +++ .../vultr/plugins/modules/vultr_block_storage.py | 382 +++++++++ .../plugins/modules/vultr_block_storage_info.py | 160 ++++ .../vultr/plugins/modules/vultr_dns_domain.py | 201 +++++ .../vultr/plugins/modules/vultr_dns_domain_info.py | 117 +++ .../vultr/plugins/modules/vultr_dns_record.py | 376 +++++++++ .../vultr/plugins/modules/vultr_firewall_group.py | 201 +++++ .../plugins/modules/vultr_firewall_group_info.py | 139 +++ .../vultr/plugins/modules/vultr_firewall_rule.py | 384 +++++++++ .../vultr/plugins/modules/vultr_network.py | 232 +++++ .../vultr/plugins/modules/vultr_network_info.py | 156 ++++ .../vultr/plugins/modules/vultr_os_info.py | 137 +++ .../plugins/modules/vultr_plan_baremetal_info.py | 140 ++++ .../vultr/plugins/modules/vultr_plan_info.py | 139 +++ .../vultr/plugins/modules/vultr_region_info.py | 129 +++ .../ngine_io/vultr/plugins/modules/vultr_server.py | 933 +++++++++++++++++++++ .../plugins/modules/vultr_server_baremetal.py | 548 ++++++++++++ .../vultr/plugins/modules/vultr_server_info.py | 300 +++++++ .../vultr/plugins/modules/vultr_ssh_key.py | 236 ++++++ .../vultr/plugins/modules/vultr_ssh_key_info.py | 141 ++++ .../vultr/plugins/modules/vultr_startup_script.py | 265 ++++++ .../plugins/modules/vultr_startup_script_info.py | 149 ++++ .../ngine_io/vultr/plugins/modules/vultr_user.py | 326 +++++++ .../vultr/plugins/modules/vultr_user_info.py | 144 ++++ 30 files changed, 6659 insertions(+) create mode 100644 ansible_collections/ngine_io/vultr/plugins/doc_fragments/__init__.py create mode 100644 ansible_collections/ngine_io/vultr/plugins/doc_fragments/vultr.py create mode 100644 ansible_collections/ngine_io/vultr/plugins/inventory/__init__.py create mode 100644 ansible_collections/ngine_io/vultr/plugins/inventory/vultr.py create mode 100644 ansible_collections/ngine_io/vultr/plugins/module_utils/vultr.py create mode 100644 ansible_collections/ngine_io/vultr/plugins/modules/__init__.py create mode 100644 ansible_collections/ngine_io/vultr/plugins/modules/vultr_account_info.py create mode 100644 ansible_collections/ngine_io/vultr/plugins/modules/vultr_block_storage.py create mode 100644 ansible_collections/ngine_io/vultr/plugins/modules/vultr_block_storage_info.py create mode 100644 ansible_collections/ngine_io/vultr/plugins/modules/vultr_dns_domain.py create mode 100644 ansible_collections/ngine_io/vultr/plugins/modules/vultr_dns_domain_info.py create mode 100644 ansible_collections/ngine_io/vultr/plugins/modules/vultr_dns_record.py create mode 100644 ansible_collections/ngine_io/vultr/plugins/modules/vultr_firewall_group.py create mode 100644 ansible_collections/ngine_io/vultr/plugins/modules/vultr_firewall_group_info.py create mode 100644 ansible_collections/ngine_io/vultr/plugins/modules/vultr_firewall_rule.py create mode 100644 ansible_collections/ngine_io/vultr/plugins/modules/vultr_network.py create mode 100644 ansible_collections/ngine_io/vultr/plugins/modules/vultr_network_info.py create mode 100644 ansible_collections/ngine_io/vultr/plugins/modules/vultr_os_info.py create mode 100644 ansible_collections/ngine_io/vultr/plugins/modules/vultr_plan_baremetal_info.py create mode 100644 ansible_collections/ngine_io/vultr/plugins/modules/vultr_plan_info.py create mode 100644 ansible_collections/ngine_io/vultr/plugins/modules/vultr_region_info.py create mode 100644 ansible_collections/ngine_io/vultr/plugins/modules/vultr_server.py create mode 100644 ansible_collections/ngine_io/vultr/plugins/modules/vultr_server_baremetal.py create mode 100644 ansible_collections/ngine_io/vultr/plugins/modules/vultr_server_info.py create mode 100644 ansible_collections/ngine_io/vultr/plugins/modules/vultr_ssh_key.py create mode 100644 ansible_collections/ngine_io/vultr/plugins/modules/vultr_ssh_key_info.py create mode 100644 ansible_collections/ngine_io/vultr/plugins/modules/vultr_startup_script.py create mode 100644 ansible_collections/ngine_io/vultr/plugins/modules/vultr_startup_script_info.py create mode 100644 ansible_collections/ngine_io/vultr/plugins/modules/vultr_user.py create mode 100644 ansible_collections/ngine_io/vultr/plugins/modules/vultr_user_info.py (limited to 'ansible_collections/ngine_io/vultr/plugins') diff --git a/ansible_collections/ngine_io/vultr/plugins/doc_fragments/__init__.py b/ansible_collections/ngine_io/vultr/plugins/doc_fragments/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/ansible_collections/ngine_io/vultr/plugins/doc_fragments/vultr.py b/ansible_collections/ngine_io/vultr/plugins/doc_fragments/vultr.py new file mode 100644 index 000000000..cb5cfb64f --- /dev/null +++ b/ansible_collections/ngine_io/vultr/plugins/doc_fragments/vultr.py @@ -0,0 +1,58 @@ +# -*- 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) +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class ModuleDocFragment(object): + + # Standard documentation fragment + DOCUMENTATION = r''' +options: + api_key: + description: + - API key of the Vultr API. + - The ENV variable C(VULTR_API_KEY) is used as default, when defined. + type: str + api_timeout: + description: + - HTTP timeout to Vultr API. + - The ENV variable C(VULTR_API_TIMEOUT) is used as default, when defined. + - Fallback value is 60 seconds if not specified. + type: int + 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. + - Fallback value is 5 retries if not specified. + type: int + api_retry_max_delay: + description: + - Retry backoff delay in seconds is exponential up to this max. value, in seconds. + - The ENV variable C(VULTR_API_RETRY_MAX_DELAY) is used as default, when defined. + - Fallback value is 12 seconds. + type: int + 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. + type: str + default: default + api_endpoint: + description: + - URL to API endpint (without trailing slash). + - The ENV variable C(VULTR_API_ENDPOINT) is used as default, when defined. + - Fallback value is U(https://api.vultr.com) if not specified. + type: str + validate_certs: + description: + - Validate SSL certs of the Vultr API. + type: bool + default: yes +requirements: + - python >= 2.6 +notes: + - Also see the API documentation on https://www.vultr.com/api/. +''' diff --git a/ansible_collections/ngine_io/vultr/plugins/inventory/__init__.py b/ansible_collections/ngine_io/vultr/plugins/inventory/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/ansible_collections/ngine_io/vultr/plugins/inventory/vultr.py b/ansible_collections/ngine_io/vultr/plugins/inventory/vultr.py new file mode 100644 index 000000000..a44b47178 --- /dev/null +++ b/ansible_collections/ngine_io/vultr/plugins/inventory/vultr.py @@ -0,0 +1,200 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2018, Yanis Guenane +# Copyright (c) 2019, René Moser +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +DOCUMENTATION = r''' + name: vultr + author: + - Yanis Guenane (@Spredzy) + - René Moser (@resmo) + short_description: Vultr inventory source + extends_documentation_fragment: + - constructed + description: + - Get inventory hosts from Vultr public cloud. + - Uses an YAML configuration file ending with either I(vultr.yml) or I(vultr.yaml) to set parameter values (also see examples). + - Uses I(api_config), I(~/.vultr.ini), I(./vultr.ini) or C(VULTR_API_CONFIG) pointing to a Vultr credentials INI file + (see U(https://docs.ansible.com/ansible/latest/scenario_guides/guide_vultr.html)). + options: + plugin: + description: Token that ensures this is a source file for the 'vultr' plugin. + type: string + required: True + choices: [ vultr ] + api_account: + description: Specify the account to be used. + type: string + default: default + api_config: + description: Path to the vultr configuration file. If not specified will be taken from regular Vultr configuration. + type: path + env: + - name: VULTR_API_CONFIG + api_key: + description: Vultr API key. If not specified will be taken from regular Vultr configuration. + type: string + env: + - name: VULTR_API_KEY + hostname: + description: Field to match the hostname. Note v4_main_ip corresponds to the main_ip field returned from the API and name to label. + type: string + default: v4_main_ip + choices: + - v4_main_ip + - v6_main_ip + - name + filter_by_tag: + description: Only return servers filtered by this tag + type: string +''' + +EXAMPLES = r''' +# inventory_vultr.yml file in YAML format +# Example command line: ansible-inventory --list -i inventory_vultr.yml + +# Group by a region as lower case and with prefix e.g. "vultr_region_amsterdam" and by OS without prefix e.g. "CentOS_7_x64" +plugin: vultr +keyed_groups: + - prefix: vultr_region + key: region | lower + - separator: "" + key: os + +# Pass a tag filter to the API +plugin: vultr +filter_by_tag: Cache +''' + +import json + +from ansible.errors import AnsibleError +from ansible.plugins.inventory import BaseInventoryPlugin, Constructable +from ansible.module_utils.six.moves import configparser +from ansible.module_utils.urls import open_url +from ansible.module_utils._text import to_native +from ..module_utils.vultr import Vultr, VULTR_API_ENDPOINT, VULTR_USER_AGENT +from ansible.module_utils.six.moves.urllib.parse import quote + + +SCHEMA = { + 'SUBID': dict(key='id'), + 'label': dict(key='name'), + 'date_created': dict(), + 'allowed_bandwidth_gb': dict(convert_to='float'), + 'auto_backups': dict(key='auto_backup_enabled', convert_to='bool'), + 'current_bandwidth_gb': dict(), + 'kvm_url': dict(), + 'default_password': dict(), + 'internal_ip': dict(), + 'disk': dict(), + 'cost_per_month': dict(convert_to='float'), + 'location': dict(key='region'), + 'main_ip': dict(key='v4_main_ip'), + 'network_v4': dict(key='v4_network'), + 'gateway_v4': dict(key='v4_gateway'), + 'os': dict(), + 'pending_charges': dict(convert_to='float'), + 'power_status': dict(), + 'ram': dict(), + 'plan': dict(), + 'server_state': dict(), + 'status': dict(), + 'firewall_group': dict(), + 'tag': dict(), + 'v6_main_ip': dict(), + 'v6_network': dict(), + 'v6_network_size': dict(), + 'v6_networks': dict(), + 'vcpu_count': dict(convert_to='int'), +} + + +def _load_conf(path, account): + + if path: + conf = configparser.ConfigParser() + conf.read(path) + + if not conf._sections.get(account): + return None + + return dict(conf.items(account)) + else: + return Vultr.read_ini_config(account) + + +def _retrieve_servers(api_key, tag_filter=None): + api_url = '%s/v1/server/list' % VULTR_API_ENDPOINT + if tag_filter is not None: + api_url = api_url + '?tag=%s' % quote(tag_filter) + + try: + response = open_url( + api_url, headers={'API-Key': api_key, 'Content-type': 'application/json'}, + http_agent=VULTR_USER_AGENT, + ) + servers_list = json.loads(response.read()) + + return servers_list.values() if servers_list else [] + except ValueError: + raise AnsibleError("Incorrect JSON payload") + except Exception as e: + raise AnsibleError("Error while fetching %s: %s" % (api_url, to_native(e))) + + +class InventoryModule(BaseInventoryPlugin, Constructable): + + NAME = 'ngine_io.vultr.vultr' + + def verify_file(self, path): + valid = False + if super(InventoryModule, self).verify_file(path): + if path.endswith(('vultr.yaml', 'vultr.yml')): + valid = True + return valid + + def parse(self, inventory, loader, path, cache=True): + super(InventoryModule, self).parse(inventory, loader, path) + self._read_config_data(path=path) + + conf = _load_conf(self.get_option('api_config'), self.get_option('api_account')) + try: + api_key = self.get_option('api_key') or conf.get('key') + except Exception: + raise AnsibleError('Could not find an API key. Check inventory file and Vultr configuration files.') + + hostname_preference = self.get_option('hostname') + + # Add a top group 'vultr' + self.inventory.add_group(group='vultr') + + # Filter by tag is supported by the api with a query + filter_by_tag = self.get_option('filter_by_tag') + for server in _retrieve_servers(api_key, filter_by_tag): + + server = Vultr.normalize_result(server, SCHEMA) + + self.inventory.add_host(host=server['name'], group='vultr') + + for attribute, value in server.items(): + self.inventory.set_variable(server['name'], attribute, value) + + if hostname_preference != 'name': + self.inventory.set_variable(server['name'], 'ansible_host', server[hostname_preference]) + + # Use constructed if applicable + strict = self.get_option('strict') + + # Composed variables + self._set_composite_vars(self.get_option('compose'), server, server['name'], strict=strict) + + # Complex groups based on jinja2 conditionals, hosts that meet the conditional are added to group + self._add_host_to_composed_groups(self.get_option('groups'), server, server['name'], strict=strict) + + # Create groups based on variable values and add the corresponding hosts to it + self._add_host_to_keyed_groups(self.get_option('keyed_groups'), server, server['name'], strict=strict) diff --git a/ansible_collections/ngine_io/vultr/plugins/module_utils/vultr.py b/ansible_collections/ngine_io/vultr/plugins/module_utils/vultr.py new file mode 100644 index 000000000..81e7b62cb --- /dev/null +++ b/ansible_collections/ngine_io/vultr/plugins/module_utils/vultr.py @@ -0,0 +1,336 @@ +# -*- coding: utf-8 -*- +# Copyright (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 random +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" +VULTR_USER_AGENT = 'Ansible Vultr' + + +def vultr_argument_spec(): + return dict( + api_key=dict(type='str', 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_retry_max_delay=dict(type='int', default=os.environ.get('VULTR_API_RETRY_MAX_DELAY')), + api_account=dict(type='str', default=os.environ.get('VULTR_API_ACCOUNT') or 'default'), + api_endpoint=dict(type='str', default=os.environ.get('VULTR_API_ENDPOINT')), + validate_certs=dict(type='bool', default=True), + ) + + +class Vultr: + + def __init__(self, module, namespace): + + if module._name.startswith('vr_'): + module.deprecate( + "The Vultr modules were renamed. The prefix of the modules changed from vr_ to vultr_", + collection_name='ngine_io.vultr', + version='2.0.0') # Was Ansbile 2.11 + + 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: + config = self.read_env_variables() + config.update(Vultr.read_ini_config(self.module.params.get('api_account'))) + 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_retry_max_delay': self.module.params.get('api_retry_max_delay') or int(config.get('retry_max_delay') or 12), + '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))) + + if not self.api_config.get('api_key'): + self.module.fail_json(msg="No API key was specified. Please refer to the documentation.") + + # 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_retry_max_delay': self.api_config['api_retry_max_delay'], + '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': VULTR_USER_AGENT, + 'Accept': 'application/json', + } + + def read_env_variables(self): + keys = ['key', 'timeout', 'retries', 'retry_max_delay', 'endpoint'] + env_conf = {} + for key in keys: + if 'VULTR_API_%s' % key.upper() not in os.environ: + continue + env_conf[key] = os.environ['VULTR_API_%s' % key.upper()] + + return env_conf + + @staticmethod + def read_ini_config(ini_group): + 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) + + if not conf._sections.get(ini_group): + return dict() + + 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 r_value in ['yes', 'no']: + if param and r_value != 'yes': + return "enable" + elif not param and r_value != 'no': + return "disable" + else: + if param and not r_value: + return "enable" + elif not param and r_value: + 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: + try: + data_list += '&%s[]=%s' % (k, urllib.quote(s)) + except AttributeError: + data_list += '&%s[]=%s' % (k, urllib.parse.quote(s)) + elif v is not None: + data_encoded[k] = v + try: + data = urllib.urlencode(data_encoded) + data_list + except AttributeError: + data = urllib.parse.urlencode(data_encoded) + data_list + + retry_max_delay = self.api_config['api_retry_max_delay'] + randomness = random.randint(0, 1000) / 1000.0 + + for retry 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'], + ) + + if info.get('status') == 200: + break + + # Vultr has a rate limiting requests per second, try to be polite + # Use exponential backoff plus a little bit of randomness + delay = 2 ** retry + randomness + if delay > retry_max_delay: + delay = retry_max_delay + randomness + time.sleep(delay) + + 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_native(res)) or {} + 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, id_key=None, optional=False): + if not value: + return {} + + r_list = None + if use_cache: + r_list = self.api_cache.get(resource) + + if not r_list: + r_list = self.api_query(path="/v1/%s/%s" % (resource, query_by), data=params) + if use_cache: + self.api_cache.update({ + resource: r_list + }) + + if not r_list: + return {} + + elif isinstance(r_list, list): + for r_data in r_list: + if str(r_data[key]) == str(value): + return r_data + if id_key is not None and to_text(r_data[id_key]) == to_text(value): + return r_data + elif isinstance(r_list, dict): + for r_id, r_data in r_list.items(): + if str(r_data[key]) == str(value): + return r_data + if id_key is not None and to_text(r_data[id_key]) == to_text(value): + return r_data + if not optional: + if id_key: + msg = "Could not find %s with ID or %s: %s" % (resource, key, value) + else: + msg = "Could not find %s with %s: %s" % (resource, key, value) + self.module.fail_json(msg=msg) + return {} + + @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) + + for search_key, config in schema.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 'transform' in config: + resource[search_key] = config['transform'](resource[search_key]) + + if 'key' in config: + resource[config['key']] = resource[search_key] + del resource[search_key] + + return resource + + def get_result(self, resource): + if resource: + if isinstance(resource, list): + self.result[self.namespace] = [Vultr.normalize_result(item, self.returns) for item in resource] + else: + self.result[self.namespace] = Vultr.normalize_result(resource, self.returns) + + return self.result + + def get_plan(self, plan=None, key='name', optional=False): + value = plan or self.module.params.get('plan') + + return self.query_resource_by_key( + key=key, + value=value, + resource='plans', + use_cache=True, + id_key='VPSPLANID', + optional=optional, + ) + + 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 + ) + + 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 + ) diff --git a/ansible_collections/ngine_io/vultr/plugins/modules/__init__.py b/ansible_collections/ngine_io/vultr/plugins/modules/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/ansible_collections/ngine_io/vultr/plugins/modules/vultr_account_info.py b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_account_info.py new file mode 100644 index 000000000..1718ff664 --- /dev/null +++ b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_account_info.py @@ -0,0 +1,130 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2019, René Moser +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: vultr_account_info +short_description: Get information about the Vultr account. +description: + - Get infos about account balance, charges and payments. +version_added: "0.1.0" +author: "René Moser (@resmo)" +extends_documentation_fragment: +- ngine_io.vultr.vultr + +''' + +EXAMPLES = r''' +- name: Get Vultr account infos + ngine_io.vultr.vultr_account_info: + register: result + +- name: Print the infos + debug: + var: result.vultr_account_info +''' + +RETURN = r''' +--- +vultr_api: + description: Response from Vultr API with a few additions/modification + returned: success + type: complex + contains: + api_account: + description: Account used in the ini file to select the key + returned: success + type: str + sample: default + api_timeout: + description: Timeout used for the API requests + returned: success + type: int + sample: 60 + api_retries: + description: Amount of max retries for the API requests + returned: success + type: int + sample: 5 + api_retry_max_delay: + description: Exponential backoff delay in seconds between retries up to this max delay value. + returned: success + type: int + sample: 12 + api_endpoint: + description: Endpoint used for the API requests + returned: success + type: str + sample: "https://api.vultr.com" +vultr_account_info: + description: Response from Vultr API + returned: success + type: complex + contains: + balance: + description: Your account balance. + returned: success + type: float + sample: -214.69 + pending_charges: + description: Charges pending. + returned: success + type: float + sample: 57.03 + last_payment_date: + description: Date of the last payment. + returned: success + type: str + sample: "2017-08-26 12:47:48" + last_payment_amount: + description: The amount of the last payment transaction. + returned: success + type: float + sample: -250.0 +''' + +from ansible.module_utils.basic import AnsibleModule +from ..module_utils.vultr import ( + Vultr, + vultr_argument_spec, +) + + +class AnsibleVultrAccountInfo(Vultr): + + def __init__(self, module): + super(AnsibleVultrAccountInfo, self).__init__(module, "vultr_account_info") + + self.returns = { + 'balance': dict(convert_to='float'), + 'pending_charges': dict(convert_to='float'), + 'last_payment_date': dict(), + 'last_payment_amount': dict(convert_to='float'), + } + + def get_account_info(self): + return self.api_query(path="/v1/account/info") + + +def main(): + argument_spec = vultr_argument_spec() + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + account_info = AnsibleVultrAccountInfo(module) + result = account_info.get_result(account_info.get_account_info()) + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/ngine_io/vultr/plugins/modules/vultr_block_storage.py b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_block_storage.py new file mode 100644 index 000000000..5cda1cd1a --- /dev/null +++ b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_block_storage.py @@ -0,0 +1,382 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2018, Yanis Guenane +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: vultr_block_storage +short_description: Manages block storage volumes on Vultr. +description: + - Manage block storage volumes on Vultr. +author: "Yanis Guenane (@Spredzy)" +version_added: "0.1.0" +options: + name: + description: + - Name of the block storage volume. + required: true + aliases: [ description, label ] + type: str + size: + description: + - Size of the block storage volume in GB. + - Required if I(state) is present. + - If it's larger than the volume's current size, the volume will be resized. + type: int + region: + description: + - Region the block storage volume is deployed into. + - Required if I(state) is present. + type: str + state: + description: + - State of the block storage volume. + default: present + choices: [ present, absent, attached, detached ] + type: str + attached_to_SUBID: + description: + - The ID of the server the volume is attached to. + - Required if I(state) is attached. + aliases: [ attached_to_id ] + type: int + live_attachment: + description: + - Whether the volume should be attached/detached, even if the server not stopped. + type: bool + default: True +extends_documentation_fragment: +- ngine_io.vultr.vultr + +''' + +EXAMPLES = ''' +- name: Ensure a block storage volume is present + ngine_io.vultr.vultr_block_storage: + name: myvolume + size: 10 + region: Amsterdam + +- name: Ensure a block storage volume is absent + ngine_io.vultr.vultr_block_storage: + name: myvolume + state: absent + +- name: Ensure a block storage volume exists and is attached to server 114 + ngine_io.vultr.vultr_block_storage: + name: myvolume + state: attached + attached_to_id: 114 + size: 10 + +- name: Ensure a block storage volume exists and is not attached to any server + ngine_io.vultr.vultr_block_storage: + name: myvolume + state: detached + size: 10 +''' + +RETURN = ''' +--- +vultr_api: + description: Response from Vultr API with a few additions/modification + returned: success + type: complex + contains: + api_account: + description: Account used in the ini file to select the key + returned: success + type: str + sample: default + api_timeout: + description: Timeout used for the API requests + returned: success + type: int + sample: 60 + api_retries: + description: Amount of max retries for the API requests + returned: success + type: int + sample: 5 + api_retry_max_delay: + description: Exponential backoff delay in seconds between retries up to this max delay value. + returned: success + type: int + sample: 12 + api_endpoint: + description: Endpoint used for the API requests + returned: success + type: str + sample: "https://api.vultr.com" +vultr_block_storage: + description: Response from Vultr API + returned: success + type: complex + contains: + attached_to_id: + description: The ID of the server the volume is attached to + returned: success + type: str + sample: "10194376" + cost_per_month: + description: Cost per month for the volume + returned: success + type: float + sample: 1.00 + date_created: + description: Date when the volume was created + returned: success + type: str + sample: "2017-08-26 12:47:48" + id: + description: ID of the block storage volume + returned: success + type: str + sample: "1234abcd" + name: + description: Name of the volume + returned: success + type: str + sample: "ansible-test-volume" + region: + description: Region the volume was deployed into + returned: success + type: str + sample: "New Jersey" + size: + description: Information about the volume size in GB + returned: success + type: int + sample: 10 + status: + description: Status about the deployment of the volume + returned: success + type: str + sample: "active" + +''' +from ansible.module_utils.basic import AnsibleModule +from ..module_utils.vultr import ( + Vultr, + vultr_argument_spec, +) + + +class AnsibleVultrBlockStorage(Vultr): + + def __init__(self, module): + super(AnsibleVultrBlockStorage, self).__init__(module, "vultr_block_storage") + + self.returns = { + 'SUBID': dict(key='id'), + 'label': dict(key='name'), + 'DCID': dict(key='region', transform=self._get_region_name), + 'attached_to_SUBID': dict(key='attached_to_id'), + 'cost_per_month': dict(convert_to='float'), + 'date_created': dict(), + 'size_gb': dict(key='size', convert_to='int'), + 'status': dict() + } + + def _get_region_name(self, region): + return self.get_region(region, 'DCID').get('name') + + def get_block_storage_volumes(self): + volumes = self.api_query(path="/v1/block/list") + if volumes: + for volume in volumes: + if volume.get('label') == self.module.params.get('name'): + return volume + return {} + + def present_block_storage_volume(self): + volume = self.get_block_storage_volumes() + if not volume: + volume = self._create_block_storage_volume(volume) + return volume + + def _create_block_storage_volume(self, volume): + self.result['changed'] = True + data = { + 'label': self.module.params.get('name'), + 'DCID': self.get_region().get('DCID'), + 'size_gb': self.module.params.get('size') + } + self.result['diff']['before'] = {} + self.result['diff']['after'] = data + + if not self.module.check_mode: + self.api_query( + path="/v1/block/create", + method="POST", + data=data + ) + volume = self.get_block_storage_volumes() + return volume + + def absent_block_storage_volume(self): + volume = self.get_block_storage_volumes() + if volume: + self.result['changed'] = True + + data = { + 'SUBID': volume['SUBID'], + } + + self.result['diff']['before'] = volume + self.result['diff']['after'] = {} + + if not self.module.check_mode: + self.api_query( + path="/v1/block/delete", + method="POST", + data=data + ) + return volume + + def detached_block_storage_volume(self): + volume = self.present_block_storage_volume() + if volume.get('attached_to_SUBID') is None: + return volume + + self.result['changed'] = True + + if not self.module.check_mode: + data = { + 'SUBID': volume['SUBID'], + 'live': self.get_yes_or_no('live_attachment') + } + self.api_query( + path='/v1/block/detach', + method='POST', + data=data + ) + + volume = self.get_block_storage_volumes() + else: + volume['attached_to_SUBID'] = None + + self.result['diff']['after'] = volume + + return volume + + def attached_block_storage_volume(self): + expected_server = self.module.params.get('attached_to_SUBID') + volume = self.present_block_storage_volume() + server = volume.get('attached_to_SUBID') + if server == expected_server: + return volume + + if server is not None: + self.module.fail_json( + msg='Volume already attached to server %s' % server + ) + + self.result['changed'] = True + + if not self.module.check_mode: + data = { + 'SUBID': volume['SUBID'], + # This API call expects a param called attach_to_SUBID, + # but all the BlockStorage API response payloads call + # this parameter attached_to_SUBID. So we'll standardize + # to the latter and attached_to_id, but we'll pass the + # expected attach_to_SUBID to this API call. + 'attach_to_SUBID': expected_server, + 'live': self.get_yes_or_no('live_attachment'), + } + self.api_query( + path='/v1/block/attach', + method='POST', + data=data + ) + volume = self.get_block_storage_volumes() + else: + volume['attached_to_SUBID'] = expected_server + + self.result['diff']['after'] = volume + + return volume + + def ensure_volume_size(self, volume, expected_size): + curr_size = volume.get('size_gb') + # When creating, attaching, or detaching a volume in check_mode, + # sadly, size_gb doesn't exist, because those methods return the + # result of get_block_storage_volumes, which is {} on check_mode. + if curr_size is None or curr_size >= expected_size: + # we only resize volumes that are smaller than + # expected. There's no shrinking operation. + return volume + + self.result['changed'] = True + + volume['size_gb'] = expected_size + self.result['diff']['after'] = volume + + if not self.module.check_mode: + data = {'SUBID': volume['SUBID'], 'size_gb': expected_size} + self.api_query( + path='/v1/block/resize', + method='POST', + data=data, + ) + + return volume + + +def main(): + argument_spec = vultr_argument_spec() + argument_spec.update(dict( + name=dict(type='str', required=True, aliases=['description', 'label']), + size=dict(type='int'), + region=dict(type='str'), + state=dict( + type='str', + choices=['present', 'absent', 'attached', 'detached'], + default='present' + ), + attached_to_SUBID=dict(type='int', aliases=['attached_to_id']), + live_attachment=dict(type='bool', default=True) + )) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[ + ['state', 'present', ['size', 'region']], + ['state', 'detached', ['size', 'region']], + ['state', 'attached', ['size', 'region', 'attached_to_SUBID']], + ] + ) + + vultr_block_storage = AnsibleVultrBlockStorage(module) + + desired_state = module.params.get('state') + if desired_state == "absent": + volume = vultr_block_storage.absent_block_storage_volume() + elif desired_state == 'attached': + volume = vultr_block_storage.attached_block_storage_volume() + elif desired_state == 'detached': + volume = vultr_block_storage.detached_block_storage_volume() + else: + volume = vultr_block_storage.present_block_storage_volume() + + expected_size = module.params.get('size') + if expected_size and desired_state != 'absent': + volume = vultr_block_storage.ensure_volume_size( + volume, + expected_size + ) + + result = vultr_block_storage.get_result(volume) + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/ngine_io/vultr/plugins/modules/vultr_block_storage_info.py b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_block_storage_info.py new file mode 100644 index 000000000..46bdbecb3 --- /dev/null +++ b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_block_storage_info.py @@ -0,0 +1,160 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2018, Yanis Guenane +# Copyright (c) 2019, René Moser +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: vultr_block_storage_info +short_description: Get information about the Vultr block storage volumes available. +description: + - Get infos about block storage volumes available in Vultr. +version_added: "0.1.0" +author: + - "Yanis Guenane (@Spredzy)" + - "René Moser (@resmo)" +extends_documentation_fragment: +- ngine_io.vultr.vultr + +''' + +EXAMPLES = r''' +- name: Get Vultr block storage infos + ngine_io.vultr.vultr_block_storage_info: + register: result + +- name: Print the infos + debug: + var: result.vultr_block_storage_info +''' + +RETURN = r''' +--- +vultr_api: + description: Response from Vultr API with a few additions/modification + returned: success + type: complex + contains: + api_account: + description: Account used in the ini file to select the key + returned: success + type: str + sample: default + api_timeout: + description: Timeout used for the API requests + returned: success + type: int + sample: 60 + api_retries: + description: Amount of max retries for the API requests + returned: success + type: int + sample: 5 + api_retry_max_delay: + description: Exponential backoff delay in seconds between retries up to this max delay value. + returned: success + type: int + sample: 12 + api_endpoint: + description: Endpoint used for the API requests + returned: success + type: str + sample: "https://api.vultr.com" +vultr_block_storage_info: + description: Response from Vultr API as list + returned: success + type: complex + contains: + id: + description: ID of the block storage. + returned: success + type: int + sample: 17332323 + size: + description: Size in GB of the block storage. + returned: success + type: int + sample: 10 + region: + description: Region the block storage is located in. + returned: success + type: str + sample: New Jersey + name: + description: Name of the block storage. + returned: success + type: str + sample: my volume + cost_per_month: + description: Cost per month of the block storage. + returned: success + type: float + sample: 1.0 + date_created: + description: Date created of the block storage. + returned: success + type: str + sample: "2018-07-24 12:59:59" + status: + description: Status of the block storage. + returned: success + type: str + sample: active + attached_to_id: + description: Block storage is attached to this server ID. + returned: success + type: str + sample: null +''' + +from ansible.module_utils.basic import AnsibleModule +from ..module_utils.vultr import ( + Vultr, + vultr_argument_spec, +) + + +class AnsibleVultrBlockStorageFacts(Vultr): + + def __init__(self, module): + super(AnsibleVultrBlockStorageFacts, self).__init__(module, "vultr_block_storage_info") + + self.returns = { + 'attached_to_SUBID': dict(key='attached_to_id'), + 'cost_per_month': dict(convert_to='float'), + 'date_created': dict(), + 'SUBID': dict(key='id'), + 'label': dict(key='name'), + 'DCID': dict(key='region', transform=self._get_region_name), + 'size_gb': dict(key='size', convert_to='int'), + 'status': dict() + } + + def _get_region_name(self, region): + return self.get_region(region, 'DCID').get('name') + + def get_block_storage_volumes(self): + return self.api_query(path="/v1/block/list") + + +def main(): + argument_spec = vultr_argument_spec() + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + volume_info = AnsibleVultrBlockStorageFacts(module) + result = volume_info.get_result(volume_info.get_block_storage_volumes()) + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/ngine_io/vultr/plugins/modules/vultr_dns_domain.py b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_dns_domain.py new file mode 100644 index 000000000..bb83d3733 --- /dev/null +++ b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_dns_domain.py @@ -0,0 +1,201 @@ +#!/usr/bin/python +# -*- 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) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: vultr_dns_domain +short_description: Manages DNS domains on Vultr. +description: + - Create and remove DNS domains. +version_added: "0.1.0" +author: "René Moser (@resmo)" +options: + name: + description: + - The domain name. + required: true + aliases: [ domain ] + type: str + server_ip: + description: + - The default server IP. + - Use M(ngine_io.vultr.vultr_dns_record) to change it once the domain is created. + - Required if C(state=present). + type: str + state: + description: + - State of the DNS domain. + default: present + choices: [ present, absent ] + type: str +extends_documentation_fragment: +- ngine_io.vultr.vultr + +''' + +EXAMPLES = r''' +- name: Ensure a domain exists + ngine_io.vultr.vultr_dns_domain: + name: example.com + server_ip: 10.10.10.10 + +- name: Ensure a domain is absent + ngine_io.vultr.vultr_dns_domain: + name: example.com + state: absent +''' + +RETURN = r''' +--- +vultr_api: + description: Response from Vultr API with a few additions/modification + returned: success + type: complex + contains: + api_account: + description: Account used in the ini file to select the key + returned: success + type: str + sample: default + api_timeout: + description: Timeout used for the API requests + returned: success + type: int + sample: 60 + api_retries: + description: Amount of max retries for the API requests + returned: success + type: int + sample: 5 + api_retry_max_delay: + description: Exponential backoff delay in seconds between retries up to this max delay value. + returned: success + type: int + sample: 12 + api_endpoint: + description: Endpoint used for the API requests + returned: success + type: str + sample: "https://api.vultr.com" +vultr_dns_domain: + description: Response from Vultr API + returned: success + type: complex + contains: + name: + description: Name of the DNS Domain. + returned: success + type: str + sample: example.com + date_created: + description: Date the DNS domain was created. + returned: success + type: str + sample: "2017-08-26 12:47:48" +''' + +from ansible.module_utils.basic import AnsibleModule + +from ..module_utils.vultr import Vultr, vultr_argument_spec + + +class AnsibleVultrDnsDomain(Vultr): + + def __init__(self, module): + super(AnsibleVultrDnsDomain, self).__init__(module, "vultr_dns_domain") + + self.returns = { + 'domain': dict(key='name'), + 'date_created': dict(), + } + + def get_domain(self): + domains = self.api_query(path="/v1/dns/list") + name = self.module.params.get('name').lower() + if domains: + for domain in domains: + if domain.get('domain').lower() == name: + return domain + return {} + + def present_domain(self): + domain = self.get_domain() + if not domain: + domain = self._create_domain(domain) + return domain + + def _create_domain(self, domain): + self.result['changed'] = True + data = { + 'domain': self.module.params.get('name'), + 'serverip': self.module.params.get('server_ip'), + } + self.result['diff']['before'] = {} + self.result['diff']['after'] = data + + if not self.module.check_mode: + self.api_query( + path="/v1/dns/create_domain", + method="POST", + data=data + ) + domain = self.get_domain() + return domain + + def absent_domain(self): + domain = self.get_domain() + if domain: + self.result['changed'] = True + + data = { + 'domain': domain['domain'], + } + + self.result['diff']['before'] = domain + self.result['diff']['after'] = {} + + if not self.module.check_mode: + self.api_query( + path="/v1/dns/delete_domain", + method="POST", + data=data + ) + return domain + + +def main(): + argument_spec = vultr_argument_spec() + argument_spec.update(dict( + name=dict(type='str', required=True, aliases=['domain']), + server_ip=dict(type='str',), + state=dict(type='str', choices=['present', 'absent'], default='present'), + )) + + module = AnsibleModule( + argument_spec=argument_spec, + required_if=[ + ('state', 'present', ['server_ip']), + ], + supports_check_mode=True, + ) + + vultr_domain = AnsibleVultrDnsDomain(module) + if module.params.get('state') == "absent": + domain = vultr_domain.absent_domain() + else: + domain = vultr_domain.present_domain() + + result = vultr_domain.get_result(domain) + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/ngine_io/vultr/plugins/modules/vultr_dns_domain_info.py b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_dns_domain_info.py new file mode 100644 index 000000000..35a47d701 --- /dev/null +++ b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_dns_domain_info.py @@ -0,0 +1,117 @@ +#!/usr/bin/python +# +# Copyright (c) 2018, Yanis Guenane +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: vultr_dns_domain_info +short_description: Gather information about the Vultr DNS domains available. +description: + - Gather information about DNS domains available in Vultr. +version_added: "0.1.0" +author: "Yanis Guenane (@Spredzy)" +extends_documentation_fragment: +- ngine_io.vultr.vultr + +''' + +EXAMPLES = r''' +- name: Gather Vultr DNS domains information + ngine_io.vultr.vultr_dns_domains_info: + register: result + +- name: Print the gathered information + debug: + var: result.vultr_dns_domain_info +''' + +RETURN = r''' +--- +vultr_api: + description: Response from Vultr API with a few additions/modification + returned: success + type: complex + contains: + api_account: + description: Account used in the ini file to select the key + returned: success + type: str + sample: default + api_timeout: + description: Timeout used for the API requests + returned: success + type: int + sample: 60 + api_retries: + description: Amount of max retries for the API requests + returned: success + type: int + sample: 5 + api_retry_max_delay: + description: Exponential backoff delay in seconds between retries up to this max delay value. + returned: success + type: int + sample: 12 + api_endpoint: + description: Endpoint used for the API requests + returned: success + type: str + sample: "https://api.vultr.com" +vultr_dns_domain_info: + description: Response from Vultr API + returned: success + type: complex + contains: + domain: + description: Name of the DNS Domain. + returned: success + type: str + sample: example.com + date_created: + description: Date the DNS domain was created. + returned: success + type: str + sample: "2017-08-26 12:47:48" +''' + +from ansible.module_utils.basic import AnsibleModule +from ..module_utils.vultr import ( + Vultr, + vultr_argument_spec, +) + + +class AnsibleVultrDnsDomainInfo(Vultr): + + def __init__(self, module): + super(AnsibleVultrDnsDomainInfo, self).__init__(module, "vultr_dns_domain_info") + + self.returns = { + "date_created": dict(), + "domain": dict(), + } + + def get_domains(self): + return self.api_query(path="/v1/dns/list") + + +def main(): + argument_spec = vultr_argument_spec() + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + domain_info = AnsibleVultrDnsDomainInfo(module) + result = domain_info.get_result(domain_info.get_domains()) + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/ngine_io/vultr/plugins/modules/vultr_dns_record.py b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_dns_record.py new file mode 100644 index 000000000..bab11c4c0 --- /dev/null +++ b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_dns_record.py @@ -0,0 +1,376 @@ +#!/usr/bin/python +# -*- 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) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: vultr_dns_record +short_description: Manages DNS records on Vultr. +description: + - Create, update and remove DNS records. +version_added: "0.1.0" +author: "René Moser (@resmo)" +options: + name: + description: + - The record name (subrecord). + default: "" + aliases: [ subrecord ] + type: str + domain: + description: + - The domain the record is related to. + type: str + required: true + record_type: + description: + - Type of the record. + default: A + choices: + - A + - AAAA + - CNAME + - MX + - SRV + - CAA + - TXT + - NS + - SSHFP + aliases: [ type ] + type: str + data: + description: + - Data of the record. + - Required if C(state=present) or C(multiple=yes). + type: str + ttl: + description: + - TTL of the record. + default: 300 + type: int + multiple: + description: + - Whether to use more than one record with similar C(name) including no name and C(record_type). + - Only allowed for a few record types, e.g. C(record_type=A), C(record_type=NS) or C(record_type=MX). + - C(data) will not be updated, instead it is used as a key to find existing records. + default: no + type: bool + priority: + description: + - Priority of the record. + default: 0 + type: int + state: + description: + - State of the DNS record. + default: present + choices: [ present, absent ] + type: str +extends_documentation_fragment: +- ngine_io.vultr.vultr + +''' + +EXAMPLES = ''' +- name: Ensure an A record exists + ngine_io.vultr.vultr_dns_record: + name: www + domain: example.com + data: 10.10.10.10 + ttl: 3600 + +- name: Ensure a second A record exists for round robin LB + ngine_io.vultr.vultr_dns_record: + name: www + domain: example.com + data: 10.10.10.11 + ttl: 60 + multiple: yes + +- name: Ensure a CNAME record exists + ngine_io.vultr.vultr_dns_record: + name: web + record_type: CNAME + domain: example.com + data: www.example.com + +- name: Ensure MX record exists + ngine_io.vultr.vultr_dns_record: + record_type: MX + domain: example.com + data: "{{ item.data }}" + priority: "{{ item.priority }}" + multiple: yes + with_items: + - { data: mx1.example.com, priority: 10 } + - { data: mx2.example.com, priority: 10 } + - { data: mx3.example.com, priority: 20 } + +- name: Ensure a record is absent + ngine_io.vultr.vultr_dns_record: + name: www + domain: example.com + state: absent + +- name: Ensure MX record is absent in case multiple exists + ngine_io.vultr.vultr_dns_record: + record_type: MX + domain: example.com + data: mx1.example.com + multiple: yes + state: absent +''' + +RETURN = ''' +--- +vultr_api: + description: Response from Vultr API with a few additions/modification + returned: success + type: complex + contains: + api_account: + description: Account used in the ini file to select the key + returned: success + type: str + sample: default + api_timeout: + description: Timeout used for the API requests + returned: success + type: int + sample: 60 + api_retries: + description: Amount of max retries for the API requests + returned: success + type: int + sample: 5 + api_retry_max_delay: + description: Exponential backoff delay in seconds between retries up to this max delay value. + returned: success + type: int + sample: 12 + api_endpoint: + description: Endpoint used for the API requests + returned: success + type: str + sample: "https://api.vultr.com" +vultr_dns_record: + description: Response from Vultr API + returned: success + type: complex + contains: + id: + description: The ID of the DNS record. + returned: success + type: int + sample: 1265277 + name: + description: The name of the DNS record. + returned: success + type: str + sample: web + record_type: + description: The name of the DNS record. + returned: success + type: str + sample: web + data: + description: Data of the DNS record. + returned: success + type: str + sample: 10.10.10.10 + domain: + description: Domain the DNS record is related to. + returned: success + type: str + sample: example.com + priority: + description: Priority of the DNS record. + returned: success + type: int + sample: 10 + ttl: + description: Time to live of the DNS record. + returned: success + type: int + sample: 300 +''' + +from ansible.module_utils.basic import AnsibleModule +from ..module_utils.vultr import ( + Vultr, + vultr_argument_spec, +) + +RECORD_TYPES = [ + 'A', + 'AAAA', + 'CNAME', + 'MX', + 'TXT', + 'NS', + 'SRV', + 'CAA', + 'SSHFP' +] + + +class AnsibleVultrDnsRecord(Vultr): + + def __init__(self, module): + super(AnsibleVultrDnsRecord, self).__init__(module, "vultr_dns_record") + + self.returns = { + 'RECORDID': dict(key='id'), + 'name': dict(), + 'record': dict(), + 'priority': dict(), + 'data': dict(), + 'type': dict(key='record_type'), + 'ttl': dict(), + } + + def get_record(self): + records = self.api_query(path="/v1/dns/records?domain=%s" % self.module.params.get('domain')) + + multiple = self.module.params.get('multiple') + data = self.module.params.get('data') + name = self.module.params.get('name') + record_type = self.module.params.get('record_type') + + result = {} + for record in records or []: + if record.get('type') != record_type: + continue + + if record.get('name') == name: + if not multiple: + if result: + self.module.fail_json(msg="More than one record with record_type=%s and name=%s params. " + "Use multiple=yes for more than one record." % (record_type, name)) + else: + result = record + elif record.get('data') == data: + return record + + return result + + def present_record(self): + record = self.get_record() + if not record: + record = self._create_record(record) + else: + record = self._update_record(record) + return record + + def _create_record(self, record): + self.result['changed'] = True + data = { + 'name': self.module.params.get('name'), + 'domain': self.module.params.get('domain'), + 'data': self.module.params.get('data'), + 'type': self.module.params.get('record_type'), + 'priority': self.module.params.get('priority'), + 'ttl': self.module.params.get('ttl'), + } + self.result['diff']['before'] = {} + self.result['diff']['after'] = data + + if not self.module.check_mode: + self.api_query( + path="/v1/dns/create_record", + method="POST", + data=data + ) + record = self.get_record() + return record + + def _update_record(self, record): + data = { + 'RECORDID': record['RECORDID'], + 'name': self.module.params.get('name'), + 'domain': self.module.params.get('domain'), + 'data': self.module.params.get('data'), + 'type': self.module.params.get('record_type'), + 'priority': self.module.params.get('priority'), + 'ttl': self.module.params.get('ttl'), + } + has_changed = [k for k in data if k in record and data[k] != record[k]] + if has_changed: + self.result['changed'] = True + + self.result['diff']['before'] = record + self.result['diff']['after'] = record.copy() + self.result['diff']['after'].update(data) + + if not self.module.check_mode: + self.api_query( + path="/v1/dns/update_record", + method="POST", + data=data + ) + record = self.get_record() + return record + + def absent_record(self): + record = self.get_record() + if record: + self.result['changed'] = True + + data = { + 'RECORDID': record['RECORDID'], + 'domain': self.module.params.get('domain'), + } + + self.result['diff']['before'] = record + self.result['diff']['after'] = {} + + if not self.module.check_mode: + self.api_query( + path="/v1/dns/delete_record", + method="POST", + data=data + ) + return record + + +def main(): + argument_spec = vultr_argument_spec() + argument_spec.update(dict( + domain=dict(type='str', required=True), + name=dict(type='str', default="", aliases=['subrecord']), + state=dict(type='str', choices=['present', 'absent'], default='present'), + ttl=dict(type='int', default=300), + record_type=dict(type='str', choices=RECORD_TYPES, default='A', aliases=['type']), + multiple=dict(type='bool', default=False), + priority=dict(type='int', default=0), + data=dict(type='str',) + )) + + module = AnsibleModule( + argument_spec=argument_spec, + required_if=[ + ('state', 'present', ['data']), + ('multiple', True, ['data']), + ], + + supports_check_mode=True, + ) + + vultr_record = AnsibleVultrDnsRecord(module) + if module.params.get('state') == "absent": + record = vultr_record.absent_record() + else: + record = vultr_record.present_record() + + result = vultr_record.get_result(record) + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/ngine_io/vultr/plugins/modules/vultr_firewall_group.py b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_firewall_group.py new file mode 100644 index 000000000..36ef3b435 --- /dev/null +++ b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_firewall_group.py @@ -0,0 +1,201 @@ +#!/usr/bin/python +# -*- 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) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: vultr_firewall_group +short_description: Manages firewall groups on Vultr. +description: + - Create and remove firewall groups. +version_added: "0.1.0" +author: "René Moser (@resmo)" +options: + name: + description: + - Name of the firewall group. + required: true + aliases: [ description ] + type: str + state: + description: + - State of the firewall group. + default: present + choices: [ present, absent ] + type: str +extends_documentation_fragment: +- ngine_io.vultr.vultr + +''' + +EXAMPLES = ''' +- name: ensure a firewall group is present + ngine_io.vultr.vultr_firewall_group: + name: my http firewall + +- name: ensure a firewall group is absent + ngine_io.vultr.vultr_firewall_group: + name: my http firewall + state: absent +''' + +RETURN = ''' +--- +vultr_api: + description: Response from Vultr API with a few additions/modification + returned: success + type: complex + contains: + api_account: + description: Account used in the ini file to select the key + returned: success + type: str + sample: default + api_timeout: + description: Timeout used for the API requests + returned: success + type: int + sample: 60 + api_retries: + description: Amount of max retries for the API requests + returned: success + type: int + sample: 5 + api_retry_max_delay: + description: Exponential backoff delay in seconds between retries up to this max delay value. + returned: success + type: int + sample: 12 + api_endpoint: + description: Endpoint used for the API requests + returned: success + type: str + sample: "https://api.vultr.com" +vultr_firewall_group: + description: Response from Vultr API + returned: success + type: complex + contains: + id: + description: ID of the firewall group + returned: success + type: str + sample: 1234abcd + name: + description: Name of the firewall group + returned: success + type: str + sample: my firewall group + date_created: + description: Date the firewall group was created + returned: success + type: str + sample: "2017-08-26 12:47:48" + date_modified: + description: Date the firewall group was modified + returned: success + type: str + sample: "2017-08-26 12:47:48" +''' + +from ansible.module_utils.basic import AnsibleModule +from ..module_utils.vultr import ( + Vultr, + vultr_argument_spec, +) + + +class AnsibleVultrFirewallGroup(Vultr): + + def __init__(self, module): + super(AnsibleVultrFirewallGroup, self).__init__(module, "vultr_firewall_group") + + self.returns = { + 'FIREWALLGROUPID': dict(key='id'), + 'description': dict(key='name'), + 'date_created': dict(), + 'date_modified': dict(), + } + + def get_firewall_group(self): + firewall_groups = self.api_query(path="/v1/firewall/group_list") + if firewall_groups: + for firewall_group_id, firewall_group_data in firewall_groups.items(): + if firewall_group_data.get('description') == self.module.params.get('name'): + return firewall_group_data + return {} + + def present_firewall_group(self): + firewall_group = self.get_firewall_group() + if not firewall_group: + firewall_group = self._create_firewall_group(firewall_group) + return firewall_group + + def _create_firewall_group(self, firewall_group): + self.result['changed'] = True + data = { + 'description': self.module.params.get('name'), + } + self.result['diff']['before'] = {} + self.result['diff']['after'] = data + + if not self.module.check_mode: + self.api_query( + path="/v1/firewall/group_create", + method="POST", + data=data + ) + firewall_group = self.get_firewall_group() + return firewall_group + + def absent_firewall_group(self): + firewall_group = self.get_firewall_group() + if firewall_group: + self.result['changed'] = True + + data = { + 'FIREWALLGROUPID': firewall_group['FIREWALLGROUPID'], + } + + self.result['diff']['before'] = firewall_group + self.result['diff']['after'] = {} + + if not self.module.check_mode: + self.api_query( + path="/v1/firewall/group_delete", + method="POST", + data=data + ) + return firewall_group + + +def main(): + argument_spec = vultr_argument_spec() + argument_spec.update(dict( + name=dict(type='str', required=True, aliases=['description']), + state=dict(type='str', choices=['present', 'absent'], default='present'), + )) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + vultr_firewall_group = AnsibleVultrFirewallGroup(module) + if module.params.get('state') == "absent": + firewall_group = vultr_firewall_group.absent_firewall_group() + else: + firewall_group = vultr_firewall_group.present_firewall_group() + + result = vultr_firewall_group.get_result(firewall_group) + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/ngine_io/vultr/plugins/modules/vultr_firewall_group_info.py b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_firewall_group_info.py new file mode 100644 index 000000000..52b3eb0ae --- /dev/null +++ b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_firewall_group_info.py @@ -0,0 +1,139 @@ +#!/usr/bin/python +# +# Copyright (c) 2018, Yanis Guenane +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: vultr_firewall_group_info +short_description: Gather information about the Vultr firewall groups available. +description: + - Gather information about firewall groups available in Vultr. +version_added: "0.1.0" +author: "Yanis Guenane (@Spredzy)" +extends_documentation_fragment: +- ngine_io.vultr.vultr + +''' + +EXAMPLES = r''' +- name: Gather Vultr firewall groups information + ngine_io.vultr.vultr_firewall_group_info: + register: result + +- name: Print the gathered information + debug: + var: result.vultr_firewall_group_info +''' + +RETURN = r''' +--- +vultr_api: + description: Response from Vultr API with a few additions/modification + returned: success + type: complex + contains: + api_account: + description: Account used in the ini file to select the key + returned: success + type: str + sample: default + api_timeout: + description: Timeout used for the API requests + returned: success + type: int + sample: 60 + api_retries: + description: Amount of max retries for the API requests + returned: success + type: int + sample: 5 + api_retry_max_delay: + description: Exponential backoff delay in seconds between retries up to this max delay value. + returned: success + type: int + sample: 12 + api_endpoint: + description: Endpoint used for the API requests + returned: success + type: str + sample: "https://api.vultr.com" +vultr_firewall_group_info: + description: Response from Vultr API + returned: success + type: complex + contains: + id: + description: ID of the firewall group + returned: success + type: str + sample: 1234abcd + description: + description: Name of the firewall group + returned: success + type: str + sample: my firewall group + date_created: + description: Date the firewall group was created + returned: success + type: str + sample: "2017-08-26 12:47:48" + date_modified: + description: Date the firewall group was modified + returned: success + type: str + sample: "2017-08-26 12:47:48" +''' + +from ansible.module_utils.basic import AnsibleModule +from ..module_utils.vultr import ( + Vultr, + vultr_argument_spec, +) + + +class AnsibleVultrFirewallGroupInfo(Vultr): + + def __init__(self, module): + super(AnsibleVultrFirewallGroupInfo, self).__init__(module, "vultr_firewall_group_info") + + self.returns = { + "FIREWALLGROUPID": dict(key='id'), + "date_created": dict(), + "date_modified": dict(), + "description": dict(), + "instance_count": dict(convert_to='int'), + "max_rule_count": dict(convert_to='int'), + "rule_count": dict(convert_to='int') + } + + def get_firewall_group(self): + return self.api_query(path="/v1/firewall/group_list") + + +def parse_fw_group_list(fwgroups_list): + if not fwgroups_list: + return [] + + return [group for id, group in fwgroups_list.items()] + + +def main(): + argument_spec = vultr_argument_spec() + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + fw_group_info = AnsibleVultrFirewallGroupInfo(module) + result = fw_group_info.get_result(parse_fw_group_list(fw_group_info.get_firewall_group())) + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/ngine_io/vultr/plugins/modules/vultr_firewall_rule.py b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_firewall_rule.py new file mode 100644 index 000000000..f9a59b2b0 --- /dev/null +++ b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_firewall_rule.py @@ -0,0 +1,384 @@ +#!/usr/bin/python +# -*- 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) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: vultr_firewall_rule +short_description: Manages firewall rules on Vultr. +description: + - Create and remove firewall rules. +version_added: "0.1.0" +author: "René Moser (@resmo)" +options: + group: + description: + - Name of the firewall group. + required: true + type: str + ip_version: + description: + - IP address version + choices: [ v4, v6 ] + default: v4 + aliases: [ ip_type ] + type: str + protocol: + description: + - Protocol of the firewall rule. + choices: [ icmp, tcp, udp, gre ] + default: tcp + type: str + cidr: + description: + - Network in CIDR format + - The CIDR format must match with the C(ip_version) value. + - Required if C(state=present). + - Defaulted to 0.0.0.0/0 or ::/0 depending on C(ip_version). + type: str + start_port: + description: + - Start port for the firewall rule. + - Required if C(protocol) is tcp or udp and I(state=present). + aliases: [ port ] + type: int + end_port: + description: + - End port for the firewall rule. + - Only considered if C(protocol) is tcp or udp and I(state=present). + type: int + state: + description: + - State of the firewall rule. + default: present + choices: [ present, absent ] + type: str +extends_documentation_fragment: +- ngine_io.vultr.vultr + +''' + +EXAMPLES = ''' +- name: ensure a firewall rule is present + ngine_io.vultr.vultr_firewall_rule: + group: application + protocol: tcp + start_port: 8000 + end_port: 9000 + cidr: 17.17.17.0/24 + +- name: open DNS port for all ipv4 and ipv6 + ngine_io.vultr.vultr_firewall_rule: + group: dns + protocol: udp + port: 53 + ip_version: "{{ item }}" + with_items: [ v4, v6 ] + +- name: allow ping + ngine_io.vultr.vultr_firewall_rule: + group: web + protocol: icmp + +- name: ensure a firewall rule is absent + ngine_io.vultr.vultr_firewall_rule: + group: application + protocol: tcp + start_port: 8000 + end_port: 9000 + cidr: 17.17.17.0/24 + state: absent +''' + +RETURN = ''' +--- +vultr_api: + description: Response from Vultr API with a few additions/modification + returned: success + type: complex + contains: + api_account: + description: Account used in the ini file to select the key + returned: success + type: str + sample: default + api_timeout: + description: Timeout used for the API requests + returned: success + type: int + sample: 60 + api_retries: + description: Amount of max retries for the API requests + returned: success + type: int + sample: 5 + api_retry_max_delay: + description: Exponential backoff delay in seconds between retries up to this max delay value. + returned: success + type: int + sample: 12 + api_endpoint: + description: Endpoint used for the API requests + returned: success + type: str + sample: "https://api.vultr.com" +vultr_firewall_rule: + description: Response from Vultr API + returned: success + type: complex + contains: + rule_number: + description: Rule number of the firewall rule + returned: success + type: int + sample: 2 + action: + description: Action of the firewall rule + returned: success + type: str + sample: accept + protocol: + description: Protocol of the firewall rule + returned: success + type: str + sample: tcp + start_port: + description: Start port of the firewall rule + returned: success and protocol is tcp or udp + type: int + sample: 80 + end_port: + description: End port of the firewall rule + returned: success and when port range and protocol is tcp or udp + type: int + sample: 8080 + cidr: + description: CIDR of the firewall rule (IPv4 or IPv6) + returned: success and when port range + type: str + sample: 0.0.0.0/0 + group: + description: Firewall group the rule is into. + returned: success + type: str + sample: web +''' + +from ansible.module_utils.basic import AnsibleModule +from ..module_utils.vultr import ( + Vultr, + vultr_argument_spec, +) + + +class AnsibleVultrFirewallRule(Vultr): + + def __init__(self, module): + super(AnsibleVultrFirewallRule, self).__init__(module, "vultr_firewall_rule") + + self.returns = { + 'rulenumber': dict(key='rule_number'), + 'action': dict(), + 'protocol': dict(), + 'start_port': dict(convert_to='int'), + 'end_port': dict(convert_to='int'), + 'cidr': dict(), + 'group': dict(), + } + self.firewall_group = None + + def get_firewall_group(self): + if self.firewall_group is not None: + return self.firewall_group + + firewall_groups = self.api_query(path="/v1/firewall/group_list") + if firewall_groups: + for firewall_group_id, firewall_group_data in firewall_groups.items(): + if firewall_group_data.get('description') == self.module.params.get('group'): + self.firewall_group = firewall_group_data + return self.firewall_group + self.fail_json(msg="Firewall group not found: %s" % self.module.params.get('group')) + + def _transform_cidr(self): + cidr = self.module.params.get('cidr') + ip_version = self.module.params.get('ip_version') + if cidr is None: + if ip_version == "v6": + cidr = "::/0" + else: + cidr = "0.0.0.0/0" + elif cidr.count('/') != 1: + self.fail_json(msg="CIDR has an invalid format: %s" % cidr) + + return cidr.split('/') + + def get_firewall_rule(self): + ip_version = self.module.params.get('ip_version') + firewall_group_id = self.get_firewall_group()['FIREWALLGROUPID'] + + firewall_rules = self.api_query( + path="/v1/firewall/rule_list" + "?FIREWALLGROUPID=%s" + "&direction=in" + "&ip_type=%s" + % (firewall_group_id, ip_version)) + + if firewall_rules: + subnet, subnet_size = self._transform_cidr() + + for firewall_rule_id, firewall_rule_data in firewall_rules.items(): + if firewall_rule_data.get('protocol') != self.module.params.get('protocol'): + continue + + if ip_version == 'v4' and (firewall_rule_data.get('subnet') or "0.0.0.0") != subnet: + continue + + if ip_version == 'v6' and (firewall_rule_data.get('subnet') or "::") != subnet: + continue + + if int(firewall_rule_data.get('subnet_size')) != int(subnet_size): + continue + + if firewall_rule_data.get('protocol') in ['tcp', 'udp']: + rule_port = firewall_rule_data.get('port') + + end_port = self.module.params.get('end_port') + start_port = self.module.params.get('start_port') + + # Port range "8000 - 8080" from the API + if ' - ' in rule_port: + if end_port is None: + continue + + port_range = "%s - %s" % (start_port, end_port) + if rule_port == port_range: + return firewall_rule_data + + # Single port + elif int(rule_port) == start_port: + return firewall_rule_data + + else: + return firewall_rule_data + + return {} + + def present_firewall_rule(self): + firewall_rule = self.get_firewall_rule() + if not firewall_rule: + firewall_rule = self._create_firewall_rule(firewall_rule) + return firewall_rule + + def _create_firewall_rule(self, firewall_rule): + protocol = self.module.params.get('protocol') + if protocol in ['tcp', 'udp']: + start_port = self.module.params.get('start_port') + + if start_port is None: + self.module.fail_on_missing_params(['start_port']) + + end_port = self.module.params.get('end_port') + if end_port is not None: + + if start_port >= end_port: + self.module.fail_json(msg="end_port must be higher than start_port") + + port_range = "%s:%s" % (start_port, end_port) + else: + port_range = start_port + else: + port_range = None + + self.result['changed'] = True + + subnet, subnet_size = self._transform_cidr() + + data = { + 'FIREWALLGROUPID': self.get_firewall_group()['FIREWALLGROUPID'], + 'direction': 'in', # currently the only option + 'ip_type': self.module.params.get('ip_version'), + 'protocol': protocol, + 'subnet': subnet, + 'subnet_size': subnet_size, + 'port': port_range + } + + self.result['diff']['after'] = data + + if not self.module.check_mode: + self.api_query( + path="/v1/firewall/rule_create", + method="POST", + data=data + ) + firewall_rule = self.get_firewall_rule() + return firewall_rule + + def absent_firewall_rule(self): + firewall_rule = self.get_firewall_rule() + if firewall_rule: + self.result['changed'] = True + + data = { + 'FIREWALLGROUPID': self.get_firewall_group()['FIREWALLGROUPID'], + 'rulenumber': firewall_rule['rulenumber'] + } + + self.result['diff']['before'] = firewall_rule + + if not self.module.check_mode: + self.api_query( + path="/v1/firewall/rule_delete", + method="POST", + data=data + ) + return firewall_rule + + def get_result(self, resource): + if resource: + if 'port' in resource and resource['protocol'] in ['tcp', 'udp']: + if ' - ' in resource['port']: + resource['start_port'], resource['end_port'] = resource['port'].split(' - ') + else: + resource['start_port'] = resource['port'] + if 'subnet' in resource: + resource['cidr'] = "%s/%s" % (resource['subnet'], resource['subnet_size']) + resource['group'] = self.get_firewall_group()['description'] + return super(AnsibleVultrFirewallRule, self).get_result(resource) + + +def main(): + argument_spec = vultr_argument_spec() + argument_spec.update(dict( + group=dict(type='str', required=True), + start_port=dict(type='int', aliases=['port']), + end_port=dict(type='int'), + protocol=dict(type='str', choices=['tcp', 'udp', 'gre', 'icmp'], default='tcp'), + cidr=dict(type='str',), + ip_version=dict(type='str', choices=['v4', 'v6'], default='v4', aliases=['ip_type']), + state=dict(type='str', choices=['present', 'absent'], default='present'), + )) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + vultr_firewall_rule = AnsibleVultrFirewallRule(module) + if module.params.get('state') == "absent": + firewall_rule = vultr_firewall_rule.absent_firewall_rule() + else: + firewall_rule = vultr_firewall_rule.present_firewall_rule() + + result = vultr_firewall_rule.get_result(firewall_rule) + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/ngine_io/vultr/plugins/modules/vultr_network.py b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_network.py new file mode 100644 index 000000000..3992e3d1e --- /dev/null +++ b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_network.py @@ -0,0 +1,232 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2018, Yanis Guenane +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: vultr_network +short_description: Manages networks on Vultr. +description: + - Manage networks on Vultr. A network cannot be updated. It needs to be deleted and re-created. +version_added: "0.1.0" +author: "Yanis Guenane (@Spredzy)" +options: + name: + description: + - Name of the network. + required: true + aliases: [ description, label ] + type: str + cidr: + description: + - The CIDR IPv4 network block to be used when attaching servers to this network. Required if I(state=present). + type: str + region: + description: + - Region the network is deployed into. Required if I(state=present). + type: str + state: + description: + - State of the network. + default: present + choices: [ present, absent ] + type: str +extends_documentation_fragment: +- ngine_io.vultr.vultr + +''' + +EXAMPLES = ''' +- name: Ensure a network is present + ngine_io.vultr.vultr_network: + name: mynet + cidr: 192.168.42.0/24 + region: Amsterdam + +- name: Ensure a network is absent + ngine_io.vultr.vultr_network: + name: mynet + state: absent +''' + +RETURN = ''' +--- +vultr_api: + description: Response from Vultr API with a few additions/modification + returned: success + type: complex + contains: + api_account: + description: Account used in the ini file to select the key + returned: success + type: str + sample: default + api_timeout: + description: Timeout used for the API requests + returned: success + type: int + sample: 60 + api_retries: + description: Amount of max retries for the API requests + returned: success + type: int + sample: 5 + api_retry_max_delay: + description: Exponential backoff delay in seconds between retries up to this max delay value. + returned: success + type: int + sample: 12 + api_endpoint: + description: Endpoint used for the API requests + returned: success + type: str + sample: "https://api.vultr.com" +vultr_network: + description: Response from Vultr API + returned: success + type: complex + contains: + id: + description: ID of the network + returned: success + type: str + sample: "net5b62c6dc63ef5" + name: + description: Name (label) of the network + returned: success + type: str + sample: "mynetwork" + date_created: + description: Date when the network was created + returned: success + type: str + sample: "2018-08-02 08:54:52" + region: + description: Region the network was deployed into + returned: success + type: str + sample: "Amsterdam" + v4_subnet: + description: IPv4 Network address + returned: success + type: str + sample: "192.168.42.0" + v4_subnet_mask: + description: Ipv4 Network mask + returned: success + type: int + sample: 24 +''' + +from ansible.module_utils.basic import AnsibleModule +from ..module_utils.vultr import ( + Vultr, + vultr_argument_spec, +) + + +class AnsibleVultrNetwork(Vultr): + + def __init__(self, module): + super(AnsibleVultrNetwork, self).__init__(module, "vultr_network") + + self.returns = { + 'NETWORKID': dict(key='id'), + 'DCID': dict(key='region', transform=self._get_region_name), + 'date_created': dict(), + 'description': dict(key='name'), + 'v4_subnet': dict(), + 'v4_subnet_mask': dict(convert_to='int'), + } + + def _get_region_name(self, region_id=None): + return self.get_region().get('name') + + def get_network(self): + networks = self.api_query(path="/v1/network/list") + if networks: + for id, network in networks.items(): + if network.get('description') == self.module.params.get('name'): + return network + return {} + + def present_network(self): + network = self.get_network() + if not network: + network = self._create_network(network) + return network + + def _create_network(self, network): + self.result['changed'] = True + data = { + 'description': self.module.params.get('name'), + 'DCID': self.get_region()['DCID'], + 'v4_subnet': self.module.params.get('cidr').split('/')[0], + 'v4_subnet_mask': self.module.params.get('cidr').split('/')[1] + } + self.result['diff']['before'] = {} + self.result['diff']['after'] = data + + if not self.module.check_mode: + self.api_query( + path="/v1/network/create", + method="POST", + data=data + ) + network = self.get_network() + return network + + def absent_network(self): + network = self.get_network() + if network: + self.result['changed'] = True + + data = { + 'NETWORKID': network['NETWORKID'], + } + + self.result['diff']['before'] = network + self.result['diff']['after'] = {} + + if not self.module.check_mode: + self.api_query( + path="/v1/network/destroy", + method="POST", + data=data + ) + return network + + +def main(): + argument_spec = vultr_argument_spec() + argument_spec.update(dict( + name=dict(type='str', required=True, aliases=['description', 'label']), + cidr=dict(type='str',), + region=dict(type='str',), + state=dict(choices=['present', 'absent'], default='present'), + )) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_if=[['state', 'present', ['cidr', 'region']]] + ) + + vultr_network = AnsibleVultrNetwork(module) + if module.params.get('state') == "absent": + network = vultr_network.absent_network() + else: + network = vultr_network.present_network() + + result = vultr_network.get_result(network) + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/ngine_io/vultr/plugins/modules/vultr_network_info.py b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_network_info.py new file mode 100644 index 000000000..85f6471ab --- /dev/null +++ b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_network_info.py @@ -0,0 +1,156 @@ +#!/usr/bin/python +# +# Copyright (c) 2018, Yanis Guenane +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: vultr_network_info +short_description: Gather information about the Vultr networks available. +description: + - Gather information about networks available in Vultr. +version_added: "0.1.0" +author: "Yanis Guenane (@Spredzy)" +extends_documentation_fragment: +- ngine_io.vultr.vultr + +''' + +EXAMPLES = r''' +- name: Gather Vultr networks information + ngine_io.vultr.vultr_network_info: + register: result + +- name: Print the gathered information + debug: + var: result.vultr_network_info +''' + +RETURN = r''' +--- +vultr_api: + description: Response from Vultr API with a few additions/modification + returned: success + type: complex + contains: + api_account: + description: Account used in the ini file to select the key + returned: success + type: str + sample: default + api_timeout: + description: Timeout used for the API requests + returned: success + type: int + sample: 60 + api_retries: + description: Amount of max retries for the API requests + returned: success + type: int + sample: 5 + api_retry_max_delay: + description: Exponential backoff delay in seconds between retries up to this max delay value. + returned: success + type: int + sample: 12 + api_endpoint: + description: Endpoint used for the API requests + returned: success + type: str + sample: "https://api.vultr.com" +vultr_network_info: + description: Response from Vultr API + returned: success + type: complex + contains: + id: + description: ID of the network + returned: success + type: str + sample: "net5b62c6dc63ef5" + name: + description: Name (label) of the network + returned: success + type: str + sample: "mynetwork" + date_created: + description: Date when the network was created + returned: success + type: str + sample: "2018-08-02 08:54:52" + region: + description: Region the network was deployed into + returned: success + type: str + sample: "Amsterdam" + v4_subnet: + description: IPv4 Network address + returned: success + type: str + sample: "192.168.42.0" + v4_subnet_mask: + description: Ipv4 Network mask + returned: success + type: int + sample: 24 +''' + +from ansible.module_utils.basic import AnsibleModule +from ..module_utils.vultr import ( + Vultr, + vultr_argument_spec, +) + + +class AnsibleVultrNetworkInfo(Vultr): + + def __init__(self, module): + super(AnsibleVultrNetworkInfo, self).__init__(module, "vultr_network_info") + + self.returns = { + 'DCID': dict(key='region', transform=self._get_region_name), + 'NETWORKID': dict(key='id'), + 'date_created': dict(), + 'description': dict(key='name'), + 'v4_subnet': dict(), + 'v4_subnet_mask': dict(convert_to='int'), + } + + def _get_region_name(self, region): + return self.query_resource_by_key( + key='DCID', + value=region, + resource='regions', + use_cache=True + )['name'] + + def get_networks(self): + return self.api_query(path="/v1/network/list") + + +def parse_network_list(network_list): + if isinstance(network_list, list): + return [] + + return [network for id, network in network_list.items()] + + +def main(): + argument_spec = vultr_argument_spec() + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + network_info = AnsibleVultrNetworkInfo(module) + result = network_info.get_result(parse_network_list(network_info.get_networks())) + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/ngine_io/vultr/plugins/modules/vultr_os_info.py b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_os_info.py new file mode 100644 index 000000000..258b50d52 --- /dev/null +++ b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_os_info.py @@ -0,0 +1,137 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2018, Yanis Guenane +# Copyright (c) 2019, René Moser +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: vultr_os_info +short_description: Get information about the Vultr OSes available. +description: + - Get infos about OSes available to boot servers. +version_added: "0.1.0" +author: + - "Yanis Guenane (@Spredzy)" + - "René Moser (@resmo)" +extends_documentation_fragment: +- ngine_io.vultr.vultr + +''' + +EXAMPLES = r''' +- name: Get Vultr OSes infos + ngine_io.vultr.vultr_os_info: + register: results + +- name: Print the gathered infos + debug: + var: results.vultr_os_info +''' + +RETURN = r''' +--- +vultr_api: + description: Response from Vultr API with a few additions/modification + returned: success + type: complex + contains: + api_account: + description: Account used in the ini file to select the key + returned: success + type: str + sample: default + api_timeout: + description: Timeout used for the API requests + returned: success + type: int + sample: 60 + api_retries: + description: Amount of max retries for the API requests + returned: success + type: int + sample: 5 + api_retry_max_delay: + description: Exponential backoff delay in seconds between retries up to this max delay value. + returned: success + type: int + sample: 12 + api_endpoint: + description: Endpoint used for the API requests + returned: success + type: str + sample: "https://api.vultr.com" +vultr_os_info: + description: Response from Vultr API as list + returned: available + type: complex + contains: + arch: + description: OS Architecture + returned: success + type: str + sample: x64 + family: + description: OS family + returned: success + type: str + sample: openbsd + name: + description: OS name + returned: success + type: str + sample: OpenBSD 6 x64 + windows: + description: OS is a MS Windows + returned: success + type: bool +''' + +from ansible.module_utils.basic import AnsibleModule +from ..module_utils.vultr import ( + Vultr, + vultr_argument_spec, +) + + +class AnsibleVultrOSInfo(Vultr): + + def __init__(self, module): + super(AnsibleVultrOSInfo, self).__init__(module, "vultr_os_info") + + self.returns = { + "OSID": dict(key='id', convert_to='int'), + "arch": dict(), + "family": dict(), + "name": dict(), + "windows": dict(convert_to='bool') + } + + def get_oses(self): + return self.api_query(path="/v1/os/list") + + +def parse_oses_list(oses_list): + return [os for id, os in oses_list.items()] + + +def main(): + argument_spec = vultr_argument_spec() + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + os_info = AnsibleVultrOSInfo(module) + result = os_info.get_result(parse_oses_list(os_info.get_oses())) + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/ngine_io/vultr/plugins/modules/vultr_plan_baremetal_info.py b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_plan_baremetal_info.py new file mode 100644 index 000000000..040ca2d94 --- /dev/null +++ b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_plan_baremetal_info.py @@ -0,0 +1,140 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2018, Yanis Guenane +# (c) 2020, Simon Baerlocher +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: vultr_plan_baremetal_info +short_description: Gather information about the Vultr Bare Metal plans available. +description: + - Gather information about Bare Metal plans available to boot servers. +version_added: "0.3.0" +author: "Simon Baerlocher (@sbaerlocher)" +extends_documentation_fragment: +- ngine_io.vultr.vultr +''' + +EXAMPLES = r''' +- name: Gather Vultr Bare Metal plans information + ngine_io.vultr.vultr_baremetal_plan_info: + register: result + +- name: Print the gathered information + debug: + var: result.vultr_baremetal_plan_info +''' + +RETURN = r''' +--- +vultr_api: + description: Response from Vultr API with a few additions/modification + returned: success + type: complex + contains: + api_account: + description: Account used in the ini file to select the key + returned: success + type: str + sample: default + api_timeout: + description: Timeout used for the API requests + returned: success + type: int + sample: 60 + api_retries: + description: Amount of max retries for the API requests + returned: success + type: int + sample: 5 + api_retry_max_delay: + description: Exponential backoff delay in seconds between retries up to this max delay value. + returned: success + type: int + sample: 12 + api_endpoint: + description: Endpoint used for the API requests + returned: success + type: str + sample: "https://api.vultr.com" +vultr_plan_baremetal_info: + description: Response from Vultr API + returned: success + type: complex + contains: + plan: + description: List of the Bare Metal plans available. + returned: success + type: list + sample: [{ + "available_locations": [ + 1 + ], + "bandwidth": 40.0, + "bandwidth_gb": 40960, + "disk": 110, + "id": 118, + "name": "32768 MB RAM,110 GB SSD,40.00 TB BW", + "plan_type": "DEDICATED", + "price_per_month": 240.0, + "ram": 32768, + "vcpu_count": 8, + "windows": false + }] +''' + +from ansible.module_utils.basic import AnsibleModule +from ..module_utils.vultr import ( + Vultr, + vultr_argument_spec, +) + + +class AnsibleVultrPlanInfo(Vultr): + + def __init__(self, module): + super(AnsibleVultrPlanInfo, self).__init__(module, "vultr_plan_baremetal_info") + + self.returns = { + "METALPLANID": dict(key='id', convert_to='int'), + "available_locations": dict(), + "bandwidth_tb": dict(convert_to='int'), + "disk": dict(), + "name": dict(), + "plan_type": dict(), + "price_per_month": dict(convert_to='float'), + "ram": dict(convert_to='int'), + "windows": dict(convert_to='bool'), + "cpu_count": dict(convert_to='int'), + "cpu_model": dict(), + "cpu_thread_count": dict(convert_to='int'), + } + + def get_plans_baremetal(self): + return self.api_query(path="/v1/plans/list_baremetal") + + +def parse_plans_baremetal_list(plans_baremetal_list): + return [plan_baremetal for id, plan_baremetal in plans_baremetal_list.items()] + + +def main(): + argument_spec = vultr_argument_spec() + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + plan_baremetal_info = AnsibleVultrPlanInfo(module) + result = plan_baremetal_info.get_result(parse_plans_baremetal_list(plan_baremetal_info.get_plans_baremetal())) + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/ngine_io/vultr/plugins/modules/vultr_plan_info.py b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_plan_info.py new file mode 100644 index 000000000..3783ab8b0 --- /dev/null +++ b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_plan_info.py @@ -0,0 +1,139 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2018, Yanis Guenane +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: vultr_plan_info +short_description: Gather information about the Vultr plans available. +description: + - Gather information about plans available to boot servers. +version_added: "0.1.0" +author: "Yanis Guenane (@Spredzy)" +extends_documentation_fragment: +- ngine_io.vultr.vultr +''' + +EXAMPLES = r''' +- name: Gather Vultr plans information + ngine_io.vultr.vultr_plan_info: + register: result + +- name: Print the gathered information + debug: + var: result.vultr_plan_info +''' + +RETURN = r''' +--- +vultr_api: + description: Response from Vultr API with a few additions/modification + returned: success + type: complex + contains: + api_account: + description: Account used in the ini file to select the key + returned: success + type: str + sample: default + api_timeout: + description: Timeout used for the API requests + returned: success + type: int + sample: 60 + api_retries: + description: Amount of max retries for the API requests + returned: success + type: int + sample: 5 + api_retry_max_delay: + description: Exponential backoff delay in seconds between retries up to this max delay value. + returned: success + type: int + sample: 12 + api_endpoint: + description: Endpoint used for the API requests + returned: success + type: str + sample: "https://api.vultr.com" +vultr_plan_info: + description: Response from Vultr API + returned: success + type: complex + contains: + plan: + description: List of the plans available. + returned: success + type: list + sample: [{ + "available_locations": [ + 1 + ], + "bandwidth": 40.0, + "bandwidth_gb": 40960, + "disk": 110, + "id": 118, + "name": "32768 MB RAM,110 GB SSD,40.00 TB BW", + "plan_type": "DEDICATED", + "price_per_month": 240.0, + "ram": 32768, + "vcpu_count": 8, + "windows": false + }] +''' + +from ansible.module_utils.basic import AnsibleModule +from ..module_utils.vultr import ( + Vultr, + vultr_argument_spec, +) + + +class AnsibleVultrPlanInfo(Vultr): + + def __init__(self, module): + super(AnsibleVultrPlanInfo, self).__init__(module, "vultr_plan_info") + + self.returns = { + "VPSPLANID": dict(key='id', convert_to='int'), + "available_locations": dict(), + "bandwidth": dict(convert_to='float'), + "bandwidth_gb": dict(convert_to='int'), + "disk": dict(convert_to='int'), + "name": dict(), + "plan_type": dict(), + "price_per_month": dict(convert_to='float'), + "ram": dict(convert_to='int'), + "vcpu_count": dict(convert_to='int'), + "windows": dict(convert_to='bool') + } + + def get_plans(self): + return self.api_query(path="/v1/plans/list") + + +def parse_plans_list(plans_list): + return [plan for id, plan in plans_list.items()] + + +def main(): + argument_spec = vultr_argument_spec() + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + plan_info = AnsibleVultrPlanInfo(module) + result = plan_info.get_result(parse_plans_list(plan_info.get_plans())) + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/ngine_io/vultr/plugins/modules/vultr_region_info.py b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_region_info.py new file mode 100644 index 000000000..2080d2d58 --- /dev/null +++ b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_region_info.py @@ -0,0 +1,129 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2018, Yanis Guenane +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: vultr_region_info +short_description: Gather information about the Vultr regions available. +description: + - Gather information about regions available to boot servers. +version_added: "0.1.0" +author: "Yanis Guenane (@Spredzy)" +extends_documentation_fragment: +- ngine_io.vultr.vultr + +''' + +EXAMPLES = r''' +- name: Gather Vultr regions information + ngine_io.vultr.vultr_region_info: + register: result + +- name: Print the gathered information + debug: + var: result.vultr_region_info +''' + +RETURN = r''' +--- +vultr_api: + description: Response from Vultr API with a few additions/modification + returned: success + type: complex + contains: + api_account: + description: Account used in the ini file to select the key + returned: success + type: str + sample: default + api_timeout: + description: Timeout used for the API requests + returned: success + type: int + sample: 60 + api_retries: + description: Amount of max retries for the API requests + returned: success + type: int + sample: 5 + api_retry_max_delay: + description: Exponential backoff delay in seconds between retries up to this max delay value. + returned: success + type: int + sample: 12 + api_endpoint: + description: Endpoint used for the API requests + returned: success + type: str + sample: "https://api.vultr.com" +vultr_region_info: + description: Response from Vultr API + returned: success + type: list + sample: [ + { + "block_storage": false, + "continent": "Europe", + "country": "GB", + "ddos_protection": true, + "id": 8, + "name": "London", + "regioncode": "LHR", + "state": "" + } + ] +''' + +from ansible.module_utils.basic import AnsibleModule +from ..module_utils.vultr import ( + Vultr, + vultr_argument_spec, +) + + +class AnsibleVultrRegionInfo(Vultr): + + def __init__(self, module): + super(AnsibleVultrRegionInfo, self).__init__(module, "vultr_region_info") + + self.returns = { + "DCID": dict(key='id', convert_to='int'), + "block_storage": dict(convert_to='bool'), + "continent": dict(), + "country": dict(), + "ddos_protection": dict(convert_to='bool'), + "name": dict(), + "regioncode": dict(), + "state": dict() + } + + def get_regions(self): + return self.api_query(path="/v1/regions/list") + + +def parse_regions_list(regions_list): + return [region for id, region in regions_list.items()] + + +def main(): + argument_spec = vultr_argument_spec() + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + region_info = AnsibleVultrRegionInfo(module) + result = region_info.get_result(parse_regions_list(region_info.get_regions())) + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/ngine_io/vultr/plugins/modules/vultr_server.py b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_server.py new file mode 100644 index 000000000..b423766e2 --- /dev/null +++ b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_server.py @@ -0,0 +1,933 @@ +#!/usr/bin/python +# -*- 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) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: vultr_server +short_description: Manages virtual servers on Vultr. +description: + - Deploy, start, stop, update, restart, reinstall servers. +version_added: "0.1.0" +author: "René Moser (@resmo)" +options: + name: + description: + - Name of the server. + required: true + aliases: [ label ] + type: str + hostname: + description: + - The hostname to assign to this server. + type: str + os: + description: + - The operating system name or ID. + - Required if the server does not yet exist and is not restoring from a snapshot. + type: str + snapshot: + description: + - Name or ID of the snapshot to restore the server from. + type: str + firewall_group: + description: + - The firewall group description or ID to assign this server to. + type: str + plan: + description: + - Plan name or ID to use for the server. + - Required if the server does not yet exist. + type: str + force: + description: + - Force stop/start the server if required to apply changes + - Otherwise a running server will not be changed. + type: bool + default: no + notify_activate: + description: + - Whether to send an activation email when the server is ready or not. + - Only considered on creation. + type: bool + default: false + private_network_enabled: + description: + - Whether to enable private networking or not. + type: bool + auto_backup_enabled: + description: + - Whether to enable automatic backups or not. + type: bool + ipv6_enabled: + description: + - Whether to enable IPv6 or not. + type: bool + tag: + description: + - Tag for the server. + type: str + user_data: + description: + - User data to be passed to the server. + type: str + startup_script: + description: + - Name or ID of the startup script to execute on boot. + - Only considered while creating the server. + type: str + ssh_keys: + description: + - List of SSH key names or IDs passed to the server on creation. + aliases: [ ssh_key ] + type: list + elements: str + reserved_ip_v4: + description: + - IP address of the floating IP to use as the main IP of this server. + - Only considered on creation. + type: str + region: + description: + - Region name or ID the server is deployed into. + - Required if the server does not yet exist. + type: str + state: + description: + - State of the server. + default: present + choices: [ present, absent, restarted, reinstalled, started, stopped ] + type: str +extends_documentation_fragment: +- ngine_io.vultr.vultr + +''' + +EXAMPLES = ''' +- name: create server + ngine_io.vultr.vultr_server: + name: "{{ vultr_server_name }}" + os: CentOS 7 x64 + plan: 1024 MB RAM,25 GB SSD,1.00 TB BW + ssh_keys: + - my_key + - your_key + region: Amsterdam + state: present + +- name: ensure a server is present and started + ngine_io.vultr.vultr_server: + name: "{{ vultr_server_name }}" + os: CentOS 7 x64 + plan: 1024 MB RAM,25 GB SSD,1.00 TB BW + firewall_group: my_group + ssh_key: my_key + region: Amsterdam + state: started + +- name: ensure a server is present and stopped provisioned using IDs + ngine_io.vultr.vultr_server: + name: "{{ vultr_server_name }}" + os: "167" + plan: "201" + region: "7" + state: stopped + +- name: ensure an existing server is stopped + ngine_io.vultr.vultr_server: + name: "{{ vultr_server_name }}" + state: stopped + +- name: ensure an existing server is started + ngine_io.vultr.vultr_server: + name: "{{ vultr_server_name }}" + state: started + +- name: ensure a server is absent + ngine_io.vultr.vultr_server: + name: "{{ vultr_server_name }}" + state: absent +''' + +RETURN = ''' +--- +vultr_api: + description: Response from Vultr API with a few additions/modification + returned: success + type: complex + contains: + api_account: + description: Account used in the ini file to select the key + returned: success + type: str + sample: default + api_timeout: + description: Timeout used for the API requests + returned: success + type: int + sample: 60 + api_retries: + description: Amount of max retries for the API requests + returned: success + type: int + sample: 5 + api_retry_max_delay: + description: Exponential backoff delay in seconds between retries up to this max delay value. + returned: success + type: int + sample: 12 + api_endpoint: + description: Endpoint used for the API requests + returned: success + type: str + sample: "https://api.vultr.com" +vultr_server: + description: Response from Vultr API with a few additions/modification + returned: success + type: complex + contains: + id: + description: ID of the server + returned: success + type: str + sample: 10194376 + name: + description: Name (label) of the server + returned: success + type: str + sample: "ansible-test-vm" + plan: + description: Plan used for the server + returned: success + type: str + sample: "1024 MB RAM,25 GB SSD,1.00 TB BW" + allowed_bandwidth_gb: + description: Allowed bandwidth to use in GB + returned: success + type: float + sample: 1000.5 + auto_backup_enabled: + description: Whether automatic backups are enabled + returned: success + type: bool + sample: false + cost_per_month: + description: Cost per month for the server + returned: success + type: float + sample: 5.00 + current_bandwidth_gb: + description: Current bandwidth used for the server + returned: success + type: int + sample: 0 + date_created: + description: Date when the server was created + returned: success + type: str + sample: "2017-08-26 12:47:48" + default_password: + description: Password to login as root into the server + returned: success + type: str + sample: "!p3EWYJm$qDWYaFr" + disk: + description: Information about the disk + returned: success + type: str + sample: "Virtual 25 GB" + v4_gateway: + description: IPv4 gateway + returned: success + type: str + sample: "45.32.232.1" + internal_ip: + description: Internal IP + returned: success + type: str + sample: "" + kvm_url: + description: URL to the VNC + returned: success + type: str + sample: "https://my.vultr.com/subs/vps/novnc/api.php?data=xyz" + region: + description: Region the server was deployed into + returned: success + type: str + sample: "Amsterdam" + v4_main_ip: + description: Main IPv4 + returned: success + type: str + sample: "45.32.233.154" + v4_netmask: + description: Netmask IPv4 + returned: success + type: str + sample: "255.255.254.0" + os: + description: Operating system used for the server + returned: success + type: str + sample: "CentOS 6 x64" + firewall_group: + description: Firewall group the server is assigned to + returned: success and available + type: str + sample: "CentOS 6 x64" + pending_charges: + description: Pending charges + returned: success + type: float + sample: 0.01 + power_status: + description: Power status of the server + returned: success + type: str + sample: "running" + ram: + description: Information about the RAM size + returned: success + type: str + sample: "1024 MB" + server_state: + description: State about the server + returned: success + type: str + sample: "ok" + status: + description: Status about the deployment of the server + returned: success + type: str + sample: "active" + tag: + description: TBD + returned: success + type: str + sample: "" + v6_main_ip: + description: Main IPv6 + returned: success + type: str + sample: "" + v6_network: + description: Network IPv6 + returned: success + type: str + sample: "" + v6_network_size: + description: Network size IPv6 + returned: success + type: str + sample: "" + v6_networks: + description: Networks IPv6 + returned: success + type: list + sample: [] + vcpu_count: + description: Virtual CPU count + returned: success + type: int + sample: 1 +''' + +import time +import base64 +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_text, to_bytes +from ..module_utils.vultr import ( + Vultr, + vultr_argument_spec, +) + + +class AnsibleVultrServer(Vultr): + + def __init__(self, module): + super(AnsibleVultrServer, self).__init__(module, "vultr_server") + + self.server = None + self.returns = { + 'SUBID': dict(key='id'), + 'label': dict(key='name'), + 'date_created': dict(), + 'allowed_bandwidth_gb': dict(convert_to='float'), + 'auto_backups': dict(key='auto_backup_enabled', convert_to='bool'), + 'current_bandwidth_gb': dict(), + 'kvm_url': dict(), + 'default_password': dict(), + 'internal_ip': dict(), + 'disk': dict(), + 'cost_per_month': dict(convert_to='float'), + 'location': dict(key='region'), + 'main_ip': dict(key='v4_main_ip'), + 'network_v4': dict(key='v4_network'), + 'gateway_v4': dict(key='v4_gateway'), + 'os': dict(), + 'pending_charges': dict(convert_to='float'), + 'power_status': dict(), + 'ram': dict(), + 'plan': dict(), + 'server_state': dict(), + 'status': dict(), + 'firewall_group': dict(), + 'tag': dict(), + 'v6_main_ip': dict(), + 'v6_network': dict(), + 'v6_network_size': dict(), + 'v6_networks': dict(), + 'vcpu_count': dict(convert_to='int'), + } + self.server_power_state = None + + def get_startup_script(self): + return self.query_resource_by_key( + key='name', + value=self.module.params.get('startup_script'), + resource='startupscript', + ) + + def get_os(self): + if self.module.params.get('snapshot'): + os_name = 'Snapshot' + else: + os_name = self.module.params.get('os') + + return self.query_resource_by_key( + key='name', + value=os_name, + resource='os', + use_cache=True, + id_key='OSID', + ) + + def get_snapshot(self): + return self.query_resource_by_key( + key='description', + value=self.module.params.get('snapshot'), + resource='snapshot', + id_key='SNAPSHOTID', + ) + + def get_ssh_keys(self): + ssh_key_names = self.module.params.get('ssh_keys') + if not ssh_key_names: + return [] + + ssh_keys = [] + for ssh_key_name in ssh_key_names: + ssh_key = self.query_resource_by_key( + key='name', + value=ssh_key_name, + resource='sshkey', + use_cache=True, + id_key='SSHKEYID', + ) + if ssh_key: + ssh_keys.append(ssh_key) + return ssh_keys + + def get_region(self): + return self.query_resource_by_key( + key='name', + value=self.module.params.get('region'), + resource='regions', + use_cache=True, + id_key='DCID', + ) + + def get_firewall_group(self): + return self.query_resource_by_key( + key='description', + value=self.module.params.get('firewall_group'), + resource='firewall', + query_by='group_list', + id_key='FIREWALLGROUPID' + ) + + def get_user_data(self): + user_data = self.module.params.get('user_data') + if user_data is not None: + user_data = to_text(base64.b64encode(to_bytes(user_data))) + return user_data + + def get_server_user_data(self, server): + if not server or not server.get('SUBID'): + return None + + user_data = self.api_query(path="/v1/server/get_user_data?SUBID=%s" % server.get('SUBID')) + return user_data.get('userdata') + + def get_server(self, refresh=False): + if self.server is None or refresh: + self.server = None + server_list = self.api_query(path="/v1/server/list") + if server_list: + for server_id, server_data in server_list.items(): + if server_data.get('label') == self.module.params.get('name'): + self.server = server_data + + plan = self.query_resource_by_key( + key='VPSPLANID', + value=server_data['VPSPLANID'], + resource='plans', + use_cache=True + ) + self.server['plan'] = plan.get('name') + + os = self.query_resource_by_key( + key='OSID', + value=int(server_data['OSID']), + resource='os', + use_cache=True + ) + self.server['os'] = os.get('name') + + fwg_id = server_data.get('FIREWALLGROUPID') + fw = self.query_resource_by_key( + key='FIREWALLGROUPID', + value=server_data.get('FIREWALLGROUPID') if fwg_id and fwg_id != "0" else None, + resource='firewall', + query_by='group_list', + use_cache=True + ) + self.server['firewall_group'] = fw.get('description') + return self.server + + def present_server(self, start_server=True): + server = self.get_server() + if not server: + server = self._create_server(server=server) + else: + server = self._update_server(server=server, start_server=start_server) + return server + + def _create_server(self, server=None): + required_params = [ + 'os', + 'plan', + 'region', + ] + + snapshot_restore = self.module.params.get('snapshot') is not None + if snapshot_restore: + required_params.remove('os') + + self.module.fail_on_missing_params(required_params=required_params) + + self.result['changed'] = True + if not self.module.check_mode: + data = { + 'DCID': self.get_region().get('DCID'), + 'VPSPLANID': self.get_plan().get('VPSPLANID'), + 'FIREWALLGROUPID': self.get_firewall_group().get('FIREWALLGROUPID'), + 'OSID': self.get_os().get('OSID'), + 'SNAPSHOTID': self.get_snapshot().get('SNAPSHOTID'), + 'label': self.module.params.get('name'), + 'hostname': self.module.params.get('hostname'), + 'SSHKEYID': ','.join([ssh_key['SSHKEYID'] for ssh_key in self.get_ssh_keys()]), + 'enable_ipv6': self.get_yes_or_no('ipv6_enabled'), + 'enable_private_network': self.get_yes_or_no('private_network_enabled'), + 'auto_backups': self.get_yes_or_no('auto_backup_enabled'), + 'notify_activate': self.get_yes_or_no('notify_activate'), + 'tag': self.module.params.get('tag'), + 'reserved_ip_v4': self.module.params.get('reserved_ip_v4'), + 'userdata': self.get_user_data(), + 'SCRIPTID': self.get_startup_script().get('SCRIPTID'), + } + self.api_query( + path="/v1/server/create", + method="POST", + data=data + ) + server = self._wait_for_state(key='status', state='active') + server = self._wait_for_state(state='running', timeout=3600 if snapshot_restore else 60) + return server + + def _update_auto_backups_setting(self, server, start_server): + auto_backup_enabled_changed = self.switch_enable_disable(server, 'auto_backup_enabled', 'auto_backups') + + if auto_backup_enabled_changed: + if auto_backup_enabled_changed == "enable" and server['auto_backups'] == 'disable': + self.module.warn("Backups are disabled. Once disabled, backups can only be enabled again by customer support") + else: + server, warned = self._handle_power_status_for_update(server, start_server) + if not warned: + self.result['changed'] = True + self.result['diff']['before']['auto_backup_enabled'] = server.get('auto_backups') + self.result['diff']['after']['auto_backup_enabled'] = self.get_yes_or_no('auto_backup_enabled') + + if not self.module.check_mode: + data = { + 'SUBID': server['SUBID'] + } + self.api_query( + path="/v1/server/backup_%s" % auto_backup_enabled_changed, + method="POST", + data=data + ) + return server + + def _update_ipv6_setting(self, server, start_server): + ipv6_enabled_changed = self.switch_enable_disable(server, 'ipv6_enabled', 'v6_main_ip') + + if ipv6_enabled_changed: + if ipv6_enabled_changed == "disable": + self.module.warn("The Vultr API does not allow to disable IPv6") + else: + server, warned = self._handle_power_status_for_update(server, start_server) + if not warned: + self.result['changed'] = True + self.result['diff']['before']['ipv6_enabled'] = False + self.result['diff']['after']['ipv6_enabled'] = True + + if not self.module.check_mode: + data = { + 'SUBID': server['SUBID'] + } + self.api_query( + path="/v1/server/ipv6_%s" % ipv6_enabled_changed, + method="POST", + data=data + ) + server = self._wait_for_state(key='v6_main_ip') + return server + + def _update_private_network_setting(self, server, start_server): + private_network_enabled_changed = self.switch_enable_disable(server, 'private_network_enabled', 'internal_ip') + if private_network_enabled_changed: + if private_network_enabled_changed == "disable": + self.module.warn("The Vultr API does not allow to disable private network") + else: + server, warned = self._handle_power_status_for_update(server, start_server) + if not warned: + self.result['changed'] = True + self.result['diff']['before']['private_network_enabled'] = False + self.result['diff']['after']['private_network_enabled'] = True + + if not self.module.check_mode: + data = { + 'SUBID': server['SUBID'] + } + self.api_query( + path="/v1/server/private_network_%s" % private_network_enabled_changed, + method="POST", + data=data + ) + return server + + def _update_plan_setting(self, server, start_server): + # Verify the exising plan is not discontined by Vultr and therefore won't be found by the API + server_plan = self.get_plan(plan=server.get('VPSPLANID'), optional=True) + if not server_plan: + plan = self.get_plan(optional=True) + if not plan: + self.module.warn("The plan used to create the server is not longer available as well as the desired plan. Assuming same plan, keeping as is.") + return server + else: + plan = self.get_plan() + + plan_changed = True if plan and plan['VPSPLANID'] != server.get('VPSPLANID') else False + if plan_changed: + server, warned = self._handle_power_status_for_update(server, start_server) + if not warned: + self.result['changed'] = True + self.result['diff']['before']['plan'] = server.get('plan') + self.result['diff']['after']['plan'] = plan['name'] + + if not self.module.check_mode: + data = { + 'SUBID': server['SUBID'], + 'VPSPLANID': plan['VPSPLANID'], + } + self.api_query( + path="/v1/server/upgrade_plan", + method="POST", + data=data + ) + return server + + def _handle_power_status_for_update(self, server, start_server): + # Remember the power state before we handle any action + if self.server_power_state is None: + self.server_power_state = server['power_status'] + + # A stopped server can be updated + if self.server_power_state == "stopped": + return server, False + + # A running server must be forced to update unless the wanted state is stopped + elif self.module.params.get('force') or not start_server: + warned = False + if not self.module.check_mode: + # Some update APIs would restart the VM, we handle the restart manually + # by stopping the server and start it at the end of the changes + server = self.stop_server(skip_results=True) + + # Warn the user that a running server won't get changed + else: + warned = True + self.module.warn("Some changes won't be applied to running instances. " + + "Use force=true to allow the instance %s to be stopped/started." % server['label']) + + return server, warned + + def _update_server(self, server=None, start_server=True): + # Wait for server to unlock if restoring + if server.get('os').strip() == 'Snapshot': + server = self._wait_for_state(key='server_status', state='ok', timeout=3600) + + # Update auto backups settings, stops server + server = self._update_auto_backups_setting(server=server, start_server=start_server) + + # Update IPv6 settings, stops server + server = self._update_ipv6_setting(server=server, start_server=start_server) + + # Update private network settings, stops server + server = self._update_private_network_setting(server=server, start_server=start_server) + + # Update plan settings, stops server + server = self._update_plan_setting(server=server, start_server=start_server) + + # User data + user_data = self.get_user_data() + server_user_data = self.get_server_user_data(server=server) + if user_data is not None and user_data != server_user_data: + self.result['changed'] = True + self.result['diff']['before']['user_data'] = server_user_data + self.result['diff']['after']['user_data'] = user_data + + if not self.module.check_mode: + data = { + 'SUBID': server['SUBID'], + 'userdata': user_data, + } + self.api_query( + path="/v1/server/set_user_data", + method="POST", + data=data + ) + + # Tags + tag = self.module.params.get('tag') + if tag is not None and tag != server.get('tag'): + self.result['changed'] = True + self.result['diff']['before']['tag'] = server.get('tag') + self.result['diff']['after']['tag'] = tag + + if not self.module.check_mode: + data = { + 'SUBID': server['SUBID'], + 'tag': tag, + } + self.api_query( + path="/v1/server/tag_set", + method="POST", + data=data + ) + + # Firewall group + firewall_group = self.get_firewall_group() + if firewall_group and firewall_group.get('description') != server.get('firewall_group'): + self.result['changed'] = True + self.result['diff']['before']['firewall_group'] = server.get('firewall_group') + self.result['diff']['after']['firewall_group'] = firewall_group.get('description') + + if not self.module.check_mode: + data = { + 'SUBID': server['SUBID'], + 'FIREWALLGROUPID': firewall_group.get('FIREWALLGROUPID'), + } + self.api_query( + path="/v1/server/firewall_group_set", + method="POST", + data=data + ) + # Start server again if it was running before the changes + if not self.module.check_mode: + if self.server_power_state in ['starting', 'running'] and start_server: + server = self.start_server(skip_results=True) + + server = self._wait_for_state(key='status', state='active') + return server + + def absent_server(self): + server = self.get_server() + if server: + self.result['changed'] = True + self.result['diff']['before']['id'] = server['SUBID'] + self.result['diff']['after']['id'] = "" + if not self.module.check_mode: + data = { + 'SUBID': server['SUBID'] + } + self.api_query( + path="/v1/server/destroy", + method="POST", + data=data + ) + for s in range(0, 60): + if server is not None: + break + time.sleep(2) + server = self.get_server(refresh=True) + else: + self.fail_json(msg="Wait for server '%s' to get deleted timed out" % server['label']) + return server + + def restart_server(self): + self.result['changed'] = True + server = self.get_server() + if server: + if not self.module.check_mode: + data = { + 'SUBID': server['SUBID'] + } + self.api_query( + path="/v1/server/reboot", + method="POST", + data=data + ) + server = self._wait_for_state(state='running') + return server + + def reinstall_server(self): + self.result['changed'] = True + server = self.get_server() + if server: + if not self.module.check_mode: + data = { + 'SUBID': server['SUBID'] + } + self.api_query( + path="/v1/server/reinstall", + method="POST", + data=data + ) + server = self._wait_for_state(state='running') + return server + + def _wait_for_state(self, key='power_status', state=None, timeout=60): + time.sleep(1) + server = self.get_server(refresh=True) + for s in range(0, timeout): + # Check for Truely if wanted state is None + if state is None and server.get(key): + break + elif server.get(key) == state: + break + time.sleep(2) + server = self.get_server(refresh=True) + + # Timed out + else: + if state is None: + msg = "Wait for '%s' timed out" % key + else: + msg = "Wait for '%s' to get into state '%s' timed out" % (key, state) + self.fail_json(msg=msg) + return server + + def start_server(self, skip_results=False): + server = self.get_server() + if server: + if server['power_status'] == 'starting': + server = self._wait_for_state(state='running') + + elif server['power_status'] != 'running': + if not skip_results: + self.result['changed'] = True + self.result['diff']['before']['power_status'] = server['power_status'] + self.result['diff']['after']['power_status'] = "running" + if not self.module.check_mode: + data = { + 'SUBID': server['SUBID'] + } + self.api_query( + path="/v1/server/start", + method="POST", + data=data + ) + server = self._wait_for_state(state='running') + return server + + def stop_server(self, skip_results=False): + server = self.get_server() + if server and server['power_status'] != "stopped": + if not skip_results: + self.result['changed'] = True + self.result['diff']['before']['power_status'] = server['power_status'] + self.result['diff']['after']['power_status'] = "stopped" + if not self.module.check_mode: + data = { + 'SUBID': server['SUBID'], + } + self.api_query( + path="/v1/server/halt", + method="POST", + data=data + ) + server = self._wait_for_state(state='stopped') + return server + + +def main(): + argument_spec = vultr_argument_spec() + argument_spec.update(dict( + name=dict(required=True, aliases=['label']), + hostname=dict(type='str'), + os=dict(type='str'), + snapshot=dict(type='str'), + plan=dict(type='str'), + force=dict(type='bool', default=False), + notify_activate=dict(type='bool', default=False), + private_network_enabled=dict(type='bool'), + auto_backup_enabled=dict(type='bool'), + ipv6_enabled=dict(type='bool'), + tag=dict(type='str'), + reserved_ip_v4=dict(type='str'), + firewall_group=dict(type='str'), + startup_script=dict(type='str'), + user_data=dict(type='str'), + ssh_keys=dict(type='list', elements='str', aliases=['ssh_key'], no_log=False), + region=dict(type='str'), + state=dict(choices=['present', 'absent', 'restarted', 'reinstalled', 'started', 'stopped'], default='present'), + )) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + vultr_server = AnsibleVultrServer(module) + if module.params.get('state') == "absent": + server = vultr_server.absent_server() + else: + if module.params.get('state') == "started": + server = vultr_server.present_server() + server = vultr_server.start_server() + elif module.params.get('state') == "stopped": + server = vultr_server.present_server(start_server=False) + server = vultr_server.stop_server() + elif module.params.get('state') == "restarted": + server = vultr_server.present_server() + server = vultr_server.restart_server() + elif module.params.get('state') == "reinstalled": + server = vultr_server.reinstall_server() + else: + server = vultr_server.present_server() + + result = vultr_server.get_result(server) + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/ngine_io/vultr/plugins/modules/vultr_server_baremetal.py b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_server_baremetal.py new file mode 100644 index 000000000..279f3d14b --- /dev/null +++ b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_server_baremetal.py @@ -0,0 +1,548 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2019, Nate River +# (c) 2020, Simon Baerlocher +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r''' +--- +module: vultr_server_baremetal +short_description: Manages baremetal servers on Vultr. +description: + - Deploy and destroy servers. +version_added: "0.3.0" +author: + - "Nate River (@vitikc)" + - "Simon Baerlocher (@sbaerlocher)" +options: + name: + description: + - Name of the server. + required: true + aliases: [ label ] + type: str + hostname: + description: + - The hostname to assign to this server. + type: str + os: + description: + - The operating system name or ID. + - Required if the server does not yet exist and is not restoring from a snapshot. + type: str + plan: + description: + - Plan name or ID to use for the server. + - Required if the server does not yet exist. + type: str + notify_activate: + description: + - Whether to send an activation email when the server is ready or not. + - Only considered on creation. + type: bool + default: false + ipv6_enabled: + description: + - Whether to enable IPv6 or not. + type: bool + tag: + description: + - Tag for the server. + type: str + user_data: + description: + - User data to be passed to the server. + type: str + startup_script: + description: + - Name or ID of the startup script to execute on boot. + - Only considered while creating the server. + type: str + ssh_keys: + description: + - List of SSH key names or IDs passed to the server on creation. + aliases: [ ssh_key ] + type: list + elements: str + reserved_ip_v4: + description: + - IP address of the floating IP to use as the main IP of this server. + - Only considered on creation. + type: str + region: + description: + - Region name or ID the server is deployed into. + - Required if the server does not yet exist. + type: str + state: + description: + - State of the server. + default: present + choices: [ present, absent ] + type: str +extends_documentation_fragment: +- ngine_io.vultr.vultr +''' + +EXAMPLES = r''' +- name: create server + ngine_io.vultr.vultr_server_baremetal: + name: "{{ vultr_server_baremetal_name }}" + os: Debian 9 x64 (stretch) + plan: 32768 MB RAM,2x 240 GB SSD,5.00 TB BW + region: Amsterdam + +- name: ensure a server is absent + ngine_io.vultr.vultr_server_baremetal: + name: "{{ vultr_server_baremetal_name }}" + state: absent +''' + +RETURN = r''' +--- +vultr_api: + description: Response from Vultr API with a few additions/modification + returned: success + type: complex + contains: + api_account: + description: Account used in the ini file to select the key + returned: success + type: str + sample: default + api_timeout: + description: Timeout used for the API requests + returned: success + type: int + sample: 60 + api_retries: + description: Amount of max retries for the API requests + returned: success + type: int + sample: 5 + api_endpoint: + description: Endpoint used for the API requests + returned: success + type: str + sample: "https://api.vultr.com" +vultr_server_baremetal: + description: Response from Vultr API with a few additions/modification + returned: success + type: complex + contains: + id: + description: ID of the server + returned: success + type: str + sample: 900000 + name: + description: Name (label) of the server + returned: success + type: str + sample: "ansible-test-baremetal" + plan: + description: Plan used for the server + returned: success + type: str + sample: "32768 MB RAM,2x 240 GB SSD,5.00 TB BW" + allowed_bandwidth_gb: + description: Allowed bandwidth to use in GB + returned: success + type: float + sample: 1000.5 + cost_per_month: + description: Cost per month for the server + returned: success + type: float + sample: 120.00 + current_bandwidth_gb: + description: Current bandwidth used for the server + returned: success + type: int + sample: 0 + date_created: + description: Date when the server was created + returned: success + type: str + sample: "2017-04-12 18:45:41" + default_password: + description: Password to login as root into the server + returned: success + type: str + sample: "ab81u!ryranq" + disk: + description: Information about the disk + returned: success + type: str + sample: "SSD 250 GB" + v4_gateway: + description: IPv4 gateway + returned: success + type: str + sample: "203.0.113.1" + internal_ip: + description: Internal IP + returned: success + type: str + sample: "" + region: + description: Region the server was deployed into + returned: success + type: str + sample: "Amsterdam" + v4_main_ip: + description: Main IPv4 + returned: success + type: str + sample: "203.0.113.10" + v4_netmask: + description: Netmask IPv4 + returned: success + type: str + sample: "255.255.255.0" + os: + description: Operating system used for the server + returned: success + type: str + sample: "Debian 9 x64" + pending_charges: + description: Pending charges + returned: success + type: float + sample: 0.18 + ram: + description: Information about the RAM size + returned: success + type: str + sample: "32768 MB" + status: + description: Status about the deployment of the server + returned: success + type: str + sample: "active" + tag: + description: Server tag + returned: success + type: str + sample: "my tag" + v6_main_ip: + description: Main IPv6 + returned: success + type: str + sample: "2001:DB8:9000::100" + v6_network: + description: Network IPv6 + returned: success + type: str + sample: "2001:DB8:9000::" + v6_network_size: + description: Network size IPv6 + returned: success + type: int + sample: 64 + v6_networks: + description: Networks IPv6 + returned: success + type: list + sample: [] +''' + +import time +import base64 +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_text, to_bytes +from ..module_utils.vultr import ( + Vultr, + vultr_argument_spec, +) + + +class AnsibleVultrServerBareMetal(Vultr): + + def __init__(self, module): + super(AnsibleVultrServerBareMetal, self).__init__(module, "vultr_server_baremetal") + + self.server = None + self.returns = { + 'SUBID': dict(key='id'), + 'label': dict(key='name'), + 'date_created': dict(), + 'allowed_bandwidth_gb': dict(convert_to='float'), + 'current_bandwidth_gb': dict(), + 'default_password': dict(), + 'internal_ip': dict(), + 'disk': dict(), + 'cost_per_month': dict(convert_to='float'), + 'location': dict(key='region'), + 'main_ip': dict(key='v4_main_ip'), + 'network_v4': dict(key='v4_network'), + 'gateway_v4': dict(key='v4_gateway'), + 'os': dict(), + 'pending_charges': dict(convert_to='float'), + 'ram': dict(), + 'plan': dict(), + 'status': dict(), + 'tag': dict(), + 'v6_main_ip': dict(), + 'v6_network': dict(), + 'v6_network_size': dict(), + 'v6_networks': dict(), + } + self.server_power_state = None + + def get_startup_script(self): + return self.query_resource_by_key( + key='name', + value=self.module.params.get('startup_script'), + resource='startupscript', + ) + + def get_os(self): + return self.query_resource_by_key( + key='name', + value=self.module.params.get('os'), + resource='os', + use_cache=True + ) + + def get_ssh_keys(self): + ssh_key_names = self.module.params.get('ssh_keys') + if not ssh_key_names: + return [] + + ssh_keys = [] + for ssh_key_name in ssh_key_names: + ssh_key = self.query_resource_by_key( + key='name', + value=ssh_key_name, + resource='sshkey', + use_cache=True + ) + if ssh_key: + ssh_keys.append(ssh_key) + return ssh_keys + + def get_region(self): + return self.query_resource_by_key( + key='name', + value=self.module.params.get('region'), + resource='regions', + use_cache=True + ) + + def get_plan(self): + return self.query_resource_by_key( + key='name', + value=self.module.params.get('plan'), + resource='plans', + query_by='list_baremetal', + use_cache=True + ) + + def get_user_data(self): + user_data = self.module.params.get('user_data') + if user_data is not None: + user_data = to_text(base64.b64encode(to_bytes(user_data))) + return user_data + + def get_server_user_data(self, server): + if not server or not server.get('SUBID'): + return None + + user_data = self.api_query(path="/v1/baremetal/get_user_data?SUBID=%s" % server.get('SUBID')) + return user_data.get('userdata') + + def get_server(self, refresh=False): + if self.server is None or refresh: + self.server = None + server_list = self.api_query(path="/v1/baremetal/list") + if server_list: + for server_id, server_data in server_list.items(): + if server_data.get('label') == self.module.params.get('name'): + self.server = server_data + + plan = self.query_resource_by_key( + key='METALPLANID', + value=server_data['METALPLANID'], + resource='plans', + query_by='list_baremetal', + use_cache=True + ) + self.server['plan'] = plan.get('name') + + os = self.query_resource_by_key( + key='OSID', + value=int(server_data['OSID']), + resource='os', + use_cache=True + ) + self.server['os'] = os.get('name') + return self.server + + def _wait_for_state(self, key='status', state=None): + time.sleep(1) + server = self.get_server(refresh=True) + for s in range(0, 500): + if state is None and server.get(key): + break + elif server.get(key) == state: + break + time.sleep(2) + server = self.get_server(refresh=True) + + # Timed out + else: + if state is None: + msg = "Wait for '%s' timed out" % key + else: + msg = "Wait for '%s' to get into state '%s' timed out" % (key, state) + self.fail_json(msg=msg) + return server + + def present_server(self, start_server=True): + server = self.get_server() + if not server: + server = self._create_server(server=server) + else: + server = self._update_server(server=server, start_server=start_server) + return server + + def _create_server(self, server=None): + required_params = [ + 'os', + 'plan', + 'region', + ] + self.module.fail_on_missing_params(required_params=required_params) + + self.result['changed'] = True + if not self.module.check_mode: + data = { + 'DCID': self.get_region().get('DCID'), + 'METALPLANID': self.get_plan().get('METALPLANID'), + 'OSID': self.get_os().get('OSID'), + 'label': self.module.params.get('name'), + 'hostname': self.module.params.get('hostname'), + 'SSHKEYID': ','.join([ssh_key['SSHKEYID'] for ssh_key in self.get_ssh_keys()]), + 'enable_ipv6': self.get_yes_or_no('ipv6_enabled'), + 'notify_activate': self.get_yes_or_no('notify_activate'), + 'tag': self.module.params.get('tag'), + 'reserved_ip_v4': self.module.params.get('reserved_ip_v4'), + 'user_data': self.get_user_data(), + 'SCRIPTID': self.get_startup_script().get('SCRIPTID'), + } + self.api_query( + path="/v1/baremetal/create", + method="POST", + data=data + ) + server = self._wait_for_state(key='status', state='active') + return server + + def _update_server(self, server=None, start_server=True): + + # Update plan settings + # server = self._update_plan_setting(server=server, start_server=start_server) + + # User data + user_data = self.get_user_data() + server_user_data = self.get_server_user_data(server=server) + if user_data is not None and user_data != server_user_data: + self.result['changed'] = True + self.result['diff']['before']['user_data'] = server_user_data + self.result['diff']['after']['user_data'] = user_data + + if not self.module.check_mode: + data = { + 'SUBID': server['SUBID'], + 'userdata': user_data, + } + self.api_query( + path="/v1/baremetal/set_user_data", + method="POST", + data=data + ) + + # Tags + tag = self.module.params.get('tag') + if tag is not None and tag != server.get('tag'): + self.result['changed'] = True + self.result['diff']['before']['tag'] = server.get('tag') + self.result['diff']['after']['tag'] = tag + + if not self.module.check_mode: + data = { + 'SUBID': server['SUBID'], + 'tag': tag, + } + self.api_query( + path="/v1/baremetal/tag_set", + method="POST", + data=data + ) + return server + + def absent_server(self): + server = self.get_server() + if server: + self.result['changed'] = True + self.result['diff']['before']['id'] = server['SUBID'] + self.result['diff']['after']['id'] = "" + if not self.module.check_mode: + data = { + 'SUBID': server['SUBID'] + } + self.api_query( + path="/v1/baremetal/destroy", + method="POST", + data=data + ) + for s in range(0, 60): + if server is not None: + break + time.sleep(2) + server = self.get_server(refresh=True) + else: + self.fail_json(msg="Wait for server '%s' to get deleted timed out" % server['label']) + return server + + +def main(): + argument_spec = vultr_argument_spec() + argument_spec.update(dict( + name=dict(required=True, aliases=['label']), + hostname=dict(type='str',), + os=dict(type='str',), + plan=dict(type='str',), + notify_activate=dict(type='bool', default=False), + ipv6_enabled=dict(type='bool'), + tag=dict(type='str',), + reserved_ip_v4=dict(type='str',), + startup_script=dict(type='str',), + user_data=dict(type='str',), + ssh_keys=dict(type='list', elements='str', aliases=['ssh_key'], no_log=False), + region=dict(type='str',), + state=dict(type='str', choices=['present', 'absent'], default='present'), + )) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + vultr_server_baremetal = AnsibleVultrServerBareMetal(module) + if module.params.get('state') == "absent": + server = vultr_server_baremetal.absent_server() + else: + server = vultr_server_baremetal.present_server() + + result = vultr_server_baremetal.get_result(server) + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/ngine_io/vultr/plugins/modules/vultr_server_info.py b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_server_info.py new file mode 100644 index 000000000..a2608ac14 --- /dev/null +++ b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_server_info.py @@ -0,0 +1,300 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2018, Yanis Guenane +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: vultr_server_info +short_description: Gather information about the Vultr servers available. +description: + - Gather information about servers available. +version_added: "0.1.0" +author: "Yanis Guenane (@Spredzy)" +extends_documentation_fragment: +- ngine_io.vultr.vultr + +''' + +EXAMPLES = r''' +- name: Gather Vultr servers information + ngine_io.vultr.vultr_server_info: + register: result + +- name: Print the gathered information + debug: + var: result.vultr_server_info +''' + +RETURN = r''' +--- +vultr_api: + description: Response from Vultr API with a few additions/modification + returned: success + type: complex + contains: + api_account: + description: Account used in the ini file to select the key + returned: success + type: str + sample: default + api_timeout: + description: Timeout used for the API requests + returned: success + type: int + sample: 60 + api_retries: + description: Amount of max retries for the API requests + returned: success + type: int + sample: 5 + api_retry_max_delay: + description: Exponential backoff delay in seconds between retries up to this max delay value. + returned: success + type: int + sample: 12 + api_endpoint: + description: Endpoint used for the API requests + returned: success + type: str + sample: "https://api.vultr.com" +vultr_server_info: + description: Response from Vultr API + returned: success + type: complex + contains: + id: + description: ID of the server + returned: success + type: str + sample: 10194376 + name: + description: Name (label) of the server + returned: success + type: str + sample: "ansible-test-vm" + plan: + description: Plan used for the server + returned: success + type: str + sample: "1024 MB RAM,25 GB SSD,1.00 TB BW" + allowed_bandwidth_gb: + description: Allowed bandwidth to use in GB + returned: success + type: float + sample: 1000.5 + auto_backup_enabled: + description: Whether automatic backups are enabled + returned: success + type: bool + sample: false + cost_per_month: + description: Cost per month for the server + returned: success + type: float + sample: 5.00 + current_bandwidth_gb: + description: Current bandwidth used for the server + returned: success + type: int + sample: 0 + date_created: + description: Date when the server was created + returned: success + type: str + sample: "2017-08-26 12:47:48" + default_password: + description: Password to login as root into the server + returned: success + type: str + sample: "!p3EWYJm$qDWYaFr" + disk: + description: Information about the disk + returned: success + type: str + sample: "Virtual 25 GB" + v4_gateway: + description: IPv4 gateway + returned: success + type: str + sample: "45.32.232.1" + internal_ip: + description: Internal IP + returned: success + type: str + sample: "" + kvm_url: + description: URL to the VNC + returned: success + type: str + sample: "https://my.vultr.com/subs/vps/novnc/api.php?data=xyz" + region: + description: Region the server was deployed into + returned: success + type: str + sample: "Amsterdam" + v4_main_ip: + description: Main IPv4 + returned: success + type: str + sample: "45.32.233.154" + v4_netmask: + description: Netmask IPv4 + returned: success + type: str + sample: "255.255.254.0" + os: + description: Operating system used for the server + returned: success + type: str + sample: "CentOS 6 x64" + firewall_group: + description: Firewall group the server is assigned to + returned: success and available + type: str + sample: "CentOS 6 x64" + pending_charges: + description: Pending charges + returned: success + type: float + sample: 0.01 + power_status: + description: Power status of the server + returned: success + type: str + sample: "running" + ram: + description: Information about the RAM size + returned: success + type: str + sample: "1024 MB" + server_state: + description: State about the server + returned: success + type: str + sample: "ok" + status: + description: Status about the deployment of the server + returned: success + type: str + sample: "active" + tag: + description: TBD + returned: success + type: str + sample: "" + v6_main_ip: + description: Main IPv6 + returned: success + type: str + sample: "" + v6_network: + description: Network IPv6 + returned: success + type: str + sample: "" + v6_network_size: + description: Network size IPv6 + returned: success + type: str + sample: "" + v6_networks: + description: Networks IPv6 + returned: success + type: list + sample: [] + vcpu_count: + description: Virtual CPU count + returned: success + type: int + sample: 1 +''' + +from ansible.module_utils.basic import AnsibleModule +from ..module_utils.vultr import ( + Vultr, + vultr_argument_spec, +) + + +class AnsibleVultrServerInfo(Vultr): + + def __init__(self, module): + super(AnsibleVultrServerInfo, self).__init__(module, "vultr_server_info") + + self.returns = { + "APPID": dict(key='application', convert_to='int', transform=self._get_application_name), + "FIREWALLGROUPID": dict(key='firewallgroup', transform=self._get_firewallgroup_name), + "SUBID": dict(key='id', convert_to='int'), + "VPSPLANID": dict(key='plan', convert_to='int', transform=self._get_plan_name), + "allowed_bandwidth_gb": dict(convert_to='float'), + 'auto_backups': dict(key='auto_backup_enabled', convert_to='bool'), + "cost_per_month": dict(convert_to='float'), + "current_bandwidth_gb": dict(convert_to='float'), + "date_created": dict(), + "default_password": dict(), + "disk": dict(), + "gateway_v4": dict(key='v4_gateway'), + "internal_ip": dict(), + "kvm_url": dict(), + "label": dict(key='name'), + "location": dict(key='region'), + "main_ip": dict(key='v4_main_ip'), + "netmask_v4": dict(key='v4_netmask'), + "os": dict(), + "pending_charges": dict(convert_to='float'), + "power_status": dict(), + "ram": dict(), + "server_state": dict(), + "status": dict(), + "tag": dict(), + "v6_main_ip": dict(), + "v6_network": dict(), + "v6_network_size": dict(), + "v6_networks": dict(), + "vcpu_count": dict(convert_to='int'), + } + + def _get_application_name(self, application): + if application == 0: + return None + + return self.get_application(application, 'APPID').get('name') + + def _get_firewallgroup_name(self, firewallgroup): + if firewallgroup == 0: + return None + + return self.get_firewallgroup(firewallgroup, 'FIREWALLGROUPID').get('description') + + def _get_plan_name(self, plan): + return self.get_plan(plan, 'VPSPLANID', optional=True).get('name') or 'N/A' + + def get_servers(self): + return self.api_query(path="/v1/server/list") + + +def parse_servers_list(servers_list): + return [server for id, server in servers_list.items()] + + +def main(): + argument_spec = vultr_argument_spec() + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + server_info = AnsibleVultrServerInfo(module) + result = server_info.get_result(parse_servers_list(server_info.get_servers())) + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/ngine_io/vultr/plugins/modules/vultr_ssh_key.py b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_ssh_key.py new file mode 100644 index 000000000..11e648ce6 --- /dev/null +++ b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_ssh_key.py @@ -0,0 +1,236 @@ +#!/usr/bin/python +# -*- 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) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: vultr_ssh_key +short_description: Manages ssh keys on Vultr. +description: + - Create, update and remove ssh keys. +version_added: "0.1.0" +author: "René Moser (@resmo)" +options: + name: + description: + - Name of the ssh key. + required: true + type: str + ssh_key: + description: + - SSH public key. + - Required if C(state=present). + type: str + state: + description: + - State of the ssh key. + default: present + choices: [ present, absent ] + type: str +extends_documentation_fragment: +- ngine_io.vultr.vultr + +''' + +EXAMPLES = ''' +- name: ensure an SSH key is present + ngine_io.vultr.vultr_ssh_key: + name: my ssh key + ssh_key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}" + +- name: ensure an SSH key is absent + ngine_io.vultr.vultr_ssh_key: + name: my ssh key + state: absent +''' + +RETURN = ''' +--- +vultr_api: + description: Response from Vultr API with a few additions/modification + returned: success + type: complex + contains: + api_account: + description: Account used in the ini file to select the key + returned: success + type: str + sample: default + api_timeout: + description: Timeout used for the API requests + returned: success + type: int + sample: 60 + api_retries: + description: Amount of max retries for the API requests + returned: success + type: int + sample: 5 + api_retry_max_delay: + description: Exponential backoff delay in seconds between retries up to this max delay value. + returned: success + type: int + sample: 12 + api_endpoint: + description: Endpoint used for the API requests + returned: success + type: str + sample: "https://api.vultr.com" +vultr_ssh_key: + description: Response from Vultr API + returned: success + type: complex + contains: + id: + description: ID of the ssh key + returned: success + type: str + sample: 5904bc6ed9234 + name: + description: Name of the ssh key + returned: success + type: str + sample: my ssh key + date_created: + description: Date the ssh key was created + returned: success + type: str + sample: "2017-08-26 12:47:48" + ssh_key: + description: SSH public key + returned: success + type: str + sample: "ssh-rsa AA... someother@example.com" +''' + +from ansible.module_utils.basic import AnsibleModule +from ..module_utils.vultr import ( + Vultr, + vultr_argument_spec, +) + + +class AnsibleVultrSshKey(Vultr): + + def __init__(self, module): + super(AnsibleVultrSshKey, self).__init__(module, "vultr_ssh_key") + + self.returns = { + 'SSHKEYID': dict(key='id'), + 'name': dict(), + 'ssh_key': dict(), + 'date_created': dict(), + } + + def get_ssh_key(self): + ssh_keys = self.api_query(path="/v1/sshkey/list") + if ssh_keys: + for ssh_key_id, ssh_key_data in ssh_keys.items(): + if ssh_key_data.get('name') == self.module.params.get('name'): + return ssh_key_data + return {} + + def present_ssh_key(self): + ssh_key = self.get_ssh_key() + if not ssh_key: + ssh_key = self._create_ssh_key(ssh_key) + else: + ssh_key = self._update_ssh_key(ssh_key) + return ssh_key + + def _create_ssh_key(self, ssh_key): + self.result['changed'] = True + data = { + 'name': self.module.params.get('name'), + 'ssh_key': self.module.params.get('ssh_key'), + } + self.result['diff']['before'] = {} + self.result['diff']['after'] = data + + if not self.module.check_mode: + self.api_query( + path="/v1/sshkey/create", + method="POST", + data=data + ) + ssh_key = self.get_ssh_key() + return ssh_key + + def _update_ssh_key(self, ssh_key): + param_ssh_key = self.module.params.get('ssh_key') + if param_ssh_key != ssh_key['ssh_key']: + self.result['changed'] = True + + data = { + 'SSHKEYID': ssh_key['SSHKEYID'], + 'ssh_key': param_ssh_key, + } + + self.result['diff']['before'] = ssh_key + self.result['diff']['after'] = data + self.result['diff']['after'].update({'date_created': ssh_key['date_created']}) + + if not self.module.check_mode: + self.api_query( + path="/v1/sshkey/update", + method="POST", + data=data + ) + ssh_key = self.get_ssh_key() + return ssh_key + + def absent_ssh_key(self): + ssh_key = self.get_ssh_key() + if ssh_key: + self.result['changed'] = True + + data = { + 'SSHKEYID': ssh_key['SSHKEYID'], + } + + self.result['diff']['before'] = ssh_key + self.result['diff']['after'] = {} + + if not self.module.check_mode: + self.api_query( + path="/v1/sshkey/destroy", + method="POST", + data=data + ) + return ssh_key + + +def main(): + argument_spec = vultr_argument_spec() + argument_spec.update(dict( + name=dict(type='str', required=True), + ssh_key=dict(type='str', no_log=False), + state=dict(type='str', choices=['present', 'absent'], default='present'), + )) + + module = AnsibleModule( + argument_spec=argument_spec, + required_if=[ + ('state', 'present', ['ssh_key']), + ], + supports_check_mode=True, + ) + + vultr_ssh_key = AnsibleVultrSshKey(module) + if module.params.get('state') == "absent": + ssh_key = vultr_ssh_key.absent_ssh_key() + else: + ssh_key = vultr_ssh_key.present_ssh_key() + + result = vultr_ssh_key.get_result(ssh_key) + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/ngine_io/vultr/plugins/modules/vultr_ssh_key_info.py b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_ssh_key_info.py new file mode 100644 index 000000000..51b2960bb --- /dev/null +++ b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_ssh_key_info.py @@ -0,0 +1,141 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2018, Yanis Guenane +# Copyright (c) 2019, René Moser + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: vultr_ssh_key_info +short_description: Get information about the Vultr SSH keys available. +description: + - Get infos about SSH keys available. +version_added: "0.1.0" +author: + - "Yanis Guenane (@Spredzy)" + - "René Moser (@resmo)" +extends_documentation_fragment: +- ngine_io.vultr.vultr + +''' + +EXAMPLES = r''' +- name: Get Vultr SSH keys infos + ngine_io.vultr.vultr_ssh_key_info: + register: result + +- name: Print the infos + debug: + var: result.vultr_ssh_key_info +''' + +RETURN = r''' +--- +vultr_api: + description: Response from Vultr API with a few additions/modification + returned: success + type: complex + contains: + api_account: + description: Account used in the ini file to select the key + returned: success + type: str + sample: default + api_timeout: + description: Timeout used for the API requests + returned: success + type: int + sample: 60 + api_retries: + description: Amount of max retries for the API requests + returned: success + type: int + sample: 5 + api_retry_max_delay: + description: Exponential backoff delay in seconds between retries up to this max delay value. + returned: success + type: int + sample: 12 + api_endpoint: + description: Endpoint used for the API requests + returned: success + type: str + sample: "https://api.vultr.com" +vultr_ssh_key_info: + description: Response from Vultr API as list + returned: success + type: complex + contains: + id: + description: ID of the ssh key + returned: success + type: str + sample: 5904bc6ed9234 + name: + description: Name of the ssh key + returned: success + type: str + sample: my ssh key + date_created: + description: Date the ssh key was created + returned: success + type: str + sample: "2017-08-26 12:47:48" + ssh_key: + description: SSH public key + returned: success + type: str + sample: "ssh-rsa AA... someother@example.com" +''' + +from ansible.module_utils.basic import AnsibleModule +from ..module_utils.vultr import ( + Vultr, + vultr_argument_spec, +) + + +class AnsibleVultrSSHKeyInfo(Vultr): + + def __init__(self, module): + super(AnsibleVultrSSHKeyInfo, self).__init__(module, "vultr_ssh_key_info") + + self.returns = { + 'SSHKEYID': dict(key='id'), + 'name': dict(), + 'ssh_key': dict(), + 'date_created': dict(), + } + + def get_sshkeys(self): + return self.api_query(path="/v1/sshkey/list") + + +def parse_keys_list(keys_list): + if not keys_list: + return [] + + return [key for id, key in keys_list.items()] + + +def main(): + argument_spec = vultr_argument_spec() + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + sshkey_info = AnsibleVultrSSHKeyInfo(module) + result = sshkey_info.get_result(parse_keys_list(sshkey_info.get_sshkeys())) + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/ngine_io/vultr/plugins/modules/vultr_startup_script.py b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_startup_script.py new file mode 100644 index 000000000..dfa58af77 --- /dev/null +++ b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_startup_script.py @@ -0,0 +1,265 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2018, René Moser +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: vultr_startup_script +short_description: Manages startup scripts on Vultr. +description: + - Create, update and remove startup scripts. +version_added: "0.1.0" +author: "René Moser (@resmo)" +options: + name: + description: + - The script name. + required: true + type: str + script_type: + description: + - The script type, can not be changed once created. + default: boot + choices: [ boot, pxe ] + aliases: [ type ] + type: str + script: + description: + - The script source code. + - Required if I(state=present). + type: str + state: + description: + - State of the script. + default: present + choices: [ present, absent ] + type: str +extends_documentation_fragment: +- ngine_io.vultr.vultr + +''' + +EXAMPLES = r''' +- name: ensure a pxe script exists, source from a file + ngine_io.vultr.vultr_startup_script: + name: my_web_script + script_type: pxe + script: "{{ lookup('file', 'path/to/script') }}" + +- name: ensure a boot script exists + ngine_io.vultr.vultr_startup_script: + name: vultr_startup_script + script: "#!/bin/bash\necho Hello World > /root/hello" + +- name: ensure a script is absent + ngine_io.vultr.vultr_startup_script: + name: my_web_script + state: absent +''' + +RETURN = r''' +--- +vultr_api: + description: Response from Vultr API with a few additions/modification + returned: success + type: complex + contains: + api_account: + description: Account used in the ini file to select the key + returned: success + type: str + sample: default + api_timeout: + description: Timeout used for the API requests + returned: success + type: int + sample: 60 + api_retries: + description: Amount of max retries for the API requests + returned: success + type: int + sample: 5 + api_retry_max_delay: + description: Exponential backoff delay in seconds between retries up to this max delay value. + returned: success + type: int + sample: 12 + api_endpoint: + description: Endpoint used for the API requests + returned: success + type: str + sample: "https://api.vultr.com" +vultr_startup_script: + description: Response from Vultr API + returned: success + type: complex + contains: + id: + description: ID of the startup script. + returned: success + type: str + sample: 249395 + name: + description: Name of the startup script. + returned: success + type: str + sample: my startup script + script: + description: The source code of the startup script. + returned: success + type: str + sample: "#!/bin/bash\necho Hello World > /root/hello" + script_type: + description: The type of the startup script. + returned: success + type: str + sample: pxe + date_created: + description: Date the startup script was created. + returned: success + type: str + sample: "2017-08-26 12:47:48" + date_modified: + description: Date the startup script was modified. + returned: success + type: str + sample: "2017-08-26 12:47:48" +''' + +from ansible.module_utils.basic import AnsibleModule +from ..module_utils.vultr import ( + Vultr, + vultr_argument_spec, +) + + +class AnsibleVultrStartupScript(Vultr): + + def __init__(self, module): + super(AnsibleVultrStartupScript, self).__init__(module, "vultr_startup_script") + + self.returns = { + 'SCRIPTID': dict(key='id'), + 'type': dict(key='script_type'), + 'name': dict(), + 'script': dict(), + 'date_created': dict(), + 'date_modified': dict(), + } + + def get_script(self): + scripts = self.api_query(path="/v1/startupscript/list") + name = self.module.params.get('name') + if scripts: + for script_id, script_data in scripts.items(): + if script_data.get('name') == name: + return script_data + return {} + + def present_script(self): + script = self.get_script() + if not script: + script = self._create_script(script) + else: + script = self._update_script(script) + return script + + def _create_script(self, script): + self.result['changed'] = True + + data = { + 'name': self.module.params.get('name'), + 'script': self.module.params.get('script'), + 'type': self.module.params.get('script_type'), + } + + self.result['diff']['before'] = {} + self.result['diff']['after'] = data + + if not self.module.check_mode: + self.api_query( + path="/v1/startupscript/create", + method="POST", + data=data + ) + script = self.get_script() + return script + + def _update_script(self, script): + if script['script'] != self.module.params.get('script'): + self.result['changed'] = True + + data = { + 'SCRIPTID': script['SCRIPTID'], + 'script': self.module.params.get('script'), + } + + self.result['diff']['before'] = script + self.result['diff']['after'] = script.copy() + self.result['diff']['after'].update(data) + + if not self.module.check_mode: + self.api_query( + path="/v1/startupscript/update", + method="POST", + data=data + ) + script = self.get_script() + return script + + def absent_script(self): + script = self.get_script() + if script: + self.result['changed'] = True + + data = { + 'SCRIPTID': script['SCRIPTID'], + } + + self.result['diff']['before'] = script + self.result['diff']['after'] = {} + + if not self.module.check_mode: + self.api_query( + path="/v1/startupscript/destroy", + method="POST", + data=data + ) + return script + + +def main(): + argument_spec = vultr_argument_spec() + argument_spec.update(dict( + name=dict(type='str', required=True), + script=dict(type='str',), + script_type=dict(type='str', default='boot', choices=['boot', 'pxe'], aliases=['type']), + state=dict(type='str', choices=['present', 'absent'], default='present'), + )) + + module = AnsibleModule( + argument_spec=argument_spec, + required_if=[ + ('state', 'present', ['script']), + ], + supports_check_mode=True, + ) + + vultr_script = AnsibleVultrStartupScript(module) + if module.params.get('state') == "absent": + script = vultr_script.absent_script() + else: + script = vultr_script.present_script() + + result = vultr_script.get_result(script) + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/ngine_io/vultr/plugins/modules/vultr_startup_script_info.py b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_startup_script_info.py new file mode 100644 index 000000000..262954ad3 --- /dev/null +++ b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_startup_script_info.py @@ -0,0 +1,149 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2018, Yanis Guenane +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: vultr_startup_script_info +short_description: Gather information about the Vultr startup scripts available. +description: + - Gather information about vultr_startup_scripts available. +version_added: "0.1.0" +author: "Yanis Guenane (@Spredzy)" +extends_documentation_fragment: +- ngine_io.vultr.vultr + +''' + +EXAMPLES = r''' +- name: Gather Vultr startup scripts information + ngine_io.vultr.vultr_startup_script_info: + register: result + +- name: Print the gathered information + debug: + var: result.vultr_startup_script_info +''' + +RETURN = r''' +--- +vultr_api: + description: Response from Vultr API with a few additions/modification + returned: success + type: complex + contains: + api_account: + description: Account used in the ini file to select the key + returned: success + type: str + sample: default + api_timeout: + description: Timeout used for the API requests + returned: success + type: int + sample: 60 + api_retries: + description: Amount of max retries for the API requests + returned: success + type: int + sample: 5 + api_retry_max_delay: + description: Exponential backoff delay in seconds between retries up to this max delay value. + returned: success + type: int + sample: 12 + api_endpoint: + description: Endpoint used for the API requests + returned: success + type: str + sample: "https://api.vultr.com" +vultr_startup_script_info: + description: Response from Vultr API + returned: success + type: complex + contains: + id: + description: ID of the startup script. + returned: success + type: str + sample: 249395 + name: + description: Name of the startup script. + returned: success + type: str + sample: my startup script + script: + description: The source code of the startup script. + returned: success + type: str + sample: "#!/bin/bash\necho Hello World > /root/hello" + type: + description: The type of the startup script. + returned: success + type: str + sample: pxe + date_created: + description: Date the startup script was created. + returned: success + type: str + sample: "2017-08-26 12:47:48" + date_modified: + description: Date the startup script was modified. + returned: success + type: str + sample: "2017-08-26 12:47:48" +''' + +from ansible.module_utils.basic import AnsibleModule +from ..module_utils.vultr import ( + Vultr, + vultr_argument_spec, +) + + +class AnsibleVultrStartupScriptInfo(Vultr): + + def __init__(self, module): + super(AnsibleVultrStartupScriptInfo, self).__init__(module, "vultr_startup_script_info") + + self.returns = { + "SCRIPTID": dict(key='id', convert_to='int'), + "date_created": dict(), + "date_modified": dict(), + "name": dict(), + "script": dict(), + "type": dict(), + } + + def get_startupscripts(self): + return self.api_query(path="/v1/startupscript/list") + + +def parse_startupscript_list(startupscipts_list): + if not startupscipts_list: + return [] + + return [startupscript for id, startupscript in startupscipts_list.items()] + + +def main(): + argument_spec = vultr_argument_spec() + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + startupscript_info = AnsibleVultrStartupScriptInfo(module) + result = startupscript_info.get_result(parse_startupscript_list(startupscript_info.get_startupscripts())) + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/ngine_io/vultr/plugins/modules/vultr_user.py b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_user.py new file mode 100644 index 000000000..53ebfeacc --- /dev/null +++ b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_user.py @@ -0,0 +1,326 @@ +#!/usr/bin/python +# -*- 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) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: vultr_user +short_description: Manages users on Vultr. +description: + - Create, update and remove users. +version_added: "0.1.0" +author: "René Moser (@resmo)" +options: + name: + description: + - Name of the user + required: true + type: str + email: + description: + - Email of the user. + - Required if C(state=present). + type: str + password: + description: + - Password of the user. + - Only considered while creating a user or when C(force=yes). + type: str + force: + description: + - Password will only be changed with enforcement. + default: no + type: bool + api_enabled: + description: + - Whether the API is enabled or not. + default: yes + type: bool + acls: + description: + - List of ACLs this users should have, see U(https://www.vultr.com/api/#user_user_list). + - Required if C(state=present). + - One or more of the choices list, some depend on each other. + choices: + - manage_users + - subscriptions + - provisioning + - billing + - support + - abuse + - dns + - upgrade + aliases: [ acl ] + type: list + elements: str + state: + description: + - State of the user. + default: present + choices: [ present, absent ] + type: str +extends_documentation_fragment: +- ngine_io.vultr.vultr + +''' + +EXAMPLES = r''' +- name: Ensure a user exists + ngine_io.vultr.vultr_user: + name: john + email: john.doe@example.com + password: s3cr3t + acls: + - upgrade + - dns + - manage_users + - subscriptions + - upgrade + +- name: Remove a user + ngine_io.vultr.vultr_user: + name: john + state: absent +''' + +RETURN = r''' +--- +vultr_api: + description: Response from Vultr API with a few additions/modification + returned: success + type: complex + contains: + api_account: + description: Account used in the ini file to select the key + returned: success + type: str + sample: default + api_timeout: + description: Timeout used for the API requests + returned: success + type: int + sample: 60 + api_retries: + description: Amount of max retries for the API requests + returned: success + type: int + sample: 5 + api_retry_max_delay: + description: Exponential backoff delay in seconds between retries up to this max delay value. + returned: success + type: int + sample: 12 + api_endpoint: + description: Endpoint used for the API requests + returned: success + type: str + sample: "https://api.vultr.com" +vultr_user: + description: Response from Vultr API + returned: success + type: complex + contains: + id: + description: ID of the user. + returned: success + type: str + sample: 5904bc6ed9234 + api_key: + description: API key of the user. + returned: only after resource was created + type: str + sample: 567E6K567E6K567E6K567E6K567E6K + name: + description: Name of the user. + returned: success + type: str + sample: john + email: + description: Email of the user. + returned: success + type: str + sample: "john@example.com" + api_enabled: + description: Whether the API is enabled or not. + returned: success + type: bool + sample: true + acls: + description: List of ACLs of the user. + returned: success + type: list + sample: [manage_users, support, upgrade] +''' + +from ansible.module_utils.basic import AnsibleModule +from ..module_utils.vultr import ( + Vultr, + vultr_argument_spec, +) + + +ACLS = [ + 'manage_users', + 'subscriptions', + 'provisioning', + 'billing', + 'support', + 'abuse', + 'dns', + 'upgrade', +] + + +class AnsibleVultrUser(Vultr): + + def __init__(self, module): + super(AnsibleVultrUser, self).__init__(module, "vultr_user") + + self.returns = { + 'USERID': dict(key='id'), + 'name': dict(), + 'email': dict(), + 'api_enabled': dict(convert_to='bool'), + 'acls': dict(), + 'api_key': dict() + } + + def _common_args(self): + return { + 'name': self.module.params.get('name'), + 'email': self.module.params.get('email'), + 'acls': self.module.params.get('acls'), + 'password': self.module.params.get('password'), + 'api_enabled': self.get_yes_or_no('api_enabled'), + } + + def get_user(self): + users = self.api_query(path="/v1/user/list") + for user in users or []: + if user.get('name') == self.module.params.get('name'): + return user + return {} + + def present_user(self): + user = self.get_user() + if not user: + user = self._create_user(user) + else: + user = self._update_user(user) + return user + + def _has_changed(self, user, data): + for k, v in data.items(): + if k not in user: + continue + elif isinstance(v, list): + for i in v: + if i not in user[k]: + return True + elif data[k] != user[k]: + return True + return False + + def _create_user(self, user): + self.module.fail_on_missing_params(required_params=['password']) + + self.result['changed'] = True + + data = self._common_args() + self.result['diff']['before'] = {} + self.result['diff']['after'] = data + + if not self.module.check_mode: + user = self.api_query( + path="/v1/user/create", + method="POST", + data=data + ) + user.update(self.get_user()) + return user + + def _update_user(self, user): + data = self._common_args() + data.update({ + 'USERID': user['USERID'], + }) + + force = self.module.params.get('force') + if not force: + del data['password'] + + if force or self._has_changed(user=user, data=data): + self.result['changed'] = True + + self.result['diff']['before'] = user + self.result['diff']['after'] = user.copy() + self.result['diff']['after'].update(data) + + if not self.module.check_mode: + self.api_query( + path="/v1/user/update", + method="POST", + data=data + ) + user = self.get_user() + return user + + def absent_user(self): + user = self.get_user() + if user: + self.result['changed'] = True + + data = { + 'USERID': user['USERID'], + } + + self.result['diff']['before'] = user + self.result['diff']['after'] = {} + + if not self.module.check_mode: + self.api_query( + path="/v1/user/delete", + method="POST", + data=data + ) + return user + + +def main(): + argument_spec = vultr_argument_spec() + argument_spec.update(dict( + name=dict(type='str', required=True), + email=dict(type='str',), + password=dict(type='str', no_log=True), + force=dict(type='bool', default=False), + api_enabled=dict(type='bool', default=True), + acls=dict(type='list', elements='str', choices=ACLS, aliases=['acl']), + state=dict(type='str', choices=['present', 'absent'], default='present'), + )) + + module = AnsibleModule( + argument_spec=argument_spec, + required_if=[ + ('state', 'present', ['email', 'acls']), + ], + supports_check_mode=True, + ) + + vultr_user = AnsibleVultrUser(module) + if module.params.get('state') == "absent": + user = vultr_user.absent_user() + else: + user = vultr_user.present_user() + + result = vultr_user.get_result(user) + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/ngine_io/vultr/plugins/modules/vultr_user_info.py b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_user_info.py new file mode 100644 index 000000000..f07d0efff --- /dev/null +++ b/ansible_collections/ngine_io/vultr/plugins/modules/vultr_user_info.py @@ -0,0 +1,144 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2018, Yanis Guenane +# Copyright (c) 2019, René Moser +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: vultr_user_info +short_description: Get information about the Vultr user available. +version_added: "0.1.0" +description: + - Get infos about users available in Vultr. +author: + - "Yanis Guenane (@Spredzy)" + - "René Moser (@resmo)" +extends_documentation_fragment: +- ngine_io.vultr.vultr + +''' + +EXAMPLES = r''' +- name: Get Vultr user infos + ngine_io.vultr.vultr_user_info: + register: result + +- name: Print the infos + debug: + var: result.vultr_user_info +''' + +RETURN = r''' +--- +vultr_api: + description: Response from Vultr API with a few additions/modification + returned: success + type: complex + contains: + api_account: + description: Account used in the ini file to select the key + returned: success + type: str + sample: default + api_timeout: + description: Timeout used for the API requests + returned: success + type: int + sample: 60 + api_retries: + description: Amount of max retries for the API requests + returned: success + type: int + sample: 5 + api_retry_max_delay: + description: Exponential backoff delay in seconds between retries up to this max delay value. + returned: success + type: int + sample: 12 + api_endpoint: + description: Endpoint used for the API requests + returned: success + type: str + sample: "https://api.vultr.com" +vultr_user_info: + description: Response from Vultr API as list + returned: available + type: complex + contains: + id: + description: ID of the user. + returned: success + type: str + sample: 5904bc6ed9234 + api_key: + description: API key of the user. + returned: only after resource was created + type: str + sample: 567E6K567E6K567E6K567E6K567E6K + name: + description: Name of the user. + returned: success + type: str + sample: john + email: + description: Email of the user. + returned: success + type: str + sample: "john@example.com" + api_enabled: + description: Whether the API is enabled or not. + returned: success + type: bool + sample: true + acls: + description: List of ACLs of the user. + returned: success + type: list + sample: [ manage_users, support, upgrade ] +''' + +from ansible.module_utils.basic import AnsibleModule +from ..module_utils.vultr import ( + Vultr, + vultr_argument_spec, +) + + +class AnsibleVultrUserInfo(Vultr): + + def __init__(self, module): + super(AnsibleVultrUserInfo, self).__init__(module, "vultr_user_info") + + self.returns = { + "USERID": dict(key='id'), + "acls": dict(), + "api_enabled": dict(), + "email": dict(), + "name": dict() + } + + def get_regions(self): + return self.api_query(path="/v1/user/list") + + +def main(): + argument_spec = vultr_argument_spec() + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + user_info = AnsibleVultrUserInfo(module) + result = user_info.get_result(user_info.get_regions()) + module.exit_json(**result) + + +if __name__ == '__main__': + main() -- cgit v1.2.3