Mini Shell
"""
This module (mostly) uses the XenAPI to manage Xen virtual machines.
Big fat warning: the XenAPI used in this file is the one bundled with
Xen Source, NOT XenServer nor Xen Cloud Platform. As a matter of fact it
*will* fail under those platforms. From what I've read, little work is needed
to adapt this code to XS/XCP, mostly playing with XenAPI version, but as
XCP is not taking precedence on Xen Source on many platforms, please keep
compatibility in mind.
Useful documentation:
. http://downloads.xen.org/Wiki/XenAPI/xenapi-1.0.6.pdf
. http://docs.vmd.citrix.com/XenServer/6.0.0/1.0/en_gb/api/
. https://github.com/xapi-project/xen-api/tree/master/scripts/examples/python
. http://xenbits.xen.org/gitweb/?p=xen.git;a=tree;f=tools/python/xen/xm;hb=HEAD
"""
import contextlib
import importlib
import os
import sys
import salt.modules.cmdmod
import salt.utils.files
import salt.utils.path
import salt.utils.stringutils
from salt.exceptions import CommandExecutionError
# Define the module's virtual name
__virtualname__ = "virt"
# This module has only been tested on Debian GNU/Linux and NetBSD, it
# probably needs more path appending for other distributions.
# The path to append is the path to python Xen libraries, where resides
# XenAPI.
def _check_xenapi():
if __grains__["os"] == "Debian":
debian_xen_version = "/usr/lib/xen-common/bin/xen-version"
if os.path.isfile(debian_xen_version):
# __salt__ is not available in __virtual__
xenversion = salt.modules.cmdmod._run_quiet(debian_xen_version)
xapipath = f"/usr/lib/xen-{xenversion}/lib/python"
if os.path.isdir(xapipath):
sys.path.append(xapipath)
try:
return importlib.import_module("xen.xm.XenAPI")
except (ImportError, AttributeError):
return False
def __virtual__():
if _check_xenapi() is not False:
return __virtualname__
return (False, "Module xapi: xenapi check failed")
@contextlib.contextmanager
def _get_xapi_session():
"""
Get a session to XenAPI. By default, use the local UNIX socket.
"""
_xenapi = _check_xenapi()
xapi_uri = __salt__["config.option"]("xapi.uri")
xapi_login = __salt__["config.option"]("xapi.login")
xapi_password = __salt__["config.option"]("xapi.password")
if not xapi_uri:
# xend local UNIX socket
xapi_uri = "httpu:///var/run/xend/xen-api.sock"
if not xapi_login:
xapi_login = ""
if not xapi_password:
xapi_password = ""
try:
session = _xenapi.Session(xapi_uri)
session.xenapi.login_with_password(xapi_login, xapi_password)
yield session.xenapi
except Exception: # pylint: disable=broad-except
raise CommandExecutionError("Failed to connect to XenAPI socket.")
finally:
session.xenapi.session.logout()
# Used rectypes (Record types):
#
# host
# host_cpu
# VM
# VIF
# VBD
def _get_xtool():
"""
Internal, returns xl or xm command line path
"""
for xtool in ["xl", "xm"]:
path = salt.utils.path.which(xtool)
if path is not None:
return path
def _get_all(xapi, rectype):
"""
Internal, returns all members of rectype
"""
return getattr(xapi, rectype).get_all()
def _get_label_uuid(xapi, rectype, label):
"""
Internal, returns label's uuid
"""
try:
return getattr(xapi, rectype).get_by_name_label(label)[0]
except Exception: # pylint: disable=broad-except
return False
def _get_record(xapi, rectype, uuid):
"""
Internal, returns a full record for uuid
"""
return getattr(xapi, rectype).get_record(uuid)
def _get_record_by_label(xapi, rectype, label):
"""
Internal, returns a full record for uuid
"""
uuid = _get_label_uuid(xapi, rectype, label)
if uuid is False:
return False
return getattr(xapi, rectype).get_record(uuid)
def _get_metrics_record(xapi, rectype, record):
"""
Internal, returns metrics record for a rectype
"""
metrics_id = record["metrics"]
return getattr(xapi, f"{rectype}_metrics").get_record(metrics_id)
def _get_val(record, keys):
"""
Internal, get value from record
"""
data = record
for key in keys:
if key in data:
data = data[key]
else:
return None
return data
def list_domains():
"""
Return a list of virtual machine names on the minion
CLI Example:
.. code-block:: bash
salt '*' virt.list_domains
"""
with _get_xapi_session() as xapi:
hosts = xapi.VM.get_all()
ret = []
for _host in hosts:
if xapi.VM.get_record(_host)["is_control_domain"] is False:
ret.append(xapi.VM.get_name_label(_host))
return ret
def vm_info(vm_=None):
"""
Return detailed information about the vms.
If you pass a VM name in as an argument then it will return info
for just the named VM, otherwise it will return all VMs.
CLI Example:
.. code-block:: bash
salt '*' virt.vm_info
"""
with _get_xapi_session() as xapi:
def _info(vm_):
vm_rec = _get_record_by_label(xapi, "VM", vm_)
if vm_rec is False:
return False
vm_metrics_rec = _get_metrics_record(xapi, "VM", vm_rec)
return {
"cpu": vm_metrics_rec["VCPUs_number"],
"maxCPU": _get_val(vm_rec, ["VCPUs_max"]),
"cputime": vm_metrics_rec["VCPUs_utilisation"],
"disks": get_disks(vm_),
"nics": get_nics(vm_),
"maxMem": int(_get_val(vm_rec, ["memory_dynamic_max"])),
"mem": int(vm_metrics_rec["memory_actual"]),
"state": _get_val(vm_rec, ["power_state"]),
}
info = {}
if vm_:
ret = _info(vm_)
if ret is not None:
info[vm_] = ret
else:
for vm_ in list_domains():
ret = _info(vm_)
if ret is not None:
info[vm_] = _info(vm_)
return info
def vm_state(vm_=None):
"""
Return list of all the vms and their state.
If you pass a VM name in as an argument then it will return info
for just the named VM, otherwise it will return all VMs.
CLI Example:
.. code-block:: bash
salt '*' virt.vm_state <vm name>
"""
with _get_xapi_session() as xapi:
info = {}
if vm_:
info[vm_] = _get_record_by_label(xapi, "VM", vm_)["power_state"]
return info
for vm_ in list_domains():
info[vm_] = _get_record_by_label(xapi, "VM", vm_)["power_state"]
return info
def node_info():
"""
Return a dict with information about this node
CLI Example:
.. code-block:: bash
salt '*' virt.node_info
"""
with _get_xapi_session() as xapi:
# get node uuid
host_rec = _get_record(xapi, "host", _get_all(xapi, "host")[0])
# get first CPU (likely to be a core) uuid
host_cpu_rec = _get_record(xapi, "host_cpu", host_rec["host_CPUs"][0])
# get related metrics
host_metrics_rec = _get_metrics_record(xapi, "host", host_rec)
# adapted / cleaned up from Xen's xm
def getCpuMhz():
cpu_speeds = [
int(host_cpu_rec["speed"])
for host_cpu_it in host_cpu_rec
if "speed" in host_cpu_it
]
if cpu_speeds:
return sum(cpu_speeds) / len(cpu_speeds)
else:
return 0
def getCpuFeatures():
if host_cpu_rec:
return host_cpu_rec["features"]
def getFreeCpuCount():
cnt = 0
for host_cpu_it in host_cpu_rec:
if not host_cpu_rec["cpu_pool"]:
cnt += 1
return cnt
info = {
"cpucores": _get_val(host_rec, ["cpu_configuration", "nr_cpus"]),
"cpufeatures": getCpuFeatures(),
"cpumhz": getCpuMhz(),
"cpuarch": _get_val(host_rec, ["software_version", "machine"]),
"cputhreads": _get_val(host_rec, ["cpu_configuration", "threads_per_core"]),
"phymemory": int(host_metrics_rec["memory_total"]) / 1024 / 1024,
"cores_per_sockets": _get_val(
host_rec, ["cpu_configuration", "cores_per_socket"]
),
"free_cpus": getFreeCpuCount(),
"free_memory": int(host_metrics_rec["memory_free"]) / 1024 / 1024,
"xen_major": _get_val(host_rec, ["software_version", "xen_major"]),
"xen_minor": _get_val(host_rec, ["software_version", "xen_minor"]),
"xen_extra": _get_val(host_rec, ["software_version", "xen_extra"]),
"xen_caps": " ".join(_get_val(host_rec, ["capabilities"])),
"xen_scheduler": _get_val(host_rec, ["sched_policy"]),
"xen_pagesize": _get_val(host_rec, ["other_config", "xen_pagesize"]),
"platform_params": _get_val(host_rec, ["other_config", "platform_params"]),
"xen_commandline": _get_val(host_rec, ["other_config", "xen_commandline"]),
"xen_changeset": _get_val(host_rec, ["software_version", "xen_changeset"]),
"cc_compiler": _get_val(host_rec, ["software_version", "cc_compiler"]),
"cc_compile_by": _get_val(host_rec, ["software_version", "cc_compile_by"]),
"cc_compile_domain": _get_val(
host_rec, ["software_version", "cc_compile_domain"]
),
"cc_compile_date": _get_val(
host_rec, ["software_version", "cc_compile_date"]
),
"xend_config_format": _get_val(
host_rec, ["software_version", "xend_config_format"]
),
}
return info
def get_nics(vm_):
"""
Return info about the network interfaces of a named vm
CLI Example:
.. code-block:: bash
salt '*' virt.get_nics <vm name>
"""
with _get_xapi_session() as xapi:
nic = {}
vm_rec = _get_record_by_label(xapi, "VM", vm_)
if vm_rec is False:
return False
for vif in vm_rec["VIFs"]:
vif_rec = _get_record(xapi, "VIF", vif)
nic[vif_rec["MAC"]] = {
"mac": vif_rec["MAC"],
"device": vif_rec["device"],
"mtu": vif_rec["MTU"],
}
return nic
def get_macs(vm_):
"""
Return a list off MAC addresses from the named vm
CLI Example:
.. code-block:: bash
salt '*' virt.get_macs <vm name>
"""
macs = []
nics = get_nics(vm_)
if nics is None:
return None
for nic in nics:
macs.append(nic)
return macs
def get_disks(vm_):
"""
Return the disks of a named vm
CLI Example:
.. code-block:: bash
salt '*' virt.get_disks <vm name>
"""
with _get_xapi_session() as xapi:
disk = {}
vm_uuid = _get_label_uuid(xapi, "VM", vm_)
if vm_uuid is False:
return False
for vbd in xapi.VM.get_VBDs(vm_uuid):
dev = xapi.VBD.get_device(vbd)
if not dev:
continue
prop = xapi.VBD.get_runtime_properties(vbd)
disk[dev] = {
"backend": prop["backend"],
"type": prop["device-type"],
"protocol": prop["protocol"],
}
return disk
def setmem(vm_, memory):
"""
Changes the amount of memory allocated to VM.
Memory is to be specified in MB
CLI Example:
.. code-block:: bash
salt '*' virt.setmem myvm 768
"""
with _get_xapi_session() as xapi:
mem_target = int(memory) * 1024 * 1024
vm_uuid = _get_label_uuid(xapi, "VM", vm_)
if vm_uuid is False:
return False
try:
xapi.VM.set_memory_dynamic_max_live(vm_uuid, mem_target)
xapi.VM.set_memory_dynamic_min_live(vm_uuid, mem_target)
return True
except Exception: # pylint: disable=broad-except
return False
def setvcpus(vm_, vcpus):
"""
Changes the amount of vcpus allocated to VM.
vcpus is an int representing the number to be assigned
CLI Example:
.. code-block:: bash
salt '*' virt.setvcpus myvm 2
"""
with _get_xapi_session() as xapi:
vm_uuid = _get_label_uuid(xapi, "VM", vm_)
if vm_uuid is False:
return False
try:
xapi.VM.set_VCPUs_number_live(vm_uuid, vcpus)
return True
except Exception: # pylint: disable=broad-except
return False
def vcpu_pin(vm_, vcpu, cpus):
"""
Set which CPUs a VCPU can use.
CLI Example:
.. code-block:: bash
salt 'foo' virt.vcpu_pin domU-id 2 1
salt 'foo' virt.vcpu_pin domU-id 2 2-6
"""
with _get_xapi_session() as xapi:
vm_uuid = _get_label_uuid(xapi, "VM", vm_)
if vm_uuid is False:
return False
# from xm's main
def cpu_make_map(cpulist):
cpus = []
for c in cpulist.split(","):
if c == "":
continue
if "-" in c:
(x, y) = c.split("-")
for i in range(int(x), int(y) + 1):
cpus.append(int(i))
else:
# remove this element from the list
if c[0] == "^":
cpus = [x for x in cpus if x != int(c[1:])]
else:
cpus.append(int(c))
cpus.sort()
return ",".join(map(str, cpus))
if cpus == "all":
cpumap = cpu_make_map("0-63")
else:
cpumap = cpu_make_map(f"{cpus}")
try:
xapi.VM.add_to_VCPUs_params_live(vm_uuid, f"cpumap{vcpu}", cpumap)
return True
# VM.add_to_VCPUs_params_live() implementation in xend 4.1+ has
# a bug which makes the client call fail.
# That code is accurate for all others XenAPI implementations, but
# for that particular one, fallback to xm / xl instead.
except Exception: # pylint: disable=broad-except
return __salt__["cmd.run"](
f"{_get_xtool()} vcpu-pin {vm_} {vcpu} {cpus}",
python_shell=False,
)
def freemem():
"""
Return an int representing the amount of memory that has not been given
to virtual machines on this node
CLI Example:
.. code-block:: bash
salt '*' virt.freemem
"""
return node_info()["free_memory"]
def freecpu():
"""
Return an int representing the number of unallocated cpus on this
hypervisor
CLI Example:
.. code-block:: bash
salt '*' virt.freecpu
"""
return node_info()["free_cpus"]
def full_info():
"""
Return the node_info, vm_info and freemem
CLI Example:
.. code-block:: bash
salt '*' virt.full_info
"""
return {"node_info": node_info(), "vm_info": vm_info()}
def shutdown(vm_):
"""
Send a soft shutdown signal to the named vm
CLI Example:
.. code-block:: bash
salt '*' virt.shutdown <vm name>
"""
with _get_xapi_session() as xapi:
vm_uuid = _get_label_uuid(xapi, "VM", vm_)
if vm_uuid is False:
return False
try:
xapi.VM.clean_shutdown(vm_uuid)
return True
except Exception: # pylint: disable=broad-except
return False
def pause(vm_):
"""
Pause the named vm
CLI Example:
.. code-block:: bash
salt '*' virt.pause <vm name>
"""
with _get_xapi_session() as xapi:
vm_uuid = _get_label_uuid(xapi, "VM", vm_)
if vm_uuid is False:
return False
try:
xapi.VM.pause(vm_uuid)
return True
except Exception: # pylint: disable=broad-except
return False
def resume(vm_):
"""
Resume the named vm
CLI Example:
.. code-block:: bash
salt '*' virt.resume <vm name>
"""
with _get_xapi_session() as xapi:
vm_uuid = _get_label_uuid(xapi, "VM", vm_)
if vm_uuid is False:
return False
try:
xapi.VM.unpause(vm_uuid)
return True
except Exception: # pylint: disable=broad-except
return False
def start(config_):
"""
Start a defined domain
CLI Example:
.. code-block:: bash
salt '*' virt.start <path to Xen cfg file>
"""
# FIXME / TODO
# This function does NOT use the XenAPI. Instead, it use good old xm / xl.
# On Xen Source, creating a virtual machine using XenAPI is really painful.
# XCP / XS make it really easy using xapi.Async.VM.start instead. Anyone?
return __salt__["cmd.run"](f"{_get_xtool()} create {config_}", python_shell=False)
def reboot(vm_):
"""
Reboot a domain via ACPI request
CLI Example:
.. code-block:: bash
salt '*' virt.reboot <vm name>
"""
with _get_xapi_session() as xapi:
vm_uuid = _get_label_uuid(xapi, "VM", vm_)
if vm_uuid is False:
return False
try:
xapi.VM.clean_reboot(vm_uuid)
return True
except Exception: # pylint: disable=broad-except
return False
def reset(vm_):
"""
Reset a VM by emulating the reset button on a physical machine
CLI Example:
.. code-block:: bash
salt '*' virt.reset <vm name>
"""
with _get_xapi_session() as xapi:
vm_uuid = _get_label_uuid(xapi, "VM", vm_)
if vm_uuid is False:
return False
try:
xapi.VM.hard_reboot(vm_uuid)
return True
except Exception: # pylint: disable=broad-except
return False
def migrate(vm_, target, live=1, port=0, node=-1, ssl=None, change_home_server=0):
"""
Migrates the virtual machine to another hypervisor
CLI Example:
.. code-block:: bash
salt '*' virt.migrate <vm name> <target hypervisor> [live] [port] [node] [ssl] [change_home_server]
Optional values:
live
Use live migration
port
Use a specified port
node
Use specified NUMA node on target
ssl
use ssl connection for migration
change_home_server
change home server for managed domains
"""
with _get_xapi_session() as xapi:
vm_uuid = _get_label_uuid(xapi, "VM", vm_)
if vm_uuid is False:
return False
other_config = {
"port": port,
"node": node,
"ssl": ssl,
"change_home_server": change_home_server,
}
try:
xapi.VM.migrate(vm_uuid, target, bool(live), other_config)
return True
except Exception: # pylint: disable=broad-except
return False
def stop(vm_):
"""
Hard power down the virtual machine, this is equivalent to pulling the
power
CLI Example:
.. code-block:: bash
salt '*' virt.stop <vm name>
"""
with _get_xapi_session() as xapi:
vm_uuid = _get_label_uuid(xapi, "VM", vm_)
if vm_uuid is False:
return False
try:
xapi.VM.hard_shutdown(vm_uuid)
return True
except Exception: # pylint: disable=broad-except
return False
def is_hyper():
"""
Returns a bool whether or not this node is a hypervisor of any kind
CLI Example:
.. code-block:: bash
salt '*' virt.is_hyper
"""
try:
if __grains__["virtual_subtype"] != "Xen Dom0":
return False
except KeyError:
# virtual_subtype isn't set everywhere.
return False
try:
with salt.utils.files.fopen("/proc/modules") as fp_:
if "xen_" not in salt.utils.stringutils.to_unicode(fp_.read()):
return False
except OSError:
return False
# there must be a smarter way...
return "xenstore" in __salt__["cmd.run"](__grains__["ps"])
def vm_cputime(vm_=None):
"""
Return cputime used by the vms on this hyper in a
list of dicts:
.. code-block:: python
[
'your-vm': {
'cputime' <int>
'cputime_percent' <int>
},
...
]
If you pass a VM name in as an argument then it will return info
for just the named VM, otherwise it will return all VMs.
CLI Example:
.. code-block:: bash
salt '*' virt.vm_cputime
"""
with _get_xapi_session() as xapi:
def _info(vm_):
host_rec = _get_record_by_label(xapi, "VM", vm_)
host_cpus = len(host_rec["host_CPUs"])
if host_rec is False:
return False
host_metrics = _get_metrics_record(xapi, "VM", host_rec)
vcpus = int(host_metrics["VCPUs_number"])
cputime = int(host_metrics["VCPUs_utilisation"]["0"])
cputime_percent = 0
if cputime:
# Divide by vcpus to always return a number between 0 and 100
cputime_percent = (1.0e-7 * cputime / host_cpus) / vcpus
return {
"cputime": int(cputime),
"cputime_percent": int(f"{cputime_percent:.0f}"),
}
info = {}
if vm_:
info[vm_] = _info(vm_)
return info
for vm_ in list_domains():
info[vm_] = _info(vm_)
return info
def vm_netstats(vm_=None):
"""
Return combined network counters used by the vms on this hyper in a
list of dicts:
.. code-block:: python
[
'your-vm': {
'io_read_kbs' : 0,
'io_total_read_kbs' : 0,
'io_total_write_kbs' : 0,
'io_write_kbs' : 0
},
...
]
If you pass a VM name in as an argument then it will return info
for just the named VM, otherwise it will return all VMs.
CLI Example:
.. code-block:: bash
salt '*' virt.vm_netstats
"""
with _get_xapi_session() as xapi:
def _info(vm_):
ret = {}
vm_rec = _get_record_by_label(xapi, "VM", vm_)
if vm_rec is False:
return False
for vif in vm_rec["VIFs"]:
vif_rec = _get_record(xapi, "VIF", vif)
ret[vif_rec["device"]] = _get_metrics_record(xapi, "VIF", vif_rec)
del ret[vif_rec["device"]]["last_updated"]
return ret
info = {}
if vm_:
info[vm_] = _info(vm_)
else:
for vm_ in list_domains():
info[vm_] = _info(vm_)
return info
def vm_diskstats(vm_=None):
"""
Return disk usage counters used by the vms on this hyper in a
list of dicts:
.. code-block:: python
[
'your-vm': {
'io_read_kbs' : 0,
'io_write_kbs' : 0
},
...
]
If you pass a VM name in as an argument then it will return info
for just the named VM, otherwise it will return all VMs.
CLI Example:
.. code-block:: bash
salt '*' virt.vm_diskstats
"""
with _get_xapi_session() as xapi:
def _info(vm_):
ret = {}
vm_uuid = _get_label_uuid(xapi, "VM", vm_)
if vm_uuid is False:
return False
for vbd in xapi.VM.get_VBDs(vm_uuid):
vbd_rec = _get_record(xapi, "VBD", vbd)
ret[vbd_rec["device"]] = _get_metrics_record(xapi, "VBD", vbd_rec)
del ret[vbd_rec["device"]]["last_updated"]
return ret
info = {}
if vm_:
info[vm_] = _info(vm_)
else:
for vm_ in list_domains():
info[vm_] = _info(vm_)
return info
Zerion Mini Shell 1.0