#!/usr/bin/perl -w # virt-inspector # Copyright (C) 2010 Red Hat Inc. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. use warnings; use strict; use Sys::Guestfs; use Sys::Guestfs::Lib qw(open_guest); use Pod::Usage; use Getopt::Long; use File::Temp qw/tempfile/; use File::Basename; use XML::Writer; use Locale::TextDomain 'libguestfs'; =encoding utf8 =head1 NAME virt-inspector - Display operating system version and other information about a virtual machine =head1 SYNOPSIS virt-inspector [--connect URI] domname virt-inspector guest.img [guest.img ...] =head1 DESCRIPTION B examines a virtual machine or disk image and tries to determine the version of the operating system and other information about the virtual machine. Virt-inspector produces XML output for feeding into other programs. In the normal usage, use C where C is the libvirt domain (see: C). You can also run virt-inspector directly on disk images from a single virtual machine. Use C. In rare cases a domain has several block devices, in which case you should list them one after another, with the first corresponding to the guest's C, the second to the guest's C and so on. Virt-inspector can only inspect and report upon I. To inspect several virtual machines, you have to run virt-inspector several times (for example, from a shell script for-loop). Because virt-inspector needs direct access to guest images, it won't normally work over remote libvirt connections. =head1 OPTIONS =over 4 =cut my $help; =item B<--help> Display brief help. =cut my $version; =item B<--version> Display version number and exit. =cut my $uri; =item B<--connect URI> | B<-c URI> If using libvirt, connect to the given I. If omitted, then we connect to the default libvirt hypervisor. Libvirt is only used if you specify a C on the command line. If you specify guest block devices directly, then libvirt is not used at all. =cut my $format; =item B<--format> raw Specify the format of disk images given on the command line. If this is omitted then the format is autodetected from the content of the disk image. If disk images are requested from libvirt, then this program asks libvirt for this information. In this case, the value of the format parameter is ignored. If working with untrusted raw-format guest disk images, you should ensure the format is always specified. =back =cut GetOptions ("help|?" => \$help, "version" => \$version, "connect|c=s" => \$uri, "format=s" => \$format, ) or pod2usage (2); pod2usage (1) if $help; if ($version) { my $g = Sys::Guestfs->new (); my %h = $g->version (); print "$h{major}.$h{minor}.$h{release}$h{extra}\n"; exit } pod2usage (__"virt-inspector: no image or VM names given") if @ARGV == 0; my @args = (\@ARGV); push @args, address => $uri if defined $uri; push @args, format => $format if defined $format; my $g = open_guest (@args); $g->launch (); my @roots = $g->inspect_os (); if (@roots == 0) { die __x("{prog}: No operating system could be detected inside this disk image.\n\nThis may be because the file is not a disk image, or is not a virtual machine\nimage, or because the OS type is not understood by libguestfs.\n\nIf you feel this is an error, please file a bug report including as much\ninformation about the disk image as possible.\n", prog => basename ($0)); } # Start the XML output. my $xml = new XML::Writer (DATA_MODE => 1, DATA_INDENT => 2); $xml->startTag ("operatingsystems"); my $root; foreach $root (@roots) { my %fses = $g->inspect_get_mountpoints ($root); my @fses = sort { length $a <=> length $b } keys %fses; foreach (@fses) { $g->mount_ro ($fses{$_}, $_); } $xml->startTag ("operatingsystem"); # Basic OS fields. $xml->dataElement (root => canonicalize ($root)); my ($s, $distro, $major_version); $s = $g->inspect_get_type ($root); $xml->dataElement (name => $s) if $s ne "unknown"; $s = $g->inspect_get_arch ($root); $xml->dataElement (arch => $s) if $s ne "unknown"; $distro = $g->inspect_get_distro ($root); $xml->dataElement (distro => $distro) if $distro ne "unknown"; $s = $g->inspect_get_product_name ($root); $xml->dataElement (product_name => $s) if $s ne "unknown"; $major_version = $g->inspect_get_major_version ($root); $xml->dataElement (major_version => $major_version); $s = $g->inspect_get_minor_version ($root); $xml->dataElement (minor_version => $s); eval { $s = $g->inspect_get_windows_systemroot ($root); $xml->dataElement (windows_systemroot => $s); }; # Mountpoints. output_mountpoints ($root, \@fses, \%fses); # Filesystems. output_filesystems ($root); # Package format / management and applications. output_applications ($root, $distro, $major_version); $xml->endTag("operatingsystem"); $g->umount_all (); } # End the XML output. $xml->endTag ("operatingsystems"); $xml->end (); sub output_mountpoints { local $_; my $root = shift; my $fskeys = shift; my $fshash = shift; $xml->startTag ("mountpoints"); foreach (@$fskeys) { $xml->dataElement ("mountpoint", $_, dev => canonicalize ($fshash->{$_})); } $xml->endTag ("mountpoints"); } sub output_filesystems { local $_; my $root = shift; $xml->startTag ("filesystems"); my @fses = $g->inspect_get_filesystems ($root); foreach (@fses) { $xml->startTag ("filesystem", dev => canonicalize ($_)); eval { my $type = $g->vfs_type ($_); $xml->dataElement (type => $type) if defined $type && $type ne ""; }; eval { my $label = $g->vfs_label ($_); $xml->dataElement (label => $label) if defined $label && $label ne ""; }; eval { my $uuid = $g->vfs_uuid ($_); $xml->dataElement (uuid => $uuid) if defined $uuid && $uuid ne ""; }; $xml->endTag ("filesystem"); } $xml->endTag ("filesystems"); } sub output_applications { local $_; my $root = shift; my $distro = shift; my $major_version = shift; # Based on the distro, take a guess at the package format # and package management. my ($package_format, $package_management); if (defined $distro) { if ($distro eq "debian") { $package_format = "dpkg"; $package_management = "apt"; } elsif ($distro eq "fedora") { $package_format = "rpm"; $package_management = "yum"; } elsif ($distro =~ /redhat/ || $distro =~ /rhel/) { if ($major_version >= 5) { $package_format = "rpm"; $package_management = "yum"; } else { $package_format = "rpm"; $package_management = "up2date"; } } # else unknown. } $xml->dataElement (package_format => $package_format) if defined $package_format; $xml->dataElement (package_management => $package_management) if defined $package_management; # Do we know how to get a list of applications? if (defined $package_format) { if ($package_format eq "rpm") { output_applications_rpm ($root); } # else no we don't. } } sub output_applications_rpm { local $_; my $root = shift; # Previous virt-inspector ran the 'rpm' program from the guest. # This is insecure, and unnecessary because we can get the same # information directly from the RPM database. my @applications; eval { my ($fh, $filename) = tempfile (UNLINK => 1); my $fddev = "/dev/fd/" . fileno ($fh); $g->download ("/var/lib/rpm/Name", $fddev); close $fh or die "close: $!"; # Read the database with the Berkeley DB dump tool. my $cmd = "db_dump -p '$filename'"; open PIPE, "$cmd |" or die "close: $!"; while () { chomp; last if /^HEADER=END$/; } while () { chomp; last if /^DATA=END$/; # First character on each data line is a space. if (length $_ > 0 && substr ($_, 0, 1) eq ' ') { $_ = substr ($_, 1); } # Name should never contain non-printable chars. die "name contains non-printable chars" if /\\/; push @applications, $_; $_ = ; # discard value } close PIPE or die "close: $!"; }; if (!$@ && @applications > 0) { @applications = sort @applications; $xml->startTag ("applications"); foreach (@applications) { $xml->startTag ("application"); $xml->dataElement (name => $_); $xml->endTag ("application"); } $xml->endTag ("applications"); } } # The reverse of device name translation, see # BLOCK DEVICE NAMING in guestfs(3). sub canonicalize { local $_ = shift; if (m{^/dev/[hv]d([a-z]\d)$}) { return "/dev/sd$1"; } $_; } =head1 SHELL QUOTING Libvirt guest names can contain arbitrary characters, some of which have meaning to the shell such as C<#> and space. You may need to quote or escape these characters on the command line. See the shell manual page L for details. =head1 SEE ALSO L, L, L, L, L, L. =head1 AUTHORS =over 4 =item * Richard W.M. Jones L =item * Matthew Booth L =back =head1 COPYRIGHT Copyright (C) 2010 Red Hat Inc. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.