1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3
4# Copyright: (c) 2007, 2012 Red Hat, Inc
5# Michael DeHaan <michael.dehaan@gmail.com>
6# Seth Vidal <skvidal@fedoraproject.org>
7# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
8
9from __future__ import absolute_import, division, print_function
10__metaclass__ = type
11
12
13DOCUMENTATION = '''
14---
15module: virt
16short_description: Manages virtual machines supported by libvirt
17description:
18     - Manages virtual machines supported by I(libvirt).
19options:
20  name:
21    description:
22      - name of the guest VM being managed. Note that VM must be previously
23        defined with xml.
24      - This option is required unless I(command) is C(list_vms) or C(info).
25    type: str
26    aliases:
27      - guest
28  state:
29    description:
30      - Note that there may be some lag for state requests like C(shutdown)
31        since these refer only to VM states. After starting a guest, it may not
32        be immediately accessible.
33        state and command are mutually exclusive except when command=list_vms. In
34        this case all VMs in specified state will be listed.
35    choices: [ destroyed, paused, running, shutdown ]
36    type: str
37  command:
38    description:
39      - In addition to state management, various non-idempotent commands are available.
40    choices: [ create, define, destroy, freemem, get_xml, info, list_vms, nodeinfo, pause, shutdown, start, status, stop, undefine, unpause, virttype ]
41    type: str
42  autostart:
43    description:
44      - start VM at host startup.
45    type: bool
46  uri:
47    description:
48      - libvirt connection uri.
49    default: qemu:///system
50    type: str
51  xml:
52    description:
53      - XML document used with the define command.
54      - Must be raw XML content using C(lookup). XML cannot be reference to a file.
55    type: str
56requirements:
57    - python >= 2.6
58    - libvirt-python
59author:
60    - Ansible Core Team
61    - Michael DeHaan
62    - Seth Vidal (@skvidal)
63'''
64
65EXAMPLES = '''
66# a playbook task line:
67- community.libvirt.virt:
68    name: alpha
69    state: running
70
71# /usr/bin/ansible invocations
72# ansible host -m virt -a "name=alpha command=status"
73# ansible host -m virt -a "name=alpha command=get_xml"
74# ansible host -m virt -a "name=alpha command=create uri=lxc:///"
75
76# defining and launching an LXC guest
77- name: define vm
78  community.libvirt.virt:
79    command: define
80    xml: "{{ lookup('template', 'container-template.xml.j2') }}"
81    uri: 'lxc:///'
82- name: start vm
83  community.libvirt.virt:
84    name: foo
85    state: running
86    uri: 'lxc:///'
87
88# setting autostart on a qemu VM (default uri)
89- name: set autostart for a VM
90  community.libvirt.virt:
91    name: foo
92    autostart: yes
93
94# Defining a VM and making is autostart with host. VM will be off after this task
95- name: define vm from xml and set autostart
96  community.libvirt.virt:
97    command: define
98    xml: "{{ lookup('template', 'vm_template.xml.j2') }}"
99    autostart: yes
100
101# Listing VMs
102- name: list all VMs
103  community.libvirt.virt:
104    command: list_vms
105  register: all_vms
106
107- name: list only running VMs
108  community.libvirt.virt:
109    command: list_vms
110    state: running
111  register: running_vms
112'''
113
114RETURN = '''
115# for list_vms command
116list_vms:
117    description: The list of vms defined on the remote system
118    type: list
119    returned: success
120    sample: [
121        "build.example.org",
122        "dev.example.org"
123    ]
124# for status command
125status:
126    description: The status of the VM, among running, crashed, paused and shutdown
127    type: str
128    sample: "success"
129    returned: success
130'''
131
132import traceback
133
134try:
135    import libvirt
136    from libvirt import libvirtError
137except ImportError:
138    HAS_VIRT = False
139else:
140    HAS_VIRT = True
141
142import re
143
144from ansible.module_utils.basic import AnsibleModule
145from ansible.module_utils._text import to_native
146
147
148VIRT_FAILED = 1
149VIRT_SUCCESS = 0
150VIRT_UNAVAILABLE = 2
151
152ALL_COMMANDS = []
153VM_COMMANDS = ['create', 'define', 'destroy', 'get_xml', 'pause', 'shutdown', 'status', 'start', 'stop', 'undefine', 'unpause']
154HOST_COMMANDS = ['freemem', 'info', 'list_vms', 'nodeinfo', 'virttype']
155ALL_COMMANDS.extend(VM_COMMANDS)
156ALL_COMMANDS.extend(HOST_COMMANDS)
157
158VIRT_STATE_NAME_MAP = {
159    0: 'running',
160    1: 'running',
161    2: 'running',
162    3: 'paused',
163    4: 'shutdown',
164    5: 'shutdown',
165    6: 'crashed',
166}
167
168
169class VMNotFound(Exception):
170    pass
171
172
173class LibvirtConnection(object):
174
175    def __init__(self, uri, module):
176
177        self.module = module
178
179        cmd = "uname -r"
180        rc, stdout, stderr = self.module.run_command(cmd)
181
182        if "xen" in stdout:
183            conn = libvirt.open(None)
184        elif "esx" in uri:
185            auth = [[libvirt.VIR_CRED_AUTHNAME, libvirt.VIR_CRED_NOECHOPROMPT], [], None]
186            conn = libvirt.openAuth(uri, auth)
187        else:
188            conn = libvirt.open(uri)
189
190        if not conn:
191            raise Exception("hypervisor connection failure")
192
193        self.conn = conn
194
195    def find_vm(self, vmid):
196        """
197        Extra bonus feature: vmid = -1 returns a list of everything
198        """
199        conn = self.conn
200
201        vms = []
202
203        # this block of code borrowed from virt-manager:
204        # get working domain's name
205        ids = conn.listDomainsID()
206        for id in ids:
207            vm = conn.lookupByID(id)
208            vms.append(vm)
209        # get defined domain
210        names = conn.listDefinedDomains()
211        for name in names:
212            vm = conn.lookupByName(name)
213            vms.append(vm)
214
215        if vmid == -1:
216            return vms
217
218        for vm in vms:
219            if vm.name() == vmid:
220                return vm
221
222        raise VMNotFound("virtual machine %s not found" % vmid)
223
224    def shutdown(self, vmid):
225        return self.find_vm(vmid).shutdown()
226
227    def pause(self, vmid):
228        return self.suspend(vmid)
229
230    def unpause(self, vmid):
231        return self.resume(vmid)
232
233    def suspend(self, vmid):
234        return self.find_vm(vmid).suspend()
235
236    def resume(self, vmid):
237        return self.find_vm(vmid).resume()
238
239    def create(self, vmid):
240        return self.find_vm(vmid).create()
241
242    def destroy(self, vmid):
243        return self.find_vm(vmid).destroy()
244
245    def undefine(self, vmid):
246        return self.find_vm(vmid).undefine()
247
248    def get_status2(self, vm):
249        state = vm.info()[0]
250        return VIRT_STATE_NAME_MAP.get(state, "unknown")
251
252    def get_status(self, vmid):
253        state = self.find_vm(vmid).info()[0]
254        return VIRT_STATE_NAME_MAP.get(state, "unknown")
255
256    def nodeinfo(self):
257        return self.conn.getInfo()
258
259    def get_type(self):
260        return self.conn.getType()
261
262    def get_xml(self, vmid):
263        vm = self.conn.lookupByName(vmid)
264        return vm.XMLDesc(0)
265
266    def get_maxVcpus(self, vmid):
267        vm = self.conn.lookupByName(vmid)
268        return vm.maxVcpus()
269
270    def get_maxMemory(self, vmid):
271        vm = self.conn.lookupByName(vmid)
272        return vm.maxMemory()
273
274    def getFreeMemory(self):
275        return self.conn.getFreeMemory()
276
277    def get_autostart(self, vmid):
278        vm = self.conn.lookupByName(vmid)
279        return vm.autostart()
280
281    def set_autostart(self, vmid, val):
282        vm = self.conn.lookupByName(vmid)
283        return vm.setAutostart(val)
284
285    def define_from_xml(self, xml):
286        return self.conn.defineXML(xml)
287
288
289class Virt(object):
290
291    def __init__(self, uri, module):
292        self.module = module
293        self.uri = uri
294
295    def __get_conn(self):
296        self.conn = LibvirtConnection(self.uri, self.module)
297        return self.conn
298
299    def get_vm(self, vmid):
300        self.__get_conn()
301        return self.conn.find_vm(vmid)
302
303    def state(self):
304        vms = self.list_vms()
305        state = []
306        for vm in vms:
307            state_blurb = self.conn.get_status(vm)
308            state.append("%s %s" % (vm, state_blurb))
309        return state
310
311    def info(self):
312        vms = self.list_vms()
313        info = dict()
314        for vm in vms:
315            data = self.conn.find_vm(vm).info()
316            # libvirt returns maxMem, memory, and cpuTime as long()'s, which
317            # xmlrpclib tries to convert to regular int's during serialization.
318            # This throws exceptions, so convert them to strings here and
319            # assume the other end of the xmlrpc connection can figure things
320            # out or doesn't care.
321            info[vm] = dict(
322                state=VIRT_STATE_NAME_MAP.get(data[0], "unknown"),
323                maxMem=str(data[1]),
324                memory=str(data[2]),
325                nrVirtCpu=data[3],
326                cpuTime=str(data[4]),
327                autostart=self.conn.get_autostart(vm),
328            )
329
330        return info
331
332    def nodeinfo(self):
333        self.__get_conn()
334        data = self.conn.nodeinfo()
335        info = dict(
336            cpumodel=str(data[0]),
337            phymemory=str(data[1]),
338            cpus=str(data[2]),
339            cpumhz=str(data[3]),
340            numanodes=str(data[4]),
341            sockets=str(data[5]),
342            cpucores=str(data[6]),
343            cputhreads=str(data[7])
344        )
345        return info
346
347    def list_vms(self, state=None):
348        self.conn = self.__get_conn()
349        vms = self.conn.find_vm(-1)
350        results = []
351        for x in vms:
352            try:
353                if state:
354                    vmstate = self.conn.get_status2(x)
355                    if vmstate == state:
356                        results.append(x.name())
357                else:
358                    results.append(x.name())
359            except Exception:
360                pass
361        return results
362
363    def virttype(self):
364        return self.__get_conn().get_type()
365
366    def autostart(self, vmid, as_flag):
367        self.conn = self.__get_conn()
368        # Change autostart flag only if needed
369        if self.conn.get_autostart(vmid) != as_flag:
370            self.conn.set_autostart(vmid, as_flag)
371            return True
372
373        return False
374
375    def freemem(self):
376        self.conn = self.__get_conn()
377        return self.conn.getFreeMemory()
378
379    def shutdown(self, vmid):
380        """ Make the machine with the given vmid stop running.  Whatever that takes.  """
381        self.__get_conn()
382        self.conn.shutdown(vmid)
383        return 0
384
385    def pause(self, vmid):
386        """ Pause the machine with the given vmid.  """
387
388        self.__get_conn()
389        return self.conn.suspend(vmid)
390
391    def unpause(self, vmid):
392        """ Unpause the machine with the given vmid.  """
393
394        self.__get_conn()
395        return self.conn.resume(vmid)
396
397    def create(self, vmid):
398        """ Start the machine via the given vmid """
399
400        self.__get_conn()
401        return self.conn.create(vmid)
402
403    def start(self, vmid):
404        """ Start the machine via the given id/name """
405
406        self.__get_conn()
407        return self.conn.create(vmid)
408
409    def destroy(self, vmid):
410        """ Pull the virtual power from the virtual domain, giving it virtually no time to virtually shut down.  """
411        self.__get_conn()
412        return self.conn.destroy(vmid)
413
414    def undefine(self, vmid):
415        """ Stop a domain, and then wipe it from the face of the earth.  (delete disk/config file) """
416
417        self.__get_conn()
418        return self.conn.undefine(vmid)
419
420    def status(self, vmid):
421        """
422        Return a state suitable for server consumption.  Aka, codes.py values, not XM output.
423        """
424        self.__get_conn()
425        return self.conn.get_status(vmid)
426
427    def get_xml(self, vmid):
428        """
429        Receive a Vm id as input
430        Return an xml describing vm config returned by a libvirt call
431        """
432
433        self.__get_conn()
434        return self.conn.get_xml(vmid)
435
436    def get_maxVcpus(self, vmid):
437        """
438        Gets the max number of VCPUs on a guest
439        """
440
441        self.__get_conn()
442        return self.conn.get_maxVcpus(vmid)
443
444    def get_max_memory(self, vmid):
445        """
446        Gets the max memory on a guest
447        """
448
449        self.__get_conn()
450        return self.conn.get_MaxMemory(vmid)
451
452    def define(self, xml):
453        """
454        Define a guest with the given xml
455        """
456        self.__get_conn()
457        return self.conn.define_from_xml(xml)
458
459
460def core(module):
461
462    state = module.params.get('state', None)
463    autostart = module.params.get('autostart', None)
464    guest = module.params.get('name', None)
465    command = module.params.get('command', None)
466    uri = module.params.get('uri', None)
467    xml = module.params.get('xml', None)
468
469    v = Virt(uri, module)
470    res = dict()
471
472    if state and command == 'list_vms':
473        res = v.list_vms(state=state)
474        if not isinstance(res, dict):
475            res = {command: res}
476        return VIRT_SUCCESS, res
477
478    if autostart is not None and command != 'define':
479        if not guest:
480            module.fail_json(msg="autostart requires 1 argument: name")
481        try:
482            v.get_vm(guest)
483        except VMNotFound:
484            module.fail_json(msg="domain %s not found" % guest)
485        res['changed'] = v.autostart(guest, autostart)
486        if not command and not state:
487            return VIRT_SUCCESS, res
488
489    if state:
490        if not guest:
491            module.fail_json(msg="state change requires a guest specified")
492
493        if state == 'running':
494            if v.status(guest) == 'paused':
495                res['changed'] = True
496                res['msg'] = v.unpause(guest)
497            elif v.status(guest) != 'running':
498                res['changed'] = True
499                res['msg'] = v.start(guest)
500        elif state == 'shutdown':
501            if v.status(guest) != 'shutdown':
502                res['changed'] = True
503                res['msg'] = v.shutdown(guest)
504        elif state == 'destroyed':
505            if v.status(guest) != 'shutdown':
506                res['changed'] = True
507                res['msg'] = v.destroy(guest)
508        elif state == 'paused':
509            if v.status(guest) == 'running':
510                res['changed'] = True
511                res['msg'] = v.pause(guest)
512        else:
513            module.fail_json(msg="unexpected state")
514
515        return VIRT_SUCCESS, res
516
517    if command:
518        if command in VM_COMMANDS:
519            if command == 'define':
520                if not xml:
521                    module.fail_json(msg="define requires xml argument")
522                if guest:
523                    # there might be a mismatch between quest 'name' in the module and in the xml
524                    module.warn("'xml' is given - ignoring 'name'")
525                try:
526                    domain_name = re.search('<name>(.*)</name>', xml).groups()[0]
527                except AttributeError:
528                    module.fail_json(msg="Could not find domain 'name' in xml")
529
530                # From libvirt docs (https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainDefineXML):
531                # -- A previous definition for this domain would be overridden if it already exists.
532                #
533                # In real world testing with libvirt versions 1.2.17-13, 2.0.0-10 and 3.9.0-14
534                # on qemu and lxc domains results in:
535                # operation failed: domain '<name>' already exists with <uuid>
536                #
537                # In case a domain would be indeed overwritten, we should protect idempotency:
538                try:
539                    existing_domain_xml = v.get_vm(domain_name).XMLDesc(
540                        libvirt.VIR_DOMAIN_XML_INACTIVE
541                    )
542                except VMNotFound:
543                    existing_domain_xml = None
544                try:
545                    domain = v.define(xml)
546                    if existing_domain_xml:
547                        # if we are here, then libvirt redefined existing domain as the doc promised
548                        if existing_domain_xml != domain.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE):
549                            res = {'changed': True, 'change_reason': 'config changed'}
550                    else:
551                        res = {'changed': True, 'created': domain.name()}
552                except libvirtError as e:
553                    if e.get_error_code() != 9:  # 9 means 'domain already exists' error
554                        module.fail_json(msg='libvirtError: %s' % e.get_error_message())
555                if autostart is not None and v.autostart(domain_name, autostart):
556                    res = {'changed': True, 'change_reason': 'autostart'}
557
558            elif not guest:
559                module.fail_json(msg="%s requires 1 argument: guest" % command)
560            else:
561                res = getattr(v, command)(guest)
562                if not isinstance(res, dict):
563                    res = {command: res}
564
565            return VIRT_SUCCESS, res
566
567        elif hasattr(v, command):
568            res = getattr(v, command)()
569            if not isinstance(res, dict):
570                res = {command: res}
571            return VIRT_SUCCESS, res
572
573        else:
574            module.fail_json(msg="Command %s not recognized" % command)
575
576    module.fail_json(msg="expected state or command parameter to be specified")
577
578
579def main():
580    module = AnsibleModule(
581        argument_spec=dict(
582            name=dict(type='str', aliases=['guest']),
583            state=dict(type='str', choices=['destroyed', 'paused', 'running', 'shutdown']),
584            autostart=dict(type='bool'),
585            command=dict(type='str', choices=ALL_COMMANDS),
586            uri=dict(type='str', default='qemu:///system'),
587            xml=dict(type='str'),
588        ),
589    )
590
591    if not HAS_VIRT:
592        module.fail_json(msg='The `libvirt` module is not importable. Check the requirements.')
593
594    rc = VIRT_SUCCESS
595    try:
596        rc, result = core(module)
597    except Exception as e:
598        module.fail_json(msg=to_native(e), exception=traceback.format_exc())
599
600    if rc != 0:  # something went wrong emit the msg
601        module.fail_json(rc=rc, msg=result)
602    else:
603        module.exit_json(**result)
604
605
606if __name__ == '__main__':
607    main()
608