PageRenderTime 97ms CodeModel.GetById 25ms app.highlight 53ms RepoModel.GetById 1ms app.codeStats 0ms

/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
  1require 'qemu-toolkit/config'
  2require 'qemu-toolkit/dsl'
  3require 'qemu-toolkit/iscsi_target'
  4
  5require 'fileutils'
  6require 'socket'
  7
  8module QemuToolkit
  9  # Abstracts a virtual machine on a vm host. This class provides all sorts
 10  # of methods that execute administration actions. 
 11  #
 12  class VM
 13    class << self # CLASS METHODS
 14      # Load all vm descriptions and provide an iterator for them. 
 15      #
 16      def all(backend=nil)
 17        Enumerator.new do |yielder|
 18          libdir = Config.etc('lib')
 19          if ::File.directory? libdir
 20            $:.unshift libdir
 21          end
 22          
 23          Dir[Config.etc('*.rb')].each do |vm_file|
 24            # Load all virtual machines from the given file
 25            dsl = DSL::File.new
 26            dsl.add_toplevel_target :virtual_machine, lambda { |name| 
 27              VM.new(backend).tap { |vm| vm.name = name } }
 28              
 29            dsl.load_file(vm_file)
 30
 31            # Yield them all in turn
 32            dsl.objects.each do |vm|
 33              yielder << vm
 34            end
 35          end
 36        end
 37      end
 38
 39      # Access the definition of a single vm. 
 40      #
 41      def [](name, backend=nil)
 42        all(backend).find { |vm| vm.name === name }
 43      end
 44    end
 45    
 46    # VM name
 47    attr_accessor :name
 48    # iSCSI target iqn and ip address to connect to
 49    attr_accessor :iscsi_target
 50    # A list of network cards that will be connected to vnics on the host. 
 51    attr_reader :nics
 52    # A list of network configuration statements that will be passed through
 53    # to qemu. 
 54    attr_reader :nets
 55    # The number of cpus to configure, defaults to 2. 
 56    attr_accessor :cpus
 57    # Ram in megabytes
 58    attr_accessor :ram
 59    # VNC display port
 60    attr_accessor :vnc_display
 61    # Keyboard layout
 62    attr_accessor :keyboard_layout
 63    # Other devices
 64    attr_reader :devices
 65    # Boot order (if set)
 66    attr_accessor :boot
 67    
 68    def initialize(backend)
 69      @disks = []
 70      @drives = []
 71      @nics = []
 72      @nets = []
 73      @cpus = 2
 74      @ram = 1024
 75      @backend = backend
 76      @vnc_display = nil
 77      @extra_args = []
 78      # TODO document
 79      @devices = []
 80    end
 81        
 82    def add_device(driver, parameters)
 83      @devices << [driver, parameters]
 84    end
 85    def add_drive(parameters)
 86      @drives << parameters
 87    end    
 88    def add_disk(path)
 89      @disks << path
 90    end
 91    def add_nic(name, parameters)
 92      @nics << [name, parameters]
 93    end
 94    def add_net(type, parameters)
 95      @nets << [type, parameters]
 96    end
 97    def add_extra_arg(argument)
 98      @extra_args << argument
 99    end
100    
101    # Runs the VM using qemu.
102    def start(dryrun, opts={})
103      if dryrun
104        puts command(opts) 
105      else
106        # Make sure var/run/qemu-toolkit/VMNAME exists.
107        FileUtils.mkdir_p run_path
108        
109        @backend.qemu("vm<#{name}>", command(opts))
110      end
111    end
112    
113    # Returns the command that is needed to run this virtual machine. Note 
114    # that this also modifies system configuration and is not just a routine 
115    # that returns a string.
116    #
117    # @return String command to run the machine
118    #
119    def command opts={}
120      cmd = []
121      cmd << "-name #{name}"
122      cmd << "-m #{ram}"
123      cmd << "-daemonize"
124      cmd << '-nographic'
125      cmd << "-cpu qemu64"
126      cmd << "-smp #{cpus}"
127      cmd << "-no-hpet"
128      cmd << "-enable-kvm"
129      cmd << "-vga cirrus"
130      cmd << "-parallel none"
131      cmd << "-usb"
132      cmd << '-usbdevice tablet'
133      
134      if keyboard_layout
135        cmd << "-k #{keyboard_layout}"
136      end
137
138      # Add disks
139      cmd += disk_options
140      
141      # Was an iso image given to boot from?
142      if iso_path=opts[:bootiso]
143        cmd << "-cdrom #{iso_path}"
144        cmd << "-boot order=cd,once=d"
145      else
146        cmd << '-boot order=cd'
147      end
148      
149      # Set paths for communication with vm
150      cmd << "-pidfile #{pid_path}"
151      
152      cmd << socket_chardev(:monitor, monitor_path)
153      cmd << "-monitor chardev:monitor"
154      
155      cmd << socket_chardev(:serial0, run_path('vm.console'))
156      cmd << "-serial chardev:serial0"
157      cmd << socket_chardev(:serial1, run_path('vm.ttyb'))
158      cmd << "-serial chardev:serial1"
159      
160      # vnc socket
161      cmd << "-vnc unix:#{run_path('vm.vnc')}"
162      
163      # If vnc_display is set, allow configuring a TCP based VNC port: 
164      if vnc_display
165        cmd << "-vnc #{vnc_display}"
166      end
167      
168      # Other devices
169      devices.each do |driver, parameters| 
170        cmd << "-device #{driver}," + 
171          parameter_list(parameters)
172      end
173
174      # Boot order
175      if boot
176        cmd << "-boot " + parameter_list(boot)
177      end
178
179      cmd += network_options      
180      
181      # Extra arguments
182      cmd += @extra_args
183      
184      return cmd
185    end
186    def network_options
187      cmd = []
188
189      # networking: nic
190      vlan = 0
191      # Look up all existing vnics for this virtual machine
192      vnics = Vnic.for_prefix(name, @backend)
193      
194      nics.each do |nic_name, parameters|
195        via = parameters.delete(:via)
196        model = parameters.delete(:model) || 'virtio'
197        macaddr = parameters.delete(:macaddr)
198
199        # All vnics that travel via the given interface (via)
200        vnic = vnics.allocate(via, macaddr)
201
202        # If no vnic has been found, create a new one. 
203        unless vnic
204          vnic = Vnic.create(name, via, @backend, macaddr)
205        end
206        
207        cmd << "-net vnic,"+
208          parameter_list(
209            parameters.merge(
210              vlan: vlan, name: nic_name, 
211              ifname: vnic.vnic_name, 
212            ))
213
214        if model == 'virtio'
215          cmd << '-device virtio-net-pci,'+
216            parameter_list(
217              mac: vnic.macaddr, 
218              tx: 'timer', x_txtimer: 200000, x_txburst: 128, 
219              vlan: vlan) 
220        else
221          cmd << "-net nic,"+
222            parameter_list(
223              vlan: vlan, name: nic_name, 
224              model: model,
225              macaddr: vnic.macaddr)
226        end
227
228        vlan += 1
229      end
230      
231      # networking: net
232      nets.each do |type, parameters|
233        map(parameters, :macaddr) { |a| Network::MacAddress.new(a) }
234        
235        cmd << "-net #{type},"+
236          parameter_list(parameters)
237      end
238
239      cmd
240    end
241    def disk_options
242      cmd = []
243      
244      if @disks.empty? && !iscsi_target && @drives.empty?
245        raise "No disks defined, can't run." 
246      end
247      
248      disk_index = 0
249      if iscsi_target
250        target = produce_target(*iscsi_target)
251        target.ensure_exists
252        
253        target.disks.each do |device|
254          params = {
255              file: device, 
256              if: 'virtio', 
257              index: disk_index, 
258              media: 'disk', 
259              cache: 'none'
260          }
261          params[:boot] = 'on' if disk_index == 0
262          cmd << "-drive " + parameter_list(params)
263          
264          disk_index += 1
265        end
266      end
267      
268      @disks.each do |path|
269        params = { file: path, if: 'virtio', index: disk_index, media: "disk" }
270        params[:boot] = 'on' if disk_index == 0 
271
272        cmd << "-drive " + parameter_list(params)
273
274        disk_index += 1
275      end
276      
277      @drives.each do |drive_options|
278        cmd << "-drive " + 
279          parameter_list(drive_options.merge(index: disk_index))
280        disk_index += 1
281      end
282      
283      return cmd
284    end
285    
286    # Connects the current terminal to the given socket. Available sockets
287    # include :monitor, :vnc, :console, :ttyb.
288    #
289    def connect(socket)
290      socket_path = run_path("vm.#{socket}")
291      cmd = "socat stdio unix-connect:#{socket_path}"
292      
293      exec cmd
294    end
295    
296    # Kills the vm the hard way. 
297    #
298    def kill
299      run_cmd "kill #{pid}"
300    end
301    
302    # Sends a shutdown command via the monitor socket of the virtual machine. 
303    # 
304    def shutdown
305      monitor_cmd 'system_powerdown'
306    end
307    
308    # Returns an ISCSITarget for host and port. 
309    #
310    def produce_target(host, port)
311      ISCSITarget.new(host, port, @backend)
312    end
313    
314    # Returns true if the virtual machine seems to be currently running. 
315    #
316    def running?
317      if File.exist?(pid_path) 
318        # Prod the process using kill. This will not actually kill the
319        # process!
320        begin
321          Process.kill(0, pid)
322        rescue Errno::ESRCH
323          # When this point is reached, the process doesn't exist. 
324          return false
325        end
326
327        return true
328      end
329      
330      return false
331    end
332    
333    # Attempts to read and return the pid of the running VM process.
334    #
335    def pid
336      Integer(File.read(pid_path).lines.first.chomp)
337    end
338    
339  private 
340    def monitor_cmd(cmd)
341      socket = ::UNIXSocket.new(monitor_path)
342      socket.puts cmd
343      socket.close
344    end
345  
346    # Maps a key from a hash to a new value returned by the block. 
347    #
348    def map hash, key, &block
349      return unless hash.has_key? key
350      hash[key] = block.call(hash[key])
351    end
352  
353    def socket_chardev(name, path)
354      "-chardev socket,id=#{name},path=#{path},server,nowait"
355    end
356
357    # Formats a parameter list as key=value,key=value
358    #
359    def parameter_list(parameters)
360      key_translator = Hash.new { |h,k| k }
361      key_translator.update(
362        x_txtimer: 'x-txtimer', 
363        x_txburst: 'x-txburst')
364      
365      parameters.
366        map { |k,v| "#{key_translator[k]}=#{v}" }.
367        join(',')
368    end
369    
370    # Returns the path below /var/run (usually) that contains runtime files
371    # for the virtual machine. 
372    #
373    def run_path(*args)
374      Config.var_run(name, *args)
375    end
376    
377    # Returns the file path of the vm pid file. 
378    #
379    def pid_path
380      run_path 'vm.pid'
381    end
382
383    # Returns the file path of the monitor socket (unix socket below /var/run)
384    # usually. )
385    #
386    def monitor_path
387      run_path 'vm.monitor'
388    end
389      
390    # Runs a command and returns its stdout. This raises an error if the 
391    # command doesn't exit with a status of 0.
392    #
393    def run_cmd(*args)
394      cmd = args.join(' ')
395      ret = %x(#{cmd})
396
397      raise "Execution error: #{cmd}." unless $?.success?
398
399      ret
400    end
401  end
402end