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