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/cloud/misc/virt.py

603 lines
17 KiB
Python
Raw Normal View History

2020-03-09 10:11:07 +01:00
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2007, 2012 Red Hat, Inc
# Michael DeHaan <michael.dehaan@gmail.com>
# Seth Vidal <skvidal@fedoraproject.org>
# 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
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: virt
short_description: Manages virtual machines supported by libvirt
description:
- Manages virtual machines supported by I(libvirt).
options:
name:
description:
- name of the guest VM being managed. Note that VM must be previously
defined with xml.
- This option is required unless I(command) is C(list_vms) or C(info).
state:
description:
- Note that there may be some lag for state requests like C(shutdown)
since these refer only to VM states. After starting a guest, it may not
be immediately accessible.
state and command are mutually exclusive except when command=list_vms. In
this case all VMs in specified state will be listed.
choices: [ destroyed, paused, running, shutdown ]
command:
description:
- In addition to state management, various non-idempotent commands are available.
choices: [ create, define, destroy, freemem, get_xml, info, list_vms, nodeinfo, pause, shutdown, start, status, stop, undefine, unpause, virttype ]
autostart:
description:
- start VM at host startup.
type: bool
uri:
description:
- libvirt connection uri.
default: qemu:///system
xml:
description:
- XML document used with the define command.
- Must be raw XML content using C(lookup). XML cannot be reference to a file.
requirements:
- python >= 2.6
- libvirt-python
author:
- Ansible Core Team
- Michael DeHaan
- Seth Vidal (@skvidal)
'''
EXAMPLES = '''
# a playbook task line:
- virt:
name: alpha
state: running
# /usr/bin/ansible invocations
# ansible host -m virt -a "name=alpha command=status"
# ansible host -m virt -a "name=alpha command=get_xml"
# ansible host -m virt -a "name=alpha command=create uri=lxc:///"
# defining and launching an LXC guest
- name: define vm
virt:
command: define
xml: "{{ lookup('template', 'container-template.xml.j2') }}"
uri: 'lxc:///'
- name: start vm
virt:
name: foo
state: running
uri: 'lxc:///'
# setting autostart on a qemu VM (default uri)
- name: set autostart for a VM
virt:
name: foo
autostart: yes
# Defining a VM and making is autostart with host. VM will be off after this task
- name: define vm from xml and set autostart
virt:
command: define
xml: "{{ lookup('template', 'vm_template.xml.j2') }}"
autostart: yes
# Listing VMs
- name: list all VMs
virt:
command: list_vms
register: all_vms
- name: list only running VMs
virt:
command: list_vms
state: running
register: running_vms
'''
RETURN = '''
# for list_vms command
list_vms:
description: The list of vms defined on the remote system
type: list
returned: success
sample: [
"build.example.org",
"dev.example.org"
]
# for status command
status:
description: The status of the VM, among running, crashed, paused and shutdown
type: str
sample: "success"
returned: success
'''
import traceback
try:
import libvirt
from libvirt import libvirtError
except ImportError:
HAS_VIRT = False
else:
HAS_VIRT = True
import re
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
VIRT_FAILED = 1
VIRT_SUCCESS = 0
VIRT_UNAVAILABLE = 2
ALL_COMMANDS = []
VM_COMMANDS = ['create', 'define', 'destroy', 'get_xml', 'pause', 'shutdown', 'status', 'start', 'stop', 'undefine', 'unpause']
HOST_COMMANDS = ['freemem', 'info', 'list_vms', 'nodeinfo', 'virttype']
ALL_COMMANDS.extend(VM_COMMANDS)
ALL_COMMANDS.extend(HOST_COMMANDS)
VIRT_STATE_NAME_MAP = {
0: 'running',
1: 'running',
2: 'running',
3: 'paused',
4: 'shutdown',
5: 'shutdown',
6: 'crashed',
}
class VMNotFound(Exception):
pass
class LibvirtConnection(object):
def __init__(self, uri, module):
self.module = module
cmd = "uname -r"
rc, stdout, stderr = self.module.run_command(cmd)
if "xen" in stdout:
conn = libvirt.open(None)
elif "esx" in uri:
auth = [[libvirt.VIR_CRED_AUTHNAME, libvirt.VIR_CRED_NOECHOPROMPT], [], None]
conn = libvirt.openAuth(uri, auth)
else:
conn = libvirt.open(uri)
if not conn:
raise Exception("hypervisor connection failure")
self.conn = conn
def find_vm(self, vmid):
"""
Extra bonus feature: vmid = -1 returns a list of everything
"""
conn = self.conn
vms = []
# this block of code borrowed from virt-manager:
# get working domain's name
ids = conn.listDomainsID()
for id in ids:
vm = conn.lookupByID(id)
vms.append(vm)
# get defined domain
names = conn.listDefinedDomains()
for name in names:
vm = conn.lookupByName(name)
vms.append(vm)
if vmid == -1:
return vms
for vm in vms:
if vm.name() == vmid:
return vm
raise VMNotFound("virtual machine %s not found" % vmid)
def shutdown(self, vmid):
return self.find_vm(vmid).shutdown()
def pause(self, vmid):
return self.suspend(vmid)
def unpause(self, vmid):
return self.resume(vmid)
def suspend(self, vmid):
return self.find_vm(vmid).suspend()
def resume(self, vmid):
return self.find_vm(vmid).resume()
def create(self, vmid):
return self.find_vm(vmid).create()
def destroy(self, vmid):
return self.find_vm(vmid).destroy()
def undefine(self, vmid):
return self.find_vm(vmid).undefine()
def get_status2(self, vm):
state = vm.info()[0]
return VIRT_STATE_NAME_MAP.get(state, "unknown")
def get_status(self, vmid):
state = self.find_vm(vmid).info()[0]
return VIRT_STATE_NAME_MAP.get(state, "unknown")
def nodeinfo(self):
return self.conn.getInfo()
def get_type(self):
return self.conn.getType()
def get_xml(self, vmid):
vm = self.conn.lookupByName(vmid)
return vm.XMLDesc(0)
def get_maxVcpus(self, vmid):
vm = self.conn.lookupByName(vmid)
return vm.maxVcpus()
def get_maxMemory(self, vmid):
vm = self.conn.lookupByName(vmid)
return vm.maxMemory()
def getFreeMemory(self):
return self.conn.getFreeMemory()
def get_autostart(self, vmid):
vm = self.conn.lookupByName(vmid)
return vm.autostart()
def set_autostart(self, vmid, val):
vm = self.conn.lookupByName(vmid)
return vm.setAutostart(val)
def define_from_xml(self, xml):
return self.conn.defineXML(xml)
class Virt(object):
def __init__(self, uri, module):
self.module = module
self.uri = uri
def __get_conn(self):
self.conn = LibvirtConnection(self.uri, self.module)
return self.conn
def get_vm(self, vmid):
self.__get_conn()
return self.conn.find_vm(vmid)
def state(self):
vms = self.list_vms()
state = []
for vm in vms:
state_blurb = self.conn.get_status(vm)
state.append("%s %s" % (vm, state_blurb))
return state
def info(self):
vms = self.list_vms()
info = dict()
for vm in vms:
data = self.conn.find_vm(vm).info()
# libvirt returns maxMem, memory, and cpuTime as long()'s, which
# xmlrpclib tries to convert to regular int's during serialization.
# This throws exceptions, so convert them to strings here and
# assume the other end of the xmlrpc connection can figure things
# out or doesn't care.
info[vm] = dict(
state=VIRT_STATE_NAME_MAP.get(data[0], "unknown"),
maxMem=str(data[1]),
memory=str(data[2]),
nrVirtCpu=data[3],
cpuTime=str(data[4]),
autostart=self.conn.get_autostart(vm),
)
return info
def nodeinfo(self):
self.__get_conn()
data = self.conn.nodeinfo()
info = dict(
cpumodel=str(data[0]),
phymemory=str(data[1]),
cpus=str(data[2]),
cpumhz=str(data[3]),
numanodes=str(data[4]),
sockets=str(data[5]),
cpucores=str(data[6]),
cputhreads=str(data[7])
)
return info
def list_vms(self, state=None):
self.conn = self.__get_conn()
vms = self.conn.find_vm(-1)
results = []
for x in vms:
try:
if state:
vmstate = self.conn.get_status2(x)
if vmstate == state:
results.append(x.name())
else:
results.append(x.name())
except Exception:
pass
return results
def virttype(self):
return self.__get_conn().get_type()
def autostart(self, vmid, as_flag):
self.conn = self.__get_conn()
# Change autostart flag only if needed
if self.conn.get_autostart(vmid) != as_flag:
self.conn.set_autostart(vmid, as_flag)
return True
return False
def freemem(self):
self.conn = self.__get_conn()
return self.conn.getFreeMemory()
def shutdown(self, vmid):
""" Make the machine with the given vmid stop running. Whatever that takes. """
self.__get_conn()
self.conn.shutdown(vmid)
return 0
def pause(self, vmid):
""" Pause the machine with the given vmid. """
self.__get_conn()
return self.conn.suspend(vmid)
def unpause(self, vmid):
""" Unpause the machine with the given vmid. """
self.__get_conn()
return self.conn.resume(vmid)
def create(self, vmid):
""" Start the machine via the given vmid """
self.__get_conn()
return self.conn.create(vmid)
def start(self, vmid):
""" Start the machine via the given id/name """
self.__get_conn()
return self.conn.create(vmid)
def destroy(self, vmid):
""" Pull the virtual power from the virtual domain, giving it virtually no time to virtually shut down. """
self.__get_conn()
return self.conn.destroy(vmid)
def undefine(self, vmid):
""" Stop a domain, and then wipe it from the face of the earth. (delete disk/config file) """
self.__get_conn()
return self.conn.undefine(vmid)
def status(self, vmid):
"""
Return a state suitable for server consumption. Aka, codes.py values, not XM output.
"""
self.__get_conn()
return self.conn.get_status(vmid)
def get_xml(self, vmid):
"""
Receive a Vm id as input
Return an xml describing vm config returned by a libvirt call
"""
self.__get_conn()
return self.conn.get_xml(vmid)
def get_maxVcpus(self, vmid):
"""
Gets the max number of VCPUs on a guest
"""
self.__get_conn()
return self.conn.get_maxVcpus(vmid)
def get_max_memory(self, vmid):
"""
Gets the max memory on a guest
"""
self.__get_conn()
return self.conn.get_MaxMemory(vmid)
def define(self, xml):
"""
Define a guest with the given xml
"""
self.__get_conn()
return self.conn.define_from_xml(xml)
def core(module):
state = module.params.get('state', None)
autostart = module.params.get('autostart', None)
guest = module.params.get('name', None)
command = module.params.get('command', None)
uri = module.params.get('uri', None)
xml = module.params.get('xml', None)
v = Virt(uri, module)
res = dict()
if state and command == 'list_vms':
res = v.list_vms(state=state)
if not isinstance(res, dict):
res = {command: res}
return VIRT_SUCCESS, res
if autostart is not None and command != 'define':
if not guest:
module.fail_json(msg="autostart requires 1 argument: name")
try:
v.get_vm(guest)
except VMNotFound:
module.fail_json(msg="domain %s not found" % guest)
res['changed'] = v.autostart(guest, autostart)
if not command and not state:
return VIRT_SUCCESS, res
if state:
if not guest:
module.fail_json(msg="state change requires a guest specified")
if state == 'running':
if v.status(guest) == 'paused':
res['changed'] = True
res['msg'] = v.unpause(guest)
elif v.status(guest) != 'running':
res['changed'] = True
res['msg'] = v.start(guest)
elif state == 'shutdown':
if v.status(guest) != 'shutdown':
res['changed'] = True
res['msg'] = v.shutdown(guest)
elif state == 'destroyed':
if v.status(guest) != 'shutdown':
res['changed'] = True
res['msg'] = v.destroy(guest)
elif state == 'paused':
if v.status(guest) == 'running':
res['changed'] = True
res['msg'] = v.pause(guest)
else:
module.fail_json(msg="unexpected state")
return VIRT_SUCCESS, res
if command:
if command in VM_COMMANDS:
if command == 'define':
if not xml:
module.fail_json(msg="define requires xml argument")
if guest:
# there might be a mismatch between quest 'name' in the module and in the xml
module.warn("'xml' is given - ignoring 'name'")
found_name = re.search('<name>(.*)</name>', xml).groups()
if found_name:
domain_name = found_name[0]
else:
module.fail_json(msg="Could not find domain 'name' in xml")
# From libvirt docs (https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainDefineXML):
# -- A previous definition for this domain would be overridden if it already exists.
#
# In real world testing with libvirt versions 1.2.17-13, 2.0.0-10 and 3.9.0-14
# on qemu and lxc domains results in:
# operation failed: domain '<name>' already exists with <uuid>
#
# In case a domain would be indeed overwritten, we should protect idempotency:
try:
existing_domain = v.get_vm(domain_name)
except VMNotFound:
existing_domain = None
try:
domain = v.define(xml)
if existing_domain:
# if we are here, then libvirt redefined existing domain as the doc promised
if existing_domain.XMLDesc() != domain.XMLDesc():
res = {'changed': True, 'change_reason': 'config changed'}
else:
res = {'changed': True, 'created': domain.name()}
except libvirtError as e:
if e.get_error_code() != 9: # 9 means 'domain already exists' error
module.fail_json(msg='libvirtError: %s' % e.message)
if autostart is not None and v.autostart(domain_name, autostart):
res = {'changed': True, 'change_reason': 'autostart'}
elif not guest:
module.fail_json(msg="%s requires 1 argument: guest" % command)
else:
res = getattr(v, command)(guest)
if not isinstance(res, dict):
res = {command: res}
return VIRT_SUCCESS, res
elif hasattr(v, command):
res = getattr(v, command)()
if not isinstance(res, dict):
res = {command: res}
return VIRT_SUCCESS, res
else:
module.fail_json(msg="Command %s not recognized" % command)
module.fail_json(msg="expected state or command parameter to be specified")
def main():
module = AnsibleModule(
argument_spec=dict(
name=dict(type='str', aliases=['guest']),
state=dict(type='str', choices=['destroyed', 'pause', 'running', 'shutdown']),
autostart=dict(type='bool'),
command=dict(type='str', choices=ALL_COMMANDS),
uri=dict(type='str', default='qemu:///system'),
xml=dict(type='str'),
),
)
if not HAS_VIRT:
module.fail_json(msg='The `libvirt` module is not importable. Check the requirements.')
rc = VIRT_SUCCESS
try:
rc, result = core(module)
except Exception as e:
module.fail_json(msg=to_native(e), exception=traceback.format_exc())
if rc != 0: # something went wrong emit the msg
module.fail_json(rc=rc, msg=result)
else:
module.exit_json(**result)
if __name__ == '__main__':
main()