inspector: List Debian packages.
[libguestfs.git] / inspector / virt-inspector
1 #!/usr/bin/perl -w
2 # virt-inspector
3 # Copyright (C) 2010 Red Hat Inc.
4 #
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.
9 #
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.
14 #
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.
18
19 use warnings;
20 use strict;
21
22 use Sys::Guestfs;
23 use Sys::Guestfs::Lib qw(open_guest);
24 use Pod::Usage;
25 use Getopt::Long;
26 use File::Temp qw/tempfile/;
27 use File::Basename;
28 use XML::Writer;
29 use Locale::TextDomain 'libguestfs';
30
31 =encoding utf8
32
33 =head1 NAME
34
35 virt-inspector - Display operating system version and other information about a virtual machine
36
37 =head1 SYNOPSIS
38
39  virt-inspector [--connect URI] domname
40
41  virt-inspector guest.img [guest.img ...]
42
43 =head1 DESCRIPTION
44
45 B<virt-inspector> examines a virtual machine or disk image and tries
46 to determine the version of the operating system and other information
47 about the virtual machine.
48
49 Virt-inspector produces XML output for feeding into other programs.
50
51 In the normal usage, use C<virt-inspector domname> where C<domname> is
52 the libvirt domain (see: C<virsh list --all>).
53
54 You can also run virt-inspector directly on disk images from a single
55 virtual machine.  Use C<virt-inspector guest.img>.  In rare cases a
56 domain has several block devices, in which case you should list them
57 one after another, with the first corresponding to the guest's
58 C</dev/sda>, the second to the guest's C</dev/sdb> and so on.
59
60 Virt-inspector can only inspect and report upon I<one domain at a
61 time>.  To inspect several virtual machines, you have to run
62 virt-inspector several times (for example, from a shell script
63 for-loop).
64
65 Because virt-inspector needs direct access to guest images, it won't
66 normally work over remote libvirt connections.
67
68 =head1 OPTIONS
69
70 =over 4
71
72 =cut
73
74 my $help;
75
76 =item B<--help>
77
78 Display brief help.
79
80 =cut
81
82 my $version;
83
84 =item B<--version>
85
86 Display version number and exit.
87
88 =cut
89
90 my $uri;
91
92 =item B<--connect URI> | B<-c URI>
93
94 If using libvirt, connect to the given I<URI>.  If omitted,
95 then we connect to the default libvirt hypervisor.
96
97 Libvirt is only used if you specify a C<domname> on the
98 command line.  If you specify guest block devices directly,
99 then libvirt is not used at all.
100
101 =cut
102
103 my $format;
104
105 =item B<--format> raw
106
107 Specify the format of disk images given on the command line.  If this
108 is omitted then the format is autodetected from the content of the
109 disk image.
110
111 If disk images are requested from libvirt, then this program asks
112 libvirt for this information.  In this case, the value of the format
113 parameter is ignored.
114
115 If working with untrusted raw-format guest disk images, you should
116 ensure the format is always specified.
117
118 =back
119
120 =cut
121
122 GetOptions ("help|?" => \$help,
123             "version" => \$version,
124             "connect|c=s" => \$uri,
125             "format=s" => \$format,
126     ) or pod2usage (2);
127 pod2usage (1) if $help;
128 if ($version) {
129     my $g = Sys::Guestfs->new ();
130     my %h = $g->version ();
131     print "$h{major}.$h{minor}.$h{release}$h{extra}\n";
132     exit
133 }
134 pod2usage (__"virt-inspector: no image or VM names given") if @ARGV == 0;
135
136 my @args = (\@ARGV);
137 push @args, address => $uri if defined $uri;
138 push @args, format => $format if defined $format;
139 my $g = open_guest (@args);
140
141 $g->launch ();
142
143 my @roots = $g->inspect_os ();
144 if (@roots == 0) {
145     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",
146             prog => basename ($0));
147 }
148
149 =head1 XML FORMAT
150
151 The virt-inspector XML is described precisely in a RELAX NG schema
152 which is supplied with libguestfs.  This section is just an overview.
153
154 The top-level element is E<lt>operatingsystemsE<gt>, and it contains
155 one or more E<lt>operatingsystemE<gt> elements.  You would only see
156 more than one E<lt>operatingsystemE<gt> element if the virtual machine
157 is multi-boot, which is vanishingly rare in real world VMs.
158
159 =head2 E<lt>operatingsystemE<gt>
160
161 In the E<lt>operatingsystemE<gt> tag are various optional fields that
162 describe the operating system, its architecture, the descriptive
163 "product name" string, the type of OS and so on, as in this example:
164
165  <operatingsystems>
166    <operatingsystem>
167      <root>/dev/sda2</root>
168      <name>windows</name>
169      <arch>i386</arch>
170      <distro>windows</distro>
171      <product_name>Windows 7 Enterprise</product_name>
172      <major_version>6</major_version>
173      <minor_version>1</minor_version>
174      <windows_systemroot>/Windows</windows_systemroot>
175
176 These fields are derived from the libguestfs inspection API, and
177 you can find more details in L<guestfs(3)/INSPECTION>.
178
179 The E<lt>rootE<gt> element is the root filesystem device, but from the
180 point of view of libguestfs (block devices may have completely
181 different names inside the VM itself).
182
183 =cut
184
185 # Start the XML output.
186 my $xml = new XML::Writer (DATA_MODE => 1, DATA_INDENT => 2);
187
188 $xml->startTag ("operatingsystems");
189
190 my $root;
191 foreach $root (@roots) {
192     my %fses = $g->inspect_get_mountpoints ($root);
193     my @fses = sort { length $a <=> length $b } keys %fses;
194     foreach (@fses) {
195         $g->mount_ro ($fses{$_}, $_);
196     }
197
198     $xml->startTag ("operatingsystem");
199
200     # Basic OS fields.
201     $xml->dataElement (root => canonicalize ($root));
202
203     my ($s, $distro, $major_version);
204     $s = $g->inspect_get_type ($root);
205     $xml->dataElement (name => $s) if $s ne "unknown";
206     $s = $g->inspect_get_arch ($root);
207     $xml->dataElement (arch => $s) if $s ne "unknown";
208     $distro = $g->inspect_get_distro ($root);
209     $xml->dataElement (distro => $distro) if $distro ne "unknown";
210     $s = $g->inspect_get_product_name ($root);
211     $xml->dataElement (product_name => $s) if $s ne "unknown";
212     $major_version = $g->inspect_get_major_version ($root);
213     $xml->dataElement (major_version => $major_version);
214     $s = $g->inspect_get_minor_version ($root);
215     $xml->dataElement (minor_version => $s);
216
217     eval {
218         $s = $g->inspect_get_windows_systemroot ($root);
219         $xml->dataElement (windows_systemroot => $s);
220     };
221
222     # Mountpoints.
223     output_mountpoints ($root, \@fses, \%fses);
224
225     # Filesystems.
226     output_filesystems ($root);
227
228     # Package format / management and applications.
229     output_applications ($root, $distro, $major_version);
230
231     $xml->endTag("operatingsystem");
232
233     $g->umount_all ();
234 }
235
236 # End the XML output.
237 $xml->endTag ("operatingsystems");
238 $xml->end ();
239
240 =head2 E<lt>mountpointsE<gt>
241
242 Un*x-like guests typically have multiple filesystems which are mounted
243 at various mountpoints, and these are described in the
244 E<lt>mountpointsE<gt> element which looks like this:
245
246  <operatingsystems>
247    <operatingsystem>
248      ...
249      <mountpoints>
250        <mountpoint dev="/dev/vg_f13x64/lv_root">/</mountpoint>
251        <mountpoint dev="/dev/sda1">/boot</mountpoint>
252      </mountpoints>
253
254 As with E<lt>rootE<gt>, devices are from the point of view of
255 libguestfs, and may have completely different names inside the guest.
256 Only mountable filesystems appear in this list, not things like swap
257 devices.
258
259 =cut
260
261 sub output_mountpoints
262 {
263     local $_;
264     my $root = shift;
265     my $fskeys = shift;
266     my $fshash = shift;
267
268     $xml->startTag ("mountpoints");
269     foreach (@$fskeys) {
270         $xml->dataElement ("mountpoint", $_,
271                            dev => canonicalize ($fshash->{$_}));
272     }
273     $xml->endTag ("mountpoints");
274 }
275
276 =head2 E<lt>filesystemsE<gt>
277
278 E<lt>filesystemsE<gt> is like E<lt>mountpointsE<gt> but covers I<all>
279 filesystems belonging to the guest, including swap and empty
280 partitions.  (In the rare case of a multi-boot guest, it covers
281 filesystems belonging to this OS or shared by this OS and other OSes).
282
283 You might see something like this:
284
285  <operatingsystems>
286    <operatingsystem>
287      ...
288      <filesystems>
289        <filesystem dev="/dev/vg_f13x64/lv_root">
290          <type>ext4</type>
291          <label>Fedora-13-x86_64</label>
292          <uuid>e6a4db1e-15c2-477b-ac2a-699181c396aa</uuid>
293        </filesystem>
294
295 The optional elements within E<lt>filesystemE<gt> are the filesystem
296 type, the label, and the UUID.
297
298 =cut
299
300 sub output_filesystems
301 {
302     local $_;
303     my $root = shift;
304
305     $xml->startTag ("filesystems");
306
307     my @fses = $g->inspect_get_filesystems ($root);
308     foreach (@fses) {
309         $xml->startTag ("filesystem",
310                         dev => canonicalize ($_));
311
312         eval {
313             my $type = $g->vfs_type ($_);
314             $xml->dataElement (type => $type)
315                 if defined $type && $type ne "";
316         };
317
318         eval {
319             my $label = $g->vfs_label ($_);
320             $xml->dataElement (label => $label)
321                 if defined $label && $label ne "";
322         };
323
324         eval {
325             my $uuid = $g->vfs_uuid ($_);
326             $xml->dataElement (uuid => $uuid)
327                 if defined $uuid && $uuid ne "";
328         };
329
330         $xml->endTag ("filesystem");
331     }
332
333     $xml->endTag ("filesystems");
334 }
335
336 =head2 E<lt>applicationsE<gt>
337
338 The related elements E<lt>package_formatE<gt>,
339 E<lt>package_managementE<gt> and E<lt>applicationsE<gt> describe
340 applications installed in the virtual machine.  At the moment we are
341 only able to list RPMs and Debian packages installed, but in future we
342 will support other Linux distros and Windows.
343
344 E<lt>package_formatE<gt>, if present, describes the packaging
345 system used.  Typical values would be C<rpm> and C<deb>.
346
347 E<lt>package_managementE<gt>, if present, describes the package
348 manager.  Typical values include C<yum>, C<up2date> and C<apt>
349
350 E<lt>applicationsE<gt> lists the packages or applications
351 installed.
352
353  <operatingsystems>
354    <operatingsystem>
355      ...
356      <applications>
357        <application>
358          <name>coreutils</name>
359          <version>8.5</version>
360          <release>1</release>
361        </application>
362
363 (The version and release fields may not be available for
364 some package types).
365
366 =cut
367
368 sub output_applications
369 {
370     local $_;
371     my $root = shift;
372     my $distro = shift;
373     my $major_version = shift;
374
375     # Based on the distro, take a guess at the package format
376     # and package management.
377     my ($package_format, $package_management);
378     if (defined $distro) {
379         if ($distro eq "debian") {
380             $package_format = "deb";
381             $package_management = "apt";
382         }
383         elsif ($distro eq "fedora") {
384             $package_format = "rpm";
385             $package_management = "yum";
386         }
387         elsif ($distro =~ /redhat/ || $distro =~ /rhel/) {
388             if ($major_version >= 5) {
389                 $package_format = "rpm";
390                 $package_management = "yum";
391             } else {
392                 $package_format = "rpm";
393                 $package_management = "up2date";
394             }
395         }
396         # else unknown.
397     }
398
399     $xml->dataElement (package_format => $package_format)
400         if defined $package_format;
401     $xml->dataElement (package_management => $package_management)
402         if defined $package_management;
403
404     # Do we know how to get a list of applications?
405     if (defined $package_format) {
406         if ($package_format eq "rpm") {
407             output_applications_rpm ($root);
408         }
409         elsif ($package_format eq "deb") {
410             output_applications_deb ($root);
411         }
412     }
413 }
414
415 sub output_applications_rpm
416 {
417     local $_;
418     my $root = shift;
419
420     # Previous virt-inspector ran the 'rpm' program from the guest.
421     # This is insecure, and unnecessary because we can get the same
422     # information directly from the RPM database.
423
424     my @applications;
425
426     eval {
427         my ($fh, $filename) = tempfile (UNLINK => 1);
428         my $fddev = "/dev/fd/" . fileno ($fh);
429         $g->download ("/var/lib/rpm/Name", $fddev);
430         close $fh or die "close: $!";
431
432         # Read the database with the Berkeley DB dump tool.
433         my $cmd = "db_dump -p '$filename'";
434         open PIPE, "$cmd |" or die "close: $!";
435         while (<PIPE>) {
436             chomp;
437             last if /^HEADER=END$/;
438         }
439         while (<PIPE>) {
440             chomp;
441             last if /^DATA=END$/;
442
443             # First character on each data line is a space.
444             if (length $_ > 0 && substr ($_, 0, 1) eq ' ') {
445                 $_ = substr ($_, 1);
446             }
447             # Name should never contain non-printable chars.
448             die "name contains non-printable chars" if /\\/;
449             push @applications, $_;
450
451             $_ = <PIPE>; # discard value
452         }
453         close PIPE or die "close: $!";
454     };
455     if (!$@ && @applications > 0) {
456         @applications = sort @applications;
457         $xml->startTag ("applications");
458         foreach (@applications) {
459             $xml->startTag ("application");
460             $xml->dataElement (name => $_);
461             $xml->endTag ("application");
462         }
463         $xml->endTag ("applications");
464     }
465 }
466
467 sub output_applications_deb
468 {
469     local $_;
470     my $root = shift;
471
472     my @applications;
473
474     eval {
475         my ($fh, $filename) = tempfile (UNLINK => 1);
476         my $fddev = "/dev/fd/" . fileno ($fh);
477         $g->download ("/var/lib/dpkg/status", $fddev);
478         close $fh or die "close: $!";
479
480         # Read the file.  Each package is separated by a blank line.
481         open FILE, $filename or die "$filename: $!";
482         my ($name, $installed, $version, $release);
483         while (<FILE>) {
484             chomp;
485             if (/^Package: (.*)/) {
486                 $name = $1;
487             } elsif (/^Status: .*\binstalled\b/) {
488                 $installed = 1;
489             } elsif (/^Version: (.*?)-(.*)/) {
490                 $version = $1;
491                 $release = $2;
492             } elsif ($_ eq "") {
493                 if ($installed &&
494                     defined $name && defined $version && defined $release) {
495                     push @applications, [ $name, $version, $release ];
496                 }
497                 $name = undef;
498                 $installed = undef;
499                 $version = undef;
500                 $release = undef;
501             }
502         }
503         close FILE or die "$filename: $!";
504     };
505     if (!$@ && @applications > 0) {
506         @applications = sort { $a->[0] cmp $b->[0] } @applications;
507         $xml->startTag ("applications");
508         foreach (@applications) {
509             $xml->startTag ("application");
510             $xml->dataElement (name => $_->[0]);
511             $xml->dataElement (version => $_->[1]);
512             $xml->dataElement (release => $_->[2]);
513             $xml->endTag ("application");
514         }
515         $xml->endTag ("applications");
516     }
517 }
518
519 # The reverse of device name translation, see
520 # BLOCK DEVICE NAMING in guestfs(3).
521 sub canonicalize
522 {
523     local $_ = shift;
524
525     if (m{^/dev/[hv]d([a-z]\d)$}) {
526         return "/dev/sd$1";
527     }
528     $_;
529 }
530
531 =head1 USING XPATH
532
533 You can use the XPath query language, and/or the xpath tool, in order
534 to select parts of the XML.
535
536 For example:
537
538  $ virt-inspector Guest | xpath //filesystems
539  Found 1 nodes:
540  -- NODE --
541  <filesystems>
542       <filesystem dev="/dev/vg_f13x64/lv_root">
543         <type>ext4</type>
544  [etc]
545
546  $ virt-inspector Guest | \
547      xpath "string(//filesystem[@dev='/dev/sda1']/type)"
548  Query didn't return a nodeset. Value: ext4
549
550 =head1 SHELL QUOTING
551
552 Libvirt guest names can contain arbitrary characters, some of which
553 have meaning to the shell such as C<#> and space.  You may need to
554 quote or escape these characters on the command line.  See the shell
555 manual page L<sh(1)> for details.
556
557 =head1 SEE ALSO
558
559 L<guestfs(3)>,
560 L<guestfish(1)>,
561 L<Sys::Guestfs(3)>,
562 L<Sys::Guestfs::Lib(3)>,
563 L<Sys::Virt(3)>,
564 L<http://www.w3.org/TR/xpath/>,
565 L<http://libguestfs.org/>.
566
567 =head1 AUTHORS
568
569 =over 4
570
571 =item *
572
573 Richard W.M. Jones L<http://people.redhat.com/~rjones/>
574
575 =item *
576
577 Matthew Booth L<mbooth@redhat.com>
578
579 =back
580
581 =head1 COPYRIGHT
582
583 Copyright (C) 2010 Red Hat Inc.
584
585 This program is free software; you can redistribute it and/or modify
586 it under the terms of the GNU General Public License as published by
587 the Free Software Foundation; either version 2 of the License, or
588 (at your option) any later version.
589
590 This program is distributed in the hope that it will be useful,
591 but WITHOUT ANY WARRANTY; without even the implied warranty of
592 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
593 GNU General Public License for more details.
594
595 You should have received a copy of the GNU General Public License
596 along with this program; if not, write to the Free Software
597 Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.