Implement virt-uptime command.
[virt-tools.git] / tools / virt-uname.pl
index 9bca435..3ff8d4c 100755 (executable)
@@ -19,6 +19,7 @@
 use strict;
 
 use Net::SNMP;
 use strict;
 
 use Net::SNMP;
+use Sys::Virt;
 use Pod::Usage;
 use Getopt::Long;
 use Locale::TextDomain 'virt-tools';
 use Pod::Usage;
 use Getopt::Long;
 use Locale::TextDomain 'virt-tools';
@@ -27,17 +28,19 @@ use Locale::TextDomain 'virt-tools';
 
 =head1 NAME
 
 
 =head1 NAME
 
-virt-uname, virt-ps, virt-ping - virtual machine information and statistics
+virt-ps, virt-ping, virt-uname, virt-uptime - virtual machine information and statistics
 
 =head1 SYNOPSIS
 
 
 =head1 SYNOPSIS
 
- virt-uname [--options] [domname]
+ virt-ifconfig [--options] [domname]
 
  virt-ps [--options] [domname]
 
  virt-ping [--options] [domname]
 
 
  virt-ps [--options] [domname]
 
  virt-ping [--options] [domname]
 
- virt-ifconfig [--options] [domname]
+ virt-uname [--options] [domname]
+
+ virt-uptime [--options] [domname]
 
 =head1 COMMON OPTIONS
 
 
 =head1 COMMON OPTIONS
 
@@ -87,6 +90,14 @@ Write out the results in CSV format (comma-separated values).  This
 format can be imported easily into databases and spreadsheets, but
 read L</NOTE ABOUT CSV FORMAT> below.
 
 format can be imported easily into databases and spreadsheets, but
 read L</NOTE ABOUT CSV FORMAT> below.
 
+=cut
+
+my $verbose;
+
+=item B<--verbose> | B<-v>
+
+Enable verbose messages, useful for debugging.
+
 =back
 
 =cut
 =back
 
 =cut
@@ -95,6 +106,7 @@ GetOptions ("help|?" => \$help,
             "version" => \$version,
             "connect|c=s" => \$uri,
             "csv" => \$csv,
             "version" => \$version,
             "connect|c=s" => \$uri,
             "csv" => \$csv,
+            "verbose|v" => \$verbose,
     ) or pod2usage (2);
 pod2usage (1) if $help;
 if ($version) {
     ) or pod2usage (2);
 pod2usage (1) if $help;
 if ($version) {
@@ -103,15 +115,17 @@ if ($version) {
 }
 
 my %subcommands = (
 }
 
 my %subcommands = (
-    "virt-uname" => [ &do_uname, &title_uname ],
-    "virt-ps" => [ &do_ps, &title_ps ],
-    "virt-ping" => [ &do_ping, &title_ping ],
+    "virt-ps" => [ \&do_ps, \&title_ps ],
+    "virt-ping" => [ \&do_ping, \&title_ping ],
+    "virt-uname" => [ \&do_uname, \&title_uname ],
+    "virt-uptime" => [ \&do_uptime, \&title_uptime ],
 );
 
 # Which subcommand?
 my ($do_it, $title_it);
 foreach (keys %subcommands) {
     if ($0 =~ /$_/) {
 );
 
 # Which subcommand?
 my ($do_it, $title_it);
 foreach (keys %subcommands) {
     if ($0 =~ /$_/) {
+        print STDERR "subcommand = $_\n" if $verbose;
         $do_it = $subcommands{$_}->[0];
         $title_it = $subcommands{$_}->[1];
         last;
         $do_it = $subcommands{$_}->[0];
         $title_it = $subcommands{$_}->[1];
         last;
@@ -119,6 +133,18 @@ foreach (keys %subcommands) {
 }
 die "$0: cannot determine which sub-command to run\n" unless $do_it;
 
 }
 die "$0: cannot determine which sub-command to run\n" unless $do_it;
 
+# If we are being run from a local directory, add that directory to
+# the path, so we can be run from the source directory without being
+# installed.
+if (substr ($0, 0, 1) ne "/") {
+    $_ = $0;
+    s{/[^/]+$}{};
+    $ENV{PATH} = "$_:$ENV{PATH}"; # XXX Windows?
+    print STDERR "PATH set to $ENV{PATH}\n" if $verbose;
+}
+
+our $errors = 0;
+
 # Do we have named guests?
 if (@ARGV == 0) {
     my $conn;
 # Do we have named guests?
 if (@ARGV == 0) {
     my $conn;
@@ -135,51 +161,42 @@ if (@ARGV == 0) {
     my @domnames = map { $_->get_name () } @doms;
 
     if (@domnames) {
     my @domnames = map { $_->get_name () } @doms;
 
     if (@domnames) {
-       title_it ();
+       &$title_it ();
         foreach (@domnames) {
         foreach (@domnames) {
-            my ($key, $transport);
-            eval {
-                $key = get_key ($_);
-                $transport = get_transport ($_);
-            };
-            if (!$@) { do_it ($_, $key, $transport) }
+            get_and_do_it ($_);
         }
     }
 } else {
         }
     }
 } else {
-    title_it ();
+    &$title_it ();
     foreach (@ARGV) {
     foreach (@ARGV) {
-        my ($key, $transport);
-        eval {
-            $key = get_key ($_);
-            $transport = get_transport ($_);
-        };
-        if (!$@) { do_it ($_, $key, $transport) }
+        get_and_do_it ($_);
     }
 }
 
     }
 }
 
-exit 0;
-
-=head1 virt-uname
-
-C<virt-uname> displays the system information (kernel version etc) of
-the guest.
-
-=cut
-
-sub title_uname
+sub get_and_do_it
 {
 {
-    print_row (__"Guest");
+    # Turn any errors into warnings.
+    eval {
+       my ($key, $transport);
+       $key = get_key ($_);
+       $transport = get_transport ($_);
+       &$do_it ($_, $key, $transport);
+    };
+    if ($@) {
+       $errors++;
+       warn "$@";
+    }
 }
 
 }
 
-sub do_uname
-{
-    my $domname = shift;
-    my $key = shift;
-    my $transport = shift;
+print STDERR "errors = $errors\n" if $verbose;
 
 
+exit ($errors == 0 ? 0 : 1);
 
 
+# virt-ifconfig is implemented as a separate program.
 
 
-}
+=head1 virt-ifconfig
+
+C<virt-ifconfig> displays the IP address of the guest.
 
 =head1 virt-ps
 
 
 =head1 virt-ps
 
@@ -225,14 +242,85 @@ sub do_ping
 
 }
 
 
 }
 
-# virt-ifconfig is implemented separately.
+=head1 virt-uname
 
 
-=head1 virt-ifconfig
+C<virt-uname> displays the system information (kernel version etc) of
+the guest.
 
 
-C<virt-ifconfig> displays the IP address of the guest.
+=cut
+
+sub title_uname
+{
+    print_row (__"Guest", __"System name");
+}
+
+sub do_uname
+{
+    my $domname = shift;
+    my $key = shift;
+    my $transport = shift;
+
+    my $session = get_snmp_session ($key, $transport);
+    my $sysDescr = "1.3.6.1.2.1.1.1.0";
+    my $r = $session->get_request (-varbindlist => [$sysDescr])
+       or die __x("SNMP error: {e}", $session->error);
+    print_row ($domname, $r->{$sysDescr});
+    $session->close;
+}
+
+=head1 virt-uptime
+
+C<virt-uptime> displays the uptime of the guest
 
 =cut
 
 
 =cut
 
+sub title_uptime
+{
+    print_row (__"Guest", __"Uptime");
+}
+
+sub do_uptime
+{
+    my $domname = shift;
+    my $key = shift;
+    my $transport = shift;
+
+    my $session = get_snmp_session ($key, $transport);
+    my $sysUpTime = "1.3.6.1.2.1.1.3.0";
+    my $r = $session->get_request (-varbindlist => [$sysUpTime])
+       or die __x("SNMP error: {e}", $session->error);
+    print_row ($domname, $r->{$sysUpTime});
+    $session->close;
+}
+
+sub print_row
+{
+    my @fields = @_;
+
+    local $_;
+    my $comma = 0;
+
+    foreach (@fields) {
+       if (!$csv) {
+           printf "%-16s ", $_
+       } else {
+           print "," if $comma;
+           $comma = 1;
+
+           # XXX Use Text::CSV here.
+           if ($_ =~ /"/) {
+               s/"/""/;
+               printf "\"%s\"", $_;
+           } elsif ($_ =~ /,/ || $_ =~ /\n/) {
+               printf "\"%s\"", $_;
+           } else {
+               print $_;
+           }
+       }
+    }
+    print "\n";
+}
+
 =head1 OVERVIEW
 
 Virt-tools are a set of tools that you can install in your virtual
 =head1 OVERVIEW
 
 Virt-tools are a set of tools that you can install in your virtual
@@ -248,39 +336,95 @@ There are two parts to any virt-tools installation: some client
 programs like C<virt-uname> and C<virt-ps> that you run on the host,
 to query guest information.  On the guest, you have to install and run
 a virt-tools service.  Between the host and guest is a transport which
 programs like C<virt-uname> and C<virt-ps> that you run on the host,
 to query guest information.  On the guest, you have to install and run
 a virt-tools service.  Between the host and guest is a transport which
-should be secured.  The L</GUEST CONFIGURATION> section describes how
-to configure guests and secure the transport.
+should be secured.
+
+The L</GUEST ARCHITECTURE> section describes how virt-tools appears
+from the guest side.
 
 
-Finally the L</ARCHITECTURE> section describes the architecture of
-virt-tools and provides information about diagnosing problems.
+The L</HOST ARCHITECTURE> section describes the architecture of
+virt-tools on the host side.
 
 
-=head1 GUEST CONFIGURATION
+=head1 GUEST ARCHITECTURE
 
 
+In most cases, you can just install the C<virt-tools-guest> package in
+your Linux guests, or the Windows virt-tools guest package in your
+Windows guests, and everything should just work.  In this section we
+describe more about how it works (or is supposed to work) from the
+guest side.
 
 
+=head2 COMMUNICATIONS DIRECTORY
 
 
+The guest writes various static, mostly unchanging, information into
+its own directory.  On Linux the directory is
+C<@localstatedir@/lib/virt-tools/> and under Windows it is
+C<%systemroot%\virttool\>.  In the discussion below, this
+communications directory is referred to as C<$GUESTCOMMSDIR>.
 
 
+The host is able to read files out of this directory using
+L<libguestfs(3)> (without any cooperation needed by the guest).
 
 
+=head2 IP ADDRESSES
 
 
+The host can't easily see the guest's IP address.  The host provides
+the guest with a network interface connected to a bridge, but the
+guest can use any IP address it likes (although well-behaved guests
+will usually have some static IPs or are allocated one by DHCP).
 
 
+So when the guest starts up, or its IP address changes (usually these
+are rare events) the guest writes a file
+C<$GUESTCOMMSDIR/ip-E<lt>ifaceE<gt>> which contains details of the IP
+address of the interface E<lt>ifaceE<gt> (eg. the file might be called
+C<ip-eth0> under Linux).
 
 
+C<virt-ifconfig> reads this file directly using L<libguestfs(3)>.
 
 
+=head2 KEYS
 
 
+When the guest is first installed (or more precisely, when the
+virt-tools-guest package is first installed in the guest), a random
+secret key is generated.  This is used to encrypt communications with
+the guest, and it is described in more detail below.
 
 
-=head1 ARCHITECTURE
+The key is written to C<$GUESTCOMMSDIR/key>.
 
 
-Guests run an SNMP (Simple Network Management Protocol) server.  The
-host client tools access this server in order to query information
-about the guest.  They query this using standard SNMP calls.
+=head2 SNMP DAEMON
+
+For process listings, and just about every other piece of data except
+for IP address, guests run a completely standard SNMP (Simple Network
+Management Protocol) server.  The host client tools access this server
+in order to query information about the guest.  They query this using
+standard SNMP calls.
 
 The protocol used is SNMPv3 (RFC 2571) which addresses security
 concerns in earlier versions of the protocol.  In order to ensure that
 
 The protocol used is SNMPv3 (RFC 2571) which addresses security
 concerns in earlier versions of the protocol.  In order to ensure that
-only the host can access the SNMP server, the guest generates a random
-secret key which the host must find out.  Also the host must find a
-suitable transport to connect to the SNMP server (eg. by finding the
-IP address of the guest or using another transport into the guest).
+only the host can access the SNMP server and see the results, all
+communications are encrypted and authenticated using the guest's key.
+
+=head2 TRANSPORT
+
+There is not necessarily a network connection between the host and the
+guest.  There are many configurations of virtualization in which the
+host has no network access to the guest: for example, if the host
+firewalls itself off from the guest (or vice versa), or if the guest
+has a physically separate network card from the host.
+
+Therefore the guest to host SNMP transport is not necessarily over an
+IP network.  Other transports are possible, including "vmchannel"
+(where "vmchannel" is the generic name for a collection of specialized
+host-guest communication channels implemented in different ways by
+different hypervisors).
+
+The transport is written to C<$GUESTCOMMSDIR/transport>.
 
 
-Once the key and the transport to the guest are worked out, the query
-is a straightforward SNMP call:
+=head1 HOST ARCHITECTURE
+
+On the host side, the host uses L<libguestfs(3)> to read the guest's
+IP address and key, and uses some heuristics to determine the
+transport to use.
+
+Once the key and the transport to the guest are worked out, programs
+like C<virt-ps>, C<virt-uname> and so on are just making
+straightforward SNMP calls:
 
  +-----------------+      +-----------------+
  | host            |      | guest           |
 
  +-----------------+      +-----------------+
  | host            |      | guest           |
@@ -290,7 +434,7 @@ is a straightforward SNMP call:
 
 The difficulty is in determining the key and the transport to use,
 which is what this section covers.  You can also use this knowledge to
 
 The difficulty is in determining the key and the transport to use,
 which is what this section covers.  You can also use this knowledge to
-diagnose problems, and to create non-standard configurations.
+diagnose problems or to create non-standard configurations.
 
 =head2 DETERMINE KEY
 
 
 =head2 DETERMINE KEY
 
@@ -298,33 +442,30 @@ All the host tools use an external helper program called
 C<virt-tools-get-key> to get the key of the guest.  (See
 L<virt-tools-get-key(8)> for the precise usage of this program).
 
 C<virt-tools-get-key> to get the key of the guest.  (See
 L<virt-tools-get-key(8)> for the precise usage of this program).
 
-The key is generated by the guest once -- when the virt-tools package
-is installed in the guest.  The key is written to a file
-C</var/lib/virt-tools/key> (in the guest) which is readable only by
-root.
-
-On Windows guests the key is written to
-C<%systemroot%\virttools.key>
+The key is generated by the guest once -- when the virt-tools-guest
+package is installed in the guest.  The key is written to a file
+C<$GUESTCOMMSDIR/key> (in the guest) which is readable only by root.
 
 Using L<libguestfs(3)> the host can read any file in the guest, so it
 can read this key out directly.  This is what the
 C<virt-tools-get-key> program does, and you can run it by hand to
 verify its operation:
 
 
 Using L<libguestfs(3)> the host can read any file in the guest, so it
 can read this key out directly.  This is what the
 C<virt-tools-get-key> program does, and you can run it by hand to
 verify its operation:
 
- # virt-tools-get-key -v domname|uuid
+ # virt-tools-get-key -v domname
  abcdef1234567890
 
 =head3 KEY CACHE
 
 C<virt-tools-get-key> caches the keys of guests that it has seen
 before so it doesn't have to read them each time.  The cache is in
  abcdef1234567890
 
 =head3 KEY CACHE
 
 C<virt-tools-get-key> caches the keys of guests that it has seen
 before so it doesn't have to read them each time.  The cache is in
-C</var/lib/virt-tools/keys/> (in the host).
+C<@localstatedir@/lib/virt-tools/keys/> (in the host).
 
 You can just delete the files in this directory at any time, I<or> you
 can drop a file in here which contains the key of a guest.
 
 
 You can just delete the files in this directory at any time, I<or> you
 can drop a file in here which contains the key of a guest.
 
-To do this, create a file C</var/lib/virt-tools/keys/E<lt>UUIDE<gt>>
-where E<lt>UUIDE<gt> is the guest's UUID as displayed by this command:
+To do this, create a file
+C<@localstatedir@/lib/virt-tools/keys/E<lt>UUIDE<gt>> where
+E<lt>UUIDE<gt> is the guest's UUID as displayed by this command:
 
  virsh domuuid <name>
 
 
  virsh domuuid <name>
 
@@ -338,6 +479,23 @@ This cache never expires, unless you remove the files by hand.
 
 sub get_key
 {
 
 sub get_key
 {
+    my $domname = shift;
+
+    my $cmd = "virt-tools-get-key";
+    $cmd .= " -v" if $verbose;
+    # XXX quoting
+    $cmd .= " -c '$uri'" if $uri;
+    $cmd .= " '$domname'";
+
+    print STDERR "$cmd\n" if $verbose;
+
+    open PIPE, "$cmd |" or die "$cmd: $!";
+    my $line = <PIPE>;
+    die __"no response from virt-tools-get-key\n" unless $line;
+    chomp $line;
+    close PIPE;
+
+    $line
 }
 
 =head2 DETERMINE TRANSPORT
 }
 
 =head2 DETERMINE TRANSPORT
@@ -351,7 +509,7 @@ This program tries a series of methods to determine how to access a
 guest, be it through a direct network connection or over some
 hypervisor-specific vmchannel.
 
 guest, be it through a direct network connection or over some
 hypervisor-specific vmchannel.
 
- # virt-tools-get-transport -v domname|uuid
+ # virt-tools-get-transport -v domname
  udp:192.168.122.33
 
 You can diagnose problems with the transport by trying to run this
  udp:192.168.122.33
 
 You can diagnose problems with the transport by trying to run this
@@ -361,7 +519,8 @@ command by hand.
 
 C<virt-tools-get-transport> caches the transports of guests that it
 has seen before so it doesn't have to determine them each time.  The
 
 C<virt-tools-get-transport> caches the transports of guests that it
 has seen before so it doesn't have to determine them each time.  The
-cache is in C</var/lib/virt-tools/transports/> (in the host).
+cache is in C<@localstatedir@/lib/virt-tools/transports/> (in the
+host).
 
 As for the L</KEY CACHE>, this directory is just some files that are
 named after the UUID of the guest, containing the transport.
 
 As for the L</KEY CACHE>, this directory is just some files that are
 named after the UUID of the guest, containing the transport.
@@ -374,8 +533,80 @@ corresponding entry in the transport cache if it is not valid.
 
 sub get_transport
 {
 
 sub get_transport
 {
+    my $domname = shift;
+
+    my $cmd = "virt-tools-get-transport";
+    $cmd .= " -v" if $verbose;
+    # XXX quoting
+    $cmd .= " -c '$uri'" if $uri;
+    $cmd .= " '$domname'";
+
+    print STDERR "$cmd\n" if $verbose;
+
+    open PIPE, "$cmd |" or die "$cmd: $!";
+    my $line = <PIPE>;
+    die __"no response from virt-tools-get-transport\n" unless $line;
+    chomp $line;
+    close PIPE;
+
+    $line
+}
+
+=head2 SNMP QUERIES
+
+Standard SNMP queries are used between the host and guest.
+
+SNMP already supports many of the features we are trying to query
+(eg. the UCD SNMP MIB provides a way to query the process list of a
+machine in a form which is a de facto standard).
+
+To determine what precise queries are sent, run the tools in verbose
+mode or examine the source.
+
+=cut
+
+sub get_snmp_session
+{
+    my $key = shift;
+    my $transport = shift;
+
+    my ($hostname, $port, $domain);
+    if ($transport =~ /^udp:(.*):(.*)/) {
+       $domain = "udp";
+       $hostname = $1;
+       $port = $2;
+    } elsif ($transport =~ /^tcp:(.*):(.*)/) {
+       $domain = "tcp";
+       $hostname = $1;
+       $port = $2;
+    } else {
+       die __x("unknown transport type: {t}", t => $transport);
+    }
+
+    if ($verbose) {
+       print STDERR "creating Net::SNMP session to $domain:$hostname:$port with key $key\n"
+    }
+
+    my ($session, $error) = Net::SNMP->session (
+       -version => 3,
+       -username => "virttools",
+       -authpassword => $key,
+       -authprotocol => "sha",
+       -privpassword => $key,
+       -privprotocol => "aes",
+       -hostname => $hostname,
+       -port => $port,
+       -domain => $domain,
+       );
+    die __x("SNMP failure: {e}", e => $error) unless $session;
+
+    $session;
 }
 
 }
 
+=head2 RUNNING YOUR OWN SNMP SERVER IN A GUEST
+
+I<(To be written)>
+
 =head1 NOTE ABOUT CSV FORMAT
 
 Comma-separated values (CSV) is a deceptive format.  It I<seems> like
 =head1 NOTE ABOUT CSV FORMAT
 
 Comma-separated values (CSV) is a deceptive format.  It I<seems> like