diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:04:41 +0000 |
commit | 975f66f2eebe9dadba04f275774d4ab83f74cf25 (patch) | |
tree | 89bd26a93aaae6a25749145b7e4bca4a1e75b2be /ansible_collections/community/libvirt/plugins/modules/virt.py | |
parent | Initial commit. (diff) | |
download | ansible-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.py | 563 |
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() |