summaryrefslogtreecommitdiffstats
path: root/ansible_collections/community/libvirt/plugins/modules/virt.py
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 12:04:41 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 12:04:41 +0000
commit975f66f2eebe9dadba04f275774d4ab83f74cf25 (patch)
tree89bd26a93aaae6a25749145b7e4bca4a1e75b2be /ansible_collections/community/libvirt/plugins/modules/virt.py
parentInitial commit. (diff)
downloadansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.tar.xz
ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.zip
Adding upstream version 7.7.0+dfsg.upstream/7.7.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/community/libvirt/plugins/modules/virt.py')
-rw-r--r--ansible_collections/community/libvirt/plugins/modules/virt.py563
1 files changed, 563 insertions, 0 deletions
diff --git a/ansible_collections/community/libvirt/plugins/modules/virt.py b/ansible_collections/community/libvirt/plugins/modules/virt.py
new file mode 100644
index 000000000..bbc2add06
--- /dev/null
+++ b/ansible_collections/community/libvirt/plugins/modules/virt.py
@@ -0,0 +1,563 @@
+#!/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
+
+
+DOCUMENTATION = '''
+---
+module: virt
+short_description: Manages virtual machines supported by libvirt
+description:
+ - Manages virtual machines supported by I(libvirt).
+extends_documentation_fragment:
+ - community.libvirt.virt.options_uri
+ - community.libvirt.virt.options_xml
+ - community.libvirt.virt.options_guest
+ - community.libvirt.virt.options_autostart
+ - community.libvirt.virt.options_state
+ - community.libvirt.virt.options_command
+ - community.libvirt.requirements
+author:
+ - Ansible Core Team
+ - Michael DeHaan
+ - Seth Vidal (@skvidal)
+'''
+
+EXAMPLES = '''
+# a playbook task line:
+- name: Start a VM
+ community.libvirt.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 a VM
+ community.libvirt.virt:
+ command: define
+ xml: "{{ lookup('template', 'container-template.xml.j2') }}"
+ uri: 'lxc:///'
+- name: start vm
+ community.libvirt.virt:
+ name: foo
+ state: running
+ uri: 'lxc:///'
+
+# setting autostart on a qemu VM (default uri)
+- name: Set autostart for a VM
+ community.libvirt.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
+ community.libvirt.virt:
+ command: define
+ xml: "{{ lookup('template', 'vm_template.xml.j2') }}"
+ autostart: yes
+
+# Listing VMs
+- name: List all VMs
+ community.libvirt.virt:
+ command: list_vms
+ register: all_vms
+
+- name: List only running VMs
+ community.libvirt.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
+ """
+
+ vms = self.conn.listAllDomains()
+
+ 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'")
+ try:
+ domain_name = re.search('<name>(.*)</name>', xml).groups()[0]
+ except AttributeError:
+ 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_xml = v.get_vm(domain_name).XMLDesc(
+ libvirt.VIR_DOMAIN_XML_INACTIVE
+ )
+ except VMNotFound:
+ existing_domain_xml = None
+ try:
+ domain = v.define(xml)
+ if existing_domain_xml:
+ # if we are here, then libvirt redefined existing domain as the doc promised
+ if existing_domain_xml != domain.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE):
+ 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.get_error_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', 'paused', '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()