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 |