1# -*- coding: utf-8 -*-
2# This code is part of Ansible, but is an independent component.
3# This particular file snippet, and this file snippet only, is BSD licensed.
4# Modules you write using this snippet, which is embedded dynamically by
5# Ansible still belong to the author of the module, and may assign their own
6# license to the complete work.
7#
8# Copyright (c), Michael DeHaan <michael.dehaan@gmail.com>, 2012-2013
9#
10# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
11
12from __future__ import (absolute_import, division, print_function)
13__metaclass__ = type
14
15
16import os
17import re
18from uuid import UUID
19
20from ansible.module_utils.six import text_type, binary_type
21
22FINAL_STATUSES = ('ACTIVE', 'ERROR')
23VOLUME_STATUS = ('available', 'attaching', 'creating', 'deleting', 'in-use',
24                 'error', 'error_deleting')
25
26CLB_ALGORITHMS = ['RANDOM', 'LEAST_CONNECTIONS', 'ROUND_ROBIN',
27                  'WEIGHTED_LEAST_CONNECTIONS', 'WEIGHTED_ROUND_ROBIN']
28CLB_PROTOCOLS = ['DNS_TCP', 'DNS_UDP', 'FTP', 'HTTP', 'HTTPS', 'IMAPS',
29                 'IMAPv4', 'LDAP', 'LDAPS', 'MYSQL', 'POP3', 'POP3S', 'SMTP',
30                 'TCP', 'TCP_CLIENT_FIRST', 'UDP', 'UDP_STREAM', 'SFTP']
31
32NON_CALLABLES = (text_type, binary_type, bool, dict, int, list, type(None))
33PUBLIC_NET_ID = "00000000-0000-0000-0000-000000000000"
34SERVICE_NET_ID = "11111111-1111-1111-1111-111111111111"
35
36
37def rax_slugify(value):
38    """Prepend a key with rax_ and normalize the key name"""
39    return 'rax_%s' % (re.sub(r'[^\w-]', '_', value).lower().lstrip('_'))
40
41
42def rax_clb_node_to_dict(obj):
43    """Function to convert a CLB Node object to a dict"""
44    if not obj:
45        return {}
46    node = obj.to_dict()
47    node['id'] = obj.id
48    node['weight'] = obj.weight
49    return node
50
51
52def rax_to_dict(obj, obj_type='standard'):
53    """Generic function to convert a pyrax object to a dict
54
55    obj_type values:
56        standard
57        clb
58        server
59
60    """
61    instance = {}
62    for key in dir(obj):
63        value = getattr(obj, key)
64        if obj_type == 'clb' and key == 'nodes':
65            instance[key] = []
66            for node in value:
67                instance[key].append(rax_clb_node_to_dict(node))
68        elif (isinstance(value, list) and len(value) > 0 and
69                not isinstance(value[0], NON_CALLABLES)):
70            instance[key] = []
71            for item in value:
72                instance[key].append(rax_to_dict(item))
73        elif (isinstance(value, NON_CALLABLES) and not key.startswith('_')):
74            if obj_type == 'server':
75                if key == 'image':
76                    if not value:
77                        instance['rax_boot_source'] = 'volume'
78                    else:
79                        instance['rax_boot_source'] = 'local'
80                key = rax_slugify(key)
81            instance[key] = value
82
83    if obj_type == 'server':
84        for attr in ['id', 'accessIPv4', 'name', 'status']:
85            instance[attr] = instance.get(rax_slugify(attr))
86
87    return instance
88
89
90def rax_find_bootable_volume(module, rax_module, server, exit=True):
91    """Find a servers bootable volume"""
92    cs = rax_module.cloudservers
93    cbs = rax_module.cloud_blockstorage
94    server_id = rax_module.utils.get_id(server)
95    volumes = cs.volumes.get_server_volumes(server_id)
96    bootable_volumes = []
97    for volume in volumes:
98        vol = cbs.get(volume)
99        if module.boolean(vol.bootable):
100            bootable_volumes.append(vol)
101    if not bootable_volumes:
102        if exit:
103            module.fail_json(msg='No bootable volumes could be found for '
104                                 'server %s' % server_id)
105        else:
106            return False
107    elif len(bootable_volumes) > 1:
108        if exit:
109            module.fail_json(msg='Multiple bootable volumes found for server '
110                                 '%s' % server_id)
111        else:
112            return False
113
114    return bootable_volumes[0]
115
116
117def rax_find_image(module, rax_module, image, exit=True):
118    """Find a server image by ID or Name"""
119    cs = rax_module.cloudservers
120    try:
121        UUID(image)
122    except ValueError:
123        try:
124            image = cs.images.find(human_id=image)
125        except(cs.exceptions.NotFound,
126               cs.exceptions.NoUniqueMatch):
127            try:
128                image = cs.images.find(name=image)
129            except (cs.exceptions.NotFound,
130                    cs.exceptions.NoUniqueMatch):
131                if exit:
132                    module.fail_json(msg='No matching image found (%s)' %
133                                         image)
134                else:
135                    return False
136
137    return rax_module.utils.get_id(image)
138
139
140def rax_find_volume(module, rax_module, name):
141    """Find a Block storage volume by ID or name"""
142    cbs = rax_module.cloud_blockstorage
143    try:
144        UUID(name)
145        volume = cbs.get(name)
146    except ValueError:
147        try:
148            volume = cbs.find(name=name)
149        except rax_module.exc.NotFound:
150            volume = None
151        except Exception as e:
152            module.fail_json(msg='%s' % e)
153    return volume
154
155
156def rax_find_network(module, rax_module, network):
157    """Find a cloud network by ID or name"""
158    cnw = rax_module.cloud_networks
159    try:
160        UUID(network)
161    except ValueError:
162        if network.lower() == 'public':
163            return cnw.get_server_networks(PUBLIC_NET_ID)
164        elif network.lower() == 'private':
165            return cnw.get_server_networks(SERVICE_NET_ID)
166        else:
167            try:
168                network_obj = cnw.find_network_by_label(network)
169            except (rax_module.exceptions.NetworkNotFound,
170                    rax_module.exceptions.NetworkLabelNotUnique):
171                module.fail_json(msg='No matching network found (%s)' %
172                                     network)
173            else:
174                return cnw.get_server_networks(network_obj)
175    else:
176        return cnw.get_server_networks(network)
177
178
179def rax_find_server(module, rax_module, server):
180    """Find a Cloud Server by ID or name"""
181    cs = rax_module.cloudservers
182    try:
183        UUID(server)
184        server = cs.servers.get(server)
185    except ValueError:
186        servers = cs.servers.list(search_opts=dict(name='^%s$' % server))
187        if not servers:
188            module.fail_json(msg='No Server was matched by name, '
189                                 'try using the Server ID instead')
190        if len(servers) > 1:
191            module.fail_json(msg='Multiple servers matched by name, '
192                                 'try using the Server ID instead')
193
194        # We made it this far, grab the first and hopefully only server
195        # in the list
196        server = servers[0]
197    return server
198
199
200def rax_find_loadbalancer(module, rax_module, loadbalancer):
201    """Find a Cloud Load Balancer by ID or name"""
202    clb = rax_module.cloud_loadbalancers
203    try:
204        found = clb.get(loadbalancer)
205    except Exception:
206        found = []
207        for lb in clb.list():
208            if loadbalancer == lb.name:
209                found.append(lb)
210
211        if not found:
212            module.fail_json(msg='No loadbalancer was matched')
213
214        if len(found) > 1:
215            module.fail_json(msg='Multiple loadbalancers matched')
216
217        # We made it this far, grab the first and hopefully only item
218        # in the list
219        found = found[0]
220
221    return found
222
223
224def rax_argument_spec():
225    """Return standard base dictionary used for the argument_spec
226    argument in AnsibleModule
227
228    """
229    return dict(
230        api_key=dict(type='str', aliases=['password'], no_log=True),
231        auth_endpoint=dict(type='str'),
232        credentials=dict(type='path', aliases=['creds_file']),
233        env=dict(type='str'),
234        identity_type=dict(type='str', default='rackspace'),
235        region=dict(type='str'),
236        tenant_id=dict(type='str'),
237        tenant_name=dict(type='str'),
238        username=dict(type='str'),
239        validate_certs=dict(type='bool', aliases=['verify_ssl']),
240    )
241
242
243def rax_required_together():
244    """Return the default list used for the required_together argument to
245    AnsibleModule"""
246    return [['api_key', 'username']]
247
248
249def setup_rax_module(module, rax_module, region_required=True):
250    """Set up pyrax in a standard way for all modules"""
251    rax_module.USER_AGENT = 'ansible/%s %s' % (module.ansible_version,
252                                               rax_module.USER_AGENT)
253
254    api_key = module.params.get('api_key')
255    auth_endpoint = module.params.get('auth_endpoint')
256    credentials = module.params.get('credentials')
257    env = module.params.get('env')
258    identity_type = module.params.get('identity_type')
259    region = module.params.get('region')
260    tenant_id = module.params.get('tenant_id')
261    tenant_name = module.params.get('tenant_name')
262    username = module.params.get('username')
263    verify_ssl = module.params.get('validate_certs')
264
265    if env is not None:
266        rax_module.set_environment(env)
267
268    rax_module.set_setting('identity_type', identity_type)
269    if verify_ssl is not None:
270        rax_module.set_setting('verify_ssl', verify_ssl)
271    if auth_endpoint is not None:
272        rax_module.set_setting('auth_endpoint', auth_endpoint)
273    if tenant_id is not None:
274        rax_module.set_setting('tenant_id', tenant_id)
275    if tenant_name is not None:
276        rax_module.set_setting('tenant_name', tenant_name)
277
278    try:
279        username = username or os.environ.get('RAX_USERNAME')
280        if not username:
281            username = rax_module.get_setting('keyring_username')
282            if username:
283                api_key = 'USE_KEYRING'
284        if not api_key:
285            api_key = os.environ.get('RAX_API_KEY')
286        credentials = (credentials or os.environ.get('RAX_CREDENTIALS') or
287                       os.environ.get('RAX_CREDS_FILE'))
288        region = (region or os.environ.get('RAX_REGION') or
289                  rax_module.get_setting('region'))
290    except KeyError as e:
291        module.fail_json(msg='Unable to load %s' % e.message)
292
293    try:
294        if api_key and username:
295            if api_key == 'USE_KEYRING':
296                rax_module.keyring_auth(username, region=region)
297            else:
298                rax_module.set_credentials(username, api_key=api_key,
299                                           region=region)
300        elif credentials:
301            credentials = os.path.expanduser(credentials)
302            rax_module.set_credential_file(credentials, region=region)
303        else:
304            raise Exception('No credentials supplied!')
305    except Exception as e:
306        if e.message:
307            msg = str(e.message)
308        else:
309            msg = repr(e)
310        module.fail_json(msg=msg)
311
312    if region_required and region not in rax_module.regions:
313        module.fail_json(msg='%s is not a valid region, must be one of: %s' %
314                         (region, ','.join(rax_module.regions)))
315
316    return rax_module
317