qemu-toolkit / lib / qemu-toolkit / vm.rb

require 'qemu-toolkit/config'
require 'qemu-toolkit/dsl'
require 'qemu-toolkit/iscsi_target'

require 'fileutils'
require 'socket'

module QemuToolkit
  # Abstracts a virtual machine on a vm host. This class provides all sorts
  # of methods that execute administration actions. 
  #
  class VM
    class << self # CLASS METHODS
      # Load all vm descriptions and provide an iterator for them. 
      #
      def all(backend=nil)
        Enumerator.new do |yielder|
          libdir = Config.etc('lib')
          if ::File.directory? libdir
            $:.unshift libdir
          end
          
          Dir[Config.etc('*.rb')].each do |vm_file|
            # Load all virtual machines from the given file
            dsl = DSL::File.new
            dsl.add_toplevel_target :virtual_machine, lambda { |name| 
              VM.new(backend).tap { |vm| vm.name = name } }
              
            dsl.load_file(vm_file)

            # Yield them all in turn
            dsl.objects.each do |vm|
              yielder << vm
            end
          end
        end
      end

      # Access the definition of a single vm. 
      #
      def [](name, backend=nil)
        all(backend).find { |vm| vm.name === name }
      end
    end
    
    # VM name
    attr_accessor :name
    # iSCSI target iqn and ip address to connect to
    attr_accessor :iscsi_target
    # A list of network cards that will be connected to vnics on the host. 
    attr_reader :nics
    # A list of network configuration statements that will be passed through
    # to qemu. 
    attr_reader :nets
    # The number of cpus to configure, defaults to 2. 
    attr_accessor :cpus
    # Ram in megabytes
    attr_accessor :ram
    # VNC display port
    attr_accessor :vnc_display
    # Keyboard layout
    attr_accessor :keyboard_layout
    # Other devices
    attr_reader :devices
    # Boot order (if set)
    attr_accessor :boot
    
    def initialize(backend)
      @disks = []
      @drives = []
      @nics = []
      @nets = []
      @cpus = 2
      @ram = 1024
      @backend = backend
      @vnc_display = nil
      @extra_args = []
      # TODO document
      @devices = []
    end
        
    def add_device(driver, parameters)
      @devices << [driver, parameters]
    end
    def add_drive(parameters)
      @drives << parameters
    end    
    def add_disk(path)
      @disks << path
    end
    def add_nic(name, parameters)
      @nics << [name, parameters]
    end
    def add_net(type, parameters)
      @nets << [type, parameters]
    end
    def add_extra_arg(argument)
      @extra_args << argument
    end
    
    # Runs the VM using qemu.
    def start(dryrun, opts={})
      if dryrun
        puts command(opts) 
      else
        # Make sure var/run/qemu-toolkit/VMNAME exists.
        FileUtils.mkdir_p run_path
        
        @backend.qemu("vm<#{name}>", command(opts))
      end
    end
    
    # Returns the command that is needed to run this virtual machine. Note 
    # that this also modifies system configuration and is not just a routine 
    # that returns a string.
    #
    # @return String command to run the machine
    #
    def command opts={}
      cmd = []
      cmd << "-name #{name}"
      cmd << "-m #{ram}"
      cmd << "-daemonize"
      cmd << '-nographic'
      cmd << "-cpu qemu64"
      cmd << "-smp #{cpus}"
      cmd << "-no-hpet"
      cmd << "-enable-kvm"
      cmd << "-vga cirrus"
      cmd << "-parallel none"
      cmd << "-usb"
      cmd << '-usbdevice tablet'
      
      if keyboard_layout
        cmd << "-k #{keyboard_layout}"
      end

      # Add disks
      cmd += disk_options
      
      # Was an iso image given to boot from?
      if iso_path=opts[:bootiso]
        cmd << "-cdrom #{iso_path}"
        cmd << "-boot order=cd,once=d"
      else
        cmd << '-boot order=cd'
      end
      
      # Set paths for communication with vm
      cmd << "-pidfile #{pid_path}"
      
      cmd << socket_chardev(:monitor, monitor_path)
      cmd << "-monitor chardev:monitor"
      
      cmd << socket_chardev(:serial0, run_path('vm.console'))
      cmd << "-serial chardev:serial0"
      cmd << socket_chardev(:serial1, run_path('vm.ttyb'))
      cmd << "-serial chardev:serial1"
      
      # vnc socket
      cmd << "-vnc unix:#{run_path('vm.vnc')}"
      
      # If vnc_display is set, allow configuring a TCP based VNC port: 
      if vnc_display
        cmd << "-vnc #{vnc_display}"
      end
      
      # Other devices
      devices.each do |driver, parameters| 
        cmd << "-device #{driver}," + 
          parameter_list(parameters)
      end

      # Boot order
      if boot
        cmd << "-boot " + parameter_list(boot)
      end
      
      # networking: nic
      vlan = 0
      # Look up all existing vnics for this virtual machine
      vnics = Vnic.for_prefix(name, @backend)
      
      nics.each do |nic_name, parameters|
        # All vnics that travel via the given interface (:via)
        vnics_for_interface = vnics[parameters[:via]] || []
        
        # Get a vnic that travels via the given interface.
        vnic = vnics_for_interface.shift ||
          Vnic.create(name, parameters[:via], @backend)
        
        cmd << "-net vnic,"+
          parameter_list(
            vlan: vlan, name: nic_name, 
            ifname: vnic.vnic_name, 
            macaddr: parameters[:macaddr])
        cmd << "-net nic,"+
          parameter_list(
            vlan: vlan, name: nic_name, 
            model: parameters[:model] || 'virtio', 
            macaddr: parameters[:macaddr])

        vlan += 1
      end
      
      # networking: net
      nets.each do |type, parameters|
        cmd << "-net #{type},"+
          parameter_list(parameters)
      end
      
      # Extra arguments
      cmd += @extra_args
      
      return cmd
    end
    def disk_options
      cmd = []
      
      if @disks.empty? && !iscsi_target && @drives.empty?
        raise "No disks defined, can't run." 
      end
      
      disk_index = 0
      if iscsi_target
        target = produce_target(*iscsi_target)
        target.ensure_exists
        
        target.disks.each do |device|
          params = {
              file: device, 
              if: 'virtio', 
              index: disk_index, 
              media: 'disk', 
              cache: 'none'
          }
          params[:boot] = 'on' if disk_index == 0
          cmd << "-drive " + parameter_list(params)
          
          disk_index += 1
        end
      end
      
      @disks.each do |path|
        cmd << "-drive file=#{path},if=virtio,index=#{disk_index},"+
          "media=disk,boot=on"
        disk_index += 1
      end
      
      @drives.each do |drive_options|
        cmd << "-drive " + 
          parameter_list(drive_options.merge(index: disk_index))
        disk_index += 1
      end
      
      return cmd
    end
    
    # Connects the current terminal to the given socket. Available sockets
    # include :monitor, :vnc, :console, :ttyb.
    #
    def connect(socket)
      socket_path = run_path("vm.#{socket}")
      cmd = "socat stdio unix-connect:#{socket_path}"
      
      exec cmd
    end
    
    # Kills the vm the hard way. 
    #
    def kill
      run_cmd "kill #{pid}"
    end
    
    # Sends a shutdown command via the monitor socket of the virtual machine. 
    # 
    def shutdown
      monitor_cmd 'system_powerdown'
    end
    
    # Returns an ISCSITarget for host and port. 
    #
    def produce_target(host, port)
      ISCSITarget.new(host, port, @backend)
    end
    
    # Returns true if the virtual machine seems to be currently running. 
    #
    def running?
      if File.exist?(pid_path) 
        # Prod the process using kill. This will not actually kill the
        # process!
        begin
          Process.kill(0, pid)
        rescue Errno::ESRCH
          # When this point is reached, the process doesn't exist. 
          return false
        end

        return true
      end
      
      return false
    end
    
    # Attempts to read and return the pid of the running VM process.
    #
    def pid
      Integer(File.read(pid_path).lines.first.chomp)
    end
    
  private 
    def monitor_cmd(cmd)
      socket = ::UNIXSocket.new(monitor_path)
      socket.puts cmd
      socket.close
    end
  
    def socket_chardev(name, path)
      "-chardev socket,id=#{name},path=#{path},server,nowait"
    end

    # Formats a parameter list as key=value,key=value
    #
    def parameter_list(parameters)
      key_translator = Hash.new { |h,k| k }
      key_translator.update(
        x_txtimer: 'x-txtimer', 
        x_txburst: 'x-txburst')
      
      parameters.
        map { |k,v| "#{key_translator[k]}=#{v}" }.
        join(',')
    end
    
    # Returns the path below /var/run (usually) that contains runtime files
    # for the virtual machine. 
    #
    def run_path(*args)
      Config.var_run(name, *args)
    end
    
    # Returns the file path of the vm pid file. 
    #
    def pid_path
      run_path 'vm.pid'
    end

    # Returns the file path of the monitor socket (unix socket below /var/run)
    # usually. )
    #
    def monitor_path
      run_path 'vm.monitor'
    end
      
    # Runs a command and returns its stdout. This raises an error if the 
    # command doesn't exit with a status of 0.
    #
    def run_cmd(*args)
      cmd = args.join(' ')
      ret = %x(#{cmd})

      raise "Execution error: #{cmd}." unless $?.success?

      ret
    end
  end
end
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.