File Coverage

blib/lib/MogileFS/Client/Fuse.pm
Criterion Covered Total %
statement 22 24 91.6
branch n/a
condition n/a
subroutine 8 8 100.0
pod n/a
total 30 32 93.7


line stmt bran cond sub pod time code
1             package MogileFS::Client::Fuse;
2              
3             =head1 NAME
4              
5             MogileFS::Client::Fuse - FUSE binding for MogileFS
6              
7             =head1 SYNOPSIS
8              
9             use MogileFS::Client::Fuse::FilePaths;
10              
11             my $fs = MogileFS::Client::Fuse::FilePaths->new(
12             'mountpoint' => '/mnt/mogile-fuse',
13             'trackers' => ['tracker1:port', 'tracker2:port'],
14             'domain' => 'fuse.example.com::namespace',
15             'class' => 'default',
16             );
17             $fs->mount();
18              
19             =head1 DESCRIPTION
20              
21             This module provides support for mounting a MogileFS file store as a local
22             filesystem.
23              
24             =cut
25              
26 1     1   9 use strict;
  1         1  
  1         45  
27 1     1   7 use warnings;
  1         2  
  1         29  
28 1     1   7 use MRO::Compat;
  1         2  
  1         253  
29 1     1   7 use mro;
  1         1  
  1         9  
30 1     1   137 use threads::shared;
  1         2  
  1         8  
31              
32             our $VERSION = '0.05';
33              
34 1     1   105 use Errno qw{EACCES EEXIST EIO ENOENT EOPNOTSUPP};
  1         2  
  1         218  
35 1     1   7 use Fcntl qw{O_WRONLY};
  1         3  
  1         63  
36 1     1   1475 use Fuse 0.11;
  0            
  0            
37             use LWP::UserAgent;
38             use MogileFS::Client;
39             use MogileFS::Client::Fuse::Constants qw{CALLBACKS :LEVELS THREADS};
40             use Params::Validate qw{validate_with ARRAYREF BOOLEAN SCALAR UNDEF};
41             use POSIX qw{strftime};
42             use Scalar::Util qw{blessed refaddr};
43              
44             ##Private static variables
45              
46             #variables to track the currently mounted Fuse object
47             my %unshared;
48              
49             # custom file class counter (used to autogenerate a file package)
50             my $fileClassIndex = 0;
51              
52             ##Static Methods
53              
54             #constructor
55             # buffered => boolean indicating if open file handles should utilize write buffering, defaults to true
56             # class => the class to store files as in MogileFS
57             # domain => the domain to use in MogileFS
58             # loglevel => the log level to use for output
59             # mountopts => options to use when mounting the Fuse filesystem
60             # mountpoint => where to mount the filesystem
61             # threaded => flag indicating if this MogileFS file system should be threaded or not
62             # trackers => the addresses for the MogileFS trackers
63             sub new {
64             #create the new MogileFS::Client::Fuse object
65             my $self = shift;
66             $self = bless(shared_clone({}), ref($self) || $self);
67              
68             #initialize and return the new object
69             return $self->_init(@_);
70             }
71              
72             ##Instance Methods
73              
74             #return the config for this MogileFS::Client::Fuse object
75             sub _config {
76             #cache the config in the local thread for faster access if threads are loaded
77             if(THREADS) {
78             my $config = $_[0]->_localElem('config');
79              
80             #copy the config to a local thread cache of it
81             if(!defined $config) {
82             #do a shallow copy of the config
83             $config = {%{$_[0]->{'config'}}};
84              
85             #store the shallow copy
86             $_[0]->_localElem('config', $config);
87             }
88              
89             #return the local cache of this object's config
90             return $config;
91             }
92              
93             #default to returning the noncached config
94             return $_[0]->{'config'};
95             }
96              
97             #method that will initialize the MogileFS::Client::Fuse object
98             sub _init {
99             my $self = shift;
100             my %opt = validate_with(
101             'allow_extra' => 1,
102             'params' => \@_,
103             'spec' => {
104             'buffered' => {'type' => BOOLEAN, 'default' => 1},
105             'class' => {'type' => SCALAR | UNDEF, 'default' => undef},
106             'domain' => {'type' => SCALAR},
107             'loglevel' => {'type' => SCALAR, 'default' => ERROR},
108             'mountopts' => {'type' => SCALAR | UNDEF, 'default' => undef},
109             'mountpoint' => {'type' => SCALAR},
110             'readonly' => {'type' => BOOLEAN, 'default' => undef},
111             'threaded' => {'type' => BOOLEAN, 'default' => THREADS},
112             'trackers' => {'type' => ARRAYREF},
113             },
114             );
115              
116             #die horribly if we are trying to reinit an existing object
117             die 'You are trying to reinitialize an existing MogileFS::Client::Fuse object, this could introduce race conditions and is unsupported' if($self->{'initialized'});
118              
119             #disable threads if they aren't loaded
120             $opt{'threaded'} = 0 if(!THREADS);
121              
122             # generate the customized file class
123             {
124             my @classes;
125             push @classes, 'MogileFS::Client::Fuse::BufferedFile' if($opt{'buffered'});
126             push @classes, 'MogileFS::Client::Fuse::File';
127              
128             # load the specified classes
129             eval "require $_;" foreach(@classes);
130             die $@ if($@);
131              
132             # create file class
133             if(@classes > 1) {
134             $opt{'fileClass'} = 'MogileFS::Client::Fuse::File::Generated' . $fileClassIndex;
135             $fileClassIndex++;
136              
137             no strict 'refs';
138             push @{$opt{'fileClass'} . '::ISA'}, @classes;
139             mro::set_mro($opt{'fileClass'}, 'c3');
140             Class::C3::reinitialize();
141             }
142             else {
143             $opt{'fileClass'} = $classes[0];
144             }
145             }
146              
147             #initialize this object
148             $self->{'config'} = shared_clone({%opt});
149             $self->{'files'} = shared_clone({});
150             $self->{'initialized'} = 1;
151              
152             #return the initialized object
153             return $self;
154             }
155              
156             #method that will access unshared object elements
157             sub _localElem {
158             my $self = ($unshared{shift->id} ||= {});
159             my $elem = shift;
160             my $old = $self->{$elem};
161             $self->{$elem} = $_[0] if(@_);
162             return $old;
163             }
164              
165             sub CLONE {
166             #destroy all unshared objects to prevent non-threadsafe objects from being accessed by multiple threads
167             %unshared = ();
168             return 1;
169             }
170              
171             #return the instance id for this object
172             sub id {
173             return is_shared($_[0]) || refaddr($_[0]);
174             }
175              
176             #function that will output a log message
177             sub log {
178             my $self = shift;
179             my ($level, $msg) = @_;
180             return if($level > $self->_config->{'loglevel'});
181             print STDERR strftime("[%Y-%m-%d %H:%M:%S] ", localtime), $msg, "\n";
182             }
183              
184             #method that will return a MogileFS object
185             sub MogileFS {
186             my $client = $_[0]->_localElem('MogileFS');
187              
188             #create and store a new client if one doesn't exist already
189             if(!defined $client) {
190             my $config = $_[0]->_config;
191             $client = MogileFS::Client->new(
192             'hosts' => [@{$config->{'trackers'}}],
193             'domain' => $config->{'domain'},
194             );
195             $_[0]->_localElem('MogileFS', $client);
196             }
197              
198             #return the MogileFS client
199             return $client;
200             }
201              
202             #Method to mount the specified MogileFS domain to the filesystem
203             sub mount {
204             my $self = shift;
205              
206             #short-circuit if a MogileFS file system is currently mounted
207             {
208             lock($self);
209             return if($self->{'mounted'});
210             $self->{'mounted'} = 1;
211             }
212              
213             #set MogileFS debugging based on the log level
214             local $MogileFS::DEBUG = ($self->_config->{'loglevel'} >= DEBUGMFS);
215              
216             #generate closures for supported callbacks
217             my %callbacks;
218             foreach(CALLBACKS) {
219             #skip unsupported callbacks
220             my $method = 'fuse_' . $_;
221             next if(!$self->can($method));
222              
223             #create closure for this callback
224             no strict "refs";
225             if($self->_config->{'loglevel'} >= DEBUG) {
226             $callbacks{$_} = sub {
227             $self->log(DEBUG, $method . '(' . join(', ', map {defined($_) ? '"' . $_ . '"' : 'undef'} ($method eq 'fuse_write' ? ($_[0], length($_[1]).' bytes', @_[2,3]) : @_)) . ')');
228             $self->$method(@_);
229             };
230             }
231             else {
232             $callbacks{$_} = sub {
233             $self->$method(@_);
234             };
235             }
236             }
237              
238             #mount the MogileFS file system
239             Fuse::main(
240             'mountopts' => $self->_config->{'mountopts'},
241             'mountpoint' => $self->_config->{'mountpoint'},
242             'threaded' => $self->_config->{'threaded'},
243             'debug' => ($self->_config->{'loglevel'} >= DEBUGFUSE),
244              
245             #callback functions
246             %callbacks,
247             );
248              
249             #release any files that are still active
250             eval{$_->release()} foreach(values %{$self->{'files'}});
251             $self->{'files'} = shared_clone({});
252              
253             #reset mounted state
254             {
255             lock($self);
256             $self->{'mounted'} = 0;
257             }
258              
259             #return
260             return;
261             }
262              
263             #thin wrapper for opening a file
264             sub openFile {
265             my $self = shift;
266             my ($path, $flags) = @_;
267              
268             #create a file object for the file being opened
269             return $self->_config->{'fileClass'}->new(
270             'fuse' => $self,
271             'path' => $path,
272             'flags' => $flags,
273             );
274             }
275              
276             sub sanitize_path {
277             # my $self = shift;
278             # my ($path) = @_;
279              
280             # return the root path if a path wasn't specified
281             return '/' if(length($_[1]) == 0 || $_[1] eq '.');
282              
283             # make sure the path starts with a /
284             return '/' . $_[1] if($_[1] !~ m!^/!s);
285              
286             # path doesn't need to be sanitized
287             return $_[1];
288             }
289              
290             #method that will return an LWP UserAgent object
291             sub ua {
292             my $ua = $_[0]->_localElem('ua');
293              
294             #create and store a new ua if one doesn't exist already
295             if(!defined $ua) {
296             $ua = LWP::UserAgent->new(
297             'keep_alive' => 60,
298             'timeout' => 5,
299             );
300             $_[0]->_localElem('ua', $ua);
301             }
302              
303             #return the UserAgent
304             return $ua;
305             }
306              
307             ##Callback Functions
308              
309             sub fuse_flush {
310             my $self = shift;
311             my ($path, $file) = @_;
312              
313             eval {$file->flush()};
314             return -EIO() if($@);
315              
316             return 0;
317             }
318              
319             sub fuse_fsync {
320             my $self = shift;
321             my ($path, $flags, $file) = @_;
322              
323             eval {$file->fsync()};
324             return -EIO() if($@);
325              
326             return 0;
327             }
328              
329             sub fuse_getattr {
330             return -EOPNOTSUPP();
331             }
332              
333             sub fuse_getdir {
334             return -EOPNOTSUPP();
335             }
336              
337             sub fuse_getxattr {
338             my $self = shift;
339             my ($path, $name) = @_;
340              
341             if($name =~ /^MogileFS\.(?:class|checksum)$/s) {
342             $path = $self->sanitize_path($path);
343             my $resp = eval {$self->MogileFS->file_info($path, {'devices' => 0})};
344             if($resp) {
345             return $resp->{'checksum'} if($name eq 'MogileFS.checksum');
346             return $resp->{'class'} if($name eq 'MogileFS.class');
347             }
348             }
349              
350             return 0;
351             }
352              
353             sub fuse_link {
354             return -EOPNOTSUPP();
355             }
356              
357             sub fuse_listxattr {
358             return (
359             'MogileFS.checksum',
360             'MogileFS.class',
361             ), 0;
362             }
363              
364             sub fuse_mknod {
365             my $self = shift;
366             my ($path) = @_;
367             $path = $self->sanitize_path($path);
368              
369             # throw an error if read-only is enabled
370             return -EACCES() if($self->_config->{'readonly'});
371              
372             #attempt creating an empty file
373             eval {$self->openFile($path, O_WRONLY)->release()};
374             return -EIO() if($@);
375              
376             #return success
377             return 0;
378             }
379              
380             sub fuse_open {
381             my $self = shift;
382             my ($path, $flags) = @_;
383             $path = $self->sanitize_path($path);
384              
385             #open the requested file
386             my $file = eval {$self->openFile($path, $flags)};
387             return -EIO() if($@);
388             return -EEXIST() if(!$file);
389              
390             #store the file in the list of open files
391             {
392             my $files = $self->{'files'};
393             lock($files);
394             return -EIO() if(defined $files->{$file->id});
395             $files->{$file->id} = $file;
396             };
397              
398             #return success and the open file handle
399             return 0, $file;
400             }
401              
402             sub fuse_read {
403             my $self = shift;
404             my ($path, $len, $off, $file) = @_;
405              
406             my $buf = eval{$file->read($len, $off)};
407             return -EIO() if($@);
408              
409             return defined($buf) ? $$buf : '';
410             }
411              
412             sub fuse_readlink {
413             return 0;
414             }
415              
416             sub fuse_release {
417             my $self = shift;
418             my ($path, $flags, $file) = @_;
419              
420             eval {
421             #remove the file from the list of active file handles
422             delete $self->{'files'}->{$file->id};
423              
424             #release the file handle
425             $file->release();
426             };
427             return -EIO() if($@);
428              
429             return 0;
430             }
431              
432             sub fuse_rename {
433             return -EOPNOTSUPP();
434             }
435              
436             sub fuse_setxattr {
437             my $self = shift;
438             my ($path, $name, $value, $flags) = @_;
439             $path = $self->sanitize_path($path);
440              
441             # switch based on xattr name
442             if($name eq 'MogileFS.class') {
443             my $resp = eval {$self->MogileFS->update_class($path, $value)};
444             return -EIO() if(!$resp || $@);
445             return 0;
446             }
447              
448             return -EOPNOTSUPP();
449             }
450              
451             sub fuse_statfs {
452             my $self = shift;
453              
454             # retrieve all device stats
455             my $resp = eval {$self->MogileFS->{'backend'}->do_request('get_devices', {})};
456              
457             # calculate the total and free space for the storage cluster in blocks
458             my $blkSize = 1024 * 1024;
459             my $total = 0;
460             my $free = 0;
461             for(my $i = 1;$i <= $resp->{'devices'}; $i++) {
462             my $dev = 'dev' . $i;
463             my $mbFree = $resp->{$dev . '_mb_free'};
464             my $mbTotal = $resp->{$dev . '_mb_total'};
465             $free += $mbFree if($mbFree && $resp->{$dev . '_status'} eq 'alive' && $resp->{$dev . '_observed_state'} eq 'writeable');
466             $total += $mbTotal if($mbTotal);
467             }
468             $total *= (1024 * 1024) / $blkSize;
469             $free *= (1024 * 1024) / $blkSize;
470              
471             # return the drive stats
472             return (
473             255, # max name length
474             1, # files
475             1, # filesfree
476             $total, # blocks
477             $free, # blocks available
478             $blkSize # block size
479             );
480             }
481              
482             sub fuse_symlink {
483             return -EOPNOTSUPP();
484             }
485              
486             sub fuse_truncate {
487             my $self = shift;
488             my ($path, $size) = @_;
489             $path = $self->sanitize_path($path);
490              
491             # throw an error if read-only is enabled
492             return -EACCES() if($self->_config->{'readonly'});
493              
494             #attempt to truncate the specified file
495             eval{
496             my $file = $self->openFile($path, O_WRONLY);
497             $file->truncate($size);
498             $file->release;
499             };
500             return -EIO() if($@);
501              
502             #return success
503             return 0;
504             }
505              
506             sub fuse_unlink {
507             my $self = shift;
508             my ($path) = @_;
509             $path = $self->sanitize_path($path);
510              
511             # throw an error if read-only is enabled
512             return -EACCES() if($self->_config->{'readonly'});
513              
514             #attempt deleting the specified file
515             my $mogc = $self->MogileFS();
516             my ($errcode, $errstr) = (-1, '');
517             eval {$mogc->delete($path)};
518             if($@) {
519             #set the error code and string if we have a MogileFS::Client object
520             if($mogc) {
521             $errcode = $mogc->errcode || -1;
522             $errstr = $mogc->errstr || '';
523             }
524             $self->log(ERROR, "Error unlinking file: $errcode: $errstr");
525             $! = $errstr;
526             $? = $errcode;
527             return -EIO();
528             }
529              
530             #return success
531             return 0;
532             }
533              
534             sub fuse_write {
535             my $self = shift;
536             my $buf = \$_[1];
537             my $offset = $_[2];
538             my $file = $_[3];
539              
540             # throw an error if read-only is enabled
541             return -EACCES() if($self->_config->{'readonly'});
542              
543             my $bytesWritten = eval{$file->write($buf, $offset)};
544             return -EIO() if($@);
545              
546             return $bytesWritten;
547             }
548              
549             1;
550              
551             __END__