class Kitchen::Driver::Qemu

QEMU driver for Kitchen.

@author Emil Renner Berthing <esmil@esmil.dk>

Public Instance Methods

create(state) click to toggle source

Creates a QEMU instance.

@param state [Hash] mutable instance and driver state @raise [ActionFailed] if the action could not be completed

# File lib/kitchen/driver/qemu.rb, line 157
      def create(state)
        Dir.chdir(config[:kitchen_root])

        monitor = monitor_path
        if File.exist?(monitor)
          begin
            mon = UNIXSocket.new(monitor)
          rescue Errno::ECONNREFUSED
            info 'Stale monitor socket detected. Assuming old QEMU already quit.'
            cleanup!
          else
            mon.close
            raise ActionFailed, "QEMU instance #{instance.to_str} already running."
          end
        end

        create_privkey or raise ActionFailed, "Unable to create file '#{privkey_path}'"

        fqdn = config[:hostname] || instance.name
        hostname = fqdn.match(/^([^.]+)/)[0]

        cmd = [
          config[:binary], '-daemonize',
          '-display', config[:display].to_s,
          '-chardev', "socket,id=mon-qmp,path=#{monitor},server,nowait",
          '-mon', 'chardev=mon-qmp,mode=control',
          '-serial', "mon:unix:path=#{serial_path},server,nowait",
          '-m', config[:memory].to_s,
        ]

        kvm = config[:kvm]
        if kvm.nil? # autodetect
          begin
            kvm = File.stat('/dev/kvm')
          rescue Errno::ENOENT
            kvm = false
            info 'KVM device /dev/kvm doesn\'t exist. Maybe the module is not loaded.'
          else
            kvm = kvm.writable? && kvm.readable?
            info 'KVM device /dev/kvm not read/writeable. Maybe add your user to the kvm group.' unless kvm
          end
        end
        if kvm
          info 'KVM enabled.'
          cmd.push('-enable-kvm', '-cpu', 'host')
        else
          info 'KVM disabled'
        end

        port = config[:port]
        port = random_free_port('127.0.0.1', config[:port_min], config[:port_max]) if port.nil?
        config[:networks].each do |network|
          cmd.push(
            '-netdev',
            network[:netdev]
              .gsub(/hostfwd=[^,]*/) { |x| x.gsub('%p', port.to_s) }
              .gsub(/hostname=%h/, "hostname=#{hostname}")
          ) if network[:netdev]
          cmd.push('-device', network[:device])
        end

        cmd.push('-bios',  config[:bios].to_s)  if config[:bios]
        cmd.push('-vga',   config[:vga].to_s)   if config[:vga]
        cmd.push('-spice', config[:spice].to_s) if config[:spice]
        cmd.push('-vnc',   config[:vnc].to_s)   if config[:vnc]

        cmd.push('-device', 'virtio-scsi-pci,id=scsi')
        config[:image].each_with_index do |image, i|
          drive = ['if=none', "id=drive#{i}"]
          drive.push("readonly=#{image[:readonly]}")           if image.has_key?(:readonly)
          drive.push("snapshot=#{image[:snapshot]}")           if image.has_key?(:snapshot)
          drive.push("discard=#{image[:discard]}")             if image.has_key?(:discard)
          drive.push("detect-zeroes=#{image[:detect_zeroes]}") if image.has_key?(:detect_zeroes)
          if ['/', '.'].include? image[:file][0]
            drive.push("file=#{image[:file]}")
          else
            drive.push("file=#{config[:image_path]}/#{image[:file]}")
          end
          cmd.push('-device', "scsi-hd,drive=drive#{i}",
                   '-drive', drive.join(','))
        end

        smp = []
        smp.push("cpus=#{config[:cpus]}")       if config.has_key?(:cpus)
        smp.push("sockets=#{config[:sockets]}") if config.has_key?(:sockets)
        smp.push("cores=#{config[:cores]}")     if config.has_key?(:cores)
        smp.push("threads=#{config[:threads]}") if config.has_key?(:threads)
        if smp.length > 0
          info 'SMP enabled.'
          cmd.push('-smp', smp.join(','))
        end

        config[:hostshares].each_with_index do |share, i|
          path = share[:path]
          path = "#{config[:kitchen_root]}/#{path}" unless path[0] == '/'
          raise ActionFailed, "Share path '#{path}' not a directory" unless
            ::File.directory?(path)
          cmd.push('-fsdev', "local,id=fsdev#{i},security_model=none,path=#{path}",
                   '-device', "virtio-9p-pci,fsdev=fsdev#{i},mount_tag=path#{i}")
        end

        config[:args].each do |arg|
          arg.each do |name, value|
            cmd.push("-#{name}")
            cmd.push(value)
          end
        end

        info 'Spawning QEMU..'
        error = nil
        Open3.popen3({ 'QEMU_AUDIO_DRV' => 'none' }, *cmd) do |_, _, err, thr|
          if not thr.value.success?
            error = err.read.strip
          end
        end
        if error
          cleanup!
          raise ActionFailed, error
        end

        state[:hostname]      = '127.0.0.1'
        state[:port]          = port
        state[:username]      = config[:username]
        state[:password]      = config[:password]
        state[:acpi_poweroff] = config[:acpi_poweroff]

        if hostname == fqdn
          names = fqdn
        else
          names = "#{fqdn} #{hostname}"
        end

        info 'Waiting for SSH..'
        conn = instance.transport.connection(state)
        conn.wait_until_ready
        conn.execute(<<-EOS)
sudo sh -s 2>/dev/null <<END
echo '127.0.0.1 #{names}' >> /etc/hosts
hostnamectl set-hostname #{hostname} || hostname #{hostname}
END
umask 0022
install -dm700 "$HOME/.ssh"
echo '#{@@PUBKEY}' > "$HOME/.ssh/authorized_keys"
EOS
        config[:hostshares].each_with_index do |share, i|
          options = share[:mount_options] ?
            share[:mount_options].join(',') : 'cache=none,access=any,version=9p2000.L'
          conn.execute("sudo sh -c 'install -dm755 \"#{share[:mountpoint]}\" && mount -t 9p -o trans=virtio,#{options} path#{i} \"#{share[:mountpoint]}\"'")
        end
        conn.close

        # from now on we want to use the private key,
        # so delete the :password field and set :ssh_key
        state.delete(:password)
        state[:ssh_key] = privkey_path
      end
destroy(state) click to toggle source

Destroys an instance.

@param state [Hash] mutable instance state @raise [ActionFailed] if the action could not be completed

# File lib/kitchen/driver/qemu.rb, line 318
def destroy(state)
  Dir.chdir(config[:kitchen_root])

  monitor = monitor_path
  return unless File.exist?(monitor)

  instance.transport.connection(state).close

  begin
    mon = QMPClient.new(UNIXSocket.new(monitor), 2)
    if state[:acpi_poweroff]
      info 'Sending ACPI poweroff..'
      mon.execute('system_powerdown')
      mon.wait_for_eof(30)
    else
      info 'Quitting QEMU..'
      mon.execute('quit')
      mon.wait_for_eof(5)
    end
    mon.close
  rescue Errno::ECONNREFUSED
    info 'Connection to monitor refused. Assuming QEMU already quit.'
  rescue QMPClient::Timeout
    mon.close
    raise ActionFailed, "QEMU instance #{instance.to_str} is unresponsive"
  end

  cleanup!
end
finalize_config!(instance) click to toggle source

A lifecycle method that should be invoked when the object is about ready to be used. A reference to an Instance is required as configuration dependant data may be access through an Instance. This also acts as a hook point where the object may wish to perform other last minute checks, validations, or configuration expansions.

@param instance [Instance] an associated instance @return [self] itself, for use in chaining @raise [ClientError] if instance parameter is nil

Calls superclass method
# File lib/kitchen/driver/qemu.rb, line 72
def finalize_config!(instance)
  super
  if not config[:binary]
    config[:binary] = @@ARCHBINARY[config[:arch]] or
      raise UserError, "Unknown architecture '#{config[:arch]}'"
  end

  # kitchen-vagrant compatibility
  config[:hostname] = config[:vm_hostname] unless config.has_key?(:hostname)

  # add default network
  if !config.has_key?(:networks)
    config[:networks] = [{
      :netdev => 'user,id=user,net=192.168.1.0/24,hostname=%h,hostfwd=tcp::%p-:22',
      :device => 'virtio-net-pci,netdev=user',
    }]
  else
    raise UserError, "Invalid network entry for #{instance.to_str}" unless
      config[:networks].kind_of?(Array)

    config[:networks].each_with_index do |network, i|
      raise UserError, "Invalid network entry #{i+1} for #{instance.to_str}" unless
        network.kind_of?(Hash) && network[:device].kind_of?(String)
      raise UserError, "Invalid network entry #{i+1} for #{instance.to_str}" if
        network.has_key?(:device) && !network[:device].kind_of?(String)
    end
  end

  acpi_poweroff = false
  if config[:image].kind_of?(String)
    config[:image] = [{
      :file     => config[:image],
      :snapshot => 'on',
    }]
  else
    raise UserError, "Invalid image entry for #{instance.to_str}" unless
      config[:image].kind_of?(Array)
    config[:image].each do |image|
      raise UserError, "Invalid image entry for #{instance.to_str}" unless
        image.kind_of?(Hash) && image[:file].kind_of?(String)
      # backwards compatibility
      image[:readonly] = 'on'  if image[:readonly].kind_of?(TrueClass)
      image[:readonly] = 'off' if image[:readonly].kind_of?(FalseClass)
      image[:snapshot] = 'on'  if image[:snapshot].kind_of?(TrueClass)
      image[:snapshot] = 'off' if image[:snapshot].kind_of?(FalseClass)
      # defaults
      image[:snapshot]      = 'on'    if !image.has_key?(:snapshot) && image[:readonly] != 'on'
      image[:discard]       = 'unmap' if !image.has_key?(:discard)  && image[:readonly] != 'on'
      image[:detect_zeroes] = 'unmap' if !image.has_key?(:detect_zeroes) &&
        image[:readonly] != 'on' && image[:snapshot] != 'on'
      acpi_poweroff = true if image[:snapshot] != 'on' && image[:readonly] != 'on'
    end
  end
  config[:acpi_poweroff] = acpi_poweroff unless config.has_key?(:acpi_poweroff)

  raise UserError, "Invalid share entry for #{instance.to_str}" unless
    config[:hostshares].kind_of?(Array)
  # kitchen-vagrant compatibility
  if config[:hostshares].empty? && config[:synced_folders].kind_of?(Array)
    config[:synced_folders].each do |folder|
      if !folder[0].kind_of?(String) || !folder[1].kind_of?(String)
        config[:hostshares].clear
        break
      end
      config[:hostshares].push({ :path => folder[0], :mountpoint => folder[1] })
    end
  else
    config[:hostshares].each do |share|
      raise UserError, "Invalid share entry for #{instance.to_str}" unless
        share.kind_of?(Hash) && share[:path].kind_of?(String)
      raise UserError, "No mountpoint defined for share '#{share[:path]}' of #{instance.to_str}" unless
        share[:mountpoint].kind_of?(String)
      raise UserError, "Invalid mount options for share '#{share[:path]}' of #{instance.to_str}" if
        share.has_key?(:mount_options) && !share[:mount_options].kind_of?(Array)
    end
  end

  config[:vga] = 'qxl' if config[:spice] && !config[:vga]
  self
end

Private Instance Methods

cleanup!() click to toggle source
# File lib/kitchen/driver/qemu.rb, line 422
def cleanup!
  begin
    File.delete(monitor_path)
  rescue Errno::ENOENT
    # do nothing
  end
  begin
    File.delete(serial_path)
  rescue Errno::ENOENT
    # do nothing
  end
end
create_privkey() click to toggle source
# File lib/kitchen/driver/qemu.rb, line 402
def create_privkey
  path = privkey_path
  return true if File.file?(path)
  File.open(path, File::CREAT|File::TRUNC|File::RDWR, 0600) { |f| f.write(@@PRIVKEY) }
end
monitor_path() click to toggle source
# File lib/kitchen/driver/qemu.rb, line 394
def monitor_path
  File.join('.kitchen', "#{instance.name}.qmp")
end
privkey_path() click to toggle source
# File lib/kitchen/driver/qemu.rb, line 390
def privkey_path
  File.join(config[:kitchen_root], '.kitchen', 'kitchen-qemu.key')
end
random_free_port(host, min, max) click to toggle source
# File lib/kitchen/driver/qemu.rb, line 408
def random_free_port(host, min, max)
  loop do
    port = rand(max - min) + min
    begin
      serv = TCPServer.new(host, port)
    rescue Errno::EADDRINUSE
      # do nothing
    else
      serv.close
      return port
    end
  end
end
serial_path() click to toggle source
# File lib/kitchen/driver/qemu.rb, line 398
def serial_path
  File.join('.kitchen', "#{instance.name}.mon")
end