###
###  Copyright 2000-2007 University of Illinois Board of Trustees
###  All rights reserved. 
###
###  TSM.pm - TSM module for psgconf
###
###  Campus Information Technologies and Educational Services
###  University of Illinois at Urbana-Champaign
###


package PSGConf::Control::TSM;

use strict;

use POSIX;

use PSGConf::Action::GenerateFile::dsm_opt;
use PSGConf::Action::GenerateFile::dsm_sys;
use PSGConf::Action::GenerateFile::tsm_inclexcl;
use PSGConf::Action::GenerateFile::Literal;
use PSGConf::Action::RestartDaemon;
use PSGConf::Data::Boolean;
use PSGConf::Data::Hash;
use PSGConf::Data::List;
use PSGConf::Data::Integer;
use PSGConf::Data::String;
use PSGConf::Control::Packages qw(_add_pkgs);


###############################################################################
###  Constructor
###############################################################################

sub new
{
	my ($class, $psgconf) = @_;
	my ($self);

	$self = {};
	bless($self, $class);

	### So that _add_pkgs knows which directives to look at
	$self->{name} = 'tsm';
	$self->{enable} = $self->{name} . '_enable';
	$self->{packages} = $self->{name} . '_packages';

	$psgconf->register_data(
		tsm_enable		=> PSGConf::Data::Boolean->new(
						value => 'false'
					),
		tsm_config_dir		=> PSGConf::Data::String->new(
						'value_abspath' => 1,
						value => '/usr/bin'
					),
		tsm_packages		=> PSGConf::Data::List->new(),
		tsm_df_command		=> PSGConf::Data::String->new(
						'value_abspath' => 1,
						value => '/usr/local/bin/df'
					),
		tsm_domain		=> PSGConf::Data::Hash->new(
						value_optional => 1,
						key_abspath => 1
					),
		tsm_domain_tree		=> PSGConf::Data::Hash->new(
						value_optional => 1,
						key_abspath => 1
					),
		tsm_domain_ignore	=> PSGConf::Data::Hash->new(
						value_optional => 1,
						key_abspath => 1
					),
		tsm_include		=> PSGConf::Data::Hash->new(
						value_type => 'HASH'
					),
		tsm_exclude		=> PSGConf::Data::Hash->new(
						value_type => 'HASH'
					),
		tsm_server		=> PSGConf::Data::String->new(),
		tsm_port		=> PSGConf::Data::Integer->new(
						value => 1500
					),
		tsm_options		=> PSGConf::Data::Hash->new()
	);

	$psgconf->register_policy($self,
		tsm_check_server	=> '_policy_check_server',
		tsm_canonify_logfiles	=> '_policy_canonify_logfiles',
		tsm_modify_rc_vars	=> '_policy_modify_rc_vars',
		tsm_add_rc_scripts	=> '_policy_add_rc_scripts',
		tsm_add_domain_trees	=> '_policy_add_domain_trees',
		tsm_default_virtualmountp
					=> '_policy_default_virtualmountp',
		tsm_default_inclexcl	=> '_policy_default_inclexcl',
		tsm_add_packages		=> '_add_pkgs'
	);

	return $self;
}


###############################################################################
###  policy methods
###############################################################################

sub _policy_modify_rc_vars
{
     my ($self, $psgconf) = @_;

     $psgconf->data_obj('rc_vars')->insert(
          { 'linux_enable'  => 'YES' }
     ) if ($psgconf->data_obj('platform')->match('freebsd') &&
		$psgconf->data_obj('tsm_enable')->equals('true') );
}

sub _policy_check_server
{
	my ($self, $psgconf) = @_;

	$psgconf->data_obj('tsm_enable')->set('false')
		if (! defined $psgconf->data_obj('tsm_server')->get());
}


### create RC script
sub _policy_add_rc_scripts
{
	my ($self, $psgconf) = @_;

	$psgconf->data_obj('rc_scripts')->insert(
		{ 'tsmsched' => { 'state' => 'enable' }}
	) if ( $psgconf->data_obj('tsm_enable')->equals('true') );
}


### prepend log_dir to logfiles if they're not absolute paths
sub _policy_canonify_logfiles
{
	my ($self, $psgconf) = @_;
	my ($log_dir, $opts);

	return
		if ( $psgconf->data_obj('tsm_enable')->equals('false') );

	$log_dir = $psgconf->data_obj('log_dir')->get();
	$opts = $psgconf->data_obj('tsm_options')->get();

	$opts->{Errorlogname} = $log_dir . '/' . $opts->{Errorlogname}
		if (exists($opts->{Errorlogname})
		    && substr($opts->{Errorlogname}, 0, 1) ne '/');
	$opts->{Schedlogname} = $log_dir . '/' . $opts->{Schedlogname}
		if (exists($opts->{Schedlogname})
		    && substr($opts->{Schedlogname}, 0, 1) ne '/');
}


### calculate TSM domain
sub _policy_add_domain_trees
{
	my ($self, $psgconf) = @_;
	my ($domain, $domain_tree);

	return
		if ( $psgconf->data_obj('tsm_enable')->equals('false') );

	$domain = $psgconf->data_obj('tsm_domain')->get();
	$domain_tree = $psgconf->data_obj('tsm_domain_tree')->get();

	map { 
		$domain->{$_} = undef
			if (-d $_);
	} (keys %$domain_tree,
	   _filesystems_beneath($psgconf->data_obj('tsm_df_command')->get(),
			keys %$domain_tree));
	map { delete $domain->{$_}; }
		(keys %{$psgconf->data_obj('tsm_domain_ignore')->get()});
}


### calculate virtual mount points
sub _policy_default_virtualmountp
{
	my ($self, $psgconf) = @_;
	my ($opts);

	return
		if ( $psgconf->data_obj('tsm_enable')->equals('false') );

	$opts = $psgconf->data_obj('tsm_options')->get();

	$opts->{virtualmountp} = [
		_not_a_filesystem($psgconf->data_obj('tsm_df_command')->get(),
				  sort keys %{$psgconf->data_obj('tsm_domain')->get()})
	]
		if (! exists($opts->{virtualmountp}));
}


### set inclexcl file if needed
sub _policy_default_inclexcl
{
	my ($self, $psgconf) = @_;
	my ($opts);

	return
		if ( $psgconf->data_obj('tsm_enable')->equals('false') );

	$opts = $psgconf->data_obj('tsm_options')->get();

	$opts->{INCLEXCL} = $psgconf->data_obj('tsm_config_dir')->get()
					    . '/inex.list'
		if (! exists($opts->{INCLEXCL})
		    && ( $psgconf->data_obj('tsm_include')->count()
		        || $psgconf->data_obj('tsm_exclude')->count() ));
}


###############################################################################
###  decide() method
###############################################################################

sub decide
{
	my ($self, $psgconf) = @_;
	my ($config_dir, $server, $opts, $include, $exclude);
	my ($label, $domain);

	return
		if ( $psgconf->data_obj('tsm_enable')->equals('false') );

	$config_dir = $psgconf->data_obj('tsm_config_dir')->get();
	$server = $psgconf->data_obj('tsm_server')->get();
	$include = $psgconf->data_obj('tsm_include')->get();
	$exclude = $psgconf->data_obj('tsm_exclude')->get();
	$opts = $psgconf->data_obj('tsm_options')->get();
	$domain = $psgconf->data_obj('tsm_domain')->get();

	### determine server label
	$label = $server;
	$label =~ s/\..*//;

	$psgconf->register_actions(
		PSGConf::Action::GenerateFile::dsm_sys->new(
			'name'		=> $config_dir . '/dsm.sys',
			'comment_str'	=> '***',
			'description'	=> 'TSM configuration file',
			'servers'	=> {
						$label => {
							server => $server,
							port => $psgconf->data_obj('tsm_port')->get(),
							opts => $opts
						}
					}
		),
		PSGConf::Action::GenerateFile::dsm_opt->new(
			'name'		=> $config_dir . '/dsm.opt',
			'comment_str'	=> '***',
			'description'	=> 'TSM options file',
			'server'	=> $label,
			'domain'	=> [ keys %$domain ]
		)
	);

	$psgconf->register_actions(
		PSGConf::Action::GenerateFile::tsm_inclexcl->new(
			'name'		=> $config_dir . '/inex.list',
			'comment_str'	=> undef,
			'include'	=> $include,
			'exclude'	=> $exclude
		)
	)
		if (%$include || %$exclude);

	###
	### Hacks to run TSM on FreeBSD systems
	###
	$psgconf->register_actions(
		PSGConf::Action::GenerateFile::Literal->new(
			'name'		=> '/compat/linux/etc/mtab',
			'comment_str'	=> undef,
			'content'	=> '/ / ext3 rw 0 0'
		),
		PSGConf::Action::GenerateFile::Literal->new(
			'name'		=> '/compat/linux/etc/ld.so.conf',
			'comment_str'	=> undef,
			'content'	=> join ("\n",
							( '/lib',
							'/usr/lib',
							'/opt/tivoli/tsm/client/ba/bin',
							'/opt/tivoli/tsm/client/api/bin' ))
		)
	) if ( $psgconf->data_obj('platform')->match('freebsd') );

	$psgconf->register_actions(
		PSGConf::Action::RestartDaemon->new(
			name           => 'TSM',
			rcscript       => $psgconf->data_obj('rc_scripts')->find('tsmsched')->{fullname},
			use_restart	=> 'true',
			filename       => [ 
					$config_dir . '/dsm.sys',
					$config_dir . '/dsm.opt',
					$config_dir . '/inex.list' ]
		)
	);
}


###############################################################################
###  Utility functions
###############################################################################

# Arguments: $_[0] ... - List of directories
# Return value: List of filesystems beneath any listed directory
sub _filesystems_beneath
{
	my ($df_cmd, @dirs) = @_;
	my (@fs_tmp, %test_fs, %filesystems, %beneath, $line, $fs);

	### Get the list of all (mounted) filesystems
	@fs_tmp = `$df_cmd`
		if ( defined $df_cmd );
	foreach $line (@fs_tmp)
	{
		$fs = (split(/\s+/, $line))[5];
		$filesystems{$fs} = 1
			if (substr($fs, 0, 1) eq '/');
	}

	map { $test_fs{$_} = 1; } @dirs;
	foreach $fs (keys %test_fs)
	{
		### If the higher-up fs name fits into the lower one, the
		### lower one is a child filesystem.
		map {
			$beneath{$_} = 1
				if (($_ =~ m/^$fs/)
				    && !exists($test_fs{$_}));
		} keys %filesystems;
	}

	return sort keys %beneath;
}


# Arguments: $_[0] ... - List of directories
# Return value: Elements of argument list not representing filesystems
sub _not_a_filesystem
{
	my ($df_cmd, @dirs) = @_;
	my (@fs_tmp, %test_fs, %filesystems, %beneath, $line, $fs);

	### Get the list of all (mounted) filesystems
	@fs_tmp = `$df_cmd`
		if ( defined $df_cmd );
	foreach $line (@fs_tmp)
	{
		$fs = (split(/\s+/, $line))[5];
		$filesystems{$fs} = 1
			if (substr($fs, 0, 1) eq '/');
	}

	map { $test_fs{$_} = 1; } @dirs;
 
	### Anything left after this is not a filesystem
	map { delete $test_fs{$_}; } keys %filesystems;

	return sort keys %test_fs;
}


###############################################################################
###  documentation
###############################################################################

1;

__END__

=head1 NAME

PSGConf::Control::TSM - psgconf control class for TSM backup client

=head1 SYNOPSIS

In F<psgconf_modules>:

  Control PSGConf::Control::TSM

=head1 DESCRIPTION

The B<PSGConf::Control::TSM> module provides a B<psgconf> control object
for configuring the TSM backup client.  It supports the following methods:

=over 4

=item new()

The constructor.  Its parameter is a reference to the B<PSGConf>
object.  It registers the following data objects:

=over 4

=item I<tsm_enable>

A B<PSGConf::Data::Boolean> object indicating whether TSM should be
configured.

=item I<tsm_config_dir>

A B<PSGConf::Data::String> object containing the absolute path to the
TSM config directory.  The default is F</usr/bin> (yuck).

=item I<tsm_df_command>

A B<PSGConf::Data::String> object containing the df(1) command to be
used when generating a list of filesystems to be backed up.  Note that
this command must have the same output format as GNU df(1).  The default
is F</usr/local/bin/df>.

=item I<tsm_domain_tree>

A B<PSGConf::Data::Hash> object whose keys are a list of absolute paths
to trees to be added to the TSM domain.  For each tree, all filesystems
under the tree will be added to the domain.  If the tree itself is not a
mount point, it will be added to the domain, and a C<virtualmountpoint>
directive will be used for it.

=item I<tsm_domain>

A B<PSGConf::Data::Hash> object whose keys are a list of absolute paths
to filesystems to be added to the TSM domain.  Unlike
I<tsm_domain_tree>, these paths do not check the contents of the
directory; they are assumed to be valid mount points and are added to
the domain with no further processing.

=item I<tsm_domain_ignore>

A B<PSGConf::Data::Hash> object whose keys are a list of absolute paths
to filesystems that should never be added to the TSM domain.  This
allows the exclusion of certain filesystems even if using
I<tsm_domain_tree> on a higher-level directory.

=item I<tsm_include>

A B<PSGConf::Data::Hash> object that indicates what files should be
included.  The hash key is the type of file, which is used as an
extension to the TSM C<include> directive (i.e., a key of C<dir> will
add to the C<include.dir> directive).  The key can be set to C<-> to use
no extension (i.e., for normal files).  The hash value is a reference to
a hash whose keys are the files to include.

=item I<tsm_exclude>

A B<PSGConf::Data::Hash> object that indicates what files should be
excluded.  The hash key is the type of file, which is used as an
extension to the TSM C<exclude> directive (i.e., a key of C<dir> will
add to the C<exclude.dir> directive).  The key can be set to C<-> to use
no extension (i.e., for normal files).  The hash value is a reference to
a hash whose keys are the files to exclude.

=item I<tsm_options>

A B<PSGConf::Data::Hash> object containing options for the F<dsm.sys>
file.

=item I<tsm_port>

A B<PSGConf::Data::Integer> object containing the port number of the TSM
server.  The default is 1500.

=item I<tsm_packages>

A B<PSGConf::Data::List> object containing the packages to install.

=item I<tsm_server>

A B<PSGConf::Data::String> object containing the hostname of the TSM
server.

=back

The constructor also registers the following policy methods:

=over 4

=item I<tsm_check_server>

If I<tsm_server> is not set, unsets I<tsm_enable>.

=item I<tsm_canonify_logfiles>

If the C<Errorlogname> or C<Schedlogname> entries in the I<tsm_options>
data object are set to relative paths, prepends the value of the
I<log_dir> data object (provided by B<PSGConf::Control::Core>).

=item I<tsm_add_rc_scripts>

Creates an RC script to start the TSM scheduler at boot time.  This is
done using the I<rc_scripts> data object, which is provided by the
B<PSGConf::Control::InitScripts> module.

=item I<tsm_add_domain_trees>

For each entry in I<tsm_domain_tree>, adds the entry and all filesystems
mounted beneath it to I<tsm_domain>.  Then, for each entry in
I<tsm_domain_ignore>, deletes the corresponding entries in I<tsm_domain>.

=item I<tsm_default_virtualmountp>

If the I<tsm_options> data object does not contain an entry for the
C<virtualmountp> option, set it to a list of entries in I<tsm_domain>
that do not correspond to mounted filesystems.

=item I<tsm_default_inclexcl>

If the I<tsm_include> or I<tsm_exclude> objects are set and the
I<tsm_options> object does not contain an entry for the C<INCLEXCL>
option, set it to point to the file F<inex.list> in the directory
specified by the I<tsm_config_dir> object.

=back

=item decide()

If I<tsm_enable> is set, registers the following action objects:

=over 4

=item *

Registers a B<PSGConf::Action::GenerateFile::dsm_opt> object to create
I<tsm_config_dir>F</dsm.opt>.

=item *

Registers a B<PSGConf::Action::GenerateFile::dsm_sys> object to create
I<tsm_config_dir>F</dsm.sys>.

=item *

If the I<tsm_include> or I<tsm_exclude> objects are set, registers
a B<PSGConf::Action::GenerateFile::tsm_inclexcl> object to create
I<tsm_config_dir>F</inex.list>.

=back

=back

=head1 BUGS

There is no support for multiple TSM servers.

The scheduler is not restarted if it's in the middle of running a
backup, which means that it's possible for config changes to never be
noticed.

The module runs an external df(1) program in order to determine
mountpoints.  It shouldn't need to invoke an external program to do this.

=head1 SEE ALSO

L<perl>

L<PSGConf>

L<PSGConf::Action::GenerateFile::dsm_opt>

L<PSGConf::Action::GenerateFile::dsm_sys>

L<PSGConf::Action::GenerateFile::tsm_inclexcl>

L<PSGConf::Action::RestartDaemon>

L<PSGConf::Control::Core>

L<PSGConf::Control::InitScripts>

L<PSGConf::Control::Packages>

L<PSGConf::Data::Boolean>

L<PSGConf::Data::Hash>

L<PSGConf::Data::List>

L<PSGConf::Data::Integer>

L<PSGConf::Data::String>

L<psgconf-intro>

=cut



syntax highlighted by Code2HTML, v. 0.9.1