#!/usr/bin/perl


# http://lithium.opennet.ru/
# Copyright (c) 2007 by Maxim Alhimenko (articles <at> lithium.opennet.ru).
#
# Программа для копирования бэкапов, создаваемых vzdump, на удаленный сервер 
# с использованием ssh и ротации архивов.


use strict;
use warnings;

############ TODO ########################
#
##########################################


my $remote_user = "loki";
my $remote_host = "gw.comtel-60.ru";
my $max_delta_files = 10;
my $save_old_backup = 1;

my $prog_ssh = "ssh";
my $prog_scp = "scp -q";
my $prog_vzlist = "vzlist";

#локальный каталог, в который делает дамны vzdump
my $local_dumpdir = "/vz/dump";
#каталог на удаленном сервере, куда переносятся бэкапы
my $remote_dumpdir = "/var/backup/loki/VPS";

#опциональные параметры для vzdump (задел на будущее для расширяемости)
#параметры --compress, --stop, --suspend, -- snapshot, --dumpdir, --xdelta и номер(а) VID указывааются не тут!!!!!!!!!!!!!!!!!
my $vzdump_opts = "";

#сжимать ли бэкапы, задает для vzdump флаг --compress
my $compress = 1;	# 0/1

#вариант остановки VPS прогаммой vzdump (см. документацию vzdump)
my $pause = 2;
#	1 -- "--stop"		stop/start VPS if running
#	2 -- "--suspend"	suspend/resume VPS when running
#	3 -- "--snapshot"	use LVM snapshot when running

#установить == 1 для включения отладки.
my $DEBUG = 0;

#подавлять болтовню других пограмм
my $quiet = 1;

#здесь можно изменить временный каталог для утилит вроде tar, если в вашем штатном временном каталоге не хватает места.
#$ENV{"TMPDIR"} = "/vz/dump/tmp";



#######################################################
# Значения нижележащих переменных править не надо!!!  #
#######################################################

#переменная с уникальным значением для этого процесса, чтобы давать уникальные имена файлам, которые будут затираться (нештатные ситуации).
my $uniqe = $$ . "." . time();

#различные служебные переменные.
my $vid;
my $command;
my @vps_list;
my $vids;
my $param;
my $have_vid;
my $have_OLD;
my %vids_dirs;
#это имя файла полного бэкапа, зависит от выбра использовать компрессию или нет
#также используется в именах разностных бэкапов, чтобы не запутаться при смене режима компресии
my $basename;


print "Разбор аргументов...\n" if ($DEBUG == 1);
if ((scalar (@ARGV)) > 0){
    foreach $param (@ARGV){
        if ($param eq "--all"){
            $vids = "all";
        }elsif ($param =~ /\d+/){
            $vids = $param;
        }elsif (($param eq "-h")  or ($param eq "--help") ){
            &usage();
	}elsif ($param eq "--compress"){
	    $compress = 1;
	}elsif ($param eq "--stop"){
	    $pause = 1;
	}elsif ($param eq "--suspend"){
	    $pause = 2;
	}elsif ($param eq "--snapshot"){
	    $pause = 3;
        }else{
            die "Cannot parse argument \"$param\", run \"$0 -h\" for help\n";
        }

    }
}



print "Создание параметров для vzdump...\n" if ($DEBUG == 1);
$vzdump_opts = $vzdump_opts . " --compress" if $compress == 1;
if ($pause == 1) {
    $vzdump_opts = $vzdump_opts . " --stop";
}elsif ($pause == 2){
    $vzdump_opts = $vzdump_opts . " --suspend";
}elsif ($pause == 3){
    $vzdump_opts = $vzdump_opts . " --snapshot";
}else{
    die "Abnormal value: \$pause = $pause";
}
print "Параметры vzdump получились такие: \'$vzdump_opts\'\n" if ($DEBUG == 1);



if ( ! defined ($vids) or ($vids eq "all")){
    print "Будем делать бэкап всех VPS, формируем список, запуск vzlist...\n" if ($DEBUG == 1);
    open VZLIST, "$prog_vzlist |" or die "Cannot run $prog_vzlist: $!";
    while (<VZLIST>){
	if ($_ =~ /^\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\S+)\s*$/){
        #               101     28      running 81.26.137.10    www2.comtel-60.ru
            my $id = $1;
	    push (@vps_list, $1);
        }
    }
    close VZLIST or die "Cannot close FD to $prog_vzlist process: $!";
}else{
    print "Бэкап для VID = $vids...\n" if ($DEBUG == 1);
    push (@vps_list, $vids);
}



$command = "$prog_ssh -l $remote_user $remote_host 'ls $remote_dumpdir' |";
print "Открываем удаленный каталог для создания хеша с именами каталогов:\n $command\n" if ($DEBUG == 1);
open (REMOTE_DIR, $command) or die "Cannot run \'$command\': $!";
while (<REMOTE_DIR>){
    chomp;
    $vids_dirs{$_} = 1;
}
close (REMOTE_DIR) or die "Cannot close fd for command \'$command\': $!";


foreach $vid (@vps_list) {
    if ($compress == 1){
	$basename = "vzdump-$vid.tgz";
    }else{
	$basename = "vzdump-$vid.tar";
    }

    #если в удаленном хранилище нет каталога для текущего VPS создаем его
    if (! exists ($vids_dirs{$vid})){
	warn "There is no dir for VPS $vid in the remote storage, creating it and full backup.\n";
	print "Каталога для бэкапа VPS нет, создаем...\n" if ($DEBUG == 1);
	$command = "$prog_ssh -l $remote_user $remote_host 'mkdir --mode=700 $remote_dumpdir/$vid'";
	system ($command) == 0 or die "Cannot run \'$command\': $!";
	
	#создаем полный бэкап
	make_full_backup ($vid);
	next;
    }
    
    #нужно составить список содержимого каталога с бэкапом нашего VPS,
    #чтобы не дергать ssh каждый раз
    my %vid_dir_list;
    $command = "$prog_ssh -l $remote_user $remote_host 'ls $remote_dumpdir/$vid' |";
    print "Открываем удаленный каталог для текущего VPS:\n $command\n" if ($DEBUG == 1);
    
    open (REMOTE_DIR, $command) or die "Cannot run \'$command\': $!";

    print "Просматриваем содержимое и ищем файл полного дампа...\n" if ($DEBUG == 1);
    while (<REMOTE_DIR>){
	chomp;
	$vid_dir_list{$_} = 1;
    }
    close (REMOTE_DIR) or die "Cannot close fd for command \'$command\': $!";

    #если не нашли
    if (!exists ($vid_dir_list{$basename})){
	warn "The full backup file for VPS $vid is not found, we create it.\n";
	make_full_backup ($vid);
	next;
    }
    
    print "Полный дамп на сервере найден, считаем количество файлов xelta...\n" if ($DEBUG == 1);
    my $xdeltas_count = 0;
    foreach (keys (%vid_dir_list)){
	$xdeltas_count++ if ($_ =~ /^$basename\.xdelta.*/);
	
    }

    #смотрим количество файлов xdelta
    if ($xdeltas_count < $max_delta_files){
	print "Число xdelta-файлов меньше $max_delta_files, создаем еще один xdelta-файл...\n" if ($DEBUG == 1);
		
	&make_xdelta($vid);
	next;
    }else{
	print "Число xdelta-файлов НЕ меньше $max_delta_files, делаем полную ротацию...\n" if ($DEBUG == 1);
	#если установлен признак копирования старого бэкапа при создании нового
	if ($save_old_backup == 1) {
	    print "Установлен признак копирования старого бэкапа\n" if ($DEBUG == 1);
	    

	    if (exists ($vid_dir_list{"OLD"})){
		print "Переименовываем старый OLD перед удалением...\n" if ($DEBUG == 1);
	    	#переименовываем старый OLD (на всякий случай)
		$command = "$prog_ssh -l $remote_user $remote_host 'mv -f $remote_dumpdir/$vid/OLD $remote_dumpdir/$vid/OLD.TEMP.$uniqe'";
	        system ($command) == 0 or die "Cannot run \'$command\': $!";
	    }
	    #перемещаем все содежимое каталога на удаленном сервере в каталог OLD
	    print "Перемещаем весь текущий бэкап на удаленном сервере в каталог OLD...\n" if ($DEBUG == 1);
	    $command = "$prog_ssh -l $remote_user $remote_host 'mkdir $remote_dumpdir/$vid/OLD && mv -f $remote_dumpdir/$vid/vzdump-$vid* $remote_dumpdir/$vid/OLD/'";
	    system ($command) == 0 or die "Cannot run \'$command\': $!";
	}
	# удаляем локальный полный бэкап
	print "Удаляем локальный full бэкап...\n" if ($DEBUG == 1);
	(unlink "$local_dumpdir/$basename" or die "Cannot unlink $local_dumpdir/$basename: $!") if (-e "$local_dumpdir/$basename");
	
	# создаем полный дамп
	make_full_backup ($vid);

        #удаляем старый OLD (и только тогда,  когда все остальное удачно завершилось)
        if (exists ($vid_dir_list{"OLD"}) and ($save_old_backup == 1)){
	    print "Удаляем старый OLD...\n" if ($DEBUG == 1);
	    $command = "$prog_ssh -l $remote_user $remote_host 'rm -rf $remote_dumpdir/$vid/OLD.TEMP.$uniqe'";
	    system ($command) == 0 or die "Cannot run \'$command\': $!";
	}
	next;
    }
}
exit 0;



sub make_full_backup{
    #процедура, делающая полный бэкап и переносящая его на удаленный сервер.
    #принимает номер VPS
    my $vid = shift @_;
    my $fulldump_filename = $basename;

    print "Делаем полный бэкап...\n" if ($DEBUG == 1);

    $command = "vzdump $vzdump_opts --dumpdir $local_dumpdir $vid";
    
#tar и vzdump генерируют неотключаемый вывод, для удаления их болтовни изолируем их от stderr и stdout
    if ($quiet == 1){
	my $chatter = `$command 2>&1`;
	if (($? >> 8) != 0){
	    die "$chatter\nvzdump exited with error: ($? >> 8)\n";
	}
    }else{
    	system ($command) == 0 or die "Cannot run \'$command\': $!";
    }

    print "И переносим файл полного бэкапа на удаленный сервер...\n" if ($DEBUG == 1);
    $command = "$prog_scp $local_dumpdir/$fulldump_filename $remote_user\@$remote_host:$remote_dumpdir/$vid/$fulldump_filename";
    system ($command) == 0 or die "Cannot run \'$command\':$!";

}

sub make_xdelta {
    my $vid = shift @_;    
    my $fulldump_filename = $basename;
    
    #на всякий случай проверим, что локально у нас есть полный дамп
    if (! -e  "$local_dumpdir/$fulldump_filename"){
    #если локально полный дамп отсутствует, копируем его из удаленного хранилища
	print "Полного дампа локально нет, переносим файл полного бэкапа из удаленного хранилища в локальное...\n" if ($DEBUG == 1);
	$command = "$prog_scp $remote_user\@$remote_host:$remote_dumpdir/$vid/$fulldump_filename $local_dumpdir/$fulldump_filename";
	system ($command) == 0 or die "Cannot run \'$command\':$!";
	warn "Local $fulldump_filename has been moved to $fulldump_filename.$uniqe\n";
    }else{
	#если файл на месте, на всякий случй проверяем что файл в удаленном хранилище и локальный идентичны
        print "Считаем контрольную сумму для локального полного бэкапа...\n" if ($DEBUG == 1);
        my $local_md5sum = `md5sum -b $local_dumpdir/$fulldump_filename`;
	chomp($local_md5sum);
        print "Считаем контрольную сумму для удаленного полного бэкапа...\n" if ($DEBUG == 1);
	$command = "$prog_ssh -l $remote_user $remote_host 'md5sum -b $remote_dumpdir/$vid/$fulldump_filename'";
        my $remote_md5sum = `$command`;
	chomp ($remote_md5sum);

        die "Local md5sum run error: nothing has returned" if (! defined ($local_md5sum));
        die "Remote md5sum run error: nothing has returned" if (! defined ($remote_md5sum));

        if ($local_md5sum =~ /(\w+)\s+\*.+$fulldump_filename$/){
	# 64cd92ef3bedfadf22a6efaf2f6212e0 */var/backup/loki/VPS/101/vzdump-101.tgz
	    $local_md5sum = $1;
        }else{
	    die "md5sum return strange string: $local_md5sum";
        }
    
	if ($remote_md5sum =~ /(\w+)\s+\*.+$fulldump_filename$/){
	    $remote_md5sum = $1;
        }else{
	    die "md5sum return strange string: $remote_md5sum";
        }
    
        #если хэши рассинхронизированны, перемещаем локальный и копируем с удаленного хранилища
	if ($local_md5sum ne $remote_md5sum){
	    warn "MD5 hash of local:\n $local_md5sum\n and remote:\n $remote_md5sum\n full backup files not identic!\n" ;

	    print "Переименовываем $local_dumpdir/$fulldump_filename в $local_dumpdir/$fulldump_filename.$uniqe...\n" if ($DEBUG == 1);
	    rename "$local_dumpdir/$fulldump_filename","$local_dumpdir/$fulldump_filename.$uniqe" or die "Cannot rename $fulldump_filename to $fulldump_filename.$uniqe: $!";
	
	    print "И переносим файл полного бэкапа из удаленного хранилища в локальное...\n" if ($DEBUG == 1);
	    $command = "$prog_scp $remote_user\@$remote_host:$remote_dumpdir/$vid/$fulldump_filename $local_dumpdir/$fulldump_filename";
	    system ($command) == 0 or die "Cannot run \'$command\':$!";
	    warn "Local $fulldump_filename has been moved to $fulldump_filename.$uniqe\n";
	}
    }

    $command = "vzdump $vzdump_opts --xdelta --dumpdir $local_dumpdir $vid";
    print "Запускаем \'$command\'...\n" if ($DEBUG == 1);
    #tar и vzdump генерируют неотключаемый вывод, для удаления их болтовни изолируем их от stderr и stdout
    if ($quiet == 1){
	my $chatter = `$command 2>&1`;
	if (($? >> 8) != 0){
	    die "$chatter\nvzdump exited with error: ($? >> 8)\n";
	}
    }else{
    	system ($command) == 0 or die "Cannot run \'$command\': $!";
    }
    
        my ($year, $month, $day) = (localtime(time())) [5,4,3];
    my $date = sprintf ("%02d.%02d.%4d", $day, $month+1, $year+1900);
    
    print "Переименовываем $local_dumpdir/vzdump-$vid.xdelta в $local_dumpdir/$basename.xdelta.$date.$uniqe...\n" if ($DEBUG == 1);
    rename "$local_dumpdir/vzdump-$vid.xdelta","$local_dumpdir/$basename.xdelta.$date.$uniqe" or die "Cannot rename $basename.xdelta to $basename.xdelta.$date.$uniqe: $!";
        
    print "Копируем его в удаленное хранилище...\n" if ($DEBUG == 1);
    $command = "$prog_scp $local_dumpdir/$basename.xdelta.$date.$uniqe $remote_user\@$remote_host:$remote_dumpdir/$vid/$basename.xdelta.$date.$uniqe";
    system ($command) == 0 or die "Cannot run \'$command\': $!";
    
    print "Удаляем локальную копию...\n" if ($DEBUG == 1);
    unlink "$local_dumpdir/$basename.xdelta.$date.$uniqe" or die "Cannot unlink $local_dumpdir/$basename.xdelta.$date.$uniqe: $!";
}

#printing help
sub usage {
    # Show usage for this program
    print "\nUsage: $0 [--all | VID] | [--help | -h ]\n";
    print "\t--all: backup all VIDs\n";
    print "\t VID: number of VID for backup\n";
    print "\t--help or -h:\tThis message.\n";
    
    print "\t--compress: use \"--compress\" mode of vzdump\n";
    print "\t--stop: use \"--stop\" mode of vzdump\n";
    print "\t--suspend: use \"--suspend\" mode of vzdump\n";
    print "\t--snapshot: use \"--snapshot\" mode of vzdump\n";
    
    exit (99);
}

