File Coverage

blib/lib/FleetConf.pm
Criterion Covered Total %
statement 25 27 92.5
branch n/a
condition n/a
subroutine 9 9 100.0
pod n/a
total 34 36 94.4


line stmt bran cond sub pod time code
1             package FleetConf;
2              
3 15     15   11668 use strict;
  15         29  
  15         533  
4 15     15   79 use warnings;
  15         27  
  15         450  
5              
6 15     15   75 use Carp;
  15         28  
  15         1555  
7 15     15   98 use Cwd ();
  15         40  
  15         286  
8 15     15   21980 use Data::Dumper;
  15         317769  
  15         1403  
9 15     15   151 use File::Basename;
  15         157  
  15         2097  
10 15     15   93 use File::Find;
  15         30  
  15         1121  
11 15     15   105 use File::Spec;
  15         30  
  15         363  
12 15     15   10612 use FleetConf::Conf;
  0            
  0            
13             use FleetConf::Log;
14             use FleetConf::Workflow::Null;
15             use Log::Dispatch;
16             use Log::Dispatch::File;
17             use Log::Dispatch::Screen;
18              
19             our $VERSION = '0.01_016';
20              
21             =head1 NAME
22              
23             FleetConf - An multi-agent, configuration management tool
24              
25             =head1 SYNOPSIS
26              
27             perl Build.PL install_base=/master/install/dir/FleetConf
28             ./Build
29             ./Build test
30             ./Build install
31              
32             vim /master/install/dir/FleetConf/etc/fleet.conf
33              
34             /master/install/dir/FleetConf/bin/fleetconf -v
35             /usr/local/Fleetconf/bin/fleetconf
36              
37             =head1 DESCRIPTION
38              
39             This is a highly configurable, Perl-based, multi-agent, configuration management tool. It was originally designed by the author to handle the account management tasks for a mixed platform environment. In order to create accounts, he needed to create directory users on a Microsoft Windows Server 2003, create home directories and establish quotas on a Linux file server, add the user to mailing lists on another Linux mail server, monitor the process, and notify the user when the operations were complete.
40              
41             This system has been further generalized to perform nearly any common configuration task needed to be performed across multiple systems. It's meant to perform actions similar to (and is being used by the author to replace) cfengine.
42              
43             =head2 HOW DOES IT WORK?
44              
45             The front-end for the tool is L. Though, the real work is all started within the Perl module named L and through another set of Perl modules used by L. (Therefore, if FleetConf doesn't quite do it right for your needs, it should be a simple matter to modify L or create a new similar script to perform the same purpose.) For details on how to use L see it's documentation.
46              
47             Essentially, the FleetConf system starts by determining the root directory of FleetConf. Using L, this is determined by moving upward to the parent directory that script is being run from. Within this root, the system opens and reads the contents of F, which contains some initial settings (see L below).
48              
49             Then, one or more plugin directories are loaded. Each plugin is a Perl file with the suffix ".pl" or ".pm" and may do whatever it likes to modify the internals of the system. Usually, this involves defining new agent language commands or defining helper functions (in the C namespace) or helper commands (in the C namespace). However, these can be used to modify any aspect of the published API they want.
50              
51             Following this, a series of "workflows" are loaded, as defined in the configuration file (and possibly plugins). Agents use the workflows to determine what to do and also to record their progress in doing it.
52              
53             Next, one or more agent directories are loaded. All files ending in ".agent" are read using the agent parser. The agent parser compiles every agent script found. Each agent is defined in a fairly simple command-based language that can be extended using plugins. For a full definition of the language see L, and any plugins you have loaded (the included plugins are linked from L).
54              
55             Finally, each agent is run. Each agent runs over each record found in the given workflow system and may perform an action permitted by the agent language. Each iteration involves running the agent in 5 phases (plus a phase before iteration begins). It is likely that future versions will feature an additional phase at the end and perhaps other phases. For a full description of running and the phases see L. For a description of which commands run in which phases, you'll need to see the L and command plugin documentation.
56              
57             =head2 HOW DO I USE IT?
58              
59             The first step, of course, is to install it. Then, you will need to configure it and write some useful agents. Then, you need to run it all over the place to copy it locally.
60              
61             For information on installation, see L. For information on configuration, see L. For information to get started with agents, see L.
62              
63             =head1 FleetConf API
64              
65             The public API is documented to allow contributors to learn about the system, for users to create plugins that take full advantage of the API, and to keep the author from forgetting what something is supposed to do.
66              
67             In generally, I try to keep the documentation up to date with the actual program, but there will be lapses. Please let me know about them by filing a bug at L if you discover such a lapse.
68              
69             =head2 API OVERVIEW
70              
71             The L object exists as a singleton created sometime after startup. (For those who may not know, a "singleton" is approximately what it sounds like: it is an object for which there can be only one for each Perl process. That means that there should only ever be one of these objects-per-run of L.) This singleton is accessed (and, during the first call, created) by calling the C method of L.
72              
73             Prior to calling the C method you will probably want to configure the global variables, which are used to determine certain parts of the configuration.
74              
75             =head2 GLOBAL VARIABLES
76              
77             B Please be aware that these variables may change. These were an easy mechanism of communication between L and L originally, but aren't very good solutions. Future revisions may include accessors from the L singleton so that their effects can be modified after startup. Currently, if you set these after the C method is called, that change may have no effect. However, it's likely that I will keep these around and use some devious Cing to allow them to be effective. So perhaps my warning doesn't matter anyway.
78              
79             This documentation shows the fully qualified name of each global variable and the initial/default value of that variable (if any).
80              
81             =over
82              
83             =item $FleetConf::verbose = 0;
84              
85             The verbose setting is used to determine how much information to output while running. All output I be routed through the logger (see C<$FleetConf::log> below), which is configured according to the value of this setting when C is called.
86              
87             When this setting is set to "0", the default log level of "warning" or "error" is used, meaning that anything below that threshold is not output. (See C<$FleetConf::no_warnings> for more information.) If verbose is set to "1", then the log level is set to "notice". If verbose is set to "2", the log level is set to "info". If verbose is set to "3", the log level is set to "debug". Higher levels will result in greater amounts of debug information being displayed. At this time, I believe level "4" is the highest implemented debug level, but more might be used in the future.
88              
89             =item $FleetConf::pretend = 0;
90              
91             All plugins should be careful to check the value of this variable. If this value is set to a true (non-zero) value, then no changes should be made to the system, the workflow, or anything. Plugins and commands should merely report back as if they were doing that work (or may log messages stating that we're pretending to do something, whatever is most appropriate).
92              
93             This flag allows the user to safely run and see what would happen without actually causing anything to happen.
94              
95             Setting this value to will cause verbose to be set to at least a value of "1" (so that log level is set to "notice").
96              
97             =item $FleetConf::no_warnings = 0;
98              
99             In some cases, it might be desireable to hide warnings from standard output. Setting this to a true (non-zero) value will mean that the log level will be set to "error" rather than "warning" as long as neither C<$FleetConf::verbose> nor C<$FleetConf::pretend> are non-zero.
100              
101             =item $FleetConf::fleetconf_root = Cwd::getcwd;
102              
103             This value is very important to set if you are writing an alternative to the L front-end. This tells FleetConf which directory to use as the basis for all relative file names and where to find the configuration file (i.e., F inside of this directory).
104              
105             it defaults to the current working directory of the current Perl interpreter, but this is a poor default.
106              
107             =item $FleetConf::log = ...;
108              
109             This log mechanism is deprecated. Please use L instead. This is still used, but only internally.
110              
111             =item $FleetConf::log_file;
112              
113             Name the log file to log to. If none is given, then logging only goes to standard error. This file will be appended to rather than overwritten.
114              
115             =item $FleetConf::log_stderr = 1;
116              
117             If this is set to a false value, then logging will not be printed to the terminal on standard error.
118              
119             =item %FleetConf::globals;
120              
121             This global is setup initially from the "globals" configuration option. It may contain any information a plugin or agent wants to store here. It may be modified by any agent or plugin and those changes stay until changed by a later agent or plugin or when FleetConf quits. The contents of this variable are not saved between runs of FleetConf.
122              
123             =back
124              
125             =cut
126              
127             our $verbose = 0;
128             our $pretend = 0;
129             our $no_warnings = 0;
130             our $fleetconf_root = Cwd::getcwd;
131             our $log_file;
132             our $log_stderr = 1;
133              
134             our $log;
135              
136             BEGIN {
137             $log = Log::Dispatch->new(
138             callbacks => sub {
139             my %args = @_;
140             my $str = localtime;
141             sprintf "%s [%s] %s\n", $str, $args{level}, $args{message};
142             },
143             );
144             $log->add(Log::Dispatch::Screen->new(
145             name => 'screen',
146             min_level => 'debug',
147             stderr => 1));
148              
149             sub strip_eol {
150             return () unless @_;
151             my $x = pop; chomp $x; return (@_, $x)
152             }
153             $SIG{__WARN__} = sub { $log->warning(strip_eol(@_)) };
154             $SIG{__DIE__} = sub { $log->error(strip_eol(@_)); die @_ };
155             }
156              
157             my $flog = FleetConf::Log->get_logger(__PACKAGE__);
158              
159             # This is first initialized to the values loaded in fleet.conf, and
160             # can be further modified by the plugins.
161             our %globals;
162              
163             =head2 METHODS
164              
165             The most important method is the C method, which returns the singleton L instance that is used to call most of the other methods.
166              
167             =over
168              
169             =item $fleetconf = FleetConf::instance
170              
171             =item $fleetconf = FleetConf->instance
172              
173             This method returns the singleton reference to the L object. This is the starting point for all things FleetConf in the API. See the rest of the method documentation below to find out what you can do with it.
174              
175             The first time this method is called, it will perform the first four phases of loading based upon the globals (see L above).
176              
177             =over
178              
179             =item Prologue
180              
181             Before doing anything it will determine the log level that should be used to output to the screen (via standard error). It then resets the logger to use this log level on the screen (prior to this call, the logger will output anything to the screen as it's rare that it should be used at all prior to this point).
182              
183             It also performs a late loading of the L library (which shouldn't, in general, be loaded directly by any plugin or application). This late loading happens so that we can capture the output of the parser trace for debugging purposes.
184              
185             =item Phase 1: Configuration
186              
187             Loading the configuration happens next. See L for details on what is stored there.
188              
189             =item Phase 2: Plugins
190              
191             We search all directories specified by the plugin search path for any file ending in ".pl" or ".pm" and we run that file as if it were Perl. We report as errors or warnings any plugin that fails to load correctly, but we otherwise ignore them.
192              
193             Most of the time, FleetConf ignores errors and continues despite them because it's possible that unrelated parts of the system should run anyway. If an error should stop some other part of the system from running a plugin should make sure and fail cleanly itself and alter the state of the system somehow to prevent the other system from failing (the C<%FleetConf::globals> would be a good place to do this).
194              
195             =item Phase 3: Workflows
196              
197             Based upon the configuration in the "workflows" variable, we will load workflow instances into the system. Agents then request named workflow instances to determine how they proceed.
198              
199             One workflow instance is ubiquitous and loaded regardless of any other configuration. This workflow is called "Null" and is defined as a basic instance of L. This module basically always returns a single empty workflow record and uses the C<$FleetConf::log> logger to record changes. See L for more information.
200              
201             =item Phase 4: Agents
202              
203             Finally, we search all of the agent include directories for files ending with ".agent" and compile the agents there according to the C method of L. (See that module for more documentation on this process.)
204              
205             =back
206              
207             At this point everything that will be loaded is loaded, but (unless a plugin does something) no action should have been taken yet. The returned object is ready to continue.
208              
209             =cut
210              
211             my $fleetconf;
212             sub instance {
213             my $class = shift;
214             my $clean = shift;
215             # FOR TESTING ONLY!!!!
216             undef $fleetconf if $clean && $clean eq 'I_WANT_A_CLEAN_INSTANCE';
217              
218             return $fleetconf if defined $fleetconf;
219              
220             my $level =
221             $verbose > 2 ? 'debug' :
222             $verbose > 1 ? 'info' :
223             ($verbose > 0 || $pretend) ? 'notice' :
224             $no_warnings ? 'error' : 'warning';
225              
226             $log->remove('screen');
227            
228             if ($log_stderr) {
229             $log->add(Log::Dispatch::Screen->new(
230             name => 'screen',
231             min_level => $level,
232             stderr => 1));
233             }
234              
235             if ($log_file) {
236             $log->add(Log::Dispatch::File->new(
237             name => 'file',
238             min_level => $level,
239             filename => $log_file,
240             mode => 'append'));
241             }
242              
243             # Load this late so that $::RD_TRACE picks up the value of
244             # $verbose
245             eval "use FleetConf::Agent";
246             die $@ if $@;
247              
248             $fleetconf = bless {}, $class;
249             $fleetconf->_load_configuration;
250             $fleetconf->_load_plugins;
251             $fleetconf->_load_workflows;
252             $fleetconf->_load_agents;
253              
254             return $fleetconf;
255             }
256              
257             =item $config = FleetConf::configuration
258              
259             =item $config = FleetConf-Econfiguration
260              
261             =item $config = $fleetconf-Econfiguration
262              
263             This method can be used to fetch the configuration file after it is read in. If this method is not called on a L instance, the C method will be called to get one and the configuration method called on that (i.e., don't call this if you aren't ready to call C).
264              
265             For details on what this variable will contain, see L.
266              
267             =cut
268              
269             sub configuration {
270             my $class = shift;
271              
272             if (ref $class) {
273             return $class->{conf};
274             } else {
275             return FleetConf->instance->{conf};
276             }
277             }
278              
279             sub _load_configuration {
280             my $self = shift;
281              
282             # TODO Should attempt to find the root if not given.
283             unless ($fleetconf_root) {
284             die "fleetconf_root is unset, can't find fleet.conf";
285             }
286              
287             my $conf_file = File::Spec->catfile(
288             $fleetconf_root, "etc", "fleet.conf"
289             );
290              
291             $flog->notice("Loading configuration '$conf_file'");
292              
293             my $parser = FleetConf::Conf->new;
294             $self->{conf} = $parser->parse_file($conf_file);
295              
296             $flog->would_log('debug') &&
297             $flog->debug(Data::Dumper->Dump([$self->{conf}],['conf']));
298              
299             if (defined $self->{conf}{globals} &&
300             ref($self->{conf}{globals}) eq 'HASH') {
301             %globals = %{ $self->{conf}{globals} };
302             $flog->would_log('debug') &&
303             $flog->debug(Data::Dumper->Dump([\%globals],['globals']));
304             }
305              
306             if (defined $self->{conf}{perl_include}) {
307             my @dirs =
308             map { File::Spec->file_name_is_absolute($_) ? $_ : File::Spec->catfile($fleetconf_root, $_) }
309             @{ $self->{conf}{perl_include} };
310              
311             for my $dir (@dirs) {
312             -d $dir && push @INC, $dir;
313             }
314             }
315             }
316              
317             sub _load_plugins {
318             my $self = shift;
319              
320             $flog->would_log('notice') &&
321             $flog->notice("Loading plugin directories: ",join(' ', @{ $self->{conf}{plugin_include} }));
322              
323             my @files;
324             my $want = sub {
325             /^\.svn$/ and $File::Find::prune = 1;
326             /\.(pm|pl)$/ and push @files, $File::Find::name;
327             };
328              
329             find($want,
330             grep { -d $_ }
331             map { File::Spec->file_name_is_absolute($_) ? $_ : File::Spec->catfile($fleetconf_root, $_) }
332             @{ $self->{conf}{plugin_include} });
333              
334             for my $file (@files) {
335             my $basefile = File::Basename::basename($file);
336             unless (my $return = do $file) {
337             warn "Failed to load plugin $basefile: $@" if $@;
338             warn "Failed to open plugin $basefile: $!" unless defined $return;
339             warn "Failed to run plugin $basefile" unless $return;
340             } elsif ($flog->would_log('notice')) {
341             $flog->notice("Loaded plugin '$basefile'.");
342             }
343             }
344             }
345              
346             sub _load_workflows {
347             my $self = shift;
348              
349             my $workflows = $self->{conf}{workflows};
350              
351             while (my ($name, $args) = each %$workflows) {
352             $flog->notice("Loading workflow '$name' using class '$args->{class}'.");
353              
354             $self->{workflows}{$name} = $args->{class}->new($args->{args});
355              
356             warn "Failed to load workflow '$name'."
357             unless $self->{workflows}{$name};
358             }
359              
360             # Setup the ever present Null workflow
361             $self->{workflows}{'Null'} = FleetConf::Workflow::Null->new;
362             }
363              
364             sub _load_agents {
365             my $self = shift;
366              
367             $flog->would_log('notice') &&
368             $flog->notice("Loading agent directories: ",join(' ', @{ $self->{conf}{agent_include} }));
369              
370             my @files;
371             my $want = sub {
372             /^\.svn$/ and $File::Find::prune = 1;
373             /\.agent$/ and push @files, $File::Find::name;
374             };
375              
376             find($want,
377             map { /\// ? $_ : "$fleetconf_root/$_" }
378             @{ $self->{conf}{agent_include} });
379              
380             for my $file (@files) {
381             my $agent = eval { FleetConf::Agent->load_file($file) };
382              
383             my $basefile = File::Basename::basename($file);
384             if ($@) {
385             warn "Failed to load agent $basefile: $@";
386             } elsif ($agent && $agent->name) {
387             push @{ $self->{agents} }, $agent;
388              
389             $flog->notice("Loaded agent named '".$agent->name."' from '$basefile'.");
390             } else {
391             warn "Failed to load agent $basefile";
392             }
393             }
394             }
395              
396             =item $fleetconf-Eselect_agents(@selectors)
397              
398             This method allows the user to specify which agents should be run. In some cases, it may be desireable to limit which agents are run externally (i.e., agents can internally refuse to run based on the situation as well). If only some of the agents should be run, then this method should be used to select the agents.
399              
400             Each element of C<@selectors> is a subroutine (code reference) that takes an L object reference as it's only argument. An agent will be used if I selector returns a true (non-zero) value when pased that agent.
401              
402             This call affects any call to C following until this method is called again. Each call to this method replaces the entire list of selectors. Calling this method with no arguments will cause all agents to be selected if immediately followed by a call to C.
403              
404             =cut
405              
406             sub select_agents {
407             my $self = shift;
408              
409             $self->{agent_selectors} = [ grep defined($_), @_ ];
410            
411             return;
412             }
413              
414             =item @agents = $fleetconf-Eagents
415              
416             This method returns a list of agents that are currently selected (according to the last call to C) or all agents if no selectors are set. See C for information on how to select agents.
417              
418             =cut
419              
420             sub agents {
421             my $self = shift;
422              
423             if ($self->{agent_selectors} && @{ $self->{agent_selectors} }) {
424             my @results;
425             AGENT: for my $agent (@{ $self->{agents} }) {
426             for my $selector (@{ $self->{agent_selectors} }) {
427             if ($selector->($agent)) {
428             push @results, $agent;
429             next AGENT;
430             }
431             }
432             }
433              
434             return @results;
435             } else {
436             return @{ $self->{agents} };
437             }
438             }
439              
440             =item $fleetconf->run_agents
441              
442             Runs all the agents that have been selected (i.e., those that would be returned by C). This basically means that each agent is loaded and the C method is called with the requested workflow passed.
443              
444             =cut
445              
446             sub run_agents {
447             my $self = shift;
448              
449             for my $agent ($self->agents) {
450             my $wf = $self->{workflows}{$agent->workflow};
451              
452             unless ($wf) {
453             warn "Cannot run agent '".$agent->name."' because workflow '".$agent->workflow."' has not been loaded.";
454             next;
455             }
456              
457             $agent->run_foreach($wf);
458             }
459             }
460              
461             =back
462              
463             =head2 CONFIGURATION
464              
465             The configuration is stored as a L file. Please see the documentation of L for information on how YAML works. The top-level of the FleetConf configuration file, F, is a hash containing keys pointing to the different configuration directives. At this time, there are very few directives.
466              
467             For a basic and simple configuration file illustrating these directives, see the file F included in the distribution.
468              
469             Each are described here:
470              
471             =over
472              
473             =item agent_include
474              
475             This is an array containing the name of the directories to search for agents.
476              
477             For example:
478              
479             agent_include:
480             - agents
481             - /etc/FleetConf/agents
482              
483             Here, all agents found in the directory or subdirectories of F in the FleetConf root directory will be searched first and then those found in F will be loaded second.
484              
485             =item plugin_include
486              
487             This is an array containing the name of the directories to search for plugins.
488              
489             For example:
490              
491             plugin_include:
492             - plugins
493             - /etc/FleetConf/plugins
494              
495             Here, all agents found in the directory or subdirectories of F in the FleetConf root directory will be searched first. Then, those found in F will be leaded second.
496              
497             =item workflows
498              
499             This option contains a hash of the all the workflows to load. Each hash key is the name of the workflow instance. Each of these should then be a hash containing two keys, "class" and "args". The "class" key is a scalar naming the Perl class that will be used to create the workflow instance (and should already be loaded as a plugin). The "args" will be set to whatever the C method of the given "class" expects to set it up.
500              
501             For example:
502              
503             workflows:
504             new_jobs:
505             class: FleetConf::Workflow::RT
506             args:
507             proxy: https://example.com/cgi-bin/rt-soap-server.pl
508             query: Queue='Jobs' AND Status='new'
509             ssl_client_certificate: /etc/ssl/client/rt-workflow.crt
510             ssl_client_key: /etc/ssl/client/rt-workflow.key
511             open_jobs:
512             class: FleetConf::Workflow::RT
513             args:
514             proxy: https://example.com/cgi-bin/rt-soap-server.pl
515             query: Queue='Jobs' AND Status='open'
516             ssl_client_certificate: /etc/ssl/client/rt-workflow.crt
517             ssl_client_key: /etc/ssl/client/rt-workflow.key
518              
519             This will instantiate two workflow instances: one named "new_jobs" and another named "open_jobs". If a workflow requires more complicated work to instantiate it, that work may be done with a plugin (though, the way to do that isn't part of teh published API yet as this isn't necessarily safe if you upgrade in the future).
520              
521             =item globals
522              
523             This option sets the initial value of C<%FleetConf::globals>. Thus, it should, obviously, be a hash.
524              
525             For example:
526              
527             globals:
528             master_path: /common/admin/FleetConf
529             local_path: /usr/local
530              
531             This would set C<$FleetConf::globals{master_path}> to "/common/admin/FleetConf" and C<$FleetConf::globals{local_path}> to "/usr/local".
532              
533             =back
534              
535             =head2 INSTALLATION
536              
537             Installation follows the typical L template with one important caveat. FleetConf is meant to operate in a more controlled environment, so C is automatically set to F. That is, by doing the typical (such as installing with L):
538              
539             perl Build.PL
540             ./Build
541             ./Build test
542             ./Build install
543              
544             The files will all be dropped within F and subdirectories within that folder. It is strongly recommended that you install FleetConf into it's very own directory, which is why you will receive a complaint if you try to install into a directory not named "FleetConf", just to try and make sure you're aware of this if you have not yet read the docs.
545              
546             Thus, the more typical installation will look like this:
547              
548             perl Build.PL install_base=/some/master/root/FleetConf
549             ./Build
550             ./Build test
551             ./Build install
552              
553             Just in case you hate using capitals, no complaint will occur if you install using all lower case, e.g., F.
554              
555             After installation, you will need to create a file named F inside the installation directory and add any agents you like.
556              
557             At this point, one agent is installed automatically, F, which will update a local installation from a global one. Delete this if you don't want it. Future versions might include this as an F or something similar. You will need to check the agent folder and make sure you have all the agents you want there. You may also want to add plugins.
558              
559             The rest is up to you. At this point, FleetConf has no standard policies on how things should be done. This will likely remain the case until either I find something useful, or a user community starts choosing those policies. I have no idea what would be best in general or what tricks might be handy.
560              
561             =head1 SEE ALSO
562              
563             L, L, L, L, L, L, L, L, L
564              
565             =head1 AUTHOR
566              
567             Andrew Sterling Hanenkamp, Ehanenkamp@users.sourceforge.netE
568              
569             =head1 COPYRIGHT AND LICENSE
570              
571             Copyright 2005 Andrew Sterling Hanenkamp. All Rights Reserved.
572              
573             FleetConf is licensed and distributed under the same terms as Perl itself.
574              
575             =cut
576              
577             1