#!/usr/bin/perl
# 
# ***** BEGIN LICENSE BLOCK *****
# 
# Portions created by Zimbra are Copyright (C) 2005, 2006 Zimbra, Inc.
# All Rights Reserved.
# 
# The Original Code is: Zimbra Network
# 
# ***** END LICENSE BLOCK *****
# 

use strict;

use Getopt::Std;
# stop further processing when command args are invalid
$Getopt::Std::STANDARD_HELP_VERSION = 1;

# called by perl when command args are invalid
sub HELP_MESSAGE() {
    usage();
}

# suppress default version message by perl
sub VERSION_MESSAGE() {
}

my $id = `whoami`;
chomp $id;
if ($id ne "zimbra") {
	print STDERR "Error: must be run as zimbra user\n";
	exit (1);
}

my %opts;
my @schedules = ();

my @cron = ();
my $cronstart;
my $cronstop;

# default schedule: full backup 1am every sunday, incr backup 1am every weekday
# deletes backups older than a month at 12am on 1st of each month
my @default = ( "0 1 * * 6 /opt/zimbra/bin/zmbackup -f -a all\n", 
				"0 1 * * 0-5 /opt/zimbra/bin/zmbackup -i -a all\n",
				"0 0 * * * /opt/zimbra/bin/zmbackup -del 1m\n");

my %commandmap = ("f", "/opt/zimbra/bin/zmbackup -f -a all",
				  "i", "/opt/zimbra/bin/zmbackup -i -a all",
				  "d", "/opt/zimbra/bin/zmbackup -del");
				  
getopts("shqARFD", \%opts) or usage();

sub usage {

	my ($msg) = (@_);

	$msg && print STDERR "\nERROR: $msg\n";
	print STDERR <<USAGE;

Schedule regular backups

Usage: zmshedulebackup [-q|-s|-A|-R|-F|-D] [schedule] [schedule...]
	-q: query (default command) - prints existing schedule
	-s: save schedule (format applicable for restoring)
	-F: flush - remove current schedule (cancel all scheduled backups)
	-A: append - adds specified backup to current schedule
	-R: replace - replace current schedule with specified schedule
	-D: Default - replace current schedule with DEFAULT schedule

	schedule: <i|f|d arg> <time specifier>
	i: incremental backup
	f: full backup
	d <arg>: delete backups. <arg> is n{d|m|y}
	time specifier: crontab style time specifier, QUOTED.  See crontab(5)
		Fields are:
			minute         0-59
			hour           0-23
			day of month   1-31
			month          1-12 
			day of week    0-7 (0 or 7 is Sun, or use names)

	Default schedule is:
	f    0 1 * * 6 
	i    0 1 * * 0-5
	d 1m 0 0 * * *

USAGE
	exit (1);
}

sub showRemainingArgs {
	foreach (@ARGV) {
		print "ARG: $_\n";
	}
}

sub checkOpts {
	# Some options are exclusive, look for problems
	if (scalar(keys %opts) > 1) {usage("Invalid options");}
	if (scalar(keys %opts) == 0) {$opts{q} = 1;}
	return;
}

sub isValidSchedule {
	my @fields = split (' ',$_[0]);

	if (scalar(@fields) != 5) {
		return 0;
	} else {
		#
		# I'm looking for legal values - not sane values...
		#
		# minute         0-59
		# hour           0-23
		# day of month   1-31
		# month          1-12 
		# day of week    0-7 (0 or 7 is Sun, or use names)

		if ($fields[0] ne "*") {
			if ($fields[0] =~ m|(\d+)(/\d+)?|) { 
				if ($1 < 0 || $1 > 59) { return 0; } 
			} else {return 0;}
		}
		if ($fields[1] ne "*") {
			if ($fields[1] =~ m|(\d+)(/\d+)?|) { 
				if ($1 < 0 || $1 > 23) { return 0; } 
			} else {return 0;}
		}
		if ($fields[2] ne "*") {
			if ($fields[2] =~ m|(\d+)(/\d+)?|) { 
				if ($1 < 1 || $1 > 31) { return 0; } 
			} else {return 0;}
		}
		if ($fields[3] ne "*") {
			if ($fields[3] =~ m|(\d+)(/\d+)?|) { 
				if ($1 < 1 || $1 > 12) { return 0; } 
			} else {return 0;}
		}
		if ($fields[4] ne "*") {
			if ($fields[4] =~ m|(\d+)(/\d+)?|) { 
				if ($1 < 0 || $1 > 7) { return 0; } 
			} else {return 0;}
		}
		return 1;
	}
}

sub parseSchedules {
	my $type = "";
	my $darg = "";
	for (my $i = 0; $i <= $#ARGV; $i++) {
		if ($type eq "") {
			if ($ARGV[$i] eq "i" || $ARGV[$i] eq "f" || $ARGV[$i] eq "d") {
				$type = $ARGV[$i];
			} else {
				usage ("Invalid schedule: @ARGV");
			}
		} elsif ($type eq "d" && $darg eq "") {
		    $darg = $ARGV[$i];
		    if ($darg !~ /^\d+(d|m|y)$/) {
		        usage ("Invalid arg in deletion schedule: $darg");
		    }
		} elsif (isValidSchedule($ARGV[$i])) {
			push (@schedules, "$ARGV[$i] $commandmap{$type} $darg\n");
			$type = "";
			$darg = "";
		} else {
			usage ("Invalid schedule: @ARGV");
		}
		#print "ARG: $ARGV[$i]\n";
	}
	if ($type ne "") {usage ("Invalid schedule: @ARGV");}
}

sub loadCron {
	open CRON, "crontab -l |";
	@cron = <CRON>;
	close CRON;

	$cronstart = -1;
	$cronstop = -1;

	my $comments_good = 0;

	my $found = 0;
	for (my $i = 0; $i <= $#cron; $i++) {
		$_ = $cron[$i];
		if (m/BACKUP END/) {
			if ($found) {
				$comments_good = 1;
			}
			last;
		}

		if ($found) {
			$cronstop = $i;
			next;
		}
		if (m/BACKUP BEGIN/) {
			$found = 1;
			$cronstart = $i;
			$cronstop = $i;
			next;
		}
	}

	if (!$comments_good) {
		print STDERR "Rebuilding backup cron\n\n";
		# One or both backup comments not found.  
		# Clean up the array, and add them to the end
		if ($cronstart == -1 && $cronstop == -1) {
			# No comments at all
			push @cron, "# BACKUP BEGIN\n";
			$cronstart = $#cron;
			$cronstop = $#cron;
			push @cron, "# BACKUP END\n";
		} else {
			# It's not possible to find an end and no start.
			# No end comment - add the end comment right after the start comment.
			splice (@cron, $cronstart+1, 0, "# BACKUP END\n");
		}
	}
}

sub saveCron {
	open CRON, ">/tmp/cron.$$";
	foreach (@cron) {
		# print;
		print CRON;
	}
	close CRON;
	`crontab /tmp/cron.$$`;
	unlink "/tmp/cron.$$";
}

sub saveCurrentSchedule {
	
	for (my $i = $cronstart+1; $i <= $cronstop; $i++) {
		$_ = $cron[$i];
		my @fields = split;
		if (/-f/) {
			print " f \"$fields[0] $fields[1] $fields[2] $fields[3] $fields[4]\"";
		} elsif (/-i/) {
			print " i \"$fields[0] $fields[1] $fields[2] $fields[3] $fields[4]\"";
		} elsif (/-del (\d+[dmy])/) {
		    print " d $1 \"$fields[0] $fields[1] $fields[2] $fields[3] $fields[4]\"";
		}
	}
	print "\n";
	exit (0);
}

sub displayCurrentSchedule {
	print "Current Schedule:\n\n";
	
	for (my $i = $cronstart+1; $i <= $cronstop; $i++) {
		$_ = $cron[$i];
		my @fields = split;
		if (/-f/) {
			print "\tf $fields[0] $fields[1] $fields[2] $fields[3] $fields[4]\n";
		} elsif (/-i/) {
			print "\ti $fields[0] $fields[1] $fields[2] $fields[3] $fields[4]\n";
		} elsif (/-del (\d+[dmy])/) {
		    print "\td $1 $fields[0] $fields[1] $fields[2] $fields[3] $fields[4]\n";
		}
	}
	exit (0);
}

sub flushCron {
	splice (@cron, $cronstart+1, $cronstop-$cronstart);
	saveCron();
	print "Schedule flushed\n\n";
	exit (0);
}

sub replaceCron {
	print "Schedule replaced\n\n";
	splice (@cron, $cronstart+1, $cronstop-$cronstart, @schedules);
	saveCron();
	loadCron();
	displayCurrentSchedule();
	exit (0);
}

sub defaultCron {
	print "Default schedule set\n\n";
	splice (@cron, $cronstart+1, $cronstop-$cronstart, @default);
	saveCron();
	loadCron();
	displayCurrentSchedule();
	exit (0);
}

sub appendCron {
	print "Schedule modified\n\n";
	splice (@cron, $cronstop+1, 0, @schedules);
	saveCron();
	loadCron();
	displayCurrentSchedule();
	exit (0);
}

$opts{h} && usage();

loadCron();

checkOpts();

parseSchedules();

($opts{q} || $opts{F} || $opts{D}) && scalar(@schedules) && usage("No schedule specification allowed");

($opts{A} || $opts{R}) && !scalar(@schedules) && usage("Schedule specification required");

$opts{q} && displayCurrentSchedule();

$opts{s} && saveCurrentSchedule();

$opts{D} && defaultCron();

$opts{F} && flushCron();

$opts{R} && replaceCron();

$opts{A} && appendCron();


# saveCron();

#showRemainingArgs();
