package Munin::Master::Utils;
# -*- cperl -*-

# $Id: Utils.pm 3236 2009-12-18 06:25:03Z feiner.tom $

use strict;
use warnings;

use Carp;
use Exporter;
use English qw(-no_match_vars);
use Fcntl qw(:DEFAULT :flock);
use File::Path;
use IO::Handle;
use Munin::Common::Defaults;
use Munin::Master::Logger;
use Munin::Master::Config;
use Munin::Common::Config;
use Log::Log4perl qw (:easy);
use POSIX qw(strftime);
use POSIX ":sys_wait_h";
use RRDs;
use Symbol 'gensym';

our (@ISA, @EXPORT);

@ISA = ('Exporter');
@EXPORT = ('munin_trend',
	   'munin_fetch',
	   'munin_nscasend',
	   'munin_createlock',
	   'munin_removelock',
	   'munin_runlock',
	   'munin_getlock',
	   'munin_readconfig',
	   'munin_writeconfig',
	   'munin_delete',
	   'munin_overwrite',
	   'munin_config',
	   'munin_draw_field',
	   'munin_get_bool',
	   'munin_get_bool_val',
	   'munin_get',
	   'munin_field_status',
	   'munin_service_status',
	   'munin_node_status',
	   'munin_category_status',
	   'munin_get_picture_filename',
	   'munin_get_html_filename',
	   'munin_get_filename',
	   'munin_graph_column_headers',
	   'munin_get_max_label_length',
	   'munin_get_field_order',
	   'munin_get_rrd_filename',
	   'munin_get_node_name',
	   'munin_get_parent_name',
	   'munin_get_node_loc',
	   'munin_get_node',
	   'munin_set_var_loc',
	   'munin_set',
	   'munin_copy_node_toloc',
	   'munin_get_separated_node',
	   'munin_mkdir_p',
	   'munin_find_field',
	   'munin_find_field_for_limits',
	   'munin_get_parent',
	   'munin_get_children',
	   'munin_get_node_partialpath',
	   'munin_has_subservices',
	   'print_version_and_exit',
	   'exit_if_run_by_super_user',
	   'look_for_child',
	   'wait_for_remaining_children',
	   );

my $VERSION = $Munin::Common::Defaults::MUNIN_VERSION;

my $nsca = new IO::Handle;
my $config = undef;

my $configfile="$Munin::Common::Defaults::MUNIN_CONFDIR/munin.conf";

# Fields to copy when "aliasing" a field
my @COPY_FIELDS    = ("label", "draw", "type", "rrdfile", "fieldname", "info");

my @dircomponents = split('/',$0);
my $me = pop(@dircomponents);


sub munin_trend {
    my (@array) = @_;
    return ($array[$#array] - $array[0]);
}


sub munin_fetch {
    my ($file,$last,$type) = @_;
    my ($start,$step,$names,$data) = RRDs::fetch $file,$type || "AVERAGE";
    unless (defined $data)
    {
        WARN ("[WARNING] Could not fetch data from $file(".($type||"AVERAGE")."): ". RRDs::error);
        return;
    }
    my @array = map { @$_[0] } splice(@$data, $#$data - ($last || 1));
    return $array[0] if (!$last);
    return @array;
}


sub munin_draw_field {
    my $hash   = shift;

    return 0 if munin_get_bool ($hash, "skipdraw", 0);
    return 0 if !munin_get_bool ($hash, "graph", 1);
    return defined $hash->{"label"};
}


sub munin_nscasend {
    my ($name,$service,$label,$level,$comment) = @_;

    if (!$nsca->opened)
    {
	open ($nsca ,'|-', "$config->{nsca} $config->{nsca_server} -c $config->{nsca_config} -to 60");
    }
    if ($label)
    {
	print $nsca "$name\t$service: $label\t$level\t$comment\n";
	DEBUG "To $nsca: $name;$service: $label;$level;$comment\n";
    }
    else
    {
	print $nsca "$name\t$service\t$level\t$comment\n";
	DEBUG "[DEBUG] To $nsca: $name;$service;$level;$comment\n";
    }
}


sub munin_createlock {
    # Create lock file, fail and die if not possible.
    my ($lockname) = @_;
    if (sysopen (LOCK,$lockname,O_WRONLY | O_CREAT | O_EXCL)) {
	DEBUG "[DEBUG] Creating lock : $lockname succeeded\n";
	print LOCK $$; # we want the pid inside for later use
	close LOCK;
	return 1;
    } else {
	LOGCROAK("Creating lock $lockname failed: $!\n");
    }
}


sub munin_removelock {
    # Remove lock or die trying.
    my ($lockname) = @_;

    unlink $lockname or
      LOGCROAK("[FATAL ERROR] Error deleting lock $lockname: $!\n");
}


sub munin_runlock {
    my ($lockname) = @_;
    unless (munin_getlock($lockname)) {
	LOGCROAK("[FATAL ERROR] Lock already exists: $lockname. Dying.\n");
    }
    return 1;
}


sub munin_getlock {
    my ($lockname) = @_;
    if (-f $lockname) {
	DEBUG "[DEBUG] Lock $lockname already exists, checking process";
	# Is the lockpid alive?

	# To check this is inteligent and so on.  It also makes for a
	# nice locking racing-condition.  BUT, since munin-* runs from
	# cron every 5 minutes this should not be a real threat.  This
	# ream of code should complete in less than 5 minutes.

	open my $LOCK, '<', $lockname or
	  LOGCROAK("Could not open $lockname for reading: $!\n");
	my $pid = <$LOCK>;
	$pid = '' if !defined($pid);
	close($LOCK) or LOGCROAK("Could not close $lockname: $!\n");
	
	DEBUG "[DEBUG] Lock contained pid '$pid'";
	
        # Make sure it's a proper pid
	if (defined($pid) and $pid =~ /^(\d+)$/ and $1 != 1) {
	    $pid = $1;
	    if (kill(0, $pid)) {
		DEBUG "[DEBUG] kill -0 $pid worked - it is still alive. Locking failed.";
	        return 0;
	    }
	    INFO "[INFO] Process $pid is dead, stealing lock, removing file";
	} else {
	    INFO "[INFO] PID in lock file is bogus.  Removing lock file";
	}
	munin_removelock($lockname);
    }
    DEBUG "[DEBUG] Creating new lock file $lockname";
    munin_createlock($lockname);
    return 1;
}


sub munin_delete {
    my ($config,$data) = @_;
    for my $domain (keys %{$data->{domain}}) {
	unless ($config->{domain}->{$domain}) {
	    DEBUG "[DEBUG] Removing domain: $domain";
	    delete ($data->{domain}->{$domain});
	    next;
	}
	for my $node (keys %{$data->{domain}->{$domain}->{node}}) {
	    unless ($config->{domain}->{$domain}->{node}->{$node}) {
		DEBUG "[DEBUG] Removing node from $domain: $node";
		delete ($data->{domain}->{$domain}->{node}->{$node});
	    }
	}
    }
    return ($data);
}


sub munin_overwrite {
    # copy from $overwrite OVER $config.

    my ($configfile,$overwrite) = @_;
    for my $key (keys %$overwrite) {
        next if $key =~ /^#%#/;
	if (ref $overwrite->{$key}) {
	    if (!defined $configfile->{$key}) {
		if (ref $overwrite->{$key} eq "HASH") {
		    $configfile->{$key}->{'#%#parent'} = $configfile;
		    $configfile->{$key}->{'#%#name'}   = $key;
		    munin_overwrite($configfile->{$key},$overwrite->{$key});
		} else {
		    $configfile->{$key} = $overwrite->{$key};
		}
	    } else {
		munin_overwrite($configfile->{$key},$overwrite->{$key});
	    }
	} else {
	    $configfile->{$key} = $overwrite->{$key};
	}
    }
    return ($configfile);
}


sub munin_readconfig {
    my ($conf, $missingok, $corruptok) = @_;

    my $config = undef;
    my @contents = undef;

    $conf ||= $configfile;
    if (! -r $conf and ! $missingok) {
	WARN "munin_readconfig: cannot open '$conf'";
	return;
    }
    if (open (my $CFG, '<', $conf)) {
	@contents = <$CFG>;
	close ($CFG);
        $config = munin_parse_config (\@contents);
    }

    # Some important defaults before we return...
    $config->{'dropdownlimit'} ||= $Munin::Common::Defaults::DROPDOWNLIMIT;
    $config->{'rundir'}        ||= $Munin::Common::Defaults::MUNIN_STATEDIR;
    $config->{'dbdir'}         ||= $Munin::Common::Defaults::MUNIN_DBDIR;
    $config->{'logdir'}        ||= $Munin::Common::Defaults::MUNIN_LOGDIR;
    $config->{'tmpldir'}       ||= "$Munin::Common::Defaults::MUNIN_CONFDIR/templates/";
    $config->{'htmldir'}       ||= $Munin::Common::Defaults::MUNIN_HTMLDIR;
    $config->{'spooldir'}      ||= $Munin::Common::Defaults::MUNIN_SSPOOLDIR;
    $config->{'#%#parent'}     = undef;
    $config->{'#%#name'}       = "root";

    return ($config);
}


sub munin_parse_config
{
    my $lines    = shift;
    my $hash     = {};
    my $prefix   = "";
    my $prevline = "";

    foreach my $line (@{$lines})
    {
	chomp $line;
	if ($line =~ /#/) {
	    next if ($line =~ /^#/);
	    $line =~ s/(^|[^\\])#.*/$1/g;
	    $line =~ s/\\#/#/g;
	}
	next unless ($line =~ /\S/);  # And empty lines...
	if (length $prevline) {
	    $line = $prevline . $line;
	    $prevline = "";
	}
	if ($line =~ /\\\\$/) {
	    $line =~ s/\\\\$/\\/;
	} elsif ($line =~ /\\$/) {
	    ($prevline = $line) =~ s/\\$//;
	    next;
	}
	$line =~ s/\s+$//g;           # And trailing whitespace...
	$line =~ s/^\s+//g;           # And heading whitespace...

	if ($line =~ /^\.(\S+)\s+(.+)\s*$/) {
	    my ($var, $val) = ($1, $2);
	    $hash = munin_set_var_path ($hash, $var, $val);
	} elsif ($line =~ /^\s*\[([^\]]*)]\s*$/) {
	    $prefix = $1;
	    if ($prefix =~ /^([^:]+);([^:;]+)$/) {
		$prefix .= ":";
	    } elsif ($prefix =~ /^([^:;]+);$/) {
		$prefix .= "";
	    } elsif ($prefix =~ /^([^:;]+);([^:;]+):(.*)$/) {
		$prefix .= ".";
	    } elsif ($prefix =~ /^([^:;]+)$/) {
		(my $domain = $prefix) =~ s/^[^\.]+\.//;
		$prefix = "$domain;$prefix:";
	    } elsif ($prefix =~ /^([^:;]+):(.*)$/) {
		(my $domain = $prefix) =~ s/^[^\.]+\.//;
		$prefix = "$domain;$prefix.";
	    }
	} elsif ($line =~ /^\s*(\S+)\s+(.+)\s*$/) {
	    my ($var, $val) = ($1, $2);
	    $hash = munin_set_var_path ($hash, "$prefix$var", $val);
	} else {
	    warn "Malformed configuration line \"$line\".";
	}
    }
    return $hash;
}


sub munin_get_var_path
{
    my $hash = shift;
    my $var  = shift;
    my $val  = shift;

    DEBUG "DEBUG: Getting var \"$var\" = \"$val\"\n";
    if ($var =~ /^\s*([^;:]+);([^;:]+):(\S+)\s*$/)
    {
	my ($dom, $host, $rest) = ($1, $2, $3);
	my @sp = split (/\./, $rest);

	if (@sp == 3)
	{
	    return $hash->{domain}->{$dom}->{node}->{$host}->{client}->{$sp[0]}->{"$sp[1].$sp[2]"};
	}
	elsif (@sp == 2)
	{
	    return $hash->{domain}->{$dom}->{node}->{$host}->{client}->{$sp[0]}->{$sp[1]};
	}
	elsif (@sp == 1)
	{
	    return $hash->{domain}->{$dom}->{node}->{$host}->{$sp[0]};
	}
	else
	{
	    warn "munin_set_var_path: Malformatted variable path \"$var\".";
	}
    }
    elsif ($var =~ /^\s*([^;:]+);([^;:]+)\s*$/)
    {
	my ($dom, $rest) = ($1, $2);
	my @sp = split (/\./, $rest);

	if (@sp == 1)
	{
	    return $hash->{domain}->{$dom}->{$sp[0]};
	}
	else
	{
	    warn "munin_set_var_path: Malformatted variable path \"$var\".";
	}
    }
    elsif ($var =~ /^\s*([^;:\.]+)\s*$/)
    {
	return $hash->{$1};
    }
    else
    {
	warn "munin_set_var_path: Malformatted variable path \"$var\".";
    }

    return;
}

 
sub munin_find_field {
    # Starting at the (presumably the root) $hash make recursive calls
    # until for example graph_title or value is found, and then
    # continue recursing and itterating to all are found.
    #
    # Then we return a array of pointers into the $hash

    my $hash  = shift;
    my $field = shift;
    my $avoid = shift;
    my $res = [];

    if (ref ($field) ne "Regexp") {
	$field = qr/^$field$/;
    }

    if (ref ($hash) eq "HASH") {
	foreach my $key (keys %{$hash}) {
	    next if $key =~ /^#%#/;
	    last if defined $avoid and $key eq $avoid;
	    if ($key =~ $field) {
		push @$res, $hash;
	    } elsif (ref ($hash->{$key}) eq "HASH") {
		push @$res, @{munin_find_field ($hash->{$key}, $field, $avoid)};
	    }
	}
    }

    return $res;
}


sub munin_find_field_for_limits {
    my $hash  = shift;
    my $field = shift;
    my $avoid = shift;
    my $res = [];

    if (ref ($field) ne "Regexp") {
	$field = qr/^$field$/;
    }

    if (ref ($hash) eq "HASH") {
	foreach my $key (keys %{$hash}) {
	    next if $key =~ /^#%#/;
	    last if defined $avoid and $key eq $avoid;
	    if (ref ($hash->{$key}) eq "HASH") {
		push @$res, @{munin_find_field_for_limits ($hash->{$key}, $field, $avoid)};
	    } elsif ($key =~ $field) {
		push @$res, $hash;
	    }
	}
    }

    return $res;
}


sub munin_get_children {
    my $hash  = shift;
    my $res = [];

    return if (ref ($hash) ne "HASH");

    foreach my $key (keys %{$hash}) {
	next if $key =~ /^#%#/;
	if (defined $hash->{$key} and ref ($hash->{$key}) eq "HASH") {
	    push @$res, $hash->{$key};
	}
    }

    return $res;
}


sub munin_get_separated_node
{
    my $hash = shift;
    my $ret  = {};

    if (ref ($hash) eq "HASH") {
	foreach my $key (keys %$hash) {
	    next if $key =~ /^#%#/;
	    if (ref ($hash->{$key}) eq "HASH") {
		$ret->{$key} = munin_get_separated_node ($hash->{$key});
	    } else {
		$ret->{$key} = $hash->{$key};
	    }
	}
    } else {
	return;
    }

    return $ret;
}


sub munin_get_parent_name
{
    my $hash = shift;

    if (ref ($hash) eq "HASH" and defined $hash->{'#%#parent'}) {
	return munin_get_node_name ($hash->{'#%#parent'});
    } else { 
	return "none";
    }
}


sub munin_get_node_name
{
    my $hash = shift;

    if (ref ($hash) eq "HASH" and defined $hash->{'#%#name'}) {
	return $hash->{'#%#name'};
    } else { 
	return;
    }
}


sub munin_get_picture_loc {
    my $hash = shift;
    my $res = [];

    if (ref ($hash) ne "HASH") { # Not a hash node
    	return;
    }
    if (defined $hash->{'#%#origin'}) {
	$res = munin_get_picture_loc ($hash->{'#%#origin'});
    } elsif (defined $hash->{'#%#parent'}) {
	$res = munin_get_picture_loc ($hash->{'#%#parent'});
	push @$res, munin_get_node_name ($hash) if defined $res;
    }
    return $res;
}


sub munin_get_node_loc {
    my $hash = shift;
    my $res = [];

    if (ref ($hash) ne "HASH") { # Not a has node
    	return;
    }
    if (defined $hash->{'#%#parent'}) {
	$res = munin_get_node_loc ($hash->{'#%#parent'});
	push @$res, munin_get_node_name ($hash) if defined $res;
    }
    return $res;
}


sub munin_get_parent {
    my $hash = shift;

    if (ref ($hash) ne "HASH") { # Not a has node
    	return;
    }
    if (defined $hash->{'#%#parent'}) {
	return $hash->{'#%#parent'};
    } else {
	return;
    }
}


sub munin_get_node {
    # From the given point in the hash itterate deeper into the
    # has along the path given by the array in $loc.
    # 
    # If any part of the path in $loc is undefined we bail.
    my $hash = shift;
    my $loc  = shift;

    foreach my $tmpvar (@$loc) {
	if ($tmpvar !~ /\S/) {
	    ERROR "[ERROR] munin_get_node: Cannot work on hash node \"$tmpvar\"";
	    return undef;
	}
	return undef if !exists $hash->{$tmpvar};
	$hash = $hash->{$tmpvar};
    }
    return $hash;
}

sub munin_set {
    my $hash = shift;
    my $var  = shift;
    my $val  = shift;

    return munin_set_var_loc ($hash, [$var], $val);
}


sub munin_set_var_loc
{
    my $hash = shift;
    my $loc  = shift;
    my $val  = shift;
    my @aloc = @$loc;

    my $tmpvar = shift @aloc;
    $tmpvar = shift @aloc while (defined $tmpvar and $tmpvar =~ /^#%#/);
    if ($tmpvar !~ /\S/) {
	ERROR "[ERROR] munin_set_var_loc: Cannot work on hash node \"$tmpvar\"";
	return;
    }
    if (@aloc > 0) {
	if (!defined $hash->{$tmpvar} or !defined $hash->{$tmpvar}->{"#%#name"}) { # Init the new node
	    $hash->{$tmpvar}->{"#%#parent"} = $hash;
	    $hash->{$tmpvar}->{"#%#name"} = $tmpvar;
	}
        return munin_set_var_loc ($hash->{$tmpvar}, \@aloc, $val);
    } else {
        WARN "[WARNING] munin_set_var_loc: Setting unknown option '$tmpvar' at "
	    . munin_get_keypath($hash)
	    unless Munin::Common::Config::cl_is_keyword($tmpvar);

	# FIX - or not.

        $hash->{$tmpvar} = $val;
	return $hash;
    }
}


sub munin_get_node_partialpath
{
    my $hash = shift;
    my $var  = shift;
    my $ret  = undef;

    return if !defined $hash or ref ($hash) ne "HASH";

    my $root    = munin_get_root_node ($hash);
    my $hashloc = munin_get_node_loc ($hash);
    my $varloc  = undef;

    if ($var =~ /^\s*([^:]+):(\S+)\s*$/) {
	my ($leftstring, $rightstring) = ($1, $2);

	my @leftarr = split (/;/, $leftstring);
	my @rightarr = split (/\./, $rightstring);
	push @$varloc, @leftarr, @rightarr
    } elsif ($var =~ /^\s*([^;:\.]+)\s*$/) {
	push @$varloc, $var;
    } elsif ($var =~ /^\s*(.+)\.([^\.:;]+)$/) {
	my ($leftstring, $rightstring) = ($1, $2);

	my @leftarr = split (/;/, $leftstring);
	my @rightarr = split (/\./, $rightstring);
	push @$varloc, @leftarr, @rightarr;
    } elsif ($var =~ /^\s*(\S+)\s*$/) {
	my @leftarr = split (/;/, $1);
	push @$varloc, @leftarr;
    } else {
	ERROR "[ERROR] munin_get_node_partialpath: Malformed variable path \"$var\".";
    }

    # We've got both parts of the loc (varloc and hashloc) -- let's figure out 
    # where they meet up.
    do {
	$ret = munin_get_node ($root, [@$hashloc, @$varloc]);
    } while (!defined $ret and pop @$hashloc);

    return $ret;
}


sub munin_set_var_path
{
    my $hash = shift;
    my $var  = shift;
    my $val  = shift;

    my $result = undef;

    DEBUG "[DEBUG] munin_set_var_path: Setting var \"$var\" = \"$val\"";

    if ($var =~ /^\s*([^:]+):(\S+)\s*$/) {
	my ($leftstring, $rightstring) = ($1, $2);

	my @leftarr = split (/;/, $leftstring);
	my @rightarr = split (/\./, $rightstring);
	$result = munin_set_var_loc ($hash, [@leftarr, @rightarr], $val);
    } elsif ($var =~ /^\s*([^;:\.]+)\s*$/) {
        $result = munin_set_var_loc ($hash, [$1], $val);
    } elsif ($var =~ /^\s*([^:;]+)$/) {
	my @leftarr = split (/\./, $1);
	$result = munin_set_var_loc ($hash, [@leftarr], $val);
    } elsif ($var =~ /^\s*(.+)\.([^\.:;]+)$/) {
	my ($leftstring, $rightstring) = ($1, $2);

	my @leftarr = split (/;/, $leftstring);
	my @rightarr = split (/\./, $rightstring);
	$result = munin_set_var_loc ($hash, [@leftarr, @rightarr], $val);
    } elsif ($var =~ /^\s*(\S+)\s*$/) {
	my @leftarr = split (/;/, $1);
	$result = munin_set_var_loc ($hash, [@leftarr], $val);
    } else {
	ERROR "Error: munin_set_var_path: Malformatted variable path \"$var\".";
    }

    if (!defined $result) {
	ERROR "Error: munin_set_var_path: Failed setting \"$var\" = \"$val\".";
    }

    return $hash;
}


sub munin_get_root_node
{
    my $hash = shift;

    return if ref ($hash) ne "HASH";

    while (defined $hash->{'#%#parent'}) {
	$hash = $hash->{'#%#parent'};
    }

    return $hash;
}


sub munin_writeconfig_loop {
    my ($hash,$fh,$pre) = @_;

    foreach my $key (keys %$hash) {
	next if $key =~ /#%#/;
	my $path = (defined $pre ? join(';', ($pre, $key)) : $key);
	if (ref ($hash->{$key}) eq "HASH") {
	    munin_writeconfig_loop ($hash->{$key}, $fh, $path);
	} else {
	    next if !defined $pre and $key eq "version"; # Handled separately
	    next if !defined $hash->{$key} or !length $hash->{$key};
            (my $outstring = $hash->{$key}) =~ s/([^\\])#/$1\\#/g;
	    # Too much.  Can be seen in the file itself.
	    # DEBUG "[DEBUG] Writing: $path $outstring\n";
	    if ($outstring =~ /\\$/)
	    { # Backslash as last char has special meaning. Avoid it.
		print $fh "$path $outstring\\\n"; 
	    } else {
		print $fh "$path $outstring\n";
	    }
	}
    }
}


sub munin_writeconfig {
    my ($datafilename,$data,$fh) = @_;

    DEBUG "[DEBUG] Writing state to $datafilename";

    if (!defined $fh) {
	$fh = gensym();
	unless (open ($fh, ">", $datafilename)) {
	    LOGCROAK "Fatal error: Could not open \"$datafilename\" for writing: $!";
	}
    }

    # Write version
    print $fh "version $VERSION\n";
    # Write datafile
    munin_writeconfig_loop ($data, $fh);

    DEBUG "[DEBUG] Closing filehandle \"$datafilename\"...\n";
    close ($fh);
}


sub munin_config {
    my $conffile = shift;
    my $config   = shift;

    $conffile ||= $configfile;
    $config     = munin_readconfig ($conffile);

    if (defined $config->{'includedir'}) {
	my $dirname = $config->{'includedir'};
	DEBUG "Includedir statement to include files in $dirname";

	my $DIR;
	opendir($DIR, $dirname) or
	    WARN "[Warning] Could not open includedir directory $dirname: $OS_ERROR\n";
	my @files = grep { ! /^\.|~$/ } readdir($DIR);
	closedir($DIR);

	@files = map { $_ = $dirname.'/'.$_; } (sort @files);

	foreach my $f (@files) {
	    INFO "Reading additional config from $f";

	    my $extra = munin_readconfig ($f);
	    $config = munin_overwrite($extra,$config);
	}
    }

    my $data    = munin_readconfig("$config->{dbdir}/datafile", 1, 1);

    $data = munin_overwrite($data,$config);
    return ($data);
}


sub munin_get_html_filename {
    # Actually only used in M::M::HTMLOld

    my $hash    = shift;

    my $loc     = munin_get_node_loc ($hash);
    my $ret     = munin_get ($hash, 'htmldir');
    my $plugin  = "index";

    # Sanitise
    $ret =~ s/[^\w_\/"'\[\]\(\)+=-]\./_/g;
    $hash =~ s/[^\w_\/"'\[\]\(\)+=-]/_/g;
    @$loc = map { 
        my $l = $_;
        $l =~ s/\//_/g; 
        $l =~ s/^\./_/g;
        $l;
    } @$loc;
	
    if (defined $hash->{'graph_title'} and !munin_has_subservices ($hash)) {
	$plugin = pop @$loc or return;
    }

    if (@$loc) { # The rest is used as directory names...
	$ret .= "/" . join ('/', @$loc);
    }

    return "$ret/$plugin.html";
}


sub munin_get_picture_filename {
    my $hash    = shift;
    my $scale   = shift;
    my $sum     = shift;

    my $loc     = munin_get_picture_loc ($hash);
    my $ret     = munin_get ($hash, 'htmldir');

    # Sanitise
    $ret =~ s/[^\w_\/"'\[\]\(\)+=-]\./_/g;
    $hash =~ s/[^\w_\/"'\[\]\(\)+=-]/_/g;
    $scale =~ s/[^\w_\/"'\[\]\(\)+=-]/_/g;
    @$loc = map { 
        my $l = $_;
        $l =~ s/\//_/g; 
        $l =~ s/^\./_/g;
        $l;
    } @$loc;
	
    my $plugin = pop @$loc or return;
    my $node   = pop @$loc or return;

    if (@$loc) { # The rest is used as directory names...
	$ret .= "/" . join ('/', @$loc);
    }

    if (defined $sum and $sum) {
	    return "$ret/$node/$plugin-$scale-sum.png";
    } elsif (defined $scale) {
	    return "$ret/$node/$plugin-$scale.png";
    } else {
	    return "$ret/$node/$plugin.png";
    }
}


sub munin_path_to_loc
{
    my $path = shift;

    my $result = undef;

    if ($path =~ /^\s*([^:]+):(\S+)\s*$/) {
	my ($leftstring, $rightstring) = ($1, $2);

	my @leftarr = split (/;/, $leftstring);
	my @rightarr = split (/\./, $rightstring);
	$result = [@leftarr, @rightarr];
    } elsif ($path =~ /^\s*([^;:\.]+)\s*$/) {
        $result = [$1];
    } elsif ($path =~ /^\s*(.+)\.([^\.:;]+)$/) {
	my ($leftstring, $rightstring) = ($1, $2);

	my @leftarr = split (/;/, $leftstring);
	my @rightarr = split (/\./, $rightstring);
	$result = [@leftarr, @rightarr];
    } elsif ($path =~ /^\s*(\S+)\s*$/) {
	my @leftarr = split (/;/, $1);
	$result = [@leftarr];
    } else {
	ERROR "[ERROR] munin_path_to_loc: Malformatted variable path \"$path\".";
    }

    if (!defined $result) {
	ERROR "[ERROR] munin_path_to_loc: Failed converting \"$path\".";
    }

    return $result;
}


sub munin_get_keypath {
    my $hash = shift;
    my $asfile = shift || '';

    my @group = ();
    my $host = 0;
    my @service = ();

    my $i = $hash;

    while (ref ($i) eq "HASH") {
	# Not sure when a #%#name node can go missing
	my $name = $i->{'#%#name'} || '*BUG*';
	goto gotoparent if $name eq '*BUG*';
	last if $name eq 'root';
	if ($host) {
	    # Into group land now
	    unshift(@group,$name);
	} else {
	    # In service land, working towards host.
	    # If i or my parent has a graph_title we're still working with services
	    if (defined $i->{'#%#parent'}{graph_title} or defined $i->{graph_title}) {
		unshift(@service,$name);
	    } else {
		$host = 1;
		unshift(@group,$name);
	    }
	}
      gotoparent:
	$i=$i->{'#%#parent'};
    }

    if ($asfile) {
	return join('/',@group).'-'.join('-',@service);
    } else {
	return join(';',@group).':'.join('.',@service);
    }
}


sub munin_get_filename {
    my $hash = shift;

    my $loc  = munin_get_keypath ($hash,1);
    my $ret  = munin_get ($hash, "dbdir");
    
    if (!defined $loc or !defined $ret) {
        return;
    }
    
    return ($ret . "/$loc-" . lc substr (munin_get($hash, "type", "GAUGE"), 0,1). ".rrd");
}


sub munin_get_bool
{
    my $hash   = shift;
    my $field  = shift;
    my $default = shift;

    my $ans = munin_get ($hash, $field, $default);
    return if not defined $ans;

    if ($ans =~ /^yes$/i or
        $ans =~ /^true$/i or
        $ans =~ /^on$/i or
        $ans =~ /^enable$/i or
        $ans =~ /^enabled$/i
       ) {
	return 1;
    } elsif ($ans =~ /^no$/i or
        $ans =~ /^false$/i or
        $ans =~ /^off$/i or
        $ans =~ /^disable$/i or
        $ans =~ /^disabled$/i
      ) {
	return 0;
    } elsif ($ans !~ /\D/) {
	return $ans;
    } else {
	return $default;
    }
}

sub munin_get_bool_val
{
    my $field    = shift;
    my $default  = shift;

    if (!defined $field)
    {
    if (!defined $default)
    {
        return 0;
    }
    else
    {
        return $default;
    }
    }

    if ($field =~ /^yes$/i or
        $field =~ /^true$/i or
        $field =~ /^on$/i or
        $field =~ /^enable$/i or
        $field =~ /^enabled$/i
       )
    {
    return 1;
    }
    elsif ($field =~ /^no$/i or
        $field =~ /^false$/i or
        $field =~ /^off$/i or
        $field =~ /^disable$/i or
        $field =~ /^disabled$/i
      )
    {
    return 0;
    }
    elsif ($field !~ /\D/)
    {
    return $field;
    }
    else
    {
    return;
    }
}


sub munin_get
{
    my $hash   = shift;
    my $field  = shift;
    my $default = shift;

    return $default if (ref ($hash) ne "HASH");
    return $hash->{$field} if defined $hash->{$field} and ref($hash->{$field}) ne "HASH";
    return $default if not defined $hash->{'#%#parent'};
    return munin_get ($hash->{'#%#parent'}, $field, $default);
}


sub munin_copy_node_toloc
{
    my $from = shift;
    my $to   = shift;
    my $loc  = shift;

    return unless defined $from and defined $to and defined $loc;

    if (ref ($from) eq "HASH") {
	foreach my $key (keys %$from) {
	    next if $key =~ /^#%#/;
	    if (ref ($from->{$key}) eq "HASH") {
		munin_copy_node_toloc ($from->{$key}, $to, [@$loc, $key]);
	    } else {
		munin_set_var_loc ($to, [@$loc, $key], $from->{$key});
	    }
	}
    } else {
	$to = $from;
    }
    return $to;
}


sub munin_copy_node
{
    my $from = shift;
    my $to   = shift;

    if (ref ($from) eq "HASH") {
	foreach my $key (keys %$from) {
	    if (ref ($from->{$key}) eq "HASH") {
                # Easier to do with the other copy function
		munin_copy_node_toloc ($from->{$key}, $to, [$key]); 
	    } else {
		munin_set_var_loc ($to, [$key], $from->{$key});
	    }
	}
    } else {
	$to = $from;
    }
    return $to;
}


sub munin_node_status
{
    my ($config, $limits, $domain, $node, $check_draw) = @_;
    my $state = "ok";

    return unless defined $config->{domain}->{$domain}->{node}->{$node};
    my $snode = $config->{domain}->{$domain}->{node}->{$node};

    foreach my $service (keys %{$snode})
    {
	my $fres  = munin_service_status ($config, $limits, $domain, $node, $service, $check_draw);
	if (defined $fres)
	{
	    if ($fres eq "critical")
	    {
		$state = $fres;
		last;
	    }
	    elsif ($fres eq "warning")
	    {
		$state = $fres;
	    }
	}
    }

    return $state;
}


sub munin_category_status {
    my $hash       = shift || return;
    my $limits     = shift || return;
    my $category   = shift || return;
    my $check_draw = 1;
    my $state      = "ok";

    return unless (defined $hash and ref ($hash) eq "HASH");

    foreach my $service (@{munin_get_children ($hash)}) {
	next if (!defined $service or ref ($service) ne "HASH");
	next if (!defined $service->{'graph_title'});
	next if ($category ne munin_get ($service, "graph_category", "other"));
	next if ($check_draw and not munin_get_bool ($service, "graph", 1));

	my $fres  = munin_service_status ($service, $limits, $check_draw);
	if (defined $fres) {
	    if ($fres eq "critical") {
		$state = $fres;
		last;
	    } elsif ($fres eq "warning") {
		$state = $fres;
	    } elsif ($fres eq "unknown" and $state eq "ok") {
		$state = $fres;
	    }
	}
    }

    return $state;
}

sub munin_service_status {
    my ($config, $limits, $check_draw) = @_;
    my $state = "ok";

    return unless defined $config;
    for my $fieldnode (@{munin_find_field ($config, "label")}) {
	my $field = munin_get_node_name ($fieldnode);
	my $fres  = munin_field_status ($fieldnode, $limits, $check_draw);
	if (defined $fres) {
	    if ($fres eq "critical") {
		$state = $fres;
		last;
	    } elsif ($fres eq "warning") {
		$state = $fres;
	    }
	}
    }

    return $state;
}


sub munin_field_status {
    my ($hash, $limits, $check_draw) = @_;
    my $state = "ok";

    # Return undef if nagios is turned off, or the field doesn't have any limits
    if ((!defined munin_get ($hash, "warning", undef)) and (!defined munin_get ($hash, "critical"))) {
	return;
    }

    if (!defined $check_draw or !$check_draw or munin_draw_field ($hash)) {
	my $loc  = munin_get_node_loc ($hash);
	my $node = munin_get_node ($limits, $loc);
	return $node->{"state"} || "ok";
    }
    return $state;
}

sub munin_graph_column_headers
{
    my ($config, $domain, $node, $serv) = @_;
    my $ret = 0;
    my @fields = ();

    foreach my $field (keys %{$config->{domain}->{$domain}->{node}->{$node}->{client}->{$serv}})
    {
	if ($field =~ /^([^\.]+)\.negative$/ and munin_draw_field ($config->{domain}->{$domain}->{node}->{$node}, $serv, $1))
	{
	    return 1;
	}
	elsif ($field =~ /^([^\.]+)\.label$/ and munin_draw_field ($config->{domain}->{$domain}->{node}->{$node}, $serv, $1))
	{
	    push @fields, $1;
	}
    }

    return 1 if (munin_get_max_label_length ($config->{'domain'}->{$domain}->{'node'}->{$node}, $config, $domain, $node, $serv, \@fields) > 20);

    return $ret;
}


sub munin_get_max_label_length
{
    my $service = shift;
    my $order   = shift;
    my $result  = 0;
    my $tot     = munin_get ($service, "graph_total");

    for my $field (@$order) {
	my $path = undef;
	(my $f = $field) =~ s/=.+//;
	next if (!munin_get_bool ($service->{$f}, "process", 1));
	next if (munin_get_bool ($service->{$f}, "skipdraw", 0));
	next if (!munin_get_bool ($service->{$f}, "graph", 1));

	my $len = length (munin_get ($service->{$f}, "label") || $f);

	if ($result < $len) {
	    $result = $len;
	}
    }
    if (defined $tot and length $tot > $result) {
	$result = length $tot;
    }
    return $result;
}


sub munin_get_field_order
{
    my $hash = shift;
    my $result  = [];

    return if !defined $hash or ref($hash) ne "HASH";

    my $order = munin_get ($hash, "graph_order");

    if (defined $hash->{graph_sources}) {
	foreach my $gs (split /\s+/, $hash->{'graph_sources'}) {
	    push (@$result, "-".$gs);
	}
    } 
    if (defined $order) {
	push (@$result, split /\s+/, $order);
    } 

    for my $fieldnode (@{munin_find_field ($hash, "label")}) {
        my $fieldname = munin_get_node_name ($fieldnode);
	push @$result,$fieldname
	    if !grep m[^\Q$fieldname\E(?:=|$)], @$result;;
    }

    for my $fieldnode (@{munin_find_field ($hash, "stack")}) {
        my $fieldname = munin_get_node_name ($fieldnode);
	push @$result,$fieldname 
	    if !grep m[^\Q$fieldname\E(?:=|$)], @$result;;
    }

    # We have seen some occurances of redundance in the graph_order
    # due to plugin bugs and so on.  This make process_service
    # generate rrd commands with multiple definitions of the same
    # data.  SO: Make sure there is no redundance in the order.

    my %seen = ();
    my $nresult = [];

    for my $field (@$result) {
	next if exists($seen{$field});

	push @$nresult, $field;
	$seen{$field}=1;
    }
    
    return $nresult;
}


sub munin_get_rrd_filename {
    my $field   = shift;
    my $path    = shift;

    my $result  = undef;
    my $name    = munin_get_node_name ($field);

    # Bail out on bad input data
    return if !defined $field or ref ($field) ne "HASH";

    # If the field has a .filename setting, use it
    if ($result = munin_get ($field, "filename")) {
	return $result;
    }

    # Handle custom paths (used in .sum, .stack, graph_order, et al)
    if (defined $path and length $path) {

	my $sourcenode = munin_get_node_partialpath ($field, $path);
	$result = munin_get_filename ($sourcenode);

	for my $f (@COPY_FIELDS) {
	    if (not exists $field->{$f} and exists $sourcenode->{$f}) {
		DEBUG "DEBUG: Copying $f...\n";
		munin_set_var_loc ($field, [$f], $sourcenode->{$f});
	    }
	}
    } else {
	$result = munin_get_filename ($field);
    }
    return $result;
}


sub munin_mkdir_p {
    my ($dirname, $umask) = @_;

    eval {
        mkpath($dirname, {mode => $umask});
    };
    return if $EVAL_ERROR;
    return 1;
}

sub exit_if_run_by_super_user {
    if ($EFFECTIVE_USER_ID == 0) {
        print qq{This program will easily break if you run it as root as you are
trying now.  Please run it as user '$Munin::Common::Defaults::MUNIN_USER'.  The correct 'su' command
on many systems is 'su - munin --shell=/bin/bash'
Aborting.
};
        exit 1;
    }
}

sub print_version_and_exit {
    print qq{munin version $Munin::Common::Defaults::MUNIN_VERSION.

Copyright (C) 2002-2009 Jimmy Olsen, Audun Ytterdal, Tore Andersson, Kjell-Magne Øierud, Nicolai Langfeldt, Linpro AS, Redpill Linpro AS and others.

This is free software released under the GNU General Public
License. There is NO warranty; not even for MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. For details, please refer to the file
COPYING that is included with this software or refer to
http://www.fsf.org/licensing/licenses/gpl.txt
};
    exit 0;
}


sub look_for_child {
    # wait for child process in blocking or non-blocking mode.
    my ($block) = @_;

    my $pid;

    if ($block) {
    	$pid = waitpid(-1, 0);
    } else {
        $pid = waitpid(-1, WNOHANG);
        if ($pid == 0) {
            return 0;
        }
    }

    if ($pid < 0) {
        ERROR "[ERROR] Unexpectedly ran out of children: $!";
	croak "[ERROR] Ran out of children: $!\n";
    }

    if ($? != 0) {
        WARN "[WARNING] Child $pid failed: " . ($? << 8) . 
	    "(signal " . ($? & 0xff) . ")";
    }
    return 1;
}


sub wait_for_remaining_children {
    my ($running) = @_;
    while ($running > 0) {
	look_for_child("block");
	--$running;
    }
    return $running;
}

sub munin_has_subservices {
    my ($hash) = @_;
    return 0 unless defined $hash;

    # Only services (which again require graph_title) can have subservices
    return 0 unless defined $hash->{'graph_title'};

    if (!defined $hash->{'#%#has_subservices'}) {
	$hash->{'#%#has_subservices'} = scalar (grep $_, map { ref($hash->{$_}) eq "HASH" and $_ ne '#%#parent' and defined $hash->{$_}->{'graph_title'} ? 1 : undef } keys %$hash);
    }
    return $hash->{'#%#has_subservices'};
}

1;

__END__

=head1 NAME

Munin::Master::Utils - Exports a lot of utility functions.

=head1 SYNOPSIS

 use Munin::Master::Utils;

=head1 SUBROUTINES

=over

=item B<munin_category_status>

Gets current status of a category.

Parameters: 
 - $hash: A ref to the hash node whose children to check
 - $limits: A ref to the root node of the limits tree
 - $category: The category to review
 - $check_draw: [optional] Ignore undrawn fields

Returns:
 - Success: The status of the field
 - Failure: undef


=item B<munin_config>



=item B<munin_copy_node>

Copy hash node.

Parameters:
 - $from: Hash node to copy
 - $to: Where to copy it to

Returns:
 - Success: $to
 - Failure: undef


=item B<munin_copy_node_toloc>

Copy hash node at.

Parameters:
 - $from: Hash node to copy
 - $to: Where to copy it to
 - $loc: Path to node under $to

Returns:
 - Success: $to
 - Failure: undef


=item B<munin_createlock>



=item B<munin_delete>



=item B<munin_draw_field>

Check whether a field will be visible in the graph or not.

Parameters:
 - $hash: A ref to the hash node for the field

Returns:
 - Success: Boolean; true if field will be graphed, false if not
 - Failure: undef

=item B<munin_fetch>



=item B<munin_field_status>

Gets current status of a field.

Parameters: 
 - $hash: A ref to the field hash node
 - $limits: A ref to the root node of the limits tree
 - $check_draw: [optional] Ignore undrawn fields

Returns:
 - Success: The status of the field
 - Failure: undef


=item B<munin_find_field>

Search a hash to find hash nodes with $field defined.

Parameters: 
 - $hash: A hash ref to search
 - $field: The name of the field to search for, or a regex
 - $avoid: [optional] Stop traversing further down if this field is found

Returns:
 - Success: A ref to an array of the hash nodes containing $field.
 - Failure: undef


=item B<munin_get>

Get variable.

Parameters:
 - $hash: Ref to hash node
 - $field: Name of field to get
 - $default: [optional] Value to return if $field isn't set

Returns:
 - Success: field contents
 - Failure: $default if defined, else undef


=item B<munin_get_bool>

Get boolean variable.

Parameters:
 - $hash: Ref to hash node
 - $field: Name of field to get
 - $default: [optional] Value to return if $field isn't set

Returns:
 - Success: 1 or 0 (true or false)
 - Failure: $default if defined, else undef


=item B<munin_get_bool_val>



=item B<munin_get_children>

Get all child hash nodes.

Parameters: 
 - $hash: A hash ref to the parent node

Returns:
 - Success: A ref to an array of the child nodes
 - Failure: undef


=item B<munin_get_field_order>

Get the field order in a graph.

Parameters:
 - $hash: A hash ref to the service

Returns:
 - Success: A ref to an array of the field names
 - Failure: undef


=item B<munin_get_filename>

Get rrd filename for a field, without any bells or whistles. Used by
munin-update to figure out which file to update.

Parameters:
 - $hash: Ref to hash field

Returns:
 - Success: Full path to rrd file
 - Failure: undef


=item B<munin_get_html_filename>

Get the full path-name of an html file.

Parameters:
 - $hash: A ref to the service hash node

Returns:
 - Success: The file name with full path
 - Failure: undef


=item B<munin_get_max_label_length>

Get the length of the longest labe in a graph.

Parameters:
 - $hash: the graph in question
 - $order: A ref to an array of fields (graph_order)

Returns:
 - Success: The length of the longest label in the graph
 - Failure: undef


=item B<munin_get_node>

Gets a node by loc.

Parameters:
 - $hash: A ref to the hash to set the variable in
 - $loc: A ref to an array with the full path of the node

Returns:
 - Success: The node ref found by $loc
 - Failure: undef

=item B<munin_get_node_loc>

Get location array for hash node.

Parameters: 
 - $hash: A ref to the node

Returns:
 - Success: Ref to an array with the full path of the variable
 - Failure: undef


=item B<munin_get_node_name>

Return the name of the hash node supplied.

Parameters: 
 - $hash: A ref to the hash node

Returns:
 - Success: The name of the node


=item B<munin_get_node_partialpath>

Gets a node from a partial path.

Parameters: 
 - $hash: A ref to the "current" location in the hash tree
 - $var: A path string with relative location (from the $hash).

Returns:
 - Success: The node
 - Failure: undef


=item B<munin_get_parent>

Get parent node of a hash.

Parameters: 
 - $hash: A ref to the node

Returns:
 - Success: Ref to an parent
 - Failure: undef


=item B<munin_get_parent_name>

Return the name of the parent of the hash node supplied

Parameters: 
 - $hash: A ref to the hash node

Returns:
 - Success: The name of the parent node
 - Failure: If no parent node exists, "none" is returned.


=item B<munin_get_picture_filename>

Get the full path+name of a picture file.

Parameters:
 - $hash: A ref to the service hash node
 - $scale: [optional] The scale (day, week, year, month)
 - $sum: [optional] Boolean value, whether it's a sum graph or not.

Returns:
 - Success: The file name with full path
 - Failure: undef


=item B<munin_get_picture_loc>

Get location array for hash node for picture purposes. Differs from
munin_get_node_loc in that it honors #%#origin metadata 

Parameters: 
 - $hash: A ref to the node 

Returns: 
 - Success: Ref to an array with the full path of the variable
 - Failure: undef


=item B<munin_get_root_node>

Get the root node of the hash tree.

Parameters:
 - $hash: A hash node to traverse up from

Returns:
 - Success: A ref to the root hash node
 - Failure: undef


=item B<munin_get_rrd_filename>

Get the name of the rrd file corresponding to a field. Checks for lots
of bells and whistles.  This function is the correct one to use when
figuring out where to fetch data from.

Parameters:
 - $field: The hash object of the field
 - $path: [optional] The path to the field (as given in graph_order/sum/stack/et al)

Returns:
 - Success: A string with the filename of the rrd file
 - Failure: undef


=item B<munin_get_separated_node>


Copy a node to a separate node without "specials".

Parameters:
 - $hash: The node to copy

Returns:
 - Success: A ref to a new node without "#%#"-fields
 - Failure: undef


=item B<munin_get_var_path>



=item B<munin_getlock>



=item B<munin_graph_column_headers>


=item B<munin_has_subservices>

  munin_has_subservices($hash);

Checks whether the service represented by $hash has subservices (multigraph),
and returns the result.

Parameters:
 - $hash: Hash reference pointing to a service

Returns:
 - true: if the hash is indeed a service, and said service has got subservices
 - false: otherwise


=item B<munin_mkdir_p>

 munin_mkdir_p('/a/path/', oct('777'));

Make a directory and recursivly any nonexistent directory in the path
to it.


=item B<munin_node_status>



=item B<munin_nscasend>



=item B<munin_overwrite>

Take contents of one config-namespace and replace/insert the instances
needed.

=item B<munin_parse_config>



=item B<munin_path_to_loc>

Returns a loc array from a path string.

Parameters: 
 - $path: A path string

Returns:
 - Success: A ref to an array with the loc
 - Failure: undef


=item B<munin_readconfig>



=item B<munin_removelock>



=item B<munin_runlock>



=item B<munin_service_status>

Gets current status of a service.

Parameters: 
 - $hash: A ref to the field hash node
 - $limits: A ref to the root node of the limits tree
 - $check_draw: [optional] Ignore undrawn fields

Returns:
 - Success: The status of the field
 - Failure: undef


=item B<munin_set>

Sets a variable in a hash.

Parameters: 
 - $hash: A ref to the hash to set the variable in
 - $var: The name of the variable
 - $val: The value to set the variable to

Returns:
 - Success: The $hash we were handed
 - Failure: undef


=item B<munin_set_var_loc>

Sets a variable in a hash.

Parameters: 
 - $hash: A ref to the hash to set the variable in
 - $loc: A ref to an array with the full path of the variable
 - $val: The value to set the variable to

Returns:
 - Success: The $hash we were handed
 - Failure: undef


=item B<munin_set_var_path>

Sets a variable in a hash.

Parameters: 
 - $hash: A ref to the hash to set the variable in
 - $var: A string with the full path of the variable
 - $val: The value to set the variable to

Returns:
 - Success: The $hash we were handed
 - Failure: The $hash we were handed


=item B<munin_trend>



=item B<munin_writeconfig>



=item B<munin_writeconfig_loop>



=back

=head1 COPYING

Copyright (C) 2003-2007 Jimmy Olsen, Audun Ytterdal

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; version 2 dated June,
1991.

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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

=cut

# vim: syntax=perl ts=8
