1#!/usr/local/bin/python3.8 2# -*- coding: utf-8 -*- 3 4# Copyright: Ansible Project 5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 7from __future__ import absolute_import, division, print_function 8 9__metaclass__ = type 10 11 12DOCUMENTATION = r""" 13--- 14module: digital_ocean_block_storage 15short_description: Create/destroy or attach/detach Block Storage volumes in DigitalOcean 16description: 17 - Create/destroy Block Storage volume in DigitalOcean, or attach/detach Block Storage volume to a droplet. 18options: 19 command: 20 description: 21 - Which operation do you want to perform. 22 choices: ['create', 'attach'] 23 required: true 24 type: str 25 state: 26 description: 27 - Indicate desired state of the target. 28 choices: ['present', 'absent'] 29 required: true 30 type: str 31 block_size: 32 description: 33 - The size of the Block Storage volume in gigabytes. 34 - Required when I(command=create) and I(state=present). 35 - If snapshot_id is included, this will be ignored. 36 - If block_size > current size of the volume, the volume is resized. 37 type: int 38 volume_name: 39 description: 40 - The name of the Block Storage volume. 41 type: str 42 required: true 43 description: 44 description: 45 - Description of the Block Storage volume. 46 type: str 47 region: 48 description: 49 - The slug of the region where your Block Storage volume should be located in. 50 - If I(snapshot_id) is included, this will be ignored. 51 type: str 52 snapshot_id: 53 description: 54 - The snapshot id you would like the Block Storage volume created with. 55 - If included, I(region) and I(block_size) will be ignored and changed to C(null). 56 type: str 57 droplet_id: 58 description: 59 - The droplet id you want to operate on. 60 - Required when I(command=attach). 61 type: int 62extends_documentation_fragment: 63- community.digitalocean.digital_ocean.documentation 64 65notes: 66 - Two environment variables can be used, DO_API_KEY and DO_API_TOKEN. 67 They both refer to the v2 token. 68 - If snapshot_id is used, region and block_size will be ignored and changed to null. 69 70author: 71 - "Harnek Sidhu (@harneksidhu)" 72""" 73 74EXAMPLES = r""" 75- name: Create new Block Storage 76 community.digitalocean.digital_ocean_block_storage: 77 state: present 78 command: create 79 api_token: <TOKEN> 80 region: nyc1 81 block_size: 10 82 volume_name: nyc1-block-storage 83 84- name: Resize an existing Block Storage 85 community.digitalocean.digital_ocean_block_storage: 86 state: present 87 command: create 88 api_token: <TOKEN> 89 region: nyc1 90 block_size: 20 91 volume_name: nyc1-block-storage 92 93- name: Delete Block Storage 94 community.digitalocean.digital_ocean_block_storage: 95 state: absent 96 command: create 97 api_token: <TOKEN> 98 region: nyc1 99 volume_name: nyc1-block-storage 100 101- name: Attach Block Storage to a Droplet 102 community.digitalocean.digital_ocean_block_storage: 103 state: present 104 command: attach 105 api_token: <TOKEN> 106 volume_name: nyc1-block-storage 107 region: nyc1 108 droplet_id: <ID> 109 110- name: Detach Block Storage from a Droplet 111 community.digitalocean.digital_ocean_block_storage: 112 state: absent 113 command: attach 114 api_token: <TOKEN> 115 volume_name: nyc1-block-storage 116 region: nyc1 117 droplet_id: <ID> 118""" 119 120RETURN = r""" 121id: 122 description: Unique identifier of a Block Storage volume returned during creation. 123 returned: changed 124 type: str 125 sample: "69b25d9a-494c-12e6-a5af-001f53126b44" 126""" 127 128import time 129import traceback 130 131from ansible.module_utils.basic import AnsibleModule 132from ansible_collections.community.digitalocean.plugins.module_utils.digital_ocean import ( 133 DigitalOceanHelper, 134) 135 136 137class DOBlockStorageException(Exception): 138 pass 139 140 141class DOBlockStorage(object): 142 def __init__(self, module): 143 self.module = module 144 self.rest = DigitalOceanHelper(module) 145 146 def get_key_or_fail(self, k): 147 v = self.module.params[k] 148 if v is None: 149 self.module.fail_json(msg="Unable to load %s" % k) 150 return v 151 152 def poll_action_for_complete_status(self, action_id): 153 url = "actions/{0}".format(action_id) 154 end_time = time.time() + self.module.params["timeout"] 155 while time.time() < end_time: 156 time.sleep(2) 157 response = self.rest.get(url) 158 status = response.status_code 159 json = response.json 160 if status == 200: 161 if json["action"]["status"] == "completed": 162 return True 163 elif json["action"]["status"] == "errored": 164 raise DOBlockStorageException(json["message"]) 165 raise DOBlockStorageException("Unable to reach api.digitalocean.com") 166 167 def get_block_storage_by_name(self, volume_name, region): 168 url = "volumes?name={0}®ion={1}".format(volume_name, region) 169 resp = self.rest.get(url) 170 if resp.status_code != 200: 171 raise DOBlockStorageException(resp.json["message"]) 172 173 volumes = resp.json["volumes"] 174 if not volumes: 175 return None 176 177 return volumes[0] 178 179 def get_attached_droplet_ID(self, volume_name, region): 180 volume = self.get_block_storage_by_name(volume_name, region) 181 if not volume or not volume["droplet_ids"]: 182 return None 183 184 return volume["droplet_ids"][0] 185 186 def attach_detach_block_storage(self, method, volume_name, region, droplet_id): 187 data = { 188 "type": method, 189 "volume_name": volume_name, 190 "region": region, 191 "droplet_id": droplet_id, 192 } 193 response = self.rest.post("volumes/actions", data=data) 194 status = response.status_code 195 json = response.json 196 if status == 202: 197 return self.poll_action_for_complete_status(json["action"]["id"]) 198 elif status == 200: 199 return True 200 elif status == 404 and method == "detach": 201 return False # Already detached 202 elif status == 422: 203 return False 204 else: 205 raise DOBlockStorageException(json["message"]) 206 207 def resize_block_storage(self, volume_name, region, desired_size): 208 if not desired_size: 209 return False 210 211 volume = self.get_block_storage_by_name(volume_name, region) 212 if volume["size_gigabytes"] == desired_size: 213 return False 214 215 data = { 216 "type": "resize", 217 "size_gigabytes": desired_size, 218 } 219 resp = self.rest.post( 220 "volumes/{0}/actions".format(volume["id"]), 221 data=data, 222 ) 223 if resp.status_code == 202: 224 return self.poll_action_for_complete_status(resp.json["action"]["id"]) 225 else: 226 # we'd get status 422 if desired_size <= current volume size 227 raise DOBlockStorageException(resp.json["message"]) 228 229 def create_block_storage(self): 230 volume_name = self.get_key_or_fail("volume_name") 231 snapshot_id = self.module.params["snapshot_id"] 232 if snapshot_id: 233 self.module.params["block_size"] = None 234 self.module.params["region"] = None 235 block_size = None 236 region = None 237 else: 238 block_size = self.get_key_or_fail("block_size") 239 region = self.get_key_or_fail("region") 240 description = self.module.params["description"] 241 data = { 242 "size_gigabytes": block_size, 243 "name": volume_name, 244 "description": description, 245 "region": region, 246 "snapshot_id": snapshot_id, 247 } 248 response = self.rest.post("volumes", data=data) 249 status = response.status_code 250 json = response.json 251 if status == 201: 252 self.module.exit_json(changed=True, id=json["volume"]["id"]) 253 elif status == 409 and json["id"] == "conflict": 254 # The volume exists already, but it might not have the desired size 255 resized = self.resize_block_storage(volume_name, region, block_size) 256 self.module.exit_json(changed=resized) 257 else: 258 raise DOBlockStorageException(json["message"]) 259 260 def delete_block_storage(self): 261 volume_name = self.get_key_or_fail("volume_name") 262 region = self.get_key_or_fail("region") 263 url = "volumes?name={0}®ion={1}".format(volume_name, region) 264 attached_droplet_id = self.get_attached_droplet_ID(volume_name, region) 265 if attached_droplet_id is not None: 266 self.attach_detach_block_storage( 267 "detach", volume_name, region, attached_droplet_id 268 ) 269 response = self.rest.delete(url) 270 status = response.status_code 271 json = response.json 272 if status == 204: 273 self.module.exit_json(changed=True) 274 elif status == 404: 275 self.module.exit_json(changed=False) 276 else: 277 raise DOBlockStorageException(json["message"]) 278 279 def attach_block_storage(self): 280 volume_name = self.get_key_or_fail("volume_name") 281 region = self.get_key_or_fail("region") 282 droplet_id = self.get_key_or_fail("droplet_id") 283 attached_droplet_id = self.get_attached_droplet_ID(volume_name, region) 284 if attached_droplet_id is not None: 285 if attached_droplet_id == droplet_id: 286 self.module.exit_json(changed=False) 287 else: 288 self.attach_detach_block_storage( 289 "detach", volume_name, region, attached_droplet_id 290 ) 291 changed_status = self.attach_detach_block_storage( 292 "attach", volume_name, region, droplet_id 293 ) 294 self.module.exit_json(changed=changed_status) 295 296 def detach_block_storage(self): 297 volume_name = self.get_key_or_fail("volume_name") 298 region = self.get_key_or_fail("region") 299 droplet_id = self.get_key_or_fail("droplet_id") 300 changed_status = self.attach_detach_block_storage( 301 "detach", volume_name, region, droplet_id 302 ) 303 self.module.exit_json(changed=changed_status) 304 305 306def handle_request(module): 307 block_storage = DOBlockStorage(module) 308 command = module.params["command"] 309 state = module.params["state"] 310 if command == "create": 311 if state == "present": 312 block_storage.create_block_storage() 313 elif state == "absent": 314 block_storage.delete_block_storage() 315 elif command == "attach": 316 if state == "present": 317 block_storage.attach_block_storage() 318 elif state == "absent": 319 block_storage.detach_block_storage() 320 321 322def main(): 323 argument_spec = DigitalOceanHelper.digital_ocean_argument_spec() 324 argument_spec.update( 325 state=dict(choices=["present", "absent"], required=True), 326 command=dict(choices=["create", "attach"], required=True), 327 block_size=dict(type="int", required=False), 328 volume_name=dict(type="str", required=True), 329 description=dict(type="str"), 330 region=dict(type="str", required=False), 331 snapshot_id=dict(type="str", required=False), 332 droplet_id=dict(type="int"), 333 ) 334 335 module = AnsibleModule(argument_spec=argument_spec) 336 337 try: 338 handle_request(module) 339 except DOBlockStorageException as e: 340 module.fail_json(msg=str(e), exception=traceback.format_exc()) 341 except KeyError as e: 342 module.fail_json(msg="Unable to load %s" % e, exception=traceback.format_exc()) 343 344 345if __name__ == "__main__": 346 main() 347