File Coverage

OpenQA/Qemu/Proc.pm
Criterion Covered Total %
statement 301 354 84.7
total 301 354 84.7


line stmt code
1   # Copyright 2018-2021 SUSE LLC
2   # SPDX-License-Identifier: GPL-2.0-or-later
3    
4   =head2 OpenQA::Qemu::Proc
5    
6   Manages the state of a QEMU virtual machine and processes. This is used by
7   backend/qemu.pm to (re)start qemu processes while tracking what settings have
8   changed between restarts and generating the necessary qemu command line
9   parameters.
10    
11   This class uses an object model which approximates QEMU's device model. The
12   idea is to allow you to set QEMU parameters in a structured way that can be
13   updated as QEMU's state changes during execution.
14    
15   This is not practical (at least in the short term) for all parameters so there
16   are static parameters which are stored as simple strings and mutable
17   parameters which are represented as complex objects.
18    
19   =cut
20    
21   use Mojo::Base -base, -signatures;
22 16  
  16  
  16  
23   use Data::Dumper;
24 16 use File::Basename;
  16  
  16  
25 16 use File::Which;
  16  
  16  
26 16 use Mojo::JSON qw(encode_json decode_json);
  16  
  16  
27 16 use Mojo::File 'path';
  16  
  16  
28 16 use OpenQA::Qemu::BlockDevConf;
  16  
  16  
29 16 use OpenQA::Qemu::ControllerConf;
  16  
  16  
30 16 use OpenQA::Qemu::DriveDevice 'QEMU_IMAGE_FORMAT';
  16  
  16  
31 16 use OpenQA::Qemu::SnapshotConf;
  16  
  16  
32 16 use osutils qw(gen_params runcmd run);
  16  
  16  
33 16 use Mojo::IOLoop::ReadWriteProcess 'process';
  16  
  16  
34 16 use Mojo::IOLoop::ReadWriteProcess::Session 'session';
  16  
  16  
35 16  
  16  
  16  
36   use POSIX ();
37 16  
  16  
  16  
38   use constant STATE_FILE => 'qemu_state.json';
39 16  
  16  
  16  
40   has qemu_bin => 'qemu-kvm';
41   has qemu_img_bin => 'qemu-img';
42   has _process => sub (@) { process(
43   pidfile => 'qemu.pid',
44   separate_err => 0,
45   blocking_stop => 1) };
46    
47   has _static_params => sub ($) { [] };
48   has _mut_params => sub ($) { [] };
49    
50    
51 150 has controller_conf => sub ($) { OpenQA::Qemu::ControllerConf->new() };
  150  
  150  
  150  
  150  
  150  
52   has blockdev_conf => sub ($) { return OpenQA::Qemu::BlockDevConf->new() };
53   has snapshot_conf => sub ($) { return OpenQA::Qemu::SnapshotConf->new() };
54    
55   my $self = $class->SUPER::new(@args);
56   $self->_push_mut($self->controller_conf);
57 50 $self->_push_mut($self->blockdev_conf);
  50  
  50  
  50  
58 50 $self->_push_mut($self->snapshot_conf);
59 50  
60 50 return $self;
61 50 }
62    
63 50 =head3 static_param
64    
65   Add a plain QEMU parameter, represented as an array of strings. The first item
66   in the array will have '-' prepended to it.
67    
68   =cut
69   if (@args < 2) {
70   push(@{$self->_static_params}, '-' . $args[0]);
71   }
72 526 else {
  526  
  526  
  526  
73 526 gen_params($self->_static_params, shift @args, shift @args, @args);
74 65 }
  65  
75   }
76    
77 461 =head3 configure_controllers
78    
79   Add SCSI, PCI and USB controllers if necessary. QEMU will automatically create
80   controllers for simple configurations.
81    
82   =cut
83   # Setting the HD or CD model to a type of SCSI controller has been
84   # deprecated for a long time.
85   for my $var (qw(HDDMODEL CDMODEL)) {
86   if ($vars->{$var} =~ /virtio-scsi.*/) {
87 36 die "Set $var to scsi-" . lc(substr($var, 0, 1)) . 'd and SCSICONTROLLER to '
  36  
  36  
  36  
88   . $vars->{$var};
89   }
90 36 }
91 72  
92   if ($vars->{CDMODEL} eq 'scsi-cd' || $vars->{HDDMODEL} eq 'scsi-hd') {
93 0 $vars->{SCSICONTROLLER} ||= "virtio-scsi-pci";
94   }
95    
96   my $scsi_con = $vars->{SCSICONTROLLER} || 0;
97 36 my $cc = $self->controller_conf;
98 28  
99   if ($scsi_con) {
100   $cc->add_controller($scsi_con, 'scsi0');
101 36 if ($vars->{MULTIPATH}) {
102 36 $cc->add_controller($scsi_con, 'scsi1');
103   }
104 36 }
105 30  
106 30 if ($vars->{ATACONTROLLER}) {
107 2 $cc->add_controller($vars->{ATACONTROLLER}, 'ahci0');
108   }
109    
110   if ($vars->{USBBOOT}) {
111 36 $cc->add_controller('qemu-xhci', 'xhci0');
112 0 }
113    
114   return $self;
115 36 }
116 0  
117   my $json = run($self->qemu_img_bin, 'info', '--output=json', $path);
118   # We can't check the exit code of qemu-img, because it sometimes returns 1
119 36 # even for a successful command on ppc. Instead we just hide and ignore
120   # JSON decode failures and assume the previous command has printed the
121   # error string already
122 38 my $map;
  38  
  38  
  38  
  38  
123 38 {
124   local $SIG{__DIE__} = 'DEFAULT';
125   $map = eval { decode_json($json) }
126   };
127   die "$json\n" if $@;
128 38 die "No $field field in: " . Dumper($map) unless defined $map->{$field};
129   return $map->{$field};
130 38 }
  38  
131 38  
  38  
132    
133 38  
134 36 =head3 configure_blockdevs
135 36  
136   Configure disk drives and their block device backing chains. See BlockDevConf.pm.
137    
138 36 =cut
  36  
  36  
  36  
  36  
139   my $bdc = $self->blockdev_conf;
140 2 my @scsi_ctrs = $self->controller_conf->get_controllers(qr/scsi/);
  2  
  2  
  2  
  2  
141    
142   $bdc->basedir($basedir);
143    
144   for my $i (1 .. $vars->{NUMDISKS}) {
145   my $hdd_model = $vars->{"HDDMODEL_$i"} // $vars->{HDDMODEL};
146   my $backing_file = $vars->{"HDD_$i"};
147 37 my $node_id = 'hd' . ($i - 1);
  37  
  37  
  37  
  37  
  37  
148 37 my $hdd_serial = $vars->{"HDDSERIAL_$i"} || $node_id;
149 37 my $size = $vars->{"HDDSIZEGB_$i"};
150   my $num_queues = $vars->{"HDDNUMQUEUES_$i"} || -1;
151 37 my $drive;
152    
153 37 $size .= 'G' if defined($size);
154 38  
155 38 if (defined $backing_file) {
156 38 $backing_file = path($backing_file)->to_abs;
157 38 # Handle files compressed as *.xz
158 38 my ($name, $path, $ext) = fileparse($backing_file, ".xz");
159 38 if ($ext =~ qr /.xz/) {
160 38 die 'unxz was not found in PATH' unless defined which('unxz');
161   bmwqemu::diag("Extracting XZ compressed file");
162 38 runcmd('nice', 'ionice', 'unxz', '-k', '-f', $backing_file);
163   $backing_file = $path . $name;
164 38 }
165 8 $size //= $self->get_img_size($backing_file);
166   $drive = $bdc->add_existing_drive($node_id, $backing_file, $hdd_model, $size, $num_queues);
167 8 } else {
168 8 $size //= $vars->{HDDSIZEGB} . 'G';
169 0 $drive = $bdc->add_new_drive($node_id, $hdd_model, $size, $num_queues);
170 0 }
171 0  
172 0 if ($i == 1 && $bootfrom eq 'disk') {
173   $drive->bootindex(0);
174 8 }
175 8  
176   $drive->serial($hdd_serial);
177 30 if ($vars->{MULTIPATH}) {
178 30 for my $c (0 .. $vars->{PATHCNT} - 1) {
179   $bdc->add_path_to_drive("path$c",
180   $drive,
181 38 $scsi_ctrs[$c % 2]);
182 18 }
183   }
184   }
185 38  
186 38 my $iso = $vars->{ISO};
187 4 if ($iso) {
188 8 $iso = path($iso)->to_abs;
189   my $size = $self->get_img_size($iso);
190   if ($vars->{USBBOOT}) {
191   $size = $vars->{USBSIZEGB} . 'G' if $vars->{USBSIZEGB};
192   my $drive = $bdc->add_iso_drive('usbstick', $iso, 'usb-storage', $size);
193   $drive->bootindex(0) if $bootfrom ne "disk";
194   }
195 37 else {
196 37 my $drive = $bdc->add_iso_drive('cd0', $iso, $vars->{CDMODEL}, $size);
197 14 $drive->serial('cd0');
198 14 $drive->bootindex(0) if $bootfrom eq "cdrom";
199 12 }
200 0 }
201 0 my $is_first = 1;
202 0 for my $k (sort grep { /^ISO_\d+$/ } keys %$vars) {
203   next unless $vars->{$k};
204   my $addoniso = path($vars->{$k})->to_abs;
205 12 my $i = $k;
206 12 $i =~ s/^ISO_//;
207 12  
208   my $size = $self->get_img_size($addoniso);
209   my $drive = $bdc->add_iso_drive("cd$i", $addoniso, $vars->{CDMODEL}, $size);
210 35 $drive->serial("cd$i");
211 35 # first connected cdrom gets ",bootindex=0 when booting from cdrom and
  547  
212 5 # there wasn't `ISO` defined
213 4 if ($is_first && $bootfrom eq "cdrom" && !$iso) {
214 4 $drive->bootindex(0);
215 4 $is_first = 0;
216   }
217 4 }
218 4  
219 4 return $self;
220   }
221    
222 4 =head3 configure_pflash
223 0  
224 0 Configure a pair of pflash drives which contain the UEFI firmware code and
225   variables. Unfortunately pflash drives are handled differently by QEMU than
226   other block devices which slightly complicates things. See BlockDevConf.pm.
227    
228 35 =cut
229   my $bdc = $self->blockdev_conf;
230    
231   return $self unless $vars->{UEFI};
232    
233   if ($vars->{UEFI_PFLASH}) {
234   die 'Mixing old and new PFLASH variables'
235   if ($vars->{UEFI_PFLASH_CODE} || $vars->{UEFI_PFLASH_VARS});
236    
237   my $file = $vars->{BIOS};
238 34 $bdc->add_pflash_drive('pflash', $file, $self->get_img_size($file));
  34  
  34  
  34  
239 34 return;
240   }
241 34  
242   my $fw = $vars->{UEFI_PFLASH_CODE};
243 8 if ($fw) {
244   $bdc->add_pflash_drive('pflash-code', $fw, $self->get_img_size($fw))
245 0 ->unit(0)
246   ->readonly('on');
247 0  
248 0 $fw = path($vars->{UEFI_PFLASH_VARS})->to_abs;
249 0 die 'Need UEFI_PFLASH_VARS with UEFI_PFLASH_CODE' unless $fw;
250   $bdc->add_pflash_drive('pflash-vars', $fw, $self->get_img_size($fw))
251   ->unit(1);
252 8 }
253 8 elsif ($vars->{UEFI_PFLASH_VARS}) {
254 6 die 'Need UEFI_PFLASH_CODE with UEFI_PFLASH_VARS';
255   }
256    
257   return $self;
258 6 }
259 6  
260 6 =head3 gen_cmdline
261    
262   Generate the QEMU command line arguments from our object model.
263    
264 0 =cut
265   return ($self->qemu_bin,
266   @{$self->_static_params},
267 8 map { $_->gen_cmdline() } @{$self->_mut_params});
268   }
269    
270   =head3 init_blockdev_images
271    
272   Create and delete storage device images based on the current state of the
273   object model e.g. if block devices are marked as needs_creating then this will
274   create them.
275 37  
  37  
  37  
276   This should only be called when QEMU is not running.
277 37  
278 37 =cut
  111  
  37  
279   for my $file ($self->blockdev_conf->gen_unlink_list()) {
280   no autodie 'unlink';
281   unlink($file) if -e $file;
282   }
283    
284   my $tries = $ENV{QEMU_IMG_CREATE_TRIES} // 3;
285   for my $qicmd ($self->blockdev_conf->gen_qemu_img_cmdlines()) {
286   for (1 .. $tries) {
287   undef $@;
288   eval { runcmd($self->qemu_img_bin, @$qicmd) };
289   last unless $@;
290 12 bmwqemu::diag("init_blockdev_images: '@$qicmd' failed: $@, try $_ out of $tries");
  12  
  12  
291 12 }
292 16 die "init_blockdev_images: '@$qicmd' failed after $tries tries: $@" if $@;
  16  
  16  
293 18 }
294    
295   bmwqemu::diag('init_blockdev_images: Finished creating block devices');
296 12 $self->blockdev_conf->mark_all_created();
297 12 }
298 16  
299 18 =head3 export_blockdev_images
300 18  
  18  
301 18 Convert some of the drive devices' block device chains to single qcow2
302 4 images. This amalgamates all the differential snapshots (overlay images) into
303   a single qcow2 file. This is used to publish 'hard drive' and pflash
304 16 images. This function uses qemu-img so can not be used while QEMU is running.
305    
306   Note that this just exports the block device images which do not contain the
307 10 VM RAM state. In order to publish a snapshot of a running machine you also
308 10 need to export the VM state (migration) file, which is effectively the same
309   thing as performing an offline migration.
310    
311   =cut
312   my $count = 0;
313    
314   for my $qicmd ($self->blockdev_conf->gen_qemu_img_convert($filter, $img_dir, $name, $qemu_compress_qcow)) {
315   runcmd('nice', 'ionice', $self->qemu_img_bin, @$qicmd);
316    
317   my $img = "$img_dir/$name";
318   my $exp_format = OpenQA::Qemu::DriveDevice::QEMU_IMAGE_FORMAT;
319   my $format = $self->get_img_format($img);
320   die "'$format': unexpected format for '$img' (expected '$exp_format'), maybe snapshotting failed" unless $format eq $exp_format;
321    
322   $count++;
323   }
324 2  
  2  
  2  
  2  
  2  
  2  
  2  
325 2 return $count;
326   }
327 2  
328 2 =head3 gen_runfile
329    
330 2 Create a shell script which will execute QEMU with the same parameters which
331 2 we are using.
332 2  
333 2 =cut
334   open(my $cmdfd, '>', 'runqemu');
335 2 print $cmdfd "#!/bin/bash\n";
336   my @args;
337   for my $arg ($self->_static_params) {
338 2 $arg =~ s,\\,\\\\,g;
339   $arg =~ s,\$,\\\$,g;
340   $arg =~ s,\",\\\",g;
341   $arg =~ s,\`,\\\`,;
342   push(@args, "\"$arg\"");
343   }
344   printf $cmdfd "%s \\\n %s \\\n \"\$@\"\n", $self->qemu_bin, join(" \\\n ", @args);
345   close $cmdfd;
346   chmod 0755, 'runqemu';
347 0 }
  0  
  0  
348 0  
349 0 my @params = $self->gen_cmdline();
350 0 session->enable;
351 0 bmwqemu::diag('starting: ' . join(' ', @params));
352 0 session->enable_subreaper;
353 0  
354 0 my $process = $self->_process;
355 0 $self->{_qemu_terminated} = 0;
356 0 $process->on(
357   collected => sub {
358 0 $self->{_qemu_terminated} = 1;
359 0 unless ($self->{_stopping}) {
360 0 my $msg = 'QEMU exited unexpectedly, see log for details';
361   $msg = 'QEMU was killed due to the system being out of memory' if ($self->check_qemu_oom == 0);
362   bmwqemu::serialize_state(component => 'backend', msg => $msg);
363 10 }
  10  
  10  
364 10 });
365 10 $process->code(sub {
366 10 $SIG{__DIE__} = undef; # overwrite the default - just exit
367 10 system $self->qemu_bin, '-version';
368   # don't try to talk to the host's PA
369 10 $ENV{QEMU_AUDIO_DRV} = "none";
370 10 exec(@params);
371   });
372   $process->separate_err(0)->start();
373 4  
374 4 fcntl($process->read_stream, Fcntl::F_SETFL, Fcntl::O_NONBLOCK) or die "can't setfl(): $!\n";
375 2 return $process->read_stream;
376 2 }
377 2  
378   $self->{_stopping} = 1;
379 10 $self->_process->stop;
380   }
381 5  
382 5  
383    
384 5 =head3 connect_qmp
385 5  
386 10 Connect to QEMU's QMP command socket so that we can control QEMU's execution
387 10 using the JSON QAPI. QMP and QAPI are documented in the QEMU source tree.
388    
389 5 =cut
390 5 my $sk;
391   osutils::attempt {
392   attempts => $ENV{QEMU_QMP_CONNECT_ATTEMPTS} // 20,
393 4 condition => sub () { $sk },
  4  
  4  
394 4 or => sub () { die "Can't open QMP socket" },
395 4 cb => sub () {
396   die "QEMU terminated before QMP connection could be established. Check for errors below\n" if $self->{_qemu_terminated};
397   $sk = IO::Socket::UNIX->new(
398 2 Type => IO::Socket::UNIX::SOCK_STREAM,
  2  
  2  
  2  
399   Peer => 'qmp_socket',
400 1 Blocking => 0
  1  
  1  
  1  
401   );
402   },
403   };
404    
405   $sk->autoflush(1);
406   binmode $sk;
407   my $flags = fcntl($sk, Fcntl::F_GETFL, 0) or die "Can't get file status flags of QMP socket: $!\n";
408 3 $flags = fcntl($sk, Fcntl::F_SETFL, $flags | Fcntl::O_NONBLOCK) or die "Can't set file status flags of QMP socket: $!\n";
  3  
  3  
409 3 return $sk;
410   }
411 19  
412 19 =head3 revert_to_snapshot
  19  
413 0  
  0  
  0  
414 15 Roll back the SUT to a previous state, including its temporary and permanent
  15  
415 15 storage as well as its CPU.
416 14  
417   =cut
418   my $bdc = $self->blockdev_conf;
419    
420   my $snapshot = $self->snapshot_conf->revert_to_snapshot($name);
421   $bdc->for_each_drive(sub ($drive) {
422 3 my $del_files = $bdc->revert_to_snapshot($drive, $snapshot);
423    
424 2 die "Snapshot $name not found for " . $drive->id unless defined($del_files);
425 2  
426 2 for my $file (@$del_files) {
427 2 bmwqemu::diag("Unlinking $file");
428 2 POSIX::remove($file);
429   }
430   });
431    
432   $self->init_blockdev_images();
433    
434   return $snapshot;
435   }
436    
437 0 =head3 serialise_state
  0  
  0  
  0  
438 0  
439   Serialise our object model of QEMU to JSON and return the JSON text.
440 0  
441 0 =cut
  0  
  0  
442 0 return encode_json({
443   blockdev_conf => $self->blockdev_conf->to_map(),
444 0 controller_conf => $self->controller_conf->to_map(),
445   snapshot_conf => $self->snapshot_conf->to_map(),
446 0 });
447 0 }
448 0  
449   =head3 save_state
450 0  
451   Save our object model of QEMU to a file.
452 0  
453   =cut
454 0 if ($self->has_state) {
455   bmwqemu::fctinfo('Saving QEMU state to ' . STATE_FILE);
456   path(STATE_FILE)->spurt($self->serialise_state());
457   } else {
458   bmwqemu::fctinfo('Refusing to save an empty state file to avoid overwriting a useful one');
459   }
460    
461   return $self;
462 14 }
  14  
  14  
463 14  
464   =head3 deserialise_state
465    
466   Deserialise our object model from a string of JSON text.
467    
468   =cut
469   my $state_map = decode_json($json);
470    
471   $self->snapshot_conf->from_map($state_map->{snapshot_conf});
472   $self->controller_conf->from_map($state_map->{controller_conf});
473   $self->blockdev_conf->from_map($state_map->{blockdev_conf},
474   $self->controller_conf,
475 4 $self->snapshot_conf);
  4  
  4  
476 4  
477 4 return $self;
478 4 }
479    
480 0 =head3 load_state
481    
482   Load our object model of QEMU from a file.
483 4  
484   =cut
485   # In order to remove this, you need to merge the new state with the
486   # existing state from disk without breaking existing snapshots (block
487   # devices).
488   die 'Trying to load state on top of existing state'
489   if $self->has_state;
490    
491 10 return $self->deserialise_state(path(STATE_FILE)->slurp());
  10  
  10  
  10  
492 10 }
493    
494 10 =head3 has_state
495 10  
496   Returns true if our object model of QEMU has been populated with non-default
497 10 state.
498    
499   =cut
500 10  
501   1;