diff --git a/library/cloud/rax_cbs b/library/cloud/rax_cbs new file mode 100644 index 00000000000..73106eb41ab --- /dev/null +++ b/library/cloud/rax_cbs @@ -0,0 +1,282 @@ +#!/usr/bin/python +# 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.6 +options: + api_key: + description: + - Rackspace API key (overrides I(credentials)) + aliases: + - password + auth_endpoint: + description: + - The URI of the authentication service + default: https://identity.api.rackspacecloud.com/v2.0/ + credentials: + description: + - File to find the Rackspace credentials in (ignored if I(api_key) and + I(username) are provided) + default: null + aliases: + - creds_file + env: + description: + - Environment as configured in ~/.pyrax.cfg, + see U(https://github.com/rackspace/pyrax/blob/master/docs/getting_started.md#pyrax-configuration) + identity_type: + description: + - Authentication machanism to use, such as rackspace or keystone + default: rackspace + region: + description: + - Region to create an instance in + default: DFW + tenant_id: + description: + - The tenant ID used for authentication + tenant_name: + description: + - The tenant name used for authentication + username: + description: + - Rackspace username (overrides I(credentials)) + verify_ssl: + description: + - Whether or not to require SSL validation of API endpoints + 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 + 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 + 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 uuid import UUID +from types import NoneType + +try: + import pyrax + HAS_PYRAX = True +except ImportError: + HAS_PYRAX = False + +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_cbs' % arg) + + if size < 100: + module.fail_json(msg='"size" must be greater than or equal to 100') + + changed = False + volume = None + instance = {} + + cbs = pyrax.cloud_blockstorage + + if cbs is None: + module.fail_json(msg='Failed to instantiate client. This ' + 'typically indicates an invalid region or an ' + 'incorrectly capitalized region name.') + + try: + UUID(name) + volume = cbs.get(name) + except ValueError: + try: + volume = cbs.find(name=name) + except Exception, e: + module.fail_json(msg='%s' % e) + + if state == 'present': + if not volume: + 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: + if wait: + attempts = wait_timeout / 5 + pyrax.utils.wait_for_build(volume, interval=5, + attempts=attempts) + + 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 volume: + 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(required=True), + 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', default=False), + wait_timeout=dict(type='int', default=300) + ) + ) + + module = AnsibleModule( + argument_spec=argument_spec, + required_together=rax_required_together() + ) + + if not HAS_PYRAX: + module.fail_json(msg='pyrax is required for this module') + + 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 = 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/library/cloud/rax_cbs_attachments b/library/cloud/rax_cbs_attachments new file mode 100644 index 00000000000..c20c03a69ea --- /dev/null +++ b/library/cloud/rax_cbs_attachments @@ -0,0 +1,314 @@ +#!/usr/bin/python +# 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.6 +options: + api_key: + description: + - Rackspace API key (overrides I(credentials)) + aliases: + - password + auth_endpoint: + description: + - The URI of the authentication service + default: https://identity.api.rackspacecloud.com/v2.0/ + credentials: + description: + - File to find the Rackspace credentials in (ignored if I(api_key) and + I(username) are provided) + default: null + aliases: + - creds_file + env: + description: + - Environment as configured in ~/.pyrax.cfg, + see U(https://github.com/rackspace/pyrax/blob/master/docs/getting_started.md#pyrax-configuration) + identity_type: + description: + - Authentication machanism to use, such as rackspace or keystone + default: rackspace + region: + description: + - Region to create an instance in + default: DFW + tenant_id: + description: + - The tenant ID used for authentication + tenant_name: + description: + - The tenant name used for authentication + username: + description: + - Rackspace username (overrides I(credentials)) + verify_ssl: + description: + - Whether or not to require SSL validation of API endpoints + device: + description: + - The device path to attach the volume to, e.g. /dev/xvde + default: null + required: true + volume: + description: + - Name or id of the volume to attach/detach + default: null + required: true + 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 + 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 + volume: my-volume + server: my-server + device: /dev/xvdd + region: DFW + wait: yes + state: present + register: my_volume +''' + +import sys + +from uuid import UUID +from types import NoneType + +try: + import pyrax + HAS_PYRAX = True +except ImportError: + HAS_PYRAX = False + +NON_CALLABLES = (basestring, bool, dict, int, list, NoneType) + + +def cloud_block_storage_attachments(module, state, volume, server, device, + wait, wait_timeout): + for arg in (state, volume, server, device): + if not arg: + module.fail_json(msg='%s is required for rax_cbs_attachments' % + arg) + + cbs = pyrax.cloud_blockstorage + cs = pyrax.cloudservers + + if cbs is None or cs is None: + module.fail_json(msg='Failed to instantiate client. This ' + 'typically indicates an invalid region or an ' + 'incorrectly capitalized region name.') + + changed = False + instance = {} + + try: + UUID(volume) + volume = cbs.get(volume) + except ValueError: + try: + volume = cbs.find(name=volume) + except Exception, e: + module.fail_json(msg='%s' % e) + + if not volume: + module.fail_json(msg='No matching storage volumes were found') + + if state == 'present': + try: + UUID(server) + server = cs.servers.get(server) + except ValueError: + servers = cs.servers.list(search_opts=dict(name='^%s$' % server)) + if not servers: + module.fail_json(msg='No Server was matched by name, ' + 'try using the Server ID instead') + if len(servers) > 1: + module.fail_json(msg='Multiple servers matched by name, ' + 'try using the Server ID instead') + + # We made it this far, grab the first and hopefully only server + # in the list + server = servers[0] + + 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=device) + 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: + attempts = wait_timeout / 5 + pyrax.utils.wait_until(volume, 'status', 'in-use', + interval=5, attempts=attempts) + + if 'msg' in result: + module.fail_json(**result) + else: + module.exit_json(**result) + + elif state == 'absent': + try: + UUID(server) + server = cs.servers.get(server) + except ValueError: + servers = cs.servers.list(search_opts=dict(name='^%s$' % server)) + if not servers: + module.fail_json(msg='No Server was matched by name, ' + 'try using the Server ID instead') + if len(servers) > 1: + module.fail_json(msg='Multiple servers matched by name, ' + 'try using the Server ID instead') + + # We made it this far, grab the first and hopefully only server + # in the list + server = servers[0] + + 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( + device=dict(required=True), + volume=dict(required=True), + server=dict(required=True), + state=dict(default='present', choices=['present', 'absent']), + wait=dict(type='bool', default=False), + wait_timeout=dict(type='int', default=300) + ) + ) + + module = AnsibleModule( + argument_spec=argument_spec, + required_together=rax_required_together() + ) + + if not HAS_PYRAX: + module.fail_json(msg='pyrax is required for this module') + + device = module.params.get('device') + volume = module.params.get('volume') + server = module.params.get('server') + state = module.params.get('state') + wait = module.params.get('wait') + wait_timeout = module.params.get('wait_timeout') + + setup_rax_module(module, pyrax) + + cloud_block_storage_attachments(module, state, volume, server, device, + wait, wait_timeout) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.rax import * + +### invoke the module +main()