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