1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3# Copyright: Ansible Project
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
10DOCUMENTATION = '''
11---
12module: rax
13short_description: create / delete an instance in Rackspace Public Cloud
14description:
15     - creates / deletes a Rackspace Public Cloud instance and optionally
16       waits for it to be 'running'.
17options:
18  auto_increment:
19    description:
20      - Whether or not to increment a single number with the name of the
21        created servers. Only applicable when used with the I(group) attribute
22        or meta key.
23    type: bool
24    default: 'yes'
25  boot_from_volume:
26    description:
27      - Whether or not to boot the instance from a Cloud Block Storage volume.
28        If C(yes) and I(image) is specified a new volume will be created at
29        boot time. I(boot_volume_size) is required with I(image) to create a
30        new volume at boot time.
31    type: bool
32    default: 'no'
33  boot_volume:
34    type: str
35    description:
36      - Cloud Block Storage ID or Name to use as the boot volume of the
37        instance
38  boot_volume_size:
39    type: int
40    description:
41      - Size of the volume to create in Gigabytes. This is only required with
42        I(image) and I(boot_from_volume).
43    default: 100
44  boot_volume_terminate:
45    description:
46      - Whether the I(boot_volume) or newly created volume from I(image) will
47        be terminated when the server is terminated
48    type: bool
49    default: 'no'
50  config_drive:
51    description:
52      - Attach read-only configuration drive to server as label config-2
53    type: bool
54    default: 'no'
55  count:
56    type: int
57    description:
58      - number of instances to launch
59    default: 1
60  count_offset:
61    type: int
62    description:
63      - number count to start at
64    default: 1
65  disk_config:
66    type: str
67    description:
68      - Disk partitioning strategy
69      - If not specified it will assume the value C(auto).
70    choices:
71      - auto
72      - manual
73  exact_count:
74    description:
75      - Explicitly ensure an exact count of instances, used with
76        state=active/present. If specified as C(yes) and I(count) is less than
77        the servers matched, servers will be deleted to match the count. If
78        the number of matched servers is fewer than specified in I(count)
79        additional servers will be added.
80    type: bool
81    default: 'no'
82  extra_client_args:
83    type: dict
84    description:
85      - A hash of key/value pairs to be used when creating the cloudservers
86        client. This is considered an advanced option, use it wisely and
87        with caution.
88  extra_create_args:
89    type: dict
90    description:
91      - A hash of key/value pairs to be used when creating a new server.
92        This is considered an advanced option, use it wisely and with caution.
93  files:
94    type: dict
95    description:
96      - Files to insert into the instance. remotefilename:localcontent
97  flavor:
98    type: str
99    description:
100      - flavor to use for the instance
101  group:
102    type: str
103    description:
104      - host group to assign to server, is also used for idempotent operations
105        to ensure a specific number of instances
106  image:
107    type: str
108    description:
109      - image to use for the instance. Can be an C(id), C(human_id) or C(name).
110        With I(boot_from_volume), a Cloud Block Storage volume will be created
111        with this image
112  instance_ids:
113    type: list
114    elements: str
115    description:
116      - list of instance ids, currently only used when state='absent' to
117        remove instances
118  key_name:
119    type: str
120    description:
121      - key pair to use on the instance
122    aliases:
123      - keypair
124  meta:
125    type: dict
126    description:
127      - A hash of metadata to associate with the instance
128  name:
129    type: str
130    description:
131      - Name to give the instance
132  networks:
133    type: list
134    elements: str
135    description:
136      - The network to attach to the instances. If specified, you must include
137        ALL networks including the public and private interfaces. Can be C(id)
138        or C(label).
139    default:
140      - public
141      - private
142  state:
143    type: str
144    description:
145      - Indicate desired state of the resource
146    choices:
147      - present
148      - absent
149    default: present
150  user_data:
151    type: str
152    description:
153      - Data to be uploaded to the servers config drive. This option implies
154        I(config_drive). Can be a file path or a string
155  wait:
156    description:
157      - wait for the instance to be in state 'running' before returning
158    type: bool
159    default: 'no'
160  wait_timeout:
161    type: int
162    description:
163      - how long before wait gives up, in seconds
164    default: 300
165author:
166    - "Jesse Keating (@omgjlk)"
167    - "Matt Martz (@sivel)"
168notes:
169  - I(exact_count) can be "destructive" if the number of running servers in
170    the I(group) is larger than that specified in I(count). In such a case, the
171    I(state) is effectively set to C(absent) and the extra servers are deleted.
172    In the case of deletion, the returned data structure will have C(action)
173    set to C(delete), and the oldest servers in the group will be deleted.
174extends_documentation_fragment:
175- community.general.rackspace.openstack
176
177'''
178
179EXAMPLES = '''
180- name: Build a Cloud Server
181  gather_facts: False
182  tasks:
183    - name: Server build request
184      local_action:
185        module: rax
186        credentials: ~/.raxpub
187        name: rax-test1
188        flavor: 5
189        image: b11d9567-e412-4255-96b9-bd63ab23bcfe
190        key_name: my_rackspace_key
191        files:
192          /root/test.txt: /home/localuser/test.txt
193        wait: yes
194        state: present
195        networks:
196          - private
197          - public
198      register: rax
199
200- name: Build an exact count of cloud servers with incremented names
201  hosts: local
202  gather_facts: False
203  tasks:
204    - name: Server build requests
205      local_action:
206        module: rax
207        credentials: ~/.raxpub
208        name: test%03d.example.org
209        flavor: performance1-1
210        image: ubuntu-1204-lts-precise-pangolin
211        state: present
212        count: 10
213        count_offset: 10
214        exact_count: yes
215        group: test
216        wait: yes
217      register: rax
218'''
219
220import json
221import os
222import re
223import time
224
225try:
226    import pyrax
227    HAS_PYRAX = True
228except ImportError:
229    HAS_PYRAX = False
230
231from ansible.module_utils.basic import AnsibleModule
232from ansible_collections.community.general.plugins.module_utils.rax import (FINAL_STATUSES, rax_argument_spec, rax_find_bootable_volume,
233                                                                            rax_find_image, rax_find_network, rax_find_volume,
234                                                                            rax_required_together, rax_to_dict, setup_rax_module)
235from ansible.module_utils.six.moves import xrange
236from ansible.module_utils.six import string_types
237
238
239def rax_find_server_image(module, server, image, boot_volume):
240    if not image and boot_volume:
241        vol = rax_find_bootable_volume(module, pyrax, server,
242                                       exit=False)
243        if not vol:
244            return None
245        volume_image_metadata = vol.volume_image_metadata
246        vol_image_id = volume_image_metadata.get('image_id')
247        if vol_image_id:
248            server_image = rax_find_image(module, pyrax,
249                                          vol_image_id, exit=False)
250            if server_image:
251                server.image = dict(id=server_image)
252
253    # Match image IDs taking care of boot from volume
254    if image and not server.image:
255        vol = rax_find_bootable_volume(module, pyrax, server)
256        volume_image_metadata = vol.volume_image_metadata
257        vol_image_id = volume_image_metadata.get('image_id')
258        if not vol_image_id:
259            return None
260        server_image = rax_find_image(module, pyrax,
261                                      vol_image_id, exit=False)
262        if image != server_image:
263            return None
264
265        server.image = dict(id=server_image)
266    elif image and server.image['id'] != image:
267        return None
268
269    return server.image
270
271
272def create(module, names=None, flavor=None, image=None, meta=None, key_name=None,
273           files=None, wait=True, wait_timeout=300, disk_config=None,
274           group=None, nics=None, extra_create_args=None, user_data=None,
275           config_drive=False, existing=None, block_device_mapping_v2=None):
276    names = [] if names is None else names
277    meta = {} if meta is None else meta
278    files = {} if files is None else files
279    nics = [] if nics is None else nics
280    extra_create_args = {} if extra_create_args is None else extra_create_args
281    existing = [] if existing is None else existing
282    block_device_mapping_v2 = [] if block_device_mapping_v2 is None else block_device_mapping_v2
283
284    cs = pyrax.cloudservers
285    changed = False
286
287    if user_data:
288        config_drive = True
289
290    if user_data and os.path.isfile(os.path.expanduser(user_data)):
291        try:
292            user_data = os.path.expanduser(user_data)
293            f = open(user_data)
294            user_data = f.read()
295            f.close()
296        except Exception as e:
297            module.fail_json(msg='Failed to load %s' % user_data)
298
299    # Handle the file contents
300    for rpath in files.keys():
301        lpath = os.path.expanduser(files[rpath])
302        try:
303            fileobj = open(lpath, 'r')
304            files[rpath] = fileobj.read()
305            fileobj.close()
306        except Exception as e:
307            module.fail_json(msg='Failed to load %s' % lpath)
308    try:
309        servers = []
310        bdmv2 = block_device_mapping_v2
311        for name in names:
312            servers.append(cs.servers.create(name=name, image=image,
313                                             flavor=flavor, meta=meta,
314                                             key_name=key_name,
315                                             files=files, nics=nics,
316                                             disk_config=disk_config,
317                                             config_drive=config_drive,
318                                             userdata=user_data,
319                                             block_device_mapping_v2=bdmv2,
320                                             **extra_create_args))
321    except Exception as e:
322        if e.message:
323            msg = str(e.message)
324        else:
325            msg = repr(e)
326        module.fail_json(msg=msg)
327    else:
328        changed = True
329
330    if wait:
331        end_time = time.time() + wait_timeout
332        infinite = wait_timeout == 0
333        while infinite or time.time() < end_time:
334            for server in servers:
335                try:
336                    server.get()
337                except Exception:
338                    server.status = 'ERROR'
339
340            if not filter(lambda s: s.status not in FINAL_STATUSES,
341                          servers):
342                break
343            time.sleep(5)
344
345    success = []
346    error = []
347    timeout = []
348    for server in servers:
349        try:
350            server.get()
351        except Exception:
352            server.status = 'ERROR'
353        instance = rax_to_dict(server, 'server')
354        if server.status == 'ACTIVE' or not wait:
355            success.append(instance)
356        elif server.status == 'ERROR':
357            error.append(instance)
358        elif wait:
359            timeout.append(instance)
360
361    untouched = [rax_to_dict(s, 'server') for s in existing]
362    instances = success + untouched
363
364    results = {
365        'changed': changed,
366        'action': 'create',
367        'instances': instances,
368        'success': success,
369        'error': error,
370        'timeout': timeout,
371        'instance_ids': {
372            'instances': [i['id'] for i in instances],
373            'success': [i['id'] for i in success],
374            'error': [i['id'] for i in error],
375            'timeout': [i['id'] for i in timeout]
376        }
377    }
378
379    if timeout:
380        results['msg'] = 'Timeout waiting for all servers to build'
381    elif error:
382        results['msg'] = 'Failed to build all servers'
383
384    if 'msg' in results:
385        module.fail_json(**results)
386    else:
387        module.exit_json(**results)
388
389
390def delete(module, instance_ids=None, wait=True, wait_timeout=300, kept=None):
391    instance_ids = [] if instance_ids is None else instance_ids
392    kept = [] if kept is None else kept
393
394    cs = pyrax.cloudservers
395
396    changed = False
397    instances = {}
398    servers = []
399
400    for instance_id in instance_ids:
401        servers.append(cs.servers.get(instance_id))
402
403    for server in servers:
404        try:
405            server.delete()
406        except Exception as e:
407            module.fail_json(msg=e.message)
408        else:
409            changed = True
410
411        instance = rax_to_dict(server, 'server')
412        instances[instance['id']] = instance
413
414    # If requested, wait for server deletion
415    if wait:
416        end_time = time.time() + wait_timeout
417        infinite = wait_timeout == 0
418        while infinite or time.time() < end_time:
419            for server in servers:
420                instance_id = server.id
421                try:
422                    server.get()
423                except Exception:
424                    instances[instance_id]['status'] = 'DELETED'
425                    instances[instance_id]['rax_status'] = 'DELETED'
426
427            if not filter(lambda s: s['status'] not in ('', 'DELETED',
428                                                        'ERROR'),
429                          instances.values()):
430                break
431
432            time.sleep(5)
433
434    timeout = filter(lambda s: s['status'] not in ('', 'DELETED', 'ERROR'),
435                     instances.values())
436    error = filter(lambda s: s['status'] in ('ERROR'),
437                   instances.values())
438    success = filter(lambda s: s['status'] in ('', 'DELETED'),
439                     instances.values())
440
441    instances = [rax_to_dict(s, 'server') for s in kept]
442
443    results = {
444        'changed': changed,
445        'action': 'delete',
446        'instances': instances,
447        'success': success,
448        'error': error,
449        'timeout': timeout,
450        'instance_ids': {
451            'instances': [i['id'] for i in instances],
452            'success': [i['id'] for i in success],
453            'error': [i['id'] for i in error],
454            'timeout': [i['id'] for i in timeout]
455        }
456    }
457
458    if timeout:
459        results['msg'] = 'Timeout waiting for all servers to delete'
460    elif error:
461        results['msg'] = 'Failed to delete all servers'
462
463    if 'msg' in results:
464        module.fail_json(**results)
465    else:
466        module.exit_json(**results)
467
468
469def cloudservers(module, state=None, name=None, flavor=None, image=None,
470                 meta=None, key_name=None, files=None, wait=True, wait_timeout=300,
471                 disk_config=None, count=1, group=None, instance_ids=None,
472                 exact_count=False, networks=None, count_offset=0,
473                 auto_increment=False, extra_create_args=None, user_data=None,
474                 config_drive=False, boot_from_volume=False,
475                 boot_volume=None, boot_volume_size=None,
476                 boot_volume_terminate=False):
477    meta = {} if meta is None else meta
478    files = {} if files is None else files
479    instance_ids = [] if instance_ids is None else instance_ids
480    networks = [] if networks is None else networks
481    extra_create_args = {} if extra_create_args is None else extra_create_args
482
483    cs = pyrax.cloudservers
484    cnw = pyrax.cloud_networks
485    if not cnw:
486        module.fail_json(msg='Failed to instantiate client. This '
487                             'typically indicates an invalid region or an '
488                             'incorrectly capitalized region name.')
489
490    if state == 'present' or (state == 'absent' and instance_ids is None):
491        if not boot_from_volume and not boot_volume and not image:
492            module.fail_json(msg='image is required for the "rax" module')
493
494        for arg, value in dict(name=name, flavor=flavor).items():
495            if not value:
496                module.fail_json(msg='%s is required for the "rax" module' %
497                                     arg)
498
499        if boot_from_volume and not image and not boot_volume:
500            module.fail_json(msg='image or boot_volume are required for the '
501                                 '"rax" with boot_from_volume')
502
503        if boot_from_volume and image and not boot_volume_size:
504            module.fail_json(msg='boot_volume_size is required for the "rax" '
505                                 'module with boot_from_volume and image')
506
507        if boot_from_volume and image and boot_volume:
508            image = None
509
510    servers = []
511
512    # Add the group meta key
513    if group and 'group' not in meta:
514        meta['group'] = group
515    elif 'group' in meta and group is None:
516        group = meta['group']
517
518    # Normalize and ensure all metadata values are strings
519    for k, v in meta.items():
520        if isinstance(v, list):
521            meta[k] = ','.join(['%s' % i for i in v])
522        elif isinstance(v, dict):
523            meta[k] = json.dumps(v)
524        elif not isinstance(v, string_types):
525            meta[k] = '%s' % v
526
527    # When using state=absent with group, the absent block won't match the
528    # names properly. Use the exact_count functionality to decrease the count
529    # to the desired level
530    was_absent = False
531    if group is not None and state == 'absent':
532        exact_count = True
533        state = 'present'
534        was_absent = True
535
536    if image:
537        image = rax_find_image(module, pyrax, image)
538
539    nics = []
540    if networks:
541        for network in networks:
542            nics.extend(rax_find_network(module, pyrax, network))
543
544    # act on the state
545    if state == 'present':
546        # Idempotent ensurance of a specific count of servers
547        if exact_count is not False:
548            # See if we can find servers that match our options
549            if group is None:
550                module.fail_json(msg='"group" must be provided when using '
551                                     '"exact_count"')
552
553            if auto_increment:
554                numbers = set()
555
556                # See if the name is a printf like string, if not append
557                # %d to the end
558                try:
559                    name % 0
560                except TypeError as e:
561                    if e.message.startswith('not all'):
562                        name = '%s%%d' % name
563                    else:
564                        module.fail_json(msg=e.message)
565
566                # regex pattern to match printf formatting
567                pattern = re.sub(r'%\d*[sd]', r'(\d+)', name)
568                for server in cs.servers.list():
569                    # Ignore DELETED servers
570                    if server.status == 'DELETED':
571                        continue
572                    if server.metadata.get('group') == group:
573                        servers.append(server)
574                    match = re.search(pattern, server.name)
575                    if match:
576                        number = int(match.group(1))
577                        numbers.add(number)
578
579                number_range = xrange(count_offset, count_offset + count)
580                available_numbers = list(set(number_range)
581                                         .difference(numbers))
582            else:  # Not auto incrementing
583                for server in cs.servers.list():
584                    # Ignore DELETED servers
585                    if server.status == 'DELETED':
586                        continue
587                    if server.metadata.get('group') == group:
588                        servers.append(server)
589                # available_numbers not needed here, we inspect auto_increment
590                # again later
591
592            # If state was absent but the count was changed,
593            # assume we only wanted to remove that number of instances
594            if was_absent:
595                diff = len(servers) - count
596                if diff < 0:
597                    count = 0
598                else:
599                    count = diff
600
601            if len(servers) > count:
602                # We have more servers than we need, set state='absent'
603                # and delete the extras, this should delete the oldest
604                state = 'absent'
605                kept = servers[:count]
606                del servers[:count]
607                instance_ids = []
608                for server in servers:
609                    instance_ids.append(server.id)
610                delete(module, instance_ids=instance_ids, wait=wait,
611                       wait_timeout=wait_timeout, kept=kept)
612            elif len(servers) < count:
613                # we have fewer servers than we need
614                if auto_increment:
615                    # auto incrementing server numbers
616                    names = []
617                    name_slice = count - len(servers)
618                    numbers_to_use = available_numbers[:name_slice]
619                    for number in numbers_to_use:
620                        names.append(name % number)
621                else:
622                    # We are not auto incrementing server numbers,
623                    # create a list of 'name' that matches how many we need
624                    names = [name] * (count - len(servers))
625            else:
626                # we have the right number of servers, just return info
627                # about all of the matched servers
628                instances = []
629                instance_ids = []
630                for server in servers:
631                    instances.append(rax_to_dict(server, 'server'))
632                    instance_ids.append(server.id)
633                module.exit_json(changed=False, action=None,
634                                 instances=instances,
635                                 success=[], error=[], timeout=[],
636                                 instance_ids={'instances': instance_ids,
637                                               'success': [], 'error': [],
638                                               'timeout': []})
639        else:  # not called with exact_count=True
640            if group is not None:
641                if auto_increment:
642                    # we are auto incrementing server numbers, but not with
643                    # exact_count
644                    numbers = set()
645
646                    # See if the name is a printf like string, if not append
647                    # %d to the end
648                    try:
649                        name % 0
650                    except TypeError as e:
651                        if e.message.startswith('not all'):
652                            name = '%s%%d' % name
653                        else:
654                            module.fail_json(msg=e.message)
655
656                    # regex pattern to match printf formatting
657                    pattern = re.sub(r'%\d*[sd]', r'(\d+)', name)
658                    for server in cs.servers.list():
659                        # Ignore DELETED servers
660                        if server.status == 'DELETED':
661                            continue
662                        if server.metadata.get('group') == group:
663                            servers.append(server)
664                        match = re.search(pattern, server.name)
665                        if match:
666                            number = int(match.group(1))
667                            numbers.add(number)
668
669                    number_range = xrange(count_offset,
670                                          count_offset + count + len(numbers))
671                    available_numbers = list(set(number_range)
672                                             .difference(numbers))
673                    names = []
674                    numbers_to_use = available_numbers[:count]
675                    for number in numbers_to_use:
676                        names.append(name % number)
677                else:
678                    # Not auto incrementing
679                    names = [name] * count
680            else:
681                # No group was specified, and not using exact_count
682                # Perform more simplistic matching
683                search_opts = {
684                    'name': '^%s$' % name,
685                    'flavor': flavor
686                }
687                servers = []
688                for server in cs.servers.list(search_opts=search_opts):
689                    # Ignore DELETED servers
690                    if server.status == 'DELETED':
691                        continue
692
693                    if not rax_find_server_image(module, server, image,
694                                                 boot_volume):
695                        continue
696
697                    # Ignore servers with non matching metadata
698                    if server.metadata != meta:
699                        continue
700                    servers.append(server)
701
702                if len(servers) >= count:
703                    # We have more servers than were requested, don't do
704                    # anything. Not running with exact_count=True, so we assume
705                    # more is OK
706                    instances = []
707                    for server in servers:
708                        instances.append(rax_to_dict(server, 'server'))
709
710                    instance_ids = [i['id'] for i in instances]
711                    module.exit_json(changed=False, action=None,
712                                     instances=instances, success=[], error=[],
713                                     timeout=[],
714                                     instance_ids={'instances': instance_ids,
715                                                   'success': [], 'error': [],
716                                                   'timeout': []})
717
718                # We need more servers to reach out target, create names for
719                # them, we aren't performing auto_increment here
720                names = [name] * (count - len(servers))
721
722        block_device_mapping_v2 = []
723        if boot_from_volume:
724            mapping = {
725                'boot_index': '0',
726                'delete_on_termination': boot_volume_terminate,
727                'destination_type': 'volume',
728            }
729            if image:
730                mapping.update({
731                    'uuid': image,
732                    'source_type': 'image',
733                    'volume_size': boot_volume_size,
734                })
735                image = None
736            elif boot_volume:
737                volume = rax_find_volume(module, pyrax, boot_volume)
738                mapping.update({
739                    'uuid': pyrax.utils.get_id(volume),
740                    'source_type': 'volume',
741                })
742            block_device_mapping_v2.append(mapping)
743
744        create(module, names=names, flavor=flavor, image=image,
745               meta=meta, key_name=key_name, files=files, wait=wait,
746               wait_timeout=wait_timeout, disk_config=disk_config, group=group,
747               nics=nics, extra_create_args=extra_create_args,
748               user_data=user_data, config_drive=config_drive,
749               existing=servers,
750               block_device_mapping_v2=block_device_mapping_v2)
751
752    elif state == 'absent':
753        if instance_ids is None:
754            # We weren't given an explicit list of server IDs to delete
755            # Let's match instead
756            search_opts = {
757                'name': '^%s$' % name,
758                'flavor': flavor
759            }
760            for server in cs.servers.list(search_opts=search_opts):
761                # Ignore DELETED servers
762                if server.status == 'DELETED':
763                    continue
764
765                if not rax_find_server_image(module, server, image,
766                                             boot_volume):
767                    continue
768
769                # Ignore servers with non matching metadata
770                if meta != server.metadata:
771                    continue
772
773                servers.append(server)
774
775            # Build a list of server IDs to delete
776            instance_ids = []
777            for server in servers:
778                if len(instance_ids) < count:
779                    instance_ids.append(server.id)
780                else:
781                    break
782
783        if not instance_ids:
784            # No server IDs were matched for deletion, or no IDs were
785            # explicitly provided, just exit and don't do anything
786            module.exit_json(changed=False, action=None, instances=[],
787                             success=[], error=[], timeout=[],
788                             instance_ids={'instances': [],
789                                           'success': [], 'error': [],
790                                           'timeout': []})
791
792        delete(module, instance_ids=instance_ids, wait=wait,
793               wait_timeout=wait_timeout)
794
795
796def main():
797    argument_spec = rax_argument_spec()
798    argument_spec.update(
799        dict(
800            auto_increment=dict(default=True, type='bool'),
801            boot_from_volume=dict(default=False, type='bool'),
802            boot_volume=dict(type='str'),
803            boot_volume_size=dict(type='int', default=100),
804            boot_volume_terminate=dict(type='bool', default=False),
805            config_drive=dict(default=False, type='bool'),
806            count=dict(default=1, type='int'),
807            count_offset=dict(default=1, type='int'),
808            disk_config=dict(choices=['auto', 'manual']),
809            exact_count=dict(default=False, type='bool'),
810            extra_client_args=dict(type='dict', default={}),
811            extra_create_args=dict(type='dict', default={}),
812            files=dict(type='dict', default={}),
813            flavor=dict(),
814            group=dict(),
815            image=dict(),
816            instance_ids=dict(type='list', elements='str'),
817            key_name=dict(aliases=['keypair']),
818            meta=dict(type='dict', default={}),
819            name=dict(),
820            networks=dict(type='list', elements='str', default=['public', 'private']),
821            state=dict(default='present', choices=['present', 'absent']),
822            user_data=dict(no_log=True),
823            wait=dict(default=False, type='bool'),
824            wait_timeout=dict(default=300, type='int'),
825        )
826    )
827
828    module = AnsibleModule(
829        argument_spec=argument_spec,
830        required_together=rax_required_together(),
831    )
832
833    if not HAS_PYRAX:
834        module.fail_json(msg='pyrax is required for this module')
835
836    auto_increment = module.params.get('auto_increment')
837    boot_from_volume = module.params.get('boot_from_volume')
838    boot_volume = module.params.get('boot_volume')
839    boot_volume_size = module.params.get('boot_volume_size')
840    boot_volume_terminate = module.params.get('boot_volume_terminate')
841    config_drive = module.params.get('config_drive')
842    count = module.params.get('count')
843    count_offset = module.params.get('count_offset')
844    disk_config = module.params.get('disk_config')
845    if disk_config:
846        disk_config = disk_config.upper()
847    exact_count = module.params.get('exact_count', False)
848    extra_client_args = module.params.get('extra_client_args')
849    extra_create_args = module.params.get('extra_create_args')
850    files = module.params.get('files')
851    flavor = module.params.get('flavor')
852    group = module.params.get('group')
853    image = module.params.get('image')
854    instance_ids = module.params.get('instance_ids')
855    key_name = module.params.get('key_name')
856    meta = module.params.get('meta')
857    name = module.params.get('name')
858    networks = module.params.get('networks')
859    state = module.params.get('state')
860    user_data = module.params.get('user_data')
861    wait = module.params.get('wait')
862    wait_timeout = int(module.params.get('wait_timeout'))
863
864    setup_rax_module(module, pyrax)
865
866    if extra_client_args:
867        pyrax.cloudservers = pyrax.connect_to_cloudservers(
868            region=pyrax.cloudservers.client.region_name,
869            **extra_client_args)
870        client = pyrax.cloudservers.client
871        if 'bypass_url' in extra_client_args:
872            client.management_url = extra_client_args['bypass_url']
873
874    if pyrax.cloudservers is None:
875        module.fail_json(msg='Failed to instantiate client. This '
876                             'typically indicates an invalid region or an '
877                             'incorrectly capitalized region name.')
878
879    cloudservers(module, state=state, name=name, flavor=flavor,
880                 image=image, meta=meta, key_name=key_name, files=files,
881                 wait=wait, wait_timeout=wait_timeout, disk_config=disk_config,
882                 count=count, group=group, instance_ids=instance_ids,
883                 exact_count=exact_count, networks=networks,
884                 count_offset=count_offset, auto_increment=auto_increment,
885                 extra_create_args=extra_create_args, user_data=user_data,
886                 config_drive=config_drive, boot_from_volume=boot_from_volume,
887                 boot_volume=boot_volume, boot_volume_size=boot_volume_size,
888                 boot_volume_terminate=boot_volume_terminate)
889
890
891if __name__ == '__main__':
892    main()
893