File Coverage

blib/lib/Sys/FS.pm
Criterion Covered Total %
statement 11 13 84.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 16 18 88.8


line stmt bran cond sub pod time code
1             package Sys::FS;
2             {
3             $Sys::FS::VERSION = '0.11';
4             }
5             BEGIN {
6 1     1   24171 $Sys::FS::AUTHORITY = 'cpan:TEX';
7             }
8             # ABSTRACT: filesystem interaction tools
9              
10 1     1   25 use 5.010_000;
  1         4  
  1         40  
11 1     1   1001 use mro 'c3';
  1         735  
  1         7  
12 1     1   39 use feature ':5.10';
  1         1  
  1         112  
13              
14 1     1   436 use Moose;
  0            
  0            
15             use namespace::autoclean;
16              
17             # use IO::Handle;
18             # use autodie;
19             # use MooseX::Params::Validate;
20              
21             use Carp;
22             use File::Spec;
23             use File::Copy qw();
24              
25             use Sys::Run;
26              
27             with 'Log::Tree::RequiredLogger';
28              
29             has 'sys' => (
30             'is' => 'ro',
31             'isa' => 'Sys::Run',
32             'lazy' => 1,
33             'builder' => '_init_sys',
34             );
35              
36             sub _init_sys {
37             my $self = shift;
38              
39             my $Sys = Sys::Run::->new( { 'logger' => $self->logger(), } );
40              
41             return $Sys;
42             }
43              
44             ############################################
45             # Usage : Create an absolute filename
46             sub filename {
47             my ( $self, @dirs ) = @_;
48              
49             if ( !@dirs ) {
50             my $msg = 'Missing option in Sys::FS::filename. Need at least one directory. Caller: ' . ( caller(1) )[3];
51             $self->logger()->log( message => $msg, level => 'error', );
52             return;
53             }
54              
55             my $filename = File::Spec->catfile(@dirs);
56             return $filename;
57             }
58              
59             sub switch {
60             my $self = shift;
61             my $sourcefile = shift;
62             my $destfile = shift;
63              
64             if($sourcefile && ref($sourcefile)) {
65             $self->logger()->log( message => 'Refusing to switch multiple source files to one destination. Check your configuration.', level => 'error', );
66             return;
67             }
68              
69             if($destfile && ref($destfile)) {
70             $self->logger()->log( message => 'Refusing to switch one source file to multiple destinations. Check your configuration.', level => 'error', );
71             return;
72             }
73              
74             my @cmds = ();
75              
76             # unlink old backup, if it doesn't exist do nothing
77             if ( -e $destfile . '.bak' ) {
78             if ( unlink( $destfile . '.bak' ) ) {
79             $self->logger()->log( message => 'source: '.$sourcefile.' - dest: '.$destfile.' - Removed old backup.', level => 'debug', );
80             }
81             else {
82             $self->logger()->log( message => 'source: '.$sourcefile.' - dest: '.$destfile.' - Could not remove old backup: ' . $!, level => 'error', );
83             return;
84             }
85             }
86              
87             # prepare new config for switch
88             if ( File::Copy::copy( $sourcefile, $destfile . '.new' ) ) {
89             $self->logger()->log( message => "source: $sourcefile - dest: $destfile - Successfully copied to .new file.", level => 'debug', );
90             }
91             else {
92             $self->logger()->log( message => "source: $sourcefile - dest: $destfile - Failed to copy $sourcefile to $destfile.new", level => 'error', );
93             return;
94             }
95              
96             # create backup of old config
97             if ( -e $destfile ) {
98             if ( File::Copy::copy( $destfile, $destfile . '.bak' ) ) {
99             $self->logger()->log( message => "source: $sourcefile - dest: $destfile - Successfully created backup.", level => 'debug', );
100             }
101             else {
102             $self->logger()->log( message => "source: $sourcefile - dest: $destfile - Failed to copy $destfile to $destfile.bak", level => 'error' );
103             return;
104             }
105             }
106              
107             # perform the final switch (atomic? should be ...)
108             if ( rename( $destfile . '.new', $destfile ) ) {
109             $self->logger()->log( message => "source: $sourcefile - dest: $destfile - Successfully switched files.", level => 'debug', );
110             return 1;
111             }
112             else {
113             $self->logger()->log( message => "source: $sourcefile - dest: $destfile - Failed to copy $destfile.new to $destfile", level => 'error', );
114             return;
115             }
116             }
117              
118             ############################################
119             # Usage : Create a directory stucture and return the created directory
120             sub makedir {
121             my $self = shift;
122             my $filename = shift;
123             my $opts = shift || {};
124              
125             if ( !$filename ) {
126             my $msg = 'No filename given in Sys::FS::makedir! Caller: ' . ( caller(1) )[3];
127             $self->logger()->log( message => $msg, level => 'error', );
128             return;
129             }
130              
131             my $mode = $opts->{'Mode'} || oct(777);
132              
133             my @dirs = File::Spec::->splitdir($filename);
134             my $dir = q{};
135             my $mkdirs = 0;
136             foreach my $i ( 0 .. $#dirs ) {
137             $dir = File::Spec::->catdir( @dirs[ 0 .. $i ] );
138             if ( !-d $dir ) {
139             my $msg = "Filename: $filename - mkdir $dir, $mode";
140             if ( mkdir( $dir, $mode ) ) {
141             $self->logger()->log( message => $msg, level => 'debug', );
142             $mkdirs++;
143             }
144             else {
145             $self->logger()->log( message => $msg . ' FAILED!', level => 'debug', );
146             }
147             if ( $opts->{'Uid'} && $opts->{'Gid'} ) {
148             chown $opts->{'Uid'}, $opts->{'Gid'} => $dir;
149             }
150             }
151             }
152             my $msg = "Filename: $filename - created $mkdirs dirs for $dir";
153             $self->logger()->log( message => $msg, level => 'debug', );
154             return $dir;
155             }
156              
157             sub spaceleft {
158             my $self = shift;
159             my $dir = shift;
160             my $host = shift || 'localhost';
161             my $opts = shift || {};
162              
163             # check free space on destination
164             local $opts->{CaptureOutput} = 1;
165             local $opts->{Chomp} = 1;
166             my $cmd = 'LANG=C /bin/df -P ' . $dir . ' | /usr/bin/tail -1';
167             my $out = $self->sys()->run( $host, $cmd, $opts );
168              
169             my ( $dev, $onekblocks, $used, $avail, $pcfree, $mount_point ) = split /\s+/, $out;
170             $avail = 0 unless $avail =~ m/^\d+$/;
171             $onekblocks = 0 unless $onekblocks =~ m/^\d+$/;
172             my $gbfree = int( $avail / ( 1024 * 1024 ) );
173             my $gbtotal = int( $onekblocks / ( 1024 * 1024 ) );
174             $pcfree =~ s/%$//;
175              
176             return wantarray ? ( $gbfree, $pcfree, $gbtotal ) : $gbfree;
177             }
178              
179             sub fsck {
180             my $self = shift;
181             my $device = shift;
182             my $fs_type = shift;
183             my $opts = shift || {};
184             if ( $fs_type && -x '/sbin/fsck.'.$fs_type ) {
185             $self->sys()->run_cmd('/sbin/fsck.'.$fs_type.' -y -p '.$device);
186             return 1;
187             }
188             else {
189             my $msg = "fsck($device,$fs_type) - dunno how to check $fs_type!";
190             $self->logger()->log( message => $msg, level => 'error', );
191             return;
192             }
193             }
194              
195             # used by get_mounted_device
196             sub mounts {
197             my $self = shift;
198             my $opts = shift || {};
199              
200             my $mounts_file = '/proc/mounts';
201             if ( open( my $FH, '<', $mounts_file ) ) {
202             my @lines = <$FH>;
203             # DGR: just reading
204             ## no critic (RequireCheckedClose)
205             close($FH);
206             ## use critic
207             my %mounts = ();
208             foreach my $line (@lines) {
209             my ( $dev, $mount_point, $fs_type, $options, $dump, $pass ) = split( /\s/, $line );
210             my $key;
211             if ( $opts->{DevAsKey} ) {
212             $key = $dev;
213             $mounts{$key}{'mount_point'} = $mount_point;
214             }
215             else { # MountPointAsKey
216             $key = $mount_point;
217             $mounts{$key}{'dev'} = $dev;
218             }
219             $mounts{$key}{'fs_type'} = $fs_type;
220             $mounts{$key}{'options'} = $options;
221             $mounts{$key}{'dump'} = $dump;
222             $mounts{$key}{'pass'} = $pass;
223             }
224             return \%mounts;
225             }
226             else {
227             my $msg = "Could not open $mounts_file: $!";
228             $self->logger()->log( message => $msg, level => 'error', );
229             return {};
230             }
231             }
232              
233             sub is_mounted {
234             my $self = shift;
235             my $device = shift;
236             my $opts = shift || {};
237              
238             # check if given device is mounted
239             local $opts->{DevAsKey} = 1;
240             my $mounts = $self->mounts($opts);
241             foreach my $dev ( keys %{$mounts} ) {
242             if ( $device =~ m/^$dev/ ) {
243             return 1;
244             }
245             }
246             return;
247             }
248              
249             sub get_mounted_device {
250             my $self = shift;
251             my $path = shift;
252             my $opts = shift || {};
253              
254             $self->logger()->log( message => "$path", level => 'debug', );
255              
256             # get mounts indexed by mount point as key, so we
257             # can traverse over the mounts to find the longest matching one
258             my $mounts = $self->mounts( { DevAsKey => 0, } );
259             my $device = undef;
260             my $mount_point = undef;
261              
262             # sort the keys be length, the shortest (the root fs at /) first
263             # and use the longest still-matching one as the mounted device
264             # for the given path
265             foreach my $key ( sort { length($a) <=> length($b) } keys %{$mounts} ) {
266             if ( $path =~ m/^$key/ ) {
267             $device = $mounts->{$key}{'dev'};
268             $mount_point = $key;
269             }
270             }
271              
272             if ( !$mount_point ) {
273             my $msg = "get_mounted_device($path) - no matching mount point found!";
274             $self->logger()->log( message => $msg, level => 'error', );
275             return;
276             }
277              
278             $self->logger()->log(
279             message => 'Path: '.$path.' - returning '.$device.', ' . $mounts->{$mount_point}{'fs_type'} . ', ' . $mounts->{$mount_point}{'options'} . ', '.$mount_point,
280             level => 'debug',
281             );
282              
283             return wantarray ? ( $device, $mounts->{$mount_point}{'fs_type'}, $mounts->{$mount_point}{'options'}, $mount_point ) : $device;
284             }
285              
286             sub mount {
287             my $self = shift;
288             my $device = shift;
289             my $mount_point = shift;
290             my $fs_type = shift;
291             my $fs_opts = shift;
292             my $opts = shift || {};
293              
294             if ( $fs_type eq 'xfs' && $fs_opts !~ m/nouuid/i ) {
295             $fs_opts .= ',nouuid';
296             }
297              
298             # device must be a block device
299             if ( !-b $device ) {
300             my $msg = "mount($device,$mount_point,$fs_type,$fs_opts) - $device is no block-device!";
301             $self->logger()->log( message => $msg, level => 'error', );
302             return;
303             }
304              
305             # mp must be a dir
306             if ( !-d $mount_point ) {
307             my $msg = "mount($device,$mount_point,$fs_type,$fs_opts) - $mount_point is no directory!";
308             $self->logger()->log( message => $msg, level => 'error', );
309             return;
310             }
311              
312             my $cmd = "/bin/mount -t $fs_type -o $fs_opts $device $mount_point";
313             local $opts->{Timeout} = 1200;
314             return $self->sys()->run_cmd( $cmd, $opts );
315             }
316              
317             sub umount {
318             my $self = shift;
319             my $device = shift;
320             my $opts = shift || {};
321              
322             # device must be a block device
323             if ( !-b $device ) {
324             my $msg = "umount($device) - $device is no block-device!";
325             $self->logger()->log( message => $msg, level => 'error', );
326             return;
327             }
328              
329             my $cmd = "/bin/umount $device >/dev/null 2>&1";
330             return $self->sys()->run_cmd( $cmd, $opts );
331             }
332              
333             no Moose;
334             __PACKAGE__->meta->make_immutable;
335              
336             1;
337              
338             __END__
339              
340             =pod
341              
342             =encoding utf-8
343              
344             =head1 NAME
345              
346             Sys::FS - filesystem interaction tools
347              
348             =head1 SYNOPSIS
349              
350             use Sys::FS;
351             use Log::Tree;
352             my $FS = Sys::FS::->new({
353             'logger' => Log::Tree::->new();
354             });
355             my $filename = $FS->filename('/tmp', qw(a list of subdirs));
356             $FS->makedir($filename);
357             my ($gb, $percent) = $FS->spaceleft($filename);
358              
359             =head1 ATTRIBUTES
360              
361             =head2 sys
362              
363             An instance of Linux::System
364              
365             =head1 METHODS
366              
367             =head2 filename
368              
369             Construct a filename out of an array of directories.
370              
371             =head2 fsck
372              
373             Run fsck on the given device.
374              
375             =head2 get_mounted_device
376              
377             Find the device mounted on the given directory.
378              
379             =head2 is_mounted
380              
381             Tests if a given device is currently mounted.
382              
383             =head2 makedir
384              
385             Create a directory stucture and return the created directory
386              
387             =head2 mount
388              
389             Mount a device on a mount point.
390              
391             =head2 mounts
392              
393             Return a hashref containing all mounted devices.
394              
395             =head2 spaceleft
396              
397             Return the amount of free space on the given device in GB.
398              
399             =head2 switch
400              
401             Reliably switch two files.
402              
403             =head2 umount
404              
405             Unmount a given device.
406              
407             =head1 NAME
408              
409             Sys::FS - Misc. Filesystem interaction methods
410              
411             =head1 AUTHOR
412              
413             Dominik Schulz <tex@cpan.org>
414              
415             =head1 COPYRIGHT AND LICENSE
416              
417             This software is copyright (c) 2012 by Dominik Schulz.
418              
419             This is free software; you can redistribute it and/or modify it under
420             the same terms as the Perl 5 programming language system itself.
421              
422             =cut