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