1#!/usr/bin/python 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__metaclass__ = type 9 10 11ANSIBLE_METADATA = {'metadata_version': '1.1', 12 'status': ['preview'], 13 'supported_by': 'community'} 14 15 16DOCUMENTATION = ''' 17--- 18module: digital_ocean_block_storage 19short_description: Create/destroy or attach/detach Block Storage volumes in DigitalOcean 20description: 21 - Create/destroy Block Storage volume in DigitalOcean, or attach/detach Block Storage volume to a droplet. 22version_added: "2.2" 23options: 24 command: 25 description: 26 - Which operation do you want to perform. 27 choices: ['create', 'attach'] 28 required: true 29 state: 30 description: 31 - Indicate desired state of the target. 32 choices: ['present', 'absent'] 33 required: true 34 block_size: 35 description: 36 - The size of the Block Storage volume in gigabytes. Required when command=create and state=present. If snapshot_id is included, this will be ignored. 37 volume_name: 38 description: 39 - The name of the Block Storage volume. 40 required: true 41 description: 42 description: 43 - Description of the Block Storage volume. 44 region: 45 description: 46 - The slug of the region where your Block Storage volume should be located in. If snapshot_id is included, this will be ignored. 47 required: true 48 snapshot_id: 49 version_added: "2.5" 50 description: 51 - The snapshot id you would like the Block Storage volume created with. If included, region and block_size will be ignored and changed to null. 52 droplet_id: 53 description: 54 - The droplet id you want to operate on. Required when command=attach. 55extends_documentation_fragment: digital_ocean.documentation 56notes: 57 - Two environment variables can be used, DO_API_KEY and DO_API_TOKEN. 58 They both refer to the v2 token. 59 - If snapshot_id is used, region and block_size will be ignored and changed to null. 60 61author: 62 - "Harnek Sidhu (@harneksidhu)" 63''' 64 65EXAMPLES = ''' 66# Create new Block Storage 67- digital_ocean_block_storage: 68 state: present 69 command: create 70 api_token: <TOKEN> 71 region: nyc1 72 block_size: 10 73 volume_name: nyc1-block-storage 74# Delete Block Storage 75- digital_ocean_block_storage: 76 state: absent 77 command: create 78 api_token: <TOKEN> 79 region: nyc1 80 volume_name: nyc1-block-storage 81# Attach Block Storage to a Droplet 82- digital_ocean_block_storage: 83 state: present 84 command: attach 85 api_token: <TOKEN> 86 volume_name: nyc1-block-storage 87 region: nyc1 88 droplet_id: <ID> 89# Detach Block Storage from a Droplet 90- digital_ocean_block_storage: 91 state: absent 92 command: attach 93 api_token: <TOKEN> 94 volume_name: nyc1-block-storage 95 region: nyc1 96 droplet_id: <ID> 97''' 98 99RETURN = ''' 100id: 101 description: Unique identifier of a Block Storage volume returned during creation. 102 returned: changed 103 type: str 104 sample: "69b25d9a-494c-12e6-a5af-001f53126b44" 105''' 106 107import time 108import traceback 109 110from ansible.module_utils.basic import AnsibleModule 111from ansible.module_utils.digital_ocean import DigitalOceanHelper 112 113 114class DOBlockStorageException(Exception): 115 pass 116 117 118class DOBlockStorage(object): 119 def __init__(self, module): 120 self.module = module 121 self.rest = DigitalOceanHelper(module) 122 123 def get_key_or_fail(self, k): 124 v = self.module.params[k] 125 if v is None: 126 self.module.fail_json(msg='Unable to load %s' % k) 127 return v 128 129 def poll_action_for_complete_status(self, action_id): 130 url = 'actions/{0}'.format(action_id) 131 end_time = time.time() + self.module.params['timeout'] 132 while time.time() < end_time: 133 time.sleep(2) 134 response = self.rest.get(url) 135 status = response.status_code 136 json = response.json 137 if status == 200: 138 if json['action']['status'] == 'completed': 139 return True 140 elif json['action']['status'] == 'errored': 141 raise DOBlockStorageException(json['message']) 142 raise DOBlockStorageException('Unable to reach api.digitalocean.com') 143 144 def get_attached_droplet_ID(self, volume_name, region): 145 url = 'volumes?name={0}®ion={1}'.format(volume_name, region) 146 response = self.rest.get(url) 147 status = response.status_code 148 json = response.json 149 if status == 200: 150 volumes = json['volumes'] 151 if len(volumes) > 0: 152 droplet_ids = volumes[0]['droplet_ids'] 153 if len(droplet_ids) > 0: 154 return droplet_ids[0] 155 return None 156 else: 157 raise DOBlockStorageException(json['message']) 158 159 def attach_detach_block_storage(self, method, volume_name, region, droplet_id): 160 data = { 161 'type': method, 162 'volume_name': volume_name, 163 'region': region, 164 'droplet_id': droplet_id 165 } 166 response = self.rest.post('volumes/actions', data=data) 167 status = response.status_code 168 json = response.json 169 if status == 202: 170 return self.poll_action_for_complete_status(json['action']['id']) 171 elif status == 200: 172 return True 173 elif status == 422: 174 return False 175 else: 176 raise DOBlockStorageException(json['message']) 177 178 def create_block_storage(self): 179 volume_name = self.get_key_or_fail('volume_name') 180 snapshot_id = self.module.params['snapshot_id'] 181 if snapshot_id: 182 self.module.params['block_size'] = None 183 self.module.params['region'] = None 184 block_size = None 185 region = None 186 else: 187 block_size = self.get_key_or_fail('block_size') 188 region = self.get_key_or_fail('region') 189 description = self.module.params['description'] 190 data = { 191 'size_gigabytes': block_size, 192 'name': volume_name, 193 'description': description, 194 'region': region, 195 'snapshot_id': snapshot_id, 196 } 197 response = self.rest.post("volumes", data=data) 198 status = response.status_code 199 json = response.json 200 if status == 201: 201 self.module.exit_json(changed=True, id=json['volume']['id']) 202 elif status == 409 and json['id'] == 'conflict': 203 self.module.exit_json(changed=False) 204 else: 205 raise DOBlockStorageException(json['message']) 206 207 def delete_block_storage(self): 208 volume_name = self.get_key_or_fail('volume_name') 209 region = self.get_key_or_fail('region') 210 url = 'volumes?name={0}®ion={1}'.format(volume_name, region) 211 attached_droplet_id = self.get_attached_droplet_ID(volume_name, region) 212 if attached_droplet_id is not None: 213 self.attach_detach_block_storage('detach', volume_name, region, attached_droplet_id) 214 response = self.rest.delete(url) 215 status = response.status_code 216 json = response.json 217 if status == 204: 218 self.module.exit_json(changed=True) 219 elif status == 404: 220 self.module.exit_json(changed=False) 221 else: 222 raise DOBlockStorageException(json['message']) 223 224 def attach_block_storage(self): 225 volume_name = self.get_key_or_fail('volume_name') 226 region = self.get_key_or_fail('region') 227 droplet_id = self.get_key_or_fail('droplet_id') 228 attached_droplet_id = self.get_attached_droplet_ID(volume_name, region) 229 if attached_droplet_id is not None: 230 if attached_droplet_id == droplet_id: 231 self.module.exit_json(changed=False) 232 else: 233 self.attach_detach_block_storage('detach', volume_name, region, attached_droplet_id) 234 changed_status = self.attach_detach_block_storage('attach', volume_name, region, droplet_id) 235 self.module.exit_json(changed=changed_status) 236 237 def detach_block_storage(self): 238 volume_name = self.get_key_or_fail('volume_name') 239 region = self.get_key_or_fail('region') 240 droplet_id = self.get_key_or_fail('droplet_id') 241 changed_status = self.attach_detach_block_storage('detach', volume_name, region, droplet_id) 242 self.module.exit_json(changed=changed_status) 243 244 245def handle_request(module): 246 block_storage = DOBlockStorage(module) 247 command = module.params['command'] 248 state = module.params['state'] 249 if command == 'create': 250 if state == 'present': 251 block_storage.create_block_storage() 252 elif state == 'absent': 253 block_storage.delete_block_storage() 254 elif command == 'attach': 255 if state == 'present': 256 block_storage.attach_block_storage() 257 elif state == 'absent': 258 block_storage.detach_block_storage() 259 260 261def main(): 262 argument_spec = DigitalOceanHelper.digital_ocean_argument_spec() 263 argument_spec.update( 264 state=dict(choices=['present', 'absent'], required=True), 265 command=dict(choices=['create', 'attach'], required=True), 266 block_size=dict(type='int', required=False), 267 volume_name=dict(type='str', required=True), 268 description=dict(type='str'), 269 region=dict(type='str', required=False), 270 snapshot_id=dict(type='str', required=False), 271 droplet_id=dict(type='int') 272 ) 273 274 module = AnsibleModule(argument_spec=argument_spec) 275 276 try: 277 handle_request(module) 278 except DOBlockStorageException as e: 279 module.fail_json(msg=e.message, exception=traceback.format_exc()) 280 except KeyError as e: 281 module.fail_json(msg='Unable to load %s' % e.message, exception=traceback.format_exc()) 282 283 284if __name__ == '__main__': 285 main() 286