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
445 # Grub version, if we care.
448 #print Dumper (\%fses);
450 #----------------------------------------------------------------------
451 # Now find out how many operating systems we've got. Usually just one.
455 foreach (sort keys %fses) {
456 if ($fses{$_}->{is_root}) {
461 get_os_version (\%r);
462 assign_mount_points (\%r);
472 $r->{os} = $r->{root}->{fsos} if exists $r->{root}->{fsos};
473 $r->{distro} = $r->{root}->{osdistro} if exists $r->{root}->{osdistro};
474 $r->{version} = $r->{root}->{osversion} if exists $r->{root}->{osversion};
477 sub assign_mount_points
482 $r->{mounts} = { "/" => $r->{root_device} };
483 $r->{filesystems} = { $r->{root_device} => $r->{root} };
485 # Use /etc/fstab if we have it to mount the rest.
486 if (exists $r->{root}->{fstab}) {
487 my @fstab = @{$r->{root}->{fstab}};
489 my ($spec, $file) = @$_;
491 my ($dev, $fs) = find_filesystem ($spec);
493 $r->{mounts}->{$file} = $dev;
494 $r->{filesystems}->{$dev} = $fs;
495 if (exists $fs->{used}) {
505 # Find filesystem by device name, LABEL=.. or UUID=..
512 foreach (sort keys %fses) {
513 if (exists $fses{$_}->{label} &&
514 $fses{$_}->{label} eq $label) {
515 return ($_, $fses{$_});
518 warn "unknown filesystem label $label\n";
520 } elsif (/^UUID=(.*)/) {
522 foreach (sort keys %fses) {
523 if (exists $fses{$_}->{uuid} &&
524 $fses{$_}->{uuid} eq $uuid) {
525 return ($_, $fses{$_});
528 warn "unknown filesystem UUID $uuid\n";
531 return ($_, $fses{$_}) if exists $fses{$_};
533 if (m{^/dev/hd(.*)} && exists $fses{"/dev/sd$1"}) {
534 return ("/dev/sd$1", $fses{"/dev/sd$1"});
536 if (m{^/dev/xvd(.*)} && exists $fses{"/dev/sd$1"}) {
537 return ("/dev/sd$1", $fses{"/dev/sd$1"});
540 return () if m{/dev/cdrom};
542 warn "unknown filesystem $_\n";
547 #print Dumper(\%oses);
549 #----------------------------------------------------------------------
550 # Mount up the disks so we can check for applications
551 # and kernels. Skip this if the output is "*fish" because
552 # we don't need to know.
554 if ($output !~ /.*fish$/) {
556 foreach $root_dev (sort keys %oses) {
557 my $mounts = $oses{$root_dev}->{mounts};
558 # Have to mount / first. Luckily '/' is early in the ASCII
559 # character set, so this should be OK.
560 foreach (sort keys %$mounts) {
561 $g->mount_ro ($mounts->{$_}, $_)
562 if $_ ne "swap" && ($_ eq '/' || $g->is_dir ($_));
565 check_for_applications ($root_dev);
566 check_for_kernels ($root_dev);
572 sub check_for_applications
575 my $root_dev = shift;
579 my $os = $oses{$root_dev}->{os};
580 if ($os eq "linux") {
581 my $distro = $oses{$root_dev}->{distro};
582 if ($distro eq "redhat") {
583 my @lines = $g->command_lines
584 (["rpm", "-q", "-a", "--qf",
585 "%{name} %{epoch} %{version} %{release} %{arch}\n"]);
587 if (m/^(.*) (.*) (.*) (.*) (.*)$/) {
589 $epoch = "" if $epoch eq "(none)";
601 } elsif ($os eq "windows") {
603 # I worked out a general plan for this, but haven't
604 # implemented it yet. We can iterate over /Program Files
605 # looking for *.EXE files, which we download, then use
606 # i686-pc-mingw32-windres on, to find the VERSIONINFO
607 # section, which has a lot of useful information.
610 $oses{$root_dev}->{apps} = \@apps;
613 sub check_for_kernels
616 my $root_dev = shift;
620 my $os = $oses{$root_dev}->{os};
621 if ($os eq "linux") {
622 # Installed kernels will have a corresponding /lib/modules/<version>
623 # directory, which is the easiest way to find out what kernels
624 # are installed, and what modules are available.
625 foreach ($g->ls ("/lib/modules")) {
626 if ($g->is_dir ("/lib/modules/$_")) {
628 $kernel{version} = $_;
638 push @kernels, \%kernel;
642 } elsif ($os eq "windows") {
646 $oses{$root_dev}->{kernels} = \@kernels;
649 #----------------------------------------------------------------------
652 if ($output eq "fish" || $output eq "ro-fish") {
653 my @osdevs = keys %oses;
654 # This only works if there is a single OS.
655 die "--fish output is only possible with a single OS\n" if @osdevs != 1;
657 my $root_dev = $osdevs[0];
660 if ($output eq "ro-fish") {
664 print " -a $_" foreach @images;
666 my $mounts = $oses{$root_dev}->{mounts};
667 # Have to mount / first. Luckily '/' is early in the ASCII
668 # character set, so this should be OK.
669 foreach (sort keys %$mounts) {
670 print " -m $mounts->{$_}:$_" if $_ ne "swap";
676 elsif ($output eq "perl") {
677 print Dumper(\%oses);
680 # Plain text output (the default).
681 elsif ($output eq "text") {
686 elsif ($output eq "xml") {
692 output_text_os ($oses{$_}) foreach sort keys %oses;
699 print $os->{os}, " " if exists $os->{os};
700 print $os->{distro}, " " if exists $os->{distro};
701 print $os->{version}, " " if exists $os->{version};
702 print "on ", $os->{root_device}, ":\n";
704 print " Mountpoints:\n";
705 my $mounts = $os->{mounts};
706 foreach (sort keys %$mounts) {
707 printf " %-30s %s\n", $mounts->{$_}, $_
710 print " Filesystems:\n";
711 my $filesystems = $os->{filesystems};
712 foreach (sort keys %$filesystems) {
714 print " label: $filesystems->{$_}{label}\n"
715 if exists $filesystems->{$_}{label};
716 print " UUID: $filesystems->{$_}{uuid}\n"
717 if exists $filesystems->{$_}{uuid};
718 print " type: $filesystems->{$_}{fstype}\n"
719 if exists $filesystems->{$_}{fstype};
720 print " content: $filesystems->{$_}{content}\n"
721 if exists $filesystems->{$_}{content};
724 print " Applications:\n";
725 my @apps = @{$os->{apps}};
727 print " $_->{name} $_->{version}\n"
731 my @kernels = @{$os->{kernels}};
733 print " $_->{version}\n"
742 print "<operatingsystems>\n";
743 output_xml_os ($oses{$_}) foreach sort keys %oses;
744 print "</operatingsystems>\n";
751 print "<operatingsystem>\n";
753 print "<os>", $os->{os}, "</os>\n" if exists $os->{os};
754 print "<distro>", $os->{distro}, "</distro>\n" if exists $os->{distro};
755 print "<version>", $os->{version}, "</version>\n" if exists $os->{version};
756 print "<root>", $os->{root_device}, "</root>\n";
758 print "<mountpoints>\n";
759 my $mounts = $os->{mounts};
760 foreach (sort keys %$mounts) {
761 printf "<mountpoint dev='%s'>%s</mountpoint>\n",
764 print "</mountpoints>\n";
766 print "<filesystems>\n";
767 my $filesystems = $os->{filesystems};
768 foreach (sort keys %$filesystems) {
769 print "<filesystem dev='$_'>\n";
770 print "<label>$filesystems->{$_}{label}</label>\n"
771 if exists $filesystems->{$_}{label};
772 print "<uuid>$filesystems->{$_}{uuid}</uuid>\n"
773 if exists $filesystems->{$_}{uuid};
774 print "<type>$filesystems->{$_}{fstype}</type>\n"
775 if exists $filesystems->{$_}{fstype};
776 print "<content>$filesystems->{$_}{content}</content>\n"
777 if exists $filesystems->{$_}{content};
778 print "</filesystem>\n";
780 print "</filesystems>\n";
782 print "<applications>\n";
783 my @apps = @{$os->{apps}};
785 print "<application>\n";
786 print "<name>$_->{name}</name><version>$_->{version}</version>\n";
787 print "</application>\n";
789 print "</applications>\n";
792 my @kernels = @{$os->{kernels}};
795 print "<version>$_->{version}</version>\n";
804 print "</kernels>\n";
806 print "</operatingsystem>\n";
818 Richard W.M. Jones L<http://et.redhat.com/~rjones/>
822 Copyright (C) 2009 Red Hat Inc.
824 This program is free software; you can redistribute it and/or modify
825 it under the terms of the GNU General Public License as published by
826 the Free Software Foundation; either version 2 of the License, or
827 (at your option) any later version.
829 This program is distributed in the hope that it will be useful,
830 but WITHOUT ANY WARRANTY; without even the implied warranty of
831 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
832 GNU General Public License for more details.
834 You should have received a copy of the GNU General Public License
835 along with this program; if not, write to the Free Software
836 Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.