Repository URL to install this package:
|
Version:
6.0.0 ▾
|
#!/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}®ion={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}®ion={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()