File Coverage

lib/Provision/Unix.pm
Criterion Covered Total %
statement 170 192 88.5
branch 39 56 69.6
condition 5 10 50.0
subroutine 23 25 92.0
pod 6 15 40.0
total 243 298 81.5


line stmt bran cond sub pod time code
1             package Provision::Unix;
2             # ABSTRACT: provision hosting accounts on unix systems
3              
4 14     14   583205 use strict;
  14         33  
  14         479  
5 14     14   70 use warnings;
  14         26  
  14         5102  
6              
7             our $VERSION = '1.05';
8              
9 14     14   11355 use Config::Tiny;
  14         14896  
  14         471  
10 14     14   83 use Cwd;
  14         21  
  14         1136  
11 14     14   6595 use Data::Dumper;
  14         69717  
  14         795  
12 14     14   88 use English qw( -no_match_vars );
  14         30  
  14         111  
13 14     14   19707 use Params::Validate qw(:all);
  14         172214  
  14         3458  
14 14     14   130 use Scalar::Util qw( openhandle );
  14         25  
  14         27478  
15              
16             sub new {
17 13     13 1 2993 my $class = shift;
18 13         790 my %p = validate(
19             @_,
20             { file => { type => SCALAR, optional => 1, },
21             fatal => { type => SCALAR, optional => 1, default => 1 },
22             debug => { type => SCALAR, optional => 1, default => 1 },
23             }
24             );
25              
26 13   50     182 my $file = $p{file} || 'provision.conf';
27 13         31 my $debug = $p{debug};
28 13         72 my $ts = get_datetime_from_epoch();
29 13         229 my $self = {
30             debug => $debug,
31             fatal => $p{fatal},
32             config => undef,
33             errors => [], # errors get appended here
34             audit => [ # status messages accumulate here
35             "launched at $ts",
36             $class . sprintf( " loaded by %s, %s, %s", caller ),
37             ],
38             last_audit => 0,
39             last_error => 0,
40             util => undef,
41             };
42              
43 13         48 bless( $self, $class );
44 13         77 my $config = $self->find_config( file => $file, debug => $debug, fatal => 0 );
45 13 50       138 if ( $config ) {
46 13         600 $self->{config} = Config::Tiny->read( $config );
47             }
48             else {
49 0         0 warn "could not find $file. Installing it in your local etc directory.\n";
50             };
51              
52 13         21192 return $self;
53             }
54              
55             sub audit {
56 294     294 1 1018 my $self = shift;
57 294         1507 my $mess = shift;
58              
59 294 50       1050 if ($mess) {
60 294         630 push @{ $self->{audit} }, $mess;
  294         1828  
61 294 50       1992 print STDERR "$mess\n" if $self->{debug};
62             }
63              
64 294         1254 return $self->{audit};
65             }
66              
67             sub dump_audit {
68 13     13 1 1475 my $self = shift;
69 13         50 my $last_line = $self->{last_audit};
70              
71             # we already dumped everything
72 13 50       26 return if $last_line == scalar @{ $self->{audit} };
  13         80  
73              
74 13         1730 print STDERR "\n\t\t\tAudit History Report \n\n";
75 13         43 my $i = 0;
76 13         30 foreach ( @{ $self->{audit} } ) {
  13         81  
77 1159         1325 $i++;
78 1159 100       3106 next if $i < $last_line;
79 205         20717 print STDERR "\t$_\n";
80             };
81 13         67 $self->{last_audit} = $i;
82 13         45 return;
83             };
84              
85             sub dump_errors {
86 10     10 0 26 my $self = shift;
87 10         22 my $last_line = $self->{last_error};
88              
89 10 50       18 return if $last_line == scalar @{ $self->{errors} }; # everything dumped
  10         80  
90              
91 10         725 print STDERR "\n\t\t\t Error History Report \n\n";
92 10         32 my $i = 0;
93 10         17 foreach ( @{ $self->{errors} } ) {
  10         40  
94 53         66 $i++;
95 53 100       141 next if $i < $last_line;
96 21         1357 print STDERR "ERROR: '$_->{errmsg}' \t\t at $_->{errloc}\n";
97             };
98 10         503 print "\n";
99 10         35 $self->{last_error} = $i;
100 10         32 return;
101             };
102              
103             sub error {
104 43     43 1 197 my $self = shift;
105 43         174 my $message = shift;
106 43         4316 my %p = validate(
107             @_,
108             { 'location' => { type => SCALAR, optional => 1, },
109             'fatal' => { type => BOOLEAN, optional => 1, default => 1 },
110             'debug' => { type => BOOLEAN, optional => 1, default => 1 },
111             },
112             );
113              
114 43         531 my $debug = $p{debug};
115 43         166 my $fatal = $p{fatal};
116 43         105 my $location = $p{location};
117              
118 43 100       255 if ( $message ) {
119 42         369 my @caller = caller;
120 42         123 push @{ $self->{audit} }, $message;
  42         327  
121              
122             # append message to $self->error stack
123 42   66     108 push @{ $self->{errors} },
  42         943  
124             {
125             errmsg => $message,
126             errloc => $location || join( ", ", $caller[0], $caller[2] ),
127             };
128             }
129             else {
130 1         8 $message = $self->get_last_error();
131             }
132              
133             # print audit and error results to stderr
134 43 100       246 if ( $debug ) {
135 10         59 $self->dump_audit();
136 10         47 $self->dump_errors();
137             }
138              
139 43 100       242 if ( $fatal ) {
140 2 50       23 if ( ! $debug ) {
141 0         0 $self->dump_audit(); # dump if err is fatal and debug is not set
142 0         0 $self->dump_errors();
143             };
144 2         83 die "FATAL ERROR";
145             };
146 41         1774 return;
147             }
148              
149             sub find_config {
150 20     20 1 400 my $self = shift;
151 20         1170 my %p = validate(
152             @_,
153             { 'file' => { type => SCALAR, },
154             'etcdir' => { type => SCALAR | UNDEF, optional => 1, },
155             'fatal' => { type => SCALAR, optional => 1, default => 1 },
156             'debug' => { type => SCALAR, optional => 1, default => 1 },
157             }
158             );
159              
160 19         171 my $file = $p{file};
161 19         42 my $etcdir = $p{etcdir};
162 19         137 my $fatal = $self->{fatal} = $p{fatal};
163 19         54 my $debug = $self->{debug} = $p{debug};
164              
165 19         164 $self->audit("searching for config $file");
166              
167 19 100       64 return $self->_find_readable( $file, $etcdir ) if $etcdir;
168              
169 17         61 my @etc_dirs = qw{ /opt/local/etc /usr/local/etc /etc etc };
170              
171 17         102399 my $working_directory = cwd;
172 17         442 push @etc_dirs, $working_directory;
173              
174 17         673 my $r = $self->_find_readable( $file, @etc_dirs );
175 17 100       217 return $r if $r;
176              
177             # try $file-dist in the working dir
178 16 100       414 if ( -r "./$file-dist" ) {
179 14         370 $self->audit("\tfound $file-dist in ./");
180 14         761 return "$working_directory/$file-dist";
181             }
182              
183 2         45 return $self->error( "could not find $file",
184             fatal => $fatal,
185             debug => $debug,
186             );
187             }
188              
189             sub get_datetime_from_epoch {
190 13     13 0 28 my ( $self, $time ) = @_;
191 13   33     1433 my @lt = localtime( $time || time() );
192 13         153 return sprintf '%04d-%02d-%02d %02d:%02d:%02d', $lt[5] + 1900, $lt[4] + 1,
193             $lt[3], $lt[2], $lt[1], $lt[0];
194             }
195              
196             sub get_dns {
197 0     0 0 0 my $self = shift;
198 0 0       0 return $self->{dns} if ref $self->{dns};
199 0         0 require Provision::Unix::DNS;
200 0         0 $self->{dns} = Provision::Unix::DNS->new(
201             prov => $self,
202             debug => $self->{debug},
203             );
204 0         0 return $self->{dns};
205             };
206              
207             sub get_debug {
208 15     15 0 106 my ($self, $debug) = @_;
209 15 50       122 return $debug if defined $debug;
210 15         72 return $self->{debug};
211             };
212              
213             sub get_errors {
214 0     0 0 0 my $self = shift;
215 0         0 return $self->{errors};
216             }
217              
218             sub get_fatal {
219 15     15 0 40 my ($self, $fatal) = @_;
220 15 50       58 return $fatal if defined $fatal;
221 15         130 return $self->{fatal};
222             };
223              
224             sub get_last_error {
225 1     1 1 5 my $self = shift;
226 1 50       3 return $self->{errors}[-1]->{errmsg} if scalar @{ $self->{errors} };
  1         11  
227 0         0 return;
228             }
229              
230             sub get_util {
231 14     14 0 49 my $self = shift;
232 14 100       86 return $self->{util} if ref $self->{util};
233 11         9270 require Provision::Unix::Utility;
234 11         203 $self->{util} = Provision::Unix::Utility->new(
235             'log' => $self,
236             debug => $self->{debug},
237             );
238 11         89 return $self->{util};
239             };
240              
241             sub get_version {
242 1     1 0 1884 print "Provision::Unix version $VERSION\n";
243 1         14 return $VERSION;
244             };
245              
246             sub progress {
247 4     4 0 41 my $self = shift;
248 4         457 my %p = validate(
249             @_,
250             { 'num' => { type => SCALAR },
251             'desc' => { type => SCALAR, optional => 1 },
252             'err' => { type => SCALAR, optional => 1 },
253             },
254             );
255              
256 4         65 my $num = $p{num};
257 4         21 my $desc = $p{desc};
258 4         19 my $err = $p{err};
259              
260 4   50     37 my $msg_length = length $desc || 0;
261 4         21 my $to_print = 10;
262 4         24 my $max_print = 70 - $msg_length;
263              
264             # if err, print and return
265 4 50       158 if ( $err ) {
266 0 0       0 if ( length( $err ) == 1 ) {
267 0         0 foreach my $error ( @{ $self->{errors} } ) {
  0         0  
268 0         0 print {*STDERR} "\n$error->{errloc}\t$error->{errmsg}\n";
  0         0  
269             }
270             }
271             else {
272 0         0 print {*STDERR} "\n\t$err\n";
  0         0  
273             }
274 0         0 return $self->error( $err, fatal => 0, debug => 0 );
275             }
276              
277 4 50       48 if ( $msg_length > 54 ) {
278 0         0 die "max message length is 55 chars\n";
279             }
280              
281 4         16 print {*STDERR} "\r[";
  4         834  
282 4         48 foreach ( 1 .. $num ) {
283 31         65 print {*STDERR} "=";
  31         3579  
284 31         514 $to_print--;
285 31         306 $max_print--;
286             }
287              
288 4         39 while ($to_print) {
289 9         14 print {*STDERR} ".";
  9         474  
290 9         20 $to_print--;
291 9         20 $max_print--;
292             }
293              
294 4         17 print {*STDERR} "] $desc";
  4         224  
295 4         31 while ($max_print) {
296 188         377 print {*STDERR} " ";
  188         11182  
297 188         848 $max_print--;
298             }
299              
300 4 100       35 if ( $num == 10 ) { print {*STDERR} "\n" }
  3         16  
  3         202  
301              
302 4         313 return 1;
303             }
304              
305             sub _find_readable {
306 102     102   409 my $self = shift;
307 102         268 my $file = shift;
308 102 100       672 my $dir = shift or return; # breaks recursion at end of @_
309              
310             #$self->audit("looking for $file in $dir") if $self->{debug};
311              
312 85 100       6147 if ( -r "$dir/$file" ) {
313 14     14   101 no warnings;
  14         34  
  14         5495  
314 2         28 $self->audit("\tfound in $dir");
315 2         20 return "$dir/$file"; # we have succeeded
316             }
317              
318 83 100       2195 if ( -d $dir ) {
319              
320             # warn about directories we don't have read access to
321 49 50       1673 if ( !-r $dir ) {
322 0         0 $self->error( "$dir is not readable", fatal => 0 );
323             }
324             else {
325              
326             # warn about files that exist but aren't readable
327 49 50       1082 if ( -e "$dir/$file" ) {
328 0         0 $self->error( "$dir/$file is not readable",
329             fatal => 0
330             );
331             }
332             }
333             }
334              
335 83         1066 return $self->_find_readable( $file, @_ );
336             }
337              
338              
339             1;
340              
341              
342              
343             =pod
344              
345             =head1 NAME
346              
347             Provision::Unix - provision hosting accounts on unix systems
348              
349             =head1 VERSION
350              
351             version 1.06
352              
353             =head1 SYNOPSIS
354              
355             use Provision::Unix;
356              
357             my $foo = Provision::Unix->new();
358             ...
359              
360             prov_dns --action=create --zone=example.com
361             prov_user --action=create --username=matt --pass='neat0app!'
362             prov_virtual --action=create --name=testVPS
363             prov_web --action=create --vhost=www.example.com
364              
365             =head1 DESCRIPTION
366              
367             Provision::Unix is a suite of applications to create, modify, and destroy
368             accounts on Unix systems in a reliable and consistent manner.
369              
370             Command line scripts are provided for humans to perform provisioning actions
371             by hand. See the documentation included in each of the prov_* scripts.
372             Programmers and automated systems should be loading the Provision::Unix
373             modules and calling the methods directly. The API provided by each method is
374             stable and only changes when additional parameters are added.
375              
376             The types of accounts that can be provisioned are organized by class with each
377             class including a standard set of methods. All classes support at least
378             create and destroy. Additional common methods are: modify, enable, and disable.
379              
380             Each class (DNS, User, VirtualOS, Web) has a general module that
381             contains the logic for selecting and dispatching requests to sub-classes which
382             are implementation specific. Selecting and dispatching is done based on the
383             environment and configuration file settings at run time.
384              
385             For example, Provision::Unix::DNS contains all the general logic for dns
386             operations (create a zone, record, alias, etc). Subclasses contain
387             specific information such as how to provision a DNS record for nictool,
388             BIND, or tinydns.
389              
390             Not all specific modules are fully implemented yet.
391             Ex: Provision::Unix::VirtualOS::Linux::Xen is fully implemented,
392             where Provision::Unix::VirtualOS::FreeBSD::Jail is not.
393              
394             Browse the perl modules to see which modules are available.
395              
396             =head1 NAME
397              
398             Provision::Unix - provision accounts on unix systems
399              
400             =head1 Programming Conventions
401              
402             All functions/methods adhere to the following:
403              
404             =head2 Exception Handling
405              
406             Errors throw exceptions. This can be overridden by calling the method with fatal=0. If you do so, you must write code to handle the errors.
407              
408             This call will throw an exception since it cannot find the file.
409              
410             $util->file_read('/etc/oopsie_a_typo');
411              
412             Setting fatal will cause it to return undef instead:
413              
414             $util->file_read('/etc/oopsie_a_typo', fatal=>0);
415              
416             =head2 Warnings and Messages
417              
418             Methods have an optional debug parameter that defaults to enabled. Often, that means methods spit out more messages than you want. Supress them by setting debug=0.
419              
420             Supressed messages are not lost! All error messages are stored in $prov->errors and all status messages are in $prov->audit. You can dump those arrays any time to to inspect the status or error messages. A handy way to do so is:
421              
422             $prov->error('test breakpoint');
423              
424             That will dump the contents of $prov->audit and $prov->errors and then terminate your program. If you want your program to continue after calling $prov->error, just set fatal=0.
425              
426             $prov->error('test breakpoint', fatal => 0);
427              
428             =head1 FUNCTIONS
429              
430             =head2 new
431              
432             Creates and returns a new Provision::Unix object.
433              
434             As part of initialization, new() finds and reads in provision.conf from /[opt/usr]/local/etc, /etc, and the current working directory.
435              
436             =head2 audit
437              
438             audit is a method that appends messages to an internal audit log. Rather than spewing messages to stdout or stderr, they are stored as a list. The list can can be inspected by calling $prov->audit or it can be printed by calling $prov->dump_audit.
439              
440             $prov->audit("knob fitzerbaum twiddled to setting 5");
441              
442             If the debug option is set ($prov->{debug}), audit messages are also printed to stderr.
443              
444             returns an arrayref of audit messages.
445              
446             =head2 dump_audit
447              
448             dump_audit prints out any audit/status messages that have accumulated since the last time dump_audit was called. It is particularly useful for RPC agents that poll for status updates during long running processes.
449              
450             =head2 dump_error
451              
452             Same as dump_audit, except dumps the error history report.
453              
454             =head2 error
455              
456             Whenever a method runs into an unexpected condition, it should call $prov->error with a human intelligible error message. It should also specify whether the error is merely a warning or a fatal condition. Errors are considered fatal unless otherwise specified.
457              
458             Examples:
459              
460             $prov->error( 'could not write to file /etc/passwd' );
461              
462             This error is fatal and will throw an exception, after printing the contents of the audit log and the last error message to stderr.
463              
464             A very helpful thing to do is call error with a location as well:
465              
466             $prov->error( 'could not write to file /etc/passwd',
467             location => join( ", ", caller ),
468             );
469              
470             Doing so will tell reveal in the error log exactly where the error was encountered as well as who called the method. The latter is more likely where the error exists, making location a very beneficial parameter.
471              
472             =head2 find_config
473              
474             This sub is used to determine which configuration file to use. The general logic is as follows:
475              
476             If the etc dir and file name are provided and the file exists, use it.
477              
478             If that fails, then go prowling around the drive and look in all the usual places, in order of preference:
479              
480             /opt/local/etc/
481             /usr/local/etc/
482             /etc
483              
484             Finally, if none of those work, then check the working directory for the named .conf file, or a .conf-dist.
485              
486             Example:
487             my $conf = $util->find_config (
488             file => 'example.conf',
489             etcdir => '/usr/local/etc',
490             )
491              
492             arguments required:
493             file - the .conf file to read in
494              
495             arguments optional:
496             etcdir - the etc directory to prefer
497             debug
498             fatal
499              
500             result:
501             0 - failure
502             the path to $file
503              
504             =head2 get_last_error
505              
506             prints and returns the last error encountered.
507              
508             =head1 BUGS
509              
510             Please report any bugs or feature requests to C<bug-unix-provision at rt.cpan.org>, or through the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Provision-Unix>. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.
511              
512             =head1 SUPPORT
513              
514             You can find documentation for this module with the perldoc command.
515              
516             perldoc Provision::Unix
517              
518             You can also look for information at:
519              
520             =over 4
521              
522             =item * RT: CPAN's request tracker
523              
524             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Provision-Unix>
525              
526             =item * AnnoCPAN: Annotated CPAN documentation
527              
528             L<http://annocpan.org/dist/Provision-Unix>
529              
530             =item * CPAN Ratings
531              
532             L<http://cpanratings.perl.org/d/Provision-Unix>
533              
534             =item * Search CPAN
535              
536             L<http://search.cpan.org/dist/Provision-Unix>
537              
538             =back
539              
540             =head1 AUTHOR
541              
542             Matt Simerson <msimerson@cpan.org>
543              
544             =head1 COPYRIGHT AND LICENSE
545              
546             This software is copyright (c) 2013 by The Network People, Inc..
547              
548             This is free software; you can redistribute it and/or modify it under
549             the same terms as the Perl 5 programming language system itself.
550              
551             =cut
552              
553              
554             __END__
555              
556