[01/12] statusmail: Main script

Message ID 20190405172940.13168-2-ipfr@tfitzgeorge.me.uk
State Dropped
Headers show
Series statusmail: Status and Log Summary Emails | expand

Commit Message

Tim FitzGeorge April 6, 2019, 4:29 a.m. UTC
Called by fcron hourly to look for scheduled messages.  StatusMail.pm declares an
object which is used to create the mail messages.

Signed-off-by: Tim FitzGeorge <ipfr@tfitzgeorge.me.uk>
---
 src/statusmail/StatusMail.pm | 530 +++++++++++++++++++++++++++++++++++++++++++
 src/statusmail/statusmail.pl | 422 ++++++++++++++++++++++++++++++++++
 2 files changed, 952 insertions(+)
 create mode 100644 src/statusmail/StatusMail.pm
 create mode 100755 src/statusmail/statusmail.pl

Patch

diff --git a/src/statusmail/StatusMail.pm b/src/statusmail/StatusMail.pm
new file mode 100644
index 000000000..fb37c3663
--- /dev/null
+++ b/src/statusmail/StatusMail.pm
@@ -0,0 +1,530 @@ 
+#!/usr/bin/perl
+
+############################################################################
+#                                                                          #
+# Send log and status emails for IPFire                                    #
+#                                                                          #
+# This 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 3 of the License, or        #
+# (at your option) any later version.                                      #
+#                                                                          #
+# This 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 IPFire; if not, write to the Free Software                    #
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA #
+#                                                                          #
+# Copyright (C) 2018 - 2019 The IPFire Team                                #
+#                                                                          #
+############################################################################
+
+use strict;
+use warnings;
+
+use lib "/usr/lib/statusmail";
+
+package StatusMail;
+
+use base qw/EncryptedMail/;
+
+############################################################################
+# Constants
+############################################################################
+
+use constant { SEC    => 0,
+               MIN    => 1,
+               HOUR   => 2,
+               MDAY   => 3,
+               MON    => 4,
+               YEAR   => 5,
+               WDAY   => 6,
+               YDAY   => 7,
+               ISDST  => 8,
+               MONSTR => 9 };
+
+use constant MONTHS => qw( Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec );
+
+use constant LOGNAME => '/var/log/messages';
+
+############################################################################
+# Configuration variables
+############################################################################
+
+my @monthnames = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug',
+                  'Sep', 'Oct', 'Nov', 'Dec');
+my %months;
+
+############################################################################
+# Variables
+############################################################################
+
+my %address_lookup_cache;
+
+############################################################################
+# Function prototypes
+############################################################################
+
+sub calculate_period( $$ );
+sub get_period_start();
+sub get_period_end();
+sub get_number_weeks();
+sub cache( $;$ );
+sub lookup_ip_address( $$ );
+sub set_host_name( $$$ );
+sub split_string( $$$ );
+
+############################################################################
+# Initialisation code
+############################################################################
+
+
+foreach (my $monindex = 0 ; $monindex < MONTHS ; $monindex++)
+{
+  $months{(MONTHS)[$monindex]} = $monindex;
+}
+
+#------------------------------------------------------------------------------
+# sub new
+#
+# Class constructor
+#------------------------------------------------------------------------------
+
+sub new
+{
+  my $invocant = shift;
+
+  my $class = ref($invocant) || $invocant;
+
+  my $self = $class->SUPER::new( @_ );
+
+  $self->{last_time} = 0;
+  $self->{last_mon}  = 0;
+  $self->{last_day}  = 0;
+  $self->{last_hour} = 0;
+
+  bless( $self, $class );
+
+  return $self;
+}
+
+#------------------------------------------------------------------------------
+# sub calculate_period( value, unit )
+#
+# Calculates the limits of the period covered by the message
+#
+# Parameters:
+#   value  Number of units
+#   unit   Unit of time
+#------------------------------------------------------------------------------
+
+sub calculate_period( $$ )
+{
+  my ( $self, $value, $unit ) = @_;
+
+  use Time::Local;
+
+  my $start_time    = 0;
+  my @start_time    = ();
+  my $end_time      = 0;
+  my @end_time      = ();
+  my $weeks_covered = 0;
+
+  @end_time = localtime();
+
+  $end_time[SEC] = 0;
+  $end_time[MIN] = 0;
+
+  $end_time = timelocal( @end_time );
+
+  if ($unit eq 'months')
+  {
+    # Go back the specified number of months
+
+    @start_time = @end_time;
+
+    $start_time[MON] -= $value;
+    if ($start_time[MON] < 0 )
+    {
+      $start_time[MON] += 12;
+      $start_time[YEAR]--;
+    }
+
+    $start_time = timelocal( @start_time );
+  }
+  else
+  {
+    my $hours   = $value;
+
+    # Go back the specified number of hours, days or weeks
+
+    $hours     *= 24      if ($unit eq 'days');
+    $hours     *= 24 *  7 if ($unit eq 'weeks');
+
+    $start_time = timelocal( @end_time ) - ($hours * 3600);
+    @start_time = localtime( $start_time );
+  }
+
+  # Adjust end to end of previous hour rather than start of current hour
+
+  $end_time--;
+  @end_time = localtime( $end_time );
+
+  # Add the alphabetic month to the end of the time lists
+
+  push @start_time, $monthnames[ $start_time[MON] ];
+  push @end_time,   $monthnames[ $end_time[MON] ];
+
+  # Calculate how many archive files have to be read
+
+  my $week_start = $start_time - ($start_time[WDAY] * 86400) - ($start_time[HOUR] * 3600) + 3600;
+  $weeks_covered = int( (time() - $week_start) / (86400 * 7) );
+
+  $self->{'start_time_array'} = \@start_time;
+  $self->{'start_time'}       = $start_time;
+  $self->{'end_time_array'}   = \@end_time;
+  $self->{'end_time'}         = $end_time;
+  $self->{'weeks_covered'}    = $weeks_covered;
+  $self->{'period'}           = "$value$unit";
+  $self->{'period'}           =~ s/s$//;
+  $self->{'total_days'}       = ($end_time - $start_time) / 86400;
+}
+
+
+#------------------------------------------------------------------------------
+# sub get_period()
+#
+# Returns the period covered by a report.
+#------------------------------------------------------------------------------
+
+sub get_period()
+{
+  my $self = shift;
+
+  return $self->{'period'};
+}
+
+
+#------------------------------------------------------------------------------
+# sub get_period_start()
+#
+# Returns the start of the period covered by a report.
+#------------------------------------------------------------------------------
+
+sub get_period_start()
+{
+  my $self = shift;
+
+  return wantarray ? @{$self->{'start_time_array'}} : $self->{'start_time'};
+}
+
+
+#------------------------------------------------------------------------------
+# sub get_period_end()
+#
+# Returns the end of the period covered by a report.
+#------------------------------------------------------------------------------
+
+sub get_period_end()
+{
+  my $self = shift;
+
+  return wantarray ? @{$self->{'end_time_array'}} : $self->{'end_time'};
+}
+
+
+#------------------------------------------------------------------------------
+# sub get_number_weeks()
+#
+# Returns the number of complete weeks covered by a report.
+#------------------------------------------------------------------------------
+
+sub get_number_weeks()
+{
+  my $self = shift;
+
+  return $self->{'weeks_covered'};
+}
+
+
+#------------------------------------------------------------------------------
+# sub cache( name [, item] )
+#
+# Either caches an item or returns the cached item.
+#
+# Parameters:
+#   Name name of item
+#   Item item to be cached (optional)
+#
+# Returns:
+#   Cached item if no item specified, undef otherwise
+#------------------------------------------------------------------------------
+
+my %cache;
+
+sub cache( $;$ )
+{
+  my ($self, $name, $item) = @_;
+
+  if ($item)
+  {
+    $cache{$name} = $item;
+  }
+  else
+  {
+    return $cache{$name};
+  }
+
+  return undef;
+}
+
+
+#------------------------------------------------------------------------------
+# sub clear_cache()
+#
+# Clears any cached values.
+#------------------------------------------------------------------------------
+
+sub clear_cache()
+{
+  %cache = ();
+}
+
+
+#------------------------------------------------------------------------------
+# sub get_message_log_line()
+#
+# Gets the next line from the message log.
+# Will cache log entries if the period covered is short.
+#------------------------------------------------------------------------------
+
+sub get_message_log_line
+{
+  my $self = shift;
+  my $line;
+
+  if (exists $self->{logindex})
+  {
+    # Reading from the cache
+
+    if ($self->{logindex} < @{ $self->{logcache} })
+    {
+      return $self->{logcache}[$self->{logindex}++];
+    }
+    else
+    {
+      # End of cache - reset to start again on next call
+
+      $self->{logindex} = 0;
+      return undef;
+    }
+  }
+
+  $self->{logfile} = $self->{'weeks_covered'} if (not exists $self->{logfile} or $self->{logfile} < 0);
+
+  LINE:
+  while (1)
+  {
+    if (not exists $self->{fh} or (exists $self->{fh} and eof $self->{fh}))
+    {
+      # Reading from a file and need to open a file
+
+      FILE:
+      while ($self->{logfile} >= 0)
+      {
+        my $name = $self->{logfile} < 1 ? LOGNAME : LOGNAME . '.' . $self->{logfile};
+        $self->{logfile}--;
+
+        if (-r $name)
+        {
+          # Not compressed
+
+          open $self->{fh}, '<', $name or die "Can't open $name: $!";
+          $self->{year} = (localtime( (stat(_))[9] ))[YEAR];
+          last FILE;
+        }
+        elsif (-r "$name.gz")
+        {
+          # Compressed
+
+          open $self->{fh}, "gzip -dc $name.gz |" or next;
+          $self->{year} = (localtime( (stat(_))[9] ))[YEAR];
+          last FILE;
+        }
+
+        # Not found - go back for next file
+      }
+
+      if ($self->{logfile} < -1)
+      {
+        # No further files - reset to start again on next call
+
+        delete $self->{fh};
+        return undef;
+      }
+    }
+
+    if (exists $self->{fh})
+    {
+      # Reading from a file
+
+      $line = readline $self->{fh};
+
+      if (eof $self->{fh})
+      {
+        if ($self->{logfile} < 0)
+        {
+          # No further files - reset to start again on next call
+
+          delete $self->{fh};
+          return undef;
+        }
+        # Go back for next file
+
+        close $self->{fh};
+        next LINE;
+      }
+
+      my ($mon, $day, $hour) = unpack 'Lsxs', $line;
+
+      if ($mon != $self->{last_mon} or $day != $self->{last_day} or $hour != $self->{last_hour})
+      {
+        # Hour, day or month changed.  Convert to unix time so we can work out
+        # whether the message time falls between the limits we're interested in.
+        # This is complicated by the lack of a year in the logged information,
+        # so assume the current year, and adjust if necessary.
+
+        my @time;
+
+        $time[YEAR] = $self->{year};
+
+        ($time[MON], $time[MDAY], $time[HOUR], $time[MIN], $time[SEC]) = split /[\s:]+/, $line;
+        $time[MON] = $months{$time[MON]};
+
+        $self->{time} = timelocal( @time );
+
+        if ($self->{time} > time())
+        {
+          # We can't have times in the future, so this must be the previous year.
+
+          $self->{year}--;
+          $time[YEAR]--;
+          $self->{time} = timelocal( @time );
+          $self->{last_time} = $self->{time};
+        }
+        elsif ($self->{time} < $self->{last_time})
+        {
+          # Time should be increasing, so we must have gone over a year boundary.
+
+          $self->{year}++;
+          $time[YEAR]++;
+          $self->{time}      = timelocal( @time );
+          $self->{last_time} = $self->{time};
+        }
+
+        ($self->{last_mon}, $self->{last_day}, $self->{last_hour}) = ($mon, $day, $hour);
+      }
+
+      # Check to see if we're within the specified limits.
+      # Note that the minutes and seconds may be incorrect, but since we only deal
+      # in hour boundaries this doesn't matter.
+
+      next LINE if ($self->{time} < $self->{start_time});
+
+      if ($self->{time} > $self->{end_time})
+      {
+        # After end time - reset to start again on next call
+
+        close $self->{fh};
+        delete $self->{fh};
+        $self->{logfile} = $self->{'weeks_covered'};
+
+        return undef;
+      }
+
+      # Cache the entry if the time covered is less than two days
+
+      push @{$self->{logcache}}, $line if ($self->{'total_days'} <= 2);
+
+      return $line;
+    }
+  }
+
+  return $line;
+}
+
+
+#------------------------------------------------------------------------------
+# sub lookup_ip_address( string )
+#
+# Converts an IP Address to a URL
+#------------------------------------------------------------------------------
+
+sub lookup_ip_address( $$ )
+{
+  my ($self, $address) = @_;
+
+  use Socket;
+
+  return $address_lookup_cache{$address} if (exists $address_lookup_cache{$address});
+
+  my $name = gethostbyaddr( inet_aton( $address ), AF_INET ) || "";
+
+  $address_lookup_cache{$address} = $name;
+
+  return $name;
+}
+
+
+#------------------------------------------------------------------------------
+# sub set_host_name( address, name )
+#
+# Records the mapping from an IP address to a name
+#------------------------------------------------------------------------------
+
+sub set_host_name( $$$ )
+{
+  my ($self, $address, $name) = @_;
+
+  return unless ($address and $name);
+  return if ($address eq $name);
+
+  if (exists $address_lookup_cache{$address})
+  {
+    $address_lookup_cache{$address} = "" if ($address_lookup_cache{$address} ne $name);
+  }
+  else
+  {
+    $address_lookup_cache{$address} = $name;
+  }
+}
+
+
+#------------------------------------------------------------------------------
+# sub spilt_string( string, size )
+#
+# Splits a string into multiple lf separated lines
+#------------------------------------------------------------------------------
+
+sub split_string( $$$ )
+{
+  my ($self, $string, $size) = @_;
+  
+  my $out = '';
+    
+  while (length $string > $size)
+  {
+    $string =~ s/(.{$size,}?)\s+//;
+    last unless ($1);
+    $out .= $1 . "\n";
+  }
+  
+  $out .= $string;
+
+  return $out;
+}
+
+1;
diff --git a/src/statusmail/statusmail.pl b/src/statusmail/statusmail.pl
new file mode 100755
index 000000000..4ced65880
--- /dev/null
+++ b/src/statusmail/statusmail.pl
@@ -0,0 +1,422 @@ 
+#!/usr/bin/perl
+
+############################################################################
+#                                                                          #
+# Send log and status emails for IPFire                                    #
+#                                                                          #
+# This 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 3 of the License, or        #
+# (at your option) any later version.                                      #
+#                                                                          #
+# This 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 IPFire; if not, write to the Free Software                    #
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA #
+#                                                                          #
+# Copyright (C) 2018 - 2019 The IPFire Team                                #
+#                                                                          #
+############################################################################
+# Main script for statusmail.                                              #
+#                                                                          #
+# Usually called by fcron when it will check to see if any schedules are   #
+# due in which case the schedule will be executed.  If the schedule        #
+# produces any output it is sent as an email to the recipients given in    #
+# the schedule.  Emails are always signed using GPG and will be encrypted  #
+# if an encryption keys is available for the user.                         #
+#                                                                          #
+# Can also be run with the name of a schedule as an argument in which case #
+# the schedule is executed immediately regrardless of whether it is due or #
+# not.                                                                     #
+#                                                                          #
+# If run from a terminal additional debugging will be turned on and log    #
+# messages will be output to the terminal.                                 #
+############################################################################
+
+use strict;
+use warnings;
+
+use Sys::Syslog qw(:standard :macros);
+
+use lib "/usr/lib/statusmail";
+
+require "/var/ipfire/general-functions.pl";
+require "${General::swroot}/lang.pl";
+
+use StatusMail;
+
+############################################################################
+# Configuration variables
+#
+# These variables give the locations of various files used by this script
+############################################################################
+
+my $lib_dir              = "/usr/lib/statusmail";
+my $plugin_dir           = "$lib_dir/plugins";
+my $stylesheet           = "$lib_dir/stylesheet.css";
+my $mainsettings         = "${General::swroot}/main/settings";
+my $mailsettings         = "${General::swroot}/dma/mail.conf";
+my $contactsettings      = "${General::swroot}/statusmail/contact_settings";
+my $schedulesettings     = "${General::swroot}/statusmail/schedule_settings";
+my $debug                = 0;
+
+############################################################################
+# Function prototypes
+############################################################################
+
+# Used by plugins
+
+sub add_mail_item( % );
+
+# Local functions
+
+sub send_email( $ );
+sub execute_schedule( $$ );
+sub abort( $ );
+sub log_message( $$ );
+sub debug( $$ );
+
+############################################################################
+# Variables
+############################################################################
+
+my %mainsettings  = ();
+my %sections      = ();
+my $contacts      = {};
+my $schedules     = {};
+my %mailsettings  = ();
+
+
+############################################################################
+# Main function
+############################################################################
+
+openlog( "statusmail", "nofatal", LOG_USER);
+log_message LOG_INFO, "Starting log and status email processing";
+
+# Check for existence of settings files
+
+exit unless (-r $contactsettings);
+exit unless (-e $mailsettings);
+exit unless (-r $schedulesettings);
+
+# Read settings
+
+General::readhash($mailsettings, \%mailsettings);
+General::readhash($mainsettings, \%mainsettings);
+
+unless ($mailsettings{'USEMAIL'} eq 'on')
+{
+  log_message LOG_WARNING, "Email disabled";
+  exit;
+};
+
+eval qx|/bin/cat $contactsettings|  if (-r $contactsettings);
+eval qx|/bin/cat $schedulesettings| if (-r $schedulesettings);
+
+# Scan for plugins
+
+opendir DIR, $plugin_dir or abort "Can't open Plug-in directory $plugin_dir: $!";
+
+foreach my $file (readdir DIR)
+{
+  next unless ($file =~ m/\.pm$/);
+
+  debug 1, "Initialising plugin $file";
+
+  require "$plugin_dir/$file";
+}
+
+# Check command line parameters
+
+if (@ARGV)
+{
+  # Command line parameters provided - try to execute the named schedule.
+
+  my ($schedule) = $ARGV[0];
+
+  if (exists $$schedules{$schedule})
+  {
+    execute_schedule( $schedule, $$schedules{$schedule} );
+  }
+  else
+  {
+    print "Schedule '$schedule' not found\n";
+  }
+
+  closelog;
+  exit;
+}
+
+# Look for a due schedule
+
+my (undef, undef, $hour, $mday, undef, undef, $wday, undef, undef) = localtime;
+
+$hour = 1 << $hour;
+$wday = 1 << $wday;
+$mday = 1 << $mday;
+
+foreach my $schedule (keys %$schedules)
+{
+  next unless ($$schedules{$schedule}{'enable'} eq 'on'); # Must be enabled
+
+  next unless ($$schedules{$schedule}{'mday'} & $mday or  # Must be due today
+               $$schedules{$schedule}{'wday'} & $wday);
+
+  next unless ($$schedules{$schedule}{'hours'} & $hour);  # Must be due this hour
+
+  debug 1, "Schedule $schedule due";
+
+  execute_schedule( $schedule, $$schedules{$schedule} );
+}
+
+closelog;
+
+exit;
+
+#------------------------------------------------------------------------------
+# sub execute_schedule( name, schedule )
+#
+# Executes the specified schedule as long as at least one of the contacts is
+# enabled.
+#
+# Parameters:
+#   name      name of Schedule
+#   schedule  reference of Schedule hash to be executed
+#------------------------------------------------------------------------------
+
+sub execute_schedule( $$ )
+{
+  my ($name, $schedule) = @_;
+  my @contacts;
+  my $status = 0;
+
+  # Check that at least one of the contacts is enabled
+
+  foreach my $contact (split '\|', $$schedule{'email'})
+  {
+    push @contacts, $contact if (exists $$contacts{$contact} and $$contacts{$contact}{'enable'} eq 'on');
+  }
+
+  if (not @contacts)
+  {
+    debug 1, "No enabled contacts";
+    return;
+  }
+
+  log_message LOG_INFO, "Executing status mail schedule $name";
+
+  # Look for a theme stylesheet
+
+  my $theme_stylesheet = "$lib_dir/$mainsettings{'THEME'}.css";
+  $stylesheet = $theme_stylesheet if (-r $theme_stylesheet);
+
+  # Create message
+
+  my $message = new StatusMail( 'format'             => $$schedule{'format'},
+                                'subject'            => $$schedule{'subject'},
+                                'to'                 => [ @contacts ],
+                                'sender'             => $mailsettings{'SENDER'},
+                                'max_lines_per_item' => $$schedule{'lines'},
+                                'stylesheet'         => $stylesheet );
+
+  if (not $message)
+  {
+    log_message LOG_WARNING, "Failed to create message object: $!";
+    return;
+  }
+
+  $message->calculate_period( $$schedule{'period-value'}, $$schedule{'period-unit'} );
+  
+  $message->add_text( "$Lang::tr{'statusmail period from'} " . localtime( $message->get_period_start ) .
+                      " $Lang::tr{'statusmail period to'} " . localtime( $message->get_period_end ) . "\n" );
+
+  # Loop through the various log items
+
+  foreach my $section ( sort keys %sections )
+  {
+    debug 3, "Section $section";
+    $message->add_section( $section );
+
+    foreach my $subsection ( sort keys %{ $sections{$section} } )
+    {
+      debug 3, "Subsection $subsection";
+      $message->add_subsection( $subsection );
+
+      foreach my $item ( sort keys %{ $sections{$section}{$subsection} } )
+      {
+        debug 3, "Item $item";
+
+        # Is the item enabled?
+
+        my $key = $sections{$section}{$subsection}{$item}{'ident'};
+
+        next unless (exists $$schedule{"enable_$key"} and $$schedule{"enable_$key"} eq 'on');
+        next unless ($sections{$section}{$subsection}{$item}{'format'} eq 'both' or
+                     $sections{$section}{$subsection}{$item}{'format'} eq $$schedule{'format'});
+
+        # Yes. Call the function to get it's content - with option if necessary
+
+        debug 2, "Process item $section :: $subsection :: $item";
+
+        $message->add_title( $item );
+
+        my $function = $sections{$section}{$subsection}{$item}{'function'};
+
+        if (exists $$schedule{"value_$key"})
+        {
+          $status += &$function( $message, $$schedule{"value_$key"} );
+        }
+        else
+        {
+          $status += &$function( $message );
+        }
+      }
+
+      $message->clear_cache;
+    }
+  }
+
+  # End the Message
+
+  if ($status > 0)
+  {
+    debug 1, "Send mail message";
+    $message->send;
+  }
+}
+
+
+#------------------------------------------------------------------------------
+# sub add_mail_item( params )
+#
+# Adds a possible status item to the section and subsection specified.  This
+# function is called from the BEGIN block of the plugin.
+#
+# Any errors cause the item to be ignored without raising an error.
+#
+# Parameters:
+#   params  hash containing details of the item to be added:
+#     section     name of the section containing this item
+#     subsection  name of the subsection containing this item
+#     item        name of the item
+#     function    function called to add item to message
+#     format      available formats for the item 'html', 'text' or 'both'
+#     option      hash specifying option parameter (optional)
+#
+# option can specify either a selection or an integer.  For a selection it
+# contains:
+#   type          must be 'option'
+#   values        array of strings representing the possible options
+#
+# For an integer option contains:
+#   type          must be 'integer'
+#   min           minimum valid value of parameter
+#   max           maximum valid value of parameter
+#------------------------------------------------------------------------------
+
+sub add_mail_item( % )
+{
+  my %params = @_;
+
+  # Check for all required parameters
+
+  return unless (exists $params{'section'}    and
+                 exists $params{'subsection'} and
+                 exists $params{'item'}       and
+                 exists $params{'function'} );
+
+  # Check the option
+
+  if ($params{'option'})
+  {
+    return unless (ref $params{'option'} eq 'HASH');
+
+    if ($params{'option'}{'type'} eq 'select')
+    {
+      return unless (ref $params{'option'}{'values'} eq 'ARRAY' and @{ $params{'option'}{'values'} } > 1);
+    }
+    elsif ($params{'option'}{'type'} eq 'integer')
+    {
+      return unless (exists $params{'option'}{'min'} and
+                     exists $params{'option'}{'max'} and
+                     $params{'option'}{'min'} < $params{'option'}{'max'});
+    }
+    else
+    {
+      return;
+    }
+  }
+
+  $params{'format'} = 'both' unless (exists $params{'format'});
+
+  # Record that the option exists
+
+  $sections{$params{'section'}}{$params{'subsection'}}{$params{'item'}} = { 'function' => $params{'function'},
+                                                                            'format'   => $params{'format'},
+                                                                            'ident'    => $params{'ident'} };
+}
+
+
+#------------------------------------------------------------------------------
+# sub abort( message )
+#
+# Aborts the update run, printing out an error message.
+#
+# Parameters:
+#   message     Message to be printed
+#------------------------------------------------------------------------------
+
+sub abort( $ )
+{
+my ($message) = @_;
+
+  log_message( LOG_ERR, $message );
+  croak $message;
+}
+
+
+#------------------------------------------------------------------------------
+# sub log_message( level, message )
+#
+# Logs a message to the system log.  If the script is run from the terminal
+# then the message is also printed locally.
+#
+# Parameters:
+#   level   Severity of message
+#   message Message to be logged
+#------------------------------------------------------------------------------
+
+sub log_message( $$ )
+{
+  my ($level, $message) = @_;
+
+  print "($level) $message\n" if (-t STDIN);
+  syslog( $level, $message );
+}
+
+
+#------------------------------------------------------------------------------
+# sub debug( level, message )
+#
+# Optionally logs a debug message
+#
+# Parameters:
+#   level   Debug level
+#   message Message to be logged
+#------------------------------------------------------------------------------
+
+sub debug( $$ )
+{
+  my ($level, $message) = @_;
+
+  if (($level <= $debug) or
+      ($level == 1 and -t STDIN))
+  {
+    log_message LOG_DEBUG, $message;
+  }
+}