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}&region={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}&region={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