###
###  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 = <MODLIST>)
	{
		### 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<PSGConf> module provides a modular framework for automating system
configuration tasks.  No real work is done by the B<PSGConf> object;
its only function is to coordinate the activity of other objects.

For a high-level overview of the B<psgconf> system, see
L<psgconf-intro>.

=head1 APPLICATION METHODS

The B<PSGConf> class supports the following methods for use by the
B<psgconf> application:

=over 4

=item new()

Creates a new B<PSGConf> 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<psgconf> config files.  Default is
F</usr/local/share/psgconf/config>.

=item files_dir

Full path to B<psgconf> data files.  Default is
F</usr/local/share/psgconf/files>.

=item modules_file

Full path to B<psgconf> modules file.  Default is
F</usr/local/etc/psgconf_modules>.

=item tmpdir

Full path to B<psgconf> temporary directory.  Default is the
environment variable F<$PSGCONF_TMPDIR/$0.$$> or if that
is not set, F</var/tmp/$0.$$>.

=item verbose

Boolean value to indicate whether B<psgconf> should be verbose.  Default
is no.

=item do_fix

Boolean value to indicate whether B<psgconf> 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<psgconf> should restart daemons when
their configuration files are changed.  Default is yes.

=item rm_tmpfiles

Boolean value to indicate whether B<psgconf> should remove files from
the temp directory before exiting.  Default is yes.

=item do_diffs

Boolean value to indicate whether B<psgconf> should print diffs to show
what changes are needed.  Default is no.

=item update_pkgs

Boolean value to indicate whether B<psgconf> 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<do_fix> or I<do_diff> attributes are set.

=item cleanup()

Invokes the cleanup() method of all Control modules.  If the I<do_fix>
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<PSGConf> class supports the following utility methods for use by
B<psgconf> modules:

=over 4

=item data_obj()

Return the Data object named by the argument.

=item I<object_name>

Calls the get() method of the Data object named I<object_name>.

=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<perl>

L<psgconf-intro>

=cut



syntax highlighted by Code2HTML, v. 0.9.1