File Coverage

blib/lib/Yars/Tools.pm
Criterion Covered Total %
statement 248 308 80.5
branch 56 100 56.0
condition 22 44 50.0
subroutine 48 54 88.8
pod 27 27 100.0
total 401 533 75.2


line stmt bran cond sub pod time code
1             package Yars::Tools;
2              
3 24     24   11166 use strict;
  24         57  
  24         626  
4 24     24   114 use warnings;
  24         46  
  24         581  
5 24     24   190 use Clustericious::Config;
  24         47  
  24         572  
6 24     24   116 use List::Util qw/shuffle/;
  24         57  
  24         1439  
7 24     24   596 use List::MoreUtils qw/uniq/;
  24         6523  
  24         172  
8 24     24   13249 use Hash::MoreUtils qw/safe_reverse/;
  24         33827  
  24         1286  
9 24     24   517 use Clustericious::Log;
  24         5560  
  24         242  
10 24     24   19218 use File::Find::Rule;
  24         139171  
  24         195  
11 24     24   1281 use File::Basename qw/dirname/;
  24         53  
  24         1129  
12 24     24   144 use File::Path qw/mkpath/;
  24         53  
  24         935  
13 24     24   127 use File::Temp;
  24         52  
  24         1469  
14 24     24   1066 use File::Compare;
  24         27458  
  24         1045  
15 24     24   165 use JSON::MaybeXS ();
  24         49  
  24         381  
16             # TODO: rm dep on stat
17 24     24   470 use File::stat qw/stat/;
  24         4971  
  24         186  
18 24     24   1461 use Mojo::ByteStream qw/b/;
  24         51  
  24         890  
19 24     24   128 use File::HomeDir;
  24         51  
  24         908  
20 24     24   124 use File::Spec;
  24         51  
  24         431  
21 24     24   683 use Mojo::UserAgent;
  24         149632  
  24         2523  
22 24     24   605 use File::Spec;
  24         49  
  24         473  
23 24     24   476 use Yars::Util qw( format_tx_error );
  24         307  
  24         23621  
24              
25             # ABSTRACT: various utility functions dealing with servers, hosts, etc
26             our $VERSION = '1.28'; # VERSION
27              
28              
29             sub new
30             {
31 32     32 1 492 my($class, $config) = @_;
32 32 100       86 WARN "No url found in config file" unless eval { $config->url };
  32         127  
33 32         693 my $self = bless {
34             bucket_to_url => { }, # map buckets to server urls
35             bucket_to_root => { }, # map buckets to disk roots
36             disk_is_local => { }, # our disk roots (values are just 1)
37             servers => { }, # all servers
38             our_url => '', # our server url
39             state_file => '', # name of file with disk states
40             ua => '', # UserAgent
41             server_status_cache => {},
42             server_status_cache_lifetime => 3,
43             default_dir => '',
44             }, $class;
45 32         172 $self->refresh_config($config);
46 32         124 $self;
47             }
48              
49             sub _set_ua
50             {
51 14     14   37 my($self, $ua) = @_;
52 14         35 $self->{ua} = $ua;
53 14         139 return;
54             }
55              
56             sub _ua
57             {
58 278     278   684 my($self) = @_;
59 278 100       1403 my $ua = $self->{ua} ? $self->{ua}->() : Mojo::UserAgent->new;
60 278         520923 $ua->max_redirects(30);
61 278         3232 $ua;
62             }
63              
64              
65             sub refresh_config {
66 1110     1110 1 14013 my $self = shift;
67 1110         2331 my $config = shift;
68 1110 100 66     4597 return 1 if defined($self->{our_url}) && keys %{ $self->{bucket_to_root} } > 0 && keys %{ $self->{bucket_to_url} } > 0;
  1110   66     7190  
  1076         6491  
69 34   66     139 $config ||= Clustericious::Config->new("Yars");
70 34 50 66     380 $self->{our_url} ||= $config->url or WARN "No url found in config file";
71 34         627 TRACE "Our url is " . $self->{our_url};
72 34         22160 for my $server ($config->servers) {
73 59         1350 $self->{servers}->{$server->{url}} = 1;
74 59         101 for my $disk (@{ $server->{disks} }) {
  59         152  
75 102         161 for my $bucket (@{ $disk->{buckets} }) {
  102         231  
76 528         1154 $self->{bucket_to_url}->{$bucket} = $server->{url};
77 528 100       1384 next unless $server->{url} eq $self->{our_url};
78 352         662 $self->{bucket_to_root}->{$bucket} = $disk->{root};
79 352 50       729 LOGDIE "Disk root not given" unless defined($disk->{root});
80 352         664 $self->{disk_is_local}->{$disk->{root}} = 1;
81             }
82             }
83             }
84 34         332 my $default_dir = $self->{default_dir} = File::HomeDir->my_home . "/var/run/yars";
85            
86 34         1486 my $state_file = $self->{state_file} = $config->state_file(default => "$default_dir/state.txt");
87 34 100       2010 -e $state_file or do {
88 32         242 INFO "Writing new state file ($state_file)";
89 32         30873 my %disks = map { ($_ => "up") } keys %{ $self->{disk_is_local} };
  60         196  
  32         139  
90 32         231 $self->_write_state({disks => \%disks});
91             };
92 34 50       1686 -e $state_file or LOGDIE "Could not write state file $state_file";
93             #TRACE "bucket2url : ".Dumper($self->{bucket_to_url});
94             }
95              
96             sub _dir_is_empty {
97             # stolen from File::Find::Rule::DirectoryEmpty
98 341     341   673 my $dir = shift;
99 341 50       6927 opendir( DIR, $dir ) or return;
100 341         3652 while ( $_ = readdir DIR ) {
101 669 100       3289 if ( !/^\.\.?$/ ) {
102 13         69 closedir DIR;
103 13         128 return 0;
104             }
105             }
106 328         1448 closedir DIR;
107 328         951 return 1;
108             }
109              
110              
111             sub disk_for {
112 940     940 1 2236 my $self = shift;
113 940         1896 my $digest = shift;
114 940 50       1745 unless (keys %{ $self->{bucket_to_root} }) {
  940         3540  
115 0         0 $self->refresh_config;
116 0 0       0 LOGDIE "No config data" unless keys %{ $self->{bucket_to_root} } > 0;
  0         0  
117             }
118 940         1801 my ($bucket) = grep { $digest =~ /^$_/i } keys %{ $self->{bucket_to_root} };
  8596         63080  
  940         4697  
119 940 100       3885 TRACE "no local disk for $digest in ".(join ' ', keys %{ $self->{bucket_to_root} }) unless defined($bucket);
  87         818  
120 940 100       60656 return unless defined($bucket);
121 853         4407 return $self->{bucket_to_root}->{$bucket};
122             }
123              
124              
125             sub local_buckets {
126 0     0 1 0 my($self) = @_;
127 0 0       0 $self->refresh_config unless keys %{ $self->{bucket_to_root} };
  0         0  
128 0         0 my %r = safe_reverse $self->{bucket_to_root};
129 0 0       0 do {$_ = [ $_ ] unless ref $_} for values %r;
  0         0  
130 0         0 return %r;
131             }
132              
133             sub _state {
134 321     321   628 my $self = shift;
135 321 50 33     4807 $self->refresh_config() unless $self->{state_file} && -e $self->{state_file};
136             # TODO: rm dep on File::stat
137 321 100 66     2554 return $self->{_state}->{cached} if $self->{_state}->{mod_time} && $self->{_state}->{mod_time} == stat($self->{state_file})->mtime;
138 24   33     106 our $j ||= JSON::MaybeXS->new;
139 24 50       232 -e $self->{state_file} or LOGDIE "Missing state file " . $self->{state_file};
140 24         257 $self->{_state}->{cached} = $j->decode(Mojo::Asset::File->new(path => $self->{state_file})->slurp);
141             # TODO: rm dep on File::stat
142 24         5429 $self->{_state}->{mod_time} = stat($self->{state_file})->mtime;
143 24         6861 return $self->{_state}->{cached};
144             }
145              
146             sub _write_state {
147 35     35   89 my $self = shift;
148 35         79 my $state = shift;
149 35         1682 my $dir = dirname($self->{state_file});
150 35   66     358 our $j ||= JSON::MaybeXS->new;
151 35         2285 mkpath $dir;
152 35         296 my $temp = File::Temp->new(DIR => $dir, UNLINK => 0);
153 35         16084 print $temp $j->encode($state);
154 35         292 $temp->close;
155 35 50       2327 rename "$temp", $self->{state_file} or return 0;
156 35         2546 return 1;
157             }
158              
159              
160             sub disk_is_up {
161 318     318 1 748 my $class = shift;
162 318         655 my $root = shift;
163 318 50 66     7971 return 0 if -d $root && ! -w $root;
164 318 100 50     1260 return 1 if ($class->_state->{disks}{$root} || 'up') eq 'up';
165 26         4008 return 0;
166             }
167              
168              
169             sub disk_is_up_verified
170             {
171 12     12 1 24 my($self, $root) = @_;
172 12 100       33 return unless $self->disk_is_up($root);
173 10         1186 my $tmpdir = File::Spec->catdir($root, 'tmp');
174 10         25 my $temp;
175 10         20 eval {
176 24     24   196 use autodie;
  24         51  
  24         210  
177 10 100       144 unless(-d $tmpdir)
178             {
179 7         1022 mkpath $tmpdir;
180 7         40 chmod 0777, $tmpdir;
181             };
182 10         1040 $temp = File::Temp->new("disk_is_up_verifiedXXXXX", DIR => $tmpdir, SUFFIX => '.txt');
183 10         3240 print $temp "test";
184 10         39 close $temp;
185 10 50       2254 die "file has zero size" if -z $temp->filename;
186 10         228 unlink $temp->filename;
187             };
188 10 50       1376 if(my $error = $@)
189             {
190 0         0 INFO "Create temp file in $tmpdir FAILED: $error";
191 0         0 return;
192             }
193             else
194             {
195 10         34 INFO "created temp file to test status: " . $temp->filename;
196 10         11549 return 1;
197             }
198             }
199              
200              
201             sub disk_is_down {
202 3     3 1 9 return not shift->disk_is_up(@_);
203             }
204              
205              
206             sub disk_is_local {
207 3     3 1 7 my $self = shift;
208 3         4 my $root = shift;
209 3         14 return $self->{disk_is_local}->{$root};
210             }
211              
212              
213             sub server_is_up {
214             # TODO use state file for this
215 0     0 1 0 my $self = shift;
216 0         0 my $server_url = shift;
217 0 0 0     0 if (exists($self->{server_status_cache}->{$server_url}) && $self->{server_status_cache}->{$server_url}{checked} > time - $self->{server_status_cache_lifetime}) {
218 0         0 return $self->{server_status_cache}->{$server_url}{result};
219             }
220 0         0 TRACE "Checking $server_url/status";
221 0         0 my $tx = $self->_ua->get( "$server_url/status" );
222 0         0 $self->{server_status_cache}->{$server_url}{checked} = time;
223 0 0       0 if (my $res = $tx->success) {
224 0         0 my $got = $res->json;
225 0 0 0     0 if (defined($got->{server_version}) && length($got->{server_version})) {
226 0         0 return ($self->{server_status_cache}->{$server_url}{result} = 1);
227             }
228 0         0 TRACE "/status did not return version, got : ". JSON::MaybeXS::encode_json($got);
229 0         0 return ($self->{server_status_cache}->{$server_url}{result} = 0);
230             }
231 0         0 TRACE "Server $server_url is not up : response was ".format_tx_error($tx->error);
232 0         0 return ($self->{server_status_cache}->{$server_url}{result} = 0);
233             }
234             sub server_is_down {
235 0     0 1 0 return not shift->server_is_up(@_);
236             }
237              
238             sub _touch {
239 0     0   0 my $path = shift;
240 0         0 my $dir = dirname($path);
241 0 0       0 -d $dir or do {
242 0         0 my $ok;
243 0         0 eval { mkpath($dir); $ok = 1; };
  0         0  
  0         0  
244 0 0       0 if($@) { WARN "mkpath $dir failed : $@;"; $ok = 0; };
  0         0  
  0         0  
245 0 0       0 return 0 unless $ok;
246             };
247 0 0       0 open my $fp, ">>$path" or return 0;
248 0         0 close $fp;
249 0         0 return 1;
250             }
251              
252              
253             sub mark_disk_down {
254 3     3 1 7 my $class = shift;
255 3         5 my $root = shift;
256 3 50       12 return 1 if $class->disk_is_down($root);
257 3         478 my $state = $class->_state;
258 3         412 INFO "Marking disk $root down";
259 3 50       3161 exists($state->{disks}{$root}) or WARN "$root not present in state file";
260 3         7 $state->{disks}{$root} = 'down';
261 3 50       11 $class->_write_state($state) and return 1;
262 0         0 ERROR "Could not mark disk $root down";
263 0         0 return 0;
264             }
265              
266             sub mark_disk_up {
267 0     0 1 0 my $class = shift;
268 0         0 my $root = shift;
269 0 0       0 return 1 if $class->disk_is_up($root);
270 0         0 my $state = $class->_state;
271 0         0 INFO "Marking disk $root up";
272 0         0 $state->{disks}{$root} = 'up';
273 0 0       0 $class->_write_state($state) and return 1;
274 0         0 ERROR "Could not mark disk up";
275 0         0 return 0;
276             }
277              
278              
279             sub server_for {
280 1101     1101 1 2455 my $self = shift;
281 1101         2092 my $digest = shift;
282 1101         1894 my $found;
283 1101 50       1908 $self->refresh_config unless keys %{ $self->{bucket_to_url} } > 0;
  1101         4766  
284 1101         4317 for my $i (0..length($digest)) {
285 2202 100       9042 last if $found = $self->{bucket_to_url}->{ uc substr($digest,0,$i) };
286 1172 100       4725 last if $found = $self->{bucket_to_url}->{ lc substr($digest,0,$i) };
287             }
288 1101         3641 return $found;
289             }
290              
291              
292             sub bucket_map {
293 9     9 1 53 return shift->{bucket_to_url};
294             }
295              
296              
297             sub storage_path {
298 1219     1219 1 2599 my $class = shift;
299 1219         2311 my $digest = shift;
300 1219   33     5580 my $root = shift || $class->disk_for($digest) || LOGDIE "No local disk for $digest";
301 1219         25735 return join "/", $root, ( grep length, split /(..)/, $digest );
302             }
303              
304              
305             sub remote_stashed_server {
306 69     69 1 165 my $self = shift;
307 69         188 my ($filename,$digest) = @_;
308              
309 69         231 my $assigned_server = $self->server_for($digest);
310             # TODO broadcast these requests all at once
311 69         179 for my $server (shuffle(keys %{ $self->{servers} })) {
  69         380  
312 118 100       1662 next if $server eq $self->{our_url};
313 52 50       194 next if $server eq $assigned_server;
314 52         342 DEBUG "Checking remote $server for $filename";
315 52         61298 my $tx = $self->_ua->head( "$server/file/$filename/$digest", { "X-Yars-Check-Stash" => 1, "Connection" => "Close" } );
316 52 100       345527 if (my $res = $tx->success) {
317             # Found it!
318 6         313 return $server;
319             }
320             }
321 63         1397 return '';
322             }
323              
324              
325             sub local_stashed_dir {
326 290     290 1 685 my $self = shift;
327 290         790 my ($filename,$md5) = @_;
328 290         730 for my $root ( shuffle(keys %{ $self->{disk_is_local} })) {
  290         1818  
329 442         1544 my $dir = $self->storage_path($md5,$root);
330 442         3984 TRACE "Checking for $dir/$filename";
331 442 100       317565 return $dir if -r "$dir/$filename";
332             }
333 235         1970 return '';
334             }
335              
336              
337             sub server_exists {
338 1     1 1 3 my $self = shift;
339 1         3 my $server_url = shift;
340 1 50       8 return exists($self->{servers}->{$server_url}) ? 1 : 0;
341             }
342              
343              
344             sub server_url {
345 1378     1378 1 7180 return shift->{our_url};
346             }
347              
348              
349             sub disk_roots {
350 67     67 1 140 return keys %{ shift->{disk_is_local} };
  67         435  
351             }
352              
353              
354             sub server_urls {
355 3     3 1 6 return keys %{ shift->{servers} }
  3         14  
356             }
357              
358              
359             sub cleanup_tree {
360 21     21 1 55 my $self = shift;
361 21         70 my ($dir) = @_;
362 21         91 while (_dir_is_empty($dir)) {
363 328 100       1054 last if $self->{disk_is_local}->{$dir};
364 320 50       17676 rmdir $dir or do { warn "cannot rmdir $dir : $!"; last; };
  0         0  
  0         0  
365 320         3303 $dir =~ s[/[^/]+$][];
366             }
367             }
368              
369              
370             sub count_files {
371 12     12 1 20 my $class = shift;
372 12         22 my $dir = shift;
373 12 50       165 -d $dir or return 0;
374 12         323 my @list = File::Find::Rule->file->in($dir);
375 12         50500 return scalar @list;
376             }
377              
378              
379             sub human_size {
380 36     36 1 54 my $class = shift;
381 36         50 my $val = shift;
382 36         81 my @units = qw/B K M G T P/;
383 36         58 my $unit = shift @units;
384 36   66     48 do {
385 108         143 $unit = shift @units;
386 108         391 $val /= 1024;
387             } until $val < 1024 || !@units;
388 36         279 return sprintf( "%.0f%s", $val + 0.5, $unit );
389             }
390              
391              
392             sub content_is_same {
393 2     2 1 5 my $class = shift;
394 2         7 my ($filename,$asset) = @_;
395 2         6 my $check;
396 2 50       20 if ($asset->isa("Mojo::Asset::File")) {
397 0         0 $asset->handle->flush;
398 0         0 $check = ( compare($filename,$asset->path) == 0 );
399             } else {
400             # Memory asset. Assume that if one can fit in memory, two can, too.
401 2         10 my $existing = Mojo::Asset::File->new(path => $filename);
402 2   66     73 $check = ( $existing->size == $asset->size && $asset->slurp eq $existing->slurp );
403             }
404 2         516 return $check;
405             }
406              
407              
408             sub hex2b64 {
409 529     529 1 1313 my $class = shift;
410 529         1135 my $hex = shift;
411 529         5113 my $b64 = b(pack 'H*', $hex)->b64_encode;
412 529         13781 local $/="\n";
413 529         2333 chomp $b64;
414 529         6881 return $b64;
415             }
416              
417             sub b642hex {
418 0     0 1   my $class = shift;
419 0           my $b64 = shift;
420 0           return unpack 'H*', b($b64)->b64_decode;
421             }
422              
423              
424             1;
425              
426             __END__
427              
428             =pod
429              
430             =encoding UTF-8
431              
432             =head1 NAME
433              
434             Yars::Tools - various utility functions dealing with servers, hosts, etc
435              
436             =head1 VERSION
437              
438             version 1.28
439              
440             =head1 DESCRIPTION
441              
442             This module is largely used internally by L<Yars>. Documentation for
443             some of its capabilities are provided here for the understanding of how
444             the rest of the L<Yars> server works, but they should not be considered
445             to be a public interface and they may change in the future, though
446             probably not for a good reason.
447              
448             =head1 FUNCTIONS
449              
450             =head2 new
451              
452             Create a new instance of Yars::Tools
453              
454             =head2 refresh_config
455              
456             Refresh the configuration data cached in memory.
457              
458             =head2 disk_for
459              
460             Given an md5 digest, calculate the root directory of this file. Undef is
461             returned if this file does not belong on the current host.
462              
463             =head2 local_buckets
464              
465             Get a hash from disk to list of buckets for this server.
466              
467             =head2 disk_is_up
468              
469             Given a disk root, return true unless the disk is marked down. A disk is
470             down if the state file indicates it, or if it exists but is unwriteable.
471              
472             =head2 disk_is_up_verified
473              
474             This is the same as disk_is_up, but doesn't trust the operating system,
475             and tries to write a file to the disk's temp directory and verify that
476             the file is not of zero size.
477              
478             =head2 disk_is_down
479              
480             Disk is not up.
481              
482             =head2 disk_is_local
483              
484             Return true if the disk is on this server.
485              
486             =head2 server_is_up, server_is_down
487              
488             Check to see if a remote server is up or down.
489              
490             =head2 mark_disk_down, mark_disk_up
491              
492             Mark a disk as up or down.
493              
494             =head2 server_for
495              
496             Given an md5, return the url for the server for this file.
497              
498             =head2 bucket_map
499              
500             Return a map from bucket prefix to server url.
501              
502             =head2 storage_path
503              
504             Calculate the directory of an md5 on disk. Optionally pass a second
505             parameter to force it onto a particular disk.
506              
507             =head2 remote_stashed_server
508              
509             Find a server which is stashing this file, if one exists.
510             Parameters :
511             $c - controller
512             $filename - filename
513             $digest - digest
514              
515             =head2 local_stashed_dir
516              
517             Find a local directory stashing this file, if one exists.
518             Parameters :
519             $filename - filename
520             $digest - digest
521             Returns :
522             The directory or false.
523              
524             =head2 server_exists
525              
526             Does this server exist?
527              
528             =head2 server_url
529              
530             Returns the url of the current server.
531              
532             =head2 disk_roots
533              
534             Return all the local directory roots, in a random order.
535              
536             =head2 server_urls
537              
538             Return all the other urls, in a random order.
539              
540             =head2 cleanup_tree
541              
542             Given a directory, traverse upwards until encountering a local disk root
543             or a non-empty directory, and remove all empty directories.
544              
545             =head2 count_files
546              
547             Count the number of files in a directory tree.
548              
549             =head2 human_size
550              
551             Given a size, format it like df -kh
552              
553             =head2 content_is_same
554              
555             Given a filename and an Asset, return true if the content is the same
556             for both.
557              
558             =head2 hex2b64, b642hex
559              
560             Convert from hex to base 64.
561              
562             =head1 SEE ALSO
563              
564             L<Yars>, L<Yars::Client>
565              
566             =head1 AUTHOR
567              
568             Original author: Marty Brandon
569              
570             Current maintainer: Graham Ollis E<lt>plicease@cpan.orgE<gt>
571              
572             Contributors:
573              
574             Brian Duggan
575              
576             Curt Tilmes
577              
578             =head1 COPYRIGHT AND LICENSE
579              
580             This software is copyright (c) 2013 by NASA GSFC.
581              
582             This is free software; you can redistribute it and/or modify it under
583             the same terms as the Perl 5 programming language system itself.
584              
585             =cut