3 # Copyright (C) 2009 Red Hat Inc.
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
28 eval "use Sys::Virt;";
34 virt-inspector - Display OS version, kernel, drivers, mount points, applications, etc. in a virtual machine
38 virt-inspector [--connect URI] domname
40 virt-inspector guest.img [guest.img ...]
44 B<virt-inspector> examines a virtual machine and tries to determine
45 the version of the OS, the kernel version, what drivers are installed,
46 whether the virtual machine is fully virtualized (FV) or
47 para-virtualized (PV), what applications are installed and more.
49 Virt-inspector can produce output in several formats, including a
50 readable text report, and XML for feeding into other programs.
52 Virt-inspector should only be run on I<inactive> virtual machines.
53 The program tries to determine that the machine is inactive and will
54 refuse to run if it thinks you are trying to inspect a running domain.
56 In the normal usage, use C<virt-inspector domname> where C<domname> is
57 the libvirt domain (see: C<virsh list --all>).
59 You can also run virt-inspector directly on disk images from a single
60 virtual machine. Use C<virt-inspector guest.img>. In rare cases a
61 domain has several block devices, in which case you should list them
62 one after another, with the first corresponding to the guest's
63 C</dev/sda>, the second to the guest's C</dev/sdb> and so on.
65 Virt-inspector can only inspect and report upon I<one domain at a
66 time>. To inspect several virtual machines, you have to run
67 virt-inspector several times (for example, from a shell script
70 Because virt-inspector needs direct access to guest images, it won't
71 normally work over remote libvirt connections.
89 =item B<--connect URI> | B<-c URI>
91 If using libvirt, connect to the given I<URI>. If omitted,
92 then we connect to the default libvirt hypervisor.
94 Libvirt is only used if you specify a C<domname> on the
95 command line. If you specify guest block devices directly,
96 then libvirt is not used at all.
104 Force reading a particular guest even if it appears to
105 be active, or if the guest image is writable. This is
106 dangerous and can even corrupt the guest image.
112 =item B<--text> (default)
122 Select the output format. The default is a readable text report.
124 If you select I<--xml> then you get XML output which can be fed
127 If you select I<--perl> then you get Perl structures output which
128 can be used directly in another Perl program.
130 If you select I<--fish> then we print a L<guestfish(1)> command
131 line which will automatically mount up the filesystems on the
132 correct mount points. Try this for example:
134 eval `virt-inspector --fish guest.img`
136 I<--ro-fish> is the same, but the I<--ro> option is passed to
137 guestfish so that the filesystems are mounted read-only.
143 GetOptions ("help|?" => \$help,
144 "connect|c=s" => \$uri,
146 "xml" => sub { $output = "xml" },
147 "perl" => sub { $output = "perl" },
148 "fish" => sub { $output = "fish" },
149 "guestfish" => sub { $output = "fish" },
150 "ro-fish" => sub { $output = "ro-fish" },
151 "ro-guestfish" => sub { $output = "ro-fish" })
153 pod2usage (1) if $help;
154 pod2usage ("$0: no image or VM names given") if @ARGV == 0;
156 # Domain name or guest image(s)?
162 # Until we get an 'add_drive_ro' call, we must check that qemu
163 # will only open this image in readonly mode.
164 # XXX Remove this hack at some point ... or at least push it
169 die "guest image $_ does not exist or is not readable\n"
170 } elsif (-w $_ && !$force) {
171 die ("guest image $_ is writable! REFUSING TO PROCEED.\n".
172 "You can use --force to override this BUT that action\n".
173 "MAY CORRUPT THE DISK IMAGE.\n");
177 die "no libvirt support (install Sys::Virt)"
178 unless exists $INC{"Sys/Virt.pm"};
180 pod2usage ("$0: too many domains listed on command line") if @ARGV > 1;
184 $vmm = Sys::Virt->new (uri => $uri, readonly => 1);
186 $vmm = Sys::Virt->new (readonly => 1);
188 die "cannot connect to libvirt $uri\n" unless $vmm;
190 my @doms = $vmm->list_defined_domains ();
193 if ($_->get_name () eq $ARGV[0]) {
198 die "$ARGV[0] is not the name of an inactive libvirt domain\n"
201 # Get the names of the image(s).
202 my $xml = $dom->get_xml_description ();
204 my $p = new XML::XPath::XMLParser (xml => $xml);
205 my $disks = $p->find ("//devices/disk");
207 foreach ($disks->get_nodelist) {
208 print XML::XPath::XMLParser::as_string($_);
214 # We've now got the list of @images, so feed them to libguestfs.
215 my $g = Sys::Guestfs->new ();
216 $g->add_drive ($_) foreach @images;
220 # We want to get the list of LVs and partitions (ie. anything that
221 # could contain a filesystem). Discard any partitions which are PVs.
222 my @partitions = $g->list_partitions ();
223 my @pvs = $g->pvs ();
227 return 1 if $_ eq $t;
231 @partitions = grep { ! is_pv ($_) } @partitions;
233 my @lvs = $g->lvs ();
239 Linux (distro + version)
243 +--- Filesystems ---------- Installed apps --- Kernel & drivers
244 ----------- -------------- ----------------
245 mount point => device List of apps Extra information
246 mount point => device and versions about kernel(s)
249 (plus lots of extra information
250 about each filesystem)
252 The output of virt-inspector is a complex two-level data structure.
254 At the top level is a list of the operating systems installed on the
255 guest. (For the vast majority of guests, only a single OS is
256 installed.) The data returned for the OS includes the name (Linux,
257 Windows), the distribution and version.
259 The diagram above shows what we return for each OS.
261 With the I<--xml> option the output is mapped into an XML document.
262 Unfortunately there is no clear schema for this document
263 (contributions welcome) but you can get an idea of the format by
264 looking at other documents and as a last resort the source for this
267 With the I<--fish> or I<--ro-fish> option the mount points are mapped to
268 L<guestfish(1)> command line parameters, so that you can go in
269 afterwards and inspect the guest with everything mounted in the
270 right place. For example:
272 eval `virt-inspector --ro-fish guest.img`
273 ==> guestfish --ro -a guest.img -m /dev/VG/LV:/ -m /dev/sda1:/boot
277 # List of possible filesystems.
278 my @devices = sort (@lvs, @partitions);
280 # Now query each one to build up a picture of what's in it.
281 my %fses = map { $_ => check_fs ($_) } @devices;
283 # Now the complex checking code itself.
284 # check_fs takes a device name (LV or partition name) and returns
285 # a hashref containing everything we can find out about the device.
288 my $dev = shift; # LV or partition name.
290 my %r; # Result hash.
292 # First try 'file(1)' on it.
293 my $file = $g->file ($dev);
294 if ($file =~ /ext2 filesystem data/) {
297 } elsif ($file =~ /ext3 filesystem data/) {
300 } elsif ($file =~ /ext4 filesystem data/) {
303 } elsif ($file =~ m{Linux/i386 swap file}) {
309 # If it's ext2/3/4, then we want the UUID and label.
310 if (exists $r{fstype} && $r{fstype} =~ /^ext/) {
311 $r{uuid} = $g->get_e2uuid ($dev);
312 $r{label} = $g->get_e2label ($dev);
315 # Try mounting it, fnarrr.
317 $r{is_mountable} = 1;
318 eval { $g->mount_ro ($dev, "/") };
320 # It's not mountable, probably empty or some format
321 # we don't understand.
322 $r{is_mountable} = 0;
327 if ($g->is_file ("/grub/menu.lst") ||
328 $g->is_file ("/grub/grub.conf")) {
329 $r{content} = "linux-grub";
335 if ($g->is_dir ("/etc") && $g->is_dir ("/bin") &&
336 $g->is_file ("/etc/fstab")) {
337 $r{content} = "linux-root";
339 check_linux_root (\%r);
344 if ($g->is_dir ("/etc") && $g->is_dir ("/bin") &&
345 $g->is_dir ("/share") && !$g->exists ("/local") &&
346 !$g->is_file ("/etc/fstab")) {
347 $r{content} = "linux-usrlocal";
352 if ($g->is_dir ("/etc") && $g->is_dir ("/bin") &&
353 $g->is_dir ("/share") && $g->exists ("/local") &&
354 !$g->is_file ("/etc/fstab")) {
355 $r{content} = "linux-usr";
360 if ($g->is_file ("/AUTOEXEC.BAT") ||
361 $g->is_file ("/autoexec.bat") ||
362 $g->is_dir ("/Program Files") ||
363 $g->is_dir ("/WINDOWS") ||
364 $g->is_file ("/ntldr")) {
365 $r{fstype} = "ntfs"; # XXX this is a guess
366 $r{fsos} = "windows";
367 $r{content} = "windows-root";
369 check_windows_root (\%r);
384 # Look into /etc to see if we recognise the operating system.
385 if ($g->is_file ("/etc/redhat-release")) {
386 $_ = $g->cat ("/etc/redhat-release");
387 if (/Fedora release (\d+\.\d+)/) {
388 $r->{osdistro} = "fedora";
389 $r->{osversion} = "$1"
390 } elsif (/(Red Hat Enterprise Linux|CentOS|Scientific Linux).*release (\d+).*Update (\d+)/) {
391 $r->{osdistro} = "redhat";
392 $r->{osversion} = "$2.$3";
393 } elsif (/(Red Hat Enterprise Linux|CentOS|Scientific Linux).*release (\d+(?:\.(\d+))?)/) {
394 $r->{osdistro} = "redhat";
395 $r->{osversion} = "$2";
397 $r->{osdistro} = "redhat";
399 } elsif ($g->is_file ("/etc/debian_version")) {
400 $_ = $g->cat ("/etc/debian_version");
402 $r->{osdistro} = "debian";
403 $r->{osversion} = "$1";
405 $r->{osdistro} = "debian";
409 # Parse the contents of /etc/fstab. This is pretty vital so
410 # we can determine where filesystems are supposed to be mounted.
411 eval "\$_ = \$g->cat ('/etc/fstab');";
413 my @lines = split /\n/;
416 my @fields = split /[ \t]+/;
418 my $spec = $fields[0]; # first column (dev/label/uuid)
419 my $file = $fields[1]; # second column (mountpoint)
420 if ($spec =~ m{^/} ||
421 $spec =~ m{^LABEL=} ||
422 $spec =~ m{^UUID=} ||
424 push @fstab, [$spec, $file]
428 $r->{fstab} = \@fstab if @fstab;
432 sub check_windows_root
437 # XXX Windows version.
438 # List of applications.
446 # XXX Kernel versions, grub version.
449 #print Dumper (\%fses);
451 #----------------------------------------------------------------------
452 # Now find out how many operating systems we've got. Usually just one.
456 foreach (sort keys %fses) {
457 if ($fses{$_}->{is_root}) {
462 get_os_version (\%r);
463 assign_mount_points (\%r);
473 $r->{os} = $r->{root}->{fsos} if exists $r->{root}->{fsos};
474 $r->{distro} = $r->{root}->{osdistro} if exists $r->{root}->{osdistro};
475 $r->{version} = $r->{root}->{osversion} if exists $r->{root}->{osversion};
478 sub assign_mount_points
483 $r->{mounts} = { "/" => $r->{root_device} };
484 $r->{filesystems} = { $r->{root_device} => $r->{root} };
486 # Use /etc/fstab if we have it to mount the rest.
487 if (exists $r->{root}->{fstab}) {
488 my @fstab = @{$r->{root}->{fstab}};
490 my ($spec, $file) = @$_;
492 my ($dev, $fs) = find_filesystem ($spec);
494 $r->{mounts}->{$file} = $dev;
495 $r->{filesystems}->{$dev} = $fs;
496 if (exists $fs->{used}) {
506 # Find filesystem by device name, LABEL=.. or UUID=..
513 foreach (sort keys %fses) {
514 if (exists $fses{$_}->{label} &&
515 $fses{$_}->{label} eq $label) {
516 return ($_, $fses{$_});
519 warn "unknown filesystem label $label\n";
521 } elsif (/^UUID=(.*)/) {
523 foreach (sort keys %fses) {
524 if (exists $fses{$_}->{uuid} &&
525 $fses{$_}->{uuid} eq $uuid) {
526 return ($_, $fses{$_});
529 warn "unknown filesystem UUID $uuid\n";
532 return ($_, $fses{$_}) if exists $fses{$_};
534 if (m{^/dev/hd(.*)} && exists $fses{"/dev/sd$1"}) {
535 return ("/dev/sd$1", $fses{"/dev/sd$1"});
537 if (m{^/dev/xvd(.*)} && exists $fses{"/dev/sd$1"}) {
538 return ("/dev/sd$1", $fses{"/dev/sd$1"});
541 return () if m{/dev/cdrom};
543 warn "unknown filesystem $_\n";
548 #print Dumper(\%oses);
550 #----------------------------------------------------------------------
551 # Mount up the disks so we can check for applications
552 # and kernels. Skip this if the output is "*fish" because
553 # we don't need to know.
555 if ($output !~ /.*fish$/) {
557 foreach $root_dev (sort keys %oses) {
558 my $mounts = $oses{$root_dev}->{mounts};
559 # Have to mount / first. Luckily '/' is early in the ASCII
560 # character set, so this should be OK.
561 foreach (sort keys %$mounts) {
562 $g->mount_ro ($mounts->{$_}, $_)
563 if $_ ne "swap" && ($_ eq '/' || $g->is_dir ($_));
566 check_for_applications ($root_dev);
567 check_for_kernels ($root_dev);
569 # umount_all in libguestfs is buggy - it doesn't unmount
570 # filesystems in the correct order. So let's unmount them
571 # in reverse first before calling umount_all as a last resort.
572 foreach (sort { $b cmp $a } keys %$mounts) {
573 eval "\$g->umount ('$_')";
579 sub check_for_applications
582 my $root_dev = shift;
584 # XXX rpm -qa, look in Program Files, or whatever
587 sub check_for_kernels
590 my $root_dev = shift;
595 #----------------------------------------------------------------------
598 if ($output eq "fish" || $output eq "ro-fish") {
599 my @osdevs = keys %oses;
600 # This only works if there is a single OS.
601 die "--fish output is only possible with a single OS\n" if @osdevs != 1;
603 my $root_dev = $osdevs[0];
606 if ($output eq "ro-fish") {
610 print " -a $_" foreach @images;
612 my $mounts = $oses{$root_dev}->{mounts};
613 # Have to mount / first. Luckily '/' is early in the ASCII
614 # character set, so this should be OK.
615 foreach (sort keys %$mounts) {
616 print " -m $mounts->{$_}:$_" if $_ ne "swap";
622 elsif ($output eq "perl") {
623 print Dumper(\%oses);
626 # Plain text output (the default).
627 elsif ($output eq "text") {
632 elsif ($output eq "xml") {
638 output_text_os ($oses{$_}) foreach sort keys %oses;
645 print $os->{os}, " " if exists $os->{os};
646 print $os->{distro}, " " if exists $os->{distro};
647 print $os->{version}, " " if exists $os->{version};
648 print "on ", $os->{root_device}, ":\n";
650 print " Mountpoints:\n";
651 my $mounts = $os->{mounts};
652 foreach (sort keys %$mounts) {
653 printf " %-30s %s\n", $mounts->{$_}, $_
656 print " Filesystems:\n";
657 my $filesystems = $os->{filesystems};
658 foreach (sort keys %$filesystems) {
660 print " label: $filesystems->{$_}{label}\n"
661 if exists $filesystems->{$_}{label};
662 print " UUID: $filesystems->{$_}{uuid}\n"
663 if exists $filesystems->{$_}{uuid};
664 print " type: $filesystems->{$_}{fstype}\n"
665 if exists $filesystems->{$_}{fstype};
666 print " content: $filesystems->{$_}{content}\n"
667 if exists $filesystems->{$_}{content};
676 print "<operatingsystems>\n";
677 output_xml_os ($oses{$_}) foreach sort keys %oses;
678 print "</operatingsystems>\n";
685 print "<operatingsystem>\n";
687 print "<os>", $os->{os}, "</os>\n" if exists $os->{os};
688 print "<distro>", $os->{distro}, "</distro>\n" if exists $os->{distro};
689 print "<version>", $os->{version}, "</version>\n" if exists $os->{version};
690 print "<root>", $os->{root_device}, "</root>\n";
692 print "<mountpoints>\n";
693 my $mounts = $os->{mounts};
694 foreach (sort keys %$mounts) {
695 printf "<mountpoint dev='%s'>%s</mountpoint>\n",
698 print "</mountpoints>\n";
700 print "<filesystems>\n";
701 my $filesystems = $os->{filesystems};
702 foreach (sort keys %$filesystems) {
703 print "<filesystem dev='$_'>\n";
704 print "<label>$filesystems->{$_}{label}</label>\n"
705 if exists $filesystems->{$_}{label};
706 print "<uuid>$filesystems->{$_}{uuid}</uuid>\n"
707 if exists $filesystems->{$_}{uuid};
708 print "<type>$filesystems->{$_}{fstype}</type>\n"
709 if exists $filesystems->{$_}{fstype};
710 print "<content>$filesystems->{$_}{content}</content>\n"
711 if exists $filesystems->{$_}{content};
712 print "</filesystem>\n";
714 print "</filesystems>\n";
718 print "</operatingsystem>\n";
730 Richard W.M. Jones L<http://et.redhat.com/~rjones/>
734 Copyright (C) 2009 Red Hat Inc.
736 This program is free software; you can redistribute it and/or modify
737 it under the terms of the GNU General Public License as published by
738 the Free Software Foundation; either version 2 of the License, or
739 (at your option) any later version.
741 This program is distributed in the hope that it will be useful,
742 but WITHOUT ANY WARRANTY; without even the implied warranty of
743 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
744 GNU General Public License for more details.
746 You should have received a copy of the GNU General Public License
747 along with this program; if not, write to the Free Software
748 Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.