From 4b996bc43291be074d25fa6d5439e1108f8e4b84 Mon Sep 17 00:00:00 2001 From: "Christopher H. Laco" Date: Fri, 8 Nov 2013 19:29:41 -0600 Subject: [PATCH] Add Rackspace Cloud Block Storage modules - Add rax_cbs to create/delete cloud block storage volumes - Add rax_cbs_attachments to attach/detach volumes from servers --- cloud/rax_cbs | 241 +++++++++++++++++++++++++++++++++++ cloud/rax_cbs_attachments | 255 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 496 insertions(+) create mode 100644 cloud/rax_cbs create mode 100644 cloud/rax_cbs_attachments diff --git a/cloud/rax_cbs b/cloud/rax_cbs new file mode 100644 index 00000000000..efa40c0ffc0 --- /dev/null +++ b/cloud/rax_cbs @@ -0,0 +1,241 @@ +#!/usr/bin/python -tt +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: rax_cbs +short_description: Manipulate Rackspace Cloud Block Storage Volumes +description: + - Manipulate Rackspace Cloud Block Storage Volumes +version_added: "1.5" +options: + api_key: + description: + - Rackspace API key (overrides C(credentials)) + credentials: + description: + - File to find the Rackspace credentials in (ignored if C(api_key) and + C(username) are provided) + default: null + aliases: ['creds_file'] + description: + description: + - Description to give the volume being created + default: null + meta: + description: + - A hash of metadata to associate with the volume + default: null + name: + description: + - Name to give the volume being created + default: null + required: true + region: + description: + - Region to create the volume in + default: DFW + size: + description: + - Size of the volume to create in Gigabytes + default: 100 + required: true + snapshot_id: + description: + - The id of the snapshot to create the volume from + default: null + state: + description: + - Indicate desired state of the resource + choices: ['present', 'absent'] + default: present + required: true + volume_type: + description: + - Type of the volume being created + choices: ['SATA', 'SSD'] + default: SATA + required: true + username: + description: + - Rackspace username (overrides C(credentials)) + wait: + description: + - wait for the volume to be in state 'available' before returning + default: "no" + choices: [ "yes", "no" ] + wait_timeout: + description: + - how long before wait gives up, in seconds + default: 300 +requirements: [ "pyrax" ] +author: Christopher H. Laco, Matt Martz +notes: + - The following environment variables can be used, C(RAX_USERNAME), + C(RAX_API_KEY), C(RAX_CREDS_FILE), C(RAX_CREDENTIALS), C(RAX_REGION). + - C(RAX_CREDENTIALS) and C(RAX_CREDS_FILE) points to a credentials file + appropriate for pyrax. See U(https://github.com/rackspace/pyrax/blob/master/docs/getting_started.md#authenticating) + - C(RAX_USERNAME) and C(RAX_API_KEY) obviate the use of a credentials file + - C(RAX_REGION) defines a Rackspace Public Cloud region (DFW, ORD, LON, ...) +''' + +EXAMPLES = ''' +- name: Build a Block Storage Volume + gather_facts: False + hosts: local + connection: local + tasks: + - name: Storage volume create request + local_action: + module: rax_cbs + credentials: ~/.raxpub + name: my-volume + description: My Volume + volume_type: SSD + size: 150 + region: DFW + wait: yes + state: present + meta: + app: my-cool-app + register: my_volume +''' + +import sys + +from types import NoneType + +try: + import pyrax +except ImportError: + print("failed=True msg='pyrax required for this module'") + sys.exit(1) + +NON_CALLABLES = (basestring, bool, dict, int, list, NoneType) +VOLUME_STATUS = ('available', 'attaching', 'creating', 'deleting', 'in-use', + 'error', 'error_deleting') + + +def cloud_block_storage(module, state, name, description, meta, size, + snapshot_id, volume_type, wait, wait_timeout): + for arg in (state, name, size, volume_type): + if not arg: + module.fail_json(msg='%s is required for rax_clb' % arg) + + if int(size) < 100: + module.fail_json(msg='"size" must be greater than or equal to 100') + + changed = False + volumes = [] + instance = {} + + cbs = pyrax.cloud_blockstorage + + for volume in cbs.list(): + if name != volume.name and name != volume.id: + continue + + volumes.append(volume) + + if len(volumes) > 1: + module.fail_json(msg='Multiple Storage Volumes were matched by name, ' + 'try using the Volume ID instead') + + if state == 'present': + if not volumes: + try: + volume = cbs.create(name, size=size, volume_type=volume_type, + description=description, + metadata=meta, + snapshot_id=snapshot_id) + changed = True + except Exception, e: + module.fail_json(msg='%s' % e.message) + else: + volume = volumes[0] + + volume.get() + for key, value in vars(volume).iteritems(): + if (isinstance(value, NON_CALLABLES) and + not key.startswith('_')): + instance[key] = value + + result = dict(changed=changed, volume=instance) + + if volume.status == 'error': + result['msg'] = '%s failed to build' % volume.id + elif wait and volume.status not in VOLUME_STATUS: + result['msg'] = 'Timeout waiting on %s' % volume.id + + if 'msg' in result: + module.fail_json(**result) + else: + module.exit_json(**result) + + elif state == 'absent': + if volumes: + volume = volumes[0] + try: + volume.delete() + changed = True + except Exception, e: + module.fail_json(msg='%s' % e.message) + + module.exit_json(changed=changed, volume=instance) + + +def main(): + argument_spec = rax_argument_spec() + argument_spec.update( + dict( + description=dict(), + meta=dict(type='dict', default={}), + name=dict(), + size=dict(type='int', default=100), + snapshot_id=dict(), + state=dict(default='present', choices=['present', 'absent']), + volume_type=dict(choices=['SSD', 'SATA'], default='SATA'), + wait=dict(type='bool'), + wait_timeout=dict(type='int', default=300) + ) + ) + + module = AnsibleModule( + argument_spec=argument_spec, + required_together=rax_required_together() + ) + + description = module.params.get('description') + meta = module.params.get('meta') + name = module.params.get('name') + size = module.params.get('size') + snapshot_id = module.params.get('snapshot_id') + state = module.params.get('state') + volume_type = module.params.get('volume_type') + wait = module.params.get('wait') + wait_timeout = int(module.params.get('wait_timeout')) + + setup_rax_module(module, pyrax) + + cloud_block_storage(module, state, name, description, meta, size, + snapshot_id, volume_type, wait, wait_timeout) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.rax import * + +### invoke the module +main() diff --git a/cloud/rax_cbs_attachments b/cloud/rax_cbs_attachments new file mode 100644 index 00000000000..2a0ac49775e --- /dev/null +++ b/cloud/rax_cbs_attachments @@ -0,0 +1,255 @@ +#!/usr/bin/python -tt +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: rax_cbs_attachments +short_description: Manipulate Rackspace Cloud Block Storage Volume Attachments +description: + - Manipulate Rackspace Cloud Block Storage Volume Attachments +version_added: "1.5" +options: + api_key: + description: + - Rackspace API key (overrides C(credentials)) + credentials: + description: + - File to find the Rackspace credentials in (ignored if C(api_key) and + C(username) are provided) + default: null + aliases: ['creds_file'] + mountpoint: + description: + - The mount point to attach the volume to + default: null + required: true + name: + description: + - Name or id of the volume to attach/detach + default: null + required: true + region: + description: + - Region the volume and server are located in + default: DFW + server: + description: + - Name or id of the server to attach/detach + default: null + required: true + state: + description: + - Indicate desired state of the resource + choices: ['present', 'absent'] + default: present + required: true + username: + description: + - Rackspace username (overrides C(credentials)) + wait: + description: + - wait for the volume to be in 'in-use'/'available' state before returning + default: "no" + choices: [ "yes", "no" ] + wait_timeout: + description: + - how long before wait gives up, in seconds + default: 300 +requirements: [ "pyrax" ] +author: Christopher H. Laco, Matt Martz +notes: + - The following environment variables can be used, C(RAX_USERNAME), + C(RAX_API_KEY), C(RAX_CREDS_FILE), C(RAX_CREDENTIALS), C(RAX_REGION). + - C(RAX_CREDENTIALS) and C(RAX_CREDS_FILE) points to a credentials file + appropriate for pyrax. See U(https://github.com/rackspace/pyrax/blob/master/docs/getting_started.md#authenticating) + - C(RAX_USERNAME) and C(RAX_API_KEY) obviate the use of a credentials file + - C(RAX_REGION) defines a Rackspace Public Cloud region (DFW, ORD, LON, ...) +''' + +EXAMPLES = ''' +- name: Attach a Block Storage Volume + gather_facts: False + hosts: local + connection: local + tasks: + - name: Storage volume attach request + local_action: + module: rax_cbs_attachments + credentials: ~/.raxpub + name: my-volume + server: my-server + mountpoint: /dev/xvdd + region: DFW + wait: yes + state: present + register: my_volume +''' + +import sys + +from types import NoneType + +try: + import pyrax +except ImportError: + print("failed=True msg='pyrax required for this module'") + sys.exit(1) + +NON_CALLABLES = (basestring, bool, dict, int, list, NoneType) +VOLUME_STATUS = ('available', 'attaching', 'creating', 'deleting', 'in-use', + 'error', 'error_deleting') + + +def cloud_block_storage_attachments(module, state, name, server, mountpoint, + wait, wait_timeout): + for arg in (state, name, server, mountpoint): + if not arg: + module.fail_json(msg='%s is required for rax_clb_attachments' % arg) + + cbs = pyrax.cloud_blockstorage + cs = pyrax.cloudservers + changed = False + volumes = [] + instance = {} + + for volume in cbs.list(): + if name == volume.display_name or name == volume.id: + volumes.append(volume) + + if len(volumes) > 1: + module.fail_json(msg='Multiple Storage Volumes were matched by name, ' + 'try using the Volume ID instead') + elif not volumes: + module.fail_json(msg='No Storage Volumes were matched by name, ' + 'try using the Volume ID instead') + + volume = volumes[0] + if state == 'present': + server = cs.servers.get(server) + + if not server: + module.fail_json(msg='No Server was matched by name, ' + 'try using the Server ID instead') + else: + if volume.attachments and volume.attachments[0]['server_id'] == server.id: + changed = False + elif volume.attachments: + module.fail_json(msg='Volume is attached to another server') + else: + try: + volume.attach_to_instance(server, mountpoint=mountpoint) + changed = True + except Exception, e: + module.fail_json(msg='%s' % e.message) + + volume.get() + + for key, value in vars(volume).iteritems(): + if (isinstance(value, NON_CALLABLES) and + not key.startswith('_')): + instance[key] = value + + result = dict(changed=changed, volume=instance) + + if volume.status == 'error': + result['msg'] = '%s failed to build' % volume.id + elif wait: + pyrax.utils.wait_until(volume, 'status', 'in-use', + interval=3, attempts=0, + verbose=False) + + if 'msg' in result: + module.fail_json(**result) + else: + module.exit_json(**result) + + elif state == 'absent': + server = cs.servers.get(server) + + if not server: + module.fail_json(msg='No Server was matched by name, ' + 'try using the Server ID instead') + else: + if volume.attachments and volume.attachments[0]['server_id'] == server.id: + try: + volume.detach() + if wait: + pyrax.utils.wait_until(volume, 'status', 'available', + interval=3, attempts=0, + verbose=False) + changed = True + except Exception, e: + module.fail_json(msg='%s' % e.message) + + volume.get() + changed = True + elif volume.attachments: + module.fail_json(msg='Volume is attached to another server') + + for key, value in vars(volume).iteritems(): + if (isinstance(value, NON_CALLABLES) and + not key.startswith('_')): + instance[key] = value + + result = dict(changed=changed, volume=instance) + + if volume.status == 'error': + result['msg'] = '%s failed to build' % volume.id + + if 'msg' in result: + module.fail_json(**result) + else: + module.exit_json(**result) + + module.exit_json(changed=changed, volume=instance) + + +def main(): + argument_spec = rax_argument_spec() + argument_spec.update( + dict( + mountpoint=dict(), + name=dict(), + server=dict(), + state=dict(default='present', choices=['present', 'absent']), + wait=dict(type='bool'), + wait_timeout=dict(type='int', default=300) + ) + ) + + module = AnsibleModule( + argument_spec=argument_spec, + required_together=rax_required_together() + ) + + mountpoint = module.params.get('mountpoint') + name = module.params.get('name') + server = module.params.get('server') + state = module.params.get('state') + wait = module.params.get('wait') + wait_timeout = int(module.params.get('wait_timeout')) + + setup_rax_module(module, pyrax) + + cloud_block_storage_attachments(module, state, name, server, mountpoint, + wait, wait_timeout) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.rax import * + +### invoke the module +main()