From 3ed142c1f08269e0ebdb4ebd630bfa85a2005550 Mon Sep 17 00:00:00 2001 From: Paul Durivage Date: Fri, 20 Dec 2013 12:09:54 -0600 Subject: [PATCH] Add module support for Rackspace Cloud Files Containers --- cloud/rax_files | 379 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 379 insertions(+) create mode 100644 cloud/rax_files diff --git a/cloud/rax_files b/cloud/rax_files new file mode 100644 index 00000000000..536cb850bf2 --- /dev/null +++ b/cloud/rax_files @@ -0,0 +1,379 @@ +#!/usr/bin/python -tt + +# (c) 2013, Paul Durivage +# +# 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: +short_description: create, fetch, and delete objects in Rackspace Cloud Files +description: + - Upload, download, and delete objects in Rackspace Cloud Files +version_added: "1.5" +options: + api_key: + description: + - Rackspace API key (overrides I(credentials))requirements: [ "pyrax" ] + clear_meta: + description: + - Optionally clear existing metadata when applying metadata to existing containers. + - Selecting this option is only appropriate when setting type=meta + choices: ["yes", "no"] + default: "no" + container: + description: + - The container to use for container or metadata operations. + required: true + credentials: + description: + - File to find the Rackspace credentials in (ignored if I(api_key) and + I(username) are provided) + default: null + aliases: ['creds_file'] + meta: + description: + - A hash of items to set as metadata values on a container + private: + description: + - Used to set a container as private, removing it from the CDN. + - Warning: Private containers, if previously made public, can have live objects available until the TTL on cached objects expires + public: + description: + - Used to set a container as public, available via the Cloud Files CDN + region: + description: + - Region to create an instance in + default: DFW + ttl: + description: + - In seconds, set a container-wide TTL for all objects cached on CDN edge nodes + - Setting a TTL is only appropriate for containers that are public + type: + description: + - Type of object to do work on: metadata object or a container object + choices: ["file", "meta"] + default: "file" + username: + description: + - Rackspace username (overrides I(credentials)) + web_error: + description: + - Sets an object to be presented as the HTTP error page when accessed by the CDN URL + web_index: + description: + - Sets an object to be presented as the HTTP index page when accessed by the CDN URL +requirements: [ "pyrax" ] +author: Paul Durivage +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: "Test Cloud Files Containers" + hosts: local + gather_facts: no + tasks: + - name: "List all containers" + rax_files: state=list + + - name: "Create container called 'mycontainer'" + rax_files: container=mycontainer + + - name: "Create container 'mycontainer2' with metadata" + rax_files: + container: mycontainer2 + meta: + key: value + file_for: paul.durivage@rackspace.com + + - name: "Set a container's web index page" + rax_files: container=mycontainer web_index=index.html + + - name: "Set a container's web error page" + rax_files: container=mycontainer web_error=error.html + + - name: "Make container public" + rax_files: container=mycontainer public=yes + + - name: "Make container public with a 24 hour TTL" + rax_files: container=mycontainer public=yes ttl=86400 + + - name: "Make container private" + rax_files: container=mycontainer private=yes + +- name: "Test Cloud Files Containers Metadata Storage" + hosts: local + gather_facts: no + tasks: + - name: "Get mycontainer2 metadata" + rax_files: + container: mycontainer2 + type: meta + + - name: "Set mycontainer2 metadata" + rax_files: + container: mycontainer2 + type: meta + meta: + uploaded_by: paul.durivage@rackspace.com + + - name: "Remove mycontainer2 metadata" + rax_files: + container: "mycontainer2" + type: meta + state: absent + meta: + key: "" + file_for: "" +''' + +from ansible import __version__ + +try: + import pyrax +except ImportError, e: + print("failed=True msg='pyrax is required for this module'") + sys.exit(1) + +EXIT_DICT = {'success': True} +META_PREFIX = 'x-container-meta-' +USER_AGENT = "Ansible/%s via pyrax" % __version__ + + +def _get_container(module, cf, container): + try: + return cf.get_container(container) + except pyrax.exc.NoSuchContainer, e: + module.fail_json(msg=e.message) + + +def _fetch_meta(module, container): + try: + EXIT_DICT['meta'] = { + k.split(META_PREFIX)[-1]: v for k, v in container.get_metadata().items() + } + except Exception, e: + module.fail_json(msg=e.message) + + +def meta(cf, module, container_, state, meta_, clear_meta): + c = _get_container(module, cf, container_) + + if meta_ and state == 'present': + try: + meta_set = c.set_metadata(meta_, clear=clear_meta) + except Exception, e: + module.fail_json(msg=e.message) + elif meta_ and state == 'absent': + remove_results = [] + for k, v in meta_.items(): + c.remove_metadata_key(k) + remove_results.append(k) + EXIT_DICT['deleted_meta_keys'] = remove_results + elif state == 'absent': + remove_results = [] + for k, v in c.get_metadata().items(): + c.remove_metadata_key(k) + remove_results.append(k) + EXIT_DICT['deleted_meta_keys'] = remove_results + + _fetch_meta(module, c) + _locals = locals().keys() + + EXIT_DICT['container'] = c.name + if 'meta_set' in _locals or 'remove_results' in _locals: + EXIT_DICT['changed'] = True + + module.exit_json(**EXIT_DICT) + + +def container(cf, module, container_, state, meta_, clear_meta, ttl, public, private, web_index, web_error): + if public and private: + module.fail_json(msg='container cannot be simultaneously ' + 'set to public and private') + + if state == 'absent' and (meta_ or clear_meta or public or private or web_index or web_error): + module.fail_json(msg='state cannot be omitted when setting/removing ' + 'attributes on a container') + + if state == 'list': + # We don't care if attributes are specified, let's list containers + EXIT_DICT['containers'] = cf.list_containers() + module.exit_json(**EXIT_DICT) + + try: + c = cf.get_container(container_) + except pyrax.exc.NoSuchContainer, e: + # Make the container if state=present, otherwise bomb out + if state == 'present': + try: + c = cf.create_container(container_) + except Exception, e: + module.fail_json(msg=e.message) + else: + EXIT_DICT['created'] = True + else: + module.fail_json(msg=e.message) + else: + # Successfully grabbed a container object + # Delete if state is absent + if state == 'absent': + try: + cont_deleted = c.delete() + except Exception, e: + module.fail_json(msg=e.message) + else: + EXIT_DICT['deleted'] = True + + if meta_: + try: + meta_set = c.set_metadata(meta_, clear=clear_meta) + except Exception, e: + module.fail_json(msg=e.message) + finally: + _fetch_meta(module, c) + + if ttl: + try: + c.cdn_ttl = ttl + except Exception, e: + module.fail_json(msg=e.message) + else: + EXIT_DICT['ttl'] = c.cdn_ttl + + if public: + try: + cont_public = c.make_public() + except Exception, e: + module.fail_json(msg=e.message) + else: + EXIT_DICT['container_urls'] = { + 'url': c.cdn_uri, + 'ssl_url': c.cdn_ssl_uri, + 'streaming_url': c.cdn_streaming_uri, + 'ios_uri': c.cdn_ios_uri + } + + if private: + try: + cont_private = c.make_private() + except Exception, e: + module.fail_json(msg=e.message) + else: + EXIT_DICT['set_private'] = True + + if web_index: + try: + cont_web_index = c.set_web_index_page(web_index) + except Exception, e: + module.fail_json(msg=e.message) + else: + EXIT_DICT['set_index'] = True + finally: + _fetch_meta(module, c) + + if web_error: + try: + cont_err_index = c.set_web_error_page(web_error) + except Exception, e: + module.fail_json(msg=e.message) + else: + EXIT_DICT['set_error'] = True + finally: + _fetch_meta(module, c) + + EXIT_DICT['container'] = c.name + EXIT_DICT['objs_in_container'] = c.object_count + EXIT_DICT['total_bytes'] = c.total_bytes + + _locals = locals().keys() + + if ('cont_created' in _locals + or 'cont_deleted' in _locals + or 'meta_set' in _locals + or 'cont_public' in _locals + or 'cont_private' in _locals + or 'cont_web_index' in _locals + or 'cont_err_index' in _locals): + EXIT_DICT['changed'] = True + + module.exit_json(**EXIT_DICT) + + +def cloudfiles(module, container_, state, meta_, clear_meta, typ, ttl, public, private, web_index, web_error): + """ Dispatch from here to work with metadata or file objects """ + cf = pyrax.cloudfiles + cf.user_agent = USER_AGENT + + if typ == "container": + container(cf, module, container_, state, meta_, clear_meta, ttl, public, private, web_index, web_error) + else: + meta(cf, module, container_, state, meta_, clear_meta) + + +def main(): + argument_spec = rax_argument_spec() + argument_spec.update( + dict( + container=dict(), + state=dict(choices=['present', 'absent', 'list'], default='present'), + meta=dict(type='dict', default={}), + clear_meta=dict(choices=BOOLEANS, default=False, type='bool'), + type=dict(choices=['container', 'meta'], default='container'), + ttl=dict(type='int'), + public=dict(choices=BOOLEANS, default=False, type='bool'), + private=dict(choices=BOOLEANS, default=False, type='bool'), + web_index=dict(), + web_error=dict() + ) + ) + + module = AnsibleModule( + argument_spec=argument_spec, + required_together=rax_required_together() + ) + + container_ = module.params.get('container') + state = module.params.get('state') + meta_ = module.params.get('meta') + clear_meta = module.params.get('clear_meta') + typ = module.params.get('type') + ttl = module.params.get('ttl') + public = module.params.get('public') + private = module.params.get('private') + web_index = module.params.get('web_index') + web_error = module.params.get('web_error') + + if state in ['present', 'absent'] and not container_: + module.fail_json(msg='please specify a container name') + if clear_meta and not typ == 'meta': + module.fail_json(msg='clear_meta can only be used when setting metadata') + + setup_rax_module(module, pyrax) + cloudfiles(module, container_, state, meta_, clear_meta, typ, ttl, public, private, web_index, web_error) + + +from ansible.module_utils.basic import * +from ansible.module_utils.rax import * + +main()