/lib/qemu-toolkit/vm.rb
https://bitbucket.org/kschiess/qemu-toolkit · Ruby · 402 lines · 293 code · 50 blank · 59 comment · 12 complexity · 8fc0e75f7f6981de4c082715426f93cb MD5 · raw file
- 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
- cmd += network_options
-
- # Extra arguments
- cmd += @extra_args
-
- return cmd
- end
- def network_options
- cmd = []
- # 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|
- via = parameters.delete(:via)
- model = parameters.delete(:model) || 'virtio'
- macaddr = parameters.delete(:macaddr)
- # All vnics that travel via the given interface (via)
- vnic = vnics.allocate(via, macaddr)
- # If no vnic has been found, create a new one.
- unless vnic
- vnic = Vnic.create(name, via, @backend, macaddr)
- end
-
- cmd << "-net vnic,"+
- parameter_list(
- parameters.merge(
- vlan: vlan, name: nic_name,
- ifname: vnic.vnic_name,
- ))
- if model == 'virtio'
- cmd << '-device virtio-net-pci,'+
- parameter_list(
- mac: vnic.macaddr,
- tx: 'timer', x_txtimer: 200000, x_txburst: 128,
- vlan: vlan)
- else
- cmd << "-net nic,"+
- parameter_list(
- vlan: vlan, name: nic_name,
- model: model,
- macaddr: vnic.macaddr)
- end
- vlan += 1
- end
-
- # networking: net
- nets.each do |type, parameters|
- map(parameters, :macaddr) { |a| Network::MacAddress.new(a) }
-
- cmd << "-net #{type},"+
- parameter_list(parameters)
- end
- 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|
- params = { file: path, if: 'virtio', index: disk_index, media: "disk" }
- params[:boot] = 'on' if disk_index == 0
- cmd << "-drive " + parameter_list(params)
- 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
-
- # Maps a key from a hash to a new value returned by the block.
- #
- def map hash, key, &block
- return unless hash.has_key? key
- hash[key] = block.call(hash[key])
- 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