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


package PSGConf::Control::Network;

use strict;

use NetAddr::IP;

use PSGConf::Action::GenerateFile::Literal;
use PSGConf::Action::GenerateFile::TLI_hosts;
use PSGConf::Action::GenerateFile::hosts;
use PSGConf::Action::GenerateFile::netmasks;
use PSGConf::Action::GenerateFile::EnvFile;
use PSGConf::Action::Remove;
use PSGConf::Action::RunCommand;
use PSGConf::Data::Boolean;
use PSGConf::Data::Hash;
use PSGConf::Data::String;
use PSGConf::Util;


###############################################################################
###  PSGConf::Action::RunCommand plugin for AIX TCP/IP config
###############################################################################

sub _check_AIX_net_config
{
	my ($action, $psgconf) = @_;
	my (@fields, @values, $line, $ct, %settings);

	### run mktcpip to get current settings for $if
	if (!open(MKTCPIP,
		  "/usr/sbin/mktcpip -S $action->{pri_if}->{name} 2>&1 |"))
	{
		warn "\n\t!!! cannot run mktcpip: $!\n";
		return -1;
	}
	$line = <MKTCPIP>;
	chomp($line);
	$line =~ s/^#//;
	@fields = split(/:/, $line);
	$line = <MKTCPIP>;
	chomp($line);
	@values = split(/:/, $line);
	close(MKTCPIP);
	for ($ct = 0; $ct < @fields; $ct++)
	{
		$settings{$fields[$ct]} = $values[$ct];
	}

	### convert netmask from hex to dotted-decimal
	if ($settings{mask} =~ m/^0x/)
	{
		my $tmp = $settings{mask};

		$tmp =~ s/^0x//;
		$tmp = join('.', unpack('C*', pack('H8', $tmp)));

		$settings{mask} = $tmp;
	}

	### see if anything needs to be changed
	if ($settings{host} ne $psgconf->data_obj('hostname')->get()
	    || $settings{addr} ne $action->{pri_if}->{addr}
	    || $settings{mask} ne $action->{pri_if}->{mask}
	    || $settings{gateway} ne $action->{pri_if}->{gateway})
	{
		return 1;
	}

	return 0;
}


###############################################################################
###  internal utility function
###############################################################################

### returns:
###   hash containing network name for each interface
###   name of default interface
###   default network
sub _get_if_nets
{
	my ($psgconf, $ifs, $nets, $addrs, $hostname) = @_;
	my ($net, $if, $addr);
	my (%if_nets, $default_if, $default_net);

	foreach $if (keys %$ifs)
	{
		next
			if ( $ifs->{$if} eq 'DHCP' );

		$addr = (get_addrs($psgconf, $ifs->{$if}))[0];
		$addr = new NetAddr::IP $addr;

		foreach $net (sort keys %$nets)
		{
			if ($nets->{$net}->contains($addr))
			{
				$if_nets{$if} = $net;
				last;
			}
		}

		### note primary interface
		if ($ifs->{$if} eq $hostname)
		{
			$default_if = $if;
			$default_net = $if_nets{$if};
		}
	}

	return ($default_if, $default_net, %if_nets);
}


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

### instantiate networks
sub _policy_validate_networks
{
	my ($self, $psgconf) = @_;
	my ($nets, $net, $netobj);

	$nets = $psgconf->data_obj('networks')->get();

	foreach $net (keys %$nets)
	{
		$netobj = new NetAddr::IP $nets->{$net};
		if (!defined($netobj))
		{
			warn "\n\t!!!invalid network specification \"$nets->{$net}\"\n";
			delete $nets->{$net};
			next;
		}
		$nets->{$net} = $netobj;
	}
}


### default gateways
sub _policy_default_gateways
{
	my ($self, $psgconf) = @_;
	my ($gws, $nets);

	$nets = $psgconf->data_obj('networks')->get();
	$gws = $psgconf->data_obj('network_gateways')->get();

	map {
		$gws->{$_} = $nets->{$_}->last()
			if (!defined($gws->{$_}));
	} keys %$nets;
}


### Handle any DHCP interfaces
sub _policy_dhcp_interfaces
{
	my ($self, $psgconf) = @_;
	my ($ifs, $dhcp_enabled);

	$ifs = $psgconf->data_obj('network_interfaces')->get();

	map {
		if ($ifs->{$_} eq 'DHCP')
		{
			$dhcp_enabled = 1;
		}
	} keys %$ifs;

	return
		if ( ! $dhcp_enabled );

	### Turn off modifying the /etc/resolv.conf files
	$psgconf->data_obj('dns_servers')->unset();

	### as well as the hosts/ipnodes files as well.
	$psgconf->data_obj('hosts_enable')->set('false');
}


### network interfaces default to hostname
sub _policy_default_interfaces
{
	my ($self, $psgconf) = @_;
	my ($ifs, $gotcha);

	$ifs = $psgconf->data_obj('network_interfaces')->get();

	map {
		if (!defined($ifs->{$_}))
		{
			die $0 . ": only one network interface can use default address\n"
				if ($gotcha);

			$ifs->{$_} = $psgconf->data_obj('hostname')->get();
			$gotcha = 1;
		}
	} keys %$ifs;
}


### add /etc/hosts entries for network interfaces
sub _policy_add_etc_hosts
{
	my ($self, $psgconf) = @_;
	my ($addrs);

	$addrs = $psgconf->data_obj('host_addrs')->get();

	map {
		$addrs->{$_} = undef
			if (!exists($addrs->{$_}));
	} values %{$psgconf->data_obj('network_interfaces')->get()};
}


### look up /etc/hosts entries
sub _policy_default_etc_hosts
{
	my ($self, $psgconf) = @_;
	my ($addrs);

	$addrs = $psgconf->data_obj('host_addrs')->get();

	map {
		if ( $_ eq 'DHCP' ) {
			delete $addrs->{$_};
		} else {
			$addrs->{$_} = (get_addrs($psgconf, $_))[0]
				if (!defined($addrs->{$_}));
		}
	} keys %$addrs;
}


### update FreeBSD /etc/rc.conf entries
sub _policy_modify_rc_vars
{
	my ($self, $psgconf) = @_;
	my ($rc_vars, $addrs, $nets, $ifs, $gws, $hostname);
	my (%if_nets, $default_if, $default_net);

	return
		if (! $psgconf->data_obj('platform')->match('freebsd') );

	$rc_vars = $psgconf->data_obj('rc_vars')->get();
	$hostname = $psgconf->data_obj('hostname')->get();
	$ifs = $psgconf->data_obj('network_interfaces')->get();
	$nets = $psgconf->data_obj('networks')->get();
	$addrs = $psgconf->data_obj('host_addrs')->get();
	$gws = $psgconf->data_obj('network_gateways')->get();

	($default_if, $default_net, %if_nets) = _get_if_nets($psgconf, $ifs, $nets,
								$addrs, $hostname);

	$rc_vars->{'hostname'} = $hostname;
	$rc_vars->{'defaultrouter'} = $gws->{$default_net};

	map {
		if ( $ifs->{$_} eq 'DHCP' ) {
			$rc_vars->{'ifconfig_' . $_} = 'DHCP';
		} else {
			$rc_vars->{'ifconfig_' . $_} = 'inet '
					       . $addrs->{$ifs->{$_}}
					       . ' netmask '
					       . $nets->{$if_nets{$_}}->mask();
		}
	} (sort keys %$ifs);
}


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

sub decide
{
	my ($self, $psgconf) = @_;
	my ($ifs, $nets, $gws, $addrs, $aliases, $etc_inet_dir);
	my ($default_if, $default_net, %if_nets, %vars);

	$etc_inet_dir = $psgconf->data_obj('etc_inet_dir')->get();
	$nets = $psgconf->data_obj('networks')->get();
	$gws = $psgconf->data_obj('network_gateways')->get();
	$ifs = $psgconf->data_obj('network_interfaces')->get();
	$addrs = $psgconf->data_obj('host_addrs')->get();
	$aliases = $psgconf->data_obj('host_aliases')->get();

	($default_if, $default_net, %if_nets) = _get_if_nets($psgconf, $ifs, $nets,
						$addrs, $psgconf->data_obj('hostname')->get());

	### Set the DHCP client id field from the hostname
	if ( ! defined $psgconf->data_obj('dhclient_hostname')->get() ) {
		my ($shost) = $psgconf->data_obj('hostname')->get();
		$shost =~ s/\..*$//;
		$psgconf->data_obj('dhclient_hostname')->set($shost);
	}

	### create /etc/defaultrouter under Solaris
	$psgconf->register_actions(
		PSGConf::Action::GenerateFile::Literal->new(
			name		=> '/etc/defaultrouter',
			comment_str	=> undef,
			content		=> $gws->{$default_net} . "\n",
			requires_reboot	=> 1
		)
	) if ($psgconf->data_obj('platform')->match('solaris')
		&& exists $gws->{$default_net}
		&& $psgconf->data_obj('use_static_routes')->equals('true'));

	### create /etc/hosts
	$psgconf->register_actions(
		PSGConf::Action::GenerateFile::hosts->new(
			name		=> $etc_inet_dir . '/hosts',
			description	=> 'Internet host table',
			requires_reboot	=> 1,
			ip_addrs	=> { map {
				$addrs->{$_} => [
					$_,
					sort keys %{$aliases->{$_}}
				] } keys %$addrs
			}
		)
	) if ($psgconf->data_obj('hosts_enable')->equals('true'));

	if ($psgconf->data_obj('platform')->match('solaris|freebsd'))
	{
		### create /etc/netmasks
		$psgconf->register_actions(
			PSGConf::Action::GenerateFile::netmasks->new(
				name		=> $etc_inet_dir . '/netmasks',
				description	=> 'network address mask table',
				requires_reboot	=> 1,
				networks	=> { map {
					$nets->{$_}->addr() => {
						mask	=> $nets->{$_}->mask(),
						comment	=> $_
					} } (values %if_nets)
				}
			)
		);
	}

	if ($psgconf->data_obj('platform')->match('solaris'))
	{
		$psgconf->register_actions(

			### create TLI hosts files
			(map {
				PSGConf::Action::GenerateFile::TLI_hosts->new(
					name		=> "/etc/net/$_/hosts",
					description	=> 'RPC hosts',
					hostname	=> $psgconf->data_obj('hostname')->get(),
					requires_reboot	=> 1
				)
			} qw(ticlts ticots ticotsord)),

			### create /etc/nodename
			PSGConf::Action::GenerateFile::Literal->new(
				name		=> '/etc/nodename',
				content		=> $psgconf->data_obj('hostname')->get() . "\n",
				comment_str	=> undef,
				requires_reboot	=> 1
			),

			### create /etc/hostname.* (and maybe /etc/dhcp.*)
			### for each interface
			(map {
				PSGConf::Action::GenerateFile::Literal->new(
					name	=> "/etc/dhcp.$_",
					comment_str	=> undef,
					content => ($_ eq $default_if)? "primary\n": ''
				) if ($ifs->{$_} eq 'DHCP');

				### If we are running DHCP, use the short hostname
				### as the host-name (#12) we send to the server.
				PSGConf::Action::GenerateFile::Literal->new(
					name		=> "/etc/hostname.$_",
					content	=> (($ifs->{$_} eq 'DHCP')?
								'' :
								$ifs->{$_}) . "\n",
					comment_str	=> undef,
					backup		=> 0,
					requires_reboot	=> 1
				);
			} sort keys %$ifs),

			### remove any other /etc/hostname.* files
			(map {
				PSGConf::Action::Remove->new(
					name		=> $_,
					requires_reboot	=> 1
				)
			} grep { m|^/etc/hostname\.(.*)$| &&
				 !exists($ifs->{$1}); }
				glob('/etc/hostname.*')),

			### and remove any other /etc/dhcp.* files
			(map {
				PSGConf::Action::Remove->new(
					name		=> $_,
					requires_reboot	=> 1
				)
			} grep { m|^/etc/dhcp\.(.*)$| &&
				 !exists($ifs->{$1}); }
				glob('/etc/dhcp.*'))
		);
	}
	elsif ($psgconf->data_obj('platform')->match('aix'))
	{
		my ($pri_if);

		$pri_if = {
			name	=> $default_if,
			addr	=> $addrs->{$psgconf->data_obj('hostname')->get()},
			mask	=> $nets->{$default_net}->mask(),
			gateway	=> $gws->{$default_net}
		};

		$psgconf->register_actions(
			PSGConf::Action::RunCommand->new(
				name		=> 'AIX TCP/IP settings',
				pri_if		=> $pri_if,
				check_func	=> \&_check_AIX_net_config,
				command		=> '/usr/sbin/mktcpip -h '
						   . $psgconf->data_obj('hostname')->get()
						   . ' -a '
						   . $pri_if->{addr}
						   . ' -i '
						   . $pri_if->{name}
						   . ' -m '
						   . $pri_if->{mask}
						   . ' -g '
						   . $pri_if->{gateway},
				requires_reboot	=> 1
			)
		);
	}
	elsif ($psgconf->data_obj('platform')->match('(-rhel-)|(-rhl-)|(-fc-)'))
	{
		$vars{'NETWORKING'} = 'yes';
		$vars{'HOSTNAME'} = $psgconf->data_obj('hostname')->get()
			if ( defined $psgconf->data_obj('hostname')->get() );
		$vars{'GATEWAY'} = $gws->{$default_net}
			if ( defined ($gws->{$default_net}) );
		$psgconf->register_actions(

			### create /etc/sysconfig/network
			PSGConf::Action::GenerateFile::EnvFile->new(
				name		=> '/etc/sysconfig/network',
				vars		=> \%vars,
				comment_str	=> undef,
				requires_reboot	=> 1
			),

			### create ifcfg-* for each interface
			(map {
				if ( $ifs->{$_} eq 'DHCP' ) {
					PSGConf::Action::GenerateFile::EnvFile->new(
						name	=> "/etc/sysconfig/network-scripts/ifcfg-$_",
						vars	=> {
							DEVICE	=> $_,
							BOOTPROTO => 'dhcp',
							TYPE		=> 'Ethernet',
							ONBOOT	=> 'yes'
						},
						comment_str	=> undef,
						backup		=> 0,
						requires_reboot	=> 1
					),
					PSGConf::Action::GenerateFile::Literal->new(
						name	=> '/etc/dhclient.conf',
						content => 'send host-name "'
								. $psgconf->data_obj('dhclient_hostname')->get() . "\";\n"
								. $psgconf->data_obj('dhclient_literal')->get()
					)
				} else {
					PSGConf::Action::GenerateFile::EnvFile->new(
						name	=> "/etc/sysconfig/network-scripts/ifcfg-$_",
						vars	=> {
							DEVICE	=> $_,
							BOOTPROTO => 'static',
							BROADCAST => $nets->{$if_nets{$_}}->broadcast()->addr(),
							IPADDR	=> $addrs->{$ifs->{$_}},
							NETMASK	=> $nets->{$if_nets{$_}}->mask(),
							NETWORK	=> $nets->{$if_nets{$_}}->addr(),
							ONBOOT	=> 'yes'
						},
						comment_str	=> undef,
						backup		=> 0,
						requires_reboot	=> 1
					)
				}
			} sort keys %$ifs),

			### remove any other ifcfg-* files
			(map {
				PSGConf::Action::Remove->new(
					name		=> $_,
					requires_reboot	=> 1
				)
			} grep { m|^/etc/sysconfig/network-scripts/ifcfg-(.*)$|
				 && $1 ne 'lo'
				 && !exists($ifs->{$1}); }
				glob('/etc/sysconfig/network-scripts/ifcfg-*'))

		);
	}
	elsif ($psgconf->data_obj('platform')->match('freebsd'))
	{
		(map {
			$psgconf->register_actions(
				PSGConf::Action::GenerateFile::Literal->new(
					name	=> '/etc/dhclient.conf',
					content => 'send host-name "'
							. $psgconf->data_obj('dhclient_hostname')->get() . "\";\n"
							. $psgconf->data_obj('dhclient_literal')->get()
				)
			) if ( $ifs->{$_} eq 'DHCP' );
		} sort keys %$ifs);
	}
}


###############################################################################
###  constructor
###############################################################################

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

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

	$self->{name} = 'Network';

	$psgconf->register_data(
		use_static_routes	=> PSGConf::Data::Boolean->new(
						value => 'true'
					),
		hosts_enable		=> PSGConf::Data::Boolean->new(
						value => 'false'
					),
		host_addrs		=> PSGConf::Data::Hash->new(
						value_optional => 1
					),
		host_aliases		=> PSGConf::Data::Hash->new(
						value_type => 'HASH'
					),
		dhclient_hostname	=> PSGConf::Data::String->new(),
		dhclient_literal	=> PSGConf::Data::String->new(),
		networks		=> PSGConf::Data::Hash->new(),
		network_gateways	=> PSGConf::Data::Hash->new(),
		network_interfaces	=> PSGConf::Data::Hash->new(
						value_optional => 1
					)
	);

	$psgconf->register_policy($self,
		net_validate_networks	=> '_policy_validate_networks',
		net_default_gateways	=> '_policy_default_gateways',
		net_default_interfaces	=> '_policy_default_interfaces',
		net_add_etc_hosts	=> '_policy_add_etc_hosts',
		net_default_etc_hosts	=> '_policy_default_etc_hosts',
		net_modify_rc_vars	=> '_policy_modify_rc_vars',
		net_dhcp_interfaces => '_policy_dhcp_interfaces'
	);

	return $self;
}


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

1;

__END__

=head1 NAME

PSGConf::Control::Network - psgconf control class for network configuration

=head1 SYNOPSIS

In F<psgconf_modules>:

  Control PSGConf::Control::Network

=head1 DESCRIPTION

The B<PSGConf::Control::Network> module provides a B<psgconf> control object
for configuring system networking.  It provides 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<hosts_enable>

A B<PSGConf::Data::Boolean> object that indicates whether the
F<I<etc_inet_dir>/hosts> file should be generated.

=item I<host_addrs>

A B<PSGConf::Data::Hash> object that adds entries for the
F<I<etc_inet_dir>/hosts> file.  The key is the hostname, and the value is
its IP address.  If the value is not defined, it will be looked up in DNS.

=item I<host_aliases>

A B<PSGConf::Data::Hash> object that lists aliases for entries in the
I<host_addrs> object.  The key is the hostname, and the value is a
reference to a hash whose keys are the aliases.

=item I<networks>

A B<PSGConf::Data::Hash> object that represents information about known
networks.  The key is the name of the network, and the value is a
string of the form C<network_address/netmask>.

=item I<network_gateways>

A B<PSGConf::Data::Hash> object that contains the default gateway for
each network.  The key is the name of the network, and the value is the
IP address of the default gateway for that network.

=item I<network_interfaces>

A B<PSGConf::Data::Hash> object that represents the system's network
interfaces.  The hash key is the name of the network interface,
and the value is the interface's IP address (or hostname, in which
case it gets resolved using DNS), or the special token F<DHCP>.
If the value is not defined, the default is to use the I<hostname>
data object, which is provided by the
B<PSGConf::Control::Core> module.

=item I<dhclient_hostname>

A B<PSGConf::Data::String> object to use in the F<DHCP> configuration
file (in field called host-name, or Hostname on Solaris).  Defaults
to the short hostname.

=item I<dhclient_literal>

A B<PSGConf::Data::String> object to add to the F</etc/dhclient.conf> file,
if F<DHCP> has been used on a network interface.

=back

The constructor also registers the following policy methods:

=over 4

=item I<net_validate_networks>

Instantiates a B<NetAddr::IP> object for each network in the I<networks>
object.  Entries for invalid networks are deleted.

=item I<net_default_gateways>

For each entry in the I<networks> data object that does not also exist
in the I<network_gateways> data object, add an entry to
I<network_gateways> for the lowest address on that network.

=item I<net_default_interfaces>

For the first entry in the I<network_interfaces> data object that does not
specify an IP address or hostname, set it to the value of the
I<hostname> data object (supplied by the B<PSGConf::Control::Core>
module).

If more than one entry does not specify an IP address or hostname, die.
(Only one interface can have the same IP address.)

=item I<net_dhcp_interfaces>

If there is an interface defined with the F<DHCP> token, disable 
I<hosts_enable> and unset I<dns_servers>, so that F</etc/resolv.conf>
does not get maintained as well.

=item I<net_add_etc_hosts>

Adds an entry to I<host_addrs> for each entry in I<network_interfaces>.

=item I<net_default_etc_hosts>

For each entry in I<host_addrs> that does not have a specified IP
address, the value is looked up in DNS.

=item I<net_modify_rc_vars>

On FreeBSD systems, modifies the I<rc_vars> data object (provided by
B<PSGConf::Control::FreeBSD>) to include network settings.

=back

=item decide()

Instantiates and registers action objects, as follows:

=over 4

=item *

Under Solaris, if I<use_static_routes> is defined, registers
a B<PSGConf::Action::GenerateFile::Literal> object to create
F</etc/defaultrouter>.

=item *

If I<hosts_enable> is set, registers a
B<PSGConf::Action::GenerateFile::hosts> object to create the
F<I<etc_inet_dir>/hosts> file.

=item *

Under Solaris, registers a B<PSGConf::Action::GenerateFile::netmasks>
object to create F<I<etc_inet_dir>/netmasks>.

=item *

Under Solaris, registers B<PSGConf::Action::GenerateFile::TLI_hosts>
objects to create the F</etc/net/ticlts/hosts>, F</etc/net/ticots/hosts>,
F</etc/net/ticotsord/hosts> files.

=item *

Under Solaris, registers a B<PSGConf::Action::GenerateFile::Literal>
object to create F</etc/nodename>.

=item *

Under Solaris, registers B<PSGConf::Action::GenerateFile::Literal>
objects to create F</etc/hostname.*> files for each entry in
I<network_interfaces>.  Also registers B<PSGConf::Action::Remove>
objects to remove F</etc/hostname.*> files for any interfaces not listed
in I<network_interfaces>.

=item *

Under AIX, registers a B<PSGConf::Action::RunCommand> object that runs
C</usr/sbin/mktcpip> to configure TCP/IP networking.

=item *

Under Linux, registers a B<PSGConf::Action::GenerateFile::EnvFile> object
to create F</etc/sysconfig/network>.

=item *

Under Linux, registers B<PSGConf::Action::GenerateFile::EnvFile> objects
to create F</etc/sysconfig/network-scripts/ifcfg-*> files for each entry
in I<network_interfaces>.  Also registers B<PSGConf::Action::Remove>
objects to remove F</etc/sysconfig/network-scripts/ifcfg-*> files for
any interfaces not listed in I<network_interfaces>.

=back

=back

=head1 SEE ALSO

L<perl>

hosts(4)

L<PSGConf>

L<PSGConf::Action::GenerateFile::Literal>

L<PSGConf::Action::GenerateFile::TLI_hosts>

L<PSGConf::Action::GenerateFile::hosts>

L<PSGConf::Action::GenerateFile::netmasks>

L<PSGConf::Action::GenerateFile::EnvFile>

L<PSGConf::Action::Remove>

L<PSGConf::Action::RunCommand>

L<PSGConf::Control::Core>

L<PSGConf::Control::FreeBSD>

L<PSGConf::Data::Boolean>

L<PSGConf::Data::Hash>

L<PSGConf::Data::String>

L<psgconf-intro>

=cut



syntax highlighted by Code2HTML, v. 0.9.1