1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00
community.general/plugins/modules/xenserver_guest.py

2031 lines
97 KiB
Python
Raw Permalink Normal View History

2020-03-09 10:11:07 +01:00
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# 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
2020-03-09 10:11:07 +01:00
from __future__ import (absolute_import, division, print_function)
2020-03-09 10:11:07 +01:00
__metaclass__ = type
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 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
2020-03-09 10:11:07 +01:00
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)
2020-03-09 10:11:07 +01:00
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 O(networks[].type), O(networks[].ip), O(networks[].gateway) etc. parameters, is supported on
2020-03-09 10:11:07 +01:00
XenServer 7.0 or newer for Windows guests by using official XenServer Guest agent support for network configuration. The module will try to
detect if such support is available and utilize it, else it will use a custom method of configuration via 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:
2020-03-09 10:11:07 +01:00
U(https://www.citrix.com/community/citrix-developer/citrix-hypervisor-developer/citrix-hypervisor-developing-products/citrix-hypervisor-staticip.html)'
- 'On platforms without official support for network configuration inside a guest OS, network parameters will be written to xenstore
2023-10-25 23:01:32 +02:00
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
2020-03-09 10:11:07 +01:00
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 will read 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 will require VM restart if any
2023-10-25 23:01:32 +02:00
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:
2020-03-09 10:11:07 +01:00
U(https://support.citrix.com/article/CTX226713)'
requirements:
- XenAPI
attributes:
check_mode:
support: full
diff_mode:
support: none
2020-03-09 10:11:07 +01:00
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.
2020-03-09 10:11:07 +01:00
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 will fail 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.
2020-03-09 10:11:07 +01:00
- 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 will be 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 will fail 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.
2020-03-09 10:11:07 +01:00
- If VM already exists, this setting will be 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
2020-03-09 10:11:07 +01:00
folder:
description:
- Destination folder for VM.
- This parameter is case sensitive.
- 'Example:'
- ' 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
2020-03-09 10:11:07 +01:00
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.
2020-03-09 10:11:07 +01:00
type: list
elements: dict
2020-03-09 10:11:07 +01:00
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, will use 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
2020-03-09 10:11:07 +01:00
cdrom:
description:
- A CD-ROM configuration for the VM.
- All parameters are case sensitive.
type: dict
suboptions:
type:
description:
- The type of CD-ROM. With V(none) the CD-ROM device will be 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
2020-03-09 10:11:07 +01:00
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.
2020-03-09 10:11:07 +01:00
type: list
elements: dict
2020-03-09 10:11:07 +01:00
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 (e.g. Windows) or unconfigured interface (e.g. 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
2020-03-09 10:11:07 +01:00
home_server:
description:
- Name of a XenServer host that will be 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.
2023-10-25 23:01:32 +02:00
- Useful for advanced users familiar with managing VM params through xe CLI.
- A custom value object takes two fields O(custom_params[].key) and O(custom_params[].value) (see example below).
2020-03-09 10:11:07 +01:00
type: list
elements: dict
suboptions:
key:
description:
- VM param name.
type: str
required: true
value:
description:
- VM param value.
type: raw
required: true
2020-03-09 10:11:07 +01:00
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.
2020-03-09 10:11:07 +01:00
- This requires XenServer Tools to be preinstalled on the VM to work properly.
type: bool
default: false
2020-03-09 10:11:07 +01:00
state_change_timeout:
description:
2023-10-25 23:01:32 +02:00
- 'By default, module will wait indefinitely for VM to acquire an IP address if O(wait_for_ip_address=true).'
2020-03-09 10:11:07 +01:00
- If this parameter is set to positive value, the module will instead wait specified number of seconds for the state change.
- In case of timeout, module will generate an error message.
type: int
default: 0
linked_clone:
description:
- Whether to create a Linked Clone from the template, existing VM or snapshot. If no, will create a full copy.
- This is equivalent to C(Use storage-level fast disk clone) option in XenCenter.
type: bool
default: false
2020-03-09 10:11:07 +01:00
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
2020-03-09 10:11:07 +01:00
extends_documentation_fragment:
- community.general.xenserver.documentation
- community.general.attributes
2020-03-09 10:11:07 +01:00
'''
EXAMPLES = r'''
- name: Create a VM from a template
community.general.xenserver_guest:
2020-03-09 10:11:07 +01:00
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
2020-03-09 10:11:07 +01:00
delegate_to: localhost
register: deploy
- name: Create a VM template
community.general.xenserver_guest:
2020-03-09 10:11:07 +01:00
hostname: "{{ xenserver_hostname }}"
username: "{{ xenserver_username }}"
password: "{{ xenserver_password }}"
folder: /testvms
name: testvm_6
is_template: true
2020-03-09 10:11:07 +01:00
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:
2020-03-09 10:11:07 +01:00
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:
2020-03-09 10:11:07 +01:00
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:
2020-03-09 10:11:07 +01:00
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:
2020-03-09 10:11:07 +01:00
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.module_utils import six
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)
2020-03-09 10:11:07 +01:00
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(XenServerVM, self).__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="XAPI ERROR: %s" % 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, six.string_types):
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"]
position = 0
for disk_change_list in 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],
"VM reconfigure disks[%s]: " % position)))
position += 1
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 "%s-%s" % (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], "VM reconfigure disks[%s]: " % 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 = "/%s" % network_params['prefix']
vif_reconfigure_needed = True
elif vm_vif_params['ipv4_addresses'] and vm_vif_params['ipv4_addresses'][0]:
network_prefix = "/%s" % 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,
"%s%s" % (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 = "/%s" % network_params['prefix6']
vif_reconfigure_needed = True
elif vm_vif_params['ipv6_addresses'] and vm_vif_params['ipv6_addresses'][0]:
network_prefix6 = "/%s" % 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,
"%s%s" % (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
2023-10-25 23:01:32 +02:00
# or mac e.g. through XenCenter and then also
2020-03-09 10:11:07 +01:00
# 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,
"vm-data/networks/%s/%s" % (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,
"vm-data/networks/%s/%s" % (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,
"vm-data/networks/%s/%s" % (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,
"vm-data/networks/%s/%s" % (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",
"%s/%s" % (network_ip, network_prefix), network_gateway)
if network_type6 and network_type6 == "static":
self.xapi_session.xenapi.VIF.configure_ipv6(vif_ref_new, "Static",
"%s/%s" % (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, "vm-data/networks/%s/%s" % (vif_device, network_param))
self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref, "vm-data/networks/%s/name" % vif_device, 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, "vm-data/networks/%s/mac" % vif_device, vm_vif_mac)
if network_type:
self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref, "vm-data/networks/%s/type" % vif_device, network_type)
if network_type == "static":
self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref,
"vm-data/networks/%s/ip" % vif_device, network_ip)
self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref,
"vm-data/networks/%s/prefix" % vif_device, network_prefix)
self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref,
"vm-data/networks/%s/netmask" % vif_device, network_netmask)
self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref,
"vm-data/networks/%s/gateway" % vif_device, network_gateway)
if network_type6:
self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref, "vm-data/networks/%s/type6" % vif_device, network_type6)
if network_type6 == "static":
self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref,
"vm-data/networks/%s/ip6" % vif_device, network_ip6)
self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref,
"vm-data/networks/%s/prefix6" % vif_device, network_prefix6)
self.xapi_session.xenapi.VM.add_to_xenstore_data(self.vm_ref,
"vm-data/networks/%s/gateway6" % vif_device, 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("VM.set_%s" % 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="XAPI ERROR: %s" % 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="XAPI ERROR: %s" % 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.
2020-03-09 10:11:07 +01:00
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="VM check disks: provided disks configuration has less disks than the target VM (%d < %d)!" %
(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], "VM check disks[%s]: " % position)
disk_name = disk_params.get('name')
if disk_name is not None and not disk_name:
self.module.fail_json(msg="VM check disks[%s]: disk name cannot be an empty string!" % position)
# 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="VM check disks[%s]: disk size is smaller than existing (%d bytes < %s bytes). "
"Reducing disk size is not allowed!" % (position, disk_size, vm_disk_params['VDI']['virtual_size']))
config_changes_disks.append(disk_changes)
# If this is a new disk.
else:
if not disk_size:
self.module.fail_json(msg="VM check disks[%s]: no valid disk size specification found!" % position)
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.
2020-03-09 10:11:07 +01:00
get_object_ref(self.module, disk_sr, disk_sr_uuid, obj_type="SR", fail=True,
msg_prefix="VM check disks[%s]: " % position)
elif self.default_sr_ref == 'OpaqueRef:NULL':
self.module.fail_json(msg="VM check disks[%s]: no default SR found! You must specify SR explicitly." % position)
if not vbd_userdevices_allowed:
self.module.fail_json(msg="VM check disks[%s]: maximum number of devices reached!" % position)
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="VM check disks[%s]: new disk position %s is out of bounds!" % (position, disk_userdevice))
# 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.
2020-03-09 10:11:07 +01:00
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="VM check networks: provided networks configuration has less interfaces than the target VM (%d < %d)!" %
(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="VM check networks[%s]: network name cannot be an empty string!" % position)
if network_name:
# Check existence only. Ignore return value.
2020-03-09 10:11:07 +01:00
get_object_ref(self.module, network_name, uuid=None, obj_type="network", fail=True,
msg_prefix="VM check networks[%s]: " % 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="VM check networks[%s]: specified MAC address '%s' is not valid!" % (position, network_mac))
# 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="VM check networks[%s]: specified IPv4 address '%s' is not valid!" % (position, network_ip))
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="VM check networks[%s]: specified IPv4 prefix '%s' is not valid!" % (position, network_prefix))
if network_netmask is not None:
if not is_valid_ip_netmask(network_netmask):
self.module.fail_json(msg="VM check networks[%s]: specified IPv4 netmask '%s' is not valid!" % (position, network_netmask))
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="VM check networks[%s]: specified IPv4 gateway '%s' is not valid!" % (position, network_gateway))
# 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="VM check networks[%s]: specified IPv6 address '%s' is not valid!" % (position, network_ip6))
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="VM check networks[%s]: specified IPv6 prefix '%s' is not valid!" % (position, network_prefix6))
# 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="VM check networks[%s]: specified IPv6 gateway '%s' is not valid!" % (position, network_gateway6))
# 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('vm-data/networks/%s/type' % vm_vif_params['device'], "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('vm-data/networks/%s/ip' % vm_vif_params['device'], ""):
network_changes.append('ip')
need_poweredoff = True
if network_prefix and network_prefix != vm_xenstore_data.get('vm-data/networks/%s/prefix' % vm_vif_params['device'], ""):
network_changes.append('prefix')
network_changes.append('netmask')
need_poweredoff = True
if network_gateway is not None and network_gateway != vm_xenstore_data.get('vm-data/networks/%s/gateway' %
vm_vif_params['device'], ""):
network_changes.append('gateway')
need_poweredoff = True
if network_type6 and network_type6 != vm_xenstore_data.get('vm-data/networks/%s/type6' % vm_vif_params['device'], "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('vm-data/networks/%s/ip6' % vm_vif_params['device'], ""):
network_changes.append('ip6')
need_poweredoff = True
if network_prefix6 and network_prefix6 != vm_xenstore_data.get('vm-data/networks/%s/prefix6' % vm_vif_params['device'], ""):
network_changes.append('prefix6')
need_poweredoff = True
if network_gateway6 is not None and network_gateway6 != vm_xenstore_data.get('vm-data/networks/%s/gateway6' %
vm_vif_params['device'], ""):
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="VM check networks[%s]: network name is required for new network interface!" % position)
if network_type and network_type == "static" and network_ip and not network_netmask:
self.module.fail_json(msg="VM check networks[%s]: IPv4 netmask or prefix is required for new network interface!" % position)
if network_type6 and network_type6 == "static" and network_ip6 and not network_prefix6:
self.module.fail_json(msg="VM check networks[%s]: IPv6 prefix is required for new network interface!" % position)
# 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="VM check networks[%s]: maximum number of network interfaces reached!" % position)
# 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="VM check networks[%s]: new network interface position %s is out of bounds!" % (position, vif_device))
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="VM check custom_params[%s]: unknown VM param '%s'!" % (position, 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="XAPI ERROR: %s" % 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="%sfailed to parse disk size! Please review value provided using documentation." % msg_prefix)
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),
2020-03-09 10:11:07 +01:00
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()