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