### ### Copyright 2000-2007 University of Illinois Board of Trustees ### All rights reserved. ### ### PSGConf.pm - main library for psgconf ### ### Campus Information Technologies and Educational Services ### University of Illinois at Urbana-Champaign ### package PSGConf; use strict; use File::Basename; use File::Copy; use File::Path; use File::stat; use POSIX; use Proc::ProcessTable; use PSGConf::Data::String; use PSGConf::Util; our $AUTOLOAD; ############################################################################### ### Global Defaults ############################################################################### my %defaults = ( config_dir => '/usr/local/share/psgconf/config', files_dir => '/usr/local/share/psgconf/files', modules_file => '/usr/local/etc/psgconf_modules', tmpdir => '/var/tmp/' . basename $0 . ".$$", verbose => 0, do_fix => 0, restart_daemons => 1, rm_tmpfiles => 1, do_diffs => 0, update_pkgs => 1, trace => 0, need_reboot => 0 ); ############################################################################### ### Constructor ############################################################################### sub new { my ($class, %opts) = @_; my ($self, $opt); ### create object $self = \%opts; bless($self, $class); ### Set from the environment variables if ### not set on the command line. $0 = basename $0; $self->{tmpdir} = $ENV{'PSGCONF_TMPDIR'} . "/$0.$$" if ( !defined $self->{tmpdir} && defined $ENV{'PSGCONF_TMPDIR'} && -d $ENV{'PSGCONF_TMPDIR'} ); ### set default options foreach $opt (keys %defaults) { $self->{$opt} = $defaults{$opt} if (!defined($self->{$opt})); } ### initialize tmpdir print $0 . ": creating temp directory...\n" if ($self->{verbose}); rmtree($self->{tmpdir}) if (-e $self->{tmpdir}); mkpath($self->{tmpdir}, 0, 0700); ### initialization $self->{datastores} = []; $self->{controls} = []; $self->{data} = {}; $self->{data_owners} = {}; $self->{actions} = []; $self->{policy} = {}; $self->{policy_order} = []; $self->{num_changed} = 0; ### set timestamps $self->{timestamp} = time; $self->{backupext} = $0 . '.' . POSIX::strftime("%Y%m%d%H%M%S", (localtime($self->{timestamp}))) if ( ! defined $self->{backupext} ); ### set platform name ($self->{platform}, @{$self->{platform_suffixes}}) = platform_name(); $self->register_data( backupext => PSGConf::Data::String->new( value => $self->{backupext} ), platform => PSGConf::Data::String->new( value => $self->{platform} ) ); ### look at process table $self->{ps} = new Proc::ProcessTable; ### load modules $self->_load_modules(); return $self; } ############################################################################### ### Load modules ############################################################################### sub _load_modules { my ($self) = @_; my ($type, $module, @args); my ($obj, $line, $method); print $0 . ": loading modules...\n" if ($self->{verbose}); open(MODLIST, "<$self->{modules_file}") || die "open('$self->{modules_file}'): $!\n"; while ($line = ) { ### strip newlines chomp($line); ### strip comments $line =~ s/#.*$//; ### skip empty lines next if (length($line) == 0); ### parse line ($type, @args) = split(/\s+/, $line); if (lc($type) eq 'policy') { $method = shift(@args); die $0 . ": too many arguments in \"policy\" entry" if (@args > 0); die $0 . ": unknown policy method \"$method\"\n" if (!exists($self->{policy}->{$method})); push(@{$self->{policy_order}}, $method); next; } ### not a policy line, so ### it must be a datastore or control entry $module = shift(@args); print $0 . ": TRACE: loading module '$module'\n" if ($self->{trace}); eval "use $module"; die $0 . ": cannot load '$module' module: $@\n" if ($@); if (lc($type) eq 'datastore') { $obj = $module->new($self, @args); push(@{$self->{datastores}}, $obj); next; } if (lc($type) eq 'control') { $obj = $module->new($self, @args); push(@{$self->{controls}}, $obj) if (defined($obj)); next; } ### unknown module type die $0 . ": unknown module type '$type'\n"; } close(MODLIST); } ############################################################################### ### Phase 0 - Access Data Store(s) ############################################################################### sub access_data_stores { my ($self) = @_; my ($obj); print $0 . ": accessing data store(s)...\n" if ($self->{verbose}); foreach $obj (@{$self->{datastores}}) { die $0 . ": data store module '" . ref($obj) . "' failed\n" if (! $self->_call_method($obj, 'read_config')); } } ############################################################################### ### _call_method() - call a method on an object ############################################################################### sub _call_method { my ($self, $obj, $method) = @_; return undef if (! $obj->can($method)); if ($self->{trace}) { my ($class) = ref($obj); print $0 . ": TRACE: calling $class->$method()\n"; } return $obj->$method($self); } ############################################################################### ### Phase 1a - enforce policy ############################################################################### sub enforce_policy { my ($self) = @_; my ($policy, $obj, $method); print $0 . ": invoking policy methods...\n" if ($self->{verbose}); foreach $policy (@{$self->{policy_order}}) { if ( exists $self->{policy}->{$policy} ) { $obj = $self->{policy}->{$policy}->{control_obj}; $method = $self->{policy}->{$policy}->{method}; $self->_call_method($obj, $method); # Delete the policy after we invoked it, so we can see # which ones we did not invoke at all. delete $self->{policy}->{$policy}; } else { warn "\t!!! Could not find the policy ($policy), maybe duplicate policy entries?\n"; } } foreach $policy ( keys %{$self->{policy}} ) { warn "\t!!!Policy registered but not in psgconf_modules '$policy'\n"; } } ############################################################################### ### Phase 1b - instantiate actions ############################################################################### sub instantiate_actions { my ($self) = @_; my ($obj, $class); print $0 . ": instantiating actions...\n" if ($self->{verbose}); foreach $obj (@{$self->{controls}}) { $self->_call_method($obj, 'decide'); } } ############################################################################### ### Phase 2 - process actions ############################################################################### sub process_actions { my ($self) = @_; my ($obj, $ret); print "\n" if ($self->{verbose}); foreach $obj (@{$self->{actions}}) { printf($0 . ': checking %-45s ... ', $obj->name) if ($self->{verbose} && ! $obj->quiet); $ret = $obj->check($self); print "ok\n" if ($ret == 0 && $self->{verbose} && ! $obj->quiet); next if ($ret <= 0); if ($ret > 0) { print "CHANGED\n" if ($self->{verbose} && ! $obj->quiet); $self->{num_changed}++; $self->{need_reboot} = 1 if ($obj->requires_reboot); } } print "\n" if ($self->{verbose}); foreach $obj (grep { $_->changed } @{$self->{actions}}) { if ($self->{do_diffs}) { $obj->diff($self); } elsif (! $self->{do_fix}) { print $0 . ': ' . $obj->name . " requires update\n"; } } if ($self->{do_fix}) { foreach $obj (grep { $_->changed } @{$self->{actions}}) { print $0 . ': updating ' . $obj->name . "\n"; next if ($obj->do($self) == -1); $self->{num_changed}--; } } } ############################################################################### ### Phase 3 - Cleanup ############################################################################### sub _call_cleanup_hooks { my ($self) = @_; my ($obj); print "\n$0: calling cleanup hooks...\n" if ($self->{verbose}); foreach $obj (@{$self->{controls}}) { $self->_call_method($obj, 'cleanup'); } if ($self->{need_reboot}) { print "\n" if ($self->{verbose}); print $0 . ": NOTE: system may need to be rebooted for changes to take effect\n"; print "\n" if ($self->{verbose}); } } sub cleanup { my ($self) = @_; $self->_call_cleanup_hooks(); $self->_clean_tmpdir(); return $self->{num_changed}; } sub _clean_tmpdir { my ($self) = @_; if (defined($self->{tmpdir}) && $self->{rm_tmpfiles}) { print "\n$0: cleaning up...\n" if ($self->{verbose}); rmtree($self->{tmpdir}); delete $self->{tmpdir}; } } ############################################################################### ### DESTROY method ############################################################################### sub DESTROY { my ($self) = @_; $self->_clean_tmpdir(); } ############################################################################### ### AUTOLOAD method ############################################################################### sub AUTOLOAD { my ($self) = @_; my ($name); $name = $AUTOLOAD; $name =~ s/.*:://; return undef if ($name eq 'DESTROY'); # my (@caller); # @caller = (caller(1)); # @caller = (caller(2)) # if ($caller[3] =~ m/^PSG::Abstraction::UserDB::UDB/); # print "[$name] : access from $caller[3]\n"; print $0 . ": TRACE: accessing data object '$name'\n" if ($self->{trace}); # push(@{$self->{data_funcs}->{$name}}, $self->{func}); return $self->{data}->{$name}->{value} if (exists($self->{data}->{$name})); return undef; } sub data_obj { my ($self, $name) = @_; print $0 . ": TRACE: accessing data object '$name'\n" if ($self->{trace}); # push(@{$self->{data_funcs}->{$name}}, $self->{func}); return $self->{data}->{$name} if (exists($self->{data}->{$name})); return undef; } ############################################################################### ### Utility Functions for Modules ### FIXME: these don't really belong here... ############################################################################### sub save_file { my ($self, $file, $copyflag) = @_; my ($savename, $st); $savename = "$file.$self->{backupext}"; if ($copyflag && -f $file) { $st = stat($file); if (! copy($file, $savename)) { warn "\t!!! copy('$file', '$savename'): $!\n"; return 0; } # print "CHMOD(" . $st->mode . ", $savename)\n"; if (! chmod($st->mode, $savename)) { warn "\t!!! chmod(" . $st->mode . ", '$savename'): $!\n"; return 0; } # print "CHOWN(" . $st->uid . ", " . $st->gid . ", $savename)\n"; if (! chown($st->uid, $st->gid, $savename)) { warn "\t!!! chown(%d, %d, '%s'): $!\n", $st->uid, $st->gid, $savename; return 0; } } else { if (! rename($file, $savename)) { warn "\t!!! rename('$file', '$savename'): $!\n"; return 0; } } return 1; } ############################################################################### ### data object registry functions ############################################################################### sub register_data { my ($self, %objs) = @_; map { $self->{data}->{$_} = $objs{$_}; $self->{data_owners}->{$_} = [ scalar(ref($objs{$_})), scalar(caller) ]; } keys %objs; } sub get_data_type { my ($self, $data) = @_; return $self->{data_owners}->{$data}->[0]; } sub get_data_owner { my ($self, $data) = @_; return $self->{data_owners}->{$data}->[1]; } sub get_all_data { my ($self) = @_; return $self->{data_owners}; } ############################################################################### ### action object registry functions ############################################################################### sub register_actions { my ($self, @actions) = @_; push(@{$self->{actions}}, @actions); } sub get_action { my ($self, $action) = @_; ### ### Changed from a foreach loop to a grep to ### help search performance, as reported by dprofpp. ### grep ($_->{name} eq $action && (return $_), @{$self->{actions}}); return undef; } ############################################################################### ### policy method registry functions ############################################################################### sub register_policy { my ($self, $control_obj, %methods) = @_; my ($method); foreach $method (keys %methods) { die("$0: cannot register undefined policy method '$method'\n") if (! $control_obj->can($methods{$method})); # warn "\t!!!Policy registered but not in psgconf_modules '$method'\n" # if ( grep (/^${method}$/, @{$self->{policy_order}})); $self->{policy}->{$method} = { method => $methods{$method}, control_obj => $control_obj }; } } sub get_all_policy { my ($self) = @_; return $self->{policy}; } ############################################################################### ### documentation ############################################################################### 1; __END__ =head1 NAME PSGConf - main Perl module for psgconf package =head1 SYNOPSIS use PSGConf; $psgconf = PSGConf->new(%opts); $psgconf->access_data_stores(); $psgconf->enforce_policy(); $psgconf->instantiate_actions(); $psgconf->process_actions(); $psgconf->cleanup(); =head1 DESCRIPTION The B module provides a modular framework for automating system configuration tasks. No real work is done by the B object; its only function is to coordinate the activity of other objects. For a high-level overview of the B system, see L. =head1 APPLICATION METHODS The B class supports the following methods for use by the B application: =over 4 =item new() Creates a new B object. The arguments are interpretted as a hash of attributes for the object. The following attributes are supported: =over 4 =item config_dir Full path to B config files. Default is F. =item files_dir Full path to B data files. Default is F. =item modules_file Full path to B modules file. Default is F. =item tmpdir Full path to B temporary directory. Default is the environment variable F<$PSGCONF_TMPDIR/$0.$$> or if that is not set, F. =item verbose Boolean value to indicate whether B should be verbose. Default is no. =item do_fix Boolean value to indicate whether B should actually implement necessary changes. (This is done by calling the do() method of any registered action objects.) Default is no. =item restart_daemons Boolean value to indicate whether B should restart daemons when their configuration files are changed. Default is yes. =item rm_tmpfiles Boolean value to indicate whether B should remove files from the temp directory before exiting. Default is yes. =item do_diffs Boolean value to indicate whether B should print diffs to show what changes are needed. Default is no. =item update_pkgs Boolean value to indicate whether B should update software packages. Default is yes. =back The new() method also loads the Control and DataStore modules specified in the modules file. =item access_data_stores() Invokes the read_config() method of all DataStore modules. =item enforce_policy() Invokes the policy methods specified in the modules file. =item instantiate_actions() Invokes the decide() method of all Control modules. =item process_actions() Invokes the check() method of all registered Action objects. Also invokes the diff() and/or do() methods of all registered Action objects, depending on whether the I or I attributes are set. =item cleanup() Invokes the cleanup() method of all Control modules. If the I attribute is enabled, returns the number of actions that were unsuccessful; otherwise, returns the number of actions that need to be performed. =back =head1 UTILITY METHODS The B class supports the following utility methods for use by B modules: =over 4 =item data_obj() Return the Data object named by the argument. =item I Calls the get() method of the Data object named I. =item save_file() Saves a backup copy of the file named by the argument. If the second argument is true, the backup is made by copying instead of renaming (i.e., the original file is not removed). =item register_data() Add Data objects. =item get_data_type() Return the name of the Data module that provided the specified Data object. =item get_data_owner() Return the name of the Control module that provided the specified Data object. =item get_all_data() Return a hash mapping Data object names to an array containing their type and the name of the module the provided them. =item register_actions() Add Action objects. =item get_action() Find a named Action object. =item register_policy() Add policy methods. =item get_all_policy() Return a hash mapping policy method names to the name of the Control module that provided them. =back =head1 SEE ALSO L L =cut