diff options
author | Sergey Poznyakoff <gray@gnu.org.ua> | 2017-03-10 08:34:08 +0200 |
---|---|---|
committer | Sergey Poznyakoff <gray@gnu.org.ua> | 2017-03-10 09:13:14 +0200 |
commit | c2c72e539e88157f4676bbb185c56e46a1b6d6b4 (patch) | |
tree | bb9b260536b3568da659b97a4e42ced5a3bff101 | |
parent | 05aa806b33f91172e0eba0cee4a28ccfec204d3a (diff) | |
download | beam-c2c72e539e88157f4676bbb185c56e46a1b6d6b4.tar.gz beam-c2c72e539e88157f4676bbb185c56e46a1b6d6b4.tar.bz2 |
Initial implementation of the restore subcommand
* lib/App/Beam/History.pm: New file.
* lib/App/Beam/History/Entry.pm: New file.
* lib/App/Beam/History/Record.pm: New file.
* lib/App/Beam.pm (lock,unlock)
(compute_triplet,status_result)
(set_result): Remove. Use App::Beam::History instead.
(begin): Create a History object.
(end): Save the History object.
(check_items): New method.
* lib/App/Beam/Backend.pm (status): Remove.
(record, restore): New method.
* lib/App/Beam/Restore.pm: New file.
* beam: Implement restore subcommand.
* lib/App/Beam/Backend/Tar.pm: Likewise.
* MANIFEST: Update.
* lib/App/Beam/Backup.pm: Update.
* lib/App/Beam/List.pm: Update.
-rw-r--r-- | MANIFEST | 4 | ||||
-rwxr-xr-x | beam | 4 | ||||
-rw-r--r-- | lib/App/Beam.pm | 166 | ||||
-rw-r--r-- | lib/App/Beam/Backend.pm | 14 | ||||
-rw-r--r-- | lib/App/Beam/Backend/Tar.pm | 81 | ||||
-rw-r--r-- | lib/App/Beam/Backup.pm | 15 | ||||
-rw-r--r-- | lib/App/Beam/History.pm | 76 | ||||
-rw-r--r-- | lib/App/Beam/History/Entry.pm | 62 | ||||
-rw-r--r-- | lib/App/Beam/History/Record.pm | 158 | ||||
-rw-r--r-- | lib/App/Beam/List.pm | 43 | ||||
-rw-r--r-- | lib/App/Beam/Restore.pm | 85 |
11 files changed, 536 insertions, 172 deletions
@@ -10,6 +10,10 @@ lib/App/Beam/Backend.pm lib/App/Beam/Backend/Tar.pm lib/App/Beam/Command.pm lib/App/Beam/List.pm +lib/App/Beam/History.pm +lib/App/Beam/Restore.pm +lib/App/Beam/History/Entry.pm +lib/App/Beam/History/Record.pm t/TestConfig.pm t/conf01.t t/conf02.t @@ -76,6 +76,10 @@ my %ctab = ( use App::Beam::Backup; return new App::Beam::Backup(@_); }, + restore => sub { + use App::Beam::Restore; + return new App::Beam::Restore(@_); + }, list => sub { use App::Beam::List; return new App::Beam::List(@_); diff --git a/lib/App/Beam.pm b/lib/App/Beam.pm index b7e004e..3bee8b9 100644 --- a/lib/App/Beam.pm +++ b/lib/App/Beam.pm @@ -5,16 +5,12 @@ use Carp; use File::Basename; use Sys::Syslog; use Unix::Sysexits; -use Fcntl qw(:flock SEEK_SET); -use Storable qw(fd_retrieve nstore_fd); use POSIX qw(strftime floor); require App::Beam::Config; our @ISA = qw(App::Beam::Config); -our @EXPORT_OK = qw(RESULT_PENDING RESULT_SUCCESS RESULT_FAILURE); -our %EXPORT_TAGS = ( - result => [ qw(RESULT_PENDING RESULT_SUCCESS RESULT_FAILURE) ] -); + +use App::Beam::History; my $default_config_file = '/etc/beam.conf'; @@ -255,130 +251,16 @@ sub abend { exit($code); } -use constant { - RESULT_PENDING => 0, - RESULT_SUCCESS => 1, - RESULT_FAILURE => 2 -}; - -# Locks the statfile and retrieves data from it into {status} -sub lock { - my $self = shift; - my $file = $self->get('core.statfile'); - unless (open($self->{statfd}, '+>>', $file)) { - $self->abend(EX_CANTCREAT, "can't open file $file: $!"); - } - unless (flock($self->{statfd}, LOCK_EX | LOCK_NB)) { - $self->abend(EX_TEMPFAIL, "can't lock file $file: $!"); - } - seek($self->{statfd}, 0, SEEK_SET); - if ((stat($self->{statfd}))[7] == 0) { - $self->{status} = []; - } else { - $self->{status} = fd_retrieve($self->{statfd}) - or $self->abend(EX_UNAVAILABLE, "can't retrieve status from $file: $!"); - if (ref($self->{status}) ne 'ARRAY') { - $self->abend(EX_DATAERR, "$file: malformed status file"); - } - } - my ($cycle, $round, $level) = $self->compute_triplet(1); - unshift @{$self->{status}}, { timestamp => time(), - result => RESULT_PENDING, - cycle => $cycle, - round => $round, - level => $level }; -} - -# Saves the current status and unlocks the status file. -sub unlock { - my ($self) = @_; - croak "unlock without lock" unless defined $self->{statfd}; - $self->debug(1, "saving state: ".$self->status('result')); - if (!$self->{dry_run} && $self->status('result') != RESULT_PENDING) { - seek($self->{statfd}, 0, SEEK_SET); - truncate($self->{statfd}, 0); - nstore_fd($self->{status}, $self->{statfd}); - } - flock($self->{statfd}, LOCK_UN); - close($self->{statfd}); -} - -sub compute_triplet { - my ($self, $off, $base) = @_; - - $base = 0 unless defined $base; - if ($#{$self->{status}} == -1) { - return (0, 0, 0); - } - croak "invalid base" unless $base <= $#{$self->{status}}; - - my ($cycle, $round, $level) = - (${$self->{status}}[$base]{cycle}, - ${$self->{status}}[$base]{round}, - ${$self->{status}}[$base]{level}); - - my ($maxround, $maxlevel) = - ($self->get('schedule', 'rounds'), - $self->get('schedule', 'levels')); - if ($off == 1) { - $level++; - if ($level > $maxlevel) { - $round++; - $level = 0; - if ($round > $maxround) { - $cycle++; - $round = 0; - } - } - } elsif ($off == -1) { - $level--; - if ($level < 0) { - $level += $maxlevel + 1; - $round--; - } - if ($round < 0) { - $round += $maxround + 1; - $cycle--; - if ($cycle == 0) { - $round = $level = 0; - last; - } - } - } elsif ($off) { - croak "invalid offset"; - } - return ($cycle, $round, $level); -} - -sub status { - my ($self,$item,$base) = @_; - $base = 0 unless defined $base; - return undef unless $base <= $#{$self->{status}}; - return $self->{status}[$base]{$item}; -} - -sub result { - my ($self) = @_; - return $self->{status}[0]{result}; -} - -sub set_result { - my ($self, $item, $result) = @_; - $self->{status}[0]{detail}{$item} = $result; - $self->{status}[0]{result} = $result - unless $self->{status}[0]{result} == RESULT_FAILURE; -} - sub format_name { - my ($self, $name, $base) = @_; - $base = 0 unless defined $base; - return undef unless $base <= $#{$self->{status}}; + my ($self, $name, $idx) = @_; + my $rec = $self->{history}->top($idx); + return undef unless defined $rec; $name .= '-' - . $self->status('cycle', $base) + . $rec->cycle . '-' - . $self->status('round', $base) + . $rec->round . '-' - . $self->status('level', $base); + . $rec->level; return $name; } @@ -458,11 +340,27 @@ sub begin { } $self->logger('info', 'startup'); - $self->load_backends; + $self->load_backends; + + eval { + my %args; + $args{dry_run} = $self->{dry_run}; + $self->{history} = + new App::Beam::History($self->get('core.statfile'), + $self->get('schedule', 'rounds'), + $self->get('schedule', 'levels'), + %args); + }; + if ($@) { + $self->logger('crit', $@); + exit(EX_CANTCREAT); + } } sub end { my $self = shift; + $self->{history}->top->finish; + $self->{history}->save; $self->logger('info', 'shutdown'); if ($self->get('logger.channel') eq 'syslog') { closelog(); @@ -474,4 +372,18 @@ sub run { $self->abend(EX_SOFTWARE, "unsupported command $_[0]"); } +sub check_items { + my $self = shift; + my @items = split /\s+/, $self->get('core.items'); + if (@_) { + push @items, $self->names_of('item'); + foreach my $item (@_) { + $self->abend(EX_USAGE, "$item: no such item defined") + unless grep { $item eq $_ } @items; + } + @items = @_; + } + return @items; +} + 1; diff --git a/lib/App/Beam/Backend.pm b/lib/App/Beam/Backend.pm index c0b8ee3..41036e6 100644 --- a/lib/App/Beam/Backend.pm +++ b/lib/App/Beam/Backend.pm @@ -5,6 +5,7 @@ use Carp; require Exporter; our @ISA = qw(Exporter); +use App::Beam; use App::Beam::Command qw(:channels); sub new { @@ -48,9 +49,9 @@ sub isset { return $self->{beam}->isset(@_); } -sub status { - my $self = shift; - return $self->{beam}->status(@_); +sub record { + my ($self,$idx) = @_; + return $self->{beam}{history}->top($idx); } sub logcommand { @@ -92,4 +93,11 @@ sub backup { return => '-locus'})); } +sub restore { + my ($self, $index, $item) = @_; + $self->error("restore method not implemented", + locus => $self->get({variable => [ 'item', $item ], + return => '-locus'})); +} + 1; diff --git a/lib/App/Beam/Backend/Tar.pm b/lib/App/Beam/Backend/Tar.pm index 9035427..bbb4730 100644 --- a/lib/App/Beam/Backend/Tar.pm +++ b/lib/App/Beam/Backend/Tar.pm @@ -8,8 +8,9 @@ use Data::Dumper; our @ISA = qw(App::Beam::Backend); use POSIX qw(strftime); -use App::Beam qw(:result); +use App::Beam; use App::Beam::Command qw(:channels); +use App::Beam::History::Entry qw(:state); sub ck_dir { my ($v) = @_; @@ -102,7 +103,7 @@ sub snapshot_name { sub mksnapshot { my ($self, $item) = @_; my $snapshot = $self->snapshot_name($item); - if ($self->status('level') != 0) { + if ($self->record->level != 0) { my $prev = $self->snapshot_name($item, 1); if ($prev) { $self->debug(1, "cp $prev $snapshot"); @@ -151,14 +152,82 @@ sub backup { } $self->debug(1, "running ".$cmd->command_line); + my $ret = STATE_SUCCESS; unless ($self->dry_run) { $cmd->run; $self->logcommand($cmd); - my $ret = $cmd->exit_code; - $self->{beam}->set_result($item, - ($ret == 0 || $ret == 2) - ? RESULT_SUCCESS : RESULT_FAILURE); + my $rc = $cmd->exit_code; + $ret = STATE_FAILURE unless ($rc == 0 || $rc == 2); } + return $ret; } + +sub find_full_backup { + my ($self, $index, $item) = @_; + + my $rec = $self->record($index); + my $cycle = $rec->cycle; + my $i; + for ($i = $index; $rec = $self->record($i); $i++) { + unless ($rec->cycle == $cycle) { + $self->logger('err', "can't find full backup of $item starting from $index"); + last; + } + unless (defined($rec->state($item))) { + $self->logger('err', "no item $item in backup $i"); + last; + } + return $i if $rec->level == 0; + } + return undef; +} + +sub restore_from { + my ($self, $index, $item) = @_; + + print "restore $item from $index\n"; + my $basename = $self->{beam}->format_name($item, $index); + croak "undefined basename" unless defined $basename; + my $archive = $self->get('core', 'archivedir') + . '/' + . $basename + . '.' + . $self->get('backend', 'tar', 'suffix'); + + my $cmd = new App::Beam::Command($self->get('backend', 'tar', 'binary')); + $cmd->set_logger(CHAN_STDERR, sub { $self->logger('err', $_) }); + if ($self->isset('backend', 'tar', 'options')) { + $cmd->add($self->get('backend', 'tar', 'options')); + } + if ($self->isset('item', $item, 'options')) { + $cmd->add($self->get('item', $item, 'options')); + } + $cmd->add('-x'); + $cmd->add('-f', $archive); + $cmd->add('-C', $self->get('item', $item, 'directory')); + $cmd->add('--listed', '/dev/null'); + + $self->debug(1, "running ".$cmd->command_line); + my $ret = STATE_SUCCESS; + unless ($self->dry_run) { + $cmd->run; + $self->logcommand($cmd); + my $rc = $cmd->exit_code; + $ret = STATE_FAILURE unless ($rc == 0 || $rc == 2); + } + return $ret; +} + +sub restore { + my ($self, $index, $item) = @_; + my $i = $self->find_full_backup($index, $item); + return unless defined $i; + + while ($i >= $index) { + $self->restore_from($i, $item); + $i--; + } +} + 1; diff --git a/lib/App/Beam/Backup.pm b/lib/App/Beam/Backup.pm index ed8953b..c2da254 100644 --- a/lib/App/Beam/Backup.pm +++ b/lib/App/Beam/Backup.pm @@ -42,22 +42,13 @@ sub run { my $self = shift; #GetOptionsFromArray(\@_, ...); - my @items = split /\s+/, $self->get('core.items'); - if (@_) { - push @items, $self->names_of('item'); - foreach my $item (@_) { - $self->abend(EX_USAGE, "$item: no such item defined") - unless grep { $item eq $_ } @items; - } - @items = @_; - } + my @items = $self->check_items(@_); - $self->lock(); foreach my $item (@items) { my $backend = $self->{backend}{$self->get("item.$item.backend")}; - $backend->backup($item); + $self->{history}->top->begin_entry($item); + $self->{history}->top->finish_entry($item, $backend->backup($item)); } - $self->unlock(); } 1; diff --git a/lib/App/Beam/History.pm b/lib/App/Beam/History.pm new file mode 100644 index 0000000..0421c3e --- /dev/null +++ b/lib/App/Beam/History.pm @@ -0,0 +1,76 @@ +package App::Beam::History; + +use strict; +use Carp; +use App::Beam::History::Record; +use App::Beam::History::Entry qw(:state); + +require Exporter; +our @ISA = qw(Exporter); + +use Fcntl qw(:flock SEEK_SET); +use Storable qw(fd_retrieve nstore_fd); + +sub new { + my $class = shift; + my $filename = shift; + my $rounds = shift; + my $levels = shift; + + my $self = bless { filename => $filename }, $class; + + my $v; + local %_ = @_; + if (defined($v = delete($_{dry_run}))) { + $self->{dry_run} = $v; + } + + if (keys(%_)) { + croak "unknown or unhandled parameters: ".join(',', keys(%_)); + } + + unless (open($self->{filehandle}, '+>>', $filename)) { + croak "can't open file $filename: $!"; + } + unless (flock($self->{filehandle}, LOCK_EX | LOCK_NB)) { + croak "can't lock file $filename: $!"; + } + seek($self->{filehandle}, 0, SEEK_SET); + if ((stat($self->{filehandle}))[7] == 0) { + $self->{history} = []; + unshift @{$self->{history}}, new App::Beam::History::Record; + } else { + $self->{history} = fd_retrieve($self->{filehandle}) + or croak "can't retrieve status from $self->{filehandle}: $!"; + if (ref($self->{history}) ne 'ARRAY') { + croak "$self->{filename}: malformed status file"; + } + unshift @{$self->{history}}, + new App::Beam::History::Record(prev => $self->{history}[0], + rounds => $rounds, + levels => $levels); + } + return $self; +} + +sub records { + my $self = shift; + return @{$self->{history}}; +} + +sub top { + my ($self, $idx) = @_; + return $self->{history}[$idx]; +} + +sub save { + my $self = shift; + if (!$self->{dry_run} && $self->top->state != STATE_PENDING) { + seek($self->{filehandle}, 0, SEEK_SET); + truncate($self->{filehandle}, 0); + nstore_fd($self->{history}, $self->{filehandle}); + } + flock($self->{filehandle}, LOCK_UN); + close($self->{filehandle}); +} + diff --git a/lib/App/Beam/History/Entry.pm b/lib/App/Beam/History/Entry.pm new file mode 100644 index 0000000..8443f3f --- /dev/null +++ b/lib/App/Beam/History/Entry.pm @@ -0,0 +1,62 @@ +package App::Beam::History::Entry; + +use strict; +use Carp; + +require Exporter; +our @ISA = qw(Exporter); +our @EXPORT_OK = qw(STATE_PENDING STATE_SUCCESS STATE_FAILURE); +our %EXPORT_TAGS = ( + state => [ qw(STATE_PENDING STATE_SUCCESS STATE_FAILURE) ] +); + +use constant { + STATE_PENDING => 0, + STATE_SUCCESS => 1, + STATE_FAILURE => 2 +}; + +my @state_str = ( + 'PENDING', + 'SUCCESS', + 'FAILURE' +); + +sub new { + my $class = shift; + return bless { start_time => time(), + end_time => time(), + state => STATE_PENDING }, $class; +} + +sub state { + my ($self, $value) = @_; + if (defined($value)) { + croak "can't modify finished entry" + unless $self->{state} == STATE_PENDING; + $self->{state} = $value; + $self->{end_time} = time(); + } + return $self->{state}; +} + +sub state_name { + my ($self) = @_; + return $state_str[$self->state]; +} + +sub start_time { + my ($self) = @_; + return $self->{start_time}; +} + +sub end_time { + my ($self) = @_; + return $self->{end_time}; +} + +1; + + + + diff --git a/lib/App/Beam/History/Record.pm b/lib/App/Beam/History/Record.pm new file mode 100644 index 0000000..6ec2bd3 --- /dev/null +++ b/lib/App/Beam/History/Record.pm @@ -0,0 +1,158 @@ +package App::Beam::History::Record; + +use strict; +use Carp; +use App::Beam::History::Entry qw(:state); + +our @ISA = qw(App::Beam::History::Entry); + +sub new { + my $class = shift; + my $self = $class->SUPER::new; + $self->{detail} = {}; + + local %_ = @_; + my $v; + + if (defined($v = delete($_{prev}))) { + my ($maxround, $maxlevel); + unless (defined($maxround = delete($_{rounds}))) { + croak "'prev' requires parameter 'rounds', which is not given"; + } + unless (defined($maxlevel = delete($_{levels}))) { + croak "'prev' requires parameter 'levels', which is not given"; + } +# print "PREV ".$v->level."\n"; + ($self->{level}, $self->{round}, $self->{cycle}) = + ($v->level, $v->round, $v->cycle); + $self->{level}++; + if ($self->{level} > $maxlevel) { + $self->{level} = 0; + $self->{round}++; + if ($self->{round} > $maxround) { + $self->{round} = 0; + $self->{cycle}++; + } + } + } else { + if (defined($v = delete($_{cycle}))) { + $self->{cycle} = $v; + } else { + $self->{cycle} = 0; + } + + if (defined($v = delete($_{round}))) { + $self->{round} = $v; + } else { + $self->{round} = 0; + } + + if (defined($v = delete($_{level}))) { + $self->{level} = $v; + } else { + $self->{level} = 0; + } + } + + if (keys(%_)) { + croak "unknown or unhandled parameters: ".join(',', keys(%_)); + } + + return $self; +} + +sub cycle { + my ($self) = @_; + return $self->{cycle}; +} + +sub round { + my ($self) = @_; + return $self->{round}; +} + +sub level { + my ($self) = @_; + return $self->{level}; +} + +sub state { + my ($self, $item) = @_; + if (defined($item)) { + return undef unless exists $self->{detail}{$item}; + return $self->{detail}{$item}->state; + } + return $self->{state}; +} + +sub start_time { + my ($self, $item) = @_; + if (defined($item)) { + return undef unless exists $self->{detail}{$item}; + return $self->{detail}{$item}->start_time; + } + return $self->SUPER::start_time; +} + +sub end_time { + my ($self, $item) = @_; + if (defined($item)) { + return undef unless exists $self->{detail}{$item}; + return $self->{detail}{$item}->end_time; + } + return $self->SUPER::end_time; +} + +sub begin_entry { + my ($self, $name) = @_; + croak "can't add entry to a finished record" + unless $self->{state} == STATE_PENDING; + my $entry = new App::Beam::History::Entry; + $self->{detail}{$name} = $entry; + if ($entry->start_time > $self->{end_time}) { + $self->{end_time} = $entry->start_time; + } +} + +sub entries { + my ($self) = @_; + return %{$self->{detail}}; +} + +sub entry_names { + my ($self) = @_; + return keys %{$self->{detail}}; +} + +sub finish_entry { + my ($self, $name, $state) = @_; + croak "can't modify item in a finished record" + unless $self->{state} == STATE_PENDING; + croak "no such entry" + unless exists $self->{detail}{$name}; + my $entry = $self->{detail}{$name}; + $entry->state($state); + if ($entry->end_time > $self->{end_time}) { + $self->{end_time} = $entry->end_time; + } +} + +sub finish { + my ($self) = @_; + croak "can't finish a finished record" + unless $self->{state} == STATE_PENDING; + my @x = map {$_->state} values %{$self->{detail}}; + if (@x) { + $self->{state} = (grep { $_ != STATE_SUCCESS } @x) + ? STATE_FAILURE + : STATE_SUCCESS; + } + return $self->{state}; +} + +1; + + + + + diff --git a/lib/App/Beam/List.pm b/lib/App/Beam/List.pm index b786e3f..018f7e3 100644 --- a/lib/App/Beam/List.pm +++ b/lib/App/Beam/List.pm @@ -5,17 +5,13 @@ use Carp; use POSIX qw(strftime); use Unix::Sysexits; use Getopt::Long qw(GetOptionsFromArray); -use Pod::Usage; -use Pod::Man; -use App::Beam qw(:result); +use App::Beam; our @ISA = qw(App::Beam); -my %result_string = ( - RESULT_PENDING() => 'PENDING', - RESULT_SUCCESS() => 'SUCCESS', - RESULT_FAILURE() => 'FAILURE' -); +use App::Beam::History; +use App::Beam::History::Record; +use App::Beam::History::Entry; =head1 NAME @@ -71,36 +67,35 @@ sub run { my $verbose; GetOptionsFromArray(\@_, - "verbose|v" => \$verbose); + "verbose|v" => \$verbose) + or exit(EX_USAGE); $self->abend(EX_USAGE, "bad number of arguments") if @_; - $self->lock(); - my @stat = @{$self->{status}}; - shift @stat; + my @records = $self->{history}->records; + shift @records; my $i = 0; - foreach my $ent (@stat) { + foreach my $rec (@records) { ++$i; printf("%3d ", $i); - print strftime "%c ", localtime $ent->{timestamp}; - printf("% 3d% 3d% 3d %s", $ent->{cycle}, $ent->{round}, $ent->{level}, - $result_string{$ent->{result}}); + print strftime "%c ", localtime $rec->start_time; + print strftime "- %c ", localtime $rec->end_time; + printf("% 3d% 3d% 3d %s", $rec->cycle, $rec->round, $rec->level, + $rec->state_name); print "\n"; if ($verbose) { - my %detail; - @detail{keys %{$ent->{detail}}} = values %{$ent->{detail}}; + my %ent = $rec->entries; foreach my $item (split /\s+/, $self->get('core.items')) { - if (exists($detail{$item})) { - printf("# %-24s %s\n", $item, $result_string{$detail{$item}}); - delete $detail{$item}; + if (exists($ent{$item})) { + printf("# %-24s %s\n", $item, $ent{$item}->state_name); + delete $ent{$item}; } } - foreach my $item (sort keys %detail) { - printf("# %-24s %s\n", $item, $result_string{$detail{$item}}); + foreach my $item (sort keys %ent) { + printf("# %-24s %s\n", $item, $ent{$item}->state_name); } } } - $self->unlock(); } 1; diff --git a/lib/App/Beam/Restore.pm b/lib/App/Beam/Restore.pm new file mode 100644 index 0000000..7e3129a --- /dev/null +++ b/lib/App/Beam/Restore.pm @@ -0,0 +1,85 @@ +package App::Beam::Restore; + +use strict; +use Carp; + +require App::Beam; +our @ISA = qw(App::Beam); + +use Unix::Sysexits; +use App::Beam; +use App::Beam::History::Entry qw(:state); +use Getopt::Long qw(GetOptionsFromArray); + +=head1 NAME + +beam restore - restore from a backup + +=head1 SYNOPSIS + +B<beam> [I<OPTIONS>] B<restore> [I<ITEM>...] + +=head1 DESCRIPTION + +To be written... + +=cut + +sub find_index { + my ($self, $cycle, $round, $level) = @_; + my @comp; + push @comp, sub { $_->cycle == $cycle } if defined $cycle; + push @comp, sub { $_->round == $round } if defined $round; + push @comp, sub { $_->level == $level } if defined $level; + + my $index = 1; + return $index unless @comp; + OUTER: + for (; my $rec = $self->{history}->top($index); $index++) { + local $_ = $rec; + foreach my $c (@comp) { + next OUTER unless &{$c}; + } + return $index; + } + return undef; +} + +sub run { + my $self = shift; + + my ($index, $cycle, $round, $level); + my $force; + GetOptionsFromArray(\@_, + "number|N=n" => \$index, + "level|l=n" => \$level, + "round|r=n" => \$round, + "cycle|c=n" => \$cycle, + "force|f" => \$force + ) or exit(EX_USAGE); + my @items = $self->check_items(@_); + + if (defined($index)) { + $self->abend(EX_USAGE, + "--index can't be used together with --cycle, --round, or --level") + if defined($cycle) || defined($round) || defined($level); + } else { + $index = $self->find_index($cycle, $round, $level); + $self->abend(EX_USAGE, "matching backup not found") + unless defined $index; + } + + $self->debug(1, "restoring from backup $index"); + $self->abend(EX_UNAVAILABLE, "backup $index was not successful") + unless $self->{history}->top($index)->state == STATE_SUCCESS; + + foreach my $item (@items) { + my $backend = $self->{backend}{$self->get("item.$item.backend")}; + $backend->restore($index, $item); + } +} + +1; + + + |