1#!/usr/bin/python
2
3# (c) 2013, Paul Durivage <paul.durivage@rackspace.com>
4# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5
6from __future__ import absolute_import, division, print_function
7__metaclass__ = type
8
9
10ANSIBLE_METADATA = {'metadata_version': '1.1',
11                    'status': ['preview'],
12                    'supported_by': 'community'}
13
14
15DOCUMENTATION = '''
16---
17module: rax_files
18short_description: Manipulate Rackspace Cloud Files Containers
19description:
20  - Manipulate Rackspace Cloud Files Containers
21version_added: "1.5"
22options:
23  clear_meta:
24    description:
25      - Optionally clear existing metadata when applying metadata to existing containers.
26        Selecting this option is only appropriate when setting type=meta
27    type: bool
28    default: "no"
29  container:
30    description:
31      - The container to use for container or metadata operations.
32    required: true
33  meta:
34    description:
35      - A hash of items to set as metadata values on a container
36  private:
37    description:
38      - Used to set a container as private, removing it from the CDN.  B(Warning!)
39        Private containers, if previously made public, can have live objects
40        available until the TTL on cached objects expires
41    type: bool
42  public:
43    description:
44      - Used to set a container as public, available via the Cloud Files CDN
45    type: bool
46  region:
47    description:
48      - Region to create an instance in
49    default: DFW
50  state:
51    description:
52      - Indicate desired state of the resource
53    choices: ['present', 'absent']
54    default: present
55  ttl:
56    description:
57      - In seconds, set a container-wide TTL for all objects cached on CDN edge nodes.
58        Setting a TTL is only appropriate for containers that are public
59  type:
60    description:
61      - Type of object to do work on, i.e. metadata object or a container object
62    choices:
63      - file
64      - meta
65    default: file
66  web_error:
67    description:
68       - Sets an object to be presented as the HTTP error page when accessed by the CDN URL
69  web_index:
70    description:
71       - Sets an object to be presented as the HTTP index page when accessed by the CDN URL
72author: "Paul Durivage (@angstwad)"
73extends_documentation_fragment:
74  - rackspace
75  - rackspace.openstack
76'''
77
78EXAMPLES = '''
79- name: "Test Cloud Files Containers"
80  hosts: local
81  gather_facts: no
82  tasks:
83    - name: "List all containers"
84      rax_files:
85        state: list
86
87    - name: "Create container called 'mycontainer'"
88      rax_files:
89        container: mycontainer
90
91    - name: "Create container 'mycontainer2' with metadata"
92      rax_files:
93        container: mycontainer2
94        meta:
95          key: value
96          file_for: someuser@example.com
97
98    - name: "Set a container's web index page"
99      rax_files:
100        container: mycontainer
101        web_index: index.html
102
103    - name: "Set a container's web error page"
104      rax_files:
105        container: mycontainer
106        web_error: error.html
107
108    - name: "Make container public"
109      rax_files:
110        container: mycontainer
111        public: yes
112
113    - name: "Make container public with a 24 hour TTL"
114      rax_files:
115        container: mycontainer
116        public: yes
117        ttl: 86400
118
119    - name: "Make container private"
120      rax_files:
121        container: mycontainer
122        private: yes
123
124- name: "Test Cloud Files Containers Metadata Storage"
125  hosts: local
126  gather_facts: no
127  tasks:
128    - name: "Get mycontainer2 metadata"
129      rax_files:
130        container: mycontainer2
131        type: meta
132
133    - name: "Set mycontainer2 metadata"
134      rax_files:
135        container: mycontainer2
136        type: meta
137        meta:
138          uploaded_by: someuser@example.com
139
140    - name: "Remove mycontainer2 metadata"
141      rax_files:
142        container: "mycontainer2"
143        type: meta
144        state: absent
145        meta:
146          key: ""
147          file_for: ""
148'''
149
150try:
151    import pyrax
152    HAS_PYRAX = True
153except ImportError as e:
154    HAS_PYRAX = False
155
156from ansible.module_utils.basic import AnsibleModule
157from ansible.module_utils.rax import rax_argument_spec, rax_required_together, setup_rax_module
158
159
160EXIT_DICT = dict(success=True)
161META_PREFIX = 'x-container-meta-'
162
163
164def _get_container(module, cf, container):
165    try:
166        return cf.get_container(container)
167    except pyrax.exc.NoSuchContainer as e:
168        module.fail_json(msg=e.message)
169
170
171def _fetch_meta(module, container):
172    EXIT_DICT['meta'] = dict()
173    try:
174        for k, v in container.get_metadata().items():
175            split_key = k.split(META_PREFIX)[-1]
176            EXIT_DICT['meta'][split_key] = v
177    except Exception as e:
178        module.fail_json(msg=e.message)
179
180
181def meta(cf, module, container_, state, meta_, clear_meta):
182    c = _get_container(module, cf, container_)
183
184    if meta_ and state == 'present':
185        try:
186            meta_set = c.set_metadata(meta_, clear=clear_meta)
187        except Exception as e:
188            module.fail_json(msg=e.message)
189    elif meta_ and state == 'absent':
190        remove_results = []
191        for k, v in meta_.items():
192            c.remove_metadata_key(k)
193            remove_results.append(k)
194            EXIT_DICT['deleted_meta_keys'] = remove_results
195    elif state == 'absent':
196        remove_results = []
197        for k, v in c.get_metadata().items():
198            c.remove_metadata_key(k)
199            remove_results.append(k)
200            EXIT_DICT['deleted_meta_keys'] = remove_results
201
202    _fetch_meta(module, c)
203    _locals = locals().keys()
204
205    EXIT_DICT['container'] = c.name
206    if 'meta_set' in _locals or 'remove_results' in _locals:
207        EXIT_DICT['changed'] = True
208
209    module.exit_json(**EXIT_DICT)
210
211
212def container(cf, module, container_, state, meta_, clear_meta, ttl, public,
213              private, web_index, web_error):
214    if public and private:
215        module.fail_json(msg='container cannot be simultaneously '
216                             'set to public and private')
217
218    if state == 'absent' and (meta_ or clear_meta or public or private or web_index or web_error):
219        module.fail_json(msg='state cannot be omitted when setting/removing '
220                             'attributes on a container')
221
222    if state == 'list':
223        # We don't care if attributes are specified, let's list containers
224        EXIT_DICT['containers'] = cf.list_containers()
225        module.exit_json(**EXIT_DICT)
226
227    try:
228        c = cf.get_container(container_)
229    except pyrax.exc.NoSuchContainer as e:
230        # Make the container if state=present, otherwise bomb out
231        if state == 'present':
232            try:
233                c = cf.create_container(container_)
234            except Exception as e:
235                module.fail_json(msg=e.message)
236            else:
237                EXIT_DICT['changed'] = True
238                EXIT_DICT['created'] = True
239        else:
240            module.fail_json(msg=e.message)
241    else:
242        # Successfully grabbed a container object
243        # Delete if state is absent
244        if state == 'absent':
245            try:
246                cont_deleted = c.delete()
247            except Exception as e:
248                module.fail_json(msg=e.message)
249            else:
250                EXIT_DICT['deleted'] = True
251
252    if meta_:
253        try:
254            meta_set = c.set_metadata(meta_, clear=clear_meta)
255        except Exception as e:
256            module.fail_json(msg=e.message)
257        finally:
258            _fetch_meta(module, c)
259
260    if ttl:
261        try:
262            c.cdn_ttl = ttl
263        except Exception as e:
264            module.fail_json(msg=e.message)
265        else:
266            EXIT_DICT['ttl'] = c.cdn_ttl
267
268    if public:
269        try:
270            cont_public = c.make_public()
271        except Exception as e:
272            module.fail_json(msg=e.message)
273        else:
274            EXIT_DICT['container_urls'] = dict(url=c.cdn_uri,
275                                               ssl_url=c.cdn_ssl_uri,
276                                               streaming_url=c.cdn_streaming_uri,
277                                               ios_uri=c.cdn_ios_uri)
278
279    if private:
280        try:
281            cont_private = c.make_private()
282        except Exception as e:
283            module.fail_json(msg=e.message)
284        else:
285            EXIT_DICT['set_private'] = True
286
287    if web_index:
288        try:
289            cont_web_index = c.set_web_index_page(web_index)
290        except Exception as e:
291            module.fail_json(msg=e.message)
292        else:
293            EXIT_DICT['set_index'] = True
294        finally:
295            _fetch_meta(module, c)
296
297    if web_error:
298        try:
299            cont_err_index = c.set_web_error_page(web_error)
300        except Exception as e:
301            module.fail_json(msg=e.message)
302        else:
303            EXIT_DICT['set_error'] = True
304        finally:
305            _fetch_meta(module, c)
306
307    EXIT_DICT['container'] = c.name
308    EXIT_DICT['objs_in_container'] = c.object_count
309    EXIT_DICT['total_bytes'] = c.total_bytes
310
311    _locals = locals().keys()
312    if ('cont_deleted' in _locals
313            or 'meta_set' in _locals
314            or 'cont_public' in _locals
315            or 'cont_private' in _locals
316            or 'cont_web_index' in _locals
317            or 'cont_err_index' in _locals):
318        EXIT_DICT['changed'] = True
319
320    module.exit_json(**EXIT_DICT)
321
322
323def cloudfiles(module, container_, state, meta_, clear_meta, typ, ttl, public,
324               private, web_index, web_error):
325    """ Dispatch from here to work with metadata or file objects """
326    cf = pyrax.cloudfiles
327
328    if cf is None:
329        module.fail_json(msg='Failed to instantiate client. This '
330                             'typically indicates an invalid region or an '
331                             'incorrectly capitalized region name.')
332
333    if typ == "container":
334        container(cf, module, container_, state, meta_, clear_meta, ttl,
335                  public, private, web_index, web_error)
336    else:
337        meta(cf, module, container_, state, meta_, clear_meta)
338
339
340def main():
341    argument_spec = rax_argument_spec()
342    argument_spec.update(
343        dict(
344            container=dict(),
345            state=dict(choices=['present', 'absent', 'list'],
346                       default='present'),
347            meta=dict(type='dict', default=dict()),
348            clear_meta=dict(default=False, type='bool'),
349            type=dict(choices=['container', 'meta'], default='container'),
350            ttl=dict(type='int'),
351            public=dict(default=False, type='bool'),
352            private=dict(default=False, type='bool'),
353            web_index=dict(),
354            web_error=dict()
355        )
356    )
357
358    module = AnsibleModule(
359        argument_spec=argument_spec,
360        required_together=rax_required_together()
361    )
362
363    if not HAS_PYRAX:
364        module.fail_json(msg='pyrax is required for this module')
365
366    container_ = module.params.get('container')
367    state = module.params.get('state')
368    meta_ = module.params.get('meta')
369    clear_meta = module.params.get('clear_meta')
370    typ = module.params.get('type')
371    ttl = module.params.get('ttl')
372    public = module.params.get('public')
373    private = module.params.get('private')
374    web_index = module.params.get('web_index')
375    web_error = module.params.get('web_error')
376
377    if state in ['present', 'absent'] and not container_:
378        module.fail_json(msg='please specify a container name')
379    if clear_meta and not typ == 'meta':
380        module.fail_json(msg='clear_meta can only be used when setting '
381                             'metadata')
382
383    setup_rax_module(module, pyrax)
384    cloudfiles(module, container_, state, meta_, clear_meta, typ, ttl, public,
385               private, web_index, web_error)
386
387
388if __name__ == '__main__':
389    main()
390