#!/usr/bin/perl -w
use strict;


# --backup=task-id id-of-backup-task
use Getopt::Long;
use POSIX qw(tmpnam);
use DBI;
use POSIX ":sys_wait_h";

my $atis_cmd = '/usr/sbin/trueimagecmd';

my $output_text = '';
my $is_error = 0;

sub shell_quote
{
    my ($qstr) = @_;
    # replace ' for \'
    # replace \ for \\
    $qstr =~ s/\\/\\\\/g;
    $qstr =~ s/"/\\\"/g;
    $qstr =~ s/\$/\\\$/g;
    return $qstr;
}

sub exec_cmd
{
    my ($program, $args) = @_;
    my $err_out_file = tmpnam();
    my ($out, $err_out) = ('', '');
    my $TI;
    
    unless (open($TI, "${program} ${args} 2>${err_out_file} |")){
        return 0;
    }
    while (<$TI>) {
        $out .= $_;
    }
    close ($TI);
    if (open ERR, $err_out_file) {
        while (<ERR>) {
            $err_out .= $_;
        }
        close ERR;
    }   
    unlink $err_out_file;
    return ($?, $out, $err_out);
}

sub print_out 
{
    my ($msg) = @_;
    # print STDERR "trueimagecmd execution failed:\n$msg\n";
    $output_text .= "${msg}";
}

sub flush_out
{
    my ($stream) = @_;
    unless (defined $stream) {
        print STDERR $output_text
    } else {
        print STDOUT $output_text;
    }
}

sub get_partitions
{
    # get list of mounts
    my %mounted;
    
    my ($ret_val, $out, $err_out) = exec_cmd('mount', '');
    if ($ret_val) {
        print_out "`mount` execution failed: $err_out";
        return;
    }

    my @lines = split(/\n/, $out);
    foreach (@lines) {
        if (/^\/dev\/(.+)?\s+on\s+(.+)?\s+type\s+[a-z0-9A-Z]+\s\([^)]+\)$/) {
#           print "$1 on $2\n";
            $mounted{$1} = $2;
        }
    }
    # get disks partitions
    ($ret_val, $out, $err_out) = exec_cmd($atis_cmd, '--list');
    if ($ret_val) {
        print_out "trueimagecmd execution failed: $err_out";
        return;
    }

    # analyze returned value
    @lines = split(/\n/, $out);
    my $num_lines = @lines;
    unless ($lines[0] =~ /^Num\s+Partition\s+Flags\s+Start\s+Size\s+Type\s+$/) {
        print_out "invalid output format of `trueimagecmd`";
        return;
    }
    my $row_regexp = '';
    unless ($lines[1] =~ /^(-+) (-+) (-+) (-+) (-+) (-+)$/) {
        print_out "invalid output format of `trueimagecmd`";
        return;
    } else {
        $row_regexp = '^(.{' . length($1) . '}) (.{' . length($2) . '}) (.{' . 
            length($3) . '}) (.{' . length($4) . '}) (.{' . length($5) . '}) (.{' . length($6) . '})';
    }

    my %ret_lines;
    for (my $i=3; $i<$num_lines; $i++) {
        if ($lines[$i] =~ m/$row_regexp/) {
            # remove spaces from the end
            my ($num, $partition, $flags, $start, $size, $type) = ($1, $2, $3, $4, $5, $6);
            foreach ($num, $partition, $flags, $start, $size, $type) {
                $_ =~ s/\s*$//g;
            }
            unless ($num =~ m/^[0-9]+-[0-9]+$/) {
                next;
            }
            my ($partition_dev, $partition_label) = split / /, $partition;
            unless (defined($partition_label)) {
                $partition_label = '';
            }
            $partition_label =~ s/^\(//;
            $partition_label =~ s/\)$//;
            my $p = '';
            if (exists($mounted{$partition_dev})) {
                $p = $mounted{$partition_dev};
            } else {
                #
                my ($k, $v);
                while (($k, $v) = each(%mounted)) {
                    if ($k =~ /\/$partition_dev$/) {
                        $p = $v;
                        last;
                    }
                }
            }
            my @cols = ($num, $partition_dev, $p, $partition_label, $start, $size, $type);
            $ret_lines{$num} = \@cols;
        }
    }
    return \%ret_lines;
}

my $help = << "EOT";
Usage:
    $0 [options]
Options:
    -h, --help              display this help message
    --backup-now            perform backup now
    --check-package         check ATIS package      
    --exec-task=<id>        execute task with id = <id>
    --get-status            get list of running tasks
    --list-parts            show all partitions
    --launch-backup-now     launch backup-now task
EOT

my ($opt_help, $opt_list_parts, $opt_check_package, $exec_task_id, $backup_now, $launch_backup_now, $get_status, $terminate_task);
my %opts_all = (
    'h'                 => \$opt_help,
    'help'              => \$opt_help,
    'list-parts'        => \$opt_list_parts,
    'check-package'     => \$opt_check_package,
    'backup-now'        => \$backup_now,
    'exec-task=n'       => \$exec_task_id,
#   'terminate-task=n'  => \$terminate_task,
    'get-status'        => \$get_status,
    'launch-backup-now' => \$launch_backup_now
);

unless(GetOptions(%opts_all)){
    print "$help\n";
    exit 0;
}

if ($opt_help) {
    print "$help\n";
    exit 0;
}

# get psa admin password
my $P;
unless (open $P, '/etc/psa/.psa.shadow') {
    $is_error = 2;
    print_out "Unable to open file `/etc/psa/.psa.shadow`: " . $! . "\n";
}
my $psa_admin_user = 'admin';
my $psa_admin_pass = '';
unless ($is_error) {
    unless (defined($psa_admin_pass=<$P>)) {
        print_out "Unable to get password from the file `/etc/psa/.psa.shadow`\n";
        $is_error = 2;
        close $P;
    } else {
        chomp $psa_admin_pass;
        close $P;
    }
}


# parse plesk config file
my %psa_params;
my $PSACONF;
unless (open $PSACONF, '/etc/psa/psa.conf') {
    
}
while (<$PSACONF>){
    chomp;
    unless (/^#/){
        if (/^(\s*[_a-zA-Z]+)\s+(.+?)\s*$/){
            # print "$1 : $2\n";
            $psa_params{$1} = $2;
        }
    }
}
close $PSACONF;

# getting time string
my $str_time = POSIX::strftime( "%A, %B %d, %Y %H:%M:%S GMT", gmtime());
my $str_stddatetime  = POSIX::strftime( "%Y-%m-%d %H:%M:%S GMT", gmtime());

my ($ret_val, $out, $err_out);

if ($opt_list_parts && !$is_error) {
    my $r = get_partitions();
    unless (defined $r) {
        flush_out(1);
        exit 1;
    }
    foreach my $k (keys %$r) {
        my $g = $r->{$k};
        print join '|', @$g;
        print "\n";
    }
    exit 0;
}

if ($opt_check_package && !$is_error) {
    ($ret_val, $out, $err_out) = exec_cmd($atis_cmd, '--list');
    if ($ret_val) {
        # error occured - package not installed, serial number don't match etc
        print "EXEC_PROBLEM\n";
        exit 1;
    } else {
        exit 0;
    }
}

my $DIR;
if ($get_status && !$is_error) {
    my @running_tasks;
    
    opendir ($DIR, '/proc');
    my $file;
    my $prog_cnt = 0;
    while (defined($file=readdir($DIR))) {
        if ($file =~ /^[0-9]+$/) {
            # this is process
            my $proc_cmd_file = '/proc/' . $file . '/cmdline';
            my $cmd = '';
            my $F;
            if (open $F, $proc_cmd_file) {
                if (defined($cmd=<$F>)) {
                    chomp $cmd;
                    $cmd =~ s/\x00/ /g;
                    #print "$file : $cmd\n\n";
                }
                close $F;
            }
            unless (defined $cmd) {
                $cmd = '';
            }
            my %h;
            $h{'pid'} = $file;
            
            if ($cmd =~ /atismodbackupmng\s+--exec-task(=|\s+)([0-9]+)/) {
                $h{'id'} = $2;
                push @running_tasks, \%h;
            } elsif ($cmd =~ /atismodbackupmng\s+--backup-now/) {
                $h{'id'} = 0;
                push @running_tasks, \%h;
            }
        }
    }
    closedir ($DIR);

    foreach my $task (@running_tasks) {
        # print $task->{'pid'} . '|' . $task->{'id'};
        my $info_filename = $psa_params{'PRODUCT_ROOT_D'} . '/var/modules/atism/run/' . $task->{'pid'};
        my $INFO_FILE;
        if (open $INFO_FILE, $info_filename) {
            print '--pid:' .  $task->{'pid'} . "\n";
            while (<$INFO_FILE>) {
                print;
            }
            print "\n";
            close $INFO_FILE;
        }

    }
    exit 0;
}

if ($terminate_task) {
    my $fname = "/proc/${terminate_task}/cmdline";
    my $FH;
    unless (open($FH, $fname)) {
        print "couldn't find process with pid=${terminate_task}\n";
        exit 1;
    }
    my $line = <$FH>;
    my $info_filename = $psa_params{'PRODUCT_ROOT_D'} . '/var/modules/atism/run/' . $terminate_task;
    $line =~ s/\x00/ /g;
    if (-e $info_filename) {
        # try to kill process with timeout = 3 sec
        my $pid = fork();
        unless (defined $pid) {
            print "unable to fork()\n";
            exit 1;
        }
        if (0 == $pid) {
            # child
            close STDIN;
            close STDOUT;
            close STDERR;

            my $cmd = "kill -INT $terminate_task";
            system($cmd);
            exit 0;
        } else {
            my $seconds = 0;
            my $kid = 0;

            print "starting kill\n";
            while ($kid <= 0) {
                sleep(1);
                $seconds++;
                $kid = waitpid($pid, WNOHANG);
                if ($seconds > 2) {
                    print "TIMEOUT\n";
                    exit 1;
                }
            }
            # successfully killed
            exit 0;
        }
    }
    close $FH;
}

my ($dbh, $sth);
my $query;
unless ($is_error) {
    # all routines which use mysql must be defined below this line
    my $dsn = 'DBI:mysql:psa:localhost';
    my %dbh_attrs = (
        AutoCommit => 1,
        RaiseError => 0,
        PrintError => 0,

    );
    unless ($dbh = DBI->connect($dsn, $psa_admin_user, $psa_admin_pass, \%dbh_attrs)) {
        print_out "unable to connect to the database: " . $DBI::errstr . "\n";
        $is_error = 2;
    }
}


if ($launch_backup_now) {
    my $in = '';
    while (<STDIN>) {
        $in .= $_;
    }

    open $P, "|$0 --backup-now";
    print $P $in;
    close $P;
    exit 0;
}
my ($atis_period_daily, $atis_period_weekly, $atis_period_monthly) = (0, 1, 2);


if ((defined ($exec_task_id) || $backup_now)  && !$is_error) {
    
    # 
    # try to find running processes
    #

    opendir ($DIR, '/proc');
    my $file;
    my $prog_cnt = 0;
    while (defined($file=readdir($DIR))) {
        if ($file =~ /^[0-9]+$/) {
            # this is process
            my $proc_cmd_file = '/proc/' . $file . '/cmdline';
            my $cmd = '';
            my $F;
            if (open $F, $proc_cmd_file) {
                if (defined($cmd=<$F>)) {
                    chomp $cmd;
                    $cmd =~ s/\x00/ /g;
                    #print "$file : $cmd\n\n";
                }
                close $F;
            }
            unless (defined $cmd) {
                $cmd = '';
            }
            if ($cmd =~ /atismodbackupmng\s+--exec-task/ || $cmd =~ /atismodbackupmng\s+--backup-now/) {
                $prog_cnt++;
            }
        }
    }
    if ($prog_cnt > 1) {
        print_out "Error: another atismodbackupmng process is running!\n";
        $is_error = 3;
    }

    my $row;
    if (defined($exec_task_id)) {
        print_out "TrueImage backup task #${exec_task_id}\n\n";
        
        $query = "SELECT * FROM module_atis_mod_backup_schedule WHERE id='$exec_task_id'";
        unless ($sth = $dbh->prepare($query)) {
            print_out "Error: unable to prepare statement for getting task: " . $dbh->errstr . "\n";
            $is_error = 1;
        }
        if (0==$is_error || 3==$is_error) {
            unless ($sth->execute()) {
                print_out  "unable to execute query: " . $sth->errstr . "\n";
                $is_error = 1;
                $sth->finish();
            }
        }

        if  (0==$is_error || 3==$is_error) {
            unless ($row=$sth->fetchrow_hashref()) {
                print_out "unable to find task with id=`$exec_task_id` \n";
                $is_error = 1;
            }
            $sth->finish();
        }   
    } elsif ($backup_now) {
        print_out("TrueImage backup\n\n");
        
        # read STDIN
        my @backup_params = qw( backup_parts storage_path filename report_email compress_level );
        my @backup_params_opt = qw( incremental archive_passwd );
        my %stdin_params;

        foreach (@backup_params, @backup_params_opt) {
            $stdin_params{$_} = '';
        }

        while (<STDIN>) {
            #print STDERR $_;
            if (/^([a-z_]+)\s*=\s*(.+)\s*$/) {
                $stdin_params{$1} = $2;
                # print STDERR "$1 : $2 \n";
            }
        }
    

        foreach (@backup_params) {
            if ($stdin_params{$_} eq '') {
                print_out "parameter `$_` is not defined\n";
                $is_error = 1;
            }
        }
        $row = \%stdin_params;
        
        my $pid = fork();
        unless (defined $pid) {
            print_out "unable to fork()\n";
            $is_error = 1;
        } else {
            if ($pid) {
                exit 0;
            }
            close STDIN;
            close STDOUT;
            close STDERR;
        }
    }

    # set SIGINT handler
    #local $SIG{INT} = sub { $SIG{INT} = 'IGNORE';};

    

    my ($backup_partitions, $backup_compress_level, $backup_incremental, $backup_storage_path, $backup_report_email, $backup_filename, $backup_password);
    my @a_backup_partitions;
    my $s_backup_partitions;
    
    my $INFO_FILE;
    my $info_filename = $psa_params{'PRODUCT_ROOT_D'} . '/var/modules/atism/run/' . $$;

    if ($is_error==0 || $is_error==3) {
        ## create information file
        unless (open ($INFO_FILE, ">$info_filename")) {
            $is_error = 1;
            print_out "unable to open information file `$info_filename` for writing:  $! \n";
        }
    }
    if ($backup_now) {
        print $INFO_FILE "backup_type: instant\n";
    } else {
        print $INFO_FILE "backup_type: scheduled\n";
    }
    if ($is_error==0 || $is_error==3) {
            
        
        $backup_partitions = $row->{'backup_parts'};
        $backup_compress_level = $row->{'compress_level'};
        $backup_incremental = $row->{'incremental'};
        $backup_storage_path = $row->{'storage_path'};
        $backup_report_email = $row->{'report_email'};
        $backup_filename = $row->{'filename'};
        $backup_password = $row->{'archive_passwd'};
        
        @a_backup_partitions = split /,/, $backup_partitions;
        ##unless ($is_error) {
            unless (-d $backup_storage_path) {
				unless ($backup_storage_path =~ m/^(smb|nfs):\/\/[^\/]+\/[^\/]+\/?$/) {
					print_out "invalid storage path: path `$backup_storage_path` not found\n";
					$is_error = 1;
				}
            }
            unless ($backup_compress_level =~ /^[0-9]+$/ && $backup_compress_level >= 0 && $backup_compress_level <= 9) {
                print_out "invalid compress_level value: `${backup_compress_level}`\n";
                $is_error = 1;
            }
            
            if (@a_backup_partitions != 0) {
                foreach (@a_backup_partitions) {
                    unless (/^[0-9]+-[0-9]+$/) {
                        print_out "invalid partition code - `$_`\n";
                        $is_error = 1;
                    }
                }
                if ($is_error==0 || $is_error==3) {
                    my $partitions = get_partitions();
                    unless (defined $partitions) {
                        print_out "unable to get partitions list\n";
                        $is_error = 1;
                    } else {
                        my @parts_names;
                        foreach (@a_backup_partitions) {
                            push @parts_names, $partitions->{$_}[1];
                        }

                        $s_backup_partitions = join(',', @parts_names);
                        print_out "Backup partitions: " . $s_backup_partitions . "\n";
                        print_out "Backup path: " . $backup_storage_path . "\n";
                        print_out "Backup file name: " . $backup_filename . ".tib\n";
                        print_out "Backup start time: ${str_time}\n"; 

                        print $INFO_FILE "partitions: $s_backup_partitions\n";
                        print $INFO_FILE "backup_path: $backup_storage_path\n";
                        print $INFO_FILE "backup_file: $backup_filename\n";
                        print $INFO_FILE "backup_start_time: ${str_stddatetime}\n";
                    }
                }
            } else {
                print_out "unable to get partitions list\n";
                $is_error = 1;
            }
        ##}
    }
    
    
    unless ($is_error) {
        my $cmd_args = "--create --partition:${backup_partitions} --compression:${backup_compress_level} --filename:${backup_storage_path}/${backup_filename}.tib";
        print_out "Password protection: ";
        if ($backup_password ne '') {
            $cmd_args .= ' --password:"' . shell_quote(${backup_password}) . '"';
            print_out "yes\n";
        } else {
            print_out "no\n";
        }
        
        print_out "Incremental backup: ";
        if (defined $backup_incremental && $backup_incremental ne '') {
            $cmd_args .= " --incremental";
            print_out "yes\n";
            print $INFO_FILE "incremental: yes\n";
        } else {
            print_out "no\n";
        }
#       print ">> $cmd_args <<\n";

        ($ret_val, $out, $err_out) = exec_cmd $atis_cmd, $cmd_args;
        unless ($ret_val) {
            # all ok
            print_out "Status: success\n";
        } else {
            print_out "Status: failed\n\nProgram error output:\n $out";
        }

    }
   

    if ($is_error || (defined $backup_report_email && defined $s_backup_partitions)) {
        # send mail to $backup_report_email
        my $sendmail = $psa_params{'QMAIL_ROOT_D'} . '/bin/sendmail -t';
        my $hostname = `hostname`;
        chomp $hostname;
        
        my $MAIL;
        unless (open $MAIL, "|$sendmail") {
            print STDERR 'unable to send mail: ' . $!;
        } else {
            print $MAIL "From: root\@${hostname}\n";
            print $MAIL "To: $backup_report_email\n";
            print $MAIL "Subject: backup partition(s) ${s_backup_partitions} at ${str_time}\n\n";
            print $MAIL $output_text;
            close $MAIL;
        }
    }
    close ($INFO_FILE);
    unlink($info_filename);
    flush_out 1;
}



if (defined $dbh) {
    $dbh->disconnect();
}

if ($is_error) {
    if (2 == $is_error) {
        flush_out 1;
    }
    exit 1;
}
