diff options
author | Tails developers <amnesia@boum.org> | 2014-12-19 00:40:08 +0100 |
---|---|---|
committer | Holger Levsen <holger@layer-acht.org> | 2014-12-21 09:45:40 +0100 |
commit | 51680b6ebb645d37ebdfcd122ca163b3a638aefa (patch) | |
tree | 337e128d2eac3cbc89ecbacf38851bfa33469cd5 /features/support/helpers/vm_helper.rb | |
parent | 44bab3c86ca3d95837f4c50cc535206352385a46 (diff) | |
download | jenkins.debian.net-51680b6ebb645d37ebdfcd122ca163b3a638aefa.tar.xz |
files copied from https://git-tails.immerda.ch/tails - many thanks to the tails developers for their nice work and documentation of it - these files have been released under the GNU General Public License version 3 or (at your option) any later version
features/images has been omitted
Diffstat (limited to 'features/support/helpers/vm_helper.rb')
-rw-r--r-- | features/support/helpers/vm_helper.rb | 426 |
1 files changed, 426 insertions, 0 deletions
diff --git a/features/support/helpers/vm_helper.rb b/features/support/helpers/vm_helper.rb new file mode 100644 index 00000000..2b5ad291 --- /dev/null +++ b/features/support/helpers/vm_helper.rb @@ -0,0 +1,426 @@ +require 'libvirt' +require 'rexml/document' + +class VM + + # These class attributes will be lazily initialized during the first + # instantiation: + # This is the libvirt connection, of which we only want one and + # which can persist for different VM instances (even in parallel) + @@virt = nil + # This is a storage helper that deals with volume manipulation. The + # storage it deals with persists across VMs, by necessity. + @@storage = nil + + def VM.storage + return @@storage + end + + def storage + return @@storage + end + + attr_reader :domain, :display, :ip, :net + + def initialize(xml_path, x_display) + @@virt ||= Libvirt::open("qemu:///system") + @xml_path = xml_path + default_domain_xml = File.read("#{@xml_path}/default.xml") + update_domain(default_domain_xml) + default_net_xml = File.read("#{@xml_path}/default_net.xml") + update_net(default_net_xml) + @display = Display.new(@domain_name, x_display) + set_cdrom_boot($tails_iso) + plug_network + # unlike the domain and net the storage pool should survive VM + # teardown (so a new instance can use e.g. a previously created + # USB drive), so we only create a new one if there is none. + @@storage ||= VMStorage.new(@@virt, xml_path) + rescue Exception => e + clean_up_net + clean_up_domain + raise e + end + + def update_domain(xml) + domain_xml = REXML::Document.new(xml) + @domain_name = domain_xml.elements['domain/name'].text + clean_up_domain + @domain = @@virt.define_domain_xml(xml) + end + + def update_net(xml) + net_xml = REXML::Document.new(xml) + @net_name = net_xml.elements['network/name'].text + @ip = net_xml.elements['network/ip/dhcp/host/'].attributes['ip'] + clean_up_net + @net = @@virt.define_network_xml(xml) + @net.create + end + + def clean_up_domain + begin + domain = @@virt.lookup_domain_by_name(@domain_name) + domain.destroy if domain.active? + domain.undefine + rescue + end + end + + def clean_up_net + begin + net = @@virt.lookup_network_by_name(@net_name) + net.destroy if net.active? + net.undefine + rescue + end + end + + def set_network_link_state(state) + domain_xml = REXML::Document.new(@domain.xml_desc) + domain_xml.elements['domain/devices/interface/link'].attributes['state'] = state + if is_running? + @domain.update_device(domain_xml.elements['domain/devices/interface'].to_s) + else + update_domain(domain_xml.to_s) + end + end + + def plug_network + set_network_link_state('up') + end + + def unplug_network + set_network_link_state('down') + end + + def set_cdrom_tray_state(state) + domain_xml = REXML::Document.new(@domain.xml_desc) + domain_xml.elements.each('domain/devices/disk') do |e| + if e.attribute('device').to_s == "cdrom" + e.elements['target'].attributes['tray'] = state + if is_running? + @domain.update_device(e.to_s) + else + update_domain(domain_xml.to_s) + end + end + end + end + + def eject_cdrom + set_cdrom_tray_state('open') + end + + def close_cdrom + set_cdrom_tray_state('closed') + end + + def set_boot_device(dev) + if is_running? + raise "boot settings can only be set for inactive vms" + end + domain_xml = REXML::Document.new(@domain.xml_desc) + domain_xml.elements['domain/os/boot'].attributes['dev'] = dev + update_domain(domain_xml.to_s) + end + + def set_cdrom_image(image) + domain_xml = REXML::Document.new(@domain.xml_desc) + domain_xml.elements.each('domain/devices/disk') do |e| + if e.attribute('device').to_s == "cdrom" + if ! e.elements['source'] + e.add_element('source') + end + e.elements['source'].attributes['file'] = image + if is_running? + @domain.update_device(e.to_s, Libvirt::Domain::DEVICE_MODIFY_FORCE) + else + update_domain(domain_xml.to_s) + end + end + end + end + + def remove_cdrom + set_cdrom_image('') + end + + def set_cdrom_boot(image) + if is_running? + raise "boot settings can only be set for inactice vms" + end + set_boot_device('cdrom') + set_cdrom_image(image) + close_cdrom + end + + def plug_drive(name, type) + # Get the next free /dev/sdX on guest + used_devs = [] + domain_xml = REXML::Document.new(@domain.xml_desc) + domain_xml.elements.each('domain/devices/disk/target') do |e| + used_devs <<= e.attribute('dev').to_s + end + letter = 'a' + dev = "sd" + letter + while used_devs.include? dev + letter = (letter[0].ord + 1).chr + dev = "sd" + letter + end + assert letter <= 'z' + + xml = REXML::Document.new(File.read("#{@xml_path}/disk.xml")) + xml.elements['disk/source'].attributes['file'] = @@storage.disk_path(name) + xml.elements['disk/driver'].attributes['type'] = @@storage.disk_format(name) + xml.elements['disk/target'].attributes['dev'] = dev + xml.elements['disk/target'].attributes['bus'] = type + if type == "usb" + xml.elements['disk/target'].attributes['removable'] = 'on' + end + + if is_running? + @domain.attach_device(xml.to_s) + else + domain_xml = REXML::Document.new(@domain.xml_desc) + domain_xml.elements['domain/devices'].add_element(xml) + update_domain(domain_xml.to_s) + end + end + + def disk_xml_desc(name) + domain_xml = REXML::Document.new(@domain.xml_desc) + domain_xml.elements.each('domain/devices/disk') do |e| + begin + if e.elements['source'].attribute('file').to_s == @@storage.disk_path(name) + return e.to_s + end + rescue + next + end + end + return nil + end + + def unplug_drive(name) + xml = disk_xml_desc(name) + @domain.detach_device(xml) + end + + def disk_dev(name) + xml = REXML::Document.new(disk_xml_desc(name)) + return "/dev/" + xml.elements['disk/target'].attribute('dev').to_s + end + + def disk_detected?(name) + return execute("test -b #{disk_dev(name)}").success? + end + + def set_disk_boot(name, type) + if is_running? + raise "boot settings can only be set for inactive vms" + end + plug_drive(name, type) + set_boot_device('hd') + # For some reason setting the boot device doesn't prevent cdrom + # boot unless it's empty + remove_cdrom + end + + # XXX-9p: Shares don't work together with snapshot save+restore. See + # XXX-9p in common_steps.rb for more information. + def add_share(source, tag) + if is_running? + raise "shares can only be added to inactice vms" + end + xml = REXML::Document.new(File.read("#{@xml_path}/fs_share.xml")) + xml.elements['filesystem/source'].attributes['dir'] = source + xml.elements['filesystem/target'].attributes['dir'] = tag + domain_xml = REXML::Document.new(@domain.xml_desc) + domain_xml.elements['domain/devices'].add_element(xml) + update_domain(domain_xml.to_s) + end + + def list_shares + list = [] + domain_xml = REXML::Document.new(@domain.xml_desc) + domain_xml.elements.each('domain/devices/filesystem') do |e| + list << e.elements['target'].attribute('dir').to_s + end + return list + end + + def set_ram_size(size, unit = "KiB") + raise "System memory can only be added to inactice vms" if is_running? + domain_xml = REXML::Document.new(@domain.xml_desc) + domain_xml.elements['domain/memory'].text = size + domain_xml.elements['domain/memory'].attributes['unit'] = unit + domain_xml.elements['domain/currentMemory'].text = size + domain_xml.elements['domain/currentMemory'].attributes['unit'] = unit + update_domain(domain_xml.to_s) + end + + def get_ram_size_in_bytes + domain_xml = REXML::Document.new(@domain.xml_desc) + unit = domain_xml.elements['domain/memory'].attribute('unit').to_s + size = domain_xml.elements['domain/memory'].text.to_i + return convert_to_bytes(size, unit) + end + + def set_arch(arch) + raise "System architecture can only be set to inactice vms" if is_running? + domain_xml = REXML::Document.new(@domain.xml_desc) + domain_xml.elements['domain/os/type'].attributes['arch'] = arch + update_domain(domain_xml.to_s) + end + + def add_hypervisor_feature(feature) + raise "Hypervisor features can only be added to inactice vms" if is_running? + domain_xml = REXML::Document.new(@domain.xml_desc) + domain_xml.elements['domain/features'].add_element(feature) + update_domain(domain_xml.to_s) + end + + def drop_hypervisor_feature(feature) + raise "Hypervisor features can only be fropped from inactice vms" if is_running? + domain_xml = REXML::Document.new(@domain.xml_desc) + domain_xml.elements['domain/features'].delete_element(feature) + update_domain(domain_xml.to_s) + end + + def disable_pae_workaround + # add_hypervisor_feature("nonpae") results in a libvirt error, and + # drop_hypervisor_feature("pae") alone won't disable pae. Hence we + # use this workaround. + xml = <<EOF + <qemu:commandline xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'> + <qemu:arg value='-cpu'/> + <qemu:arg value='pentium,-pae'/> + </qemu:commandline> +EOF + domain_xml = REXML::Document.new(@domain.xml_desc) + domain_xml.elements['domain'].add_element(REXML::Document.new(xml)) + update_domain(domain_xml.to_s) + end + + def set_os_loader(type) + if is_running? + raise "boot settings can only be set for inactice vms" + end + if type == 'UEFI' + domain_xml = REXML::Document.new(@domain.xml_desc) + domain_xml.elements['domain/os'].add_element(REXML::Document.new( + '<loader>/usr/share/ovmf/OVMF.fd</loader>' + )) + update_domain(domain_xml.to_s) + else + raise "unsupported OS loader type" + end + end + + def is_running? + begin + return @domain.active? + rescue + return false + end + end + + def execute(cmd, user = "root") + return VMCommand.new(self, cmd, { :user => user, :spawn => false }) + end + + def execute_successfully(cmd, user = "root") + p = execute(cmd, user) + assert_vmcommand_success(p) + return p + end + + def spawn(cmd, user = "root") + return VMCommand.new(self, cmd, { :user => user, :spawn => true }) + end + + def wait_until_remote_shell_is_up(timeout = 30) + VMCommand.wait_until_remote_shell_is_up(self, timeout) + end + + def host_to_guest_time_sync + host_time= DateTime.now.strftime("%s").to_s + execute("date -s '@#{host_time}'").success? + end + + def has_network? + return execute("/sbin/ifconfig eth0 | grep -q 'inet addr'").success? + end + + def has_process?(process) + return execute("pidof -x -o '%PPID' " + process).success? + end + + def pidof(process) + return execute("pidof -x -o '%PPID' " + process).stdout.chomp.split + end + + def file_exist?(file) + execute("test -e #{file}").success? + end + + def file_content(file, user = 'root') + # We don't quote #{file} on purpose: we sometimes pass environment variables + # or globs that we want to be interpreted by the shell. + cmd = execute("cat #{file}", user) + assert(cmd.success?, + "Could not cat '#{file}':\n#{cmd.stdout}\n#{cmd.stderr}") + return cmd.stdout + end + + def save_snapshot(path) + @domain.save(path) + @display.stop + end + + def restore_snapshot(path) + # Clean up current domain so its snapshot can be restored + clean_up_domain + Libvirt::Domain::restore(@@virt, path) + @domain = @@virt.lookup_domain_by_name(@domain_name) + @display.start + end + + def start + return if is_running? + @domain.create + @display.start + end + + def reset + # ruby-libvirt 0.4 does not support the reset method. + # XXX: Once we use Jessie, use @domain.reset instead. + system("virsh -c qemu:///system reset " + @domain_name) if is_running? + end + + def power_off + @domain.destroy if is_running? + @display.stop + end + + def destroy + clean_up_domain + clean_up_net + power_off + end + + def take_screenshot(description) + @display.take_screenshot(description) + end + + def get_remote_shell_port + domain_xml = REXML::Document.new(@domain.xml_desc) + domain_xml.elements.each('domain/devices/serial') do |e| + if e.attribute('type').to_s == "tcp" + return e.elements['source'].attribute('service').to_s.to_i + end + end + end + +end |