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,461 @@
====================================
Community DigitalOcean Release Notes
====================================
.. contents:: Topics
v1.23.0
=======
Minor Changes
-------------
- digital_ocean_load_balancer - add support for C(size_unit) over deprecated C(size); deprecate C(algorithm) completely (https://github.com/ansible-collections/community.digitalocean/issues/270).
- documentation - refresh the "Testing and Development" section of the C(README.md) (https://github.com/ansible-collections/community.digitalocean/issues/268).
- integration tests - add a dedicated integration test for C(digital_ocean_database_info) (https://github.com/ansible-collections/community.digitalocean/issues/289).
- integration tests - set pull request integration tests to run against branch instead of last commit (https://github.com/ansible-collections/community.digitalocean/issues/291).
Bugfixes
--------
- inventory plugin - bugfix for baseurl parameter (https://github.com/ansible-collections/community.digitalocean/pull/297).
- integration tests - add missing `environment` directive on pull request integration testing (https://github.com/ansible-collections/community.digitalocean/issues/293).
v1.22.0
=======
Minor Changes
-------------
- collection - added an action group C(community.digitalocean.all) for use with module defaults (https://docs.ansible.com/ansible/latest/user_guide/playbooks_module_defaults.html) (https://github.com/ansible-collections/community.digitalocean/issues/281).
- digital_ocean_vpc - add C(vpc) key to returned VPC data on create (https://github.com/ansible-collections/community.digitalocean/issues/276).
- integration tests - perform integration testing on all modules for changes in C(plugins/module_utils) or by changed module in C(plugins/modules) (https://github.com/ansible-collections/community.digitalocean/issues/286).
- integration tests - split the integration tests by module and run them serially (https://github.com/ansible-collections/community.digitalocean/issues/280).
v1.21.0
=======
Minor Changes
-------------
- digital_ocean - add sanity test ignores for Ansible 2.12 and 2.13 (https://github.com/ansible-collections/community.digitalocean/issues/247).
Bugfixes
--------
- digital_ocean_droplet - if the JSON response lacks a key and the associated variable is set to ``None``, then don't treat that variable like a ``dict`` and call ``get()`` on it without first testing it (https://github.com/ansible-collections/community.digitalocean/issues/272).
v1.20.0
=======
Minor Changes
-------------
- digital_ocean_cdn_endpoints - update Spaces endpoint and add a few delays to the integration test (https://github.com/ansible-collections/community.digitalocean/issues/267).
- digital_ocean_load_balancer - Allow creating a load balancer and associating droplets by tag as an alternative to ``droplet_ids``.
Bugfixes
--------
- digital_ocean_droplet - fix regression in droplet deletion where ``name`` and ``unique_name`` (set to true) are required and ``id`` alone is insufficient (though ``id`` is sufficient to uniquely identify a droplet for deletion). (https://github.com/ansible-collections/community.digitalocean/issues/260)
- digital_ocean_droplet - fix regression where droplet info (for example networking) doesn't update when waiting during creation unless ``unique_name`` is set to true (https://github.com/ansible-collections/community.digitalocean/issues/220).
v1.19.0
=======
Minor Changes
-------------
- digital_ocean - reference C(DO_API_TOKEN) consistently in module documentation and examples (https://github.com/ansible-collections/community.digitalocean/issues/248).
Bugfixes
--------
- digital_ocean_cdn_endpoints - remove non-API parameters before posting to the API (https://github.com/ansible-collections/community.digitalocean/issues/252).
- digital_ocean_cdn_endpoints - use the correct module name in the C(EXAMPLES) (https://github.com/ansible-collections/community.digitalocean/issues/251).
v1.18.0
=======
Minor Changes
-------------
- ci - adding stable-2.13 to sanity and unit testing (https://github.com/ansible-collections/community.digitalocean/issues/239).
- digital_ocean_spaces - set C(no_log=True) for C(aws_access_key_id) parameter (https://github.com/ansible-collections/community.digitalocean/issues/243).
- digital_ocean_spaces_info - set C(no_log=True) for C(aws_access_key_id) parameter (https://github.com/ansible-collections/community.digitalocean/issues/243).
v1.17.0
=======
Minor Changes
-------------
- digital_ocean - parameterize the DigitalOcean API base url (https://github.com/ansible-collections/community.digitalocean/issues/237).
v1.16.0
=======
Minor Changes
-------------
- black test - added a 15 minute timeout (https://github.com/ansible-collections/community.digitalocean/issues/228).
- digital_ocean_domain - add support for IPv6 apex domain records (https://github.com/ansible-collections/community.digitalocean/issues/226).
- integration tests - added a 120 minute timeout (https://github.com/ansible-collections/community.digitalocean/issues/228).
- sanity and unit tests - added a 30 minute timeout (https://github.com/ansible-collections/community.digitalocean/issues/228).
Bugfixes
--------
- digital_ocean_kubernetes - add missing elements type to C(node_pools.tags) and C(node_pools.taints) options (https://github.com/ansible-collections/community.digitalocean/issues/232).
New Modules
-----------
- digital_ocean_domain_record_info - Gather information about DigitalOcean domain records
v1.15.1
=======
Minor Changes
-------------
- Updates DigitalOcean API documentation links to current domain with working URL anchors (https://github.com/ansible-collections/community.digitalocean/issues/223).
Bugfixes
--------
- digital_ocean_droplet - fix reporting of changed state when ``firewall`` argument is present (https://github.com/ansible-collections/community.digitalocean/pull/219).
v1.15.0
=======
Bugfixes
--------
- digital_ocean_droplet - move Droplet data under "droplet" key in returned payload (https://github.com/ansible-collections/community.digitalocean/issues/211).
New Modules
-----------
- digital_ocean_spaces - Create and remove DigitalOcean Spaces.
- digital_ocean_spaces_info - List DigitalOcean Spaces.
v1.14.0
=======
Minor Changes
-------------
- digital_ocean_kubernetes_info - switching C(changed=True) to C(changed=False) since getting information is read-only in nature (https://github.com/ansible-collections/community.digitalocean/issues/204).
Bugfixes
--------
- Update README.md with updated Droplet examples (https://github.com/ansible-collections/community.digitalocean/issues/199).
- digital_ocean_cdn_endpoints - defaulting optional string parameters as strings (https://github.com/ansible-collections/community.digitalocean/issues/205).
- digital_ocean_cdn_endpoints - updating Spaces endpoint for the integration test (https://github.com/ansible-collections/community.digitalocean/issues/205).
- digital_ocean_droplet - ensure that Droplet creation is successful (https://github.com/ansible-collections/community.digitalocean/issues/197).
- digital_ocean_droplet - fixing project assignment for the C(unique_name=False) case (https://github.com/ansible-collections/community.digitalocean/issues/201).
- digital_ocean_droplet - update Droplet examples (https://github.com/ansible-collections/community.digitalocean/issues/199).
v1.13.0
=======
Minor Changes
-------------
- Set Python 3.9 as the C(python-version) and C(target-python-version) in the integration, sanity, and unit tests for Ansible > 2.9 (3.8 otherwise).
- digital_ocean_droplet - allow the user to override the Droplet action and status polling interval (https://github.com/ansible-collections/community.digitalocean/issues/194).
- digital_ocean_kubernetes - adding support for HA control plane (https://github.com/ansible-collections/community.digitalocean/issues/190).
v1.12.0
=======
Minor Changes
-------------
- digital_ocean_block_storage - adding Project support (https://github.com/ansible-collections/community.digitalocean/issues/171).
- digital_ocean_database - adding Project support (https://github.com/ansible-collections/community.digitalocean/issues/171).
- digital_ocean_domain - adding Project support (https://github.com/ansible-collections/community.digitalocean/issues/171).
- digital_ocean_droplet - adding Project support (https://github.com/ansible-collections/community.digitalocean/issues/171).
- digital_ocean_droplet - adding ability to apply and remove firewall by using droplet module (https://github.com/ansible-collections/community.digitalocean/issues/159).
- digital_ocean_droplet - require unique_name for state=absent to avoid unintentional droplet deletions.
- digital_ocean_firewall - inbound_rules and outbound_rules are no longer required for firewall removal (https://github.com/ansible-collections/community.digitalocean/issues/181).
- digital_ocean_floating_ip - adding Project support (https://github.com/ansible-collections/community.digitalocean/issues/171).
- digital_ocean_floating_ip - adding attach and detach states to floating ip module (https://github.com/ansible-collections/community.digitalocean/issues/170).
- digital_ocean_load_balancer - adding Project support (https://github.com/ansible-collections/community.digitalocean/issues/171).
- digitalocean integration tests - adding integration tests for CDN Endpoints (https://github.com/ansible-collections/community.digitalocean/issues/179).
Bugfixes
--------
- Update the tests so that they only run once (https://github.com/ansible-collections/community.digitalocean/issues/186).
- digital_ocean_droplet - fix resizing with C(state: active) does not actually turn Droplet on (https://github.com/ansible-collections/community.digitalocean/issues/140).
- digital_ocean_kubernetes - fix return value consistency (https://github.com/ansible-collections/community.digitalocean/issues/174).
v1.11.0
=======
Minor Changes
-------------
- digitalocean inventory script - add support for Droplet tag filtering (https://github.com/ansible-collections/community.digitalocean/issues/7).
Bugfixes
--------
- Adding missing status badges for black and unit tests (https://github.com/ansible-collections/community.digitalocean/pull/164).
- Documentation URLs are fixed for the C(digital_ocean_domain_record) and C(digital_ocean_droplet_info) modules (https://github.com/ansible-collections/community.digitalocean/pull/163).
- Serializing the cloud integration tests (https://github.com/ansible-collections/community.digitalocean/pull/165).
- digital_ocean_floating_ip - make floating ip return data idempotent (https://github.com/ansible-collections/community.digitalocean/pull/162).
- digitalocean inventory - enforce the C(timeout) parameter (https://github.com/ansible-collections/community.digitalocean/issues/168).
v1.10.0
=======
Minor Changes
-------------
- digital_ocean_kubernetes - adding the C(taints), C(auto_scale), C(min_nodes) and C(max_nodes) parameters to the C(node_pools) definition (https://github.com/ansible-collections/community.digitalocean/issues/157).
Bugfixes
--------
- digital_ocean_block_storage - fix block volumes detach idempotency (https://github.com/ansible-collections/community.digitalocean/issues/149).
- digital_ocean_droplet - ensure "active" state before issuing "power on" action (https://github.com/ansible-collections/community.digitalocean/issues/150)
- digital_ocean_droplet - power on should poll/wait, resize should support "active" state (https://github.com/ansible-collections/community.digitalocean/pull/143).
- digital_ocean_load_balancer - C(droplet_ids) are not required when C(state=absent) is chosen (https://github.com/ansible-collections/community.digitalocean/pull/147).
- digital_ocean_load_balancer - when C(state=absent) is chosen the API returns an empty response (https://github.com/ansible-collections/community.digitalocean/pull/147).
New Modules
-----------
- digital_ocean_cdn_endpoints - Create and delete DigitalOcean CDN Endpoints
- digital_ocean_cdn_endpoints_info - Gather information about DigitalOcean CDN Endpoints
- digital_ocean_load_balancer - Manage DigitalOcean Load Balancers
- digital_ocean_monitoring_alerts - Create and delete DigitalOcean Monitoring alerts
- digital_ocean_monitoring_alerts_info - Gather information about DigitalOcean Monitoring alerts
v1.9.0
======
Minor Changes
-------------
- digital_ocean - running and enforcing psf/black in the codebase (https://github.com/ansible-collections/community.digitalocean/issues/136).
- digital_ocean_floating_ip_info - new integration test for the `digital_ocean_floating_ip_info` module (https://github.com/ansible-collections/community.digitalocean/issues/130).
Bugfixes
--------
- digital_ocean_database - increase the database creation integration test timeout (https://github.com/ansible-collections/community.digitalocean).
- digital_ocean_floating_ip - delete all Floating IPs initially during the integration test run (https://github.com/ansible-collections/community.digitalocean/issues/129).
- digitalocean inventory - respect the TRANSFORM_INVALID_GROUP_CHARS configuration setting (https://github.com/ansible-collections/community.digitalocean/pull/138).
- info modules - adding missing check mode support (https://github.com/ansible-collections/community.digitalocean/issues/139).
v1.8.0
======
Minor Changes
-------------
- digital_ocean_database - add support for MongoDB (https://github.com/ansible-collections/community.digitalocean/issues/124).
Bugfixes
--------
- digital_ocean - integration tests need community.general and jmespath (https://github.com/ansible-collections/community.digitalocean/issues/121).
- digital_ocean_firewall - fixed idempotence (https://github.com/ansible-collections/community.digitalocean/issues/122).
v1.7.0
======
Minor Changes
-------------
- digital_ocean_kubernetes - set "latest" as the default version for new clusters (https://github.com/ansible-collections/community.digitalocean/issues/114).
Bugfixes
--------
- digital_ocean_certificate - fixing integration test (https://github.com/ansible-collections/community.digitalocean/issues/114).
- digital_ocean_droplet - state `present` with `wait` was not waiting (https://github.com/ansible-collections/community.digitalocean/issues/116).
- digital_ocean_firewall - fixing integration test (https://github.com/ansible-collections/community.digitalocean/issues/114).
- digital_ocean_tag - fixing integration test (https://github.com/ansible-collections/community.digitalocean/issues/114).
- digitalocean - update README.md with project_info and project module (https://github.com/ansible-collections/community.digitalocean/pull/112).
New Modules
-----------
- digital_ocean_snapshot - Create and delete DigitalOcean snapshots
- digital_ocean_vpc - Create and delete DigitalOcean VPCs
- digital_ocean_vpc_info - Gather information about DigitalOcean VPCs
v1.6.0
======
Bugfixes
--------
- digital_ocean_certificate_info - ensure return type is a list (https://github.com/ansible-collections/community.digitalocean/issues/55).
- digital_ocean_domain_info - ensure return type is a list (https://github.com/ansible-collections/community.digitalocean/issues/55).
- digital_ocean_firewall_info - ensure return type is a list (https://github.com/ansible-collections/community.digitalocean/issues/55).
- digital_ocean_load_balancer_info - ensure return type is a list (https://github.com/ansible-collections/community.digitalocean/issues/55).
- digital_ocean_tag_info - ensure return type is a list (https://github.com/ansible-collections/community.digitalocean/issues/55).
- digitalocean inventory plugin - attributes available to filters are limited to explicitly required attributes and are prefixed with ``var_prefix`` (https://github.com/ansible-collections/community.digitalocean/pull/102).
New Modules
-----------
- digital_ocean_project - Manage a DigitalOcean project
- digital_ocean_project_info - Gather information about DigitalOcean Projects
v1.5.1
======
Bugfixes
--------
- digitalocean inventory plugin - Wire up advertised caching functionality (https://github.com/ansible-collections/community.digitalocean/pull/97).
v1.5.0
======
Minor Changes
-------------
- digitalocean - Filter droplets in dynamic inventory plugin using arbitrary. jinja2 expressions (https://github.com/ansible-collections/community.digitalocean/pull/96).
- digitalocean - Support templates in API tokens when using the dynamic inventory plugin (https://github.com/ansible-collections/community.digitalocean/pull/98).
Bugfixes
--------
- digital_ocean_database - Fixed DB attribute settings (https://github.com/ansible-collections/community.digitalocean/issues/94).
- digital_ocean_database_info - Cleanup unused attribs (https://github.com/ansible-collections/community.digitalocean/pulls/100).
- digital_ocean_snapshot_info - Fix lookup of snapshot_info by_id (https://github.com/ansible-collections/community.digitalocean/issues/92).
- digital_ocean_tag - Fix tag idempotency (https://github.com/ansible-collections/community.digitalocean/issues/61).
v1.4.2
======
Bugfixes
--------
- digital_ocean_droplet - Fixed Droplet inactive state (https://github.com/ansible-collections/community.digitalocean/pull/88).
- digital_ocean_sshkey - Fixed SSH Key Traceback Issue (https://github.com/ansible-collections/community.digitalocean/issues/68).
v1.4.1
======
Bugfixes
--------
- digital_ocean_droplet - Add integration tests for Droplet active and inactive states (https://github.com/ansible-collections/community.digitalocean/issues/66).
- digital_ocean_droplet - Fix Droplet inactive state (https://github.com/ansible-collections/community.digitalocean/issues/83).
v1.4.0
======
Bugfixes
--------
- digital_ocean_droplet_info - Fix documentation link for `digital_ocean_droplet_info` (https://github.com/ansible-collections/community.digitalocean/pull/81).
- digitalocean - Fix return docs for digital_ocean_sshkey_info (https://github.com/ansible-collections/community.digitalocean/issues/56).
- digitalocean - Update README.md for K8s and databases (https://github.com/ansible-collections/community.digitalocean/pull/80).
New Modules
-----------
- digital_ocean_droplet_info - Gather information about DigitalOcean Droplets
v1.3.0
======
New Modules
-----------
- digital_ocean_database - Create and delete a DigitalOcean database
- digital_ocean_database_info - Gather information about DigitalOcean databases
- digital_ocean_kubernetes - Create and delete a DigitalOcean Kubernetes cluster
- digital_ocean_kubernetes_info - Returns information about an existing DigitalOcean Kubernetes cluster
v1.2.0
======
Minor Changes
-------------
- digital_ocean - ``ssh_key_ids`` list entries are now validated to be strings (https://github.com/ansible-collections/community.digitalocean/issues/13).
- digital_ocean_droplet - ``ssh_keys``, ``tags``, and ``volumes`` list entries are now validated to be strings (https://github.com/ansible-collections/community.digitalocean/issues/13).
- digital_ocean_droplet - adding ``active`` and ``inactive`` states (https://github.com/ansible-collections/community.digitalocean/issues/23).
- digital_ocean_droplet - adds Droplet resize functionality (https://github.com/ansible-collections/community.digitalocean/issues/4).
Bugfixes
--------
- digital_ocean inventory script - fail cleaner on invalid ``HOST`` argument to ``--host`` option (https://github.com/ansible-collections/community.digitalocean/pull/44).
- digital_ocean inventory script - implement unimplemented ``use_private_network`` option and register missing ``do_ip_address``, ``do_private_ip_address`` host vars (https://github.com/ansible-collections/community.digitalocean/pull/45/files).
- digital_ocean inventory script - return JSON consistent with specification with ``--host`` (https://github.com/ansible-collections/community.digitalocean/pull/44).
- digital_ocean_domain - return zone records when creating a new zone (https://github.com/ansible-collections/community.digitalocean/issues/46).
- digital_ocean_droplet - add missing ``required=True`` on ``do_oauth_token`` in ``argument_spec`` (https://github.com/ansible-collections/community.digitalocean/issues/13).
- digital_ocean_floating_ip - fixes idempotence (https://github.com/ansible-collections/community.digitalocean/issues/5).
New Modules
-----------
- digital_ocean_balance_info - Display DigitalOcean customer balance
v1.1.1
======
Bugfixes
--------
- digitalocean - Drop collection version from README.md (https://github.com/ansible-collections/community.digitalocean/issues/63).
v1.1.0
======
Minor Changes
-------------
- digital_ocean_block_storage - included ability to resize Block Storage Volumes (https://github.com/ansible-collections/community.digitalocean/issues/38).
Bugfixes
--------
- digital_ocean_certificate_info - fix retrieving certificate by ID (https://github.com/ansible-collections/community.digitalocean/issues/35).
- digital_ocean_domain - module is now idempotent when called without IP (https://github.com/ansible-collections/community.digitalocean/issues/21).
- digital_ocean_load_balancer_info - fix retrieving load balancer by ID (https://github.com/ansible-collections/community.digitalocean/issues/35).
New Plugins
-----------
Inventory
~~~~~~~~~
- digitalocean - DigitalOcean Inventory Plugin
New Modules
-----------
- digital_ocean_domain_record - Manage DigitalOcean domain records
- digital_ocean_firewall - Manage cloud firewalls within DigitalOcean
v1.0.0
======
Bugfixes
--------
- Sanity test documentation fixes (https://github.com/ansible-collections/community.digitalocean/pull/3).
- Update docs examples to use FQCN (https://github.com/ansible-collections/community.digitalocean/issues/14).
v0.1.0
======
Release Summary
---------------
Initial release of the collection after extracing the modules from `community.general <https://github.com/ansible-collections/community.general/>`_.

View File

@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

View File

@@ -0,0 +1,61 @@
{
"collection_info": {
"namespace": "community",
"name": "digitalocean",
"version": "1.23.0",
"authors": [
"Ansible (https://github.com/ansible)",
"BondAnthony (https://github.com/BondAnthony)",
"Akasurde (https://github.com/Akasurde)",
"pmarques (https://github.com/pmarques)",
"geerlingguy (https://www.jeffgeerling.com/)",
"Andres Hermosilla (https://github.com/rezen)",
"Luis (https://github.com/lalvarezguillen)",
"grzs (https://github.com/grzs)",
"Lucas Basquerotto (https://github.com/lucasbasquerotto)",
"Tadej Borov\u0161ak (https://github.com/tadeboro)",
"Mark Mercado (https://github.com/mamercad)",
"Mike Pontillo (https://github.com/mpontillo)",
"Felix Fontein (https://github.com/felixfontein)",
"Andrew Starr-Bochicchio (https://github.com/andrewsomething)",
"Sam Pinkus (https://github.com/sgpinkus)",
"Luis (https://github.com/lalvarezguillen)",
"John R Barker (https://github.com/gundalow)",
"Andrew Klychkov (https://github.com/Andersson007)",
"Tyler Auerbeck (https://github.com/tylerauerbeck)",
"Angel Aviel Domaoan (https://github.com/tenshiAMD)",
"Max Truxa (https://github.com/maxtruxa)",
"Franco Posa (https://github.com/francoposa)",
"magicrobotmonkey (https://github.com/magicrobotmonkey)",
"radioactive73 (https://github.com/radioactive73)",
"danxg87 (https://github.com/danxg87)",
"Sviatoslav Sydorenko (https://github.com/webknjaz)",
"Vitaly Khabarov (https://github.com/vitkhab)",
"Onur G\u00fczel (https://github.com/onurguzel)",
"Shuaib Munshi (https://github.com/shuaibmunshi)",
"Corey Wright (https://github.com/coreywright)"
],
"readme": "README.md",
"tags": [
"digitalocean",
"cloud",
"droplet"
],
"description": "DigitalOcean Ansible Collection.",
"license": [],
"license_file": "COPYING",
"dependencies": {},
"repository": "https://github.com/ansible-collections/community.digitalocean",
"documentation": "https://docs.ansible.com/ansible/latest/collections/community/digitalocean/",
"homepage": "https://github.com/ansible-collections/community.digitalocean",
"issues": "https://github.com/ansible-collections/community.digitalocean/issues"
},
"file_manifest_file": {
"name": "FILES.json",
"ftype": "file",
"chksum_type": "sha256",
"chksum_sha256": "73859de3a30b2cdcb242f4b496ef31c47fc8d36f64646d3149622c7738d1929f",
"format": 1
},
"format": 1
}

View File

@@ -0,0 +1,254 @@
# DigitalOcean Community Collection
[![coverage](https://img.shields.io/codecov/c/github/ansible-collections/community.digitalocean)](https://codecov.io/gh/ansible-collections/community.digitalocean)
[![black](https://github.com/ansible-collections/community.digitalocean/actions/workflows/black.yml/badge.svg)](https://github.com/ansible-collections/community.digitalocean/actions/workflows/black.yml)
[![integration](https://github.com/ansible-collections/community.digitalocean/actions/workflows/ansible-test-integration.yml/badge.svg)](https://github.com/ansible-collections/community.digitalocean/actions/workflows/ansible-test-integration.yml)
[![sanity](https://github.com/ansible-collections/community.digitalocean/actions/workflows/ansible-test-sanity.yml/badge.svg)](https://github.com/ansible-collections/community.digitalocean/actions/workflows/ansible-test-sanity.yml)
[![unit](https://github.com/ansible-collections/community.digitalocean/actions/workflows/ansible-test-unit.yml/badge.svg)](https://github.com/ansible-collections/community.digitalocean/actions/workflows/ansible-test-unit.yml)
This collection contains modules and plugins to assist in automating [DigitalOcean](https://www.digitalocean.com) infrastructure and API interactions with Ansible.
## Included content
- [digital_ocean](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_module.html) Create/delete a droplet/SSH_key in DigitalOcean
- [digital_ocean_account_facts](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_account_facts_module.html) Gather information about DigitalOcean User account
- [digital_ocean_account_info](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_account_info_module.html) Gather information about DigitalOcean User account
- [digital_ocean_balance_info](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_balance_info_module.html) Display DigitalOcean customer balance
- [digital_ocean_block_storage](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_block_storage_module.html) Create/destroy or attach/detach Block Storage volumes in DigitalOcean
- [digital_ocean_cdn_endpoints_info](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_cdn_endpoints_info_module.html) Gather information about DigitalOcean CDN Endpoints
- [digital_ocean_cdn_endpoints](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_cdn_endpoints_module.html) Create and delete DigitalOcean CDN Endpoints
- [digital_ocean_certificate](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_certificate_module.html) Manage certificates in DigitalOcean.
- [digital_ocean_certificate_facts](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_certificate_facts_module.html) Gather information about DigitalOcean certificates
- [digital_ocean_certificate_info](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_certificate_info_module.html) Gather information about DigitalOcean certificates
- [digital_ocean_database_info](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_database_info_module.html) Gather information about DigitalOcean databases
- [digital_ocean_database](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_database_module.html) Create and delete DigitalOcean databases
- [digital_ocean_domain](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_domain_module.html) Create/delete a DNS domain in DigitalOcean
- [digital_ocean_domain_facts](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_domain_facts_module.html) Gather information about DigitalOcean Domains
- [digital_ocean_domain_info](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_domain_info_module.html) Gather information about DigitalOcean Domains
- [digital_ocean_domain_record_info](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_domain_record_info_module.html) Gather information about DigitalOcean domain records
- [digital_ocean_domain_record](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_domain_record_module.html) Create and delete DigitalOcean Domain Records
- [digital_ocean_droplet](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_droplet_module.html) Create and delete a DigitalOcean droplet
- [digital_ocean_droplet_info](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_droplet_info_module.html) - Gather information about DigitalOcean Droplets
- [digital_ocean_firewall](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_firewall_module.html) Create and delete DigitalOcean firewalls
- [digital_ocean_firewall_facts](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_firewall_facts_module.html) Gather information about DigitalOcean firewalls
- [digital_ocean_firewall_info](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_firewall_info_module.html) Gather information about DigitalOcean firewalls
- [digital_ocean_floating_ip](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_floating_ip_module.html) Manage DigitalOcean Floating IPs
- [digital_ocean_floating_ip_facts](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_floating_ip_facts_module.html) DigitalOcean Floating IPs information
- [digital_ocean_floating_ip_info](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_floating_ip_info_module.html) DigitalOcean Floating IPs information
- [digital_ocean_image_facts](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_image_facts_module.html) Gather information about DigitalOcean images
- [digital_ocean_image_info](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_image_info_module.html) Gather information about DigitalOcean images
- [digital_ocean_kubernetes_info](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_kubernetes_info_module.html) Gather information about DigitalOcean Kubernetes clusters
- [digital_ocean_kubernetes](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_kubernetes_module.html) Create and delete DigitalOcean Kubernetes clusters
- [digital_ocean_load_balancer](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_load_balancer_module.html) Create and delete DigitalOcean load balancers
- [digital_ocean_load_balancer_facts](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_load_balancer_facts_module.html) Gather information about DigitalOcean load balancers
- [digital_ocean_load_balancer_info](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_load_balancer_info_module.html) Gather information about DigitalOcean load balancers
- [digital_ocean_monitoring_alerts](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_monitoring_alerts_module.html) Create and delete DigitalOcean Monitoring alerts
- [digital_ocean_monitoring_alerts_info](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_monitoring_alerts_info_module.html) Gather information about DigitalOcean Monitoring alerts
- [digital_ocean_project_info](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_load_project_info_module.html) Gather information about DigitalOcean projects
- [digital_ocean_project](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_load_project_module.html) Manage DigitalOcean projects
- [digital_ocean_region_facts](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_region_facts_module.html) Gather information about DigitalOcean regions
- [digital_ocean_region_info](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_region_info_module.html) Gather information about DigitalOcean regions
- [digital_ocean_size_facts](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_size_facts_module.html) Gather information about DigitalOcean Droplet sizes
- [digital_ocean_size_info](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_size_info_module.html) Gather information about DigitalOcean Droplet sizes
- [digital_ocean_snapshot_facts](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_snapshot_facts_module.html) Gather information about DigitalOcean Snapshot
- [digital_ocean_snapshot_info](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_snapshot_info_module.html) Gather information about DigitalOcean Snapshot
- [digital_ocean_snapshot](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_snapshot_module.html) Manage DigitalOcean Snapshots
- [digital_ocean_spaces_info](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_spaces_info_module.html) Gather information about DigitalOcean Spaces
- [digital_ocean_spaces](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_spaces_module.html) Manage DigitalOcean Spaces
- [digital_ocean_sshkey](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_sshkey_module.html) Manage DigitalOcean SSH keys
- [digital_ocean_sshkey_facts](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_sshkey_facts_module.html) DigitalOcean SSH keys facts
- [digital_ocean_sshkey_info](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_sshkey_info_module.html) Gather information about DigitalOcean SSH keys
- [digital_ocean_tag](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_tag_module.html) Create and remove tag(s) to DigitalOcean resource.
- [digital_ocean_tag_facts](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_tag_facts_module.html) Gather information about DigitalOcean tags
- [digital_ocean_tag_info](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_tag_info_module.html) Gather information about DigitalOcean tags
- [digital_ocean_volume_facts](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_volume_facts_module.html) Gather information about DigitalOcean volumes
- [digital_ocean_volume_info](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_volume_info_module.html) Gather information about DigitalOcean volumes
- [digital_ocean_vpc](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_vpc_module.html) Create and delete DigitalOcean VPCs
- [digital_ocean_vpc_info](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digital_ocean_vpc_info_module.html) Gather information about DigitalOcean VPCs
- [digitalocean](https://docs.ansible.com/ansible/latest/collections/community/digitalocean/digitalocean_inventory.html) DigitalOcean Inventory Plugin
## Installation and Usage
### Requirements
The collection is tested and supported with:
- ansible >= 2.9.10 or ansible-core >= 2.11 (as well as the [devel branch](https://github.com/ansible/ansible))
- python >= 3.6
### Installing the Collection from Ansible Galaxy
Before using the DigitalOcean collection, you need to install it with the Ansible Galaxy CLI:
ansible-galaxy collection install community.digitalocean
You can also include it in a `requirements.yml` file and install it via `ansible-galaxy collection install -r requirements.yml`, using the format:
```yaml
---
collections:
- name: community.digitalocean
```
### Using modules from the DigitalOcean Collection in your playbooks
It's preferable to use content in this collection using their Fully Qualified Collection Namespace (FQCN), for example `community.digitalocean.digital_ocean_droplet`:
```yaml
---
- hosts: localhost
gather_facts: false
connection: local
vars:
oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
# You can also default the value of a variable for every DO module using module_defaults
# module_defaults:
# group/community.digitalocean.all:
# oauth_token: "{{ lookup('ansible.builtin.env', 'DO_API_TOKEN') }}"
tasks:
- name: Create SSH key
community.digitalocean.digital_ocean_sshkey:
oauth_token: "{{ oauth_token }}"
name: mykey
ssh_pub_key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDDHr/jh2Jy4yALcK4JyWbVkPRaWmhck3IgCoeOO3z1e2dBowLh64QAM+Qb72pxekALga2oi4GvT+TlWNhzPH4V example"
state: present
register: my_ssh_key
- name: Create a new Droplet
community.digitalocean.digital_ocean_droplet:
oauth_token: "{{ oauth_token }}"
state: present
name: mydroplet
unique_name: true
size: s-1vcpu-1gb
region: sfo3
image: ubuntu-20-04-x64
wait_timeout: 500
ssh_keys:
- "{{ my_ssh_key.data.ssh_key.id }}"
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: Tag a resource; creating the tag if it does not exist
community.digitalocean.digital_ocean_tag:
oauth_token: "{{ oauth_token }}"
name: "{{ item }}"
resource_id: "{{ my_droplet.data.droplet.id }}"
state: present
loop:
- staging
- dbserver
```
If upgrading older playbooks which were built prior to Ansible 2.10 and this collection's existence, you can also define `collections` in your play and refer to this collection's modules as you did in Ansible 2.9 and below, as in this example:
```yaml
---
- hosts: localhost
gather_facts: false
connection: local
collections:
- community.digitalocean
tasks:
- name: Create ssh key
digital_ocean_sshkey:
oauth_token: "{{ oauth_token }}"
...
```
## Testing and Development
If you want to develop new content for this collection or improve what's already here, the easiest way to work on the collection is to clone it into one of the configured [`COLLECTIONS_PATHS`](https://docs.ansible.com/ansible/latest/reference_appendices/config.html#collections-paths), and work on it there.
Alternatively, to develop completely out of `~/src/ansible-dev`, one could:
mkdir -p ~/src/ansible-dev
cd ~/src/ansible-dev
python3 -m venv venv
source venv/bin/activate
git clone https://github.com/ansible/ansible.git
pip install --requirement ansible/requirements.txt
pip install kubernetes
source ansible/hacking/env-setup
export ANSIBLE_COLLECTIONS_PATHS="~/src/ansible-dev/ansible_collections"
ansible-galaxy collection install community.digitalocean community.general
This gives us a self-contained environment in `~/src/ansible-dev` consisting of Python, Ansible, and this collection (located in `~/src/ansible-dev/ansible_collections/community/digitalocean`).
This collection requires functionality from `community.general`, and as such, we install it as well.
If you would like to contribute any changes which you have made to the collection, you will have to push them to your fork.
If you do not have a fork yet, you can create one [here](https://github.com/ansible-collections/community.digitalocean/fork).
Once you have a fork:
cd ~/src/ansible-dev/ansible_collections/community/digitalocean
git remote add origin git@github.com:{your fork organization}/community.digitalocean.git
git checkout -b my-awesome-fixes
git commit -am "My awesome fixes"
git push -u origin my-awesome-fixes
Now, you should be ready to create a [Pull Request](https://github.com/ansible-collections/community.digitalocean/pulls).
### Testing with `ansible-test`
The `tests` directory inside the collection root contains configuration for
running unit, sanity, and integration tests using [`ansible-test`](https://docs.ansible.com/ansible/latest/dev_guide/testing_integration.html).
You can run the collection's test suites with the commands:
ansible-test units --venv --python 3.9
ansible-test sanity --venv --python 3.9
ansible-test integration --venv --python 3.9
Replace `--venv` with `--docker` if you'd like to use Docker for the testing runtime environment.
Note: To run integration tests, you must add an [`tests/integration/integration_config.yml`](https://docs.ansible.com/ansible/latest/dev_guide/testing_integration.html#integration-config-yml) file with a valid DigitalOcean API Key (variable `do_api_key`),
AWS Access ID and Secret Key (variables `aws_access_key_id` and `aws_secret_access_key`, respectively). The AWS
variables are used for the DigitalOcean Spaces and CDN Endpoints integration tests.
## Release notes
See the [changelog](https://github.com/ansible-collections/community.digitalocean/blob/main/CHANGELOG.rst).
### Release process
Releases are automatically built and pushed to Ansible Galaxy for any new tag. Before tagging a release, make sure to do the following:
1. Update `galaxy.yml` and this README's `requirements.yml` example with the new `version` for the collection. Make sure all new modules have references above.
1. Update the CHANGELOG:
1. Make sure you have [`antsibull-changelog`](https://pypi.org/project/antsibull-changelog/) installed.
1. Make sure there are fragments for all known changes in `changelogs/fragments`.
1. Run `antsibull-changelog release`.
1. Don't forget to add new folks to `galaxy.yml`.
1. Commit the changes and create a PR with the changes. Wait for tests to pass, then merge it once they have.
1. Tag the version in Git and push to GitHub.
1. Determine the next version (collections follow [semver](https://docs.ansible.com/ansible/latest/dev_guide/developing_collections.html#collection-versions) semantics) by listing tags or looking at the [releases](https://github.com/ansible-collections/community.digitalocean/releases).
1. List tags with `git tag --list`
1. Create a new tag with `git tag 1.2.3`
1. Push tags upstream with `git push upstream --tags`
After the version is published, verify it exists on the [DigitalOcean Collection Galaxy page](https://galaxy.ansible.com/community/digitalocean).
## More information
- [DigitalOcean Working Group](https://github.com/ansible/community/wiki/Digital-Ocean)
- [Ansible Collection overview](https://github.com/ansible-collections/overview)
- [Ansible User guide](https://docs.ansible.com/ansible/latest/user_guide/index.html)
- [Ansible Developer guide](https://docs.ansible.com/ansible/latest/dev_guide/index.html)
- [Ansible Community code of conduct](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html)
## Licensing
GNU General Public License v3.0 or later.
See [COPYING](https://www.gnu.org/licenses/gpl-3.0.txt) to see the full text.

View File

@@ -0,0 +1 @@
/.plugin-cache.yaml

View File

@@ -0,0 +1,516 @@
ancestor: null
releases:
0.1.0:
changes:
release_summary: Initial release of the collection after extracing the modules
from `community.general <https://github.com/ansible-collections/community.general/>`_.
fragments:
- 0.1.0.yml
release_date: '2020-07-07'
1.0.0:
changes:
bugfixes:
- Sanity test documentation fixes (https://github.com/ansible-collections/community.digitalocean/pull/3).
- Update docs examples to use FQCN (https://github.com/ansible-collections/community.digitalocean/issues/14).
fragments:
- 14-docs-fqcn.yaml
- 3-sanity-docs-fixes.yaml
release_date: '2020-08-17'
1.1.0:
changes:
bugfixes:
- digital_ocean_certificate_info - fix retrieving certificate by ID (https://github.com/ansible-collections/community.digitalocean/issues/35).
- digital_ocean_domain - module is now idempotent when called without IP (https://github.com/ansible-collections/community.digitalocean/issues/21).
- digital_ocean_load_balancer_info - fix retrieving load balancer by ID (https://github.com/ansible-collections/community.digitalocean/issues/35).
minor_changes:
- digital_ocean_block_storage - included ability to resize Block Storage Volumes
(https://github.com/ansible-collections/community.digitalocean/issues/38).
fragments:
- 22-digital_ocean_domain-idempotent.yml
- 38-resize-volumes.yml
- 49-fix-lb-and-cert-info.yaml
modules:
- description: Manage DigitalOcean domain records
name: digital_ocean_domain_record
namespace: ''
- description: Manage cloud firewalls within DigitalOcean
name: digital_ocean_firewall
namespace: ''
plugins:
inventory:
- description: DigitalOcean Inventory Plugin
name: digitalocean
namespace: null
release_date: '2021-04-01'
1.1.1:
changes:
bugfixes:
- digitalocean - Drop collection version from README.md (https://github.com/ansible-collections/community.digitalocean/issues/63).
fragments:
- 63-readme-version.yml
release_date: '2021-04-18'
1.10.0:
changes:
bugfixes:
- digital_ocean_block_storage - fix block volumes detach idempotency (https://github.com/ansible-collections/community.digitalocean/issues/149).
- digital_ocean_droplet - ensure "active" state before issuing "power on" action
(https://github.com/ansible-collections/community.digitalocean/issues/150)
- digital_ocean_droplet - power on should poll/wait, resize should support "active"
state (https://github.com/ansible-collections/community.digitalocean/pull/143).
- digital_ocean_load_balancer - C(droplet_ids) are not required when C(state=absent)
is chosen (https://github.com/ansible-collections/community.digitalocean/pull/147).
- digital_ocean_load_balancer - when C(state=absent) is chosen the API returns
an empty response (https://github.com/ansible-collections/community.digitalocean/pull/147).
minor_changes:
- digital_ocean_kubernetes - adding the C(taints), C(auto_scale), C(min_nodes)
and C(max_nodes) parameters to the C(node_pools) definition (https://github.com/ansible-collections/community.digitalocean/issues/157).
fragments:
- 143-droplet-resize-wait-active.yaml
- 147-load-balancer-fixes.yaml
- 149-block-detach-not-idempotent.yaml
- 150-droplet-active-power-on.yaml
- 157-doks-auto-scale.yaml
modules:
- description: Create and delete DigitalOcean CDN Endpoints
name: digital_ocean_cdn_endpoints
namespace: ''
- description: Gather information about DigitalOcean CDN Endpoints
name: digital_ocean_cdn_endpoints_info
namespace: ''
- description: Manage DigitalOcean Load Balancers
name: digital_ocean_load_balancer
namespace: ''
- description: Create and delete DigitalOcean Monitoring alerts
name: digital_ocean_monitoring_alerts
namespace: ''
- description: Gather information about DigitalOcean Monitoring alerts
name: digital_ocean_monitoring_alerts_info
namespace: ''
release_date: '2021-09-11'
1.11.0:
changes:
bugfixes:
- Adding missing status badges for black and unit tests (https://github.com/ansible-collections/community.digitalocean/pull/164).
- Documentation URLs are fixed for the C(digital_ocean_domain_record) and C(digital_ocean_droplet_info)
modules (https://github.com/ansible-collections/community.digitalocean/pull/163).
- Serializing the cloud integration tests (https://github.com/ansible-collections/community.digitalocean/pull/165).
- digital_ocean_floating_ip - make floating ip return data idempotent (https://github.com/ansible-collections/community.digitalocean/pull/162).
- digitalocean inventory - enforce the C(timeout) parameter (https://github.com/ansible-collections/community.digitalocean/issues/168).
minor_changes:
- digitalocean inventory script - add support for Droplet tag filtering (https://github.com/ansible-collections/community.digitalocean/issues/7).
fragments:
- 162-floating-ip-data-idempotency.yaml
- 163-documentation-broken-links.yaml
- 164-add-badges.yaml
- 165-serialize-integration-tests.yml
- 168-inventory-timeout.yaml
- 7-inventory-script-droplet-tags.yaml
release_date: '2021-10-23'
1.12.0:
changes:
bugfixes:
- Update the tests so that they only run once (https://github.com/ansible-collections/community.digitalocean/issues/186).
- 'digital_ocean_droplet - fix resizing with C(state: active) does not actually
turn Droplet on (https://github.com/ansible-collections/community.digitalocean/issues/140).'
- digital_ocean_kubernetes - fix return value consistency (https://github.com/ansible-collections/community.digitalocean/issues/174).
minor_changes:
- digital_ocean_block_storage - adding Project support (https://github.com/ansible-collections/community.digitalocean/issues/171).
- digital_ocean_database - adding Project support (https://github.com/ansible-collections/community.digitalocean/issues/171).
- digital_ocean_domain - adding Project support (https://github.com/ansible-collections/community.digitalocean/issues/171).
- digital_ocean_droplet - adding Project support (https://github.com/ansible-collections/community.digitalocean/issues/171).
- digital_ocean_droplet - adding ability to apply and remove firewall by using
droplet module (https://github.com/ansible-collections/community.digitalocean/issues/159).
- digital_ocean_droplet - require unique_name for state=absent to avoid unintentional
droplet deletions.
- digital_ocean_firewall - inbound_rules and outbound_rules are no longer required
for firewall removal (https://github.com/ansible-collections/community.digitalocean/issues/181).
- digital_ocean_floating_ip - adding Project support (https://github.com/ansible-collections/community.digitalocean/issues/171).
- digital_ocean_floating_ip - adding attach and detach states to floating ip
module (https://github.com/ansible-collections/community.digitalocean/issues/170).
- digital_ocean_load_balancer - adding Project support (https://github.com/ansible-collections/community.digitalocean/issues/171).
- digitalocean integration tests - adding integration tests for CDN Endpoints
(https://github.com/ansible-collections/community.digitalocean/issues/179).
fragments:
- 140-resize-state-active.yaml
- 159-add-droplet-firewall.yaml
- 170-add-flip-states.yaml
- 171-add-project-support.yaml
- 174-return-value-fix.yml
- 179-cdn-integration-tests.yaml
- 181-firewall-destroy-fix.yaml
- 186-tests-running-twice.yaml
release_date: '2021-11-06'
1.13.0:
changes:
minor_changes:
- Set Python 3.9 as the C(python-version) and C(target-python-version) in the
integration, sanity, and unit tests for Ansible > 2.9 (3.8 otherwise).
- digital_ocean_droplet - allow the user to override the Droplet action and
status polling interval (https://github.com/ansible-collections/community.digitalocean/issues/194).
- digital_ocean_kubernetes - adding support for HA control plane (https://github.com/ansible-collections/community.digitalocean/issues/190).
fragments:
- 190-kubernetes-ha.yaml
- 194-droplet-sleep-variable.yaml
release_date: '2021-12-10'
1.14.0:
changes:
bugfixes:
- Update README.md with updated Droplet examples (https://github.com/ansible-collections/community.digitalocean/issues/199).
- digital_ocean_cdn_endpoints - defaulting optional string parameters as strings
(https://github.com/ansible-collections/community.digitalocean/issues/205).
- digital_ocean_cdn_endpoints - updating Spaces endpoint for the integration
test (https://github.com/ansible-collections/community.digitalocean/issues/205).
- digital_ocean_droplet - ensure that Droplet creation is successful (https://github.com/ansible-collections/community.digitalocean/issues/197).
- digital_ocean_droplet - fixing project assignment for the C(unique_name=False)
case (https://github.com/ansible-collections/community.digitalocean/issues/201).
- digital_ocean_droplet - update Droplet examples (https://github.com/ansible-collections/community.digitalocean/issues/199).
minor_changes:
- digital_ocean_kubernetes_info - switching C(changed=True) to C(changed=False)
since getting information is read-only in nature (https://github.com/ansible-collections/community.digitalocean/issues/204).
fragments:
- 199-update-droplet-docs-examples.yaml
- 201-droplet-unique-name.yaml
- 204-kubernetes-tags-and-info.yaml
- 205-cdn-endpoints.yaml
release_date: '2021-12-20'
1.15.0:
changes:
bugfixes:
- digital_ocean_droplet - move Droplet data under "droplet" key in returned
payload (https://github.com/ansible-collections/community.digitalocean/issues/211).
fragments:
- 211-droplet-data-key.yaml
modules:
- description: Create and remove DigitalOcean Spaces.
name: digital_ocean_spaces
namespace: ''
- description: List DigitalOcean Spaces.
name: digital_ocean_spaces_info
namespace: ''
release_date: '2022-01-19'
1.15.1:
changes:
bugfixes:
- digital_ocean_droplet - fix reporting of changed state when ``firewall`` argument
is present (https://github.com/ansible-collections/community.digitalocean/pull/219).
minor_changes:
- Updates DigitalOcean API documentation links to current domain with working
URL anchors (https://github.com/ansible-collections/community.digitalocean/issues/223).
fragments:
- 219-droplet-firewall-changed-reporting.yaml
- 223-fix-digitalocean-api-documentation-links.yaml
release_date: '2022-02-17'
1.16.0:
changes:
bugfixes:
- digital_ocean_kubernetes - add missing elements type to C(node_pools.tags)
and C(node_pools.taints) options (https://github.com/ansible-collections/community.digitalocean/issues/232).
minor_changes:
- black test - added a 15 minute timeout (https://github.com/ansible-collections/community.digitalocean/issues/228).
- digital_ocean_domain - add support for IPv6 apex domain records (https://github.com/ansible-collections/community.digitalocean/issues/226).
- integration tests - added a 120 minute timeout (https://github.com/ansible-collections/community.digitalocean/issues/228).
- sanity and unit tests - added a 30 minute timeout (https://github.com/ansible-collections/community.digitalocean/issues/228).
fragments:
- 226-ipv6-apex-domain-records.yml
- 228-integration-test-timeouts.yml
- 232-kubernetes-documentation-elements.yml
modules:
- description: Gather information about DigitalOcean domain records
name: digital_ocean_domain_record_info
namespace: ''
release_date: '2022-03-19'
1.17.0:
changes:
minor_changes:
- digital_ocean - parameterize the DigitalOcean API base url (https://github.com/ansible-collections/community.digitalocean/issues/237).
fragments:
- 237-parameterize-do-api-baseurl.yaml
release_date: '2022-04-28'
1.18.0:
changes:
minor_changes:
- ci - adding stable-2.13 to sanity and unit testing (https://github.com/ansible-collections/community.digitalocean/issues/239).
- digital_ocean_spaces - set C(no_log=True) for C(aws_access_key_id) parameter
(https://github.com/ansible-collections/community.digitalocean/issues/243).
- digital_ocean_spaces_info - set C(no_log=True) for C(aws_access_key_id) parameter
(https://github.com/ansible-collections/community.digitalocean/issues/243).
fragments:
- 239-ci-stable-2.13.yaml
- 243-no-log-spaces-access-key-id.yaml
release_date: '2022-05-03'
1.19.0:
changes:
bugfixes:
- digital_ocean_cdn_endpoints - remove non-API parameters before posting to
the API (https://github.com/ansible-collections/community.digitalocean/issues/252).
- digital_ocean_cdn_endpoints - use the correct module name in the C(EXAMPLES)
(https://github.com/ansible-collections/community.digitalocean/issues/251).
minor_changes:
- digital_ocean - reference C(DO_API_TOKEN) consistently in module documentation
and examples (https://github.com/ansible-collections/community.digitalocean/issues/248).
fragments:
- 248-oauth-token-consistency.yaml
- 251-cdn-endpoints-examples-wrong-module.yaml
- 252-cdn-endpoints-http-500.yaml
release_date: '2022-05-11'
1.2.0:
changes:
bugfixes:
- digital_ocean inventory script - fail cleaner on invalid ``HOST`` argument
to ``--host`` option (https://github.com/ansible-collections/community.digitalocean/pull/44).
- digital_ocean inventory script - implement unimplemented ``use_private_network``
option and register missing ``do_ip_address``, ``do_private_ip_address`` host
vars (https://github.com/ansible-collections/community.digitalocean/pull/45/files).
- digital_ocean inventory script - return JSON consistent with specification
with ``--host`` (https://github.com/ansible-collections/community.digitalocean/pull/44).
- digital_ocean_domain - return zone records when creating a new zone (https://github.com/ansible-collections/community.digitalocean/issues/46).
- digital_ocean_droplet - add missing ``required=True`` on ``do_oauth_token``
in ``argument_spec`` (https://github.com/ansible-collections/community.digitalocean/issues/13).
- digital_ocean_floating_ip - fixes idempotence (https://github.com/ansible-collections/community.digitalocean/issues/5).
minor_changes:
- digital_ocean - ``ssh_key_ids`` list entries are now validated to be strings
(https://github.com/ansible-collections/community.digitalocean/issues/13).
- digital_ocean_droplet - ``ssh_keys``, ``tags``, and ``volumes`` list entries
are now validated to be strings (https://github.com/ansible-collections/community.digitalocean/issues/13).
- digital_ocean_droplet - adding ``active`` and ``inactive`` states (https://github.com/ansible-collections/community.digitalocean/issues/23).
- digital_ocean_droplet - adds Droplet resize functionality (https://github.com/ansible-collections/community.digitalocean/issues/4).
fragments:
- 13-fix-sanity-tests.yaml
- 23-add-active-inactive-droplet.yaml
- 4-droplet-resize.yaml
- 44-fixes-inv-script-host-option.yaml
- 45-fix-use_private_network.yaml
- 46-fix-domain-create-return-records.yaml
- 5-fix-floating-ip-idempotence.yaml
modules:
- description: Display DigitalOcean customer balance
name: digital_ocean_balance_info
namespace: ''
release_date: '2021-05-02'
1.20.0:
changes:
bugfixes:
- digital_ocean_droplet - fix regression in droplet deletion where ``name``
and ``unique_name`` (set to true) are required and ``id`` alone is insufficient
(though ``id`` is sufficient to uniquely identify a droplet for deletion).
(https://github.com/ansible-collections/community.digitalocean/issues/260)
- digital_ocean_droplet - fix regression where droplet info (for example networking)
doesn't update when waiting during creation unless ``unique_name`` is set
to true (https://github.com/ansible-collections/community.digitalocean/issues/220).
minor_changes:
- digital_ocean_cdn_endpoints - update Spaces endpoint and add a few delays
to the integration test (https://github.com/ansible-collections/community.digitalocean/issues/267).
- digital_ocean_load_balancer - Allow creating a load balancer and associating
droplets by tag as an alternative to ``droplet_ids``.
fragments:
- 258-load-balancer-with-tag.yml
- 261-fix_deleting_by_id_only.yaml
- 265-get_updated_droplet_info_on_create_wait_by_id.yaml
- 267-update-cdn-endpoints.yaml
release_date: '2022-06-10'
1.21.0:
changes:
bugfixes:
- digital_ocean_droplet - if the JSON response lacks a key and the associated
variable is set to ``None``, then don't treat that variable like a ``dict``
and call ``get()`` on it without first testing it (https://github.com/ansible-collections/community.digitalocean/issues/272).
minor_changes:
- digital_ocean - add sanity test ignores for Ansible 2.12 and 2.13 (https://github.com/ansible-collections/community.digitalocean/issues/247).
fragments:
- 247-sanity-checks-2.12-13.yaml
- 273-Dont_call_get_on_None.yaml
release_date: '2022-06-29'
1.22.0:
changes:
minor_changes:
- collection - added an action group C(community.digitalocean.all) for use with
module defaults (https://docs.ansible.com/ansible/latest/user_guide/playbooks_module_defaults.html)
(https://github.com/ansible-collections/community.digitalocean/issues/281).
- digital_ocean_vpc - add C(vpc) key to returned VPC data on create (https://github.com/ansible-collections/community.digitalocean/issues/276).
- integration tests - perform integration testing on all modules for changes
in C(plugins/module_utils) or by changed module in C(plugins/modules) (https://github.com/ansible-collections/community.digitalocean/issues/286).
- integration tests - split the integration tests by module and run them serially
(https://github.com/ansible-collections/community.digitalocean/issues/280).
fragments:
- 276-vpc-inconsistent-data-return.yml
- 281-default-all-action-group.yml
- 286-refactor-pr-integration-testing.yaml
release_date: '2022-10-03'
1.23.0:
changes:
bugfixes:
- inventory plugin - bugfix for baseurl parameter (https://github.com/ansible-collections/community.digitalocean/pull/297).
- integration tests - add missing `environment` directive on pull request integration
testing (https://github.com/ansible-collections/community.digitalocean/issues/293).
minor_changes:
- digital_ocean_load_balancer - add support for C(size_unit) over deprecated
C(size); deprecate C(algorithm) completely (https://github.com/ansible-collections/community.digitalocean/issues/270).
- documentation - refresh the "Testing and Development" section of the C(README.md)
(https://github.com/ansible-collections/community.digitalocean/issues/268).
- integration tests - add a dedicated integration test for C(digital_ocean_database_info)
(https://github.com/ansible-collections/community.digitalocean/issues/289).
- integration tests - set pull request integration tests to run against branch
instead of last commit (https://github.com/ansible-collections/community.digitalocean/issues/291).
fragments:
- 268-update-dev-test-setup.yaml
- 270-load-balancer-size-unit.yaml
- 289-database-info-integration-test.yml
- 291-pr-integration-tests-branch.yaml
- 293-integration-test-pr-environment.yaml
release_date: '2022-12-29'
1.3.0:
modules:
- description: Create and delete a DigitalOcean database
name: digital_ocean_database
namespace: ''
- description: Gather information about DigitalOcean databases
name: digital_ocean_database_info
namespace: ''
- description: Create and delete a DigitalOcean Kubernetes cluster
name: digital_ocean_kubernetes
namespace: ''
- description: Returns information about an existing DigitalOcean Kubernetes cluster
name: digital_ocean_kubernetes_info
namespace: ''
release_date: '2021-05-07'
1.4.0:
changes:
bugfixes:
- digital_ocean_droplet_info - Fix documentation link for `digital_ocean_droplet_info`
(https://github.com/ansible-collections/community.digitalocean/pull/81).
- digitalocean - Fix return docs for digital_ocean_sshkey_info (https://github.com/ansible-collections/community.digitalocean/issues/56).
- digitalocean - Update README.md for K8s and databases (https://github.com/ansible-collections/community.digitalocean/pull/80).
fragments:
- 80-update-readme-k8s-dbs.yaml
- 81-fix-document-link-digital-ocean-droplet-info.yml
- 82-fix-sshkey-info.yaml
modules:
- description: Gather information about DigitalOcean Droplets
name: digital_ocean_droplet_info
namespace: ''
release_date: '2021-05-14'
1.4.1:
changes:
bugfixes:
- digital_ocean_droplet - Add integration tests for Droplet active and inactive
states (https://github.com/ansible-collections/community.digitalocean/issues/66).
- digital_ocean_droplet - Fix Droplet inactive state (https://github.com/ansible-collections/community.digitalocean/issues/83).
fragments:
- 83-droplet-inactive.yaml
release_date: '2021-05-15'
1.4.2:
changes:
bugfixes:
- digital_ocean_droplet - Fixed Droplet inactive state (https://github.com/ansible-collections/community.digitalocean/pull/88).
- digital_ocean_sshkey - Fixed SSH Key Traceback Issue (https://github.com/ansible-collections/community.digitalocean/issues/68).
fragments:
- 68-fix-sshkey-traceback.yaml
- 88-droplet-integration-tests.yaml
release_date: '2021-05-21'
1.5.0:
changes:
bugfixes:
- digital_ocean_database - Fixed DB attribute settings (https://github.com/ansible-collections/community.digitalocean/issues/94).
- digital_ocean_database_info - Cleanup unused attribs (https://github.com/ansible-collections/community.digitalocean/pulls/100).
- digital_ocean_snapshot_info - Fix lookup of snapshot_info by_id (https://github.com/ansible-collections/community.digitalocean/issues/92).
- digital_ocean_tag - Fix tag idempotency (https://github.com/ansible-collections/community.digitalocean/issues/61).
minor_changes:
- digitalocean - Filter droplets in dynamic inventory plugin using arbitrary.
jinja2 expressions (https://github.com/ansible-collections/community.digitalocean/pull/96).
- digitalocean - Support templates in API tokens when using the dynamic inventory
plugin (https://github.com/ansible-collections/community.digitalocean/pull/98).
fragments:
- 100-fix-database-info.yaml
- 61-fix-tag-idempotency.yaml
- 92-snapshot-info-fix-get-by-id.yaml
- 94-fix-db-attribs.yaml
- 96-filter-droplets-through-jinja.yml
- 98-api-token-command.yml
release_date: '2021-05-26'
1.5.1:
changes:
bugfixes:
- digitalocean inventory plugin - Wire up advertised caching functionality (https://github.com/ansible-collections/community.digitalocean/pull/97).
fragments:
- 97-wire-up-inventory-cache.yml
release_date: '2021-06-04'
1.6.0:
changes:
bugfixes:
- digital_ocean_certificate_info - ensure return type is a list (https://github.com/ansible-collections/community.digitalocean/issues/55).
- digital_ocean_domain_info - ensure return type is a list (https://github.com/ansible-collections/community.digitalocean/issues/55).
- digital_ocean_firewall_info - ensure return type is a list (https://github.com/ansible-collections/community.digitalocean/issues/55).
- digital_ocean_load_balancer_info - ensure return type is a list (https://github.com/ansible-collections/community.digitalocean/issues/55).
- digital_ocean_tag_info - ensure return type is a list (https://github.com/ansible-collections/community.digitalocean/issues/55).
- digitalocean inventory plugin - attributes available to filters are limited
to explicitly required attributes and are prefixed with ``var_prefix`` (https://github.com/ansible-collections/community.digitalocean/pull/102).
fragments:
- 102-filters-prefixed-vars.yml
- 55-fix-info-module-return-type.yaml
modules:
- description: Manage a DigitalOcean project
name: digital_ocean_project
namespace: ''
- description: Gather information about DigitalOcean Projects
name: digital_ocean_project_info
namespace: ''
release_date: '2021-06-10'
1.7.0:
changes:
bugfixes:
- digital_ocean_certificate - fixing integration test (https://github.com/ansible-collections/community.digitalocean/issues/114).
- digital_ocean_droplet - state `present` with `wait` was not waiting (https://github.com/ansible-collections/community.digitalocean/issues/116).
- digital_ocean_firewall - fixing integration test (https://github.com/ansible-collections/community.digitalocean/issues/114).
- digital_ocean_tag - fixing integration test (https://github.com/ansible-collections/community.digitalocean/issues/114).
- digitalocean - update README.md with project_info and project module (https://github.com/ansible-collections/community.digitalocean/pull/112).
minor_changes:
- digital_ocean_kubernetes - set "latest" as the default version for new clusters
(https://github.com/ansible-collections/community.digitalocean/issues/114).
fragments:
- 112-update-readme.yaml
- 114-fix-integration-tests.yaml
- 116-droplet-present-wait.yaml
modules:
- description: Create and delete DigitalOcean snapshots
name: digital_ocean_snapshot
namespace: ''
- description: Create and delete DigitalOcean VPCs
name: digital_ocean_vpc
namespace: ''
- description: Gather information about DigitalOcean VPCs
name: digital_ocean_vpc_info
namespace: ''
release_date: '2021-06-21'
1.8.0:
changes:
bugfixes:
- digital_ocean - integration tests need community.general and jmespath (https://github.com/ansible-collections/community.digitalocean/issues/121).
- digital_ocean_firewall - fixed idempotence (https://github.com/ansible-collections/community.digitalocean/issues/122).
minor_changes:
- digital_ocean_database - add support for MongoDB (https://github.com/ansible-collections/community.digitalocean/issues/124).
fragments:
- 121-integration-tests.yaml
- 122-firewall-idempotence.yaml
- 124-add-mongodb.yaml
release_date: '2021-07-05'
1.9.0:
changes:
bugfixes:
- digital_ocean_database - increase the database creation integration test timeout
(https://github.com/ansible-collections/community.digitalocean).
- digital_ocean_floating_ip - delete all Floating IPs initially during the integration
test run (https://github.com/ansible-collections/community.digitalocean/issues/129).
- digitalocean inventory - respect the TRANSFORM_INVALID_GROUP_CHARS configuration
setting (https://github.com/ansible-collections/community.digitalocean/pull/138).
- info modules - adding missing check mode support (https://github.com/ansible-collections/community.digitalocean/issues/139).
minor_changes:
- digital_ocean - running and enforcing psf/black in the codebase (https://github.com/ansible-collections/community.digitalocean/issues/136).
- digital_ocean_floating_ip_info - new integration test for the `digital_ocean_floating_ip_info`
module (https://github.com/ansible-collections/community.digitalocean/issues/130).
fragments:
- 131-floating-ip-tests.yaml
- 134-database-tests.yaml
- 134-psf-black.yaml
- 138-group-name-transformations.yaml
- 139-info-modules-check-mode.yaml
release_date: '2021-08-17'

View File

@@ -0,0 +1,29 @@
changelog_filename_template: ../CHANGELOG.rst
changelog_filename_version_depth: 0
changes_file: changelog.yaml
changes_format: combined
keep_fragments: false
mention_ancestor: true
new_plugins_after_name: removed_features
notesdir: fragments
prelude_section_name: release_summary
prelude_section_title: Release Summary
sections:
- - major_changes
- Major Changes
- - minor_changes
- Minor Changes
- - breaking_changes
- Breaking Changes / Porting Guide
- - deprecated_features
- Deprecated Features
- - removed_features
- Removed Features (previously deprecated)
- - security_fixes
- Security Fixes
- - bugfixes
- Bugfixes
- - known_issues
- Known Issues
title: Community DigitalOcean
trivial_section_name: trivial

View File

@@ -0,0 +1,10 @@
coverage:
status:
project:
default:
informational: true # Don't fail CI
# Don't count lines of code in these directories
ignore:
- tests
fixes:
- "ansible_collections/community/digitalocean/::"

View File

@@ -0,0 +1,120 @@
requires_ansible: '>=2.9.10'
action_groups:
all:
- digital_ocean
- digital_ocean_account_facts
- digital_ocean_account_info
- digital_ocean_balance_info
- digital_ocean_block_storage
- digital_ocean_cdn_endpoints_info
- digital_ocean_cdn_endpoints
- digital_ocean_certificate
- digital_ocean_certificate_facts
- digital_ocean_certificate_info
- digital_ocean_database_info
- digital_ocean_database
- digital_ocean_domain
- digital_ocean_domain_facts
- digital_ocean_domain_info
- digital_ocean_domain_record_info
- digital_ocean_domain_record
- digital_ocean_droplet
- digital_ocean_droplet_info
- digital_ocean_firewall
- digital_ocean_firewall_facts
- digital_ocean_firewall_info
- digital_ocean_floating_ip
- digital_ocean_floating_ip_facts
- digital_ocean_floating_ip_info
- digital_ocean_image_facts
- digital_ocean_image_info
- digital_ocean_kubernetes_info
- digital_ocean_kubernetes
- digital_ocean_load_balancer
- digital_ocean_load_balancer_facts
- digital_ocean_load_balancer_info
- digital_ocean_monitoring_alerts
- digital_ocean_monitoring_alerts_info
- digital_ocean_project_info
- digital_ocean_project
- digital_ocean_region_facts
- digital_ocean_region_info
- digital_ocean_size_facts
- digital_ocean_size_info
- digital_ocean_snapshot_facts
- digital_ocean_snapshot_info
- digital_ocean_snapshot
- digital_ocean_spaces_info
- digital_ocean_spaces
- digital_ocean_sshkey
- digital_ocean_sshkey_facts
- digital_ocean_sshkey_info
- digital_ocean_tag
- digital_ocean_tag_facts
- digital_ocean_tag_info
- digital_ocean_volume_facts
- digital_ocean_volume_info
- digital_ocean_vpc
- digital_ocean_vpc_info
plugin_routing:
modules:
digital_ocean:
deprecation:
removal_version: 2.0.0
warning_text: see plugin documentation for details
digital_ocean_account_facts:
deprecation:
removal_version: 2.0.0
warning_text: see plugin documentation for details
digital_ocean_certificate_facts:
deprecation:
removal_version: 2.0.0
warning_text: see plugin documentation for details
digital_ocean_domain_facts:
deprecation:
removal_version: 2.0.0
warning_text: see plugin documentation for details
digital_ocean_firewall_facts:
deprecation:
removal_version: 2.0.0
warning_text: see plugin documentation for details
digital_ocean_floating_ip_facts:
deprecation:
removal_version: 2.0.0
warning_text: see plugin documentation for details
digital_ocean_image_facts:
deprecation:
removal_version: 2.0.0
warning_text: see plugin documentation for details
digital_ocean_load_balancer_facts:
deprecation:
removal_version: 2.0.0
warning_text: see plugin documentation for details
digital_ocean_region_facts:
deprecation:
removal_version: 2.0.0
warning_text: see plugin documentation for details
digital_ocean_size_facts:
deprecation:
removal_version: 2.0.0
warning_text: see plugin documentation for details
digital_ocean_snapshot_facts:
deprecation:
removal_version: 2.0.0
warning_text: see plugin documentation for details
digital_ocean_sshkey_facts:
deprecation:
removal_version: 2.0.0
warning_text: see plugin documentation for details
digital_ocean_tag_facts:
deprecation:
removal_version: 2.0.0
warning_text: see plugin documentation for details
digital_ocean_volume_facts:
deprecation:
removal_version: 2.0.0
warning_text: see plugin documentation for details
docker_image_facts:
deprecation:
removal_version: 2.0.0
warning_text: see plugin documentation for details

View File

@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2018, Ansible Project
# Copyright: (c) 2018, Abhijeet Kasurde (akasurde@redhat.com)
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
class ModuleDocFragment(object):
# Parameters for DigitalOcean modules
DOCUMENTATION = r"""
options:
baseurl:
description:
- DigitalOcean API base url.
type: str
default: https://api.digitalocean.com/v2
oauth_token:
description:
- DigitalOcean OAuth token.
- "There are several other environment variables which can be used to provide this value."
- "i.e., - 'DO_API_TOKEN', 'DO_API_KEY', 'DO_OAUTH_TOKEN' and 'OAUTH_TOKEN'"
type: str
aliases: [ api_token ]
timeout:
description:
- The timeout in seconds used for polling DigitalOcean's API.
type: int
default: 30
validate_certs:
description:
- If set to C(no), the SSL certificates will not be validated.
- This should only set to C(no) used on personally controlled sites using self-signed certificates.
type: bool
default: yes
"""

View File

@@ -0,0 +1,274 @@
# -*- coding: utf-8 -*-
# Copyright: (c), Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r"""
name: digitalocean
author:
- Janos Gerzson (@grzs)
- Tadej Borovšak (@tadeboro)
- Max Truxa (@maxtruxa)
short_description: DigitalOcean Inventory Plugin
version_added: "1.1.0"
description:
- DigitalOcean (DO) inventory plugin.
- Acquires droplet list from DO API.
- Uses configuration file that ends with '(do_hosts|digitalocean|digital_ocean).(yaml|yml)'.
extends_documentation_fragment:
- community.digitalocean.digital_ocean.documentation
- constructed
- inventory_cache
options:
plugin:
description:
- The name of the DigitalOcean Inventory Plugin,
this should always be C(community.digitalocean.digitalocean).
required: true
choices: ['community.digitalocean.digitalocean']
api_token:
description:
- DigitalOcean OAuth token.
- Template expressions can be used in this field.
required: true
type: str
aliases: [ oauth_token ]
env:
- name: DO_API_TOKEN
attributes:
description: >-
Droplet attributes to add as host vars to each inventory host.
Check out the DO API docs for full list of attributes at
U(https://docs.digitalocean.com/reference/api/api-reference/#operation/list_all_droplets).
type: list
elements: str
default:
- id
- name
- networks
- region
- size_slug
var_prefix:
description:
- Prefix of generated varible names (e.g. C(tags) -> C(do_tags))
type: str
default: 'do_'
pagination:
description:
- Maximum droplet objects per response page.
- If the number of droplets related to the account exceeds this value,
the query will be broken to multiple requests (pages).
- DigitalOcean currently allows a maximum of 200.
type: int
default: 200
filters:
description:
- Filter hosts with Jinja templates.
- If no filters are specified, all hosts are added to the inventory.
type: list
elements: str
default: []
version_added: '1.5.0'
"""
EXAMPLES = r"""
# Using keyed groups and compose for hostvars
plugin: community.digitalocean.digitalocean
api_token: '{{ lookup("pipe", "./get-do-token.sh" }}'
attributes:
- id
- name
- memory
- vcpus
- disk
- size
- image
- networks
- volume_ids
- tags
- region
keyed_groups:
- key: do_region.slug
prefix: 'region'
separator: '_'
- key: do_tags | lower
prefix: ''
separator: ''
compose:
ansible_host: do_networks.v4 | selectattr('type','eq','public')
| map(attribute='ip_address') | first
class: do_size.description | lower
distro: do_image.distribution | lower
filters:
- '"kubernetes" in do_tags'
- 'do_region.slug == "fra1"'
"""
import re
import json
from ansible.errors import AnsibleError, AnsibleParserError
from ansible.inventory.group import to_safe_group_name
from ansible.module_utils._text import to_native
from ansible.module_utils.urls import Request
from ansible.module_utils.six.moves.urllib.error import URLError, HTTPError
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable
class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
NAME = "community.digitalocean.digitalocean"
# Constructable methods use the following function to construct group names. By
# default, characters that are not valid in python variables, are always replaced by
# underscores. We are overriding this with a function that respects the
# TRANSFORM_INVALID_GROUP_CHARS configuration option and allows users to control the
# behavior.
_sanitize_group_name = staticmethod(to_safe_group_name)
def verify_file(self, path):
valid = False
if super(InventoryModule, self).verify_file(path):
if path.endswith(
(
"do_hosts.yaml",
"do_hosts.yml",
"digitalocean.yaml",
"digitalocean.yml",
"digital_ocean.yaml",
"digital_ocean.yml",
)
):
valid = True
else:
self.display.vvv(
"Skipping due to inventory source file name mismatch. "
"The file name has to end with one of the following: "
"do_hosts.yaml, do_hosts.yml "
"digitalocean.yaml, digitalocean.yml, "
"digital_ocean.yaml, digital_ocean.yml."
)
return valid
def _template_option(self, option):
value = self.get_option(option)
self.templar.available_variables = {}
return self.templar.template(value)
def _get_payload(self):
# request parameters
api_token = self._template_option("api_token")
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer {0}".format(api_token),
}
# build url
pagination = self.get_option("pagination")
url = "https://api.digitalocean.com/v2"
if self.get_option("baseurl"):
url = self.get_option("baseurl")
url += "/droplets?per_page=" + str(pagination)
# send request(s)
self.req = Request(headers=headers, timeout=self.get_option("timeout"))
payload = []
try:
while url:
self.display.vvv("Sending request to {0}".format(url))
response = json.load(self.req.get(url))
payload.extend(response["droplets"])
url = response.get("links", {}).get("pages", {}).get("next")
except ValueError:
raise AnsibleParserError("something went wrong with JSON loading")
except (URLError, HTTPError) as error:
raise AnsibleParserError(error)
return payload
def _populate(self, records):
attributes = self.get_option("attributes")
var_prefix = self.get_option("var_prefix")
strict = self.get_option("strict")
host_filters = self.get_option("filters")
for record in records:
host_name = record.get("name")
if not host_name:
continue
host_vars = {}
for k, v in record.items():
if k in attributes:
host_vars[var_prefix + k] = v
if not self._passes_filters(host_filters, host_vars, host_name, strict):
self.display.vvv("Host {0} did not pass all filters".format(host_name))
continue
# add host to inventory
self.inventory.add_host(host_name)
# set variables for host
for k, v in host_vars.items():
self.inventory.set_variable(host_name, k, v)
self._set_composite_vars(
self.get_option("compose"),
self.inventory.get_host(host_name).get_vars(),
host_name,
strict,
)
# set composed and keyed groups
self._add_host_to_composed_groups(
self.get_option("groups"), dict(), host_name, strict
)
self._add_host_to_keyed_groups(
self.get_option("keyed_groups"), dict(), host_name, strict
)
def _passes_filters(self, filters, variables, host, strict=False):
if filters and isinstance(filters, list):
for template in filters:
try:
if not self._compose(template, variables):
return False
except Exception as e:
if strict:
raise AnsibleError(
"Could not evaluate host filter {0} for host {1}: {2}".format(
template, host, to_native(e)
)
)
# Better be safe and not include any hosts by accident.
return False
return True
def parse(self, inventory, loader, path, cache=True):
super(InventoryModule, self).parse(inventory, loader, path)
self._read_config_data(path)
# cache settings
cache_key = self.get_cache_key(path)
use_cache = self.get_option("cache") and cache
update_cache = self.get_option("cache") and not cache
records = None
if use_cache:
try:
records = self._cache[cache_key]
except KeyError:
update_cache = True
if records is None:
records = self._get_payload()
if update_cache:
self._cache[cache_key] = records
self._populate(records)

View File

@@ -0,0 +1,305 @@
# This code is part of Ansible, but is an independent component.
# This particular file snippet, and this file snippet only, is BSD licensed.
# Modules you write using this snippet, which is embedded dynamically by Ansible
# still belong to the author of the module, and may assign their own license
# to the complete work.
#
# Copyright (c), Ansible Project 2017
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import json
import os
from ansible.module_utils.urls import fetch_url
from ansible.module_utils._text import to_text
from ansible.module_utils.basic import env_fallback
class Response(object):
def __init__(self, resp, info):
self.body = None
if resp:
self.body = resp.read()
self.info = info
@property
def json(self):
if not self.body:
if "body" in self.info:
return json.loads(to_text(self.info["body"]))
return None
try:
return json.loads(to_text(self.body))
except ValueError:
return None
@property
def status_code(self):
return self.info["status"]
class DigitalOceanHelper:
baseurl = "https://api.digitalocean.com/v2"
def __init__(self, module):
self.module = module
self.baseurl = module.params.get("baseurl", DigitalOceanHelper.baseurl)
self.timeout = module.params.get("timeout", 30)
self.oauth_token = module.params.get("oauth_token")
self.headers = {
"Authorization": "Bearer {0}".format(self.oauth_token),
"Content-type": "application/json",
}
# Check if api_token is valid or not
response = self.get("account")
if response.status_code == 401:
self.module.fail_json(
msg="Failed to login using API token, please verify validity of API token."
)
def _url_builder(self, path):
if path[0] == "/":
path = path[1:]
return "%s/%s" % (self.baseurl, path)
def send(self, method, path, data=None):
url = self._url_builder(path)
data = self.module.jsonify(data)
if method == "DELETE":
if data == "null":
data = None
resp, info = fetch_url(
self.module,
url,
data=data,
headers=self.headers,
method=method,
timeout=self.timeout,
)
return Response(resp, info)
def get(self, path, data=None):
return self.send("GET", path, data)
def put(self, path, data=None):
return self.send("PUT", path, data)
def post(self, path, data=None):
return self.send("POST", path, data)
def delete(self, path, data=None):
return self.send("DELETE", path, data)
@staticmethod
def digital_ocean_argument_spec():
return dict(
baseurl=dict(
type="str", required=False, default="https://api.digitalocean.com/v2"
),
validate_certs=dict(type="bool", required=False, default=True),
oauth_token=dict(
no_log=True,
# Support environment variable for DigitalOcean OAuth Token
fallback=(
env_fallback,
["DO_API_TOKEN", "DO_API_KEY", "DO_OAUTH_TOKEN", "OAUTH_TOKEN"],
),
required=False,
aliases=["api_token"],
),
timeout=dict(type="int", default=30),
)
def get_paginated_data(
self,
base_url=None,
data_key_name=None,
data_per_page=40,
expected_status_code=200,
):
"""
Function to get all paginated data from given URL
Args:
base_url: Base URL to get data from
data_key_name: Name of data key value
data_per_page: Number results per page (Default: 40)
expected_status_code: Expected returned code from DigitalOcean (Default: 200)
Returns: List of data
"""
page = 1
has_next = True
ret_data = []
status_code = None
response = None
while has_next or status_code != expected_status_code:
required_url = "{0}page={1}&per_page={2}".format(
base_url, page, data_per_page
)
response = self.get(required_url)
status_code = response.status_code
# stop if any error during pagination
if status_code != expected_status_code:
break
page += 1
ret_data.extend(response.json[data_key_name])
try:
has_next = (
"pages" in response.json["links"]
and "next" in response.json["links"]["pages"]
)
except KeyError:
# There's a bug in the API docs: GET v2/cdn/endpoints doesn't return a "links" key
has_next = False
if status_code != expected_status_code:
msg = "Failed to fetch %s from %s" % (data_key_name, base_url)
if response:
msg += " due to error : %s" % response.json["message"]
self.module.fail_json(msg=msg)
return ret_data
class DigitalOceanProjects:
def __init__(self, module, rest):
self.module = module
self.rest = rest
self.get_all_projects()
def get_all_projects(self):
"""Fetches all projects."""
self.projects = self.rest.get_paginated_data(
base_url="projects?", data_key_name="projects"
)
def get_default(self):
"""Fetches the default project.
Returns:
error_message -- project fetch error message (or "" if no error)
project -- project dictionary representation (or {} if error)
"""
project = [
project for project in self.projects if project.get("is_default", False)
]
if len(project) == 0:
return "Unexpected error; no default project found", {}
if len(project) > 1:
return "Unexpected error; more than one default project", {}
return "", project[0]
def get_by_id(self, id):
"""Fetches the project with the given id.
Returns:
error_message -- project fetch error message (or "" if no error)
project -- project dictionary representation (or {} if error)
"""
project = [project for project in self.projects if project.get("id") == id]
if len(project) == 0:
return "No project with id {0} found".format(id), {}
elif len(project) > 1:
return "Unexpected error; more than one project with the same id", {}
return "", project[0]
def get_by_name(self, name):
"""Fetches the project with the given name.
Returns:
error_message -- project fetch error message (or "" if no error)
project -- project dictionary representation (or {} if error)
"""
project = [project for project in self.projects if project.get("name") == name]
if len(project) == 0:
return "No project with name {0} found".format(name), {}
elif len(project) > 1:
return "Unexpected error; more than one project with the same name", {}
return "", project[0]
def assign_to_project(self, project_name, urn):
"""Assign resource (urn) to project (name).
Keyword arguments:
project_name -- project name to associate the resource with
urn -- resource URN (has the form do:resource_type:resource_id)
Returns:
assign_status -- ok, not_found, assigned, already_assigned, service_down
error_message -- assignment error message (empty on success)
resources -- resources assigned (or {} if error)
Notes:
For URN examples, see https://docs.digitalocean.com/reference/api/api-reference/#tag/Project-Resources
Projects resources are identified by uniform resource names or URNs.
A valid URN has the following format: do:resource_type:resource_id.
The following resource types are supported:
Resource Type | Example URN
Database | do:dbaas:83c7a55f-0d84-4760-9245-aba076ec2fb2
Domain | do:domain:example.com
Droplet | do:droplet:4126873
Floating IP | do:floatingip:192.168.99.100
Load Balancer | do:loadbalancer:39052d89-8dd4-4d49-8d5a-3c3b6b365b5b
Space | do:space:my-website-assets
Volume | do:volume:6fc4c277-ea5c-448a-93cd-dd496cfef71f
"""
error_message, project = self.get_by_name(project_name)
if not project:
return "", error_message, {}
project_id = project.get("id", None)
if not project_id:
return (
"",
"Unexpected error; cannot find project id for {0}".format(project_name),
{},
)
data = {"resources": [urn]}
response = self.rest.post(
"projects/{0}/resources".format(project_id), data=data
)
status_code = response.status_code
json = response.json
if status_code != 200:
message = json.get("message", "No error message returned")
return (
"",
"Unable to assign resource {0} to project {1} [HTTP {2}: {3}]".format(
urn, project_name, status_code, message
),
{},
)
resources = json.get("resources", [])
if len(resources) == 0:
return (
"",
"Unexpected error; no resources returned (but assignment was successful)",
{},
)
if len(resources) > 1:
return (
"",
"Unexpected error; more than one resource returned (but assignment was successful)",
{},
)
status = resources[0].get(
"status",
"Unexpected error; no status returned (but assignment was successful)",
)
return (
status,
"Assigned {0} to project {1}".format(urn, project_name),
resources[0],
)

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

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