Update changelog for 0.20.
[tails-test.git] / features / support / helpers / vm_helper.rb
blobf5e4f7ed656107d82669daee00c1fe6d43263cc0
1 require 'libvirt'
2 require 'rexml/document'
4 class VM
6   # These class attributes will be lazily initialized during the first
7   # instantiation:
8   # This is the libvirt connection, of which we only want one and
9   # which can persist for different VM instances (even in parallel)
10   @@virt = nil
11   # This is a storage helper that deals with volume manipulation. The
12   # storage it deals with persists across VMs, by necessity.
13   @@storage = nil
15   def VM.storage
16     return @@storage
17   end
19   def storage
20     return @@storage
21   end
23   attr_reader :domain, :display, :ip, :mac, :net
25   def initialize(xml_path, x_display)
26     @@virt ||= Libvirt::open("qemu:///system")
27     @xml_path = xml_path
28     default_domain_xml = File.read("#{@xml_path}/default.xml")
29     update_domain(default_domain_xml)
30     default_net_xml = File.read("#{@xml_path}/default_net.xml")
31     update_net(default_net_xml)
32     @display = Display.new(@domain_name, x_display)
33     set_cdrom_boot($tails_iso)
34     plug_network
35     # unlike the domain and net the storage pool should survive VM
36     # teardown (so a new instance can use e.g. a previously created
37     # USB drive), so we only create a new one if there is none.
38     @@storage ||= VMStorage.new(@@virt, xml_path)
39   rescue Exception => e
40     clean_up_net
41     clean_up_domain
42     raise e
43   end
45   def update_domain(xml)
46     domain_xml = REXML::Document.new(xml)
47     @domain_name = domain_xml.elements['domain/name'].text
48     clean_up_domain
49     @domain = @@virt.define_domain_xml(xml)
50   end
52   def update_net(xml)
53     net_xml = REXML::Document.new(xml)
54     @net_name = net_xml.elements['network/name'].text
55     @ip  = net_xml.elements['network/ip/dhcp/host/'].attributes['ip']
56     @mac = net_xml.elements['network/ip/dhcp/host/'].attributes['mac']
57     clean_up_net
58     @net = @@virt.define_network_xml(xml)
59     @net.create
60   end
62   def clean_up_domain
63     begin
64       domain = @@virt.lookup_domain_by_name(@domain_name)
65       domain.destroy if domain.active?
66       domain.undefine
67     rescue
68     end
69   end
71   def clean_up_net
72     begin
73       net = @@virt.lookup_network_by_name(@net_name)
74       net.destroy if net.active?
75       net.undefine
76     rescue
77     end
78   end
80   def set_network_link_state(state)
81     domain_xml = REXML::Document.new(@domain.xml_desc)
82     domain_xml.elements['domain/devices/interface/link'].attributes['state'] = state
83     if is_running?
84       @domain.update_device(domain_xml.elements['domain/devices/interface'].to_s)
85     else
86       update_domain(domain_xml.to_s)
87     end
88   end
90   def plug_network
91     set_network_link_state('up')
92   end
94   def unplug_network
95     set_network_link_state('down')
96   end
98   def set_cdrom_tray_state(state)
99     domain_xml = REXML::Document.new(@domain.xml_desc)
100     domain_xml.elements.each('domain/devices/disk') do |e|
101       if e.attribute('device').to_s == "cdrom"
102         e.elements['target'].attributes['tray'] = state
103         if is_running?
104           @domain.update_device(e.to_s)
105         else
106           update_domain(domain_xml.to_s)
107         end
108       end
109     end
110   end
112   def eject_cdrom
113     set_cdrom_tray_state('open')
114   end
116   def close_cdrom
117     set_cdrom_tray_state('closed')
118   end
120   def set_boot_device(dev)
121     if is_running?
122       raise "boot settings can only be set for inactive vms"
123     end
124     domain_xml = REXML::Document.new(@domain.xml_desc)
125     domain_xml.elements['domain/os/boot'].attributes['dev'] = dev
126     update_domain(domain_xml.to_s)
127   end
129   def set_cdrom_image(image)
130     domain_xml = REXML::Document.new(@domain.xml_desc)
131     domain_xml.elements.each('domain/devices/disk') do |e|
132       if e.attribute('device').to_s == "cdrom"
133         if ! e.elements['source']
134           e.add_element('source')
135         end
136         e.elements['source'].attributes['file'] = image
137         if is_running?
138           @domain.update_device(e.to_s, Libvirt::Domain::DEVICE_MODIFY_FORCE)
139         else
140           update_domain(domain_xml.to_s)
141         end
142       end
143     end
144   end
146   def remove_cdrom
147     set_cdrom_image('')
148   end
150   def set_cdrom_boot(image)
151     if is_running?
152       raise "boot settings can only be set for inactice vms"
153     end
154     set_boot_device('cdrom')
155     set_cdrom_image(image)
156     close_cdrom
157   end
159   def plug_drive(name, type)
160     # Get the next free /dev/sdX on guest
161     used_devs = []
162     domain_xml = REXML::Document.new(@domain.xml_desc)
163     domain_xml.elements.each('domain/devices/disk/target') do |e|
164       used_devs <<= e.attribute('dev').to_s
165     end
166     letter = 'a'
167     dev = "sd" + letter
168     while used_devs.include? dev
169       letter = (letter[0].ord + 1).chr
170       dev = "sd" + letter
171     end
172     assert letter <= 'z'
174     xml = REXML::Document.new(File.read("#{@xml_path}/disk.xml"))
175     xml.elements['disk/source'].attributes['file'] = @@storage.disk_path(name)
176     xml.elements['disk/driver'].attributes['type'] = @@storage.disk_format(name)
177     xml.elements['disk/target'].attributes['dev'] = dev
178     xml.elements['disk/target'].attributes['bus'] = type
179     if type == "usb"
180       xml.elements['disk/target'].attributes['removable'] = 'on'
181     end
183     if is_running?
184       @domain.attach_device(xml.to_s)
185     else
186       domain_xml = REXML::Document.new(@domain.xml_desc)
187       domain_xml.elements['domain/devices'].add_element(xml)
188       update_domain(domain_xml.to_s)
189     end
190   end
192   def disk_xml_desc(name)
193     domain_xml = REXML::Document.new(@domain.xml_desc)
194     domain_xml.elements.each('domain/devices/disk') do |e|
195       begin
196         if e.elements['source'].attribute('file').to_s == @@storage.disk_path(name)
197           return e.to_s
198         end
199       rescue
200         next
201       end
202     end
203     return nil
204   end
206   def unplug_drive(name)
207     xml = disk_xml_desc(name)
208     @domain.detach_device(xml)
209   end
211   def disk_dev(name)
212     xml = REXML::Document.new(disk_xml_desc(name))
213     return "/dev/" + xml.elements['disk/target'].attribute('dev').to_s
214   end
216   def disk_detected?(name)
217     return execute("test -b #{disk_dev(name)}").success?
218   end
220   def set_disk_boot(name, type)
221     if is_running?
222       raise "boot settings can only be set for inactive vms"
223     end
224     plug_drive(name, type)
225     set_boot_device('hd')
226     # For some reason setting the boot device doesn't prevent cdrom
227     # boot unless it's empty
228     remove_cdrom
229   end
231   # XXX-9p: Shares don't work together with snapshot save+restore. See
232   # XXX-9p in common_steps.rb for more information.
233   def add_share(source, tag)
234     if is_running?
235       raise "shares can only be added to inactice vms"
236     end
237     xml = REXML::Document.new(File.read("#{@xml_path}/fs_share.xml"))
238     xml.elements['filesystem/source'].attributes['dir'] = source
239     xml.elements['filesystem/target'].attributes['dir'] = tag
240     domain_xml = REXML::Document.new(@domain.xml_desc)
241     domain_xml.elements['domain/devices'].add_element(xml)
242     update_domain(domain_xml.to_s)
243   end
245   def list_shares
246     list = []
247     domain_xml = REXML::Document.new(@domain.xml_desc)
248     domain_xml.elements.each('domain/devices/filesystem') do |e|
249       list << e.elements['target'].attribute('dir').to_s
250     end
251     return list
252   end
254   def set_ram_size(size, unit = "KiB")
255     raise "System memory can only be added to inactice vms" if is_running?
256     domain_xml = REXML::Document.new(@domain.xml_desc)
257     domain_xml.elements['domain/memory'].text = size
258     domain_xml.elements['domain/memory'].attributes['unit'] = unit
259     domain_xml.elements['domain/currentMemory'].text = size
260     domain_xml.elements['domain/currentMemory'].attributes['unit'] = unit
261     update_domain(domain_xml.to_s)
262   end
264   def get_ram_size_in_bytes
265     domain_xml = REXML::Document.new(@domain.xml_desc)
266     unit = domain_xml.elements['domain/memory'].attribute('unit').to_s
267     size = domain_xml.elements['domain/memory'].text.to_i
268     return convert_to_bytes(size, unit)
269   end
271   def set_arch(arch)
272     raise "System architecture can only be set to inactice vms" if is_running?
273     domain_xml = REXML::Document.new(@domain.xml_desc)
274     domain_xml.elements['domain/os/type'].attributes['arch'] = arch
275     update_domain(domain_xml.to_s)
276   end
278   def add_hypervisor_feature(feature)
279     raise "Hypervisor features can only be added to inactice vms" if is_running?
280     domain_xml = REXML::Document.new(@domain.xml_desc)
281     domain_xml.elements['domain/features'].add_element(feature)
282     update_domain(domain_xml.to_s)
283   end
285   def drop_hypervisor_feature(feature)
286     raise "Hypervisor features can only be fropped from inactice vms" if is_running?
287     domain_xml = REXML::Document.new(@domain.xml_desc)
288     domain_xml.elements['domain/features'].delete_element(feature)
289     update_domain(domain_xml.to_s)
290   end
292   def disable_pae_workaround
293     # add_hypervisor_feature("nonpae") results in a libvirt error, and
294     # drop_hypervisor_feature("pae") alone won't disable pae. Hence we
295     # use this workaround.
296     xml = <<EOF
297   <qemu:commandline xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'>
298     <qemu:arg value='-cpu'/>
299     <qemu:arg value='pentium,-pae'/>
300   </qemu:commandline>
302     domain_xml = REXML::Document.new(@domain.xml_desc)
303     domain_xml.elements['domain'].add_element(REXML::Document.new(xml))
304     update_domain(domain_xml.to_s)
305   end
307   def is_running?
308     begin
309       return @domain.active?
310     rescue
311       return false
312     end
313   end
315   def execute(cmd, user = "root")
316     return VMCommand.new(self, cmd, { :user => user, :spawn => false })
317   end
319   def spawn(cmd, user = "root")
320     return VMCommand.new(self, cmd, { :user => user, :spawn => true })
321   end
323   def wait_until_remote_shell_is_up(timeout = 30)
324     VMCommand.wait_until_remote_shell_is_up(self, timeout)
325   end
327   def host_to_guest_time_sync
328     host_time= DateTime.now.strftime("%s").to_s
329     execute("date -s '@#{host_time}'").success?
330   end
332   def has_network?
333     return execute("/sbin/ifconfig eth0 | grep -q 'inet addr'").success?
334   end
336   def has_process?(process)
337     return execute("pidof " + process).success?
338   end
340   def save_snapshot(path)
341     @domain.save(path)
342     @display.stop
343   end
345   def restore_snapshot(path)
346     # Clean up current domain so its snapshot can be restored
347     clean_up_domain
348     Libvirt::Domain::restore(@@virt, path)
349     @domain = @@virt.lookup_domain_by_name(@domain_name)
350     @display.start
351   end
353   def start
354     return if is_running?
355     @domain.create
356     @display.start
357   end
359   def power_off
360     @domain.destroy if is_running?
361     @display.stop
362   end
364   def destroy
365     clean_up_domain
366     clean_up_net
367     power_off
368   end
370   def take_screenshot(description)
371     @display.take_screenshot(description)
372   end
374   def get_remote_shell_port
375     domain_xml = REXML::Document.new(@domain.xml_desc)
376     domain_xml.elements.each('domain/devices/serial') do |e|
377       if e.attribute('type').to_s == "tcp"
378         return e.elements['source'].attribute('service').to_s.to_i
379       end
380     end
381   end