/lib/qemu-toolkit/vm.rb
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