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

New module: xenserver_guest - manages XenServer VMs (#41041)

* XenServer related modules - initial commit

 - New module_util: xenserver. Contains common module arguments, functions
   and classes useful for future XenServer related modules.
 - New module_docs_fragment: xenserver. Describes common module arguments.
 - New module: xenserver_guest. Supports VM deployment, reconfiguration,
   removal, detection of changes, state management, fact gathering and
   Ansible check mode. Module is fully documented.
 - Updated: developing_module_utilities.rst.
 - Module params, workflow and some functions are based on or taken from
   vmware_guest module.

* Implemented support for configuring custom VM params in xenserver_guest module

* Compatibility fixes and documentation update

 - xenserver module_util: implemented support for XenAPI.py version older
   than 7.2.
 - xenserver module_util: PEP8 fixes.
 - xenserver module_util: Added missing imports.
 - xenserver module_util: Copyright notice fixes.
 - xenserver_guest module: updated module documentation with notes regarding
   module requirements and compatibility.
 - xenserver_guest module: bumped version_added to 2.7.
 - xenserver_guest module: minor fixes.

* VM power state management refactoring, subargument specs in xenserver_guest module, other fixes

 - VM power state management code moved from xenserver_guest module to xenserver
   module_util (set_vm_power_state function).
 - Code for waiting for VM IP address moved from xenserver_guest module to
   xenserver module_util (wait_for_vm_ip_address function).
 - xenserver module_util: implemented get_object_ref function to clean up
   a lot of repeated code in xenserver_guest module.
 - xenserver module_util: added additional aliases 'host' and 'pool' for
   'hostname' common module argument. They are more in line with what
   XenServer users are familiar with.
 - xenserver module_util: minor fixes.
 - xenserver_guest module: removed VM state management other than 'present',
   'absent' and 'poweredon'. Other states are to be managed by separate module.
 - xenserver_guest module: added subargument specs and cleaned up custom code
   for subargument validation.
 - xenserver_guest module: reorganized code for disk and network reconfiguation
   to minimize code duplication.
 - xenserver_guest module: renamed 'cdrom.iso' module argument to
   'cdrom.iso_name', avoids cryptic error message when parameter is
   missing.
 - xenserver_guest module: documentation update.
 - xenserver_guest module: changes in error messages.
 - xenserver_guest module: minor fixes.

* Implemented guest OS network parameter configuration and other

 - xenserver module_util: moved code for validating MAC addresses from
   xenserver_guest module and implemented a range of functions for validating
   IP addresses and related entities and converting prefixes to netmasks and
   vice versa.
 - xenserver module_util: updated fact gathering code to support guest OS
   network parameters.
 - xenserver module_util: added docstrings.
 - xenserver module_util: minor changes.
 - xenserver_guest module: implemented support for guest OS network parameter
   configuration.
 - xenserver_guest module: changed CD-ROM handling code.
 - xenserver_guest module: changed so that user friendly version of changes
   list is always returned in module result.
 - xenserver_guest module: error message changes.
 - xenserver_guest module: added docstrings.
 - xenserver_guest module: documentation update.
 - xenserver_guest module: minor changes and fixes.

* Various fixes and code cleanup

 - xenserver module_util: implemented get_xenserver_version function.
 - xenserver module_util: moved customization agent detection code to
   gather_vm_params function. customization_agent variable is now part of
   vm_params. An exception in customization agent detection code that prevented
   deployment of new VMs is also fixed.
 - xenserver module_util: added support for alternative VM state names with
   dash and underscore in set_power_state function.
 - xenserver_guest module: removed customization agent detection code
   as it is now implemented in xenserver module_util.
 - xenserver_guest module: fixed a bug in xenserver_data update code that
   occured when "networks.mac" was not specified in module params and
   other fixes.
 - xenserver_guest module: some code cleanup.
 - xenserver_guest module: bumped version_added to 2.8.
This commit is contained in:
Bojan Vitnik 2018-11-19 07:50:20 +01:00 committed by ansibot
parent 5f6a350b3a
commit b0cc240665
5 changed files with 2804 additions and 0 deletions

View file

@ -78,3 +78,4 @@ The following is a list of ``module_utils`` files and a general description. The
- utm_utils.py - Contains base class for creating new Sophos UTM Modules and helper functions for handling the rest interface of Sophos UTM - utm_utils.py - Contains base class for creating new Sophos UTM Modules and helper functions for handling the rest interface of Sophos UTM
- vca.py - Contains utilities for modules that work with VMware vCloud Air - vca.py - Contains utilities for modules that work with VMware vCloud Air
- vmware.py - Contains utilities for modules that work with VMware vSphere VMs - vmware.py - Contains utilities for modules that work with VMware vSphere VMs
- xenserver.py - Contains utilities for modules that work with XenServer.

View file

@ -0,0 +1,867 @@
# -*- coding: utf-8 -*-
#
# Copyright: (c) 2018, Bojan Vitnik <bvitnik@mainstream.rs>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import atexit
import time
import re
HAS_XENAPI = False
try:
import XenAPI
HAS_XENAPI = True
except ImportError:
pass
from ansible.module_utils._text import to_text
from ansible.module_utils.urls import fetch_url
from ansible.module_utils.six import integer_types, iteritems, string_types
from ansible.module_utils.basic import env_fallback
from ansible.module_utils.ansible_release import __version__ as ANSIBLE_VERSION
def xenserver_common_argument_spec():
return dict(
hostname=dict(type='str',
aliases=['host', 'pool'],
required=False,
default='localhost',
fallback=(env_fallback, ['XENSERVER_HOST']),
),
username=dict(type='str',
aliases=['user', 'admin'],
required=False,
default='root',
fallback=(env_fallback, ['XENSERVER_USER'])),
password=dict(type='str',
aliases=['pass', 'pwd'],
required=False,
no_log=True,
fallback=(env_fallback, ['XENSERVER_PASSWORD'])),
validate_certs=dict(type='bool',
required=False,
default=True,
fallback=(env_fallback, ['XENSERVER_VALIDATE_CERTS'])),
)
def xapi_to_module_vm_power_state(power_state):
"""Maps XAPI VM power states to module VM power states."""
module_power_state_map = {
"running": "poweredon",
"halted": "poweredoff",
"suspended": "suspended",
"paused": "paused"
}
return module_power_state_map.get(power_state)
def module_to_xapi_vm_power_state(power_state):
"""Maps module VM power states to XAPI VM power states."""
vm_power_state_map = {
"poweredon": "running",
"poweredoff": "halted",
"restarted": "running",
"suspended": "suspended",
"shutdownguest": "halted",
"rebootguest": "running",
}
return vm_power_state_map.get(power_state)
def is_valid_mac_addr(mac_addr):
"""Validates given string as MAC address.
Args:
mac_addr (str): string to validate as MAC address.
Returns:
bool: True if string is valid MAC address, else False.
"""
mac_addr_regex = re.compile('[0-9a-f]{2}([-:])[0-9a-f]{2}(\\1[0-9a-f]{2}){4}$')
return bool(mac_addr_regex.match(mac_addr.lower()))
def is_valid_ip_addr(ip_addr):
"""Validates given string as IPv4 address for given string.
Args:
ip_addr (str): string to validate as IPv4 address.
Returns:
bool: True if string is valid IPv4 address, else False.
"""
ip_addr_split = ip_addr.split('.')
if len(ip_addr_split) != 4:
return False
for ip_addr_octet in ip_addr_split:
if not ip_addr_octet.isdigit():
return False
ip_addr_octet_int = int(ip_addr_octet)
if ip_addr_octet_int < 0 or ip_addr_octet_int > 255:
return False
return True
def is_valid_ip_netmask(ip_netmask):
"""Validates given string as IPv4 netmask.
Args:
ip_netmask (str): string to validate as IPv4 netmask.
Returns:
bool: True if string is valid IPv4 netmask, else False.
"""
ip_netmask_split = ip_netmask.split('.')
if len(ip_netmask_split) != 4:
return False
valid_octet_values = ['0', '128', '192', '224', '240', '248', '252', '254', '255']
for ip_netmask_octet in ip_netmask_split:
if ip_netmask_octet not in valid_octet_values:
return False
if ip_netmask_split[0] != '255' and (ip_netmask_split[1] != '0' or ip_netmask_split[2] != '0' or ip_netmask_split[3] != '0'):
return False
elif ip_netmask_split[1] != '255' and (ip_netmask_split[2] != '0' or ip_netmask_split[3] != '0'):
return False
elif ip_netmask_split[2] != '255' and ip_netmask_split[3] != '0':
return False
return True
def is_valid_ip_prefix(ip_prefix):
"""Validates given string as IPv4 prefix.
Args:
ip_prefix (str): string to validate as IPv4 prefix.
Returns:
bool: True if string is valid IPv4 prefix, else False.
"""
if not ip_prefix.isdigit():
return False
ip_prefix_int = int(ip_prefix)
if ip_prefix_int < 0 or ip_prefix_int > 32:
return False
return True
def ip_prefix_to_netmask(ip_prefix, skip_check=False):
"""Converts IPv4 prefix to netmask.
Args:
ip_prefix (str): IPv4 prefix to convert.
skip_check (bool): Skip validation of IPv4 prefix
(default: False). Use if you are sure IPv4 prefix is valid.
Returns:
str: IPv4 netmask equivalent to given IPv4 prefix if
IPv4 prefix is valid, else an empty string.
"""
if skip_check:
ip_prefix_valid = True
else:
ip_prefix_valid = is_valid_ip_prefix(ip_prefix)
if ip_prefix_valid:
return '.'.join([str((0xffffffff << (32 - int(ip_prefix)) >> i) & 0xff) for i in [24, 16, 8, 0]])
else:
return ""
def ip_netmask_to_prefix(ip_netmask, skip_check=False):
"""Converts IPv4 netmask to prefix.
Args:
ip_netmask (str): IPv4 netmask to convert.
skip_check (bool): Skip validation of IPv4 netmask
(default: False). Use if you are sure IPv4 netmask is valid.
Returns:
str: IPv4 prefix equivalent to given IPv4 netmask if
IPv4 netmask is valid, else an empty string.
"""
if skip_check:
ip_netmask_valid = True
else:
ip_netmask_valid = is_valid_ip_netmask(ip_netmask)
if ip_netmask_valid:
return str(sum([bin(int(i)).count("1") for i in ip_netmask.split(".")]))
else:
return ""
def is_valid_ip6_addr(ip6_addr):
"""Validates given string as IPv6 address.
Args:
ip6_addr (str): string to validate as IPv6 address.
Returns:
bool: True if string is valid IPv6 address, else False.
"""
ip6_addr = ip6_addr.lower()
ip6_addr_split = ip6_addr.split(':')
if ip6_addr_split[0] == "":
ip6_addr_split.pop(0)
if ip6_addr_split[-1] == "":
ip6_addr_split.pop(-1)
if len(ip6_addr_split) > 8:
return False
if ip6_addr_split.count("") > 1:
return False
elif ip6_addr_split.count("") == 1:
ip6_addr_split.remove("")
else:
if len(ip6_addr_split) != 8:
return False
ip6_addr_hextet_regex = re.compile('^[0-9a-f]{1,4}$')
for ip6_addr_hextet in ip6_addr_split:
if not bool(ip6_addr_hextet_regex.match(ip6_addr_hextet)):
return False
return True
def is_valid_ip6_prefix(ip6_prefix):
"""Validates given string as IPv6 prefix.
Args:
ip6_prefix (str): string to validate as IPv6 prefix.
Returns:
bool: True if string is valid IPv6 prefix, else False.
"""
if not ip6_prefix.isdigit():
return False
ip6_prefix_int = int(ip6_prefix)
if ip6_prefix_int < 0 or ip6_prefix_int > 128:
return False
return True
def get_object_ref(module, name, uuid=None, obj_type="VM", fail=True, msg_prefix=""):
"""Finds and returns a reference to arbitary XAPI object.
An object is searched by using either name (name_label) or UUID
with UUID taken precendence over name.
Args:
module: Reference to Ansible module object.
name (str): Name (name_label) of an object to search for.
uuid (str): UUID of an object to search for.
obj_type (str): Any valid XAPI object type. See XAPI docs.
fail (bool): Should function fail with error message if object
is not found or exit silently (default: True). The function
always fails if multiple objects with same name are found.
msg_prefix (str): A string error messages should be prefixed
with (default: "").
Returns:
XAPI reference to found object or None if object is not found
and fail=False.
"""
xapi_session = XAPI.connect(module)
if obj_type in ["template", "snapshot"]:
real_obj_type = "VM"
elif obj_type == "home server":
real_obj_type = "host"
elif obj_type == "ISO image":
real_obj_type = "VDI"
else:
real_obj_type = obj_type
obj_ref = None
# UUID has precendence over name.
if uuid:
try:
# Find object by UUID. If no object is found using given UUID,
# an exception will be generated.
obj_ref = xapi_session.xenapi_request("%s.get_by_uuid" % real_obj_type, (uuid,))
except XenAPI.Failure as f:
if fail:
module.fail_json(msg="%s%s with UUID '%s' not found!" % (msg_prefix, obj_type, uuid))
elif name:
try:
# Find object by name (name_label).
obj_ref_list = xapi_session.xenapi_request("%s.get_by_name_label" % real_obj_type, (name,))
except XenAPI.Failure as f:
module.fail_json(msg="XAPI ERROR: %s" % f.details)
# If obj_ref_list is empty.
if not obj_ref_list:
if fail:
module.fail_json(msg="%s%s with name '%s' not found!" % (msg_prefix, obj_type, name))
# If obj_ref_list contains multiple object references.
elif len(obj_ref_list) > 1:
module.fail_json(msg="%smultiple %ss with name '%s' found! Please use UUID." % (msg_prefix, obj_type, name))
# The obj_ref_list contains only one object reference.
else:
obj_ref = obj_ref_list[0]
else:
module.fail_json(msg="%sno valid name or UUID supplied for %s!" % (msg_prefix, obj_type))
return obj_ref
def gather_vm_params(module, vm_ref):
"""Gathers all VM parameters available in XAPI database.
Args:
module: Reference to Ansible module object.
vm_ref (str): XAPI reference to VM.
Returns:
dict: VM parameters.
"""
# We silently return empty vm_params if bad vm_ref was supplied.
if not vm_ref or vm_ref == "OpaqueRef:NULL":
return {}
xapi_session = XAPI.connect(module)
try:
vm_params = xapi_session.xenapi.VM.get_record(vm_ref)
# We need some params like affinity, VBDs, VIFs, VDIs etc. dereferenced.
# Affinity.
if vm_params['affinity'] != "OpaqueRef:NULL":
vm_affinity = xapi_session.xenapi.host.get_record(vm_params['affinity'])
vm_params['affinity'] = vm_affinity
else:
vm_params['affinity'] = {}
# VBDs.
vm_vbd_params_list = [xapi_session.xenapi.VBD.get_record(vm_vbd_ref) for vm_vbd_ref in vm_params['VBDs']]
# List of VBDs is usually sorted by userdevice but we sort just
# in case. We need this list sorted by userdevice so that we can
# make positional pairing with module.params['disks'].
vm_vbd_params_list = sorted(vm_vbd_params_list, key=lambda vm_vbd_params: int(vm_vbd_params['userdevice']))
vm_params['VBDs'] = vm_vbd_params_list
# VDIs.
for vm_vbd_params in vm_params['VBDs']:
if vm_vbd_params['VDI'] != "OpaqueRef:NULL":
vm_vdi_params = xapi_session.xenapi.VDI.get_record(vm_vbd_params['VDI'])
else:
vm_vdi_params = {}
vm_vbd_params['VDI'] = vm_vdi_params
# VIFs.
vm_vif_params_list = [xapi_session.xenapi.VIF.get_record(vm_vif_ref) for vm_vif_ref in vm_params['VIFs']]
# List of VIFs is usually sorted by device but we sort just
# in case. We need this list sorted by device so that we can
# make positional pairing with module.params['networks'].
vm_vif_params_list = sorted(vm_vif_params_list, key=lambda vm_vif_params: int(vm_vif_params['device']))
vm_params['VIFs'] = vm_vif_params_list
# Networks.
for vm_vif_params in vm_params['VIFs']:
if vm_vif_params['network'] != "OpaqueRef:NULL":
vm_network_params = xapi_session.xenapi.network.get_record(vm_vif_params['network'])
else:
vm_network_params = {}
vm_vif_params['network'] = vm_network_params
# Guest metrics.
if vm_params['guest_metrics'] != "OpaqueRef:NULL":
vm_guest_metrics = xapi_session.xenapi.VM_guest_metrics.get_record(vm_params['guest_metrics'])
vm_params['guest_metrics'] = vm_guest_metrics
else:
vm_params['guest_metrics'] = {}
# Detect customization agent.
xenserver_version = get_xenserver_version(module)
if (int(xenserver_version[0]) >= 7 and int(xenserver_version[1]) >= 0 and vm_params.get('guest_metrics') and
"feature-static-ip-setting" in vm_params['guest_metrics']['other']):
vm_params['customization_agent'] = "native"
else:
vm_params['customization_agent'] = "custom"
except XenAPI.Failure as f:
module.fail_json(msg="XAPI ERROR: %s" % f.details)
return vm_params
def gather_vm_facts(module, vm_params):
"""Gathers VM facts.
Args:
module: Reference to Ansible module object.
vm_params (dict): A dictionary with VM parameters as returned
by gather_vm_params() function.
Returns:
dict: VM facts.
"""
# We silently return empty vm_facts if no vm_params are available.
if not vm_params:
return {}
xapi_session = XAPI.connect(module)
# Gather facts.
vm_facts = {
"state": xapi_to_module_vm_power_state(vm_params['power_state'].lower()),
"name": vm_params['name_label'],
"name_desc": vm_params['name_description'],
"uuid": vm_params['uuid'],
"is_template": vm_params['is_a_template'],
"folder": vm_params['other_config'].get('folder', ''),
"hardware": {
"num_cpus": int(vm_params['VCPUs_max']),
"num_cpu_cores_per_socket": int(vm_params['platform'].get('cores-per-socket', '1')),
"memory_mb": int(int(vm_params['memory_dynamic_max']) / 1048576),
},
"disks": [],
"cdrom": {},
"networks": [],
"home_server": vm_params['affinity'].get('name_label', ''),
"domid": vm_params['domid'],
"platform": vm_params['platform'],
"other_config": vm_params['other_config'],
"xenstore_data": vm_params['xenstore_data'],
"customization_agent": vm_params['customization_agent'],
}
for vm_vbd_params in vm_params['VBDs']:
if vm_vbd_params['type'] == "Disk":
vm_disk_sr_params = xapi_session.xenapi.SR.get_record(vm_vbd_params['VDI']['SR'])
vm_disk_params = {
"size": int(vm_vbd_params['VDI']['virtual_size']),
"name": vm_vbd_params['VDI']['name_label'],
"name_desc": vm_vbd_params['VDI']['name_description'],
"sr": vm_disk_sr_params['name_label'],
"sr_uuid": vm_disk_sr_params['uuid'],
"os_device": vm_vbd_params['device'],
"vbd_userdevice": vm_vbd_params['userdevice'],
}
vm_facts['disks'].append(vm_disk_params)
elif vm_vbd_params['type'] == "CD":
if vm_vbd_params['empty']:
vm_facts['cdrom'].update(type="none")
else:
vm_facts['cdrom'].update(type="iso")
vm_facts['cdrom'].update(iso=vm_vbd_params['VDI']['name_label'])
for vm_vif_params in vm_params['VIFs']:
vm_guest_metrics_networks = vm_params['guest_metrics'].get('networks', {})
vm_network_params = {
"name": vm_vif_params['network']['name_label'],
"mac": vm_vif_params['MAC'],
"vif_device": vm_vif_params['device'],
"mtu": vm_vif_params['MTU'],
"ip": vm_guest_metrics_networks.get("%s/ip" % vm_vif_params['device'], ''),
"prefix": "",
"netmask": "",
"gateway": "",
"ip6": [vm_guest_metrics_networks[ipv6] for ipv6 in vm_guest_metrics_networks.keys() if ipv6.startswith("%s/ipv6/" % vm_vif_params['device'])],
"prefix6": "",
"gateway6": "",
}
if vm_params['customization_agent'] == "native":
if vm_vif_params['ipv4_addresses'] and vm_vif_params['ipv4_addresses'][0]:
vm_network_params['prefix'] = vm_vif_params['ipv4_addresses'][0].split('/')[1]
vm_network_params['netmask'] = ip_prefix_to_netmask(vm_network_params['prefix'])
vm_network_params['gateway'] = vm_vif_params['ipv4_gateway']
if vm_vif_params['ipv6_addresses'] and vm_vif_params['ipv6_addresses'][0]:
vm_network_params['prefix6'] = vm_vif_params['ipv6_addresses'][0].split('/')[1]
vm_network_params['gateway6'] = vm_vif_params['ipv6_gateway']
elif vm_params['customization_agent'] == "custom":
vm_xenstore_data = vm_params['xenstore_data']
for f in ['prefix', 'netmask', 'gateway', 'prefix6', 'gateway6']:
vm_network_params[f] = vm_xenstore_data.get("vm-data/networks/%s/%s" % (vm_vif_params['device'], f), "")
vm_facts['networks'].append(vm_network_params)
return vm_facts
def set_vm_power_state(module, vm_ref, power_state, timeout=300):
"""Controls VM power state.
Args:
module: Reference to Ansible module object.
vm_ref (str): XAPI reference to VM.
power_state (str): Power state to put VM into. Accepted values:
- poweredon
- poweredoff
- restarted
- suspended
- shutdownguest
- rebootguest
timeout (int): timeout in seconds (default: 300).
Returns:
tuple (bool, str): Bool element is True if VM power state has
changed by calling this function, else False. Str element carries
a value of resulting power state as defined by XAPI - 'running',
'halted' or 'suspended'.
"""
# Fail if we don't have a valid VM reference.
if not vm_ref or vm_ref == "OpaqueRef:NULL":
module.fail_json(msg="Cannot set VM power state. Invalid VM reference supplied!")
xapi_session = XAPI.connect(module)
power_state = power_state.replace('_', '').replace('-', '').lower()
vm_power_state_resulting = module_to_xapi_vm_power_state(power_state)
state_changed = False
try:
# Get current state of the VM.
vm_power_state_current = xapi_to_module_vm_power_state(xapi_session.xenapi.VM.get_power_state(vm_ref).lower())
if vm_power_state_current != power_state:
if power_state == "poweredon":
if not module.check_mode:
# VM can be in either halted, suspended, paused or running state.
# For VM to be in running state, start has to be called on halted,
# resume on suspended and unpause on paused VM.
if vm_power_state_current == "poweredoff":
xapi_session.xenapi.VM.start(vm_ref, False, False)
elif vm_power_state_current == "suspended":
xapi_session.xenapi.VM.resume(vm_ref, False, False)
elif vm_power_state_current == "paused":
xapi_session.xenapi.VM.unpause(vm_ref)
elif power_state == "poweredoff":
if not module.check_mode:
# hard_shutdown will halt VM regardless of current state.
xapi_session.xenapi.VM.hard_shutdown(vm_ref)
elif power_state == "restarted":
# hard_restart will restart VM only if VM is in paused or running state.
if vm_power_state_current in ["paused", "poweredon"]:
if not module.check_mode:
xapi_session.xenapi.VM.hard_reboot(vm_ref)
else:
module.fail_json(msg="Cannot restart VM in state '%s'!" % vm_power_state_current)
elif power_state == "suspended":
# running state is required for suspend.
if vm_power_state_current == "poweredon":
if not module.check_mode:
xapi_session.xenapi.VM.suspend(vm_ref)
else:
module.fail_json(msg="Cannot suspend VM in state '%s'!" % vm_power_state_current)
elif power_state == "shutdownguest":
# running state is required for guest shutdown.
if vm_power_state_current == "poweredon":
if not module.check_mode:
if timeout == 0:
xapi_session.xenapi.VM.clean_shutdown(vm_ref)
else:
task_ref = xapi_session.xenapi.Async.VM.clean_shutdown(vm_ref)
task_result = wait_for_task(module, task_ref, timeout)
if task_result:
module.fail_json(msg="Guest shutdown task failed: '%s'!" % task_result)
else:
module.fail_json(msg="Cannot shutdown guest when VM is in state '%s'." % vm_power_state_current)
elif power_state == "rebootguest":
# running state is required for guest reboot.
if vm_power_state_current == "poweredon":
if not module.check_mode:
if timeout == 0:
xapi_session.xenapi.VM.clean_reboot(vm_ref)
else:
task_ref = xapi_session.xenapi.Async.VM.clean_reboot(vm_ref)
task_result = wait_for_task(module, task_ref, timeout)
if task_result:
module.fail_json(msg="Guest reboot task failed: '%s'!" % task_result)
else:
module.fail_json(msg="Cannot reboot guest when VM is in state '%s'." % vm_power_state_current)
else:
module.fail_json(msg="Requested VM power state '%s' is unsupported!" % power_state)
state_changed = True
except XenAPI.Failure as f:
module.fail_json(msg="XAPI ERROR: %s" % f.details)
return (state_changed, vm_power_state_resulting)
def wait_for_task(module, task_ref, timeout=300):
"""Waits for async XAPI task to finish.
Args:
module: Reference to Ansible module object.
task_ref (str): XAPI reference to task.
timeout (int): timeout in seconds (default: 300).
Returns:
str: failure message on failure, else an empty string.
"""
# Fail if we don't have a valid task reference.
if not task_ref or task_ref == "OpaqueRef:NULL":
module.fail_json(msg="Cannot wait for task. Invalid task reference supplied!")
xapi_session = XAPI.connect(module)
interval = 2
result = ""
# If we have to wait indefinitely, make timeout larger than 0 so we can
# enter while loop.
if timeout == 0:
timeout = 1
try:
while timeout > 0:
task_status = xapi_session.xenapi.task.get_status(task_ref).lower()
if task_status == "pending":
# Task is still running.
time.sleep(interval)
# We decrease timeout only if we don't wait indefinitely.
if timeout != 0:
timeout -= interval
continue
elif task_status == "success":
# Task is done.
break
else:
# Task failed.
result = task_status
break
else:
# We timed out.
result = "timeout"
xapi_session.xenapi.task.destroy(task_ref)
except XenAPI.Failure as f:
module.fail_json(msg="XAPI ERROR: %s" % f.details)
return result
def wait_for_vm_ip_address(module, vm_ref, timeout=300):
"""Waits for VM to acquire an IP address.
Args:
module: Reference to Ansible module object.
vm_ref (str): XAPI reference to VM.
timeout (int): timeout in seconds (default: 300).
Returns:
dict: VM guest metrics as retrieved by
VM_guest_metrics.get_record() XAPI method with info
on IP address acquired.
"""
# Fail if we don't have a valid VM reference.
if not vm_ref or vm_ref == "OpaqueRef:NULL":
module.fail_json(msg="Cannot wait for VM IP address. Invalid VM reference supplied!")
xapi_session = XAPI.connect(module)
vm_guest_metrics = {}
try:
# We translate VM power state string so that error message can be
# consistent with module VM power states.
vm_power_state = xapi_to_module_vm_power_state(xapi_session.xenapi.VM.get_power_state(vm_ref).lower())
if vm_power_state != 'poweredon':
module.fail_json(msg="Cannot wait for VM IP address when VM is in state '%s'." % vm_power_state)
interval = 2
# If we have to wait indefinitely, make time_left larger than 0 so we can
# enter while loop.
if timeout == 0:
time_left = 1
else:
time_left = timeout
while time_left > 0:
vm_guest_metrics_ref = xapi_session.xenapi.VM.get_guest_metrics(vm_ref)
if vm_guest_metrics_ref != "OpaqueRef:NULL":
vm_guest_metrics = xapi_session.xenapi.VM_guest_metrics.get_record(vm_guest_metrics_ref)
vm_ips = vm_guest_metrics['networks']
if "0/ip" in vm_ips:
break
time.sleep(interval)
# We decrease time_left only if we don't wait indefinitely.
if timeout != 0:
time_left -= interval
else:
# We timed out.
module.fail_json(msg="Timed out waiting for VM IP address!")
except XenAPI.Failure as f:
module.fail_json(msg="XAPI ERROR: %s" % f.details)
return vm_guest_metrics
def get_xenserver_version(module):
"""Returns XenServer version.
Args:
module: Reference to Ansible module object.
Returns:
list: Element [0] is major version. Element [1] i minor version.
"""
xapi_session = XAPI.connect(module)
host_ref = xapi_session.xenapi.session.get_this_host(xapi_session._session)
return xapi_session.xenapi.host.get_software_version(host_ref)['product_version_text_short'].split('.')
class XAPI(object):
"""Class for XAPI session management."""
_xapi_session = None
@classmethod
def connect(cls, module, disconnect_atexit=True):
"""Establishes XAPI connection and returns session reference.
If no existing session is available, establishes a new one
and returns it, else returns existing one.
Args:
module: Reference to Ansible module object.
disconnect_atexit (bool): Controls if method should
register atexit handler to disconnect from XenServer
on module exit (default: True).
Returns:
XAPI session reference.
"""
if cls._xapi_session is not None:
return cls._xapi_session
hostname = module.params['hostname']
username = module.params['username']
password = module.params['password']
ignore_ssl = not module.params['validate_certs']
if hostname == 'localhost':
cls._xapi_session = XenAPI.xapi_local()
username = ''
password = ''
else:
# If scheme is not specified we default to http:// because https://
# is problematic in most setups.
if not hostname.startswith("http://") and not hostname.startswith("https://"):
hostname = "http://%s" % hostname
try:
# ignore_ssl is supported in XenAPI.py 7.2 onward but there
# is no way to tell which version we are using. TypeError will
# be raised if ignore_ssl is not supported. Additionally,
# ignore_ssl requires Python 2.7.9 or newer.
cls._xapi_session = XenAPI.Session(hostname, ignore_ssl=ignore_ssl)
except TypeError:
# Try without ignore_ssl.
cls._xapi_session = XenAPI.Session(hostname)
if not password:
password = ''
try:
cls._xapi_session.login_with_password(username, password, ANSIBLE_VERSION, 'Ansible')
except XenAPI.Failure as f:
module.fail_json(msg="Unable to log on to XenServer at %s as %s: %s" % (hostname, username, f.details))
# Disabling atexit should be used in special cases only.
if disconnect_atexit:
atexit.register(cls._xapi_session.logout)
return cls._xapi_session
class XenServerObject(object):
"""Base class for all XenServer objects.
This class contains active XAPI session reference and common
attributes with useful info about XenServer host/pool.
Attributes:
module: Reference to Ansible module object.
xapi_session: Reference to XAPI session.
pool_ref (str): XAPI reference to a pool currently connected to.
default_sr_ref (str): XAPI reference to a pool default
Storage Repository.
host_ref (str): XAPI rerefence to a host currently connected to.
xenserver_version (list of str): Contains XenServer major and
minor version.
"""
def __init__(self, module):
"""Inits XenServerObject using common module parameters.
Args:
module: Reference to Ansible module object.
"""
if not HAS_XENAPI:
module.fail_json(changed=False, msg="XenAPI.py required for this module! Please download XenServer SDK and copy XenAPI.py to your site-packages.")
if module:
self.module = module
else:
module.fail_json(msg="XenServerObject: Invalid module object passed!")
self.xapi_session = XAPI.connect(module)
try:
self.pool_ref = self.xapi_session.xenapi.pool.get_all()[0]
self.default_sr_ref = self.xapi_session.xenapi.pool.get_default_SR(self.pool_ref)
self.xenserver_version = get_xenserver_version(module)
except XenAPI.Failure as f:
self.module.fail_json(msg="XAPI ERROR: %s" % f.details)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,35 @@
# Copyright: (c) 2018, Bojan Vitnik <bvitnik@mainstream.rs>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
class ModuleDocFragment(object):
# Parameters for XenServer modules
DOCUMENTATION = '''
options:
hostname:
description:
- The hostname or IP address of the XenServer host or XenServer pool master.
- If the value is not specified in the task, the value of environment variable C(XENSERVER_HOST) will be used instead.
required: False
default: 'localhost'
aliases: ['host', 'pool']
username:
description:
- The username to use for connecting to XenServer.
- If the value is not specified in the task, the value of environment variable C(XENSERVER_USER) will be used instead.
required: False
default: 'root'
aliases: ['user', 'admin']
password:
description:
- The password to use for connecting to XenServer.
- If the value is not specified in the task, the value of environment variable C(XENSERVER_PASSWORD) will be used instead.
required: False
aliases: ['pass', 'pwd']
validate_certs:
description:
- Allows connection when SSL certificates are not valid. Set to C(false) when certificates are not trusted.
- If the value is not specified in the task, the value of environment variable C(XENSERVER_VALIDATE_CERTS) will be used instead.
default: 'yes'
type: bool
'''