#!/usr/bin/perl -w

# page-writer - write web pages from templates
# $Id: page-writer.pl,v 1.14 2002/08/14 11:32:55 remstats Exp $
# from remstats 1.0.13a

# Copyright 1999, 2000, 2001, 2002 (c) Thomas Erskine <terskine@users.sourceforge.net>
# See the COPYRIGHT file with the distribution.

# - - -   Configuration   - - -

use strict;

# What is this program called, for error-messages and file-names
$main::prog = 'page-writer';
# Where is the configuration dir
$main::config_dir = '/etc/remstats/config';
# How to show the status of an RRD variable (DS)
$main::status_cgi = '/remstats/var_status.cgi';
# Where to find the list of pages to generate
$main::pages_file = $main::config_dir . '/pages';
# How many levels of nesting to permit in variable substitution
$main::max_substitution_nesting = 5;
# What mode to use when making directories
$main::dir_mode = 0755;

# - - -   Version History   - - -

$main::version = (split(' ', '$Revision: 1.14 $'))[1];

# - - -   Setup   - - -

use lib '.', '/usr/lib/remstats/lib', '/usr/lib/perl5/';
require 5.005;
require "remstats.pl";
require "htmlstuff.pl";
require "alertstuff.pl";
use RRDs;
use Getopt::Std;

# Parse the command-line
my %opt = ();
getopts('d:f:hp:u', \%opt);

if( defined $opt{'h'}) { &usage(); } # no return
if( defined $opt{'d'}) { $main::debug = $opt{'d'}; } else { $main::debug = 0; }
if( defined $opt{'f'}) { $main::config_dir = $opt{'f'}; }
if( defined $opt{'p'}) { $main::pages_file = $opt{'p'}; }

&read_config_dir($main::config_dir, 'general', 'alerts', 'links', 'colors', 
	'tools', 'times', 'html', 'oids', 'rrds', 'customgraphs', 'groups', 
	'host-templates', 'hosts', 'alert-template-map');

# Make sure that rrdcgi is where we expect
if (-f $main::config{RRDCGI}) {
	$main::shebang = '#!' . $main::config{RRDCGI} . "\n";
}
else { &abort("rrdcgi ($main::config{RRDCGI}) isn't there"); }

# Get the indices
%main::indices = &init_indices();

# Which tags are simple
%main::simple_tag = (
	'ALERTSTATUS' => 1,
	'BAREHEADER' => 1,
	'CHDIR' => 1,
	'CHMOD' => 1,
	'CHGRP' => 1,
	'CONFIG' => 1,
	'CUSTOMGRAPH' => 1,
	'DEBUG::OUTPUT' => 1,
	'FOOTER' => 1,
	'GRAPH' => 1, 
	'HEADER' => 1,
	'INCLUDE' => 1,
	'MKDIR' => 1,
	'PAGE' => 1,
	'RRDCGI' => 1,
	'SET::VAR' => 1,
	'SET::VAR::FIXED' => 1,
	'SET::VAR::CONFIG' => 1,
	'RRDHEADER' => 1,
	'STATUS' => 1,
	'STATUSHEADER' => 1,
	'TEMPLATE' => 1,
	'TOOLBAR' => 1,
	'VAR' => 1,
	'VAR::WILD' => 1,
	'VAR::FIXED' => 1,
	'VAR::FIXED::WILD' => 1,
);

# And which are paired with end tags
%main::paired_tag = (
	'FOR::GROUP' => 1,
	'FOR::HOST' => 1,
	'FOR::RRD' => 1,
	'FOR::GRAPH' => 1,
	'FOR::GRAPHTIME' => 1,
	'FOR::CUSTOMGRAPH' => 1,
);


# Open the list of pages to generate
open( PAGES, "<$main::pages_file") 
	or &abort("can't open $main::pages_file: $!");

my( $name, $value, $template_name, $file, $current_dir, $line);

# Remember where we are
$current_dir = $main::config{HTMLDIR};
chdir $current_dir or &abort("can't chdir to $current_dir: $!");

# Umask makes chmod work differently to the way I want.  Mash it.
umask 0; # I.E. do what I said

# - - -   Mainline   - - -

while(<PAGES>) {
	chomp;
	next if( /^#/ or /^\s*$/); # no comments
	$line = $_;
	( $name, $value) = split(' ', $_, 2);
	$name = uc $name;

	# Generate a page
	if( $name eq 'PAGE') {
		$value = &var_arg_sub( 'page', $value);
		($template_name, $file) = split(' ', $value, 2);
		unless( $template_name and $file) {
			&abort("bad page directive: $line");
		}
		&debug("PAGE template=$template_name file=$file") if( $main::debug);
		&do_page( $template_name, $file);
		# FIXME pass in $current_dir, to make sure
	}

	# Change the current directory (making it if necessary)
	elsif( $name eq 'CHDIR') {
		$value = &var_arg_sub( 'chdir', $value);
		&debug("CHDIR to $value") if( $main::debug);
		unless( -d $value) {
			&do_mkdir( $value, $main::dir_mode);
			&note("mkdir (for chdir)$value");
		}
		chdir $value or &abort("can't chdir to $value");
		# FIXME keep track of where we are
	}

	# Change the mode of the specified file/dir
	elsif( $name eq 'CHMOD') {
		$value = &var_arg_sub( 'chmod', $value);
		&do_chmod( split(' ', $value, 2));
	}

	# Change the owner/group of the file
	elsif( $name eq 'CHGRP') {
		$value = &var_arg_sub( 'chgrp', $value);
		&do_chgrp( split(' ', $value, 2));
	}

	# Just make a directory, without changing to it
	elsif( $name eq 'MKDIR') {
		$value = &var_arg_sub( 'mkdir', $value);
		&debug("MKDIR $value") if( $main::debug);
		unless( -d $value) {
			&do_mkdir( $value, $main::dir_mode);
		}
	}

	# Set arbitrary variables ahead of time
	elsif( $name eq 'VAR') {
		($name, $value) = split(' ', $value, 2);
		unless( defined $value) { $value = ''; }
		$value = &var_arg_sub( 'var', $value);
		&debug("VAR $name = '$value'") if( $main::debug);
		$main::var{uc $name} = $value;
	}

	# Set arbitrary variables ahead of time, but fixed for using in paths
	elsif( $name eq 'VAR::FIXED') {
		($name, $value) = split(' ', $value, 2);
		unless( defined $value) { $value = ''; }
		$value = &var_arg_sub( 'var::fixed', $value);
		$value = &to_filename( $value);
		&debug("VAR::FIXED $name = '$value'") if( $main::debug);
		$main::var{uc $name} = $value;
	}

	# Set arbitrary variables from configuration
	elsif( $name eq 'VAR::CONFIG') {
		($name, $value) = split(' ', $value, 2);
		$value = &var_arg_sub( 'var::config', $value);
		$value = &get_config_value( $value);
		&debug("VAR $name = '$value'") if( $main::debug);
		$main::var{uc $name} = $value;
	}

	# Don't know that one
	else {
		&abort("unknown directive for pages config-file: $line");
	}
}
close(PAGES);

exit 0;

#------------------------------------------------------------ get_file ---
# Return the whole contents of a file appropriately as an array or string
#-----------------------------------------------------------------------
sub get_file {
	my( $file, $directory) = @_;
	my( @lines);
	if( defined $directory) { $file = $directory . '/' . $file; }

	open( GETFILE, "<$file") or do {
		&error("can't open $file: $!");
		return '';
	};
	@lines = <GETFILE> or do {
		&error("can't read $file: $!");
		return ''
	};
	return (wantarray) ? @lines : join('', @lines);
}

#----------------------------------------------------------- do_page ---
#-----------------------------------------------------------------------
sub do_page {
	my ($template_name, $file, $directory) = @_;
	my( $html);

# Generate the page from the template
	$html = &read_template( $template_name);

	# Shebang provided (or not) by page definition
	$html = &process_template( $template_name, $html);

# Write it
	open (PAGEFILE, ">$file.new") or do {
		&error("can't open $file.new: $!; skipped");
		return '';
	};
	print PAGEFILE $html or do {
		&error("can't write $file.new: $!");
		return '';
	};
	close (PAGEFILE) or do {
		&error("can't close $file.new: $!");
		return '';
	};
	chmod 0755, "$file.new" or do {
		&error("can't chmod $file.new: $!");
		return '';
	};
	rename "$file.new", $file or do {
		&error("can't rename $file.new to $file: $!");
		return '';
	};
	&debug("WROTE $file in ", &get_pwd()) if( $main::debug);

}

#-------------------------------------------------------------- get_pwd ---
# Figure out what the current directory is
#--------------------------------------------------------------------------
sub get_pwd {
	my $string = `pwd`;
	chomp $string;
	return $string;
}

#---------------------------------------------------------- some_of ---
# Return the leading text from a string, suffixed with ... if we had
# to truncate it.  So that a template can show what it's working on
# without getting too huge.
#-----------------------------------------------------------------------
sub some_of {
	my $string = shift @_;
	my $maxlen = 320;
	if( length($string) > $maxlen) {
		return substr($string, 0, $maxlen) . '...';
	}
	else { return $string; }
}

#------------------------------------------------------- show_vars ---
#-----------------------------------------------------------------------
sub show_vars {
	my $varname;
	&debug("variables are:");
	foreach $varname ('GROUP', 'HOST', 'RRD', 'GRAPH', 'GRAPHTIME',
			'CUSTOMGRAPH') {
		if( defined $main::var{$varname}) {
			&debug("  VAR $varname = '$main::var{$varname}'");
		}
		else {
			&debug("  VAR $varname undefined");
		}
	}
}

#------------------------------------------------------- process_template ---
#-----------------------------------------------------------------------
sub process_template {
	my( $template_name, $rest) = @_;
	&debug("process_template: $template_name for:\n", &some_of($rest))
		if( $main::debug>1);
	&show_vars() if( $main::debug>2);

	my( $page_tail, $whole_tag, $tag_name, $args, $temp, $html, $middle);
	$html = '';

	while( $rest) {

		# Tags need to be processed from the outside in, I.E. first
		# things first.  This is because the outside ones can cause 
		# the inside ones to be replicated
		if( $rest =~ /^(.*?)(<REMSTATS::([:A-Z]+)(\s+(.*?))?>)(.*)$/s) {
			$html .= (defined $1) ? $1 : '';
			$whole_tag = $2;
			$tag_name = $3;
			$args = defined $5 ? $5 : ''; # $4 includes leading spaces
			$rest = $6;

			# Simple tags are simple, i.e. no ends to worry about
			if( $main::simple_tag{$tag_name}) {
				$html .= &do_simple_tag( $template_name, $tag_name, $args);
			}

			# Paired tags need special handling
			elsif( $main::paired_tag{$tag_name}) {
				# Find the part between the tags
				if( $rest =~ m#^(.*?)</REMSTATS::$tag_name>(.*)$#s) {
					$middle = $1;
					$rest = $2;
					&debug("  found the end tag for $template_name; \n" .
						"middle=$middle\nrest=$rest\n") 
						if( $main::debug>1);
				}
				else {
					&error("can't find end-tag for $tag_name " .
						"in $template_name");
					return "[can't find end-tag for $tag_name " .
						"in $template_name]";
				}

				$html .= &do_paired_tag( $template_name, $tag_name, 
					$args, $middle);
			}

			else {
				&error("unknown tag '$tag_name' in template $template_name");
				return "[unknown tag '$tag_name' in template $template_name]";
			}
		}

		# No more tags
		else {
			$html .= $rest;
			$rest = '';
		}
	}
	return $html;
}


#----------------------------------------------------- do_simple_tag ---
# Process a tag which has no end-tag required.
#-----------------------------------------------------------------------
sub do_simple_tag {
	my( $template_name, $tag_name, $args) = @_;
	my( $template);

	if( $tag_name eq 'GRAPH') {
		$args = &var_arg_sub( 'graph tag', $args);
		&debug("$tag_name for $template_name with '$args'")
				if( $main::debug);
		$args =~ s/(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)/&do_graph($template_name, $1, $2, $3, $4, $5)/es;
	}

	elsif( $tag_name eq 'CUSTOMGRAPH') {
		$args = &var_arg_sub( 'customgraph tag', $args);
		&debug("$tag_name for $template_name with '$args'")
				if( $main::debug);
		$args =~ s/(\S+)\s+(\S+)\s+(\S+)/&do_customgraph( $template_name, $1, $2, $3)/es;
	}

	elsif( $tag_name eq 'INCLUDE') {
		$args = &var_arg_sub( 'include tag', $args);
		&debug("$tag_name for $template_name with '$args'")
				if( $main::debug);
		$args =~ s/(\S+)\s*/&do_include( $template_name, $1)/es;
	}

	elsif( $tag_name eq 'STATUS') {
		$args = &var_arg_sub( 'status tag', $args);
		&debug("$tag_name for $template_name with '$args'")
				if( $main::debug);
		$args =~ s/(\S+)\s+([^>]+)/&do_status( $1, $2)/es;
	}

	elsif( $tag_name eq 'ALERTSTATUS') {
		$args = &var_arg_sub( 'alertstatus tag', $args);
		&debug("$tag_name for $template_name with '$args'")
				if( $main::debug);
		$args =~ s/(\S+)\s+(\S+)\s+(\S+)(\s+value)?/&do_alert_status( $1, $2, $3)/es;
	}

	elsif( $tag_name eq 'HEADER') {
		$args = &var_arg_sub( 'header tag', $args);
		&debug("$tag_name for $template_name with '$args'")
				if( $main::debug);
		$args =~ s/([^>]+)/&html_header( $1, '', %main::indices)/es;
	}

	elsif( $tag_name eq 'BAREHEADER') {
		$args = &var_arg_sub( 'bareheader tag', $args);
		&debug("$tag_name for $template_name with '$args'")
				if( $main::debug);
		$args =~ s/\s*([^>]*$)/&bare_html_header( $1)/es;
	}

	elsif( $tag_name eq 'FOOTER') {
		&debug("$tag_name for $template_name")
				if( $main::debug);
		$args = &html_footer();
	}

	elsif( $tag_name eq 'CHDIR') {
		$args = &var_arg_sub( 'chdir tag', $args);
		&debug("$tag_name for $template_name with '$args'")
				if( $main::debug);
		unless( -d $args) {
			&do_mkdir( $args, $main::dir_mode);
			&note("mkdir (for chdir) $args");
		}
		chdir $args or do {
			$args = "[chdir $args failed: $!]";
			&error( $args);
			return $args;
		};
		$args = ''; # don't print anything
		&debug("  after chdir $args cwd is: ", &get_pwd()) if( $main::debug);
	}

	elsif( $tag_name eq 'CHMOD') {
		$args = &var_arg_sub( 'chmod tag', $args);
		&debug("$tag_name for $template_name with '$args'")
			if( $main::debug);
		&do_chmod( split(' ', $args, 2));
		$args = ''; # don't print anything
	}

	elsif( $tag_name eq 'CHGRP') {
		$args = &var_arg_sub( 'chgrp tag', $args);
		&debug("$tag_name for $template_name with '$args'")
			if( $main::debug);
		&do_chgrp( split(' ', $args, 2));
		$args = ''; # don't print anything
	}

	elsif( $tag_name eq 'MKDIR') {
		$args = &var_arg_sub( 'mkdir tag', $args);
		&debug("$tag_name for $template_name with '$args'")
			if( $main::debug);
		unless( -d $args) {
			&do_mkdir( $args, $main::dir_mode);
			&note("mkdir $args");
		}
		$args = ''; # don't print anything
	}

	# Insert the current host's toolbar
	elsif( $tag_name eq 'TOOLBAR') {
		$args = &var_arg_sub( 'toolbar tag', $args);
		&debug("$tag_name for $template_name with '$args'")
				if( $main::debug);
		if( defined $main::var{HOST}) {
			if( defined $main::config{HOST}{$main::var{HOST}}{IP}) {
				$args = &toolbar( $main::var{HOST}, 
					$main::config{HOST}{$main::var{HOST}}{IP});
			}
			else {
				$args = &toolbar( $main::var{HOST});
			}
		}
		else {
			&error("can't do a TOOLBAR with no current HOST");
			return '';
		}
	}

	# Insert the current host's status header
	elsif( $tag_name eq 'STATUSHEADER') {
		$args = &var_arg_sub( 'statusheader tag', $args);
		&debug("$tag_name for $template_name with '$args'")
				if( $main::debug);
		if( defined $main::var{HOST}) {
			if( defined $main::config{HOST}{IP}) {
				$args = &status_header( $main::var{HOST}, 
					$main::config{HOST}{IP});
			}
			else {
				$args = &status_header( $main::var{HOST});
			}
		}
		else {
			&error("can't do a STATUSHEADER with no current HOST");
			return '';
		}
	}

	# Insert the current graph's status header
	elsif( $tag_name eq 'RRDHEADER') {
		$args = &var_arg_sub( 'rrdheader tag', $args);
		&debug("$tag_name for $template_name with '$args'")
				if( $main::debug);
		if( defined $main::var{HOST} and defined $main::var{RRD}) {
			my ($wildrrd, $wildpart, $fixedrrd) = 
				&get_rrd( $main::var{RRD});
			$args = &rrd_status_header( $main::var{HOST}, $main::var{RRD},
				$wildrrd, $fixedrrd, $main::var{GRAPH});
		}
		else {
			&error("can't do a STATUSHEADER with no current HOST");
			return '';
		}
	}

	# Generate a page
	elsif( $tag_name eq 'PAGE') {
		$args = &var_arg_sub( 'page tag', $args);
		&debug("$tag_name for $template_name with '$args'")
				if( $main::debug);
		&do_page( split(' ', $args, 3));
		$args = '';
	}

	# Insert the processed output of a template here
	elsif( $tag_name eq 'TEMPLATE') {
		$args = &var_arg_sub( 'template tag', $args);
		&debug("$tag_name for $template_name with '$args'")
				if( $main::debug);
		$template = &read_template( $args);
		$args = &process_template( $args, $template);
	}

	elsif( $tag_name eq 'SET::VAR') {
		$args = &var_arg_sub( 'set::var tag', $args);
		&debug("$tag_name for $template_name with '$args'")
				if( $main::debug);
		my( $name, $value) = split(' ', $args, 2);
		unless( defined $value) { $value = ''; }
		$main::var{uc $name} = $value;
		$args = '';
	}

	elsif( $tag_name eq 'SET::VAR::FIXED') {
		$args = &var_arg_sub( 'set::var::fixed tag', $args);
		&debug("$tag_name for $template_name with '$args'")
				if( $main::debug);
		my( $name, $value) = split(' ', $args, 2);
		unless( defined $value) { $value = ''; }
		$value = &to_filename( $value);
		$main::var{uc $name} = $value;
		$args = '';
	}

	elsif( $tag_name eq 'SET::VAR::CONFIG') {
		$args = &var_arg_sub( 'set:var:config tag', $args);
		&debug("$tag_name for $template_name with '$args'")
				if( $main::debug);
		my( $name, $value) = split(' ', $args, 2);
		$value = &get_config_value( $args);
		$main::var{uc $name} = $value;
		$args = '';
	}

	elsif( $tag_name eq 'RRDCGI') {
		&debug("$tag_name for $template_name")
				if( $main::debug);
		$args = $main::shebang;
	}

	elsif( $tag_name eq 'VAR') {
		if( defined $main::var{uc $args}) {
			&debug("$tag_name for $template_name with '$args'")
					if( $main::debug);
			$args = $main::var{uc $args};
			$args = &wild_graph_sub( $args);
		}
		else {
			&error("unknown var '$args' in $template_name");
			$args = "[unknown var '$args' in $template_name]";
		}
	}

	elsif( $tag_name eq 'VAR::WILD') {
		if( defined $main::var{uc $args}) {
			&debug("$tag_name for $template_name with '$args'")
					if( $main::debug);
			$args = $main::var{uc $args};
		}
		else {
			&error("unknown var '$args' in $template_name");
			$args = "[unknown var '$args' in $template_name]";
		}
	}

	elsif( $tag_name eq 'VAR::FIXED') {
		if( defined $main::var{uc $args}) {
			&debug("$tag_name for $template_name with '$args'")
					if( $main::debug);
			$args = $main::var{uc $args};
			$args = &wild_graph_sub( $args);
			$args = &to_filename( $args);
		}
		else {
			&error("unknown var '$args' in $template_name");
			$args = "[unknown var '$args' in $template_name]";
		}
	}

	elsif( $tag_name eq 'VAR::FIXED::WILD') {
		if( defined $main::var{uc $args}) {
			&debug("$tag_name for $template_name with '$args'")
					if( $main::debug);
			$args = $main::var{uc $args};
			$args = &to_filename( $args);
		}
		else {
			&error("unknown var '$args' in $template_name");
			$args = "[unknown var '$args' in $template_name]";
		}
	}

	elsif( $tag_name eq 'CONFIG') {
		$args = &var_arg_sub( 'config tag', $args);
		&debug("$tag_name for $template_name with '$args'")
				if( $main::debug);
		my $insert = &get_config_value( $args);
		if( defined $insert) {
			$args = $insert;
		}
		else {
			&error("unknown config value '$args'");
			$args = "[unknown config value '$args']";
		}
	}

	elsif( $tag_name eq 'DEBUG::OUTPUT') {
		$args = &var_arg_sub( 'debug::output tag', $args);
		&debug($tag_name, ' for ', $template_name, ' with \'', $args, '\'')
			if( $main::debug);
		&debug('OUTPUT: ', $args); # this is correct; no debug check
		$args = '';
	}

	else {
		&error("unknown simple tag '$tag_name' in template $template_name");
		$args = "[unknown simple tag '$tag_name' in template $template_name]";
	}
	return $args;
}

#------------------------------------------------------ wild_graph_sub ---
# If this has a '*', and we're in a graph (i.e. var{GRAPH} is defined)
# Then change the '*' to wildpart.
#-----------------------------------------------------------------------
sub wild_graph_sub {
	my $string = shift @_;
	if( defined $main::var{GRAPH} and defined $main::var{RRD}) {
		my (undef, $wildpart) = &get_rrd( $main::var{RRD});
		$string =~ s/\*/$wildpart/ if( defined $wildpart);
	}
	return $string;
}

#------------------------------------------------------------ var_arg_sub ---
# Substitute variable cookies for their values in the args of a tag
#-----------------------------------------------------------------------
sub var_arg_sub {
	my( $where, $args) = @_;
	my $save_args = $args;
	my $loops = 0;
	while (++$loops < $main::max_substitution_nesting) {
		if( $args =~ /##([A-Z0-9]+)##/) {
			$args =~ s/##([A-Z0-9]+)##/&do_var( $where, $1)/eg;
		}
		else { last; }
	}
	&debug("var_arg_sub: old='$save_args', new='$args'") if( $main::debug>1);
	return $args;
}

#------------------------------------------------------------ do_var ---
# Returns the value of the named variable.  Could be replaced with a
# simple $main::var{$varname} if I could trust people to always get
# them right.  Since I sometimes make mistakes, I'll allow everyone
# else the same privelege and put out an error message here.
#-----------------------------------------------------------------------------
sub do_var {
	my( $where, $varname) = @_;
	my $string;
	if( defined $main::var{$varname}) {
		$string = $main::var{$varname};
		$string = &wild_graph_sub( $string);
	}
	else {
		&error("unknown variable '$varname' in args in $where");
		return "[unknown variable '$varname' in args]";
	}
	return $string;
}

#-------------------------------------------------------- get_config_value ---
# Pull a value out of the $main::config (the whole configuration), up to
# five levels down.  I've never needed more than two, so five ought to be
# enough.
#-----------------------------------------------------------------------------
sub get_config_value {
	my ($k1, $k2, $k3, $k4, $k5) = split(' ', scalar(shift @_));
	my $value;

	# I *could* construct a string and eval it, but this is probably
	# going to be faster, though the code will be a bit bigger.
	if( defined $k5) {
		$value = $main::config{$k1}{$k2}{$k3}{$k4}{$k5};
	}
	elsif( defined $k4) {
		$value = $main::config{$k1}{$k2}{$k3}{$k4};
	}
	elsif( defined $k3) {
		$value = $main::config{$k1}{$k2}{$k3};
	}
	elsif( defined $k2) {
		$value = $main::config{$k1}{$k2};
	}
	elsif( defined $k1) {
		$value = $main::config{$k1};
	}
	else {
		&error("REMSTATS::CONFIG must have at least one arg");
		$value = '';
	}
	return $value;
}

#----------------------------------------------------------- list_of ---
# Make a list of the requested type of thing.
#-----------------------------------------------------------------------
sub list_of {
	my ($template_name, $type, $within, $key) = @_;
	&debug("list_of: $type within='",
		(defined $within ? $within : '') , "'") if( $main::debug>2);
	my @names;
	if( $type =~ /^[^:]+::(.*)/) { $type = $1; } # lose the FOR::

	# List of GROUPs, in defined order
	if( $type eq 'GROUP') {
		# Keep the natural order
		@names = @{$main::config{GROUPS}};
	}

	# List of HOSTs
	elsif( $type eq 'HOST') {
		# HOSTs within a GROUP
		if( defined $within) {
			if( defined $main::var{GROUP} &&
					defined $main::config{GROUP}{$main::var{GROUP}}) {
				@names = sort @{$main::config{GROUP}{$main::var{GROUP}}}
			}
			else {
				&error('unknown or empty group \'', $main::var{GROUP},
					'\' for FOR::HOST in ', $template_name);
				return ();
			}
		}

		# HOSTs with a KEY
		elsif( defined $key) {
			@names = &hosts_with_key( $key);
		}

		# All hosts in alpha order
		else {
			@names = sort keys %{$main::config{HOST}};
		}
	}

	# List of RRDs
	elsif( $type eq 'RRD') {
		if( defined $within) {
			if( defined $main::var{HOST} && 
					defined $main::config{HOST}{$main::var{HOST}}) {
				if( defined $main::config{HOST}{$main::var{HOST}}{RRDS}) {
					@names = @{$main::config{HOST}{$main::var{HOST}}{RRDS}};
				}
				else {
					&error($main::var{HOST}, ' has no RRDs defined?');
					@names = ();
				}
			}
			else {
				&error("unknown host '$main::var{HOST}' for FOR::RRD " .
					"in $template_name");
				return ();
			}
		}

		# RRDs with a key
		elsif( defined $key) {
			@names = &rrds_with_key( $key);
		}

		# All RRDs
		else {
			@names = sort keys %{$main::config{RRD}};
		}
	}

	# List of GRAPHs is always in definition order, and requires WITHIN
	elsif( $type eq 'GRAPH') {

		# GRAPHs within RRD
		if( $within and $within eq 'RRD') {

			# Do we have ann RRD specified?
			unless( defined $main::var{RRD}) {
				&error("graph must be within RRD or CUSTOMGRAPH");
				return ();
			}

			# Do we have that RRD?
			my ($wildrrd) = &get_rrd( $main::var{RRD});
			unless( defined $wildrrd) {
				&error( "unknown rrd '$main::var{RRD}' for $template_name");
				return ();
			}

			# Keep them in their natural order
			if( defined $main::config{RRD}{$wildrrd}{GRAPHS}) {
				@names = @{$main::config{RRD}{$wildrrd}{GRAPHS}};
			}
			else {
				&debug("no graphs defined for RRD $wildrrd; skipped")
					if( $main::debug>1);
				return ();
			}
		}

		# GRAPHs within CUSTOMGRAPH
		elsif( $within and $within eq 'CUSTOMGRAPH') {
			
			# Do we have a CUSTOMGRAPH?
			unless( defined $main::var{CUSTOMGRAPH}) {
				&error("graph must be within RRD or CUSTOMGRAPH");
				return ();
			}

			# Does that CUSTOMGRAPH exist?
			unless( defined $main::var{CUSTOMGRAPH} && defined 
					$main::config{CUSTOMGRAPH}{$main::var{CUSTOMGRAPH}}) {
				&error("unknown CUSTOMGRAPH '", $main::var{CUSTOMGRAPH},
					"' for ", $template_name);
				return ();
			}

			# Keep them in their natural order
			if( defined $main::var{CUSTOMGRAPH} && defined 
					$main::config{CUSTOMGRAPH}{$main::var{CUSTOMGRAPH}}{GRAPH}){
				@names = @{$main::config{CUSTOMGRAPH}{$main::var{CUSTOMGRAPH}}{GRAPH}};
			}
			else {
				&error('no graphs defined for CUSTOMGRAPH ',
					$main::var{CUSTOMGRAPH});
			}

		}

		else {
			&error('rrd for graph not specified in template ', $template_name);
			return ();
		}
		# Drop thumbnail graphs
		@names = grep {!/^thumb/} @names;
	}

	elsif( $type eq 'CUSTOMGRAPH') {

		# CUSTOMGRAPHs within a HOST
		if( $within and $within eq 'HOST') {

			# Do we have a host?
			unless( $main::var{HOST}) {
				&error("CUSTOMGRAPH within HOST must have HOST defined");
				return ();
			}

			# Does this host exist?
			unless( $main::config{HOST}{$main::var{HOST}}) {
				&error("unknown host '$main::var{HOST}' for $template_name");
			}

			# Does this host have any CUSTOMGRAPHs?
			unless( $main::config{HOST}{$main::var{HOST}}{CUSTOMGRAPHS}) {
				&debug($main::var{HOST}, ' doesn\'t have any CUSTOMGRAPHS for ',
					$template_name) if( $main::debug>1);
				return ();
			}

			@names = @{$main::config{HOST}{$main::var{HOST}}{CUSTOMGRAPHS}};
		}

		# All CUSTOMGRAPHS
		else {
			if( defined $main::config{CUSTOMGRAPHS}) {
				@names = @{$main::config{CUSTOMGRAPHS}};
			}
			else { @names = (); }
		}

	}

	# List of GRAPHTIMEs, by increasing timespan covered, and within
	# that, from later start-time to earlier
	elsif( $type eq 'GRAPHTIME') {

		# GRAPHTIMES within RRD
		if( $within and $within eq 'GRAPH') {

			# Must have HOST and RRD defined for GRAPH
			unless( $main::var{HOST} && $main::var{RRD}
					and $main::var{GRAPH}) {
				&error("GRAPHTIME must be within HOST/RRD/GRAPH " .
					"or CUSTOMGRAPH for $template_name");
				return ();
			}

			# Does that RRD exist?
			my ($wildrrd) = &get_rrd( $main::var{RRD});
			unless( $wildrrd) {
				&error("unknown rrd '$main::var{RRD}' for $template_name");
				return ();
			}

			@names = @{$main::config{RRD}{$wildrrd}{TIMES}};
		}

		# GRAPHTIMEs within CUSTOMGRAPH
		elsif( $within and $within eq 'CUSTOMGRAPH') {

			# Do we have a CUSTOMGRAPH?
			unless( $main::var{CUSTOMGRAPH}) {
				&error("GRAPHTIME must be within HOST/RRD/GRAPH " .
					"or CUSTOMGRAPH for $template_name");
				return ();
			}

			# Does that CUSTOMGRAPH exist?
			unless( $main::config{CUSTOMGRAPH}{$main::var{CUSTOMGRAPH}}) {
				&error("unknown CUSTOMGRAPH '", $main::var{CUSTOMGRAPH},
					"' for ", $template_name);
				return ();
			}

			@names = @{$main::config{CUSTOMGRAPH}{$main::var{CUSTOMGRAPH}}{TIMES}};
		}

		# All graphtimes
		else {
			@names = sort bygraphtime keys %{$main::config{TIME}};
		}
		# Remove the thumnail time-spans
		@names = grep {!/^thumb/} @names;
	}

	# Wazzat?
	else {
		&error("unknown type '$type' to list in $template_name");
		return ();
	}

	&debug('   ', join(', ', @names)) if( $main::debug>2);
	return @names;
}

#---------------------------------------------------------- bygraphtime ---
# Sort routine to sort by increasing timespan covered, and within
# that, from later start-time to earlier
#-----------------------------------------------------------------------
sub bygraphtime {
	my ($aspan, $bspan);
	$aspan = $main::config{TIME}{$a}{FINISH} - $main::config{TIME}{$a}{START};
	$bspan = $main::config{TIME}{$b}{FINISH} - $main::config{TIME}{$b}{START};

	if( $aspan < $bspan) { return -1; }
	elsif( $aspan > $bspan) { return 1; }
	else {
		my $result = -1 * ($main::config{TIME}{$a}{START} <=> 
			$main::config{TIME}{$b}{START});
		return $result;
	}
}

#--------------------------------------------------------- do_paired_tag ---
# Manage a tag with an end-tag required.
#---------------------------------------------------------------------------
sub do_paired_tag {
	my( $template_name, $tag_name, $args, $middle) = @_;
	my( @names, $value, $html, $type, $new);
	&debug("$tag_name for $template_name with '$args'") if( $main::debug);

	# Figure out the values for the iterator variable
	if( $tag_name =~ /^FOR::(.*)/) { $type = uc $1; }
	else { $type = uc $tag_name; }
	@names = &get_name_list( $template_name, $type, $args);

	# Now replicate it, passing each iteration through process_template
	$html = '';
	foreach $value (@names) {
		# Set the iterator-related var
		$main::var{$type} = $value;
		&debug("  set $type to '$value'") if( $main::debug>1);

		# Recurse on the part between the start and end tags (middle)
		$new = &process_template( $template_name, $middle);
		$html .= $new;
		&debug("new html=$new") if( $main::debug>1);

		&debug("  $template_name: done $tag_name with '$value'")
			if( $main::debug>1);
	}

	# Remove the iterator-related variable
	undef $main::var{$type};

	return $html;
}

#-------------------------------------------------------- get_name_list ---
# Given a type of iterator tag ($tag_name) and the args it was passed, 
# figure out which names it should apply to.
#--------------------------------------------------------------------------
sub get_name_list {
	my( $template_name, $tag_name, $args) = @_;
	&debug("get_name_list: for $template_name $tag_name from '$args'")
		if( $main::debug>1);
	my( $arg, $name, $list, $within, @names, $key);

	if( $args eq '') {
		@names = &list_of( $template_name, $tag_name);
		&debug("  list of $tag_name from all") if( $main::debug>1);
	}
	else {

		# See if there is a key specified
		foreach $arg (split(' ', $args)) {
			if( $arg =~ /^KEY="([^"]+)"/) {
				$key = $1;
			}
		}

		# Which kind are we getting names from
		foreach $arg (split(' ', $args)) {
			if( $arg =~ /^WITHIN="([^"]+)"/) {
				$within = $1;
				@names = &list_of( $template_name, $tag_name, $within, $key);
				&debug("  list of $tag_name from within") if( $main::debug>1);
			}
			elsif ($arg =~ /^NAME="([^"]+)/) {
				$name = $1;
				@names = ($name);
				if( defined $key) {
					unless( &host_has_key( $name, $key)) {
						@names = ();
					}
				}
				&debug("  list of $tag_name from name") if( $main::debug>1);
			}
			elsif( $arg =~ /^LIST="([^"]+)/) {
				$list = $1;
				if( defined $key) {
					@names = ();
					my @temp = split(',', $list);
					foreach my $name (@temp) {
						next unless( &host_has_key( $name, $key));
						push @names, $name;
					}
				}
				else {
					@names = split(',', $list);
				}
				&debug("  list of $tag_name from list") if( $main::debug>1);
			}

			# KEY= is the only selector
			elsif( $arg =~ /^KEY=/) {
				my @temp = &list_of( $template_name, $tag_name, $within, $key);
				@names = ();
				foreach my $name( @temp) {
					if( $tag_name eq 'HOST') {
						next unless( &host_has_key( $name, $key));
					}
					elsif( $tag_name eq 'RRD') {
						next unless( &rrd_has_key( $name, $key));
					}
					push @names, $name;
				}
			}

			# Hmm.  I don't know what they want.
			else {
				&error("unknown args '$arg' for $tag_name in $template_name");
				return "[unknown args '$arg' for $tag_name in $template_name]";
			}

			# Make sure that we have one of NAME, LIST
			if( ($name and $list) or ($name and $within) or 
					($list and $within)) {
				&error("only one of NAME, LIST, or WITHIN for $tag_name " .
					"in template $template_name");
				return "[only one of NAME, LIST, or WITHIN for $tag_name " .
					"in template $template_name]";
			}
		}
	}

	# Special case: a host config-file can specify nograph to drop graphs
	if( $tag_name eq 'GRAPH') {
		if( defined $main::var{HOST} and defined $main::var{RRD}) {
			my @temp_names = @names;
			@names = ();
			foreach $name (@temp_names) {
				unless( defined $main::config{HOST}{$main::var{HOST}}{NOGRAPH}{$main::var{RRD}}{$name}) {
					push @names, $name;
				}
			}
		}
	}

	&debug("  got names list:\n", join(', ', @names)) if( $main::debug>1);
	return @names;
}

#-------------------------------------------------------- do_status ---
#-----------------------------------------------------------------------
sub do_status {
	my ($host, $statusfile) = @_;
	my ($status) = &get_status($host, $statusfile);
	if ($status ne 'MISSING') {
		$status = '<RRD::INCLUDE '. $main::config{DATADIR} .'/'.
			$host .'/'. $statusfile .'>';
	}
$status;
}

#--------------------------------------------------- do_alert_status ---
#-----------------------------------------------------------------------
sub do_alert_status {
	my ($host, $realrrd, $var) = @_;
	my $fixed_host = $host;
	my $fixed_rrd = $realrrd;
	my $fixed_var = $var;
	my $status = <<"EOD_STATUS";
<IMG SRC="$main::status_cgi?host=$fixed_host&rrd=$fixed_rrd&var=$fixed_var" 
	ALT="alert status of $fixed_host $fixed_rrd $fixed_var">
EOD_STATUS
	return $status;
}

#----------------------------------------------------------- do_graph ---
#------------------------------------------------------------------------
sub do_graph {
	my ($template_name, $dir, $host, $realrrd, $graph, $graphtime) = @_;
	my ($wildrrd, $wildpart) = &get_rrd($realrrd);
	my ($html, $desc);

	$graph =~ s/-$wildpart$/-\*/ if( defined $wildpart);
	unless (defined $main::config{RRD}{$wildrrd}{GRAPH}{$graph}) {
		&error("no graph '$graph' in rrd '$wildrrd' for '$template_name'");
		$html = "[no graph '$graph' in rrd '$wildrrd' for '$template_name']";
		return $html;
	}

	# Make sure we have at least a default GRAPHTIME
	if (!defined $graphtime or (defined $graphtime and $graphtime eq '')) {
		$graphtime = 'day';
	}

	# Make sure that this HOST wants this RRD
	unless( defined $main::config{HOST}{$host}{RRD}{$realrrd}) {
		&debug("$host doesn't have $realrrd; skipped") if( $main::debug>1);
		return '';
	}

	# Pull in the description, if there is one
	if( defined $main::config{HOST}{$host}{RRDDESC}{$realrrd}) {
		$desc = $main::config{HOST}{$host}{RRDDESC}{$realrrd};
	}
	else {
		$desc = $host . ' - ' . $realrrd . ' - ' . $graph . ' (' . 
			$graphtime . ')';
	}

	# The HTML, please
	&debug("GRAPH for $template_name with '$host $realrrd $graph $graphtime'")
		if ($main::debug);
	if( defined $dir and $dir eq '') { undef $dir; }
	$html = &make_rrdcgi_graph( $dir, undef, $host, $realrrd, $graph, 
		$graphtime, $desc);

	return $html;
}

#---------------------------------------------------- do_customgraph ---
#-----------------------------------------------------------------------
sub do_customgraph {
	my ($template_name, $dir, $customgraph, $graphtime) = @_;
	my $html;
	if (defined $main::config{CUSTOMGRAPH}{$customgraph}) {

		# Make sure we have at least a default GRAPHTIME
		if (!defined $graphtime or (defined $graphtime and $graphtime eq '')) {
			$graphtime = 'day';
		}
		&debug("CUSTOMGRAPH for $customgraph with '$graphtime'")
			if ($main::debug);
		if ( defined $dir and $dir eq '') { undef $dir; }
		$html = &make_custom_graph( $dir, undef, $customgraph, $graphtime);
	}
	else {
		&error("no such customgraph as '$customgraph' for template " .
			"'$template_name'; skipped");
		$html = "[no such customgraph as '$customgraph' for template " .
			"'$template_name']";
	}
	return $html;
}

#------------------------------------------------------ do_include ---
#-----------------------------------------------------------------------
sub do_include {
	my ($template_name, $file) = @_;
	&debug("  include: $file") if ($main::debug);
	my $html = '<RRD::INCLUDE '. $file .'>';
$html;
}

#----------------------------------------------------------------- usage ---
#-----------------------------------------------------------------------
sub usage {
	print STDERR <<"EOD_USAGE";
$main::prog version $main::version from remstats 1.0.13a
usage: $0 [options] collector
where options are:
  -d nnn  enable debugging output at level 'nnn'
  -f fff  use 'fff' for config-dir [$main::config_dir]
  -h      show this help
  -p ppp  use 'ppp' as the pages file instead
EOD_USAGE
	exit 0;
}

#----------------------------------------------------------------- debug ---
#-----------------------------------------------------------------------
sub debug {
	print STDERR 'DEBUG: ', @_, "\n";
}

#----------------------------------------------------------------- error ---
#-----------------------------------------------------------------------
sub error {
	print STDERR 'ERROR: ', @_, "\n";
}

#----------------------------------------------------------------- abort ---
#-----------------------------------------------------------------------
sub abort {
	print STDERR 'ABORT: ', @_, "\n";
	exit 1;
}

#------------------------------------------------------------ read_template ---
#------------------------------------------------------------------------------
sub read_template {
	my $template_name = shift @_;
	my $string;

	# Pull it from the cache if it's there
	if( defined $main::template_cache{$template_name}) {
		$string = $main::template_cache{$template_name};
	}

	# Have to read from file and munge
	else {
		$string = &get_file( $template_name, $main::config_dir . '/' .  
			$main::config{PAGETEMPLATEDIR});
		$string =~ s/\\\n//mg;
	}
	return $string;
}

#------------------------------------------------------------- do_chmod ---
#--------------------------------------------------------------------------
sub do_chmod {
	my( $mode, $file) = @_;
	&debug("CHMOD $file to $mode") if( $main::debug);
	if( -e $file) {
		chmod oct($mode), $file or do {
			&error( "can't chmod $file: $!");
			return 0;
		};
	}
	else {
		&error("can't chmod $file because it doesn't exist.");
		return 0;
	}
	return 1;
}

#------------------------------------------------------------- do_chgrp ---
#--------------------------------------------------------------------------
sub do_chgrp {
	my( $group, $file) = @_;
	my( $gid);
	&debug("CHGRP $file to $group") if( $main::debug);

	# Need the gid for group
	(undef, undef, $gid) = getgrnam( $group) or do {
		&error("can't chgrp: can't get gid for $group: $!");
		return 0;
	};

	# Does it exist?
	if( -e $file) {
		chown -1, $gid, $file or do {
			&error( "can't chgrp $file: $!");
			return 0;
		};
	}
	else {
		&error("can't chgrp $file because it doesn't exist.");
		return 0;
	}
	return 1;
}

#------------------------------------------------------------ do_mkdir ---
sub do_mkdir {
	my( $dir, $mode) = @_;
	mkdir $dir, $mode, or
		&error("can't mkdir $dir from ", &get_pwd(), ": $!");
}

#---------------------------------------------------------------- note ---
sub note {
	print STDERR 'NOTE: ', @_, "\n";
}
