###
### Copyright 2000-2007 University of Illinois Board of Trustees
### All rights reserved.
###
### AnonFTP.pm - anonymous FTP module for psgconf
###
### Campus Information Technologies and Educational Services
### University of Illinois at Urbana-Champaign
###
package PSGConf::Control::AnonFTP;
use strict;
use File::stat;
use Unix::Mknod qw(major minor);
use PSGConf::Action::MkDir;
use PSGConf::Action::MkNod;
use PSGConf::Action::Symlink;
use PSGConf::Action::CopyFile;
use PSGConf::Action::CreateFile;
use PSGConf::Action::GenerateFile::EnvFile;
use PSGConf::Action::GenerateFile::Literal;
use PSGConf::Action::GenerateFile::etc_passwd;
use PSGConf::Action::GenerateFile::ftpaccess;
use PSGConf::Action::Remove;
use PSGConf::Action::svcs::setprop;
use PSGConf::Data::Boolean;
use PSGConf::Data::Hash;
use PSGConf::Data::List;
use PSGConf::Data::String;
use PSGConf::Control::PAM qw(_add_pam);
use PSGConf::Control::Packages qw(_add_pkgs);
use PSGConf::Control::syslog qw(_add_syslog);
###############################################################################
### policy methods
###############################################################################
### ftp server name should default to hostname
sub _policy_default_servername
{
my ($self, $psgconf) = @_;
my ($tmp);
return
if ($psgconf->data_obj('anon_ftp_enable')->equals('false'));
$psgconf->data_obj('anon_ftp_server_name')->set(
$psgconf->data_obj('hostname')->get()
) if (!defined $psgconf->data_obj('anon_ftp_server_name')->get());
$tmp = 'ftpadmin@' . $psgconf->data_obj('anon_ftp_server_name')->get();
$psgconf->data_obj('anon_ftp_options')->insert(
{ 'email' => $tmp }
) if (!defined $psgconf->data_obj('anon_ftp_options')->find('email'));
}
### uploads should be disabled by default
sub _policy_default_uploads
{
my ($self, $psgconf) = @_;
my ($tmp);
return
if ($psgconf->data_obj('anon_ftp_enable')->equals('false'));
### no uploading allowed by default
$tmp = $psgconf->data_obj('anon_ftp_dir')->get() . ' *';
$psgconf->data_obj('anon_ftp_upload')->insert(
{ $tmp => 'no' }
) if (!defined $psgconf->data_obj('anon_ftp_upload')->find($tmp));
}
### create ftp user
sub _policy_add_user
{
my ($self, $psgconf) = @_;
my ($user, $group);
return
if ($psgconf->data_obj('anon_ftp_enable')->equals('false'));
$user = $psgconf->data_obj('anon_ftp_user')->get();
$group = $psgconf->data_obj('anon_ftp_group')->get();
$psgconf->data_obj('group_info')->insert(
{ $group => {} }
) if (!defined $psgconf->data_obj('group_info')->find($group));
$psgconf->data_obj('user_info')->insert(
{ $user => {
group => $group,
passwd => $psgconf->data_obj('passwd_token')->get(),
home => $psgconf->data_obj('anon_ftp_dir')->get(),
gecos => 'Anonymous FTP',
shell => '/bin/false',
home_mode => 0555
}
}
) if (!defined $psgconf->data_obj('user_info')->find($user));
}
### create ftp aliases
sub _policy_add_sendmail_aliases
{
my ($self, $psgconf) = @_;
my ($user);
return
if ($psgconf->data_obj('anon_ftp_enable')->equals('false'));
$user = $psgconf->data_obj('anon_ftp_user')->get();
$psgconf->data_obj('sendmail_aliases')->insert(
{ $user => 'root' }
) if (!defined $psgconf->data_obj('sendmail_aliases')->find($user));
$psgconf->data_obj('sendmail_aliases')->insert(
{ 'ftpadmin' => $user }
) if (!$psgconf->data_obj('sendmail_aliases')->exists('ftpadmin'));
}
### add PAM configs
sub _policy_add_pam
{
my ($self, $psgconf) = @_;
my ($service, $lines);
$service = ( $psgconf->data_obj('platform')->match('linux'))
? 'ftp'
: 'ftpd';
$self->{pam_name} = $service;
$self->{pam_conf} = $psgconf->data_obj('anon_ftp_pam_conf')->get();
$self->_add_pam($psgconf)
if ($psgconf->data_obj('anon_ftp_pam_conf')->count());
}
### add syslog entry
sub _policy_add_syslog
{
my ($self, $psgconf) = @_;
$self->_add_syslog($psgconf)
if (exists($self->{syslog}));
}
### add inetd entry
sub _policy_add_inetd_entry
{
my ($self, $psgconf) = @_;
return
if ($psgconf->data_obj('anon_ftp_enable')->equals('false'));
$psgconf->data_obj('inetd')->insert(
{ 'ftp/tcp' => {
'server' => $psgconf->data_obj('anon_ftp_ftpd_path')->get()
}
}
) if ( ! $psgconf->data_obj('platform')->match('solaris10') );
$psgconf->data_obj('inetd')->insert(
{ 'ftp/tcp' => {
'server_args' =>
'-a -d -l -r ' . $psgconf->data_obj('anon_ftp_dir')->get()
}
}
) if ( $psgconf->data_obj('anon_ftp_use_vsftpd')->equals('false') &&
! $psgconf->data_obj('platform')->match('solaris10') );
}
sub _enable_rc_scripts
{
my ($self, $psgconf) = @_;
$psgconf->data_obj('rc_scripts')->insert(
{ 'ftp' => { 'state' => 'enable' }}
) if ( $psgconf->data_obj('anon_ftp_enable')->equals('true')
&& defined $psgconf->data_obj('rc_scripts')->find('ftp'));
}
### add TCP wrappers entry
sub _policy_add_tcpwrapper_entry
{
my ($self, $psgconf) = @_;
my ($cmd);
return
if ($psgconf->data_obj('anon_ftp_enable')->equals('false'));
$cmd = ($psgconf->data_obj('anon_ftp_use_vsftpd')->equals('true'))
? 'vsftpd'
: 'in.ftpd';
$psgconf->data_obj('tcp_wrappers')->insert_row(
{ 1 => 'all' },
[ $cmd, 'all', 'allow' ]
)
if (! $psgconf->data_obj('tcp_wrappers')->find_row(
{ 0 => qr/\b$cmd\b/ }));
}
###############################################################################
### decide() method
###############################################################################
sub decide
{
my ($self, $psgconf) = @_;
my ($file, $dest, $mode, $uid, $gid, $conversions, $userinfo, $user);
$mode = PSGConf::Data::Integer->new();
### if we're not enabled, remove ftpaccess and return
if ($psgconf->data_obj('anon_ftp_enable')->equals('false'))
{
$psgconf->register_actions(
PSGConf::Action::Remove->new(
name => $psgconf->data_obj('anon_ftp_cfg_dir')->get() . '/ftpaccess'
)
);
return;
}
### add fake /etc/passwd file with just the anon ftp account.
$user = $psgconf->data_obj('anon_ftp_user')->get();
%{$userinfo->{'ftp'}} = %{$psgconf->data_obj('user_info')->find($user)};
$userinfo->{'ftp'}->{home} = '/';
map {
$conversions .= $_
. ":"
. $psgconf->data_obj('anon_ftp_conversions')->find($_)
. "\n"
} keys %{$psgconf->data_obj('anon_ftp_conversions')->get()};
$uid = ( defined $psgconf->data_obj('user_info')->find($psgconf->data_obj('anon_ftp_user')->get()))?
$psgconf->data_obj('user_info')->find($psgconf->data_obj('anon_ftp_user')->get())->{'uid'}:
-1;
$gid = ( defined $psgconf->data_obj('user_info')->find($psgconf->data_obj('anon_ftp_user')->get()))?
$psgconf->data_obj('user_info')->find($psgconf->data_obj('anon_ftp_user')->get())->{'gid'}:
-1;
$psgconf->register_actions(
(map {
$file = $psgconf->data_obj('anon_ftp_chroot_files')->find($_);
if ( exists $file->{location} ) {
$dest = $psgconf->data_obj('anon_ftp_dir')->get() . $file->{location};
} else {
$dest = $psgconf->data_obj('anon_ftp_dir')->get() . $_;
}
$uid = ( exists $file->{owner} )?
(( defined $psgconf->data_obj('user_info')->find($file->{owner}) )?
$psgconf->data_obj('user_info')->find($file->{owner})->{uid}: 0): 0;
$gid = ( exists $file->{group} )?
(( defined $psgconf->data_obj('user_info')->find($file->{group}) )?
$psgconf->data_obj('user_info')->find($file->{group})->{gid}: 0): 0;
### If the source file does not exist, assume you
### want to create a directory.
if ( -d $_ || ! -e $_ ) {
$mode->set((exists $file->{mode})? $file->{mode}: 0111);
PSGConf::Action::MkDir->new(
name => $dest,
mode => $mode->get(),
uid => $uid,
gid => $gid
);
} elsif ( -l $_ ) {
PSGConf::Action::Symlink->new(
name => $dest,
link_to => $_
);
} elsif ( -b $_ || -c $_ ) {
my ($st) = stat $_;
$mode->set((exists $file->{mode})? $file->{mode}: 0644);
PSGConf::Action::MkNod->new(
name => $dest,
mode => $mode->get(),
type => (-b $_)? 'b': 'c',
major => major($st->rdev),
minor => minor($st->rdev)
);
} elsif ( -f $_ ) {
$mode->set((exists $file->{mode})? $file->{mode}: 0444);
PSGConf::Action::CopyFile->new(
name => $dest,
copy_from => $_,
mode => $mode->get(),
uid => $uid,
gid => $gid
);
}
} keys %{$psgconf->data_obj('anon_ftp_chroot_files')->get()}),
PSGConf::Action::GenerateFile::etc_passwd->new(
'name' => $psgconf->data_obj('anon_ftp_dir')->get() . '/etc/passwd',
'comment_str' => undef,
'mode' => 0444,
'sort_func' => sub { $a cmp $b },
'user_info' => $userinfo,
'passwd_token' => $psgconf->data_obj('passwd_token')->get(),
'add_passwords' => ($psgconf->data_obj('platform')->match('hpux'))
),
PSGConf::Action::CreateFile->new(
'name' => $psgconf->data_obj('anon_ftp_dir')->get() . '/var/adm/wtmpx',
)
);
### Now take care of the vsftpd/wu-ftpd specific files
if ( $psgconf->data_obj('anon_ftp_use_vsftpd')->equals('true') ) {
$psgconf->register_actions(
PSGConf::Action::GenerateFile::EnvFile->new(
name => '/etc/vsftpd/vsftpd.conf',
vars => $psgconf->data_obj('anon_ftp_vsftpd_options')->get()
),
PSGConf::Action::GenerateFile::Literal->new(
name => '/etc/vsftpd.ftpusers',
content => join ("\n", @{$psgconf->data_obj('anon_ftp_users')->get()}) . "\n"
),
PSGConf::Action::GenerateFile::Literal->new(
name => '/etc/vsftpd.user_list',
content => join ("\n", @{$psgconf->data_obj('anon_ftp_users')->get()}) . "\n"
)
);
} else {
$psgconf->register_actions(
PSGConf::Action::GenerateFile::ftpaccess->new(
name => $psgconf->data_obj('anon_ftp_cfg_dir')->get() . '/ftpaccess',
description => 'FTP server configuration file',
classes => $psgconf->data_obj('anon_ftp_class')->get(),
autogroups => $psgconf->data_obj('anon_ftp_autogroup')->get(),
limits => $psgconf->data_obj('anon_ftp_limit')->get(),
options => $psgconf->data_obj('anon_ftp_options')->get(),
messages => $psgconf->data_obj('anon_ftp_message')->get(),
readmes => $psgconf->data_obj('anon_ftp_readme')->get(),
uploads => $psgconf->data_obj('anon_ftp_upload')->get(),
literal_config => $psgconf->data_obj('anon_ftp_literal')->get()
),
PSGConf::Action::GenerateFile::Literal->new(
name => '/etc/ftpusers',
content => join ("\n", @{$psgconf->data_obj('anon_ftp_users')->get()}) . "\n"
),
(map {
PSGConf::Action::GenerateFile::Literal->new(
name => $psgconf->data_obj('anon_ftp_dir')->get() .
$psgconf->data_obj('anon_ftp_cfg_dir')->get() .
'/ftp-banners/' . $_,
comment_str => undef,
content => $psgconf->data_obj('anon_ftp_banners')->find($_)
)
} sort keys %{$psgconf->data_obj('anon_ftp_banners')->get()}),
PSGConf::Action::GenerateFile::Literal->new(
name => $psgconf->data_obj('anon_ftp_dir')->get() .
$psgconf->data_obj('anon_ftp_cfg_dir')->get() .
'/ftpconversions',
content => $conversions
)
);
}
if ( $psgconf->data_obj('platform')->match('solaris10') ) {
$psgconf->register_actions(
PSGConf::Action::svcs::setprop->new(
name => 'enable FTP startup options',
FMRI => 'svc:/network/ftp',
property => 'inetd_start/exec',
value =>
'"' . $psgconf->data_obj('anon_ftp_ftpd_path')->get() .
' -a -d -l -r ' . $psgconf->data_obj('anon_ftp_dir')->get() . '"'
)
);
}
}
###############################################################################
### Constructor
###############################################################################
sub new
{
my ($class, $psgconf) = @_;
my ($self);
$self = {};
bless($self, $class);
### So that _add_pkgs knows which directives to look at
$self->{name} = 'anon_ftp';
$self->{enable} = $self->{name} . '_enable';
$self->{packages} = $self->{name} . '_packages';
### So that _add_syslog knows to add syslog file support.
### If we are running on Linux or the *BSD's then use ftp facility,
### otherwise the system logs by default to DAEMON
if ( $psgconf->data_obj('platform')->match('linux|bsd')) {
$self->{syslog} = 'ftp';
$self->{facility} = $self->{syslog} . '.info';
}
$psgconf->register_data(
anon_ftp_autogroup => PSGConf::Data::Hash->new(),
anon_ftp_banners => PSGConf::Data::Hash->new(),
anon_ftp_conversions => PSGConf::Data::Hash->new(),
anon_ftp_class => PSGConf::Data::List->new(),
anon_ftp_user => PSGConf::Data::String->new(
'value' => 'ftp'
),
anon_ftp_users => PSGConf::Data::List->new(),
anon_ftp_group => PSGConf::Data::String->new(
'value' => 'ftp'
),
anon_ftp_dir => PSGConf::Data::String->new(
'value_abspath' => 1,
'value' => '/services/ftp'
),
anon_ftp_enable => PSGConf::Data::Boolean->new(
'value' => 'false'
),
anon_ftp_packages => PSGConf::Data::List->new(),
anon_ftp_limit => PSGConf::Data::List->new(),
anon_ftp_literal => PSGConf::Data::String->new(),
anon_ftp_cfg_dir => PSGConf::Data::String->new(
'value_abspath' => 1,
'value' => '/etc'
),
anon_ftp_ftpd_path => PSGConf::Data::String->new(
'value_abspath' => 1,
'value' => '/usr/sbin/in.ftpd'
),
anon_ftp_message => PSGConf::Data::Hash->new(),
anon_ftp_options => PSGConf::Data::Hash->new(),
anon_ftp_vsftpd_options => PSGConf::Data::Hash->new(),
anon_ftp_chroot_files => PSGConf::Data::Hash->new(
value_type => 'HASH'
),
anon_ftp_readme => PSGConf::Data::Hash->new(),
anon_ftp_server_name => PSGConf::Data::String->new(),
anon_ftp_upload => PSGConf::Data::Hash->new(
key_abspath => 1
),
anon_ftp_pam_conf => PSGConf::Data::List->new(),
anon_ftp_use_vsftpd => PSGConf::Data::Boolean->new(
value => 'false'
)
);
$psgconf->register_policy($self,
anon_ftp_default_servername
=> '_policy_default_servername',
anon_ftp_default_uploads
=> '_policy_default_uploads',
anon_ftp_add_user
=> '_policy_add_user',
anon_ftp_add_sendmail_aliases
=> '_policy_add_sendmail_aliases',
anon_ftp_add_inetd_entry
=> '_policy_add_inetd_entry',
anon_ftp_add_tcpwrapper_entry
=> '_policy_add_tcpwrapper_entry',
anon_ftp_add_syslog
=> '_policy_add_syslog',
anon_ftp_add_packages
=> '_add_pkgs',
anon_ftp_add_pam
=> '_policy_add_pam',
anon_ftp_enable_rc_scripts
=> '_enable_rc_scripts'
);
return $self;
}
###############################################################################
### documentation
###############################################################################
1;
__END__
=head1 NAME
PSGConf::Control::AnonFTP - psgconf control class for anonymous FTP
=head1 SYNOPSIS
In F<psgconf_modules>:
Control PSGConf::Control::AnonFTP
=head1 DESCRIPTION
The B<PSGConf::Control::AnonFTP> module provides a B<psgconf> control
object for configuring anonymous FTP using wu-ftpd. 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<anon_ftp_ftpd_path>
A B<PSGConf::Data::String> object containing the absolute path to the
wu-ftpd binary. The default is F</usr/local/sbin/in.ftpd>.
=item I<anon_ftp_autogroup>
A B<PSGConf::Data::Hash> object that specifies autogroup mappings. The
hash key is the group name, and the value is a space-seperated list of
classes.
=item I<anon_ftp_banners>
A B<PSGConf::Data::Hash> object that specifies what FTP banner files
need to be created. The hash key is the name of the file, and the value
is the file's contents.
=item I<anon_ftp_class>
A B<PSGConf::Data::List> object that contains a list of classes to
define in F<ftpaccess>.
=item I<anon_ftp_dir>
A B<PSGConf::Data::String> object that sets the path to the anonymous
FTP root. It must be an absolute path. If not set, it defaults to
F</services/ftp>.
=item I<anon_ftp_user>
A B<PSGConf::Data::String> object that sets the login for the ftpd to
run as. Defaults to C<ftp>.
=item I<anon_ftp_group>
A B<PSGConf::Data::String> object that sets the group for the ftpd to
run as. Defaults to C<ftp>.
=item I<anon_ftp_users>
A B<PSGConf::Data::List> object that defines the contents of the ftpusers file.
=item I<anon_ftp_enable>
A B<PSGConf::Data::Boolean> object that enables anonymous FTP. Default
is off.
=item I<anon_ftp_packages>
A B<PSGConf::Data::List> object lists what packages to install.
=item I<anon_ftp_limit>
A B<PSGConf::Data::List> object that contains a list of limits to define
in F<ftpaccess>.
=item I<anon_ftp_conversions>
A B<PSGConf::Data::Hash> object containing the contents of the
F<ftpconversions> file, where the key is the first 4 fields of
the ftpconversions file (the Strip prefix, Strip postfix, Addon
prefix, and Addon postfix) and the value is the rest of the line
in the file.
=item I<anon_ftp_cfg_dir>
A B<PSGConf::Data::String> object that contains the location of all the
ftpd configuration files. Defaults to F</etc>.
=item I<anon_ftp_literal>
A B<PSGConf::Data::String> object that contains literal text to add to
F<ftpaccess>. (This is a nasty hack, and should be avoided.)
=item I<anon_ftp_message>
A B<PSGConf::Data::Hash> object that specifies the C<message> directives
to add to F<ftpaccess>. The hash key is the name of the message
file, and the value sets when the directive is triggered.
=item I<anon_ftp_options>
A B<PSGConf::Data::Hash> object that contains miscellaneous options for
F<ftpaccess>. The hash key is the option name, and the value is
the option value.
=item I<anon_ftp_vsftpd_options>
A B<PSGConf::Data::Hash> object that contains the contents of the
F<vsftpd.conf> file.
=item I<anon_ftp_chroot_files>
A B<PSGConf::Data::Hash> object listing what files are needed to setup
the chroot'ed ftp environment. The key is the file on the system,
and points to a hash with the following sub keys:
=item I<anon_ftp_pam_conf>
A B<PSGConf::Data::List> object to set I<pam_conf> (provided by
B<PSGConf::Control::PAM>), to configure PAM for ftp server support.
=item I<anon_ftp_use_vsftpd>
A B<PSGConf::Data::Boolean> object to tell the control object to configure
using C<vsftpd> instead of C<wu-ftpd>. Defaults to off.
=over 4
=item I<location>
The location to put the file under I<anon_ftp_dir>.
=item I<mode>
The mode the file should have.
=back
=item I<anon_ftp_readme>
A B<PSGConf::Data::Hash> object that specifies the C<readme> directives
to add to F<ftpaccess>. The hash key is the name of the F<README>
file, and the value sets when the directive is triggered.
=item I<anon_ftp_server_name>
A B<PSGConf::Data::String> object that contains the advertised server
name of the FTP server. If not set, it defaults to the value of the
I<hostname> object, which is supplied by B<PSGConf::Control::Core>.
=item I<anon_ftp_upload>
A B<PSGConf::Data::Hash> object that specifies the C<upload> directives
to add to F<ftpaccess>. The hash key is a string containing the
root directory, followed by a space, followed by the list of relative
directories (or a C<*>). The value is C<no>, or C<yes> followed by
optional owner and permissions for the uploaded files.
=back
In addition, the constructor registers the following policy methods:
=over 4
=item I<anon_ftp_default_servername>
If the I<anon_ftp_server_name> object is unset, it is set to the value
of the I<hostname> object, which is provided by
B<PSGConf::Control::Core>.
If the I<anon_ftp_options> object does not contain a setting for the
C<email> option, it is set to C<ftpadmin@> followed by the value of the
I<anon_ftp_server_name> object.
=item I<anon_ftp_default_uploads>
If the I<anon_ftp_upload> object is unset, an entry is added for root
directory I<anon_ftp_dir>, subdirectory C<*>, value C<no>.
=item I<anon_ftp_add_user>
Adds entries to the I<user_info> and I<group_info> Data objects (supplied
by the B<PSGConf::Control::Users> module) for user and group C<ftp>.
=item I<anon_ftp_add_pam>
Adds entries from I<anon_ftp_pam_conf> to the I<pam_conf> Data object
(supplied by the B<PSGConf::Control::PAM> module) for the C<ftp> service.
=item I<anon_ftp_add_sendmail_aliases>
If mail aliases for C<ftp> and C<ftpadmin> do not exist, aliases are
created that point to C<root>. This is done using the
I<sendmail_aliases> Data object, which is provided by the
B<PSGConf::Control::sendmail> module.
=item I<anon_ftp_enable_rc_scripts>
If there is an entry in the I<rc_scripts> hash for I<ftp>, then go ahead
and enable it.
=item I<anon_ftp_add_inetd_entry>
If there is no entry in the I<inetd> data object (provided by
B<PSGConf::Control::inetd>) for C<ftp/tcp>, create one that invokes the
binary specified by the I<anon_ftp_ftpd_path> data object with the
arguments C<-a -d -l -r anon_ftp_dir>.
=item I<anon_ftp_add_tcpwrapper_entry>
If the I<tcp_wrappers> object has no entry for C<in.ftpd>, add one with
the values C<in.ftpd>, C<all>, C<allow>. This is done using
the I<tcp_wrappers> object, which is provided by
B<PSGConf::Control::TCPWrappers>.
=item I<anon_ftp_add_syslog>
Under Linux and *BSD, adds an F<ftp> entry to the I<syslog> data object
(supplied by B<PSGConf::Control::syslog>). (On other platforms, there
is no C<ftp> syslog facility, so F<wu-ftpd> logs to the C<daemon>
facility.)
=item I<anon_ftp_add_packages>
Adds F<wu-ftpd> to the list of requested packages. This
uses the I<pkg_install_list> config object supplied by
B<PSGConf::Control::Packages>.
=back
=item decide()
Instantiates and registers the following action objects:
=over 4
=item *
If I<anon_ftp_enable> is not set, it registers a
B<PSGConf::Action::Remove> object to remove F<ftpaccess> and
returns immediately.
=item *
Instantiates a B<PSGConf::Action::GenerateFile::ftpaccess> object to
create F<ftpaccess>.
=item *
Instantiates B<PSGConf::Action::GenerateFile::Literal> objects to create
each of the files listed in the I<anon_ftp_banners> object. The files
are created in the F<anon_ftp_dir/ftp-banners> directory.
=item *
Instantiates B<PSGConf::Action::GenerateFile::Literal> objects to create
the ftpconversions file.
=item *
Instantiates B<PSGConf::Action::CreateFile> objects to create
the F<anon_ftp_dir/var/adm/wtmp> file.
=item *
Instantiates B<PSGConf::Action::GenerateFile::etc_passwd> objects to create
the I<anon_ftp_dir>/etc/passwd file.
=item *
If we are running on Solaris10, instantiates
B<PSGConf::Action::svcs::setprop> objects to set the SMF properties for
ftpd to C<-a -d -l -r anon_ftp_dir>.
=item *
Instantiates B<PSGConf::Action::CopyFile> object for each file listed in
I<anon_ftp_chroot_files> object. Instantiates B<PSGConf::Action::SymLink>
for each symlink, B<PSGConf::Action::MkNod> for each block or character
special file and B<PSGConf::Action::MkDir> for each directory or if the
file does not exist.
=back
=back
=head1 SEE ALSO
L<perl>
ftpaccess(5)
L<File::stat>
L<Unix::Mknod>
L<PSGConf>
L<PSGConf::Action::CopyFile>
L<PSGConf::Action::MkDir>
L<PSGConf::Action::MkNod>
L<PSGConf::Action::Symlink>
L<PSGConf::Action::GenerateFile::ftpaccess>
L<PSGConf::Action::GenerateFile::etc_passwd>
L<PSGConf::Action::GenerateFile::EnvFile>
L<PSGConf::Action::GenerateFile::Literal>
L<PSGConf::Action::Remove>
L<PSGConf::Action::CreateFile>
L<PSGConf::Control::inetd>
L<PSGConf::Control::Packages>
L<PSGConf::Control::sendmail>
L<PSGConf::Control::syslog>
L<PSGConf::Control::PAM>
L<PSGConf::Control::Packages>
L<PSGConf::Control::TCPWrappers>
L<PSGConf::Control::Users>
L<PSGConf::Data::Boolean>
L<PSGConf::Data::Hash>
L<PSGConf::Data::List>
L<PSGConf::Data::String>
L<psgconf-intro>
=cut
syntax highlighted by Code2HTML, v. 0.9.1