summaryrefslogtreecommitdiffstats
path: root/ansible_collections/community/digitalocean/plugins
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 16:03:42 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 16:03:42 +0000
commit66cec45960ce1d9c794e9399de15c138acb18aed (patch)
tree59cd19d69e9d56b7989b080da7c20ef1a3fe2a5a /ansible_collections/community/digitalocean/plugins
parentInitial commit. (diff)
downloadansible-upstream.tar.xz
ansible-upstream.zip
Adding upstream version 7.3.0+dfsg.upstream/7.3.0+dfsgupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/community/digitalocean/plugins')
-rw-r--r--ansible_collections/community/digitalocean/plugins/doc_fragments/digital_ocean.py39
-rw-r--r--ansible_collections/community/digitalocean/plugins/inventory/digitalocean.py274
-rw-r--r--ansible_collections/community/digitalocean/plugins/module_utils/digital_ocean.py305
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean.py525
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_account_facts.py93
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_account_info.py93
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_balance_info.py73
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_block_storage.py411
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_cdn_endpoints.py256
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_cdn_endpoints_info.py93
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_certificate.py181
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_certificate_facts.py126
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_certificate_info.py126
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_database.py437
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_database_info.py214
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_domain.py325
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_domain_facts.py152
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_domain_info.py152
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_domain_record.py508
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_domain_record_info.py227
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_droplet.py918
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_droplet_info.py266
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_firewall.py560
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_firewall_facts.py143
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_firewall_info.py143
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_floating_ip.py519
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_floating_ip_facts.py136
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_floating_ip_info.py136
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_image_facts.py160
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_image_info.py160
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_kubernetes.py493
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_kubernetes_info.py234
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_load_balancer.py881
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_load_balancer_facts.py128
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_load_balancer_info.py128
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_monitoring_alerts.py325
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_monitoring_alerts_info.py155
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_project.py315
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_project_info.py121
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_region_facts.py125
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_region_info.py125
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_size_facts.py124
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_size_info.py124
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_snapshot.py309
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_snapshot_facts.py180
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_snapshot_info.py180
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_spaces.py241
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_spaces_info.py160
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_sshkey.py282
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_sshkey_facts.py107
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_sshkey_info.py107
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_tag.py219
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_tag_facts.py126
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_tag_info.py126
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_volume_facts.py149
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_volume_info.py149
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_vpc.py284
-rw-r--r--ansible_collections/community/digitalocean/plugins/modules/digital_ocean_vpc_info.py161
58 files changed, 13809 insertions, 0 deletions
diff --git a/ansible_collections/community/digitalocean/plugins/doc_fragments/digital_ocean.py b/ansible_collections/community/digitalocean/plugins/doc_fragments/digital_ocean.py
new file mode 100644
index 00000000..bc65ad38
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/doc_fragments/digital_ocean.py
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2018, Ansible Project
+# Copyright: (c) 2018, Abhijeet Kasurde (akasurde@redhat.com)
+# 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):
+ # Parameters for DigitalOcean modules
+ DOCUMENTATION = r"""
+options:
+ baseurl:
+ description:
+ - DigitalOcean API base url.
+ type: str
+ default: https://api.digitalocean.com/v2
+ oauth_token:
+ description:
+ - DigitalOcean OAuth token.
+ - "There are several other environment variables which can be used to provide this value."
+ - "i.e., - 'DO_API_TOKEN', 'DO_API_KEY', 'DO_OAUTH_TOKEN' and 'OAUTH_TOKEN'"
+ type: str
+ aliases: [ api_token ]
+ timeout:
+ description:
+ - The timeout in seconds used for polling DigitalOcean's API.
+ type: int
+ default: 30
+ validate_certs:
+ description:
+ - If set to C(no), the SSL certificates will not be validated.
+ - This should only set to C(no) used on personally controlled sites using self-signed certificates.
+ type: bool
+ default: yes
+"""
diff --git a/ansible_collections/community/digitalocean/plugins/inventory/digitalocean.py b/ansible_collections/community/digitalocean/plugins/inventory/digitalocean.py
new file mode 100644
index 00000000..6aafb5f4
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/inventory/digitalocean.py
@@ -0,0 +1,274 @@
+# -*- coding: utf-8 -*-
+
+# Copyright: (c), Ansible Project
+# 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: digitalocean
+author:
+ - Janos Gerzson (@grzs)
+ - Tadej Borovšak (@tadeboro)
+ - Max Truxa (@maxtruxa)
+short_description: DigitalOcean Inventory Plugin
+version_added: "1.1.0"
+description:
+ - DigitalOcean (DO) inventory plugin.
+ - Acquires droplet list from DO API.
+ - Uses configuration file that ends with '(do_hosts|digitalocean|digital_ocean).(yaml|yml)'.
+extends_documentation_fragment:
+ - community.digitalocean.digital_ocean.documentation
+ - constructed
+ - inventory_cache
+options:
+ plugin:
+ description:
+ - The name of the DigitalOcean Inventory Plugin,
+ this should always be C(community.digitalocean.digitalocean).
+ required: true
+ choices: ['community.digitalocean.digitalocean']
+ api_token:
+ description:
+ - DigitalOcean OAuth token.
+ - Template expressions can be used in this field.
+ required: true
+ type: str
+ aliases: [ oauth_token ]
+ env:
+ - name: DO_API_TOKEN
+ attributes:
+ description: >-
+ Droplet attributes to add as host vars to each inventory host.
+ Check out the DO API docs for full list of attributes at
+ U(https://docs.digitalocean.com/reference/api/api-reference/#operation/list_all_droplets).
+ type: list
+ elements: str
+ default:
+ - id
+ - name
+ - networks
+ - region
+ - size_slug
+ var_prefix:
+ description:
+ - Prefix of generated varible names (e.g. C(tags) -> C(do_tags))
+ type: str
+ default: 'do_'
+ pagination:
+ description:
+ - Maximum droplet objects per response page.
+ - If the number of droplets related to the account exceeds this value,
+ the query will be broken to multiple requests (pages).
+ - DigitalOcean currently allows a maximum of 200.
+ type: int
+ default: 200
+ filters:
+ description:
+ - Filter hosts with Jinja templates.
+ - If no filters are specified, all hosts are added to the inventory.
+ type: list
+ elements: str
+ default: []
+ version_added: '1.5.0'
+"""
+
+EXAMPLES = r"""
+# Using keyed groups and compose for hostvars
+plugin: community.digitalocean.digitalocean
+api_token: '{{ lookup("pipe", "./get-do-token.sh" }}'
+attributes:
+ - id
+ - name
+ - memory
+ - vcpus
+ - disk
+ - size
+ - image
+ - networks
+ - volume_ids
+ - tags
+ - region
+keyed_groups:
+ - key: do_region.slug
+ prefix: 'region'
+ separator: '_'
+ - key: do_tags | lower
+ prefix: ''
+ separator: ''
+compose:
+ ansible_host: do_networks.v4 | selectattr('type','eq','public')
+ | map(attribute='ip_address') | first
+ class: do_size.description | lower
+ distro: do_image.distribution | lower
+filters:
+ - '"kubernetes" in do_tags'
+ - 'do_region.slug == "fra1"'
+"""
+
+import re
+import json
+from ansible.errors import AnsibleError, AnsibleParserError
+from ansible.inventory.group import to_safe_group_name
+from ansible.module_utils._text import to_native
+from ansible.module_utils.urls import Request
+from ansible.module_utils.six.moves.urllib.error import URLError, HTTPError
+from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable
+
+
+class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
+
+ NAME = "community.digitalocean.digitalocean"
+
+ # Constructable methods use the following function to construct group names. By
+ # default, characters that are not valid in python variables, are always replaced by
+ # underscores. We are overriding this with a function that respects the
+ # TRANSFORM_INVALID_GROUP_CHARS configuration option and allows users to control the
+ # behavior.
+ _sanitize_group_name = staticmethod(to_safe_group_name)
+
+ def verify_file(self, path):
+ valid = False
+ if super(InventoryModule, self).verify_file(path):
+ if path.endswith(
+ (
+ "do_hosts.yaml",
+ "do_hosts.yml",
+ "digitalocean.yaml",
+ "digitalocean.yml",
+ "digital_ocean.yaml",
+ "digital_ocean.yml",
+ )
+ ):
+ valid = True
+ else:
+ self.display.vvv(
+ "Skipping due to inventory source file name mismatch. "
+ "The file name has to end with one of the following: "
+ "do_hosts.yaml, do_hosts.yml "
+ "digitalocean.yaml, digitalocean.yml, "
+ "digital_ocean.yaml, digital_ocean.yml."
+ )
+ return valid
+
+ def _template_option(self, option):
+ value = self.get_option(option)
+ self.templar.available_variables = {}
+ return self.templar.template(value)
+
+ def _get_payload(self):
+ # request parameters
+ api_token = self._template_option("api_token")
+ headers = {
+ "Content-Type": "application/json",
+ "Authorization": "Bearer {0}".format(api_token),
+ }
+
+ # build url
+ pagination = self.get_option("pagination")
+ url = "https://api.digitalocean.com/v2"
+ if self.get_option("baseurl"):
+ url = self.get_option("baseurl")
+ url += "/droplets?per_page=" + str(pagination)
+
+ # send request(s)
+ self.req = Request(headers=headers, timeout=self.get_option("timeout"))
+ payload = []
+ try:
+ while url:
+ self.display.vvv("Sending request to {0}".format(url))
+ response = json.load(self.req.get(url))
+ payload.extend(response["droplets"])
+ url = response.get("links", {}).get("pages", {}).get("next")
+ except ValueError:
+ raise AnsibleParserError("something went wrong with JSON loading")
+ except (URLError, HTTPError) as error:
+ raise AnsibleParserError(error)
+
+ return payload
+
+ def _populate(self, records):
+ attributes = self.get_option("attributes")
+ var_prefix = self.get_option("var_prefix")
+ strict = self.get_option("strict")
+ host_filters = self.get_option("filters")
+ for record in records:
+
+ host_name = record.get("name")
+ if not host_name:
+ continue
+
+ host_vars = {}
+ for k, v in record.items():
+ if k in attributes:
+ host_vars[var_prefix + k] = v
+
+ if not self._passes_filters(host_filters, host_vars, host_name, strict):
+ self.display.vvv("Host {0} did not pass all filters".format(host_name))
+ continue
+
+ # add host to inventory
+ self.inventory.add_host(host_name)
+
+ # set variables for host
+ for k, v in host_vars.items():
+ self.inventory.set_variable(host_name, k, v)
+
+ self._set_composite_vars(
+ self.get_option("compose"),
+ self.inventory.get_host(host_name).get_vars(),
+ host_name,
+ strict,
+ )
+
+ # set composed and keyed groups
+ self._add_host_to_composed_groups(
+ self.get_option("groups"), dict(), host_name, strict
+ )
+ self._add_host_to_keyed_groups(
+ self.get_option("keyed_groups"), dict(), host_name, strict
+ )
+
+ def _passes_filters(self, filters, variables, host, strict=False):
+ if filters and isinstance(filters, list):
+ for template in filters:
+ try:
+ if not self._compose(template, variables):
+ return False
+ except Exception as e:
+ if strict:
+ raise AnsibleError(
+ "Could not evaluate host filter {0} for host {1}: {2}".format(
+ template, host, to_native(e)
+ )
+ )
+ # Better be safe and not include any hosts by accident.
+ return False
+ return True
+
+ def parse(self, inventory, loader, path, cache=True):
+ super(InventoryModule, self).parse(inventory, loader, path)
+
+ self._read_config_data(path)
+
+ # cache settings
+ cache_key = self.get_cache_key(path)
+ use_cache = self.get_option("cache") and cache
+ update_cache = self.get_option("cache") and not cache
+
+ records = None
+ if use_cache:
+ try:
+ records = self._cache[cache_key]
+ except KeyError:
+ update_cache = True
+
+ if records is None:
+ records = self._get_payload()
+
+ if update_cache:
+ self._cache[cache_key] = records
+
+ self._populate(records)
diff --git a/ansible_collections/community/digitalocean/plugins/module_utils/digital_ocean.py b/ansible_collections/community/digitalocean/plugins/module_utils/digital_ocean.py
new file mode 100644
index 00000000..44ca3ccd
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/module_utils/digital_ocean.py
@@ -0,0 +1,305 @@
+# This code is part of Ansible, but is an independent component.
+# This particular file snippet, and this file snippet only, is BSD licensed.
+# Modules you write using this snippet, which is embedded dynamically by Ansible
+# still belong to the author of the module, and may assign their own license
+# to the complete work.
+#
+# Copyright (c), Ansible Project 2017
+# 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 json
+import os
+from ansible.module_utils.urls import fetch_url
+from ansible.module_utils._text import to_text
+from ansible.module_utils.basic import env_fallback
+
+
+class Response(object):
+ def __init__(self, resp, info):
+ self.body = None
+ if resp:
+ self.body = resp.read()
+ self.info = info
+
+ @property
+ def json(self):
+ if not self.body:
+ if "body" in self.info:
+ return json.loads(to_text(self.info["body"]))
+ return None
+ try:
+ return json.loads(to_text(self.body))
+ except ValueError:
+ return None
+
+ @property
+ def status_code(self):
+ return self.info["status"]
+
+
+class DigitalOceanHelper:
+ baseurl = "https://api.digitalocean.com/v2"
+
+ def __init__(self, module):
+ self.module = module
+ self.baseurl = module.params.get("baseurl", DigitalOceanHelper.baseurl)
+ self.timeout = module.params.get("timeout", 30)
+ self.oauth_token = module.params.get("oauth_token")
+ self.headers = {
+ "Authorization": "Bearer {0}".format(self.oauth_token),
+ "Content-type": "application/json",
+ }
+
+ # Check if api_token is valid or not
+ response = self.get("account")
+ if response.status_code == 401:
+ self.module.fail_json(
+ msg="Failed to login using API token, please verify validity of API token."
+ )
+
+ def _url_builder(self, path):
+ if path[0] == "/":
+ path = path[1:]
+ return "%s/%s" % (self.baseurl, path)
+
+ def send(self, method, path, data=None):
+ url = self._url_builder(path)
+ data = self.module.jsonify(data)
+
+ if method == "DELETE":
+ if data == "null":
+ data = None
+
+ resp, info = fetch_url(
+ self.module,
+ url,
+ data=data,
+ headers=self.headers,
+ method=method,
+ timeout=self.timeout,
+ )
+
+ return Response(resp, info)
+
+ def get(self, path, data=None):
+ return self.send("GET", path, data)
+
+ def put(self, path, data=None):
+ return self.send("PUT", path, data)
+
+ def post(self, path, data=None):
+ return self.send("POST", path, data)
+
+ def delete(self, path, data=None):
+ return self.send("DELETE", path, data)
+
+ @staticmethod
+ def digital_ocean_argument_spec():
+ return dict(
+ baseurl=dict(
+ type="str", required=False, default="https://api.digitalocean.com/v2"
+ ),
+ validate_certs=dict(type="bool", required=False, default=True),
+ oauth_token=dict(
+ no_log=True,
+ # Support environment variable for DigitalOcean OAuth Token
+ fallback=(
+ env_fallback,
+ ["DO_API_TOKEN", "DO_API_KEY", "DO_OAUTH_TOKEN", "OAUTH_TOKEN"],
+ ),
+ required=False,
+ aliases=["api_token"],
+ ),
+ timeout=dict(type="int", default=30),
+ )
+
+ def get_paginated_data(
+ self,
+ base_url=None,
+ data_key_name=None,
+ data_per_page=40,
+ expected_status_code=200,
+ ):
+ """
+ Function to get all paginated data from given URL
+ Args:
+ base_url: Base URL to get data from
+ data_key_name: Name of data key value
+ data_per_page: Number results per page (Default: 40)
+ expected_status_code: Expected returned code from DigitalOcean (Default: 200)
+ Returns: List of data
+
+ """
+ page = 1
+ has_next = True
+ ret_data = []
+ status_code = None
+ response = None
+ while has_next or status_code != expected_status_code:
+ required_url = "{0}page={1}&per_page={2}".format(
+ base_url, page, data_per_page
+ )
+ response = self.get(required_url)
+ status_code = response.status_code
+ # stop if any error during pagination
+ if status_code != expected_status_code:
+ break
+ page += 1
+ ret_data.extend(response.json[data_key_name])
+ try:
+ has_next = (
+ "pages" in response.json["links"]
+ and "next" in response.json["links"]["pages"]
+ )
+ except KeyError:
+ # There's a bug in the API docs: GET v2/cdn/endpoints doesn't return a "links" key
+ has_next = False
+
+ if status_code != expected_status_code:
+ msg = "Failed to fetch %s from %s" % (data_key_name, base_url)
+ if response:
+ msg += " due to error : %s" % response.json["message"]
+ self.module.fail_json(msg=msg)
+
+ return ret_data
+
+
+class DigitalOceanProjects:
+ def __init__(self, module, rest):
+ self.module = module
+ self.rest = rest
+ self.get_all_projects()
+
+ def get_all_projects(self):
+ """Fetches all projects."""
+ self.projects = self.rest.get_paginated_data(
+ base_url="projects?", data_key_name="projects"
+ )
+
+ def get_default(self):
+ """Fetches the default project.
+
+ Returns:
+ error_message -- project fetch error message (or "" if no error)
+ project -- project dictionary representation (or {} if error)
+ """
+ project = [
+ project for project in self.projects if project.get("is_default", False)
+ ]
+ if len(project) == 0:
+ return "Unexpected error; no default project found", {}
+ if len(project) > 1:
+ return "Unexpected error; more than one default project", {}
+ return "", project[0]
+
+ def get_by_id(self, id):
+ """Fetches the project with the given id.
+
+ Returns:
+ error_message -- project fetch error message (or "" if no error)
+ project -- project dictionary representation (or {} if error)
+ """
+ project = [project for project in self.projects if project.get("id") == id]
+ if len(project) == 0:
+ return "No project with id {0} found".format(id), {}
+ elif len(project) > 1:
+ return "Unexpected error; more than one project with the same id", {}
+ return "", project[0]
+
+ def get_by_name(self, name):
+ """Fetches the project with the given name.
+
+ Returns:
+ error_message -- project fetch error message (or "" if no error)
+ project -- project dictionary representation (or {} if error)
+ """
+ project = [project for project in self.projects if project.get("name") == name]
+ if len(project) == 0:
+ return "No project with name {0} found".format(name), {}
+ elif len(project) > 1:
+ return "Unexpected error; more than one project with the same name", {}
+ return "", project[0]
+
+ def assign_to_project(self, project_name, urn):
+ """Assign resource (urn) to project (name).
+
+ Keyword arguments:
+ project_name -- project name to associate the resource with
+ urn -- resource URN (has the form do:resource_type:resource_id)
+
+ Returns:
+ assign_status -- ok, not_found, assigned, already_assigned, service_down
+ error_message -- assignment error message (empty on success)
+ resources -- resources assigned (or {} if error)
+
+ Notes:
+ For URN examples, see https://docs.digitalocean.com/reference/api/api-reference/#tag/Project-Resources
+
+ Projects resources are identified by uniform resource names or URNs.
+ A valid URN has the following format: do:resource_type:resource_id.
+
+ The following resource types are supported:
+ Resource Type | Example URN
+ Database | do:dbaas:83c7a55f-0d84-4760-9245-aba076ec2fb2
+ Domain | do:domain:example.com
+ Droplet | do:droplet:4126873
+ Floating IP | do:floatingip:192.168.99.100
+ Load Balancer | do:loadbalancer:39052d89-8dd4-4d49-8d5a-3c3b6b365b5b
+ Space | do:space:my-website-assets
+ Volume | do:volume:6fc4c277-ea5c-448a-93cd-dd496cfef71f
+ """
+ error_message, project = self.get_by_name(project_name)
+ if not project:
+ return "", error_message, {}
+
+ project_id = project.get("id", None)
+ if not project_id:
+ return (
+ "",
+ "Unexpected error; cannot find project id for {0}".format(project_name),
+ {},
+ )
+
+ data = {"resources": [urn]}
+ response = self.rest.post(
+ "projects/{0}/resources".format(project_id), data=data
+ )
+ status_code = response.status_code
+ json = response.json
+ if status_code != 200:
+ message = json.get("message", "No error message returned")
+ return (
+ "",
+ "Unable to assign resource {0} to project {1} [HTTP {2}: {3}]".format(
+ urn, project_name, status_code, message
+ ),
+ {},
+ )
+
+ resources = json.get("resources", [])
+ if len(resources) == 0:
+ return (
+ "",
+ "Unexpected error; no resources returned (but assignment was successful)",
+ {},
+ )
+ if len(resources) > 1:
+ return (
+ "",
+ "Unexpected error; more than one resource returned (but assignment was successful)",
+ {},
+ )
+
+ status = resources[0].get(
+ "status",
+ "Unexpected error; no status returned (but assignment was successful)",
+ )
+ return (
+ status,
+ "Assigned {0} to project {1}".format(urn, project_name),
+ resources[0],
+ )
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean.py
new file mode 100644
index 00000000..1c33f2c7
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean.py
@@ -0,0 +1,525 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright: Ansible Project
+# 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: digital_ocean
+short_description: Create/delete a droplet/SSH_key in DigitalOcean
+deprecated:
+ removed_in: 2.0.0 # was Ansible 2.12
+ why: Updated module to remove external dependency with increased functionality.
+ alternative: Use M(community.digitalocean.digital_ocean_droplet) instead.
+description:
+ - Create/delete a droplet in DigitalOcean and optionally wait for it to be 'running', or deploy an SSH key.
+author: "Vincent Viallet (@zbal)"
+options:
+ command:
+ description:
+ - Which target you want to operate on.
+ default: droplet
+ choices: ['droplet', 'ssh']
+ type: str
+ state:
+ description:
+ - Indicate desired state of the target.
+ default: present
+ choices: ['present', 'active', 'absent', 'deleted']
+ type: str
+ api_token:
+ description:
+ - DigitalOcean api token.
+ type: str
+ aliases:
+ - API_TOKEN
+ id:
+ description:
+ - Numeric, the droplet id you want to operate on.
+ aliases: ['droplet_id']
+ type: int
+ name:
+ description:
+ - String, this is the name of the droplet - must be formatted by hostname rules, or the name of a SSH key.
+ type: str
+ unique_name:
+ description:
+ - Bool, require unique hostnames. By default, DigitalOcean allows multiple hosts with the same name. Setting this to "yes" allows only one host
+ per name. Useful for idempotence.
+ type: bool
+ default: 'no'
+ size_id:
+ description:
+ - This is the slug of the size you would like the droplet created with.
+ type: str
+ image_id:
+ description:
+ - This is the slug of the image you would like the droplet created with.
+ type: str
+ region_id:
+ description:
+ - This is the slug of the region you would like your server to be created in.
+ type: str
+ ssh_key_ids:
+ description:
+ - Optional, array of SSH key (numeric) ID that you would like to be added to the server.
+ type: list
+ elements: str
+ virtio:
+ description:
+ - "Bool, turn on virtio driver in droplet for improved network and storage I/O."
+ type: bool
+ default: 'yes'
+ private_networking:
+ description:
+ - "Bool, add an additional, private network interface to droplet for inter-droplet communication."
+ type: bool
+ default: 'no'
+ backups_enabled:
+ description:
+ - Optional, Boolean, enables backups for your droplet.
+ type: bool
+ default: 'no'
+ user_data:
+ description:
+ - opaque blob of data which is made available to the droplet
+ type: str
+ ipv6:
+ description:
+ - Optional, Boolean, enable IPv6 for your droplet.
+ type: bool
+ default: 'no'
+ wait:
+ description:
+ - Wait for the droplet to be in state 'running' before returning. If wait is "no" an ip_address may not be returned.
+ type: bool
+ default: 'yes'
+ wait_timeout:
+ description:
+ - How long before wait gives up, in seconds.
+ default: 300
+ type: int
+ ssh_pub_key:
+ description:
+ - The public SSH key you want to add to your account.
+ type: str
+
+notes:
+ - Two environment variables can be used, DO_API_KEY and DO_API_TOKEN. They both refer to the v2 token.
+ - As of Ansible 1.9.5 and 2.0, Version 2 of the DigitalOcean API is used, this removes C(client_id) and C(api_key) options in favor of C(api_token).
+ - If you are running Ansible 1.9.4 or earlier you might not be able to use the included version of this module as the API version used has been retired.
+ Upgrade Ansible or, if unable to, try downloading the latest version of this module from github and putting it into a 'library' directory.
+requirements:
+ - "python >= 2.6"
+ - dopy
+"""
+
+
+EXAMPLES = r"""
+# Ensure a SSH key is present
+# If a key matches this name, will return the ssh key id and changed = False
+# If no existing key matches this name, a new key is created, the ssh key id is returned and changed = False
+
+- name: Ensure a SSH key is present
+ community.digitalocean.digital_ocean:
+ state: present
+ oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
+ command: ssh
+ name: my_ssh_key
+ ssh_pub_key: 'ssh-rsa AAAA...'
+
+# Will return the droplet details including the droplet id (used for idempotence)
+- name: Create a new Droplet
+ community.digitalocean.digital_ocean:
+ state: present
+ oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
+ command: droplet
+ name: mydroplet
+ size_id: 2gb
+ region_id: ams2
+ image_id: fedora-19-x64
+ wait_timeout: 500
+ register: my_droplet
+
+- debug:
+ msg: "ID is {{ my_droplet.droplet.id }}"
+
+- debug:
+ msg: "IP is {{ my_droplet.droplet.ip_address }}"
+
+# Ensure a droplet is present
+# If droplet id already exist, will return the droplet details and changed = False
+# If no droplet matches the id, a new droplet will be created and the droplet details (including the new id) are returned, changed = True.
+
+- name: Ensure a droplet is present
+ community.digitalocean.digital_ocean:
+ state: present
+ oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
+ command: droplet
+ id: 123
+ name: mydroplet
+ size_id: 2gb
+ region_id: ams2
+ image_id: fedora-19-x64
+ wait_timeout: 500
+
+# Create a droplet with ssh key
+# The ssh key id can be passed as argument at the creation of a droplet (see ssh_key_ids).
+# Several keys can be added to ssh_key_ids as id1,id2,id3
+# The keys are used to connect as root to the droplet.
+
+- name: Create a droplet with ssh key
+ community.digitalocean.digital_ocean:
+ state: present
+ oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
+ ssh_key_ids: 123,456
+ name: mydroplet
+ size_id: 2gb
+ region_id: ams2
+ image_id: fedora-19-x64
+"""
+
+import os
+import time
+import traceback
+
+try:
+ from packaging.version import Version
+
+ HAS_PACKAGING = True
+except ImportError:
+ HAS_PACKAGING = False
+
+try:
+ # Imported as a dependency for dopy
+ import ansible.module_utils.six
+
+ HAS_SIX = True
+except ImportError:
+ HAS_SIX = False
+
+HAS_DOPY = False
+try:
+ import dopy
+ from dopy.manager import DoError, DoManager
+
+ # NOTE: Expressing Python dependencies isn't really possible:
+ # https://github.com/ansible/ansible/issues/62733#issuecomment-537098744
+ if HAS_PACKAGING:
+ if Version(dopy.__version__) >= Version("0.3.2"):
+ HAS_DOPY = True
+ else:
+ if dopy.__version__ >= "0.3.2": # Naive lexographical check
+ HAS_DOPY = True
+except ImportError:
+ pass
+
+from ansible.module_utils.basic import AnsibleModule, env_fallback
+
+
+class TimeoutError(Exception):
+ def __init__(self, msg, id_):
+ super(TimeoutError, self).__init__(msg)
+ self.id = id_
+
+
+class JsonfyMixIn(object):
+ def to_json(self):
+ return self.__dict__
+
+
+class Droplet(JsonfyMixIn):
+ manager = None
+
+ def __init__(self, droplet_json):
+ self.status = "new"
+ self.__dict__.update(droplet_json)
+
+ def is_powered_on(self):
+ return self.status == "active"
+
+ def update_attr(self, attrs=None):
+ if attrs:
+ for k, v in attrs.items():
+ setattr(self, k, v)
+ networks = attrs.get("networks", {})
+ for network in networks.get("v6", []):
+ if network["type"] == "public":
+ setattr(self, "public_ipv6_address", network["ip_address"])
+ else:
+ setattr(self, "private_ipv6_address", network["ip_address"])
+ else:
+ json = self.manager.show_droplet(self.id)
+ if json["ip_address"]:
+ self.update_attr(json)
+
+ def power_on(self):
+ if self.status != "off":
+ raise AssertionError("Can only power on a closed one.")
+ json = self.manager.power_on_droplet(self.id)
+ self.update_attr(json)
+
+ def ensure_powered_on(self, wait=True, wait_timeout=300):
+ if self.is_powered_on():
+ return
+ if self.status == "off": # powered off
+ self.power_on()
+
+ if wait:
+ end_time = time.monotonic() + wait_timeout
+ while time.monotonic() < end_time:
+ time.sleep(10)
+ self.update_attr()
+ if self.is_powered_on():
+ if not self.ip_address:
+ raise TimeoutError("No ip is found.", self.id)
+ return
+ raise TimeoutError("Wait for droplet running timeout", self.id)
+
+ def destroy(self):
+ return self.manager.destroy_droplet(self.id, scrub_data=True)
+
+ @classmethod
+ def setup(cls, api_token):
+ cls.manager = DoManager(None, api_token, api_version=2)
+
+ @classmethod
+ def add(
+ cls,
+ name,
+ size_id,
+ image_id,
+ region_id,
+ ssh_key_ids=None,
+ virtio=True,
+ private_networking=False,
+ backups_enabled=False,
+ user_data=None,
+ ipv6=False,
+ ):
+ private_networking_lower = str(private_networking).lower()
+ backups_enabled_lower = str(backups_enabled).lower()
+ ipv6_lower = str(ipv6).lower()
+ json = cls.manager.new_droplet(
+ name,
+ size_id,
+ image_id,
+ region_id,
+ ssh_key_ids=ssh_key_ids,
+ virtio=virtio,
+ private_networking=private_networking_lower,
+ backups_enabled=backups_enabled_lower,
+ user_data=user_data,
+ ipv6=ipv6_lower,
+ )
+ droplet = cls(json)
+ return droplet
+
+ @classmethod
+ def find(cls, id=None, name=None):
+ if not id and not name:
+ return False
+
+ droplets = cls.list_all()
+
+ # Check first by id. digital ocean requires that it be unique
+ for droplet in droplets:
+ if droplet.id == id:
+ return droplet
+
+ # Failing that, check by hostname.
+ for droplet in droplets:
+ if droplet.name == name:
+ return droplet
+
+ return False
+
+ @classmethod
+ def list_all(cls):
+ json = cls.manager.all_active_droplets()
+ return list(map(cls, json))
+
+
+class SSH(JsonfyMixIn):
+ manager = None
+
+ def __init__(self, ssh_key_json):
+ self.__dict__.update(ssh_key_json)
+
+ update_attr = __init__
+
+ def destroy(self):
+ self.manager.destroy_ssh_key(self.id)
+ return True
+
+ @classmethod
+ def setup(cls, api_token):
+ cls.manager = DoManager(None, api_token, api_version=2)
+
+ @classmethod
+ def find(cls, name):
+ if not name:
+ return False
+ keys = cls.list_all()
+ for key in keys:
+ if key.name == name:
+ return key
+ return False
+
+ @classmethod
+ def list_all(cls):
+ json = cls.manager.all_ssh_keys()
+ return list(map(cls, json))
+
+ @classmethod
+ def add(cls, name, key_pub):
+ json = cls.manager.new_ssh_key(name, key_pub)
+ return cls(json)
+
+
+def core(module):
+ def getkeyordie(k):
+ v = module.params[k]
+ if v is None:
+ module.fail_json(msg="Unable to load %s" % k)
+ return v
+
+ api_token = module.params["api_token"]
+ changed = True
+ command = module.params["command"]
+ state = module.params["state"]
+
+ if command == "droplet":
+ Droplet.setup(api_token)
+ if state in ("active", "present"):
+
+ # First, try to find a droplet by id.
+ droplet = Droplet.find(id=module.params["id"])
+
+ # If we couldn't find the droplet and the user is allowing unique
+ # hostnames, then check to see if a droplet with the specified
+ # hostname already exists.
+ if not droplet and module.params["unique_name"]:
+ droplet = Droplet.find(name=getkeyordie("name"))
+
+ # If both of those attempts failed, then create a new droplet.
+ if not droplet:
+ droplet = Droplet.add(
+ name=getkeyordie("name"),
+ size_id=getkeyordie("size_id"),
+ image_id=getkeyordie("image_id"),
+ region_id=getkeyordie("region_id"),
+ ssh_key_ids=module.params["ssh_key_ids"],
+ virtio=module.params["virtio"],
+ private_networking=module.params["private_networking"],
+ backups_enabled=module.params["backups_enabled"],
+ user_data=module.params.get("user_data"),
+ ipv6=module.params["ipv6"],
+ )
+
+ if droplet.is_powered_on():
+ changed = False
+
+ droplet.ensure_powered_on(
+ wait=getkeyordie("wait"), wait_timeout=getkeyordie("wait_timeout")
+ )
+
+ module.exit_json(changed=changed, droplet=droplet.to_json())
+
+ elif state in ("absent", "deleted"):
+ # First, try to find a droplet by id.
+ droplet = Droplet.find(module.params["id"])
+
+ # If we couldn't find the droplet and the user is allowing unique
+ # hostnames, then check to see if a droplet with the specified
+ # hostname already exists.
+ if not droplet and module.params["unique_name"]:
+ droplet = Droplet.find(name=getkeyordie("name"))
+
+ if not droplet:
+ module.exit_json(changed=False, msg="The droplet is not found.")
+
+ droplet.destroy()
+ module.exit_json(changed=True)
+
+ elif command == "ssh":
+ SSH.setup(api_token)
+ name = getkeyordie("name")
+ if state in ("active", "present"):
+ key = SSH.find(name)
+ if key:
+ module.exit_json(changed=False, ssh_key=key.to_json())
+ key = SSH.add(name, getkeyordie("ssh_pub_key"))
+ module.exit_json(changed=True, ssh_key=key.to_json())
+
+ elif state in ("absent", "deleted"):
+ key = SSH.find(name)
+ if not key:
+ module.exit_json(
+ changed=False,
+ msg="SSH key with the name of %s is not found." % name,
+ )
+ key.destroy()
+ module.exit_json(changed=True)
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ command=dict(choices=["droplet", "ssh"], default="droplet"),
+ state=dict(
+ choices=["active", "present", "absent", "deleted"], default="present"
+ ),
+ api_token=dict(
+ aliases=["API_TOKEN"],
+ no_log=True,
+ fallback=(env_fallback, ["DO_API_TOKEN", "DO_API_KEY"]),
+ ),
+ name=dict(type="str"),
+ size_id=dict(),
+ image_id=dict(),
+ region_id=dict(),
+ ssh_key_ids=dict(type="list", elements="str", no_log=False),
+ virtio=dict(type="bool", default=True),
+ private_networking=dict(type="bool", default=False),
+ backups_enabled=dict(type="bool", default=False),
+ id=dict(aliases=["droplet_id"], type="int"),
+ unique_name=dict(type="bool", default=False),
+ user_data=dict(default=None),
+ ipv6=dict(type="bool", default=False),
+ wait=dict(type="bool", default=True),
+ wait_timeout=dict(default=300, type="int"),
+ ssh_pub_key=dict(type="str"),
+ ),
+ required_together=(["size_id", "image_id", "region_id"],),
+ mutually_exclusive=(
+ ["size_id", "ssh_pub_key"],
+ ["image_id", "ssh_pub_key"],
+ ["region_id", "ssh_pub_key"],
+ ),
+ required_one_of=(["id", "name"],),
+ )
+ if not HAS_DOPY and not HAS_SIX:
+ module.fail_json(
+ msg="dopy >= 0.3.2 is required for this module. dopy requires six but six is not installed. "
+ "Make sure both dopy and six are installed."
+ )
+ if not HAS_DOPY:
+ module.fail_json(msg="dopy >= 0.3.2 required for this module")
+
+ try:
+ core(module)
+ except TimeoutError as e:
+ module.fail_json(msg=str(e), id=e.id)
+ except (DoError, Exception) as e:
+ module.fail_json(msg=str(e), exception=traceback.format_exc())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_account_facts.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_account_facts.py
new file mode 100644
index 00000000..46ccd3e5
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_account_facts.py
@@ -0,0 +1,93 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2018, Ansible Project
+# Copyright: (c) 2018, Abhijeet Kasurde <akasurde@redhat.com>
+# 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: digital_ocean_account_info
+short_description: Gather information about DigitalOcean User account
+description:
+ - This module can be used to gather information about User account.
+ - This module was called C(digital_ocean_account_facts) before Ansible 2.9. The usage did not change.
+author: "Abhijeet Kasurde (@Akasurde)"
+
+requirements:
+ - "python >= 2.6"
+
+extends_documentation_fragment:
+- community.digitalocean.digital_ocean.documentation
+
+"""
+
+
+EXAMPLES = r"""
+- name: Gather information about user account
+ community.digitalocean.digital_ocean_account_info:
+ oauth_token: "{{ oauth_token }}"
+"""
+
+
+RETURN = r"""
+data:
+ description: DigitalOcean account information
+ returned: success
+ type: dict
+ sample: {
+ "droplet_limit": 10,
+ "email": "testuser1@gmail.com",
+ "email_verified": true,
+ "floating_ip_limit": 3,
+ "status": "active",
+ "status_message": "",
+ "uuid": "aaaaaaaaaaaaaa"
+ }
+"""
+
+from traceback import format_exc
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+from ansible.module_utils._text import to_native
+
+
+def core(module):
+ rest = DigitalOceanHelper(module)
+
+ response = rest.get("account")
+ if response.status_code != 200:
+ module.fail_json(
+ msg="Failed to fetch 'account' information due to error : %s"
+ % response.json["message"]
+ )
+
+ module.exit_json(changed=False, data=response.json["account"])
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
+ if module._name in (
+ "digital_ocean_account_facts",
+ "community.digitalocean.digital_ocean_account_facts",
+ ):
+ module.deprecate(
+ "The 'digital_ocean_account_facts' module has been renamed to 'digital_ocean_account_info'",
+ version="2.0.0",
+ collection_name="community.digitalocean",
+ ) # was Ansible 2.13
+ try:
+ core(module)
+ except Exception as e:
+ module.fail_json(msg=to_native(e), exception=format_exc())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_account_info.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_account_info.py
new file mode 100644
index 00000000..46ccd3e5
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_account_info.py
@@ -0,0 +1,93 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2018, Ansible Project
+# Copyright: (c) 2018, Abhijeet Kasurde <akasurde@redhat.com>
+# 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: digital_ocean_account_info
+short_description: Gather information about DigitalOcean User account
+description:
+ - This module can be used to gather information about User account.
+ - This module was called C(digital_ocean_account_facts) before Ansible 2.9. The usage did not change.
+author: "Abhijeet Kasurde (@Akasurde)"
+
+requirements:
+ - "python >= 2.6"
+
+extends_documentation_fragment:
+- community.digitalocean.digital_ocean.documentation
+
+"""
+
+
+EXAMPLES = r"""
+- name: Gather information about user account
+ community.digitalocean.digital_ocean_account_info:
+ oauth_token: "{{ oauth_token }}"
+"""
+
+
+RETURN = r"""
+data:
+ description: DigitalOcean account information
+ returned: success
+ type: dict
+ sample: {
+ "droplet_limit": 10,
+ "email": "testuser1@gmail.com",
+ "email_verified": true,
+ "floating_ip_limit": 3,
+ "status": "active",
+ "status_message": "",
+ "uuid": "aaaaaaaaaaaaaa"
+ }
+"""
+
+from traceback import format_exc
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+from ansible.module_utils._text import to_native
+
+
+def core(module):
+ rest = DigitalOceanHelper(module)
+
+ response = rest.get("account")
+ if response.status_code != 200:
+ module.fail_json(
+ msg="Failed to fetch 'account' information due to error : %s"
+ % response.json["message"]
+ )
+
+ module.exit_json(changed=False, data=response.json["account"])
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
+ if module._name in (
+ "digital_ocean_account_facts",
+ "community.digitalocean.digital_ocean_account_facts",
+ ):
+ module.deprecate(
+ "The 'digital_ocean_account_facts' module has been renamed to 'digital_ocean_account_info'",
+ version="2.0.0",
+ collection_name="community.digitalocean",
+ ) # was Ansible 2.13
+ try:
+ core(module)
+ except Exception as e:
+ module.fail_json(msg=to_native(e), exception=format_exc())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_balance_info.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_balance_info.py
new file mode 100644
index 00000000..3aea5353
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_balance_info.py
@@ -0,0 +1,73 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2021, Ansible Project
+# Copyright: (c) 2021, Mark Mercado <mamercad@gmail.com>
+# 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: digital_ocean_balance_info
+short_description: Display DigitalOcean customer balance
+description:
+ - This module can be used to display the DigitalOcean customer balance.
+author: "Mark Mercado (@mamercad)"
+version_added: 1.2.0
+extends_documentation_fragment:
+ - community.digitalocean.digital_ocean.documentation
+"""
+
+
+EXAMPLES = r"""
+- name: Display DigitalOcean customer balance
+ community.digitalocean.digital_ocean_balance_info:
+ oauth_token: "{{ oauth_token }}"
+"""
+
+
+RETURN = r"""
+# DigitalOcean API info https://docs.digitalocean.com/reference/api/api-reference/#operation/get_customer_balance
+data:
+ description: DigitalOcean customer balance
+ returned: success
+ type: dict
+ sample: {
+ "account_balance": "-27.52",
+ "generated_at": "2021-04-11T05:08:24Z",
+ "month_to_date_balance": "-27.40",
+ "month_to_date_usage": "0.00"
+ }
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+
+
+def run(module):
+ rest = DigitalOceanHelper(module)
+
+ response = rest.get("customers/my/balance")
+ if response.status_code != 200:
+ module.fail_json(
+ msg="Failed to fetch 'customers/my/balance' information due to error : %s"
+ % response.json["message"]
+ )
+
+ module.exit_json(changed=False, data=response.json)
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
+
+ run(module)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_block_storage.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_block_storage.py
new file mode 100644
index 00000000..8597eb1e
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_block_storage.py
@@ -0,0 +1,411 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: Ansible Project
+# 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: digital_ocean_block_storage
+short_description: Create/destroy or attach/detach Block Storage volumes in DigitalOcean
+description:
+ - Create/destroy Block Storage volume in DigitalOcean, or attach/detach Block Storage volume to a droplet.
+options:
+ command:
+ description:
+ - Which operation do you want to perform.
+ choices: ['create', 'attach']
+ required: true
+ type: str
+ state:
+ description:
+ - Indicate desired state of the target.
+ choices: ['present', 'absent']
+ required: true
+ type: str
+ block_size:
+ description:
+ - The size of the Block Storage volume in gigabytes.
+ - Required when I(command=create) and I(state=present).
+ - If snapshot_id is included, this will be ignored.
+ - If block_size > current size of the volume, the volume is resized.
+ type: int
+ volume_name:
+ description:
+ - The name of the Block Storage volume.
+ type: str
+ required: true
+ description:
+ description:
+ - Description of the Block Storage volume.
+ type: str
+ region:
+ description:
+ - The slug of the region where your Block Storage volume should be located in.
+ - If I(snapshot_id) is included, this will be ignored.
+ type: str
+ snapshot_id:
+ description:
+ - The snapshot id you would like the Block Storage volume created with.
+ - If included, I(region) and I(block_size) will be ignored and changed to C(null).
+ type: str
+ droplet_id:
+ description:
+ - The droplet id you want to operate on.
+ - Required when I(command=attach).
+ type: int
+ project_name:
+ aliases: ["project"]
+ description:
+ - Project to assign the resource to (project name, not UUID).
+ - Defaults to the default project of the account (empty string).
+ - Currently only supported when C(command=create).
+ type: str
+ required: false
+ default: ""
+extends_documentation_fragment:
+- community.digitalocean.digital_ocean.documentation
+
+notes:
+ - Two environment variables can be used, DO_API_KEY and DO_API_TOKEN.
+ They both refer to the v2 token.
+ - If snapshot_id is used, region and block_size will be ignored and changed to null.
+
+author:
+ - "Harnek Sidhu (@harneksidhu)"
+"""
+
+EXAMPLES = r"""
+- name: Create new Block Storage
+ community.digitalocean.digital_ocean_block_storage:
+ state: present
+ oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
+ command: create
+ region: nyc1
+ block_size: 10
+ volume_name: nyc1-block-storage
+
+- name: Create new Block Storage (and assign to Project "test")
+ community.digitalocean.digital_ocean_block_storage:
+ state: present
+ oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
+ command: create
+ region: nyc1
+ block_size: 10
+ volume_name: nyc1-block-storage
+ project_name: test
+
+- name: Resize an existing Block Storage
+ community.digitalocean.digital_ocean_block_storage:
+ state: present
+ oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
+ command: create
+ region: nyc1
+ block_size: 20
+ volume_name: nyc1-block-storage
+
+- name: Delete Block Storage
+ community.digitalocean.digital_ocean_block_storage:
+ state: absent
+ oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
+ command: create
+ region: nyc1
+ volume_name: nyc1-block-storage
+
+- name: Attach Block Storage to a Droplet
+ community.digitalocean.digital_ocean_block_storage:
+ state: present
+ oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
+ command: attach
+ volume_name: nyc1-block-storage
+ region: nyc1
+ droplet_id: <ID>
+
+- name: Detach Block Storage from a Droplet
+ community.digitalocean.digital_ocean_block_storage:
+ state: absent
+ oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
+ command: attach
+ volume_name: nyc1-block-storage
+ region: nyc1
+ droplet_id: <ID>
+"""
+
+RETURN = r"""
+id:
+ description: Unique identifier of a Block Storage volume returned during creation.
+ returned: changed
+ type: str
+ sample: "69b25d9a-494c-12e6-a5af-001f53126b44"
+msg:
+ description: Informational or error message encountered during execution
+ returned: changed
+ type: str
+ sample: No project named test2 found
+assign_status:
+ description: Assignment status (ok, not_found, assigned, already_assigned, service_down)
+ returned: changed
+ type: str
+ sample: assigned
+resources:
+ description: Resource assignment involved in project assignment
+ returned: changed
+ type: dict
+ sample:
+ assigned_at: '2021-10-25T17:39:38Z'
+ links:
+ self: https://api.digitalocean.com/v2/volumes/8691c49e-35ba-11ec-9406-0a58ac1472b9
+ status: assigned
+ urn: do:volume:8691c49e-35ba-11ec-9406-0a58ac1472b9
+"""
+
+import time
+import traceback
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+ DigitalOceanProjects,
+)
+
+
+class DOBlockStorageException(Exception):
+ pass
+
+
+class DOBlockStorage(object):
+ def __init__(self, module):
+ self.module = module
+ self.rest = DigitalOceanHelper(module)
+ if self.module.params.get("project"):
+ # only load for non-default project assignments
+ self.projects = DigitalOceanProjects(module, self.rest)
+
+ def get_key_or_fail(self, k):
+ v = self.module.params[k]
+ if v is None:
+ self.module.fail_json(msg="Unable to load %s" % k)
+ return v
+
+ def poll_action_for_complete_status(self, action_id):
+ url = "actions/{0}".format(action_id)
+ end_time = time.monotonic() + self.module.params["timeout"]
+ while time.monotonic() < end_time:
+ time.sleep(10)
+ response = self.rest.get(url)
+ status = response.status_code
+ json = response.json
+ if status == 200:
+ if json["action"]["status"] == "completed":
+ return True
+ elif json["action"]["status"] == "errored":
+ raise DOBlockStorageException(json["message"])
+ raise DOBlockStorageException(
+ "Unable to reach the DigitalOcean API at %s"
+ % self.module.params.get("baseurl")
+ )
+
+ def get_block_storage_by_name(self, volume_name, region):
+ url = "volumes?name={0}&region={1}".format(volume_name, region)
+ resp = self.rest.get(url)
+ if resp.status_code != 200:
+ raise DOBlockStorageException(resp.json["message"])
+
+ volumes = resp.json["volumes"]
+ if not volumes:
+ return None
+
+ return volumes[0]
+
+ def get_attached_droplet_ID(self, volume_name, region):
+ volume = self.get_block_storage_by_name(volume_name, region)
+ if not volume or not volume["droplet_ids"]:
+ return None
+
+ return volume["droplet_ids"][0]
+
+ def attach_detach_block_storage(self, method, volume_name, region, droplet_id):
+ data = {
+ "type": method,
+ "volume_name": volume_name,
+ "region": region,
+ "droplet_id": droplet_id,
+ }
+ response = self.rest.post("volumes/actions", data=data)
+ status = response.status_code
+ json = response.json
+ if status == 202:
+ return self.poll_action_for_complete_status(json["action"]["id"])
+ elif status == 200:
+ return True
+ elif status == 404 and method == "detach":
+ return False # Already detached
+ elif status == 422:
+ return False
+ else:
+ raise DOBlockStorageException(json["message"])
+
+ def resize_block_storage(self, volume_name, region, desired_size):
+ if not desired_size:
+ return False
+
+ volume = self.get_block_storage_by_name(volume_name, region)
+ if volume["size_gigabytes"] == desired_size:
+ return False
+
+ data = {
+ "type": "resize",
+ "size_gigabytes": desired_size,
+ }
+ resp = self.rest.post(
+ "volumes/{0}/actions".format(volume["id"]),
+ data=data,
+ )
+ if resp.status_code == 202:
+ return self.poll_action_for_complete_status(resp.json["action"]["id"])
+ else:
+ # we'd get status 422 if desired_size <= current volume size
+ raise DOBlockStorageException(resp.json["message"])
+
+ def create_block_storage(self):
+ volume_name = self.get_key_or_fail("volume_name")
+ snapshot_id = self.module.params["snapshot_id"]
+ if snapshot_id:
+ self.module.params["block_size"] = None
+ self.module.params["region"] = None
+ block_size = None
+ region = None
+ else:
+ block_size = self.get_key_or_fail("block_size")
+ region = self.get_key_or_fail("region")
+ description = self.module.params["description"]
+ data = {
+ "size_gigabytes": block_size,
+ "name": volume_name,
+ "description": description,
+ "region": region,
+ "snapshot_id": snapshot_id,
+ }
+ response = self.rest.post("volumes", data=data)
+ status = response.status_code
+ json = response.json
+ if status == 201:
+ project_name = self.module.params.get("project")
+ if (
+ project_name
+ ): # empty string is the default project, skip project assignment
+ urn = "do:volume:{0}".format(json["volume"]["id"])
+ (
+ assign_status,
+ error_message,
+ resources,
+ ) = self.projects.assign_to_project(project_name, urn)
+ self.module.exit_json(
+ changed=True,
+ id=json["volume"]["id"],
+ msg=error_message,
+ assign_status=assign_status,
+ resources=resources,
+ )
+ else:
+ self.module.exit_json(changed=True, id=json["volume"]["id"])
+ elif status == 409 and json["id"] == "conflict":
+ # The volume exists already, but it might not have the desired size
+ resized = self.resize_block_storage(volume_name, region, block_size)
+ self.module.exit_json(changed=resized)
+ else:
+ raise DOBlockStorageException(json["message"])
+
+ def delete_block_storage(self):
+ volume_name = self.get_key_or_fail("volume_name")
+ region = self.get_key_or_fail("region")
+ url = "volumes?name={0}&region={1}".format(volume_name, region)
+ attached_droplet_id = self.get_attached_droplet_ID(volume_name, region)
+ if attached_droplet_id is not None:
+ self.attach_detach_block_storage(
+ "detach", volume_name, region, attached_droplet_id
+ )
+ response = self.rest.delete(url)
+ status = response.status_code
+ json = response.json
+ if status == 204:
+ self.module.exit_json(changed=True)
+ elif status == 404:
+ self.module.exit_json(changed=False)
+ else:
+ raise DOBlockStorageException(json["message"])
+
+ def attach_block_storage(self):
+ volume_name = self.get_key_or_fail("volume_name")
+ region = self.get_key_or_fail("region")
+ droplet_id = self.get_key_or_fail("droplet_id")
+ attached_droplet_id = self.get_attached_droplet_ID(volume_name, region)
+ if attached_droplet_id is not None:
+ if attached_droplet_id == droplet_id:
+ self.module.exit_json(changed=False)
+ else:
+ self.attach_detach_block_storage(
+ "detach", volume_name, region, attached_droplet_id
+ )
+ changed_status = self.attach_detach_block_storage(
+ "attach", volume_name, region, droplet_id
+ )
+ self.module.exit_json(changed=changed_status)
+
+ def detach_block_storage(self):
+ volume_name = self.get_key_or_fail("volume_name")
+ region = self.get_key_or_fail("region")
+ droplet_id = self.get_key_or_fail("droplet_id")
+ changed_status = self.attach_detach_block_storage(
+ "detach", volume_name, region, droplet_id
+ )
+ self.module.exit_json(changed=changed_status)
+
+
+def handle_request(module):
+ block_storage = DOBlockStorage(module)
+ command = module.params["command"]
+ state = module.params["state"]
+ if command == "create":
+ if state == "present":
+ block_storage.create_block_storage()
+ elif state == "absent":
+ block_storage.delete_block_storage()
+ elif command == "attach":
+ if state == "present":
+ block_storage.attach_block_storage()
+ elif state == "absent":
+ block_storage.detach_block_storage()
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ argument_spec.update(
+ state=dict(choices=["present", "absent"], required=True),
+ command=dict(choices=["create", "attach"], required=True),
+ block_size=dict(type="int", required=False),
+ volume_name=dict(type="str", required=True),
+ description=dict(type="str"),
+ region=dict(type="str", required=False),
+ snapshot_id=dict(type="str", required=False),
+ droplet_id=dict(type="int"),
+ project_name=dict(type="str", aliases=["project"], required=False, default=""),
+ )
+
+ module = AnsibleModule(argument_spec=argument_spec)
+
+ try:
+ handle_request(module)
+ except DOBlockStorageException as e:
+ module.fail_json(msg=str(e), exception=traceback.format_exc())
+ except KeyError as e:
+ module.fail_json(msg="Unable to load %s" % e, exception=traceback.format_exc())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_cdn_endpoints.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_cdn_endpoints.py
new file mode 100644
index 00000000..d3617758
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_cdn_endpoints.py
@@ -0,0 +1,256 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2021, Ansible Project
+# Copyright: (c) 2021, Mark Mercado <mamercad@gmail.com>
+# 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: digital_ocean_cdn_endpoints
+short_description: Create, update, and delete DigitalOcean CDN Endpoints
+description:
+ - Create, update, and delete DigitalOcean CDN Endpoints
+author: "Mark Mercado (@mamercad)"
+version_added: 1.10.0
+options:
+ state:
+ description:
+ - The usual, C(present) to create, C(absent) to destroy
+ type: str
+ choices: ["present", "absent"]
+ default: present
+ origin:
+ description:
+ - The fully qualified domain name (FQDN) for the origin server which provides the content for the CDN.
+ - This is currently restricted to a Space.
+ type: str
+ required: true
+ ttl:
+ description:
+ - The amount of time the content is cached by the CDN's edge servers in seconds.
+ - TTL must be one of 60, 600, 3600, 86400, or 604800.
+ - Defaults to 3600 (one hour) when excluded.
+ type: int
+ choices: [60, 600, 3600, 86400, 604800]
+ default: 3600
+ required: false
+ certificate_id:
+ description:
+ - The ID of a DigitalOcean managed TLS certificate used for SSL when a custom subdomain is provided.
+ type: str
+ required: false
+ custom_domain:
+ description:
+ - The fully qualified domain name (FQDN) of the custom subdomain used with the CDN endpoint.
+ type: str
+ required: false
+extends_documentation_fragment:
+ - community.digitalocean.digital_ocean.documentation
+"""
+
+
+EXAMPLES = r"""
+- name: Create DigitalOcean CDN Endpoint
+ community.digitalocean.digital_ocean_cdn_endpoints:
+ state: present
+ oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
+ origin: mamercad.nyc3.digitaloceanspaces.com
+
+- name: Update DigitalOcean CDN Endpoint (change ttl to 600, default is 3600)
+ community.digitalocean.digital_ocean_cdn_endpoints:
+ state: present
+ oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
+ origin: mamercad.nyc3.digitaloceanspaces.com
+ ttl: 600
+
+- name: Delete DigitalOcean CDN Endpoint
+ community.digitalocean.digital_ocean_cdn_endpoints:
+ state: absent
+ oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
+ origin: mamercad.nyc3.digitaloceanspaces.com
+"""
+
+
+RETURN = r"""
+data:
+ description: DigitalOcean CDN Endpoints
+ returned: success
+ type: dict
+ sample:
+ data:
+ endpoint:
+ created_at: '2021-09-05T13:47:23Z'
+ endpoint: mamercad.nyc3.cdn.digitaloceanspaces.com
+ id: 01739563-3f50-4da4-a451-27f6d59d7573
+ origin: mamercad.nyc3.digitaloceanspaces.com
+ ttl: 3600
+"""
+
+
+from ansible.module_utils.basic import AnsibleModule, env_fallback
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+
+
+class DOCDNEndpoint(object):
+ def __init__(self, module):
+ self.module = module
+ self.rest = DigitalOceanHelper(module)
+ # pop the oauth token so we don't include it in the POST data
+ self.token = self.module.params.pop("oauth_token")
+
+ def get_cdn_endpoints(self):
+ cdns = self.rest.get_paginated_data(
+ base_url="cdn/endpoints?", data_key_name="endpoints"
+ )
+ return cdns
+
+ def get_cdn_endpoint(self):
+ cdns = self.rest.get_paginated_data(
+ base_url="cdn/endpoints?", data_key_name="endpoints"
+ )
+ found = None
+ for cdn in cdns:
+ if cdn.get("origin") == self.module.params.get("origin"):
+ found = cdn
+ for key in ["ttl", "certificate_id"]:
+ if self.module.params.get(key) != cdn.get(key):
+ return found, True
+ return found, False
+
+ def create(self):
+ cdn, needs_update = self.get_cdn_endpoint()
+
+ if cdn is not None:
+ if not needs_update:
+ # Have it already
+ self.module.exit_json(changed=False, msg=cdn)
+ if needs_update:
+ # Check mode
+ if self.module.check_mode:
+ self.module.exit_json(changed=True)
+
+ # Update it
+ request_params = dict(self.module.params)
+
+ endpoint = "cdn/endpoints"
+ response = self.rest.put(
+ "{0}/{1}".format(endpoint, cdn.get("id")), data=request_params
+ )
+ status_code = response.status_code
+ json_data = response.json
+
+ # The API docs are wrong (they say 202 but return 200)
+ if status_code != 200:
+ self.module.fail_json(
+ changed=False,
+ msg="Failed to put {0} information due to error [HTTP {1}: {2}]".format(
+ endpoint,
+ status_code,
+ json_data.get("message", "(empty error message)"),
+ ),
+ )
+
+ self.module.exit_json(changed=True, data=json_data)
+ else:
+ # Check mode
+ if self.module.check_mode:
+ self.module.exit_json(changed=True)
+
+ # Create it
+ request_params = dict(self.module.params)
+
+ endpoint = "cdn/endpoints"
+ response = self.rest.post(endpoint, data=request_params)
+ status_code = response.status_code
+ json_data = response.json
+
+ if status_code != 201:
+ self.module.fail_json(
+ changed=False,
+ msg="Failed to post {0} information due to error [HTTP {1}: {2}]".format(
+ endpoint,
+ status_code,
+ json_data.get("message", "(empty error message)"),
+ ),
+ )
+
+ self.module.exit_json(changed=True, data=json_data)
+
+ def delete(self):
+ cdn, needs_update = self.get_cdn_endpoint()
+ if cdn is not None:
+ # Check mode
+ if self.module.check_mode:
+ self.module.exit_json(changed=True)
+
+ # Delete it
+ endpoint = "cdn/endpoints/{0}".format(cdn.get("id"))
+ response = self.rest.delete(endpoint)
+ status_code = response.status_code
+ json_data = response.json
+
+ if status_code != 204:
+ self.module.fail_json(
+ changed=False,
+ msg="Failed to delete {0} information due to error [HTTP {1}: {2}]".format(
+ endpoint,
+ status_code,
+ json_data.get("message", "(empty error message)"),
+ ),
+ )
+
+ self.module.exit_json(
+ changed=True,
+ msg="Deleted CDN Endpoint {0} ({1})".format(
+ cdn.get("origin"), cdn.get("id")
+ ),
+ )
+ else:
+ self.module.exit_json(changed=False)
+
+
+def run(module):
+ state = module.params.pop("state")
+ c = DOCDNEndpoint(module)
+
+ # Pop these away (don't need them beyond DOCDNEndpoint)
+ module.params.pop("baseurl")
+ module.params.pop("validate_certs")
+ module.params.pop("timeout")
+
+ if state == "present":
+ c.create()
+ elif state == "absent":
+ c.delete()
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ argument_spec.update(
+ state=dict(choices=["present", "absent"], default="present"),
+ origin=dict(type="str", required=True),
+ ttl=dict(
+ type="int",
+ choices=[60, 600, 3600, 86400, 604800],
+ required=False,
+ default=3600,
+ ),
+ certificate_id=dict(type="str", default=""),
+ custom_domain=dict(type="str", default=""),
+ )
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+ run(module)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_cdn_endpoints_info.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_cdn_endpoints_info.py
new file mode 100644
index 00000000..7c8de494
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_cdn_endpoints_info.py
@@ -0,0 +1,93 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2021, Ansible Project
+# Copyright: (c) 2021, Mark Mercado <mamercad@gmail.com>
+# 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: digital_ocean_cdn_endpoints_info
+short_description: Display DigitalOcean CDN Endpoints
+description:
+ - Display DigitalOcean CDN Endpoints
+author: "Mark Mercado (@mamercad)"
+version_added: 1.10.0
+options:
+ state:
+ description:
+ - The usual, C(present) to create, C(absent) to destroy
+ type: str
+ choices: ["present", "absent"]
+ default: present
+extends_documentation_fragment:
+ - community.digitalocean.digital_ocean.documentation
+"""
+
+
+EXAMPLES = r"""
+- name: Display DigitalOcean CDN Endpoints
+ community.digitalocean.digital_ocean_cdn_endpoints_info:
+ oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
+"""
+
+
+RETURN = r"""
+data:
+ description: DigitalOcean CDN Endpoints
+ returned: success
+ type: dict
+ sample:
+ data:
+ endpoints:
+ - created_at: '2021-09-05T13:47:23Z'
+ endpoint: mamercad.nyc3.cdn.digitaloceanspaces.com
+ id: 01739563-3f50-4da4-a451-27f6d59d7573
+ origin: mamercad.nyc3.digitaloceanspaces.com
+ ttl: 3600
+ meta:
+ total: 1
+"""
+
+
+from ansible.module_utils.basic import AnsibleModule, env_fallback
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+
+
+def run(module):
+ rest = DigitalOceanHelper(module)
+
+ endpoint = "cdn/endpoints"
+ response = rest.get(endpoint)
+ json_data = response.json
+ status_code = response.status_code
+
+ if status_code != 200:
+ module.fail_json(
+ changed=False,
+ msg="Failed to get {0} information due to error [HTTP {1}: {2}]".format(
+ endpoint, status_code, json_data.get("message", "(empty error message)")
+ ),
+ )
+
+ module.exit_json(changed=False, data=json_data)
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ argument_spec.update(state=dict(choices=["present", "absent"], default="present"))
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+ run(module)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_certificate.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_certificate.py
new file mode 100644
index 00000000..60dd0fea
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_certificate.py
@@ -0,0 +1,181 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright (c) 2017, Abhijeet Kasurde <akasurde@redhat.com>
+#
+# 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: digital_ocean_certificate
+short_description: Manage certificates in DigitalOcean
+description:
+ - Create, Retrieve and remove certificates DigitalOcean.
+author: "Abhijeet Kasurde (@Akasurde)"
+options:
+ name:
+ description:
+ - The name of the certificate.
+ required: True
+ type: str
+ private_key:
+ description:
+ - A PEM-formatted private key content of SSL Certificate.
+ type: str
+ leaf_certificate:
+ description:
+ - A PEM-formatted public SSL Certificate.
+ type: str
+ certificate_chain:
+ description:
+ - The full PEM-formatted trust chain between the certificate authority's certificate and your domain's SSL certificate.
+ type: str
+ state:
+ description:
+ - Whether the certificate should be present or absent.
+ default: present
+ choices: ['present', 'absent']
+ type: str
+extends_documentation_fragment:
+- community.digitalocean.digital_ocean.documentation
+
+notes:
+ - Two environment variables can be used, DO_API_KEY, DO_OAUTH_TOKEN and DO_API_TOKEN.
+ They both refer to the v2 token.
+"""
+
+
+EXAMPLES = r"""
+- name: Create a certificate
+ community.digitalocean.digital_ocean_certificate:
+ name: production
+ state: present
+ private_key: "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkM8OI7pRpgyj1I\n-----END PRIVATE KEY-----"
+ leaf_certificate: "-----BEGIN CERTIFICATE-----\nMIIFDmg2Iaw==\n-----END CERTIFICATE-----"
+ oauth_token: b7d03a6947b217efb6f3ec3bd365652
+
+- name: Create a certificate using file lookup plugin
+ community.digitalocean.digital_ocean_certificate:
+ name: production
+ state: present
+ private_key: "{{ lookup('file', 'test.key') }}"
+ leaf_certificate: "{{ lookup('file', 'test.cert') }}"
+ oauth_token: "{{ oauth_token }}"
+
+- name: Create a certificate with trust chain
+ community.digitalocean.digital_ocean_certificate:
+ name: production
+ state: present
+ private_key: "{{ lookup('file', 'test.key') }}"
+ leaf_certificate: "{{ lookup('file', 'test.cert') }}"
+ certificate_chain: "{{ lookup('file', 'chain.cert') }}"
+ oauth_token: "{{ oauth_token }}"
+
+- name: Remove a certificate
+ community.digitalocean.digital_ocean_certificate:
+ name: production
+ state: absent
+ oauth_token: "{{ oauth_token }}"
+
+"""
+
+
+RETURN = r""" # """
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+from ansible.module_utils._text import to_native
+
+
+def core(module):
+ state = module.params["state"]
+ name = module.params["name"]
+
+ rest = DigitalOceanHelper(module)
+
+ results = dict(changed=False)
+
+ response = rest.get("certificates")
+ status_code = response.status_code
+ resp_json = response.json
+
+ if status_code != 200:
+ module.fail_json(msg="Failed to retrieve certificates for DigitalOcean")
+
+ if state == "present":
+ for cert in resp_json["certificates"]:
+ if cert["name"] == name:
+ module.fail_json(msg="Certificate name %s already exists" % name)
+
+ # Certificate does not exist, let us create it
+ cert_data = dict(
+ name=name,
+ private_key=module.params["private_key"],
+ leaf_certificate=module.params["leaf_certificate"],
+ )
+
+ if module.params["certificate_chain"] is not None:
+ cert_data.update(certificate_chain=module.params["certificate_chain"])
+
+ response = rest.post("certificates", data=cert_data)
+ status_code = response.status_code
+ if status_code == 500:
+ module.fail_json(
+ msg="Failed to upload certificates as the certificates are malformed."
+ )
+
+ resp_json = response.json
+ if status_code == 201:
+ results.update(changed=True, response=resp_json)
+ elif status_code == 422:
+ results.update(changed=False, response=resp_json)
+
+ elif state == "absent":
+ cert_id_del = None
+ for cert in resp_json["certificates"]:
+ if cert["name"] == name:
+ cert_id_del = cert["id"]
+
+ if cert_id_del is not None:
+ url = "certificates/{0}".format(cert_id_del)
+ response = rest.delete(url)
+ if response.status_code == 204:
+ results.update(changed=True)
+ else:
+ results.update(changed=False)
+ else:
+ module.fail_json(msg="Failed to find certificate %s" % name)
+
+ module.exit_json(**results)
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ argument_spec.update(
+ name=dict(type="str", required=True),
+ leaf_certificate=dict(type="str"),
+ private_key=dict(type="str", no_log=True),
+ state=dict(choices=["present", "absent"], default="present"),
+ certificate_chain=dict(type="str"),
+ )
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ required_if=[
+ ("state", "present", ["leaf_certificate", "private_key"]),
+ ],
+ )
+
+ try:
+ core(module)
+ except Exception as e:
+ module.fail_json(msg=to_native(e))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_certificate_facts.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_certificate_facts.py
new file mode 100644
index 00000000..c9125985
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_certificate_facts.py
@@ -0,0 +1,126 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2018, Ansible Project
+# Copyright: (c) 2018, Abhijeet Kasurde <akasurde@redhat.com>
+# 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: digital_ocean_certificate_info
+short_description: Gather information about DigitalOcean certificates
+description:
+ - This module can be used to gather information about DigitalOcean provided certificates.
+ - This module was called C(digital_ocean_certificate_facts) before Ansible 2.9. The usage did not change.
+author: "Abhijeet Kasurde (@Akasurde)"
+options:
+ certificate_id:
+ description:
+ - Certificate ID that can be used to identify and reference a certificate.
+ required: false
+ type: str
+requirements:
+ - "python >= 2.6"
+extends_documentation_fragment:
+- community.digitalocean.digital_ocean.documentation
+
+"""
+
+
+EXAMPLES = r"""
+- name: Gather information about all certificates
+ community.digitalocean.digital_ocean_certificate_info:
+ oauth_token: "{{ oauth_token }}"
+
+- name: Gather information about certificate with given id
+ community.digitalocean.digital_ocean_certificate_info:
+ oauth_token: "{{ oauth_token }}"
+ certificate_id: "892071a0-bb95-49bc-8021-3afd67a210bf"
+
+- name: Get not after information about certificate
+ community.digitalocean.digital_ocean_certificate_info:
+ register: resp_out
+- set_fact:
+ not_after_date: "{{ item.not_after }}"
+ loop: "{{ resp_out.data | community.general.json_query(name) }}"
+ vars:
+ name: "[?name=='web-cert-01']"
+- debug:
+ var: not_after_date
+"""
+
+
+RETURN = r"""
+data:
+ description: DigitalOcean certificate information
+ returned: success
+ type: list
+ elements: dict
+ sample: [
+ {
+ "id": "892071a0-bb95-49bc-8021-3afd67a210bf",
+ "name": "web-cert-01",
+ "not_after": "2017-02-22T00:23:00Z",
+ "sha1_fingerprint": "dfcc9f57d86bf58e321c2c6c31c7a971be244ac7",
+ "created_at": "2017-02-08T16:02:37Z"
+ },
+ ]
+"""
+
+from traceback import format_exc
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+from ansible.module_utils._text import to_native
+
+
+def core(module):
+ certificate_id = module.params.get("certificate_id", None)
+ rest = DigitalOceanHelper(module)
+
+ base_url = "certificates"
+ if certificate_id is not None:
+ response = rest.get("%s/%s" % (base_url, certificate_id))
+ status_code = response.status_code
+
+ if status_code != 200:
+ module.fail_json(msg="Failed to retrieve certificates for DigitalOcean")
+
+ certificate = [response.json["certificate"]]
+ else:
+ certificate = rest.get_paginated_data(
+ base_url=base_url + "?", data_key_name="certificates"
+ )
+
+ module.exit_json(changed=False, data=certificate)
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ argument_spec.update(
+ certificate_id=dict(type="str", required=False),
+ )
+ module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
+ if module._name in (
+ "digital_ocean_certificate_facts",
+ "community.digitalocean.digital_ocean_certificate_facts",
+ ):
+ module.deprecate(
+ "The 'digital_ocean_certificate_facts' module has been renamed to 'digital_ocean_certificate_info'",
+ version="2.0.0",
+ collection_name="community.digitalocean",
+ ) # was Ansible 2.13
+
+ try:
+ core(module)
+ except Exception as e:
+ module.fail_json(msg=to_native(e), exception=format_exc())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_certificate_info.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_certificate_info.py
new file mode 100644
index 00000000..c9125985
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_certificate_info.py
@@ -0,0 +1,126 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2018, Ansible Project
+# Copyright: (c) 2018, Abhijeet Kasurde <akasurde@redhat.com>
+# 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: digital_ocean_certificate_info
+short_description: Gather information about DigitalOcean certificates
+description:
+ - This module can be used to gather information about DigitalOcean provided certificates.
+ - This module was called C(digital_ocean_certificate_facts) before Ansible 2.9. The usage did not change.
+author: "Abhijeet Kasurde (@Akasurde)"
+options:
+ certificate_id:
+ description:
+ - Certificate ID that can be used to identify and reference a certificate.
+ required: false
+ type: str
+requirements:
+ - "python >= 2.6"
+extends_documentation_fragment:
+- community.digitalocean.digital_ocean.documentation
+
+"""
+
+
+EXAMPLES = r"""
+- name: Gather information about all certificates
+ community.digitalocean.digital_ocean_certificate_info:
+ oauth_token: "{{ oauth_token }}"
+
+- name: Gather information about certificate with given id
+ community.digitalocean.digital_ocean_certificate_info:
+ oauth_token: "{{ oauth_token }}"
+ certificate_id: "892071a0-bb95-49bc-8021-3afd67a210bf"
+
+- name: Get not after information about certificate
+ community.digitalocean.digital_ocean_certificate_info:
+ register: resp_out
+- set_fact:
+ not_after_date: "{{ item.not_after }}"
+ loop: "{{ resp_out.data | community.general.json_query(name) }}"
+ vars:
+ name: "[?name=='web-cert-01']"
+- debug:
+ var: not_after_date
+"""
+
+
+RETURN = r"""
+data:
+ description: DigitalOcean certificate information
+ returned: success
+ type: list
+ elements: dict
+ sample: [
+ {
+ "id": "892071a0-bb95-49bc-8021-3afd67a210bf",
+ "name": "web-cert-01",
+ "not_after": "2017-02-22T00:23:00Z",
+ "sha1_fingerprint": "dfcc9f57d86bf58e321c2c6c31c7a971be244ac7",
+ "created_at": "2017-02-08T16:02:37Z"
+ },
+ ]
+"""
+
+from traceback import format_exc
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+from ansible.module_utils._text import to_native
+
+
+def core(module):
+ certificate_id = module.params.get("certificate_id", None)
+ rest = DigitalOceanHelper(module)
+
+ base_url = "certificates"
+ if certificate_id is not None:
+ response = rest.get("%s/%s" % (base_url, certificate_id))
+ status_code = response.status_code
+
+ if status_code != 200:
+ module.fail_json(msg="Failed to retrieve certificates for DigitalOcean")
+
+ certificate = [response.json["certificate"]]
+ else:
+ certificate = rest.get_paginated_data(
+ base_url=base_url + "?", data_key_name="certificates"
+ )
+
+ module.exit_json(changed=False, data=certificate)
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ argument_spec.update(
+ certificate_id=dict(type="str", required=False),
+ )
+ module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
+ if module._name in (
+ "digital_ocean_certificate_facts",
+ "community.digitalocean.digital_ocean_certificate_facts",
+ ):
+ module.deprecate(
+ "The 'digital_ocean_certificate_facts' module has been renamed to 'digital_ocean_certificate_info'",
+ version="2.0.0",
+ collection_name="community.digitalocean",
+ ) # was Ansible 2.13
+
+ try:
+ core(module)
+ except Exception as e:
+ module.fail_json(msg=to_native(e), exception=format_exc())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_database.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_database.py
new file mode 100644
index 00000000..ffae82db
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_database.py
@@ -0,0 +1,437 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright: Ansible Project
+# Copyright: (c) 2021, Mark Mercado <mmercado@digitalocean.com>
+# 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: digital_ocean_database
+short_description: Create and delete a DigitalOcean database
+description:
+ - Create and delete a database in DigitalOcean and optionally wait for it to be online.
+ - DigitalOcean's managed database service simplifies the creation and management of highly available database clusters.
+ - Currently, it offers support for PostgreSQL, Redis, MySQL, and MongoDB.
+version_added: 1.3.0
+author: "Mark Mercado (@mamercad)"
+options:
+ state:
+ description:
+ - Indicates the desired state of the target.
+ default: present
+ choices: ['present', 'absent']
+ type: str
+ id:
+ description:
+ - A unique ID that can be used to identify and reference a database cluster.
+ type: int
+ aliases: ['database_id']
+ name:
+ description:
+ - A unique, human-readable name for the database cluster.
+ type: str
+ required: true
+ engine:
+ description:
+ - A slug representing the database engine used for the cluster.
+ - The possible values are C(pg) for PostgreSQL, C(mysql) for MySQL, C(redis) for Redis, and C(mongodb) for MongoDB.
+ type: str
+ required: true
+ choices: ['pg', 'mysql', 'redis', 'mongodb']
+ version:
+ description:
+ - A string representing the version of the database engine in use for the cluster.
+ - For C(pg), versions are 10, 11 and 12.
+ - For C(mysql), version is 8.
+ - For C(redis), version is 5.
+ - For C(mongodb), version is 4.
+ type: str
+ size:
+ description:
+ - The slug identifier representing the size of the nodes in the database cluster.
+ - See U(https://docs.digitalocean.com/reference/api/api-reference/#operation/create_database_cluster) for supported sizes.
+ type: str
+ required: true
+ aliases: ['size_id']
+ region:
+ description:
+ - The slug identifier for the region where the database cluster is located.
+ type: str
+ required: true
+ aliases: ['region_id']
+ num_nodes:
+ description:
+ - The number of nodes in the database cluster.
+ - Valid choices are 1, 2 or 3.
+ type: int
+ default: 1
+ choices: [1, 2, 3]
+ tags:
+ description:
+ - An array of tags that have been applied to the database cluster.
+ type: list
+ elements: str
+ private_network_uuid:
+ description:
+ - A string specifying the UUID of the VPC to which the database cluster is assigned.
+ type: str
+ wait:
+ description:
+ - Wait for the database to be online before returning.
+ required: False
+ default: True
+ type: bool
+ wait_timeout:
+ description:
+ - How long before wait gives up, in seconds, when creating a database.
+ default: 600
+ type: int
+ project_name:
+ aliases: ["project"]
+ description:
+ - Project to assign the resource to (project name, not UUID).
+ - Defaults to the default project of the account (empty string).
+ - Currently only supported when creating databases.
+ type: str
+ required: false
+ default: ""
+extends_documentation_fragment:
+ - community.digitalocean.digital_ocean.documentation
+"""
+
+
+EXAMPLES = r"""
+- name: Create a Redis database
+ community.digitalocean.digital_ocean_database:
+ oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_KEY') }}"
+ state: present
+ name: testdatabase1
+ engine: redis
+ size: db-s-1vcpu-1gb
+ region: nyc1
+ num_nodes: 1
+ register: my_database
+
+- name: Create a Redis database (and assign to Project "test")
+ community.digitalocean.digital_ocean_database:
+ oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_KEY') }}"
+ state: present
+ name: testdatabase1
+ engine: redis
+ size: db-s-1vcpu-1gb
+ region: nyc1
+ num_nodes: 1
+ project_name: test
+ register: my_database
+"""
+
+
+RETURN = r"""
+data:
+ description: A DigitalOcean database
+ returned: success
+ type: dict
+ sample:
+ database:
+ connection:
+ database: ""
+ host: testdatabase1-do-user-3097135-0.b.db.ondigitalocean.com
+ password: REDACTED
+ port: 25061
+ protocol: rediss
+ ssl: true
+ uri: rediss://default:REDACTED@testdatabase1-do-user-3097135-0.b.db.ondigitalocean.com:25061
+ user: default
+ created_at: "2021-04-21T15:41:14Z"
+ db_names: null
+ engine: redis
+ id: 37de10e4-808b-4f4b-b25f-7b5b3fd194ac
+ maintenance_window:
+ day: monday
+ hour: 11:33:47
+ pending: false
+ name: testdatabase1
+ num_nodes: 1
+ private_connection:
+ database: ""
+ host: private-testdatabase1-do-user-3097135-0.b.db.ondigitalocean.com
+ password: REDIS
+ port: 25061
+ protocol: rediss
+ ssl: true
+ uri: rediss://default:REDACTED@private-testdatabase1-do-user-3097135-0.b.db.ondigitalocean.com:25061
+ user: default
+ private_network_uuid: 0db3519b-9efc-414a-8868-8f2e6934688c,
+ region: nyc1
+ size: db-s-1vcpu-1gb
+ status: online
+ tags: null
+ users: null
+ version: 6
+msg:
+ description: Informational or error message encountered during execution
+ returned: changed
+ type: str
+ sample: No project named test2 found
+assign_status:
+ description: Assignment status (ok, not_found, assigned, already_assigned, service_down)
+ returned: changed
+ type: str
+ sample: assigned
+resources:
+ description: Resource assignment involved in project assignment
+ returned: changed
+ type: dict
+ sample:
+ assigned_at: '2021-10-25T17:39:38Z'
+ links:
+ self: https://api.digitalocean.com/v2/databases/126355fa-b147-40a6-850a-c44f5d2ad418
+ status: assigned
+ urn: do:dbaas:126355fa-b147-40a6-850a-c44f5d2ad418
+"""
+
+
+import json
+import time
+from ansible.module_utils.basic import AnsibleModule, env_fallback
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+ DigitalOceanProjects,
+)
+
+
+class DODatabase(object):
+ def __init__(self, module):
+ self.module = module
+ self.rest = DigitalOceanHelper(module)
+ if self.module.params.get("project"):
+ # only load for non-default project assignments
+ self.projects = DigitalOceanProjects(module, self.rest)
+ # pop wait and wait_timeout so we don't include it in the POST data
+ self.wait = self.module.params.pop("wait", True)
+ self.wait_timeout = self.module.params.pop("wait_timeout", 600)
+ # pop the oauth token so we don't include it in the POST data
+ self.module.params.pop("oauth_token")
+ self.id = None
+ self.name = None
+ self.engine = None
+ self.version = None
+ self.num_nodes = None
+ self.region = None
+ self.status = None
+ self.size = None
+
+ def get_by_id(self, database_id):
+ if database_id is None:
+ return None
+ response = self.rest.get("databases/{0}".format(database_id))
+ json_data = response.json
+ if response.status_code == 200:
+ database = json_data.get("database", None)
+ if database is not None:
+ self.id = database.get("id", None)
+ self.name = database.get("name", None)
+ self.engine = database.get("engine", None)
+ self.version = database.get("version", None)
+ self.num_nodes = database.get("num_nodes", None)
+ self.region = database.get("region", None)
+ self.status = database.get("status", None)
+ self.size = database.get("size", None)
+ return json_data
+ return None
+
+ def get_by_name(self, database_name):
+ if database_name is None:
+ return None
+ page = 1
+ while page is not None:
+ response = self.rest.get("databases?page={0}".format(page))
+ json_data = response.json
+ if response.status_code == 200:
+ databases = json_data.get("databases", None)
+ if databases is None or not isinstance(databases, list):
+ return None
+ for database in databases:
+ if database.get("name", None) == database_name:
+ self.id = database.get("id", None)
+ self.name = database.get("name", None)
+ self.engine = database.get("engine", None)
+ self.version = database.get("version", None)
+ self.status = database.get("status", None)
+ self.num_nodes = database.get("num_nodes", None)
+ self.region = database.get("region", None)
+ self.size = database.get("size", None)
+ return {"database": database}
+ if (
+ "links" in json_data
+ and "pages" in json_data["links"]
+ and "next" in json_data["links"]["pages"]
+ ):
+ page += 1
+ else:
+ page = None
+ return None
+
+ def get_database(self):
+ json_data = self.get_by_id(self.module.params["id"])
+ if not json_data:
+ json_data = self.get_by_name(self.module.params["name"])
+ return json_data
+
+ def ensure_online(self, database_id):
+ end_time = time.monotonic() + self.wait_timeout
+ while time.monotonic() < end_time:
+ response = self.rest.get("databases/{0}".format(database_id))
+ json_data = response.json
+ database = json_data.get("database", None)
+ if database is not None:
+ status = database.get("status", None)
+ if status is not None:
+ if status == "online":
+ return json_data
+ time.sleep(10)
+ self.module.fail_json(msg="Waiting for database online timeout")
+
+ def create(self):
+ json_data = self.get_database()
+
+ if json_data is not None:
+ database = json_data.get("database", None)
+ if database is not None:
+ self.module.exit_json(changed=False, data=json_data)
+ else:
+ self.module.fail_json(
+ changed=False, msg="Unexpected error, please file a bug"
+ )
+
+ if self.module.check_mode:
+ self.module.exit_json(changed=True)
+
+ request_params = dict(self.module.params)
+ del request_params["id"]
+
+ response = self.rest.post("databases", data=request_params)
+ json_data = response.json
+ if response.status_code >= 400:
+ self.module.fail_json(changed=False, msg=json_data["message"])
+ database = json_data.get("database", None)
+ if database is None:
+ self.module.fail_json(
+ changed=False,
+ msg="Unexpected error; please file a bug https://github.com/ansible-collections/community.digitalocean/issues",
+ )
+
+ database_id = database.get("id", None)
+ if database_id is None:
+ self.module.fail_json(
+ changed=False,
+ msg="Unexpected error; please file a bug https://github.com/ansible-collections/community.digitalocean/issues",
+ )
+
+ if self.wait:
+ json_data = self.ensure_online(database_id)
+
+ project_name = self.module.params.get("project")
+ if project_name: # empty string is the default project, skip project assignment
+ urn = "do:dbaas:{0}".format(database_id)
+ assign_status, error_message, resources = self.projects.assign_to_project(
+ project_name, urn
+ )
+ self.module.exit_json(
+ changed=True,
+ data=json_data,
+ msg=error_message,
+ assign_status=assign_status,
+ resources=resources,
+ )
+ else:
+ self.module.exit_json(changed=True, data=json_data)
+
+ def delete(self):
+ json_data = self.get_database()
+ if json_data is not None:
+ if self.module.check_mode:
+ self.module.exit_json(changed=True)
+ database = json_data.get("database", None)
+ database_id = database.get("id", None)
+ database_name = database.get("name", None)
+ database_region = database.get("region", None)
+ if database_id is not None:
+ response = self.rest.delete("databases/{0}".format(database_id))
+ json_data = response.json
+ if response.status_code == 204:
+ self.module.exit_json(
+ changed=True,
+ msg="Deleted database {0} ({1}) in region {2}".format(
+ database_name, database_id, database_region
+ ),
+ )
+ self.module.fail_json(
+ changed=False,
+ msg="Failed to delete database {0} ({1}) in region {2}: {3}".format(
+ database_name,
+ database_id,
+ database_region,
+ json_data["message"],
+ ),
+ )
+ else:
+ self.module.fail_json(
+ changed=False, msg="Unexpected error, please file a bug"
+ )
+ else:
+ self.module.exit_json(
+ changed=False,
+ msg="Database {0} in region {1} not found".format(
+ self.module.params["name"], self.module.params["region"]
+ ),
+ )
+
+
+def run(module):
+ state = module.params.pop("state")
+ database = DODatabase(module)
+ if state == "present":
+ database.create()
+ elif state == "absent":
+ database.delete()
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ argument_spec.update(
+ state=dict(choices=["present", "absent"], default="present"),
+ id=dict(type="int", aliases=["database_id"]),
+ name=dict(type="str", required=True),
+ engine=dict(choices=["pg", "mysql", "redis", "mongodb"], required=True),
+ version=dict(type="str"),
+ size=dict(type="str", aliases=["size_id"], required=True),
+ region=dict(type="str", aliases=["region_id"], required=True),
+ num_nodes=dict(type="int", choices=[1, 2, 3], default=1),
+ tags=dict(type="list", elements="str"),
+ private_network_uuid=dict(type="str"),
+ wait=dict(type="bool", default=True),
+ wait_timeout=dict(default=600, type="int"),
+ project_name=dict(type="str", aliases=["project"], required=False, default=""),
+ )
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ required_one_of=(["id", "name"],),
+ required_if=(
+ [
+ ("state", "present", ["name", "size", "engine", "region"]),
+ ("state", "absent", ["name", "size", "engine", "region"]),
+ ]
+ ),
+ supports_check_mode=True,
+ )
+ run(module)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_database_info.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_database_info.py
new file mode 100644
index 00000000..cc599661
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_database_info.py
@@ -0,0 +1,214 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright: Ansible Project
+# Copyright: (c) 2021, Mark Mercado <mmercado@digitalocean.com>
+# 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: digital_ocean_database_info
+short_description: Gather information about DigitalOcean databases
+description:
+ - Gather information about DigitalOcean databases.
+version_added: 1.3.0
+author: "Mark Mercado (@mamercad)"
+options:
+ id:
+ description:
+ - A unique ID that can be used to identify and reference a database cluster.
+ type: int
+ aliases: ['database_id']
+ required: false
+ name:
+ description:
+ - A unique, human-readable name for the database cluster.
+ type: str
+ required: false
+extends_documentation_fragment:
+ - community.digitalocean.digital_ocean.documentation
+"""
+
+
+EXAMPLES = r"""
+- name: Gather all DigitalOcean databases
+ community.digitalocean.digital_ocean_database_info:
+ oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_KEY') }}"
+ register: my_databases
+"""
+
+
+RETURN = r"""
+data:
+ description: List of DigitalOcean databases
+ returned: success
+ type: list
+ sample: [
+ {
+ "connection": {
+ "database": "",
+ "host": "testdatabase1-do-user-3097135-0.b.db.ondigitalocean.com",
+ "password": "REDACTED",
+ "port": 25061,
+ "protocol":"rediss",
+ "ssl": true,
+ "uri": "rediss://default:REDACTED@testdatabase1-do-user-3097135-0.b.db.ondigitalocean.com:25061",
+ "user": "default"
+ },
+ "created_at": "2021-04-21T15:41:14Z",
+ "db_names": null,
+ "engine": "redis",
+ "id": "37de10e4-808b-4f4b-b25f-7b5b3fd194ac",
+ "maintenance_window": {
+ "day": "monday",
+ "hour": "11:33:47",
+ "pending": false
+ },
+ "name": "testdatabase1",
+ "num_nodes": 1,
+ "private_connection": {
+ "database": "",
+ "host": "private-testdatabase1-do-user-3097135-0.b.db.ondigitalocean.com",
+ "password": "REDACTED",
+ "port": 25061,
+ "protocol": "rediss",
+ "ssl": true,
+ "uri": "rediss://default:REDACTED@private-testdatabase1-do-user-3097135-0.b.db.ondigitalocean.com:25061",
+ "user": "default"
+ },
+ "private_network_uuid": "0db3519b-9efc-414a-8868-8f2e6934688c",
+ "region": "nyc1",
+ "size": "db-s-1vcpu-1gb",
+ "status": "online",
+ "tags": null,
+ "users": null,
+ "version": "6"
+ },
+ ...
+ ]
+"""
+
+
+from ansible.module_utils.basic import AnsibleModule, env_fallback
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+
+
+class DODatabaseInfo(object):
+ def __init__(self, module):
+ self.module = module
+ self.rest = DigitalOceanHelper(module)
+ # pop the oauth token so we don't include it in the POST data
+ self.module.params.pop("oauth_token")
+ self.id = None
+ self.name = None
+
+ def get_by_id(self, database_id):
+ if database_id is None:
+ return None
+ response = self.rest.get("databases/{0}".format(database_id))
+ json_data = response.json
+ if response.status_code == 200:
+ database = json_data.get("database", None)
+ if database is not None:
+ self.id = database.get("id", None)
+ self.name = database.get("name", None)
+ return json_data
+ return None
+
+ def get_by_name(self, database_name):
+ if database_name is None:
+ return None
+ page = 1
+ while page is not None:
+ response = self.rest.get("databases?page={0}".format(page))
+ json_data = response.json
+ if response.status_code == 200:
+ for database in json_data["databases"]:
+ if database.get("name", None) == database_name:
+ self.id = database.get("id", None)
+ self.name = database.get("name", None)
+ return {"database": database}
+ if (
+ "links" in json_data
+ and "pages" in json_data["links"]
+ and "next" in json_data["links"]["pages"]
+ ):
+ page += 1
+ else:
+ page = None
+ return None
+
+ def get_database(self):
+ json_data = self.get_by_id(self.module.params["id"])
+ if not json_data:
+ json_data = self.get_by_name(self.module.params["name"])
+ return json_data
+
+ def get_databases(self):
+ all_databases = []
+ page = 1
+ while page is not None:
+ response = self.rest.get("databases?page={0}".format(page))
+ json_data = response.json
+ if response.status_code == 200:
+ databases = json_data.get("databases", None)
+ if databases is not None and isinstance(databases, list):
+ all_databases.append(databases)
+ if (
+ "links" in json_data
+ and "pages" in json_data["links"]
+ and "next" in json_data["links"]["pages"]
+ ):
+ page += 1
+ else:
+ page = None
+ return {"databases": all_databases}
+
+
+def run(module):
+ id = module.params.get("id", None)
+ name = module.params.get("name", None)
+
+ database = DODatabaseInfo(module)
+
+ if id is not None or name is not None:
+ the_database = database.get_database()
+ if the_database: # Found it
+ module.exit_json(changed=False, data=the_database)
+ else: # Didn't find it
+ if id is not None and name is not None:
+ module.fail_json(
+ change=False, msg="Database {0} ({1}) not found".format(id, name)
+ )
+ elif id is not None and name is None:
+ module.fail_json(change=False, msg="Database {0} not found".format(id))
+ elif id is None and name is not None:
+ module.fail_json(
+ change=False, msg="Database {0} not found".format(name)
+ )
+ else:
+ all_databases = database.get_databases()
+ module.exit_json(changed=False, data=all_databases)
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ argument_spec.update(
+ id=dict(type="int", aliases=["database_id"]),
+ name=dict(type="str"),
+ )
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+ run(module)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_domain.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_domain.py
new file mode 100644
index 00000000..234c6cf2
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_domain.py
@@ -0,0 +1,325 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright: Ansible Project
+# 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: digital_ocean_domain
+short_description: Create/delete a DNS domain in DigitalOcean
+description:
+ - Create/delete a DNS domain in DigitalOcean.
+author: "Michael Gregson (@mgregson)"
+options:
+ state:
+ description:
+ - Indicate desired state of the target.
+ default: present
+ choices: ['present', 'absent']
+ type: str
+ id:
+ description:
+ - The droplet id you want to operate on.
+ aliases: ['droplet_id']
+ type: int
+ name:
+ description:
+ - The name of the droplet - must be formatted by hostname rules, or the name of a SSH key, or the name of a domain.
+ type: str
+ ip:
+ description:
+ - An 'A' record for '@' ($ORIGIN) will be created with the value 'ip'. 'ip' is an IP version 4 address.
+ type: str
+ aliases: ['ip4', 'ipv4']
+ ip6:
+ description:
+ - An 'AAAA' record for '@' ($ORIGIN) will be created with the value 'ip6'. 'ip6' is an IP version 6 address.
+ type: str
+ aliases: ['ipv6']
+ project_name:
+ aliases: ["project"]
+ description:
+ - Project to assign the resource to (project name, not UUID).
+ - Defaults to the default project of the account (empty string).
+ - Currently only supported when creating domains.
+ type: str
+ required: false
+ default: ""
+extends_documentation_fragment:
+- community.digitalocean.digital_ocean.documentation
+
+notes:
+ - Environment variables DO_OAUTH_TOKEN can be used for the oauth_token.
+ - As of Ansible 1.9.5 and 2.0, Version 2 of the DigitalOcean API is used, this removes C(client_id) and C(api_key) options in favor of C(oauth_token).
+ - If you are running Ansible 1.9.4 or earlier you might not be able to use the included version of this module as the API version used has been retired.
+
+requirements:
+ - "python >= 2.6"
+"""
+
+
+EXAMPLES = r"""
+- name: Create a domain
+ community.digitalocean.digital_ocean_domain:
+ state: present
+ name: my.digitalocean.domain
+ ip: 127.0.0.1
+
+- name: Create a domain (and associate to Project "test")
+ community.digitalocean.digital_ocean_domain:
+ state: present
+ name: my.digitalocean.domain
+ ip: 127.0.0.1
+ project: test
+
+# Create a droplet and corresponding domain
+- name: Create a droplet
+ community.digitalocean.digital_ocean:
+ state: present
+ name: test_droplet
+ size_id: 1gb
+ region_id: sgp1
+ image_id: ubuntu-14-04-x64
+ register: test_droplet
+
+- name: Create a corresponding domain
+ community.digitalocean.digital_ocean_domain:
+ state: present
+ name: "{{ test_droplet.droplet.name }}.my.domain"
+ ip: "{{ test_droplet.droplet.ip_address }}"
+
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+ DigitalOceanProjects,
+)
+import time
+
+ZONE_FILE_ATTEMPTS = 5
+ZONE_FILE_SLEEP = 3
+
+
+class DoManager(DigitalOceanHelper, object):
+ def __init__(self, module):
+ super(DoManager, self).__init__(module)
+ self.domain_name = module.params.get("name", None)
+ self.domain_ip = module.params.get("ip", None)
+ self.domain_id = module.params.get("id", None)
+
+ @staticmethod
+ def jsonify(response):
+ return response.status_code, response.json
+
+ def all_domains(self):
+ resp = self.get("domains/")
+ return resp
+
+ def find(self):
+ if self.domain_name is None and self.domain_id is None:
+ return None
+
+ domains = self.all_domains()
+ status, json = self.jsonify(domains)
+ for domain in json["domains"]:
+ if domain["name"] == self.domain_name:
+ return domain
+ return None
+
+ def add(self):
+ params = {"name": self.domain_name, "ip_address": self.domain_ip}
+ resp = self.post("domains/", data=params)
+ status = resp.status_code
+ json = resp.json
+ if status == 201:
+ return json["domain"]
+ else:
+ return json
+
+ def all_domain_records(self):
+ resp = self.get("domains/%s/records/" % self.domain_name)
+ return resp.json
+
+ def domain_record(self):
+ resp = self.get("domains/%s" % self.domain_name)
+ status, json = self.jsonify(resp)
+ return json
+
+ def destroy_domain(self):
+ resp = self.delete("domains/%s" % self.domain_name)
+ status, json = self.jsonify(resp)
+ if status == 204:
+ return True
+ else:
+ return json
+
+ def edit_domain_record(self, record):
+ if self.module.params.get("ip"):
+ params = {"name": "@", "data": self.module.params.get("ip")}
+ if self.module.params.get("ip6"):
+ params = {"name": "@", "data": self.module.params.get("ip6")}
+
+ resp = self.put(
+ "domains/%s/records/%s" % (self.domain_name, record["id"]), data=params
+ )
+ status, json = self.jsonify(resp)
+
+ return json["domain_record"]
+
+ def create_domain_record(self):
+ if self.module.params.get("ip"):
+ params = {"name": "@", "type": "A", "data": self.module.params.get("ip")}
+ if self.module.params.get("ip6"):
+ params = {
+ "name": "@",
+ "type": "AAAA",
+ "data": self.module.params.get("ip6"),
+ }
+
+ resp = self.post("domains/%s/records" % (self.domain_name), data=params)
+ status, json = self.jsonify(resp)
+
+ return json["domain_record"]
+
+
+def run(module):
+ do_manager = DoManager(module)
+ state = module.params.get("state")
+
+ if module.params.get("project"):
+ # only load for non-default project assignments
+ projects = DigitalOceanProjects(module, do_manager)
+
+ domain = do_manager.find()
+ if state == "present":
+ if not domain:
+ domain = do_manager.add()
+ if "message" in domain:
+ module.fail_json(changed=False, msg=domain["message"])
+ else:
+ # We're at the mercy of a backend process which we have no visibility into:
+ # https://docs.digitalocean.com/reference/api/api-reference/#operation/create_domain
+ #
+ # In particular: "Keep in mind that, upon creation, the zone_file field will
+ # have a value of null until a zone file is generated and propagated through
+ # an automatic process on the DigitalOcean servers."
+ #
+ # Arguably, it's nice to see the records versus null, so, we'll just try a
+ # few times before giving up and returning null.
+
+ domain_name = module.params.get("name")
+ project_name = module.params.get("project")
+ urn = "do:domain:{0}".format(domain_name)
+
+ for i in range(ZONE_FILE_ATTEMPTS):
+ record = do_manager.domain_record()
+ if record is not None and "domain" in record:
+ domain = record.get("domain", None)
+ if domain is not None and "zone_file" in domain:
+ if (
+ project_name
+ ): # empty string is the default project, skip project assignment
+ (
+ assign_status,
+ error_message,
+ resources,
+ ) = projects.assign_to_project(project_name, urn)
+ module.exit_json(
+ changed=True,
+ domain=domain,
+ msg=error_message,
+ assign_status=assign_status,
+ resources=resources,
+ )
+ else:
+ module.exit_json(changed=True, domain=domain)
+ time.sleep(ZONE_FILE_SLEEP)
+ if (
+ project_name
+ ): # empty string is the default project, skip project assignment
+ (
+ assign_status,
+ error_message,
+ resources,
+ ) = projects.assign_to_project(project_name, urn)
+ module.exit_json(
+ changed=True,
+ domain=domain,
+ msg=error_message,
+ assign_status=assign_status,
+ resources=resources,
+ )
+ else:
+ module.exit_json(changed=True, domain=domain)
+ else:
+ records = do_manager.all_domain_records()
+ if module.params.get("ip"):
+ at_record = None
+ for record in records["domain_records"]:
+ if record["name"] == "@" and record["type"] == "A":
+ at_record = record
+
+ if not at_record:
+ do_manager.create_domain_record()
+ module.exit_json(changed=True, domain=do_manager.find())
+ elif not at_record["data"] == module.params.get("ip"):
+ do_manager.edit_domain_record(at_record)
+ module.exit_json(changed=True, domain=do_manager.find())
+
+ if module.params.get("ip6"):
+ at_record = None
+ for record in records["domain_records"]:
+ if record["name"] == "@" and record["type"] == "AAAA":
+ at_record = record
+
+ if not at_record:
+ do_manager.create_domain_record()
+ module.exit_json(changed=True, domain=do_manager.find())
+ elif not at_record["data"] == module.params.get("ip6"):
+ do_manager.edit_domain_record(at_record)
+ module.exit_json(changed=True, domain=do_manager.find())
+
+ module.exit_json(changed=False, domain=do_manager.domain_record())
+
+ elif state == "absent":
+ if not domain:
+ module.exit_json(changed=False, msg="Domain not found")
+ else:
+ delete_event = do_manager.destroy_domain()
+ if not delete_event:
+ module.fail_json(changed=False, msg=delete_event["message"])
+ else:
+ module.exit_json(changed=True, event=None)
+ delete_event = do_manager.destroy_domain()
+ module.exit_json(changed=delete_event)
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ argument_spec.update(
+ state=dict(choices=["present", "absent"], default="present"),
+ name=dict(type="str"),
+ id=dict(aliases=["droplet_id"], type="int"),
+ ip=dict(type="str", aliases=["ip4", "ipv4"]),
+ ip6=dict(type="str", aliases=["ipv6"]),
+ project_name=dict(type="str", aliases=["project"], required=False, default=""),
+ )
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ required_one_of=(["id", "name"],),
+ mutually_exclusive=[("ip", "ip6")],
+ )
+
+ run(module)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_domain_facts.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_domain_facts.py
new file mode 100644
index 00000000..32382b28
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_domain_facts.py
@@ -0,0 +1,152 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2018, Ansible Project
+# Copyright: (c) 2018, Abhijeet Kasurde <akasurde@redhat.com>
+# 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: digital_ocean_domain_info
+short_description: Gather information about DigitalOcean Domains
+description:
+ - This module can be used to gather information about DigitalOcean provided Domains.
+ - This module was called C(digital_ocean_domain_facts) before Ansible 2.9. The usage did not change.
+author: "Abhijeet Kasurde (@Akasurde)"
+options:
+ domain_name:
+ description:
+ - Name of the domain to gather information for.
+ required: false
+ type: str
+requirements:
+ - "python >= 2.6"
+extends_documentation_fragment:
+- community.digitalocean.digital_ocean.documentation
+
+"""
+
+
+EXAMPLES = r"""
+- name: Gather information about all domains
+ community.digitalocean.digital_ocean_domain_info:
+ oauth_token: "{{ oauth_token }}"
+
+- name: Gather information about domain with given name
+ community.digitalocean.digital_ocean_domain_info:
+ oauth_token: "{{ oauth_token }}"
+ domain_name: "example.com"
+
+- name: Get ttl from domain
+ community.digitalocean.digital_ocean_domain_info:
+ register: resp_out
+- set_fact:
+ domain_ttl: "{{ item.ttl }}"
+ loop: "{{ resp_out.data | community.general.json_query(name) }}"
+ vars:
+ name: "[?name=='example.com']"
+- debug:
+ var: domain_ttl
+"""
+
+
+RETURN = r"""
+data:
+ description: DigitalOcean Domain information
+ returned: success
+ elements: dict
+ type: list
+ sample: [
+ {
+ "domain_records": [
+ {
+ "data": "ns1.digitalocean.com",
+ "flags": null,
+ "id": 37826823,
+ "name": "@",
+ "port": null,
+ "priority": null,
+ "tag": null,
+ "ttl": 1800,
+ "type": "NS",
+ "weight": null
+ },
+ ],
+ "name": "myexample123.com",
+ "ttl": 1800,
+ "zone_file": "myexample123.com. IN SOA ns1.digitalocean.com. hostmaster.myexample123.com. 1520702984 10800 3600 604800 1800\n",
+ },
+ ]
+"""
+
+from traceback import format_exc
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+from ansible.module_utils._text import to_native
+
+
+def core(module):
+ domain_name = module.params.get("domain_name", None)
+ rest = DigitalOceanHelper(module)
+ domain_results = []
+
+ if domain_name is not None:
+ response = rest.get("domains/%s" % domain_name)
+ status_code = response.status_code
+
+ if status_code != 200:
+ module.fail_json(msg="Failed to retrieve domain for DigitalOcean")
+
+ resp_json = response.json
+ domains = [resp_json["domain"]]
+ else:
+ domains = rest.get_paginated_data(base_url="domains?", data_key_name="domains")
+
+ for temp_domain in domains:
+ temp_domain_dict = {
+ "name": temp_domain["name"],
+ "ttl": temp_domain["ttl"],
+ "zone_file": temp_domain["zone_file"],
+ "domain_records": list(),
+ }
+
+ base_url = "domains/%s/records?" % temp_domain["name"]
+
+ temp_domain_dict["domain_records"] = rest.get_paginated_data(
+ base_url=base_url, data_key_name="domain_records"
+ )
+ domain_results.append(temp_domain_dict)
+
+ module.exit_json(changed=False, data=domain_results)
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ argument_spec.update(
+ domain_name=dict(type="str", required=False),
+ )
+ module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
+ if module._name in (
+ "digital_ocean_domain_facts",
+ "community.digitalocean.digital_ocean_domain_facts",
+ ):
+ module.deprecate(
+ "The 'digital_ocean_domain_facts' module has been renamed to 'digital_ocean_domain_info'",
+ version="2.0.0",
+ collection_name="community.digitalocean",
+ ) # was Ansible 2.13
+
+ try:
+ core(module)
+ except Exception as e:
+ module.fail_json(msg=to_native(e), exception=format_exc())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_domain_info.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_domain_info.py
new file mode 100644
index 00000000..32382b28
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_domain_info.py
@@ -0,0 +1,152 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2018, Ansible Project
+# Copyright: (c) 2018, Abhijeet Kasurde <akasurde@redhat.com>
+# 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: digital_ocean_domain_info
+short_description: Gather information about DigitalOcean Domains
+description:
+ - This module can be used to gather information about DigitalOcean provided Domains.
+ - This module was called C(digital_ocean_domain_facts) before Ansible 2.9. The usage did not change.
+author: "Abhijeet Kasurde (@Akasurde)"
+options:
+ domain_name:
+ description:
+ - Name of the domain to gather information for.
+ required: false
+ type: str
+requirements:
+ - "python >= 2.6"
+extends_documentation_fragment:
+- community.digitalocean.digital_ocean.documentation
+
+"""
+
+
+EXAMPLES = r"""
+- name: Gather information about all domains
+ community.digitalocean.digital_ocean_domain_info:
+ oauth_token: "{{ oauth_token }}"
+
+- name: Gather information about domain with given name
+ community.digitalocean.digital_ocean_domain_info:
+ oauth_token: "{{ oauth_token }}"
+ domain_name: "example.com"
+
+- name: Get ttl from domain
+ community.digitalocean.digital_ocean_domain_info:
+ register: resp_out
+- set_fact:
+ domain_ttl: "{{ item.ttl }}"
+ loop: "{{ resp_out.data | community.general.json_query(name) }}"
+ vars:
+ name: "[?name=='example.com']"
+- debug:
+ var: domain_ttl
+"""
+
+
+RETURN = r"""
+data:
+ description: DigitalOcean Domain information
+ returned: success
+ elements: dict
+ type: list
+ sample: [
+ {
+ "domain_records": [
+ {
+ "data": "ns1.digitalocean.com",
+ "flags": null,
+ "id": 37826823,
+ "name": "@",
+ "port": null,
+ "priority": null,
+ "tag": null,
+ "ttl": 1800,
+ "type": "NS",
+ "weight": null
+ },
+ ],
+ "name": "myexample123.com",
+ "ttl": 1800,
+ "zone_file": "myexample123.com. IN SOA ns1.digitalocean.com. hostmaster.myexample123.com. 1520702984 10800 3600 604800 1800\n",
+ },
+ ]
+"""
+
+from traceback import format_exc
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+from ansible.module_utils._text import to_native
+
+
+def core(module):
+ domain_name = module.params.get("domain_name", None)
+ rest = DigitalOceanHelper(module)
+ domain_results = []
+
+ if domain_name is not None:
+ response = rest.get("domains/%s" % domain_name)
+ status_code = response.status_code
+
+ if status_code != 200:
+ module.fail_json(msg="Failed to retrieve domain for DigitalOcean")
+
+ resp_json = response.json
+ domains = [resp_json["domain"]]
+ else:
+ domains = rest.get_paginated_data(base_url="domains?", data_key_name="domains")
+
+ for temp_domain in domains:
+ temp_domain_dict = {
+ "name": temp_domain["name"],
+ "ttl": temp_domain["ttl"],
+ "zone_file": temp_domain["zone_file"],
+ "domain_records": list(),
+ }
+
+ base_url = "domains/%s/records?" % temp_domain["name"]
+
+ temp_domain_dict["domain_records"] = rest.get_paginated_data(
+ base_url=base_url, data_key_name="domain_records"
+ )
+ domain_results.append(temp_domain_dict)
+
+ module.exit_json(changed=False, data=domain_results)
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ argument_spec.update(
+ domain_name=dict(type="str", required=False),
+ )
+ module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
+ if module._name in (
+ "digital_ocean_domain_facts",
+ "community.digitalocean.digital_ocean_domain_facts",
+ ):
+ module.deprecate(
+ "The 'digital_ocean_domain_facts' module has been renamed to 'digital_ocean_domain_info'",
+ version="2.0.0",
+ collection_name="community.digitalocean",
+ ) # was Ansible 2.13
+
+ try:
+ core(module)
+ except Exception as e:
+ module.fail_json(msg=to_native(e), exception=format_exc())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_domain_record.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_domain_record.py
new file mode 100644
index 00000000..05bc4a45
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_domain_record.py
@@ -0,0 +1,508 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: Ansible Project
+# 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: digital_ocean_domain_record
+author: "Adam Papai (@woohgit)"
+version_added: 1.1.0
+short_description: Manage DigitalOcean domain records
+description:
+ - Create/delete a domain record in DigitalOcean.
+options:
+ state:
+ description:
+ - Indicate desired state of the target.
+ default: present
+ choices: [ present, absent ]
+ type: str
+ record_id:
+ description:
+ - Used with C(force_update=yes) and C(state='absent') to update or delete a specific record.
+ type: int
+ force_update:
+ description:
+ - If there is already a record with the same C(name) and C(type) force update it.
+ default: false
+ type: bool
+ domain:
+ description:
+ - Name of the domain.
+ required: true
+ type: str
+ type:
+ description:
+ - The type of record you would like to create.
+ choices: [ A, AAAA, CNAME, MX, TXT, SRV, NS, CAA ]
+ type: str
+ data:
+ description:
+ - This is the value of the record, depending on the record type.
+ default: ""
+ type: str
+ name:
+ description:
+ - Required for C(A, AAAA, CNAME, TXT, SRV) records. The host name, alias, or service being defined by the record.
+ default: "@"
+ type: str
+ priority:
+ description:
+ - The priority of the host for C(SRV, MX) records).
+ type: int
+ port:
+ description:
+ - The port that the service is accessible on for SRV records only.
+ type: int
+ weight:
+ description:
+ - The weight of records with the same priority for SRV records only.
+ type: int
+ ttl:
+ description:
+ - Time to live for the record, in seconds.
+ default: 1800
+ type: int
+ flags:
+ description:
+ - An unsignedinteger between 0-255 used for CAA records.
+ type: int
+ tag:
+ description:
+ - The parameter tag for CAA records.
+ choices: [ issue, wildissue, iodef ]
+ type: str
+ oauth_token:
+ description:
+ - DigitalOcean OAuth token. Can be specified in C(DO_API_KEY), C(DO_API_TOKEN), or C(DO_OAUTH_TOKEN) environment variables
+ aliases: ['API_TOKEN']
+ type: str
+
+notes:
+ - Version 2 of DigitalOcean API is used.
+ - The number of requests that can be made through the API is currently limited to 5,000 per hour per OAuth token.
+"""
+
+EXAMPLES = """
+- name: Create default A record for example.com
+ community.digitalocean.digital_ocean_domain_record:
+ state: present
+ oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
+ domain: example.com
+ type: A
+ name: "@"
+ data: 127.0.0.1
+
+- name: Create A record for www
+ community.digitalocean.digital_ocean_domain_record:
+ state: present
+ oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
+ domain: example.com
+ type: A
+ name: www
+ data: 127.0.0.1
+
+- name: Update A record for www based on name/type/data
+ community.digitalocean.digital_ocean_domain_record:
+ state: present
+ oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
+ domain: example.com
+ type: A
+ name: www
+ data: 127.0.0.2
+ force_update: yes
+
+- name: Update A record for www based on record_id
+ community.digitalocean.digital_ocean_domain_record:
+ state: present
+ oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
+ domain: example.com
+ record_id: 123456
+ type: A
+ name: www
+ data: 127.0.0.2
+ force_update: yes
+
+- name: Remove www record based on name/type/data
+ community.digitalocean.digital_ocean_domain_record:
+ state: absent
+ oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
+ domain: example.com
+ type: A
+ name: www
+ data: 127.0.0.1
+
+- name: Remove www record based on record_id
+ community.digitalocean.digital_ocean_domain_record:
+ state: absent
+ oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
+ domain: example.com
+ record_id: 1234567
+
+- name: Create MX record with priority 10 for example.com
+ community.digitalocean.digital_ocean_domain_record:
+ state: present
+ oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
+ domain: example.com
+ type: MX
+ data: mail1.example.com
+ priority: 10
+"""
+
+RETURN = r"""
+data:
+ description: a DigitalOcean Domain Record
+ returned: success
+ type: dict
+ sample: {
+ "id": 3352896,
+ "type": "CNAME",
+ "name": "www",
+ "data": "192.168.0.1",
+ "priority": 10,
+ "port": 5556,
+ "ttl": 3600,
+ "weight": 10,
+ "flags": 16,
+ "tag": "issue"
+ }
+"""
+
+
+from ansible.module_utils.basic import env_fallback
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+
+
+class DigitalOceanDomainRecordManager(DigitalOceanHelper, object):
+ def __init__(self, module):
+ super(DigitalOceanDomainRecordManager, self).__init__(module)
+ self.module = module
+ self.domain = module.params.get("domain").lower()
+ self.records = self.__get_all_records()
+ self.payload = self.__build_payload()
+ self.force_update = module.params.get("force_update", False)
+ self.record_id = module.params.get("record_id", None)
+
+ def check_credentials(self):
+ # Check if oauth_token is valid or not
+ response = self.get("account")
+ if response.status_code == 401:
+ self.module.fail_json(
+ msg="Failed to login using oauth_token, please verify validity of oauth_token"
+ )
+
+ def verify_domain(self):
+ # URL https://api.digitalocean.com/v2/domains/[NAME]
+ response = self.get("domains/%s" % self.domain)
+ status_code = response.status_code
+ json = response.json
+
+ if status_code not in (200, 404):
+ self.module.fail_json(
+ msg="Error getting domain [%(status_code)s: %(json)s]"
+ % {"status_code": status_code, "json": json}
+ )
+ elif status_code == 404:
+ self.module.fail_json(
+ msg="No domain named '%s' found. Please create a domain first"
+ % self.domain
+ )
+
+ def __get_all_records(self):
+
+ records = []
+ page = 1
+ while True:
+ # GET /v2/domains/$DOMAIN_NAME/records
+ response = self.get(
+ "domains/%(domain)s/records?page=%(page)s"
+ % {"domain": self.domain, "page": page}
+ )
+ status_code = response.status_code
+ json = response.json
+
+ if status_code != 200:
+ self.module.fail_json(
+ msg="Error getting domain records [%(status_code)s: %(json)s]"
+ % {"status_code": status_code, "json": json}
+ )
+
+ for record in json["domain_records"]:
+ records.append(dict([(str(k), v) for k, v in record.items()]))
+
+ if "pages" in json["links"] and "next" in json["links"]["pages"]:
+ page += 1
+ else:
+ break
+
+ return records
+
+ def __normalize_data(self):
+ # for the MX, CNAME, SRV, CAA records make sure the data ends with a dot
+ if (
+ self.payload["type"] in ["CNAME", "MX", "SRV", "CAA"]
+ and self.payload["data"] != "@"
+ and not self.payload["data"].endswith(".")
+ ):
+ data = "%s." % self.payload["data"]
+ else:
+ data = self.payload["data"]
+
+ return data
+
+ def __find_record_by_id(self, record_id):
+ for record in self.records:
+ if record["id"] == record_id:
+ return record
+ return None
+
+ def __get_matching_records(self):
+ """Collect exact and similar records
+
+ It returns an exact record if there is any match along with the record_id.
+ It also returns multiple records if there is no exact match
+ """
+
+ # look for exactly the same record used by (create, delete)
+ for record in self.records:
+ r = dict(record)
+ del r["id"]
+ # python3 does not have cmp so let's use the official workaround
+ if r == self.payload:
+ return r, record["id"], None
+
+ # look for similar records used by (update)
+ similar_records = []
+ for record in self.records:
+ if (
+ record["type"] == self.payload["type"]
+ and record["name"] == self.payload["name"]
+ ):
+ similar_records.append(record)
+
+ if similar_records:
+ return None, None, similar_records
+
+ # if no exact neither similar records
+ return None, None, None
+
+ def __create_record(self):
+ # before data comparison, we need to make sure that
+ # the payload['data'] is not normalized, but
+ # during create/update digitalocean expects normalized data
+ self.payload["data"] = self.__normalize_data()
+
+ # POST /v2/domains/$DOMAIN_NAME/records
+ response = self.post("domains/%s/records" % self.domain, data=self.payload)
+ status_code = response.status_code
+ json = response.json
+ if status_code == 201:
+ changed = True
+ return changed, json["domain_record"]
+ else:
+ self.module.fail_json(
+ msg="Error creating domain record [%(status_code)s: %(json)s]"
+ % {"status_code": status_code, "json": json}
+ )
+
+ def create_or_update_record(self):
+
+ # if record_id is given we need to update the record no matter what
+ if self.record_id:
+ changed, result = self.__update_record(self.record_id)
+ return changed, result
+
+ record, record_id, similar_records = self.__get_matching_records()
+
+ # create the record if no similar or exact record were found
+ if not record and not similar_records:
+ changed, result = self.__create_record()
+ return changed, result
+
+ # no exact match, but we have similar records
+ # so if force_update == True we should update it
+ if not record and similar_records:
+ # if we have 1 similar record
+ if len(similar_records) == 1:
+ # update if we were told to do it so
+ if self.force_update:
+ record_id = similar_records[0]["id"]
+ changed, result = self.__update_record(record_id)
+ # if no update was given, create it
+ else:
+ changed, result = self.__create_record()
+ return changed, result
+ # we have multiple similar records, bun not exact match
+ else:
+ # we have multiple similar records, can't decide what to do
+ if self.force_update:
+ self.module.fail_json(
+ msg="Can't update record, too many similar records: %s"
+ % similar_records
+ )
+ # create it
+ else:
+ changed, result = self.__create_record()
+ return changed, result
+ # record matches
+ else:
+ changed = False
+ result = "Record has been already created"
+ return changed, result
+
+ def __update_record(self, record_id):
+ # before data comparison, we need to make sure that
+ # the payload['data'] is not normalized, but
+ # during create/update digitalocean expects normalized data
+ self.payload["data"] = self.__normalize_data()
+
+ # double check if the record exist
+ record = self.__find_record_by_id(record_id)
+
+ # record found
+ if record:
+ # PUT /v2/domains/$DOMAIN_NAME/records/$RECORD_ID
+ response = self.put(
+ "domains/%(domain)s/records/%(record_id)s"
+ % {"domain": self.domain, "record_id": record_id},
+ data=self.payload,
+ )
+ status_code = response.status_code
+ json = response.json
+ if status_code == 200:
+ changed = True
+ return changed, json["domain_record"]
+ else:
+ self.module.fail_json(
+ msg="Error updating domain record [%(status_code)s: %(json)s]"
+ % {"status_code": status_code, "json": json}
+ )
+ # recond not found
+ else:
+ self.module.fail_json(
+ msg="Error updating domain record. Record does not exist. [%s]"
+ % record_id
+ )
+
+ def __build_payload(self):
+
+ payload = dict(
+ data=self.module.params.get("data"),
+ flags=self.module.params.get("flags"),
+ name=self.module.params.get("name"),
+ port=self.module.params.get("port"),
+ priority=self.module.params.get("priority"),
+ type=self.module.params.get("type"),
+ tag=self.module.params.get("tag"),
+ ttl=self.module.params.get("ttl"),
+ weight=self.module.params.get("weight"),
+ )
+
+ # DigitalOcean stores every data in lowercase except TXT
+ if payload["type"] != "TXT" and payload["data"]:
+ payload["data"] = payload["data"].lower()
+
+ # digitalocean stores data: '@' if the data=domain
+ if payload["data"] == self.domain:
+ payload["data"] = "@"
+
+ return payload
+
+ def delete_record(self):
+
+ # if record_id is given, try to find the record based on the id
+ if self.record_id:
+ record = self.__find_record_by_id(self.record_id)
+ record_id = self.record_id
+ # if no record_id is given, try to a single matching record
+ else:
+ record, record_id, similar_records = self.__get_matching_records()
+ if not record and similar_records:
+ if len(similar_records) == 1:
+ record, record_id = similar_records[0], similar_records[0]["id"]
+ else:
+ self.module.fail_json(
+ msg="Can't delete record, too many similar records: %s"
+ % similar_records
+ )
+ # record was not found, we're done
+ if not record:
+ changed = False
+ return changed, record
+ # record found, lets delete it
+ else:
+ # DELETE /v2/domains/$DOMAIN_NAME/records/$RECORD_ID.
+ response = self.delete(
+ "domains/%(domain)s/records/%(id)s"
+ % {"domain": self.domain, "id": record_id}
+ )
+ status_code = response.status_code
+ json = response.json
+ if status_code == 204:
+ changed = True
+ msg = "Successfully deleted %s" % record["name"]
+ return changed, msg
+ else:
+ self.module.fail_json(
+ msg="Error deleting domain record. [%(status_code)s: %(json)s]"
+ % {"status_code": status_code, "json": json}
+ )
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ state=dict(choices=["present", "absent"], default="present"),
+ oauth_token=dict(
+ aliases=["API_TOKEN"],
+ no_log=True,
+ fallback=(
+ env_fallback,
+ ["DO_API_TOKEN", "DO_API_KEY", "DO_OAUTH_TOKEN"],
+ ),
+ ),
+ force_update=dict(type="bool", default=False),
+ record_id=dict(type="int"),
+ domain=dict(type="str", required=True),
+ type=dict(choices=["A", "AAAA", "CNAME", "MX", "TXT", "SRV", "NS", "CAA"]),
+ name=dict(type="str", default="@"),
+ data=dict(type="str"),
+ priority=dict(type="int"),
+ port=dict(type="int"),
+ weight=dict(type="int"),
+ ttl=dict(type="int", default=1800),
+ tag=dict(choices=["issue", "wildissue", "iodef"]),
+ flags=dict(type="int"),
+ ),
+ # TODO
+ # somehow define the absent requirements: record_id OR ('name', 'type', 'data')
+ required_if=[("state", "present", ("type", "name", "data"))],
+ )
+
+ manager = DigitalOceanDomainRecordManager(module)
+
+ # verify credentials and domain
+ manager.check_credentials()
+ manager.verify_domain()
+
+ state = module.params.get("state")
+
+ if state == "present":
+ changed, result = manager.create_or_update_record()
+ elif state == "absent":
+ changed, result = manager.delete_record()
+
+ module.exit_json(changed=changed, result=result)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_domain_record_info.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_domain_record_info.py
new file mode 100644
index 00000000..b42a7aaa
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_domain_record_info.py
@@ -0,0 +1,227 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: Ansible Project
+# 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: digital_ocean_domain_record_info
+short_description: Gather information about DigitalOcean domain records
+description:
+ - Gather information about DigitalOcean domain records.
+version_added: 1.16.0
+author:
+ - "Adam Papai (@woohgit)"
+ - Mark Mercado (@mamercad)
+options:
+ state:
+ description:
+ - Indicate desired state of the target.
+ default: present
+ choices: ["present"]
+ type: str
+ name:
+ description:
+ - Name of the domain.
+ required: true
+ type: str
+ aliases: ["domain", "domain_name"]
+ record_id:
+ description:
+ - Used to retrieve a specific record.
+ type: int
+ type:
+ description:
+ - The type of record you would like to retrieve.
+ choices: ["A", "AAAA", "CNAME", "MX", "TXT", "SRV", "NS", "CAA"]
+ type: str
+extends_documentation_fragment:
+ - community.digitalocean.digital_ocean.documentation
+notes:
+ - Version 2 of DigitalOcean API is used.
+ - The number of requests that can be made through the API is currently limited to 5,000 per hour per OAuth token.
+"""
+
+EXAMPLES = r"""
+- name: Retrieve all domain records for example.com
+ community.digitalocean.digital_ocean_domain_record_info:
+ state: present
+ oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
+ domain: example.com
+
+- name: Get specific domain record by ID
+ community.digitalocean.digital_ocean_domain_record_info:
+ state: present
+ oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
+ record_id: 12345789
+ register: result
+
+- name: Retrieve all A domain records for example.com
+ community.digitalocean.digital_ocean_domain_record_info:
+ state: present
+ oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
+ domain: example.com
+ type: A
+"""
+
+RETURN = r"""
+data:
+ description: list of DigitalOcean domain records
+ returned: success
+ type: list
+ elements: dict
+ sample:
+ - data: ns1.digitalocean.com
+ flags: null
+ id: 296972269
+ name: '@'
+ port: null
+ priority: null
+ tag: null
+ ttl: 1800
+ type: NS
+ weight: null
+"""
+
+
+from ansible.module_utils.basic import env_fallback
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+
+
+class DigitalOceanDomainRecordManager(DigitalOceanHelper, object):
+ def __init__(self, module):
+ super(DigitalOceanDomainRecordManager, self).__init__(module)
+ self.module = module
+ self.domain = module.params.get("name").lower()
+ self.records = self.__get_all_records()
+ self.payload = self.__build_payload()
+ self.force_update = module.params.get("force_update", False)
+ self.record_id = module.params.get("record_id", None)
+ self.records_by_id = self.__find_record_by_id(self.record_id)
+
+ def check_credentials(self):
+ # Check if oauth_token is valid or not
+ response = self.get("account")
+ if response.status_code == 401:
+ self.module.fail_json(
+ msg="Failed to login using oauth_token, please verify validity of oauth_token"
+ )
+
+ def __get_all_records(self):
+
+ records = []
+ page = 1
+ while True:
+ # GET /v2/domains/$DOMAIN_NAME/records
+ type = self.module.params.get("type")
+ if type:
+ response = self.get(
+ "domains/%(domain)s/records?type=%(type)s&page=%(page)s"
+ % {"domain": self.domain, "type": type, "page": page}
+ )
+ else:
+ response = self.get(
+ "domains/%(domain)s/records?page=%(page)s"
+ % {"domain": self.domain, "page": page}
+ )
+ status_code = response.status_code
+ json = response.json
+
+ if status_code != 200:
+ self.module.exit_json(
+ msg="Error getting domain records [%(status_code)s: %(json)s]"
+ % {"status_code": status_code, "json": json}
+ )
+
+ domain_records = json.get("domain_records", [])
+ for record in domain_records:
+ records.append(dict([(str(k), v) for k, v in record.items()]))
+
+ links = json.get("links")
+ if links:
+ pages = links.get("pages")
+ if pages:
+ if "next" in pages:
+ page += 1
+ else:
+ break
+ else:
+ break
+ else:
+ break
+
+ return records
+
+ def get_records(self):
+ return False, self.records
+
+ def get_records_by_id(self):
+ if self.records_by_id:
+ return False, [self.records_by_id]
+ else:
+ return False, []
+
+ def __find_record_by_id(self, record_id):
+ for record in self.records:
+ if record["id"] == record_id:
+ return record
+ return None
+
+ def __build_payload(self):
+
+ payload = dict(
+ name=self.module.params.get("name"),
+ type=self.module.params.get("type"),
+ )
+
+ payload_data = payload.get("data")
+ if payload_data:
+ # digitalocean stores data: '@' if the data=domain
+ if payload["data"] == self.domain:
+ payload["data"] = "@"
+
+ return payload
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ argument_spec.update(
+ state=dict(choices=["present"], default="present"),
+ name=dict(type="str", aliases=["domain", "domain_name"], required=True),
+ record_id=dict(type="int"),
+ type=dict(
+ type="str",
+ choices=["A", "AAAA", "CNAME", "MX", "TXT", "SRV", "NS", "CAA"],
+ ),
+ )
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+
+ manager = DigitalOceanDomainRecordManager(module)
+
+ # verify credentials and domain
+ manager.check_credentials()
+
+ state = module.params.get("state")
+ record_id = module.params.get("record_id")
+
+ if state == "present":
+ if record_id:
+ changed, result = manager.get_records_by_id()
+ else:
+ changed, result = manager.get_records()
+ module.exit_json(changed=changed, data={"records": result})
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_droplet.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_droplet.py
new file mode 100644
index 00000000..791f2891
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_droplet.py
@@ -0,0 +1,918 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright: Ansible Project
+# 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: digital_ocean_droplet
+short_description: Create and delete a DigitalOcean droplet
+description:
+ - Create and delete a droplet in DigitalOcean and optionally wait for it to be active.
+author:
+ - Gurchet Rai (@gurch101)
+ - Mark Mercado (@mamercad)
+options:
+ state:
+ description:
+ - Indicate desired state of the target.
+ - C(present) will create the named droplet; be mindful of the C(unique_name) parameter.
+ - C(absent) will delete the named droplet, if it exists.
+ - C(active) will create the named droplet (unless it exists) and ensure that it is powered on.
+ - C(inactive) will create the named droplet (unless it exists) and ensure that it is powered off.
+ default: present
+ choices: ["present", "absent", "active", "inactive"]
+ type: str
+ id:
+ description:
+ - The Droplet ID you want to operate on.
+ aliases: ["droplet_id"]
+ type: int
+ name:
+ description:
+ - This is the name of the Droplet.
+ - Must be formatted by hostname rules.
+ type: str
+ unique_name:
+ description:
+ - Require unique hostnames.
+ - By default, DigitalOcean allows multiple hosts with the same name.
+ - Setting this to C(true) allows only one host per name.
+ - Useful for idempotence.
+ default: false
+ type: bool
+ size:
+ description:
+ - This is the slug of the size you would like the Droplet created with.
+ - Please see U(https://slugs.do-api.dev/) for current slugs.
+ aliases: ["size_id"]
+ type: str
+ image:
+ description:
+ - This is the slug of the image you would like the Droplet created with.
+ aliases: ["image_id"]
+ type: str
+ region:
+ description:
+ - This is the slug of the region you would like your Droplet to be created in.
+ aliases: ["region_id"]
+ type: str
+ ssh_keys:
+ description:
+ - Array of SSH key fingerprints that you would like to be added to the Droplet.
+ required: false
+ type: list
+ elements: str
+ firewall:
+ description:
+ - Array of firewall names to apply to the Droplet.
+ - Omitting a firewall name that is currently applied to a droplet will remove it.
+ required: false
+ type: list
+ elements: str
+ private_networking:
+ description:
+ - Add an additional, private network interface to the Droplet (for inter-Droplet communication).
+ default: false
+ type: bool
+ vpc_uuid:
+ description:
+ - A string specifying the UUID of the VPC to which the Droplet will be assigned.
+ - If excluded, the Droplet will be assigned to the account's default VPC for the region.
+ type: str
+ version_added: 0.1.0
+ user_data:
+ description:
+ - Opaque blob of data which is made available to the Droplet.
+ required: False
+ type: str
+ ipv6:
+ description:
+ - Enable IPv6 for the Droplet.
+ required: false
+ default: false
+ type: bool
+ wait:
+ description:
+ - Wait for the Droplet to be active before returning.
+ - If wait is C(false) an IP address may not be returned.
+ required: false
+ default: true
+ type: bool
+ wait_timeout:
+ description:
+ - How long before C(wait) gives up, in seconds, when creating a Droplet.
+ default: 120
+ type: int
+ backups:
+ description:
+ - Indicates whether automated backups should be enabled.
+ required: false
+ default: false
+ type: bool
+ monitoring:
+ description:
+ - Indicates whether to install the DigitalOcean agent for monitoring.
+ required: false
+ default: false
+ type: bool
+ tags:
+ description:
+ - A list of tag names as strings to apply to the Droplet after it is created.
+ - Tag names can either be existing or new tags.
+ required: false
+ type: list
+ elements: str
+ volumes:
+ description:
+ - A list including the unique string identifier for each Block Storage volume to be attached to the Droplet.
+ required: False
+ type: list
+ elements: str
+ resize_disk:
+ description:
+ - Whether to increase disk size on resize.
+ - Only consulted if the C(unique_name) is C(true).
+ - Droplet C(size) must dictate an increase.
+ required: false
+ default: false
+ type: bool
+ project_name:
+ aliases: ["project"]
+ description:
+ - Project to assign the resource to (project name, not UUID).
+ - Defaults to the default project of the account (empty string).
+ - Currently only supported when creating.
+ type: str
+ required: false
+ default: ""
+ sleep_interval:
+ description:
+ - How long to C(sleep) in between action and status checks.
+ - Default is 10 seconds; this should be less than C(wait_timeout) and nonzero.
+ default: 10
+ type: int
+extends_documentation_fragment:
+- community.digitalocean.digital_ocean.documentation
+"""
+
+
+EXAMPLES = r"""
+- name: Create a new Droplet
+ community.digitalocean.digital_ocean_droplet:
+ state: present
+ oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
+ name: mydroplet
+ size: s-1vcpu-1gb
+ region: sfo3
+ image: ubuntu-20-04-x64
+ wait_timeout: 500
+ ssh_keys: [ .... ]
+ register: my_droplet
+
+- name: Show Droplet info
+ ansible.builtin.debug:
+ msg: |
+ Droplet ID is {{ my_droplet.data.droplet.id }}
+ First Public IPv4 is {{ (my_droplet.data.droplet.networks.v4 | selectattr('type', 'equalto', 'public')).0.ip_address | default('<none>', true) }}
+ First Private IPv4 is {{ (my_droplet.data.droplet.networks.v4 | selectattr('type', 'equalto', 'private')).0.ip_address | default('<none>', true) }}
+
+- name: Create a new Droplet (and assign to Project "test")
+ community.digitalocean.digital_ocean_droplet:
+ state: present
+ oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
+ name: mydroplet
+ size: s-1vcpu-1gb
+ region: sfo3
+ image: ubuntu-20-04-x64
+ wait_timeout: 500
+ ssh_keys: [ .... ]
+ project: test
+ register: my_droplet
+
+- name: Ensure a Droplet is present
+ community.digitalocean.digital_ocean_droplet:
+ state: present
+ oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
+ id: 123
+ name: mydroplet
+ size: s-1vcpu-1gb
+ region: sfo3
+ image: ubuntu-20-04-x64
+ wait_timeout: 500
+
+- name: Ensure a Droplet is present and has firewall rules applied
+ community.digitalocean.digital_ocean_droplet:
+ state: present
+ oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
+ id: 123
+ name: mydroplet
+ size: s-1vcpu-1gb
+ region: sfo3
+ image: ubuntu-20-04-x64
+ firewall: ['myfirewall', 'anotherfirewall']
+ wait_timeout: 500
+
+- name: Ensure a Droplet is present with SSH keys installed
+ community.digitalocean.digital_ocean_droplet:
+ state: present
+ oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
+ id: 123
+ name: mydroplet
+ size: s-1vcpu-1gb
+ region: sfo3
+ ssh_keys: ['1534404', '1784768']
+ image: ubuntu-20-04-x64
+ wait_timeout: 500
+"""
+
+RETURN = r"""
+# Digital Ocean API info https://docs.digitalocean.com/reference/api/api-reference/#tag/Droplets
+data:
+ description: a DigitalOcean Droplet
+ returned: changed
+ type: dict
+ sample:
+ ip_address: 104.248.118.172
+ ipv6_address: 2604:a880:400:d1::90a:6001
+ private_ipv4_address: 10.136.122.141
+ droplet:
+ id: 3164494
+ name: example.com
+ memory: 512
+ vcpus: 1
+ disk: 20
+ locked: true
+ status: new
+ kernel:
+ id: 2233
+ name: Ubuntu 14.04 x64 vmlinuz-3.13.0-37-generic
+ version: 3.13.0-37-generic
+ created_at: "2014-11-14T16:36:31Z"
+ features: ["virtio"]
+ backup_ids: []
+ snapshot_ids: []
+ image: {}
+ volume_ids: []
+ size: {}
+ size_slug: 512mb
+ networks: {}
+ region: {}
+ tags: ["web"]
+msg:
+ description: Informational or error message encountered during execution
+ returned: changed
+ type: str
+ sample: No project named test2 found
+assign_status:
+ description: Assignment status (ok, not_found, assigned, already_assigned, service_down)
+ returned: changed
+ type: str
+ sample: assigned
+resources:
+ description: Resource assignment involved in project assignment
+ returned: changed
+ type: dict
+ sample:
+ assigned_at: '2021-10-25T17:39:38Z'
+ links:
+ self: https://api.digitalocean.com/v2/droplets/3164494
+ status: assigned
+ urn: do:droplet:3164494
+"""
+
+import time
+from ansible.module_utils.basic import AnsibleModule, env_fallback
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+ DigitalOceanProjects,
+)
+
+
+class DODroplet(object):
+
+ failure_message = {
+ "empty_response": "Empty response from the DigitalOcean API; please try again or open a bug if it never "
+ "succeeds.",
+ "resizing_off": "Droplet must be off prior to resizing: "
+ "https://docs.digitalocean.com/reference/api/api-reference/#operation/post_droplet_action",
+ "unexpected": "Unexpected error [{0}]; please file a bug: "
+ "https://github.com/ansible-collections/community.digitalocean/issues",
+ "support_action": "Error status on Droplet action [{0}], please try again or contact DigitalOcean support: "
+ "https://docs.digitalocean.com/support/",
+ "failed_to": "Failed to {0} {1} [HTTP {2}: {3}]",
+ }
+
+ def __init__(self, module):
+ self.rest = DigitalOceanHelper(module)
+ self.module = module
+ self.wait = self.module.params.pop("wait", True)
+ self.wait_timeout = self.module.params.pop("wait_timeout", 120)
+ self.unique_name = self.module.params.pop("unique_name", False)
+ # pop the oauth token so we don't include it in the POST data
+ self.module.params.pop("oauth_token")
+ self.id = None
+ self.name = None
+ self.size = None
+ self.status = None
+ if self.module.params.get("project"):
+ # only load for non-default project assignments
+ self.projects = DigitalOceanProjects(module, self.rest)
+ self.firewalls = self.get_firewalls()
+ self.sleep_interval = self.module.params.pop("sleep_interval", 10)
+ if self.wait:
+ if self.sleep_interval > self.wait_timeout:
+ self.module.fail_json(
+ msg="Sleep interval {0} should be less than {1}".format(
+ self.sleep_interval, self.wait_timeout
+ )
+ )
+ if self.sleep_interval <= 0:
+ self.module.fail_json(
+ msg="Sleep interval {0} should be greater than zero".format(
+ self.sleep_interval
+ )
+ )
+
+ def get_firewalls(self):
+ response = self.rest.get("firewalls")
+ status_code = response.status_code
+ json_data = response.json
+ if status_code != 200:
+ self.module.fail_json(msg="Failed to get firewalls", data=json_data)
+
+ return self.rest.get_paginated_data(
+ base_url="firewalls?", data_key_name="firewalls"
+ )
+
+ def get_firewall_by_name(self):
+ rule = {}
+ item = 0
+ for firewall in self.firewalls:
+ for firewall_name in self.module.params["firewall"]:
+ if firewall_name in firewall["name"]:
+ rule[item] = {}
+ rule[item].update(firewall)
+ item += 1
+ if len(rule) > 0:
+ return rule
+ return None
+
+ def add_droplet_to_firewalls(self):
+ changed = False
+ rule = self.get_firewall_by_name()
+ if rule is None:
+ err = "Failed to find firewalls: {0}".format(self.module.params["firewall"])
+ return err
+ json_data = self.get_droplet()
+ if json_data is not None:
+ request_params = {}
+ droplet = json_data.get("droplet", None)
+ droplet_id = droplet.get("id", None)
+ request_params["droplet_ids"] = [droplet_id]
+ for firewall in rule:
+ if droplet_id not in rule[firewall]["droplet_ids"]:
+ response = self.rest.post(
+ "firewalls/{0}/droplets".format(rule[firewall]["id"]),
+ data=request_params,
+ )
+ json_data = response.json
+ status_code = response.status_code
+ if status_code != 204:
+ err = "Failed to add droplet {0} to firewall {1}".format(
+ droplet_id, rule[firewall]["id"]
+ )
+ return err, changed
+ changed = True
+ return None, changed
+
+ def remove_droplet_from_firewalls(self):
+ changed = False
+ json_data = self.get_droplet()
+ if json_data is not None:
+ request_params = {}
+ droplet = json_data.get("droplet", None)
+ droplet_id = droplet.get("id", None)
+ request_params["droplet_ids"] = [droplet_id]
+ for firewall in self.firewalls:
+ if (
+ firewall["name"] not in self.module.params["firewall"]
+ and droplet_id in firewall["droplet_ids"]
+ ):
+ response = self.rest.delete(
+ "firewalls/{0}/droplets".format(firewall["id"]),
+ data=request_params,
+ )
+ json_data = response.json
+ status_code = response.status_code
+ if status_code != 204:
+ err = "Failed to remove droplet {0} from firewall {1}".format(
+ droplet_id, firewall["id"]
+ )
+ return err, changed
+ changed = True
+ return None, changed
+
+ def get_by_id(self, droplet_id):
+ if not droplet_id:
+ return None
+ response = self.rest.get("droplets/{0}".format(droplet_id))
+ status_code = response.status_code
+ json_data = response.json
+ if json_data is None:
+ self.module.fail_json(
+ changed=False,
+ msg=DODroplet.failure_message["empty_response"],
+ )
+ else:
+ if status_code == 200:
+ droplet = json_data.get("droplet", None)
+ if droplet is not None:
+ self.id = droplet.get("id", None)
+ self.name = droplet.get("name", None)
+ self.size = droplet.get("size_slug", None)
+ self.status = droplet.get("status", None)
+ return json_data
+ return None
+
+ def get_by_name(self, droplet_name):
+ if not droplet_name:
+ return None
+ page = 1
+ while page is not None:
+ response = self.rest.get("droplets?page={0}".format(page))
+ json_data = response.json
+ status_code = response.status_code
+ if json_data is None:
+ self.module.fail_json(
+ changed=False,
+ msg=DODroplet.failure_message["empty_response"],
+ )
+ else:
+ if status_code == 200:
+ droplets = json_data.get("droplets", [])
+ for droplet in droplets:
+ if droplet.get("name", None) == droplet_name:
+ self.id = droplet.get("id", None)
+ self.name = droplet.get("name", None)
+ self.size = droplet.get("size_slug", None)
+ self.status = droplet.get("status", None)
+ return {"droplet": droplet}
+ if (
+ "links" in json_data
+ and "pages" in json_data["links"]
+ and "next" in json_data["links"]["pages"]
+ ):
+ page += 1
+ else:
+ page = None
+ return None
+
+ def get_addresses(self, data):
+ """Expose IP addresses as their own property allowing users extend to additional tasks"""
+ _data = data
+ for k, v in data.items():
+ setattr(self, k, v)
+ networks = _data["droplet"]["networks"]
+ for network in networks.get("v4", []):
+ if network["type"] == "public":
+ _data["ip_address"] = network["ip_address"]
+ else:
+ _data["private_ipv4_address"] = network["ip_address"]
+ for network in networks.get("v6", []):
+ if network["type"] == "public":
+ _data["ipv6_address"] = network["ip_address"]
+ else:
+ _data["private_ipv6_address"] = network["ip_address"]
+ return _data
+
+ def get_droplet(self):
+ json_data = self.get_by_id(self.module.params["id"])
+ if not json_data and self.unique_name:
+ json_data = self.get_by_name(self.module.params["name"])
+ return json_data
+
+ def resize_droplet(self, state, droplet_id):
+ if self.status != "off":
+ self.module.fail_json(
+ changed=False,
+ msg=DODroplet.failure_message["resizing_off"],
+ )
+
+ self.wait_action(
+ droplet_id,
+ {
+ "type": "resize",
+ "disk": self.module.params["resize_disk"],
+ "size": self.module.params["size"],
+ },
+ )
+
+ if state == "active":
+ self.ensure_power_on(droplet_id)
+
+ # Get updated Droplet data
+ json_data = self.get_droplet()
+ droplet = json_data.get("droplet", None)
+ if droplet is None:
+ self.module.fail_json(
+ changed=False,
+ msg=DODroplet.failure_message["unexpected"].format("no Droplet"),
+ )
+
+ self.module.exit_json(
+ changed=True,
+ msg="Resized Droplet {0} ({1}) from {2} to {3}".format(
+ self.name, self.id, self.size, self.module.params["size"]
+ ),
+ data={"droplet": droplet},
+ )
+
+ def wait_status(self, droplet_id, desired_statuses):
+ # Make sure Droplet is active first
+ end_time = time.monotonic() + self.wait_timeout
+ while time.monotonic() < end_time:
+ response = self.rest.get("droplets/{0}".format(droplet_id))
+ json_data = response.json
+ status_code = response.status_code
+ message = json_data.get("message", "no error message")
+ droplet = json_data.get("droplet", None)
+ droplet_status = droplet.get("status", None) if droplet else None
+
+ if droplet is None or droplet_status is None:
+ self.module.fail_json(
+ changed=False,
+ msg=DODroplet.failure_message["unexpected"].format(
+ "no Droplet or status"
+ ),
+ )
+
+ if status_code >= 400:
+ self.module.fail_json(
+ changed=False,
+ msg=DODroplet.failure_message["failed_to"].format(
+ "get", "Droplet", status_code, message
+ ),
+ )
+
+ if droplet_status in desired_statuses:
+ return
+
+ time.sleep(self.sleep_interval)
+
+ self.module.fail_json(
+ msg="Wait for Droplet [{0}] status timeout".format(
+ ",".join(desired_statuses)
+ )
+ )
+
+ def wait_check_action(self, droplet_id, action_id):
+ end_time = time.monotonic() + self.wait_timeout
+ while time.monotonic() < end_time:
+ response = self.rest.get(
+ "droplets/{0}/actions/{1}".format(droplet_id, action_id)
+ )
+ json_data = response.json
+ status_code = response.status_code
+ message = json_data.get("message", "no error message")
+ action = json_data.get("action", None)
+ action_id = action.get("id", None)
+ action_status = action.get("status", None)
+
+ if action is None or action_id is None or action_status is None:
+ self.module.fail_json(
+ changed=False,
+ msg=DODroplet.failure_message["unexpected"].format(
+ "no action, ID, or status"
+ ),
+ )
+
+ if status_code >= 400:
+ self.module.fail_json(
+ changed=False,
+ msg=DODroplet.failure_message["failed_to"].format(
+ "get", "action", status_code, message
+ ),
+ )
+
+ if action_status == "errored":
+ self.module.fail_json(
+ changed=True,
+ msg=DODroplet.failure_message["support_action"].format(action_id),
+ )
+
+ if action_status == "completed":
+ return
+
+ time.sleep(self.sleep_interval)
+
+ self.module.fail_json(msg="Wait for Droplet action timeout")
+
+ def wait_action(self, droplet_id, desired_action_data):
+ action_type = desired_action_data.get("type", "undefined")
+
+ response = self.rest.post(
+ "droplets/{0}/actions".format(droplet_id), data=desired_action_data
+ )
+ json_data = response.json
+ status_code = response.status_code
+ message = json_data.get("message", "no error message")
+ action = json_data.get("action", None)
+ action_id = action.get("id", None)
+ action_status = action.get("status", None)
+
+ if action is None or action_id is None or action_status is None:
+ self.module.fail_json(
+ changed=False,
+ msg=DODroplet.failure_message["unexpected"].format(
+ "no action, ID, or status"
+ ),
+ )
+
+ if status_code >= 400:
+ self.module.fail_json(
+ changed=False,
+ msg=DODroplet.failure_message["failed_to"].format(
+ "post", "action", status_code, message
+ ),
+ )
+
+ # Keep checking till it is done or times out
+ self.wait_check_action(droplet_id, action_id)
+
+ def ensure_power_on(self, droplet_id):
+ # Make sure Droplet is active or off first
+ self.wait_status(droplet_id, ["active", "off"])
+ # Trigger power-on
+ self.wait_action(droplet_id, {"type": "power_on"})
+
+ def ensure_power_off(self, droplet_id):
+ # Make sure Droplet is active first
+ self.wait_status(droplet_id, ["active"])
+ # Trigger power-off
+ self.wait_action(droplet_id, {"type": "power_off"})
+
+ def create(self, state):
+ json_data = self.get_droplet()
+ # We have the Droplet
+ if json_data is not None:
+ droplet = json_data.get("droplet", None)
+ droplet_id = droplet.get("id", None)
+ droplet_size = droplet.get("size_slug", None)
+
+ if droplet_id is None or droplet_size is None:
+ self.module.fail_json(
+ changed=False,
+ msg=DODroplet.failure_message["unexpected"].format(
+ "no Droplet ID or size"
+ ),
+ )
+
+ # Add droplet to a firewall if specified
+ if self.module.params["firewall"] is not None:
+ firewall_changed = False
+ if len(self.module.params["firewall"]) > 0:
+ firewall_add, add_changed = self.add_droplet_to_firewalls()
+ if firewall_add is not None:
+ self.module.fail_json(
+ changed=False,
+ msg=firewall_add,
+ data={"droplet": droplet, "firewall": firewall_add},
+ )
+ firewall_changed = firewall_changed or add_changed
+ firewall_remove, remove_changed = self.remove_droplet_from_firewalls()
+ if firewall_remove is not None:
+ self.module.fail_json(
+ changed=False,
+ msg=firewall_remove,
+ data={"droplet": droplet, "firewall": firewall_remove},
+ )
+ firewall_changed = firewall_changed or remove_changed
+ self.module.exit_json(
+ changed=firewall_changed,
+ data={"droplet": droplet},
+ )
+
+ # Check mode
+ if self.module.check_mode:
+ self.module.exit_json(changed=False)
+
+ # Ensure Droplet size
+ if droplet_size != self.module.params.get("size", None):
+ self.resize_droplet(state, droplet_id)
+
+ # Ensure Droplet power state
+ droplet_data = self.get_addresses(json_data)
+ droplet_id = droplet.get("id", None)
+ droplet_status = droplet.get("status", None)
+ if droplet_id is not None and droplet_status is not None:
+ if state == "active" and droplet_status != "active":
+ self.ensure_power_on(droplet_id)
+ # Get updated Droplet data (fallback to current data)
+ json_data = self.get_droplet()
+ droplet = json_data.get("droplet", droplet)
+ self.module.exit_json(changed=True, data={"droplet": droplet})
+ elif state == "inactive" and droplet_status != "off":
+ self.ensure_power_off(droplet_id)
+ # Get updated Droplet data (fallback to current data)
+ json_data = self.get_droplet()
+ droplet = json_data.get("droplet", droplet)
+ self.module.exit_json(changed=True, data={"droplet": droplet})
+ else:
+ self.module.exit_json(changed=False, data={"droplet": droplet})
+
+ # We don't have the Droplet, create it
+
+ # Check mode
+ if self.module.check_mode:
+ self.module.exit_json(changed=True)
+
+ request_params = dict(self.module.params)
+ del request_params["id"]
+
+ response = self.rest.post("droplets", data=request_params)
+ json_data = response.json
+ status_code = response.status_code
+ message = json_data.get("message", "no error message")
+ droplet = json_data.get("droplet", None)
+
+ # Ensure that the Droplet is created
+ if status_code != 202:
+ self.module.fail_json(
+ changed=False,
+ msg=DODroplet.failure_message["failed_to"].format(
+ "create", "Droplet", status_code, message
+ ),
+ )
+
+ droplet_id = droplet.get("id", None)
+ if droplet is None or droplet_id is None:
+ self.module.fail_json(
+ changed=False,
+ msg=DODroplet.failure_message["unexpected"].format("no Droplet or ID"),
+ )
+
+ if status_code >= 400:
+ self.module.fail_json(
+ changed=False,
+ msg=DODroplet.failure_message["failed_to"].format(
+ "create", "Droplet", status_code, message
+ ),
+ )
+
+ if self.wait:
+ if state == "present" or state == "active":
+ self.ensure_power_on(droplet_id)
+ if state == "inactive":
+ self.ensure_power_off(droplet_id)
+ else:
+ if state == "inactive":
+ self.ensure_power_off(droplet_id)
+
+ # Get updated Droplet data (fallback to current data)
+ if self.wait:
+ json_data = self.get_by_id(droplet_id)
+ if json_data:
+ droplet = json_data.get("droplet", droplet)
+
+ project_name = self.module.params.get("project")
+ if project_name: # empty string is the default project, skip project assignment
+ urn = "do:droplet:{0}".format(droplet_id)
+ assign_status, error_message, resources = self.projects.assign_to_project(
+ project_name, urn
+ )
+ self.module.exit_json(
+ changed=True,
+ data={"droplet": droplet},
+ msg=error_message,
+ assign_status=assign_status,
+ resources=resources,
+ )
+ # Add droplet to firewall if specified
+ if self.module.params["firewall"] is not None:
+ # raise Exception(self.module.params["firewall"])
+ firewall_add = self.add_droplet_to_firewalls()
+ if firewall_add is not None:
+ self.module.fail_json(
+ changed=False,
+ msg=firewall_add,
+ data={"droplet": droplet, "firewall": firewall_add},
+ )
+ firewall_remove = self.remove_droplet_from_firewalls()
+ if firewall_remove is not None:
+ self.module.fail_json(
+ changed=False,
+ msg=firewall_remove,
+ data={"droplet": droplet, "firewall": firewall_remove},
+ )
+ self.module.exit_json(changed=True, data={"droplet": droplet})
+
+ self.module.exit_json(changed=True, data={"droplet": droplet})
+
+ def delete(self):
+ # to delete a droplet we need to know the droplet id or unique name, ie
+ # name is not None and unique_name is True, but as "id or name" is
+ # enforced elsewhere, we only need to enforce "id or unique_name" here
+ if not self.module.params["id"] and not self.unique_name:
+ self.module.fail_json(
+ changed=False,
+ msg="id must be set or unique_name must be true for deletes",
+ )
+ json_data = self.get_droplet()
+ if json_data is None:
+ self.module.exit_json(changed=False, msg="Droplet not found")
+
+ # Check mode
+ if self.module.check_mode:
+ self.module.exit_json(changed=True)
+
+ # Delete it
+ droplet = json_data.get("droplet", None)
+ droplet_id = droplet.get("id", None)
+ droplet_name = droplet.get("name", None)
+
+ if droplet is None or droplet_id is None:
+ self.module.fail_json(
+ changed=False,
+ msg=DODroplet.failure_message["unexpected"].format(
+ "no Droplet, name, or ID"
+ ),
+ )
+
+ response = self.rest.delete("droplets/{0}".format(droplet_id))
+ json_data = response.json
+ status_code = response.status_code
+ if status_code == 204:
+ self.module.exit_json(
+ changed=True,
+ msg="Droplet {0} ({1}) deleted".format(droplet_name, droplet_id),
+ )
+ else:
+ self.module.fail_json(
+ changed=False,
+ msg="Failed to delete Droplet {0} ({1})".format(
+ droplet_name, droplet_id
+ ),
+ )
+
+
+def core(module):
+ state = module.params.pop("state")
+ droplet = DODroplet(module)
+ if state in ["present", "active", "inactive"]:
+ droplet.create(state)
+ elif state == "absent":
+ droplet.delete()
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ argument_spec.update(
+ state=dict(
+ choices=["present", "absent", "active", "inactive"], default="present"
+ ),
+ name=dict(type="str"),
+ size=dict(aliases=["size_id"]),
+ image=dict(aliases=["image_id"]),
+ region=dict(aliases=["region_id"]),
+ ssh_keys=dict(type="list", elements="str", no_log=False),
+ private_networking=dict(type="bool", default=False),
+ vpc_uuid=dict(type="str"),
+ backups=dict(type="bool", default=False),
+ monitoring=dict(type="bool", default=False),
+ id=dict(aliases=["droplet_id"], type="int"),
+ user_data=dict(default=None),
+ ipv6=dict(type="bool", default=False),
+ volumes=dict(type="list", elements="str"),
+ tags=dict(type="list", elements="str"),
+ wait=dict(type="bool", default=True),
+ wait_timeout=dict(default=120, type="int"),
+ unique_name=dict(type="bool", default=False),
+ resize_disk=dict(type="bool", default=False),
+ project_name=dict(type="str", aliases=["project"], required=False, default=""),
+ firewall=dict(type="list", elements="str", default=None),
+ sleep_interval=dict(default=10, type="int"),
+ )
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ required_one_of=(["id", "name"],),
+ required_if=(
+ [
+ ("state", "present", ["name", "size", "image", "region"]),
+ ("state", "active", ["name", "size", "image", "region"]),
+ ("state", "inactive", ["name", "size", "image", "region"]),
+ ]
+ ),
+ supports_check_mode=True,
+ )
+
+ core(module)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_droplet_info.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_droplet_info.py
new file mode 100644
index 00000000..474b9af2
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_droplet_info.py
@@ -0,0 +1,266 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2018, Ansible Project
+# Copyright: (c) 2020, Tyler Auerbeck <tauerbec@redhat.com>
+# 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: digital_ocean_droplet_info
+short_description: Gather information about DigitalOcean Droplets
+description:
+ - This module can be used to gather information about Droplets.
+author: "Tyler Auerbeck (@tylerauerbeck)"
+version_added: 1.4.0
+
+options:
+ id:
+ description:
+ - Droplet ID that can be used to identify and reference a droplet.
+ type: str
+ name:
+ description:
+ - Droplet name that can be used to identify and reference a droplet.
+ type: str
+
+extends_documentation_fragment:
+- community.digitalocean.digital_ocean
+"""
+
+
+EXAMPLES = r"""
+- name: Gather information about all droplets
+ community.digitalocean.digital_ocean_droplet_info:
+ oauth_token: "{{ oauth_token }}"
+
+- name: Gather information about a specific droplet by name
+ community.digitalocean.digital_ocean_droplet_info:
+ oauth_token: "{{ oauth_token }}"
+ name: my-droplet-name
+
+- name: Gather information about a specific droplet by id
+ community.digitalocean.digital_ocean_droplet_info:
+ oauth_token: "{{ oauth_token }}"
+ id: abc-123-d45
+
+- name: Get information about all droplets to loop through
+ community.digitalocean.digital_ocean_droplet_info:
+ oauth_token: "{{ oauth_token }}"
+ register: droplets
+
+- name: Get number of droplets
+ set_fact:
+ droplet_count: "{{ droplets.data | length }}"
+"""
+
+RETURN = r"""
+data:
+ description: "DigitalOcean droplet information"
+ elements: dict
+ returned: success
+ sample:
+ - backup_ids: []
+ created_at: "2021-04-07T00:44:53Z"
+ disk: 25
+ features:
+ - private_networking
+ id: 123456789
+ image:
+ created_at: "2020-10-20T08:49:55Z"
+ description: "Ubuntu 18.04 x86 image"
+ distribution: Ubuntu
+ id: 987654321
+ min_disk_size: 15
+ name: "18.04 (LTS) x64"
+ public: false
+ regions: []
+ size_gigabytes: 0.34
+ slug: ~
+ status: retired
+ tags: []
+ type: base
+ kernel: ~
+ locked: false
+ memory: 1024
+ name: my-droplet-01
+ networks:
+ v4:
+ - gateway: ""
+ ip_address: "1.2.3.4"
+ netmask: "255.255.240.0"
+ type: private
+ - gateway: "5.6.7.8"
+ ip_address: "4.3.2.1"
+ netmask: "255.255.240.0"
+ type: public
+ v6: []
+ next_backup_window: ~
+ region:
+ available: true
+ features:
+ - backups
+ - ipv6
+ - metadata
+ - install_agent
+ - storage
+ - image_transfer
+ name: "New York 1"
+ sizes:
+ - s-1vcpu-1gb
+ - s-1vcpu-1gb-intel
+ - s-1vcpu-2gb
+ - s-1vcpu-2gb-intel
+ - s-2vcpu-2gb
+ - s-2vcpu-2gb-intel
+ - s-2vcpu-4gb
+ - s-2vcpu-4gb-intel
+ - s-4vcpu-8gb
+ - c-2
+ - c2-2vcpu-4gb
+ - s-4vcpu-8gb-intel
+ - g-2vcpu-8gb
+ - gd-2vcpu-8gb
+ - s-8vcpu-16gb
+ - m-2vcpu-16gb
+ - c-4
+ - c2-4vcpu-8gb
+ - s-8vcpu-16gb-intel
+ - m3-2vcpu-16gb
+ - g-4vcpu-16gb
+ - so-2vcpu-16gb
+ - m6-2vcpu-16gb
+ - gd-4vcpu-16gb
+ - so1_5-2vcpu-16gb
+ - m-4vcpu-32gb
+ - c-8
+ - c2-8vcpu-16gb
+ - m3-4vcpu-32gb
+ - g-8vcpu-32gb
+ - so-4vcpu-32gb
+ - m6-4vcpu-32gb
+ - gd-8vcpu-32gb
+ - so1_5-4vcpu-32gb
+ - m-8vcpu-64gb
+ - c-16
+ - c2-16vcpu-32gb
+ - m3-8vcpu-64gb
+ - g-16vcpu-64gb
+ - so-8vcpu-64gb
+ - m6-8vcpu-64gb
+ - gd-16vcpu-64gb
+ - so1_5-8vcpu-64gb
+ - m-16vcpu-128gb
+ - c-32
+ - c2-32vcpu-64gb
+ - m3-16vcpu-128gb
+ - m-24vcpu-192gb
+ - g-32vcpu-128gb
+ - so-16vcpu-128gb
+ - m6-16vcpu-128gb
+ - gd-32vcpu-128gb
+ - m3-24vcpu-192gb
+ - g-40vcpu-160gb
+ - so1_5-16vcpu-128gb
+ - m-32vcpu-256gb
+ - gd-40vcpu-160gb
+ - so-24vcpu-192gb
+ - m6-24vcpu-192gb
+ - m3-32vcpu-256gb
+ - so1_5-24vcpu-192gb
+ - so-32vcpu-256gb
+ - m6-32vcpu-256gb
+ - so1_5-32vcpu-256gb
+ slug: nyc1
+ size:
+ available: true
+ description: Basic
+ disk: 25
+ memory: 1024
+ price_hourly: 0.00744
+ price_monthly: 5.0
+ regions:
+ - ams2
+ - ams3
+ - blr1
+ - fra1
+ - lon1
+ - nyc1
+ - nyc2
+ - nyc3
+ - sfo1
+ - sfo3
+ - sgp1
+ - tor1
+ slug: s-1vcpu-1gb
+ transfer: 1.0
+ vcpus: 1
+ size_slug: s-1vcpu-1gb
+ snapshot_ids: []
+ status: active
+ tags:
+ - tag1
+ vcpus: 1
+ volume_ids: []
+ vpc_uuid: 123-abc-567a
+ type: list
+"""
+
+from traceback import format_exc
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+
+
+def run(module):
+ rest = DigitalOceanHelper(module)
+
+ if module.params["id"]:
+ path = "droplets/" + module.params["id"]
+ response = rest.get(path)
+ if response.status_code != 200:
+ module.fail_json(
+ msg="Failed to fetch 'droplets' information due to error: %s"
+ % response.json["message"]
+ )
+ else:
+ response = rest.get_paginated_data(
+ base_url="droplets?", data_key_name="droplets"
+ )
+
+ if module.params["id"]:
+ data = [response.json["droplet"]]
+ elif module.params["name"]:
+ data = [d for d in response if d["name"] == module.params["name"]]
+ if not data:
+ module.fail_json(
+ msg="Failed to fetch 'droplets' information due to error: Unable to find droplet with name %s"
+ % module.params["name"]
+ )
+ else:
+ data = response
+
+ module.exit_json(changed=False, data=data)
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ argument_spec.update(
+ name=dict(type="str", required=False, default=None),
+ id=dict(type="str", required=False, default=None),
+ )
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ mutually_exclusive=[("id", "name")],
+ )
+ run(module)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_firewall.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_firewall.py
new file mode 100644
index 00000000..24b7c420
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_firewall.py
@@ -0,0 +1,560 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2018, Ansible Project
+# Copyright: (c) 2018, Anthony Bond <ajbond2005@gmail.com>
+# 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: digital_ocean_firewall
+short_description: Manage cloud firewalls within DigitalOcean
+description:
+ - This module can be used to add or remove firewalls on the DigitalOcean cloud platform.
+author:
+ - Anthony Bond (@BondAnthony)
+ - Lucas Basquerotto (@lucasbasquerotto)
+version_added: "1.1.0"
+options:
+ name:
+ type: str
+ description:
+ - Name of the firewall rule to create or manage
+ required: true
+ state:
+ type: str
+ choices: ['present', 'absent']
+ default: present
+ description:
+ - Assert the state of the firewall rule. Set to 'present' to create or update and 'absent' to remove.
+ droplet_ids:
+ type: list
+ elements: str
+ description:
+ - List of droplet ids to be assigned to the firewall
+ required: false
+ tags:
+ type: list
+ elements: str
+ description:
+ - List of tags to be assigned to the firewall
+ required: false
+ inbound_rules:
+ type: list
+ elements: dict
+ description:
+ - Firewall rules specifically targeting inbound network traffic into DigitalOcean
+ required: false
+ suboptions:
+ protocol:
+ type: str
+ choices: ['udp', 'tcp', 'icmp']
+ default: tcp
+ description:
+ - Network protocol to be accepted.
+ required: false
+ ports:
+ type: str
+ description:
+ - The ports on which traffic will be allowed, single, range, or all
+ required: true
+ sources:
+ type: dict
+ description:
+ - Dictionary of locations from which inbound traffic will be accepted
+ required: true
+ suboptions:
+ addresses:
+ type: list
+ elements: str
+ description:
+ - List of strings containing the IPv4 addresses, IPv6 addresses, IPv4 CIDRs,
+ and/or IPv6 CIDRs to which the firewall will allow traffic
+ required: false
+ droplet_ids:
+ type: list
+ elements: str
+ description:
+ - List of integers containing the IDs of the Droplets to which the firewall will allow traffic
+ required: false
+ load_balancer_uids:
+ type: list
+ elements: str
+ description:
+ - List of strings containing the IDs of the Load Balancers to which the firewall will allow traffic
+ required: false
+ tags:
+ type: list
+ elements: str
+ description:
+ - List of strings containing the names of Tags corresponding to groups of Droplets to
+ which the Firewall will allow traffic
+ required: false
+ outbound_rules:
+ type: list
+ elements: dict
+ description:
+ - Firewall rules specifically targeting outbound network traffic from DigitalOcean
+ required: false
+ suboptions:
+ protocol:
+ type: str
+ choices: ['udp', 'tcp', 'icmp']
+ default: tcp
+ description:
+ - Network protocol to be accepted.
+ required: false
+ ports:
+ type: str
+ description:
+ - The ports on which traffic will be allowed, single, range, or all
+ required: true
+ destinations:
+ type: dict
+ description:
+ - Dictionary of locations from which outbound traffic will be allowed
+ required: true
+ suboptions:
+ addresses:
+ type: list
+ elements: str
+ description:
+ - List of strings containing the IPv4 addresses, IPv6 addresses, IPv4 CIDRs,
+ and/or IPv6 CIDRs to which the firewall will allow traffic
+ required: false
+ droplet_ids:
+ type: list
+ elements: str
+ description:
+ - List of integers containing the IDs of the Droplets to which the firewall will allow traffic
+ required: false
+ load_balancer_uids:
+ type: list
+ elements: str
+ description:
+ - List of strings containing the IDs of the Load Balancers to which the firewall will allow traffic
+ required: false
+ tags:
+ type: list
+ elements: str
+ description:
+ - List of strings containing the names of Tags corresponding to groups of Droplets to
+ which the Firewall will allow traffic
+ required: false
+extends_documentation_fragment:
+ - community.digitalocean.digital_ocean.documentation
+"""
+
+EXAMPLES = """
+# Allows tcp connections to port 22 (SSH) from specific sources
+# Allows tcp connections to ports 80 and 443 from any source
+# Allows outbound access to any destination for protocols tcp, udp and icmp
+# The firewall rules will be applied to any droplets with the tag "sample"
+- name: Create a Firewall named my-firewall
+ digital_ocean_firewall:
+ name: my-firewall
+ state: present
+ inbound_rules:
+ - protocol: "tcp"
+ ports: "22"
+ sources:
+ addresses: ["1.2.3.4"]
+ droplet_ids: ["my_droplet_id_1", "my_droplet_id_2"]
+ load_balancer_uids: ["my_lb_id_1", "my_lb_id_2"]
+ tags: ["tag_1", "tag_2"]
+ - protocol: "tcp"
+ ports: "80"
+ sources:
+ addresses: ["0.0.0.0/0", "::/0"]
+ - protocol: "tcp"
+ ports: "443"
+ sources:
+ addresses: ["0.0.0.0/0", "::/0"]
+ outbound_rules:
+ - protocol: "tcp"
+ ports: "1-65535"
+ destinations:
+ addresses: ["0.0.0.0/0", "::/0"]
+ - protocol: "udp"
+ ports: "1-65535"
+ destinations:
+ addresses: ["0.0.0.0/0", "::/0"]
+ - protocol: "icmp"
+ ports: "1-65535"
+ destinations:
+ addresses: ["0.0.0.0/0", "::/0"]
+ droplet_ids: []
+ tags: ["sample"]
+"""
+
+RETURN = """
+data:
+ description: DigitalOcean firewall resource
+ returned: success
+ type: dict
+ sample: {
+ "created_at": "2020-08-11T18:41:30Z",
+ "droplet_ids": [],
+ "id": "7acd6ee2-257b-434f-8909-709a5816d4f9",
+ "inbound_rules": [
+ {
+ "ports": "443",
+ "protocol": "tcp",
+ "sources": {
+ "addresses": [
+ "1.2.3.4"
+ ],
+ "droplet_ids": [
+ "my_droplet_id_1",
+ "my_droplet_id_2"
+ ],
+ "load_balancer_uids": [
+ "my_lb_id_1",
+ "my_lb_id_2"
+ ],
+ "tags": [
+ "tag_1",
+ "tag_2"
+ ]
+ }
+ },
+ {
+ "sources": {
+ "addresses": [
+ "0.0.0.0/0",
+ "::/0"
+ ]
+ },
+ "ports": "80",
+ "protocol": "tcp"
+ },
+ {
+ "sources": {
+ "addresses": [
+ "0.0.0.0/0",
+ "::/0"
+ ]
+ },
+ "ports": "443",
+ "protocol": "tcp"
+ }
+ ],
+ "name": "my-firewall",
+ "outbound_rules": [
+ {
+ "destinations": {
+ "addresses": [
+ "0.0.0.0/0",
+ "::/0"
+ ]
+ },
+ "ports": "1-65535",
+ "protocol": "tcp"
+ },
+ {
+ "destinations": {
+ "addresses": [
+ "0.0.0.0/0",
+ "::/0"
+ ]
+ },
+ "ports": "1-65535",
+ "protocol": "udp"
+ },
+ {
+ "destinations": {
+ "addresses": [
+ "0.0.0.0/0",
+ "::/0"
+ ]
+ },
+ "ports": "1-65535",
+ "protocol": "icmp"
+ }
+ ],
+ "pending_changes": [],
+ "status": "succeeded",
+ "tags": ["sample"]
+ }
+"""
+
+from traceback import format_exc
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+from ansible.module_utils._text import to_native
+
+address_spec = dict(
+ addresses=dict(type="list", elements="str", required=False),
+ droplet_ids=dict(type="list", elements="str", required=False),
+ load_balancer_uids=dict(type="list", elements="str", required=False),
+ tags=dict(type="list", elements="str", required=False),
+)
+
+inbound_spec = dict(
+ protocol=dict(type="str", choices=["udp", "tcp", "icmp"], default="tcp"),
+ ports=dict(type="str", required=True),
+ sources=dict(type="dict", required=True, options=address_spec),
+)
+
+outbound_spec = dict(
+ protocol=dict(type="str", choices=["udp", "tcp", "icmp"], default="tcp"),
+ ports=dict(type="str", required=True),
+ destinations=dict(type="dict", required=True, options=address_spec),
+)
+
+
+class DOFirewall(object):
+ def __init__(self, module):
+ self.rest = DigitalOceanHelper(module)
+ self.module = module
+ self.name = self.module.params.get("name")
+ self.baseurl = "firewalls"
+ self.firewalls = self.get_firewalls()
+
+ def get_firewalls(self):
+ base_url = self.baseurl + "?"
+ response = self.rest.get("%s" % base_url)
+ status_code = response.status_code
+ status_code_success = 200
+
+ if status_code != status_code_success:
+ error = response.json
+ info = response.info
+
+ if error:
+ error.update({"status_code": status_code})
+ error.update({"status_code_success": status_code_success})
+ self.module.fail_json(msg=error)
+ elif info:
+ info.update({"status_code_success": status_code_success})
+ self.module.fail_json(msg=info)
+ else:
+ msg_error = "Failed to retrieve firewalls from DigitalOcean"
+ self.module.fail_json(
+ msg=msg_error
+ + " (url="
+ + self.rest.baseurl
+ + "/"
+ + self.baseurl
+ + ", status="
+ + str(status_code or "")
+ + " - expected:"
+ + str(status_code_success)
+ + ")"
+ )
+
+ return self.rest.get_paginated_data(
+ base_url=base_url, data_key_name="firewalls"
+ )
+
+ def get_firewall_by_name(self):
+ rule = {}
+ for firewall in self.firewalls:
+ if firewall["name"] == self.name:
+ rule.update(firewall)
+ return rule
+ return None
+
+ def ordered(self, obj):
+ if isinstance(obj, dict):
+ return sorted((k, self.ordered(v)) for k, v in obj.items())
+ if isinstance(obj, list):
+ return sorted(self.ordered(x) for x in obj)
+ else:
+ return obj
+
+ def fill_protocol_defaults(self, obj):
+ if obj.get("protocol") is None:
+ obj["protocol"] = "tcp"
+
+ return obj
+
+ def fill_source_and_destination_defaults_inner(self, obj):
+ addresses = obj.get("addresses") or []
+
+ droplet_ids = obj.get("droplet_ids") or []
+ droplet_ids = [str(droplet_id) for droplet_id in droplet_ids]
+
+ load_balancer_uids = obj.get("load_balancer_uids") or []
+ load_balancer_uids = [str(uid) for uid in load_balancer_uids]
+
+ tags = obj.get("tags") or []
+
+ data = {
+ "addresses": addresses,
+ "droplet_ids": droplet_ids,
+ "load_balancer_uids": load_balancer_uids,
+ "tags": tags,
+ }
+
+ return data
+
+ def fill_sources_and_destinations_defaults(self, obj, prop):
+ value = obj.get(prop)
+
+ if value is None:
+ value = {}
+ else:
+ value = self.fill_source_and_destination_defaults_inner(value)
+
+ obj[prop] = value
+
+ return obj
+
+ def fill_data_defaults(self, obj):
+ inbound_rules = obj.get("inbound_rules")
+
+ if inbound_rules is None:
+ inbound_rules = []
+ else:
+ inbound_rules = [self.fill_protocol_defaults(x) for x in inbound_rules]
+ inbound_rules = [
+ self.fill_sources_and_destinations_defaults(x, "sources")
+ for x in inbound_rules
+ ]
+
+ outbound_rules = obj.get("outbound_rules")
+
+ if outbound_rules is None:
+ outbound_rules = []
+ else:
+ outbound_rules = [self.fill_protocol_defaults(x) for x in outbound_rules]
+ outbound_rules = [
+ self.fill_sources_and_destinations_defaults(x, "destinations")
+ for x in outbound_rules
+ ]
+
+ droplet_ids = obj.get("droplet_ids") or []
+ droplet_ids = [str(droplet_id) for droplet_id in droplet_ids]
+
+ tags = obj.get("tags") or []
+
+ data = {
+ "name": obj.get("name"),
+ "inbound_rules": inbound_rules,
+ "outbound_rules": outbound_rules,
+ "droplet_ids": droplet_ids,
+ "tags": tags,
+ }
+
+ return data
+
+ def data_to_compare(self, obj):
+ return self.ordered(self.fill_data_defaults(obj))
+
+ def update(self, obj, id):
+ if id is None:
+ status_code_success = 202
+ resp = self.rest.post(path=self.baseurl, data=obj)
+ else:
+ status_code_success = 200
+ resp = self.rest.put(path=self.baseurl + "/" + id, data=obj)
+ status_code = resp.status_code
+ if status_code != status_code_success:
+ error = resp.json
+ error.update(
+ {
+ "context": "error when trying to "
+ + ("create" if (id is None) else "update")
+ + " firewalls"
+ }
+ )
+ error.update({"status_code": status_code})
+ error.update({"status_code_success": status_code_success})
+ self.module.fail_json(msg=error)
+ self.module.exit_json(changed=True, data=resp.json["firewall"])
+
+ def create(self):
+ rule = self.get_firewall_by_name()
+ data = {
+ "name": self.module.params.get("name"),
+ "inbound_rules": self.module.params.get("inbound_rules"),
+ "outbound_rules": self.module.params.get("outbound_rules"),
+ "droplet_ids": self.module.params.get("droplet_ids"),
+ "tags": self.module.params.get("tags"),
+ }
+ if rule is None:
+ self.update(data, None)
+ else:
+ rule_data = {
+ "name": rule.get("name"),
+ "inbound_rules": rule.get("inbound_rules"),
+ "outbound_rules": rule.get("outbound_rules"),
+ "droplet_ids": rule.get("droplet_ids"),
+ "tags": rule.get("tags"),
+ }
+
+ user_data = {
+ "name": data.get("name"),
+ "inbound_rules": data.get("inbound_rules"),
+ "outbound_rules": data.get("outbound_rules"),
+ "droplet_ids": data.get("droplet_ids"),
+ "tags": data.get("tags"),
+ }
+
+ if self.data_to_compare(user_data) == self.data_to_compare(rule_data):
+ self.module.exit_json(changed=False, data=rule)
+ else:
+ self.update(data, rule.get("id"))
+
+ def destroy(self):
+ rule = self.get_firewall_by_name()
+ if rule is None:
+ self.module.exit_json(changed=False, data="Firewall does not exist")
+ else:
+ endpoint = self.baseurl + "/" + rule["id"]
+ resp = self.rest.delete(path=endpoint)
+ status_code = resp.status_code
+ if status_code != 204:
+ self.module.fail_json(msg="Failed to delete firewall")
+ self.module.exit_json(
+ changed=True,
+ data="Deleted firewall rule: {0} - {1}".format(
+ rule["name"], rule["id"]
+ ),
+ )
+
+
+def core(module):
+ state = module.params.get("state")
+ firewall = DOFirewall(module)
+
+ if state == "present":
+ firewall.create()
+ elif state == "absent":
+ firewall.destroy()
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ argument_spec.update(
+ name=dict(type="str", required=True),
+ state=dict(type="str", choices=["present", "absent"], default="present"),
+ droplet_ids=dict(type="list", elements="str", required=False),
+ tags=dict(type="list", elements="str", required=False),
+ inbound_rules=dict(
+ type="list", elements="dict", options=inbound_spec, required=False
+ ),
+ outbound_rules=dict(
+ type="list", elements="dict", options=outbound_spec, required=False
+ ),
+ ),
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ required_if=[("state", "present", ["inbound_rules", "outbound_rules"])],
+ )
+
+ try:
+ core(module)
+ except Exception as e:
+ module.fail_json(msg=to_native(e), exception=format_exc())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_firewall_facts.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_firewall_facts.py
new file mode 100644
index 00000000..44239667
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_firewall_facts.py
@@ -0,0 +1,143 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2018, Ansible Project
+# Copyright: (c) 2018, Anthony Bond <ajbond2005@gmail.com>
+# 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: digital_ocean_firewall_info
+short_description: Gather information about DigitalOcean firewalls
+description:
+ - This module can be used to gather information about DigitalOcean firewalls.
+ - This module was called C(digital_ocean_firewall_facts) before Ansible 2.9. The usage did not change.
+author: "Anthony Bond (@BondAnthony)"
+options:
+ name:
+ description:
+ - Firewall rule name that can be used to identify and reference a specific firewall rule.
+ required: false
+ type: str
+requirements:
+ - "python >= 2.6"
+extends_documentation_fragment:
+- community.digitalocean.digital_ocean.documentation
+
+"""
+
+
+EXAMPLES = r"""
+- name: Gather information about all firewalls
+ community.digitalocean.digital_ocean_firewall_info:
+ oauth_token: "{{ oauth_token }}"
+
+- name: Gather information about a specific firewall by name
+ community.digitalocean.digital_ocean_firewall_info:
+ oauth_token: "{{ oauth_token }}"
+ name: "firewall_name"
+
+- name: Gather information from a firewall rule
+ community.digitalocean.digital_ocean_firewall_info:
+ name: SSH
+ register: resp_out
+
+- set_fact:
+ firewall_id: "{{ resp_out.data.id }}"
+
+- debug:
+ msg: "{{ firewall_id }}"
+"""
+
+
+RETURN = r"""
+data:
+ description: DigitalOcean firewall information
+ returned: success
+ type: list
+ elements: dict
+ sample: [
+ {
+ "id": "435tbg678-1db53-32b6-t543-28322569t252",
+ "name": "metrics",
+ "status": "succeeded",
+ "inbound_rules": [
+ {
+ "protocol": "tcp",
+ "ports": "9100",
+ "sources": {
+ "addresses": [
+ "1.1.1.1"
+ ]
+ }
+ }
+ ],
+ "outbound_rules": [],
+ "created_at": "2018-01-15T07:04:25Z",
+ "droplet_ids": [
+ 87426985
+ ],
+ "tags": [],
+ "pending_changes": []
+ },
+ ]
+"""
+
+from traceback import format_exc
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+from ansible.module_utils._text import to_native
+
+
+def core(module):
+ firewall_name = module.params.get("name", None)
+ rest = DigitalOceanHelper(module)
+ base_url = "firewalls?"
+
+ response = rest.get("%s" % base_url)
+ status_code = response.status_code
+ if status_code != 200:
+ module.fail_json(msg="Failed to retrieve firewalls from Digital Ocean")
+ firewalls = rest.get_paginated_data(base_url=base_url, data_key_name="firewalls")
+
+ if firewall_name is not None:
+ rule = {}
+ for firewall in firewalls:
+ if firewall["name"] == firewall_name:
+ rule.update(firewall)
+ firewalls = [rule]
+ module.exit_json(changed=False, data=firewalls)
+ else:
+ module.exit_json(changed=False, data=firewalls)
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ argument_spec.update(
+ name=dict(type="str", required=False),
+ )
+ module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
+ if module._name in (
+ "digital_ocean_firewall_facts",
+ "community.digitalocean.digital_ocean_firewall_facts",
+ ):
+ module.deprecate(
+ "The 'digital_ocean_firewall_facts' module has been renamed to 'digital_ocean_firewall_info'",
+ version="2.0.0",
+ collection_name="community.digitalocean",
+ ) # was Ansible 2.13
+
+ try:
+ core(module)
+ except Exception as e:
+ module.fail_json(msg=to_native(e), exception=format_exc())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_firewall_info.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_firewall_info.py
new file mode 100644
index 00000000..44239667
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_firewall_info.py
@@ -0,0 +1,143 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2018, Ansible Project
+# Copyright: (c) 2018, Anthony Bond <ajbond2005@gmail.com>
+# 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: digital_ocean_firewall_info
+short_description: Gather information about DigitalOcean firewalls
+description:
+ - This module can be used to gather information about DigitalOcean firewalls.
+ - This module was called C(digital_ocean_firewall_facts) before Ansible 2.9. The usage did not change.
+author: "Anthony Bond (@BondAnthony)"
+options:
+ name:
+ description:
+ - Firewall rule name that can be used to identify and reference a specific firewall rule.
+ required: false
+ type: str
+requirements:
+ - "python >= 2.6"
+extends_documentation_fragment:
+- community.digitalocean.digital_ocean.documentation
+
+"""
+
+
+EXAMPLES = r"""
+- name: Gather information about all firewalls
+ community.digitalocean.digital_ocean_firewall_info:
+ oauth_token: "{{ oauth_token }}"
+
+- name: Gather information about a specific firewall by name
+ community.digitalocean.digital_ocean_firewall_info:
+ oauth_token: "{{ oauth_token }}"
+ name: "firewall_name"
+
+- name: Gather information from a firewall rule
+ community.digitalocean.digital_ocean_firewall_info:
+ name: SSH
+ register: resp_out
+
+- set_fact:
+ firewall_id: "{{ resp_out.data.id }}"
+
+- debug:
+ msg: "{{ firewall_id }}"
+"""
+
+
+RETURN = r"""
+data:
+ description: DigitalOcean firewall information
+ returned: success
+ type: list
+ elements: dict
+ sample: [
+ {
+ "id": "435tbg678-1db53-32b6-t543-28322569t252",
+ "name": "metrics",
+ "status": "succeeded",
+ "inbound_rules": [
+ {
+ "protocol": "tcp",
+ "ports": "9100",
+ "sources": {
+ "addresses": [
+ "1.1.1.1"
+ ]
+ }
+ }
+ ],
+ "outbound_rules": [],
+ "created_at": "2018-01-15T07:04:25Z",
+ "droplet_ids": [
+ 87426985
+ ],
+ "tags": [],
+ "pending_changes": []
+ },
+ ]
+"""
+
+from traceback import format_exc
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+from ansible.module_utils._text import to_native
+
+
+def core(module):
+ firewall_name = module.params.get("name", None)
+ rest = DigitalOceanHelper(module)
+ base_url = "firewalls?"
+
+ response = rest.get("%s" % base_url)
+ status_code = response.status_code
+ if status_code != 200:
+ module.fail_json(msg="Failed to retrieve firewalls from Digital Ocean")
+ firewalls = rest.get_paginated_data(base_url=base_url, data_key_name="firewalls")
+
+ if firewall_name is not None:
+ rule = {}
+ for firewall in firewalls:
+ if firewall["name"] == firewall_name:
+ rule.update(firewall)
+ firewalls = [rule]
+ module.exit_json(changed=False, data=firewalls)
+ else:
+ module.exit_json(changed=False, data=firewalls)
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ argument_spec.update(
+ name=dict(type="str", required=False),
+ )
+ module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
+ if module._name in (
+ "digital_ocean_firewall_facts",
+ "community.digitalocean.digital_ocean_firewall_facts",
+ ):
+ module.deprecate(
+ "The 'digital_ocean_firewall_facts' module has been renamed to 'digital_ocean_firewall_info'",
+ version="2.0.0",
+ collection_name="community.digitalocean",
+ ) # was Ansible 2.13
+
+ try:
+ core(module)
+ except Exception as e:
+ module.fail_json(msg=to_native(e), exception=format_exc())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_floating_ip.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_floating_ip.py
new file mode 100644
index 00000000..d4d6ff26
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_floating_ip.py
@@ -0,0 +1,519 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# (c) 2015, Patrick F. Marques <patrickfmarques@gmail.com>
+# 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: digital_ocean_floating_ip
+short_description: Manage DigitalOcean Floating IPs
+description:
+ - Create/delete/assign a floating IP.
+author:
+ - "Patrick Marques (@pmarques)"
+ - "Daniel George (@danxg87)"
+options:
+ state:
+ description:
+ - Indicate desired state of the target.
+ - If C(state=present) Create (and optionally attach) floating IP
+ - If C(state=absent) Delete floating IP
+ - If C(state=attached) attach floating IP to a droplet
+ - If C(state=detached) detach floating IP from a droplet
+ default: present
+ choices: ['present', 'absent', 'attached', 'detached']
+ type: str
+ ip:
+ description:
+ - Public IP address of the Floating IP. Used to remove an IP
+ type: str
+ aliases: ['id']
+ region:
+ description:
+ - The region that the Floating IP is reserved to.
+ type: str
+ droplet_id:
+ description:
+ - The Droplet that the Floating IP has been assigned to.
+ type: str
+ oauth_token:
+ description:
+ - DigitalOcean OAuth token.
+ required: true
+ type: str
+ timeout:
+ description:
+ - Floating IP creation timeout.
+ type: int
+ default: 30
+ validate_certs:
+ description:
+ - If set to C(no), the SSL certificates will not be validated.
+ - This should only set to C(no) used on personally controlled sites using self-signed certificates.
+ type: bool
+ default: true
+ project_name:
+ aliases: ["project"]
+ description:
+ - Project to assign the resource to (project name, not UUID).
+ - Defaults to the default project of the account (empty string).
+ - Currently only supported when creating.
+ type: str
+ required: false
+ default: ""
+notes:
+ - Version 2 of DigitalOcean API is used.
+requirements:
+ - "python >= 2.6"
+"""
+
+
+EXAMPLES = r"""
+- name: "Create a Floating IP in region lon1"
+ community.digitalocean.digital_ocean_floating_ip:
+ state: present
+ region: lon1
+
+- name: Create a Floating IP in region lon1 (and assign to Project "test")
+ community.digitalocean.digital_ocean_floating_ip:
+ state: present
+ region: lon1
+ project: test
+
+- name: "Create a Floating IP assigned to Droplet ID 123456"
+ community.digitalocean.digital_ocean_floating_ip:
+ state: present
+ droplet_id: 123456
+
+- name: "Attach an existing Floating IP of 1.2.3.4 to Droplet ID 123456"
+ community.digitalocean.digital_ocean_floating_ip:
+ state: attached
+ ip: "1.2.3.4"
+ droplet_id: 123456
+
+- name: "Detach an existing Floating IP of 1.2.3.4 from its Droplet"
+ community.digitalocean.digital_ocean_floating_ip:
+ state: detached
+ ip: "1.2.3.4"
+
+- name: "Delete a Floating IP with ip 1.2.3.4"
+ community.digitalocean.digital_ocean_floating_ip:
+ state: absent
+ ip: "1.2.3.4"
+
+"""
+
+
+RETURN = r"""
+# Digital Ocean API info https://docs.digitalocean.com/reference/api/api-reference/#tag/Floating-IPs
+data:
+ description: a DigitalOcean Floating IP resource
+ returned: success and no resource constraint
+ type: dict
+ sample:
+ action:
+ id: 68212728
+ status: in-progress
+ type: assign_ip
+ started_at: '2015-10-15T17:45:44Z'
+ completed_at: null
+ resource_id: 758603823
+ resource_type: floating_ip
+ region:
+ name: New York 3
+ slug: nyc3
+ sizes:
+ - 512mb,
+ - 1gb,
+ - 2gb,
+ - 4gb,
+ - 8gb,
+ - 16gb,
+ - 32gb,
+ - 48gb,
+ - 64gb
+ features:
+ - private_networking
+ - backups
+ - ipv6
+ - metadata
+ available: true
+ region_slug: nyc3
+msg:
+ description: Informational or error message encountered during execution
+ returned: changed
+ type: str
+ sample: No project named test2 found
+assign_status:
+ description: Assignment status (ok, not_found, assigned, already_assigned, service_down)
+ returned: changed
+ type: str
+ sample: assigned
+resources:
+ description: Resource assignment involved in project assignment
+ returned: changed
+ type: dict
+ sample:
+ assigned_at: '2021-10-25T17:39:38Z'
+ links:
+ self: https://api.digitalocean.com/v2/floating_ips/157.230.64.107
+ status: assigned
+ urn: do:floatingip:157.230.64.107
+"""
+
+import json
+import time
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.basic import env_fallback
+from ansible.module_utils.urls import fetch_url
+
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+ DigitalOceanProjects,
+)
+
+
+class Response(object):
+ def __init__(self, resp, info):
+ self.body = None
+ if resp:
+ self.body = resp.read()
+ self.info = info
+
+ @property
+ def json(self):
+ if not self.body:
+ if "body" in self.info:
+ return json.loads(self.info["body"])
+ return None
+ try:
+ return json.loads(self.body)
+ except ValueError:
+ return None
+
+ @property
+ def status_code(self):
+ return self.info["status"]
+
+
+class Rest(object):
+ def __init__(self, module, headers):
+ self.module = module
+ self.headers = headers
+ self.baseurl = "https://api.digitalocean.com/v2"
+
+ def _url_builder(self, path):
+ if path[0] == "/":
+ path = path[1:]
+ return "%s/%s" % (self.baseurl, path)
+
+ def send(self, method, path, data=None, headers=None):
+ url = self._url_builder(path)
+ data = self.module.jsonify(data)
+ timeout = self.module.params["timeout"]
+
+ resp, info = fetch_url(
+ self.module,
+ url,
+ data=data,
+ headers=self.headers,
+ method=method,
+ timeout=timeout,
+ )
+
+ # Exceptions in fetch_url may result in a status -1, the ensures a
+ if info["status"] == -1:
+ self.module.fail_json(msg=info["msg"])
+
+ return Response(resp, info)
+
+ def get(self, path, data=None, headers=None):
+ return self.send("GET", path, data, headers)
+
+ def put(self, path, data=None, headers=None):
+ return self.send("PUT", path, data, headers)
+
+ def post(self, path, data=None, headers=None):
+ return self.send("POST", path, data, headers)
+
+ def delete(self, path, data=None, headers=None):
+ return self.send("DELETE", path, data, headers)
+
+
+def wait_action(module, rest, ip, action_id, timeout=60):
+ end_time = time.monotonic() + timeout
+ while time.monotonic() < end_time:
+ response = rest.get("floating_ips/{0}/actions/{1}".format(ip, action_id))
+ json_data = response.json
+ status_code = response.status_code
+ status = response.json["action"]["status"]
+ if status_code == 200:
+ if status == "completed":
+ return json_data
+ elif status == "errored":
+ module.fail_json(
+ msg="Floating ip action error [ip: {0}: action: {1}]".format(
+ ip, action_id
+ ),
+ data=json,
+ )
+ time.sleep(10)
+ module.fail_json(
+ msg="Floating ip action timeout [ip: {0}: action: {1}]".format(ip, action_id),
+ data=json,
+ )
+
+
+def core(module):
+ api_token = module.params["oauth_token"]
+ state = module.params["state"]
+ ip = module.params["ip"]
+ droplet_id = module.params["droplet_id"]
+
+ rest = Rest(
+ module,
+ {
+ "Authorization": "Bearer {0}".format(api_token),
+ "Content-type": "application/json",
+ },
+ )
+
+ if state in ("present"):
+ if droplet_id is not None and module.params["ip"] is not None:
+ # Lets try to associate the ip to the specified droplet
+ associate_floating_ips(module, rest)
+ else:
+ create_floating_ips(module, rest)
+
+ elif state in ("attached"):
+ if droplet_id is not None and module.params["ip"] is not None:
+ associate_floating_ips(module, rest)
+
+ elif state in ("detached"):
+ if module.params["ip"] is not None:
+ detach_floating_ips(module, rest, module.params["ip"])
+
+ elif state in ("absent"):
+ response = rest.delete("floating_ips/{0}".format(ip))
+ status_code = response.status_code
+ json_data = response.json
+ if status_code == 204:
+ module.exit_json(changed=True)
+ elif status_code == 404:
+ module.exit_json(changed=False)
+ else:
+ module.exit_json(changed=False, data=json_data)
+
+
+def get_floating_ip_details(module, rest):
+ ip = module.params["ip"]
+
+ response = rest.get("floating_ips/{0}".format(ip))
+ status_code = response.status_code
+ json_data = response.json
+ if status_code == 200:
+ return json_data["floating_ip"]
+ else:
+ module.fail_json(
+ msg="Error assigning floating ip [{0}: {1}]".format(
+ status_code, json_data["message"]
+ ),
+ region=module.params["region"],
+ )
+
+
+def assign_floating_id_to_droplet(module, rest):
+ ip = module.params["ip"]
+
+ payload = {
+ "type": "assign",
+ "droplet_id": module.params["droplet_id"],
+ }
+
+ response = rest.post("floating_ips/{0}/actions".format(ip), data=payload)
+ status_code = response.status_code
+ json_data = response.json
+ if status_code == 201:
+ json_data = wait_action(module, rest, ip, json_data["action"]["id"])
+
+ module.exit_json(changed=True, data=json_data)
+ else:
+ module.fail_json(
+ msg="Error creating floating ip [{0}: {1}]".format(
+ status_code, json_data["message"]
+ ),
+ region=module.params["region"],
+ )
+
+
+def detach_floating_ips(module, rest, ip):
+ payload = {"type": "unassign"}
+ response = rest.post("floating_ips/{0}/actions".format(ip), data=payload)
+ status_code = response.status_code
+ json_data = response.json
+
+ if status_code == 201:
+ json_data = wait_action(module, rest, ip, json_data["action"]["id"])
+ module.exit_json(
+ changed=True, msg="Detached floating ip {0}".format(ip), data=json_data
+ )
+ action = json_data.get("action", None)
+ action_id = action.get("id", None)
+ if action is None:
+ module.fail_json(
+ changed=False,
+ msg="Error retrieving detach action. Got: {0}".format(action),
+ )
+ if action_id is None:
+ module.fail_json(
+ changed=False,
+ msg="Error retrieving detach action ID. Got: {0}".format(action_id),
+ )
+ else:
+ module.fail_json(
+ changed=False,
+ msg="Error detaching floating ip [{0}: {1}]".format(
+ status_code, json_data["message"]
+ ),
+ )
+
+
+def associate_floating_ips(module, rest):
+ floating_ip = get_floating_ip_details(module, rest)
+ droplet = floating_ip["droplet"]
+
+ # TODO: If already assigned to a droplet verify if is one of the specified as valid
+ if droplet is not None and str(droplet["id"]) in [module.params["droplet_id"]]:
+ module.exit_json(changed=False)
+ else:
+ assign_floating_id_to_droplet(module, rest)
+
+
+def create_floating_ips(module, rest):
+ payload = {}
+
+ if module.params["region"] is not None:
+ payload["region"] = module.params["region"]
+ if module.params["droplet_id"] is not None:
+ payload["droplet_id"] = module.params["droplet_id"]
+
+ # Get existing floating IPs
+ response = rest.get("floating_ips/")
+ status_code = response.status_code
+ json_data = response.json
+
+ # Exit unchanged if any of them are assigned to this Droplet already
+ if status_code == 200:
+ floating_ips = json_data.get("floating_ips", [])
+ if len(floating_ips) != 0:
+ for floating_ip in floating_ips:
+ droplet = floating_ip.get("droplet", None)
+ if droplet is not None:
+ droplet_id = droplet.get("id", None)
+ if droplet_id is not None:
+ if str(droplet_id) == module.params["droplet_id"]:
+ ip = floating_ip.get("ip", None)
+ if ip is not None:
+ module.exit_json(
+ changed=False, data={"floating_ip": floating_ip}
+ )
+ else:
+ module.fail_json(
+ changed=False,
+ msg="Unexpected error querying floating ip",
+ )
+
+ response = rest.post("floating_ips", data=payload)
+ status_code = response.status_code
+ json_data = response.json
+ if status_code == 202:
+ if module.params.get(
+ "project"
+ ): # only load for non-default project assignments
+ rest = DigitalOceanHelper(module)
+ projects = DigitalOceanProjects(module, rest)
+ project_name = module.params.get("project")
+ if (
+ project_name
+ ): # empty string is the default project, skip project assignment
+ floating_ip = json_data.get("floating_ip")
+ ip = floating_ip.get("ip")
+ if ip:
+ urn = "do:floatingip:{0}".format(ip)
+ (
+ assign_status,
+ error_message,
+ resources,
+ ) = projects.assign_to_project(project_name, urn)
+ module.exit_json(
+ changed=True,
+ data=json_data,
+ msg=error_message,
+ assign_status=assign_status,
+ resources=resources,
+ )
+ else:
+ module.exit_json(
+ changed=True,
+ msg="Floating IP created but not assigned to the {0} Project (missing information from the API response)".format(
+ project_name
+ ),
+ data=json_data,
+ )
+ else:
+ module.exit_json(changed=True, data=json_data)
+ else:
+ module.exit_json(changed=True, data=json_data)
+ else:
+ module.fail_json(
+ msg="Error creating floating ip [{0}: {1}]".format(
+ status_code, json_data["message"]
+ ),
+ region=module.params["region"],
+ )
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ state=dict(
+ choices=["present", "absent", "attached", "detached"], default="present"
+ ),
+ ip=dict(aliases=["id"], required=False),
+ region=dict(required=False),
+ droplet_id=dict(required=False),
+ oauth_token=dict(
+ no_log=True,
+ # Support environment variable for DigitalOcean OAuth Token
+ fallback=(
+ env_fallback,
+ ["DO_API_TOKEN", "DO_API_KEY", "DO_OAUTH_TOKEN"],
+ ),
+ required=True,
+ ),
+ validate_certs=dict(type="bool", default=True),
+ timeout=dict(type="int", default=30),
+ project_name=dict(
+ type="str", aliases=["project"], required=False, default=""
+ ),
+ ),
+ required_if=[
+ ("state", "delete", ["ip"]),
+ ("state", "attached", ["ip", "droplet_id"]),
+ ("state", "detached", ["ip"]),
+ ],
+ mutually_exclusive=[["region", "droplet_id"]],
+ )
+
+ core(module)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_floating_ip_facts.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_floating_ip_facts.py
new file mode 100644
index 00000000..3d232a4a
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_floating_ip_facts.py
@@ -0,0 +1,136 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright: (C) 2017-18, Ansible Project
+# 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: digital_ocean_floating_ip_info
+short_description: DigitalOcean Floating IPs information
+description:
+ - This module can be used to fetch DigitalOcean Floating IPs information.
+ - This module was called C(digital_ocean_floating_ip_facts) before Ansible 2.9. The usage did not change.
+author: "Patrick Marques (@pmarques)"
+extends_documentation_fragment:
+- community.digitalocean.digital_ocean.documentation
+
+notes:
+ - Version 2 of DigitalOcean API is used.
+requirements:
+ - "python >= 2.6"
+"""
+
+
+EXAMPLES = r"""
+- name: "Gather information about all Floating IPs"
+ community.digitalocean.digital_ocean_floating_ip_info:
+ register: result
+
+- name: "List of current floating ips"
+ debug:
+ var: result.floating_ips
+"""
+
+
+RETURN = r"""
+# Digital Ocean API info https://docs.digitalocean.com/reference/api/api-reference/#tag/Floating-IPs
+floating_ips:
+ description: a DigitalOcean Floating IP resource
+ returned: success and no resource constraint
+ type: list
+ sample: [
+ {
+ "ip": "45.55.96.47",
+ "droplet": null,
+ "region": {
+ "name": "New York 3",
+ "slug": "nyc3",
+ "sizes": [
+ "512mb",
+ "1gb",
+ "2gb",
+ "4gb",
+ "8gb",
+ "16gb",
+ "32gb",
+ "48gb",
+ "64gb"
+ ],
+ "features": [
+ "private_networking",
+ "backups",
+ "ipv6",
+ "metadata"
+ ],
+ "available": true
+ },
+ "locked": false
+ }
+ ]
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+from ansible.module_utils._text import to_native
+
+
+def core(module):
+ rest = DigitalOceanHelper(module)
+
+ page = 1
+ has_next = True
+ floating_ips = []
+ status_code = None
+ while has_next or status_code != 200:
+ response = rest.get("floating_ips?page={0}&per_page=20".format(page))
+ status_code = response.status_code
+ # stop if any error during pagination
+ if status_code != 200:
+ break
+ page += 1
+ floating_ips.extend(response.json["floating_ips"])
+ has_next = (
+ "pages" in response.json["links"]
+ and "next" in response.json["links"]["pages"]
+ )
+
+ if status_code == 200:
+ module.exit_json(changed=False, floating_ips=floating_ips)
+ else:
+ module.fail_json(
+ msg="Error fetching information [{0}: {1}]".format(
+ status_code, response.json["message"]
+ )
+ )
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=DigitalOceanHelper.digital_ocean_argument_spec(),
+ supports_check_mode=True,
+ )
+ if module._name in (
+ "digital_ocean_floating_ip_facts",
+ "community.digitalocean.digital_ocean_floating_ip_facts",
+ ):
+ module.deprecate(
+ "The 'digital_ocean_floating_ip_facts' module has been renamed to 'digital_ocean_floating_ip_info'",
+ version="2.0.0",
+ collection_name="community.digitalocean",
+ ) # was Ansible 2.13
+
+ try:
+ core(module)
+ except Exception as e:
+ module.fail_json(msg=to_native(e))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_floating_ip_info.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_floating_ip_info.py
new file mode 100644
index 00000000..3d232a4a
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_floating_ip_info.py
@@ -0,0 +1,136 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright: (C) 2017-18, Ansible Project
+# 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: digital_ocean_floating_ip_info
+short_description: DigitalOcean Floating IPs information
+description:
+ - This module can be used to fetch DigitalOcean Floating IPs information.
+ - This module was called C(digital_ocean_floating_ip_facts) before Ansible 2.9. The usage did not change.
+author: "Patrick Marques (@pmarques)"
+extends_documentation_fragment:
+- community.digitalocean.digital_ocean.documentation
+
+notes:
+ - Version 2 of DigitalOcean API is used.
+requirements:
+ - "python >= 2.6"
+"""
+
+
+EXAMPLES = r"""
+- name: "Gather information about all Floating IPs"
+ community.digitalocean.digital_ocean_floating_ip_info:
+ register: result
+
+- name: "List of current floating ips"
+ debug:
+ var: result.floating_ips
+"""
+
+
+RETURN = r"""
+# Digital Ocean API info https://docs.digitalocean.com/reference/api/api-reference/#tag/Floating-IPs
+floating_ips:
+ description: a DigitalOcean Floating IP resource
+ returned: success and no resource constraint
+ type: list
+ sample: [
+ {
+ "ip": "45.55.96.47",
+ "droplet": null,
+ "region": {
+ "name": "New York 3",
+ "slug": "nyc3",
+ "sizes": [
+ "512mb",
+ "1gb",
+ "2gb",
+ "4gb",
+ "8gb",
+ "16gb",
+ "32gb",
+ "48gb",
+ "64gb"
+ ],
+ "features": [
+ "private_networking",
+ "backups",
+ "ipv6",
+ "metadata"
+ ],
+ "available": true
+ },
+ "locked": false
+ }
+ ]
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+from ansible.module_utils._text import to_native
+
+
+def core(module):
+ rest = DigitalOceanHelper(module)
+
+ page = 1
+ has_next = True
+ floating_ips = []
+ status_code = None
+ while has_next or status_code != 200:
+ response = rest.get("floating_ips?page={0}&per_page=20".format(page))
+ status_code = response.status_code
+ # stop if any error during pagination
+ if status_code != 200:
+ break
+ page += 1
+ floating_ips.extend(response.json["floating_ips"])
+ has_next = (
+ "pages" in response.json["links"]
+ and "next" in response.json["links"]["pages"]
+ )
+
+ if status_code == 200:
+ module.exit_json(changed=False, floating_ips=floating_ips)
+ else:
+ module.fail_json(
+ msg="Error fetching information [{0}: {1}]".format(
+ status_code, response.json["message"]
+ )
+ )
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=DigitalOceanHelper.digital_ocean_argument_spec(),
+ supports_check_mode=True,
+ )
+ if module._name in (
+ "digital_ocean_floating_ip_facts",
+ "community.digitalocean.digital_ocean_floating_ip_facts",
+ ):
+ module.deprecate(
+ "The 'digital_ocean_floating_ip_facts' module has been renamed to 'digital_ocean_floating_ip_info'",
+ version="2.0.0",
+ collection_name="community.digitalocean",
+ ) # was Ansible 2.13
+
+ try:
+ core(module)
+ except Exception as e:
+ module.fail_json(msg=to_native(e))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_image_facts.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_image_facts.py
new file mode 100644
index 00000000..7feb3af0
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_image_facts.py
@@ -0,0 +1,160 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2018, Ansible Project
+# Copyright: (c) 2018, Abhijeet Kasurde <akasurde@redhat.com>
+# 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: digital_ocean_image_info
+short_description: Gather information about DigitalOcean images
+description:
+ - This module can be used to gather information about DigitalOcean provided images.
+ - These images can be either of type C(distribution), C(application) and C(private).
+ - This module was called C(digital_ocean_image_facts) before Ansible 2.9. The usage did not change.
+author: "Abhijeet Kasurde (@Akasurde)"
+options:
+ image_type:
+ description:
+ - Specifies the type of image information to be retrieved.
+ - If set to C(application), then information are gathered related to all application images.
+ - If set to C(distribution), then information are gathered related to all distribution images.
+ - If set to C(private), then information are gathered related to all private images.
+ - If not set to any of above, then information are gathered related to all images.
+ default: 'all'
+ choices: [ 'all', 'application', 'distribution', 'private' ]
+ required: false
+ type: str
+requirements:
+ - "python >= 2.6"
+extends_documentation_fragment:
+- community.digitalocean.digital_ocean.documentation
+
+"""
+
+
+EXAMPLES = r"""
+- name: Gather information about all images
+ community.digitalocean.digital_ocean_image_info:
+ image_type: all
+ oauth_token: "{{ oauth_token }}"
+
+- name: Gather information about application images
+ community.digitalocean.digital_ocean_image_info:
+ image_type: application
+ oauth_token: "{{ oauth_token }}"
+
+- name: Gather information about distribution images
+ community.digitalocean.digital_ocean_image_info:
+ image_type: distribution
+ oauth_token: "{{ oauth_token }}"
+
+- name: Get distribution about image with slug coreos-beta
+ community.digitalocean.digital_ocean_image_info:
+ register: resp_out
+- set_fact:
+ distribution_name: "{{ item.distribution }}"
+ loop: "{{ resp_out.data | community.general.json_query(name) }}"
+ vars:
+ name: "[?slug=='coreos-beta']"
+- debug:
+ var: distribution_name
+
+"""
+
+
+RETURN = r"""
+data:
+ description: DigitalOcean image information
+ returned: success
+ type: list
+ sample: [
+ {
+ "created_at": "2018-02-02T07:11:43Z",
+ "distribution": "CoreOS",
+ "id": 31434061,
+ "min_disk_size": 20,
+ "name": "1662.1.0 (beta)",
+ "public": true,
+ "regions": [
+ "nyc1",
+ "sfo1",
+ "nyc2",
+ "ams2",
+ "sgp1",
+ "lon1",
+ "nyc3",
+ "ams3",
+ "fra1",
+ "tor1",
+ "sfo2",
+ "blr1"
+ ],
+ "size_gigabytes": 0.42,
+ "slug": "coreos-beta",
+ "type": "snapshot"
+ },
+ ]
+"""
+
+from traceback import format_exc
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+from ansible.module_utils._text import to_native
+
+
+def core(module):
+ image_type = module.params["image_type"]
+
+ rest = DigitalOceanHelper(module)
+
+ base_url = "images?"
+ if image_type == "distribution":
+ base_url += "type=distribution&"
+ elif image_type == "application":
+ base_url += "type=application&"
+ elif image_type == "private":
+ base_url += "private=true&"
+
+ images = rest.get_paginated_data(base_url=base_url, data_key_name="images")
+
+ module.exit_json(changed=False, data=images)
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ argument_spec.update(
+ image_type=dict(
+ type="str",
+ required=False,
+ choices=["all", "application", "distribution", "private"],
+ default="all",
+ )
+ )
+
+ module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
+ if module._name in (
+ "digital_ocean_image_facts",
+ "community.digitalocean.digital_ocean_image_facts",
+ ):
+ module.deprecate(
+ "The 'digital_ocean_image_facts' module has been renamed to 'digital_ocean_image_info'",
+ version="2.0.0",
+ collection_name="community.digitalocean",
+ ) # was Ansible 2.13
+
+ try:
+ core(module)
+ except Exception as e:
+ module.fail_json(msg=to_native(e), exception=format_exc())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_image_info.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_image_info.py
new file mode 100644
index 00000000..7feb3af0
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_image_info.py
@@ -0,0 +1,160 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2018, Ansible Project
+# Copyright: (c) 2018, Abhijeet Kasurde <akasurde@redhat.com>
+# 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: digital_ocean_image_info
+short_description: Gather information about DigitalOcean images
+description:
+ - This module can be used to gather information about DigitalOcean provided images.
+ - These images can be either of type C(distribution), C(application) and C(private).
+ - This module was called C(digital_ocean_image_facts) before Ansible 2.9. The usage did not change.
+author: "Abhijeet Kasurde (@Akasurde)"
+options:
+ image_type:
+ description:
+ - Specifies the type of image information to be retrieved.
+ - If set to C(application), then information are gathered related to all application images.
+ - If set to C(distribution), then information are gathered related to all distribution images.
+ - If set to C(private), then information are gathered related to all private images.
+ - If not set to any of above, then information are gathered related to all images.
+ default: 'all'
+ choices: [ 'all', 'application', 'distribution', 'private' ]
+ required: false
+ type: str
+requirements:
+ - "python >= 2.6"
+extends_documentation_fragment:
+- community.digitalocean.digital_ocean.documentation
+
+"""
+
+
+EXAMPLES = r"""
+- name: Gather information about all images
+ community.digitalocean.digital_ocean_image_info:
+ image_type: all
+ oauth_token: "{{ oauth_token }}"
+
+- name: Gather information about application images
+ community.digitalocean.digital_ocean_image_info:
+ image_type: application
+ oauth_token: "{{ oauth_token }}"
+
+- name: Gather information about distribution images
+ community.digitalocean.digital_ocean_image_info:
+ image_type: distribution
+ oauth_token: "{{ oauth_token }}"
+
+- name: Get distribution about image with slug coreos-beta
+ community.digitalocean.digital_ocean_image_info:
+ register: resp_out
+- set_fact:
+ distribution_name: "{{ item.distribution }}"
+ loop: "{{ resp_out.data | community.general.json_query(name) }}"
+ vars:
+ name: "[?slug=='coreos-beta']"
+- debug:
+ var: distribution_name
+
+"""
+
+
+RETURN = r"""
+data:
+ description: DigitalOcean image information
+ returned: success
+ type: list
+ sample: [
+ {
+ "created_at": "2018-02-02T07:11:43Z",
+ "distribution": "CoreOS",
+ "id": 31434061,
+ "min_disk_size": 20,
+ "name": "1662.1.0 (beta)",
+ "public": true,
+ "regions": [
+ "nyc1",
+ "sfo1",
+ "nyc2",
+ "ams2",
+ "sgp1",
+ "lon1",
+ "nyc3",
+ "ams3",
+ "fra1",
+ "tor1",
+ "sfo2",
+ "blr1"
+ ],
+ "size_gigabytes": 0.42,
+ "slug": "coreos-beta",
+ "type": "snapshot"
+ },
+ ]
+"""
+
+from traceback import format_exc
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+from ansible.module_utils._text import to_native
+
+
+def core(module):
+ image_type = module.params["image_type"]
+
+ rest = DigitalOceanHelper(module)
+
+ base_url = "images?"
+ if image_type == "distribution":
+ base_url += "type=distribution&"
+ elif image_type == "application":
+ base_url += "type=application&"
+ elif image_type == "private":
+ base_url += "private=true&"
+
+ images = rest.get_paginated_data(base_url=base_url, data_key_name="images")
+
+ module.exit_json(changed=False, data=images)
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ argument_spec.update(
+ image_type=dict(
+ type="str",
+ required=False,
+ choices=["all", "application", "distribution", "private"],
+ default="all",
+ )
+ )
+
+ module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
+ if module._name in (
+ "digital_ocean_image_facts",
+ "community.digitalocean.digital_ocean_image_facts",
+ ):
+ module.deprecate(
+ "The 'digital_ocean_image_facts' module has been renamed to 'digital_ocean_image_info'",
+ version="2.0.0",
+ collection_name="community.digitalocean",
+ ) # was Ansible 2.13
+
+ try:
+ core(module)
+ except Exception as e:
+ module.fail_json(msg=to_native(e), exception=format_exc())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_kubernetes.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_kubernetes.py
new file mode 100644
index 00000000..eda9c424
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_kubernetes.py
@@ -0,0 +1,493 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2020, Ansible Project
+# 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: digital_ocean_kubernetes
+short_description: Create and delete a DigitalOcean Kubernetes cluster
+description:
+ - Create and delete a Kubernetes cluster in DigitalOcean (and optionally wait for it to be running).
+version_added: 1.3.0
+author: Mark Mercado (@mamercad)
+options:
+ oauth_token:
+ description:
+ - DigitalOcean OAuth token; can be specified in C(DO_API_KEY), C(DO_API_TOKEN), or C(DO_OAUTH_TOKEN) environment variables
+ type: str
+ aliases: ['API_TOKEN']
+ required: true
+ state:
+ description:
+ - The usual, C(present) to create, C(absent) to destroy
+ type: str
+ choices: ['present', 'absent']
+ default: present
+ name:
+ description:
+ - A human-readable name for a Kubernetes cluster.
+ type: str
+ required: true
+ region:
+ description:
+ - The slug identifier for the region where the Kubernetes cluster will be created.
+ type: str
+ aliases: ['region_id']
+ default: nyc1
+ version:
+ description:
+ - The slug identifier for the version of Kubernetes used for the cluster. See the /v2/kubernetes/options endpoint for available versions.
+ type: str
+ required: false
+ default: latest
+ auto_upgrade:
+ description:
+ - A boolean value indicating whether the cluster will be automatically upgraded to new patch releases during its maintenance window.
+ type: bool
+ required: false
+ default: false
+ surge_upgrade:
+ description:
+ - A boolean value indicating whether surge upgrade is enabled/disabled for the cluster.
+ - Surge upgrade makes cluster upgrades fast and reliable by bringing up new nodes before destroying the outdated nodes.
+ type: bool
+ required: false
+ default: false
+ tags:
+ description:
+ - A flat array of tag names as strings to be applied to the Kubernetes cluster.
+ - All clusters will be automatically tagged "k8s" and "k8s:$K8S_CLUSTER_ID" in addition to any tags provided by the user.
+ required: false
+ type: list
+ elements: str
+ maintenance_policy:
+ description:
+ - An object specifying the maintenance window policy for the Kubernetes cluster (see table below).
+ type: dict
+ required: false
+ node_pools:
+ description:
+ - An object specifying the details of the worker nodes available to the Kubernetes cluster (see table below).
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ type: str
+ description: A human-readable name for the node pool.
+ size:
+ type: str
+ description: The slug identifier for the type of Droplet used as workers in the node pool.
+ count:
+ type: int
+ description: The number of Droplet instances in the node pool.
+ tags:
+ type: list
+ elements: str
+ description:
+ - An array containing the tags applied to the node pool.
+ - All node pools are automatically tagged C("k8s"), C("k8s-worker"), and C("k8s:$K8S_CLUSTER_ID").
+ labels:
+ type: dict
+ description: An object containing a set of Kubernetes labels. The keys are user-defined.
+ taints:
+ type: list
+ elements: dict
+ description:
+ - An array of taints to apply to all nodes in a pool.
+ - Taints will automatically be applied to all existing nodes and any subsequent nodes added to the pool.
+ - When a taint is removed, it is removed from all nodes in the pool.
+ auto_scale:
+ type: bool
+ description:
+ - A boolean value indicating whether auto-scaling is enabled for this node pool.
+ min_nodes:
+ type: int
+ description:
+ - The minimum number of nodes that this node pool can be auto-scaled to.
+ - The value will be C(0) if C(auto_scale) is set to C(false).
+ max_nodes:
+ type: int
+ description:
+ - The maximum number of nodes that this node pool can be auto-scaled to.
+ - The value will be C(0) if C(auto_scale) is set to C(false).
+ default:
+ - name: worker-pool
+ size: s-1vcpu-2gb
+ count: 1
+ tags: []
+ labels: {}
+ taints: []
+ auto_scale: false
+ min_nodes: 0
+ max_nodes: 0
+ vpc_uuid:
+ description:
+ - A string specifying the UUID of the VPC to which the Kubernetes cluster will be assigned.
+ - If excluded, the cluster will be assigned to your account's default VPC for the region.
+ type: str
+ required: false
+ return_kubeconfig:
+ description:
+ - Controls whether or not to return the C(kubeconfig).
+ type: bool
+ required: false
+ default: false
+ wait:
+ description:
+ - Wait for the cluster to be running before returning.
+ type: bool
+ required: false
+ default: true
+ wait_timeout:
+ description:
+ - How long before wait gives up, in seconds, when creating a cluster.
+ type: int
+ default: 600
+ ha:
+ description:
+ - A boolean value indicating whether the control plane is run in a highly available configuration in the cluster.
+ - Highly available control planes incur less downtime.
+ type: bool
+ default: false
+"""
+
+
+EXAMPLES = r"""
+- name: Create a new DigitalOcean Kubernetes cluster in New York 1
+ community.digitalocean.digital_ocean_kubernetes:
+ state: present
+ oauth_token: "{{ lookup('env', 'DO_API_TOKEN') }}"
+ name: hacktoberfest
+ region: nyc1
+ node_pools:
+ - name: hacktoberfest-workers
+ size: s-1vcpu-2gb
+ count: 3
+ return_kubeconfig: yes
+ wait_timeout: 600
+ register: my_cluster
+
+- name: Show the kubeconfig for the cluster we just created
+ debug:
+ msg: "{{ my_cluster.data.kubeconfig }}"
+
+- name: Destroy (delete) an existing DigitalOcean Kubernetes cluster
+ community.digitalocean.digital_ocean_kubernetes:
+ state: absent
+ oauth_token: "{{ lookup('env', 'DO_API_TOKEN') }}"
+ name: hacktoberfest
+"""
+
+
+# Digital Ocean API info https://docs.digitalocean.com/reference/api/api-reference/#tag/Kubernetes
+# The only variance from the documented response is that the kubeconfig is (if return_kubeconfig is True) merged in at data['kubeconfig']
+RETURN = r"""
+data:
+ description: A DigitalOcean Kubernetes cluster (and optional C(kubeconfig))
+ returned: changed
+ type: dict
+ sample:
+ kubeconfig: |-
+ apiVersion: v1
+ clusters:
+ - cluster:
+ certificate-authority-data: REDACTED
+ server: https://REDACTED.k8s.ondigitalocean.com
+ name: do-nyc1-hacktoberfest
+ contexts:
+ - context:
+ cluster: do-nyc1-hacktoberfest
+ user: do-nyc1-hacktoberfest-admin
+ name: do-nyc1-hacktoberfest
+ current-context: do-nyc1-hacktoberfest
+ kind: Config
+ preferences: {}
+ users:
+ - name: do-nyc1-hacktoberfest-admin
+ user:
+ token: REDACTED
+ kubernetes_cluster:
+ auto_upgrade: false
+ cluster_subnet: 10.244.0.0/16
+ created_at: '2020-09-27T00:55:37Z'
+ endpoint: https://REDACTED.k8s.ondigitalocean.com
+ id: REDACTED
+ ipv4: REDACTED
+ maintenance_policy:
+ day: any
+ duration: 4h0m0s
+ start_time: '15:00'
+ name: hacktoberfest
+ node_pools:
+ - auto_scale: false
+ count: 1
+ id: REDACTED
+ labels: null
+ max_nodes: 0
+ min_nodes: 0
+ name: hacktoberfest-workers
+ nodes:
+ - created_at: '2020-09-27T00:55:37Z'
+ droplet_id: '209555245'
+ id: REDACTED
+ name: hacktoberfest-workers-3tdq1
+ status:
+ state: running
+ updated_at: '2020-09-27T00:58:36Z'
+ size: s-1vcpu-2gb
+ tags:
+ - k8s
+ - k8s:REDACTED
+ - k8s:worker
+ taints: []
+ region: nyc1
+ service_subnet: 10.245.0.0/16
+ status:
+ state: running
+ surge_upgrade: false
+ tags:
+ - k8s
+ - k8s:REDACTED
+ updated_at: '2020-09-27T01:00:37Z'
+ version: 1.18.8-do.1
+ vpc_uuid: REDACTED
+"""
+
+
+import traceback
+import time
+import json
+from traceback import format_exc
+from ansible.module_utils._text import to_native
+from ansible.module_utils.basic import AnsibleModule, env_fallback
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+
+
+class DOKubernetes(object):
+ def __init__(self, module):
+ self.rest = DigitalOceanHelper(module)
+ self.module = module
+ # Pop these values so we don't include them in the POST data
+ self.return_kubeconfig = self.module.params.pop("return_kubeconfig", False)
+ self.wait = self.module.params.pop("wait", True)
+ self.wait_timeout = self.module.params.pop("wait_timeout", 600)
+ self.module.params.pop("oauth_token")
+ self.cluster_id = None
+
+ def get_by_id(self):
+ """Returns an existing DigitalOcean Kubernetes cluster matching on id"""
+ response = self.rest.get("kubernetes/clusters/{0}".format(self.cluster_id))
+ json_data = response.json
+ if response.status_code == 200:
+ return json_data
+ return None
+
+ def get_all_clusters(self):
+ """Returns all DigitalOcean Kubernetes clusters"""
+ response = self.rest.get("kubernetes/clusters")
+ json_data = response.json
+ if response.status_code == 200:
+ return json_data
+ return None
+
+ def get_by_name(self, cluster_name):
+ """Returns an existing DigitalOcean Kubernetes cluster matching on name"""
+ if not cluster_name:
+ return None
+ clusters = self.get_all_clusters()
+ for cluster in clusters["kubernetes_clusters"]:
+ if cluster["name"] == cluster_name:
+ return cluster
+ return None
+
+ def get_kubernetes_kubeconfig(self):
+ """Returns the kubeconfig for an existing DigitalOcean Kubernetes cluster"""
+ response = self.rest.get(
+ "kubernetes/clusters/{0}/kubeconfig".format(self.cluster_id)
+ )
+ if response.status_code == 200:
+ return response.body
+ else:
+ self.module.fail_json(msg="Failed to retrieve kubeconfig")
+
+ def get_kubernetes(self):
+ """Returns an existing DigitalOcean Kubernetes cluster by name"""
+ json_data = self.get_by_name(self.module.params["name"])
+ if json_data:
+ self.cluster_id = json_data["id"]
+ return json_data
+ else:
+ return None
+
+ def get_kubernetes_options(self):
+ """Fetches DigitalOcean Kubernetes options: regions, sizes, versions.
+ API reference: https://docs.digitalocean.com/reference/api/api-reference/#operation/list_kubernetes_options
+ """
+ response = self.rest.get("kubernetes/options")
+ json_data = response.json
+ if response.status_code == 200:
+ return json_data
+ return None
+
+ def ensure_running(self):
+ """Waits for the newly created DigitalOcean Kubernetes cluster to be running"""
+ end_time = time.monotonic() + self.wait_timeout
+ while time.monotonic() < end_time:
+ cluster = self.get_by_id()
+ if cluster["kubernetes_cluster"]["status"]["state"] == "running":
+ return cluster
+ time.sleep(10)
+ self.module.fail_json(msg="Wait for Kubernetes cluster to be running")
+
+ def create(self):
+ """Creates a DigitalOcean Kubernetes cluster
+ API reference: https://docs.digitalocean.com/reference/api/api-reference/#operation/create_kubernetes_cluster
+ """
+ # Get valid Kubernetes options (regions, sizes, versions)
+ kubernetes_options = self.get_kubernetes_options()["options"]
+ # Validate region
+ valid_regions = [str(x["slug"]) for x in kubernetes_options["regions"]]
+ if self.module.params.get("region") not in valid_regions:
+ self.module.fail_json(
+ msg="Invalid region {0} (valid regions are {1})".format(
+ self.module.params.get("region"), ", ".join(valid_regions)
+ )
+ )
+ # Validate version
+ valid_versions = [str(x["slug"]) for x in kubernetes_options["versions"]]
+ valid_versions.append("latest")
+ if self.module.params.get("version") not in valid_versions:
+ self.module.fail_json(
+ msg="Invalid version {0} (valid versions are {1})".format(
+ self.module.params.get("version"), ", ".join(valid_versions)
+ )
+ )
+ # Validate size
+ valid_sizes = [str(x["slug"]) for x in kubernetes_options["sizes"]]
+ for node_pool in self.module.params.get("node_pools"):
+ if node_pool["size"] not in valid_sizes:
+ self.module.fail_json(
+ msg="Invalid size {0} (valid sizes are {1})".format(
+ node_pool["size"], ", ".join(valid_sizes)
+ )
+ )
+
+ # Create the Kubernetes cluster
+ json_data = self.get_kubernetes()
+ if json_data:
+ # Add the kubeconfig to the return
+ if self.return_kubeconfig:
+ json_data["kubeconfig"] = self.get_kubernetes_kubeconfig()
+ self.module.exit_json(changed=False, data=json_data)
+ if self.module.check_mode:
+ self.module.exit_json(changed=True)
+ request_params = dict(self.module.params)
+ response = self.rest.post("kubernetes/clusters", data=request_params)
+ json_data = response.json
+ if response.status_code >= 400:
+ self.module.fail_json(changed=False, msg=json_data)
+ # Set the cluster_id
+ self.cluster_id = json_data["kubernetes_cluster"]["id"]
+ if self.wait:
+ json_data = self.ensure_running()
+ # Add the kubeconfig to the return
+ if self.return_kubeconfig:
+ json_data["kubeconfig"] = self.get_kubernetes_kubeconfig()
+ self.module.exit_json(changed=True, data=json_data["kubernetes_cluster"])
+
+ def delete(self):
+ """Deletes a DigitalOcean Kubernetes cluster
+ API reference: https://docs.digitalocean.com/reference/api/api-reference/#operation/delete_kubernetes_cluster
+ """
+ json_data = self.get_kubernetes()
+ if json_data:
+ if self.module.check_mode:
+ self.module.exit_json(changed=True)
+ response = self.rest.delete(
+ "kubernetes/clusters/{0}".format(json_data["id"])
+ )
+ if response.status_code == 204:
+ self.module.exit_json(
+ changed=True, data=json_data, msg="Kubernetes cluster deleted"
+ )
+ self.module.fail_json(
+ changed=False, msg="Failed to delete Kubernetes cluster"
+ )
+ json_data = response.json
+ else:
+ self.module.exit_json(changed=False, msg="Kubernetes cluster not found")
+
+
+def run(module):
+ state = module.params.pop("state")
+ cluster = DOKubernetes(module)
+ if state == "present":
+ cluster.create()
+ elif state == "absent":
+ cluster.delete()
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ state=dict(choices=["present", "absent"], default="present"),
+ oauth_token=dict(
+ aliases=["API_TOKEN"],
+ no_log=True,
+ fallback=(
+ env_fallback,
+ ["DO_API_TOKEN", "DO_API_KEY", "DO_OAUTH_TOKEN"],
+ ),
+ required=True,
+ ),
+ name=dict(type="str", required=True),
+ region=dict(aliases=["region_id"], default="nyc1"),
+ version=dict(type="str", default="latest"),
+ auto_upgrade=dict(type="bool", default=False),
+ surge_upgrade=dict(type="bool", default=False),
+ tags=dict(type="list", elements="str"),
+ maintenance_policy=dict(type="dict"),
+ node_pools=dict(
+ type="list",
+ elements="dict",
+ default=[
+ {
+ "name": "worker-pool",
+ "size": "s-1vcpu-2gb",
+ "count": 1,
+ "tags": [],
+ "labels": {},
+ "taints": [],
+ "auto_scale": False,
+ "min_nodes": 0,
+ "max_nodes": 0,
+ }
+ ],
+ ),
+ vpc_uuid=dict(type="str"),
+ return_kubeconfig=dict(type="bool", default=False),
+ wait=dict(type="bool", default=True),
+ wait_timeout=dict(type="int", default=600),
+ ha=dict(type="bool", default=False),
+ ),
+ required_if=(
+ [
+ ("state", "present", ["name", "region", "version", "node_pools"]),
+ ]
+ ),
+ supports_check_mode=True,
+ )
+
+ run(module)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_kubernetes_info.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_kubernetes_info.py
new file mode 100644
index 00000000..d60e9b4a
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_kubernetes_info.py
@@ -0,0 +1,234 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2020, Ansible Project
+# 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: digital_ocean_kubernetes_info
+short_description: Returns information about an existing DigitalOcean Kubernetes cluster
+description:
+ - Returns information about an existing DigitalOcean Kubernetes cluster.
+version_added: 1.3.0
+author: Mark Mercado (@mamercad)
+options:
+ oauth_token:
+ description:
+ - DigitalOcean OAuth token; can be specified in C(DO_API_KEY), C(DO_API_TOKEN), or C(DO_OAUTH_TOKEN) environment variables
+ type: str
+ aliases: ['API_TOKEN']
+ required: true
+ name:
+ description:
+ - A human-readable name for a Kubernetes cluster.
+ type: str
+ required: true
+ return_kubeconfig:
+ description:
+ - Controls whether or not to return the C(kubeconfig).
+ type: bool
+ required: false
+ default: false
+"""
+
+
+EXAMPLES = r"""
+- name: Get information about an existing DigitalOcean Kubernetes cluster
+ community.digitalocean.digital_ocean_kubernetes_info:
+ oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
+ name: hacktoberfest
+ return_kubeconfig: yes
+ register: my_cluster
+
+- ansible.builtin.debug:
+ msg: "Cluster name is {{ my_cluster.data.name }}, ID is {{ my_cluster.data.id }}"
+
+- ansible.builtin.debug:
+ msg: "Cluster kubeconfig is {{ my_cluster.data.kubeconfig }}"
+"""
+
+
+# Digital Ocean API info https://docs.digitalocean.com/reference/api/api-reference/#operation/list_all_kubernetes_clusters
+# The only variance from the documented response is that the kubeconfig is (if return_kubeconfig is True) merged in at data['kubeconfig']
+RETURN = r"""
+data:
+ description: A DigitalOcean Kubernetes cluster (and optional C(kubeconfig))
+ returned: changed
+ type: dict
+ sample:
+ auto_upgrade: false
+ cluster_subnet: 10.244.0.0/16
+ created_at: '2020-09-26T21:36:18Z'
+ endpoint: https://REDACTED.k8s.ondigitalocean.com
+ id: REDACTED
+ ipv4: REDACTED
+ kubeconfig: |-
+ apiVersion: v1
+ clusters:
+ - cluster:
+ certificate-authority-data: REDACTED
+ server: https://REDACTED.k8s.ondigitalocean.com
+ name: do-nyc1-hacktoberfest
+ contexts:
+ - context:
+ cluster: do-nyc1-hacktoberfest
+ user: do-nyc1-hacktoberfest-admin
+ name: do-nyc1-hacktoberfest
+ current-context: do-nyc1-hacktoberfest
+ kind: Config
+ preferences: {}
+ users:
+ - name: do-nyc1-hacktoberfest-admin
+ user:
+ token: REDACTED
+ maintenance_policy:
+ day: any
+ duration: 4h0m0s
+ start_time: '13:00'
+ name: hacktoberfest
+ node_pools:
+ - auto_scale: false
+ count: 1
+ id: REDACTED
+ labels: null
+ max_nodes: 0
+ min_nodes: 0
+ name: hacktoberfest-workers
+ nodes:
+ - created_at: '2020-09-26T21:36:18Z'
+ droplet_id: 'REDACTED'
+ id: REDACTED
+ name: hacktoberfest-workers-3tv46
+ status:
+ state: running
+ updated_at: '2020-09-26T21:40:28Z'
+ size: s-1vcpu-2gb
+ tags:
+ - k8s
+ - k8s:REDACTED
+ - k8s:worker
+ taints: []
+ region: nyc1
+ service_subnet: 10.245.0.0/16
+ status:
+ state: running
+ surge_upgrade: false
+ tags:
+ - k8s
+ - k8s:REDACTED
+ updated_at: '2020-09-26T21:42:29Z'
+ version: 1.18.8-do.0
+ vpc_uuid: REDACTED
+"""
+
+
+import traceback
+import time
+import json
+from traceback import format_exc
+from ansible.module_utils._text import to_native
+from ansible.module_utils.basic import AnsibleModule, env_fallback
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+
+
+class DOKubernetesInfo(object):
+ def __init__(self, module):
+ self.rest = DigitalOceanHelper(module)
+ self.module = module
+ # Pop these values so we don't include them in the POST data
+ self.module.params.pop("oauth_token")
+ self.return_kubeconfig = self.module.params.pop("return_kubeconfig")
+ self.cluster_id = None
+
+ def get_by_id(self):
+ """Returns an existing DigitalOcean Kubernetes cluster matching on id"""
+ response = self.rest.get("kubernetes/clusters/{0}".format(self.cluster_id))
+ json_data = response.json
+ if response.status_code == 200:
+ return json_data
+ return None
+
+ def get_all_clusters(self):
+ """Returns all DigitalOcean Kubernetes clusters"""
+ response = self.rest.get("kubernetes/clusters")
+ json_data = response.json
+ if response.status_code == 200:
+ return json_data
+ return None
+
+ def get_by_name(self, cluster_name):
+ """Returns an existing DigitalOcean Kubernetes cluster matching on name"""
+ if not cluster_name:
+ return None
+ clusters = self.get_all_clusters()
+ for cluster in clusters["kubernetes_clusters"]:
+ if cluster["name"] == cluster_name:
+ return cluster
+ return None
+
+ def get_kubernetes_kubeconfig(self):
+ """Returns the kubeconfig for an existing DigitalOcean Kubernetes cluster"""
+ response = self.rest.get(
+ "kubernetes/clusters/{0}/kubeconfig".format(self.cluster_id)
+ )
+ if response.status_code == 200:
+ return response.body
+ else:
+ self.module.fail_json(msg="Failed to retrieve kubeconfig")
+
+ def get_kubernetes(self):
+ """Returns an existing DigitalOcean Kubernetes cluster by name"""
+ json_data = self.get_by_name(self.module.params["name"])
+ if json_data:
+ self.cluster_id = json_data["id"]
+ return json_data
+ else:
+ return None
+
+ def get(self):
+ """Fetches an existing DigitalOcean Kubernetes cluster
+ API reference: https://docs.digitalocean.com/reference/api/api-reference/#operation/list_all_kubernetes_clusters
+ """
+ json_data = self.get_kubernetes()
+ if json_data:
+ if self.return_kubeconfig:
+ json_data["kubeconfig"] = self.get_kubernetes_kubeconfig()
+ self.module.exit_json(changed=False, data=json_data)
+ self.module.fail_json(changed=False, msg="Kubernetes cluster not found")
+
+
+def run(module):
+ cluster = DOKubernetesInfo(module)
+ cluster.get()
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ oauth_token=dict(
+ aliases=["API_TOKEN"],
+ no_log=True,
+ fallback=(
+ env_fallback,
+ ["DO_API_TOKEN", "DO_API_KEY", "DO_OAUTH_TOKEN"],
+ ),
+ required=True,
+ ),
+ name=dict(type="str", required=True),
+ return_kubeconfig=dict(type="bool", default=False),
+ ),
+ supports_check_mode=True,
+ )
+
+ run(module)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_load_balancer.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_load_balancer.py
new file mode 100644
index 00000000..ecc9efa4
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_load_balancer.py
@@ -0,0 +1,881 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Mark Mercado <mamercad@gmail.com>
+#
+# 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: digital_ocean_load_balancer
+version_added: 1.10.0
+short_description: Manage DigitalOcean Load Balancers
+description:
+ - Manage DigitalOcean Load Balancers
+author: "Mark Mercado (@mamercad)"
+options:
+ state:
+ description:
+ - The usual, C(present) to create, C(absent) to destroy
+ type: str
+ choices: ["present", "absent"]
+ default: present
+ name:
+ description:
+ - A human-readable name for a load balancer instance.
+ - Required and must be unique (current API documentation is not up-to-date for this parameter).
+ type: str
+ required: true
+ size:
+ description:
+ - The size of the load balancer.
+ - The available sizes are C(lb-small), C(lb-medium), or C(lb-large).
+ - You can resize load balancers after creation up to once per hour.
+ - You cannot resize a load balancer within the first hour of its creation.
+ - This field has been replaced by the C(size_unit) field for all regions except in C(ams2), C(nyc2), and C(sfo1).
+ - Each available load balancer size now equates to the load balancer having a set number of nodes.
+ - The formula is C(lb-small) = 1 node, C(lb-medium) = 3 nodes, C(lb-large) = 6 nodes.
+ required: false
+ type: str
+ choices: ["lb-small", "lb-medium", "lb-large"]
+ default: lb-small
+ size_unit:
+ description:
+ - How many nodes the load balancer contains.
+ - Each additional node increases the load balancer's ability to manage more connections.
+ - Load balancers can be scaled up or down, and you can change the number of nodes after creation up to once per hour.
+ - This field is currently not available in the C(ams2), C(nyc2), or C(sfo1) regions.
+ - Use the C(size) field to scale load balancers that reside in these regions.
+ - The value must be in the range 1-100.
+ required: false
+ type: int
+ default: 1
+ droplet_ids:
+ description:
+ - An array containing the IDs of the Droplets assigned to the load balancer.
+ - Required when creating load balancers.
+ - Mutually exclusive with tag, you can either define tag or droplet_ids but not both.
+ required: false
+ type: list
+ elements: int
+ tag:
+ description:
+ - A tag associated with the droplets that you want to dynamically assign to the load balancer.
+ - Required when creating load balancers.
+ - Mutually exclusive with droplet_ids, you can either define tag or droplet_ids but not both.
+ required: false
+ type: str
+ region:
+ description:
+ - The slug identifier for the region where the resource will initially be available.
+ required: false
+ type: str
+ aliases: ["region_id"]
+ forwarding_rules:
+ description:
+ - An array of objects specifying the forwarding rules for a load balancer.
+ - Required when creating load balancers.
+ required: false
+ type: list
+ elements: dict
+ suboptions:
+ entry_protocol:
+ type: str
+ description: Entry protocol
+ default: http
+ entry_port:
+ type: int
+ description: Entry port
+ default: 8080
+ target_protocol:
+ type: str
+ description: Target protocol
+ default: http
+ target_port:
+ type: int
+ description: Target port
+ default: 8080
+ certificate_id:
+ type: str
+ description: Certificate ID
+ default: ""
+ tls_passthrough:
+ type: bool
+ description: TLS passthrough
+ default: false
+ default:
+ - entry_protocol: http
+ entry_port: 8080
+ target_protocol: http
+ target_port: 8080
+ certificate_id: ""
+ tls_passthrough: false
+ health_check:
+ description:
+ - An object specifying health check settings for the load balancer.
+ required: false
+ type: dict
+ suboptions:
+ protocol:
+ description: Protocol
+ type: str
+ required: false
+ default: http
+ port:
+ description: Port
+ type: int
+ required: false
+ default: 80
+ path:
+ description: Path
+ type: str
+ required: false
+ default: /
+ check_interval_seconds:
+ description: Check interval seconds
+ type: int
+ required: false
+ default: 10
+ response_timeout_seconds:
+ description: Response timeout seconds
+ type: int
+ required: false
+ default: 5
+ healthy_threshold:
+ description: Healthy threshold
+ type: int
+ required: false
+ default: 5
+ unhealthy_threshold:
+ description: Unhealthy threshold
+ type: int
+ required: false
+ default: 3
+ default:
+ protocol: http
+ port: 80
+ path: /
+ check_interval_seconds: 10
+ response_timeout_seconds: 5
+ healthy_threshold: 5
+ unhealthy_threshold: 3
+ sticky_sessions:
+ description:
+ - An object specifying sticky sessions settings for the load balancer.
+ required: false
+ type: dict
+ suboptions:
+ type:
+ description: Type
+ type: str
+ required: false
+ default: none
+ default:
+ type: none
+ redirect_http_to_https:
+ description:
+ - A boolean value indicating whether HTTP requests to the load balancer on port 80 will be redirected to HTTPS on port 443.
+ type: bool
+ required: false
+ default: false
+ enable_proxy_protocol:
+ description:
+ - A boolean value indicating whether PROXY Protocol is in use.
+ type: bool
+ required: false
+ default: false
+ enable_backend_keepalive:
+ description:
+ - A boolean value indicating whether HTTP keepalive connections are maintained to target Droplets.
+ type: bool
+ required: false
+ default: false
+ vpc_uuid:
+ description:
+ - A string specifying the UUID of the VPC to which the load balancer is assigned.
+ - If unspecified, uses the default VPC in the region.
+ type: str
+ required: false
+ wait:
+ description:
+ - Wait for the Load Balancer to be running before returning.
+ type: bool
+ required: false
+ default: true
+ wait_timeout:
+ description:
+ - How long before wait gives up, in seconds, when creating a Load Balancer.
+ type: int
+ default: 600
+ project_name:
+ aliases: ["project"]
+ description:
+ - Project to assign the resource to (project name, not UUID).
+ - Defaults to the default project of the account (empty string).
+ - Currently only supported when creating.
+ type: str
+ required: false
+ default: ""
+extends_documentation_fragment:
+ - community.digitalocean.digital_ocean.documentation
+"""
+
+
+EXAMPLES = r"""
+- name: Create a Load Balancer
+ community.digitalocean.digital_ocean_load_balancer:
+ state: present
+ name: test-loadbalancer-1
+ droplet_ids:
+ - 12345678
+ region: nyc1
+ forwarding_rules:
+ - entry_protocol: http
+ entry_port: 8080
+ target_protocol: http
+ target_port: 8080
+ certificate_id: ""
+ tls_passthrough: false
+
+- name: Create a Load Balancer (and assign to Project "test")
+ community.digitalocean.digital_ocean_load_balancer:
+ state: present
+ name: test-loadbalancer-1
+ droplet_ids:
+ - 12345678
+ region: nyc1
+ forwarding_rules:
+ - entry_protocol: http
+ entry_port: 8080
+ target_protocol: http
+ target_port: 8080
+ certificate_id: ""
+ tls_passthrough: false
+ project: test
+
+- name: Create a Load Balancer and associate it with a tag
+ community.digitalocean.digital_ocean_load_balancer:
+ state: present
+ name: test-loadbalancer-1
+ tag: test-tag
+ region: tor1
+"""
+
+
+RETURN = r"""
+data:
+ description: A DigitalOcean Load Balancer
+ returned: changed
+ type: dict
+ sample:
+ load_balancer:
+ algorithm: round_robin
+ created_at: '2021-08-22T14:23:41Z'
+ droplet_ids:
+ - 261172461
+ enable_backend_keepalive: false
+ enable_proxy_protocol: false
+ forwarding_rules:
+ - certificate_id: ''
+ entry_port: 8080
+ entry_protocol: http
+ target_port: 8080
+ target_protocol: http
+ tls_passthrough: false
+ health_check:
+ check_interval_seconds: 10
+ healthy_threshold: 5
+ path: /
+ port: 80
+ protocol: http
+ response_timeout_seconds: 5
+ unhealthy_threshold: 3
+ id: b4fdb507-70e8-4325-a89e-d02271b93618
+ ip: 159.203.150.113
+ name: test-loadbalancer-1
+ redirect_http_to_https: false
+ region:
+ available: true
+ features:
+ - backups
+ - ipv6
+ - metadata
+ - install_agent
+ - storage
+ - image_transfer
+ name: New York 3
+ sizes:
+ - s-1vcpu-1gb
+ - s-1vcpu-1gb-amd
+ - s-1vcpu-1gb-intel
+ - s-1vcpu-2gb
+ - s-1vcpu-2gb-amd
+ - s-1vcpu-2gb-intel
+ - s-2vcpu-2gb
+ - s-2vcpu-2gb-amd
+ - s-2vcpu-2gb-intel
+ - s-2vcpu-4gb
+ - s-2vcpu-4gb-amd
+ - s-2vcpu-4gb-intel
+ - s-4vcpu-8gb
+ - c-2
+ - c2-2vcpu-4gb
+ - s-4vcpu-8gb-amd
+ - s-4vcpu-8gb-intel
+ - g-2vcpu-8gb
+ - gd-2vcpu-8gb
+ - s-8vcpu-16gb
+ - m-2vcpu-16gb
+ - c-4
+ - c2-4vcpu-8gb
+ - s-8vcpu-16gb-amd
+ - s-8vcpu-16gb-intel
+ - m3-2vcpu-16gb
+ - g-4vcpu-16gb
+ - so-2vcpu-16gb
+ - m6-2vcpu-16gb
+ - gd-4vcpu-16gb
+ - so1_5-2vcpu-16gb
+ - m-4vcpu-32gb
+ - c-8
+ - c2-8vcpu-16gb
+ - m3-4vcpu-32gb
+ - g-8vcpu-32gb
+ - so-4vcpu-32gb
+ - m6-4vcpu-32gb
+ - gd-8vcpu-32gb
+ - so1_5-4vcpu-32gb
+ - m-8vcpu-64gb
+ - c-16
+ - c2-16vcpu-32gb
+ - m3-8vcpu-64gb
+ - g-16vcpu-64gb
+ - so-8vcpu-64gb
+ - m6-8vcpu-64gb
+ - gd-16vcpu-64gb
+ - so1_5-8vcpu-64gb
+ - m-16vcpu-128gb
+ - c-32
+ - c2-32vcpu-64gb
+ - m3-16vcpu-128gb
+ - m-24vcpu-192gb
+ - g-32vcpu-128gb
+ - so-16vcpu-128gb
+ - m6-16vcpu-128gb
+ - gd-32vcpu-128gb
+ - m3-24vcpu-192gb
+ - g-40vcpu-160gb
+ - so1_5-16vcpu-128gb
+ - m-32vcpu-256gb
+ - gd-40vcpu-160gb
+ - so-24vcpu-192gb
+ - m6-24vcpu-192gb
+ - m3-32vcpu-256gb
+ - so1_5-24vcpu-192gb
+ - m6-32vcpu-256gb
+ slug: nyc3
+ size: lb-small
+ status: active
+ sticky_sessions:
+ type: none
+ tag: ''
+ vpc_uuid: b8fd9a58-d93d-4329-b54a-78a397d64855
+msg:
+ description: Informational or error message encountered during execution
+ returned: changed
+ type: str
+ sample: No project named test2 found
+assign_status:
+ description: Assignment status (ok, not_found, assigned, already_assigned, service_down)
+ returned: changed
+ type: str
+ sample: assigned
+resources:
+ description: Resource assignment involved in project assignment
+ returned: changed
+ type: dict
+ sample:
+ assigned_at: '2021-10-25T17:39:38Z'
+ links:
+ self: https://api.digitalocean.com/v2/load_balancers/17d171d0-8a8b-4251-9c18-c96cc515d36d
+ status: assigned
+ urn: do:loadbalancer:17d171d0-8a8b-4251-9c18-c96cc515d36d
+"""
+
+
+import time
+from ansible.module_utils._text import to_native
+from ansible.module_utils.basic import AnsibleModule, env_fallback
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+ DigitalOceanProjects,
+)
+
+
+class DOLoadBalancer(object):
+
+ # Regions which use 'size' versus 'size_unit'
+ size_regions = {"ams2", "nyc2", "sfo1"}
+ all_sizes = {"lb-small", "lb-medium", "lb-large"}
+ default_size = "lb-small"
+ min_size_unit = 1
+ max_size_unit = 100
+ default_size_unit = 1
+
+ def __init__(self, module):
+ self.rest = DigitalOceanHelper(module)
+ self.module = module
+ self.id = None
+ self.name = self.module.params.get("name")
+ self.region = self.module.params.get("region")
+
+ # Handle size versus size_unit
+ if self.region in DOLoadBalancer.size_regions:
+ self.module.params.pop("size_unit")
+ # Ensure that we have size
+ size = self.module.params.get("size", None)
+ if not size:
+ self.module.fail_json(msg="Missing required 'size' parameter")
+ elif size not in DOLoadBalancer.all_sizes:
+ self.module.fail_json(
+ msg="Invalid 'size' parameter '{0}', must be one of: {1}".format(
+ size, ", ".join(DOLoadBalancer.all_sizes)
+ )
+ )
+ else:
+ self.module.params.pop("size")
+ # Ensure that we have size_unit
+ size_unit = self.module.params.get("size_unit", None)
+ if not size_unit:
+ self.module.fail_json(msg="Missing required 'size_unit' parameter")
+ elif (
+ size_unit < DOLoadBalancer.min_size_unit
+ or size_unit > DOLoadBalancer.max_size_unit
+ ):
+ self.module.fail_json(
+ msg="Invalid 'size_unit' parameter '{0}', must be in range: {1}-{2}".format(
+ size_unit,
+ DOLoadBalancer.min_size_unit,
+ DOLoadBalancer.max_size_unit,
+ )
+ )
+
+ self.updates = []
+
+ # Pop these values so we don't include them in the POST data
+ self.module.params.pop("oauth_token")
+ self.wait = self.module.params.pop("wait", True)
+ self.wait_timeout = self.module.params.pop("wait_timeout", 600)
+ if self.module.params.get("project"):
+ # only load for non-default project assignments
+ self.projects = DigitalOceanProjects(module, self.rest)
+
+ def get_by_id(self):
+ """Fetch an existing DigitalOcean Load Balancer (by id)
+ API reference: https://docs.digitalocean.com/reference/api/api-reference/#operation/get_load_balancer
+ """
+ response = self.rest.get("load_balancers/{0}".format(self.id))
+ json_data = response.json
+ if response.status_code == 200:
+ # Found one with the given id:
+ lb = json_data.get("load_balancer", None)
+ if lb is not None:
+ self.lb = lb
+ return lb
+ else:
+ self.module.fail_json(
+ msg="Unexpected error; please file a bug: get_by_id"
+ )
+ return None
+
+ def get_by_name(self):
+ """Fetch all existing DigitalOcean Load Balancers
+ API reference: https://docs.digitalocean.com/reference/api/api-reference/#operation/list_all_load_balancers
+ """
+ page = 1
+ while page is not None:
+ response = self.rest.get("load_balancers?page={0}".format(page))
+ json_data = response.json
+ if json_data is None:
+ self.module.fail_json(
+ msg="Empty response from the DigitalOcean API; please try again or open a bug if it never succeeds."
+ )
+ if response.status_code == 200:
+ lbs = json_data.get("load_balancers", [])
+ for lb in lbs:
+ # Found one with the same name:
+ name = lb.get("name", None)
+ if name == self.name:
+ # Make sure the region is the same!
+ region = lb.get("region", None)
+ if region is not None:
+ region_slug = region.get("slug", None)
+ if region_slug is not None:
+ if region_slug == self.region:
+ self.lb = lb
+ return lb
+ else:
+ self.module.fail_json(
+ msg="Cannot change load balancer region -- delete and re-create"
+ )
+ else:
+ self.module.fail_json(
+ msg="Unexpected error; please file a bug: get_by_name"
+ )
+ else:
+ self.module.fail_json(
+ msg="Unexpected error; please file a bug: get_by_name"
+ )
+ if (
+ "links" in json_data
+ and "pages" in json_data["links"]
+ and "next" in json_data["links"]["pages"]
+ ):
+ page += 1
+ else:
+ page = None
+ else:
+ self.module.fail_json(
+ msg="Unexpected error; please file a bug: get_by_name"
+ )
+ return None
+
+ def ensure_active(self):
+ """Wait for the existing Load Balancer to be active"""
+ end_time = time.monotonic() + self.wait_timeout
+ while time.monotonic() < end_time:
+ if self.get_by_id():
+ status = self.lb.get("status", None)
+ if status is not None:
+ if status == "active":
+ return True
+ else:
+ self.module.fail_json(
+ msg="Unexpected error; please file a bug: ensure_active"
+ )
+ else:
+ self.module.fail_json(
+ msg="Load Balancer {0} in {1} not found".format(
+ self.id, self.region
+ )
+ )
+ time.sleep(10)
+ self.module.fail_json(
+ msg="Timed out waiting for Load Balancer {0} in {1} to be active".format(
+ self.id, self.region
+ )
+ )
+
+ def is_same(self, found_lb):
+ """Checks if exising Load Balancer is the same as requested"""
+
+ check_attributes = [
+ "droplet_ids",
+ "size",
+ "size_unit",
+ "forwarding_rules",
+ "health_check",
+ "sticky_sessions",
+ "redirect_http_to_https",
+ "enable_proxy_protocol",
+ "enable_backend_keepalive",
+ ]
+
+ lb_region = found_lb.get("region", None)
+ if not lb_region:
+ self.module.fail_json(
+ msg="Unexpected error; please file a bug should this persist: empty load balancer region"
+ )
+
+ lb_region_slug = lb_region.get("slug", None)
+ if not lb_region_slug:
+ self.module.fail_json(
+ msg="Unexpected error; please file a bug should this persist: empty load balancer region slug"
+ )
+
+ for attribute in check_attributes:
+ if (
+ attribute == "size"
+ and lb_region_slug not in DOLoadBalancer.size_regions
+ ):
+ continue
+ if (
+ attribute == "size_unit"
+ and lb_region_slug in DOLoadBalancer.size_regions
+ ):
+ continue
+ if self.module.params.get(attribute, None) != found_lb.get(attribute, None):
+ self.updates.append(attribute)
+
+ # Check if the VPC needs changing.
+ vpc_uuid = self.lb.get("vpc_uuid", None)
+ if vpc_uuid is not None:
+ if vpc_uuid != found_lb.get("vpc_uuid", None):
+ self.updates.append("vpc_uuid")
+
+ if len(self.updates):
+ return False
+ else:
+ return True
+
+ def update(self):
+ """Updates a DigitalOcean Load Balancer
+ API reference: https://docs.digitalocean.com/reference/api/api-reference/#operation/update_load_balancer
+ """
+ request_params = dict(self.module.params)
+ self.id = self.lb.get("id", None)
+ self.name = self.lb.get("name", None)
+ self.vpc_uuid = self.lb.get("vpc_uuid", None)
+ if self.id is not None and self.name is not None and self.vpc_uuid is not None:
+ request_params["vpc_uuid"] = self.vpc_uuid
+ response = self.rest.put(
+ "load_balancers/{0}".format(self.id), data=request_params
+ )
+ json_data = response.json
+ if response.status_code == 200:
+ self.module.exit_json(
+ changed=True,
+ msg="Load Balancer {0} ({1}) in {2} updated: {3}".format(
+ self.name, self.id, self.region, ", ".join(self.updates)
+ ),
+ )
+ else:
+ self.module.fail_json(
+ changed=False,
+ msg="Error updating Load Balancer {0} ({1}) in {2}: {3}".format(
+ self.name, self.id, self.region, json_data["message"]
+ ),
+ )
+ else:
+ self.module.fail_json(msg="Unexpected error; please file a bug: update")
+
+ def create(self):
+ """Creates a DigitalOcean Load Balancer
+ API reference: https://docs.digitalocean.com/reference/api/api-reference/#operation/create_load_balancer
+ """
+
+ # Check if it exists already (the API docs aren't up-to-date right now,
+ # "name" is required and must be unique across the account.
+ found_lb = self.get_by_name()
+ if found_lb is not None:
+ # Do we need to update it?
+ if not self.is_same(found_lb):
+ if self.module.check_mode:
+ self.module.exit_json(
+ changed=False,
+ msg="Load Balancer {0} already exists in {1} (and needs changes)".format(
+ self.name, self.region
+ ),
+ data={"load_balancer": found_lb},
+ )
+ else:
+ self.update()
+ else:
+ self.module.exit_json(
+ changed=False,
+ msg="Load Balancer {0} already exists in {1} (and needs no changes)".format(
+ self.name, self.region
+ ),
+ data={"load_balancer": found_lb},
+ )
+
+ # Check mode.
+ if self.module.check_mode:
+ self.module.exit_json(
+ changed=False,
+ msg="Would create Load Balancer {0} in {1}".format(
+ self.name, self.region
+ ),
+ )
+
+ # Create it.
+ request_params = dict(self.module.params)
+ response = self.rest.post("load_balancers", data=request_params)
+ json_data = response.json
+ if response.status_code != 202:
+ self.module.fail_json(
+ msg="Failed creating Load Balancer {0} in {1}: {2}".format(
+ self.name, self.region, json_data["message"]
+ )
+ )
+
+ # Store it.
+ lb = json_data.get("load_balancer", None)
+ if lb is None:
+ self.module.fail_json(
+ msg="Unexpected error; please file a bug: create empty lb"
+ )
+
+ self.id = lb.get("id", None)
+ if self.id is None:
+ self.module.fail_json(
+ msg="Unexpected error; please file a bug: create missing id"
+ )
+
+ if self.wait:
+ self.ensure_active()
+
+ project_name = self.module.params.get("project")
+ if project_name: # empty string is the default project, skip project assignment
+ urn = "do:loadbalancer:{0}".format(self.id)
+ (
+ assign_status,
+ error_message,
+ resources,
+ ) = self.projects.assign_to_project(project_name, urn)
+ self.module.exit_json(
+ changed=True,
+ data=json_data,
+ msg=error_message,
+ assign_status=assign_status,
+ resources=resources,
+ )
+ else:
+ self.module.exit_json(changed=True, data=json_data)
+
+ def delete(self):
+ """Deletes a DigitalOcean Load Balancer
+ API reference: https://docs.digitalocean.com/reference/api/api-reference/#operation/delete_load_balancer
+ """
+
+ lb = self.get_by_name()
+ if lb is not None:
+ id = lb.get("id", None)
+ name = lb.get("name", None)
+ lb_region = lb.get("region", None)
+ if not lb_region:
+ self.module.fail_json(
+ msg="Unexpected error; please file a bug: delete missing region"
+ )
+ lb_region_slug = lb_region.get("slug", None)
+ if id is None or name is None or lb_region_slug is None:
+ self.module.fail_json(
+ msg="Unexpected error; please file a bug: delete missing id, name, or region slug"
+ )
+ else:
+ response = self.rest.delete("load_balancers/{0}".format(id))
+ json_data = response.json
+ if response.status_code == 204:
+ # Response body should be empty
+ self.module.exit_json(
+ changed=True,
+ msg="Load Balancer {0} ({1}) in {2} deleted".format(
+ name, id, lb_region_slug
+ ),
+ )
+ else:
+ message = json_data.get(
+ "message", "Empty failure message from the DigitalOcean API!"
+ )
+ self.module.fail_json(
+ changed=False,
+ msg="Failed to delete Load Balancer {0} ({1}) in {2}: {3}".format(
+ name, id, lb_region_slug, message
+ ),
+ )
+ else:
+ self.module.fail_json(
+ changed=False,
+ msg="Load Balancer {0} not found in {1}".format(self.name, self.region),
+ )
+
+
+def run(module):
+ state = module.params.pop("state")
+ lb = DOLoadBalancer(module)
+ if state == "present":
+ lb.create()
+ elif state == "absent":
+ lb.delete()
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ argument_spec.update(
+ state=dict(choices=["present", "absent"], default="present"),
+ name=dict(type="str", required=True),
+ size=dict(
+ type="str",
+ choices=list(DOLoadBalancer.all_sizes),
+ required=False,
+ default=DOLoadBalancer.default_size,
+ ),
+ size_unit=dict(
+ type="int",
+ required=False,
+ default=DOLoadBalancer.default_size_unit,
+ ),
+ droplet_ids=dict(type="list", elements="int", required=False),
+ tag=dict(type="str", required=False),
+ region=dict(
+ aliases=["region_id"],
+ required=False,
+ ),
+ forwarding_rules=dict(
+ type="list",
+ elements="dict",
+ required=False,
+ default=[
+ {
+ "entry_protocol": "http",
+ "entry_port": 8080,
+ "target_protocol": "http",
+ "target_port": 8080,
+ "certificate_id": "",
+ "tls_passthrough": False,
+ }
+ ],
+ ),
+ health_check=dict(
+ type="dict",
+ required=False,
+ default=dict(
+ {
+ "protocol": "http",
+ "port": 80,
+ "path": "/",
+ "check_interval_seconds": 10,
+ "response_timeout_seconds": 5,
+ "healthy_threshold": 5,
+ "unhealthy_threshold": 3,
+ }
+ ),
+ ),
+ sticky_sessions=dict(
+ type="dict", required=False, default=dict({"type": "none"})
+ ),
+ redirect_http_to_https=dict(type="bool", required=False, default=False),
+ enable_proxy_protocol=dict(type="bool", required=False, default=False),
+ enable_backend_keepalive=dict(type="bool", required=False, default=False),
+ vpc_uuid=dict(type="str", required=False),
+ wait=dict(type="bool", default=True),
+ wait_timeout=dict(type="int", default=600),
+ project_name=dict(type="str", aliases=["project"], required=False, default=""),
+ )
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ required_if=(
+ [
+ ("state", "present", ["forwarding_rules"]),
+ ("state", "present", ["tag", "droplet_ids"], True),
+ ]
+ ),
+ # Droplet ID and tag are mutually exclusive, check that both have not been defined
+ mutually_exclusive=(
+ [
+ ("tag", "droplet_ids"),
+ ("size", "size_unit"),
+ ]
+ ),
+ supports_check_mode=True,
+ )
+
+ run(module)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_load_balancer_facts.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_load_balancer_facts.py
new file mode 100644
index 00000000..bc1f9250
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_load_balancer_facts.py
@@ -0,0 +1,128 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2018, Ansible Project
+# Copyright: (c) 2018, Abhijeet Kasurde <akasurde@redhat.com>
+# 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: digital_ocean_load_balancer_info
+short_description: Gather information about DigitalOcean load balancers
+description:
+ - This module can be used to gather information about DigitalOcean provided load balancers.
+ - This module was called C(digital_ocean_load_balancer_facts) before Ansible 2.9. The usage did not change.
+author: "Abhijeet Kasurde (@Akasurde)"
+options:
+ load_balancer_id:
+ description:
+ - Load balancer ID that can be used to identify and reference a load_balancer.
+ required: false
+ type: str
+requirements:
+ - "python >= 2.6"
+extends_documentation_fragment:
+- community.digitalocean.digital_ocean.documentation
+
+"""
+
+
+EXAMPLES = r"""
+- name: Gather information about all load balancers
+ community.digitalocean.digital_ocean_load_balancer_info:
+ oauth_token: "{{ oauth_token }}"
+
+- name: Gather information about load balancer with given id
+ community.digitalocean.digital_ocean_load_balancer_info:
+ oauth_token: "{{ oauth_token }}"
+ load_balancer_id: "4de7ac8b-495b-4884-9a69-1050c6793cd6"
+
+- name: Get name from load balancer id
+ community.digitalocean.digital_ocean_load_balancer_info:
+ register: resp_out
+- set_fact:
+ load_balancer_name: "{{ item.name }}"
+ loop: "{{ resp_out.data | community.general.json_query(name) }}"
+ vars:
+ name: "[?id=='4de7ac8b-495b-4884-9a69-1050c6793cd6']"
+- debug:
+ var: load_balancer_name
+"""
+
+
+RETURN = r"""
+data:
+ description: DigitalOcean Load balancer information
+ returned: success
+ type: list
+ elements: dict
+ sample: [
+ {
+ "id": "4de7ac8b-495b-4884-9a69-1050c6793cd6",
+ "name": "example-lb-01",
+ "ip": "104.131.186.241",
+ "algorithm": "round_robin",
+ "status": "new",
+ "created_at": "2017-02-01T22:22:58Z",
+ ...
+ },
+ ]
+"""
+
+from traceback import format_exc
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+from ansible.module_utils._text import to_native
+
+
+def core(module):
+ load_balancer_id = module.params.get("load_balancer_id", None)
+ rest = DigitalOceanHelper(module)
+
+ base_url = "load_balancers"
+ if load_balancer_id is not None:
+ response = rest.get("%s/%s" % (base_url, load_balancer_id))
+ status_code = response.status_code
+
+ if status_code != 200:
+ module.fail_json(msg="Failed to retrieve load balancers for DigitalOcean")
+
+ load_balancer = [response.json["load_balancer"]]
+ else:
+ load_balancer = rest.get_paginated_data(
+ base_url=base_url + "?", data_key_name="load_balancers"
+ )
+
+ module.exit_json(changed=False, data=load_balancer)
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ argument_spec.update(
+ load_balancer_id=dict(type="str", required=False),
+ )
+ module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
+ if module._name in (
+ "digital_ocean_load_balancer_facts",
+ "community.digitalocean.digital_ocean_load_balancer_facts",
+ ):
+ module.deprecate(
+ "The 'digital_ocean_load_balancer_facts' module has been renamed to 'digital_ocean_load_balancer_info'",
+ version="2.0.0",
+ collection_name="community.digitalocean",
+ ) # was Ansible 2.13
+
+ try:
+ core(module)
+ except Exception as e:
+ module.fail_json(msg=to_native(e), exception=format_exc())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_load_balancer_info.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_load_balancer_info.py
new file mode 100644
index 00000000..bc1f9250
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_load_balancer_info.py
@@ -0,0 +1,128 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2018, Ansible Project
+# Copyright: (c) 2018, Abhijeet Kasurde <akasurde@redhat.com>
+# 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: digital_ocean_load_balancer_info
+short_description: Gather information about DigitalOcean load balancers
+description:
+ - This module can be used to gather information about DigitalOcean provided load balancers.
+ - This module was called C(digital_ocean_load_balancer_facts) before Ansible 2.9. The usage did not change.
+author: "Abhijeet Kasurde (@Akasurde)"
+options:
+ load_balancer_id:
+ description:
+ - Load balancer ID that can be used to identify and reference a load_balancer.
+ required: false
+ type: str
+requirements:
+ - "python >= 2.6"
+extends_documentation_fragment:
+- community.digitalocean.digital_ocean.documentation
+
+"""
+
+
+EXAMPLES = r"""
+- name: Gather information about all load balancers
+ community.digitalocean.digital_ocean_load_balancer_info:
+ oauth_token: "{{ oauth_token }}"
+
+- name: Gather information about load balancer with given id
+ community.digitalocean.digital_ocean_load_balancer_info:
+ oauth_token: "{{ oauth_token }}"
+ load_balancer_id: "4de7ac8b-495b-4884-9a69-1050c6793cd6"
+
+- name: Get name from load balancer id
+ community.digitalocean.digital_ocean_load_balancer_info:
+ register: resp_out
+- set_fact:
+ load_balancer_name: "{{ item.name }}"
+ loop: "{{ resp_out.data | community.general.json_query(name) }}"
+ vars:
+ name: "[?id=='4de7ac8b-495b-4884-9a69-1050c6793cd6']"
+- debug:
+ var: load_balancer_name
+"""
+
+
+RETURN = r"""
+data:
+ description: DigitalOcean Load balancer information
+ returned: success
+ type: list
+ elements: dict
+ sample: [
+ {
+ "id": "4de7ac8b-495b-4884-9a69-1050c6793cd6",
+ "name": "example-lb-01",
+ "ip": "104.131.186.241",
+ "algorithm": "round_robin",
+ "status": "new",
+ "created_at": "2017-02-01T22:22:58Z",
+ ...
+ },
+ ]
+"""
+
+from traceback import format_exc
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+from ansible.module_utils._text import to_native
+
+
+def core(module):
+ load_balancer_id = module.params.get("load_balancer_id", None)
+ rest = DigitalOceanHelper(module)
+
+ base_url = "load_balancers"
+ if load_balancer_id is not None:
+ response = rest.get("%s/%s" % (base_url, load_balancer_id))
+ status_code = response.status_code
+
+ if status_code != 200:
+ module.fail_json(msg="Failed to retrieve load balancers for DigitalOcean")
+
+ load_balancer = [response.json["load_balancer"]]
+ else:
+ load_balancer = rest.get_paginated_data(
+ base_url=base_url + "?", data_key_name="load_balancers"
+ )
+
+ module.exit_json(changed=False, data=load_balancer)
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ argument_spec.update(
+ load_balancer_id=dict(type="str", required=False),
+ )
+ module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
+ if module._name in (
+ "digital_ocean_load_balancer_facts",
+ "community.digitalocean.digital_ocean_load_balancer_facts",
+ ):
+ module.deprecate(
+ "The 'digital_ocean_load_balancer_facts' module has been renamed to 'digital_ocean_load_balancer_info'",
+ version="2.0.0",
+ collection_name="community.digitalocean",
+ ) # was Ansible 2.13
+
+ try:
+ core(module)
+ except Exception as e:
+ module.fail_json(msg=to_native(e), exception=format_exc())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_monitoring_alerts.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_monitoring_alerts.py
new file mode 100644
index 00000000..67825ccc
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_monitoring_alerts.py
@@ -0,0 +1,325 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Mark Mercado <mamercad@gmail.com>
+#
+# 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: digital_ocean_monitoring_alerts
+version_added: 1.10.0
+short_description: Programmatically retrieve metrics as well as configure alert policies based on these metrics
+description:
+ - The DigitalOcean Monitoring API makes it possible to programmatically retrieve metrics as well as configure alert policies based on these metrics.
+ - The Monitoring API can help you gain insight into how your apps are performing and consuming resources.
+author: "Mark Mercado (@mamercad)"
+options:
+ oauth_token:
+ description:
+ - DigitalOcean OAuth token; can be specified in C(DO_API_KEY), C(DO_API_TOKEN), or C(DO_OAUTH_TOKEN) environment variables
+ type: str
+ aliases: ["API_TOKEN"]
+ required: true
+ state:
+ description:
+ - The usual, C(present) to create, C(absent) to destroy
+ type: str
+ choices: ["present", "absent"]
+ default: present
+ alerts:
+ description:
+ - Alert object, required for C(state=present)
+ - Supports C(email["email1", "email2", ...]) and C(slack[{"channel1", "url1"}, {"channel2", "url2"}, ...])
+ type: dict
+ required: false
+ compare:
+ description: Alert comparison, required for C(state=present)
+ type: str
+ required: false
+ choices: ["GreaterThan", "LessThan"]
+ description:
+ description: Alert description, required for C(state=present)
+ type: str
+ required: false
+ enabled:
+ description: Enabled or not, required for C(state=present)
+ type: bool
+ required: false
+ entities:
+ description: Alert entities, required for C(state=present)
+ type: list
+ elements: str
+ required: false
+ tags:
+ description: Alert tags, required for C(state=present)
+ type: list
+ elements: str
+ required: false
+ type:
+ description:
+ - Alert type, required for C(state=present)
+ - See U(https://docs.digitalocean.com/reference/api/api-reference/#operation/create_alert_policy) for valid types
+ type: str
+ required: false
+ choices:
+ - v1/insights/droplet/load_1
+ - v1/insights/droplet/load_5
+ - v1/insights/droplet/load_15
+ - v1/insights/droplet/memory_utilization_percent
+ - v1/insights/droplet/disk_utilization_percent
+ - v1/insights/droplet/cpu
+ - v1/insights/droplet/disk_read
+ - v1/insights/droplet/disk_write
+ - v1/insights/droplet/public_outbound_bandwidth
+ - v1/insights/droplet/public_inbound_bandwidth
+ - v1/insights/droplet/private_outbound_bandwidth
+ - v1/insights/droplet/private_inbound_bandwidth
+ value:
+ description: Alert threshold, required for C(state=present)
+ type: float
+ required: false
+ window:
+ description: Alert window, required for C(state=present)
+ type: str
+ choices: ["5m", "10m", "30m", "1h"]
+ required: false
+ uuid:
+ description: Alert uuid, required for C(state=absent)
+ type: str
+ required: false
+"""
+
+
+EXAMPLES = r"""
+- name: Create Droplet Monitoring alerts policy
+ community.digitalocean.digital_ocean_monitoring_alerts:
+ state: present
+ oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
+ alerts:
+ email: ["alerts@example.com"]
+ slack: []
+ compare: GreaterThan
+ description: Droplet load1 alert
+ enabled: true
+ entities: ["{{ droplet_id }}"]
+ tags: ["my_alert_tag"]
+ type: v1/insights/droplet/load_1
+ value: 3.14159
+ window: 5m
+ register: monitoring_alert_policy
+
+- name: Delete Droplet Monitoring alerts policy
+ community.digitalocean.digital_ocean_monitoring_alerts:
+ state: absent
+ oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
+ uuid: "{{ monitoring_alert_policy.data.uuid }}"
+"""
+
+
+RETURN = r"""
+data:
+ description: A DigitalOcean Monitoring alerts policy
+ returned: changed
+ type: dict
+ sample:
+ alerts:
+ email:
+ - mamercad@gmail.com
+ slack: []
+ compare: GreaterThan
+ description: Droplet load1 alert
+ enabled: true
+ entities:
+ - '262383737'
+ tags:
+ - my_alert_tag
+ type: v1/insights/droplet/load_1
+ uuid: 9f988f00-4690-443d-b638-ed5a99bbad3b
+ value: 3.14159
+ window: 5m
+"""
+
+
+from ansible.module_utils._text import to_native
+from ansible.module_utils.basic import AnsibleModule, env_fallback
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+
+
+alert_types = [
+ "v1/insights/droplet/load_1",
+ "v1/insights/droplet/load_5",
+ "v1/insights/droplet/load_15",
+ "v1/insights/droplet/memory_utilization_percent",
+ "v1/insights/droplet/disk_utilization_percent",
+ "v1/insights/droplet/cpu",
+ "v1/insights/droplet/disk_read",
+ "v1/insights/droplet/disk_write",
+ "v1/insights/droplet/public_outbound_bandwidth",
+ "v1/insights/droplet/public_inbound_bandwidth",
+ "v1/insights/droplet/private_outbound_bandwidth",
+ "v1/insights/droplet/private_inbound_bandwidth",
+]
+
+alert_keys = [
+ "alerts",
+ "compare",
+ "description",
+ "enabled",
+ "entities",
+ "tags",
+ "type",
+ "value",
+ "window",
+]
+
+alert_windows = ["5m", "10m", "30m", "1h"]
+
+
+class DOMonitoringAlerts(object):
+ def __init__(self, module):
+ self.rest = DigitalOceanHelper(module)
+ self.module = module
+ # Pop these values so we don't include them in the POST data
+ self.module.params.pop("oauth_token")
+
+ def get_alerts(self):
+ alerts = self.rest.get_paginated_data(
+ base_url="monitoring/alerts?", data_key_name="policies"
+ )
+ return alerts
+
+ def get_alert(self):
+ alerts = self.rest.get_paginated_data(
+ base_url="monitoring/alerts?", data_key_name="policies"
+ )
+ for alert in alerts:
+ for alert_key in alert_keys:
+ if alert.get(alert_key, None) != self.module.params.get(
+ alert_key, None
+ ):
+ break # This key doesn't match, try the next alert.
+ else:
+ return alert # Didn't hit break, this alert matches.
+ return None
+
+ def create(self):
+ # Check for an existing (same) one.
+ alert = self.get_alert()
+ if alert is not None:
+ self.module.exit_json(
+ changed=False,
+ data=alert,
+ )
+
+ # Check mode
+ if self.module.check_mode:
+ self.module.exit_json(changed=True)
+
+ # Create it.
+ request_params = dict(self.module.params)
+ response = self.rest.post("monitoring/alerts", data=request_params)
+ if response.status_code == 200:
+ alert = self.get_alert()
+ if alert is not None:
+ self.module.exit_json(
+ changed=True,
+ data=alert,
+ )
+ else:
+ self.module.fail_json(
+ changed=False, msg="Unexpected error; please file a bug: create"
+ )
+ else:
+ self.module.fail_json(
+ msg="Create Monitoring Alert '{0}' failed [HTTP {1}: {2}]".format(
+ self.module.params.get("description"),
+ response.status_code,
+ response.json.get("message", None),
+ )
+ )
+
+ def delete(self):
+ uuid = self.module.params.get("uuid", None)
+ if uuid is not None:
+
+ # Check mode
+ if self.module.check_mode:
+ self.module.exit_json(changed=True)
+
+ # Delete it
+ response = self.rest.delete("monitoring/alerts/{0}".format(uuid))
+ if response.status_code == 204:
+ self.module.exit_json(
+ changed=True,
+ msg="Deleted Monitoring Alert {0}".format(uuid),
+ )
+ else:
+ self.module.fail_json(
+ msg="Delete Monitoring Alert {0} failed [HTTP {1}: {2}]".format(
+ uuid,
+ response.status_code,
+ response.json.get("message", None),
+ )
+ )
+ else:
+ self.module.fail_json(
+ changed=False, msg="Unexpected error; please file a bug: delete"
+ )
+
+
+def run(module):
+ state = module.params.pop("state")
+ alerts = DOMonitoringAlerts(module)
+ if state == "present":
+ alerts.create()
+ else:
+ alerts.delete()
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ oauth_token=dict(
+ aliases=["API_TOKEN"],
+ no_log=True,
+ fallback=(
+ env_fallback,
+ ["DO_API_TOKEN", "DO_API_KEY", "DO_OAUTH_TOKEN"],
+ ),
+ required=True,
+ ),
+ state=dict(
+ choices=["present", "absent"], default="present", required=False
+ ),
+ alerts=dict(type="dict", required=False),
+ compare=dict(
+ type="str", choices=["GreaterThan", "LessThan"], required=False
+ ),
+ description=dict(type="str", required=False),
+ enabled=dict(type="bool", required=False),
+ entities=dict(type="list", elements="str", required=False),
+ tags=dict(type="list", elements="str", required=False),
+ type=dict(type="str", choices=alert_types, required=False),
+ value=dict(type="float", required=False),
+ window=dict(type="str", choices=alert_windows, required=False),
+ uuid=dict(type="str", required=False),
+ ),
+ required_if=(
+ [
+ ("state", "present", alert_keys),
+ ("state", "absent", ["uuid"]),
+ ]
+ ),
+ supports_check_mode=True,
+ )
+ run(module)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_monitoring_alerts_info.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_monitoring_alerts_info.py
new file mode 100644
index 00000000..a5d87ad6
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_monitoring_alerts_info.py
@@ -0,0 +1,155 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Mark Mercado <mamercad@gmail.com>
+#
+# 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: digital_ocean_monitoring_alerts_info
+version_added: 1.10.0
+short_description: Programmatically retrieve metrics as well as configure alert policies based on these metrics
+description:
+ - The DigitalOcean Monitoring API makes it possible to programmatically retrieve metrics as well as configure alert policies based on these metrics.
+ - The Monitoring API can help you gain insight into how your apps are performing and consuming resources.
+author: "Mark Mercado (@mamercad)"
+options:
+ state:
+ description:
+ - C(present) to return alerts
+ type: str
+ choices: ["present"]
+ default: present
+ oauth_token:
+ description:
+ - DigitalOcean OAuth token; can be specified in C(DO_API_KEY), C(DO_API_TOKEN), or C(DO_OAUTH_TOKEN) environment variables
+ type: str
+ aliases: ["API_TOKEN"]
+ required: true
+ uuid:
+ description:
+ - Alert uuid (if specified only returns the specific alert policy)
+ type: str
+ required: false
+"""
+
+
+EXAMPLES = r"""
+- name: Get Droplet Monitoring alerts polices
+ community.digitalocean.digital_ocean_monitoring_alerts_info:
+ oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
+ register: monitoring_alerts
+
+- name: Get specific Droplet Monitoring alerts policy
+ community.digitalocean.digital_ocean_monitoring_alerts_info:
+ oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
+ uuid: ec48b0e7-23bb-4a7f-95f2-d83da62fcd60
+ register: monitoring_alert
+"""
+
+
+RETURN = r"""
+data:
+ description: DigitalOcean Monitoring alerts policies
+ returned: changed
+ type: dict
+ sample:
+ data:
+ - alerts:
+ email:
+ - mamercad@gmail.com
+ slack: []
+ compare: GreaterThan
+ description: Droplet load1 alert
+ enabled: true
+ entities:
+ - '262383737'
+ tags:
+ - my_alert_tag
+ type: v1/insights/droplet/load_1
+ uuid: ec48b0e7-23bb-4a7f-95f2-d83da62fcd60
+ value: 3.14159
+ window: 5m
+"""
+
+
+from ansible.module_utils._text import to_native
+from ansible.module_utils.basic import AnsibleModule, env_fallback
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+
+
+class DOMonitoringAlertsInfo(object):
+ def __init__(self, module):
+ self.rest = DigitalOceanHelper(module)
+ self.module = module
+ # Pop these values so we don't include them in the POST data
+ self.module.params.pop("oauth_token")
+
+ def get_alerts(self):
+ alerts = self.rest.get_paginated_data(
+ base_url="monitoring/alerts?", data_key_name="policies"
+ )
+ self.module.exit_json(
+ changed=False,
+ data=alerts,
+ )
+
+ def get_alert(self, uuid):
+ alerts = self.rest.get_paginated_data(
+ base_url="monitoring/alerts?", data_key_name="policies"
+ )
+ for alert in alerts:
+ alert_uuid = alert.get("uuid", None)
+ if alert_uuid is not None:
+ if alert_uuid == uuid:
+ self.module.exit_json(
+ changed=False,
+ data=alert,
+ )
+ else:
+ self.module.fail_json(
+ changed=False, msg="Unexpected error; please file a bug: get_alert"
+ )
+ self.module.exit_json(
+ changed=False,
+ data=[],
+ )
+
+
+def run(module):
+ alerts = DOMonitoringAlertsInfo(module)
+ uuid = module.params.get("uuid", None)
+ if uuid is None:
+ alerts.get_alerts()
+ else:
+ alerts.get_alert(uuid)
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ state=dict(choices=["present"], default="present"),
+ oauth_token=dict(
+ aliases=["API_TOKEN"],
+ no_log=True,
+ fallback=(
+ env_fallback,
+ ["DO_API_TOKEN", "DO_API_KEY", "DO_OAUTH_TOKEN"],
+ ),
+ required=True,
+ ),
+ uuid=dict(type="str", required=False),
+ ),
+ supports_check_mode=True,
+ )
+ run(module)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_project.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_project.py
new file mode 100644
index 00000000..e0333883
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_project.py
@@ -0,0 +1,315 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright: Ansible Project
+# 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: digital_ocean_project
+short_description: Manage a DigitalOcean project
+description:
+ - Manage a project in DigitalOcean
+author: "Tyler Auerbeck (@tylerauerbeck)"
+version_added: 1.6.0
+
+options:
+ state:
+ description:
+ - Indicate desired state of the target.
+ - C(present) will create the project
+ - C(absent) will delete the project, if it exists.
+ default: present
+ choices: ['present', 'absent']
+ type: str
+ oauth_token:
+ description:
+ - DigitalOcean OAuth token. Can be specified in C(DO_API_KEY), C(DO_API_TOKEN), or C(DO_OAUTH_TOKEN) environment variables
+ aliases: ['API_TOKEN']
+ type: str
+ required: true
+ environment:
+ description:
+ - The environment of the projects resources.
+ choices: ['Development', 'Staging', 'Production']
+ type: str
+ is_default:
+ description:
+ - If true, all resources will be added to this project if no project is specified.
+ default: False
+ type: bool
+ name:
+ description:
+ - The human-readable name for the project. The maximum length is 175 characters and the name must be unique.
+ type: str
+ id:
+ description:
+ - UUID of the project
+ type: str
+ purpose:
+ description:
+ - The purpose of the project. The maximum length is 255 characters
+ - Required if state is C(present)
+ - If not one of DO provided purposes, will be prefixed with C(Other)
+ - DO provided purposes can be found below
+ - C(Just trying out DigitalOcean)
+ - C(Class project/Educational Purposes)
+ - C(Website or blog)
+ - C(Web Application)
+ - C(Service or API)
+ - C(Mobile Application)
+ - C(Machine Learning/AI/Data Processing)
+ - C(IoT)
+ - C(Operational/Developer tooling)
+ type: str
+ description:
+ description:
+ - The description of the project. The maximum length is 255 characters.
+ type: str
+"""
+
+
+EXAMPLES = r"""
+# Creates a new project
+- community.digitalocean.digital_ocean_project:
+ name: "TestProj"
+ state: "present"
+ description: "This is a test project"
+ purpose: "IoT"
+ environment: "Development"
+
+# Updates the existing project with the new environment
+- community.digitalocean.digital_ocean_project:
+ name: "TestProj"
+ state: "present"
+ description: "This is a test project"
+ purpose: "IoT"
+ environment: "Production"
+
+# This renames an existing project by utilizing the id of the project
+- community.digitalocean.digital_ocean_project:
+ name: "TestProj2"
+ id: "12312312-abcd-efgh-ijkl-123123123123"
+ state: "present"
+ description: "This is a test project"
+ purpose: "IoT"
+ environment: "Development"
+
+# This creates a project that results with a purpose of "Other: My Prod App"
+- community.digitalocean.digital_ocean_project:
+ name: "ProdProj"
+ state: "present"
+ description: "This is a prod app"
+ purpose: "My Prod App"
+ environment: "Production"
+
+# This removes a project
+- community.digitalocean.digital_ocean_project:
+ name: "ProdProj"
+ state: "absent"
+"""
+
+RETURN = r"""
+# Digital Ocean API info https://docs.digitalocean.com/reference/api/api-reference/#tag/Projects
+data:
+ description: a DigitalOcean Project
+ returned: changed
+ type: dict
+ sample: {
+ "project": {
+ "created_at": "2021-05-28T00:00:00Z",
+ "description": "This is a test description",
+ "environment": "Development",
+ "id": "12312312-abcd-efgh-1234-abcdefgh123",
+ "is_default": false,
+ "name": "Test123",
+ "owner_id": 1234567,
+ "owner_uuid": "12312312-1234-5678-abcdefghijklm",
+ "purpose": "IoT",
+ "updated_at": "2021-05-29T00:00:00Z",
+ }
+ }
+"""
+
+import time
+import json
+from ansible.module_utils.basic import AnsibleModule, env_fallback
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+
+
+class DOProject(object):
+ def __init__(self, module):
+ self.rest = DigitalOceanHelper(module)
+ self.module = module
+ # pop the oauth token so we don't include it in the POST data
+ self.module.params.pop("oauth_token")
+ self.id = None
+ self.name = None
+ self.purpose = None
+ self.description = None
+ self.environment = None
+ self.is_default = None
+
+ def get_by_id(self, project_id):
+ if not project_id:
+ return None
+ response = self.rest.get("projects/{0}".format(project_id))
+ json_data = response.json
+ if response.status_code == 200:
+ project = json_data.get("project", None)
+ if project is not None:
+ self.id = project.get("id", None)
+ self.name = project.get("name", None)
+ self.purpose = project.get("purpose", None)
+ self.description = project.get("description", None)
+ self.environment = project.get("environment", None)
+ self.is_default = project.get("is_default", None)
+ return json_data
+ return None
+
+ def get_by_name(self, project_name):
+ if not project_name:
+ return None
+ page = 1
+ while page is not None:
+ response = self.rest.get("projects?page={0}".format(page))
+ json_data = response.json
+ if response.status_code == 200:
+ for project in json_data["projects"]:
+ if project.get("name", None) == project_name:
+ self.id = project.get("id", None)
+ self.name = project.get("name", None)
+ self.description = project.get("description", None)
+ self.purpose = project.get("purpose", None)
+ self.environment = project.get("environment", None)
+ self.is_default = project.get("is_default", None)
+ return {"project": project}
+ if (
+ "links" in json_data
+ and "pages" in json_data["links"]
+ and "next" in json_data["links"]["pages"]
+ ):
+ page += 1
+ else:
+ page = None
+ return None
+
+ def get_project(self):
+ json_data = self.get_by_id(self.module.params["id"])
+ if not json_data:
+ json_data = self.get_by_name(self.module.params["name"])
+ return json_data
+
+ def create(self, state):
+ json_data = self.get_project()
+ request_params = dict(self.module.params)
+
+ if json_data is not None:
+ changed = False
+ valid_purpose = [
+ "Just trying out DigitalOcean",
+ "Class project/Educational Purposes",
+ "Website or blog",
+ "Web Application",
+ "Service or API",
+ "Mobile Application",
+ "Machine Learning/AI/Data Processing",
+ "IoT",
+ "Operational/Developer tooling",
+ ]
+ for key in request_params.keys():
+ if (
+ key == "purpose"
+ and request_params[key] is not None
+ and request_params[key] not in valid_purpose
+ ):
+ param = "Other: " + request_params[key]
+ else:
+ param = request_params[key]
+
+ if json_data["project"][key] != param and param is not None:
+ changed = True
+
+ if changed:
+ response = self.rest.put(
+ "projects/{0}".format(json_data["project"]["id"]),
+ data=request_params,
+ )
+ if response.status_code != 200:
+ self.module.fail_json(changed=False, msg="Unable to update project")
+ self.module.exit_json(changed=True, data=response.json)
+ else:
+ self.module.exit_json(changed=False, data=json_data)
+ else:
+ response = self.rest.post("projects", data=request_params)
+
+ if response.status_code != 201:
+ self.module.fail_json(changed=False, msg="Unable to create project")
+ self.module.exit_json(changed=True, data=response.json)
+
+ def delete(self):
+ json_data = self.get_project()
+ if json_data:
+ if self.module.check_mode:
+ self.module.exit_json(changed=True)
+ response = self.rest.delete(
+ "projects/{0}".format(json_data["project"]["id"])
+ )
+ json_data = response.json
+ if response.status_code == 204:
+ self.module.exit_json(changed=True, msg="Project deleted")
+ self.module.fail_json(changed=False, msg="Failed to delete project")
+ else:
+ self.module.exit_json(changed=False, msg="Project not found")
+
+
+def core(module):
+ state = module.params.pop("state")
+ project = DOProject(module)
+ if state == "present":
+ project.create(state)
+ elif state == "absent":
+ project.delete()
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ state=dict(choices=["present", "absent"], default="present", type="str"),
+ oauth_token=dict(
+ aliases=["API_TOKEN"],
+ no_log=True,
+ fallback=(
+ env_fallback,
+ ["DO_API_TOKEN", "DO_API_KEY", "DO_OAUTH_TOKEN"],
+ ),
+ required=True,
+ ),
+ name=dict(type="str"),
+ id=dict(type="str"),
+ description=dict(type="str"),
+ purpose=dict(type="str"),
+ is_default=dict(type="bool", default=False),
+ environment=dict(
+ choices=["Development", "Staging", "Production"], type="str"
+ ),
+ ),
+ required_one_of=(["id", "name"],),
+ required_if=(
+ [
+ ("state", "present", ["purpose"]),
+ ]
+ ),
+ )
+
+ core(module)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_project_info.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_project_info.py
new file mode 100644
index 00000000..0c6ac670
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_project_info.py
@@ -0,0 +1,121 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2018, Ansible Project
+# Copyright: (c) 2020, Tyler Auerbeck <tauerbec@redhat.com>
+# 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: digital_ocean_project_info
+short_description: Gather information about DigitalOcean Projects
+description:
+ - This module can be used to gather information about Projects.
+author: "Tyler Auerbeck (@tylerauerbeck)"
+version_added: 1.6.0
+
+options:
+ id:
+ description:
+ - Project ID that can be used to identify and reference a project.
+ type: str
+ name:
+ description:
+ - Project name that can be used to identify and reference a project.
+ type: str
+
+extends_documentation_fragment:
+- community.digitalocean.digital_ocean
+"""
+
+
+EXAMPLES = r"""
+# Get specific project by id
+- community.digitalocean.digital_ocean_project_info:
+ id: cb1ef55e-3cd8-4c7c-aa5d-07c32bf41627
+
+# Get specific project by name
+- community.digitalocean.digital_ocean_project_info:
+ name: my-project-name
+
+# Get all projects
+- community.digitalocean.digital_ocean_project_info:
+ register: projects
+"""
+
+RETURN = r"""
+data:
+ description: "DigitalOcean project information"
+ elements: dict
+ returned: success
+ type: list
+ sample:
+ - created_at: "2021-03-11T00:00:00Z"
+ description: "My project description"
+ environment: "Development"
+ id: "12345678-abcd-efgh-5678-10111213"
+ is_default: false
+ name: "my-test-project"
+ owner_id: 12345678
+ owner_uuid: "12345678-1234-4321-abcd-20212223"
+ purpose: ""
+ updated_at: "2021-03-11T00:00:00Z"
+"""
+
+from traceback import format_exc
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+
+
+def run(module):
+ rest = DigitalOceanHelper(module)
+
+ if module.params["id"]:
+ response = rest.get("projects/{0}".format(module.params["id"]))
+ if response.status_code != 200:
+ module.fail_json(
+ msg="Failed to fetch 'projects' information due to error: %s"
+ % response.json["message"]
+ )
+ else:
+ response = rest.get_paginated_data(
+ base_url="projects?", data_key_name="projects"
+ )
+
+ if module.params["id"]:
+ data = [response.json["project"]]
+ elif module.params["name"]:
+ data = [d for d in response if d["name"] == module.params["name"]]
+ if not data:
+ module.fail_json(
+ msg="Failed to fetch 'projects' information due to error: Unable to find project with name %s"
+ % module.params["name"]
+ )
+ else:
+ data = response
+
+ module.exit_json(changed=False, data=data)
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ argument_spec.update(
+ name=dict(type="str", required=False, default=None),
+ id=dict(type="str", required=False, default=None),
+ )
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ mutually_exclusive=[("id", "name")],
+ )
+ run(module)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_region_facts.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_region_facts.py
new file mode 100644
index 00000000..9ffb8ff3
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_region_facts.py
@@ -0,0 +1,125 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2018, Ansible Project
+# Copyright: (c) 2018, Abhijeet Kasurde <akasurde@redhat.com>
+# 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: digital_ocean_region_info
+short_description: Gather information about DigitalOcean regions
+description:
+ - This module can be used to gather information about regions.
+ - This module was called C(digital_ocean_region_facts) before Ansible 2.9. The usage did not change.
+author: "Abhijeet Kasurde (@Akasurde)"
+extends_documentation_fragment:
+- community.digitalocean.digital_ocean.documentation
+
+requirements:
+ - "python >= 2.6"
+"""
+
+
+EXAMPLES = r"""
+- name: Gather information about all regions
+ community.digitalocean.digital_ocean_region_info:
+ oauth_token: "{{ oauth_token }}"
+
+- name: Get Name of region where slug is known
+ community.digitalocean.digital_ocean_region_info:
+ oauth_token: "{{ oauth_token }}"
+ register: resp_out
+- debug: var=resp_out
+- set_fact:
+ region_slug: "{{ item.name }}"
+ loop: "{{ resp_out.data | community.general.json_query(name) }}"
+ vars:
+ name: "[?slug==`nyc1`]"
+- debug:
+ var: region_slug
+"""
+
+
+RETURN = r"""
+data:
+ description: DigitalOcean regions information
+ returned: success
+ type: list
+ sample: [
+ {
+ "available": true,
+ "features": [
+ "private_networking",
+ "backups",
+ "ipv6",
+ "metadata",
+ "install_agent",
+ "storage"
+ ],
+ "name": "New York 1",
+ "sizes": [
+ "512mb",
+ "s-1vcpu-1gb",
+ "1gb",
+ "s-3vcpu-1gb",
+ "s-1vcpu-2gb",
+ "s-2vcpu-2gb",
+ "2gb",
+ "s-1vcpu-3gb",
+ "s-2vcpu-4gb",
+ "4gb",
+ "c-2",
+ "m-1vcpu-8gb",
+ "8gb",
+ "s-4vcpu-8gb",
+ "s-6vcpu-16gb",
+ "16gb"
+ ],
+ "slug": "nyc1"
+ },
+ ]
+"""
+
+from traceback import format_exc
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+from ansible.module_utils._text import to_native
+
+
+def core(module):
+ rest = DigitalOceanHelper(module)
+
+ base_url = "regions?"
+ regions = rest.get_paginated_data(base_url=base_url, data_key_name="regions")
+
+ module.exit_json(changed=False, data=regions)
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
+ if module._name in (
+ "digital_ocean_region_facts",
+ "community.digitalocean.digital_ocean_region_facts",
+ ):
+ module.deprecate(
+ "The 'digital_ocean_region_facts' module has been renamed to 'digital_ocean_region_info'",
+ version="2.0.0",
+ collection_name="community.digitalocean",
+ ) # was Ansible 2.13
+
+ try:
+ core(module)
+ except Exception as e:
+ module.fail_json(msg=to_native(e), exception=format_exc())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_region_info.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_region_info.py
new file mode 100644
index 00000000..9ffb8ff3
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_region_info.py
@@ -0,0 +1,125 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2018, Ansible Project
+# Copyright: (c) 2018, Abhijeet Kasurde <akasurde@redhat.com>
+# 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: digital_ocean_region_info
+short_description: Gather information about DigitalOcean regions
+description:
+ - This module can be used to gather information about regions.
+ - This module was called C(digital_ocean_region_facts) before Ansible 2.9. The usage did not change.
+author: "Abhijeet Kasurde (@Akasurde)"
+extends_documentation_fragment:
+- community.digitalocean.digital_ocean.documentation
+
+requirements:
+ - "python >= 2.6"
+"""
+
+
+EXAMPLES = r"""
+- name: Gather information about all regions
+ community.digitalocean.digital_ocean_region_info:
+ oauth_token: "{{ oauth_token }}"
+
+- name: Get Name of region where slug is known
+ community.digitalocean.digital_ocean_region_info:
+ oauth_token: "{{ oauth_token }}"
+ register: resp_out
+- debug: var=resp_out
+- set_fact:
+ region_slug: "{{ item.name }}"
+ loop: "{{ resp_out.data | community.general.json_query(name) }}"
+ vars:
+ name: "[?slug==`nyc1`]"
+- debug:
+ var: region_slug
+"""
+
+
+RETURN = r"""
+data:
+ description: DigitalOcean regions information
+ returned: success
+ type: list
+ sample: [
+ {
+ "available": true,
+ "features": [
+ "private_networking",
+ "backups",
+ "ipv6",
+ "metadata",
+ "install_agent",
+ "storage"
+ ],
+ "name": "New York 1",
+ "sizes": [
+ "512mb",
+ "s-1vcpu-1gb",
+ "1gb",
+ "s-3vcpu-1gb",
+ "s-1vcpu-2gb",
+ "s-2vcpu-2gb",
+ "2gb",
+ "s-1vcpu-3gb",
+ "s-2vcpu-4gb",
+ "4gb",
+ "c-2",
+ "m-1vcpu-8gb",
+ "8gb",
+ "s-4vcpu-8gb",
+ "s-6vcpu-16gb",
+ "16gb"
+ ],
+ "slug": "nyc1"
+ },
+ ]
+"""
+
+from traceback import format_exc
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+from ansible.module_utils._text import to_native
+
+
+def core(module):
+ rest = DigitalOceanHelper(module)
+
+ base_url = "regions?"
+ regions = rest.get_paginated_data(base_url=base_url, data_key_name="regions")
+
+ module.exit_json(changed=False, data=regions)
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
+ if module._name in (
+ "digital_ocean_region_facts",
+ "community.digitalocean.digital_ocean_region_facts",
+ ):
+ module.deprecate(
+ "The 'digital_ocean_region_facts' module has been renamed to 'digital_ocean_region_info'",
+ version="2.0.0",
+ collection_name="community.digitalocean",
+ ) # was Ansible 2.13
+
+ try:
+ core(module)
+ except Exception as e:
+ module.fail_json(msg=to_native(e), exception=format_exc())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_size_facts.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_size_facts.py
new file mode 100644
index 00000000..19aba33e
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_size_facts.py
@@ -0,0 +1,124 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2018, Ansible Project
+# Copyright: (c) 2018, Abhijeet Kasurde <akasurde@redhat.com>
+# 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: digital_ocean_size_info
+short_description: Gather information about DigitalOcean Droplet sizes
+description:
+ - This module can be used to gather information about droplet sizes.
+ - This module was called C(digital_ocean_size_facts) before Ansible 2.9. The usage did not change.
+author: "Abhijeet Kasurde (@Akasurde)"
+requirements:
+ - "python >= 2.6"
+extends_documentation_fragment:
+- community.digitalocean.digital_ocean.documentation
+
+"""
+
+
+EXAMPLES = r"""
+- name: Gather information about all droplet sizes
+ community.digitalocean.digital_ocean_size_info:
+ oauth_token: "{{ oauth_token }}"
+
+- name: Get droplet Size Slug where vcpus is 1
+ community.digitalocean.digital_ocean_size_info:
+ oauth_token: "{{ oauth_token }}"
+ register: resp_out
+- debug: var=resp_out
+- set_fact:
+ size_slug: "{{ item.slug }}"
+ loop: "{{ resp_out.data | community.general.json_query(name) }}"
+ vars:
+ name: "[?vcpus==`1`]"
+- debug:
+ var: size_slug
+
+
+"""
+
+
+RETURN = r"""
+data:
+ description: DigitalOcean droplet size information
+ returned: success
+ type: list
+ sample: [
+ {
+ "available": true,
+ "disk": 20,
+ "memory": 512,
+ "price_hourly": 0.00744,
+ "price_monthly": 5.0,
+ "regions": [
+ "ams2",
+ "ams3",
+ "blr1",
+ "fra1",
+ "lon1",
+ "nyc1",
+ "nyc2",
+ "nyc3",
+ "sfo1",
+ "sfo2",
+ "sgp1",
+ "tor1"
+ ],
+ "slug": "512mb",
+ "transfer": 1.0,
+ "vcpus": 1
+ },
+ ]
+"""
+
+from traceback import format_exc
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+from ansible.module_utils._text import to_native
+
+
+def core(module):
+ rest = DigitalOceanHelper(module)
+
+ response = rest.get("sizes")
+ if response.status_code != 200:
+ module.fail_json(
+ msg="Failed to fetch 'sizes' information due to error : %s"
+ % response.json["message"]
+ )
+
+ module.exit_json(changed=False, data=response.json["sizes"])
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
+ if module._name in (
+ "digital_ocean_size_facts",
+ "community.digitalocean.digital_ocean_size_facts",
+ ):
+ module.deprecate(
+ "The 'digital_ocean_size_facts' module has been renamed to 'digital_ocean_size_info'",
+ version="2.0.0",
+ collection_name="community.digitalocean",
+ ) # was Ansible 2.13
+
+ try:
+ core(module)
+ except Exception as e:
+ module.fail_json(msg=to_native(e), exception=format_exc())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_size_info.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_size_info.py
new file mode 100644
index 00000000..19aba33e
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_size_info.py
@@ -0,0 +1,124 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2018, Ansible Project
+# Copyright: (c) 2018, Abhijeet Kasurde <akasurde@redhat.com>
+# 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: digital_ocean_size_info
+short_description: Gather information about DigitalOcean Droplet sizes
+description:
+ - This module can be used to gather information about droplet sizes.
+ - This module was called C(digital_ocean_size_facts) before Ansible 2.9. The usage did not change.
+author: "Abhijeet Kasurde (@Akasurde)"
+requirements:
+ - "python >= 2.6"
+extends_documentation_fragment:
+- community.digitalocean.digital_ocean.documentation
+
+"""
+
+
+EXAMPLES = r"""
+- name: Gather information about all droplet sizes
+ community.digitalocean.digital_ocean_size_info:
+ oauth_token: "{{ oauth_token }}"
+
+- name: Get droplet Size Slug where vcpus is 1
+ community.digitalocean.digital_ocean_size_info:
+ oauth_token: "{{ oauth_token }}"
+ register: resp_out
+- debug: var=resp_out
+- set_fact:
+ size_slug: "{{ item.slug }}"
+ loop: "{{ resp_out.data | community.general.json_query(name) }}"
+ vars:
+ name: "[?vcpus==`1`]"
+- debug:
+ var: size_slug
+
+
+"""
+
+
+RETURN = r"""
+data:
+ description: DigitalOcean droplet size information
+ returned: success
+ type: list
+ sample: [
+ {
+ "available": true,
+ "disk": 20,
+ "memory": 512,
+ "price_hourly": 0.00744,
+ "price_monthly": 5.0,
+ "regions": [
+ "ams2",
+ "ams3",
+ "blr1",
+ "fra1",
+ "lon1",
+ "nyc1",
+ "nyc2",
+ "nyc3",
+ "sfo1",
+ "sfo2",
+ "sgp1",
+ "tor1"
+ ],
+ "slug": "512mb",
+ "transfer": 1.0,
+ "vcpus": 1
+ },
+ ]
+"""
+
+from traceback import format_exc
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+from ansible.module_utils._text import to_native
+
+
+def core(module):
+ rest = DigitalOceanHelper(module)
+
+ response = rest.get("sizes")
+ if response.status_code != 200:
+ module.fail_json(
+ msg="Failed to fetch 'sizes' information due to error : %s"
+ % response.json["message"]
+ )
+
+ module.exit_json(changed=False, data=response.json["sizes"])
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
+ if module._name in (
+ "digital_ocean_size_facts",
+ "community.digitalocean.digital_ocean_size_facts",
+ ):
+ module.deprecate(
+ "The 'digital_ocean_size_facts' module has been renamed to 'digital_ocean_size_info'",
+ version="2.0.0",
+ collection_name="community.digitalocean",
+ ) # was Ansible 2.13
+
+ try:
+ core(module)
+ except Exception as e:
+ module.fail_json(msg=to_native(e), exception=format_exc())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_snapshot.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_snapshot.py
new file mode 100644
index 00000000..67dc0100
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_snapshot.py
@@ -0,0 +1,309 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2018, Ansible Project
+# Copyright: (c) 2021, Mark Mercado <mamercad@gmail.com>
+# 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: digital_ocean_snapshot
+short_description: Create and delete DigitalOcean snapshots
+version_added: 1.7.0
+description:
+ - This module can be used to create and delete DigitalOcean Droplet and volume snapshots.
+author: "Mark Mercado (@mamercad)"
+options:
+ state:
+ description:
+ - Whether the snapshot should be present (created) or absent (deleted).
+ default: present
+ choices:
+ - present
+ - absent
+ type: str
+ snapshot_type:
+ description:
+ - Specifies the type of snapshot information to be create or delete.
+ - If set to C(droplet), then a Droplet snapshot is created.
+ - If set to C(volume), then a volume snapshot is created.
+ choices:
+ - droplet
+ - volume
+ default: droplet
+ type: str
+ snapshot_name:
+ description:
+ - Name of the snapshot to create.
+ type: str
+ snapshot_tags:
+ description:
+ - List of tags to apply to the volume snapshot.
+ - Only applies to volume snapshots (not Droplets).
+ type: list
+ elements: str
+ droplet_id:
+ description:
+ - Droplet ID to snapshot.
+ type: str
+ volume_id:
+ description:
+ - Volume ID to snapshot.
+ type: str
+ snapshot_id:
+ description:
+ - Snapshot ID to delete.
+ type: str
+ wait:
+ description:
+ - Wait for the snapshot to be created before returning.
+ default: True
+ type: bool
+ wait_timeout:
+ description:
+ - How long before wait gives up, in seconds, when creating a snapshot.
+ default: 120
+ type: int
+extends_documentation_fragment:
+- community.digitalocean.digital_ocean.documentation
+
+"""
+
+
+EXAMPLES = r"""
+- name: Snapshot a Droplet
+ community.digitalocean.digital_ocean_snapshot:
+ state: present
+ snapshot_type: droplet
+ droplet_id: 250329179
+ register: result
+
+- name: Delete a Droplet snapshot
+ community.digitalocean.digital_ocean_snapshot:
+ state: absent
+ snapshot_type: droplet
+ snapshot_id: 85905825
+ register: result
+
+- name: Snapshot a Volume
+ community.digitalocean.digital_ocean_snapshot:
+ state: present
+ snapshot_type: volume
+ snapshot_name: mysnapshot1
+ volume_id: 9db5e329-cc68-11eb-b027-0a58ac144f91
+
+- name: Delete a Volume snapshot
+ community.digitalocean.digital_ocean_snapshot:
+ state: absent
+ snapshot_type: volume
+ snapshot_id: a902cdba-cc68-11eb-a701-0a58ac145708
+"""
+
+
+RETURN = r"""
+data:
+ description: Snapshot creation or deletion action.
+ returned: success
+ type: dict
+ sample:
+ - completed_at: '2021-06-14T12:36:00Z'
+ id: 1229119156
+ region:
+ available: true
+ features:
+ - backups
+ - ipv6
+ - metadata
+ - install_agent
+ - storage
+ - image_transfer
+ name: New York 1
+ sizes:
+ - s-1vcpu-1gb
+ - s-1vcpu-1gb-amd
+ - s-1vcpu-1gb-intel
+ - <snip>
+ slug: nyc1
+ region_slug: nyc1
+ resource_id: 250445117
+ resource_type: droplet
+ started_at: '2021-06-14T12:35:25Z'
+ status: completed
+ type: snapshot
+ - created_at: '2021-06-14T12:55:10Z'
+ id: c06d4a86-cd0f-11eb-b13c-0a58ac145472
+ min_disk_size: 1
+ name: my-snapshot-1
+ regions:
+ - nbg1
+ resource_id: f0adea7e-cd0d-11eb-b027-0a58ac144f91
+ resource_type: volume
+ size_gigabytes: 0
+ tags:
+ - tag1
+ - tag2
+"""
+
+
+import time
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+
+
+class DOSnapshot(object):
+ def __init__(self, module):
+ self.rest = DigitalOceanHelper(module)
+ self.module = module
+ self.wait = self.module.params.pop("wait", True)
+ self.wait_timeout = self.module.params.pop("wait_timeout", 120)
+ # pop the oauth token so we don't include it in the POST data
+ self.module.params.pop("oauth_token")
+ self.snapshot_type = module.params["snapshot_type"]
+ self.snapshot_name = module.params["snapshot_name"]
+ self.snapshot_tags = module.params["snapshot_tags"]
+ self.snapshot_id = module.params["snapshot_id"]
+ self.volume_id = module.params["volume_id"]
+
+ def wait_finished(self):
+ current_time = time.monotonic()
+ end_time = current_time + self.wait_timeout
+ while current_time < end_time:
+ response = self.rest.get("actions/{0}".format(str(self.action_id)))
+ status = response.status_code
+ if status != 200:
+ self.module.fail_json(
+ msg="Unable to find action {0}, please file a bug".format(
+ str(self.action_id)
+ )
+ )
+ json = response.json
+ if json["action"]["status"] == "completed":
+ return json
+ time.sleep(10)
+ self.module.fail_json(
+ msg="Timed out waiting for snapshot, action {0}".format(str(self.action_id))
+ )
+
+ def create(self):
+ if self.module.check_mode:
+ return self.module.exit_json(changed=True)
+
+ if self.snapshot_type == "droplet":
+ droplet_id = self.module.params["droplet_id"]
+ data = {
+ "type": "snapshot",
+ }
+ if self.snapshot_name is not None:
+ data["name"] = self.snapshot_name
+ response = self.rest.post(
+ "droplets/{0}/actions".format(str(droplet_id)), data=data
+ )
+ status = response.status_code
+ json = response.json
+ if status == 201:
+ self.action_id = json["action"]["id"]
+ if self.wait:
+ json = self.wait_finished()
+ self.module.exit_json(
+ changed=True,
+ msg="Created snapshot, action {0}".format(self.action_id),
+ data=json["action"],
+ )
+ self.module.exit_json(
+ changed=True,
+ msg="Created snapshot, action {0}".format(self.action_id),
+ data=json["action"],
+ )
+ else:
+ self.module.fail_json(
+ changed=False,
+ msg="Failed to create snapshot: {0}".format(json["message"]),
+ )
+ elif self.snapshot_type == "volume":
+ data = {
+ "name": self.snapshot_name,
+ "tags": self.snapshot_tags,
+ }
+ response = self.rest.post(
+ "volumes/{0}/snapshots".format(str(self.volume_id)), data=data
+ )
+ status = response.status_code
+ json = response.json
+ if status == 201:
+ self.module.exit_json(
+ changed=True,
+ msg="Created snapshot, snapshot {0}".format(json["snapshot"]["id"]),
+ data=json["snapshot"],
+ )
+ else:
+ self.module.fail_json(
+ changed=False,
+ msg="Failed to create snapshot: {0}".format(json["message"]),
+ )
+
+ def delete(self):
+ if self.module.check_mode:
+ return self.module.exit_json(changed=True)
+
+ response = self.rest.delete("snapshots/{0}".format(str(self.snapshot_id)))
+ status = response.status_code
+ if status == 204:
+ self.module.exit_json(
+ changed=True,
+ msg="Deleted snapshot {0}".format(str(self.snapshot_id)),
+ )
+ else:
+ json = response.json
+ self.module.fail_json(
+ changed=False,
+ msg="Failed to delete snapshot {0}: {1}".format(
+ self.snapshot_id, json["message"]
+ ),
+ )
+
+
+def run(module):
+ state = module.params.pop("state")
+ snapshot = DOSnapshot(module)
+ if state == "present":
+ snapshot.create()
+ elif state == "absent":
+ snapshot.delete()
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ argument_spec.update(
+ state=dict(choices=["present", "absent"], default="present"),
+ snapshot_type=dict(
+ type="str", required=False, choices=["droplet", "volume"], default="droplet"
+ ),
+ snapshot_name=dict(type="str"),
+ snapshot_tags=dict(type="list", elements="str", default=[]),
+ droplet_id=dict(type="str"),
+ volume_id=dict(type="str"),
+ snapshot_id=dict(type="str"),
+ wait=dict(type="bool", default=True),
+ wait_timeout=dict(default=120, type="int"),
+ )
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ required_if=[
+ ["state", "present", ["droplet_id", "volume_id"], True],
+ ["state", "absent", ["snapshot_id"]],
+ ],
+ mutually_exclusive=[["droplet_id", "volume_id"]],
+ supports_check_mode=True,
+ )
+
+ run(module)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_snapshot_facts.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_snapshot_facts.py
new file mode 100644
index 00000000..19d3c77d
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_snapshot_facts.py
@@ -0,0 +1,180 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2018, Ansible Project
+# Copyright: (c) 2018, Abhijeet Kasurde <akasurde@redhat.com>
+# 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: digital_ocean_snapshot_info
+short_description: Gather information about DigitalOcean Snapshot
+description:
+ - This module can be used to gather information about snapshot information based upon provided values such as droplet, volume and snapshot id.
+ - This module was called C(digital_ocean_snapshot_facts) before Ansible 2.9. The usage did not change.
+author: "Abhijeet Kasurde (@Akasurde)"
+options:
+ snapshot_type:
+ description:
+ - Specifies the type of snapshot information to be retrieved.
+ - If set to C(droplet), then information are gathered related to snapshots based on Droplets only.
+ - If set to C(volume), then information are gathered related to snapshots based on volumes only.
+ - If set to C(by_id), then information are gathered related to snapshots based on snapshot id only.
+ - If not set to any of the above, then information are gathered related to all snapshots.
+ default: 'all'
+ choices: [ 'all', 'droplet', 'volume', 'by_id']
+ required: false
+ type: str
+ snapshot_id:
+ description:
+ - To retrieve information about a snapshot, please specify this as a snapshot id.
+ - If set to actual snapshot id, then information are gathered related to that particular snapshot only.
+ - This is required parameter, if C(snapshot_type) is set to C(by_id).
+ required: false
+ type: str
+requirements:
+ - "python >= 2.6"
+extends_documentation_fragment:
+- community.digitalocean.digital_ocean.documentation
+
+"""
+
+
+EXAMPLES = r"""
+- name: Gather information about all snapshots
+ community.digitalocean.digital_ocean_snapshot_info:
+ snapshot_type: all
+ oauth_token: "{{ oauth_token }}"
+
+- name: Gather information about droplet snapshots
+ community.digitalocean.digital_ocean_snapshot_info:
+ snapshot_type: droplet
+ oauth_token: "{{ oauth_token }}"
+
+- name: Gather information about volume snapshots
+ community.digitalocean.digital_ocean_snapshot_info:
+ snapshot_type: volume
+ oauth_token: "{{ oauth_token }}"
+
+- name: Gather information about snapshot by snapshot id
+ community.digitalocean.digital_ocean_snapshot_info:
+ snapshot_type: by_id
+ snapshot_id: 123123123
+ oauth_token: "{{ oauth_token }}"
+
+- name: Get information about snapshot named big-data-snapshot1
+ community.digitalocean.digital_ocean_snapshot_info:
+ register: resp_out
+- set_fact:
+ snapshot_id: "{{ item.id }}"
+ loop: "{{ resp_out.data | community.general.json_query(name) }}"
+ vars:
+ name: "[?name=='big-data-snapshot1']"
+- debug:
+ var: snapshot_id
+
+"""
+
+
+RETURN = r"""
+data:
+ description: DigitalOcean snapshot information
+ returned: success
+ elements: dict
+ type: list
+ sample: [
+ {
+ "id": "4f60fc64-85d1-11e6-a004-000f53315871",
+ "name": "big-data-snapshot1",
+ "regions": [
+ "nyc1"
+ ],
+ "created_at": "2016-09-28T23:14:30Z",
+ "resource_id": "89bcc42f-85cf-11e6-a004-000f53315871",
+ "resource_type": "volume",
+ "min_disk_size": 10,
+ "size_gigabytes": 0
+ },
+ ]
+"""
+
+from traceback import format_exc
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+from ansible.module_utils._text import to_native
+
+
+def core(module):
+ snapshot_type = module.params["snapshot_type"]
+
+ rest = DigitalOceanHelper(module)
+
+ base_url = "snapshots"
+ snapshot = []
+
+ if snapshot_type == "by_id":
+ base_url += "/{0}".format(module.params.get("snapshot_id"))
+ response = rest.get(base_url)
+ status_code = response.status_code
+
+ if status_code != 200:
+ module.fail_json(
+ msg="Failed to fetch snapshot information due to error : %s"
+ % response.json["message"]
+ )
+
+ snapshot.extend(response.json["snapshots"])
+ else:
+ if snapshot_type == "droplet":
+ base_url += "?resource_type=droplet&"
+ elif snapshot_type == "volume":
+ base_url += "?resource_type=volume&"
+ else:
+ base_url += "?"
+
+ snapshot = rest.get_paginated_data(base_url=base_url, data_key_name="snapshots")
+ module.exit_json(changed=False, data=snapshot)
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ argument_spec.update(
+ snapshot_type=dict(
+ type="str",
+ required=False,
+ choices=["all", "droplet", "volume", "by_id"],
+ default="all",
+ ),
+ snapshot_id=dict(type="str", required=False),
+ )
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ required_if=[
+ ["snapshot_type", "by_id", ["snapshot_id"]],
+ ],
+ )
+ if module._name in (
+ "digital_ocean_snapshot_facts",
+ "community.digitalocean.digital_ocean_snapshot_facts",
+ ):
+ module.deprecate(
+ "The 'digital_ocean_snapshot_facts' module has been renamed to 'digital_ocean_snapshot_info'",
+ version="2.0.0",
+ collection_name="community.digitalocean",
+ ) # was Ansible 2.13
+
+ try:
+ core(module)
+ except Exception as e:
+ module.fail_json(msg=to_native(e), exception=format_exc())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_snapshot_info.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_snapshot_info.py
new file mode 100644
index 00000000..19d3c77d
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_snapshot_info.py
@@ -0,0 +1,180 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2018, Ansible Project
+# Copyright: (c) 2018, Abhijeet Kasurde <akasurde@redhat.com>
+# 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: digital_ocean_snapshot_info
+short_description: Gather information about DigitalOcean Snapshot
+description:
+ - This module can be used to gather information about snapshot information based upon provided values such as droplet, volume and snapshot id.
+ - This module was called C(digital_ocean_snapshot_facts) before Ansible 2.9. The usage did not change.
+author: "Abhijeet Kasurde (@Akasurde)"
+options:
+ snapshot_type:
+ description:
+ - Specifies the type of snapshot information to be retrieved.
+ - If set to C(droplet), then information are gathered related to snapshots based on Droplets only.
+ - If set to C(volume), then information are gathered related to snapshots based on volumes only.
+ - If set to C(by_id), then information are gathered related to snapshots based on snapshot id only.
+ - If not set to any of the above, then information are gathered related to all snapshots.
+ default: 'all'
+ choices: [ 'all', 'droplet', 'volume', 'by_id']
+ required: false
+ type: str
+ snapshot_id:
+ description:
+ - To retrieve information about a snapshot, please specify this as a snapshot id.
+ - If set to actual snapshot id, then information are gathered related to that particular snapshot only.
+ - This is required parameter, if C(snapshot_type) is set to C(by_id).
+ required: false
+ type: str
+requirements:
+ - "python >= 2.6"
+extends_documentation_fragment:
+- community.digitalocean.digital_ocean.documentation
+
+"""
+
+
+EXAMPLES = r"""
+- name: Gather information about all snapshots
+ community.digitalocean.digital_ocean_snapshot_info:
+ snapshot_type: all
+ oauth_token: "{{ oauth_token }}"
+
+- name: Gather information about droplet snapshots
+ community.digitalocean.digital_ocean_snapshot_info:
+ snapshot_type: droplet
+ oauth_token: "{{ oauth_token }}"
+
+- name: Gather information about volume snapshots
+ community.digitalocean.digital_ocean_snapshot_info:
+ snapshot_type: volume
+ oauth_token: "{{ oauth_token }}"
+
+- name: Gather information about snapshot by snapshot id
+ community.digitalocean.digital_ocean_snapshot_info:
+ snapshot_type: by_id
+ snapshot_id: 123123123
+ oauth_token: "{{ oauth_token }}"
+
+- name: Get information about snapshot named big-data-snapshot1
+ community.digitalocean.digital_ocean_snapshot_info:
+ register: resp_out
+- set_fact:
+ snapshot_id: "{{ item.id }}"
+ loop: "{{ resp_out.data | community.general.json_query(name) }}"
+ vars:
+ name: "[?name=='big-data-snapshot1']"
+- debug:
+ var: snapshot_id
+
+"""
+
+
+RETURN = r"""
+data:
+ description: DigitalOcean snapshot information
+ returned: success
+ elements: dict
+ type: list
+ sample: [
+ {
+ "id": "4f60fc64-85d1-11e6-a004-000f53315871",
+ "name": "big-data-snapshot1",
+ "regions": [
+ "nyc1"
+ ],
+ "created_at": "2016-09-28T23:14:30Z",
+ "resource_id": "89bcc42f-85cf-11e6-a004-000f53315871",
+ "resource_type": "volume",
+ "min_disk_size": 10,
+ "size_gigabytes": 0
+ },
+ ]
+"""
+
+from traceback import format_exc
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+from ansible.module_utils._text import to_native
+
+
+def core(module):
+ snapshot_type = module.params["snapshot_type"]
+
+ rest = DigitalOceanHelper(module)
+
+ base_url = "snapshots"
+ snapshot = []
+
+ if snapshot_type == "by_id":
+ base_url += "/{0}".format(module.params.get("snapshot_id"))
+ response = rest.get(base_url)
+ status_code = response.status_code
+
+ if status_code != 200:
+ module.fail_json(
+ msg="Failed to fetch snapshot information due to error : %s"
+ % response.json["message"]
+ )
+
+ snapshot.extend(response.json["snapshots"])
+ else:
+ if snapshot_type == "droplet":
+ base_url += "?resource_type=droplet&"
+ elif snapshot_type == "volume":
+ base_url += "?resource_type=volume&"
+ else:
+ base_url += "?"
+
+ snapshot = rest.get_paginated_data(base_url=base_url, data_key_name="snapshots")
+ module.exit_json(changed=False, data=snapshot)
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ argument_spec.update(
+ snapshot_type=dict(
+ type="str",
+ required=False,
+ choices=["all", "droplet", "volume", "by_id"],
+ default="all",
+ ),
+ snapshot_id=dict(type="str", required=False),
+ )
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ required_if=[
+ ["snapshot_type", "by_id", ["snapshot_id"]],
+ ],
+ )
+ if module._name in (
+ "digital_ocean_snapshot_facts",
+ "community.digitalocean.digital_ocean_snapshot_facts",
+ ):
+ module.deprecate(
+ "The 'digital_ocean_snapshot_facts' module has been renamed to 'digital_ocean_snapshot_info'",
+ version="2.0.0",
+ collection_name="community.digitalocean",
+ ) # was Ansible 2.13
+
+ try:
+ core(module)
+ except Exception as e:
+ module.fail_json(msg=to_native(e), exception=format_exc())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_spaces.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_spaces.py
new file mode 100644
index 00000000..0d101b50
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_spaces.py
@@ -0,0 +1,241 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: Ansible Project
+# 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: digital_ocean_spaces
+short_description: Create and remove DigitalOcean Spaces.
+description:
+ - Create and remove DigitalOcean Spaces.
+author: Mark Mercado (@mamercad)
+version_added: 1.15.0
+options:
+ state:
+ description:
+ - Whether the Space should be present or absent.
+ default: present
+ type: str
+ choices: ["present", "absent"]
+ name:
+ description:
+ - The name of the Spaces to create or delete.
+ required: true
+ type: str
+ region:
+ description:
+ - The region to create or delete the Space in.
+ aliases: ["region_id"]
+ required: true
+ type: str
+ aws_access_key_id:
+ description:
+ - The AWS_ACCESS_KEY_ID to use.
+ required: true
+ type: str
+ aliases: ["AWS_ACCESS_KEY_ID"]
+ aws_secret_access_key:
+ description:
+ - The AWS_SECRET_ACCESS_KEY to use.
+ required: true
+ type: str
+ aliases: ["AWS_SECRET_ACCESS_KEY"]
+requirements:
+ - boto3
+extends_documentation_fragment:
+ - community.digitalocean.digital_ocean.documentation
+"""
+
+
+EXAMPLES = r"""
+- name: Create a Space in nyc3
+ community.digitalocean.digital_ocean_spaces:
+ state: present
+ name: my-space
+ region: nyc3
+
+- name: Delete a Space in nyc3
+ community.digitalocean.digital_ocean_spaces:
+ state: absent
+ name: my-space
+ region: nyc3
+"""
+
+
+RETURN = r"""
+data:
+ description: DigitalOcean Space
+ returned: present
+ type: dict
+ sample:
+ space:
+ endpoint_url: https://nyc3.digitaloceanspaces.com
+ name: gh-ci-space-1
+ region: nyc3
+ space_url: https://gh-ci-space-1.nyc3.digitaloceanspaces.com
+msg:
+ description: Informational message
+ returned: always
+ type: str
+ sample: Created Space gh-ci-space-1 in nyc3
+"""
+
+from ansible.module_utils.basic import (
+ AnsibleModule,
+ missing_required_lib,
+ env_fallback,
+ to_native,
+)
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+from traceback import format_exc
+
+try:
+ import boto3
+
+ HAS_BOTO3 = True
+except Exception:
+ HAS_BOTO3 = False
+
+
+def run(module):
+ state = module.params.get("state")
+ name = module.params.get("name")
+ region = module.params.get("region")
+ aws_access_key_id = module.params.get("aws_access_key_id")
+ aws_secret_access_key = module.params.get("aws_secret_access_key")
+
+ try:
+ session = boto3.session.Session()
+ client = session.client(
+ "s3",
+ region_name=region,
+ endpoint_url=f"https://{region}.digitaloceanspaces.com",
+ aws_access_key_id=aws_access_key_id,
+ aws_secret_access_key=aws_secret_access_key,
+ )
+ response = client.list_buckets()
+ except Exception as e:
+ module.fail_json(msg=to_native(e), exception=format_exc())
+
+ response_metadata = response.get("ResponseMetadata")
+ http_status_code = response_metadata.get("HTTPStatusCode")
+
+ if http_status_code == 200:
+ spaces = [
+ {
+ "name": space["Name"],
+ "region": region,
+ "endpoint_url": f"https://{region}.digitaloceanspaces.com",
+ "space_url": f"https://{space['Name']}.{region}.digitaloceanspaces.com",
+ }
+ for space in response["Buckets"]
+ ]
+ else:
+ module.fail_json(changed=False, msg=f"Failed to list Spaces in {region}")
+
+ if state == "present":
+ for space in spaces:
+ if space["name"] == name:
+ module.exit_json(changed=False, data={"space": space})
+
+ if module.check_mode:
+ module.exit_json(changed=True, msg=f"Would create Space {name} in {region}")
+
+ try:
+ response = client.create_bucket(Bucket=name)
+ except Exception as e:
+ module.fail_json(msg=to_native(e), exception=format_exc())
+
+ response_metadata = response.get("ResponseMetadata")
+ http_status_code = response_metadata.get("HTTPStatusCode")
+ if http_status_code == 200:
+ module.exit_json(
+ changed=True,
+ msg=f"Created Space {name} in {region}",
+ data={
+ "space": {
+ "name": name,
+ "region": region,
+ "endpoint_url": f"https://{region}.digitaloceanspaces.com",
+ "space_url": f"https://{name}.{region}.digitaloceanspaces.com",
+ }
+ },
+ )
+
+ module.fail_json(
+ changed=False, msg=f"Failed to create Space {name} in {region}"
+ )
+
+ elif state == "absent":
+ have_it = False
+ for space in spaces:
+ if space["name"] == name:
+ have_it = True
+
+ if module.check_mode:
+ if have_it:
+ module.exit_json(
+ changed=True, msg=f"Would delete Space {name} in {region}"
+ )
+ else:
+ module.exit_json(changed=False, msg=f"No Space {name} in {region}")
+
+ if have_it:
+ try:
+ reponse = client.delete_bucket(Bucket=name)
+ except Exception as e:
+ module.fail_json(msg=to_native(e), exception=format_exc())
+
+ response_metadata = response.get("ResponseMetadata")
+ http_status_code = response_metadata.get("HTTPStatusCode")
+ if http_status_code == 200:
+ module.exit_json(changed=True, msg=f"Deleted Space {name} in {region}")
+
+ module.fail_json(
+ changed=True, msg=f"Failed to delete Space {name} in {region}"
+ )
+
+ module.exit_json(changed=False, msg=f"No Space {name} in {region}")
+
+
+def main():
+
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ argument_spec.update(
+ state=dict(type="str", choices=["present", "absent"], default="present"),
+ name=dict(type="str", required=True),
+ region=dict(type="str", aliases=["region_id"], required=True),
+ aws_access_key_id=dict(
+ type="str",
+ aliases=["AWS_ACCESS_KEY_ID"],
+ fallback=(env_fallback, ["AWS_ACCESS_KEY_ID"]),
+ required=True,
+ no_log=True,
+ ),
+ aws_secret_access_key=dict(
+ type="str",
+ aliases=["AWS_SECRET_ACCESS_KEY"],
+ fallback=(env_fallback, ["AWS_SECRET_ACCESS_KEY"]),
+ required=True,
+ no_log=True,
+ ),
+ )
+ module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
+
+ if not HAS_BOTO3:
+ module.fail_json(msg=missing_required_lib("boto3"))
+
+ run(module)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_spaces_info.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_spaces_info.py
new file mode 100644
index 00000000..50f05002
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_spaces_info.py
@@ -0,0 +1,160 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: Ansible Project
+# 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: digital_ocean_spaces_info
+short_description: List DigitalOcean Spaces.
+description:
+ - List DigitalOcean Spaces.
+author: Mark Mercado (@mamercad)
+version_added: 1.15.0
+options:
+ state:
+ description:
+ - Only present is supported.
+ default: present
+ type: str
+ choices: ["present"]
+ region:
+ description:
+ - The region from which to list Spaces.
+ aliases: ["region_id"]
+ required: true
+ type: str
+ aws_access_key_id:
+ description:
+ - The AWS_ACCESS_KEY_ID to use.
+ required: true
+ type: str
+ aliases: ["AWS_ACCESS_KEY_ID"]
+ aws_secret_access_key:
+ description:
+ - The AWS_SECRET_ACCESS_KEY to use.
+ required: true
+ type: str
+ aliases: ["AWS_SECRET_ACCESS_KEY"]
+requirements:
+ - boto3
+extends_documentation_fragment:
+ - community.digitalocean.digital_ocean.documentation
+"""
+
+
+EXAMPLES = r"""
+- name: List all Spaces in nyc3
+ community.digitalocean.digital_ocean_spaces_info:
+ state: present
+ region: nyc3
+"""
+
+
+RETURN = r"""
+data:
+ description: List of DigitalOcean Spaces
+ returned: always
+ type: dict
+ sample:
+ spaces:
+ - endpoint_url: https://nyc3.digitaloceanspaces.com
+ name: gh-ci-space
+ region: nyc3
+ space_url: https://gh-ci-space.nyc3.digitaloceanspaces.com
+"""
+
+from ansible.module_utils.basic import (
+ AnsibleModule,
+ missing_required_lib,
+ env_fallback,
+ to_native,
+)
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+from traceback import format_exc
+
+try:
+ import boto3
+
+ HAS_BOTO3 = True
+except Exception:
+ HAS_BOTO3 = False
+
+
+def run(module):
+ state = module.params.get("state")
+ region = module.params.get("region")
+ aws_access_key_id = module.params.get("aws_access_key_id")
+ aws_secret_access_key = module.params.get("aws_secret_access_key")
+
+ if state == "present":
+ try:
+ session = boto3.session.Session()
+ client = session.client(
+ "s3",
+ region_name=region,
+ endpoint_url=f"https://{region}.digitaloceanspaces.com",
+ aws_access_key_id=aws_access_key_id,
+ aws_secret_access_key=aws_secret_access_key,
+ )
+ response = client.list_buckets()
+ except Exception as e:
+ module.fail_json(msg=to_native(e), exception=format_exc())
+
+ response_metadata = response.get("ResponseMetadata")
+ http_status_code = response_metadata.get("HTTPStatusCode")
+
+ if http_status_code == 200:
+ spaces = [
+ {
+ "name": space["Name"],
+ "region": region,
+ "endpoint_url": f"https://{region}.digitaloceanspaces.com",
+ "space_url": f"https://{space['Name']}.{region}.digitaloceanspaces.com",
+ }
+ for space in response["Buckets"]
+ ]
+ module.exit_json(changed=False, data={"spaces": spaces})
+
+ module.fail_json(changed=False, msg=f"Failed to list Spaces in {region}")
+
+
+def main():
+
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ argument_spec.update(
+ state=dict(type="str", choices=["present"], default="present"),
+ region=dict(type="str", aliases=["region_id"], required=True),
+ aws_access_key_id=dict(
+ type="str",
+ aliases=["AWS_ACCESS_KEY_ID"],
+ fallback=(env_fallback, ["AWS_ACCESS_KEY_ID"]),
+ required=True,
+ no_log=True,
+ ),
+ aws_secret_access_key=dict(
+ type="str",
+ aliases=["AWS_SECRET_ACCESS_KEY"],
+ fallback=(env_fallback, ["AWS_SECRET_ACCESS_KEY"]),
+ required=True,
+ no_log=True,
+ ),
+ )
+ module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
+
+ if not HAS_BOTO3:
+ module.fail_json(msg=missing_required_lib("boto3"))
+
+ run(module)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_sshkey.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_sshkey.py
new file mode 100644
index 00000000..3a7e662b
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_sshkey.py
@@ -0,0 +1,282 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: Ansible Project
+# 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: digital_ocean_sshkey
+short_description: Manage DigitalOcean SSH keys
+description:
+ - Create/delete DigitalOcean SSH keys.
+author: "Patrick Marques (@pmarques)"
+options:
+ state:
+ description:
+ - Indicate desired state of the target.
+ default: present
+ choices: ['present', 'absent']
+ type: str
+ fingerprint:
+ description:
+ - This is a unique identifier for the SSH key used to delete a key
+ aliases: ['id']
+ type: str
+ name:
+ description:
+ - The name for the SSH key
+ type: str
+ ssh_pub_key:
+ description:
+ - The Public SSH key to add.
+ type: str
+notes:
+ - Version 2 of DigitalOcean API is used.
+requirements:
+ - "python >= 2.6"
+extends_documentation_fragment:
+ - community.digitalocean.digital_ocean.documentation
+"""
+
+
+EXAMPLES = r"""
+- name: "Create ssh key"
+ community.digitalocean.digital_ocean_sshkey:
+ oauth_token: "{{ oauth_token }}"
+ name: "My SSH Public Key"
+ ssh_pub_key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDDHr/jh2Jy4yALcK4JyWbVkPRaWmhck3IgCoeOO3z1e2dBowLh64QAM+Qb72pxekALga2oi4GvT+TlWNhzPH4V example"
+ state: present
+ register: result
+
+- name: "Delete ssh key"
+ community.digitalocean.digital_ocean_sshkey:
+ oauth_token: "{{ oauth_token }}"
+ state: "absent"
+ fingerprint: "3b:16:bf:e4:8b:00:8b:b8:59:8c:a9:d3:f0:19:45:fa"
+"""
+
+
+RETURN = r"""
+# Digital Ocean API info https://docs.digitalocean.com/reference/api/api-reference/#tag/SSH-Keys
+data:
+ description: This is only present when C(state=present)
+ returned: when C(state=present)
+ type: dict
+ sample: {
+ "ssh_key": {
+ "id": 512189,
+ "fingerprint": "3b:16:bf:e4:8b:00:8b:b8:59:8c:a9:d3:f0:19:45:fa",
+ "name": "My SSH Public Key",
+ "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDDHr/jh2Jy4yALcK4JyWbVkPRaWmhck3IgCoeOO3z1e2dBowLh64QAM+Qb72pxekALga2oi4GvT+TlWNhzPH4V example"
+ }
+ }
+"""
+
+import json
+import hashlib
+import base64
+
+from ansible.module_utils.basic import env_fallback
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.urls import fetch_url
+
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+
+
+class Response(object):
+ def __init__(self, resp, info):
+ self.body = None
+ if resp:
+ self.body = resp.read()
+ self.info = info
+
+ @property
+ def json(self):
+ if not self.body:
+ if "body" in self.info:
+ return json.loads(self.info["body"])
+ return None
+ try:
+ return json.loads(self.body)
+ except ValueError:
+ return None
+
+ @property
+ def status_code(self):
+ return self.info["status"]
+
+
+class Rest(object):
+ def __init__(self, module, headers):
+ self.module = module
+ self.headers = headers
+ self.baseurl = module.params.get("baseurl")
+
+ def _url_builder(self, path):
+ if path[0] == "/":
+ path = path[1:]
+ return "%s/%s" % (self.baseurl, path)
+
+ def send(self, method, path, data=None, headers=None):
+ url = self._url_builder(path)
+ data = self.module.jsonify(data)
+ timeout = self.module.params["timeout"]
+
+ resp, info = fetch_url(
+ self.module,
+ url,
+ data=data,
+ headers=self.headers,
+ method=method,
+ timeout=timeout,
+ )
+
+ # Exceptions in fetch_url may result in a status -1, the ensures a
+ if info["status"] == -1:
+ self.module.fail_json(msg=info["msg"])
+
+ return Response(resp, info)
+
+ def get(self, path, data=None, headers=None):
+ return self.send("GET", path, data, headers)
+
+ def put(self, path, data=None, headers=None):
+ return self.send("PUT", path, data, headers)
+
+ def post(self, path, data=None, headers=None):
+ return self.send("POST", path, data, headers)
+
+ def delete(self, path, data=None, headers=None):
+ return self.send("DELETE", path, data, headers)
+
+
+def core(module):
+ api_token = module.params["oauth_token"]
+ state = module.params["state"]
+ fingerprint = module.params["fingerprint"]
+ name = module.params["name"]
+ ssh_pub_key = module.params["ssh_pub_key"]
+
+ rest = Rest(
+ module,
+ {
+ "Authorization": "Bearer {0}".format(api_token),
+ "Content-type": "application/json",
+ },
+ )
+
+ fingerprint = fingerprint or ssh_key_fingerprint(module, ssh_pub_key)
+ response = rest.get("account/keys/{0}".format(fingerprint))
+ status_code = response.status_code
+ json = response.json
+
+ if status_code not in (200, 404):
+ module.fail_json(
+ msg="Error getting ssh key [{0}: {1}]".format(
+ status_code, response.json["message"]
+ ),
+ fingerprint=fingerprint,
+ )
+
+ if state in ("present"):
+ if status_code == 404:
+ # IF key not found create it!
+
+ if module.check_mode:
+ module.exit_json(changed=True)
+
+ payload = {"name": name, "public_key": ssh_pub_key}
+ response = rest.post("account/keys", data=payload)
+ status_code = response.status_code
+ json = response.json
+ if status_code == 201:
+ module.exit_json(changed=True, data=json)
+
+ module.fail_json(
+ msg="Error creating ssh key [{0}: {1}]".format(
+ status_code, response.json["message"]
+ )
+ )
+
+ elif status_code == 200:
+ # If key found was found, check if name needs to be updated
+ if name is None or json["ssh_key"]["name"] == name:
+ module.exit_json(changed=False, data=json)
+
+ if module.check_mode:
+ module.exit_json(changed=True)
+
+ payload = {
+ "name": name,
+ }
+ response = rest.put("account/keys/{0}".format(fingerprint), data=payload)
+ status_code = response.status_code
+ json = response.json
+ if status_code == 200:
+ module.exit_json(changed=True, data=json)
+
+ module.fail_json(
+ msg="Error updating ssh key name [{0}: {1}]".format(
+ status_code, response.json["message"]
+ ),
+ fingerprint=fingerprint,
+ )
+
+ elif state in ("absent"):
+ if status_code == 404:
+ module.exit_json(changed=False)
+
+ if module.check_mode:
+ module.exit_json(changed=True)
+
+ response = rest.delete("account/keys/{0}".format(fingerprint))
+ status_code = response.status_code
+ json = response.json
+ if status_code == 204:
+ module.exit_json(changed=True)
+
+ module.fail_json(
+ msg="Error creating ssh key [{0}: {1}]".format(
+ status_code, response.json["message"]
+ )
+ )
+
+
+def ssh_key_fingerprint(module, ssh_pub_key):
+ try:
+ key = ssh_pub_key.split(None, 2)[1]
+ fingerprint = hashlib.md5(base64.b64decode(key)).hexdigest()
+ return ":".join(a + b for a, b in zip(fingerprint[::2], fingerprint[1::2]))
+ except IndexError:
+ module.fail_json(
+ msg="This does not appear to be a valid public key. Please verify the format and value provided in ssh_public_key."
+ )
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ argument_spec.update(
+ state=dict(choices=["present", "absent"], default="present"),
+ fingerprint=dict(aliases=["id"], required=False),
+ name=dict(required=False),
+ ssh_pub_key=dict(required=False),
+ )
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ required_one_of=(("fingerprint", "ssh_pub_key"),),
+ supports_check_mode=True,
+ )
+
+ core(module)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_sshkey_facts.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_sshkey_facts.py
new file mode 100644
index 00000000..cfc61310
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_sshkey_facts.py
@@ -0,0 +1,107 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright: Ansible Project
+# 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: digital_ocean_sshkey_info
+short_description: Gather information about DigitalOcean SSH keys
+description:
+ - This module can be used to gather information about DigitalOcean SSH keys.
+ - This module replaces the C(digital_ocean_sshkey_facts) module.
+author: "Patrick Marques (@pmarques)"
+extends_documentation_fragment:
+- community.digitalocean.digital_ocean.documentation
+
+notes:
+ - Version 2 of DigitalOcean API is used.
+requirements:
+ - "python >= 2.6"
+"""
+
+
+EXAMPLES = r"""
+- name: Gather information about DigitalOcean SSH keys
+ community.digitalocean.digital_ocean_sshkey_info:
+ oauth_token: "{{ my_do_key }}"
+ register: ssh_keys
+
+- name: Set facts based on the gathered information
+ set_fact:
+ pubkey: "{{ item.public_key }}"
+ loop: "{{ ssh_keys.data | community.general.json_query(ssh_pubkey) }}"
+ vars:
+ ssh_pubkey: "[?name=='ansible_ctrl']"
+
+- name: Print SSH public key
+ debug:
+ msg: "{{ pubkey }}"
+"""
+
+
+RETURN = r"""
+# Digital Ocean API info https://docs.digitalocean.com/reference/api/api-reference/#operation/list_all_keys
+data:
+ description: List of SSH keys on DigitalOcean
+ returned: success and no resource constraint
+ type: list
+ elements: dict
+ sample: [
+ {
+ "id": 512189,
+ "fingerprint": "3b:16:bf:e4:8b:00:8b:b8:59:8c:a9:d3:f0:19:45:fa",
+ "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDDHr/jh2Jy4yALcK4JyWbVkPRaWmhck3IgCoeOO3z1e2dBowLh64QAM+Qb72pxekALga2oi4GvT+TlWNhzPH4V example",
+ "name": "My SSH Public Key"
+ }
+ ]
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+
+
+def core(module):
+ rest = DigitalOceanHelper(module)
+
+ response = rest.get("account/keys")
+ status_code = response.status_code
+ json = response.json
+ if status_code == 200:
+ module.exit_json(changed=False, data=json["ssh_keys"])
+ else:
+ module.fail_json(
+ msg="Error fetching SSH Key information [{0}: {1}]".format(
+ status_code, response.json["message"]
+ )
+ )
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=DigitalOceanHelper.digital_ocean_argument_spec(),
+ supports_check_mode=True,
+ )
+ if module._name in (
+ "digital_ocean_sshkey_facts",
+ "community.digitalocean.digital_ocean_sshkey_facts",
+ ):
+ module.deprecate(
+ "The 'digital_ocean_sshkey_facts' module has been renamed to 'digital_ocean_sshkey_info'",
+ version="2.0.0",
+ collection_name="community.digitalocean",
+ ) # was Ansible 2.13
+ core(module)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_sshkey_info.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_sshkey_info.py
new file mode 100644
index 00000000..cfc61310
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_sshkey_info.py
@@ -0,0 +1,107 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright: Ansible Project
+# 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: digital_ocean_sshkey_info
+short_description: Gather information about DigitalOcean SSH keys
+description:
+ - This module can be used to gather information about DigitalOcean SSH keys.
+ - This module replaces the C(digital_ocean_sshkey_facts) module.
+author: "Patrick Marques (@pmarques)"
+extends_documentation_fragment:
+- community.digitalocean.digital_ocean.documentation
+
+notes:
+ - Version 2 of DigitalOcean API is used.
+requirements:
+ - "python >= 2.6"
+"""
+
+
+EXAMPLES = r"""
+- name: Gather information about DigitalOcean SSH keys
+ community.digitalocean.digital_ocean_sshkey_info:
+ oauth_token: "{{ my_do_key }}"
+ register: ssh_keys
+
+- name: Set facts based on the gathered information
+ set_fact:
+ pubkey: "{{ item.public_key }}"
+ loop: "{{ ssh_keys.data | community.general.json_query(ssh_pubkey) }}"
+ vars:
+ ssh_pubkey: "[?name=='ansible_ctrl']"
+
+- name: Print SSH public key
+ debug:
+ msg: "{{ pubkey }}"
+"""
+
+
+RETURN = r"""
+# Digital Ocean API info https://docs.digitalocean.com/reference/api/api-reference/#operation/list_all_keys
+data:
+ description: List of SSH keys on DigitalOcean
+ returned: success and no resource constraint
+ type: list
+ elements: dict
+ sample: [
+ {
+ "id": 512189,
+ "fingerprint": "3b:16:bf:e4:8b:00:8b:b8:59:8c:a9:d3:f0:19:45:fa",
+ "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDDHr/jh2Jy4yALcK4JyWbVkPRaWmhck3IgCoeOO3z1e2dBowLh64QAM+Qb72pxekALga2oi4GvT+TlWNhzPH4V example",
+ "name": "My SSH Public Key"
+ }
+ ]
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+
+
+def core(module):
+ rest = DigitalOceanHelper(module)
+
+ response = rest.get("account/keys")
+ status_code = response.status_code
+ json = response.json
+ if status_code == 200:
+ module.exit_json(changed=False, data=json["ssh_keys"])
+ else:
+ module.fail_json(
+ msg="Error fetching SSH Key information [{0}: {1}]".format(
+ status_code, response.json["message"]
+ )
+ )
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=DigitalOceanHelper.digital_ocean_argument_spec(),
+ supports_check_mode=True,
+ )
+ if module._name in (
+ "digital_ocean_sshkey_facts",
+ "community.digitalocean.digital_ocean_sshkey_facts",
+ ):
+ module.deprecate(
+ "The 'digital_ocean_sshkey_facts' module has been renamed to 'digital_ocean_sshkey_info'",
+ version="2.0.0",
+ collection_name="community.digitalocean",
+ ) # was Ansible 2.13
+ core(module)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_tag.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_tag.py
new file mode 100644
index 00000000..083129a9
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_tag.py
@@ -0,0 +1,219 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: Ansible Project
+# 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: digital_ocean_tag
+short_description: Create and remove tag(s) to DigitalOcean resource.
+description:
+ - Create and remove tag(s) to DigitalOcean resource.
+author: "Victor Volle (@kontrafiktion)"
+options:
+ name:
+ description:
+ - The name of the tag. The supported characters for names include
+ alphanumeric characters, dashes, and underscores.
+ required: true
+ type: str
+ resource_id:
+ description:
+ - The ID of the resource to operate on.
+ - The data type of resource_id is changed from integer to string since Ansible 2.5.
+ aliases: ['droplet_id']
+ type: str
+ resource_type:
+ description:
+ - The type of resource to operate on. Currently, only tagging of
+ droplets is supported.
+ default: droplet
+ choices: ['droplet']
+ type: str
+ state:
+ description:
+ - Whether the tag should be present or absent on the resource.
+ default: present
+ type: str
+ choices: ['present', 'absent']
+extends_documentation_fragment:
+- community.digitalocean.digital_ocean.documentation
+
+notes:
+ - Two environment variables can be used, DO_API_KEY and DO_API_TOKEN.
+ They both refer to the v2 token.
+ - As of Ansible 2.0, Version 2 of the DigitalOcean API is used.
+
+requirements:
+ - "python >= 2.6"
+"""
+
+
+EXAMPLES = r"""
+- name: Create a tag
+ community.digitalocean.digital_ocean_tag:
+ name: production
+ state: present
+
+- name: Tag a resource; creating the tag if it does not exist
+ community.digitalocean.digital_ocean_tag:
+ name: "{{ item }}"
+ resource_id: "73333005"
+ state: present
+ loop:
+ - staging
+ - dbserver
+
+- name: Untag a resource
+ community.digitalocean.digital_ocean_tag:
+ name: staging
+ resource_id: "73333005"
+ state: absent
+
+# Deleting a tag also untags all the resources that have previously been
+# tagged with it
+- name: Remove a tag
+ community.digitalocean.digital_ocean_tag:
+ name: dbserver
+ state: absent
+"""
+
+
+RETURN = r"""
+data:
+ description: a DigitalOcean Tag resource
+ returned: success and no resource constraint
+ type: dict
+ sample: {
+ "tag": {
+ "name": "awesome",
+ "resources": {
+ "droplets": {
+ "count": 0,
+ "last_tagged": null
+ }
+ }
+ }
+ }
+"""
+
+from traceback import format_exc
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+from ansible.module_utils._text import to_native
+
+
+def core(module):
+ state = module.params["state"]
+ name = module.params["name"]
+ resource_id = module.params["resource_id"]
+ resource_type = module.params["resource_type"]
+
+ rest = DigitalOceanHelper(module)
+ response = rest.get("tags/{0}".format(name))
+
+ if state == "present":
+ status_code = response.status_code
+ resp_json = response.json
+ changed = False
+ if status_code == 200 and resp_json["tag"]["name"] == name:
+ changed = False
+ else:
+ # Ensure Tag exists
+ response = rest.post("tags", data={"name": name})
+ status_code = response.status_code
+ resp_json = response.json
+ if status_code == 201:
+ changed = True
+ elif status_code == 422:
+ changed = False
+ else:
+ module.exit_json(changed=False, data=resp_json)
+
+ if resource_id is None:
+ # No resource defined, we're done.
+ module.exit_json(changed=changed, data=resp_json)
+ else:
+ # Check if resource is already tagged or not
+ found = False
+ url = "{0}?tag_name={1}".format(resource_type, name)
+ if resource_type == "droplet":
+ url = "droplets?tag_name={0}".format(name)
+ response = rest.get(url)
+ status_code = response.status_code
+ resp_json = response.json
+ if status_code == 200:
+ for resource in resp_json["droplets"]:
+ if not found and resource["id"] == int(resource_id):
+ found = True
+ break
+ if not found:
+ # If resource is not tagged, tag a resource
+ url = "tags/{0}/resources".format(name)
+ payload = {
+ "resources": [
+ {"resource_id": resource_id, "resource_type": resource_type}
+ ]
+ }
+ response = rest.post(url, data=payload)
+ if response.status_code == 204:
+ module.exit_json(changed=True)
+ else:
+ module.fail_json(
+ msg="error tagging resource '{0}': {1}".format(
+ resource_id, response.json["message"]
+ )
+ )
+ else:
+ # Already tagged resource
+ module.exit_json(changed=False)
+ else:
+ # Unable to find resource specified by user
+ module.fail_json(msg=resp_json["message"])
+
+ elif state == "absent":
+ if response.status_code == 200:
+ if resource_id:
+ url = "tags/{0}/resources".format(name)
+ payload = {
+ "resources": [
+ {"resource_id": resource_id, "resource_type": resource_type}
+ ]
+ }
+ response = rest.delete(url, data=payload)
+ else:
+ url = "tags/{0}".format(name)
+ response = rest.delete(url)
+ if response.status_code == 204:
+ module.exit_json(changed=True)
+ else:
+ module.exit_json(changed=False)
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ argument_spec.update(
+ name=dict(type="str", required=True),
+ resource_id=dict(aliases=["droplet_id"], type="str"),
+ resource_type=dict(choices=["droplet"], default="droplet"),
+ state=dict(choices=["present", "absent"], default="present"),
+ )
+
+ module = AnsibleModule(argument_spec=argument_spec)
+
+ try:
+ core(module)
+ except Exception as e:
+ module.fail_json(msg=to_native(e), exception=format_exc())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_tag_facts.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_tag_facts.py
new file mode 100644
index 00000000..43ccaa9c
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_tag_facts.py
@@ -0,0 +1,126 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2018, Ansible Project
+# Copyright: (c) 2018, Abhijeet Kasurde <akasurde@redhat.com>
+# 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: digital_ocean_tag_info
+short_description: Gather information about DigitalOcean tags
+description:
+ - This module can be used to gather information about DigitalOcean provided tags.
+ - This module was called C(digital_ocean_tag_facts) before Ansible 2.9. The usage did not change.
+author: "Abhijeet Kasurde (@Akasurde)"
+options:
+ tag_name:
+ description:
+ - Tag name that can be used to identify and reference a tag.
+ required: false
+ type: str
+requirements:
+ - "python >= 2.6"
+extends_documentation_fragment:
+- community.digitalocean.digital_ocean.documentation
+
+"""
+
+
+EXAMPLES = r"""
+- name: Gather information about all tags
+ community.digitalocean.digital_ocean_tag_info:
+ oauth_token: "{{ oauth_token }}"
+
+- name: Gather information about tag with given name
+ community.digitalocean.digital_ocean_tag_info:
+ oauth_token: "{{ oauth_token }}"
+ tag_name: "extra_awesome_tag"
+
+- name: Get resources from tag name
+ community.digitalocean.digital_ocean_tag_info:
+ register: resp_out
+- set_fact:
+ resources: "{{ item.resources }}"
+ loop: "{{ resp_out.data | community.general.json_query(name) }}"
+ vars:
+ name: "[?name=='extra_awesome_tag']"
+- debug:
+ var: resources
+"""
+
+
+RETURN = r"""
+data:
+ description: DigitalOcean tag information
+ returned: success
+ type: list
+ elements: dict
+ sample: [
+ {
+ "name": "extra-awesome",
+ "resources": {
+ "droplets": {
+ "count": 1,
+ ...
+ }
+ }
+ },
+ ]
+"""
+
+from traceback import format_exc
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+from ansible.module_utils._text import to_native
+
+
+def core(module):
+ tag_name = module.params.get("tag_name", None)
+ rest = DigitalOceanHelper(module)
+
+ base_url = "tags"
+ if tag_name is not None:
+ response = rest.get("%s/%s" % (base_url, tag_name))
+ status_code = response.status_code
+
+ if status_code != 200:
+ module.fail_json(msg="Failed to retrieve tags for DigitalOcean")
+
+ tag = [response.json["tag"]]
+ else:
+ tag = rest.get_paginated_data(base_url=base_url + "?", data_key_name="tags")
+
+ module.exit_json(changed=False, data=tag)
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ argument_spec.update(
+ tag_name=dict(type="str", required=False),
+ )
+ module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
+ if module._name in (
+ "digital_ocean_tag_facts",
+ "community.digitalocean.digital_ocean_tag_facts",
+ ):
+ module.deprecate(
+ "The 'digital_ocean_tag_facts' module has been renamed to 'digital_ocean_tag_info'",
+ version="2.0.0",
+ collection_name="community.digitalocean",
+ ) # was Ansible 2.13
+
+ try:
+ core(module)
+ except Exception as e:
+ module.fail_json(msg=to_native(e), exception=format_exc())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_tag_info.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_tag_info.py
new file mode 100644
index 00000000..43ccaa9c
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_tag_info.py
@@ -0,0 +1,126 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2018, Ansible Project
+# Copyright: (c) 2018, Abhijeet Kasurde <akasurde@redhat.com>
+# 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: digital_ocean_tag_info
+short_description: Gather information about DigitalOcean tags
+description:
+ - This module can be used to gather information about DigitalOcean provided tags.
+ - This module was called C(digital_ocean_tag_facts) before Ansible 2.9. The usage did not change.
+author: "Abhijeet Kasurde (@Akasurde)"
+options:
+ tag_name:
+ description:
+ - Tag name that can be used to identify and reference a tag.
+ required: false
+ type: str
+requirements:
+ - "python >= 2.6"
+extends_documentation_fragment:
+- community.digitalocean.digital_ocean.documentation
+
+"""
+
+
+EXAMPLES = r"""
+- name: Gather information about all tags
+ community.digitalocean.digital_ocean_tag_info:
+ oauth_token: "{{ oauth_token }}"
+
+- name: Gather information about tag with given name
+ community.digitalocean.digital_ocean_tag_info:
+ oauth_token: "{{ oauth_token }}"
+ tag_name: "extra_awesome_tag"
+
+- name: Get resources from tag name
+ community.digitalocean.digital_ocean_tag_info:
+ register: resp_out
+- set_fact:
+ resources: "{{ item.resources }}"
+ loop: "{{ resp_out.data | community.general.json_query(name) }}"
+ vars:
+ name: "[?name=='extra_awesome_tag']"
+- debug:
+ var: resources
+"""
+
+
+RETURN = r"""
+data:
+ description: DigitalOcean tag information
+ returned: success
+ type: list
+ elements: dict
+ sample: [
+ {
+ "name": "extra-awesome",
+ "resources": {
+ "droplets": {
+ "count": 1,
+ ...
+ }
+ }
+ },
+ ]
+"""
+
+from traceback import format_exc
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+from ansible.module_utils._text import to_native
+
+
+def core(module):
+ tag_name = module.params.get("tag_name", None)
+ rest = DigitalOceanHelper(module)
+
+ base_url = "tags"
+ if tag_name is not None:
+ response = rest.get("%s/%s" % (base_url, tag_name))
+ status_code = response.status_code
+
+ if status_code != 200:
+ module.fail_json(msg="Failed to retrieve tags for DigitalOcean")
+
+ tag = [response.json["tag"]]
+ else:
+ tag = rest.get_paginated_data(base_url=base_url + "?", data_key_name="tags")
+
+ module.exit_json(changed=False, data=tag)
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ argument_spec.update(
+ tag_name=dict(type="str", required=False),
+ )
+ module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
+ if module._name in (
+ "digital_ocean_tag_facts",
+ "community.digitalocean.digital_ocean_tag_facts",
+ ):
+ module.deprecate(
+ "The 'digital_ocean_tag_facts' module has been renamed to 'digital_ocean_tag_info'",
+ version="2.0.0",
+ collection_name="community.digitalocean",
+ ) # was Ansible 2.13
+
+ try:
+ core(module)
+ except Exception as e:
+ module.fail_json(msg=to_native(e), exception=format_exc())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_volume_facts.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_volume_facts.py
new file mode 100644
index 00000000..4e2cc179
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_volume_facts.py
@@ -0,0 +1,149 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2018, Ansible Project
+# Copyright: (c) 2018, Abhijeet Kasurde <akasurde@redhat.com>
+# 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: digital_ocean_volume_info
+short_description: Gather information about DigitalOcean volumes
+description:
+ - This module can be used to gather information about DigitalOcean provided volumes.
+ - This module was called C(digital_ocean_volume_facts) before Ansible 2.9. The usage did not change.
+author: "Abhijeet Kasurde (@Akasurde)"
+options:
+ region_name:
+ description:
+ - Name of region to restrict results to volumes available in a specific region.
+ - Please use M(community.digitalocean.digital_ocean_region_info) for getting valid values related regions.
+ required: false
+ type: str
+requirements:
+ - "python >= 2.6"
+
+extends_documentation_fragment:
+- community.digitalocean.digital_ocean.documentation
+
+"""
+
+
+EXAMPLES = r"""
+- name: Gather information about all volume
+ community.digitalocean.digital_ocean_volume_info:
+ oauth_token: "{{ oauth_token }}"
+
+- name: Gather information about volume in given region
+ community.digitalocean.digital_ocean_volume_info:
+ region_name: nyc1
+ oauth_token: "{{ oauth_token }}"
+
+- name: Get information about volume named nyc3-test-volume
+ community.digitalocean.digital_ocean_volume_info:
+ register: resp_out
+- set_fact:
+ volume_id: "{{ item.id }}"
+ loop: "{{ resp_out.data | community.general.json_query(name) }}"
+ vars:
+ name: "[?name=='nyc3-test-volume']"
+- debug: var=volume_id
+"""
+
+
+RETURN = r"""
+data:
+ description: DigitalOcean volume information
+ returned: success
+ type: list
+ sample: [
+ {
+ "id": "506f78a4-e098-11e5-ad9f-000f53306ae1",
+ "region": {
+ "name": "New York 1",
+ "slug": "nyc1",
+ "sizes": [
+ "s-1vcpu-1gb",
+ "s-1vcpu-2gb",
+ "s-1vcpu-3gb",
+ "s-2vcpu-2gb",
+ "s-3vcpu-1gb",
+ "s-2vcpu-4gb",
+ "s-4vcpu-8gb",
+ "s-6vcpu-16gb",
+ "s-8vcpu-32gb",
+ "s-12vcpu-48gb",
+ "s-16vcpu-64gb",
+ "s-20vcpu-96gb",
+ "s-24vcpu-128gb",
+ "s-32vcpu-192gb"
+ ],
+ "features": [
+ "private_networking",
+ "backups",
+ "ipv6",
+ "metadata"
+ ],
+ "available": true
+ },
+ "droplet_ids": [
+
+ ],
+ "name": "example",
+ "description": "Block store for examples",
+ "size_gigabytes": 10,
+ "created_at": "2016-03-02T17:00:49Z"
+ }
+ ]
+"""
+
+from traceback import format_exc
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+from ansible.module_utils._text import to_native
+
+
+def core(module):
+ region_name = module.params.get("region_name", None)
+
+ rest = DigitalOceanHelper(module)
+
+ base_url = "volumes?"
+ if region_name is not None:
+ base_url += "region=%s&" % region_name
+
+ volumes = rest.get_paginated_data(base_url=base_url, data_key_name="volumes")
+
+ module.exit_json(changed=False, data=volumes)
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ argument_spec.update(
+ region_name=dict(type="str", required=False),
+ )
+ module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
+ if module._name in (
+ "digital_ocean_volume_facts",
+ "community.digitalocean.digital_ocean_volume_facts",
+ ):
+ module.deprecate(
+ "The 'digital_ocean_volume_facts' module has been renamed to 'digital_ocean_volume_info'",
+ version="2.0.0",
+ collection_name="community.digitalocean",
+ ) # was Ansible 2.13
+
+ try:
+ core(module)
+ except Exception as e:
+ module.fail_json(msg=to_native(e), exception=format_exc())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_volume_info.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_volume_info.py
new file mode 100644
index 00000000..4e2cc179
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_volume_info.py
@@ -0,0 +1,149 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2018, Ansible Project
+# Copyright: (c) 2018, Abhijeet Kasurde <akasurde@redhat.com>
+# 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: digital_ocean_volume_info
+short_description: Gather information about DigitalOcean volumes
+description:
+ - This module can be used to gather information about DigitalOcean provided volumes.
+ - This module was called C(digital_ocean_volume_facts) before Ansible 2.9. The usage did not change.
+author: "Abhijeet Kasurde (@Akasurde)"
+options:
+ region_name:
+ description:
+ - Name of region to restrict results to volumes available in a specific region.
+ - Please use M(community.digitalocean.digital_ocean_region_info) for getting valid values related regions.
+ required: false
+ type: str
+requirements:
+ - "python >= 2.6"
+
+extends_documentation_fragment:
+- community.digitalocean.digital_ocean.documentation
+
+"""
+
+
+EXAMPLES = r"""
+- name: Gather information about all volume
+ community.digitalocean.digital_ocean_volume_info:
+ oauth_token: "{{ oauth_token }}"
+
+- name: Gather information about volume in given region
+ community.digitalocean.digital_ocean_volume_info:
+ region_name: nyc1
+ oauth_token: "{{ oauth_token }}"
+
+- name: Get information about volume named nyc3-test-volume
+ community.digitalocean.digital_ocean_volume_info:
+ register: resp_out
+- set_fact:
+ volume_id: "{{ item.id }}"
+ loop: "{{ resp_out.data | community.general.json_query(name) }}"
+ vars:
+ name: "[?name=='nyc3-test-volume']"
+- debug: var=volume_id
+"""
+
+
+RETURN = r"""
+data:
+ description: DigitalOcean volume information
+ returned: success
+ type: list
+ sample: [
+ {
+ "id": "506f78a4-e098-11e5-ad9f-000f53306ae1",
+ "region": {
+ "name": "New York 1",
+ "slug": "nyc1",
+ "sizes": [
+ "s-1vcpu-1gb",
+ "s-1vcpu-2gb",
+ "s-1vcpu-3gb",
+ "s-2vcpu-2gb",
+ "s-3vcpu-1gb",
+ "s-2vcpu-4gb",
+ "s-4vcpu-8gb",
+ "s-6vcpu-16gb",
+ "s-8vcpu-32gb",
+ "s-12vcpu-48gb",
+ "s-16vcpu-64gb",
+ "s-20vcpu-96gb",
+ "s-24vcpu-128gb",
+ "s-32vcpu-192gb"
+ ],
+ "features": [
+ "private_networking",
+ "backups",
+ "ipv6",
+ "metadata"
+ ],
+ "available": true
+ },
+ "droplet_ids": [
+
+ ],
+ "name": "example",
+ "description": "Block store for examples",
+ "size_gigabytes": 10,
+ "created_at": "2016-03-02T17:00:49Z"
+ }
+ ]
+"""
+
+from traceback import format_exc
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+from ansible.module_utils._text import to_native
+
+
+def core(module):
+ region_name = module.params.get("region_name", None)
+
+ rest = DigitalOceanHelper(module)
+
+ base_url = "volumes?"
+ if region_name is not None:
+ base_url += "region=%s&" % region_name
+
+ volumes = rest.get_paginated_data(base_url=base_url, data_key_name="volumes")
+
+ module.exit_json(changed=False, data=volumes)
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ argument_spec.update(
+ region_name=dict(type="str", required=False),
+ )
+ module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
+ if module._name in (
+ "digital_ocean_volume_facts",
+ "community.digitalocean.digital_ocean_volume_facts",
+ ):
+ module.deprecate(
+ "The 'digital_ocean_volume_facts' module has been renamed to 'digital_ocean_volume_info'",
+ version="2.0.0",
+ collection_name="community.digitalocean",
+ ) # was Ansible 2.13
+
+ try:
+ core(module)
+ except Exception as e:
+ module.fail_json(msg=to_native(e), exception=format_exc())
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_vpc.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_vpc.py
new file mode 100644
index 00000000..598ec2bf
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_vpc.py
@@ -0,0 +1,284 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2018, Ansible Project
+# Copyright: (c) 2021, Mark Mercado <mamercad@gmail.com>
+# 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: digital_ocean_vpc
+short_description: Create and delete DigitalOcean VPCs
+version_added: 1.7.0
+description:
+ - This module can be used to create and delete DigitalOcean VPCs.
+author: "Mark Mercado (@mamercad)"
+options:
+ state:
+ description:
+ - Whether the VPC should be present (created) or absent (deleted).
+ default: present
+ choices:
+ - present
+ - absent
+ type: str
+ name:
+ description:
+ - The name of the VPC.
+ - Must be unique and contain alphanumeric characters, dashes, and periods only.
+ type: str
+ required: true
+ description:
+ description:
+ - A free-form text field for describing the VPC's purpose.
+ - It may be a maximum of 255 characters.
+ type: str
+ default:
+ description:
+ - A boolean value indicating whether or not the VPC is the default network for the region.
+ - All applicable resources are placed into the default VPC network unless otherwise specified during their creation.
+ - The C(default) field cannot be unset from C(true).
+ - If you want to set a new default VPC network, update the C(default) field of another VPC network in the same region.
+ - The previous network's C(default) field will be set to C(false) when a new default VPC has been defined.
+ type: bool
+ default: false
+ region:
+ description:
+ - The slug identifier for the region where the VPC will be created.
+ type: str
+ ip_range:
+ description:
+ - The requested range of IP addresses for the VPC in CIDR notation.
+ - Network ranges cannot overlap with other networks in the same account and must be in range of private addresses as defined in RFC1918.
+ - It may not be smaller than /24 nor larger than /16.
+ - If no IP range is specified, a /20 network range is generated that won't conflict with other VPC networks in your account.
+ type: str
+extends_documentation_fragment:
+- community.digitalocean.digital_ocean.documentation
+
+"""
+
+
+EXAMPLES = r"""
+- name: Create a VPC
+ community.digitalocean.digital_ocean_vpc:
+ state: present
+ name: myvpc1
+ region: nyc1
+
+- name: Create a VPC (choose IP range)
+ community.digitalocean.digital_ocean_vpc:
+ state: present
+ name: myvpc1
+ region: nyc1
+ ip_range: 192.168.192.0/24
+
+- name: Update a VPC (make it default)
+ community.digitalocean.digital_ocean_vpc:
+ state: present
+ name: myvpc1
+ region: nyc1
+ default: true
+
+- name: Update a VPC (change description)
+ community.digitalocean.digital_ocean_vpc:
+ state: present
+ name: myvpc1
+ region: nyc1
+ description: myvpc
+
+- name: Delete a VPC
+ community.digitalocean.digital_ocean_vpc:
+ state: absent
+ name: myvpc1
+"""
+
+
+RETURN = r"""
+data:
+ description: A DigitalOcean VPC.
+ returned: success
+ type: dict
+ sample:
+ msg: Created VPC myvpc1 in nyc1
+ vpc:
+ created_at: '2021-06-17T11:43:12.12121565Z'
+ default: false
+ description: ''
+ id: a3b72d97-192f-4984-9d71-08a5faf2e0c7
+ ip_range: 10.116.16.0/20
+ name: testvpc1
+ region: nyc1
+ urn: do:vpc:a3b72d97-192f-4984-9d71-08a5faf2e0c7
+"""
+
+
+import time
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+
+
+class DOVPC(object):
+ def __init__(self, module):
+ self.rest = DigitalOceanHelper(module)
+ self.module = module
+ # pop the oauth token so we don't include it in the POST data
+ self.module.params.pop("oauth_token")
+ self.name = module.params.get("name", None)
+ self.description = module.params.get("description", None)
+ self.default = module.params.get("default", False)
+ self.region = module.params.get("region", None)
+ self.ip_range = module.params.get("ip_range", None)
+ self.vpc_id = module.params.get("vpc_id", None)
+
+ def get_by_name(self):
+ page = 1
+ while page is not None:
+ response = self.rest.get("vpcs?page={0}".format(page))
+ json_data = response.json
+ if response.status_code == 200:
+ for vpc in json_data["vpcs"]:
+ if vpc.get("name", None) == self.name:
+ return vpc
+ if (
+ "links" in json_data
+ and "pages" in json_data["links"]
+ and "next" in json_data["links"]["pages"]
+ ):
+ page += 1
+ else:
+ page = None
+ return None
+
+ def create(self):
+ if self.module.check_mode:
+ return self.module.exit_json(changed=True)
+
+ vpc = self.get_by_name()
+ if vpc is not None: # update
+ vpc_id = vpc.get("id", None)
+ if vpc_id is not None:
+ data = {
+ "name": self.name,
+ }
+ if self.description is not None:
+ data["description"] = self.description
+ if self.default is not False:
+ data["default"] = True
+ response = self.rest.put("vpcs/{0}".format(vpc_id), data=data)
+ json = response.json
+ if response.status_code != 200:
+ self.module.fail_json(
+ msg="Failed to update VPC {0} in {1}: {2}".format(
+ self.name, self.region, json["message"]
+ )
+ )
+ else:
+ self.module.exit_json(
+ changed=False,
+ data=json,
+ msg="Updated VPC {0} in {1}".format(self.name, self.region),
+ )
+ else:
+ self.module.fail_json(
+ changed=False, msg="Unexpected error, please file a bug"
+ )
+
+ else: # create
+ data = {
+ "name": self.name,
+ "region": self.region,
+ }
+ if self.description is not None:
+ data["description"] = self.description
+ if self.ip_range is not None:
+ data["ip_range"] = self.ip_range
+
+ response = self.rest.post("vpcs", data=data)
+ status = response.status_code
+ json = response.json
+ if status == 201:
+ self.module.exit_json(
+ changed=True,
+ data=json,
+ msg="Created VPC {0} in {1}".format(self.name, self.region),
+ )
+ else:
+ self.module.fail_json(
+ changed=False,
+ msg="Failed to create VPC {0} in {1}: {2}".format(
+ self.name, self.region, json["message"]
+ ),
+ )
+
+ def delete(self):
+ if self.module.check_mode:
+ return self.module.exit_json(changed=True)
+
+ vpc = self.get_by_name()
+ if vpc is None:
+ self.module.fail_json(
+ msg="Unable to find VPC {0} in {1}".format(self.name, self.region)
+ )
+ else:
+ vpc_id = vpc.get("id", None)
+ if vpc_id is not None:
+ response = self.rest.delete("vpcs/{0}".format(str(vpc_id)))
+ status = response.status_code
+ json = response.json
+ if status == 204:
+ self.module.exit_json(
+ changed=True,
+ msg="Deleted VPC {0} in {1} ({2})".format(
+ self.name, self.region, vpc_id
+ ),
+ )
+ else:
+ json = response.json
+ self.module.fail_json(
+ changed=False,
+ msg="Failed to delete VPC {0} ({1}): {2}".format(
+ self.name, vpc_id, json["message"]
+ ),
+ )
+
+
+def run(module):
+ state = module.params.pop("state")
+ vpc = DOVPC(module)
+ if state == "present":
+ vpc.create()
+ elif state == "absent":
+ vpc.delete()
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ argument_spec.update(
+ state=dict(choices=["present", "absent"], default="present"),
+ name=dict(type="str", required=True),
+ description=dict(type="str"),
+ default=dict(type="bool", default=False),
+ region=dict(type="str"),
+ ip_range=dict(type="str"),
+ )
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ required_if=[
+ ["state", "present", ["name", "region"]],
+ ["state", "absent", ["name"]],
+ ],
+ supports_check_mode=True,
+ )
+
+ run(module)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_vpc_info.py b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_vpc_info.py
new file mode 100644
index 00000000..c2b11078
--- /dev/null
+++ b/ansible_collections/community/digitalocean/plugins/modules/digital_ocean_vpc_info.py
@@ -0,0 +1,161 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2018, Ansible Project
+# Copyright: (c) 2021, Mark Mercado <mamercad@gmail.com>
+# 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: digital_ocean_vpc_info
+short_description: Gather information about DigitalOcean VPCs
+version_added: 1.7.0
+description:
+ - This module can be used to gather information about DigitalOcean VPCs.
+author: "Mark Mercado (@mamercad)"
+options:
+ members:
+ description:
+ - Return VPC members (instead of all VPCs).
+ type: bool
+ default: False
+ name:
+ description:
+ - The name of the VPC.
+ type: str
+extends_documentation_fragment:
+- community.digitalocean.digital_ocean.documentation
+"""
+
+
+EXAMPLES = r"""
+- name: Fetch all VPCs
+ community.digitalocean.digital_ocean_vpc_info:
+ register: my_vpcs
+
+- name: Fetch members of a VPC
+ community.digitalocean.digital_ocean_vpc_info:
+ members: true
+ name: myvpc1
+ register: my_vpc_members
+"""
+
+
+RETURN = r"""
+data:
+ description: All DigitalOcean VPCs, or, members of a VPC (with C(members=True)).
+ returned: success
+ type: dict
+ sample:
+ - created_at: '2021-02-06T17:57:22Z'
+ default: true
+ description: ''
+ id: 0db3519b-9efc-414a-8868-8f2e6934688c
+ ip_range: 10.116.0.0/20
+ name: default-nyc1
+ region: nyc1
+ urn: do:vpc:0db3519b-9efc-414a-8868-8f2e6934688c
+ - links: {}
+ members: []
+ meta:
+ total: 0
+"""
+
+
+import time
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import (
+ DigitalOceanHelper,
+)
+
+
+class DOVPCInfo(object):
+ def __init__(self, module):
+ self.rest = DigitalOceanHelper(module)
+ self.module = module
+ # pop the oauth token so we don't include it in the POST data
+ self.module.params.pop("oauth_token")
+ self.name = self.module.params.pop("name", "")
+ self.members = self.module.params.pop("members", False)
+
+ def get_by_name(self):
+ page = 1
+ while page is not None:
+ response = self.rest.get("vpcs?page={0}".format(page))
+ json_data = response.json
+ if response.status_code == 200:
+ for vpc in json_data["vpcs"]:
+ if vpc.get("name", None) == self.name:
+ return vpc
+ if (
+ "links" in json_data
+ and "pages" in json_data["links"]
+ and "next" in json_data["links"]["pages"]
+ ):
+ page += 1
+ else:
+ page = None
+ return None
+
+ def get(self):
+ if self.module.check_mode:
+ return self.module.exit_json(changed=False)
+
+ if not self.members:
+ base_url = "vpcs?"
+ vpcs = self.rest.get_paginated_data(base_url=base_url, data_key_name="vpcs")
+ self.module.exit_json(changed=False, data=vpcs)
+ else:
+ vpc = self.get_by_name()
+ if vpc is not None:
+ vpc_id = vpc.get("id", None)
+ if vpc_id is not None:
+ response = self.rest.get("vpcs/{0}/members".format(vpc_id))
+ json = response.json
+ if response.status_code != 200:
+ self.module.fail_json(
+ msg="Failed to find VPC named {0}: {1}".format(
+ self.name, json["message"]
+ )
+ )
+ else:
+ self.module.exit_json(changed=False, data=json)
+ else:
+ self.module.fail_json(
+ changed=False, msg="Unexpected error, please file a bug"
+ )
+ else:
+ self.module.fail_json(
+ changed=False,
+ msg="Could not find a VPC named {0}".format(self.name),
+ )
+
+
+def run(module):
+ vpcs = DOVPCInfo(module)
+ vpcs.get()
+
+
+def main():
+ argument_spec = DigitalOceanHelper.digital_ocean_argument_spec()
+ argument_spec.update(
+ members=dict(type="bool", default=False),
+ name=dict(type="str"),
+ )
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ required_if=[
+ ["members", True, ["name"]],
+ ],
+ supports_check_mode=True,
+ )
+
+ run(module)
+
+
+if __name__ == "__main__":
+ main()