#!/usr/bin/perl -w
#
# Copyright (c) 2006 Zmanda Inc.  All Rights Reserved.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 2 as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
# for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
#
# Contact information: Zmanda Inc, 505 N Mathlida Ave, Suite 120
# Sunnyvale, CA 94085, USA, or: http://www.zmanda.com
#
#


use warnings;
use Getopt::Long;
use Time::Local;
use File::Copy;
use File::Temp qw/ :mktemp  /;
use Fcntl ':flock';
use lib "/usr/local/lib/mysql-zrm";
use ZRM::Common;


$oldPATH = $ENV{'PATH'};
$ENV{'PATH'} = "/usr/local/bin:/opt/csw/bin:/usr/bin:/usr/sbin:/sbin:/bin:/usr/ucb"; # force known path
my $prog="mysql-zrm-scheduler";
my $logdir="/var/log/mysql-zrm";
my $confdir="/etc/mysql-zrm";
my $def_dest="/var/lib/mysql-zrm";         # backup root
my $logfile="$logdir/$prog.log";
my $zrm="/usr/local/bin/mysql-zrm";
my $pre_scheduler="/usr/local/bin/zrm-pre-scheduler";
my $zrm_pre_backup="$pre_scheduler --action backup";
my $zrm_backup="$zrm --action backup";
my $zrm_purge="$zrm --action purge";
my $zrm_check="$zrm --action check";
my $directory;
my $LOG;
my $level_str="";
my $mode;
my $def_monthly_hour=0;  # default start time for monthly backup is midnight
my $def_weekly_hour=2;
my $def_daily_hour=3;
my $retention_line_seen=0;
my $mycrontab_fh;
my $mycrontab;
my $purge_time="0 4 * * *"; #start purging backup file at 4am daily
my $verbose="--noquiet";
my $retention_line="$zrm_purge";


my $MYSQL_ZRM_BASEDIR="/etc/mysql-zrm/";

my $MYSQL_ZRM_CONFIG_FILE=$MYSQL_ZRM_BASEDIR . "mysql-zrm.conf";


#usage strings
my $USAGE_SCHEDULER_STRING=
     "\t\t--add           To add a mysql-zrm schedule entry\n".
     "\t\t--query         To query existing mysql-zrm schedule entry\n".
     "\t\t--delete        To delete an mysql-zrm schedule entry\n".
     "\t\t  Use --start to delete entry with specific start time\n".
     "\t\t--interval <interval>   Default: weekly\n".
     "\t\t  valid value: daily, weekly or monthly\n".
     "\t\t--start-time <start-time>    e.g: 15:30 [start at 3:30pm]\n".
     "\t\t--day-of-week <day> e.g: 0 to start on Sunday, 1-3 to start on Mon, Tue and Wed.\n".
     "\t\t--day-of-month <day> e.g: 1 to start on first day of the month,\n".
     "\t\t	10-13 to start on the 10th, 11th, 12th and 13th day of the month.\n".
     "\t\t--backup-level <level> Full(0) or incremental(1)\n".
     "\t\t--backup-set <backupSet name> \n".
     "\t\t--now           To start mysql-zrm now\n".
     "\t\t--help\n";


my @SCHEDULEROPT= qw/html-report-directory=s
                     interval=s
                     start-time=s
                     day-of-week=s
                     day-of-month=s
                     backup-level=i
                     add!
                     delete!
 		     query!
 		     now!/;  #help defined in Common.pm.
                             #Common.pm parsed backup-set and export it as $backupset


sub mprint {
    my $d=localtime();
    print STDOUT @_;
    &lockLog();
    print LOG "$d: ";
    print LOG @_;
    &unlockLog();
}

sub log_and_die {
    my $d=localtime();
    &lockLog();
    print LOG "$d: ";
    print LOG  @_;
    &unlockLog();
    die @_;
}

# validate user input to interval
sub check_inp {
    local($inp) = @_;
    $inp =~ tr/A-Z/a-z/;
    my $found = 0;
    @valid_inps = ( "daily", "weekly", "monthly" );
    foreach $elt (@valid_inps) {
	if ($elt eq $inp) {
	    $found = 1;
	    last;
	}
    }
    unless ( $found ) {
	&usage("valid inputs to --intervals or --delete are <daily, weekly or monthly>\n");
    }
}

#check if user is allowed to run crontab command
#if not, exit gracefully.
sub check_cron_allow {
    system ("crontab -l &> /dev/null");
    if ( $? != 0 ) {   #suse crontab returns 1 for "no crontab found"
                       #should work on all locale, "no crontab found"
                       #message is not localized
        system ("crontab -l 2>&1 |grep 'no crontab for' > /dev/null");
        $exit_value  = $? >> 8;
        if ( $exit_value != 0 ) {
	    &mprint ("ERROR: crontab -l failed. \n");
	    &log_and_die ("Make sure you are allowed to run crontab on this machine\n");
	}
    }
}

sub check_options {
    system ("$zrm_check --backup-set $backupset $level_str &> /dev/null");
    if ( $? !=0 ) {
      unlink $mycrontab;
      &mprint ("ERROR: Mysql-zrm configuration check failed, check /var/log/mysql-zrm/mysql-zrm.log for detail.\n");
      &log_and_die ("ERROR: Please resolve the error and rerun mysql-zrm-scheduler.\n");
    }
}


sub call_reporter ()
  {
    my $name = $_[0];
    my $date=`date +%Y%m%d%H%M%S`;
    chomp($date);
    my $report_out="$name.$date.html";  # report name is $backupsetname.timestamp.html
    my $reporter="/usr/local/bin/mysql-zrm-reporter";
    # generate html output
    if ( $inputs{'html-report-directory'} && $inputs{'html-report-directory'} ne "" ) {
      system "$reporter", "--where", "backup-set=$name", "--latest", "--type", "html", "--output", "$report_out";
    }
  }


# main


$action = "schedule";
$USAGE_STRING = $USAGE_SCHEDULER_STRING;
&initCommon(@SCHEDULEROPT);


my $interval    =$inputs{"interval"};
my $start       =$inputs{"start-time"};
my $day_of_week =$inputs{"day-of-week"};
my $day_of_month=$inputs{"day-of-month"};
my $level       =$inputs{"backup-level"};
my $add         =$inputs{"add"};
my $delete      =$inputs{"delete"};
my $query       =$inputs{"query"};
my $now         =$inputs{"now"};


unless ( -d $logdir ) {
    mkdir ($logdir, 0750) ||
	die ("ERROR: mkdir $logdir failed: $!\n");
}

open (LOG, ">>$logfile") || &log_and_die ("ERROR: Cannot create/open logfile: $!\n");
print STDOUT "Logging to $logfile\n";


unless ( defined $delete || defined $add || defined $query || defined $now) {
    &usage("one of --add, --delete, --query or --now must be specified\n");
}


if ( defined $delete ) {
    unless ( defined $interval ) {
	&usage("--interval must be specified to use --delete\n");
    }
    $mode="del";
} elsif ( defined $add ) {
    $mode="add";
}


if ( defined $level ) {
    unless ( $level =~ /[0-1]/ ) {
	&usage ("backup-level must be either 0 or 1\n");
    }
    $level_str="--backup-level $level";
}

if ( defined $day_of_month ) {
	if ( ! defined $interval || $interval ne "monthly" ) {
	&usage ("ERROR: if day-of-month is specified, interval must be specified as monthly\n");
    }
}

if ( defined $interval && $interval eq "daily" ) {
  if ( defined $day_of_week || defined $day_of_month ) {
    &usage ("ERROR: if interval is daily, day-of-week or day-of-month is not allowed\n");
    }
  $day_of_week="*";   #default
  $day_of_month="*";
}


#now is specified, just start mysql-zrm
#Not doing anything with crontab.
if ( $now ) {
    system ("$zrm_backup $verbose $level_str --backup-set $backupset");
    $exit_value  = $? >> 8;
    if ( $exit_value !=0 ) {
	&mprint ("ERROR: $zrm did not finish successfully\n");
	&call_reporter($backupset);
	exit 1;
    } else {
	&mprint ("$zrm started successfully\n");
	&call_reporter($backupset);
	exit;
    }
}

&check_cron_allow();

if ( $query ) {
    system ("crontab -l | grep $pre_scheduler");
    $exit_value  = $? >> 8;
    if ( $exit_value == 2 ) { # grep returns 2 when errors seen
	&mprint ("ERROR: crontab -l failed.\n");
	&mprint ("Make sure you are allowed to run crontab on this machine\n");
	exit 1;
    } elsif ( $exit_value == 1 ) {
	&mprint ("No mysql-zrm related schedule entry found.\n");
    }
    exit;
}


if ( defined $interval ) {
    check_inp($interval);
}
elsif ( $mode eq "add" ) {  # interval not specified
    $interval="weekly";     # use default for add;
}

my $minute=0;
my $hour=0;
my $month="*";

if ( defined $start ) {
    ($hour, $minute) = split(":", $start);
    $hour   =~ s/0([0-9])/$1/;	# remove leading zero
    $minute =~ s/0([0-9])/$1/;
    if (!defined $hour || $hour eq "") {
	$hour=0;
    }
    if (! defined $minute || $minute eq "") {
	$minute=0;
    }

    unless ( $hour =~ /[0-9]/ &&  $minute =~ /[0-9]/ ) {
	&usage("start time must be in HH:MM format\n");
    }

    if ( $hour < 0 || $hour > 23 || $minute < 0 || $minute > 59) {
	&usage("invalid start time entered\n");
    }
} else {
    # if start-time is not specified, spread out the
    # starting time according to interval
    $hour=$def_monthly_hour if ( $interval eq "monthly" );
    $hour=$def_weekly_hour  if ( $interval eq "weekly" );
    $hour=$def_daily_hour   if ( $interval eq "daily" );
}

if ( $interval eq "weekly" ) {
  if ( defined $day_of_week ) {
    (my ($low_wday, $hyphen, $hi_wday) = $day_of_week =~ /^([0-7])(\-?)([0-7]?)$/) ||
      &log_and_die ("ERROR: Invalid day-of-week entered.\n");;
    #print "$low_wday, $hyphen, $hi_wday\n";
    if (  $low_wday < 0 || $low_wday > 7 ) {
      &usage ("ERROR: Invalid day-of-week entered.\n");
    }
    if (  $hyphen eq "-" &&  $hi_wday =~ /^[0-7]/ ) {
      if ( $low_wday >= $hi_wday || $low_wday > 7 ||$hi_wday > 7 ) {
	&usage ("ERROR: Invalid day-of-week entered.\n");
      }
      $day_of_week = $low_wday . $hyphen . $hi_wday;
    } else {
      $day_of_week=$low_wday;

    }
  } else {
    $day_of_week="0";   # start on Sunday is no day-of-week
  }
  $day_of_month="*";
}

if ( $interval eq "monthly" ) {
  if ( defined $day_of_month ) {
   (my ($low_mday, $hyphen2, $hi_mday) = $day_of_month =~ /^([0-9][0-9]?)(\-?)([0-9]{0,2})$/ ) ||
      &log_and_die ("ERROR: Invalid day-of-month entered.\n");
    #print "$low_mday, $hyphen2, $hi_mday\n";
    if ( $low_mday < 0 || $low_mday > 31 ) {
      &usage ("ERROR: Invalid day-of-month entered.\n");
    }
    if ( $hyphen2 eq "-" &&  $hi_mday =~ /^[0-9]+/ ) {
      if ( $low_mday >= $hi_mday || $hi_mday > 31 ) {
	&usage ("ERROR: Invalid day-of-month entered.\n");
      }
      $day_of_month = $low_mday . $hyphen2 . $hi_mday;
    } else {
      $day_of_month=$low_mday;

    }
  } else {
    $day_of_month="1";  # start on first day of the month if no day-of-month
  }
  $day_of_week="*";
}


($mycrontab_fh, $mycrontab) = mkstemp( "/tmp/zrmcrontabXXXXX" );

system ("crontab -l > $mycrontab 2> /dev/null");
$exit_value  = $? >> 8;
if ( $exit_value > 1 ) { # 0 is fine
                         # suse crontab returns 1 for "no crontab found"
    &mprint ("ERROR: crontab -l failed.\n");
    &mprint ("Make sure you are allowed to run crontab on this machine\n");
    unlink $mycrontab;
    exit 1;
}


# now either delete or add depending on mode
if ( $mode eq "add" ) {
    &check_options();
    open (INF, "$mycrontab") || &log_and_die ("ERROR: Cannot open $mycrontab : $!\n");
    open (OUTF, ">$mycrontab.tmp") || &log_and_die ("ERROR: Cannot create $mycrontab.tmp : $!\n");
	while ( <INF> ) {
	  if ( $retention_line_seen < 1 && /$retention_line/ ) {
	    $retention_line_seen++;
	  }

	    #don't repeat suse crontab's comment
	    unless ( /^# DO NOT EDIT.*/ || /^# \(\/tmp\/.*crontab.*/ || /^# \(Cron version .*/ ) {
		     print OUTF;
		 }
	}
    # add content to crontab file
    print OUTF "$minute $hour $day_of_month $month $day_of_week $zrm_pre_backup --backup-set $backupset $level_str --interval $interval\n";
    if ( $retention_line_seen == 0 ) {
      print OUTF "$purge_time $retention_line\n";
    }
    move ("$mycrontab.tmp", "$mycrontab") || &log_and_die ("ERROR: move failed: $!\n");
    close OUTF;
    close INF;
}


if ( $mode eq "del" ) {
    my $found=0;
    $month="\\*";
    $day_of_week =~ s#\*#\\\*#g;  # escape "*"
    $day_of_month =~ s#\*#\\\*#g;
    @ARGV="$mycrontab";
    $^I=""; #inplace editing
    #print "$minute $hour $day_of_month $month $day_of_week\n";
    while ( <> ) {
	if  ( /^$minute\s+$hour\s+$day_of_month\s+$month\s+$day_of_week.*$zrm_pre_backup/ && ! /--delay/){
	    $found++;
	}
	unless ( (/^$minute\s+$hour\s+$day_of_month\s+$month\s+$day_of_week.*$zrm_pre_backup/ && ! /--delay/ )
			|| /^# DO NOT EDIT.*/ || /^# \(\/tmp\/crontab.*/ || /^# \(Cron version --.*/ ) {
		 print;
	     }
    }
    if ( $found > 0 ) {
	&mprint ("$found crontab entry deleted\n");
    } else {
	unlink $mycrontab;
	&log_and_die ("No crontab entry matched\n");
    }
}


#now all is fine, install the new crontab file for this user

system ("crontab $mycrontab");
$exit_value  = $? >> 8;
if ( $exit_value !=0 ) {
    &mprint ("ERROR: crontab -l failed.\n");
    &mprint ("Make sure you are allowed to run crontab on this machine\n");
    unlink $mycrontab;
    exit;
}
else {
    unlink $mycrontab;
    $ENV{'PATH'} = $oldPATH;
    &mprint ("DONE\n");
}
