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_ro ($_) 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} = $_;
632 foreach ($g->find ("/lib/modules/$_")) {
633 if (m,/([^/]+)\.ko,) {
638 $kernel{modules} = \@modules;
640 push @kernels, \%kernel;
644 } elsif ($os eq "windows") {
648 $oses{$root_dev}->{kernels} = \@kernels;
651 #----------------------------------------------------------------------
654 if ($output eq "fish" || $output eq "ro-fish") {
655 my @osdevs = keys %oses;
656 # This only works if there is a single OS.
657 die "--fish output is only possible with a single OS\n" if @osdevs != 1;
659 my $root_dev = $osdevs[0];
662 if ($output eq "ro-fish") {
666 print " -a $_" foreach @images;
668 my $mounts = $oses{$root_dev}->{mounts};
669 # Have to mount / first. Luckily '/' is early in the ASCII
670 # character set, so this should be OK.
671 foreach (sort keys %$mounts) {
672 print " -m $mounts->{$_}:$_" if $_ ne "swap";
678 elsif ($output eq "perl") {
679 print Dumper(\%oses);
682 # Plain text output (the default).
683 elsif ($output eq "text") {
688 elsif ($output eq "xml") {
694 output_text_os ($oses{$_}) foreach sort keys %oses;
701 print $os->{os}, " " if exists $os->{os};
702 print $os->{distro}, " " if exists $os->{distro};
703 print $os->{version}, " " if exists $os->{version};
704 print "on ", $os->{root_device}, ":\n";
706 print " Mountpoints:\n";
707 my $mounts = $os->{mounts};
708 foreach (sort keys %$mounts) {
709 printf " %-30s %s\n", $mounts->{$_}, $_
712 print " Filesystems:\n";
713 my $filesystems = $os->{filesystems};
714 foreach (sort keys %$filesystems) {
716 print " label: $filesystems->{$_}{label}\n"
717 if exists $filesystems->{$_}{label};
718 print " UUID: $filesystems->{$_}{uuid}\n"
719 if exists $filesystems->{$_}{uuid};
720 print " type: $filesystems->{$_}{fstype}\n"
721 if exists $filesystems->{$_}{fstype};
722 print " content: $filesystems->{$_}{content}\n"
723 if exists $filesystems->{$_}{content};
726 print " Applications:\n";
727 my @apps = @{$os->{apps}};
729 print " $_->{name} $_->{version}\n"
733 my @kernels = @{$os->{kernels}};
735 print " $_->{version}\n";
736 my @modules = @{$_->{modules}};
745 print "<operatingsystems>\n";
746 output_xml_os ($oses{$_}) foreach sort keys %oses;
747 print "</operatingsystems>\n";
754 print "<operatingsystem>\n";
756 print "<os>", $os->{os}, "</os>\n" if exists $os->{os};
757 print "<distro>", $os->{distro}, "</distro>\n" if exists $os->{distro};
758 print "<version>", $os->{version}, "</version>\n" if exists $os->{version};
759 print "<root>", $os->{root_device}, "</root>\n";
761 print "<mountpoints>\n";
762 my $mounts = $os->{mounts};
763 foreach (sort keys %$mounts) {
764 printf "<mountpoint dev='%s'>%s</mountpoint>\n",
767 print "</mountpoints>\n";
769 print "<filesystems>\n";
770 my $filesystems = $os->{filesystems};
771 foreach (sort keys %$filesystems) {
772 print "<filesystem dev='$_'>\n";
773 print "<label>$filesystems->{$_}{label}</label>\n"
774 if exists $filesystems->{$_}{label};
775 print "<uuid>$filesystems->{$_}{uuid}</uuid>\n"
776 if exists $filesystems->{$_}{uuid};
777 print "<type>$filesystems->{$_}{fstype}</type>\n"
778 if exists $filesystems->{$_}{fstype};
779 print "<content>$filesystems->{$_}{content}</content>\n"
780 if exists $filesystems->{$_}{content};
781 print "</filesystem>\n";
783 print "</filesystems>\n";
785 print "<applications>\n";
786 my @apps = @{$os->{apps}};
788 print "<application>\n";
789 print "<name>$_->{name}</name><version>$_->{version}</version>\n";
790 print "</application>\n";
792 print "</applications>\n";
795 my @kernels = @{$os->{kernels}};
798 print "<version>$_->{version}</version>\n";
800 my @modules = @{$_->{modules}};
802 print "<module>$_</module>\n";
804 print "</modules>\n";
807 print "</kernels>\n";
809 print "</operatingsystem>\n";
821 Richard W.M. Jones L<http://et.redhat.com/~rjones/>
825 Copyright (C) 2009 Red Hat Inc.
827 This program is free software; you can redistribute it and/or modify
828 it under the terms of the GNU General Public License as published by
829 the Free Software Foundation; either version 2 of the License, or
830 (at your option) any later version.
832 This program is distributed in the hope that it will be useful,
833 but WITHOUT ANY WARRANTY; without even the implied warranty of
834 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
835 GNU General Public License for more details.
837 You should have received a copy of the GNU General Public License
838 along with this program; if not, write to the Free Software
839 Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.