mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-02-04 07:51:50 +00:00
* Resolve E713 and E714 (not in/is tests). * Address UP018 (unnecessary str call). * UP045 requires Python 3.10+. * Address UP007 (X | Y for type annotations). * Address UP035 (import Callable from collections.abc). * Address UP006 (t.Dict -> dict). * Address UP009 (UTF-8 encoding comment). * Address UP034 (extraneous parantheses). * Address SIM910 (dict.get() with None default). * Address F401 (unused import). * Address UP020 (use builtin open). * Address B009 and B010 (getattr/setattr with constant name). * Address SIM300 (Yoda conditions). * UP029 isn't in use anyway. * Address FLY002 (static join). * Address B034 (re.sub positional args). * Address B020 (loop variable overrides input). * Address B017 (assert raise Exception). * Address SIM211 (if expression with false/true). * Address SIM113 (enumerate for loop). * Address UP036 (sys.version_info checks). * Remove unnecessary UP039. * Address SIM201 (not ==). * Address SIM212 (if expr with twisted arms). * Add changelog fragment. * Reformat.
2286 lines
102 KiB
Python
2286 lines
102 KiB
Python
#!/usr/bin/python
|
|
#
|
|
# Copyright (c) 2018, Bojan Vitnik <bvitnik@mainstream.rs>
|
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
from __future__ import annotations
|
|
|
|
DOCUMENTATION = r"""
|
|
module: xenserver_guest
|
|
short_description: Manages virtual machines running on Citrix Hypervisor/XenServer host or pool
|
|
description: >-
|
|
This module can be used to create new virtual machines from templates or other virtual machines, modify various virtual
|
|
machine components like network and disk, rename a virtual machine and remove a virtual machine with associated components.
|
|
author:
|
|
- Bojan Vitnik (@bvitnik) <bvitnik@mainstream.rs>
|
|
notes:
|
|
- Minimal supported version of XenServer is 5.6.
|
|
- Module was tested with XenServer 6.5, 7.1, 7.2, 7.6, Citrix Hypervisor 8.0, XCP-ng 7.6 and 8.0.
|
|
- 'To acquire XenAPI Python library, just run C(pip install XenAPI) on your Ansible Control Node. The library can also be
|
|
found inside Citrix Hypervisor/XenServer SDK (downloadable from Citrix website). Copy the C(XenAPI.py) file from the SDK
|
|
to your Python site-packages on your Ansible Control Node to use it. Latest version of the library can also be acquired
|
|
from GitHub: U(https://raw.githubusercontent.com/xapi-project/xen-api/master/scripts/examples/python/XenAPI/XenAPI.py).'
|
|
- If no scheme is specified in O(hostname), module defaults to C(http://) because C(https://) is problematic in most setups.
|
|
Make sure you are accessing XenServer host in trusted environment or use C(https://) scheme explicitly.
|
|
- To use C(https://) scheme for O(hostname) you have to either import host certificate to your OS certificate store or use
|
|
O(validate_certs=false) which requires XenAPI library from XenServer 7.2 SDK or newer and Python 2.7.9 or newer.
|
|
- 'Network configuration inside a guest OS, by using parameters O(networks[].type), O(networks[].ip), O(networks[].gateway)
|
|
and so on, is supported on XenServer 7.0 or newer for Windows guests by using official XenServer Guest agent support for
|
|
network configuration. The module tries to detect if such support is available and utilize it, else it uses a custom method
|
|
of configuration using xenstore. Since XenServer Guest agent only support None and Static types of network configuration,
|
|
where None means DHCP configured interface, O(networks[].type) and O(networks[].type6) values V(none) and V(dhcp) have
|
|
same effect. More info here:
|
|
U(https://web.archive.org/web/20180218110151/https://xenserver.org/blog/entry/set-windows-guest-vm-static-ip-address-in-xenserver.html).'
|
|
- 'On platforms without official support for network configuration inside a guest OS, network parameters are written to
|
|
xenstore 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 through 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 to implement a boot time scripts or custom agent that reads the parameters
|
|
from xenstore and configure network with given parameters. Take note that for xenstore data to become available inside
|
|
a guest, a VM restart is needed hence module requires VM restart if any parameter is changed. This is a limitation of
|
|
XenAPI and xenstore. Considering these limitations, network configuration through xenstore is most useful for bootstrapping
|
|
newly deployed VMs, much less for reconfiguring existing ones. More info here: U(https://support.citrix.com/article/CTX226713).'
|
|
requirements:
|
|
- XenAPI
|
|
attributes:
|
|
check_mode:
|
|
support: full
|
|
diff_mode:
|
|
support: none
|
|
options:
|
|
state:
|
|
description:
|
|
- Specify the state VM should be in.
|
|
- If O(state) is set to V(present) and VM exists, ensure the VM configuration conforms to given parameters.
|
|
- If O(state) is set to V(present) and VM does not exist, then VM is deployed with given parameters.
|
|
- If O(state) is set to V(absent) and VM exists, then VM is removed with its associated components.
|
|
- If O(state) is set to V(poweredon) and VM does not exist, then VM is deployed with given parameters and powered on
|
|
automatically.
|
|
type: str
|
|
default: present
|
|
choices: [present, absent, poweredon]
|
|
name:
|
|
description:
|
|
- Name of the VM to work with.
|
|
- VMs running on XenServer do not necessarily have unique names. The module fails if multiple VMs with same name are
|
|
found.
|
|
- In case of multiple VMs with same name, use O(uuid) to uniquely specify VM to manage.
|
|
- This parameter is case sensitive.
|
|
type: str
|
|
aliases: [name_label]
|
|
name_desc:
|
|
description:
|
|
- VM description.
|
|
type: str
|
|
uuid:
|
|
description:
|
|
- UUID of the VM to manage if known. This is XenServer's unique identifier.
|
|
- It is required if name is not unique.
|
|
- Please note that a supplied UUID is ignored on VM creation, as XenServer creates the UUID internally.
|
|
type: str
|
|
template:
|
|
description:
|
|
- Name of a template, an existing VM (must be shut down) or a snapshot that should be used to create VM.
|
|
- Templates/VMs/snapshots on XenServer do not necessarily have unique names. The module fails if multiple templates
|
|
with same name are found.
|
|
- In case of multiple templates/VMs/snapshots with same name, use O(template_uuid) to uniquely specify source template.
|
|
- If VM already exists, this setting is ignored.
|
|
- This parameter is case sensitive.
|
|
type: str
|
|
aliases: [template_src]
|
|
template_uuid:
|
|
description:
|
|
- UUID of a template, an existing VM or a snapshot that should be used to create VM.
|
|
- It is required if template name is not unique.
|
|
type: str
|
|
is_template:
|
|
description:
|
|
- Convert VM to template.
|
|
type: bool
|
|
default: false
|
|
folder:
|
|
description:
|
|
- Destination folder for VM.
|
|
- This parameter is case sensitive.
|
|
- 'Example: O(folder=/folder1/folder2).'
|
|
type: str
|
|
hardware:
|
|
description:
|
|
- Manage VM's hardware parameters. VM needs to be shut down to reconfigure these parameters.
|
|
type: dict
|
|
suboptions:
|
|
num_cpus:
|
|
description:
|
|
- Number of CPUs.
|
|
type: int
|
|
num_cpu_cores_per_socket:
|
|
description:
|
|
- Number of Cores Per Socket. O(hardware.num_cpus) has to be a multiple of O(hardware.num_cpu_cores_per_socket).
|
|
type: int
|
|
memory_mb:
|
|
description:
|
|
- Amount of memory in MB.
|
|
type: int
|
|
disks:
|
|
description:
|
|
- A list of disks to add to VM.
|
|
- All parameters are case sensitive.
|
|
- Removing or detaching existing disks of VM is not supported.
|
|
- New disks are required to have either a O(disks[].size) or one of O(ignore:disks[].size_[tb,gb,mb,kb,b]) parameters
|
|
specified.
|
|
- VM needs to be shut down to reconfigure disk size.
|
|
type: list
|
|
elements: dict
|
|
aliases: [disk]
|
|
suboptions:
|
|
size:
|
|
description:
|
|
- 'Disk size with unit. Unit must be: V(b), V(kb), V(mb), V(gb), V(tb). VM needs to be shut down to reconfigure
|
|
this parameter.'
|
|
- If no unit is specified, size is assumed to be in bytes.
|
|
type: str
|
|
size_b:
|
|
description:
|
|
- Disk size in bytes.
|
|
type: str
|
|
size_kb:
|
|
description:
|
|
- Disk size in kilobytes.
|
|
type: str
|
|
size_mb:
|
|
description:
|
|
- Disk size in megabytes.
|
|
type: str
|
|
size_gb:
|
|
description:
|
|
- Disk size in gigabytes.
|
|
type: str
|
|
size_tb:
|
|
description:
|
|
- Disk size in terabytes.
|
|
type: str
|
|
name:
|
|
description:
|
|
- Disk name.
|
|
type: str
|
|
aliases: [name_label]
|
|
name_desc:
|
|
description:
|
|
- Disk description.
|
|
type: str
|
|
sr:
|
|
description:
|
|
- Storage Repository to create disk on. If not specified, it uses default SR. Cannot be used for moving disk to
|
|
other SR.
|
|
type: str
|
|
sr_uuid:
|
|
description:
|
|
- UUID of a SR to create disk on. Use if SR name is not unique.
|
|
type: str
|
|
cdrom:
|
|
description:
|
|
- A CD-ROM configuration for the VM.
|
|
- All parameters are case sensitive.
|
|
type: dict
|
|
suboptions:
|
|
type:
|
|
description:
|
|
- The type of CD-ROM. When V(none) the CD-ROM device is present but empty.
|
|
type: str
|
|
choices: [none, iso]
|
|
iso_name:
|
|
description:
|
|
- The file name of an ISO image from one of the XenServer ISO Libraries (implies O(cdrom.type=iso)).
|
|
- Required if O(cdrom.type) is set to V(iso).
|
|
type: str
|
|
networks:
|
|
description:
|
|
- A list of networks (in the order of the NICs).
|
|
- All parameters are case sensitive.
|
|
- Name is required for new NICs. Other parameters are optional in all cases.
|
|
type: list
|
|
elements: dict
|
|
aliases: [network]
|
|
suboptions:
|
|
name:
|
|
description:
|
|
- Name of a XenServer network to attach the network interface to.
|
|
type: str
|
|
aliases: [name_label]
|
|
mac:
|
|
description:
|
|
- Customize MAC address of the interface.
|
|
type: str
|
|
type:
|
|
description:
|
|
- Type of IPv4 assignment. Value V(none) means whatever is default for OS.
|
|
- On some operating systems it could be DHCP configured (for example Windows) or unconfigured interface (for example
|
|
Linux).
|
|
type: str
|
|
choices: [none, dhcp, static]
|
|
ip:
|
|
description:
|
|
- Static IPv4 address (implies O(networks[].type=static)). Can include prefix in format C(<IPv4 address>/<prefix>)
|
|
instead of using C(netmask).
|
|
type: str
|
|
netmask:
|
|
description:
|
|
- Static IPv4 netmask required for O(networks[].ip) if prefix is not specified.
|
|
type: str
|
|
gateway:
|
|
description:
|
|
- Static IPv4 gateway.
|
|
type: str
|
|
type6:
|
|
description:
|
|
- Type of IPv6 assignment. Value V(none) means whatever is default for OS.
|
|
type: str
|
|
choices: [none, dhcp, static]
|
|
ip6:
|
|
description:
|
|
- Static IPv6 address (implies O(networks[].type6=static)) with prefix in format C(<IPv6 address>/<prefix>).
|
|
type: str
|
|
gateway6:
|
|
description:
|
|
- Static IPv6 gateway.
|
|
type: str
|
|
home_server:
|
|
description:
|
|
- Name of a XenServer host that is a Home Server for the VM.
|
|
- This parameter is case sensitive.
|
|
type: str
|
|
custom_params:
|
|
description:
|
|
- Define a list of custom VM params to set on VM.
|
|
- Useful for advanced users familiar with managing VM params through C(xe) CLI.
|
|
- A custom value object takes two fields O(custom_params[].key) and O(custom_params[].value) (see example below).
|
|
type: list
|
|
elements: dict
|
|
suboptions:
|
|
key:
|
|
description:
|
|
- VM param name.
|
|
type: str
|
|
required: true
|
|
value:
|
|
description:
|
|
- VM param value.
|
|
type: raw
|
|
required: true
|
|
wait_for_ip_address:
|
|
description:
|
|
- Wait until XenServer detects an IP address for the VM. If O(state) is set to V(absent), this parameter is ignored.
|
|
- This requires XenServer Tools to be preinstalled on the VM to work properly.
|
|
type: bool
|
|
default: false
|
|
state_change_timeout:
|
|
description:
|
|
- By default, the module waits indefinitely for VM to acquire an IP address if O(wait_for_ip_address=true).
|
|
- If this parameter is set to a positive value, the module instead waits the specified number of seconds for the state
|
|
change.
|
|
- In case of timeout, module generates an error message.
|
|
type: int
|
|
default: 0
|
|
linked_clone:
|
|
description:
|
|
- Whether to create a Linked Clone from the template, existing VM or snapshot. If V(false), it creates a full copy.
|
|
- This is equivalent to C(Use storage-level fast disk clone) option in XenCenter.
|
|
type: bool
|
|
default: false
|
|
force:
|
|
description:
|
|
- Ignore warnings and complete the actions.
|
|
- This parameter is useful for removing VM in running state or reconfiguring VM params that require VM to be shut down.
|
|
type: bool
|
|
default: false
|
|
extends_documentation_fragment:
|
|
- community.general.xenserver.documentation
|
|
- community.general.attributes
|
|
"""
|
|
|
|
EXAMPLES = r"""
|
|
- name: Create a VM from a template
|
|
community.general.xenserver_guest:
|
|
hostname: "{{ xenserver_hostname }}"
|
|
username: "{{ xenserver_username }}"
|
|
password: "{{ xenserver_password }}"
|
|
folder: /testvms
|
|
name: testvm_2
|
|
state: poweredon
|
|
template: CentOS 7
|
|
disks:
|
|
- size_gb: 10
|
|
sr: my_sr
|
|
hardware:
|
|
num_cpus: 6
|
|
num_cpu_cores_per_socket: 3
|
|
memory_mb: 512
|
|
cdrom:
|
|
type: iso
|
|
iso_name: guest-tools.iso
|
|
networks:
|
|
- name: VM Network
|
|
mac: aa:bb:dd:aa:00:14
|
|
wait_for_ip_address: true
|
|
delegate_to: localhost
|
|
register: deploy
|
|
|
|
- name: Create a VM template
|
|
community.general.xenserver_guest:
|
|
hostname: "{{ xenserver_hostname }}"
|
|
username: "{{ xenserver_username }}"
|
|
password: "{{ xenserver_password }}"
|
|
folder: /testvms
|
|
name: testvm_6
|
|
is_template: true
|
|
disk:
|
|
- size_gb: 10
|
|
sr: my_sr
|
|
hardware:
|
|
memory_mb: 512
|
|
num_cpus: 1
|
|
delegate_to: localhost
|
|
register: deploy
|
|
|
|
- name: Rename a VM (requires the VM's UUID)
|
|
community.general.xenserver_guest:
|
|
hostname: "{{ xenserver_hostname }}"
|
|
username: "{{ xenserver_username }}"
|
|
password: "{{ xenserver_password }}"
|
|
uuid: 421e4592-c069-924d-ce20-7e7533fab926
|
|
name: new_name
|
|
state: present
|
|
delegate_to: localhost
|
|
|
|
- name: Remove a VM by UUID
|
|
community.general.xenserver_guest:
|
|
hostname: "{{ xenserver_hostname }}"
|
|
username: "{{ xenserver_username }}"
|
|
password: "{{ xenserver_password }}"
|
|
uuid: 421e4592-c069-924d-ce20-7e7533fab926
|
|
state: absent
|
|
delegate_to: localhost
|
|
|
|
- name: Modify custom params (boot order)
|
|
community.general.xenserver_guest:
|
|
hostname: "{{ xenserver_hostname }}"
|
|
username: "{{ xenserver_username }}"
|
|
password: "{{ xenserver_password }}"
|
|
name: testvm_8
|
|
state: present
|
|
custom_params:
|
|
- key: HVM_boot_params
|
|
value: {"order": "ndc"}
|
|
delegate_to: localhost
|
|
|
|
- name: Customize network parameters
|
|
community.general.xenserver_guest:
|
|
hostname: "{{ xenserver_hostname }}"
|
|
username: "{{ xenserver_username }}"
|
|
password: "{{ xenserver_password }}"
|
|
name: testvm_10
|
|
networks:
|
|
- name: VM Network
|
|
ip: 192.168.1.100/24
|
|
gateway: 192.168.1.1
|
|
- type: dhcp
|
|
delegate_to: localhost
|
|
"""
|
|
|
|
RETURN = r"""
|
|
instance:
|
|
description: Metadata about the VM.
|
|
returned: always
|
|
type: dict
|
|
sample:
|
|
{
|
|
"cdrom": {
|
|
"type": "none"
|
|
},
|
|
"customization_agent": "native",
|
|
"disks": [
|
|
{
|
|
"name": "testvm_11-0",
|
|
"name_desc": "",
|
|
"os_device": "xvda",
|
|
"size": 42949672960,
|
|
"sr": "Local storage",
|
|
"sr_uuid": "0af1245e-bdb0-ba33-1446-57a962ec4075",
|
|
"vbd_userdevice": "0"
|
|
},
|
|
{
|
|
"name": "testvm_11-1",
|
|
"name_desc": "",
|
|
"os_device": "xvdb",
|
|
"size": 42949672960,
|
|
"sr": "Local storage",
|
|
"sr_uuid": "0af1245e-bdb0-ba33-1446-57a962ec4075",
|
|
"vbd_userdevice": "1"
|
|
}
|
|
],
|
|
"domid": "56",
|
|
"folder": "",
|
|
"hardware": {
|
|
"memory_mb": 8192,
|
|
"num_cpu_cores_per_socket": 2,
|
|
"num_cpus": 4
|
|
},
|
|
"home_server": "",
|
|
"is_template": false,
|
|
"name": "testvm_11",
|
|
"name_desc": "",
|
|
"networks": [
|
|
{
|
|
"gateway": "192.168.0.254",
|
|
"gateway6": "fc00::fffe",
|
|
"ip": "192.168.0.200",
|
|
"ip6": [
|
|
"fe80:0000:0000:0000:e9cb:625a:32c5:c291",
|
|
"fc00:0000:0000:0000:0000:0000:0000:0001"
|
|
],
|
|
"mac": "ba:91:3a:48:20:76",
|
|
"mtu": "1500",
|
|
"name": "Pool-wide network associated with eth1",
|
|
"netmask": "255.255.255.128",
|
|
"prefix": "25",
|
|
"prefix6": "64",
|
|
"vif_device": "0"
|
|
}
|
|
],
|
|
"other_config": {
|
|
"base_template_name": "Windows Server 2016 (64-bit)",
|
|
"import_task": "OpaqueRef:e43eb71c-45d6-5351-09ff-96e4fb7d0fa5",
|
|
"install-methods": "cdrom",
|
|
"instant": "true",
|
|
"mac_seed": "f83e8d8a-cfdc-b105-b054-ef5cb416b77e"
|
|
},
|
|
"platform": {
|
|
"acpi": "1",
|
|
"apic": "true",
|
|
"cores-per-socket": "2",
|
|
"device_id": "0002",
|
|
"hpet": "true",
|
|
"nx": "true",
|
|
"pae": "true",
|
|
"timeoffset": "-25200",
|
|
"vga": "std",
|
|
"videoram": "8",
|
|
"viridian": "true",
|
|
"viridian_reference_tsc": "true",
|
|
"viridian_time_ref_count": "true"
|
|
},
|
|
"state": "poweredon",
|
|
"uuid": "e3c0b2d5-5f05-424e-479c-d3df8b3e7cda",
|
|
"xenstore_data": {
|
|
"vm-data": ""
|
|
}
|
|
}
|
|
changes:
|
|
description: Detected or made changes to VM.
|
|
returned: always
|
|
type: list
|
|
sample:
|
|
[
|
|
{
|
|
"hardware": [
|
|
"num_cpus"
|
|
]
|
|
},
|
|
{
|
|
"disks_changed": [
|
|
[],
|
|
[
|
|
"size"
|
|
]
|
|
]
|
|
},
|
|
{
|
|
"disks_new": [
|
|
{
|
|
"name": "new-disk",
|
|
"name_desc": "",
|
|
"position": 2,
|
|
"size_gb": "4",
|
|
"vbd_userdevice": "2"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"cdrom": [
|
|
"type",
|
|
"iso_name"
|
|
]
|
|
},
|
|
{
|
|
"networks_changed": [
|
|
[
|
|
"mac"
|
|
]
|
|
]
|
|
},
|
|
{
|
|
"networks_new": [
|
|
{
|
|
"name": "Pool-wide network associated with eth2",
|
|
"position": 1,
|
|
"vif_device": "1"
|
|
}
|
|
]
|
|
},
|
|
"need_poweredoff"
|
|
]
|
|
"""
|
|
|
|
import re
|
|
|
|
HAS_XENAPI = False
|
|
try:
|
|
import XenAPI
|
|
|
|
HAS_XENAPI = True
|
|
except ImportError:
|
|
pass
|
|
|
|
from ansible.module_utils.basic import AnsibleModule
|
|
from ansible.module_utils.common.network import is_mac
|
|
from ansible_collections.community.general.plugins.module_utils.xenserver import (
|
|
xenserver_common_argument_spec,
|
|
XenServerObject,
|
|
get_object_ref,
|
|
gather_vm_params,
|
|
gather_vm_facts,
|
|
set_vm_power_state,
|
|
wait_for_vm_ip_address,
|
|
is_valid_ip_addr,
|
|
is_valid_ip_netmask,
|
|
is_valid_ip_prefix,
|
|
ip_prefix_to_netmask,
|
|
ip_netmask_to_prefix,
|
|
is_valid_ip6_addr,
|
|
is_valid_ip6_prefix,
|
|
)
|
|
|
|
|
|
class XenServerVM(XenServerObject):
|
|
"""Class for managing XenServer VM.
|
|
|
|
Attributes:
|
|
vm_ref (str): XAPI reference to VM.
|
|
vm_params (dict): A dictionary with VM parameters as returned
|
|
by gather_vm_params() function.
|
|
"""
|
|
|
|
def __init__(self, module):
|
|
"""Inits XenServerVM using module parameters.
|
|
|
|
Args:
|
|
module: Reference to Ansible module object.
|
|
"""
|
|
super().__init__(module)
|
|
|
|
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: ",
|
|
)
|
|
self.gather_params()
|
|
|
|
def exists(self):
|
|
"""Returns True if VM exists, else False."""
|
|
return True if self.vm_ref is not None else False
|
|
|
|
def gather_params(self):
|
|
"""Gathers all VM parameters available in XAPI database."""
|
|
self.vm_params = gather_vm_params(self.module, self.vm_ref)
|
|
|
|
def gather_facts(self):
|
|
"""Gathers and returns VM facts."""
|
|
return gather_vm_facts(self.module, self.vm_params)
|
|
|
|
def set_power_state(self, power_state):
|
|
"""Controls VM power state."""
|
|
state_changed, current_state = set_vm_power_state(
|
|
self.module, self.vm_ref, power_state, self.module.params["state_change_timeout"]
|
|
)
|
|
|
|
# If state has changed, update vm_params.
|
|
if state_changed:
|
|
self.vm_params["power_state"] = current_state.capitalize()
|
|
|
|
return state_changed
|
|
|
|
def wait_for_ip_address(self):
|
|
"""Waits for VM to acquire an IP address."""
|
|
self.vm_params["guest_metrics"] = wait_for_vm_ip_address(
|
|
self.module, self.vm_ref, self.module.params["state_change_timeout"]
|
|
)
|
|
|
|
def deploy(self):
|
|
"""Deploys new VM from template."""
|
|
# Safety check.
|
|
if self.exists():
|
|
self.module.fail_json(msg="Called deploy on existing VM!")
|
|
|
|
try:
|
|
templ_ref = get_object_ref(
|
|
self.module,
|
|
self.module.params["template"],
|
|
self.module.params["template_uuid"],
|
|
obj_type="template",
|
|
fail=True,
|
|
msg_prefix="VM deploy: ",
|
|
)
|
|
|
|
# Is this an existing running VM?
|
|
if self.xapi_session.xenapi.VM.get_power_state(templ_ref).lower() != "halted":
|
|
self.module.fail_json(msg="VM deploy: running VM cannot be used as a template!")
|
|
|
|
# Find a SR we can use for VM.copy(). We use SR of the first disk
|
|
# if specified or default SR if not specified.
|
|
disk_params_list = self.module.params["disks"]
|
|
|
|
sr_ref = None
|
|
|
|
if disk_params_list:
|
|
disk_params = disk_params_list[0]
|
|
|
|
disk_sr_uuid = disk_params.get("sr_uuid")
|
|
disk_sr = disk_params.get("sr")
|
|
|
|
if disk_sr_uuid is not None or disk_sr is not None:
|
|
sr_ref = get_object_ref(
|
|
self.module, disk_sr, disk_sr_uuid, obj_type="SR", fail=True, msg_prefix="VM deploy disks[0]: "
|
|
)
|
|
|
|
if not sr_ref:
|
|
if self.default_sr_ref != "OpaqueRef:NULL":
|
|
sr_ref = self.default_sr_ref
|
|
else:
|
|
self.module.fail_json(
|
|
msg="VM deploy disks[0]: no default SR found! You must specify SR explicitly."
|
|
)
|
|
|
|
# VM name could be an empty string which is bad.
|
|
if self.module.params["name"] is not None and not self.module.params["name"]:
|
|
self.module.fail_json(msg="VM deploy: VM name must not be an empty string!")
|
|
|
|
# Support for Ansible check mode.
|
|
if self.module.check_mode:
|
|
return
|
|
|
|
# Now we can instantiate VM. We use VM.clone for linked_clone and
|
|
# VM.copy for non linked_clone.
|
|
if self.module.params["linked_clone"]:
|
|
self.vm_ref = self.xapi_session.xenapi.VM.clone(templ_ref, self.module.params["name"])
|
|
else:
|
|
self.vm_ref = self.xapi_session.xenapi.VM.copy(templ_ref, self.module.params["name"], sr_ref)
|
|
|
|
# Description is copied over from template so we reset it.
|
|
self.xapi_session.xenapi.VM.set_name_description(self.vm_ref, "")
|
|
|
|
# If template is one of built-in XenServer templates, we have to
|
|
# do some additional steps.
|
|
# Note: VM.get_is_default_template() is supported from XenServer 7.2
|
|
# onward so we use an alternative way.
|
|
templ_other_config = self.xapi_session.xenapi.VM.get_other_config(templ_ref)
|
|
|
|
if "default_template" in templ_other_config and templ_other_config["default_template"]:
|
|
# other_config of built-in XenServer templates have a key called
|
|
# 'disks' with the following content:
|
|
# disks: <provision><disk bootable="true" device="0" size="10737418240" sr="" type="system"/></provision>
|
|
# This value of other_data is copied to cloned or copied VM and
|
|
# it prevents provisioning of VM because sr is not specified and
|
|
# XAPI returns an error. To get around this, we remove the
|
|
# 'disks' key and add disks to VM later ourselves.
|
|
vm_other_config = self.xapi_session.xenapi.VM.get_other_config(self.vm_ref)
|
|
|
|
if "disks" in vm_other_config:
|
|
del vm_other_config["disks"]
|
|
|
|
self.xapi_session.xenapi.VM.set_other_config(self.vm_ref, vm_other_config)
|
|
|
|
# At this point we have VM ready for provisioning.
|
|
self.xapi_session.xenapi.VM.provision(self.vm_ref)
|
|
|
|
# After provisioning we can prepare vm_params for reconfigure().
|
|
self.gather_params()
|
|
|
|
# VM is almost ready. We just need to reconfigure it...
|
|
self.reconfigure()
|
|
|
|
# Power on VM if needed.
|
|
if self.module.params["state"] == "poweredon":
|
|
self.set_power_state("poweredon")
|
|
|
|
except XenAPI.Failure as f:
|
|
self.module.fail_json(msg=f"XAPI ERROR: {f.details}")
|
|
|
|
def reconfigure(self):
|
|
"""Reconfigures an existing VM.
|
|
|
|
Returns:
|
|
list: parameters that were reconfigured.
|
|
"""
|
|
# Safety check.
|
|
if not self.exists():
|
|
self.module.fail_json(msg="Called reconfigure on non existing VM!")
|
|
|
|
config_changes = self.get_changes()
|
|
|
|
vm_power_state_save = self.vm_params["power_state"].lower()
|
|
|
|
if "need_poweredoff" in config_changes and vm_power_state_save != "halted" and not self.module.params["force"]:
|
|
self.module.fail_json(
|
|
msg="VM reconfigure: VM has to be in powered off state to reconfigure but force was not specified!"
|
|
)
|
|
|
|
# Support for Ansible check mode.
|
|
if self.module.check_mode:
|
|
return config_changes
|
|
|
|
if "need_poweredoff" in config_changes and vm_power_state_save != "halted" and self.module.params["force"]:
|
|
self.set_power_state("shutdownguest")
|
|
|
|
try:
|
|
for change in config_changes:
|
|
if isinstance(change, str):
|
|
if change == "name":
|
|
self.xapi_session.xenapi.VM.set_name_label(self.vm_ref, self.module.params["name"])
|
|
elif change == "name_desc":
|
|
self.xapi_session.xenapi.VM.set_name_description(self.vm_ref, self.module.params["name_desc"])
|
|
elif change == "folder":
|
|
self.xapi_session.xenapi.VM.remove_from_other_config(self.vm_ref, "folder")
|
|
|
|
if self.module.params["folder"]:
|
|
self.xapi_session.xenapi.VM.add_to_other_config(
|
|
self.vm_ref, "folder", self.module.params["folder"]
|
|
)
|
|
elif change == "home_server":
|
|
if self.module.params["home_server"]:
|
|
host_ref = self.xapi_session.xenapi.host.get_by_name_label(
|
|
self.module.params["home_server"]
|
|
)[0]
|
|
else:
|
|
host_ref = "OpaqueRef:NULL"
|
|
|
|
self.xapi_session.xenapi.VM.set_affinity(self.vm_ref, host_ref)
|
|
elif isinstance(change, dict):
|
|
if change.get("hardware"):
|
|
for hardware_change in change["hardware"]:
|
|
if hardware_change == "num_cpus":
|
|
num_cpus = int(self.module.params["hardware"]["num_cpus"])
|
|
|
|
if num_cpus < int(self.vm_params["VCPUs_at_startup"]):
|
|
self.xapi_session.xenapi.VM.set_VCPUs_at_startup(self.vm_ref, str(num_cpus))
|
|
self.xapi_session.xenapi.VM.set_VCPUs_max(self.vm_ref, str(num_cpus))
|
|
else:
|
|
self.xapi_session.xenapi.VM.set_VCPUs_max(self.vm_ref, str(num_cpus))
|
|
self.xapi_session.xenapi.VM.set_VCPUs_at_startup(self.vm_ref, str(num_cpus))
|
|
elif hardware_change == "num_cpu_cores_per_socket":
|
|
self.xapi_session.xenapi.VM.remove_from_platform(self.vm_ref, "cores-per-socket")
|
|
num_cpu_cores_per_socket = int(
|
|
self.module.params["hardware"]["num_cpu_cores_per_socket"]
|
|
)
|
|
|
|
if num_cpu_cores_per_socket > 1:
|
|
self.xapi_session.xenapi.VM.add_to_platform(
|
|
self.vm_ref, "cores-per-socket", str(num_cpu_cores_per_socket)
|
|
)
|
|
elif hardware_change == "memory_mb":
|
|
memory_b = str(int(self.module.params["hardware"]["memory_mb"]) * 1048576)
|
|
vm_memory_static_min_b = str(
|
|
min(int(memory_b), int(self.vm_params["memory_static_min"]))
|
|
)
|
|
|
|
self.xapi_session.xenapi.VM.set_memory_limits(
|
|
self.vm_ref, vm_memory_static_min_b, memory_b, memory_b, memory_b
|
|
)
|
|
elif change.get("disks_changed"):
|
|
vm_disk_params_list = [
|
|
disk_params for disk_params in self.vm_params["VBDs"] if disk_params["type"] == "Disk"
|
|
]
|
|
|
|
for position, disk_change_list in enumerate(change["disks_changed"]):
|
|
for disk_change in disk_change_list:
|
|
vdi_ref = self.xapi_session.xenapi.VDI.get_by_uuid(
|
|
vm_disk_params_list[position]["VDI"]["uuid"]
|
|
)
|
|
|
|
if disk_change == "name":
|
|
self.xapi_session.xenapi.VDI.set_name_label(
|
|
vdi_ref, self.module.params["disks"][position]["name"]
|
|
)
|
|
elif disk_change == "name_desc":
|
|
self.xapi_session.xenapi.VDI.set_name_description(
|
|
vdi_ref, self.module.params["disks"][position]["name_desc"]
|
|
)
|
|
elif disk_change == "size":
|
|
self.xapi_session.xenapi.VDI.resize(
|
|
vdi_ref,
|
|
str(
|
|
self.get_normalized_disk_size(
|
|
self.module.params["disks"][position],
|
|
f"VM reconfigure disks[{position}]: ",
|
|
)
|
|
),
|
|
)
|
|
|
|
elif change.get("disks_new"):
|
|
for position, disk_userdevice in change["disks_new"]:
|
|
disk_params = self.module.params["disks"][position]
|
|
|
|
disk_name = (
|
|
disk_params["name"]
|
|
if disk_params.get("name")
|
|
else f"{self.vm_params['name_label']}-{position}"
|
|
)
|
|
disk_name_desc = disk_params["name_desc"] if disk_params.get("name_desc") else ""
|
|
|
|
if disk_params.get("sr_uuid"):
|
|
sr_ref = self.xapi_session.xenapi.SR.get_by_uuid(disk_params["sr_uuid"])
|
|
elif disk_params.get("sr"):
|
|
sr_ref = self.xapi_session.xenapi.SR.get_by_name_label(disk_params["sr"])[0]
|
|
else:
|
|
sr_ref = self.default_sr_ref
|
|
|
|
disk_size = str(
|
|
self.get_normalized_disk_size(
|
|
self.module.params["disks"][position], f"VM reconfigure disks[{position}]: "
|
|
)
|
|
)
|
|
|
|
new_disk_vdi = {
|
|
"name_label": disk_name,
|
|
"name_description": disk_name_desc,
|
|
"SR": sr_ref,
|
|
"virtual_size": disk_size,
|
|
"type": "user",
|
|
"sharable": False,
|
|
"read_only": False,
|
|
"other_config": {},
|
|
}
|
|
|
|
new_disk_vbd = {
|
|
"VM": self.vm_ref,
|
|
"VDI": None,
|
|
"userdevice": disk_userdevice,
|
|
"bootable": False,
|
|
"mode": "RW",
|
|
"type": "Disk",
|
|
"empty": False,
|
|
"other_config": {},
|
|
"qos_algorithm_type": "",
|
|
"qos_algorithm_params": {},
|
|
}
|
|
|
|
new_disk_vbd["VDI"] = self.xapi_session.xenapi.VDI.create(new_disk_vdi)
|
|
vbd_ref_new = self.xapi_session.xenapi.VBD.create(new_disk_vbd)
|
|
|
|
if self.vm_params["power_state"].lower() == "running":
|
|
self.xapi_session.xenapi.VBD.plug(vbd_ref_new)
|
|
|
|
elif change.get("cdrom"):
|
|
vm_cdrom_params_list = [
|
|
cdrom_params for cdrom_params in self.vm_params["VBDs"] if cdrom_params["type"] == "CD"
|
|
]
|
|
|
|
# If there is no CD present, we have to create one.
|
|
if not vm_cdrom_params_list:
|
|
# We will try to place cdrom at userdevice position
|
|
# 3 (which is default) if it is not already occupied
|
|
# else we will place it at first allowed position.
|
|
cdrom_userdevices_allowed = self.xapi_session.xenapi.VM.get_allowed_VBD_devices(self.vm_ref)
|
|
|
|
if "3" in cdrom_userdevices_allowed:
|
|
cdrom_userdevice = "3"
|
|
else:
|
|
cdrom_userdevice = cdrom_userdevices_allowed[0]
|
|
|
|
cdrom_vbd = {
|
|
"VM": self.vm_ref,
|
|
"VDI": "OpaqueRef:NULL",
|
|
"userdevice": cdrom_userdevice,
|
|
"bootable": False,
|
|
"mode": "RO",
|
|
"type": "CD",
|
|
"empty": True,
|
|
"other_config": {},
|
|
"qos_algorithm_type": "",
|
|
"qos_algorithm_params": {},
|
|
}
|
|
|
|
cdrom_vbd_ref = self.xapi_session.xenapi.VBD.create(cdrom_vbd)
|
|
else:
|
|
cdrom_vbd_ref = self.xapi_session.xenapi.VBD.get_by_uuid(vm_cdrom_params_list[0]["uuid"])
|
|
|
|
cdrom_is_empty = self.xapi_session.xenapi.VBD.get_empty(cdrom_vbd_ref)
|
|
|
|
for cdrom_change in change["cdrom"]:
|
|
if cdrom_change == "type":
|
|
cdrom_type = self.module.params["cdrom"]["type"]
|
|
|
|
if cdrom_type == "none" and not cdrom_is_empty:
|
|
self.xapi_session.xenapi.VBD.eject(cdrom_vbd_ref)
|
|
elif cdrom_type == "host":
|
|
# Unimplemented!
|
|
pass
|
|
|
|
elif cdrom_change == "iso_name":
|
|
if not cdrom_is_empty:
|
|
self.xapi_session.xenapi.VBD.eject(cdrom_vbd_ref)
|
|
|
|
cdrom_vdi_ref = self.xapi_session.xenapi.VDI.get_by_name_label(
|
|
self.module.params["cdrom"]["iso_name"]
|
|
)[0]
|
|
self.xapi_session.xenapi.VBD.insert(cdrom_vbd_ref, cdrom_vdi_ref)
|
|
elif change.get("networks_changed"):
|
|
position = 0
|
|
|
|
for network_change_list in change["networks_changed"]:
|
|
if network_change_list:
|
|
vm_vif_params = self.vm_params["VIFs"][position]
|
|
network_params = self.module.params["networks"][position]
|
|
|
|
vif_ref = self.xapi_session.xenapi.VIF.get_by_uuid(vm_vif_params["uuid"])
|
|
network_ref = self.xapi_session.xenapi.network.get_by_uuid(
|
|
vm_vif_params["network"]["uuid"]
|
|
)
|
|
|
|
vif_recreated = False
|
|
|
|
if "name" in network_change_list or "mac" in network_change_list:
|
|
# To change network or MAC, we destroy old
|
|
# VIF and then create a new one with changed
|
|
# parameters. That's how XenCenter does it.
|
|
|
|
# Copy all old parameters to new VIF record.
|
|
vif = {
|
|
"device": vm_vif_params["device"],
|
|
"network": network_ref,
|
|
"VM": vm_vif_params["VM"],
|
|
"MAC": vm_vif_params["MAC"],
|
|
"MTU": vm_vif_params["MTU"],
|
|
"other_config": vm_vif_params["other_config"],
|
|
"qos_algorithm_type": vm_vif_params["qos_algorithm_type"],
|
|
"qos_algorithm_params": vm_vif_params["qos_algorithm_params"],
|
|
"locking_mode": vm_vif_params["locking_mode"],
|
|
"ipv4_allowed": vm_vif_params["ipv4_allowed"],
|
|
"ipv6_allowed": vm_vif_params["ipv6_allowed"],
|
|
}
|
|
|
|
if "name" in network_change_list:
|
|
network_ref_new = self.xapi_session.xenapi.network.get_by_name_label(
|
|
network_params["name"]
|
|
)[0]
|
|
vif["network"] = network_ref_new
|
|
vif["MTU"] = self.xapi_session.xenapi.network.get_MTU(network_ref_new)
|
|
|
|
if "mac" in network_change_list:
|
|
vif["MAC"] = network_params["mac"].lower()
|
|
|
|
if self.vm_params["power_state"].lower() == "running":
|
|
self.xapi_session.xenapi.VIF.unplug(vif_ref)
|
|
|
|
self.xapi_session.xenapi.VIF.destroy(vif_ref)
|
|
vif_ref_new = self.xapi_session.xenapi.VIF.create(vif)
|
|
|
|
if self.vm_params["power_state"].lower() == "running":
|
|
self.xapi_session.xenapi.VIF.plug(vif_ref_new)
|
|
|
|
vif_ref = vif_ref_new
|
|
vif_recreated = True
|
|
|
|
if self.vm_params["customization_agent"] == "native":
|
|
vif_reconfigure_needed = False
|
|
|
|
if "type" in network_change_list:
|
|
network_type = network_params["type"].capitalize()
|
|
vif_reconfigure_needed = True
|
|
else:
|
|
network_type = vm_vif_params["ipv4_configuration_mode"]
|
|
|
|
if "ip" in network_change_list:
|
|
network_ip = network_params["ip"]
|
|
vif_reconfigure_needed = True
|
|
elif vm_vif_params["ipv4_addresses"]:
|
|
network_ip = vm_vif_params["ipv4_addresses"][0].split("/")[0]
|
|
else:
|
|
network_ip = ""
|
|
|
|
if "prefix" in network_change_list:
|
|
network_prefix = f"/{network_params['prefix']}"
|
|
vif_reconfigure_needed = True
|
|
elif vm_vif_params["ipv4_addresses"] and vm_vif_params["ipv4_addresses"][0]:
|
|
network_prefix = f"/{vm_vif_params['ipv4_addresses'][0].split('/')[1]}"
|
|
else:
|
|
network_prefix = ""
|
|
|
|
if "gateway" in network_change_list:
|
|
network_gateway = network_params["gateway"]
|
|
vif_reconfigure_needed = True
|
|
else:
|
|
network_gateway = vm_vif_params["ipv4_gateway"]
|
|
|
|
if vif_recreated or vif_reconfigure_needed:
|
|
self.xapi_session.xenapi.VIF.configure_ipv4(
|
|
vif_ref, network_type, f"{network_ip}{network_prefix}", network_gateway
|
|
)
|
|
|
|
vif_reconfigure_needed = False
|
|
|
|
if "type6" in network_change_list:
|
|
network_type6 = network_params["type6"].capitalize()
|
|
vif_reconfigure_needed = True
|
|
else:
|
|
network_type6 = vm_vif_params["ipv6_configuration_mode"]
|
|
|
|
if "ip6" in network_change_list:
|
|
network_ip6 = network_params["ip6"]
|
|
vif_reconfigure_needed = True
|
|
elif vm_vif_params["ipv6_addresses"]:
|
|
network_ip6 = vm_vif_params["ipv6_addresses"][0].split("/")[0]
|
|
else:
|
|
network_ip6 = ""
|
|
|
|
if "prefix6" in network_change_list:
|
|
network_prefix6 = f"/{network_params['prefix6']}"
|
|
vif_reconfigure_needed = True
|
|
elif vm_vif_params["ipv6_addresses"] and vm_vif_params["ipv6_addresses"][0]:
|
|
network_prefix6 = f"/{vm_vif_params['ipv6_addresses'][0].split('/')[1]}"
|
|
else:
|
|
network_prefix6 = ""
|
|
|
|
if "gateway6" in network_change_list:
|
|
network_gateway6 = network_params["gateway6"]
|
|
vif_reconfigure_needed = True
|
|
else:
|
|
network_gateway6 = vm_vif_params["ipv6_gateway"]
|
|
|
|
if vif_recreated or vif_reconfigure_needed:
|
|
self.xapi_session.xenapi.VIF.configure_ipv6(
|
|
vif_ref, network_type6, f"{network_ip6}{network_prefix6}", network_gateway6
|
|
)
|
|
|
|
elif self.vm_params["customization_agent"] == "custom":
|
|
vif_device = vm_vif_params["device"]
|
|
|
|
# A user could have manually changed network
|
|
# or mac e.g. through XenCenter and then also
|
|
# make those changes in playbook manually.
|
|
# In that case, module will not detect any
|
|
# changes and info in xenstore_data will
|
|
# become stale. For that reason we always
|
|
# update name and mac in xenstore_data.
|
|
|
|
# Since we handle name and mac differently,
|
|
# we have to remove them from
|
|
# network_change_list.
|
|
network_change_list_tmp = [
|
|
net_chg for net_chg in network_change_list if net_chg not in ["name", "mac"]
|
|
]
|
|
|
|
for network_change in network_change_list_tmp + ["name", "mac"]:
|
|
self.xapi_session.xenapi.VM.remove_from_xenstore_data(
|
|
self.vm_ref, f"vm-data/networks/{vif_device}/{network_change}"
|
|
)
|
|
|
|
if network_params.get("name"):
|
|
network_name = network_params["name"]
|
|
else:
|
|
network_name = vm_vif_params["network"]["name_label"]
|
|
|
|
self.xapi_session.xenapi.VM.add_to_xenstore_data(
|
|
self.vm_ref, f"vm-data/networks/{vif_device}/name", network_name
|
|
)
|
|
|
|
if network_params.get("mac"):
|
|
network_mac = network_params["mac"].lower()
|
|
else:
|
|
network_mac = vm_vif_params["MAC"].lower()
|
|
|
|
self.xapi_session.xenapi.VM.add_to_xenstore_data(
|
|
self.vm_ref, f"vm-data/networks/{vif_device}/mac", network_mac
|
|
)
|
|
|
|
for network_change in network_change_list_tmp:
|
|
self.xapi_session.xenapi.VM.add_to_xenstore_data(
|
|
self.vm_ref,
|
|
f"vm-data/networks/{vif_device}/{network_change}",
|
|
network_params[network_change],
|
|
)
|
|
|
|
position += 1
|
|
elif change.get("networks_new"):
|
|
for position, vif_device in change["networks_new"]:
|
|
network_params = self.module.params["networks"][position]
|
|
|
|
network_ref = self.xapi_session.xenapi.network.get_by_name_label(network_params["name"])[0]
|
|
|
|
network_name = network_params["name"]
|
|
network_mac = network_params["mac"] if network_params.get("mac") else ""
|
|
network_type = network_params.get("type")
|
|
network_ip = network_params["ip"] if network_params.get("ip") else ""
|
|
network_prefix = network_params["prefix"] if network_params.get("prefix") else ""
|
|
network_netmask = network_params["netmask"] if network_params.get("netmask") else ""
|
|
network_gateway = network_params["gateway"] if network_params.get("gateway") else ""
|
|
network_type6 = network_params.get("type6")
|
|
network_ip6 = network_params["ip6"] if network_params.get("ip6") else ""
|
|
network_prefix6 = network_params["prefix6"] if network_params.get("prefix6") else ""
|
|
network_gateway6 = network_params["gateway6"] if network_params.get("gateway6") else ""
|
|
|
|
vif = {
|
|
"device": vif_device,
|
|
"network": network_ref,
|
|
"VM": self.vm_ref,
|
|
"MAC": network_mac,
|
|
"MTU": self.xapi_session.xenapi.network.get_MTU(network_ref),
|
|
"other_config": {},
|
|
"qos_algorithm_type": "",
|
|
"qos_algorithm_params": {},
|
|
}
|
|
|
|
vif_ref_new = self.xapi_session.xenapi.VIF.create(vif)
|
|
|
|
if self.vm_params["power_state"].lower() == "running":
|
|
self.xapi_session.xenapi.VIF.plug(vif_ref_new)
|
|
|
|
if self.vm_params["customization_agent"] == "native":
|
|
if network_type and network_type == "static":
|
|
self.xapi_session.xenapi.VIF.configure_ipv4(
|
|
vif_ref_new, "Static", f"{network_ip}/{network_prefix}", network_gateway
|
|
)
|
|
|
|
if network_type6 and network_type6 == "static":
|
|
self.xapi_session.xenapi.VIF.configure_ipv6(
|
|
vif_ref_new, "Static", f"{network_ip6}/{network_prefix6}", network_gateway6
|
|
)
|
|
elif self.vm_params["customization_agent"] == "custom":
|
|
# We first have to remove any existing data
|
|
# from xenstore_data because there could be
|
|
# some old leftover data from some interface
|
|
# that once occupied same device location as
|
|
# our new interface.
|
|
for network_param in [
|
|
"name",
|
|
"mac",
|
|
"type",
|
|
"ip",
|
|
"prefix",
|
|
"netmask",
|
|
"gateway",
|
|
"type6",
|
|
"ip6",
|
|
"prefix6",
|
|
"gateway6",
|
|
]:
|
|
self.xapi_session.xenapi.VM.remove_from_xenstore_data(
|
|
self.vm_ref, f"vm-data/networks/{vif_device}/{network_param}"
|
|
)
|
|
|
|
self.xapi_session.xenapi.VM.add_to_xenstore_data(
|
|
self.vm_ref, f"vm-data/networks/{vif_device}/name", network_name
|
|
)
|
|
|
|
# We get MAC from VIF itself instead of
|
|
# networks.mac because it could be
|
|
# autogenerated.
|
|
vm_vif_mac = self.xapi_session.xenapi.VIF.get_MAC(vif_ref_new)
|
|
self.xapi_session.xenapi.VM.add_to_xenstore_data(
|
|
self.vm_ref, f"vm-data/networks/{vif_device}/mac", vm_vif_mac
|
|
)
|
|
|
|
if network_type:
|
|
self.xapi_session.xenapi.VM.add_to_xenstore_data(
|
|
self.vm_ref, f"vm-data/networks/{vif_device}/type", network_type
|
|
)
|
|
|
|
if network_type == "static":
|
|
self.xapi_session.xenapi.VM.add_to_xenstore_data(
|
|
self.vm_ref, f"vm-data/networks/{vif_device}/ip", network_ip
|
|
)
|
|
self.xapi_session.xenapi.VM.add_to_xenstore_data(
|
|
self.vm_ref, f"vm-data/networks/{vif_device}/prefix", network_prefix
|
|
)
|
|
self.xapi_session.xenapi.VM.add_to_xenstore_data(
|
|
self.vm_ref, f"vm-data/networks/{vif_device}/netmask", network_netmask
|
|
)
|
|
self.xapi_session.xenapi.VM.add_to_xenstore_data(
|
|
self.vm_ref, f"vm-data/networks/{vif_device}/gateway", network_gateway
|
|
)
|
|
|
|
if network_type6:
|
|
self.xapi_session.xenapi.VM.add_to_xenstore_data(
|
|
self.vm_ref, f"vm-data/networks/{vif_device}/type6", network_type6
|
|
)
|
|
|
|
if network_type6 == "static":
|
|
self.xapi_session.xenapi.VM.add_to_xenstore_data(
|
|
self.vm_ref, f"vm-data/networks/{vif_device}/ip6", network_ip6
|
|
)
|
|
self.xapi_session.xenapi.VM.add_to_xenstore_data(
|
|
self.vm_ref, f"vm-data/networks/{vif_device}/prefix6", network_prefix6
|
|
)
|
|
self.xapi_session.xenapi.VM.add_to_xenstore_data(
|
|
self.vm_ref, f"vm-data/networks/{vif_device}/gateway6", network_gateway6
|
|
)
|
|
|
|
elif change.get("custom_params"):
|
|
for position in change["custom_params"]:
|
|
custom_param_key = self.module.params["custom_params"][position]["key"]
|
|
custom_param_value = self.module.params["custom_params"][position]["value"]
|
|
self.xapi_session.xenapi_request(
|
|
f"VM.set_{custom_param_key}", (self.vm_ref, custom_param_value)
|
|
)
|
|
|
|
if self.module.params["is_template"]:
|
|
self.xapi_session.xenapi.VM.set_is_a_template(self.vm_ref, True)
|
|
elif (
|
|
"need_poweredoff" in config_changes and self.module.params["force"] and vm_power_state_save != "halted"
|
|
):
|
|
self.set_power_state("poweredon")
|
|
|
|
# Gather new params after reconfiguration.
|
|
self.gather_params()
|
|
|
|
except XenAPI.Failure as f:
|
|
self.module.fail_json(msg=f"XAPI ERROR: {f.details}")
|
|
|
|
return config_changes
|
|
|
|
def destroy(self):
|
|
"""Removes an existing VM with associated disks"""
|
|
# Safety check.
|
|
if not self.exists():
|
|
self.module.fail_json(msg="Called destroy on non existing VM!")
|
|
|
|
if self.vm_params["power_state"].lower() != "halted" and not self.module.params["force"]:
|
|
self.module.fail_json(
|
|
msg="VM destroy: VM has to be in powered off state to destroy but force was not specified!"
|
|
)
|
|
|
|
# Support for Ansible check mode.
|
|
if self.module.check_mode:
|
|
return
|
|
|
|
# Make sure that VM is poweredoff before we can destroy it.
|
|
self.set_power_state("poweredoff")
|
|
|
|
try:
|
|
# Destroy VM!
|
|
self.xapi_session.xenapi.VM.destroy(self.vm_ref)
|
|
|
|
vm_disk_params_list = [
|
|
disk_params for disk_params in self.vm_params["VBDs"] if disk_params["type"] == "Disk"
|
|
]
|
|
|
|
# Destroy all VDIs associated with VM!
|
|
for vm_disk_params in vm_disk_params_list:
|
|
vdi_ref = self.xapi_session.xenapi.VDI.get_by_uuid(vm_disk_params["VDI"]["uuid"])
|
|
|
|
self.xapi_session.xenapi.VDI.destroy(vdi_ref)
|
|
|
|
except XenAPI.Failure as f:
|
|
self.module.fail_json(msg=f"XAPI ERROR: {f.details}")
|
|
|
|
def get_changes(self):
|
|
"""Finds VM parameters that differ from specified ones.
|
|
|
|
This method builds a dictionary with hierarchy of VM parameters
|
|
that differ from those specified in module parameters.
|
|
|
|
Returns:
|
|
list: VM parameters that differ from those specified in
|
|
module parameters.
|
|
"""
|
|
# Safety check.
|
|
if not self.exists():
|
|
self.module.fail_json(msg="Called get_changes on non existing VM!")
|
|
|
|
need_poweredoff = False
|
|
|
|
if self.module.params["is_template"]:
|
|
need_poweredoff = True
|
|
|
|
try:
|
|
# This VM could be a template or a snapshot. In that case we fail
|
|
# because we can't reconfigure them or it would just be too
|
|
# dangerous.
|
|
if self.vm_params["is_a_template"] and not self.vm_params["is_a_snapshot"]:
|
|
self.module.fail_json(
|
|
msg="VM check: targeted VM is a template! Template reconfiguration is not supported."
|
|
)
|
|
|
|
if self.vm_params["is_a_snapshot"]:
|
|
self.module.fail_json(
|
|
msg="VM check: targeted VM is a snapshot! Snapshot reconfiguration is not supported."
|
|
)
|
|
|
|
# Let's build a list of parameters that changed.
|
|
config_changes = []
|
|
|
|
# Name could only differ if we found an existing VM by uuid.
|
|
if self.module.params["name"] is not None and self.module.params["name"] != self.vm_params["name_label"]:
|
|
if self.module.params["name"]:
|
|
config_changes.append("name")
|
|
else:
|
|
self.module.fail_json(msg="VM check name: VM name cannot be an empty string!")
|
|
|
|
if (
|
|
self.module.params["name_desc"] is not None
|
|
and self.module.params["name_desc"] != self.vm_params["name_description"]
|
|
):
|
|
config_changes.append("name_desc")
|
|
|
|
# Folder parameter is found in other_config.
|
|
vm_other_config = self.vm_params["other_config"]
|
|
vm_folder = vm_other_config.get("folder", "")
|
|
|
|
if self.module.params["folder"] is not None and self.module.params["folder"] != vm_folder:
|
|
config_changes.append("folder")
|
|
|
|
if self.module.params["home_server"] is not None:
|
|
if self.module.params["home_server"] and (
|
|
not self.vm_params["affinity"]
|
|
or self.module.params["home_server"] != self.vm_params["affinity"]["name_label"]
|
|
):
|
|
# Check existence only. Ignore return value.
|
|
get_object_ref(
|
|
self.module,
|
|
self.module.params["home_server"],
|
|
uuid=None,
|
|
obj_type="home server",
|
|
fail=True,
|
|
msg_prefix="VM check home_server: ",
|
|
)
|
|
|
|
config_changes.append("home_server")
|
|
elif not self.module.params["home_server"] and self.vm_params["affinity"]:
|
|
config_changes.append("home_server")
|
|
|
|
config_changes_hardware = []
|
|
|
|
if self.module.params["hardware"]:
|
|
num_cpus = self.module.params["hardware"].get("num_cpus")
|
|
|
|
if num_cpus is not None:
|
|
# Kept for compatibility with older Ansible versions that
|
|
# do not support subargument specs.
|
|
try:
|
|
num_cpus = int(num_cpus)
|
|
except ValueError as e:
|
|
self.module.fail_json(msg="VM check hardware.num_cpus: parameter should be an integer value!")
|
|
|
|
if num_cpus < 1:
|
|
self.module.fail_json(msg="VM check hardware.num_cpus: parameter should be greater than zero!")
|
|
|
|
# We can use VCPUs_at_startup or VCPUs_max parameter. I'd
|
|
# say the former is the way to go but this needs
|
|
# confirmation and testing.
|
|
if num_cpus != int(self.vm_params["VCPUs_at_startup"]):
|
|
config_changes_hardware.append("num_cpus")
|
|
# For now, we don't support hotpluging so VM has to be in
|
|
# poweredoff state to reconfigure.
|
|
need_poweredoff = True
|
|
|
|
num_cpu_cores_per_socket = self.module.params["hardware"].get("num_cpu_cores_per_socket")
|
|
|
|
if num_cpu_cores_per_socket is not None:
|
|
# Kept for compatibility with older Ansible versions that
|
|
# do not support subargument specs.
|
|
try:
|
|
num_cpu_cores_per_socket = int(num_cpu_cores_per_socket)
|
|
except ValueError as e:
|
|
self.module.fail_json(
|
|
msg="VM check hardware.num_cpu_cores_per_socket: parameter should be an integer value!"
|
|
)
|
|
|
|
if num_cpu_cores_per_socket < 1:
|
|
self.module.fail_json(
|
|
msg="VM check hardware.num_cpu_cores_per_socket: parameter should be greater than zero!"
|
|
)
|
|
|
|
if num_cpus and num_cpus % num_cpu_cores_per_socket != 0:
|
|
self.module.fail_json(
|
|
msg="VM check hardware.num_cpus: parameter should be a multiple of hardware.num_cpu_cores_per_socket!"
|
|
)
|
|
|
|
vm_platform = self.vm_params["platform"]
|
|
vm_cores_per_socket = int(vm_platform.get("cores-per-socket", 1))
|
|
|
|
if num_cpu_cores_per_socket != vm_cores_per_socket:
|
|
config_changes_hardware.append("num_cpu_cores_per_socket")
|
|
# For now, we don't support hotpluging so VM has to be
|
|
# in poweredoff state to reconfigure.
|
|
need_poweredoff = True
|
|
|
|
memory_mb = self.module.params["hardware"].get("memory_mb")
|
|
|
|
if memory_mb is not None:
|
|
# Kept for compatibility with older Ansible versions that
|
|
# do not support subargument specs.
|
|
try:
|
|
memory_mb = int(memory_mb)
|
|
except ValueError as e:
|
|
self.module.fail_json(msg="VM check hardware.memory_mb: parameter should be an integer value!")
|
|
|
|
if memory_mb < 1:
|
|
self.module.fail_json(msg="VM check hardware.memory_mb: parameter should be greater than zero!")
|
|
|
|
# There are multiple memory parameters:
|
|
# - memory_dynamic_max
|
|
# - memory_dynamic_min
|
|
# - memory_static_max
|
|
# - memory_static_min
|
|
# - memory_target
|
|
#
|
|
# memory_target seems like a good candidate but it returns 0 for
|
|
# halted VMs so we can't use it.
|
|
#
|
|
# I decided to use memory_dynamic_max and memory_static_max
|
|
# and use whichever is larger. This strategy needs validation
|
|
# and testing.
|
|
#
|
|
# XenServer stores memory size in bytes so we need to divide
|
|
# it by 1024*1024 = 1048576.
|
|
if memory_mb != int(
|
|
max(int(self.vm_params["memory_dynamic_max"]), int(self.vm_params["memory_static_max"]))
|
|
/ 1048576
|
|
):
|
|
config_changes_hardware.append("memory_mb")
|
|
# For now, we don't support hotpluging so VM has to be in
|
|
# poweredoff state to reconfigure.
|
|
need_poweredoff = True
|
|
|
|
if config_changes_hardware:
|
|
config_changes.append({"hardware": config_changes_hardware})
|
|
|
|
config_changes_disks = []
|
|
config_new_disks = []
|
|
|
|
# Find allowed userdevices.
|
|
vbd_userdevices_allowed = self.xapi_session.xenapi.VM.get_allowed_VBD_devices(self.vm_ref)
|
|
|
|
if self.module.params["disks"]:
|
|
# Get the list of all disk. Filter out any CDs found.
|
|
vm_disk_params_list = [
|
|
disk_params for disk_params in self.vm_params["VBDs"] if disk_params["type"] == "Disk"
|
|
]
|
|
|
|
# Number of disks defined in module params have to be same or
|
|
# higher than a number of existing disks attached to the VM.
|
|
# We don't support removal or detachment of disks.
|
|
if len(self.module.params["disks"]) < len(vm_disk_params_list):
|
|
self.module.fail_json(
|
|
msg=f"VM check disks: provided disks configuration has less disks than the "
|
|
f"target VM ({len(self.module.params['disks'])} < {len(vm_disk_params_list)})!"
|
|
)
|
|
|
|
# Find the highest disk occupied userdevice.
|
|
if not vm_disk_params_list:
|
|
vm_disk_userdevice_highest = "-1"
|
|
else:
|
|
vm_disk_userdevice_highest = vm_disk_params_list[-1]["userdevice"]
|
|
|
|
for position in range(len(self.module.params["disks"])):
|
|
if position < len(vm_disk_params_list):
|
|
vm_disk_params = vm_disk_params_list[position]
|
|
else:
|
|
vm_disk_params = None
|
|
|
|
disk_params = self.module.params["disks"][position]
|
|
|
|
disk_size = self.get_normalized_disk_size(
|
|
self.module.params["disks"][position], f"VM check disks[{position}]: "
|
|
)
|
|
|
|
disk_name = disk_params.get("name")
|
|
|
|
if disk_name is not None and not disk_name:
|
|
self.module.fail_json(msg=f"VM check disks[{position}]: disk name cannot be an empty string!")
|
|
|
|
# If this is an existing disk.
|
|
if vm_disk_params and vm_disk_params["VDI"]:
|
|
disk_changes = []
|
|
|
|
if disk_name and disk_name != vm_disk_params["VDI"]["name_label"]:
|
|
disk_changes.append("name")
|
|
|
|
disk_name_desc = disk_params.get("name_desc")
|
|
|
|
if disk_name_desc is not None and disk_name_desc != vm_disk_params["VDI"]["name_description"]:
|
|
disk_changes.append("name_desc")
|
|
|
|
if disk_size:
|
|
if disk_size > int(vm_disk_params["VDI"]["virtual_size"]):
|
|
disk_changes.append("size")
|
|
need_poweredoff = True
|
|
elif disk_size < int(vm_disk_params["VDI"]["virtual_size"]):
|
|
self.module.fail_json(
|
|
msg=f"VM check disks[{position}]: disk size is smaller than existing ({disk_size} bytes < "
|
|
f"{vm_disk_params['VDI']['virtual_size']} bytes). Reducing disk size is not allowed!"
|
|
)
|
|
|
|
config_changes_disks.append(disk_changes)
|
|
# If this is a new disk.
|
|
else:
|
|
if not disk_size:
|
|
self.module.fail_json(
|
|
msg=f"VM check disks[{position}]: no valid disk size specification found!"
|
|
)
|
|
|
|
disk_sr_uuid = disk_params.get("sr_uuid")
|
|
disk_sr = disk_params.get("sr")
|
|
|
|
if disk_sr_uuid is not None or disk_sr is not None:
|
|
# Check existence only. Ignore return value.
|
|
get_object_ref(
|
|
self.module,
|
|
disk_sr,
|
|
disk_sr_uuid,
|
|
obj_type="SR",
|
|
fail=True,
|
|
msg_prefix=f"VM check disks[{position}]: ",
|
|
)
|
|
elif self.default_sr_ref == "OpaqueRef:NULL":
|
|
self.module.fail_json(
|
|
msg=f"VM check disks[{position}]: no default SR found! You must specify SR explicitly."
|
|
)
|
|
|
|
if not vbd_userdevices_allowed:
|
|
self.module.fail_json(msg=f"VM check disks[{position}]: maximum number of devices reached!")
|
|
|
|
disk_userdevice = None
|
|
|
|
# We need to place a new disk right above the highest
|
|
# placed existing disk to maintain relative disk
|
|
# positions pairable with disk specifications in
|
|
# module params. That place must not be occupied by
|
|
# some other device like CD-ROM.
|
|
for userdevice in vbd_userdevices_allowed:
|
|
if int(userdevice) > int(vm_disk_userdevice_highest):
|
|
disk_userdevice = userdevice
|
|
vbd_userdevices_allowed.remove(userdevice)
|
|
vm_disk_userdevice_highest = userdevice
|
|
break
|
|
|
|
# If no place was found.
|
|
if disk_userdevice is None:
|
|
# Highest occupied place could be a CD-ROM device
|
|
# so we have to include all devices regardless of
|
|
# type when calculating out-of-bound position.
|
|
disk_userdevice = str(int(self.vm_params["VBDs"][-1]["userdevice"]) + 1)
|
|
self.module.fail_json(
|
|
msg=f"VM check disks[{position}]: new disk position {disk_userdevice} is out of bounds!"
|
|
)
|
|
|
|
# For new disks we only track their position.
|
|
config_new_disks.append((position, disk_userdevice))
|
|
|
|
# We should append config_changes_disks to config_changes only
|
|
# if there is at least one changed disk, else skip.
|
|
for disk_change in config_changes_disks:
|
|
if disk_change:
|
|
config_changes.append({"disks_changed": config_changes_disks})
|
|
break
|
|
|
|
if config_new_disks:
|
|
config_changes.append({"disks_new": config_new_disks})
|
|
|
|
config_changes_cdrom = []
|
|
|
|
if self.module.params["cdrom"]:
|
|
# Get the list of all CD-ROMs. Filter out any regular disks
|
|
# found. If we found no existing CD-ROM, we will create it
|
|
# later else take the first one found.
|
|
vm_cdrom_params_list = [
|
|
cdrom_params for cdrom_params in self.vm_params["VBDs"] if cdrom_params["type"] == "CD"
|
|
]
|
|
|
|
# If no existing CD-ROM is found, we will need to add one.
|
|
# We need to check if there is any userdevice allowed.
|
|
if not vm_cdrom_params_list and not vbd_userdevices_allowed:
|
|
self.module.fail_json(msg="VM check cdrom: maximum number of devices reached!")
|
|
|
|
cdrom_type = self.module.params["cdrom"].get("type")
|
|
cdrom_iso_name = self.module.params["cdrom"].get("iso_name")
|
|
|
|
# If cdrom.iso_name is specified but cdrom.type is not,
|
|
# then set cdrom.type to 'iso', unless cdrom.iso_name is
|
|
# an empty string, in that case set cdrom.type to 'none'.
|
|
if not cdrom_type:
|
|
if cdrom_iso_name:
|
|
cdrom_type = "iso"
|
|
elif cdrom_iso_name is not None:
|
|
cdrom_type = "none"
|
|
|
|
self.module.params["cdrom"]["type"] = cdrom_type
|
|
|
|
# If type changed.
|
|
if cdrom_type and (
|
|
not vm_cdrom_params_list or cdrom_type != self.get_cdrom_type(vm_cdrom_params_list[0])
|
|
):
|
|
config_changes_cdrom.append("type")
|
|
|
|
if cdrom_type == "iso":
|
|
# Check if ISO exists.
|
|
# Check existence only. Ignore return value.
|
|
get_object_ref(
|
|
self.module,
|
|
cdrom_iso_name,
|
|
uuid=None,
|
|
obj_type="ISO image",
|
|
fail=True,
|
|
msg_prefix="VM check cdrom.iso_name: ",
|
|
)
|
|
|
|
# Is ISO image changed?
|
|
if cdrom_iso_name and (
|
|
not vm_cdrom_params_list
|
|
or not vm_cdrom_params_list[0]["VDI"]
|
|
or cdrom_iso_name != vm_cdrom_params_list[0]["VDI"]["name_label"]
|
|
):
|
|
config_changes_cdrom.append("iso_name")
|
|
|
|
if config_changes_cdrom:
|
|
config_changes.append({"cdrom": config_changes_cdrom})
|
|
|
|
config_changes_networks = []
|
|
config_new_networks = []
|
|
|
|
# Find allowed devices.
|
|
vif_devices_allowed = self.xapi_session.xenapi.VM.get_allowed_VIF_devices(self.vm_ref)
|
|
|
|
if self.module.params["networks"]:
|
|
# Number of VIFs defined in module params have to be same or
|
|
# higher than a number of existing VIFs attached to the VM.
|
|
# We don't support removal of VIFs.
|
|
if len(self.module.params["networks"]) < len(self.vm_params["VIFs"]):
|
|
self.module.fail_json(
|
|
msg=f"VM check networks: provided networks configuration has less interfaces than the target "
|
|
f"VM ({len(self.module.params['networks'])} < {len(self.vm_params['VIFs'])})!"
|
|
)
|
|
|
|
# Find the highest occupied device.
|
|
if not self.vm_params["VIFs"]:
|
|
vif_device_highest = "-1"
|
|
else:
|
|
vif_device_highest = self.vm_params["VIFs"][-1]["device"]
|
|
|
|
for position in range(len(self.module.params["networks"])):
|
|
if position < len(self.vm_params["VIFs"]):
|
|
vm_vif_params = self.vm_params["VIFs"][position]
|
|
else:
|
|
vm_vif_params = None
|
|
|
|
network_params = self.module.params["networks"][position]
|
|
|
|
network_name = network_params.get("name")
|
|
|
|
if network_name is not None and not network_name:
|
|
self.module.fail_json(
|
|
msg=f"VM check networks[{position}]: network name cannot be an empty string!"
|
|
)
|
|
|
|
if network_name:
|
|
# Check existence only. Ignore return value.
|
|
get_object_ref(
|
|
self.module,
|
|
network_name,
|
|
uuid=None,
|
|
obj_type="network",
|
|
fail=True,
|
|
msg_prefix=f"VM check networks[{position}]: ",
|
|
)
|
|
|
|
network_mac = network_params.get("mac")
|
|
|
|
if network_mac is not None:
|
|
network_mac = network_mac.lower()
|
|
|
|
if not is_mac(network_mac):
|
|
self.module.fail_json(
|
|
msg=f"VM check networks[{position}]: specified MAC address '{network_mac}' is not valid!"
|
|
)
|
|
|
|
# IPv4 reconfiguration.
|
|
network_type = network_params.get("type")
|
|
network_ip = network_params.get("ip")
|
|
network_netmask = network_params.get("netmask")
|
|
network_prefix = None
|
|
|
|
# If networks.ip is specified and networks.type is not,
|
|
# then set networks.type to 'static'.
|
|
if not network_type and network_ip:
|
|
network_type = "static"
|
|
|
|
# XenServer natively supports only 'none' and 'static'
|
|
# type with 'none' being the same as 'dhcp'.
|
|
if self.vm_params["customization_agent"] == "native" and network_type and network_type == "dhcp":
|
|
network_type = "none"
|
|
|
|
if network_type and network_type == "static":
|
|
if network_ip is not None:
|
|
network_ip_split = network_ip.split("/")
|
|
network_ip = network_ip_split[0]
|
|
|
|
if network_ip and not is_valid_ip_addr(network_ip):
|
|
self.module.fail_json(
|
|
msg=f"VM check networks[{position}]: specified IPv4 address '{network_ip}' is not valid!"
|
|
)
|
|
|
|
if len(network_ip_split) > 1:
|
|
network_prefix = network_ip_split[1]
|
|
|
|
if not is_valid_ip_prefix(network_prefix):
|
|
self.module.fail_json(
|
|
msg=f"VM check networks[{position}]: specified IPv4 prefix '{network_prefix}' is not valid!"
|
|
)
|
|
|
|
if network_netmask is not None:
|
|
if not is_valid_ip_netmask(network_netmask):
|
|
self.module.fail_json(
|
|
msg=f"VM check networks[{position}]: specified IPv4 netmask '{network_netmask}' is not valid!"
|
|
)
|
|
|
|
network_prefix = ip_netmask_to_prefix(network_netmask, skip_check=True)
|
|
elif network_prefix is not None:
|
|
network_netmask = ip_prefix_to_netmask(network_prefix, skip_check=True)
|
|
|
|
# If any parameter is overridden at this point, update it.
|
|
if network_type:
|
|
network_params["type"] = network_type
|
|
|
|
if network_ip:
|
|
network_params["ip"] = network_ip
|
|
|
|
if network_netmask:
|
|
network_params["netmask"] = network_netmask
|
|
|
|
if network_prefix:
|
|
network_params["prefix"] = network_prefix
|
|
|
|
network_gateway = network_params.get("gateway")
|
|
|
|
# Gateway can be an empty string (when removing gateway
|
|
# configuration) but if it is not, it should be validated.
|
|
if network_gateway and not is_valid_ip_addr(network_gateway):
|
|
self.module.fail_json(
|
|
msg=f"VM check networks[{position}]: specified IPv4 gateway '{network_gateway}' is not valid!"
|
|
)
|
|
|
|
# IPv6 reconfiguration.
|
|
network_type6 = network_params.get("type6")
|
|
network_ip6 = network_params.get("ip6")
|
|
network_prefix6 = None
|
|
|
|
# If networks.ip6 is specified and networks.type6 is not,
|
|
# then set networks.type6 to 'static'.
|
|
if not network_type6 and network_ip6:
|
|
network_type6 = "static"
|
|
|
|
# XenServer natively supports only 'none' and 'static'
|
|
# type with 'none' being the same as 'dhcp'.
|
|
if self.vm_params["customization_agent"] == "native" and network_type6 and network_type6 == "dhcp":
|
|
network_type6 = "none"
|
|
|
|
if network_type6 and network_type6 == "static":
|
|
if network_ip6 is not None:
|
|
network_ip6_split = network_ip6.split("/")
|
|
network_ip6 = network_ip6_split[0]
|
|
|
|
if network_ip6 and not is_valid_ip6_addr(network_ip6):
|
|
self.module.fail_json(
|
|
msg=f"VM check networks[{position}]: specified IPv6 address '{network_ip6}' is not valid!"
|
|
)
|
|
|
|
if len(network_ip6_split) > 1:
|
|
network_prefix6 = network_ip6_split[1]
|
|
|
|
if not is_valid_ip6_prefix(network_prefix6):
|
|
self.module.fail_json(
|
|
msg=f"VM check networks[{position}]: specified IPv6 prefix '{network_prefix6}' is not valid!"
|
|
)
|
|
|
|
# If any parameter is overridden at this point, update it.
|
|
if network_type6:
|
|
network_params["type6"] = network_type6
|
|
|
|
if network_ip6:
|
|
network_params["ip6"] = network_ip6
|
|
|
|
if network_prefix6:
|
|
network_params["prefix6"] = network_prefix6
|
|
|
|
network_gateway6 = network_params.get("gateway6")
|
|
|
|
# Gateway can be an empty string (when removing gateway
|
|
# configuration) but if it is not, it should be validated.
|
|
if network_gateway6 and not is_valid_ip6_addr(network_gateway6):
|
|
self.module.fail_json(
|
|
msg=f"VM check networks[{position}]: specified IPv6 gateway '{network_gateway6}' is not valid!"
|
|
)
|
|
|
|
# If this is an existing VIF.
|
|
if vm_vif_params and vm_vif_params["network"]:
|
|
network_changes = []
|
|
|
|
if network_name and network_name != vm_vif_params["network"]["name_label"]:
|
|
network_changes.append("name")
|
|
|
|
if network_mac and network_mac != vm_vif_params["MAC"].lower():
|
|
network_changes.append("mac")
|
|
|
|
if self.vm_params["customization_agent"] == "native":
|
|
if network_type and network_type != vm_vif_params["ipv4_configuration_mode"].lower():
|
|
network_changes.append("type")
|
|
|
|
if network_type and network_type == "static":
|
|
if network_ip and (
|
|
not vm_vif_params["ipv4_addresses"]
|
|
or not vm_vif_params["ipv4_addresses"][0]
|
|
or network_ip != vm_vif_params["ipv4_addresses"][0].split("/")[0]
|
|
):
|
|
network_changes.append("ip")
|
|
|
|
if network_prefix and (
|
|
not vm_vif_params["ipv4_addresses"]
|
|
or not vm_vif_params["ipv4_addresses"][0]
|
|
or network_prefix != vm_vif_params["ipv4_addresses"][0].split("/")[1]
|
|
):
|
|
network_changes.append("prefix")
|
|
network_changes.append("netmask")
|
|
|
|
if network_gateway is not None and network_gateway != vm_vif_params["ipv4_gateway"]:
|
|
network_changes.append("gateway")
|
|
|
|
if network_type6 and network_type6 != vm_vif_params["ipv6_configuration_mode"].lower():
|
|
network_changes.append("type6")
|
|
|
|
if network_type6 and network_type6 == "static":
|
|
if network_ip6 and (
|
|
not vm_vif_params["ipv6_addresses"]
|
|
or not vm_vif_params["ipv6_addresses"][0]
|
|
or network_ip6 != vm_vif_params["ipv6_addresses"][0].split("/")[0]
|
|
):
|
|
network_changes.append("ip6")
|
|
|
|
if network_prefix6 and (
|
|
not vm_vif_params["ipv6_addresses"]
|
|
or not vm_vif_params["ipv6_addresses"][0]
|
|
or network_prefix6 != vm_vif_params["ipv6_addresses"][0].split("/")[1]
|
|
):
|
|
network_changes.append("prefix6")
|
|
|
|
if network_gateway6 is not None and network_gateway6 != vm_vif_params["ipv6_gateway"]:
|
|
network_changes.append("gateway6")
|
|
|
|
elif self.vm_params["customization_agent"] == "custom":
|
|
vm_xenstore_data = self.vm_params["xenstore_data"]
|
|
|
|
if network_type and network_type != vm_xenstore_data.get(
|
|
f"vm-data/networks/{vm_vif_params['device']}/type", "none"
|
|
):
|
|
network_changes.append("type")
|
|
need_poweredoff = True
|
|
|
|
if network_type and network_type == "static":
|
|
if network_ip and network_ip != vm_xenstore_data.get(
|
|
f"vm-data/networks/{vm_vif_params['device']}/ip", ""
|
|
):
|
|
network_changes.append("ip")
|
|
need_poweredoff = True
|
|
|
|
if network_prefix and network_prefix != vm_xenstore_data.get(
|
|
f"vm-data/networks/{vm_vif_params['device']}/prefix", ""
|
|
):
|
|
network_changes.append("prefix")
|
|
network_changes.append("netmask")
|
|
need_poweredoff = True
|
|
|
|
_device_gw_path = f"vm-data/networks/{vm_vif_params['device']}/gateway"
|
|
if network_gateway is not None and network_gateway != vm_xenstore_data.get(
|
|
_device_gw_path, ""
|
|
):
|
|
network_changes.append("gateway")
|
|
need_poweredoff = True
|
|
|
|
if network_type6 and network_type6 != vm_xenstore_data.get(
|
|
f"vm-data/networks/{vm_vif_params['device']}/type6", "none"
|
|
):
|
|
network_changes.append("type6")
|
|
need_poweredoff = True
|
|
|
|
if network_type6 and network_type6 == "static":
|
|
if network_ip6 and network_ip6 != vm_xenstore_data.get(
|
|
f"vm-data/networks/{vm_vif_params['device']}/ip6", ""
|
|
):
|
|
network_changes.append("ip6")
|
|
need_poweredoff = True
|
|
|
|
if network_prefix6 and network_prefix6 != vm_xenstore_data.get(
|
|
f"vm-data/networks/{vm_vif_params['device']}/prefix6", ""
|
|
):
|
|
network_changes.append("prefix6")
|
|
need_poweredoff = True
|
|
|
|
_device_gw6_path = f"vm-data/networks/{vm_vif_params['device']}/gateway6"
|
|
if network_gateway6 is not None and network_gateway6 != vm_xenstore_data.get(
|
|
_device_gw6_path, ""
|
|
):
|
|
network_changes.append("gateway6")
|
|
need_poweredoff = True
|
|
|
|
config_changes_networks.append(network_changes)
|
|
# If this is a new VIF.
|
|
else:
|
|
if not network_name:
|
|
self.module.fail_json(
|
|
msg=f"VM check networks[{position}]: network name is required for new network interface!"
|
|
)
|
|
|
|
if network_type and network_type == "static" and network_ip and not network_netmask:
|
|
self.module.fail_json(
|
|
msg=f"VM check networks[{position}]: IPv4 netmask or prefix is required for new network interface!"
|
|
)
|
|
|
|
if network_type6 and network_type6 == "static" and network_ip6 and not network_prefix6:
|
|
self.module.fail_json(
|
|
msg=f"VM check networks[{position}]: IPv6 prefix is required for new network interface!"
|
|
)
|
|
|
|
# Restart is needed if we are adding new network
|
|
# interface with IP/gateway parameters specified
|
|
# and custom agent is used.
|
|
if self.vm_params["customization_agent"] == "custom":
|
|
for parameter in ["type", "ip", "prefix", "gateway", "type6", "ip6", "prefix6", "gateway6"]:
|
|
if network_params.get(parameter):
|
|
need_poweredoff = True
|
|
break
|
|
|
|
if not vif_devices_allowed:
|
|
self.module.fail_json(
|
|
msg=f"VM check networks[{position}]: maximum number of network interfaces reached!"
|
|
)
|
|
|
|
# We need to place a new network interface right above the
|
|
# highest placed existing interface to maintain relative
|
|
# positions pairable with network interface specifications
|
|
# in module params.
|
|
vif_device = str(int(vif_device_highest) + 1)
|
|
|
|
if vif_device not in vif_devices_allowed:
|
|
self.module.fail_json(
|
|
msg=f"VM check networks[{position}]: new network interface position {vif_device} is out of bounds!"
|
|
)
|
|
|
|
vif_devices_allowed.remove(vif_device)
|
|
vif_device_highest = vif_device
|
|
|
|
# For new VIFs we only track their position.
|
|
config_new_networks.append((position, vif_device))
|
|
|
|
# We should append config_changes_networks to config_changes only
|
|
# if there is at least one changed network, else skip.
|
|
for network_change in config_changes_networks:
|
|
if network_change:
|
|
config_changes.append({"networks_changed": config_changes_networks})
|
|
break
|
|
|
|
if config_new_networks:
|
|
config_changes.append({"networks_new": config_new_networks})
|
|
|
|
config_changes_custom_params = []
|
|
|
|
if self.module.params["custom_params"]:
|
|
for position in range(len(self.module.params["custom_params"])):
|
|
custom_param = self.module.params["custom_params"][position]
|
|
|
|
custom_param_key = custom_param["key"]
|
|
custom_param_value = custom_param["value"]
|
|
|
|
if custom_param_key not in self.vm_params:
|
|
self.module.fail_json(
|
|
msg=f"VM check custom_params[{position}]: unknown VM param '{custom_param_key}'!"
|
|
)
|
|
|
|
if custom_param_value != self.vm_params[custom_param_key]:
|
|
# We only need to track custom param position.
|
|
config_changes_custom_params.append(position)
|
|
|
|
if config_changes_custom_params:
|
|
config_changes.append({"custom_params": config_changes_custom_params})
|
|
|
|
if need_poweredoff:
|
|
config_changes.append("need_poweredoff")
|
|
|
|
return config_changes
|
|
|
|
except XenAPI.Failure as f:
|
|
self.module.fail_json(msg=f"XAPI ERROR: {f.details}")
|
|
|
|
def get_normalized_disk_size(self, disk_params, msg_prefix=""):
|
|
"""Parses disk size parameters and returns disk size in bytes.
|
|
|
|
This method tries to parse disk size module parameters. It fails
|
|
with an error message if size cannot be parsed.
|
|
|
|
Args:
|
|
disk_params (dist): A dictionary with disk parameters.
|
|
msg_prefix (str): A string error messages should be prefixed
|
|
with (default: "").
|
|
|
|
Returns:
|
|
int: disk size in bytes if disk size is successfully parsed or
|
|
None if no disk size parameters were found.
|
|
"""
|
|
# There should be only single size spec but we make a list of all size
|
|
# specs just in case. Priority is given to 'size' but if not found, we
|
|
# check for 'size_tb', 'size_gb', 'size_mb' etc. and use first one
|
|
# found.
|
|
disk_size_spec = [
|
|
x for x in disk_params.keys() if disk_params[x] is not None and (x.startswith("size_") or x == "size")
|
|
]
|
|
|
|
if disk_size_spec:
|
|
try:
|
|
# size
|
|
if "size" in disk_size_spec:
|
|
size_regex = re.compile(r"(\d+(?:\.\d+)?)\s*(.*)")
|
|
disk_size_m = size_regex.match(disk_params["size"])
|
|
|
|
if disk_size_m:
|
|
size = disk_size_m.group(1)
|
|
unit = disk_size_m.group(2)
|
|
else:
|
|
raise ValueError
|
|
# size_tb, size_gb, size_mb, size_kb, size_b
|
|
else:
|
|
size = disk_params[disk_size_spec[0]]
|
|
unit = disk_size_spec[0].split("_")[-1]
|
|
|
|
if not unit:
|
|
unit = "b"
|
|
else:
|
|
unit = unit.lower()
|
|
|
|
if re.match(r"\d+\.\d+", size):
|
|
# We found float value in string, let's typecast it.
|
|
if unit == "b":
|
|
# If we found float but unit is bytes, we get the integer part only.
|
|
size = int(float(size))
|
|
else:
|
|
size = float(size)
|
|
else:
|
|
# We found int value in string, let's typecast it.
|
|
size = int(size)
|
|
|
|
if not size or size < 0:
|
|
raise ValueError
|
|
|
|
except (TypeError, ValueError, NameError):
|
|
# Common failure
|
|
self.module.fail_json(
|
|
msg=f"{msg_prefix}failed to parse disk size! Please review value provided using documentation."
|
|
)
|
|
|
|
disk_units = dict(tb=4, gb=3, mb=2, kb=1, b=0)
|
|
|
|
if unit in disk_units:
|
|
return int(size * (1024 ** disk_units[unit]))
|
|
else:
|
|
self.module.fail_json(
|
|
msg="%s'%s' is not a supported unit for disk size! Supported units are ['%s']."
|
|
% (msg_prefix, unit, "', '".join(sorted(disk_units.keys(), key=lambda key: disk_units[key])))
|
|
)
|
|
else:
|
|
return None
|
|
|
|
@staticmethod
|
|
def get_cdrom_type(vm_cdrom_params):
|
|
"""Returns VM CD-ROM type."""
|
|
# TODO: implement support for detecting type host. No server to test
|
|
# this on at the moment.
|
|
if vm_cdrom_params["empty"]:
|
|
return "none"
|
|
else:
|
|
return "iso"
|
|
|
|
|
|
def main():
|
|
argument_spec = xenserver_common_argument_spec()
|
|
argument_spec.update(
|
|
state=dict(type="str", default="present", choices=["present", "absent", "poweredon"]),
|
|
name=dict(type="str", aliases=["name_label"]),
|
|
name_desc=dict(type="str"),
|
|
uuid=dict(type="str"),
|
|
template=dict(type="str", aliases=["template_src"]),
|
|
template_uuid=dict(type="str"),
|
|
is_template=dict(type="bool", default=False),
|
|
folder=dict(type="str"),
|
|
hardware=dict(
|
|
type="dict",
|
|
options=dict(
|
|
num_cpus=dict(type="int"),
|
|
num_cpu_cores_per_socket=dict(type="int"),
|
|
memory_mb=dict(type="int"),
|
|
),
|
|
),
|
|
disks=dict(
|
|
type="list",
|
|
elements="dict",
|
|
options=dict(
|
|
size=dict(type="str"),
|
|
size_tb=dict(type="str"),
|
|
size_gb=dict(type="str"),
|
|
size_mb=dict(type="str"),
|
|
size_kb=dict(type="str"),
|
|
size_b=dict(type="str"),
|
|
name=dict(type="str", aliases=["name_label"]),
|
|
name_desc=dict(type="str"),
|
|
sr=dict(type="str"),
|
|
sr_uuid=dict(type="str"),
|
|
),
|
|
aliases=["disk"],
|
|
mutually_exclusive=[
|
|
["size", "size_tb", "size_gb", "size_mb", "size_kb", "size_b"],
|
|
["sr", "sr_uuid"],
|
|
],
|
|
),
|
|
cdrom=dict(
|
|
type="dict",
|
|
options=dict(
|
|
type=dict(type="str", choices=["none", "iso"]),
|
|
iso_name=dict(type="str"),
|
|
),
|
|
required_if=[
|
|
["type", "iso", ["iso_name"]],
|
|
],
|
|
),
|
|
networks=dict(
|
|
type="list",
|
|
elements="dict",
|
|
options=dict(
|
|
name=dict(type="str", aliases=["name_label"]),
|
|
mac=dict(type="str"),
|
|
type=dict(type="str", choices=["none", "dhcp", "static"]),
|
|
ip=dict(type="str"),
|
|
netmask=dict(type="str"),
|
|
gateway=dict(type="str"),
|
|
type6=dict(type="str", choices=["none", "dhcp", "static"]),
|
|
ip6=dict(type="str"),
|
|
gateway6=dict(type="str"),
|
|
),
|
|
aliases=["network"],
|
|
required_if=[
|
|
["type", "static", ["ip"]],
|
|
["type6", "static", ["ip6"]],
|
|
],
|
|
),
|
|
home_server=dict(type="str"),
|
|
custom_params=dict(
|
|
type="list",
|
|
elements="dict",
|
|
options=dict(
|
|
key=dict(type="str", required=True, no_log=False),
|
|
value=dict(type="raw", required=True),
|
|
),
|
|
),
|
|
wait_for_ip_address=dict(type="bool", default=False),
|
|
state_change_timeout=dict(type="int", default=0),
|
|
linked_clone=dict(type="bool", default=False),
|
|
force=dict(type="bool", default=False),
|
|
)
|
|
|
|
module = AnsibleModule(
|
|
argument_spec=argument_spec,
|
|
supports_check_mode=True,
|
|
required_one_of=[
|
|
["name", "uuid"],
|
|
],
|
|
mutually_exclusive=[
|
|
["template", "template_uuid"],
|
|
],
|
|
)
|
|
|
|
result = {"failed": False, "changed": False}
|
|
|
|
vm = XenServerVM(module)
|
|
|
|
# Find existing VM
|
|
if vm.exists():
|
|
if module.params["state"] == "absent":
|
|
vm.destroy()
|
|
result["changed"] = True
|
|
elif module.params["state"] == "present":
|
|
config_changes = vm.reconfigure()
|
|
|
|
if config_changes:
|
|
result["changed"] = True
|
|
|
|
# Make new disk and network changes more user friendly
|
|
# and informative.
|
|
for change in config_changes:
|
|
if isinstance(change, dict):
|
|
if change.get("disks_new"):
|
|
disks_new = []
|
|
|
|
for position, userdevice in change["disks_new"]:
|
|
disk_new_params = {"position": position, "vbd_userdevice": userdevice}
|
|
disk_params = module.params["disks"][position]
|
|
|
|
for k in disk_params.keys():
|
|
if disk_params[k] is not None:
|
|
disk_new_params[k] = disk_params[k]
|
|
|
|
disks_new.append(disk_new_params)
|
|
|
|
if disks_new:
|
|
change["disks_new"] = disks_new
|
|
|
|
elif change.get("networks_new"):
|
|
networks_new = []
|
|
|
|
for position, device in change["networks_new"]:
|
|
network_new_params = {"position": position, "vif_device": device}
|
|
network_params = module.params["networks"][position]
|
|
|
|
for k in network_params.keys():
|
|
if network_params[k] is not None:
|
|
network_new_params[k] = network_params[k]
|
|
|
|
networks_new.append(network_new_params)
|
|
|
|
if networks_new:
|
|
change["networks_new"] = networks_new
|
|
|
|
result["changes"] = config_changes
|
|
|
|
elif module.params["state"] in [
|
|
"poweredon",
|
|
"poweredoff",
|
|
"restarted",
|
|
"shutdownguest",
|
|
"rebootguest",
|
|
"suspended",
|
|
]:
|
|
result["changed"] = vm.set_power_state(module.params["state"])
|
|
elif module.params["state"] != "absent":
|
|
vm.deploy()
|
|
result["changed"] = True
|
|
|
|
if module.params["wait_for_ip_address"] and module.params["state"] != "absent":
|
|
vm.wait_for_ip_address()
|
|
|
|
result["instance"] = vm.gather_facts()
|
|
|
|
if result["failed"]:
|
|
module.fail_json(**result)
|
|
else:
|
|
module.exit_json(**result)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|