1#!/usr/local/bin/python3.8 2# -*- coding: utf-8 -*- 3 4# Copyright: (c) 2019, Nurfet Becirevic <nurfet.becirevic@gmail.com> 5# Copyright: (c) 2017, Tomas Karasek <tom.to.the.k@gmail.com> 6# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 7 8from __future__ import (absolute_import, division, print_function) 9__metaclass__ = type 10 11 12DOCUMENTATION = ''' 13--- 14module: packet_volume_attachment 15 16short_description: Attach/detach a volume to a device in the Packet host. 17 18description: 19 - Attach/detach a volume to a device in the Packet host. 20 - API is documented at U(https://www.packet.com/developers/api/volumes/). 21 - "This module creates the attachment route in the Packet API. In order to discover 22 the block devices on the server, you have to run the Attach Scripts, 23 as documented at U(https://help.packet.net/technical/storage/packet-block-storage-linux)." 24 25version_added: '0.2.0' 26 27author: 28 - Tomas Karasek (@t0mk) <tom.to.the.k@gmail.com> 29 - Nurfet Becirevic (@nurfet-becirevic) <nurfet.becirevic@gmail.com> 30 31options: 32 state: 33 description: 34 - Indicate desired state of the attachment. 35 default: present 36 choices: ['present', 'absent'] 37 type: str 38 39 auth_token: 40 description: 41 - Packet API token. You can also supply it in env var C(PACKET_API_TOKEN). 42 type: str 43 44 project_id: 45 description: 46 - UUID of the project to which the device and volume belong. 47 type: str 48 required: true 49 50 volume: 51 description: 52 - Selector for the volume. 53 - It can be a UUID, an API-generated volume name, or user-defined description string. 54 - 'Example values: 4a347482-b546-4f67-8300-fb5018ef0c5, volume-4a347482, "my volume"' 55 type: str 56 required: true 57 58 device: 59 description: 60 - Selector for the device. 61 - It can be a UUID of the device, or a hostname. 62 - 'Example values: 98a14f7a-3d27-4478-b7cf-35b5670523f3, "my device"' 63 type: str 64 65requirements: 66 - "python >= 2.6" 67 - "packet-python >= 1.35" 68 69''' 70 71EXAMPLES = ''' 72# All the examples assume that you have your Packet API token in env var PACKET_API_TOKEN. 73# You can also pass the api token in module param auth_token. 74 75- hosts: localhost 76 77 vars: 78 volname: testvol 79 devname: testdev 80 project_id: 52000fb2-ee46-4673-93a8-de2c2bdba33b 81 82 tasks: 83 - name: Create volume 84 packet_volume: 85 description: "{{ volname }}" 86 project_id: "{{ project_id }}" 87 facility: ewr1 88 plan: storage_1 89 state: present 90 size: 10 91 snapshot_policy: 92 snapshot_count: 10 93 snapshot_frequency: 1day 94 95 - name: Create a device 96 packet_device: 97 project_id: "{{ project_id }}" 98 hostnames: "{{ devname }}" 99 operating_system: ubuntu_16_04 100 plan: baremetal_0 101 facility: ewr1 102 state: present 103 104 - name: Attach testvol to testdev 105 community.general.packet_volume_attachment: 106 project_id: "{{ project_id }}" 107 volume: "{{ volname }}" 108 device: "{{ devname }}" 109 110 - name: Detach testvol from testdev 111 community.general.packet_volume_attachment: 112 project_id: "{{ project_id }}" 113 volume: "{{ volname }}" 114 device: "{{ devname }}" 115 state: absent 116''' 117 118RETURN = ''' 119volume_id: 120 description: UUID of volume addressed by the module call. 121 type: str 122 returned: success 123 124device_id: 125 description: UUID of device addressed by the module call. 126 type: str 127 returned: success 128''' 129 130import uuid 131 132from ansible.module_utils.basic import AnsibleModule, env_fallback 133from ansible.module_utils.common.text.converters import to_native 134 135HAS_PACKET_SDK = True 136 137 138try: 139 import packet 140except ImportError: 141 HAS_PACKET_SDK = False 142 143 144PACKET_API_TOKEN_ENV_VAR = "PACKET_API_TOKEN" 145 146STATES = ["present", "absent"] 147 148 149def is_valid_uuid(myuuid): 150 try: 151 val = uuid.UUID(myuuid, version=4) 152 except ValueError: 153 return False 154 return str(val) == myuuid 155 156 157def get_volume_selector(spec): 158 if is_valid_uuid(spec): 159 return lambda v: v['id'] == spec 160 else: 161 return lambda v: v['name'] == spec or v['description'] == spec 162 163 164def get_device_selector(spec): 165 if is_valid_uuid(spec): 166 return lambda v: v['id'] == spec 167 else: 168 return lambda v: v['hostname'] == spec 169 170 171def do_attach(packet_conn, vol_id, dev_id): 172 api_method = "storage/{0}/attachments".format(vol_id) 173 packet_conn.call_api( 174 api_method, 175 params={"device_id": dev_id}, 176 type="POST") 177 178 179def do_detach(packet_conn, vol, dev_id=None): 180 def dev_match(a): 181 return (dev_id is None) or (a['device']['id'] == dev_id) 182 for a in vol['attachments']: 183 if dev_match(a): 184 packet_conn.call_api(a['href'], type="DELETE") 185 186 187def validate_selected(l, resource_type, spec): 188 if len(l) > 1: 189 _msg = ("more than one {0} matches specification {1}: {2}".format( 190 resource_type, spec, l)) 191 raise Exception(_msg) 192 if len(l) == 0: 193 _msg = "no {0} matches specification: {1}".format(resource_type, spec) 194 raise Exception(_msg) 195 196 197def get_attached_dev_ids(volume_dict): 198 if len(volume_dict['attachments']) == 0: 199 return [] 200 else: 201 return [a['device']['id'] for a in volume_dict['attachments']] 202 203 204def act_on_volume_attachment(target_state, module, packet_conn): 205 return_dict = {'changed': False} 206 volspec = module.params.get("volume") 207 devspec = module.params.get("device") 208 if devspec is None and target_state == 'present': 209 raise Exception("If you want to attach a volume, you must specify a device.") 210 project_id = module.params.get("project_id") 211 volumes_api_method = "projects/{0}/storage".format(project_id) 212 volumes = packet_conn.call_api(volumes_api_method, 213 params={'include': 'facility,attachments.device'})['volumes'] 214 v_match = get_volume_selector(volspec) 215 matching_volumes = [v for v in volumes if v_match(v)] 216 validate_selected(matching_volumes, "volume", volspec) 217 volume = matching_volumes[0] 218 return_dict['volume_id'] = volume['id'] 219 220 device = None 221 if devspec is not None: 222 devices_api_method = "projects/{0}/devices".format(project_id) 223 devices = packet_conn.call_api(devices_api_method)['devices'] 224 d_match = get_device_selector(devspec) 225 matching_devices = [d for d in devices if d_match(d)] 226 validate_selected(matching_devices, "device", devspec) 227 device = matching_devices[0] 228 return_dict['device_id'] = device['id'] 229 230 attached_device_ids = get_attached_dev_ids(volume) 231 232 if target_state == "present": 233 if len(attached_device_ids) == 0: 234 do_attach(packet_conn, volume['id'], device['id']) 235 return_dict['changed'] = True 236 elif device['id'] not in attached_device_ids: 237 # Don't reattach volume which is attached to a different device. 238 # Rather fail than force remove a device on state == 'present'. 239 raise Exception("volume {0} is already attached to device {1}".format( 240 volume, attached_device_ids)) 241 else: 242 if device is None: 243 if len(attached_device_ids) > 0: 244 do_detach(packet_conn, volume) 245 return_dict['changed'] = True 246 elif device['id'] in attached_device_ids: 247 do_detach(packet_conn, volume, device['id']) 248 return_dict['changed'] = True 249 250 return return_dict 251 252 253def main(): 254 module = AnsibleModule( 255 argument_spec=dict( 256 state=dict(choices=STATES, default="present"), 257 auth_token=dict( 258 type='str', 259 fallback=(env_fallback, [PACKET_API_TOKEN_ENV_VAR]), 260 no_log=True 261 ), 262 volume=dict(type="str", required=True), 263 project_id=dict(type="str", required=True), 264 device=dict(type="str"), 265 ), 266 supports_check_mode=True, 267 ) 268 269 if not HAS_PACKET_SDK: 270 module.fail_json(msg='packet required for this module') 271 272 if not module.params.get('auth_token'): 273 _fail_msg = ("if Packet API token is not in environment variable {0}, " 274 "the auth_token parameter is required".format(PACKET_API_TOKEN_ENV_VAR)) 275 module.fail_json(msg=_fail_msg) 276 277 auth_token = module.params.get('auth_token') 278 279 packet_conn = packet.Manager(auth_token=auth_token) 280 281 state = module.params.get('state') 282 283 if state in STATES: 284 if module.check_mode: 285 module.exit_json(changed=False) 286 287 try: 288 module.exit_json( 289 **act_on_volume_attachment(state, module, packet_conn)) 290 except Exception as e: 291 module.fail_json( 292 msg="failed to set volume_attachment state {0}: {1}".format(state, to_native(e))) 293 else: 294 module.fail_json(msg="{0} is not a valid state for this module".format(state)) 295 296 297if __name__ == '__main__': 298 main() 299