251 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			Perl
		
	
	
		
			Executable File
		
	
	
			
		
		
	
	
			251 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			Perl
		
	
	
		
			Executable File
		
	
	
#!/usr/bin/perl
 | 
						|
#
 | 
						|
# multideb - compare package installation differences over
 | 
						|
#            many machines remotely.
 | 
						|
#
 | 
						|
# example ~/.multideb.conf file:
 | 
						|
#
 | 
						|
#    servera port=2210 user=bob classes=web,db
 | 
						|
#    serverb user=root classes=web
 | 
						|
#    serverc user=root classes=web
 | 
						|
#    serverd user=mysql classes=db
 | 
						|
#    localhost classes=web
 | 
						|
#
 | 
						|
# Port 22 is the default.  User required unless servername is localhost.
 | 
						|
#
 | 
						|
 | 
						|
use strict;
 | 
						|
use Getopt::Long;
 | 
						|
use Data::Dumper;
 | 
						|
 | 
						|
my $MD_DIR = ensure_dir("$ENV{'HOME'}/.multideb");
 | 
						|
 | 
						|
my %host;     # hostname -> {..., classes => { classname => 1 } }
 | 
						|
my %classes;  # classname -> { hostname => 1 }
 | 
						|
my %core;     # classname -> [ packagename, ... ]
 | 
						|
 | 
						|
my $opt_full = 0;
 | 
						|
my $opt_core = 0;
 | 
						|
help() unless GetOptions(
 | 
						|
			 "full" => \$opt_full,
 | 
						|
			 "core" => \$opt_core,
 | 
						|
			 );
 | 
						|
 | 
						|
my $mode = shift @ARGV;
 | 
						|
 | 
						|
if ($mode eq "check")
 | 
						|
{
 | 
						|
    load_conf();
 | 
						|
    my @pkgs =  @ARGV;
 | 
						|
    my @hosts = sort keys %host;
 | 
						|
 | 
						|
    foreach my $host (@hosts) {
 | 
						|
	my $h = $host{$host};
 | 
						|
	parse_status($h);
 | 
						|
    }
 | 
						|
 | 
						|
    foreach my $pkg (@pkgs) 
 | 
						|
    {
 | 
						|
	my %whowhat;
 | 
						|
	foreach my $host (@hosts) {
 | 
						|
	    my $h = $host{$host};
 | 
						|
	    my $p = $h->{'pkg'}->{$pkg};
 | 
						|
	    
 | 
						|
	    my $status = status_of_package($p);
 | 
						|
	    push @{$whowhat{$status}}, $host;
 | 
						|
	}
 | 
						|
 | 
						|
	print "$pkg\n";
 | 
						|
	foreach my $stat (sort keys %whowhat) {
 | 
						|
	    print "  $stat: @{$whowhat{$stat}}\n";
 | 
						|
	}
 | 
						|
    }
 | 
						|
    
 | 
						|
    exit 0;
 | 
						|
}
 | 
						|
 | 
						|
if ($mode eq "compare") 
 | 
						|
{
 | 
						|
    load_conf();
 | 
						|
    my $class = shift @ARGV;
 | 
						|
    my @hosts = sort keys %{$classes{$class}};
 | 
						|
 | 
						|
    unless (@hosts) {
 | 
						|
	die "No matching '$class' hosts.\n";
 | 
						|
    }
 | 
						|
 | 
						|
    my @comp_list;
 | 
						|
 | 
						|
    my %epkg;  # existing packages:  { name => 1 }
 | 
						|
    foreach my $host (@hosts)
 | 
						|
    {
 | 
						|
	my $h = $host{$host};
 | 
						|
	parse_status($h);
 | 
						|
	foreach (keys %{$h->{'pkg'}}) {
 | 
						|
	    $epkg{$_} = 1;
 | 
						|
	}
 | 
						|
    }
 | 
						|
    
 | 
						|
    if ($opt_core) {
 | 
						|
	unless (defined $core{$class}) {
 | 
						|
	    die "No core packages defined for class '$class'\n";
 | 
						|
	}
 | 
						|
	@comp_list = @{$core{$class}};
 | 
						|
    } else {
 | 
						|
	@comp_list = sort keys %epkg;
 | 
						|
    }
 | 
						|
 | 
						|
    # iterate through all packages, showing differences:
 | 
						|
    foreach my $pkg (@comp_list)
 | 
						|
    {
 | 
						|
	my %whowhat;
 | 
						|
	my $installed = 0;
 | 
						|
	foreach my $host (@hosts) {
 | 
						|
	    my $h = $host{$host};
 | 
						|
	    my $p = $h->{'pkg'}->{$pkg};
 | 
						|
 | 
						|
	    my $status = status_of_package($p);
 | 
						|
	    push @{$whowhat{$status}}, $host;
 | 
						|
	    
 | 
						|
	    my ($sa, $sb, $sc) = split(/ /, $p->{'Status'});
 | 
						|
	    if ($sc eq "installed") { $installed = 1; }
 | 
						|
	}
 | 
						|
	if ($installed && 
 | 
						|
	    ($opt_full || scalar(keys %whowhat) > 1)) 
 | 
						|
	{
 | 
						|
	    print "$pkg\n";
 | 
						|
	    foreach my $stat (sort keys %whowhat) {
 | 
						|
		print "  $stat: @{$whowhat{$stat}}\n";
 | 
						|
	    }
 | 
						|
	}
 | 
						|
    }
 | 
						|
 | 
						|
    exit 0;    
 | 
						|
}
 | 
						|
 | 
						|
if ($mode eq "update") 
 | 
						|
{
 | 
						|
    load_conf();
 | 
						|
    foreach my $host (sort keys %host)
 | 
						|
    {
 | 
						|
	my $h = $host{$host};
 | 
						|
	my $user = $h->{'user'};
 | 
						|
	my $port = $h->{'port'};
 | 
						|
 | 
						|
	print "$host...\n";
 | 
						|
	if ($host eq "localhost") {
 | 
						|
	    system("rsync", "/var/lib/dpkg/status",
 | 
						|
		   "$MD_DIR/$host.status");
 | 
						|
	} else {
 | 
						|
	    system("rsync", "-e", "ssh -p $port", "-az", 
 | 
						|
		   "$user\@$host:/var/lib/dpkg/status",
 | 
						|
		   "$MD_DIR/$host.status");
 | 
						|
	}
 | 
						|
    }
 | 
						|
 | 
						|
    print "Done.\n";
 | 
						|
    exit 0;
 | 
						|
}
 | 
						|
 | 
						|
help();
 | 
						|
 | 
						|
sub status_of_package 
 | 
						|
{
 | 
						|
    my $p = shift;
 | 
						|
 | 
						|
    my $status = $p->{'Version'};
 | 
						|
    if ($p->{'Status'} ne "install ok installed") {
 | 
						|
	$status .= "/" if $status;
 | 
						|
	$status .= "$p->{'Status'}";
 | 
						|
    }
 | 
						|
    $status ||= "(unknown)";
 | 
						|
 | 
						|
    return $status;
 | 
						|
}
 | 
						|
 | 
						|
sub parse_status
 | 
						|
{
 | 
						|
    my $h = shift;
 | 
						|
    my $fname = "$MD_DIR/$h->{'host'}.status";
 | 
						|
    unless (-e $fname) {
 | 
						|
	die "$fname doesn't exist.  Run update.\n";
 | 
						|
    }
 | 
						|
    open (F, $fname);
 | 
						|
    while (<F>) {
 | 
						|
	unless (/^Package: (.+)/) {
 | 
						|
	    die "Corrupt $h->{'host'} status file?\n";
 | 
						|
	}
 | 
						|
	my $pkg = $1;
 | 
						|
	my $p = $h->{'pkg'}->{$pkg} = {
 | 
						|
	    'Package' => $pkg,
 | 
						|
	};
 | 
						|
	my $lastkey = 'Package';
 | 
						|
	while (<F>) {
 | 
						|
	    chomp;
 | 
						|
	    unless ($_) { last; }
 | 
						|
	    if (/^(\w+):\s*(.+)/i) {
 | 
						|
		$p->{$1} = $2;
 | 
						|
	    }
 | 
						|
	}
 | 
						|
    }
 | 
						|
    close F;
 | 
						|
}
 | 
						|
 | 
						|
sub help 
 | 
						|
{
 | 
						|
    die("Usage:\n".
 | 
						|
	"    multideb update\n".
 | 
						|
	"    multideb compare <classname>\n".
 | 
						|
	"    multideb check <packagename> ...\n");
 | 
						|
}
 | 
						|
 | 
						|
sub load_conf {
 | 
						|
    open (C, "$ENV{'HOME'}/.multideb.conf");
 | 
						|
    while (<C>)
 | 
						|
    {
 | 
						|
	s/^\s+//; s/\s+$//;
 | 
						|
	next if (/^\#/);
 | 
						|
	next unless $_;
 | 
						|
	chomp;
 | 
						|
	my ($p1, @opts) = split(/\s+/, $_);
 | 
						|
 | 
						|
	if ($p1 =~ /^core:(\w+)/) {
 | 
						|
	    $core{$1} = [ @opts ];
 | 
						|
	    next;
 | 
						|
	}
 | 
						|
	
 | 
						|
	my $host = $p1;
 | 
						|
	my $h = $host{$host} = { 
 | 
						|
	    'host' => $host,
 | 
						|
	    'port' => 22,
 | 
						|
	};
 | 
						|
	foreach (@opts) {
 | 
						|
	    my ($k, $v) = split(/=/, $_);
 | 
						|
	    if ($k eq "classes") {
 | 
						|
		foreach (split(/,/, $v)) {
 | 
						|
		    $h->{'classes'}->{$_} = 1;
 | 
						|
		    $classes{$_}->{$host} = 1;
 | 
						|
		}
 | 
						|
	    } else {
 | 
						|
		$h->{$k} = $v;
 | 
						|
	    }
 | 
						|
	}
 | 
						|
    }
 | 
						|
    close C;
 | 
						|
}
 | 
						|
 | 
						|
sub ensure_dir {
 | 
						|
    my $dir = shift;
 | 
						|
    unless (-e $dir) {
 | 
						|
	if (mkdir $dir) {
 | 
						|
	    return $dir;
 | 
						|
	} else {
 | 
						|
	    die "Can't create $dir directory\n";
 | 
						|
	}
 | 
						|
    }
 | 
						|
    unless (-w $dir) {
 | 
						|
	die "Can't write to $dir directory\n";
 | 
						|
    }
 | 
						|
    return $dir;
 | 
						|
}
 |