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;
|
|
}
|