File Coverage

blib/lib/App/MBUtiny.pm
Criterion Covered Total %
statement 57 271 21.0
branch 0 98 0.0
condition 0 112 0.0
subroutine 19 41 46.3
pod 6 6 100.0
total 82 528 15.5


line stmt bran cond sub pod time code
1             package App::MBUtiny; # $Id: MBUtiny.pm 129 2019-07-07 11:21:56Z abalama $
2 1     1   67936 use strict;
  1         11  
  1         29  
3 1     1   628 use utf8;
  1         15  
  1         6  
4              
5             =encoding utf-8
6              
7             =head1 NAME
8              
9             App::MBUtiny - Websites and any file system elements backup tool
10              
11             =head1 VERSION
12              
13             Version 1.12
14              
15             =head1 SYNOPSIS
16              
17             # mbutiny test
18              
19             # mbutiny backup
20              
21             # mbutiny restore
22              
23             # mbutiny report
24              
25             =head1 DESCRIPTION
26              
27             Websites and any file system elements backup tool
28              
29             =head2 FEATURES
30              
31             =over 4
32              
33             =item Backup Files and Folders
34              
35             =item Backup small databases
36              
37             =item Run external utilities for object preparation
38              
39             =item Supported storage of backups on local drives
40              
41             =item Supported storage of backups on remote SFTP storages
42              
43             =item Supported storage of backups on remote FTP storages
44              
45             =item Supported storage of backups on remote HTTP storages
46              
47             =item Easy configuration
48              
49             =item Monitoring feature enabled
50              
51             =back
52              
53             =head2 SYSTEM REQUIREMENTS
54              
55             =over 4
56              
57             =item Perl v5.16+
58              
59             =item SSH client
60              
61             =item libwww
62              
63             =item libnet
64              
65             =item zlib
66              
67             =back
68              
69             Recommended: Apache 2.2+ with CGI/FCGI modules
70              
71             =head2 INSTALLATION
72              
73             # sudo cpan install App::MBUtiny
74              
75             ...and then:
76              
77             # sudo mbutiny configure
78              
79             =head2 CONFIGURATION
80              
81             By default configuration file located in C directory
82              
83             Every configuration directive detailed described in C file, also
84             see C file for MBUtiny backup hosts configuration
85              
86             =head2 CRONTAB
87              
88             To automatically launch the program, we recommend using standard scheduling tools, such as crontab
89              
90             0 2 * * * mbutiny -l backup >/dev/null 2>>/var/log/mbutiny-error.log
91              
92             Or for selected hosts only:
93              
94             0 2 * * * mbutiny -l backup foo bar >/dev/null 2>>/var/log/mbutiny-error.log
95             15 2 * * * mbutiny -l backup baz >/dev/null 2>>/var/log/mbutiny-error.log
96              
97             For daily reporting:
98              
99             0 9 * * * mbutiny -l report >/dev/null 2>>/var/log/mbutiny-error.log
100              
101             =head2 COLLECTOR
102              
103             Collector is a monitoring server that allows you to collect data on the status of performs backups.
104             The collector allows you to build reports on the collected data from various servers.
105              
106             How it work?
107              
108             +------------+
109             | Monitoring |<--http/https-+
110             +------------+ |
111             |
112             +----------+ +-----+-----+ +----------+
113             | Server 1 |--local-->| COLLECTOR |--DBI-->| DataBase |
114             +----------+ +-----+-----+ +----------+
115             ^
116             +----------+ |
117             | Server 2 |---http/https---+
118             +----------+
119              
120             For installation of the collector Your need Apache 2.2/2.4 web server and CGI/FastCGI script.
121             See C in C directory
122              
123             =head2 HTTP SERVER
124              
125             If you want to use the HTTP server as a storage for backups, you need to install the CGI/FastCGI
126             script on Apache 2.2/2.4 web server.
127              
128             See C
129              
130             =head1 INTERNAL METHODS
131              
132             =over 4
133              
134             =item B
135              
136             The CTK method for classes extension. For internal use only!
137              
138             See L
139              
140             =item B
141              
142             The internal method for initializing the project
143              
144             =item B
145              
146             my $excdir = $app->excdir;
147              
148             Returns path to processed exclusions
149              
150              
151             =item B
152              
153             my $dbi = $app->getdbi;
154              
155             Returns DBI object
156              
157             =item B
158              
159             my $objdir = $app->objdir;
160              
161             Returns path to processed objects
162              
163             =item B
164              
165             my $rstdir = $app->rstdir;
166              
167             Returns path to restored backups
168              
169             =back
170              
171             =head1 HISTORY
172              
173             See C file
174              
175             =head1 DEPENDENCIES
176              
177             L
178              
179             =head1 TO DO
180              
181             See C file
182              
183             =head1 BUGS
184              
185             * none noted
186              
187             =head1 SEE ALSO
188              
189             L, L
190              
191             =head1 AUTHOR
192              
193             Serż Minus (Sergey Lepenkov) L Eabalama@cpan.orgE
194              
195             =head1 COPYRIGHT
196              
197             Copyright (C) 1998-2019 D&D Corporation. All Rights Reserved
198              
199             =head1 LICENSE
200              
201             This program is free software; you can redistribute it and/or
202             modify it under the same terms as Perl itself.
203              
204             See C file and L
205              
206             =cut
207              
208 1     1   68 use vars qw/ $VERSION @EXPORT /;
  1         2  
  1         67  
209             $VERSION = '1.12';
210              
211 1     1   6 use feature qw/say/;
  1         1  
  1         148  
212 1     1   6 use Carp;
  1         3  
  1         51  
213              
214 1     1   485 use Text::SimpleTable;
  1         2468  
  1         32  
215 1     1   6 use File::Spec;
  1         2  
  1         26  
216 1     1   5 use File::Path; # mkpath / rmtree
  1         2  
  1         73  
217 1     1   476 use Sys::Hostname qw/hostname/;
  1         1094  
  1         58  
218              
219 1     1   476 use CTK::Skel;
  1         202235  
  1         47  
220 1         82 use CTK::Util qw/
221             preparedir touch dtf dformat date2dig trim correct_number
222             execute sharedstatedir sendmail variant_stf
223 1     1   8 /;
  1         3  
224 1     1   7 use CTK::ConfGenUtil;
  1         2  
  1         73  
225 1     1   8 use CTK::TFVals qw/ :ALL /;
  1         1  
  1         209  
226              
227 1     1   492 use App::MBUtiny::Storage;
  1         4  
  1         42  
228 1     1   8 use App::MBUtiny::Util qw/ filesize sha1sum md5sum xcopy node2anode explain /;
  1         1  
  1         77  
229 1     1   504 use App::MBUtiny::Collector qw/ int2type /;
  1         49  
  1         71  
230 1     1   10 use App::MBUtiny::Collector::DBI qw/COLLECTOR_DB_FILENAME COLLECTOR_DB_FILE/;
  1         2  
  1         50  
231              
232 1     1   7 use base qw/ Exporter CTK::App /;
  1         2  
  1         584  
233              
234             use constant {
235 1         12780 PROJECTNAME => 'MBUtiny',
236             PREFIX => 'mbutiny',
237             OBJECTS_DIR => 'files',
238             EXCLUDE_DIR => 'excludes',
239             RESTORE_DIR => 'restore',
240             VOIDFILE => 'void.txt',
241             DATE_FORMAT => '%YYYY-%MM-%DD %hh:%mm:%ss',
242             ARC_MASK => '[HOST]-[YEAR]-[MONTH]-[DAY][EXT]',
243             TABLE_HEADERS => [(
244             [19, 'DATE'],
245             [20, 'PROCESS NAME'],
246             [58, 'DESCRIPTION OF PROCCESS / DATA OF PROCCESS'],
247             [4, 'STAT'],
248             )],
249             TEST_HEADERS => [(
250             [20, 'TEST NAME'],
251             [60, 'TEST DETAILS / TEST DATA'],
252             [4, 'STAT'],
253             )],
254             REPORT_TABLE_HEADERS => [(
255             [32, 'HOST/ADDR'],
256             [32, 'FILE/SIZE'],
257             [3, 'TYP'],
258             [19, 'DATE'],
259             [4, 'STAT'],
260             )],
261             REPORT_HOSTS_HEADERS => [(
262             [32, 'HOST'],
263             [4, 'STAT'],
264             )],
265             REPORT_COLLECTORS_HEADERS => [(
266             [95, 'URL'],
267             [4, 'STAT'],
268             )],
269              
270 1     1   6744 };
  1         3  
271              
272             @EXPORT = (qw/
273             PROJECTNAME PREFIX
274             /);
275              
276             my $TTY = 1 if -t STDOUT;
277             my $hostname = hostname() // 'unknown host';
278              
279             sub again {
280 0     0 1   my $self = shift;
281 0 0         $App::MBUtiny::Util::DEBUG = 1 if $self->debugmode;
282              
283             # Datadir & Tempdir
284 0 0         if ($self->option("datadir")) {
285 0           preparedir( $self->datadir() );
286             } else {
287 0           $self->datadir($self->tempdir());
288             }
289 0           preparedir( $self->tempdir() );
290              
291             # Collector dir
292 0           my $dbdir = File::Spec->catdir(sharedstatedir(), PREFIX);
293 0 0         preparedir( $dbdir, 0777 ) unless -e $dbdir;
294              
295             # Set paths
296 0           my $objdir = File::Spec->catdir($self->datadir, OBJECTS_DIR);
297 0           my $excdir = File::Spec->catdir($self->datadir, EXCLUDE_DIR);
298 0           my $rstdir = File::Spec->catdir($self->datadir, RESTORE_DIR);
299 0           $self->{objdir} = $objdir;
300 0           $self->{excdir} = $excdir;
301 0           $self->{rstdir} = $rstdir;
302              
303             # Prepare dirs
304 0           preparedir({
305             objdir => $objdir,
306             excdir => $excdir,
307             rstdir => $rstdir,
308             });
309              
310             # Set VoidFile
311 0           $self->{voidfile} = File::Spec->catfile($self->tempdir(), VOIDFILE);
312 0           touch($self->{voidfile});
313              
314             # Set DBI
315 0           $self->{_dbi} = undef;
316              
317 0           return $self->SUPER::again;
318             }
319 0     0 1   sub excdir {shift->{excdir}}
320 0     0 1   sub objdir {shift->{objdir}}
321 0     0 1   sub rstdir {shift->{rstdir}}
322 0     0 1   sub getdbi {shift->{_dbi}}
323              
324             __PACKAGE__->register_handler(
325             handler => "configure",
326             description => sprintf("Configure %s", PROJECTNAME),
327             code => sub { shift->configure });
328              
329             __PACKAGE__->register_handler(
330             handler => "config",
331             description => "Alias for configure command",
332             code => sub { shift->configure });
333              
334             __PACKAGE__->register_handler(
335             handler => "test",
336             description => "Testing",
337             code => sub {
338             ### CODE:
339             my ($self, $meta, @arguments) = @_;
340             $self->configure or return 0;
341             my $status = 1;
342             if ($self->testmode) {
343             say("CLI arguments: ", join("; ",@arguments) || 'none' );
344             say("Meta: ", explain($meta));
345             say("CTK object: ", explain($self));
346             say("App handlers: ", join(", ", $self->list_handlers));
347             return 1;
348             }
349              
350             # Get host-list
351             my @hosts = $self->_getHosts();
352             unless (scalar(@hosts)) {
353             $self->log_warn("No enabled configuration section found");
354             return 1;
355             }
356              
357             # Start
358             foreach my $pair (sort {(keys(%$a))[0] cmp (keys(%$b))[0]} @hosts) {
359             my @header;
360             my @errors;
361             my $step = '';
362             my $ostat = 1; # Operation status
363              
364              
365             #
366             # Init
367             #
368             my $name = _getName($pair); # Backup name
369             my $host = node($pair, $name); # Config section
370             my $hostskip = (!@arguments || grep {lc($name) eq lc($_)} @arguments) ? 0 : 1;
371             my $enabled = value($host, 'enable') ? 1 : 0;
372             if ($hostskip || !$enabled) {
373             $self->log_info("Skip testing for \"%s\" backup host section", $name);
374             next;
375             }
376             my $tbl = Text::SimpleTable->new(@{(TEST_HEADERS)});
377             $self->log_info("Start testing for \"%s\" backup host section", $name);
378             push @header, ["Backup name", $name];
379             push @errors, $self->getdbi->dsn, $self->getdbi->error, "" if $self->getdbi->error;
380              
381              
382             #
383             # Loading backup data
384             #
385             my $buday = (value($host, 'buday') // $self->config('buday')) || 0;
386             my $buweek = (value($host, 'buweek') // $self->config('buweek')) || 0;
387             my $bumonth = (value($host, 'bumonth') // $self->config('bumonth')) || 0;
388             push @header, (
389             ["Daily backups", $buday],
390             ["Weekly backups", $buweek],
391             ["Monthly backups", $bumonth],
392             );
393              
394             # Get mask vars
395             my $arc = $self->_getArc($host);
396             my $arcmask = value($host, 'arcmask') || ARC_MASK;
397             $arcmask =~ s/\[DEFAULT\]/ARC_MASK()/gie;
398             my %maskfmt = (
399             HOST => $name,
400             YEAR => '',
401             MONTH => '',
402             DAY => '',
403             EXT => value($arc, 'ext') || '',
404             );
405             push @header, ["Backup mask", $arcmask];
406              
407             # Get saved dates
408             my @dates = $self->_getDates($buday, $buweek, $bumonth);
409              
410             # Get paths
411             push @header, (
412             ["Work directory", $self->datadir],
413             ["Directory for backups", $self->objdir],
414             ["Directory for restores", $self->rstdir],
415             );
416              
417             # Regular objects
418             my $objects = array($host, 'object');
419             my $regular_objects = 0;
420             {
421             my $i = 0;
422             foreach my $o (@$objects) {
423             next unless $o;
424             my $st = (-e $o) ? 1 : 0;
425             $regular_objects++ if $st;
426             $tbl->row(sprintf("R-Object #%d", ++$i), $o, $st ? 'PASS' : 'SKIP');
427             }
428             }
429              
430             # Exclusive objects
431             my $exclude_node = _node_correct(node($host, "exclude"), "object");
432             my $exclusive_objects = 0;
433             {
434             my $i = 0;
435             foreach my $exclude (@$exclude_node) {
436             my $sgn = sprintf("X-object #%d", ++$i);
437             my $exc_name = _getName($exclude);
438             my $exc_object = uv2null(value($exclude, $exc_name, "object"));
439             if (-e $exc_object and -d $exc_object) {
440             $exclusive_objects++;
441             $tbl->row($sgn, sprintf("%s: %s", $exc_name, $exc_object), 'PASS');
442             } else {
443             $tbl->row($sgn, sprintf("%s: %s", $exc_name, $exc_object || "none"), 'SKIP');
444             }
445             }
446             }
447              
448             # Check objects
449             if ($regular_objects + $exclusive_objects) {
450             $tbl->row("Objects", sprintf("%d objects found", $regular_objects + $exclusive_objects), 'PASS');
451             } else {
452             $tbl->row("Objects", "No available objects", 'FAIL');
453             $ostat = 0;
454             }
455              
456              
457             #
458             # Checking collectors
459             #
460             $step = "Collectors checking";
461             $self->debug($step);
462             my $collector = new App::MBUtiny::Collector(
463             collector_config => $self->_getCollector($host),
464             dbi => $self->getdbi, # For local storage only
465             );
466             my $colret = $collector->check;
467             if ($collector->error) {
468             $self->log_error(sprintf("Collector error: %s", $collector->error));
469             push @errors, $collector->error, "";
470             $ostat = 0;
471             }
472             $tbl->row($step,
473             $collector->error ? "No available collectors" : $colret,
474             $collector->error ? 'FAIL' : $colret ? 'PASS' : 'SKIP',
475             );
476              
477              
478             #
479             # Testing storages
480             #
481             $step = "Storages testing";
482             $self->debug($step);
483             my $storage = new App::MBUtiny::Storage(
484             name => $name, # Backup name
485             host => $host, # Host config section
486             );
487             my $test = $storage->test or do {
488             $self->log_error($storage->error);
489             push @errors, $storage->error;
490             $ostat = 0;
491             };
492             {
493             my ($i, $j) = (0, 0);
494             foreach my $tr ($storage->test_report) {
495             my ($st, $vl, $er) = @$tr;
496             $j++ if $st && $st > 0;
497             $tbl->row(sprintf("Storage #%d", ++$i),
498             $vl, $st ? $st < 0 ? 'SKIP' : 'PASS' : 'FAIL'
499             );
500             push @errors, $er if $er;
501             }
502             $tbl->row($step,
503             $j ? sprintf("%d available storages found", $j) : "No available storages found",
504             $test ? $test < 0 ? 'SKIP' : 'PASS' : 'FAIL'
505             );
506             push @errors, "" unless $test;
507             }
508              
509              
510             #
511             # File list fetching
512             #
513             $step = "Get file list";
514             $self->debug($step);
515             my @filelist = $storage->list;
516             my $files_number = scalar(@filelist) || 0;
517             $tbl->row($step,
518             $files_number ? sprintf("%d files found", $files_number) : "No files found",
519             $storage->error ? 'FAIL' : $files_number ? 'PASS' : 'SKIP',
520             );
521             if ($storage->error) {
522             $self->log_error($storage->error);
523             push @errors, $storage->error, "";
524             $ostat = 0;
525             };
526             my $last_file = (sort {$b cmp $a} @filelist)[0];
527             if ($files_number && $last_file) {
528             push @header, ["Last backup file", $last_file];
529             my $list = hash($storage->{list});
530             foreach my $k (keys %$list) {
531             my $l = array($list, $k);
532             my $st = (grep {$_ eq $last_file} @$l) ? 1 : 0;
533             $tbl->row(sprintf("%s storage", $k),
534             $st ? sprintf("File %s is available", $last_file) : sprintf("File %s missing", $last_file),
535             $st ? 'PASS' : 'SKIP',
536             );
537             }
538             #say(explain($storage->{list}));
539             }
540              
541              
542             #
543             # Getting information about file on collector
544             #
545             my %info = $collector->info(name => $name, file => $last_file);
546             if ($collector->error) {
547             $self->log_error(sprintf("Collector error: %s", $collector->error));
548             push @errors, $collector->error, "";
549             }
550             if ($info{status}) {
551             push @header, (
552             ["File size", $info{size}],
553             ["File MD5", $info{md5}],
554             ["File SHA1", $info{sha1}],
555             );
556             }
557              
558              
559             #
560             # Get SendMail config
561             #
562             my $sm = $self->_getSendmail($host);
563             my $to = uv2null(value($sm, "to"));
564             my $send_report = 1 if $to
565             && ($to !~ /\@example.com$/)
566             && (value($sm, "sendreport") || (value($sm, "senderrorreport") && !$ostat));
567             push @header, ["Send report to", $to] if $send_report;
568              
569              
570             #
571             # Report generate
572             #
573             $tbl->hr;
574             $tbl->row('RESULT',
575             $ostat ? 'All tests successful' : 'Errors have occurred!',
576             $ostat ? 'PASS' : 'FAIL'
577             );
578             push @header, ["Summary status", $ostat ? 'PASS' : 'FAIL'];
579             my @report;
580             my $report_name = $ostat ? "report" : "error report";
581             push @report, $self->_report_common(@header); # Common information
582             push @report, $self->_report_summary($ostat ? "All tests successful" : "Errors occurred while testing"); # Summary table
583             push @report, $tbl->draw() || ''; # Table
584             push @report, $self->_report_errors(@errors); # List of occurred errors
585             if ($TTY || $self->verbosemode) { # Draw to TTY
586             printf("%s\n\n", "~" x 94);
587             printf("The %s for %s backup host\n\n", $report_name, $name);
588             print join("\n", @report, "");
589             }
590              
591              
592             #
593             # SendMail (Send report)
594             #
595             if ($send_report) {
596             unshift @report, $self->_report_title($report_name, $name);
597             push @report, $self->_report_footer();
598             my %ma = (); foreach my $k (keys %$sm) { $ma{"-".$k} = $sm->{$k} };
599             $ma{"-subject"} = sprintf("%s %s (%s on %s)", PROJECTNAME, $report_name, $name, $hostname);
600             $ma{"-message"} = join("\n", @report);
601              
602             # Send!
603             my $sent = sendmail(%ma);
604             if ($sent) { $self->debug(sprintf("Mail has been sent to: %s", $to)) }
605             else { $self->error(sprintf("Mail was not sent to: %s", $to)) }
606             }
607              
608             # Finish testing
609             $self->log_info("Finish testing for \"%s\" backup host section", $name);
610              
611             # General status
612             $status = 0 unless $ostat;
613             }
614              
615             return $status;
616             });
617              
618             __PACKAGE__->register_handler(
619             handler => "backup",
620             description => "Backup hosts",
621             code => sub {
622             ### CODE:
623             my ($self, $meta, @arguments) = @_;
624             $self->configure or return 0;
625             my $status = 1;
626              
627             # Get host-list
628             my @hosts = $self->_getHosts();
629             unless (scalar(@hosts)) {
630             $self->log_warn("No enabled configuration section found");
631             return 1;
632             }
633              
634             # Start
635             foreach my $pair (sort {(keys(%$a))[0] cmp (keys(%$b))[0]} @hosts) {
636             my @header;
637             my @errors;
638             my @paths_for_remove;
639             my $step = '';
640             my $ostat = 1; # Operation status
641              
642              
643             #
644             # Init
645             #
646             my $name = _getName($pair); # Backup name
647             my $host = node($pair, $name); # Config section
648             my $hostskip = (!@arguments || grep {lc($name) eq lc($_)} @arguments) ? 0 : 1;
649             my $enabled = value($host, 'enable') ? 1 : 0;
650             if ($hostskip || !$enabled) {
651             $self->log_info("Skip backup process for \"%s\" backup host section", $name);
652             next;
653             }
654             my $tbl = Text::SimpleTable->new(@{(TABLE_HEADERS)});
655             $self->log_info("Start backup process for \"%s\" backup host section", $name);
656             push @header, ["Backup name", $name];
657             push @errors, $self->getdbi->dsn, $self->getdbi->error, "" if $self->getdbi->error;
658              
659              
660             #
661             # Loading backup data
662             #
663             my $buday = (value($host, 'buday') // $self->config('buday')) || 0;
664             my $buweek = (value($host, 'buweek') // $self->config('buweek')) || 0;
665             my $bumonth = (value($host, 'bumonth') // $self->config('bumonth')) || 0;
666             push @header, (
667             ["Daily backups", $buday],
668             ["Weekly backups", $buweek],
669             ["Monthly backups", $bumonth],
670             );
671              
672             # Get mask vars
673             my $arc = $self->_getArc($host);
674             my $arcmask = value($host, 'arcmask') || ARC_MASK;
675             $arcmask =~ s/\[DEFAULT\]/ARC_MASK()/gie;
676             my %maskfmt = (
677             HOST => $name,
678             YEAR => '',
679             MONTH => '',
680             DAY => '',
681             EXT => value($arc, 'ext') || '',
682             );
683             push @header, ["Backup mask", $arcmask];
684              
685             # Get saved dates
686             my @dates = $self->_getDates($buday, $buweek, $bumonth);
687              
688             # Set exclusions files by dates
689             my %keepfiles;
690             foreach my $td (@dates) {
691             ($maskfmt{YEAR}, $maskfmt{MONTH}, $maskfmt{DAY}) = ($1,$2,$3) if $td =~ /(\d{4})(\d{2})(\d{2})/;
692             $keepfiles{dformat($arcmask, {%maskfmt})} = $td;
693             }
694             #say(explain(\%keepfiles));
695              
696             # Get objects
697             my $objects = array($host, 'object');
698              
699              
700             #
701             # Checking collectors
702             #
703             $step = "Collectors checking";
704             $self->debug($step);
705             my $collector = new App::MBUtiny::Collector(
706             collector_config => $self->_getCollector($host),
707             dbi => $self->getdbi, # For local storage only
708             );
709             my $colret = $collector->check;
710             if ($collector->error) {
711             $self->log_error(sprintf("Collector error: %s", $collector->error));
712             push @errors, $collector->error, "";
713             }
714             $tbl->row(dtf(DATE_FORMAT), $step,
715             $collector->error ? "No available collectors" : $colret,
716             $collector->error ? 'FAIL' : $colret ? 'PASS' : 'SKIP',
717             );
718              
719              
720             #
721             # Running triggers (commands)
722             # NOTE! Rundom order!
723             #
724             $step = "Triggers running";
725             $self->debug($step);
726             my $triggers = array($host, 'trigger');
727             my $i = 0;
728             foreach my $trg (@$triggers) {
729             my $exe_err = '';
730             my $exe_out = execute($trg, undef, \$exe_err);
731             my $exe_stt = ($? >> 8) ? 0 : 1;
732             if ($exe_stt) {
733             $self->debug(sprintf("# %s", $trg));
734             $self->debug(sprintf("%s\n", $exe_out))
735             if $self->verbosemode && defined($exe_out) && length($exe_out);
736             } else {
737             $self->log_error(sprintf("Trigger \"%s\":\n%s", $trg, $exe_err));
738             push @errors, sprintf("# %s", $trg), $exe_err, "";
739             }
740             $tbl->row(dtf(DATE_FORMAT), sprintf("Running trigger #%d", ++$i), $trg, $exe_stt ? 'PASS' : 'FAIL');
741             }
742             $tbl->row(dtf(DATE_FORMAT), $step, "No triggers found", 'SKIP') unless $i;
743              
744              
745             #
746             # Exclusion handling
747             #
748             # # -- SubDirectory name for EXCLUDE_DIR, optional
749             # Object /tmp/exclude1 # -- Source directory
750             # Target /tmp/exclude2 # -- Destination directory, optional
751             # Exclude file1.txt
752             # Exclude file2.txt
753             # Exclude foo/file2.txt
754             #
755             #
756             $step = "Exclusion handling";
757             $self->debug($step);
758             my $exclude_node = _node_correct(node($host, "exclude"), "object");
759             #say(explain($exclude_node));
760             $i = 0;
761             foreach my $exclude (@$exclude_node) {
762             my $sgn = sprintf("Exc copying #%d", ++$i);
763             my $exc_name = _getName($exclude);
764             my $exc_data = hash($exclude, $exc_name);
765             my $exc_object = uv2null(value($exc_data, "object"));
766             unless ($exc_object && (-e $exc_object and -d $exc_object)) {
767             $tbl->row(dtf(DATE_FORMAT), $sgn, sprintf("%s: %s", $exc_name, $exc_object || 'no object'), 'SKIP');
768             my $msg = sprintf("Object in section missing or incorrect directory \"%s\"", $exc_name, $exc_object);
769             $self->log_warning($msg);
770             push @errors, $msg, "";
771              
772             next;
773             }
774             my $exc_target = value($exc_data, "target") || File::Spec->catdir($self->excdir, $exc_name);
775             if ($exc_target && -e $exc_target) {
776             $tbl->row(dtf(DATE_FORMAT), $sgn, sprintf("%s: %s", $exc_name, $exc_object), 'SKIP');
777             my $msg = sprintf("Target directory that specified in section already exists: \"%s\"", $exc_name, $exc_target);
778             $self->log_warning($msg);
779             push @errors, $msg, "";
780             next;
781             }
782             my $exc_exclude = array($exc_data, "exclude") || [];
783             $self->debug(sprintf("# X-Copy \"%s\" -> \"%s\"", $exc_object, $exc_target));
784              
785             # Exclusive copying!
786             if (xcopy($exc_object, $exc_target, $exc_exclude)) {
787             $tbl->row(dtf(DATE_FORMAT), $sgn, sprintf("%s: %s", $exc_name, $exc_object), 'PASS');
788             push @$objects, $exc_target;
789             push @paths_for_remove, $exc_target;
790             } else {
791             $tbl->row(dtf(DATE_FORMAT), $sgn, sprintf("%s: %s", $exc_name, $exc_object), 'FAIL');
792             my $msg = sprintf("Copying directory \"%s\" to \"%s\" in exclusive mode failed!",
793             $exc_object, $exc_target
794             );
795             $self->log_error($msg);
796             push @errors, $msg, "";
797             }
798             }
799              
800              
801             #
802             # Objects checking
803             #
804             $step = "Objects checking";
805             $self->debug($step);
806             if (@$objects) {
807             my $j = 0; $i = 0;
808             foreach my $o (@$objects) {
809             my $st = (-e $o) ? 1 : 0;
810             $tbl->row(dtf(DATE_FORMAT), sprintf("Checking object #%d", ++$i), $o, $st ? 'PASS' : 'SKIP');
811             if ($st) { $j++ } else { $o = undef }
812             }
813             $tbl->row(dtf(DATE_FORMAT), $step,
814             $j ? sprintf("Will be processed %d objects", $j) : "No available objects found",
815             $j ? 'PASS' : 'FAIL');
816             } else {
817             $ostat = 0;
818             $tbl->row(dtf(DATE_FORMAT), $step, "Nothing to do! No objects found", 'FAIL');
819             }
820              
821              
822             #
823             # Compressing
824             #
825             $step = "Objects compressing";
826             $self->debug($step);
827             my $cdd = date2dig();
828             ($maskfmt{YEAR}, $maskfmt{MONTH}, $maskfmt{DAY}) = ($1,$2,$3) if $cdd =~ /(\d{4})(\d{2})(\d{2})/;
829             my %tmpmsk = %maskfmt; $tmpmsk{EXT} = "";
830             my $archive_name = dformat($arcmask, {%maskfmt});
831             my $archive_file = File::Spec->catfile($self->objdir, $archive_name);
832             my ($size, $md5, $sha1) = (0, "", "");
833             {
834             my $n = $self->_compress(
835             list => [grep {$_} @$objects],
836             arcdef => $arc,
837             archive=> File::Spec->catfile($self->objdir, dformat($arcmask, {%tmpmsk})),
838             );
839             my $st = $n && (-e $archive_file) ? 1 : 0;
840             if ($st) {
841             # Checksums calculation
842             $size = filesize($archive_file) // 0;
843             $md5 = md5sum($archive_file) // "";
844             $sha1 = sha1sum($archive_file) // "";
845             push @header, (
846             ["Archive name", $archive_name],
847             ["Archive size", $size],
848             ["Archive MD5", $md5],
849             ["Archive SHA1", $sha1],
850             );
851             } else {
852             my $msg = sprintf("Compressing objects to \"%s\" failed: %s", $archive_file, $self->error);
853             $self->log_error($msg);
854             push @errors, $msg, "";
855             $ostat = 0;
856             }
857             $tbl->row(dtf(DATE_FORMAT), $step, $archive_name, $st ? 'PASS' : 'FAIL');
858             }
859              
860              
861             #
862             # Testing storages
863             #
864             $step = "Storages testing";
865             $self->debug($step);
866             my $storage = new App::MBUtiny::Storage(
867             name => $name, # Backup name
868             host => $host, # Host config section
869             path => $self->objdir, # Where is located backup archive
870             fixup => sub {
871             my $strg = shift; # Storage object
872             my $oper = shift // 'noop'; # Operation name
873             my $colret;
874             if ($oper =~ /^(del)|(rem)/i) {
875             my $f = shift;
876             $colret = $collector->fixup(
877             operation => $oper,
878             name => $name,
879             file => $f,
880             );
881             } else {
882             my $stts = shift // 0; # Operation status
883             my $cmnt = shift // ''; # Comment (details)
884             $colret = $collector->fixup(
885             operation => $oper,
886             status => $stts,
887             error => $strg->error,
888             name => $name,
889             file => $archive_name,
890             size => $size,
891             md5 => $md5,
892             sha1 => $sha1,
893             comment => $cmnt,
894             );
895             }
896             if ($collector->error) {
897             my $msg = sprintf("Fixing error: %s", $collector->error);
898             $self->log_error($msg);
899             push @errors, $msg, "";
900             }
901             $tbl->row(dtf(DATE_FORMAT), "Fixing on collector",
902             $colret || "No available collectors found",
903             $collector->error ? 'FAIL' : $colret ? 'PASS' : 'SKIP',
904             );
905             },
906             );
907             my $test = $storage->test or do {
908             $self->log_error($storage->error);
909             push @errors, $storage->error;
910             $ostat = 0;
911             };
912             {
913             my $j = 0; $i = 0;
914             foreach my $tr ($storage->test_report) {
915             my ($st, $vl, $er) = @$tr;
916             $j++ if $st && $st > 0;
917             $tbl->row(dtf(DATE_FORMAT), sprintf("Testing storage #%d", ++$i),
918             $vl, $st ? $st < 0 ? 'SKIP' : 'PASS' : 'FAIL'
919             );
920             push @errors, $er if $er;
921             }
922             $tbl->row(dtf(DATE_FORMAT), $step,
923             $j ? sprintf("Will be used %d storages", $j) : "No available storages found",
924             $test ? $test < 0 ? 'SKIP' : 'PASS' : 'FAIL'
925             );
926             push @errors, "" unless $test;
927             }
928              
929              
930             #
931             # File list fetching
932             #
933             $step = "File list fetching";
934             $self->debug($step);
935             my @filelist = $storage->list;
936             $tbl->row(dtf(DATE_FORMAT), $step,
937             join("\n", @filelist) || "No files found",
938             $storage->error ? 'FAIL' : @filelist ? 'PASS' : 'SKIP',
939             );
940             if ($storage->error) {
941             $self->log_error($storage->error);
942             push @errors, $storage->error, "";
943             $ostat = 0;
944             };
945             #say(explain(\@filelist));
946              
947              
948             #
949             # Deleting old files
950             #
951             #say(explain(\%keepfiles));
952             $step = "Deleting old files";
953             $self->debug($step);
954             {
955             my $j = 0; $i = 0;
956             foreach my $f (@filelist) {
957             next if $keepfiles{$f};
958             my $st = -1; # SKIP
959             if ($test > 0) { # Test PASSed!
960             $st = $storage->del($f);
961             if ($st) {
962             $j++;
963             } else {
964             $self->log_error($storage->error);
965             push @errors, $storage->error, "";
966             $ostat = 0;
967             };
968             }
969             $tbl->row(dtf(DATE_FORMAT), sprintf("Deleting file #%d", ++$i),
970             $f,
971             $st ? $st < 0 ? 'SKIP' : 'PASS' : 'FAIL'
972             );
973             }
974             $tbl->row(dtf(DATE_FORMAT), $step,
975             $j ? sprintf("Were deleted %d files", $j) : "No files for delete found",
976             $test ? $test < 0 ? 'SKIP' : 'PASS' : 'FAIL'
977             );
978             }
979              
980              
981             #
982             # Backup archive
983             #
984             $step = "Backup performing";
985             $self->debug($step);
986             {
987             my $st = -1; # SKIP
988             if ($test > 0) { # Test PASSed!
989             $st = $storage->put(
990             name => $archive_name,
991             file => $archive_file,
992             size => $size,
993             );
994             unless ($st) {
995             $self->log_error($storage->error);
996             push @errors, $storage->error, "";
997             $ostat = 0;
998             };
999             }
1000             $tbl->row(dtf(DATE_FORMAT), $step,
1001             $archive_name,
1002             $st ? $st < 0 ? 'SKIP' : 'PASS' : 'FAIL'
1003             );
1004             }
1005              
1006              
1007             #
1008             # Removing temporary data
1009             #
1010             $step = "Cleaning";
1011             $self->debug($step);
1012             $self->error("");
1013             if (-e $archive_file) {
1014             $self->debug(sprintf("# unlink \"%s\"", $archive_file));
1015             if (unlink($archive_file)) {
1016             $tbl->row(dtf(DATE_FORMAT), $step, $archive_file, 'PASS');
1017             } else {
1018             my $msg = sprintf("Can't delete file %s: %s", $archive_file, $!);
1019             $self->log_error($msg);
1020             push @errors, $msg, "";
1021             $ostat = 0;
1022             $tbl->row(dtf(DATE_FORMAT), $step, $archive_file, 'FAIL');
1023             }
1024             } else {
1025             $tbl->row(dtf(DATE_FORMAT), $step, $archive_file, 'SKIP');
1026             }
1027             foreach my $rmo (@paths_for_remove) {
1028             $self->debug(sprintf("# rmtree \"%s\"", $rmo));
1029             rmtree($rmo) if -e $rmo;
1030             }
1031              
1032             #
1033             # Get SendMail config
1034             #
1035             my $sm = $self->_getSendmail($host);
1036             my $to = uv2null(value($sm, "to"));
1037             my $send_report = 1 if $to
1038             && ($to !~ /\@example.com$/)
1039             && (value($sm, "sendreport") || (value($sm, "senderrorreport") && !$ostat));
1040             push @header, ["Send report to", $to] if $send_report;
1041              
1042             #
1043             # Report generate
1044             #
1045             $tbl->hr;
1046             $tbl->row(dtf(DATE_FORMAT), 'RESULT',
1047             $ostat ? 'All processes successful' : 'Errors have occurred!',
1048             $ostat ? 'PASS' : 'FAIL'
1049             );
1050             push @header, ["Summary status", $ostat ? 'PASS' : 'FAIL'];
1051             my @report;
1052             my $report_name = $ostat ? "backup report" : "backup error report";
1053             push @report, $self->_report_common(@header); # Common information
1054             push @report, $self->_report_summary($ostat ? "Backup is done" : "Errors occurred while performing backup"); # Summary table
1055             push @report, $tbl->draw() || ''; # Table
1056             push @report, $self->_report_errors(@errors); # List of occurred errors
1057             if ($TTY || $self->verbosemode) { # Draw to TTY
1058             printf("%s\n\n", "~" x 114);
1059             printf("The %s for %s backup host\n\n", $report_name, $name);
1060             print join("\n", @report, "");
1061             }
1062              
1063              
1064             #
1065             # SendMail (Send report)
1066             #
1067             if ($send_report) {
1068             unshift @report, $self->_report_title($report_name, $name);
1069             push @report, $self->_report_footer();
1070             my %ma = (); foreach my $k (keys %$sm) { $ma{"-".$k} = $sm->{$k} };
1071             $ma{"-subject"} = sprintf("%s %s (%s on %s)", PROJECTNAME, $report_name, $name, $hostname);
1072             $ma{"-message"} = join("\n", @report);
1073              
1074             # Send!
1075             my $sent = sendmail(%ma);
1076             if ($sent) { $self->debug(sprintf("Mail has been sent to: %s", $to)) }
1077             else { $self->error(sprintf("Mail was not sent to: %s", $to)) }
1078             }
1079              
1080             # Finish backup
1081             $self->log_info("Finish backup process for \"%s\" backup host section", $name);
1082              
1083             # General status
1084             $status = 0 unless $ostat;
1085             }
1086              
1087             return $status;
1088             });
1089              
1090             __PACKAGE__->register_handler(
1091             handler => "restore",
1092             description => "Restore hosts",
1093             code => sub {
1094             ### CODE:
1095             my ($self, $meta, @arguments) = @_;
1096             $self->configure or return 0;
1097             my $status = 1;
1098              
1099             # Get host-list
1100             my @hosts = $self->_getHosts();
1101             unless (scalar(@hosts)) {
1102             $self->log_warn("No enabled configuration section found");
1103             return 1;
1104             }
1105              
1106             # Date defined
1107             my $tdate = pop @arguments;
1108             my ( $_y, $_m, $_d ) = (localtime( time ))[5,4,3];
1109             my @ymd = (($_y+1900), ($_m+1), $_d);
1110             my $is_now = 1;
1111             if (defined($tdate)) {
1112             if ($tdate =~ /(\d{4})\D+(\d{2})\D+(\d{2})/) { # YYYY-MM-DD
1113             @ymd = ($1,$2,$3);
1114             $is_now = 0;
1115             } elsif ($tdate =~ /(\d{2})\D+(\d{2})\D+(\d{4})/) { # DD-MM-YYY
1116             @ymd = ($3,$2,$1);
1117             $is_now = 0;
1118             } else {
1119             push @arguments, $tdate;
1120             }
1121             }
1122              
1123             # Start
1124             foreach my $pair (sort {(keys(%$a))[0] cmp (keys(%$b))[0]} @hosts) {
1125             my @header;
1126             my @errors;
1127             my $step = '';
1128             my $ostat = 1; # Operation status
1129              
1130              
1131             #
1132             # Init
1133             #
1134             my $name = _getName($pair); # Backup name
1135             my $host = node($pair, $name); # Config section
1136             my $hostskip = (!@arguments || grep {lc($name) eq lc($_)} @arguments) ? 0 : 1;
1137             my $enabled = value($host, 'enable') ? 1 : 0;
1138             if ($hostskip || !$enabled) {
1139             $self->log_info("Skip restore process for \"%s\" backup host section", $name);
1140             next;
1141             }
1142             my $tbl = Text::SimpleTable->new(@{(TABLE_HEADERS)});
1143             $self->log_info("Start restore process for \"%s\" backup host section", $name);
1144             push @header, ["Backup name", $name];
1145             push @errors, $self->getdbi->dsn, $self->getdbi->error, "" if $self->getdbi->error;
1146              
1147             # Get mask vars
1148             my $arc = $self->_getArc($host);
1149             my $arcmask = value($host, 'arcmask') || ARC_MASK;
1150             $arcmask =~ s/\[DEFAULT\]/ARC_MASK()/gie;
1151             my %maskfmt = (
1152             HOST => $name,
1153             YEAR => sprintf("%04d", $ymd[0]),
1154             MONTH => sprintf("%02d", $ymd[1]),
1155             DAY => sprintf("%02d", $ymd[2]),
1156             EXT => value($arc, 'ext') || '',
1157             );
1158             my $archive_name = dformat($arcmask, {%maskfmt});
1159             push @header, ["Backup mask", $arcmask];
1160              
1161              
1162             #
1163             # Checking collectors
1164             #
1165             $step = "Collectors checking";
1166             $self->debug($step);
1167             my $collector = new App::MBUtiny::Collector(
1168             collector_config => $self->_getCollector($host),
1169             dbi => $self->getdbi, # For local storage only
1170             );
1171             my $colret = $collector->check;
1172             if ($collector->error) {
1173             $self->log_error(sprintf("Collector error: %s", $collector->error));
1174             push @errors, $collector->error, "";
1175             }
1176             $tbl->row(dtf(DATE_FORMAT), $step,
1177             $collector->error ? "No available collectors" : $colret,
1178             $collector->error ? 'FAIL' : $colret ? 'PASS' : 'SKIP',
1179             );
1180              
1181              
1182             #
1183             # Getting information about file on collector
1184             #
1185             my %info = $collector->info(name => $name, file => $is_now ? undef : $archive_name);
1186             if ($collector->error) {
1187             $self->log_error(sprintf("Collector error: %s", $collector->error));
1188             push @errors, $collector->error, "";
1189             }
1190             if ($info{status}) {
1191             $archive_name = $info{file} if $is_now;
1192             push @header, ["Archive name", $archive_name];
1193             push @header, (
1194             ["Archive size", $info{size}],
1195             ["Archive MD5", $info{md5}],
1196             ["Archive SHA1", $info{sha1}],
1197             );
1198             } else {
1199             push @header, ["Archive name", $archive_name];
1200             }
1201             my $archive_file = File::Spec->catfile($self->rstdir, $archive_name);
1202             push @header, ["Archive file", $archive_file];
1203              
1204              
1205             #
1206             # Testing storages
1207             #
1208             $step = "Storages testing";
1209             $self->debug($step);
1210             my $storage = new App::MBUtiny::Storage(
1211             name => $name, # Backup name
1212             host => $host, # Host config section
1213             path => $self->rstdir, # Where is located restored backup archive
1214             validate => sub {
1215             my $strg = shift; # storage object
1216             my $file = shift; # fetched file
1217             if ($info{size}) { # Valid sizes
1218             my $size = filesize($file) // 0;
1219             unless ($size == $info{size}) {
1220             $strg->error(sprintf("File size incorrect: got=%d; expected=%d", $size, $info{size}));
1221             return 0;
1222             }
1223             }
1224             if ($info{md5}) { # Valid md5
1225             my $md5 = md5sum($file) // "";
1226             unless ($md5 eq $info{md5}) {
1227             $strg->error(sprintf("File MD5 checksum incorrect: got=%s; expected=%s", $md5, $info{md5}));
1228             return 0;
1229             }
1230             }
1231             if ($info{sha1}) { # Valid sha1
1232             my $sha1 = sha1sum($file) // "";
1233             unless ($sha1 eq $info{sha1}) {
1234             $strg->error(sprintf("File SHA1 checksum incorrect: got=%s; expected=%s", $sha1, $info{sha1}));
1235             return 0;
1236             }
1237             }
1238             return 1;
1239             });
1240             my $test = $storage->test or do {
1241             $self->log_error($storage->error);
1242             push @errors, $storage->error;
1243             $ostat = 0;
1244             };
1245             {
1246             my $j = 0; my $i = 0;
1247             foreach my $tr ($storage->test_report) {
1248             my ($st, $vl, $er) = @$tr;
1249             $j++ if $st && $st > 0;
1250             $tbl->row(dtf(DATE_FORMAT), sprintf("Testing storage #%d", ++$i),
1251             $vl, $st ? $st < 0 ? 'SKIP' : 'PASS' : 'FAIL'
1252             );
1253             push @errors, $er if $er;
1254             }
1255             $tbl->row(dtf(DATE_FORMAT), $step,
1256             $j ? sprintf("Will be used %d storages", $j) : "No available storages found",
1257             $test ? $test < 0 ? 'SKIP' : 'PASS' : 'FAIL'
1258             );
1259             push @errors, "" unless $test;
1260             }
1261              
1262              
1263             #
1264             # File list fetching
1265             #
1266             $step = "File list fetching";
1267             $self->debug($step);
1268             my @filelist = $storage->list;
1269             $tbl->row(dtf(DATE_FORMAT), $step,
1270             join("\n", @filelist) || "No files found",
1271             $storage->error ? 'FAIL' : @filelist ? 'PASS' : 'SKIP',
1272             );
1273             if ($storage->error) {
1274             $self->log_error($storage->error);
1275             push @errors, $storage->error, "";
1276             $ostat = 0;
1277             };
1278             my $is_exists = 0;
1279             if (grep {$_ eq $archive_name} @filelist) {
1280             $tbl->row(dtf(DATE_FORMAT), "The file searching", $archive_name, 'PASS');
1281             $is_exists = 1;
1282             } else {
1283             $tbl->row(dtf(DATE_FORMAT), "The file searching", "File not found", 'SKIP');
1284             }
1285              
1286              
1287             #
1288             # Restore archive
1289             #
1290             $step = "Restore performing";
1291             $self->debug($step);
1292             my $is_downloaded = 0;
1293             {
1294             my $st = -1; # SKIP
1295             if ($is_exists && $test > 0) { # Test PASSed and file is exists on storages!
1296             $st = $storage->get(
1297             name => $archive_name,
1298             file => $archive_file,
1299             );
1300             if ($st) {
1301             $is_downloaded = 1 if $st == 1;
1302             } else {
1303             $self->log_error($storage->error);
1304             push @errors, $storage->error, "";
1305             $ostat = 0;
1306             };
1307             }
1308             $tbl->row(dtf(DATE_FORMAT), $step,
1309             $archive_name,
1310             $st ? $st < 0 ? 'SKIP' : 'PASS' : 'FAIL'
1311             );
1312             }
1313             #print(explain($storage->{storages}));
1314              
1315             #
1316             # Extracting archive
1317             #
1318             $step = "Extracting archive";
1319             $self->debug($step);
1320             my $restore_dir = File::Spec->catdir($self->rstdir, $name,
1321             sprintf("%04d-%02d-%02d", $ymd[0], $ymd[1], $ymd[2]));
1322             if ($is_downloaded) {
1323             preparedir($restore_dir);
1324             my $st = $self->_extract(
1325             arcdef => $arc,
1326             archive=> $archive_file,
1327             dirdst => $restore_dir,
1328             );
1329             if ($st) {
1330             push @header, ["Location of restored backup", $restore_dir];
1331             $self->log_info("Downloaded backup archive: %s", $archive_file);
1332             $self->log_info("Location of restored backup: %s", $restore_dir);
1333             } else {
1334             my $msg = sprintf("Extracting archive \"%s\" failed: %s", $archive_file, $self->error);
1335             $self->log_error($msg);
1336             push @errors, $msg, "";
1337             $ostat = 0;
1338             }
1339             $tbl->row(dtf(DATE_FORMAT), $step, $archive_name, $st ? 'PASS' : 'FAIL');
1340             } else {
1341             $tbl->row(dtf(DATE_FORMAT), $step, $archive_name, 'SKIP');
1342             }
1343              
1344              
1345             #
1346             # Report generate
1347             #
1348             $tbl->hr;
1349             $tbl->row(dtf(DATE_FORMAT), 'RESULT',
1350             $ostat ? 'All processes successful' : 'Errors have occurred!',
1351             $ostat ? 'PASS' : 'FAIL'
1352             );
1353             push @header, ["Summary status", $ostat ? 'PASS' : 'FAIL'];
1354             my @report;
1355             my $report_name = $ostat ? "restore report" : "restore error report";
1356             push @report, $self->_report_common(@header); # Common information
1357             push @report, $self->_report_summary($ostat ? "Restore is done" : "Errors occurred while performing restore"); # Summary table
1358             push @report, $tbl->draw() || ''; # Table
1359             push @report, $self->_report_errors(@errors); # List of occurred errors
1360             if ($TTY || $self->verbosemode) { # Draw to TTY
1361             printf("%s\n\n", "~" x 114);
1362             printf("The %s for %s backup host\n\n", $report_name, $name);
1363             print join("\n", @report, "");
1364             }
1365              
1366              
1367             # Finish restore
1368             $self->log_info("Finish restore process for \"%s\" backup host section", $name);
1369              
1370             # General status
1371             $status = 0 unless $ostat;
1372             }
1373              
1374             return $status;
1375             });
1376              
1377             __PACKAGE__->register_handler(
1378             handler => "report",
1379             description => "Reporting",
1380             code => sub {
1381             ### CODE:
1382             my ($self, $meta, @arguments) = @_;
1383             $self->configure or return 0;
1384             my $status = 1;
1385             my @header;
1386             my @errors;
1387             my @comments;
1388              
1389             # Get host-list
1390             my @hosts = $self->_getHosts();
1391             unless (scalar(@hosts)) {
1392             $self->log_warn("No enabled configuration section found");
1393             return 1;
1394             }
1395              
1396              
1397             #
1398             # Init
1399             #
1400             my $tbl_report = Text::SimpleTable->new(@{(REPORT_TABLE_HEADERS)});
1401             my $tbl_hosts = Text::SimpleTable->new(@{(REPORT_HOSTS_HEADERS)});
1402             my $tbl_collectors = Text::SimpleTable->new(@{(REPORT_COLLECTORS_HEADERS)});
1403             $self->log_info("Start reporting for \"%s\"", $hostname);
1404             push @header, ["Hostname", $hostname];
1405             push @errors, $self->getdbi->dsn, $self->getdbi->error, "" if $self->getdbi->error;
1406             my @req_hosts = map {$_ = trim($_) } split(/\s+/, $self->config('require') || '');
1407              
1408             #
1409             # Hosts processing
1410             #
1411             my @collectors = ();
1412             foreach my $pair (sort {(keys(%$a))[0] cmp (keys(%$b))[0]} @hosts) {
1413             my $name = _getName($pair); # Backup name
1414             my $host = node($pair, $name); # Config section
1415             my $hostskip = (!@arguments || grep {lc($name) eq lc($_)} @arguments) ? 0 : 1;
1416             my $enabled = value($host, 'enable') ? 1 : 0;
1417             $tbl_hosts->row($name, ($hostskip || !$enabled) ? 'SKIP' : 'PASS');
1418             if ($hostskip || !$enabled) {
1419             $self->log_info("Skip reporting for \"%s\" backup host section", $name);
1420             next;
1421             }
1422             my $lcols = $self->_getCollector($host);
1423             push @collectors, @$lcols;
1424             }
1425             push @collectors, {} unless @collectors; # Default support
1426             #say(explain(\@collectors));
1427              
1428             #
1429             # Select collectors
1430             #
1431             my %cols;
1432             foreach my $col (@collectors) {
1433             my $url = value($col, 'url') || 'local';
1434             $cols{$url} = $col unless $cols{$url};
1435             }
1436             @collectors = values %cols;
1437             #say(explain(\@collectors));
1438              
1439              
1440             #
1441             # Collectors checking
1442             #
1443             my @ok_collectors = ();
1444             foreach my $col (@collectors) {
1445             my $url = value($col, 'url') || 'local';
1446             my $comment = value($col, 'comment');
1447             my $collector = new App::MBUtiny::Collector(
1448             collector_config => [$col],
1449             dbi => $self->getdbi, # For local storage only
1450             );
1451             my $colret = $collector->check;
1452             $tbl_collectors->row($colret, $collector->error ? 'FAIL' : $colret ? 'PASS' : 'SKIP');
1453             push @comments, sprintf("%s: %s", $colret || $url, $comment), "" if $comment;
1454             if ($collector->error) {
1455             $self->log_error(sprintf("Collector error: %s", $collector->error));
1456             push @errors, $collector->error, "";
1457             next;
1458             }
1459             next unless $colret;
1460             push @ok_collectors, $col
1461             }
1462              
1463             #
1464             # Collectors processing
1465             #
1466             my @backups;
1467             if (@ok_collectors) {
1468             my $collector = new App::MBUtiny::Collector(
1469             collector_config => [@ok_collectors],
1470             dbi => $self->getdbi, # For local storage only
1471             );
1472             @backups = $collector->report(); # start => 1561799600;
1473             if ($collector->error) {
1474             $self->log_error(sprintf("Collector error: %s", $collector->error));
1475             push @errors, $collector->error, "";
1476             }
1477             }
1478              
1479             #
1480             # Get report data about LAST backups on collector for each available host
1481             #
1482             my %requires;
1483             foreach (@req_hosts) {$requires{$_} = 0};
1484             foreach my $rec (@backups) {
1485             push @comments, sprintf("%s: %s", uv2null($rec->{file}), $rec->{comment}), "" if $rec->{comment};
1486             push @errors, uv2null($rec->{file}), $rec->{error}, "" if $rec->{error};
1487             my $nm = $rec->{name} || 'virtual';
1488             $tbl_report->row(
1489             sprintf("%s\n%s", $nm, uv2null($rec->{addr})),
1490             sprintf("%s\n%s (%s bytes)",
1491             variant_stf(uv2null($rec->{file}), 32),
1492             _fbytes(uv2zero($rec->{size})),
1493             correct_number(uv2zero($rec->{size}))
1494             ),
1495             uc(substr(int2type(uv2zero($rec->{type})), 0, 3)),
1496             dtf(DATE_FORMAT, $rec->{'time'}),
1497             $rec->{status} ? 'PASS' : 'FAIL',
1498             );
1499             $requires{$nm} = 1 if $rec->{status};
1500             }
1501              
1502             #
1503             # Requires
1504             #
1505             if (grep { !$_ } values(%requires)) {
1506             $tbl_report->hr;
1507             foreach my $nm (grep {!$requires{$_}} keys %requires) {
1508             $tbl_report->row($nm,'','','',"UNKN");
1509             }
1510             $status = 0;
1511             }
1512              
1513              
1514             #
1515             # Get SendMail config
1516             #
1517             my $sm = $self->_getSendmail();
1518             my $to = uv2null(value($sm, "to"));
1519             my $send_report = 1 if $to
1520             && ($to !~ /\@example.com$/)
1521             && (value($sm, "sendreport") || (!$status && value($sm, "senderrorreport")));
1522             push @header, ["Send report to", $to] if $send_report;
1523              
1524              
1525             #
1526             # Report generate
1527             #
1528             $tbl_report->hr;
1529             $tbl_report->row('RESULT', '', '', '', $status ? 'PASS' : 'FAIL');
1530             push @header, ["Summary status", $status ? 'PASS' : 'FAIL'];
1531             my @report;
1532             my $report_name = $status ? "report" : "error report";
1533             push @report, $self->_report_common(@header); # Common information
1534             push @report, "Hosts:", $tbl_hosts->draw(); # Hosts table
1535             push @report, "Collectors:", $tbl_collectors->draw(); # Hosts table
1536             push @report, $self->_report_summary($status ? "All tests successful" : "Errors occurred while testing"); # Summary table
1537             push @report, $tbl_report->draw(); # Report table
1538             push @report, "Comments:", "", @comments, "" if @comments;
1539             push @report, $self->_report_errors(@errors); # List of occurred errors
1540             if ($TTY || $self->verbosemode) { # Draw to TTY
1541             printf("%s\n\n", "~" x 106);
1542             printf("The %s for all backup hosts on %s\n\n", $report_name, $hostname);
1543             print join("\n", @report, "");
1544             }
1545              
1546              
1547             #
1548             # SendMail (Send report)
1549             #
1550             if ($send_report) {
1551             unshift @report, $self->_report_title($report_name, "last backups");
1552             push @report, $self->_report_footer();
1553             my %ma = (); foreach my $k (keys %$sm) { $ma{"-".$k} = $sm->{$k} };
1554             $ma{"-subject"} = sprintf("%s %s (%s on %s)", PROJECTNAME, $report_name, "last backups", $hostname);
1555             $ma{"-message"} = join("\n", @report);
1556              
1557             # Send!
1558             my $sent = sendmail(%ma);
1559             if ($sent) { $self->debug(sprintf("Mail has been sent to: %s", $to)) }
1560             else { $self->error(sprintf("Mail was not sent to: %s", $to)) }
1561             }
1562              
1563             # Finish reporting
1564             $self->log_info("Finish reporting for \"%s\"", $hostname);
1565              
1566             return $status;
1567             });
1568              
1569              
1570             sub configure {
1571 0     0 1   my $self = shift;
1572 0           my $config = $self->configobj;
1573              
1574             # DBI object
1575 0   0       my $dbi_conf = $self->config('dbi') || {};
1576 0 0         $dbi_conf = {} unless is_hash($dbi_conf);
1577 0           my $dbi = new App::MBUtiny::Collector::DBI(%$dbi_conf);
1578 0           $self->{_dbi} = $dbi;
1579 0 0         if ($config->status) {
1580 0 0         $self->error($dbi->error) if $dbi->error;
1581 0           return 1;
1582             }
1583              
1584             # Creting DB
1585 0 0         if ($dbi->is_sqlite) {
1586 0           printf("Creating local database %s...\n", $dbi->{file});
1587             } else {
1588 0           printf("Checking database %s...\n", $dbi->dsn);
1589             }
1590 0 0         if ($dbi->error) {
1591 0           say "Fail.";
1592 0           $self->error($dbi->error);
1593             } else {
1594 0           say "Done.";
1595             }
1596              
1597             # Creating configuration
1598 0           my $skel = new CTK::Skel (
1599             -name => PROJECTNAME,
1600             -root => $self->root,
1601             -skels => {
1602             config => 'App::MBUtiny::ConfigSkel',
1603             },
1604             -vars => {
1605             PROJECT => PROJECTNAME,
1606             PROJECTNAME => PROJECTNAME,
1607             PREFIX => PREFIX,
1608             },
1609             -debug => $self->debugmode,
1610             );
1611             #say("Skel object: ", explain($skel));
1612 0           printf("Creating configuration to %s...\n", $self->root);
1613 0 0         if ($skel->build("config")) {
1614 0           $self->CTK::Plugin::Config::init;
1615 0           $config = $self->configobj;
1616 0 0         unless ($config->status) {
1617 0           say "Fail.";
1618 0           return 0;
1619             }
1620 0           say "Done.";
1621             } else {
1622 0           say "Fail.";
1623 0           $self->error(sprintf("Can't %s initialize: %s", PREFIX, $self->root));
1624 0           return 0;
1625             }
1626 0           return 1;
1627             }
1628              
1629             # Private methods
1630             sub _getHosts { # Get host-sections as array of hashes
1631 0     0     my $self = shift;
1632 0           my $hosts = $self->config("host");
1633 0           my @jobs = ();
1634 0 0 0       if (ref($hosts) eq 'ARRAY') {
    0          
1635 0           foreach my $r (@$hosts) {
1636 0 0 0       if ((ref($r) eq 'HASH') && exists $r->{enable}) {
    0          
1637 0           push @jobs, $r;
1638             } elsif (ref($r) eq 'HASH') {
1639 0           foreach my $k (keys %$r) {
1640 0           push @jobs, { $k => $r->{$k} };
1641             }
1642             }
1643             }
1644             } elsif ((ref($hosts) eq 'HASH') && !exists $hosts->{enable}) {
1645 0           foreach my $k (keys %$hosts) {
1646 0           push @jobs, { $k => $hosts->{$k} };
1647             }
1648             } else {
1649 0           push @jobs, $hosts;
1650             }
1651 0           return @jobs;
1652             }
1653             sub _getDates { # Get available dates
1654 0     0     my $self = shift;
1655 0   0       my $buday = shift || 0; # Dayly
1656 0   0       my $buweek = shift || 0; # Weekly
1657 0   0       my $bumonth = shift || 0; # Monthly
1658              
1659 0           my %dates = ();
1660 0           my $wcnt = 0;
1661 0           my $mcnt = 0;
1662              
1663             # Set period as maximum days to "back"
1664 0 0         my $period = 7 * $buweek > $buday ? 7 * $buweek : $buday;
1665 0 0         $period = 30 * $bumonth if 30 * $bumonth > $period;
1666              
1667 0           for (my $i=0; $i<$period; $i++) {
1668 0           my ( $y, $m, $d, $wd ) = (localtime( time - $i * 86400 ))[5,4,3,6];
1669 0           my $date = sprintf( "%04d%02d%02d", ($y+1900), ($m+1), $d );
1670              
1671 0 0 0       if (($i < $buday)
      0        
      0        
      0        
1672             || (($i < $buweek * 7) && $wd == 0) # do weekly backups on sunday
1673             || (($i < $bumonth * 30) && $d == 1)) # do monthly backups on 1-st day of month
1674             {
1675 0           $dates{ $date } = 1;
1676             } else {
1677 0           $dates{ $date } = 0;
1678             }
1679              
1680 0 0 0       if (($i < $buday) || (($wd == 0) && (($wcnt++) < $buweek)) || (($d == 1) && (($mcnt++) < $bumonth))) {
      0        
      0        
      0        
1681 0           $dates{$date} ++;
1682             }
1683              
1684 0 0         delete $dates{$date} unless $dates{$date};
1685             }
1686              
1687 0           return sort keys %dates;
1688             }
1689             sub _getArc { # Get arc section or default arcdef record
1690 0     0     my $self = shift;
1691 0   0       my $host = shift || {};
1692 0           my $arcdef = hash($self->config('arc'));
1693 0 0         $arcdef = CTK::Plugin::Archive::ARC_OPTIONS()->{CTK::Plugin::Archive::ARC_DEFAULT()}
1694             unless value($arcdef, 'ext');
1695 0           my $arc = hash($host, 'arc');
1696 0 0         return $arcdef unless value($arc, 'ext');
1697 0           return $arc;
1698             }
1699             sub _getCollector {
1700 0     0     my $self = shift;
1701 0   0       my $host = shift || {};
1702 0           my $collector_def = $self->config('collector');
1703 0           my $collector = node($host, 'collector');
1704 0 0         return node2anode($collector_def) if is_void($collector);
1705 0           return node2anode($collector);
1706             }
1707             sub _getSendmail {
1708 0     0     my $self = shift;
1709 0   0       my $host = shift || {};
1710 0           my $sm_def = hash($self->config('sendmail'));
1711 0           my $sm = hash($host, 'sendmail');
1712 0           my %out = %$sm_def;
1713 0           foreach my $k (keys %$sm) {
1714 0 0         $out{$k} = $sm->{$k} if exists($sm->{$k})
1715             }
1716              
1717 0   0       $out{sendreport} = (value($host => 'sendreport') // $self->config('sendreport')) || 0;
1718 0   0       $out{senderrorreport} = (value($host => 'senderrorreport') // $self->config('senderrorreport')) || 0;
1719              
1720 0           return {%out};
1721             }
1722             sub _compress {
1723 0     0     my $self = shift;
1724 0           my %args = @_;
1725 0   0       my $list = $args{list} || [];
1726 0   0       my $arcdef = $args{arcdef} || {};
1727 0   0       my $archive = $args{archive} || "";
1728              
1729             # Arc
1730 0           my $arc_create = $arcdef->{create};
1731 0   0       my $arc_append = $arcdef->{append} || $arc_create;
1732 0   0       my $arc_ext = $arcdef->{ext} || '';
1733 0           my $arc_proc = $arcdef->{postprocess};
1734              
1735             # Compress
1736 0           my $count = 0;
1737 0           foreach my $o (@$list) {
1738 0           my $rplc = {
1739             NAME => $archive,
1740             EXT => $arc_ext,
1741             FILE => sprintf("%s%s", $archive, $arc_ext),
1742             LIST => $o,
1743             };
1744 0 0         my $cmd = $count ? dformat($arc_append, $rplc) : dformat($arc_create, $rplc);
1745 0           $self->debug(sprintf("# %s", $cmd));
1746 0           my $errdata = "";
1747 0           my $outdata = execute( $cmd, undef, \$errdata, 1 );
1748 0           my $exe_stt = $? >> 8;
1749 0 0 0       $self->debug($outdata) if $self->verbosemode && defined($outdata) && length($outdata);
      0        
1750 0 0         $self->error($errdata) if $exe_stt;
1751 0           $count++;
1752             }
1753              
1754             # PostProc
1755 0           my @postproc;
1756 0 0 0       if ($arc_proc && ref($arc_proc) eq "ARRAY") {@postproc = @$arc_proc}
  0 0          
1757 0           elsif ($arc_proc) {@postproc = ($arc_proc)}
1758 0           foreach my $proc (@postproc) {
1759 0 0         next unless $proc;
1760 0           my $rplc = {
1761             NAME => $archive,
1762             EXT => $arc_ext,
1763             FILE => sprintf("%s%s", $archive, $arc_ext),
1764             };
1765 0           my $cmd = dformat($proc, $rplc);
1766 0           $self->debug(sprintf("# %s", $cmd));
1767 0           my $errdata = "";
1768 0           my $outdata = execute( $cmd, undef, \$errdata, 1 );
1769 0           my $exe_stt = $? >> 8;
1770 0 0 0       $self->debug($outdata) if $self->verbosemode && defined($outdata) && length($outdata);
      0        
1771 0 0         $self->error($errdata) if $exe_stt;
1772             }
1773              
1774 0           return $count; # Number of objects
1775             }
1776             sub _extract {
1777 0     0     my $self = shift;
1778 0           my %args = @_;
1779 0   0       my $arcdef = $args{arcdef} || {};
1780 0   0       my $archive = $args{archive} || "";
1781 0   0       my $dirdst = $args{dirdst} || $self->rstdir;
1782              
1783             # Extract
1784             my $rplc = {
1785             FILE => $archive,
1786 0   0       EXT => $arcdef->{ext} || '',
1787             DIRDST => $dirdst,
1788             DIROUT => $dirdst,
1789             };
1790 0           my $cmd = dformat($arcdef->{extract}, $rplc);
1791 0           $self->debug(sprintf("# %s", $cmd));
1792 0           my $errdata = "";
1793 0           my $outdata = execute( $cmd, undef, \$errdata, 1 );
1794 0           my $exe_stt = $? >> 8;
1795 0 0 0       $self->debug($outdata) if $self->verbosemode && defined($outdata) && length($outdata);
      0        
1796 0 0         $self->error($errdata) if $exe_stt;
1797              
1798 0 0         return $exe_stt ? 0 : 1;
1799             }
1800              
1801             # Report internal methods
1802             sub _report_title {
1803 0     0     my $self = shift;
1804 0   0       my $title = shift || "report";
1805 0   0       my $name = shift || "virtual";
1806             return (
1807 0           sprintf("Dear %s user,", PROJECTNAME),"",
1808             sprintf("This is a automatic-generated %s for %s backup\non %s, created by %s/%s",
1809             $title, $name, $hostname, __PACKAGE__, $VERSION),"",
1810             "Sections of this report:","",
1811             " * Common information",
1812             " * Summary",
1813             " * List of occurred errors","",
1814             );
1815             }
1816             sub _report_common {
1817 0     0     my $self = shift;
1818 0           my @hdr = @_;
1819 0           my @rep = (
1820             "-"x32,
1821             "COMMON INFORMATION",
1822             "-"x32,"",
1823             );
1824 0           my $maxlen = 0;
1825 0           foreach my $r (@hdr) {
1826 0 0         $maxlen = length($r->[0]) if $maxlen < length($r->[0])
1827             }
1828 0           foreach my $r (@hdr) {
1829 0           push @rep, sprintf("%s %s: %s", $r->[0], " "x($maxlen-length($r->[0])), $r->[1]);
1830             }
1831 0           push @rep, "";
1832 0           return (@rep);
1833             }
1834             sub _report_summary {
1835 0     0     my $self = shift;
1836 0   0       my $summary = shift || "Ok";
1837 0           my @rep = (
1838             "-"x32,
1839             "SUMMARY",
1840             "-"x32,"",
1841             );
1842 0           push @rep, $summary, "";
1843 0           return (@rep);
1844             }
1845             sub _report_errors {
1846 0     0     my $self = shift;
1847 0           my @errs = @_;
1848 0           my @rep = (
1849             "-"x32,
1850             "LIST OF OCCURRED ERRORS",
1851             "-"x32,"",
1852             );
1853 0 0         if (@errs) {
1854 0           push @rep, @errs;
1855             } else {
1856 0           push @rep, "No errors occurred";
1857             }
1858 0           return (@rep, "");
1859             }
1860             sub _report_footer {
1861 0     0     my $self = shift;
1862 0           return sprintf(join("\n",
1863             "",
1864             "---",
1865             "Hostname : %s",
1866             "Program : %s (%s, Perl %s)",
1867             "Version : %s/%s",
1868             "Config file : %s",
1869             "PID : %d",
1870             "Work time : %s",
1871             "Generated : %s"
1872             ),
1873             $hostname, $0, $^O, $^V, PROJECTNAME, $VERSION, $self->configfile(),
1874             $$, $self->tms(), dtf("%w, %DD %MON %YYYY %hh:%mm:%ss"),
1875             );
1876             }
1877              
1878             # Private functions
1879             sub _getName { # Get normalized name og structure
1880 0     0     my $struct = hash(shift);
1881 0           my @ks = keys %$struct;
1882 0 0         return '' unless @ks;
1883 0 0         return 'VIRTUAL' if exists $ks[1];
1884 0 0 0       return ($ks[0] && ref($struct->{$ks[0]}) eq 'HASH') ? $ks[0] : 'VIRTUAL';
1885             }
1886             sub _node_correct { # Virtual nodes supports
1887 0     0     my $j = shift; # Node
1888 0   0       my $kk = shift || 'object';
1889              
1890 0           my @nc = ();
1891 0 0 0       if (ref($j) eq 'ARRAY') {
    0          
1892 0           my $i = 0;
1893 0           foreach my $r (@$j) {$i++;
  0            
1894 0 0 0       if ((ref($r) eq 'HASH') && exists $r->{$kk}) {
    0          
1895 0           push @nc, { sprintf("virtual_%03d",$i) => $r };
1896             } elsif (ref($r) eq 'HASH') {
1897 0           foreach my $k (keys %$r) {
1898 0           push @nc, { $k => $r->{$k} };
1899             }
1900             }
1901             }
1902             } elsif ((ref($j) eq 'HASH') && !exists $j->{$kk}) {
1903 0           foreach my $k (keys %$j) {
1904 0           push @nc, { $k => $j->{$k} };
1905             }
1906             } else {
1907 0 0         push @nc, { "virtual" => $j } if defined $j;
1908             }
1909 0           return [@nc];
1910             }
1911             sub _fbytes {
1912 0     0     my $n = int(shift);
1913 0 0         if ($n >= 1024 ** 3) {
    0          
    0          
1914 0           return sprintf "%.3g GB", $n / (1024 ** 3);
1915             } elsif ($n >= 1024 * 1024) {
1916 0           return sprintf "%.3g MB", $n / (1024.0 * 1024);
1917             } elsif ($n >= 1024) {
1918 0           return sprintf "%.3g KB", $n / 1024.0;
1919             } else {
1920 0           return "$n bytes";
1921             }
1922             }
1923              
1924             sub DESTROY {
1925 0     0     my $self = shift;
1926              
1927 0 0 0       rmtree($self->{objdir}) if $self->{objdir} && -e $self->{objdir};
1928 0 0 0       rmtree($self->{excdir}) if $self->{excdir} && -e $self->{excdir};
1929              
1930 0           1;
1931             }
1932              
1933             1;
1934              
1935             __END__