File Coverage

blib/lib/App/Yabsm/Tools.pm
Criterion Covered Total %
statement 79 132 59.8
branch 40 74 54.0
condition 34 48 70.8
subroutine 14 25 56.0
pod 0 16 0.0
total 167 295 56.6


line stmt bran cond sub pod time code
1             # Author: Nicholas Hubbard
2             # WWW: https://github.com/NicholasBHubbard/yabsm
3             # License: MIT
4              
5             # Miscellaneous tools to aid in the development of Yabsm.
6             #
7             # See t/Tools.t for this modules tests.
8              
9 9     9   455 use strict;
  9         17  
  9         189  
10 9     9   30 use warnings;
  9         12  
  9         144  
11 9     9   59 use v5.16.3;
  9         54  
12              
13             package App::Yabsm::Tools;
14              
15 9     9   4173 use Time::Piece;
  9         88542  
  9         32  
16 9     9   3652 use Feature::Compat::Try;
  9         3283  
  9         33  
17 9     9   27474 use Carp qw(confess);
  9         18  
  9         366  
18 9     9   42 use File::Path qw(make_path);
  9         15  
  9         472  
19 9     9   47 use File::Basename qw(dirname);
  9         11  
  9         723  
20              
21 9     9   48 use Exporter qw(import);
  9         11  
  9         13537  
22             our @EXPORT_OK = qw(have_prerequisites
23             have_prerequisites_or_die
24             arg_count_or_die
25             with_error_catch_log
26             have_sudo_access_to_btrfs
27             have_sudo_access_to_btrfs_or_die
28             is_btrfs_dir
29             is_btrfs_dir_or_die
30             is_btrfs_subvolume
31             is_btrfs_subvolume_or_die
32             nums_denote_valid_date
33             nums_denote_valid_date_or_die
34             system_or_die
35             make_path_or_die
36             i_am_root
37             i_am_root_or_die
38             );
39             our %EXPORT_TAGS = ( ALL => [ @EXPORT_OK ] );
40              
41             ####################################
42             # SUBROUTINES #
43             ####################################
44              
45             sub have_prerequisites {
46              
47             # Return 1 if we are running on a Linux OS and have sudo, OpenSSH, and
48             # btrfs-progs installed.
49              
50 4 50   4 0 1326 return 0 unless $^O =~ /linux/i;
51 4 50       16614 return 0 unless 0 == system('which btrfs >/dev/null 2>&1');
52 0 0       0 return 0 unless `ssh -V 2>&1` =~ /^OpenSSH/;
53 0 0       0 return 0 unless 0 == system('which sudo >/dev/null 2>&1');
54              
55 0         0 return 1;
56             }
57              
58             sub have_prerequisites_or_die {
59              
60             # Like &have_prerequisites except die if the prerequisites are not met.
61              
62 0 0   0 0 0 unless ($^O =~ /linux/i) {
63 0         0 die "yabsm: internal error: not a Linux OS, this is a '$^O' OS\n";
64             }
65              
66 0 0       0 unless (0 == system('which btrfs >/dev/null 2>&1')) {
67 0         0 die 'yabsm: internal error: btrfs-progs not installed'."\n";
68             }
69              
70 0 0       0 unless (`ssh -V 2>&1` =~ /^OpenSSH/) {
71 0         0 die 'yabsm: internal error: OpenSSH not installed'."\n";
72             }
73              
74 0 0       0 unless (0 == system('which sudo >/dev/null 2>&1')) {
75 0         0 die 'yabsm: internal error: sudo not installed'."\n";
76             }
77              
78 0         0 return 1;
79             }
80              
81             sub arg_count_or_die {
82              
83             # Carp::Confess unless $num_args is in range $lower-$upper. If $lower equals
84             # '_' then it is assumed to be 0 and if $upper equals '_' it is assumed to
85             # be infinity.
86              
87 3356     3356 0 7176 my $lower = shift;
88 3356         2968 my $upper = shift;
89 3356         3150 my $num_args = scalar @_;
90              
91 3356 100       4727 $lower = 0 if $lower eq '_';
92              
93 3356         3281 my $lower_ok = $lower <= $num_args;
94 3356 100       4194 my $upper_ok = $upper eq '_' ? 1 : $upper >= $num_args;
95              
96 3356 100 100     6739 unless ($lower_ok && $upper_ok) {
97 4         13 my $caller = ( caller(1) )[3];
98 4         10 my $error_msg = "yabsm: internal error: called '$caller' with $num_args args but it expects";
99 4         4 my $range_msg;
100 4 100       8 if ($upper eq '_') { $range_msg = "at least $lower args" }
  1 100       2  
101 1         2 elsif ($lower == $upper) { $range_msg = "$lower args" }
102 2         3 else { $range_msg = "$lower-$upper args" }
103 4         33 confess("$error_msg $range_msg");
104             }
105              
106 3352         4072 return 1;
107             }
108              
109             sub with_error_catch_log {
110              
111             # Call $sub with @args within a Feature::Compat::Try try/catch block to catch
112             # any exception and log it to /var/log/yabsm instead of killing the program.
113              
114 0     0 0 0 my $sub = shift;
115 0         0 my @args = @_;
116              
117             try {
118             $sub->(@args);
119             }
120 0         0 catch ($e) {
121             if (-f '/var/log/yabsm' && open(my $fh, '>>', '/var/log/yabsm')) {
122             $e =~ s/^\s+|\s+$//g;
123             my $t = localtime();
124             my ($yr, $mon, $day, $hr, $min) = map { sprintf '%02d', $_ } $t->year, $t->mon, $t->mday, $t->hour, $t->min;
125             say $fh "[${yr}_${mon}_${day}_$hr:$min]: $e";
126             close $fh;
127             }
128             }
129             }
130              
131             sub have_sudo_access_to_btrfs {
132              
133             # Return 1 if we can run 'btrfs' with 'sudo -n' and return 0 otherwise.
134              
135 0     0 0 0 arg_count_or_die(0, 0, @_);
136              
137 0         0 return 0+(0 == system('sudo -n btrfs --help >/dev/null 2>&1'));
138             }
139              
140             sub have_sudo_access_to_btrfs_or_die {
141              
142             # Wrapper around have_sudo_access_to_btrfs() that Carp::Confess's if it
143             # returns false.
144              
145 0     0 0 0 arg_count_or_die(0, 0, @_);
146              
147 0         0 my $username = getpwuid $<;
148              
149 0 0       0 have_sudo_access_to_btrfs() ? return 1 : die("yabsm: internal error: no sudo access rights to 'btrfs' while running as user '$username'");
150             }
151              
152             sub is_btrfs_dir {
153              
154             # Return 1 if $dir is a directory residing on a btrfs subvolume
155             # and return 0 otherwise.
156              
157 0     0 0 0 arg_count_or_die(1, 1, @_);
158              
159 0         0 my $dir = shift;
160              
161 0 0       0 return 0 unless -d $dir;
162              
163 0         0 return 0+(0 == system("btrfs property list '$dir' >/dev/null 2>&1"));
164             }
165              
166             sub is_btrfs_dir_or_die {
167              
168             # Wrapper around is_btrfs_dir() that Carp::Confess's if it returns false.
169              
170 0     0 0 0 arg_count_or_die(1, 1, @_);
171              
172 0         0 my $dir = shift;
173              
174 0 0       0 is_btrfs_dir($dir) ? return 1 : die("yabsm: internal error: '$dir' is not a directory residing on a btrfs filesystem\n")
175             }
176              
177             sub is_btrfs_subvolume {
178              
179             # Return 1 if $dir is a btrfs subvolume on this OS and return 0
180             # otherwise.
181             #
182             # A btrfs subvolume is identified by inode number 256
183              
184 0     0 0 0 arg_count_or_die(1, 1, @_);
185              
186 0         0 my $dir = shift;
187              
188 0 0       0 return 0 unless is_btrfs_dir($dir);
189              
190 0         0 my $inode_num = (split /\s+/, `ls -di '$dir' 2>/dev/null`, 2)[0];
191              
192 0         0 return 0+(256 == $inode_num);
193             }
194              
195             sub is_btrfs_subvolume_or_die {
196              
197             # Wrapper around is_btrfs_subvolume() that Carp::Confess's if it returns
198             # false.
199              
200 0     0 0 0 arg_count_or_die(1, 1, @_);
201              
202 0         0 my $dir = shift;
203              
204 0 0       0 is_btrfs_subvolume($dir) ? return 1 : die("yabsm: internal error: '$dir' is not a btrfs subvolume")
205             }
206              
207             sub nums_denote_valid_date {
208              
209             # Return 1 if passed a year, month, month-day, hour, and minute
210             # that denote a valid date and return 0 otherwise.
211              
212 214     214 0 558 arg_count_or_die(5, 5, @_);
213              
214 214         320 my ($yr, $mon, $day, $hr, $min) = @_;
215              
216 214 100       374 return 0 unless $yr >= 1;
217 212 100 66     452 return 0 unless $mon >= 1 && $mon <= 12;
218 210 100 66     444 return 0 unless $hr >= 0 && $hr <= 23;
219 208 100 66     436 return 0 unless $min >= 0 && $min <= 59;
220              
221             # month days are a bit more complicated to figure out
222              
223 207 100 33     768 if ($mon == 1 || $mon == 3 || $mon == 5 || $mon == 7 || $mon == 8 || $mon == 10 || $mon == 12) {
    100 66        
      66        
      66        
      66        
      100        
      66        
      100        
      66        
224 200 100 66     432 return 0 unless $day >= 1 && $day <= 31;
225             }
226             elsif ($mon == 4 || $mon == 6 || $mon == 9 || $mon == 11) {
227 3 100 66     15 return 0 unless $day >= 1 && $day <= 30;
228             }
229             else { # February
230 4         6 my $is_leap_yr;
231              
232 4 50       16 if ($yr % 400 == 0) { $is_leap_yr = 1 }
  0 50       0  
    100          
233 0         0 elsif ($yr % 100 == 0) { $is_leap_yr = 0 }
234 2         3 elsif ($yr % 4 == 0) { $is_leap_yr = 1 }
235 2         3 else { $is_leap_yr = 0 }
236              
237 4 100       8 my $upper = $is_leap_yr ? 29 : 28;
238              
239 4 100 66     22 return 0 unless $day >= 1 && $day <= $upper;
240             }
241              
242 202         353 return 1;
243             }
244              
245             sub nums_denote_valid_date_or_die {
246              
247             # Wrapper around &nums_denote_valid_date that Carp::Confess's if it
248             # returns false.
249              
250 39     39 0 478 arg_count_or_die(5, 5, @_);
251              
252 39 100       57 unless ( nums_denote_valid_date(@_) ) {
253 2         5 my ($yr, $mon, $day, $hr, $min) = @_;
254 2         19 confess("yabsm: internal error: '${yr}_${mon}_${day}_$hr:$min' does not denote a valid yr_mon_day_hr:min date");
255             }
256              
257 37         50 return 1;
258             }
259              
260             sub system_or_die {
261              
262             # Wrapper around system that Carp::Confess's if the system command exits
263             # with a non-zero status. Redirects STDOUT and STDERR to /dev/null.
264              
265 2     2 0 2060 open my $NULLFD, '>', '/dev/null';
266 2         40 open my $OLD_STDOUT, '>&', STDOUT;
267 2         35 open my $OLD_STDERR, '>&', STDERR;
268 2         47 open STDOUT, '>&', $NULLFD;
269 2         39 open STDERR, '>&', $NULLFD;
270              
271 2         6530 my $status = system @_;
272              
273 2         131 open STDOUT, '>&', $OLD_STDOUT;
274 2         41 open STDERR, '>&', $OLD_STDERR;
275 2         25 close $NULLFD;
276 2         19 close $OLD_STDOUT;
277 2         17 close $OLD_STDERR;
278              
279 2 100       34 unless (0 == $status) {
280 1         94 confess("yabsm: internal error: system command '@_' exited with non-zero status '$status'");
281             }
282              
283 1         120 return 1;
284             }
285              
286             sub make_path_or_die {
287              
288             # Wrapper around File::Path::make_path() that Carp::Confess's if the path
289             # cannot be created. The UID and GID of the $path will be set to that of the
290             # deepest existing sub-directory in $path.
291              
292 0     0 0   my $path = shift;
293              
294 0 0         $path =~ /^\//
295             or die "yabsm: internal error: '$path' is not an absolute path starting with '/'";
296              
297 0           my $dir = $path;
298              
299 0           until (-d $dir) {
300 0           $dir = dirname($dir);
301             }
302              
303 0           my ($uid, $gid) = (stat $dir)[4,5];
304              
305 0 0         -d $path and return 1;
306              
307 0 0         make_path($path, {uid => $uid, group => $gid}) and return 1;
308              
309 0           my $username = getpwuid $<;
310              
311 0           die "yabsm: error: could not create path '$path' while running as user '$username'\n";
312             }
313              
314             sub i_am_root {
315              
316             # Return 1 if current user is root and return 0 otherwise.
317              
318 0     0 0   return 0+(0 == $<);
319             }
320              
321             sub i_am_root_or_die {
322              
323             # Die unless running as the root user.
324              
325 0     0 0   arg_count_or_die(0, 0, @_);
326              
327 0 0         unless (i_am_root()) {
328 0           my $username = getpwuid $<;
329 0           confess("yabsm: internal error: not running as root - running as '$username'");
330             }
331              
332 0           return 1;
333             }
334              
335             1;