#!/usr/bin/perl -w

eval 'exec /usr/bin/perl -w -S $0 ${1+"$@"}'
    if 0; # not running under some shell


#$Id: ncat_config.PL,v 1.10 2002/03/27 12:50:59 jallison Exp $
#
# ncat_config - Generate NCAT config file from template
#
#
# Copyright (C) 2002  George M. Jones <gmj@users.sourceforge.net>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA


=head1 NAME

ncat_config - Generate NCAT config file from template

=head1 SYNOPSIS

B<ncat_config> [OPTIONS] <ncat.conf.MASTER > ncat.conf

=head1 DESCRIPTION

B<ncat_config> reads an ncat config file template (F</etc/ncat.conf.MASTER>)
and writes a usable config.  The purpose is to support automatic generation
of ncat.conf files with the correct values for items that change with 
each installation.  Examples include external interface names, local
address blocks, addresses that should be permitted in various access lists, etc.

Edit the .MASTER file to set correct values, then run ncat_config to produce
the ncat.conf file that you use.

=head1 OPTIONS

=over 8

=item B<-n, --noprompt>

The C<--noprompt> flag says that the values in the master file
are teo be used as-is.  The users is not prompted for confirmation.
Default behavior is to prompt and allow the user to change the values.    

=item B<-u, --update_master>

The C<--update_master> tells ncat_config to update the master
file with the values entered.  This reduced the need to re-enter
default values.


=item B<-V, --version>

The C<--version> option displays the current program version.

=item B<-y, --yes>

The C<--yes> option changes the default answer for yes/no questions
(i.e. whether to include optional rule groups) from "no" to "yes".
This is useful in conjunction with "--noprompt" and "--update_master".
To create a customized ncat.conf.MASTER file, do the following

    * ncat_config --update_master

      This will let you input appropriate default values and save
      them in /etc/ncat.conf.MASTER

    * hand-edit /etc/ncat.conf.MASTER and comment out
      all the ConfigClass: entries you don't intend to use
      (the ones you answered "no" to)

    * On successive runs of ncat_config, simply say

        ncat_config --noprompt --yes

      to generate a new config.  You may change your
      default values either by hand editing /etc/ncat.conf.MASTER
      or by using --update_master again.

=back

=head1 MASTER FILE SYNTAX

=over 8

The Master File syntax is the same as the ncat.conf syntax.
See the description in the ncat man page.

=head1 RETURN VALUE

0  - success
>0 - some error occurred

=head1 FILES

=over 8

F</etc/ncat.conf.MASTER>    - The ncat.conf template
F</etc/ncat.conf>    	  - The ncat.conf output file    


=back


=head1 CAVEATS

Doing a "Make install" will overwrite your master file, incuding changes saved with "--update_master".

=head1 BUGS

Yes.

=head1 AUTHOR

George M. Jones <gmj@users.sourceforge.net>

=head1 CREDIT WHERE CREDIT IS DUE

=cut

#
# $Log: ncat_config.PL,v $
# Revision 1.10  2002/03/27 12:50:59  jallison
# changed grep statement to look for $(foo) style variables
#
# Revision 1.9  2002/03/21 16:24:41  gmj
# * updated POD documentation to describe all rule syntax
#
# Revision 1.8  2002/03/20 10:15:17  gmj
# * Added support for class dependancies (require/forbid groups of rules
#   if another group of rules is present/absent)
# * Added support for local values to be dependant on classes (e.g. only
#   prompt for ingress/egress info if router is in class
#   external_router, etc.)
# * Config file parsing moved to NCAT.pl, single parser now used for
#   .MASTER and .config files
# * Syntax for classes  ("ConfigClass.*:") and local rules  ("ConfigLocal.*")
#   is now consistent (using same parser) as other config file options
#   ("Rule.*:", "ConfigGlobal.*:")
# * Saved .conf file and updated (-u) .MASTER file are now generated
#   from parsed rules, not textual substitution on entire .MASTER
# * Prompt user for optional rule classes to include
# * Prompt user for optional local values based on selected classes
#
# Revision 1.7  2002/03/16 13:37:01  gmj
# * Added code to do class depenancies (one requires/forbids others)
#
# Revision 1.6  2002/03/15 22:27:12  gmj
# * Added "--yes" flag and documented.
# * Changed ConfigLocal: to allow the inclusion of a list of ruleclasses
#   to which required the particular ConfigLocal parameter.
# * Added ConfigClass:
# * Prompt user for inclusion/exclusion of rule classes listed
#   in ConfigClass: statements.
# * Print comments at start of output file telling when the ncat.conf
#   file was created, which optional rulesets were included, and
#   what optional values were used.
# * Only prompt for ConfigLocal: values reuiqred by ConfigClass: classes
#   that the user selects.
#
# Revision 1.5  2002/03/14 20:12:11  gmj
# * Merged Neal Zirings code to allow help when prompting for default values
#
# Revision 1.4  2002/03/13 21:17:27  gmj
# * minor comment change
#
# Revision 1.3  2002/03/06 21:10:44  gmj
# * tell people to edit $PREFIX/etc/ncat.conf.MASTER
#
# Revision 1.2  2002/03/06 14:30:06  gmj
# * Finished initial version
#
# Revision 1.1  2002/03/05 23:51:02  gmj
# initial version
#
#

$ENV{'SHELL'}  = '/bin/sh';
$ENV{'IFS'}    = '';

use lib '/usr/lib';


use Getopt::Long;
use IO::Handle;
use strict;
use Config;

use vars qw($opt_debug);

# Imports

use NCAT qw(&ParseRules
	    %RulesDefined
	    %RuleFieldNames
	    %RuleFieldValues
	    %ConfigClassClassesDefined
	    %ConfigClassFieldValues
	    %ConfigClassFieldNames
    	    %ConfigLocalsDefined
	    %ConfigLocalFieldValues
	    %ConfigLocalFieldNames
  	    %ConfigGlobalsDefined
	    %ConfigGlobalFieldValues
	    %ConfigGlobalFieldNames
	
	    @ConfigLines
	    );

# Exports
$opt_debug = '';


my (
    $opt_dir,
    $opt_help,         $opt_version,
    $opt_yes,
    $opt_verbose,
    $opt_noprompt,
    $opt_update_master,
    $RulesFile,
    $TemplateFile,
    $File,
    $line,
    $answer,
    $key,
    $value,
    $splitme,
    $info,
    $class,
    $saw_continue,
    $class_list,
    $default,
    $requested_class,
    $class_dependancieses,
    $class_dependancy,
    $selected_class,
    $conflicting_class,
    $missing_class,
    $prereq,
    $have_all_prereqs,
    $have_a_prereq,
    $local_name,
    $global_name,
    $current_value,
    $tmp,
    $RuleName,
    ) = ('','','','','','','','','','','','','','','','','','','','','','','','','','','','','',
         ,'','','','');


my ($i,
    $BaseLineNum, $LineNum,
    $OffSet) = 0;

my (@RulesLines,
    @RequestedClasses) =
   ((),());

my (
    %LocalSubstitutions,%UserRequestedClasses,%ClassDependancies
      ) = ((),());

#default values
my $progname      = $0;
$progname         =~ s,.*/,,;    # only basename left in progname
$progname         =~ s/\.\w*$//; # strip extension if any

$TemplateFile     = '/etc/ncat.conf.MASTER';
$RulesFile        = '/etc/ncat.conf';
$opt_debug        = "";

#parse command line

Getopt::Long::Configure ("bundling_override");
GetOptions(
	   # Common
	   "help|h", 		\$opt_help,
	   "noprompt|n",	\$opt_noprompt,
	   "update_master|u", 	\$opt_update_master,
	   "yes|y",	 	\$opt_yes,
	   "verbose|v", 	\$opt_verbose,
	   "version|V",		\$opt_version,
	   "debug|d=s", 	\$opt_debug,

	  )
  or &Usage("");

$default = $opt_yes ? "yes" : "no";

&Version if $opt_version;

sub Version {
    print STDERR "This is $progname version ", NCAT::Version, "\n";
    print STDERR '$Id: ncat_config.PL,v 1.10 2002/03/27 12:50:59 jallison Exp $';
    print STDERR "\n\n";
    print STDERR "Copyright 2002, George M. Jones\n";
    print STDERR "\n";
    print STDERR "This program is free software; you can redistribute it and/or\n";
    print STDERR "modify it under the same terms as Perl itself\n";

    exit(0);
}


sub Usage {
  my($msg) = @_;
  print STDERR "$progname: $msg\n" unless ($msg eq "");
  print STDERR "Usage:\n";
  print STDERR "  $progname [options] [$TemplateFile [$RulesFile]]\n";
  print STDERR "    -n, --noprompt\n";  
  print STDERR "    -d, --debug DEBUG_OPTIONS\n";  
  print STDERR "    -V, --version\n";  
  exit 1;
}


# process command line stuff

&Usage("") if ($opt_help);

#my($ConfigName) = shift || &Usage("");

# Shift off command line stuff

if ($File = shift) {
    $TemplateFile = $File;

    if ($File = shift) {
	$RulesFile = $File;
    }
}

# sanity check

unless (-s $TemplateFile) {
    print STDERR "$progname: $TemplateFile does not exist.\n";
    exit 1;
}

# Parse the rules file.

printf STDERR "$progname: Reading $TemplateFile\n";
printf STDERR "\n";
ParseRules($TemplateFile);


#
# open output files
#

open (RULES, ">$RulesFile") || die "Unable to open $RulesFile for writing: $!";

#
# Now, prompt the user for class selections and changes to default values
#

if (%ConfigClassClassesDefined and not $opt_noprompt) {
    printf "Please answer the questions below about your network and\n";
    printf "router configuration.  Type ? to get a short explanation of\n";
    printf "any parameter.  If you are unsure about what value to give\n";
    printf "for a parameter, hit RETURN to take the default value.\n";
    printf "\n\n";
}


#
# Prompt for rule classes to configure
#

unless($opt_noprompt) {
    print "Select types of optional rules to be applied:\n\n";
}

foreach $class (keys %ConfigClassClassesDefined) {

    # First, skip any classes that conflict with classes already selected

    $conflicting_class = 0;

    for $requested_class (@RequestedClasses) {

	print "class: /$class/, requested_class: $requested_class\n"
	    if ($opt_debug =~ /classmatch/i);

	if (defined ($ConfigClassFieldValues{"$requested_class:configclassconflictswith"})) {

	    print "  conflicts($requested_class): " .
		$ConfigClassFieldValues{"$requested_class:configclassconflictswith"} . " ?\n"
		    if ($opt_debug =~ /classmatch/i);

	    if ($class =~ /$ConfigClassFieldValues{"$requested_class:configclassconflictswith"}/i) {

		print STDERR
		    "$progname: Skipping class \"$class\".  It is incompatable with \"$requested_class\".\n";

		$conflicting_class = 1;
		last;
	    }
	}
    }

    next if $conflicting_class;

    # skip any class that has a prereq until we've asked about all other classes

    next if (defined ($ConfigClassFieldValues{"$class:configclassprereq"}));

    # Now, promt the user for inclusion

    $info = defined($ConfigClassFieldValues{"$class:configclassdescription"}) ?
	$ConfigClassFieldValues{"$class:configclassdescription"} :
	    "";

    if (ask_user("Apply rules for class $class","class",$class,$default,$info,1)) {
	$class =~ s/^\s+//;
	$class =~ s/\s+$//;
	push @RequestedClasses,$class;

	# now make each rule of this class a member of the "default" class

	foreach $RuleName (keys %RulesDefined) {
	    if (defined $RuleFieldValues{"$RuleName:ruleclass:$class"} and not
		defined $RuleFieldValues{"$RuleName:ruleclass:default"}) {
		$RuleFieldValues{"$RuleName:ruleclass"} .= ",default";
	    }

	}
    }
} # foreach class

#
# Ask user about classes with a prerequisite
#

foreach $class (keys %ConfigClassClassesDefined) {

    # skip any class that does not have a prereq

    next unless (defined ($ConfigClassFieldValues{"$class:configclassprereq"}));

    # skip unless needed prereqs are defined

    $have_all_prereqs = 1;
    $tmp = join(",",@RequestedClasses) . ",";

    for $prereq (split(/,/,$ConfigClassFieldValues{"$class:configclassprereq"})) {
	unless ("$tmp"=~ /$prereq,/i) {
	    print STDERR
		"$progname: Skipping $class because prerequisite $prereq was not selected.\n";
	    $have_all_prereqs = 0;
	    last;
	}
    }

    next unless ($have_all_prereqs);

    # Now, promt the user for inclusion

    $info = defined($ConfigClassFieldValues{"$class:configclassdescription"}) ?
	$ConfigClassFieldValues{"$class:configclassdescription"} :
	    "";

    if (ask_user("Apply rules for class $class","class",$class,$default,$info,1)) {
	$class =~ s/^\s+//;
	$class =~ s/\s+$//;
	push @RequestedClasses,$class;

	# now make each rule of this class a member of the "default" class

	foreach $RuleName (keys %RulesDefined) {
	    if (defined $RuleFieldValues{"$RuleName:ruleclass:$class"} and not
		defined $RuleFieldValues{"$RuleName:ruleclass:default"}) {
		$RuleFieldValues{"$RuleName:ruleclass"} .= ",default";
	    }

	}


    }
} # foreach class

#
# Ask for local config values
#

unless($opt_noprompt) {
    print "\n";
    print "Change default configuration values:\n\n";
}


foreach $local_name (sort keys %ConfigLocalsDefined) {

    # skip any that has a prereq class which is not selected

    if (defined ($ConfigLocalFieldValues{"$local_name:configlocalclassprereq"})) {

	$have_a_prereq = 0;

	$tmp = join(",",@RequestedClasses) . ",";

	for $prereq (split(/,/,$ConfigLocalFieldValues{"$local_name:configlocalclassprereq"})) {
	    if ("$tmp" =~ /$prereq,/i) {
		$have_a_prereq = 1;
		last;
	    }
	}

	unless ($have_a_prereq) {
	    print STDERR
		"$progname: Skipping $local_name because none of the prerequisite classes (".
		    $ConfigLocalFieldValues{"$local_name:configlocalclassprereq"} .
			") were selected.\n";
	    next;
	} # if we have a prereq
    } # if this local value has prefrequisites

    # Now, promt the user for inclusion

    $info = defined($ConfigLocalFieldValues{"$local_name:configlocaldescription"}) ?
	$ConfigLocalFieldValues{"$local_name:configlocaldescription"} :
	    "";

    $current_value = defined($ConfigLocalFieldValues{"$local_name:configlocalvalue"}) ?
	$ConfigLocalFieldValues{"$local_name:configlocalvalue"} :
	    "";

    if ($current_value = ask_user("Enter value for $local_name","local value",$local_name,$current_value,$info,0)) {
	$ConfigLocalFieldValues{"$local_name:configlocalvalue"} = $current_value;
    }
} # foreach local value


# Now that we've parsed and stored the updated values, create
# a new version of the configuration file and make the
# necessary substitutions
#

printf STDERR "\n";

@RulesLines = ();

push @RulesLines, "# DO NOT EDIT THIS FILE.\n";
push @RulesLines, "#\n";
push @RulesLines, "# This file was automaticaly generated by ncat_config at " . gmtime(time) . " GMT\n";
push @RulesLines, "#\n";
push @RulesLines, "# To change, edit /etc/ncat.conf.MASTER and rerun ncat_config\n";
push @RulesLines, "#\n";
push @RulesLines, "# The following optional rule classes were selected as 'default' classes:\n";
push @RulesLines, "#\n";
push @RulesLines, "#   " . join(",\n#   ",@RequestedClasses) . "\n";
push @RulesLines, "#\n";
push @RulesLines, "#\n";
push @RulesLines, "# The following optional rule classes were available for selection:\n";
push @RulesLines, "#\n";

foreach $class (sort keys %ConfigClassClassesDefined) {

    push @RulesLines, "\n";
    push @RulesLines, "ConfigClass:" . $ConfigClassFieldValues{"$class:configclass"} . "\n";

    foreach $key  (sort keys %ConfigClassFieldNames) {

	next if ($key =~ /ConfigClass$/i);

	push @RulesLines, "$key:" . join("\\\n",split(/\n/,$ConfigClassFieldValues{"$class:$key"})) . "\n"
	    if (defined($ConfigClassFieldValues{"$class:$key"}));
    }

    push @RulesLines, "\n";

}

push @RulesLines, "#\n";
push @RulesLines, "# The following user supplied parameters were used:\n";
push @RulesLines, "#\n";

foreach $local_name (sort keys %ConfigLocalsDefined) {

    push @RulesLines, "\n";
    push @RulesLines, "ConfigLocalName:" . $ConfigLocalFieldValues{"$local_name:configlocalname"} . "\n";

    foreach $key  (sort keys %ConfigLocalFieldNames) {

	next if ($key =~ /ConfigLocalName$/i);

	push @RulesLines, "$key:" . join("\\\n",split(/\n/,$ConfigLocalFieldValues{"$local_name:$key"})) . "\n"
	    if (defined($ConfigLocalFieldValues{"$local_name:$key"}));
    }

    push @RulesLines, "\n";

}

push @RulesLines, "#\n";
push @RulesLines, "# The rules checked by this configuration:\n";
push @RulesLines, "#\n";

foreach $local_name (sort keys %RulesDefined) {

    push @RulesLines, "\n";
    push @RulesLines, "RuleName:" . $RuleFieldValues{"$local_name:rulename"} . "\n";

    foreach $key  (sort keys %RuleFieldNames) {

	next if ($key =~ /RuleName$/i);

	push @RulesLines, "$key:" . join("\\\n",split(/\n/,$RuleFieldValues{"$local_name:$key"})) . "\n"
	    if (defined($RuleFieldValues{"$local_name:$key"}));
    }

    push @RulesLines, "\n";

}

push @RulesLines, "#\n";
push @RulesLines, "# The folowing global configuration parameters were applied:\n";
push @RulesLines, "#\n";


push @RulesLines, "\n";

foreach $global_name (sort keys %ConfigGlobalsDefined) {

    push @RulesLines, "$global_name:" . join("\\\n",split(/\n/,$ConfigGlobalFieldValues{"$global_name"})) . "\n"
	    if (defined($ConfigGlobalFieldValues{"$global_name"}));

    push @RulesLines, "\n";

}

#
# Update .MASTER file if asked to do so
#

if ($opt_update_master) {
    printf STDERR "$progname: Updating $TemplateFile...";
    open(MASTER,">$TemplateFile") || die "Can't open $TemplateFile for writing: $!";
    print MASTER join("",@RulesLines) || die "$progname: error writing to $TemplateFile: $!";
    close(MASTER);
    printf "Done.\n";
}

#
# Perform substitutions and write new ncat.conf file
#

# do subs of longer strings first to avoid problems with 
# overlap (e.g. "interface_1" before "interface")

sub longest_first {
    length($ConfigLocalFieldValues{"$b:configlocalvalue"}) <=>
	length($ConfigLocalFieldValues{"$a:configlocalvalue"});
}

foreach $local_name (sort longest_first keys %ConfigLocalsDefined) {
    $local_name = lc($local_name);
    grep {s/\$\($local_name\)/$ConfigLocalFieldValues{"$local_name:configlocalvalue"}/ig unless (/^ConfigLocalName:/i)} @RulesLines
}

printf STDERR "$progname: Writing $RulesFile...";
print RULES join("",@RulesLines) || die "$progname: error writing to $RulesFile: $!";
close(RULES);

printf "Done.\n";
printf "\n";
printf "$progname: Now examine $RulesFile.\n";
printf "$progname: Edit $TemplateFile and rerun $progname if not satisfactory.\n";

exit 0;

#
# Prompt for user for yes/no or a value
#

sub ask_user {
    my($prompt,$type,$instance,$default,$info,$yesno) = @_;

    while (1) {

	# ask user for response
	
	$answer = "";
	
	unless ($opt_noprompt) {
	    print "$progname: $prompt [$default] ? ";
	    $answer = <>;
	    chomp $answer;
	}
	
	$answer = $default if ($answer =~ /^\s*$/);
	
	if ($answer =~ m/^([?])|(help)\s*$/) {
	    if ($info ne "") {
		print " Help for $type $class:\n\n";
		print " " . $info . "\n\n";
	    } else {
		print " Sorry, no help for $type $instance\n";
	    }
	    print " Default value is: $default\n\n";
	    next;
	}

	if ($yesno) {
	    if ($answer =~ /(^\s*$)|(\s*no\s*)|(\s*n\s*)/i) {
		return 0;
	    } elsif ($answer =~ /(\s*yes\s*)|(\s*ye\s*)|(\s*y\s*)/i) {
		return 1;
	    } # if we got "yes" or "no"
	    } else {
		return $answer;
	    }

    } # while need an answer
}



