File Coverage

lib/Provision/Unix.pm
Criterion Covered Total %
statement 19 21 90.4
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 26 28 92.8


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