1#
2# Copyright 2007 Google Inc. Released under the GPL v2
3
4"""
5This module defines the KVM class
6
7        KVM: a KVM virtual machine monitor
8"""
9
10__author__ = """
11mbligh@google.com (Martin J. Bligh),
12poirier@google.com (Benjamin Poirier),
13stutsman@google.com (Ryan Stutsman)
14"""
15
16import os
17
18from autotest_lib.client.common_lib import error
19from autotest_lib.server import hypervisor, utils, hosts
20
21
22_qemu_ifup_script= """\
23#!/bin/sh
24# $1 is the name of the new qemu tap interface
25
26ifconfig $1 0.0.0.0 promisc up
27brctl addif br0 $1
28"""
29
30_check_process_script= """\
31if [ -f "%(pid_file_name)s" ]
32then
33        pid=$(cat "%(pid_file_name)s")
34        if [ -L /proc/$pid/exe ] && stat /proc/$pid/exe |
35                grep -q --  "-> \`%(qemu_binary)s\'\$"
36        then
37                echo "process present"
38        else
39                rm -f "%(pid_file_name)s"
40                rm -f "%(monitor_file_name)s"
41        fi
42fi
43"""
44
45_hard_reset_script= """\
46import socket
47
48monitor_socket= socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
49monitor_socket.connect("%(monitor_file_name)s")
50monitor_socket.send("system_reset\\n")\n')
51"""
52
53_remove_modules_script= """\
54if $(grep -q "^kvm_intel [[:digit:]]\+ 0" /proc/modules)
55then
56        rmmod kvm-intel
57fi
58
59if $(grep -q "^kvm_amd [[:digit:]]\+ 0" /proc/modules)
60then
61        rmmod kvm-amd
62fi
63
64if $(grep -q "^kvm [[:digit:]]\+ 0" /proc/modules)
65then
66        rmmod kvm
67fi
68"""
69
70
71class KVM(hypervisor.Hypervisor):
72    """
73    This class represents a KVM virtual machine monitor.
74
75    Implementation details:
76    This is a leaf class in an abstract class hierarchy, it must
77    implement the unimplemented methods in parent classes.
78    """
79
80    build_dir= None
81    pid_dir= None
82    support_dir= None
83    addresses= []
84    insert_modules= True
85    modules= {}
86
87
88    def __del__(self):
89        """
90        Destroy a KVM object.
91
92        Guests managed by this hypervisor that are still running will
93        be killed.
94        """
95        self.deinitialize()
96
97
98    def _insert_modules(self):
99        """
100        Insert the kvm modules into the kernel.
101
102        The modules inserted are the ones from the build directory, NOT
103        the ones from the kernel.
104
105        This function should only be called after install(). It will
106        check that the modules are not already loaded before attempting
107        to insert them.
108        """
109        cpu_flags= self.host.run('cat /proc/cpuinfo | '
110                'grep -e "^flags" | head -1 | cut -d " " -f 2-'
111                ).stdout.strip()
112
113        if cpu_flags.find('vmx') != -1:
114            module_type= "intel"
115        elif cpu_flags.find('svm') != -1:
116            module_type= "amd"
117        else:
118            raise error.AutoservVirtError("No harware "
119                    "virtualization extensions found, "
120                    "KVM cannot run")
121
122        self.host.run('if ! $(grep -q "^kvm " /proc/modules); '
123                'then insmod "%s"; fi' % (utils.sh_escape(
124                os.path.join(self.build_dir, "kernel/kvm.ko")),))
125        if module_type == "intel":
126            self.host.run('if ! $(grep -q "^kvm_intel " '
127                    '/proc/modules); then insmod "%s"; fi' %
128                    (utils.sh_escape(os.path.join(self.build_dir,
129                    "kernel/kvm-intel.ko")),))
130        elif module_type == "amd":
131            self.host.run('if ! $(grep -q "^kvm_amd " '
132                    '/proc/modules); then insmod "%s"; fi' %
133                    (utils.sh_escape(os.path.join(self.build_dir,
134                    "kernel/kvm-amd.ko")),))
135
136
137    def _remove_modules(self):
138        """
139        Remove the kvm modules from the kernel.
140
141        This function checks that they're not in use before trying to
142        remove them.
143        """
144        self.host.run(_remove_modules_script)
145
146
147    def install(self, addresses, build=True, insert_modules=True, syncdir=None):
148        """
149        Compile the kvm software on the host that the object was
150        initialized with.
151
152        The kvm kernel modules are compiled, for this, the kernel
153        sources must be available. A custom qemu is also compiled.
154        Note that 'make install' is not run, the kernel modules and
155        qemu are run from where they were built, therefore not
156        conflicting with what might already be installed.
157
158        Args:
159                addresses: a list of dict entries of the form
160                        {"mac" : "xx:xx:xx:xx:xx:xx",
161                        "ip" : "yyy.yyy.yyy.yyy"} where x and y
162                        are replaced with sensible values. The ip
163                        address may be a hostname or an IPv6 instead.
164
165                        When a new virtual machine is created, the
166                        first available entry in that list will be
167                        used. The network card in the virtual machine
168                        will be assigned the specified mac address and
169                        autoserv will use the specified ip address to
170                        connect to the virtual host via ssh. The virtual
171                        machine os must therefore be configured to
172                        configure its network with the ip corresponding
173                        to the mac.
174                build: build kvm from the source material, if False,
175                        it is assumed that the package contains the
176                        source tree after a 'make'.
177                insert_modules: build kvm modules from the source
178                        material and insert them. Otherwise, the
179                        running kernel is assumed to already have
180                        kvm support and nothing will be done concerning
181                        the modules.
182
183        TODO(poirier): check dependencies before building
184        kvm needs:
185        libasound2-dev
186        libsdl1.2-dev (or configure qemu with --disable-gfx-check, how?)
187        bridge-utils
188        """
189        self.addresses= [
190                {"mac" : address["mac"],
191                "ip" : address["ip"],
192                "is_used" : False} for address in addresses]
193
194        self.build_dir = self.host.get_tmp_dir()
195        self.support_dir= self.host.get_tmp_dir()
196
197        self.host.run('echo "%s" > "%s"' % (
198                utils.sh_escape(_qemu_ifup_script),
199                utils.sh_escape(os.path.join(self.support_dir,
200                        "qemu-ifup.sh")),))
201        self.host.run('chmod a+x "%s"' % (
202                utils.sh_escape(os.path.join(self.support_dir,
203                        "qemu-ifup.sh")),))
204
205        self.host.send_file(self.source_material, self.build_dir)
206        remote_source_material= os.path.join(self.build_dir,
207                        os.path.basename(self.source_material))
208
209        self.build_dir= utils.unarchive(self.host,
210                remote_source_material)
211
212        if insert_modules:
213            configure_modules= ""
214            self.insert_modules= True
215        else:
216            configure_modules= "--with-patched-kernel "
217            self.insert_modules= False
218
219        # build
220        if build:
221            try:
222                self.host.run('make -C "%s" clean' % (
223                        utils.sh_escape(self.build_dir),),
224                        timeout=600)
225            except error.AutoservRunError:
226                # directory was already clean and contained
227                # no makefile
228                pass
229            self.host.run('cd "%s" && ./configure %s' % (
230                    utils.sh_escape(self.build_dir),
231                    configure_modules,), timeout=600)
232            if syncdir:
233                cmd = 'cd "%s/kernel" && make sync LINUX=%s' % (
234                utils.sh_escape(self.build_dir),
235                utils.sh_escape(syncdir))
236                self.host.run(cmd)
237            self.host.run('make -j%d -C "%s"' % (
238                    self.host.get_num_cpu() * 2,
239                    utils.sh_escape(self.build_dir),), timeout=3600)
240            # remember path to modules
241            self.modules['kvm'] = "%s" %(
242                    utils.sh_escape(os.path.join(self.build_dir,
243                    "kernel/kvm.ko")))
244            self.modules['kvm-intel'] = "%s" %(
245                    utils.sh_escape(os.path.join(self.build_dir,
246                    "kernel/kvm-intel.ko")))
247            self.modules['kvm-amd'] = "%s" %(
248                    utils.sh_escape(os.path.join(self.build_dir,
249                    "kernel/kvm-amd.ko")))
250            print self.modules
251
252        self.initialize()
253
254
255    def initialize(self):
256        """
257        Initialize the hypervisor.
258
259        Loads needed kernel modules and creates temporary directories.
260        The logic is that you could compile once and
261        initialize - deinitialize many times. But why you would do that
262        has yet to be figured.
263
264        Raises:
265                AutoservVirtError: cpuid doesn't report virtualization
266                        extentions (vmx for intel or svm for amd), in
267                        this case, kvm cannot run.
268        """
269        self.pid_dir= self.host.get_tmp_dir()
270
271        if self.insert_modules:
272            self._remove_modules()
273            self._insert_modules()
274
275
276    def deinitialize(self):
277        """
278        Terminate the hypervisor.
279
280        Kill all the virtual machines that are still running and
281        unload the kernel modules.
282        """
283        self.refresh_guests()
284        for address in self.addresses:
285            if address["is_used"]:
286                self.delete_guest(address["ip"])
287        self.pid_dir= None
288
289        if self.insert_modules:
290            self._remove_modules()
291
292
293    def new_guest(self, qemu_options):
294        """
295        Start a new guest ("virtual machine").
296
297        Returns:
298                The ip that was picked from the list supplied to
299                install() and assigned to this guest.
300
301        Raises:
302                AutoservVirtError: no more addresses are available.
303        """
304        for address in self.addresses:
305            if not address["is_used"]:
306                break
307        else:
308            raise error.AutoservVirtError(
309                    "No more addresses available")
310
311        retval= self.host.run(
312                '%s'
313                # this is the line of options that can be modified
314                ' %s '
315                '-pidfile "%s" -daemonize -nographic '
316                #~ '-serial telnet::4444,server '
317                '-monitor unix:"%s",server,nowait '
318                '-net nic,macaddr="%s" -net tap,script="%s" -L "%s"' % (
319                utils.sh_escape(os.path.join(
320                        self.build_dir,
321                        "qemu/x86_64-softmmu/qemu-system-x86_64")),
322                qemu_options,
323                utils.sh_escape(os.path.join(
324                        self.pid_dir,
325                        "vhost%s_pid" % (address["ip"],))),
326                utils.sh_escape(os.path.join(
327                        self.pid_dir,
328                        "vhost%s_monitor" % (address["ip"],))),
329                utils.sh_escape(address["mac"]),
330                utils.sh_escape(os.path.join(
331                        self.support_dir,
332                        "qemu-ifup.sh")),
333                utils.sh_escape(os.path.join(
334                        self.build_dir,
335                        "qemu/pc-bios")),))
336
337        address["is_used"]= True
338        return address["ip"]
339
340
341    def refresh_guests(self):
342        """
343        Refresh the list of guests addresses.
344
345        The is_used status will be updated according to the presence
346        of the process specified in the pid file that was written when
347        the virtual machine was started.
348
349        TODO(poirier): there are a lot of race conditions in this code
350        because the process might terminate on its own anywhere in
351        between
352        """
353        for address in self.addresses:
354            if address["is_used"]:
355                pid_file_name= utils.sh_escape(os.path.join(
356                        self.pid_dir,
357                        "vhost%s_pid" % (address["ip"],)))
358                monitor_file_name= utils.sh_escape(os.path.join(
359                        self.pid_dir,
360                        "vhost%s_monitor" % (address["ip"],)))
361                retval= self.host.run(
362                        _check_process_script % {
363                        "pid_file_name" : pid_file_name,
364                        "monitor_file_name" : monitor_file_name,
365                        "qemu_binary" : utils.sh_escape(
366                                os.path.join(self.build_dir,
367                                "qemu/x86_64-softmmu/"
368                                "qemu-system-x86_64")),})
369                if (retval.stdout.strip() !=
370                        "process present"):
371                    address["is_used"]= False
372
373
374    def delete_guest(self, guest_hostname):
375        """
376        Terminate a virtual machine.
377
378        Args:
379                guest_hostname: the ip (as it was specified in the
380                        address list given to install()) of the guest
381                        to terminate.
382
383        Raises:
384                AutoservVirtError: the guest_hostname argument is
385                        invalid
386
387        TODO(poirier): is there a difference in qemu between
388        sending SIGTEM or quitting from the monitor?
389        TODO(poirier): there are a lot of race conditions in this code
390        because the process might terminate on its own anywhere in
391        between
392        """
393        for address in self.addresses:
394            if address["ip"] == guest_hostname:
395                if address["is_used"]:
396                    break
397                else:
398                    # Will happen if deinitialize() is
399                    # called while guest objects still
400                    # exit and these are del'ed after.
401                    # In that situation, nothing is to
402                    # be done here, don't throw an error
403                    # either because it will print an
404                    # ugly message during garbage
405                    # collection. The solution would be to
406                    # delete the guest objects before
407                    # calling deinitialize(), this can't be
408                    # done by the KVM class, it has no
409                    # reference to those objects and it
410                    # cannot have any either. The Guest
411                    # objects already need to have a
412                    # reference to their managing
413                    # hypervisor. If the hypervisor had a
414                    # reference to the Guest objects it
415                    # manages, it would create a circular
416                    # reference and those objects would
417                    # not be elligible for garbage
418                    # collection. In turn, this means that
419                    # the KVM object would not be
420                    # automatically del'ed at the end of
421                    # the program and guests that are still
422                    # running would be left unattended.
423                    # Note that this circular reference
424                    # problem could be avoided by using
425                    # weakref's in class KVM but the
426                    # control file will most likely also
427                    # have references to the guests.
428                    return
429        else:
430            raise error.AutoservVirtError("Unknown guest hostname")
431
432        pid_file_name= utils.sh_escape(os.path.join(self.pid_dir,
433                "vhost%s_pid" % (address["ip"],)))
434        monitor_file_name= utils.sh_escape(os.path.join(self.pid_dir,
435                "vhost%s_monitor" % (address["ip"],)))
436
437        retval= self.host.run(
438                _check_process_script % {
439                "pid_file_name" : pid_file_name,
440                "monitor_file_name" : monitor_file_name,
441                "qemu_binary" : utils.sh_escape(os.path.join(
442                        self.build_dir,
443                        "qemu/x86_64-softmmu/qemu-system-x86_64")),})
444        if retval.stdout.strip() == "process present":
445            self.host.run('kill $(cat "%s")' %(
446                    pid_file_name,))
447            self.host.run('rm -f "%s"' %(
448                    pid_file_name,))
449            self.host.run('rm -f "%s"' %(
450                    monitor_file_name,))
451        address["is_used"]= False
452
453
454    def reset_guest(self, guest_hostname):
455        """
456        Perform a hard reset on a virtual machine.
457
458        Args:
459                guest_hostname: the ip (as it was specified in the
460                        address list given to install()) of the guest
461                        to terminate.
462
463        Raises:
464                AutoservVirtError: the guest_hostname argument is
465                        invalid
466        """
467        for address in self.addresses:
468            if address["ip"] is guest_hostname:
469                if address["is_used"]:
470                    break
471                else:
472                    raise error.AutoservVirtError("guest "
473                            "hostname not in use")
474        else:
475            raise error.AutoservVirtError("Unknown guest hostname")
476
477        monitor_file_name= utils.sh_escape(os.path.join(self.pid_dir,
478                "vhost%s_monitor" % (address["ip"],)))
479
480        self.host.run('python -c "%s"' % (utils.sh_escape(
481                _hard_reset_script % {
482                "monitor_file_name" : monitor_file_name,}),))
483