send-config.pl
changeset 6 e7973168471d
parent 3 6cc86a8d1085
child 7 19ba8f130480
equal deleted inserted replaced
5:482b827bb2cd 6:e7973168471d
     1 #! /usr/bin/perl -w
     1 #!/usr/bin/perl -w
     2 # $Id$
       
     3 # $URL$
       
     4 
       
     5 my $USAGE = <<'_';
       
     6 Usage: $ME [options] host
       
     7        host		destination host
       
     8        -v --[no]verbose	be verbose [$opt_verbose]
       
     9        -k --keepgoing	don't stop on errors [$opt_keepgoing]
       
    10 _
       
    11 
     2 
    12 use strict;
     3 use strict;
    13 use File::Basename;
     4 use File::Basename;
       
     5 use Pod::Usage;
    14 use Getopt::Long;
     6 use Getopt::Long;
       
     7 use Config::Tiny;
    15 use Net::SSH qw(sshopen2);
     8 use Net::SSH qw(sshopen2);
    16 use Sys::Hostname::Long;
     9 use Sys::Hostname::Long;
    17 use Socket;
    10 use Socket;
       
    11 use IO::File;
    18 
    12 
    19 my $ME = basename $0;
    13 my $ME = basename $0;
    20 my $CONFIG = "/etc/send-config/config";
    14 
    21 
    15 my $opt_rules_file = '/etc/send-config/filter.rules';
    22 my $opt_verbose = 0;
    16 my $opt_config     = '/etc/send-config/config';
    23 my $opt_keepgoing = 0;
    17 my $opt_dry_run    = 0;
       
    18 my $opt_keepgoing  = 0;
       
    19 my $opt_verbose    = 0;
       
    20 my $opt_debug      = 0;
       
    21 
       
    22 sub debug;
       
    23 sub readConfig($);
       
    24 sub rsync;
       
    25 sub readFilter($);
    24 
    26 
    25 MAIN: {
    27 MAIN: {
       
    28 
    26     GetOptions(
    29     GetOptions(
    27 	"verbose!"  => \$opt_verbose,
    30         "r|rules-file=s" => \$opt_rules_file,
    28 	"keepgoing" => \$opt_keepgoing,
    31         "c|config=s"     => \$opt_config,
    29     ) or die eval "\"$USAGE\"";
    32         "k|keepgoing!"   => \$opt_keepgoing,
    30 
    33         "n|dry-run!"     => \$opt_dry_run,
    31     unless (scalar(@ARGV) == 1) { die eval "\"$USAGE\""; }
    34         "v|verbose!"     => \$opt_verbose,
    32     
    35         "d|debug!"       => \$opt_debug,
    33     (my $host = $ARGV[0]) =~ s/.*\/([a-z0-9.-]+).*/$1/;
    36         "h|help"         => sub { pod2usage( -verbose => 0, -exitval => 0 ) }
    34     my $ip = scalar gethostbyname($host);
    37     ) or pod2usage();
    35     
    38 
    36     $ip or do {
    39     unless ( scalar(@ARGV) == 1 ) {
    37 	warn "can't resolve $host\n";
    40         pod2usage( -verbose => 0, -exitval => 0 );
    38 	next;
    41     }
    39     };
    42 
    40     my $addr = inet_ntoa($ip);
    43     my $dest    = $ARGV[0];
    41     my $hostname_long = hostname_long();
    44     my $dest_ip = scalar gethostbyname($dest);
    42 
    45 
    43     my $user = "root";
    46     ($dest_ip) or die "$ME: Can't resolve $dest\n";
    44     my $path = "/root/Configs/Hosts/$hostname_long";
    47 
    45     my $ssh_cmd = "mkdir -m 0700 -p $path";
    48     my $source_host = hostname_long();
    46     
    49 
    47     sshopen2("$user\@$host", *READER, *WRITER, "$ssh_cmd") || die "ssh: $!";
    50     debug(  "PROG destination host: [" 
    48     close(READER); close(WRITER);
    51           . $dest . "]" 
    49 
    52           . " ip: ["
    50     open(CONF, $CONFIG) or die "$ME: Can't open $CONFIG: $!\n";
    53           . inet_ntoa($dest_ip)
       
    54           . "]" )
       
    55       if $opt_debug;
       
    56     debug( "PROG source host: [" . $source_host . "]" ) if $opt_debug;
       
    57 
       
    58     my ( $username, $path, $stamp ) = readConfig($opt_config);
       
    59 
       
    60     # adding trailing / to destination directory
       
    61     $path .= "/" unless ( $path =~ /\/$/ );
       
    62     my $dest_path = $path . $source_host;
       
    63 
       
    64     # create remote destination directory
       
    65     my $command = "mkdir -m 0700 -p $dest_path";
       
    66 
       
    67     unless ($opt_dry_run) {
       
    68         sshopen2( "$username\@$dest", *READER, *WRITER, "$command" )
       
    69           || die "ssh: $!";
       
    70     }
       
    71     debug("PROG ssh command: ssh $username\@$dest $command") if $opt_debug;
       
    72     close(READER);
       
    73     close(WRITER);
       
    74 
       
    75     rsync( $username, $dest, $dest_path, $stamp );
       
    76 
       
    77 }
       
    78 
       
    79 sub debug {
       
    80     print map( "DEBUG: $_\n", @_ );
       
    81 }
       
    82 
       
    83 sub readConfig($) {
       
    84 
       
    85     my $file = shift;
       
    86 
       
    87     my $config = Config::Tiny->read($file) or die "$ME: Can't open $file: $!\n";
       
    88 
       
    89     my $username = $config->{_}->{username} ? $config->{_}->{username} : 'root';
       
    90     my $path =
       
    91       $config->{_}->{path} ? $config->{_}->{path} : '/root/Configs/Hosts/';
       
    92     my $stamp =
       
    93         $config->{_}->{stamp}
       
    94       ? $config->{_}->{stamp}
       
    95       : '/var/tmp/get-config.stamp';
       
    96 
       
    97     debug(
       
    98         "CONF remote username: $username",
       
    99         "CONF destination path: $path",
       
   100         "CONF stamp path: $stamp"
       
   101     ) if $opt_debug;
       
   102 
       
   103     return ( $username, $path, $stamp );
       
   104 
       
   105 }
       
   106 
       
   107 sub rsync() {
       
   108 
       
   109     my ( $username, $dest, $dest_path, $stamp ) = @_;
       
   110     my @status = ();
    51 
   111 
    52     my @cmd = (
   112     my @cmd = (
    53 	qw(rsync --rsh), "ssh -x",
   113         qw(rsync --rsh), "ssh -x",
    54 	qw(--compress --numeric-ids
   114         qw(--compress --numeric-ids
    55 	   --delete --delete-excluded
   115           --delete --delete-excluded
    56 	   --archive --relative)
   116           --archive --relative)
    57     );
   117     );
       
   118 
    58     push @cmd, "--verbose" if $opt_verbose;
   119     push @cmd, "--verbose" if $opt_verbose;
    59     
   120     push @cmd, "--dry-run" if $opt_dry_run;
    60     while (<CONF>) {
   121 
    61 	chomp;
   122     my @filter = readFilter($opt_rules_file);
    62 	 /^!(.*)\s*/ or next;
   123 
    63 	 push @cmd, "--exclude", $1;
   124     my @src = map { /^\+\s*(.*)/ ? ( glob($1) ) : () } @filter;
    64     }
   125 
    65     seek CONF, 0, 0 or die "$ME: Can't seek $CONFIG: $!\n";
   126     if (
    66 
   127         my @excludes =
    67     while (<CONF>) {
   128         map { /^-\s*(.*)/ ? ( "--exclude" => glob($1) ) : () } @filter
    68 	chomp;
   129       )
    69 	/^\// or next;
   130     {
    70 	my $status = "";
   131         push @cmd, @excludes;
    71 	my $src = "$_";
   132     }
    72 	my $dst = "$user\@$host:$path";
   133 
    73 	print "* $src -> $dst$_\n";
   134     push @cmd, @src, "$username\@$dest:$dest_path";
    74 	system @cmd, $src, $dst;
   135 
    75 	if ($?) {
   136     debug( "PROG rsync command: " . join( " ", @cmd ) ) if $opt_debug;
    76 	    $status = "ERR " . ($? >> 8);
   137 
    77 	    $status .= " SIGNAL " . ($? & 127);
   138     open( TMP, "+>/tmp/$ME.$$" )
    78 	} else { $status = "OK"; };
   139       or die "$ME: Can't open /tmp/$ME.$$";
    79 
   140 
    80 	if ($status ne "OK") {
   141     my $pid = fork();
    81 	    warn "$ME: ???. system command ended with $status" unless $opt_keepgoing;
   142     die "Can't fork" if not defined $pid;
    82 	}
   143 
    83     }
   144     if ( $pid == 0 ) {
    84 
   145         open( STDERR, ">&TMP" ) or die "$!";
    85     my $STAMP = "/var/tmp/get-config.stamp";
   146         open( STDOUT, ">/dev/null" ) unless $opt_verbose;
    86     open(TOUCH, ">$STAMP") or die "$ME: Can't open >>$STAMP: $!\n";
   147         exec @cmd;
    87     close(TOUCH);
   148     }
    88 
   149     else {
    89 }
   150         waitpid $pid, 0;
    90 
   151     }
    91 # vim:sts=4 sw=4 aw ai sm:
   152 
       
   153     close(TMP);
       
   154 
       
   155     if ($?) {
       
   156 
       
   157         open( TMP, "+</tmp/$ME.$$" )
       
   158           or die "$ME: Can't open /tmp/$ME.$$";
       
   159         unlink "/tmp/$ME.$$";
       
   160 
       
   161         while (<TMP>) {
       
   162             if (/(.*)(?<=failed:)\s+(.*)\s*\(\d+\)/) {
       
   163                 push @status, "[WARNING] $1 $2"
       
   164                   unless ( $opt_keepgoing and !$opt_dry_run );
       
   165             }
       
   166         }
       
   167         close(TMP);
       
   168     }
       
   169 
       
   170     if (@status) {
       
   171         unless ($opt_keepgoing) {
       
   172             warn join( "\n", @status ), "\n";
       
   173             warn "[WARNING] $stamp not touched!\n";
       
   174             warn "\nUse option --keepgoing, to ignore this warning.\n";
       
   175         }
       
   176     }
       
   177     else {
       
   178         open( STAMP, ">$stamp" )
       
   179           or die "$ME: Can't open: $stamp: $!\n"
       
   180           unless $opt_dry_run;
       
   181         close(STAMP) unless $opt_dry_run;
       
   182         warn "[OK] touched $stamp.\n" unless $opt_dry_run;
       
   183     }
       
   184 
       
   185 }
       
   186 
       
   187 sub readFilter($) {
       
   188 
       
   189     my $file = shift;
       
   190     my @filter;
       
   191 
       
   192     my $cf = new IO::File $file or die "$ME: Can't open $file: $!\n";
       
   193 
       
   194     while (<$cf>) {
       
   195         next if /^(;#\s*$)/;
       
   196         push @filter, $_;
       
   197     }
       
   198 
       
   199     return @filter;
       
   200 
       
   201 }
       
   202 
       
   203 __END__
       
   204 
       
   205 =pod
       
   206 
       
   207 =head1 NAME
       
   208 
       
   209 B<send-config> - copies files and directories to a remote machine
       
   210 
       
   211 =head1 SYNOPSIS
       
   212 
       
   213 B<send-config> [OPTION...] DEST
       
   214 
       
   215 =over
       
   216 
       
   217 =item B<-r, --rules-file=FILE>
       
   218 
       
   219 =item B<-c, --config=FILE>
       
   220 
       
   221 =item B<-n, --dry-run>
       
   222 
       
   223 =item B<-k, --keepgoing>
       
   224 
       
   225 =item B<-v, --verbose>
       
   226 
       
   227 =item B<-d, --debug>
       
   228 
       
   229 =item B<-h, --help>
       
   230 
       
   231 =back
       
   232 
       
   233 =head1 DESCRIPTION
       
   234 
       
   235 B<send-config> copies files and directories to a remote machine. It uses L<rsync(1)> for data transfer
       
   236 and uses the same authentication and provides the same security as L<ssh(1)>.
       
   237 
       
   238 =over
       
   239 
       
   240 =item B<DEST>
       
   241 
       
   242 remote machine
       
   243 
       
   244 =back
       
   245 
       
   246 =head1 OPTIONS
       
   247 
       
   248 =over 7
       
   249 
       
   250 =item B<-r, --rules-file=FILE>
       
   251 
       
   252 read exclude/include patterns from FILE (default: F</etc/send-config/filter.rules>)
       
   253 
       
   254 =item B<-c, --config=FILE>
       
   255 
       
   256 configuration file for send-config (default: F</etc/send-config/config>)
       
   257 
       
   258 =item B<-n, --dry-run>
       
   259 
       
   260 perform a trial run with no changes made
       
   261 
       
   262 =item B<-k, --keepgoing>
       
   263 
       
   264 don't stop on warnings
       
   265 
       
   266 =item B<-v, --verbose>
       
   267 
       
   268 explain what is being done
       
   269 
       
   270 =item B<-d, --debug>
       
   271 
       
   272 print debug informations
       
   273 
       
   274 =item B<-h, --help>
       
   275 
       
   276 display short help and exit
       
   277 
       
   278 =back
       
   279 
       
   280 =head1 EXAMPLES
       
   281 
       
   282 F</etc/send-config/filter.rules>
       
   283 
       
   284 Lines started with an # or ; are comments. New lines will be ignored. Lines with prefix + or - will be (+ included) or (- excluded) by the rsync process.
       
   285 
       
   286  - *~
       
   287  - /usr/src
       
   288  - /etc/samba/codepages
       
   289  - /usr/local/uvscan
       
   290  - /usr/local/src
       
   291  - /root/local-packages
       
   292 
       
   293  + /etc
       
   294  + /var/cache/debconf
       
   295  + /var/lib/dpkg/status
       
   296  + /boot/config*
       
   297  + /boot/grub/menu.lst
       
   298  + /root/.bash*
       
   299  + /root/.profile
       
   300  + /root/LOG*
       
   301  + /root/.ssh
       
   302  + /proc/config.gz
       
   303  + /usr/local
       
   304  + /usr/lib/AntiVir/*.[Kk][Ee][Yy]
       
   305  + /usr/src/*/*.config
       
   306  + /usr/src/*config*
       
   307 
       
   308 F</etc/send-config/config>
       
   309 
       
   310 I<username> is the remote login user for ssh and rsync process. I<path> is the absolute destination path on the remote machine where the files will be stored. I<stamp> is the absolute path on the local machine to create or touch an stamp file, you can use it e.g. for nagios (check_file_age).
       
   311 
       
   312  username = root
       
   313  path = /root/Configs/Hosts/
       
   314  stamp = /var/tmp/get-config.stamp
       
   315 
       
   316 =head1 FILES
       
   317 
       
   318 F</etc/send-config/config>
       
   319 
       
   320 F</etc/send-config/filter.rules>
       
   321 
       
   322 =head1 AUTHOR
       
   323 
       
   324 Christian Arnold <arnold@schlittermann.de>
       
   325 
       
   326 =head1 COPYRIGHT
       
   327 
       
   328 Copyright (C) 2010 Schlittermann - internet & unix support. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
       
   329 
       
   330 This is free software: you are free to change and redistribute it.  There is NO WARRANTY, to the extent permitted by law.
       
   331 
       
   332 =head1 SEE ALSO
       
   333 
       
   334 L<ssh(1)>, L<rsync(1)>
       
   335 
       
   336 =cut
       
   337