File Coverage

blib/script/narada-install
Criterion Covered Total %
statement 146 155 94.1
branch 50 54 92.5
condition 24 27 88.8
subroutine 24 25 96.0
pod n/a
total 244 261 93.4


line stmt bran cond sub pod time code
1             #!/usr/bin/env perl
2 10     10   5151 use 5.010001;
  10         23  
3 10     10   34 use warnings;
  10         12  
  10         284  
4 10     10   34 use strict;
  10         8  
  10         201  
5 10     10   5436 use utf8;
  10         82  
  10         54  
6              
7             our $VERSION = 'v2.3.7';
8              
9 10     10   353 use FindBin;
  10         12  
  10         469  
10 10     10   4006 use lib "$FindBin::Bin/../lib/perl5";
  10         5000  
  10         60  
11 10     10   1000 use Path::Tiny;
  10         12  
  10         393  
12 10     10   2925 use Narada;
  10         15  
  10         265  
13 10     10   3033 use Narada::Lock qw( exclusive_lock unlock_new shared_lock unlock );
  10         21  
  10         56  
14 10     10   23367 use App::migrate;
  10         31887  
  10         315  
15 10     10   6180 use Getopt::Long qw( GetOptionsFromArray );
  10         71867  
  10         36  
16              
17 10     10   1408 use constant INITIAL_VERSION => '0.0.0';
  10         13  
  10         554  
18 10     10   34 use constant FULL_BACKUP => path('.backup/full.tar');
  10         12  
  10         55  
19 10     10   870 use constant USAGE => <<'EOUSAGE';
  10         13  
  10         11359  
20             Usage:
21             narada-install [--allow-downgrade|-D] [--allow-restore|-R] [-f ]
22             narada-install [-f ] --path|-p ...
23             narada-install --check|-c
24             narada-install --help|-h
25             EOUSAGE
26              
27             my $Last_backup; # OPTIMIZATION delay copying last backup to drop exclusive_lock faster
28              
29              
30             main(@ARGV) if !caller;
31              
32              
33 9     9   233 sub err { die "narada-install: @_\n" };
34              
35             sub main {
36             ## no critic (RequireCarping)
37 62     62   291858 my ($allow_downgrade, $allow_restore, @files) = (0, 0);
38 62         83 my ($is_path, $check, $is_help);
39 0         0 my ($prev_version, $next_version, $path);
40 62 100       334 GetOptionsFromArray(\@_,
41             'D|allow-downgrade' => \$allow_downgrade,
42             'R|allow-restore' => \$allow_restore,
43             'f=s@' => \@files,
44             'p|path' => \$is_path,
45             'c|check=s' => \$check,
46             'h|help' => \$is_help,
47             ) or die USAGE;
48 61 100       24428 if ($is_help) {
49 4         87 print USAGE;
50 4         15 return;
51             }
52 57 100       125 if (defined $check) {
53 12 100 100     109 die USAGE if @_ || $is_path || $allow_downgrade || $allow_restore || @files;
      100        
      100        
      100        
54 7         190 say "Checking $check";
55 7         30 App::migrate->new->load($check);
56 2         9415 return;
57             }
58 45 100       120 if ($is_path) {
59 7 100       32 die USAGE if @_ <= 2;
60 3         5 $path = [@_];
61 3         4 $prev_version = $path->[0];
62 3         3 $next_version = $path->[-1];
63 3         2 $allow_downgrade= 1;
64 3         4 $allow_restore = 1;
65             }
66             else {
67 38 100       129 die USAGE if @_ != 1;
68 34         48 $next_version = $_[0];
69 34         44 $prev_version = INITIAL_VERSION;
70 34 100       98 if (path(q{.})->children(qr/\A(?![.]release\z|[.]backup\z|[.]lock)/ms)) {
71 23         4157 Narada::detect('narada');
72 23         59 ($prev_version) = path('VERSION')->lines({chomp=>1});
73             }
74             }
75 37 100       4902 if ($next_version eq $prev_version) {
76 5         19 return;
77             }
78              
79 32         115 my $migrate = load($prev_version, $next_version, @files);
80 32   66     242 $path ||= get_path($migrate, $prev_version, $next_version);
81 32         111 check_path($migrate, $path, $allow_downgrade, $allow_restore);
82 31         87 migrate($migrate, $path);
83              
84 25         4114 return;
85             }
86              
87             sub load {
88 29     29   80 my ($prev_version, $next_version, @files) = @_;
89 29         250 my $migrate = App::migrate->new;
90 29 100       450 if ($next_version ne INITIAL_VERSION) {
91 14         52 push @files, ".release/$next_version.migrate";
92             }
93 29 100       385 if (-f ".release/$prev_version.migrate") {
94 20         62 push @files, ".release/$prev_version.migrate";
95             }
96 29         68 for (@files) {
97 51         174460 say "Loading $_";
98 51         219 $migrate->load($_);
99             }
100 29         136131 return $migrate;
101             }
102              
103             sub get_path {
104 33     33   1324 my ($migrate, $prev_version, $next_version) = @_;
105 33         143 my @paths = $migrate->find_paths($prev_version, $next_version);
106 33 100       1685 if (0 == @paths) {
    100          
107 1         4 err "Unable to find migration path from $prev_version to $next_version";
108             }
109             elsif (1 != @paths) {
110             err join "\n",
111             'Found more than one upgrade path, run one of these commands to choose a path:',
112 1         3 map {"\tnarada-install --path @{$_}"} @paths;
  2         2  
  2         9  
113             }
114 31         120 return $paths[0];
115             }
116              
117             sub check_path {
118 39     39   456 my ($migrate, $path, $allow_downgrade, $allow_restore) = @_;
119 39 100       96 if ($allow_restore) {
120 15         24 $allow_downgrade = 1;
121             }
122 39         135 for my $step ($migrate->get_steps($path)) {
123 759         1646 my $t = $step->{type};
124 759   100     1332 my $is_down = $t eq 'downgrade' || $t eq 'after_downgrade';
125 759 100 100     1470 err 'Downgrade required, use --allow-downgrade to continue'
126             if $is_down && !$allow_downgrade;
127 757 100 100     1091 err 'Restore from backup required, use --allow-restore to continue'
128             if $t eq 'RESTORE' && !$allow_restore;
129 754 100       941 if ($t eq 'RESTORE') {
130 7         48 my $f = path(".backup/full-$step->{next_version}.tar");
131 7 100       242 err "Required backup not found: $f" if !$f->is_file;
132             }
133             }
134 31         73 return;
135             }
136              
137             sub migrate {
138 28     28   59 my ($migrate, $path) = @_;
139              
140 28         401 say 'Acquire exclusive lock ...';
141 28         170 exclusive_lock();
142             {
143 28         112 local $ENV{NARADA_SKIP_LOCK} = 1;
  28         565  
144 28         105 $Last_backup = undef; # tests may call main() many times
145 28         330 $migrate->on(BACKUP => \&_backup)
146             ->on(RESTORE => \&_restore)
147             ->on(VERSION => \&_version)
148             ->on(error => \&_error)
149             ->run($path);
150             }
151 22         859 unlock_new();
152              
153 22         175 shared_lock();
154 22 50 33     293 if ($Last_backup && !FULL_BACKUP->exists) {
155 22         548 path($Last_backup)->copy(FULL_BACKUP);
156             }
157 22         13173 unlock();
158              
159 22         52 return;
160             }
161              
162             sub ask {
163 0     0     my ($msg, $match) = @_;
164 0           print $msg;
165             # TODO try IO::Prompter
166             ## no critic (ProhibitExplicitStdin)
167 0           while () {
168 0           chomp;
169 0 0         return $_ if /$match/ms;
170 0           print $msg;
171             }
172 0           die "Interrupted by EOF\n";
173             }
174              
175             sub _backup {
176 76     76   5237 my ($step) = @_;
177 76 100       356 if ($step->{version} ne INITIAL_VERSION) {
178 66 100       263 if ($Last_backup) {
179 37         179 path($Last_backup)->copy(FULL_BACKUP);
180             }
181 66         21445 my $file = ".backup/full-$step->{version}.tar";
182 66         933 say "Backuping to $file";
183 66 100       54109964 system('narada-backup') == 0 or die "BACKUP failed\n";
184 62         638 $Last_backup = $file;
185 62         2608 FULL_BACKUP->move($Last_backup);
186             }
187 72         64644 return;
188             }
189              
190             sub _restore {
191 9     9   780 my ($step) = @_;
192 9         53 my $file = ".backup/full-$step->{version}.tar";
193 9         194 say "Restoring from $file";
194 9 100       2437159 system('narada-restore', $file) == 0 or die "RESTORE failed\n";
195 6         170 return;
196             }
197              
198             sub _version {
199 72     72   3678107 my ($step) = @_;
200 72         1672 say "Migration to $step->{version} completed";
201 72 100       495 if ($step->{version} eq INITIAL_VERSION) {
202 13         571 path('VERSION')->remove;
203             }
204             else {
205 59         664 path('VERSION')->spew_utf8("$step->{version}\n");
206             }
207 72         69855 return;
208             }
209              
210             sub _error {
211 10     10   17580 say q{};
212 10         98 say 'YOU NEED TO MANUALLY FIX THIS ISSUE RIGHT NOW';
213 10         410 my $prompt
214             = "Please choose what to do:\n"
215             . " shell - run $ENV{SHELL} (exit from it to return to this menu)\n"
216             . " continue - continue migration (use if you have fixed this issue)\n"
217             . " restore - interrupt migration and RESTORE previous version from backup\n"
218             . 'Enter action [shell]: '
219             ;
220 10         37 while (1) {
221 10         208 my $ans = lc ask($prompt, qr/\A(?:shell|continue|restore|\s*)\z/msi);
222 10 100       229 if ($ans eq 'restore') {
    50          
223 6         138 die "Migration failed\n";
224             }
225             elsif ($ans eq 'continue') {
226 4         27 last;
227             }
228             else {
229 0         0 system $ENV{SHELL};
230             }
231             }
232 4         24 return;
233             }
234              
235              
236             1; # Magic true value required at end of module
237             __END__