Init: mediaserver

This commit is contained in:
2023-02-08 12:13:28 +01:00
parent 848bc9739c
commit f7c23d4ba9
31914 changed files with 6175775 additions and 0 deletions

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

Some files were not shown because too many files have changed in this diff Show More