1#!/usr/bin/python 2# -*- coding: utf-8 -*- 3# 4# Copyright: (c) 2018, Bojan Vitnik <bvitnik@mainstream.rs> 5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 7from __future__ import absolute_import, division, print_function 8__metaclass__ = type 9 10ANSIBLE_METADATA = {'metadata_version': '1.1', 11 'status': ['preview'], 12 'supported_by': 'community'} 13 14DOCUMENTATION = r''' 15--- 16module: xenserver_guest 17short_description: Manages virtual machines running on Citrix Hypervisor/XenServer host or pool 18description: > 19 This module can be used to create new virtual machines from templates or other virtual machines, 20 modify various virtual machine components like network and disk, rename a virtual machine and 21 remove a virtual machine with associated components. 22version_added: '2.8' 23author: 24- Bojan Vitnik (@bvitnik) <bvitnik@mainstream.rs> 25notes: 26- Minimal supported version of XenServer is 5.6. 27- Module was tested with XenServer 6.5, 7.1, 7.2, 7.6, Citrix Hypervisor 8.0, XCP-ng 7.6 and 8.0. 28- 'To acquire XenAPI Python library, just run C(pip install XenAPI) on your Ansible Control Node. The library can also be found inside 29 Citrix Hypervisor/XenServer SDK (downloadable from Citrix website). Copy the XenAPI.py file from the SDK to your Python site-packages on your 30 Ansible Control Node to use it. Latest version of the library can also be acquired from GitHub: 31 https://raw.githubusercontent.com/xapi-project/xen-api/master/scripts/examples/python/XenAPI.py' 32- 'If no scheme is specified in C(hostname), module defaults to C(http://) because C(https://) is problematic in most setups. Make sure you are 33 accessing XenServer host in trusted environment or use C(https://) scheme explicitly.' 34- 'To use C(https://) scheme for C(hostname) you have to either import host certificate to your OS certificate store or use C(validate_certs: no) 35 which requires XenAPI library from XenServer 7.2 SDK or newer and Python 2.7.9 or newer.' 36- 'Network configuration inside a guest OS, by using C(networks.type), C(networks.ip), C(networks.gateway) etc. parameters, is supported on 37 XenServer 7.0 or newer for Windows guests by using official XenServer Guest agent support for network configuration. The module will try to 38 detect if such support is available and utilize it, else it will use a custom method of configuration via xenstore. Since XenServer Guest 39 agent only support None and Static types of network configuration, where None means DHCP configured interface, C(networks.type) and C(networks.type6) 40 values C(none) and C(dhcp) have same effect. More info here: 41 https://www.citrix.com/community/citrix-developer/citrix-hypervisor-developer/citrix-hypervisor-developing-products/citrix-hypervisor-staticip.html' 42- 'On platforms without official support for network configuration inside a guest OS, network parameters will be written to xenstore 43 C(vm-data/networks/<vif_device>) key. Parameters can be inspected by using C(xenstore ls) and C(xenstore read) tools on \*nix guests or trough 44 WMI interface on Windows guests. They can also be found in VM facts C(instance.xenstore_data) key as returned by the module. It is up to the user 45 to implement a boot time scripts or custom agent that will read the parameters from xenstore and configure network with given parameters. 46 Take note that for xenstore data to become available inside a guest, a VM restart is needed hence module will require VM restart if any 47 parameter is changed. This is a limitation of XenAPI and xenstore. Considering these limitations, network configuration trough xenstore is most 48 useful for bootstraping newly deployed VMs, much less for reconfiguring existing ones. More info here: 49 https://support.citrix.com/article/CTX226713' 50requirements: 51- python >= 2.6 52- XenAPI 53options: 54 state: 55 description: 56 - Specify the state VM should be in. 57 - If C(state) is set to C(present) and VM exists, ensure the VM configuration conforms to given parameters. 58 - If C(state) is set to C(present) and VM does not exist, then VM is deployed with given parameters. 59 - If C(state) is set to C(absent) and VM exists, then VM is removed with its associated components. 60 - If C(state) is set to C(poweredon) and VM does not exist, then VM is deployed with given parameters and powered on automatically. 61 type: str 62 default: present 63 choices: [ present, absent, poweredon ] 64 name: 65 description: 66 - Name of the VM to work with. 67 - VMs running on XenServer do not necessarily have unique names. The module will fail if multiple VMs with same name are found. 68 - In case of multiple VMs with same name, use C(uuid) to uniquely specify VM to manage. 69 - This parameter is case sensitive. 70 type: str 71 required: yes 72 aliases: [ name_label ] 73 name_desc: 74 description: 75 - VM description. 76 type: str 77 uuid: 78 description: 79 - UUID of the VM to manage if known. This is XenServer's unique identifier. 80 - It is required if name is not unique. 81 - Please note that a supplied UUID will be ignored on VM creation, as XenServer creates the UUID internally. 82 type: str 83 template: 84 description: 85 - Name of a template, an existing VM (must be shut down) or a snapshot that should be used to create VM. 86 - Templates/VMs/snapshots on XenServer do not necessarily have unique names. The module will fail if multiple templates with same name are found. 87 - In case of multiple templates/VMs/snapshots with same name, use C(template_uuid) to uniquely specify source template. 88 - If VM already exists, this setting will be ignored. 89 - This parameter is case sensitive. 90 type: str 91 aliases: [ template_src ] 92 template_uuid: 93 description: 94 - UUID of a template, an existing VM or a snapshot that should be used to create VM. 95 - It is required if template name is not unique. 96 type: str 97 is_template: 98 description: 99 - Convert VM to template. 100 type: bool 101 default: no 102 folder: 103 description: 104 - Destination folder for VM. 105 - This parameter is case sensitive. 106 - 'Example:' 107 - ' folder: /folder1/folder2' 108 type: str 109 hardware: 110 description: 111 - Manage VM's hardware parameters. VM needs to be shut down to reconfigure these parameters. 112 - 'Valid parameters are:' 113 - ' - C(num_cpus) (integer): Number of CPUs.' 114 - ' - C(num_cpu_cores_per_socket) (integer): Number of Cores Per Socket. C(num_cpus) has to be a multiple of C(num_cpu_cores_per_socket).' 115 - ' - C(memory_mb) (integer): Amount of memory in MB.' 116 type: dict 117 disks: 118 description: 119 - A list of disks to add to VM. 120 - All parameters are case sensitive. 121 - Removing or detaching existing disks of VM is not supported. 122 - 'Required parameters per entry:' 123 - ' - C(size_[tb,gb,mb,kb,b]) (integer): Disk storage size in specified unit. VM needs to be shut down to reconfigure this parameter.' 124 - 'Optional parameters per entry:' 125 - ' - C(name) (string): Disk name. You can also use C(name_label) as an alias.' 126 - ' - C(name_desc) (string): Disk description.' 127 - ' - C(sr) (string): Storage Repository to create disk on. If not specified, will use default SR. Cannot be used for moving disk to other SR.' 128 - ' - C(sr_uuid) (string): UUID of a SR to create disk on. Use if SR name is not unique.' 129 type: list 130 aliases: [ disk ] 131 cdrom: 132 description: 133 - A CD-ROM configuration for the VM. 134 - All parameters are case sensitive. 135 - 'Valid parameters are:' 136 - ' - C(type) (string): The type of CD-ROM, valid options are C(none) or C(iso). With C(none) the CD-ROM device will be present but empty.' 137 - ' - C(iso_name) (string): The file name of an ISO image from one of the XenServer ISO Libraries (implies C(type: iso)). 138 Required if C(type) is set to C(iso).' 139 type: dict 140 networks: 141 description: 142 - A list of networks (in the order of the NICs). 143 - All parameters are case sensitive. 144 - 'Required parameters per entry:' 145 - ' - C(name) (string): Name of a XenServer network to attach the network interface to. You can also use C(name_label) as an alias.' 146 - 'Optional parameters per entry (used for VM hardware):' 147 - ' - C(mac) (string): Customize MAC address of the interface.' 148 - 'Optional parameters per entry (used for OS customization):' 149 - ' - C(type) (string): Type of IPv4 assignment, valid options are C(none), C(dhcp) or C(static). Value C(none) means whatever is default for OS. 150 On some operating systems it could be DHCP configured (e.g. Windows) or unconfigured interface (e.g. Linux).' 151 - ' - C(ip) (string): Static IPv4 address (implies C(type: static)). Can include prefix in format <IPv4 address>/<prefix> instead of using C(netmask).' 152 - ' - C(netmask) (string): Static IPv4 netmask required for C(ip) if prefix is not specified.' 153 - ' - C(gateway) (string): Static IPv4 gateway.' 154 - ' - C(type6) (string): Type of IPv6 assignment, valid options are C(none), C(dhcp) or C(static). Value C(none) means whatever is default for OS. 155 On some operating systems it could be DHCP configured (e.g. Windows) or unconfigured interface (e.g. Linux).' 156 - ' - C(ip6) (string): Static IPv6 address (implies C(type6: static)) with prefix in format <IPv6 address>/<prefix>.' 157 - ' - C(gateway6) (string): Static IPv6 gateway.' 158 type: list 159 aliases: [ network ] 160 home_server: 161 description: 162 - Name of a XenServer host that will be a Home Server for the VM. 163 - This parameter is case sensitive. 164 type: str 165 custom_params: 166 description: 167 - Define a list of custom VM params to set on VM. 168 - Useful for advanced users familiar with managing VM params trough xe CLI. 169 - A custom value object takes two fields C(key) and C(value) (see example below). 170 type: list 171 wait_for_ip_address: 172 description: 173 - Wait until XenServer detects an IP address for the VM. If C(state) is set to C(absent), this parameter is ignored. 174 - This requires XenServer Tools to be preinstalled on the VM to work properly. 175 type: bool 176 default: no 177 state_change_timeout: 178 description: 179 - 'By default, module will wait indefinitely for VM to accquire an IP address if C(wait_for_ip_address: yes).' 180 - If this parameter is set to positive value, the module will instead wait specified number of seconds for the state change. 181 - In case of timeout, module will generate an error message. 182 type: int 183 default: 0 184 linked_clone: 185 description: 186 - Whether to create a Linked Clone from the template, existing VM or snapshot. If no, will create a full copy. 187 - This is equivalent to C(Use storage-level fast disk clone) option in XenCenter. 188 type: bool 189 default: no 190 force: 191 description: 192 - Ignore warnings and complete the actions. 193 - This parameter is useful for removing VM in running state or reconfiguring VM params that require VM to be shut down. 194 type: bool 195 default: no 196extends_documentation_fragment: xenserver.documentation 197''' 198 199EXAMPLES = r''' 200- name: Create a VM from a template 201 xenserver_guest: 202 hostname: "{{ xenserver_hostname }}" 203 username: "{{ xenserver_username }}" 204 password: "{{ xenserver_password }}" 205 validate_certs: no 206 folder: /testvms 207 name: testvm_2 208 state: poweredon 209 template: CentOS 7 210 disks: 211 - size_gb: 10 212 sr: my_sr 213 hardware: 214 num_cpus: 6 215 num_cpu_cores_per_socket: 3 216 memory_mb: 512 217 cdrom: 218 type: iso 219 iso_name: guest-tools.iso 220 networks: 221 - name: VM Network 222 mac: aa:bb:dd:aa:00:14 223 wait_for_ip_address: yes 224 delegate_to: localhost 225 register: deploy 226 227- name: Create a VM template 228 xenserver_guest: 229 hostname: "{{ xenserver_hostname }}" 230 username: "{{ xenserver_username }}" 231 password: "{{ xenserver_password }}" 232 validate_certs: no 233 folder: /testvms 234 name: testvm_6 235 is_template: yes 236 disk: 237 - size_gb: 10 238 sr: my_sr 239 hardware: 240 memory_mb: 512 241 num_cpus: 1 242 delegate_to: localhost 243 register: deploy 244 245- name: Rename a VM (requires the VM's UUID) 246 xenserver_guest: 247 hostname: "{{ xenserver_hostname }}" 248 username: "{{ xenserver_username }}" 249 password: "{{ xenserver_password }}" 250 uuid: 421e4592-c069-924d-ce20-7e7533fab926 251 name: new_name 252 state: present 253 delegate_to: localhost 254 255- name: Remove a VM by UUID 256 xenserver_guest: 257 hostname: "{{ xenserver_hostname }}" 258 username: "{{ xenserver_username }}" 259 password: "{{ xenserver_password }}" 260 uuid: 421e4592-c069-924d-ce20-7e7533fab926 261 state: absent 262 delegate_to: localhost 263 264- name: Modify custom params (boot order) 265 xenserver_guest: 266 hostname: "{{ xenserver_hostname }}" 267 username: "{{ xenserver_username }}" 268 password: "{{ xenserver_password }}" 269 name: testvm_8 270 state: present 271 custom_params: 272 - key: HVM_boot_params 273 value: { "order": "ndc" } 274 delegate_to: localhost 275 276- name: Customize network parameters 277 xenserver_guest: 278 hostname: "{{ xenserver_hostname }}" 279 username: "{{ xenserver_username }}" 280 password: "{{ xenserver_password }}" 281 name: testvm_10 282 networks: 283 - name: VM Network 284 ip: 192.168.1.100/24 285 gateway: 192.168.1.1 286 - type: dhcp 287 delegate_to: localhost 288''' 289 290RETURN = r''' 291instance: 292 description: Metadata about the VM 293 returned: always 294 type: dict 295 sample: { 296 "cdrom": { 297 "type": "none" 298 }, 299 "customization_agent": "native", 300 "disks": [ 301 { 302 "name": "testvm_11-0", 303 "name_desc": "", 304 "os_device": "xvda", 305 "size": 42949672960, 306 "sr": "Local storage", 307 "sr_uuid": "0af1245e-bdb0-ba33-1446-57a962ec4075", 308 "vbd_userdevice": "0" 309 }, 310 { 311 "name": "testvm_11-1", 312 "name_desc": "", 313 "os_device": "xvdb", 314 "size": 42949672960, 315 "sr": "Local storage", 316 "sr_uuid": "0af1245e-bdb0-ba33-1446-57a962ec4075", 317 "vbd_userdevice": "1" 318 } 319 ], 320 "domid": "56", 321 "folder": "", 322 "hardware": { 323 "memory_mb": 8192, 324 "num_cpu_cores_per_socket": 2, 325 "num_cpus": 4 326 }, 327 "home_server": "", 328 "is_template": false, 329 "name": "testvm_11", 330 "name_desc": "", 331 "networks": [ 332 { 333 "gateway": "192.168.0.254", 334 "gateway6": "fc00::fffe", 335 "ip": "192.168.0.200", 336 "ip6": [ 337 "fe80:0000:0000:0000:e9cb:625a:32c5:c291", 338 "fc00:0000:0000:0000:0000:0000:0000:0001" 339 ], 340 "mac": "ba:91:3a:48:20:76", 341 "mtu": "1500", 342 "name": "Pool-wide network associated with eth1", 343 "netmask": "255.255.255.128", 344 "prefix": "25", 345 "prefix6": "64", 346 "vif_device": "0" 347 } 348 ], 349 "other_config": { 350 "base_template_name": "Windows Server 2016 (64-bit)", 351 "import_task": "OpaqueRef:e43eb71c-45d6-5351-09ff-96e4fb7d0fa5", 352 "install-methods": "cdrom", 353 "instant": "true", 354 "mac_seed": "f83e8d8a-cfdc-b105-b054-ef5cb416b77e" 355 }, 356 "platform": { 357 "acpi": "1", 358 "apic": "true", 359 "cores-per-socket": "2", 360 "device_id": "0002", 361 "hpet": "true", 362 "nx": "true", 363 "pae": "true", 364 "timeoffset": "-25200", 365 "vga": "std", 366 "videoram": "8", 367 "viridian": "true", 368 "viridian_reference_tsc": "true", 369 "viridian_time_ref_count": "true" 370 }, 371 "state": "poweredon", 372 "uuid": "e3c0b2d5-5f05-424e-479c-d3df8b3e7cda", 373 "xenstore_data": { 374 "vm-data": "" 375 } 376 } 377changes: 378 description: Detected or made changes to VM 379 returned: always 380 type: list 381 sample: [ 382 { 383 "hardware": [ 384 "num_cpus" 385 ] 386 }, 387 { 388 "disks_changed": [ 389 [], 390 [ 391 "size" 392 ] 393 ] 394 }, 395 { 396 "disks_new": [ 397 { 398 "name": "new-disk", 399 "name_desc": "", 400 "position": 2, 401 "size_gb": "4", 402 "vbd_userdevice": "2" 403 } 404 ] 405 }, 406 { 407 "cdrom": [ 408 "type", 409 "iso_name" 410 ] 411 }, 412 { 413 "networks_changed": [ 414 [ 415 "mac" 416 ], 417 ] 418 }, 419 { 420 "networks_new": [ 421 { 422 "name": "Pool-wide network associated with eth2", 423 "position": 1, 424 "vif_device": "1" 425 } 426 ] 427 }, 428 "need_poweredoff" 429 ] 430''' 431 432import re 433 434HAS_XENAPI = False 435try: 436 import XenAPI 437 HAS_XENAPI = True 438except ImportError: 439 pass 440 441from ansible.module_utils.basic import AnsibleModule 442from ansible.module_utils.common.network import is_mac 443from ansible.module_utils import six 444from ansible.module_utils.xenserver import (xenserver_common_argument_spec, XAPI, XenServerObject, get_object_ref, 445 gather_vm_params, gather_vm_facts, set_vm_power_state, wait_for_vm_ip_address, 446 is_valid_ip_addr, is_valid_ip_netmask, is_valid_ip_prefix, 447 ip_prefix_to_netmask, ip_netmask_to_prefix, 448 is_valid_ip6_addr, is_valid_ip6_prefix) 449 450 451class XenServerVM(XenServerObject): 452 """Class for managing XenServer VM. 453 454 Attributes: 455 vm_ref (str): XAPI reference to VM. 456 vm_params (dict): A dictionary with VM parameters as returned 457 by gather_vm_params() function. 458 """ 459 460 def __init__(self, module): 461 """Inits XenServerVM using module parameters. 462 463 Args: 464 module: Reference to Ansible module object. 465 """ 466 super(XenServerVM, self).__init__(module) 467 468 self.vm_ref = get_object_ref(self.module, self.module.params['name'], self.module.params['uuid'], obj_type="VM", fail=False, msg_prefix="VM search: ") 469 self.gather_params() 470 471 def exists(self): 472 """Returns True if VM exists, else False.""" 473 return True if self.vm_ref is not None else False 474 475 def gather_params(self): 476 """Gathers all VM parameters available in XAPI database.""" 477 self.vm_params = gather_vm_params(self.module, self.vm_ref) 478 479 def gather_facts(self): 480 """Gathers and returns VM facts.""" 481 return gather_vm_facts(self.module, self.vm_params) 482 483 def set_power_state(self, power_state): 484 """Controls VM power state.""" 485 state_changed, current_state = set_vm_power_state(self.module, self.vm_ref, power_state, self.module.params['state_change_timeout']) 486 487 # If state has changed, update vm_params. 488 if state_changed: 489 self.vm_params['power_state'] = current_state.capitalize() 490 491 return state_changed 492 493 def wait_for_ip_address(self): 494 """Waits for VM to acquire an IP address.""" 495 self.vm_params['guest_metrics'] = wait_for_vm_ip_address(self.module, self.vm_ref, self.module.params['state_change_timeout']) 496 497 def deploy(self): 498 """Deploys new VM from template.""" 499 # Safety check. 500 if self.exists(): 501 self.module.fail_json(msg="Called deploy on existing VM!") 502 503 try: 504 templ_ref = get_object_ref(self.module, self.module.params['template'], self.module.params['template_uuid'], obj_type="template", fail=True, 505 msg_prefix="VM deploy: ") 506 507 # Is this an existing running VM? 508 if self.xapi_session.xenapi.VM.get_power_state(templ_ref).lower() != 'halted': 509 self.module.fail_json(msg="VM deploy: running VM cannot be used as a template!") 510 511 # Find a SR we can use for VM.copy(). We use SR of the first disk 512 # if specified or default SR if not specified. 513 disk_params_list = self.module.params['disks'] 514 515 sr_ref = None 516 517 if disk_params_list: 518 disk_params = disk_params_list[0] 519 520 disk_sr_uuid = disk_params.get('sr_uuid') 521 disk_sr = disk_params.get('sr') 522 523 if disk_sr_uuid is not None or disk_sr is not None: 524 sr_ref = get_object_ref(self.module, disk_sr, disk_sr_uuid, obj_type="SR", fail=True, 525 msg_prefix="VM deploy disks[0]: ") 526 527 if not sr_ref: 528 if self.default_sr_ref != "OpaqueRef:NULL": 529 sr_ref = self.default_sr_ref 530 else: 531 self.module.fail_json(msg="VM deploy disks[0]: no default SR found! You must specify SR explicitly.") 532 533 # VM name could be an empty string which is bad. 534 if self.module.params['name'] is not None and not self.module.params['name']: 535 self.module.fail_json(msg="VM deploy: VM name must not be an empty string!") 536 537 # Support for Ansible check mode. 538 if self.module.check_mode: 539 return 540 541 # Now we can instantiate VM. We use VM.clone for linked_clone and 542 # VM.copy for non linked_clone. 543 if self.module.params['linked_clone']: 544 self.vm_ref = self.xapi_session.xenapi.VM.clone(templ_ref, self.module.params['name']) 545 else: 546 self.vm_ref = self.xapi_session.xenapi.VM.copy(templ_ref, self.module.params['name'], sr_ref) 547 548 # Description is copied over from template so we reset it. 549 self.xapi_session.xenapi.VM.set_name_description(self.vm_ref, "") 550 551 # If template is one of built-in XenServer templates, we have to 552 # do some additional steps. 553 # Note: VM.get_is_default_template() is supported from XenServer 7.2 554 # onward so we use an alternative way. 555 templ_other_config = self.xapi_session.xenapi.VM.get_other_config(templ_ref) 556 557 if "default_template" in templ_other_config and templ_other_config['default_template']: 558 # other_config of built-in XenServer templates have a key called 559 # 'disks' with the following content: 560 # disks: <provision><disk bootable="true" device="0" size="10737418240" sr="" type="system"/></provision> 561 # This value of other_data is copied to cloned or copied VM and 562 # it prevents provisioning of VM because sr is not specified and 563 # XAPI returns an error. To get around this, we remove the 564 # 'disks' key and add disks to VM later ourselves. 565 vm_other_config = self.xapi_session.xenapi.VM.get_other_config(self.vm_ref) 566 567 if "disks" in vm_other_config: 568 del vm_other_config['disks'] 569 570 self.xapi_session.xenapi.VM.set_other_config(self.vm_ref, vm_other_config) 571 572 # At this point we have VM ready for provisioning. 573 self.xapi_session.xenapi.VM.provision(self.vm_ref) 574 575 # After provisioning we can prepare vm_params for reconfigure(). 576 self.gather_params() 577 578 # VM is almost ready. We just need to reconfigure it... 579 self.reconfigure() 580 581 # Power on VM if needed. 582 if self.module.params['state'] == "poweredon": 583 self.set_power_state("poweredon") 584 585 except XenAPI.Failure as f: 586 self.module.fail_json(msg="XAPI ERROR: %s" % f.details) 587 588 def reconfigure(self): 589 """Reconfigures an existing VM. 590 591 Returns: 592 list: parameters that were reconfigured. 593 """ 594 # Safety check. 595 if not self.exists(): 596 self.module.fail_json(msg="Called reconfigure on non existing VM!") 597 598 config_changes = self.get_changes() 599 600 vm_power_state_save = self.vm_params['power_state'].lower() 601 602 if "need_poweredoff" in config_changes and vm_power_state_save != 'halted' and not self.module.params['force']: 603 self.module.fail_json(msg="VM reconfigure: VM has to be in powered off state to reconfigure but force was not specified!") 604 605 # Support for Ansible check mode. 606 if self.module.check_mode: 607 return config_changes 608 609 if "need_poweredoff" in config_changes and vm_power_state_save != 'halted' and self.module.params['force']: 610 self.set_power_state("shutdownguest") 611 612 try: 613 for change in config_changes: 614 if isinstance(change, six.string_types): 615 if change == "name": 616 self.xapi_session.xenapi.VM.set_name_label(self.vm_ref, self.module.params['name']) 617 elif change == "name_desc": 618 self.xapi_session.xenapi.VM.set_name_description(self.vm_ref, self.module.params['name_desc']) 619 elif change == "folder": 620 self.xapi_session.xenapi.VM.remove_from_other_config(self.vm_ref, 'folder') 621 622 if self.module.params['folder']: 623 self.xapi_session.xenapi.VM.add_to_other_config(self.vm_ref, 'folder', self.module.params['folder']) 624 elif change == "home_server": 625 if self.module.params['home_server']: 626 host_ref = self.xapi_session.xenapi.host.get_by_name_label(self.module.params['home_server'])[0] 627 else: 628 host_ref = "OpaqueRef:NULL" 629 630 self.xapi_session.xenapi.VM.set_affinity(self.vm_ref, host_ref) 631 elif isinstance(change, dict): 632 if change.get('hardware'): 633 for hardware_change in change['hardware']: 634 if hardware_change == "num_cpus": 635 num_cpus = int(self.module.params['hardware']['num_cpus']) 636 637 if num_cpus < int(self.vm_params['VCPUs_at_startup']): 638 self.xapi_session.xenapi.VM.set_VCPUs_at_startup(self.vm_ref, str(num_cpus)) 639 self.xapi_session.xenapi.VM.set_VCPUs_max(self.vm_ref, str(num_cpus)) 640 else: 641 self.xapi_session.xenapi.VM.set_VCPUs_max(self.vm_ref, str(num_cpus)) 642 self.xapi_session.xenapi.VM.set_VCPUs_at_startup(self.vm_ref, str(num_cpus)) 643 elif hardware_change == "num_cpu_cores_per_socket": 644 self.xapi_session.xenapi.VM.remove_from_platform(self.vm_ref, 'cores-per-socket') 645 num_cpu_cores_per_socket = int(self.module.params['hardware']['num_cpu_cores_per_socket']) 646 647 if num_cpu_cores_per_socket > 1: 648 self.xapi_session.xenapi.VM.add_to_platform(self.vm_ref, 'cores-per-socket', str(num_cpu_cores_per_socket)) 649 elif hardware_change == "memory_mb": 650 memory_b = str(int(self.module.params['hardware']['memory_mb']) * 1048576) 651 vm_memory_static_min_b = str(min(int(memory_b), int(self.vm_params['memory_static_min']))) 652 653 self.xapi_session.xenapi.VM.set_memory_limits(self.vm_ref, vm_memory_static_min_b, memory_b, memory_b, memory_b) 654 elif change.get('disks_changed'): 655 vm_disk_params_list = [disk_params for disk_params in self.vm_params['VBDs'] if disk_params['type'] == "Disk"] 656 position = 0 657 658 for disk_change_list in change['disks_changed']: 659 for disk_change in disk_change_list: 660 vdi_ref = self.xapi_session.xenapi.VDI.get_by_uuid(vm_disk_params_list[position]['VDI']['uuid']) 661 662 if disk_change == "name": 663 self.xapi_session.xenapi.VDI.set_name_label(vdi_ref, self.module.params['disks'][position]['name']) 664 elif disk_change == "name_desc": 665 self.xapi_session.xenapi.VDI.set_name_description(vdi_ref, self.module.params['disks'][position]['name_desc']) 666 elif disk_change == "size": 667 self.xapi_session.xenapi.VDI.resize(vdi_ref, str(self.get_normalized_disk_size(self.module.params['disks'][position], 668 "VM reconfigure disks[%s]: " % position))) 669 670 position += 1 671 elif change.get('disks_new'): 672 for position, disk_userdevice in change['disks_new']: 673 disk_params = self.module.params['disks'][position] 674 675 disk_name = disk_params['name'] if disk_params.get('name') else "%s-%s" % (self.vm_params['name_label'], position) 676 disk_name_desc = disk_params['name_desc'] if disk_params.get('name_desc') else "" 677 678 if disk_params.get('sr_uuid'): 679 sr_ref = self.xapi_session.xenapi.SR.get_by_uuid(disk_params['sr_uuid']) 680 elif disk_params.get('sr'): 681 sr_ref = self.xapi_session.xenapi.SR.get_by_name_label(disk_params['sr'])[0] 682 else: 683 sr_ref = self.default_sr_ref 684 685 disk_size = str(self.get_normalized_disk_size(self.module.params['disks'][position], "VM reconfigure disks[%s]: " % position)) 686 687 new_disk_vdi = { 688 "name_label": disk_name, 689 "name_description": disk_name_desc, 690 "SR": sr_ref, 691 "virtual_size": disk_size, 692 "type": "user", 693 "sharable": False, 694 "read_only": False, 695 "other_config": {}, 696 } 697 698 new_disk_vbd = { 699 "VM": self.vm_ref, 700 "VDI": None, 701 "userdevice": disk_userdevice, 702 "bootable": False, 703 "mode": "RW", 704 "type": "Disk", 705 "empty": False, 706 "other_config": {}, 707 "qos_algorithm_type": "", 708 "qos_algorithm_params": {}, 709 } 710 711 new_disk_vbd['VDI'] = self.xapi_session.xenapi.VDI.create(new_disk_vdi) 712 vbd_ref_new = self.xapi_session.xenapi.VBD.create(new_disk_vbd) 713 714 if self.vm_params['power_state'].lower() == "running": 715 self.xapi_session.xenapi.VBD.plug(vbd_ref_new) 716 717 elif change.get('cdrom'): 718 vm_cdrom_params_list = [cdrom_params for cdrom_params in self.vm_params['VBDs'] if cdrom_params['type'] == "CD"] 719 720 # If there is no CD present, we have to create one. 721 if not vm_cdrom_params_list: 722 # We will try to place cdrom at userdevice position 723 # 3 (which is default) if it is not already occupied 724 # else we will place it at first allowed position. 725 cdrom_userdevices_allowed = self.xapi_session.xenapi.VM.get_allowed_VBD_devices(self.vm_ref) 726 727 if "3" in cdrom_userdevices_allowed: 728 cdrom_userdevice = "3" 729 else: 730 cdrom_userdevice = cdrom_userdevices_allowed[0] 731 732 cdrom_vbd = { 733 "VM": self.vm_ref, 734 "VDI": "OpaqueRef:NULL", 735 "userdevice": cdrom_userdevice, 736 "bootable": False, 737 "mode": "RO", 738 "type": "CD", 739 "empty": True, 740 "other_config": {}, 741 "qos_algorithm_type": "", 742 "qos_algorithm_params": {}, 743 } 744 745 cdrom_vbd_ref = self.xapi_session.xenapi.VBD.create(cdrom_vbd) 746 else: 747 cdrom_vbd_ref = self.xapi_session.xenapi.VBD.get_by_uuid(vm_cdrom_params_list[0]['uuid']) 748 749 cdrom_is_empty = self.xapi_session.xenapi.VBD.get_empty(cdrom_vbd_ref) 750 751 for cdrom_change in change['cdrom']: 752 if cdrom_change == "type": 753 cdrom_type = self.module.params['cdrom']['type'] 754 755 if cdrom_type == "none" and not cdrom_is_empty: 756 self.xapi_session.xenapi.VBD.eject(cdrom_vbd_ref) 757 elif cdrom_type == "host": 758 # Unimplemented! 759 pass 760 761 elif cdrom_change == "iso_name": 762 if not cdrom_is_empty: 763 self.xapi_session.xenapi.VBD.eject(cdrom_vbd_ref) 764 765 cdrom_vdi_ref = self.xapi_session.xenapi.VDI.get_by_name_label(self.module.params['cdrom']['iso_name'])[0] 766 self.xapi_session.xenapi.VBD.insert(cdrom_vbd_ref, cdrom_vdi_ref) 767 elif change.get('networks_changed'): 768 position = 0 769 770 for network_change_list in change['networks_changed']: 771 if network_change_list: 772 vm_vif_params = self.vm_params['VIFs'][position] 773 network_params = self.module.params['networks'][position] 774 775 vif_ref = self.xapi_session.xenapi.VIF.get_by_uuid(vm_vif_params['uuid']) 776 network_ref = self.xapi_session.xenapi.network.get_by_uuid(vm_vif_params['network']['uuid']) 777 778 vif_recreated = False 779 780 if "name" in network_change_list or "mac" in network_change_list: 781 # To change network or MAC, we destroy old 782 # VIF and then create a new one with changed 783 # parameters. That's how XenCenter does it. 784 785 # Copy all old parameters to new VIF record. 786 vif = { 787 "device": vm_vif_params['device'], 788 "network": network_ref, 789 "VM": vm_vif_params['VM'], 790 "MAC": vm_vif_params['MAC'], 791 "MTU": vm_vif_params['MTU'], 792 "other_config": vm_vif_params['other_config'], 793 "qos_algorithm_type": vm_vif_params['qos_algorithm_type'], 794 "qos_algorithm_params": vm_vif_params['qos_algorithm_params'], 795 "locking_mode": vm_vif_params['locking_mode'], 796 "ipv4_allowed": vm_vif_params['ipv4_allowed'], 797 "ipv6_allowed": vm_vif_params['ipv6_allowed'], 798 } 799 800 if "name" in network_change_list: 801 network_ref_new = self.xapi_session.xenapi.network.get_by_name_label(network_params['name'])[0] 802 vif['network'] = network_ref_new 803 vif['MTU'] = self.xapi_session.xenapi.network.get_MTU(network_ref_new) 804 805 if "mac" in network_change_list: 806 vif['MAC'] = network_params['mac'].lower() 807 808 if self.vm_params['power_state'].lower() == "running": 809 self.xapi_session.xenapi.VIF.unplug(vif_ref) 810 811 self.xapi_session.xenapi.VIF.destroy(vif_ref) 812 vif_ref_new = self.xapi_session.xenapi.VIF.create(vif) 813 814 if self.vm_params['power_state'].lower() == "running": 815 self.xapi_session.xenapi.VIF.plug(vif_ref_new) 816 817 vif_ref = vif_ref_new 818 vif_recreated = True 819 820 if self.vm_params['customization_agent'] == "native": 821 vif_reconfigure_needed = False 822 823 if "type" in network_change_list: 824 network_type = network_params['type'].capitalize() 825 vif_reconfigure_needed = True 826 else: 827 network_type = vm_vif_params['ipv4_configuration_mode'] 828 829 if "ip" in network_change_list: 830 network_ip = network_params['ip'] 831 vif_reconfigure_needed = True 832 elif vm_vif_params['ipv4_addresses']: 833 network_ip = vm_vif_params['ipv4_addresses'][0].split('/')[0] 834 else: 835 network_ip = "" 836 837 if "prefix" in network_change_list: 838 network_prefix = "/%s" % network_params['prefix'] 839 vif_reconfigure_needed = True 840 elif vm_vif_params['ipv4_addresses'] and vm_vif_params['ipv4_addresses'][0]: 841 network_prefix = "/%s" % vm_vif_params['ipv4_addresses'][0].split('/')[1] 842 else: 843 network_prefix = "" 844 845 if "gateway" in network_change_list: 846 network_gateway = network_params['gateway'] 847 vif_reconfigure_needed = True 848 else: 849 network_gateway = vm_vif_params['ipv4_gateway'] 850 851 if vif_recreated or vif_reconfigure_needed: 852 self.xapi_session.xenapi.VIF.configure_ipv4(vif_ref, network_type, 853 "%s%s" % (network_ip, network_prefix), network_gateway) 854 855 vif_reconfigure_needed = False 856 857 if "type6" in network_change_list: 858 network_type6 = network_params['type6'].capitalize() 859 vif_reconfigure_needed = True 860 else: 861 network_type6 = vm_vif_params['ipv6_configuration_mode'] 862 863 if "ip6" in network_change_list: 864 network_ip6 = network_params['ip6'] 865 vif_reconfigure_needed = True 866 elif vm_vif_params['ipv6_addresses']: 867 network_ip6 = vm_vif_params['ipv6_addresses'][0].split('/')[0] 868 else: 869 network_ip6 = "" 870 871 if "prefix6" in network_change_list: 872 network_prefix6 = "/%s" % network_params['prefix6'] 873 vif_reconfigure_needed = True 874 elif vm_vif_params['ipv6_addresses'] and vm_vif_params['ipv6_addresses'][0]: 875 network_prefix6 = "/%s" % vm_vif_params['ipv6_addresses'][0].split('/')[1] 876 else: 877 network_prefix6 = "" 878 879 if "gateway6" in network_change_list: 880 network_gateway6 = network_params['gateway6'] 881 vif_reconfigure_needed = True 882 else: 883 network_gateway6 = vm_vif_params['ipv6_gateway'] 884 885 if vif_recreated or vif_reconfigure_needed: 886 self.xapi_session.xenapi.VIF.configure_ipv6(vif_ref, network_type6, 887 "%s%s" % (network_ip6, network_prefix6), network_gateway6) 888 889 elif self.vm_params['customization_agent'] == "custom": 890 vif_device = vm_vif_params['device'] 891 892 # A user could have manually changed network 893 # or mac e.g. trough XenCenter and then also 894 # make those changes in playbook manually. 895 # In that case, module will not detect any 896 # changes and info in xenstore_data will 897 # become stale. For that reason we always 898 # update name and mac in xenstore_data. 899 900 # Since we handle name and mac differently, 901 # we have to remove them from 902 # network_change_list. 903 network_change_list_tmp = [net_chg for net_chg in network_change_list if net_chg not in ['name', 'mac']] 904 905 for network_change in network_change_list_tmp + ['name', 'mac']: 906 self.xapi_session.xenapi.VM.remove_from_xenstore_data(self.vm_ref, 907 "vm-data/networks/%s/%s" % (vif_device, network_change)) 908 909 if network_params.get('name'): 910 network_name = network_params['name'] 911 else: 912 network_name = vm_vif_params['network']['name_label'] 913 914 self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref, 915 "vm-data/networks/%s/%s" % (vif_device, 'name'), network_name) 916 917 if network_params.get('mac'): 918 network_mac = network_params['mac'].lower() 919 else: 920 network_mac = vm_vif_params['MAC'].lower() 921 922 self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref, 923 "vm-data/networks/%s/%s" % (vif_device, 'mac'), network_mac) 924 925 for network_change in network_change_list_tmp: 926 self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref, 927 "vm-data/networks/%s/%s" % (vif_device, network_change), 928 network_params[network_change]) 929 930 position += 1 931 elif change.get('networks_new'): 932 for position, vif_device in change['networks_new']: 933 network_params = self.module.params['networks'][position] 934 935 network_ref = self.xapi_session.xenapi.network.get_by_name_label(network_params['name'])[0] 936 937 network_name = network_params['name'] 938 network_mac = network_params['mac'] if network_params.get('mac') else "" 939 network_type = network_params.get('type') 940 network_ip = network_params['ip'] if network_params.get('ip') else "" 941 network_prefix = network_params['prefix'] if network_params.get('prefix') else "" 942 network_netmask = network_params['netmask'] if network_params.get('netmask') else "" 943 network_gateway = network_params['gateway'] if network_params.get('gateway') else "" 944 network_type6 = network_params.get('type6') 945 network_ip6 = network_params['ip6'] if network_params.get('ip6') else "" 946 network_prefix6 = network_params['prefix6'] if network_params.get('prefix6') else "" 947 network_gateway6 = network_params['gateway6'] if network_params.get('gateway6') else "" 948 949 vif = { 950 "device": vif_device, 951 "network": network_ref, 952 "VM": self.vm_ref, 953 "MAC": network_mac, 954 "MTU": self.xapi_session.xenapi.network.get_MTU(network_ref), 955 "other_config": {}, 956 "qos_algorithm_type": "", 957 "qos_algorithm_params": {}, 958 } 959 960 vif_ref_new = self.xapi_session.xenapi.VIF.create(vif) 961 962 if self.vm_params['power_state'].lower() == "running": 963 self.xapi_session.xenapi.VIF.plug(vif_ref_new) 964 965 if self.vm_params['customization_agent'] == "native": 966 if network_type and network_type == "static": 967 self.xapi_session.xenapi.VIF.configure_ipv4(vif_ref_new, "Static", 968 "%s/%s" % (network_ip, network_prefix), network_gateway) 969 970 if network_type6 and network_type6 == "static": 971 self.xapi_session.xenapi.VIF.configure_ipv6(vif_ref_new, "Static", 972 "%s/%s" % (network_ip6, network_prefix6), network_gateway6) 973 elif self.vm_params['customization_agent'] == "custom": 974 # We first have to remove any existing data 975 # from xenstore_data because there could be 976 # some old leftover data from some interface 977 # that once occupied same device location as 978 # our new interface. 979 for network_param in ['name', 'mac', 'type', 'ip', 'prefix', 'netmask', 'gateway', 'type6', 'ip6', 'prefix6', 'gateway6']: 980 self.xapi_session.xenapi.VM.remove_from_xenstore_data(self.vm_ref, "vm-data/networks/%s/%s" % (vif_device, network_param)) 981 982 self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref, "vm-data/networks/%s/name" % vif_device, network_name) 983 984 # We get MAC from VIF itself instead of 985 # networks.mac because it could be 986 # autogenerated. 987 vm_vif_mac = self.xapi_session.xenapi.VIF.get_MAC(vif_ref_new) 988 self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref, "vm-data/networks/%s/mac" % vif_device, vm_vif_mac) 989 990 if network_type: 991 self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref, "vm-data/networks/%s/type" % vif_device, network_type) 992 993 if network_type == "static": 994 self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref, 995 "vm-data/networks/%s/ip" % vif_device, network_ip) 996 self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref, 997 "vm-data/networks/%s/prefix" % vif_device, network_prefix) 998 self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref, 999 "vm-data/networks/%s/netmask" % vif_device, network_netmask) 1000 self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref, 1001 "vm-data/networks/%s/gateway" % vif_device, network_gateway) 1002 1003 if network_type6: 1004 self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref, "vm-data/networks/%s/type6" % vif_device, network_type6) 1005 1006 if network_type6 == "static": 1007 self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref, 1008 "vm-data/networks/%s/ip6" % vif_device, network_ip6) 1009 self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref, 1010 "vm-data/networks/%s/prefix6" % vif_device, network_prefix6) 1011 self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref, 1012 "vm-data/networks/%s/gateway6" % vif_device, network_gateway6) 1013 1014 elif change.get('custom_params'): 1015 for position in change['custom_params']: 1016 custom_param_key = self.module.params['custom_params'][position]['key'] 1017 custom_param_value = self.module.params['custom_params'][position]['value'] 1018 self.xapi_session.xenapi_request("VM.set_%s" % custom_param_key, (self.vm_ref, custom_param_value)) 1019 1020 if self.module.params['is_template']: 1021 self.xapi_session.xenapi.VM.set_is_a_template(self.vm_ref, True) 1022 elif "need_poweredoff" in config_changes and self.module.params['force'] and vm_power_state_save != 'halted': 1023 self.set_power_state("poweredon") 1024 1025 # Gather new params after reconfiguration. 1026 self.gather_params() 1027 1028 except XenAPI.Failure as f: 1029 self.module.fail_json(msg="XAPI ERROR: %s" % f.details) 1030 1031 return config_changes 1032 1033 def destroy(self): 1034 """Removes an existing VM with associated disks""" 1035 # Safety check. 1036 if not self.exists(): 1037 self.module.fail_json(msg="Called destroy on non existing VM!") 1038 1039 if self.vm_params['power_state'].lower() != 'halted' and not self.module.params['force']: 1040 self.module.fail_json(msg="VM destroy: VM has to be in powered off state to destroy but force was not specified!") 1041 1042 # Support for Ansible check mode. 1043 if self.module.check_mode: 1044 return 1045 1046 # Make sure that VM is poweredoff before we can destroy it. 1047 self.set_power_state("poweredoff") 1048 1049 try: 1050 # Destroy VM! 1051 self.xapi_session.xenapi.VM.destroy(self.vm_ref) 1052 1053 vm_disk_params_list = [disk_params for disk_params in self.vm_params['VBDs'] if disk_params['type'] == "Disk"] 1054 1055 # Destroy all VDIs associated with VM! 1056 for vm_disk_params in vm_disk_params_list: 1057 vdi_ref = self.xapi_session.xenapi.VDI.get_by_uuid(vm_disk_params['VDI']['uuid']) 1058 1059 self.xapi_session.xenapi.VDI.destroy(vdi_ref) 1060 1061 except XenAPI.Failure as f: 1062 self.module.fail_json(msg="XAPI ERROR: %s" % f.details) 1063 1064 def get_changes(self): 1065 """Finds VM parameters that differ from specified ones. 1066 1067 This method builds a dictionary with hierarchy of VM parameters 1068 that differ from those specified in module parameters. 1069 1070 Returns: 1071 list: VM parameters that differ from those specified in 1072 module parameters. 1073 """ 1074 # Safety check. 1075 if not self.exists(): 1076 self.module.fail_json(msg="Called get_changes on non existing VM!") 1077 1078 need_poweredoff = False 1079 1080 if self.module.params['is_template']: 1081 need_poweredoff = True 1082 1083 try: 1084 # This VM could be a template or a snapshot. In that case we fail 1085 # because we can't reconfigure them or it would just be too 1086 # dangerous. 1087 if self.vm_params['is_a_template'] and not self.vm_params['is_a_snapshot']: 1088 self.module.fail_json(msg="VM check: targeted VM is a template! Template reconfiguration is not supported.") 1089 1090 if self.vm_params['is_a_snapshot']: 1091 self.module.fail_json(msg="VM check: targeted VM is a snapshot! Snapshot reconfiguration is not supported.") 1092 1093 # Let's build a list of parameters that changed. 1094 config_changes = [] 1095 1096 # Name could only differ if we found an existing VM by uuid. 1097 if self.module.params['name'] is not None and self.module.params['name'] != self.vm_params['name_label']: 1098 if self.module.params['name']: 1099 config_changes.append('name') 1100 else: 1101 self.module.fail_json(msg="VM check name: VM name cannot be an empty string!") 1102 1103 if self.module.params['name_desc'] is not None and self.module.params['name_desc'] != self.vm_params['name_description']: 1104 config_changes.append('name_desc') 1105 1106 # Folder parameter is found in other_config. 1107 vm_other_config = self.vm_params['other_config'] 1108 vm_folder = vm_other_config.get('folder', '') 1109 1110 if self.module.params['folder'] is not None and self.module.params['folder'] != vm_folder: 1111 config_changes.append('folder') 1112 1113 if self.module.params['home_server'] is not None: 1114 if (self.module.params['home_server'] and 1115 (not self.vm_params['affinity'] or self.module.params['home_server'] != self.vm_params['affinity']['name_label'])): 1116 1117 # Check existance only. Ignore return value. 1118 get_object_ref(self.module, self.module.params['home_server'], uuid=None, obj_type="home server", fail=True, 1119 msg_prefix="VM check home_server: ") 1120 1121 config_changes.append('home_server') 1122 elif not self.module.params['home_server'] and self.vm_params['affinity']: 1123 config_changes.append('home_server') 1124 1125 config_changes_hardware = [] 1126 1127 if self.module.params['hardware']: 1128 num_cpus = self.module.params['hardware'].get('num_cpus') 1129 1130 if num_cpus is not None: 1131 # Kept for compatibility with older Ansible versions that 1132 # do not support subargument specs. 1133 try: 1134 num_cpus = int(num_cpus) 1135 except ValueError as e: 1136 self.module.fail_json(msg="VM check hardware.num_cpus: parameter should be an integer value!") 1137 1138 if num_cpus < 1: 1139 self.module.fail_json(msg="VM check hardware.num_cpus: parameter should be greater than zero!") 1140 1141 # We can use VCPUs_at_startup or VCPUs_max parameter. I'd 1142 # say the former is the way to go but this needs 1143 # confirmation and testing. 1144 if num_cpus != int(self.vm_params['VCPUs_at_startup']): 1145 config_changes_hardware.append('num_cpus') 1146 # For now, we don't support hotpluging so VM has to be in 1147 # poweredoff state to reconfigure. 1148 need_poweredoff = True 1149 1150 num_cpu_cores_per_socket = self.module.params['hardware'].get('num_cpu_cores_per_socket') 1151 1152 if num_cpu_cores_per_socket is not None: 1153 # Kept for compatibility with older Ansible versions that 1154 # do not support subargument specs. 1155 try: 1156 num_cpu_cores_per_socket = int(num_cpu_cores_per_socket) 1157 except ValueError as e: 1158 self.module.fail_json(msg="VM check hardware.num_cpu_cores_per_socket: parameter should be an integer value!") 1159 1160 if num_cpu_cores_per_socket < 1: 1161 self.module.fail_json(msg="VM check hardware.num_cpu_cores_per_socket: parameter should be greater than zero!") 1162 1163 if num_cpus and num_cpus % num_cpu_cores_per_socket != 0: 1164 self.module.fail_json(msg="VM check hardware.num_cpus: parameter should be a multiple of hardware.num_cpu_cores_per_socket!") 1165 1166 vm_platform = self.vm_params['platform'] 1167 vm_cores_per_socket = int(vm_platform.get('cores-per-socket', 1)) 1168 1169 if num_cpu_cores_per_socket != vm_cores_per_socket: 1170 config_changes_hardware.append('num_cpu_cores_per_socket') 1171 # For now, we don't support hotpluging so VM has to be 1172 # in poweredoff state to reconfigure. 1173 need_poweredoff = True 1174 1175 memory_mb = self.module.params['hardware'].get('memory_mb') 1176 1177 if memory_mb is not None: 1178 # Kept for compatibility with older Ansible versions that 1179 # do not support subargument specs. 1180 try: 1181 memory_mb = int(memory_mb) 1182 except ValueError as e: 1183 self.module.fail_json(msg="VM check hardware.memory_mb: parameter should be an integer value!") 1184 1185 if memory_mb < 1: 1186 self.module.fail_json(msg="VM check hardware.memory_mb: parameter should be greater than zero!") 1187 1188 # There are multiple memory parameters: 1189 # - memory_dynamic_max 1190 # - memory_dynamic_min 1191 # - memory_static_max 1192 # - memory_static_min 1193 # - memory_target 1194 # 1195 # memory_target seems like a good candidate but it returns 0 for 1196 # halted VMs so we can't use it. 1197 # 1198 # I decided to use memory_dynamic_max and memory_static_max 1199 # and use whichever is larger. This strategy needs validation 1200 # and testing. 1201 # 1202 # XenServer stores memory size in bytes so we need to divide 1203 # it by 1024*1024 = 1048576. 1204 if memory_mb != int(max(int(self.vm_params['memory_dynamic_max']), int(self.vm_params['memory_static_max'])) / 1048576): 1205 config_changes_hardware.append('memory_mb') 1206 # For now, we don't support hotpluging so VM has to be in 1207 # poweredoff state to reconfigure. 1208 need_poweredoff = True 1209 1210 if config_changes_hardware: 1211 config_changes.append({"hardware": config_changes_hardware}) 1212 1213 config_changes_disks = [] 1214 config_new_disks = [] 1215 1216 # Find allowed userdevices. 1217 vbd_userdevices_allowed = self.xapi_session.xenapi.VM.get_allowed_VBD_devices(self.vm_ref) 1218 1219 if self.module.params['disks']: 1220 # Get the list of all disk. Filter out any CDs found. 1221 vm_disk_params_list = [disk_params for disk_params in self.vm_params['VBDs'] if disk_params['type'] == "Disk"] 1222 1223 # Number of disks defined in module params have to be same or 1224 # higher than a number of existing disks attached to the VM. 1225 # We don't support removal or detachment of disks. 1226 if len(self.module.params['disks']) < len(vm_disk_params_list): 1227 self.module.fail_json(msg="VM check disks: provided disks configuration has less disks than the target VM (%d < %d)!" % 1228 (len(self.module.params['disks']), len(vm_disk_params_list))) 1229 1230 # Find the highest disk occupied userdevice. 1231 if not vm_disk_params_list: 1232 vm_disk_userdevice_highest = "-1" 1233 else: 1234 vm_disk_userdevice_highest = vm_disk_params_list[-1]['userdevice'] 1235 1236 for position in range(len(self.module.params['disks'])): 1237 if position < len(vm_disk_params_list): 1238 vm_disk_params = vm_disk_params_list[position] 1239 else: 1240 vm_disk_params = None 1241 1242 disk_params = self.module.params['disks'][position] 1243 1244 disk_size = self.get_normalized_disk_size(self.module.params['disks'][position], "VM check disks[%s]: " % position) 1245 1246 disk_name = disk_params.get('name') 1247 1248 if disk_name is not None and not disk_name: 1249 self.module.fail_json(msg="VM check disks[%s]: disk name cannot be an empty string!" % position) 1250 1251 # If this is an existing disk. 1252 if vm_disk_params and vm_disk_params['VDI']: 1253 disk_changes = [] 1254 1255 if disk_name and disk_name != vm_disk_params['VDI']['name_label']: 1256 disk_changes.append('name') 1257 1258 disk_name_desc = disk_params.get('name_desc') 1259 1260 if disk_name_desc is not None and disk_name_desc != vm_disk_params['VDI']['name_description']: 1261 disk_changes.append('name_desc') 1262 1263 if disk_size: 1264 if disk_size > int(vm_disk_params['VDI']['virtual_size']): 1265 disk_changes.append('size') 1266 need_poweredoff = True 1267 elif disk_size < int(vm_disk_params['VDI']['virtual_size']): 1268 self.module.fail_json(msg="VM check disks[%s]: disk size is smaller than existing (%d bytes < %s bytes). " 1269 "Reducing disk size is not allowed!" % (position, disk_size, vm_disk_params['VDI']['virtual_size'])) 1270 1271 config_changes_disks.append(disk_changes) 1272 # If this is a new disk. 1273 else: 1274 if not disk_size: 1275 self.module.fail_json(msg="VM check disks[%s]: no valid disk size specification found!" % position) 1276 1277 disk_sr_uuid = disk_params.get('sr_uuid') 1278 disk_sr = disk_params.get('sr') 1279 1280 if disk_sr_uuid is not None or disk_sr is not None: 1281 # Check existance only. Ignore return value. 1282 get_object_ref(self.module, disk_sr, disk_sr_uuid, obj_type="SR", fail=True, 1283 msg_prefix="VM check disks[%s]: " % position) 1284 elif self.default_sr_ref == 'OpaqueRef:NULL': 1285 self.module.fail_json(msg="VM check disks[%s]: no default SR found! You must specify SR explicitly." % position) 1286 1287 if not vbd_userdevices_allowed: 1288 self.module.fail_json(msg="VM check disks[%s]: maximum number of devices reached!" % position) 1289 1290 disk_userdevice = None 1291 1292 # We need to place a new disk right above the highest 1293 # placed existing disk to maintain relative disk 1294 # positions pairable with disk specifications in 1295 # module params. That place must not be occupied by 1296 # some other device like CD-ROM. 1297 for userdevice in vbd_userdevices_allowed: 1298 if int(userdevice) > int(vm_disk_userdevice_highest): 1299 disk_userdevice = userdevice 1300 vbd_userdevices_allowed.remove(userdevice) 1301 vm_disk_userdevice_highest = userdevice 1302 break 1303 1304 # If no place was found. 1305 if disk_userdevice is None: 1306 # Highest occupied place could be a CD-ROM device 1307 # so we have to include all devices regardless of 1308 # type when calculating out-of-bound position. 1309 disk_userdevice = str(int(self.vm_params['VBDs'][-1]['userdevice']) + 1) 1310 self.module.fail_json(msg="VM check disks[%s]: new disk position %s is out of bounds!" % (position, disk_userdevice)) 1311 1312 # For new disks we only track their position. 1313 config_new_disks.append((position, disk_userdevice)) 1314 1315 # We should append config_changes_disks to config_changes only 1316 # if there is at least one changed disk, else skip. 1317 for disk_change in config_changes_disks: 1318 if disk_change: 1319 config_changes.append({"disks_changed": config_changes_disks}) 1320 break 1321 1322 if config_new_disks: 1323 config_changes.append({"disks_new": config_new_disks}) 1324 1325 config_changes_cdrom = [] 1326 1327 if self.module.params['cdrom']: 1328 # Get the list of all CD-ROMs. Filter out any regular disks 1329 # found. If we found no existing CD-ROM, we will create it 1330 # later else take the first one found. 1331 vm_cdrom_params_list = [cdrom_params for cdrom_params in self.vm_params['VBDs'] if cdrom_params['type'] == "CD"] 1332 1333 # If no existing CD-ROM is found, we will need to add one. 1334 # We need to check if there is any userdevice allowed. 1335 if not vm_cdrom_params_list and not vbd_userdevices_allowed: 1336 self.module.fail_json(msg="VM check cdrom: maximum number of devices reached!") 1337 1338 cdrom_type = self.module.params['cdrom'].get('type') 1339 cdrom_iso_name = self.module.params['cdrom'].get('iso_name') 1340 1341 # If cdrom.iso_name is specified but cdrom.type is not, 1342 # then set cdrom.type to 'iso', unless cdrom.iso_name is 1343 # an empty string, in that case set cdrom.type to 'none'. 1344 if not cdrom_type: 1345 if cdrom_iso_name: 1346 cdrom_type = "iso" 1347 elif cdrom_iso_name is not None: 1348 cdrom_type = "none" 1349 1350 self.module.params['cdrom']['type'] = cdrom_type 1351 1352 # If type changed. 1353 if cdrom_type and (not vm_cdrom_params_list or cdrom_type != self.get_cdrom_type(vm_cdrom_params_list[0])): 1354 config_changes_cdrom.append('type') 1355 1356 if cdrom_type == "iso": 1357 # Check if ISO exists. 1358 # Check existance only. Ignore return value. 1359 get_object_ref(self.module, cdrom_iso_name, uuid=None, obj_type="ISO image", fail=True, 1360 msg_prefix="VM check cdrom.iso_name: ") 1361 1362 # Is ISO image changed? 1363 if (cdrom_iso_name and 1364 (not vm_cdrom_params_list or 1365 not vm_cdrom_params_list[0]['VDI'] or 1366 cdrom_iso_name != vm_cdrom_params_list[0]['VDI']['name_label'])): 1367 config_changes_cdrom.append('iso_name') 1368 1369 if config_changes_cdrom: 1370 config_changes.append({"cdrom": config_changes_cdrom}) 1371 1372 config_changes_networks = [] 1373 config_new_networks = [] 1374 1375 # Find allowed devices. 1376 vif_devices_allowed = self.xapi_session.xenapi.VM.get_allowed_VIF_devices(self.vm_ref) 1377 1378 if self.module.params['networks']: 1379 # Number of VIFs defined in module params have to be same or 1380 # higher than a number of existing VIFs attached to the VM. 1381 # We don't support removal of VIFs. 1382 if len(self.module.params['networks']) < len(self.vm_params['VIFs']): 1383 self.module.fail_json(msg="VM check networks: provided networks configuration has less interfaces than the target VM (%d < %d)!" % 1384 (len(self.module.params['networks']), len(self.vm_params['VIFs']))) 1385 1386 # Find the highest occupied device. 1387 if not self.vm_params['VIFs']: 1388 vif_device_highest = "-1" 1389 else: 1390 vif_device_highest = self.vm_params['VIFs'][-1]['device'] 1391 1392 for position in range(len(self.module.params['networks'])): 1393 if position < len(self.vm_params['VIFs']): 1394 vm_vif_params = self.vm_params['VIFs'][position] 1395 else: 1396 vm_vif_params = None 1397 1398 network_params = self.module.params['networks'][position] 1399 1400 network_name = network_params.get('name') 1401 1402 if network_name is not None and not network_name: 1403 self.module.fail_json(msg="VM check networks[%s]: network name cannot be an empty string!" % position) 1404 1405 if network_name: 1406 # Check existance only. Ignore return value. 1407 get_object_ref(self.module, network_name, uuid=None, obj_type="network", fail=True, 1408 msg_prefix="VM check networks[%s]: " % position) 1409 1410 network_mac = network_params.get('mac') 1411 1412 if network_mac is not None: 1413 network_mac = network_mac.lower() 1414 1415 if not is_mac(network_mac): 1416 self.module.fail_json(msg="VM check networks[%s]: specified MAC address '%s' is not valid!" % (position, network_mac)) 1417 1418 # IPv4 reconfiguration. 1419 network_type = network_params.get('type') 1420 network_ip = network_params.get('ip') 1421 network_netmask = network_params.get('netmask') 1422 network_prefix = None 1423 1424 # If networks.ip is specified and networks.type is not, 1425 # then set networks.type to 'static'. 1426 if not network_type and network_ip: 1427 network_type = "static" 1428 1429 # XenServer natively supports only 'none' and 'static' 1430 # type with 'none' being the same as 'dhcp'. 1431 if self.vm_params['customization_agent'] == "native" and network_type and network_type == "dhcp": 1432 network_type = "none" 1433 1434 if network_type and network_type == "static": 1435 if network_ip is not None: 1436 network_ip_split = network_ip.split('/') 1437 network_ip = network_ip_split[0] 1438 1439 if network_ip and not is_valid_ip_addr(network_ip): 1440 self.module.fail_json(msg="VM check networks[%s]: specified IPv4 address '%s' is not valid!" % (position, network_ip)) 1441 1442 if len(network_ip_split) > 1: 1443 network_prefix = network_ip_split[1] 1444 1445 if not is_valid_ip_prefix(network_prefix): 1446 self.module.fail_json(msg="VM check networks[%s]: specified IPv4 prefix '%s' is not valid!" % (position, network_prefix)) 1447 1448 if network_netmask is not None: 1449 if not is_valid_ip_netmask(network_netmask): 1450 self.module.fail_json(msg="VM check networks[%s]: specified IPv4 netmask '%s' is not valid!" % (position, network_netmask)) 1451 1452 network_prefix = ip_netmask_to_prefix(network_netmask, skip_check=True) 1453 elif network_prefix is not None: 1454 network_netmask = ip_prefix_to_netmask(network_prefix, skip_check=True) 1455 1456 # If any parameter is overridden at this point, update it. 1457 if network_type: 1458 network_params['type'] = network_type 1459 1460 if network_ip: 1461 network_params['ip'] = network_ip 1462 1463 if network_netmask: 1464 network_params['netmask'] = network_netmask 1465 1466 if network_prefix: 1467 network_params['prefix'] = network_prefix 1468 1469 network_gateway = network_params.get('gateway') 1470 1471 # Gateway can be an empty string (when removing gateway 1472 # configuration) but if it is not, it should be validated. 1473 if network_gateway and not is_valid_ip_addr(network_gateway): 1474 self.module.fail_json(msg="VM check networks[%s]: specified IPv4 gateway '%s' is not valid!" % (position, network_gateway)) 1475 1476 # IPv6 reconfiguration. 1477 network_type6 = network_params.get('type6') 1478 network_ip6 = network_params.get('ip6') 1479 network_prefix6 = None 1480 1481 # If networks.ip6 is specified and networks.type6 is not, 1482 # then set networks.type6 to 'static'. 1483 if not network_type6 and network_ip6: 1484 network_type6 = "static" 1485 1486 # XenServer natively supports only 'none' and 'static' 1487 # type with 'none' being the same as 'dhcp'. 1488 if self.vm_params['customization_agent'] == "native" and network_type6 and network_type6 == "dhcp": 1489 network_type6 = "none" 1490 1491 if network_type6 and network_type6 == "static": 1492 if network_ip6 is not None: 1493 network_ip6_split = network_ip6.split('/') 1494 network_ip6 = network_ip6_split[0] 1495 1496 if network_ip6 and not is_valid_ip6_addr(network_ip6): 1497 self.module.fail_json(msg="VM check networks[%s]: specified IPv6 address '%s' is not valid!" % (position, network_ip6)) 1498 1499 if len(network_ip6_split) > 1: 1500 network_prefix6 = network_ip6_split[1] 1501 1502 if not is_valid_ip6_prefix(network_prefix6): 1503 self.module.fail_json(msg="VM check networks[%s]: specified IPv6 prefix '%s' is not valid!" % (position, network_prefix6)) 1504 1505 # If any parameter is overridden at this point, update it. 1506 if network_type6: 1507 network_params['type6'] = network_type6 1508 1509 if network_ip6: 1510 network_params['ip6'] = network_ip6 1511 1512 if network_prefix6: 1513 network_params['prefix6'] = network_prefix6 1514 1515 network_gateway6 = network_params.get('gateway6') 1516 1517 # Gateway can be an empty string (when removing gateway 1518 # configuration) but if it is not, it should be validated. 1519 if network_gateway6 and not is_valid_ip6_addr(network_gateway6): 1520 self.module.fail_json(msg="VM check networks[%s]: specified IPv6 gateway '%s' is not valid!" % (position, network_gateway6)) 1521 1522 # If this is an existing VIF. 1523 if vm_vif_params and vm_vif_params['network']: 1524 network_changes = [] 1525 1526 if network_name and network_name != vm_vif_params['network']['name_label']: 1527 network_changes.append('name') 1528 1529 if network_mac and network_mac != vm_vif_params['MAC'].lower(): 1530 network_changes.append('mac') 1531 1532 if self.vm_params['customization_agent'] == "native": 1533 if network_type and network_type != vm_vif_params['ipv4_configuration_mode'].lower(): 1534 network_changes.append('type') 1535 1536 if network_type and network_type == "static": 1537 if network_ip and (not vm_vif_params['ipv4_addresses'] or 1538 not vm_vif_params['ipv4_addresses'][0] or 1539 network_ip != vm_vif_params['ipv4_addresses'][0].split('/')[0]): 1540 network_changes.append('ip') 1541 1542 if network_prefix and (not vm_vif_params['ipv4_addresses'] or 1543 not vm_vif_params['ipv4_addresses'][0] or 1544 network_prefix != vm_vif_params['ipv4_addresses'][0].split('/')[1]): 1545 network_changes.append('prefix') 1546 network_changes.append('netmask') 1547 1548 if network_gateway is not None and network_gateway != vm_vif_params['ipv4_gateway']: 1549 network_changes.append('gateway') 1550 1551 if network_type6 and network_type6 != vm_vif_params['ipv6_configuration_mode'].lower(): 1552 network_changes.append('type6') 1553 1554 if network_type6 and network_type6 == "static": 1555 if network_ip6 and (not vm_vif_params['ipv6_addresses'] or 1556 not vm_vif_params['ipv6_addresses'][0] or 1557 network_ip6 != vm_vif_params['ipv6_addresses'][0].split('/')[0]): 1558 network_changes.append('ip6') 1559 1560 if network_prefix6 and (not vm_vif_params['ipv6_addresses'] or 1561 not vm_vif_params['ipv6_addresses'][0] or 1562 network_prefix6 != vm_vif_params['ipv6_addresses'][0].split('/')[1]): 1563 network_changes.append('prefix6') 1564 1565 if network_gateway6 is not None and network_gateway6 != vm_vif_params['ipv6_gateway']: 1566 network_changes.append('gateway6') 1567 1568 elif self.vm_params['customization_agent'] == "custom": 1569 vm_xenstore_data = self.vm_params['xenstore_data'] 1570 1571 if network_type and network_type != vm_xenstore_data.get('vm-data/networks/%s/type' % vm_vif_params['device'], "none"): 1572 network_changes.append('type') 1573 need_poweredoff = True 1574 1575 if network_type and network_type == "static": 1576 if network_ip and network_ip != vm_xenstore_data.get('vm-data/networks/%s/ip' % vm_vif_params['device'], ""): 1577 network_changes.append('ip') 1578 need_poweredoff = True 1579 1580 if network_prefix and network_prefix != vm_xenstore_data.get('vm-data/networks/%s/prefix' % vm_vif_params['device'], ""): 1581 network_changes.append('prefix') 1582 network_changes.append('netmask') 1583 need_poweredoff = True 1584 1585 if network_gateway is not None and network_gateway != vm_xenstore_data.get('vm-data/networks/%s/gateway' % 1586 vm_vif_params['device'], ""): 1587 network_changes.append('gateway') 1588 need_poweredoff = True 1589 1590 if network_type6 and network_type6 != vm_xenstore_data.get('vm-data/networks/%s/type6' % vm_vif_params['device'], "none"): 1591 network_changes.append('type6') 1592 need_poweredoff = True 1593 1594 if network_type6 and network_type6 == "static": 1595 if network_ip6 and network_ip6 != vm_xenstore_data.get('vm-data/networks/%s/ip6' % vm_vif_params['device'], ""): 1596 network_changes.append('ip6') 1597 need_poweredoff = True 1598 1599 if network_prefix6 and network_prefix6 != vm_xenstore_data.get('vm-data/networks/%s/prefix6' % vm_vif_params['device'], ""): 1600 network_changes.append('prefix6') 1601 need_poweredoff = True 1602 1603 if network_gateway6 is not None and network_gateway6 != vm_xenstore_data.get('vm-data/networks/%s/gateway6' % 1604 vm_vif_params['device'], ""): 1605 network_changes.append('gateway6') 1606 need_poweredoff = True 1607 1608 config_changes_networks.append(network_changes) 1609 # If this is a new VIF. 1610 else: 1611 if not network_name: 1612 self.module.fail_json(msg="VM check networks[%s]: network name is required for new network interface!" % position) 1613 1614 if network_type and network_type == "static" and network_ip and not network_netmask: 1615 self.module.fail_json(msg="VM check networks[%s]: IPv4 netmask or prefix is required for new network interface!" % position) 1616 1617 if network_type6 and network_type6 == "static" and network_ip6 and not network_prefix6: 1618 self.module.fail_json(msg="VM check networks[%s]: IPv6 prefix is required for new network interface!" % position) 1619 1620 # Restart is needed if we are adding new network 1621 # interface with IP/gateway parameters specified 1622 # and custom agent is used. 1623 if self.vm_params['customization_agent'] == "custom": 1624 for parameter in ['type', 'ip', 'prefix', 'gateway', 'type6', 'ip6', 'prefix6', 'gateway6']: 1625 if network_params.get(parameter): 1626 need_poweredoff = True 1627 break 1628 1629 if not vif_devices_allowed: 1630 self.module.fail_json(msg="VM check networks[%s]: maximum number of network interfaces reached!" % position) 1631 1632 # We need to place a new network interface right above the 1633 # highest placed existing interface to maintain relative 1634 # positions pairable with network interface specifications 1635 # in module params. 1636 vif_device = str(int(vif_device_highest) + 1) 1637 1638 if vif_device not in vif_devices_allowed: 1639 self.module.fail_json(msg="VM check networks[%s]: new network interface position %s is out of bounds!" % (position, vif_device)) 1640 1641 vif_devices_allowed.remove(vif_device) 1642 vif_device_highest = vif_device 1643 1644 # For new VIFs we only track their position. 1645 config_new_networks.append((position, vif_device)) 1646 1647 # We should append config_changes_networks to config_changes only 1648 # if there is at least one changed network, else skip. 1649 for network_change in config_changes_networks: 1650 if network_change: 1651 config_changes.append({"networks_changed": config_changes_networks}) 1652 break 1653 1654 if config_new_networks: 1655 config_changes.append({"networks_new": config_new_networks}) 1656 1657 config_changes_custom_params = [] 1658 1659 if self.module.params['custom_params']: 1660 for position in range(len(self.module.params['custom_params'])): 1661 custom_param = self.module.params['custom_params'][position] 1662 1663 custom_param_key = custom_param['key'] 1664 custom_param_value = custom_param['value'] 1665 1666 if custom_param_key not in self.vm_params: 1667 self.module.fail_json(msg="VM check custom_params[%s]: unknown VM param '%s'!" % (position, custom_param_key)) 1668 1669 if custom_param_value != self.vm_params[custom_param_key]: 1670 # We only need to track custom param position. 1671 config_changes_custom_params.append(position) 1672 1673 if config_changes_custom_params: 1674 config_changes.append({"custom_params": config_changes_custom_params}) 1675 1676 if need_poweredoff: 1677 config_changes.append('need_poweredoff') 1678 1679 return config_changes 1680 1681 except XenAPI.Failure as f: 1682 self.module.fail_json(msg="XAPI ERROR: %s" % f.details) 1683 1684 def get_normalized_disk_size(self, disk_params, msg_prefix=""): 1685 """Parses disk size parameters and returns disk size in bytes. 1686 1687 This method tries to parse disk size module parameters. It fails 1688 with an error message if size cannot be parsed. 1689 1690 Args: 1691 disk_params (dist): A dictionary with disk parameters. 1692 msg_prefix (str): A string error messages should be prefixed 1693 with (default: ""). 1694 1695 Returns: 1696 int: disk size in bytes if disk size is successfully parsed or 1697 None if no disk size parameters were found. 1698 """ 1699 # There should be only single size spec but we make a list of all size 1700 # specs just in case. Priority is given to 'size' but if not found, we 1701 # check for 'size_tb', 'size_gb', 'size_mb' etc. and use first one 1702 # found. 1703 disk_size_spec = [x for x in disk_params.keys() if disk_params[x] is not None and (x.startswith('size_') or x == 'size')] 1704 1705 if disk_size_spec: 1706 try: 1707 # size 1708 if "size" in disk_size_spec: 1709 size_regex = re.compile(r'(\d+(?:\.\d+)?)\s*(.*)') 1710 disk_size_m = size_regex.match(disk_params['size']) 1711 1712 if disk_size_m: 1713 size = disk_size_m.group(1) 1714 unit = disk_size_m.group(2) 1715 else: 1716 raise ValueError 1717 # size_tb, size_gb, size_mb, size_kb, size_b 1718 else: 1719 size = disk_params[disk_size_spec[0]] 1720 unit = disk_size_spec[0].split('_')[-1] 1721 1722 if not unit: 1723 unit = "b" 1724 else: 1725 unit = unit.lower() 1726 1727 if re.match(r'\d+\.\d+', size): 1728 # We found float value in string, let's typecast it. 1729 if unit == "b": 1730 # If we found float but unit is bytes, we get the integer part only. 1731 size = int(float(size)) 1732 else: 1733 size = float(size) 1734 else: 1735 # We found int value in string, let's typecast it. 1736 size = int(size) 1737 1738 if not size or size < 0: 1739 raise ValueError 1740 1741 except (TypeError, ValueError, NameError): 1742 # Common failure 1743 self.module.fail_json(msg="%sfailed to parse disk size! Please review value provided using documentation." % msg_prefix) 1744 1745 disk_units = dict(tb=4, gb=3, mb=2, kb=1, b=0) 1746 1747 if unit in disk_units: 1748 return int(size * (1024 ** disk_units[unit])) 1749 else: 1750 self.module.fail_json(msg="%s'%s' is not a supported unit for disk size! Supported units are ['%s']." % 1751 (msg_prefix, unit, "', '".join(sorted(disk_units.keys(), key=lambda key: disk_units[key])))) 1752 else: 1753 return None 1754 1755 @staticmethod 1756 def get_cdrom_type(vm_cdrom_params): 1757 """Returns VM CD-ROM type.""" 1758 # TODO: implement support for detecting type host. No server to test 1759 # this on at the moment. 1760 if vm_cdrom_params['empty']: 1761 return "none" 1762 else: 1763 return "iso" 1764 1765 1766def main(): 1767 argument_spec = xenserver_common_argument_spec() 1768 argument_spec.update( 1769 state=dict(type='str', default='present', 1770 choices=['present', 'absent', 'poweredon']), 1771 name=dict(type='str', aliases=['name_label']), 1772 name_desc=dict(type='str'), 1773 uuid=dict(type='str'), 1774 template=dict(type='str', aliases=['template_src']), 1775 template_uuid=dict(type='str'), 1776 is_template=dict(type='bool', default=False), 1777 folder=dict(type='str'), 1778 hardware=dict( 1779 type='dict', 1780 options=dict( 1781 num_cpus=dict(type='int'), 1782 num_cpu_cores_per_socket=dict(type='int'), 1783 memory_mb=dict(type='int'), 1784 ), 1785 ), 1786 disks=dict( 1787 type='list', 1788 elements='dict', 1789 options=dict( 1790 size=dict(type='str'), 1791 size_tb=dict(type='str'), 1792 size_gb=dict(type='str'), 1793 size_mb=dict(type='str'), 1794 size_kb=dict(type='str'), 1795 size_b=dict(type='str'), 1796 name=dict(type='str', aliases=['name_label']), 1797 name_desc=dict(type='str'), 1798 sr=dict(type='str'), 1799 sr_uuid=dict(type='str'), 1800 ), 1801 aliases=['disk'], 1802 mutually_exclusive=[ 1803 ['size', 'size_tb', 'size_gb', 'size_mb', 'size_kb', 'size_b'], 1804 ['sr', 'sr_uuid'], 1805 ], 1806 ), 1807 cdrom=dict( 1808 type='dict', 1809 options=dict( 1810 type=dict(type='str', choices=['none', 'iso']), 1811 iso_name=dict(type='str'), 1812 ), 1813 required_if=[ 1814 ['type', 'iso', ['iso_name']], 1815 ], 1816 ), 1817 networks=dict( 1818 type='list', 1819 elements='dict', 1820 options=dict( 1821 name=dict(type='str', aliases=['name_label']), 1822 mac=dict(type='str'), 1823 type=dict(type='str', choices=['none', 'dhcp', 'static']), 1824 ip=dict(type='str'), 1825 netmask=dict(type='str'), 1826 gateway=dict(type='str'), 1827 type6=dict(type='str', choices=['none', 'dhcp', 'static']), 1828 ip6=dict(type='str'), 1829 gateway6=dict(type='str'), 1830 ), 1831 aliases=['network'], 1832 required_if=[ 1833 ['type', 'static', ['ip']], 1834 ['type6', 'static', ['ip6']], 1835 ], 1836 ), 1837 home_server=dict(type='str'), 1838 custom_params=dict( 1839 type='list', 1840 elements='dict', 1841 options=dict( 1842 key=dict(type='str', required=True), 1843 value=dict(type='raw', required=True), 1844 ), 1845 ), 1846 wait_for_ip_address=dict(type='bool', default=False), 1847 state_change_timeout=dict(type='int', default=0), 1848 linked_clone=dict(type='bool', default=False), 1849 force=dict(type='bool', default=False), 1850 ) 1851 1852 module = AnsibleModule(argument_spec=argument_spec, 1853 supports_check_mode=True, 1854 required_one_of=[ 1855 ['name', 'uuid'], 1856 ], 1857 mutually_exclusive=[ 1858 ['template', 'template_uuid'], 1859 ], 1860 ) 1861 1862 result = {'failed': False, 'changed': False} 1863 1864 vm = XenServerVM(module) 1865 1866 # Find existing VM 1867 if vm.exists(): 1868 if module.params['state'] == "absent": 1869 vm.destroy() 1870 result['changed'] = True 1871 elif module.params['state'] == "present": 1872 config_changes = vm.reconfigure() 1873 1874 if config_changes: 1875 result['changed'] = True 1876 1877 # Make new disk and network changes more user friendly 1878 # and informative. 1879 for change in config_changes: 1880 if isinstance(change, dict): 1881 if change.get('disks_new'): 1882 disks_new = [] 1883 1884 for position, userdevice in change['disks_new']: 1885 disk_new_params = {"position": position, "vbd_userdevice": userdevice} 1886 disk_params = module.params['disks'][position] 1887 1888 for k in disk_params.keys(): 1889 if disk_params[k] is not None: 1890 disk_new_params[k] = disk_params[k] 1891 1892 disks_new.append(disk_new_params) 1893 1894 if disks_new: 1895 change['disks_new'] = disks_new 1896 1897 elif change.get('networks_new'): 1898 networks_new = [] 1899 1900 for position, device in change['networks_new']: 1901 network_new_params = {"position": position, "vif_device": device} 1902 network_params = module.params['networks'][position] 1903 1904 for k in network_params.keys(): 1905 if network_params[k] is not None: 1906 network_new_params[k] = network_params[k] 1907 1908 networks_new.append(network_new_params) 1909 1910 if networks_new: 1911 change['networks_new'] = networks_new 1912 1913 result['changes'] = config_changes 1914 1915 elif module.params['state'] in ["poweredon", "poweredoff", "restarted", "shutdownguest", "rebootguest", "suspended"]: 1916 result['changed'] = vm.set_power_state(module.params['state']) 1917 elif module.params['state'] != "absent": 1918 vm.deploy() 1919 result['changed'] = True 1920 1921 if module.params['wait_for_ip_address'] and module.params['state'] != "absent": 1922 vm.wait_for_ip_address() 1923 1924 result['instance'] = vm.gather_facts() 1925 1926 if result['failed']: 1927 module.fail_json(**result) 1928 else: 1929 module.exit_json(**result) 1930 1931 1932if __name__ == '__main__': 1933 main() 1934