| #!/usr/bin/python |
| # |
| # Copyright 2007 Google Inc. Released under the GPL v2 |
| |
| """ |
| This module defines the KVM class |
| |
| KVM: a KVM virtual machine monitor |
| """ |
| |
| __author__ = """ |
| mbligh@google.com (Martin J. Bligh), |
| poirier@google.com (Benjamin Poirier), |
| stutsman@google.com (Ryan Stutsman) |
| """ |
| |
| import os |
| |
| from autotest_lib.client.common_lib import error |
| from autotest_lib.server import hypervisor, utils, hosts |
| |
| |
| _qemu_ifup_script= """\ |
| #!/bin/sh |
| # $1 is the name of the new qemu tap interface |
| |
| ifconfig $1 0.0.0.0 promisc up |
| brctl addif br0 $1 |
| """ |
| |
| _check_process_script= """\ |
| if [ -f "%(pid_file_name)s" ] |
| then |
| pid=$(cat "%(pid_file_name)s") |
| if [ -L /proc/$pid/exe ] && stat /proc/$pid/exe | |
| grep -q -- "-> \`%(qemu_binary)s\'\$" |
| then |
| echo "process present" |
| else |
| rm -f "%(pid_file_name)s" |
| rm -f "%(monitor_file_name)s" |
| fi |
| fi |
| """ |
| |
| _hard_reset_script= """\ |
| import socket |
| |
| monitor_socket= socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) |
| monitor_socket.connect("%(monitor_file_name)s") |
| monitor_socket.send("system_reset\\n")\n') |
| """ |
| |
| _remove_modules_script= """\ |
| if $(grep -q "^kvm_intel [[:digit:]]\+ 0" /proc/modules) |
| then |
| rmmod kvm-intel |
| fi |
| |
| if $(grep -q "^kvm_amd [[:digit:]]\+ 0" /proc/modules) |
| then |
| rmmod kvm-amd |
| fi |
| |
| if $(grep -q "^kvm [[:digit:]]\+ 0" /proc/modules) |
| then |
| rmmod kvm |
| fi |
| """ |
| |
| |
| class KVM(hypervisor.Hypervisor): |
| """ |
| This class represents a KVM virtual machine monitor. |
| |
| Implementation details: |
| This is a leaf class in an abstract class hierarchy, it must |
| implement the unimplemented methods in parent classes. |
| """ |
| |
| build_dir= None |
| pid_dir= None |
| support_dir= None |
| addresses= [] |
| insert_modules= True |
| modules= {} |
| |
| |
| def __del__(self): |
| """ |
| Destroy a KVM object. |
| |
| Guests managed by this hypervisor that are still running will |
| be killed. |
| """ |
| self.deinitialize() |
| |
| |
| def _insert_modules(self): |
| """ |
| Insert the kvm modules into the kernel. |
| |
| The modules inserted are the ones from the build directory, NOT |
| the ones from the kernel. |
| |
| This function should only be called after install(). It will |
| check that the modules are not already loaded before attempting |
| to insert them. |
| """ |
| cpu_flags= self.host.run('cat /proc/cpuinfo | ' |
| 'grep -e "^flags" | head -1 | cut -d " " -f 2-' |
| ).stdout.strip() |
| |
| if cpu_flags.find('vmx') != -1: |
| module_type= "intel" |
| elif cpu_flags.find('svm') != -1: |
| module_type= "amd" |
| else: |
| raise error.AutoservVirtError("No harware " |
| "virtualization extensions found, " |
| "KVM cannot run") |
| |
| self.host.run('if ! $(grep -q "^kvm " /proc/modules); ' |
| 'then insmod "%s"; fi' % (utils.sh_escape( |
| os.path.join(self.build_dir, "kernel/kvm.ko")),)) |
| if module_type == "intel": |
| self.host.run('if ! $(grep -q "^kvm_intel " ' |
| '/proc/modules); then insmod "%s"; fi' % |
| (utils.sh_escape(os.path.join(self.build_dir, |
| "kernel/kvm-intel.ko")),)) |
| elif module_type == "amd": |
| self.host.run('if ! $(grep -q "^kvm_amd " ' |
| '/proc/modules); then insmod "%s"; fi' % |
| (utils.sh_escape(os.path.join(self.build_dir, |
| "kernel/kvm-amd.ko")),)) |
| |
| |
| def _remove_modules(self): |
| """ |
| Remove the kvm modules from the kernel. |
| |
| This function checks that they're not in use before trying to |
| remove them. |
| """ |
| self.host.run(_remove_modules_script) |
| |
| |
| def install(self, addresses, build=True, insert_modules=True, syncdir=None): |
| """ |
| Compile the kvm software on the host that the object was |
| initialized with. |
| |
| The kvm kernel modules are compiled, for this, the kernel |
| sources must be available. A custom qemu is also compiled. |
| Note that 'make install' is not run, the kernel modules and |
| qemu are run from where they were built, therefore not |
| conflicting with what might already be installed. |
| |
| Args: |
| addresses: a list of dict entries of the form |
| {"mac" : "xx:xx:xx:xx:xx:xx", |
| "ip" : "yyy.yyy.yyy.yyy"} where x and y |
| are replaced with sensible values. The ip |
| address may be a hostname or an IPv6 instead. |
| |
| When a new virtual machine is created, the |
| first available entry in that list will be |
| used. The network card in the virtual machine |
| will be assigned the specified mac address and |
| autoserv will use the specified ip address to |
| connect to the virtual host via ssh. The virtual |
| machine os must therefore be configured to |
| configure its network with the ip corresponding |
| to the mac. |
| build: build kvm from the source material, if False, |
| it is assumed that the package contains the |
| source tree after a 'make'. |
| insert_modules: build kvm modules from the source |
| material and insert them. Otherwise, the |
| running kernel is assumed to already have |
| kvm support and nothing will be done concerning |
| the modules. |
| |
| TODO(poirier): check dependencies before building |
| kvm needs: |
| libasound2-dev |
| libsdl1.2-dev (or configure qemu with --disable-gfx-check, how?) |
| bridge-utils |
| """ |
| self.addresses= [ |
| {"mac" : address["mac"], |
| "ip" : address["ip"], |
| "is_used" : False} for address in addresses] |
| |
| self.build_dir = self.host.get_tmp_dir() |
| self.support_dir= self.host.get_tmp_dir() |
| |
| self.host.run('echo "%s" > "%s"' % ( |
| utils.sh_escape(_qemu_ifup_script), |
| utils.sh_escape(os.path.join(self.support_dir, |
| "qemu-ifup.sh")),)) |
| self.host.run('chmod a+x "%s"' % ( |
| utils.sh_escape(os.path.join(self.support_dir, |
| "qemu-ifup.sh")),)) |
| |
| self.host.send_file(self.source_material, self.build_dir) |
| remote_source_material= os.path.join(self.build_dir, |
| os.path.basename(self.source_material)) |
| |
| self.build_dir= utils.unarchive(self.host, |
| remote_source_material) |
| |
| if insert_modules: |
| configure_modules= "" |
| self.insert_modules= True |
| else: |
| configure_modules= "--with-patched-kernel " |
| self.insert_modules= False |
| |
| # build |
| if build: |
| try: |
| self.host.run('make -C "%s" clean' % ( |
| utils.sh_escape(self.build_dir),), |
| timeout=600) |
| except error.AutoservRunError: |
| # directory was already clean and contained |
| # no makefile |
| pass |
| self.host.run('cd "%s" && ./configure %s' % ( |
| utils.sh_escape(self.build_dir), |
| configure_modules,), timeout=600) |
| if syncdir: |
| cmd = 'cd "%s/kernel" && make sync LINUX=%s' % ( |
| utils.sh_escape(self.build_dir), |
| utils.sh_escape(syncdir)) |
| self.host.run(cmd) |
| self.host.run('make -j%d -C "%s"' % ( |
| self.host.get_num_cpu() * 2, |
| utils.sh_escape(self.build_dir),), timeout=3600) |
| # remember path to modules |
| self.modules['kvm'] = "%s" %( |
| utils.sh_escape(os.path.join(self.build_dir, |
| "kernel/kvm.ko"))) |
| self.modules['kvm-intel'] = "%s" %( |
| utils.sh_escape(os.path.join(self.build_dir, |
| "kernel/kvm-intel.ko"))) |
| self.modules['kvm-amd'] = "%s" %( |
| utils.sh_escape(os.path.join(self.build_dir, |
| "kernel/kvm-amd.ko"))) |
| print self.modules |
| |
| self.initialize() |
| |
| |
| def initialize(self): |
| """ |
| Initialize the hypervisor. |
| |
| Loads needed kernel modules and creates temporary directories. |
| The logic is that you could compile once and |
| initialize - deinitialize many times. But why you would do that |
| has yet to be figured. |
| |
| Raises: |
| AutoservVirtError: cpuid doesn't report virtualization |
| extentions (vmx for intel or svm for amd), in |
| this case, kvm cannot run. |
| """ |
| self.pid_dir= self.host.get_tmp_dir() |
| |
| if self.insert_modules: |
| self._remove_modules() |
| self._insert_modules() |
| |
| |
| def deinitialize(self): |
| """ |
| Terminate the hypervisor. |
| |
| Kill all the virtual machines that are still running and |
| unload the kernel modules. |
| """ |
| self.refresh_guests() |
| for address in self.addresses: |
| if address["is_used"]: |
| self.delete_guest(address["ip"]) |
| self.pid_dir= None |
| |
| if self.insert_modules: |
| self._remove_modules() |
| |
| |
| def new_guest(self, qemu_options): |
| """ |
| Start a new guest ("virtual machine"). |
| |
| Returns: |
| The ip that was picked from the list supplied to |
| install() and assigned to this guest. |
| |
| Raises: |
| AutoservVirtError: no more addresses are available. |
| """ |
| for address in self.addresses: |
| if not address["is_used"]: |
| break |
| else: |
| raise error.AutoservVirtError( |
| "No more addresses available") |
| |
| retval= self.host.run( |
| '%s' |
| # this is the line of options that can be modified |
| ' %s ' |
| '-pidfile "%s" -daemonize -nographic ' |
| #~ '-serial telnet::4444,server ' |
| '-monitor unix:"%s",server,nowait ' |
| '-net nic,macaddr="%s" -net tap,script="%s" -L "%s"' % ( |
| utils.sh_escape(os.path.join( |
| self.build_dir, |
| "qemu/x86_64-softmmu/qemu-system-x86_64")), |
| qemu_options, |
| utils.sh_escape(os.path.join( |
| self.pid_dir, |
| "vhost%s_pid" % (address["ip"],))), |
| utils.sh_escape(os.path.join( |
| self.pid_dir, |
| "vhost%s_monitor" % (address["ip"],))), |
| utils.sh_escape(address["mac"]), |
| utils.sh_escape(os.path.join( |
| self.support_dir, |
| "qemu-ifup.sh")), |
| utils.sh_escape(os.path.join( |
| self.build_dir, |
| "qemu/pc-bios")),)) |
| |
| address["is_used"]= True |
| return address["ip"] |
| |
| |
| def refresh_guests(self): |
| """ |
| Refresh the list of guests addresses. |
| |
| The is_used status will be updated according to the presence |
| of the process specified in the pid file that was written when |
| the virtual machine was started. |
| |
| TODO(poirier): there are a lot of race conditions in this code |
| because the process might terminate on its own anywhere in |
| between |
| """ |
| for address in self.addresses: |
| if address["is_used"]: |
| pid_file_name= utils.sh_escape(os.path.join( |
| self.pid_dir, |
| "vhost%s_pid" % (address["ip"],))) |
| monitor_file_name= utils.sh_escape(os.path.join( |
| self.pid_dir, |
| "vhost%s_monitor" % (address["ip"],))) |
| retval= self.host.run( |
| _check_process_script % { |
| "pid_file_name" : pid_file_name, |
| "monitor_file_name" : monitor_file_name, |
| "qemu_binary" : utils.sh_escape( |
| os.path.join(self.build_dir, |
| "qemu/x86_64-softmmu/" |
| "qemu-system-x86_64")),}) |
| if (retval.stdout.strip() != |
| "process present"): |
| address["is_used"]= False |
| |
| |
| def delete_guest(self, guest_hostname): |
| """ |
| Terminate a virtual machine. |
| |
| Args: |
| guest_hostname: the ip (as it was specified in the |
| address list given to install()) of the guest |
| to terminate. |
| |
| Raises: |
| AutoservVirtError: the guest_hostname argument is |
| invalid |
| |
| TODO(poirier): is there a difference in qemu between |
| sending SIGTEM or quitting from the monitor? |
| TODO(poirier): there are a lot of race conditions in this code |
| because the process might terminate on its own anywhere in |
| between |
| """ |
| for address in self.addresses: |
| if address["ip"] == guest_hostname: |
| if address["is_used"]: |
| break |
| else: |
| # Will happen if deinitialize() is |
| # called while guest objects still |
| # exit and these are del'ed after. |
| # In that situation, nothing is to |
| # be done here, don't throw an error |
| # either because it will print an |
| # ugly message during garbage |
| # collection. The solution would be to |
| # delete the guest objects before |
| # calling deinitialize(), this can't be |
| # done by the KVM class, it has no |
| # reference to those objects and it |
| # cannot have any either. The Guest |
| # objects already need to have a |
| # reference to their managing |
| # hypervisor. If the hypervisor had a |
| # reference to the Guest objects it |
| # manages, it would create a circular |
| # reference and those objects would |
| # not be elligible for garbage |
| # collection. In turn, this means that |
| # the KVM object would not be |
| # automatically del'ed at the end of |
| # the program and guests that are still |
| # running would be left unattended. |
| # Note that this circular reference |
| # problem could be avoided by using |
| # weakref's in class KVM but the |
| # control file will most likely also |
| # have references to the guests. |
| return |
| else: |
| raise error.AutoservVirtError("Unknown guest hostname") |
| |
| pid_file_name= utils.sh_escape(os.path.join(self.pid_dir, |
| "vhost%s_pid" % (address["ip"],))) |
| monitor_file_name= utils.sh_escape(os.path.join(self.pid_dir, |
| "vhost%s_monitor" % (address["ip"],))) |
| |
| retval= self.host.run( |
| _check_process_script % { |
| "pid_file_name" : pid_file_name, |
| "monitor_file_name" : monitor_file_name, |
| "qemu_binary" : utils.sh_escape(os.path.join( |
| self.build_dir, |
| "qemu/x86_64-softmmu/qemu-system-x86_64")),}) |
| if retval.stdout.strip() == "process present": |
| self.host.run('kill $(cat "%s")' %( |
| pid_file_name,)) |
| self.host.run('rm -f "%s"' %( |
| pid_file_name,)) |
| self.host.run('rm -f "%s"' %( |
| monitor_file_name,)) |
| address["is_used"]= False |
| |
| |
| def reset_guest(self, guest_hostname): |
| """ |
| Perform a hard reset on a virtual machine. |
| |
| Args: |
| guest_hostname: the ip (as it was specified in the |
| address list given to install()) of the guest |
| to terminate. |
| |
| Raises: |
| AutoservVirtError: the guest_hostname argument is |
| invalid |
| """ |
| for address in self.addresses: |
| if address["ip"] is guest_hostname: |
| if address["is_used"]: |
| break |
| else: |
| raise error.AutoservVirtError("guest " |
| "hostname not in use") |
| else: |
| raise error.AutoservVirtError("Unknown guest hostname") |
| |
| monitor_file_name= utils.sh_escape(os.path.join(self.pid_dir, |
| "vhost%s_monitor" % (address["ip"],))) |
| |
| self.host.run('python -c "%s"' % (utils.sh_escape( |
| _hard_reset_script % { |
| "monitor_file_name" : monitor_file_name,}),)) |