1 | #!/usr/bin/perl |
---|
2 | |
---|
3 | # $Id$ |
---|
4 | |
---|
5 | =head1 NAME |
---|
6 | |
---|
7 | transfert - A script to transfert data using rsync |
---|
8 | |
---|
9 | =head1 VERSION |
---|
10 | |
---|
11 | CVS: $Revision$ |
---|
12 | |
---|
13 | =cut |
---|
14 | |
---|
15 | use strict; |
---|
16 | use warnings; |
---|
17 | use Getopt::Long; |
---|
18 | use Config::IniFiles; |
---|
19 | use Mail::Sendmail; |
---|
20 | use POSIX qw(strftime); |
---|
21 | use Pod::Usage; |
---|
22 | use Digest::MD5; |
---|
23 | |
---|
24 | # 0 .. 6 => debug, info, report, warn, error, die |
---|
25 | my $verbose = 1; |
---|
26 | |
---|
27 | GetOptions( |
---|
28 | 'config|c=s' => \my $configfile, |
---|
29 | 'v' => sub { $verbose-- }, |
---|
30 | 'd|debug' => \my $debug, |
---|
31 | 'nomail' => \my $nomail, |
---|
32 | 'mailto' => \my $mailto, |
---|
33 | 'logfile=s' => \my $logfile, |
---|
34 | 'help|h' => sub { pod2usage(-exitval => 0) }, |
---|
35 | ) or pod2usage(); |
---|
36 | |
---|
37 | =head1 SYNOPSIS |
---|
38 | |
---|
39 | transfert -c <configfile> [...] |
---|
40 | |
---|
41 | config|c <configfile> use this config file |
---|
42 | v increase verbosity |
---|
43 | nomail do not sent any mail |
---|
44 | logfile <logfilefmt> write log into this file |
---|
45 | help|h print this message |
---|
46 | debug|d debug mode |
---|
47 | |
---|
48 | =head1 OPTIONS |
---|
49 | |
---|
50 | =head1 CONFIGURATION FILE |
---|
51 | |
---|
52 | =head2 Description |
---|
53 | |
---|
54 | The configuration is an INI file like. Each section describe |
---|
55 | a path to transfert. The section named 'default' is used to store |
---|
56 | global or default values. |
---|
57 | |
---|
58 | Example: |
---|
59 | |
---|
60 | [default] |
---|
61 | |
---|
62 | [musique] |
---|
63 | source=/source/ |
---|
64 | dest=host:/dest |
---|
65 | mailto=user@domain |
---|
66 | |
---|
67 | =head2 Globals values |
---|
68 | |
---|
69 | Following paramater should be set into [default] section: |
---|
70 | |
---|
71 | =over |
---|
72 | |
---|
73 | =item * smtp Mail host to use to send mail, default is 'localhost' |
---|
74 | |
---|
75 | =item * mailfrom the mail adress set into mail as from, only the form |
---|
76 | user@domain should appear |
---|
77 | |
---|
78 | =item * mailheader a message to copy at the beggining of each mail |
---|
79 | |
---|
80 | =item * mailfooter a message to copy at the end of each mail |
---|
81 | |
---|
82 | =item * logfile redirect log into this file. |
---|
83 | |
---|
84 | logfile is a strftime format. See L<strftime> man page, or L<POSIX> |
---|
85 | perl module documentation. |
---|
86 | |
---|
87 | =head2 Per transfert parameters |
---|
88 | |
---|
89 | All this paramter can be set into [default] section |
---|
90 | |
---|
91 | =item * source the source file to transfert. |
---|
92 | |
---|
93 | This paramater is passed directly to rsync, so can be a glob. |
---|
94 | |
---|
95 | =item * destination the destination where file should be copied |
---|
96 | |
---|
97 | =item * cmd the command to use to trnsfert files |
---|
98 | |
---|
99 | =item * mailto the list of coma separate mail address which will receive |
---|
100 | transfert log |
---|
101 | |
---|
102 | =item * postrun a command to run for each file, %s is replaced by the path |
---|
103 | of the file |
---|
104 | |
---|
105 | =back |
---|
106 | |
---|
107 | =cut |
---|
108 | |
---|
109 | $verbose = 0 if ($debug); |
---|
110 | my $maillog; # mail => { level =>, msg => @} |
---|
111 | my $logcallback = undef; |
---|
112 | my $loghandle = \*STDERR; |
---|
113 | |
---|
114 | if (!$configfile) { |
---|
115 | loging(6, "No config file given"); |
---|
116 | exit(1); |
---|
117 | } |
---|
118 | |
---|
119 | loging(0, "Reading config '%s'", $configfile); |
---|
120 | my $config = new Config::IniFiles( |
---|
121 | -file => $configfile, |
---|
122 | -default => 'default', |
---|
123 | -allowcontinue => 1 |
---|
124 | ); |
---|
125 | |
---|
126 | if (!($config && -f $configfile && -r _)) { |
---|
127 | loging(6, "Can't open %s", $configfile); |
---|
128 | exit(1); |
---|
129 | } |
---|
130 | |
---|
131 | $logfile ||= $config->val('default', 'logfile'); |
---|
132 | if ($logfile) { |
---|
133 | loging(0, "Openning log file %s (%s)", strftime($logfile, gmtime), $logfile); |
---|
134 | if (!open($loghandle, '> ' . strftime($logfile, gmtime))) { |
---|
135 | $loghandle = \*STDERR; |
---|
136 | loging(5, "Can't open log file %s (%s)", strftime($logfile, gmtime), $logfile); |
---|
137 | } |
---|
138 | } |
---|
139 | |
---|
140 | |
---|
141 | foreach my $entry ($config->Sections) { |
---|
142 | $entry eq 'default' and next; |
---|
143 | loging(0, "Starting section '%s'", $entry); |
---|
144 | my @mail = split(/\s*,\s*/, $config->val($entry, 'mailto')); |
---|
145 | loging(0, "mail to '%s' in section '%s'", $config->val($entry, 'mailto') || "", $entry); |
---|
146 | loging(0, "after filter, mail sent to %s in section '%s'", |
---|
147 | join(", ", map { "'$_'" } @mail), $entry); |
---|
148 | |
---|
149 | $logcallback = sub { |
---|
150 | my ($level, $msg) = @_; |
---|
151 | foreach (@mail) { |
---|
152 | push(@{$maillog->{$_}}, "[$level] $msg"); |
---|
153 | } |
---|
154 | } unless($nomail); |
---|
155 | |
---|
156 | transfert($entry); |
---|
157 | $logcallback = undef; |
---|
158 | send_mail() unless($nomail); |
---|
159 | |
---|
160 | } |
---|
161 | |
---|
162 | loging(0, "End of transfert, exiting"); |
---|
163 | close($loghandle); |
---|
164 | exit(0); |
---|
165 | |
---|
166 | ################# |
---|
167 | # MAIN LOOP END # |
---|
168 | ################# |
---|
169 | |
---|
170 | sub loging { |
---|
171 | my ($level, $fmt, @val) = @_; |
---|
172 | my $msg = sprintf($fmt, @val); |
---|
173 | if($level >= 0 && $level >= $verbose) { |
---|
174 | if ($logcallback) { |
---|
175 | $logcallback->($level, $msg); |
---|
176 | } |
---|
177 | if (!$logcallback || $debug) { |
---|
178 | print $loghandle "[$level] $msg\n"; |
---|
179 | } |
---|
180 | } |
---|
181 | $msg |
---|
182 | } |
---|
183 | |
---|
184 | =head1 DATA TRANSFERT |
---|
185 | |
---|
186 | =head2 Finding files |
---|
187 | |
---|
188 | The scripts look for L<source> parameter, if it is a directory, the script |
---|
189 | perform a glob using 'source/*', otherwise, it perform a glob using directly |
---|
190 | the source parameter |
---|
191 | |
---|
192 | If the source parameter is not defined, the script log an error and skip the |
---|
193 | section. |
---|
194 | |
---|
195 | The second required parameter is the destination of files, L<dest> parameter |
---|
196 | in config files. It should be accepted by the L<cmd> program that will |
---|
197 | transfert data, the form "user@host:/path" is generally use as the default |
---|
198 | program is rsync. |
---|
199 | |
---|
200 | For each files found according L<source> parameter: |
---|
201 | |
---|
202 | - we look if a .md5sum exists (using file name as prefix) and if it is more |
---|
203 | recent, if yes, we assume file has been already transfered. |
---|
204 | |
---|
205 | - a .md5sum is create, this files contains some comment and the md5sum of |
---|
206 | the data file, this .md5sum can be used by md5sum -c to check data file |
---|
207 | integrity |
---|
208 | |
---|
209 | - both data file and its md5sum file are tranfered, if tranfer failed, the |
---|
210 | md5sum file is deleted |
---|
211 | |
---|
212 | - L<postrun> command is run for both data file and its md5sum file if tranfert |
---|
213 | is succefully completed. |
---|
214 | |
---|
215 | =cut |
---|
216 | |
---|
217 | sub transfert { |
---|
218 | my ($entry) = @_; |
---|
219 | my $source = $config->val($entry, 'source') or do { |
---|
220 | loging(5, "No source defined for %s", $entry); |
---|
221 | return 0; |
---|
222 | }; |
---|
223 | |
---|
224 | my $dest = $config->val($entry, 'dest') or do { |
---|
225 | loging(5, "No destination defined for %s", $entry); |
---|
226 | return 0; |
---|
227 | }; |
---|
228 | |
---|
229 | my @files = -d $source ? glob("$source/*") : glob($source); |
---|
230 | my @datafiles; |
---|
231 | |
---|
232 | foreach my $f (@files) { |
---|
233 | if ($f =~ /.md5sum$/) { |
---|
234 | loging(0, "%s is md5sum file, skipping", $f); |
---|
235 | next; |
---|
236 | } |
---|
237 | |
---|
238 | if (! -f $f) { |
---|
239 | loging(0, "%s is not a regular file, skipping", $f); |
---|
240 | next; |
---|
241 | } |
---|
242 | my ($fsize, $ftime); |
---|
243 | { |
---|
244 | my @st = stat($f); |
---|
245 | $fsize = $st[7]; |
---|
246 | $ftime = $st[9]; |
---|
247 | } |
---|
248 | |
---|
249 | # Does the transfert need |
---|
250 | if (-f "$f.md5sum") { |
---|
251 | if ((stat("$f.md5sum"))[9] > $ftime) { |
---|
252 | loging(0, "%s exists and is newer, assume transfert is not need", "$f.md5sum"); |
---|
253 | next; |
---|
254 | } |
---|
255 | } |
---|
256 | |
---|
257 | # Creating md5sum file |
---|
258 | if(open(my $md5h, "> $f.md5sum")) { |
---|
259 | if (open(my $fh, "< $f")) { |
---|
260 | loging(0, "Creating %s md5 file", "$f.md5sum"); |
---|
261 | binmode($fh); |
---|
262 | my $md5 = new Digest::MD5; |
---|
263 | $md5->addfile($fh); |
---|
264 | printf $md5h |
---|
265 | "# %s\n# %s (%d octets)\n# %s\n%s %s\n", |
---|
266 | q$Id$, |
---|
267 | $f, $fsize, |
---|
268 | scalar(localtime), |
---|
269 | $md5->hexdigest, |
---|
270 | ($f =~ m!(?:.*/)?(.*)$!)[0]; |
---|
271 | close($fh); |
---|
272 | } else { |
---|
273 | login(5, "Can't open %s for reading", $f); |
---|
274 | } |
---|
275 | close($md5h); |
---|
276 | } else { |
---|
277 | login(5, "Can't open %s for writting", "$f.md5sum"); |
---|
278 | } |
---|
279 | |
---|
280 | my $error = 0; |
---|
281 | foreach my $filetotranfert (grep { -f $_ } ("$f.md5sum", $f)) { |
---|
282 | my $cmd = sprintf( |
---|
283 | "%s %s %s", |
---|
284 | $config->val($entry, 'cmd', 'rsync -e ssh'), |
---|
285 | $filetotranfert, |
---|
286 | $dest, |
---|
287 | ); |
---|
288 | loging(0, "running `%s'", $cmd); |
---|
289 | if (system($cmd)) { |
---|
290 | loging(4, "tranfert failed for %s, exit code: %d", $filetotranfert, $? >> 8); |
---|
291 | unlink("$f.md5sum"); # ensure transfert will be retried |
---|
292 | $error++; |
---|
293 | } |
---|
294 | } |
---|
295 | |
---|
296 | if ($error) { |
---|
297 | loging(5, "Failed to transfert %s", $f); |
---|
298 | next; |
---|
299 | } else { |
---|
300 | loging(1, "%s (%d) transfer ok", $f, $fsize); |
---|
301 | push(@datafiles, { file => $f, size => $fsize }); |
---|
302 | } |
---|
303 | |
---|
304 | # No error running action: |
---|
305 | if (my $postrun = $config->val($entry, 'postrun')) { |
---|
306 | loging(0, "found postrun: %s for %s", $postrun || '(unset)', $entry); |
---|
307 | foreach my $filetotranfert (grep { -f $_ } ("$f.md5sum", $f)) { |
---|
308 | my $fpostrun = sprintf($postrun, $filetotranfert); |
---|
309 | loging(0, "Running `%s'", $fpostrun); |
---|
310 | my $postrunres = system($fpostrun); |
---|
311 | loging(0, "Postrun exit: %d", $postrunres); |
---|
312 | } |
---|
313 | } |
---|
314 | } |
---|
315 | |
---|
316 | my $totalsize = 0; |
---|
317 | $totalsize += $_->{size} foreach (@datafiles); |
---|
318 | loging(2, "%s: %d file transmit, %d octets", $entry, scalar(@datafiles), $totalsize); |
---|
319 | |
---|
320 | return 0; |
---|
321 | } |
---|
322 | |
---|
323 | |
---|
324 | sub send_mail { |
---|
325 | foreach $mailto (keys %${maillog}) { |
---|
326 | my %mail = ( |
---|
327 | To => $mailto, |
---|
328 | Subject => "Data transfert report", |
---|
329 | 'X-Mailer' => "Data transfert", |
---|
330 | 'X-OSBDATA-HOST' => "", |
---|
331 | Smtp => $config->val('default', 'smtp', 'localhost'), |
---|
332 | From => 'Data transfert bot <' . $config->val( |
---|
333 | 'default', |
---|
334 | 'mailfrom', |
---|
335 | 'olivier.thauvin@aerov.jussieu.fr') .'>', |
---|
336 | |
---|
337 | ); |
---|
338 | $mail{message} = ""; |
---|
339 | $mail{message} .= $config->val('default', 'mailheader', ''); |
---|
340 | $mail{message} .= "$_\n" foreach (@{$maillog->{$mailto}}); |
---|
341 | $mail{message} .= $config->val('default', 'mailfooter', ''); |
---|
342 | my $hostname = `hostname`; chomp($hostname); |
---|
343 | $mail{message} .= sprintf( |
---|
344 | "\n-- \nMail sent by transfert bot on %s\n%s\n", |
---|
345 | $hostname, |
---|
346 | q$Id$, |
---|
347 | ); |
---|
348 | |
---|
349 | sendmail(%mail) or loging(5, "Can't sent mail to %s: %s", $mailto, $Mail::Sendmail::error); |
---|
350 | } |
---|
351 | } |
---|
352 | |
---|
353 | __END__ |
---|
354 | |
---|
355 | =head1 AUTHOR |
---|
356 | |
---|
357 | Olivier Thauvin <olivier.thauvin@aerov.jussieu.fr> |
---|
358 | |
---|
359 | =head1 CHANGELOG |
---|
360 | |
---|
361 | $Log$ |
---|
362 | Revision 1.9 2005/11/22 07:48:49 thauvin |
---|
363 | - update doc |
---|
364 | |
---|
365 | Revision 1.8 2005/11/21 19:11:49 thauvin |
---|
366 | - add signature in mail |
---|
367 | |
---|
368 | Revision 1.7 2005/11/21 18:44:07 thauvin |
---|
369 | - put relative path to file for md5sum |
---|
370 | |
---|
371 | Revision 1.6 2005/11/21 18:04:46 thauvin |
---|
372 | - rework transfert, create md5sum file |
---|
373 | |
---|
374 | Revision 1.5 2005/11/18 18:40:27 thauvin |
---|
375 | - add postrun doc |
---|
376 | |
---|
377 | Revision 1.4 2005/11/18 18:29:08 thauvin |
---|
378 | - add postrun function |
---|
379 | |
---|
380 | Revision 1.3 2005/11/08 18:40:43 thauvin |
---|
381 | - add documentation |
---|
382 | |
---|
383 | Revision 1.2 2005/11/08 17:17:29 thauvin |
---|
384 | - add logfile support |
---|
385 | |
---|