#!/usr/bin/perl -w ################################################################################ # # # # # # # # # Version: $Id: rbs,v 1.29 2007/12/25 16:02:44 shurra Exp $ # # # ################################################################################ use strict; use vars qw(%CONFIG $config %progs %menu %limits %CONFIG_NEW $str $batchmode $ver); use Fcntl ':flock'; use Fcntl; use File::Copy; use constant FILE_NOT_EXISTS => 0; use constant FILE_EXISTS => 1; use constant FILE_DIRECTORY => 2; use constant FILE_NOT_FILE => 3; use constant FILE_NOT_READABLE => 4; use constant FILE_FAILED => 5; $| = 1; $batchmode = 0; $ver = '$Id: rbs,v 1.29 2007/12/25 16:02:44 shurra Exp $'; $ver =~ s/^(?:.+?)(rbs,)v\s+([0-9\.]+)\s+(\d+)\/(\d+)\/(\d+)\s(.+)$/$1 Version $2: $5-$4-$3/; ################################################################################ # add file's directory to @INC BEGIN { my $b = $0; $b =~ s/\/[^\/]+$//; push(@INC, $b); $config = "$b/rbs.conf"; } ################################################################################ %progs = ("stty" => "/bin/stty", "openssl" => "/usr/bin/openssl", "ssh" => "/usr/bin/ssh", "ssh-keygen" => "?/opt/usr/bin/ssh-keygen", "bzip2" => "/usr/bin/bzip2", "gzip" => "/usr/bin/gzip", "tar" => "/usr/bin/tar", "sh" => "/bin/sh", "less" => "/usr/bin/less", "exit" => "");#|| exit 1"); ################################################################################ Init(); ReadConfig($config); CheckProgs(); RebuildMenu(); umask(oct($CONFIG{'umask'})) if ($CONFIG{'umask'} && $CONFIG{'umask'} =~ m/^0[0-7]+$/); if (@ARGV) { $batchmode = 1; # test(); # usage(); if ($ARGV[0] eq 'restore') { $str = ''; if (defined($ARGV[1]) && ($ARGV[1] eq 'last' || $ARGV[1] eq 'list'|| $ARGV[1] =~ m/^\d{8}-\d{6}-[a-z0-9\-]+$/i)) { $str = $ARGV[1] if ($ARGV[1] eq 'last' || $ARGV[1] =~ m/^\d{8}-\d{6}-[a-z0-9\-]+$/i); if ($str) { Restore($str); } elsif ($ARGV[1] eq 'list') { GetBackupsList(); } } else { if (defined($ARGV[1])) { print STDERR "No rigth parameter $ARGV[1] for restore command\n"; } else { print STDERR "Restore command require one parameter\n" } usage(); } } elsif ($ARGV[0] eq 'openssl') { $str = $ARGV[1] if (defined($ARGV[1]) && $ARGV[1]=~ m/^[\w\-\._]+$/); OpenSSL($str || $CONFIG{'openssl.key'}); } elsif ($ARGV[0] eq 'store') { $str = $ARGV[1] if (defined($ARGV[1]) && $ARGV[1] =~ m/^[2-9A-Z]$/); Store($str || ''); } else { print STDERR "Parameter $ARGV[0] unknown\n" unless ($ARGV[0] =~ m/^((--)?help|-h)$/); usage(); } exit; } else { Proccess(); } exit; ################################################################################ sub test { foreach (sort keys %CONFIG) { print "$_\t= $CONFIG{$_}\n"; if (ref($CONFIG{$_}) eq 'ARRAY') { foreach (@{$CONFIG{$_}}) { next unless($_); print " $_\n"; } } } RebuildMenu(); } ################################################################################ sub CheckProgs { my (@path) = ('/bin/', '/usr/bin/', '/usr/local/bin/', '/usr/sbin/'); my ($flag, $orig, @tmp, $found, $OS, $progName); $OS = $^O; foreach my $prog (keys %progs) { $progName = $prog; if ($OS =~ m/openbsd/i && $prog eq 'tar') { # on OpenBSD we need gtar (GNU tar), not system tar! $progs{'tar'} =~ s/tar$/gtar/; $progName = 'gtar'; } if ($progs{$prog} =~ m/^([^\/]+)(\/.+)$/) { $flag = $1; $orig = $2; } else { $flag = ''; $orig = $progs{$prog}; } @tmp = (); foreach(@path) { push(@tmp, "$_$progName"); } $found = 0; foreach my $try ($orig, @tmp) { if ($try && -x $try) { $progs{$prog} = "$flag$try"; $found = 1; last; } } if ($found == 0 && $progs{$prog}) { Log("Can't find path to program $prog (use $progs{$prog})", 'error'); print STDERR "Can't find path to program $prog (use $progs{$prog})\n"; } } } ################################################################################ sub Proccess { my ($c, $menu, $submenu, $str, $param); # test(); $menu = ''; $submenu = ''; while (1) { ClearScreen(); $c = PrintMenu($menu, $submenu); if ($c =~ m/[0xq]/i) { if ($menu eq '1' && $submenu eq '') { foreach (keys %CONFIG_NEW) { if (!defined($CONFIG{$_}) || $CONFIG{$_} ne $CONFIG_NEW{$_} || defined($CONFIG_NEW{'SAVE'})) { SaveParams($config) if (AskYesNo("\nSave parameters?", "Y")); last; } } } if ($submenu) { $submenu = ''; } elsif ($menu) { $menu = ''; } else { last; } next; } %CONFIG_NEW = %CONFIG if ($menu eq '' && $submenu eq '' && $c eq '1'); $str = "items" . ($menu ? "-$menu" . ($submenu? "-$submenu" : ''): ''); if ($menu{$str}) { foreach (@{$menu{$str}}) { if (m/^\Q$c\E\./) { $str = ''; last; } } unless ($str) { # try to open menu if (!$menu && $menu{"items-$c"}) { $menu = $c; next; } elsif (!$submenu && $menu{"items-$menu-$c"}) { $submenu = $c; next; } else { print "\n"; # process the menu command if ($menu eq '' && $c eq '6') { Help(); next; } elsif ($menu eq '1') { if ($submenu eq '' && $c eq '7') { # save changes SaveParams($config); next; } elsif ($submenu eq '3' && $c !~ /[56]/) { if ($c eq '1') { ViewBackupTask(); next; } elsif ($c eq '2') { EditBackupTask(); next; } elsif ($c eq '3') { EditBackupTask('new'); next; } elsif($c eq '4') { DeleteBackupTask(); next; } } elsif ($submenu eq '' && $c eq '6') { TestSSH('store'); TestSSH('restore'); WaitChar(); next; } else { ($str, $param) = GetMenuItem($menu, $submenu, $c); if ($str && $param) { ChangeParam("Input " . Lower($str), $param); next; } } } elsif ($menu eq '2') { if ($c eq '1') { OpenSSL($CONFIG{'openssl.key'}); next; } elsif ($c eq '2') { OpenSSLCheck($CONFIG{'openssl.key'}); next; } } elsif ($menu eq '3') { if ($c eq '1') { SSHKeyGenerate('store', $CONFIG{'ssh.store-key'}, $CONFIG{'ssh.public-path'}, $CONFIG{'ssh.type'}); next; } elsif ($c eq '2') { SSHKeyGenerate('restore', $CONFIG{'ssh.restore-key'}, $CONFIG{'ssh.public-path'}, $CONFIG{'ssh.type'}); next; } } elsif ($menu eq '4') { if ($c =~ m/^[1-9A-Z]$/) { Store($c); next; } } elsif ($menu eq '5') { if ($c eq '1') { GetBackupsList(); next; } elsif ($c eq '2' || $c eq '3') { Restore($c eq '2' ? 'last' : '', undef); next; } elsif ($c eq '4' || $c eq '5') { ViewRemoteLog(($c eq '4' ? 'access' : 'error')); next; } } print "Not implemented yet (" . ($menu ? $menu : '0') . ($submenu ? "-$submenu" : '') . "-$c)!\n"; sleep $CONFIG{'post-delay'}; } } } else { print "Invalid command.\n\n"; next; } } } ################################################################################ sub GetMenuItem { my @menu = @_; my $text = ''; my ($item, $str, $param); while ($item = shift(@menu)) { last unless (@menu); $text .= "-$item"; } $str = ''; if ($menu{"items$text"} && ref($menu{"items$text"}) eq 'ARRAY') { foreach (@{$menu{"items$text"}}) { if (m/^\Q$item\E\.\s*(\S[^\(\$]+)(?:[\s\(\$]|$)/) { $str = $1; trim($str); $param = $1 if (m/\$([^\$]+)\$/); last; } } } return ($str, $param) if (wantarray); return $str; } ################################################################################ sub ChangeParam { my ($prompt, $param) = @_; my ($new, $value); if ($param) { $prompt = "Input new value for $param" unless ($prompt); if (defined($limits{$param}) && $limits{$param}) { ($value = $limits{$param}) =~ s/\|/, /g; $value =~ s/\[([^\-]+\-[^\-]+)\]/$1/g; $value =~ s/^\\d\+$/numeric/; $prompt .= " ($value)"; } $value = (defined($CONFIG_NEW{$param}) ? $CONFIG_NEW{$param} : defined($CONFIG{$param}) ? $CONFIG{$param} : ''); do { $new = GetLine($prompt, $value); } while (defined($limits{$param}) && $limits{$param} && !CheckValue($new, $param)); $CONFIG_NEW{$param} = $new; } } ################################################################################ sub DoParams { my ($section, $catRef, %NEW) = @_; my ($out, $tab); $out = ''; foreach my $key (sort keys (%NEW)) { next unless ($key =~ m/^\Q$section\E(.+)$/); $tab = (length($1) <=8 ? "\t\t" : "\t"); if (ref($NEW{$key}) eq 'ARRAY') { foreach (@{$NEW{$key}}) { next unless($_); $out .= "$1$tab$_\n"; } } else { $out .= "$1$tab$NEW{$key}\n"; } } @{$catRef} = grep { "$_." ne $section } @{$catRef}; return $out; } ################################################################################ sub SaveParams { my $config = shift; my ($str, $new, $param, $section, $skip, $skipsection, %NEW, @categories); delete($CONFIG_NEW{'SAVE'}); $section = ''; $skipsection = 0; if (IsFile($config) == FILE_EXISTS) { if (open(OLD, $config)) { do { $new = "$config-" . rnd(); } while (-e $new); if (sysopen(NEW, $new, O_CREAT | O_WRONLY | O_TRUNC)) { %NEW = %CONFIG_NEW; @categories = @{$NEW{'CATEGORIES'}}; while () { chomp; if (m/^\s*\[([\w\-]+)\]\s*$/) { print NEW DoParams($section, \@categories, %NEW) if ($section); if (exists($CONFIG_NEW{$1}) && !defined($CONFIG_NEW{$1})) { $skipsection = 1; } else { $skipsection = 0; } $section = "$1."; } next if ($skipsection); if (m/^\s*^([a-z0-9\-\.]+)((\s*=?\s*)([\w\-\+\\\/\.:;\"\'\(\)\*][\w\-\+\\\/\.\s:;=>,\@\"\'\(\)\*]*))?$/) { $skip = '' if ($skip && $skip ne $1); next if ($skip); if ($2 && defined($CONFIG_NEW{"$section$1"})) { if (ref($CONFIG_NEW{"$section$1"}) eq 'ARRAY') { foreach(@{$CONFIG_NEW{"$section$1"}}) { print NEW $1 . $3 . $_ . "\n"; } $skip = $1; } else { print NEW $1 . $3 . $CONFIG_NEW{"$section$1"} . "\n"; } delete($NEW{"$section$1"}); } else { print NEW "$_\n"; } } else { print NEW "$_\n"; } } print NEW DoParams($section, \@categories, %NEW); foreach (grep { defined($_) && $_ } @categories) { print NEW "\n[$_]\n"; print NEW DoParams("$_.", \@categories, %NEW); } close(OLD) or warn $!; close(NEW) or warn $!; if (rename($new, $config)) { %CONFIG = %CONFIG_NEW; print "Parameters saved\n"; sleep $CONFIG{'post-delay'}; return; } else { Log("Can't rename file $config to $new: $!", 'error'); print "Can't save changes: $!"; } } else { Log("Can't open file for writing: $!", 'error'); print "Can't open file for writing: $!\n"; } } else { Log("Can't open config file: $!", 'error'); print "Can't open config file: $!\n"; } } else { # foreach (sort keys (%CONFIG_NEW)) { # print "$_\t= $CONFIG{$_}\n"; # if (ref($CONFIG{$_}) eq 'ARRAY') { # foreach my $a (@{$CONFIG{$_}}) { # print " $a\n"; # } # } # } } return WaitChar(); } ################################################################################ sub CheckValue { my ($value, $limit) = @_; return 0 unless($value); if ($value =~ m/^\d+$/ && $limits{$limit} =~ m/^\[(\d+)\-(\d+)\]$/) { return ($value >= $1 && $value <= $2 ? 1 : 0); } else { return ($value =~ m/^($limits{$limit})$/); } } ################################################################################ sub GetBackupsList { my ($lf) = shift || 0; my ($status, @output); my %length; return WaitChar() unless (CheckSSHKey('restore')); ($status, @output) = ExecCmd($progs{'ssh'}, undef, "-o 'NumberOfPasswordPrompts 0'", (CheckValue($CONFIG{'ssh.protocol'}, 'ssh.protocol') && $CONFIG{'ssh.protocol'} =~ m/^\d$/? "-$CONFIG{'ssh.protocol'}" : ''), '-i', $CONFIG{'ssh.restore-key'}, $CONFIG{'ssh.server'}, '-l', $CONFIG{'ssh.username'}, 'list'); unless ($status) { if (@output && @output > 1) { $length{'name'} = 0; $length{'size'} = 0; foreach (@output) { next unless (m/^\s*\d+-\d+\-([^\s]+)\s+(\d+)/); $length{'name'} = length($1) if (length($1) > $length{'name'}); $length{'size'} = length($2) if (length($2) > $length{'size'}); } print "Date Time " . "Name" . (" " x ($length{'name'} - 3)) . "Size"; print ((" " x $length{'size'}) . "File name") if ($lf); print "\n"; foreach (@output) { if (m/^\s*(\d{4})(\d{2})(\d{2})\-(\d{2})(\d{2})(\d{2})\-([^\s]+)\s+(\d+)/) { print "$1-$2-$3 $4:$5:$6 $7" . (" " x ($length{'name'} - length($7))) . " $8"; print (" " x ($length{'size'} - length($8)) . " $1$2$3-$4$5$6-$7") if ($lf); print "\n"; } elsif (!$lf && m/^\s*(\d{3}):\s(.+)$/) { print "$2\n"; } elsif (!$lf) { print "$_\n"; } } } elsif (@output && $output[0] =~ m/^\s*(\d{3}):\s(.+)$/) { print "$2\n"; } else { print join("\n", @output) . "\n"; } } else { print "Authentification failed\n"; return WaitChar(); } return WaitChar() unless ($lf); } ################################################################################ sub ViewRemoteLog { my $log = shift; my (@args, $status, @output); return WaitChar() unless (CheckSSHKey('restore')); push(@args, "-i", $CONFIG{'ssh.restore-key'}, "-o 'NumberOfPasswordPrompts 0'"); push(@args, "-$CONFIG{'ssh.protocol'}") if (CheckValue($CONFIG{'ssh.protocol'}, 'ssh.protocol') && $CONFIG{'ssh.protocol'} =~ m/^\d$/); push(@args, $CONFIG{'ssh.server'}, '-l', $CONFIG{'ssh.username'}, "$log-log"); ($status, @output) = ExecCmd($progs{"ssh"}, undef, @args); if ($status) { print "Can't get $log log\n"; if (grep {m/permission\s+denied/i} @output) { print "Authentification failed\n"; } else { DumpErrors("SSH", @output) if (@output); } } elsif (@output) { if ($output[0] =~ /^251:/) { shift(@output); $status = "Remote backup server $log log file:\n\n" . join("\n", @output) . "\n"; pager(\$status); return; } elsif ($output[0] =~ /^(\d+):\s*(.+)$/) { Log("View $log log failed: $2 (errcode $1)", 'error'); print "View $log log: $2\n"; } else { Log('Unknown error', 'error'); print "Unknown error\n"; } } return WaitChar(); } ################################################################################ sub Store { my ($item) = @_; my (@args, $method, $compression, $name, $status, @output, $task); $task = $item; if ($task eq '1') { $task = ''; } elsif ($task) { $task = "-$task"; } if (IsFile($CONFIG{'openssl.key'}) != FILE_EXISTS) { print "SSL key $CONFIG{'openssl.key'} not found\n"; Log("SSL key $CONFIG{'openssl.key'} not found", 'error'); return WaitChar(); } return WaitChar() unless (CheckSSHKey('store')); $CONFIG{'rotation'} = '' unless(defined($CONFIG{'rotation'}) && $CONFIG{'rotation'} =~ m/^\d+$/); if ($CONFIG{"backup$task.include"}) { $name = $CONFIG{"backup$task.backup-name"} || 'mybackup'; push(@args, $progs{"tar"}, "-cf - -P"); # at first, add exclude files if ($CONFIG{"backup$task.exclude"}) { if (ref($CONFIG{"backup$task.exclude"}) eq 'ARRAY') { foreach (@{$CONFIG{"backup$task.exclude"}}) { push(@args, "--exclude", $_); } } else { push(@args, "--exclude", $CONFIG{"backup$task.exclude"}); } } # then, add a list of files to backup if (ref($CONFIG{"backup$task.include"}) eq 'ARRAY') { foreach (@{$CONFIG{"backup$task.include"}}) { push(@args, $_); } } else { push(@args, $CONFIG{"backup$task.include"}); } $method = $CONFIG{"backup$task.method"} || $CONFIG{"backup.method"} || ''; $compression = $CONFIG{"backup$task.compression"} || $CONFIG{"backup.compression"} || ''; $method = 'none' unless CheckValue($method, 'backup.method'); if ($method =~ m/^(gzip|bzip2)$/) { push(@args, "|", $progs{$1}, "-c", "-$compression"); } # openssl cryption push(@args, "|", $progs{"openssl"}, "des3 -salt -kfile", $CONFIG{'openssl.key'}); # send data over SSH to backup server push(@args, "|", $progs{"ssh"}, "-i", $CONFIG{'ssh.store-key'}, "-o 'NumberOfPasswordPrompts 0'"); push(@args, "-$CONFIG{'ssh.protocol'}") if (CheckValue($CONFIG{'ssh.protocol'}, 'ssh.protocol') && $CONFIG{'ssh.protocol'} =~ m/^\d$/); push(@args, $CONFIG{'ssh.server'}, '-l', $CONFIG{'ssh.username'}, $name, $CONFIG{'rotation'}); $status = $args[0]; $args[0] = { 'inout' => "" }; ($status, @output) = ExecCmd($status, @args); if ($status) { if (grep {m/permission\s+denied/i} @output) { print "Authentification failed\n"; } else { print "Backup failed\n"; DumpErrors("Backup", @output) if (@output); } } elsif (@output) { if ($output[0] =~ /^201:\s*(.+)$/) { print "$1\n"; } elsif ($output[0] =~ /^(\d+):\s*(.+)$/) { Log("Backup failed: $2 (errcode $1)", 'error'); print "Backup failed: $2\n"; } else { Log('Unknown backup error', 'error'); print "Unknown backup error\n"; } } } else { print "No backup task $item!\n"; } return WaitChar(); } ################################################################################ sub Restore { my ($command, $filename) = shift; my ($pathToRestore, $status, @output, @args, $tmpDir, $tmpDir2, $i); if (IsFile($CONFIG{'openssl.key'}) != FILE_EXISTS) { print "SSL key $CONFIG{'openssl.key'} not found\n"; Log("SSL key $CONFIG{'openssl.key'} not found", 'error'); return WaitChar(); } return WaitChar() unless (CheckSSHKey('restore')); if (defined($command) && $command eq 'last') { $command = 'getlast'; } else { $command = 'get '; do { $filename = GetLine("Backup to restore in format YYYYMMDD-HHMMSS-name ('?' - view backup list, Enter - exit)", $filename); return unless($filename); if ($filename eq '?') { GetBackupsList(1); $filename = ''; } } while ($filename !~ m/^[\w\.\.\-\(\)\[\]]+$/i); $command .= $filename; } do { if ($batchmode) { $pathToRestore = $CONFIG{'restore.restore-dir'}; } else { $pathToRestore = GetFile("Directory to save backup:", $CONFIG{'restore.restore-dir'}); } if (!-d $pathToRestore && AskYesNo("Directory doesn't exists. Create?", "Y")) { unless (MkDir($pathToRestore)) { $status = "Can't create directory: $!"; Log($status, 'error'); print "$status\n"; return WaitChar(); } } } while (!-d $pathToRestore && !$batchmode); do { $tmpDir = "$pathToRestore/tmp-" . rnd(); } while (-d $tmpDir); if (MkDir($tmpDir)) { Log("Mkdir $tmpDir for retcieving files", 'debug'); } else { Log("Can't create temporary directory $tmpDir: $!", 'error'); print "Can't create temporary directory $tmpDir: $!\n"; return WaitChar(); } push(@args, $progs{'ssh'}, "-i", $CONFIG{'ssh.restore-key'}, "-o 'NumberOfPasswordPrompts 0'"); push(@args, "-$CONFIG{'ssh.protocol'}") if (CheckValue($CONFIG{'ssh.protocol'}, 'ssh.protocol') && $CONFIG{'ssh.protocol'} =~ m/^\d$/); push(@args, $CONFIG{'ssh.server'}, '-l', $CONFIG{'ssh.username'}, $command, $progs{'exit'}); push(@args, "|", $progs{'openssl'}, "des3 -d -kfile", $CONFIG{'openssl.key'}, $progs{'exit'}); push(@args, "|", $progs{'tar'}, "-xf - -C", $tmpDir); ($status, @output) = ExecCmd(shift(@args), {'allout' => 1}, @args); if (@output) { if ($output[0] =~ m/^\s*211:\s+backup\s+file\s+\d{8}\-\d{6}\-([^\s]+)\s+(\d+)\s+(?:bytes)?/) { print "SSH data receiving OK (get $2 bytes)\n"; if (rename($tmpDir, "$pathToRestore/$1")) { print "Restored files located in $pathToRestore/$1 directory\n"; } else { $i = 0; do { $tmpDir2 = "$pathToRestore/$1.$i"; $i++; } while (-d $tmpDir2); if (rename($tmpDir, $tmpDir2)) { print "Restored files located in $tmpDir2 (because $pathToRestore/$1 exists)\n"; Log("Restored files located in $tmpDir2 (because $pathToRestore/$1 exists)", 'debug'); } else { Log("Can't rename tmp directory to $1: $!", 'error'); print "Can't rename tmp directory to $!: $!\n"; } } shift(@output); $status += 1024 if ($status); } elsif ($output[0] =~ m/\s*\d+:\s+(\S.+)$/) { print "SSH data receiving failed: $1\n"; Log("SSH data receiving failed: $1", 'error'); $status = 1; shift(@output); } } if ($status) { if (grep {m/permission\s+denied/i} @output) { print "Authentification failed\n"; } else { print "Backup restore failed.\n" if ($status < 1024); DumpErrors("Restore", @output) if (@output); } rmdir($tmpDir) or Log("Can't delete temporary directory: $!", 'error'); return WaitChar(); } return WaitChar(); } ################################################################################ sub TestSSH { my $action = shift; my ($status, @output); $action = 'store' unless ($action && $action =~ m/^(re)?store$/); return unless (CheckSSHKey($action)); ($status, @output) = ExecCmd($progs{'ssh'}, undef, "-i", $CONFIG_NEW{"ssh.$action-key"}, (CheckValue($CONFIG_NEW{'ssh.protocol'}, 'ssh.protocol') && $CONFIG_NEW{'ssh.protocol'} =~ m/^\d$/ ? "-$CONFIG_NEW{'ssh.protocol'}" : ''), "-o 'NumberOfPasswordPrompts 0'", $CONFIG_NEW{'ssh.server'}, '-l', $CONFIG{'ssh.username'}, ($action eq 'restore' ? 'help' : '')); if ($status) { print "SSH $action connection failed\n"; DumpErrors("SSH", @output); } elsif (@output && (($action eq 'store' && ($output[0] =~ m/^401:/ || $output[1] =~ m/^401:/)) || ($action eq 'restore' && $output[0] =~ m/^usage:/))) { print "SSH $action connection OK\n"; } else { print "SSH $action connection failed\n"; } } ################################################################################ sub GetTaskList { my (@tasks); foreach (sort @{$CONFIG_NEW{'CATEGORIES'}}) { next unless (m/^backup(-([a-z2-9]))?$/); push(@tasks, ($2 ? $2 : '1')); } return @tasks; } ################################################################################ sub ViewBackupTask { my ($task, $num) = @_; my ($str, $i, $j, $count, @a); return WaitChar() unless (CheckSSHKey('restore')); $num = '' unless($num); $j = 1; $count = 23; $count += 3 if ($num); unless ($task) { @a = GetTaskList(); ClearScreen(); $i = 0; foreach (@a) { ViewBackupTask($_); WaitChar(($i++ == $#a ? "" : "Next...")); print "\n"; } return; } @a = (); $task = lc($task); if ($task eq '1') { $str = ''; } else { $str = "-$task"; } if (defined($CONFIG_NEW{"backup$str.include"})) { print " Backup task \U$task" . (!$str ? " (main)" : '') . ":\n\n"; if ($num) { print " " . $j++ . "." } else { print " " } print " Backup name : " . ($CONFIG_NEW{"backup$str.backup-name"} ? $CONFIG_NEW{"backup$str.backup-name"} : '(none)') . "\n"; if ($num) { print " " . $j++ . "." } else { print " " } if (ref($CONFIG_NEW{"backup$str.include"}) eq 'ARRAY') { @a = @{$CONFIG_NEW{"backup$str.include"}}; } else { push(@a, $CONFIG_NEW{"backup$str.include"}); } $i = 0; foreach (@a) { print(($i++ ? " " x $count : " Include files : ") . "$_\n"); } if ($num) { print " " . $j++ . "." } else { print " " } @a = (); if ($CONFIG_NEW{"backup$str.exclude"}) { if (ref($CONFIG_NEW{"backup$str.exclude"}) eq 'ARRAY') { @a = @{$CONFIG_NEW{"backup$str.exclude"}}; } else { push(@a, $CONFIG_NEW{"backup$str.exclude"}); } } else { push(@a, ''); } $i = 0; foreach (@a) { print(($i++ ? " " x $count : " Exclude files : ") . "$_\n"); } $i = $CONFIG_NEW{"backup$str.method"} || $CONFIG_NEW{"backup.method"} || '(none)'; if ($num) { print " " . $j++ . "." } else { print " " } print " Compression method : $i\n"; $i = $CONFIG_NEW{"backup$str.compression"} || $CONFIG_NEW{"backup.compression"} || '(default)'; if ($num) { print " " . $j++ . "." } else { print " " } print " Compression level : $i\n"; print " 0. Exit\n" if ($num); print "\n"; } else { if (AskYesNo("Backup task $task havn't include parameter. Delete?", "Y")) { DeleteBackupTask($task, 'Y'); } } } ################################################################################ sub DeleteBackupTask { my ($task, $confirm) = @_; my @tasks; @tasks = GetTaskList(); unless ($task) { do { $task = GetKey("Enter task to delete (Enter - exit) (" . join(", ", @tasks) . ")", "0"); return if ($task eq '0'); } while (!(grep { /^\Q$task\E$/i } @tasks)); } unless (defined($confirm) && $confirm eq 'Y') { return unless (AskYesNo("Delete backup task $task?", "N")); } @{$CONFIG_NEW{'CATEGORIES'}} = grep { $_ ne "backup-$task" } @{$CONFIG_NEW{'CATEGORIES'}}; foreach (keys %CONFIG_NEW) { delete($CONFIG_NEW{$_}) if (m/^\Qbackup-$task.\E/); } $CONFIG_NEW{"backup-$task"} = undef; $CONFIG_NEW{'SAVE'} = 1; } ################################################################################ sub EditBackupTask { my $task = shift || ''; my (@tasks, $c, $new, $t, $mask, $i, $inc); @tasks = GetTaskList(); if ($task eq 'new') { $i = join("", "1".."9", "a".."z"); foreach(@tasks) { $i =~ s/\Q$_\E//; } $task = substr($i, 0, 1); push(@{$CONFIG_NEW{'CATEGORIES'}}, "backup-$task"); $CONFIG_NEW{"backup-$task.include"} = ''; } unless ($task) { do { $task = GetKey("Enter task to edit (Enter - exit) (" . join(", ", @tasks) . ")", "0"); return if ($task eq '0'); } while (!(grep { /^\Q$task\E$/i } @tasks)); } $task = lc($task); if ($task ne '1') { $t = "-$task"; } else { $t = ''; } do { ClearScreen(); ViewBackupTask($task, 1); $c = GetKey("Input your choice", '-'); if ($c eq '0') { unless ($CONFIG_NEW{"backup$t.include"}) { print "You must specify include parameter!\n"; if (AskYesNo("Delete this task?", "Y")) { @{$CONFIG_NEW{'CATEGORIES'}} = grep { $_ ne "backup$t"} @{$CONFIG_NEW{'CATEGORIES'}}; foreach (keys %CONFIG_NEW) { delete($CONFIG_NEW{$_}) if (m/^backup$t\./); } } else { $c = '2'; } } } if ($c eq '1') { $CONFIG_NEW{"backup$t.backup-name"} = GetLine("Input backup name", $CONFIG_NEW{"backup$t.backup-name"}); } elsif ($c eq '2' || $c eq '3') { my @a; $inc = ($c eq '2' ? 'in' : 'ex') . 'clude'; if ($CONFIG_NEW{"backup$t.$inc"}) { if (ref($CONFIG_NEW{"backup$t.$inc"}) eq 'ARRAY') { @a = @{$CONFIG_NEW{"backup$t.$inc"}}; } else { push(@a, $CONFIG_NEW{"backup$t.$inc"}); } } else { push(@a, ''); } $i = 0; print "Input one mask or path at one line:\n"; do { $mask = GetLine("Files to $inc (" . (scalar @a && $i <= $#a ? "- to delete" : "Enter to exit") . ")", (defined($a[$i]) ? $a[$i] : '')); if ($mask =~ m/^-$/) { $a[$i] = undef; $mask = ''; } else { $a[$i] = $mask; } $i++; } while ($i <= $#a || $mask); @a = grep { defined($_) && $_ ne '' } @a; if ((scalar @a) <= 1) { $CONFIG_NEW{"backup$t.$inc"} = $a[0] || ''; } else { $CONFIG_NEW{"backup$t.$inc"} = \@a; } } elsif ($c eq '4') { $new = $CONFIG_NEW{'backup.method'}; ChangeParam("Input compression type", 'backup.method'); print $CONFIG_NEW{'backup.method'} . "\n"; $CONFIG_NEW{"backup$t.method"} = $CONFIG_NEW{'backup.method'}; $CONFIG_NEW{"backup.method"} = $new if ($t); } elsif ($c eq '5') { $new = $CONFIG_NEW{"backup.compression"}; ChangeParam("Input compression level", 'backup.compression'); $CONFIG_NEW{"backup$t.compression"} = $CONFIG_NEW{'backup.compression'}; $CONFIG_NEW{'backup.compression'} = $new if ($t); } } while ($c ne '0'); } ################################################################################ sub GetFile { my ($prompt, $file) = @_; $prompt = "Input file name" unless ($prompt); $file = '' unless ($file); do { $file = GetLine($prompt, $file); } while ($file eq ''); return $file; } ################################################################################ sub OpenSSL { my ($file) = @_; my ($tmp, $i, $status, @output); while (-f ($file = GetFile("File name to save SSL key", $file)) && !-z $file) { if (AskYesNo("File $file exists. Overwrite?", 'N')) { $tmp = "$file.old"; $i = 1; while (-f $tmp && !-z $tmp) { $tmp = "$file.old-$i"; $i++; } if (AskYesNo("Save old SSL key file as $tmp?", 'Y')) { if (rename($file, $tmp)) { Log("Old SSL key $file moved to $tmp", 'debug'); } else { Log("Can't move old SSL key $file to $tmp: $!", 'error'); print "Can't move old SSL key: $!\n"; return WaitChar(); } } else { next unless (AskYesNo("Without current SSL key you can't restore your old backups! Continue?", 'N')); } last; } } $status = IsFile($file); if ($status == FILE_EXISTS || $status == FILE_NOT_EXISTS) { ($status, @output) = ExecCmd($progs{'openssl'}, undef, "genrsa -out $file $CONFIG{'openssl.size'}"); } elsif ($status == FILE_NOT_EXISTS || $status == FILE_NOT_FILE) { Log("Can't open file for writing: $!", 'error'); print "Can't open file for writing: $!\n"; return WaitChar(); } unless ($status) { if (open(SSL, $file)) { if (open(SSLNEW, ">$file.tmp")) { while () { chomp; next if (m/PRIVATE\s+KEY/); print SSLNEW $_; } close(SSLNEW); } else { print "Can't open temporary SSL key: $!\n"; Log("Can't open temporary SSL key: $!", 'error'); return WaitChar(); } close(SSL); unless (rename("$file.tmp", $file)) { Log("Can't rename file $file.tmp to $file: $!", 'error'); print "Can't rename file $file.tmp to $file: $!\n"; return WaitChar(); } } else { print "Can't open SSL key: $!\n"; Log("Can't open SSL key: $!", 'error'); return WaitChar(); } } if ($status) { print "SSL key not created.\n"; DumpErrors("openssl", @output); return WaitChar(); } else { print "SSL key created.\n"; sleep $CONFIG{'post-delay'}; } } ################################################################################ sub OpenSSLCheck { my ($file) = @_; my ($result, $i); if (IsFile($file) != FILE_EXISTS) { print "SSL key file $file not found\n"; Log("SSL key file $file not found", 'error'); } else { print "SSL key seems to be valid\n"; } sleep $CONFIG{'post-delay'}; } ################################################################################ sub SSHKeyGenerate { my ($key, $privateKey, $publicPath, $type) = @_; my ($status, @output); unless ($key =~ m/^(re)?store$/) { $status = "Type $key is unknown"; Log($status, 'error'); print "$status\n"; return WaitChar(); } unless (CheckValue($type, 'ssh.type')) { $status = "Can't use SSH type $key"; Log($status, 'error'); print "$status\n"; return WaitChar(); } $status = IsFile($privateKey); if ($status == FILE_EXISTS || $status == FILE_NOT_EXISTS) { if ($status == FILE_EXISTS) { unlink($privateKey); print "Current SSH key $privateKey deleted\n"; Log("Delete old SSH key $privateKey", 'debug'); } unless (-d $publicPath) { if (AskYesNo("Directory $publicPath doesn't exists. Create?", "Y")) { unless (MkDir($publicPath)) { $status = "Can't create directory: $!"; Log($status, 'error'); print "$status\n"; return WaitChar(); } } else { print "Directory for public key should be created before generation this key\n"; return WaitChar(); } } print "Notice: to use SSH keys in batch mode, leave passphrase empty\n"; ($status, @output) = ExecCmd($progs{'ssh-keygen'}, undef, "-b", $CONFIG{'ssh.size'}, "-t", (CheckValue($CONFIG{'ssh.type'}, 'ssh.type') ? $CONFIG{'ssh.type'} : 'rsa1'), "-f", $privateKey); if ($status) { print "SSH $key key not created.\n"; DumpErrors("ssh-keygen", @output); return WaitChar(); } else { print "SSH $key key created.\n"; unless (move("$privateKey.pub", $publicPath)) { $status = "Failed to move public key to $publicPath: $!"; Log($status, 'error'); print "$status\n"; return WaitChar(); } sleep $CONFIG{'post-delay'}; } } else { $status = "Can't create private key: $!"; Log($status, 'error'); print "$status\n"; return WaitChar(); } } ################################################################################ sub MkDir { my $dir = shift; if (!-e $dir) { return 1 if (mkdir($dir)); } return 0; } ################################################################################ sub IsFile { # return info about $file: # - file exists and redable (FILE_EXISTS) # - file not exists, but can be created (FILE_NOT_EXISTS) # - is not a file (FILE_NOT_FILE) # - file exists, but nor readable (FILE_NOT_READABLE) # - file not exists and can't be created (FILE_FAILED) my $file = shift; if (-f $file) { if (open(FILE, $file)) { close(FILE); return FILE_EXISTS; } else { Log("Can't read file $file: $!"); return FILE_NOT_READABLE } } elsif (-d $file) { return FILE_DIRECTORY; } elsif (-e $file) { return FILE_NOT_FILE; } else { if (sysopen(FILE, $file, O_CREAT | O_WRONLY | O_TRUNC)) { close(FILE); unlink $file if (-f $file); return FILE_NOT_EXISTS; } else { Log("File $file creation failed: $!"); return FILE_FAILED; } } } ################################################################################ sub ClearScreen { my $rows = 50; return unless ($CONFIG{'clear-screen'}); if (open(STTY, "$progs{'stty'} size 2>/dev/null |")) { $_ = ; chomp; close(STTY); $rows = $1 if (m/^\s*(\d+)/); } print "\n" x $rows; } ################################################################################ sub ReadConfig { my ($config, $caller) = @_; my ($current, $line, $category, $key, $tmp); $config = "rbs.conf" unless ($config && -f $config); $caller = '' unless ($caller); $current = -1; if (open(CONF, $config)) { while () { chomp; next if (m/^\s*#/); trim($_); $line = $_; $line =~ s/\s*#.*$//; if ($line =~ m/^\[([\w\-]+)\]/) { $category = $1; push (@{$CONFIG{'CATEGORIES'}}, $category); if ($current != 1 && $category eq $caller) { $current = 1; } elsif ($caller) { $current = 0; } } if ($current) { if ($line =~ m/^([a-z0-9\-\.]+)(\s*=?\s*([\w\-\+\\\/\.:;\"\'\(\)\*][\w\-\+\\\/\.\s:;=>,\@\"\'\(\)\*]*))?$/i) { $key = $1; $key = "$category.$1" if ($category && !$caller); if ($2) { if ($3 eq 'yes') { $CONFIG{$key} = 1; } elsif ($3 eq 'no') { $CONFIG{$key} = 0; } else { $line = $key; ($tmp = $3) =~ s/^\"(.+?)\"$/$1/; if ($CONFIG{$line}) { if (ref($CONFIG{$line}) eq 'ARRAY') { push(@{$CONFIG{$line}}, $tmp); } else { $key = $CONFIG{$line}; $CONFIG{$line} = undef; my @a = ($key, $tmp); $CONFIG{$line} = \@a; } } else { $CONFIG{$line} = $tmp; } } } else { $CONFIG{$1} = 1; } } } } close(CONF); } } ################################################################################ sub trim { foreach (@_) { $_ =~ s/^\s+//; $_ =~ s/\s+$//; } } ################################################################################ sub GetKey { my ($prompt, $default) = @_; my ($ttycap); my ($key); $default = "" unless (defined($default)); print "$prompt: " if (defined($prompt) && $prompt ne ""); $ttycap = `$progs{'stty'} -g`; system("$progs{'stty'} cbreak /dev/tty 2>/dev/null"); $key = getc(); $key = '' unless (defined($key)); system("$progs{'stty'} $ttycap /dev/tty 2>/dev/null"); if ($key eq "\n") { $key = $default if (defined($default)); } else { print "\n"; } return uc($key); } ################################################################################ sub WaitChar { my $str = shift || "Press any key..."; return if ($batchmode); print $str; return GetKey(); } ################################################################################ sub GetLine { my ($prompt, $default) = @_; my ($line); print $prompt . (defined($default) ? " [$default]" : '') . ": "; $line = ; $line = (defined($line) ? $line : ''); chomp ($line); $line =~ s/^\s+//; $line =~ s/\s+$//; $default = '' unless(defined($default)); $line = $default if ($line eq ""); return $line; } ################################################################################ sub AskYesNo { my ($prompt, $default) = @_; my ($key); $prompt = "" unless (defined($prompt)); $default = "" unless (defined($default)); if ($default =~ /^y/i) { $prompt .= " ([Yes]/No)"; } elsif ($default =~ /^n/i) { $prompt .= " (Yes/[No])"; } else { $prompt .= " (Yes/No)"; } while (1) { $key = GetKey($prompt, $default); return 1 if ($key eq "Y"); return 0 if ($key eq "N"); } } ################################################################################ sub PrintMenu { my ($menu, $submenu) = @_; my ($i, $str); $menu = '' unless (defined($menu)); $submenu = '' unless (defined($submenu)); if ($menu) { if ($submenu) { foreach (@{$menu{"items"}}) { if (m/^$menu\.\s*(\S[^\(]+)(?:\s\(|$)/) { print " $1:"; last; } } foreach (@{$menu{"items-$menu"}}) { if (m/^$submenu\.\s*(\S[^\(]+)[\s\(]/) { print " $1\n\n"; last; } } foreach (@{$menu{"items-$menu-$submenu"}}) { $str = $_; $str =~ s/\$([\w0-9\-\.]+)\$/(defined($CONFIG_NEW{$1}) && $1 ? $CONFIG_NEW{$1} : '')/gei; print " $str\n"; } } else { foreach (@{$menu{"items"}}) { if (m/^$menu\.\s*(\S[^\(]+)[\s\(]/) { print " $1\n\n"; last; } } foreach (@{$menu{"items-$menu"}}) { print " $_\n"; } } } else { print " " . $menu{'name'} . "\n\n"; foreach (@{$menu{"items"}}) { print " $_\n"; } } print "\n"; return GetKey("Input your choice", '-'); } ################################################################################ sub Log { my ($str, $level) = @_; $level = 'warning' unless ($level); return 0 if ($str eq '' || ($CONFIG{'security.log-level'} eq 'medium' && $level =~ m/^(debug|info)$/) || ($CONFIG{'security.log-level'} eq 'low' && $level =~ m/^(warning|debug|info)$/)); $str =~ s/\n$//m; $str =~ s/\n//gm; if ($CONFIG{'security.log-file'}) { unless (defined($CONFIG{'STDERR'}) || open(LOG, ">>$CONFIG{'security.log-file'}")) { print STDERR "Can't open log file: $!\n"; print STDERR "All error and debug messages will be written to STDERR\n"; $CONFIG{'STDERR'} = 1; } } else { return 0; } if (defined($CONFIG{'STDERR'})) { print STDERR "[" . scalar(localtime) . "] [$level] $str\n"; } else { flock(LOG, LOCK_EX); print LOG "[" . scalar(localtime) . "] [$level] $str\n"; close(LOG); } return 1; } ################################################################################ sub ExecCmd { my ($cmd, $hr, @args) = @_; my ($status, @output, $echo, $str); $echo = 0; if ($cmd =~ m/^\?(.+)$/) { $cmd = $1; $echo = 1; } $status = 0; if (-x $cmd) { if (defined($hr) && ref($hr) eq 'HASH' && defined($hr->{'allout'})) { map { s/"/\\"/ } @args; unshift(@args, "-c \"( $cmd"); push(@args, ">&2 ) 2>&1\""); $cmd = $progs{'sh'}; } if (open(CMD, "$cmd " . join(" ", @args) . " " . (defined($hr) && ref($hr) eq 'HASH' && defined($hr->{'inout'}) ? $hr->{'inout'} : "2>&1") . " |")) { $str = ''; while (read(CMD, $_, 1)) { print $_ if ($echo); if (m/\n/) { push(@output, $str); $str = ''; } else { $str .= $_; } } unless(close(CMD)) { $status = $?; Log("Command $cmd exited with status: $?", 'error'); } } else { $output[0] = "Can't execute program $cmd: $!"; Log($output[0], 'error'); $status = -2; } } else { $output[0] = "Can't execute program $cmd: program not found"; Log($output[0], 'error'); $status = -1; } return ($status, @output) if (wantarray); return $status; } ################################################################################ sub usage { my $b = $0; $b =~ s/^(?:.*\/)?([^\/]+)$/$1/; print "usage: $b config\n"; print " $b openssl [ssl-file]\n"; print " $b store [backup-name]\n"; print " $b restore last\n"; print " $b restore [backup-name]\n"; print " $b restore list\n"; print " $b help\n"; print " $b\n\n"; print " To run $b utility in interactive mode leave parameters empty.\n\n"; print " The $b utility allow you to make backups of your files\n"; print " to backup.colocall.net server very easy.\n\n"; print " At first, you cat run this utility in interactive mode\n"; print " (whithout parameters). Then you understood how it's work -\n"; print " simply use command line parameters to make and restore backups.\n\n"; print " Parameters:\n\n"; print " config - allow you in interactive mode change parameters of\n"; print " config file (the same you can do editing config file,\n"; print " but via utility it's easy).\n\n"; print " openssl - generete RSA key to crypt your data before it's upload\n"; print " to backup server and decrypt backup atfer receiving\n"; print " to your server. Pay great attention to save this key file\n"; print " in secret.\n"; print " store - make backup to backup server (use in batch mode)\n"; print " restore - restore backup data from server\n"; } ################################################################################ sub Init { my ($line, $section, $i, $j); $section = ''; # seek(DATA, 0, 0); while () { chomp; next if (m/^\s*$/); if (m/^\s*\[([\w]+)\]\s*$/i) { $section = $1; next; } if ($section eq 'menu') { $menu{'name'} = $1 if (m/^\s*([^\d].+)\s*$/ && !(scalar %menu)); if (m/^(\d+).\s+(\S.+)\s*$/) { $i = $1; $menu{"items"} = \@{[]} unless (defined($menu{"items"})); push(@{$menu{"items"}}, "$i. $2"); } elsif (m/^\s{2}(\d+).\s+(\S.+)\s*$/ && $i) { $j = $1; $menu{"items-$i"} = \@{[]} unless (defined($menu{"items-$i"})); push(@{$menu{"items-$i"}}, "$j. $2"); } elsif (m/^\s{4}(\d+).\s+(\S.+)\s*$/ && $i) { $menu{"items-$i-$j"} = \@{[]} unless (defined($menu{"items-$i-$j"})); push (@{$menu{"items-$i-$j"}}, "$1. $2"); } } elsif ($section eq 'limits') { $limits{$1} = $2 if (m/^\s*([\w\-\.]+)\s+([\w\-\+\\\|\[\]]+)\s*$/i); } } } ################################################################################ sub Help { my ($section, $out); ClearScreen(); $section = ''; $out = ''; seek(DATA, 0, 0); while () { chomp; if (m/^\s*\[(\w+)\]\s*$/i) { $section = $1; next; } if ($section eq 'help') { $out .= "$_\n"; } } $out .= "$ver\n"; pager(\$out); } ################################################################################ sub pager { my $dataRef = shift; my $pager = $ENV{'PAGER'} || $progs{'less'}; if (open(PAGER, " | " . $pager)) { print PAGER $$dataRef; close(PAGER); } else { print $$dataRef; return WaitChar(); } } ################################################################################ sub RebuildMenu { # change some dynamically menu items my ($tmp, $i); # backups menu $tmp = $menu{"items-4"}->[$#{$menu{"items-4"}}]; $#{$menu{"items-4"}} = 0; $i = 2; foreach (@{$CONFIG{'CATEGORIES'}}) { next unless (m/^backup-([a-z2-9])$/i); push(@{$menu{"items-4"}} , uc($1) . ". Make " . ($CONFIG{"backup-$1.name"} ? $CONFIG{"backup-$1.name"} : "task $1") . " backup"); } push(@{$menu{"items-4"}}, $tmp); } ################################################################################ sub Lower { my $str = shift; my ($tmp, $upper); if ($str) { ($tmp = $str) =~ s/^(\S+)\s*.*/$1/; ($upper = $tmp) =~ s/[^A-Z]//g; } return (length($upper) > 1 ? $str : lcfirst($str)); } ################################################################################ sub DumpErrors { my ($prog, @output) = @_; return unless ($prog); print "\n$prog program exited with:\n"; print " " . join ("\n ", @output) . "\n\n"; } ################################################################################ sub rnd { # Get random base64 character my $str; for (1..32) { $str .= substr("0123456789ABCDEF", rand (16), 1); } return $str; } ################################################################################ sub CheckSSHKey { my $key = shift; if (IsFile($CONFIG{"ssh.$key-key"}) != FILE_EXISTS) { print "SSH $key key " . $CONFIG{"ssh.$key-key"} . " not found\n"; Log("SSH $key key " . $CONFIG{"ssh.$key-key"} . " not found", 'error'); return 0; } return 1; } ################################################################################ __DATA__ [menu] Remote backup system program 1. Configure program (check and configure program parameters) 1. SSH (change SSH parameters) 1. Backup server host name $ssh.server$ 2. SSH protocol version $ssh.protocol$ 3. Store private key file name $ssh.store-key$ 4. Restore private key file name $ssh.restore-key$ 5. SSH key type $ssh.type$ 6. SSH key's size $ssh.size$ 7. Directory to save public keys $ssh.public-path$ 0. Previous menu 2. OpenSSL (manipulate SSL data encryption) 1. Path to SSL key file $openssl.key$ 2. SSL key file size $openssl.size$ 0. Previous menu 3. Backup tasks (create and modify backup tasks) 1. View backup tasks 2. Edit backup task 3. Add a new backup task 4. Delete backup task 5. Directory to restore backups $restore.restore-dir$ 6. Number of copies to leave at server $rotation$ 0. Previous menu 4. Compression (change default method of data compression) 1. Compression method $backup.method$ 2. Compression level $backup.compression$ 0. Previous menu 5. Security (program logging and reports) 1. Log file $security.log-file$ 2. Log level $security.log-level$ 3. Reports e-mail $security.report-email$ 0. Previous menu 6. Make test connection (make store and restore test procedure) 7. Save changes (save changes to config file) 0. Main menu 2. OpenSSL (generate SSL key file) 1. Generate SSL key file 2. Check SSL key file 0. Main menu 3. SSH keys (generate store and restore SSH keys) 1. Generate store SSH key 2. Generate restore SSH key 0. Main menu 4. Backup (make backup) 1. Make main backup 0. Main menu 5. Restore (view stored backups and receive them) 1. View backups list 2. Get last backup 3. Get backup 4. View backup server access log 5. View backup server error log 0. Main menu 6. Help (view program help) 0. Exit (exit program) [help] REMOTE BACKUP SYSTEM PROGRAM (client) DESCRIPTION Утилита rbs позволяет производить резервное копирование (бэкапы) файлов и каталогов на сервер резервного копирования backup.colocall.net. Для знакомства с утилитой в первый раз вы можете просто запустить её в интерактивном режиме, для чего следует запустить утилиту без параметров командной строки. В дальнейшем, после знакомства и настройки программы, Вы можете использовать утилиту в пакетном режиме (batch mode), используя параметры командной строки. Система резервного копирования предоставляет пользователю определенное дисковое пространство на сервере резервного копирования (backup-сервер). В пределах этого дискового пространства пользователь может размещать резервные копии своих данных. При выполнении резервного копирования предыдущие резервные копии на backup-сервере ротируются до заполнения квоты пользователя или до достижения указанного пользователем числа копий для хранения. При этом самые ранние копии автоматически удаляются. Сам метод выполнения резервного копирования производится с использованием стандартных процедур: данные для резервного копирования сперва организуются в единый архив (с компрессией или без), затем эти данные шифруются на сервере клиента (с помощью openssl и специального файла - SSL-ключа). Полученный закшифрованный архив с помощью SSH передается на backup-сервер, где и сохраняется. Процесс восстановления данных из резервной копии производится в обратном порядке. Благодаря использованию шифрования данных на стороне клиента, Вы можете не беспокоиться за конфиденциальность данных за пределами Вашего сервера, т.к. доступ к архиву без SSL-ключа не позволит получить сами данные. Передача данных между сервером клиента и backup-сервером производится с использованием специальных 2 ID-ключей - один для выполнения резервного копирования и второй - для получения резервных копий обратно (восстановления данных). Для этого в процессе инициализации программы Вам следует сгенерировать указанные SSH-ключи, публичные части которых необходимо передать в службу поддержки компании Колокол. Генерировать эти ключи удобнее всего в интерактивном режиме (см. раздел COMMANDS). Следует уделить особое внимание на сохранность полученных 2 приватных ключей (к примеру, key-store и key-restore), а ключ восстановления данных (key-restore) - желательно вообще не хранить на сервере, а на съемном носителе (например, usb-flash, дискете и т.п.). Подробное описание утилиты rbs смотрите в следующем разделе. На данный момент система резервного копирования проходит стадию тестирования и предоставляется как есть (AS IS). COMMANDS (interactive mode) Большинство настроек утилиты резервного копирования можно легко и быстро сделать в интерактивном режиме. Для вызова той или иной команды необходимо выбрать соответствующий номер пункта меню. Выход из программы или подменю осуществляется с помощью клавиш "0" или "q". 1. Настройка программы (Configure program) * SSH (change SSH parameters) В данном разделе Вы можете поменять некоторые параметры SSH-соединения, через которое происходит передача данных. Большинство параметров имеют значения по-умолчанию, которые можно не менять. Описание параметров: - Backup server host name - имя backup-сервера - SSH protocol version - версия протокола соединения - Store private key file name - путь к ключу выполнения бэкапа - Restore private key file name - путь к ключу восстановления данных - SSH key type - тип ключа - SSH key's size - размер ключа - Directory to save public keys - каталог для сохранения публичных ключей (требуется только при инициализации) * OpenSSL (manipulate SSL data encryption) В данном разделе можно изменить следующие параметры SSL-шифрования: - Path to SSL key file - путь к ключу - SSL key file size - размер ключа * Backup tasks (create and modify backup tasks) В данном разделе производится управление и просмотр списка файлов и каталогов для резервного копирования. Каждый такой набор мы называем заданием (task). При выполнении резервного копирования достаточно указать имя задания - все дополнительные параметры будут взяты из настроек этого задания. Каждое задание имеет следующие параметры (в скобках указан имя параметра в конфиг-фале): - Backup name (backup-name) Имя файла резервной копии (рекомендуем использовать понятные имена). Это имя будет определять имя файла на backup-сервере. - Include files (include) Список файлов и каталогов для включения в резервную копию (допускается использование маски *). - Exclude files (exclude) Список файлов и каталогов, которые не нужно включать в резервную копию (также допускается маска *). Например: *.core, *.bak. - Compression method (method) Метод используемой компрессии данных. - Compression level (compression) Степень компрессии (1 - самый слабый, но быстрый, 9 - самый сильный, но медленный). Описание команд: - View backup tasks - просмотр заданий - Edit backup task - редактирование задания - Add a new backup task - добавление задания - Delete backup task - удаление задания - Directory to restore backups - путь к директории, в которую будут записаны восстановленные данные (при операции восстановления) - Number of copies to leave at server - число копий, которые нужно хранить на backup-сервере (0 - максимально возможное число) * Compression (change default method of data compression) В данном разделе указывается метод и тип компрессии данных, которые используются в основном задании, а также во всех остальных, если в них данные параметры не переопределены (т.е. это параметры по-умолчанию для всех заданий). - Compression method - метод компрессии - Compression level - степень компрессии * Security (program logging and reports) В данном разделе вы можете изменить параметры логирования работы утилиты резервного копирования. - Log file - путь к лог-файлу утилиты - Log level - степень логирования (low, medium, high) - Reports e-mail - e-mail адрес, на который будут отправляться результат работы резервного копирования (для пакетного режима). ПОКА НЕ РЕАЛИЗОВАНО * Make test connection (make store and restore test procedure) После выполнения процедур генерации SSH-клчей и передачи их публичных частей на наш backup-сервер, Вы можете проверить работоспособность SSH-соединений для копирования и восстановления данных. * Save changes (save changes to config file) Сохранение выполненных изменений в конфиг-файл. 2. Генерация SSL-ключа (OpenSSL) В этом разделе Вы можете выполнить генерацию SSL-ключа, который будет использоваться для шифрования и дешифрования Ваших данных. Полученный SSL-ключ необходимо сохранять в труднодоступном месте - без него у Вас не будет возможности востановленяи данных, така же как и его получение сторонними лицами может привести к доступу к Вашим данным при перехвате архива. * Generate SSL key file Генерация новго SSL-ключа. * Check SSL key file Проверка на доступ и валидность SSL-ключа. 3. Генерация SSH ключей (SSH keys) Для возможности производить резервное копирование и восстановление данных Вам нужно прежде всего сгенерировать SSH ключи. Публичные их части Вам нужно передать на backup-сервер, после чего Вы сможете производить резервное копирование. * Generate store SSH key Генерация ключа резервного копирования данных. * Generate restore SSH key Генерация ключа восстановления данных. 4. Выполнение резервного копирования (Backup) В этом разделе Вы можете в интерактивном режиме выполнять резервное копирование. Для этого нужно просто выбрать задание, которое нужно выполнить (меню данного раздела меняется автоматически исходя из числа заданий). 5. Восстановление данных (Restore) В этом разделе Вы можете просмотреть и восстановить резервные копии. * View backups list Просмотр списка резервных копий, которые находятся на backup-сервере. * Get last backup Получение самого последней резервной копии. * Get backup Получение произвольной резервной копии. Для этого Вам нужно ввести имя файла в формате YYYYMMDD-HHmmSS-BACKUP_NAME, где YYYYMMDD - дата резервной копии (например, 20061201) HHmmSS - время резервной копии (например, 145434) BACKUP_NAME - имя резервной копии (по названию задания) Получить эти имена Вы можете при восстановлении, введя вместо имени файла знак вопроса - Вам будет показан список всех резервных копий на backup-сервере и соответствующие им имена. OPTIONS (batch mode) Параметры командной строки позволяют выполнять операции по резервному копированию и восстановлению в пакетном режиме (для запуска из системного crontab и т.п.). Параметры командной строки: - openssl [ssl-file] Генерация SSL-ключа шифрования данных - store [backup-name] Выполнение резервного копирования. При указании имени задания backup-name производится выполнение этого задания. При отсутствии этого параметра выполняется основное задание (секция [backup] конфиг-файла). - restore last Восстановление последней резервной копии. - restore [backup-name] Востановление резервной копии. backup-name - имя файла резервной копии на backup-сервере. - restore list Получение списка резервных копий на backup-сервере. - help Вызов краткой справки об использовании утилиты. WARNINGS Безопасность данных, передаваемых на backup-сервер зависит только от Вашей внимательности. Поскольку при использовании данной утилиты данные на backup-сервер передаются в шифрованном виде, получить доступ к Вашим оригинальным данным возможно только при владении SSL-клоючом шифрования. Берегите данный ключ в надежном месте. Сохранность самих копий на backup-сервере обеспечивается системой резервного копирования на backup-сервере как на аппаратном, так и на программном уровнях. Доступ к Вашим данным на backup-сервере возможно ограничить с определенного числа IP адресов, сетей или доменов (это желательно сделать). Причем эти ограничения могут быть различными для операции резервного копирования и восстановления данных. SUPPORT Backup system support: support@colocall.net Billing questions: billing@colocall.net [limits] ssh.type rsa1|rsa|dsa ssh.size [512-8092] ssh.protocol auto|1|2 backup.method gzip|bzip2|none backup.compression [1-9] openssl.size [128-8092] security.log-level low|medium|high