#!/usr/bin/perl -w

# Programm zur Rechnerkonfiguration
# Diplomarbeit von Stephan Löscher
# Bei Rückfragen:
# loescher@gmx.de oder 08142/7257

######################################################################
###
###   IMPORTANT!
###   Please read the warranty and legal notice 
###   at the end of this file!
###
######################################################################

require 5.000;
use lib '/usr/local/bin',"$ENV{HOME}/bin",'/usr/stud/loescher/bin';
use lib 'd:/bin','c:/mydos','c:/bin';
use slutil; # Verfügbar unter: http://www.leo.org/~loescher/progdata/slutil.pm
use English;
use FileHandle;
use File::Copy;
use Carp;

######################################################################
### Unterprogramm-Funktionen für interne Zwecke
######################################################################

sub debug_rsh;  # Ausgabe von Informationen über remote-shell
sub debug;      # Ausgabe von Informationen
sub warning;    # Ausgabe von Warnungen
sub error;      # Ausgabe von Fehlern
sub myexit;     # Exit und Aufräumen
sub loginit;    # Schreibt einen kleinen Header ins LOG-file
sub logdie;     # Schreibt einen Text ins LOG-file und stirbt dann
sub logprint;   # Schreibt einen Text ins LOG-file

######################################################################
### Voreinstellungen
######################################################################

$version = '1.0';
$appname = 'SysConf';

# Wohin soll das logging erfolgen?
$logfile = ">>/tmp/\L$appname\E.log"; # Log in eine Datei
# $logfile = ">&STDERR"; # Log auf STDERR

# Exitcodes beim Beenden
$NormalExitCode = 0;
$ErrorExitCode  = 1;

$0 = $0; # Oh, very strange... :-) ... Kommandozeile verbergen.

######################################################################
### Log-File und Signal-Handler
######################################################################

# LOG bereits hier starten, denn Konfigurationsfehler sind entscheidend!
open(LOG, $logfile) || die "Kann Logfile '$logfile' nicht schreiben!\n";
select(LOG); $|=1; select(STDOUT); # Buffer ausschalten
loginit;

# Signal-Handler installieren
$SIG{HUP}  = \&catch_signal;
$SIG{INT}  = \&catch_signal;
$SIG{QUIT} = \&catch_signal;
$SIG{ABRT} = \&catch_signal;
$SIG{TERM} = \&catch_signal;
$SIG{'__WARN__'} = \&catch_warning;

# Welche Meldungen sollen ausgegeben werden?
if ( (defined $ARGV[0]) && ($ARGV[0] =~ /^-w(\d)/) )
{
  $logLevel = $1;
  shift;
}
else
{
  $logLevel = 0;
}
logprint "Log-Level: $logLevel\n";

######################################################################
### Hauptprogramm
######################################################################

if ($#ARGV<0)
{
  logprint "Ohne Parameter aufgerufen.\n";
  print "\nSie sollten $appname nicht ohne Parameter aufrufen.
Sie haben zur Auswahl:
  1. man-page erzeugen und ins aktuelle Verzeichnis schreiben
  2. HTML-Dokumentation ins aktuelle Verzeichnis schreiben
  3. LaTeX-Dokumentation ins aktuelle Verzeichnis schreiben
  4. Alle Dokumentationen (1 bis 3) erzeugen
  5. Kurzhilfe anzeigen
Auswahl: ";
  $input = readkey(); print "\n";
  POD_Ausgabe('man')   if $input =~ /1/;
  POD_Ausgabe('html')  if $input =~ /2/;
  POD_Ausgabe('latex') if $input =~ /3/;
  if ($input =~ /4/)
  {
    POD_Ausgabe('man');
    POD_Ausgabe('html');
    POD_Ausgabe('latex');
  }
  &Hilfe               if $input =~ /5/;
  myexit;
}

printumlaute Kopf();

# Einstellungen aus Hauptkonfigurationsdatei lesen
$sysconfroot = '';
$UseSyscheck = $FALSE;
$htmldir     = '';
ReadConfigFile();
debug "SYSCONF_ROOT = $sysconfroot\n";
debug "USE_SYSCHECK = $UseSyscheck\n";
debug "HTMLDIR      = $htmldir\n";

$checkpass = ReadWithoutEcho('Syscheck-Paßwort: ','STDIN') if $UseSyscheck;
print "\n";

%klassendef = RechnerKlassenEinlesen();

$beschreibungen = RechnerBeschreibungenEinlesen();

$param = ParameterEinlesen(@ARGV);
debug "Aktion:     ", $param->Aktion,"\n";
debug "Subsysteme: ", join(',',$param->Subsysteme), "\n";
debug "Rechner:    ", join(',',$param->Rechner),    "\n";

$dependencies    = Dependencies::new();
$files           = Files::new();
$templatepattern = TemplatePattern::new();


Documentation($beschreibungen,$param) if $param->Aktion eq 'documentation';


foreach $rechner ($param->Rechner)
{
  # Hier ist es pro Rechner parallelisierbar
  
  # Testen, ob es eine Beschreibung zu dem Rechner gibt
  unless (defined $beschreibungen->Betriebssystem($rechner))
  {
    warning "Zu dem Rechner '$rechner' gibt es keine Breschreibung!\n";
    next;
  }

  # Überprüfen durch CheckSubsystems():
  # - alle Subsysteme?
  # - darf dieser Rechner diese Subsysteme bekommen?
  # - dann stehen in @subsys die gewünschten Subsysteme
  @subsys = CheckSubsystems($rechner,$beschreibungen,$param);

  unless (@subsys)
  {
    warning "Keine Subsysteme für '$rechner' zu konfigurieren!\n";
    next;
  }

  %RechnerVars = ReadVariables($rechner);
  debug "Rechnervariablen:\n";
  foreach (keys %RechnerVars)
  { debug "'$_'='$RechnerVars{$_}'\n"; }

  # Datei "files.sc" für dieses Betriebssystem einlesen
  ReadFilesSC($beschreibungen->Betriebssystem($rechner));
  debug "Nach ReadFilesSC().\n";
  
 SWITCH: {
    # Init
    if ($param->Aktion eq 'init')  
    {
      @subsys = 
      CheckAndAddDependencies($beschreibungen->Betriebssystem($rechner),
                              @subsys);
      Init($rechner,$beschreibungen->Betriebssystem($rechner),\%RechnerVars,
           @subsys);
      last SWITCH;
    }

    # Update
    if ($param->Aktion eq 'update')
    {
      @subsys = 
      CheckAndAddDependencies($beschreibungen->Betriebssystem($rechner),
                              @subsys);
      Update($rechner,$beschreibungen->Betriebssystem($rechner),\%RechnerVars,
             @subsys);
      last SWITCH;
    }

    # Start
    if ($param->Aktion eq 'start')
    {
      # Hier kein CheckAndAddDependencies(), weil sonst alle möglichen 
      # Subsysteme beeinflußt werden!
      Start($rechner,$beschreibungen->Betriebssystem($rechner),@subsys);
      last SWITCH;
    }

    # Stop
    if ($param->Aktion eq 'stop')
    {
      # Hier kein CheckAndAddDependencies(), weil sonst alle möglichen 
      # Subsysteme beeinflußt werden!
      Stop($rechner,$beschreibungen->Betriebssystem($rechner),@subsys);
      last SWITCH;
    }

    # Remove
    if ($param->Aktion eq 'remove')
    {
      # Hier kein CheckAndAddDependencies(), weil sonst alle möglichen 
      # Subsysteme entfernt werden!
      Remove($rechner,$beschreibungen->Betriebssystem($rechner),@subsys);
      last SWITCH;
    }

    # Dokumentation
    if ($param->Aktion eq 'documentation')
    {
     Documentation($rechner,$beschreibungen->Betriebssystem($rechner),
                   \%RechnerVars,@subsys);
     last SWITCH;
    }
    logdie "Die Aktion ",$param->Aktion," gibt es nicht!\n";
  }

  print '-'x70,"\n";
}


myexit($NormalExitCode);


######################################################################
### Unterprogramme
######################################################################

sub Init
{
  # Initialisieren eines Rechners
  # Parameter: Rechner, Betriebssystem, Referenz auf Hash der Rechnervariablen,
  #            Liste der Subsysteme

  my $rechner = shift;
  my $bs      = shift;
  my $refvar  = shift;
  my @subsys  = @_;

  print "Führe Aktion 'init' mit '$rechner' für die ",
  "Subsysteme\n",join(',',@subsys), "\nin genau dieser Reihenfolge aus.\n";

  # Subsystem-unabhängige Files

  TransferFiles($rechner, $bs, 'GLOBAL', 'installshell',$refvar);
  TransferFiles($rechner, $bs, 'GLOBAL', 'install',     $refvar);
  TransferFiles($rechner, $bs, 'GLOBAL', 'installlink', $refvar);
  # Init-Files/Templates kopieren
  TransferFiles($rechner, $bs, 'GLOBAL', 'initshell',   $refvar);
  TransferFiles($rechner, $bs, 'GLOBAL', 'init',        $refvar);
  TransferFiles($rechner, $bs, 'GLOBAL', 'inittemplate',$refvar);
  TransferFiles($rechner, $bs, 'GLOBAL', 'initlink',    $refvar);
  # Rest, den "update' auch überträgt
  TransferFiles($rechner, $bs, 'GLOBAL', 'shell',       $refvar);
  TransferFiles($rechner, $bs, 'GLOBAL', 'file',        $refvar);
  TransferFiles($rechner, $bs, 'GLOBAL', 'template',    $refvar);
  TransferFiles($rechner, $bs, 'GLOBAL', 'link',        $refvar);

  # Für alle Subsysteme
  my $subsystem;
  foreach $subsystem (@subsys)
  {
    print "Behandle Subsystem '$subsystem'\n";

    my $sub = SubsystemObject->new($bs, $rechner, $subsystem);
    
    # Ist das Subsystem bereits installiert?
    if ($sub->IsInstalled)
    {
      # Läuft das Subsystem gerade?
      if ($sub->IsRunning)
      {
        # Stoppen (damit auch alle abhängigen)
        $sub->Stop || warning "Stoppen von '$subsystem' nicht erfolgreich!\n";
      }
    }
    else
    {
      # Files, die zur Installation benötigt werden übertragen
      TransferFiles($rechner, $bs, $subsystem, 'installshell',$refvar);
      TransferFiles($rechner, $bs, $subsystem, 'install',     $refvar);
      TransferFiles($rechner, $bs, $subsystem, 'installlink', $refvar);
      # Subsystem installieren
      unless ($sub->Install)
      {
        warning "Install von '$subsystem' nicht erfolgreich!\n";
        warning "Breche '$subsystem' ab!\n";
        return;
      }
    }
    # Init-Files/Templates kopieren
    TransferFiles($rechner, $bs, $subsystem, 'initshell',   $refvar);
    TransferFiles($rechner, $bs, $subsystem, 'init',        $refvar);
    TransferFiles($rechner, $bs, $subsystem, 'inittemplate',$refvar);
    TransferFiles($rechner, $bs, $subsystem, 'initlink',    $refvar);
    # Rest, den "update' auch überträgt
    TransferFiles($rechner, $bs, $subsystem, 'shell',       $refvar);
    TransferFiles($rechner, $bs, $subsystem, 'file',        $refvar);
    TransferFiles($rechner, $bs, $subsystem, 'template',    $refvar);
    TransferFiles($rechner, $bs, $subsystem, 'link',        $refvar);
    # Subsystem starten
    $sub->Start || warning "Start von '$subsystem' nicht erfolgreich!\n";
  }
}


sub Update
{
  # Updaten eines Rechners
  # Parameter: Rechner, Betriebssystem, Referenz auf Hash der Rechnervariablen,
  #            Liste der Subsysteme

  my $rechner = shift;
  my $bs      = shift;
  my $refvar  = shift;
  my @subsys  = @_;

  print "Führe Aktion 'update' mit '$rechner' für die ",
  "Subsysteme\n",join(',',@subsys), "\nin genau dieser Reihenfolge aus.\n";

  # Subsystem-unabhängige Files

  TransferFiles($rechner, $bs, 'GLOBAL', 'installshell',$refvar);
  TransferFiles($rechner, $bs, 'GLOBAL', 'install',     $refvar);
  TransferFiles($rechner, $bs, 'GLOBAL', 'installlink', $refvar);
  TransferFiles($rechner, $bs, 'GLOBAL', 'shell',       $refvar);
  TransferFiles($rechner, $bs, 'GLOBAL', 'file',        $refvar);
  TransferFiles($rechner, $bs, 'GLOBAL', 'link',        $refvar);
  TransferFiles($rechner, $bs, 'GLOBAL', 'template',    $refvar);

  # Für alle Subsysteme
  my $subsystem;
  foreach $subsystem (@subsys)
  {
    print "Behandle Subsystem '$subsystem'\n";

    my $sub = SubsystemObject->new($bs, $rechner, $subsystem);
    
    # Ist das Subsystem bereits installiert?
    if ($sub->IsInstalled)
    {
      # Läuft das Subsystem gerade?
      if ($sub->IsRunning)
      {
        # Stoppen (damit auch alle abhängigen)
        $sub->Stop || warning "Stoppen von '$subsystem' nicht erfolgreich!\n";
      }
      # Files für Update übertragen
      TransferFiles($rechner, $bs, $subsystem, 'shell',   $refvar);
      TransferFiles($rechner, $bs, $subsystem, 'file',    $refvar);
      TransferFiles($rechner, $bs, $subsystem, 'template',$refvar);
      TransferFiles($rechner, $bs, $subsystem, 'link',    $refvar);
      
      $sub->Reconfigure || 
      warning "Rekonfigurieren von '$subsystem' nicht erfolgreich!\n";
      # Subsystem starten
      $sub->Start || warning "Start von '$subsystem' nicht erfolgreich!\n";
    }
    else
    {
      # Wenn das Subsystem nicht installiert ist, dann Init() aufrufen
      Init($rechner,$bs,$refvar,@subsys);
    }
  }
}


sub Remove
{
  # Parameter: Rechner, Betriebssystem, Liste der Subsysteme

  my $rechner = shift;
  my $bs      = shift;
  my @subsys  = @_;

  print "Führe Aktion 'remove' mit '$rechner' für die ",
  "Subsysteme\n",join(',',@subsys), "\nin genau dieser Reihenfolge aus.\n";

  # Für alle Subsysteme
  my $subsystem;
  foreach $subsystem (@subsys)
  {
    print "Behandle Subsystem '$subsystem'\n";

    my $sub = SubsystemObject->new($bs, $rechner, $subsystem);
    
    # Ist das Subsystem überhaupt installiert?
    unless ($sub->IsInstalled)
    {
      print "'$subsystem' ist auf '$rechner' nicht installiert!\n";
      next;
    }

    # Läuft das Subsystem gerade?
    if ($sub->IsRunning)
    {
      # Stoppen (damit auch alle abhängigen)
      $sub->Stop || warning "Stoppen von '$subsystem' nicht erfolgreich!\n";
    }
    # Subsystem de-installieren
    $sub->Remove || warning "Remove von '$subsystem' nicht erfolgreich!\n";
  }
}


sub Start
{
  # Parameter: Rechner, Betriebssystem, Liste der Subsysteme

  my $rechner = shift;
  my $bs      = shift;
  my @subsys  = @_;

  print "Führe Aktion 'start' mit '$rechner' für die ",
  "Subsysteme\n",join(',',@subsys), "\nin genau dieser Reihenfolge aus.\n";

  # Für alle Subsysteme
  my $subsystem;
  foreach $subsystem (@subsys)
  {
    print "Behandle Subsystem '$subsystem'\n";

    my $sub = SubsystemObject->new($bs, $rechner, $subsystem);
    
    # Ist das Subsystem überhaupt installiert?
    unless ($sub->IsInstalled)
    {
      print "'$subsystem' ist auf '$rechner' nicht installiert!\n";
      next;
    }

    # Läuft das Subsystem gerade?
    if ($sub->IsRunning)
    {
      print "'$subsystem' läuft auf '$rechner' bereits!\n";
      next;
    }

    # Subsystem starten
    $sub->Start || warning "Start von '$subsystem' nicht erfolgreich!\n";
  }
}


sub Stop
{
  # Parameter: Rechner, Betriebssystem, Liste der Subsysteme

  my $rechner = shift;
  my $bs      = shift;
  my @subsys  = @_;

  print "Führe Aktion 'stop' mit '$rechner' für die ",
  "Subsysteme\n",join(',',@subsys), "\nin genau dieser Reihenfolge aus.\n";

  # Für alle Subsysteme
  my $subsystem;
  foreach $subsystem (@subsys)
  {
    print "Behandle Subsystem '$subsystem'\n";

    my $sub = SubsystemObject->new($bs, $rechner, $subsystem);
    
    # Ist das Subsystem überhaupt installiert?
    unless ($sub->IsInstalled)
    {
      print "'$subsystem' ist auf '$rechner' nicht installiert!\n";
      next;
    }

    # Läuft das Subsystem gerade?
    if ($sub->IsRunning)
    {
      # Subsystem stoppen
      $sub->Stop || warning "Stoppen von '$subsystem' nicht erfolgreich!\n";
    }
    else
    {
      print "'$subsystem' läuft auf '$rechner' gar nicht!\n";
    }
  }
}


sub TransferFiles
{
  # Überträgt Files von der Konfigurationsdatenbank auf Zielrechner
  # und führt Textersetzungen durch und startet Shellkommandos
  # Parameter: Rechner, Betriebssystem, Subsystem, File-Art,
  #            Referenz auf Hash mit den Rechnervariablen
  # Return: -

  my ($rechner, $bs, $subsys, $art, $refvar) = @_;

  unless ( $files->IstArtGueltig($art) )
  {
    carp("TransferFiles() mit falschem Art-Parameter aufgerufen!\n");
    myexit($main::ErrorExitCode);
  }

  my %files = %{$files->Get($bs,$subsys,$art)};
  print "Übertrage Files... ($art)\n";
  my ($file, $zielfile); 

  foreach $file (keys %files)
  { # foreach $file
    @zielfiles = @{$files{$file}};
    
    foreach $zielfile (@zielfiles)
    { # foreach $zielfile
      
      # Installfiles, Initfiles, Files
      if ($art =~ /^install$|^init$|^file$/)
      {
        RemoteCopy($file,$rechner,$zielfile);
        next;
      }
      # Inittemplates, Templates
      if ($art =~ /^inittemplate$|^template$/)
      {
        # Ersetzungsmuster anwenden
        my $pattern = $templatepattern->Get($bs,$subsys,$file,$zielfile);
        my $tempfile = "/tmp/sysconf.template.$$";
        TextModify::ErsetzeMuster($file, $tempfile, $pattern, $refvar);
        
        # Owner,Gruppe,Permissions auf das neue Tempfile übertragen
        my ($mode,$uid,$gid) = (stat($file))[2,4,5];
        chmod $mode,    $tempfile || logdie "Fehler bei chmod('$tempfile')!\n";
        chown $uid,$gid,$tempfile || logdie "Fehler bei chown('$tempfile')!\n";
        
        RemoteCopy($tempfile,$rechner,$zielfile);
        unlink $tempfile;
        next;
      }
      # Links
      if ($art =~ /^link$|^installlink$|^initlink$/)
      {
        RemoteCreateLink($file,$rechner,$zielfile);
        next;
      }
      # Shellkommandos
      if ($art =~ /^shell$|^installshell$|^initshell$/)
      {
        my $variablenCode   = '';
        foreach (keys %$refvar)
        {
          my $refvarquote = $$refvar{$_}; $refvarquote =~ s/\'/\\\'/g;
          $variablenCode .= "my \$$_='$refvarquote';\n";
        }
        # Variablenersetzung
        {
          local $FehlerInShellVariablenErsetzung_Kommando = $file;
          local $SIG{__WARN__} = \&FehlerInShellVariablenErsetzung;
          eval $variablenCode.'$file =~ s/(\$\w+)/$1/eeg;';
        }
        RemoteShell($rechner,$file);
        next;
      }
      # Sonst
      logdie "Interner Fehler: File-Art ist ungültig!\n";
      
    }
    
  }
}


sub Documentation
{
  # HTML-Dokumentation erzeugen
  # Parameter: Beschreibungen-Objekt, ParameterListe-Objekt
  # Return;    -
  #
  my ($beschreibungen,$param) = @_;
  
  my $head = '<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html>
  <head>
    <title>Systemkonfiguration</title>
  </head>

  <body>
    <h1>Systemkonfiguration</h1>

    <table border=1>
      <tr>
        <td>Rechner</td>
        <td>Subsysteme</td>
        <td>Variablen</td>
        <td>Betriebssystem</td>
      </tr>
      
';
  my $tail = '    </table>

    <hr>
    <address>
      Diese Seite wurde automatisch generiert auf dem Rechner '
  .`hostname`.
  " aus der Datenbank in $sysconfroot durch<br>"
  .$appname.' '.$version.' von
      <A HREF="http://www.leo.org/~loescher/">Stephan L&ouml;scher</A>,
      <a href="mailto:loescher@gmx.de">loescher@gmx.de</a>,<br>
      '.date.'
    </address>
  </body>
</html>
';

  print "Führe Aktion 'documentation' aus.\n";
  my $fh = FileHandle->new();
  open($fh, ">$htmldir${slash}index.html") || logdie
  "Kann '$htmldir${slash}index.html' nicht zum Schreiben öffnen!\n";
  print $fh $head;

  my $rechner;
  my $bs;
  foreach $rechner ($param->Rechner)
  {
    # Testen, ob es eine Beschreibung zu dem Rechner gibt
    unless (defined ($bs = $beschreibungen->Betriebssystem($rechner)) )
    {
      warning "Zu dem Rechner '$rechner' gibt es keine Breschreibung!\n";
      next;
    }
    my @subsys = CheckSubsystems($rechner,$beschreibungen,$param);
    unless (@subsys)
    {
      warning "Keine Subsysteme für '$rechner' vorhanden!\n";
      next;
    }
    my %RechnerVars = ReadVariables($rechner);
    # Datei "files.sc" für dieses Betriebssystem einlesen
    ReadFilesSC($beschreibungen->Betriebssystem($rechner));
   
    # Dateien der Variablen-Inhalte erstellen
    my ($key,$value);
    while (($key,$value) = each %RechnerVars)
    {
      my $vfh = FileHandle->new();
      open($vfh, ">$htmldir${slash}$rechner-$key.txt") || logdie
      "Kann '$htmldir${slash}$rechner-$key.txt' nicht zum Schreiben öffnen!\n";
      print $vfh $value;
      close $vfh;
    }
    print $fh "      <tr>
        <td valign=top>$rechner</td>
        <td valign=top>\n";
    foreach (@subsys)
    {
      print $fh "          <a href=\"$rechner-sub-$_.html\">$_</a><br>\n";
      GeneriereSubsystemHTML($rechner, $bs, $_, \%RechnerVars);
    }
    print $fh "        </td>\n        <td valign=top>\n";
    foreach (keys %RechnerVars)
    {
      print $fh "          <a href=\"$rechner-$_.txt\">$_</a><br>\n";
    }
    print $fh "        </td>
        <td valign=top>",$beschreibungen->Betriebssystem($rechner),"</td>
      </tr>
";
  }

  print $fh $tail;
  close $fh;
  myexit($NormalExitCode);
}


sub GeneriereSubsystemHTML
{
  # Erstellt HTML-Seiten zu einem Subsystem analog wie TransferFiles() arbeitet
  # Parameter: Rechner, Betriebssystem, Subsystem,
  #            Referenz auf Hash mit den Rechnervariablen
  # Return: -

  my ($rechner, $bs, $subsys, $refvar) = @_;
  my @artliste = ('install','installlink','installshell',
                  'init','inittemplate','initlink','initshell',
                  'file','template','link','shell');
  my $art;

  my $head = "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML//EN\">
<html>
  <head>
    <title>Subsystem '$subsys' auf '$rechner'</title>
  </head>

  <body>
    <h2>Subsystemkonfiguration: ".
    "Dateien f&uuml;r '$subsys' auf Rechner '$rechner'</h2>

    <table border=1>
      <tr>
        <td><b>Dateiart</b></td>
        <td><b>Datei/Inhalt</b></td>
      </tr>\n";

  my $tail = '    <hr>
    <address>
      Diese Seite wurde automatisch generiert durch<br>
      '.$appname.' '.$version.' von
      <A HREF="http://www.leo.org/~loescher/">Stephan L&ouml;scher</A>,
      <a href="mailto:loescher@gmx.de">loescher@gmx.de</a>,<br>
      '.date.'
    </address>
  </body>
</html>
';

  my $fh = FileHandle->new();
  open($fh, ">$htmldir${slash}$rechner-sub-$subsys.html") || logdie
  "Kann '$htmldir${slash}$rechner-sub-$subsys.html' nicht zum Schreiben ".
  "öffnen!\n";
  print $fh $head;

  # Alle Dateiarten durchlaufen
  foreach $art (@artliste)
  { # foreach $art
    my %files = %{$files->Get($bs,$subsys,$art)};
    my ($file, $zielfile); 
    
    print $fh "      <tr>\n        <td valign=top>$art</td>\n        <td>\n";
    
    foreach $file (keys %files)
    { # foreach $file
      @zielfiles = @{$files{$file}};
      
      foreach $zielfile (@zielfiles)
      { # foreach $zielfile
        
        my $zielhtml = $zielfile;
        $zielhtml =~ s!/!_!g;
        # Installfiles, Initfiles, Files
        if ($art =~ /^install$|^init$|^file$/)
        {
          # Nur kopieren, wenn es eine Text-Datei ist
          if (-T $file)
          {
            copy($file, "$htmldir$slash$rechner-sub-$subsys-$zielhtml.txt");
            print $fh "          <a href=\"$rechner-sub-$subsys-".
            "$zielhtml.txt\">$zielfile</a><br>\n";
          }
          else
          {
            print $fh "          $zielfile (bin&auml;r)<br>\n";
          }
          next;
        }
        # Inittemplates, Templates
        if ($art =~ /^inittemplate$|^template$/)
        {
          # Ersetzungsmuster anwenden
          my $pattern = $templatepattern->Get($bs,$subsys,$file,$zielfile);
          my $tempfile = "/tmp/sysconf.template.$$";
          TextModify::ErsetzeMuster($file, $tempfile, $pattern, $refvar);
          copy($tempfile, "$htmldir$slash$rechner-sub-$subsys-$zielhtml.txt");
          print $fh "          <a href=\"$rechner-sub-$subsys-".
          "$zielhtml.txt\">$zielfile</a><br>\n";
          unlink $tempfile;
          next;
        }
        # Links
        if ($art =~ /^link$|^installlink$|^initlink$/)
        {
          print $fh "          $file<br>\n";
          next;
        }
        # Shellkommandos
        if ($art =~ /^shell$|^installshell$|^initshell$/)
        {
          my $variablenCode   = '';
          foreach (keys %$refvar)
          {
            my $refvarquote = $$refvar{$_}; $refvarquote =~ s/\'/\\\'/g;
            $variablenCode .= "my \$$_='$refvarquote';\n";
          }
          # Variablenersetzung
          {
            local $FehlerInShellVariablenErsetzung_Kommando = $file;
            local $SIG{__WARN__} = \&FehlerInShellVariablenErsetzung;
            eval $variablenCode.'$file =~ s/(\$\w+)/$1/eeg;';
          }
          print $fh "          <pre>$file</pre>\n";
          next;
        }
        # Sonst
        logdie "Interner Fehler: File-Art ist ungültig!\n";
      }
    }
    print $fh "          <br>\n        </td>\n";
  }
  print $fh "      </tr>\n";


  # Kommando-Dateien auflisten
  print $fh "    </table>\n    <br>\n";
  print $fh "    <b>Kommandos:</b><br>\n";
  my $sub = SubsystemObject->new($bs, $rechner, $subsys);
  print $fh $sub->GetHTML($rechner,$htmldir);

  print $fh $tail;
  close $fh;
}


sub FehlerInShellVariablenErsetzung
{
  # Signal-Handler für Fehler in Variablenersetzungen in Shell-Kommandos
  # Wichtig: Die globale (local) Variable
  #          $FehlerInShellVariablenErsetzung_Kommando muß gesetzt sein!
  # Parameter: -
  # Return:    -
  my $fehler = shift;
  if ($fehler =~ /Use of uninitialized value at/)
  {
    warning "Fehler in der Variablenersetzung!\n";
    warning "Sie haben in folgendem Shell-Kommando eine Variable ".
    "verwendet,\n";
    warning "die Sie aber nicht definiert haben:\n";
    warning "'$FehlerInShellVariablenErsetzung_Kommando'\n";
  }
  else
  {
    warning $fehler;
    warning "Nicht abgefangener Fehler in Shell-Variablenersetzung!\n";
  }
}


sub TesteDateiBesitzer
{
  # Parameter: Voller Pfad einer Datei
  # Kein Returnwert. (Bei Gefahr sofort Abbruch.)
  #
  # Es wird überprüft, ob eine Konfigurationsdatei nur für den Menschen
  # schreibbar ist, der auch sysconf ausführt.
  # Sonst könnte irgendjemand die Konfigurationsfiles verändern und Root
  # läßt das dann aufs System los.
  #
  my $file = shift;
  if ( (lstat($file))[2] != 33188 ) # "-rw-r--r--"
  {
    logdie "Sicherheitslücke: Das File '$file' ist nicht Mode 644!\n" 
  }
  unless (-o $file)
  {
    logdie "Sicherheitslücke: Sie sind nicht Besitzer von '$file'\n"
  }
}


sub RemoteMkdir
{
  # Erstellt ein Verzeichnis auf einem anderen Rechner
  # Parameter: Rechner, Verzeichnisname

  my $rechner = shift;
  my $verz    = shift;

  my $rsh = $beschreibungen->GetRSH($rechner);

  debug_rsh "$rsh $rechner mkdir -p $verz\n";
  system("$rsh $rechner mkdir -p $verz");
  logdie "RemoteShell liefert Fehler!\n" if ($?>>8);
}


sub RemoteCopy
{
  # Kopiert ein File auf einen anderen Rechner
  # Parameter: Quelle, Rechner, Ziel

  my $quelle  = shift;
  my $rechner = shift;
  my $ziel    = shift;

  # Vor dem Kopieren die Verzeichnisse anlegen
  $ziel =~ /(.*)$slashsuch[^$slashsuch]+/;
  my $pfad = $1;
  RemoteMkdir($rechner, $pfad);

  my $rsh = $beschreibungen->GetRSH($rechner);
  my $rcp = $beschreibungen->GetRCP($rechner);

  debug_rsh "$rcp $quelle $rechner:$ziel\n";
  system("$rcp -p $quelle $rechner:$ziel");
  logdie "RemoteCopy liefert Fehler!\n" if ($?>>8);
  
  # Syscheck starten, wenn erwünscht
  StartSyscheck($rsh, $checkpass, $rechner, $ziel) if $UseSyscheck;
  
  # Owner / Gruppe übertragen
  my ($uid,$gid) = (stat($quelle))[4,5];
  my $user   = getpwuid($uid);
  my $gruppe = getgrgid($gid);
  debug_rsh "$rsh $rechner chown $user.$gruppe $ziel\n";
  system("$rsh $rechner chown $user.$gruppe $ziel");
  logdie "RemoteShell liefert Fehler!\n" if ($?>>8);
}


sub StartSyscheck
{
  # Parameter: Remote-Shell, Syscheck-Paßwort, Rechner, Dateiname
  # Return:    -
  #
  my ($rsh, $checkpass, $rechner, $file) = @_;
  my $fh = FileHandle->new();
  debug_rsh "|$rsh $rechner syscheck -q -w3 update\n";
  open($fh, "|$rsh $rechner syscheck -q -w3 update") || logdie
  "Kann '|$rsh $rechner' für Syscheck nicht öffnen!\n";
  print $fh "$checkpass\n$file\n";
  close $fh;
}


sub RemoteCreateLink
{
  # Erstellt einen Link auf einem anderen Rechner
  # Parameter: Quelle, Rechner, Ziel

  my $quelle  = shift;
  my $rechner = shift;
  my $ziel    = shift;

  # Vor dem Link anlegen die Verzeichnisse anlegen
  $ziel =~ /(.*)$slashsuch[^$slashsuch]+/;
  my $pfad = $1;
  RemoteMkdir($rechner, $pfad);

  my $rsh = $beschreibungen->GetRSH($rechner);

  debug_rsh "$rsh $rechner ln -sf $quelle $ziel\n";
  system("$rsh $rechner ln -sf $quelle $ziel");
  logdie "RemoteShell liefert Fehler!\n" if ($?>>8);
}


sub RemoteShell
{
  # Startet einen RemoteShell
  # Parameter: Rechner, Kommando

  my $rechner  = shift;
  my $kommando = shift;

  my $rsh = $beschreibungen->GetRSH($rechner);

  debug_rsh "$rsh $rechner $kommando\n";
  system("$rsh $rechner $kommando");
  logdie "RemoteShell liefert Fehler!\n" if ($?>>8);
}


sub ReadVariables
{
  # Einlesen der Variablen pro Rechner, welche in
  # $sysconfroot/variables/rechnername.var
  # stehen.
  # Parameter: Rechnername
  # Return:    Hash mit den Variablen

  my $rechner = shift;
  my %hash  = ();

  # Gibt es das Verzeichnis für die Variablen?
  unless (-d "$sysconfroot${slash}variables")
  {
   logdie "Das Verzeichnis '$sysconfroot${slash}variables' existiert nicht!\n";
  }

  my $file = "$sysconfroot${slash}variables$slash$rechner.var";
  # Ist das File lesbar?
  unless (-r $file)
  {
    logdie "Das File '$file' existiert nicht oder ist nicht lesbar!\n";
  }
  
  TesteDateiBesitzer($file);

  my $fh = FileHandle->new();
  open($fh, $file) || logdie "Kann '$file' nicht öffnen!\n";
  # Die Variablen-Dateien haben den Aufbau:
  # variable=wert
  # oder
  # variable=
  # oder
  # variable include FILENAME
  while(<$fh>)
  {
    next if /^\#/;   # Kommentare
    next if /^\s*$/; # Leerzeilen
    my ($var,$zuweisung,$wert);
  SWITCH:
    {
      if (/^(\S+)\s*=(.*)/)
      {
        ($var,$zuweisung,$wert) = ($1,'=',(defined $2 ? $2 : ''));
        last SWITCH;
      }
      if (/^(\S+)\s*include\s+(.+)/)
      {
        ($var,$zuweisung,$wert) = ($1,'include',$2);
        last SWITCH;
      }
      logdie "Fehler in den Variablen im File '$file' ",
      "in Zeile $.:\n",$_,"\n";      
    }
    if (defined $hash{$var})
    {
      logdie "Variable '$var' doppelt deklariert im File '$file' ",
      "in Zeile $.!\n";
    }
    # Direkte Zuweisung
    if ($zuweisung eq '=')
    {
      $hash{$var} = $wert;
    }
    # Zuweisung eines File-Inhalts
    else
    {
      # In $wert steht eine Liste von Dateinamen getrennt durch Leerzeichen
      foreach $einzelfile (split(/\s+/,$wert))
      {
        my $varfile = "$sysconfroot${slash}variables$slash$einzelfile";
        logdie "File '$einzelfile' kann in '$sysconfroot${slash}variables' ".
        "nicht gelesen/gefunden werden!\n" unless -r $varfile;
        my $fh = FileHandle->new();
        open($fh, $varfile) || logdie "Fehler beim Öffnen von '$varfile'!\n";
        {
          local $/ = undef; # In einem Stück einlesen
          $hash{$var} .= <$fh>;
        }
        close $fh;
      }
    }
  }
  return %hash;
}


sub CheckAndAddDependencies
{
  # Überprüfung der Abhängigkeiten der Subsysteme und Hinzufügen fehlender
  # Subsysteme
  # Parameter: Betriebssystem, Liste der Subsysteme
  # Return:    Ergänzte Liste der Subsysteme in richtiger Reihenfolge

  my $betriebssystem = shift;
  my @subsysteme = @_;

  ReadDependencies($betriebssystem);
  
  my %dep = ( defined $dependencies->Get($betriebssystem) ? 
              $dependencies->Get($betriebssystem) : () );

  my @alle_subs = ();
  foreach (@subsysteme)
  {
    print "Gewünschtes Subsys: $_\n";
    unless (defined $dep{$_})
    {
      push @alle_subs, $_;
      next;
    }
    push @alle_subs, @{$dep{$_}}, $_;
  }

  @alle_subs = KompaktiereSubsystemListe(@alle_subs);

  return @alle_subs;
}


sub ReadDependencies
{
  # File mit den Abhängigkeiten der Subsysteme einlesen
  # Es wird das globale Dependencies-Objekt gesetzt
  # Parameter: Betriebssystem
  # Return:    -

  my $depfilename = 'dependencies.sc';
  my $bs = shift;

  # Wenn für dieses Betriebssystem die Abhängigkeiten schon eingelesen sind
  # dann nichts tun.
  return if defined ($dependencies->Get($bs));

  # Gibt es das Verzeichnis für das Betriebssystem?
  unless (-d "$sysconfroot$slash$bs")
  {
    logdie "Das Verzeichnis '$sysconfroot$slash$bs' existiert nicht!\n";
  }

  my $depfile = "$sysconfroot$slash$bs$slash$depfilename";
  unless (-r $depfile)
  {
    logdie "Das File '$depfile' existiert nicht oder ist nicht lesbar!\n";
  }

  TesteDateiBesitzer($depfile);

  my %dep = ();

  ###
  ### Abhängigkeiten einlesen
  ###
  my $fh = FileHandle->new();
  open($fh, $depfile) || logdie "Kann '$depfile' nicht öffnen!\n";
  # Die Datei "dependencies.sc" hat den Aufbau:
  # S : D1 D2 D3 ...
  # Bedeutung: Subsystem "S" hängt von Subsystemen "D1", "D2", "D3", ... ab.
  while(<$fh>)
  {
    next if /^\#/;   # Kommentare
    next if /^\s*$/; # Leerzeilen
    unless (/^(\S+)\s*:\s*(.+)/)
    {
      logdie "Fehler in den Abhängikeiten in Zeile $.:\n",$_,"\n";      
    }
    my ($sub,$deps) = ($1,$2);
    if (defined $dep{$sub})
    {
      logdie "Abhängigkeiten für '$sub' mehrfach definiert in Zeile $.!\n";
    }
    $dep{$sub} = [ split(/\s+/,$deps) ];
  }

  ###
  ### Rekursive Abhängigkeiten auflösen
  ###
  my @deps        = ();
  my @result_deps = ();
 NOCHMAL:
  my $neuer_durchlauf_notwendig = $FALSE;
  my $subsys;
  foreach $subsys (keys %dep)
  {
    @result_deps = ();
    @deps = @{$dep{$subsys}};
    my $d;
    foreach $d (@deps)
    {
      if ($d eq $subsys)
      {
        logdie "Zyklische Abhängigkeit für Subsystem '$d' in '$depfile'!\n";
      }
      if (defined $dep{$d})
      {
        # Abhängigkeit auflösen
        push @result_deps, @{$dep{$d}};
        $neuer_durchlauf_notwendig = $TRUE;
      }
      else
      {
        # Subsystem direkt übernhemen
        push @result_deps, $d;
      }
    }
    $dep{$subsys} = [ @result_deps ];
  }
  goto NOCHMAL if $neuer_durchlauf_notwendig;

  ###
  ### Subsysteme "zusammenschnurren" lassen, z.B.:
  ###
  #   0 7 8 5 4 8 9 5 4 8 4
  #   wird zu
  #   0 7 8 5 4 9
  foreach $subsys (keys %dep)
  {
    @result_deps = KompaktiereSubsystemListe( @{$dep{$subsys}} );
    $dep{$subsys} = [ @result_deps ];
  }

  debug "-----\n";
  debug "Abhängigkeiten:\n";
  foreach (keys %dep) { debug "$_ -> @{$dep{$_}}\n"; }

  close $fh;
  $dependencies->Set($bs,%dep);
}


sub ReadFilesSC
{
  # File "files.sc" mit den Files für die Subsysteme einlesen
  # Es wird das globale Files-Objekt gesetzt
  # Parameter: Betriebssystem
  # Return:    -

  my $files_sc = 'files.sc';
  my $bs = shift;
  # Zuordung von Schlüsselwörtern zu Objekt-Parametern
  my %artkey = (
                inst  => 'install',
                I     => 'install',
                initf => 'init',
                F     => 'init',
                initt => 'inittemplate',
                T     => 'inittemplate',
                f     => 'file',
                t     => 'template',
                # Links
                L     => 'link',
                instL => 'installlink',
                initL => 'initlink',
                # Shell-Kommandos
                S     => 'shell',
                instS => 'installshell',
                initS => 'initshell',
               );

  # Wenn für dieses Betriebssystem die "files.sc" schon eingelesen sind
  # dann nichts tun.

  return if $files->BereitsEingelesen($bs);

  # Gibt es das Verzeichnis für das Betriebssystem?
  unless (-d "$sysconfroot$slash$bs")
  {
    logdie "Das Verzeichnis '$sysconfroot$slash$bs' existiert nicht!\n";
  }

  my $file = "$sysconfroot$slash$bs$slash$files_sc";
  unless (-r $file)
  {
    logdie "Das File '$file' existiert nicht oder ist nicht lesbar!\n";
  }

  TesteDateiBesitzer($file);

  ###
  ### "files.sc" einlesen
  ###
  my $fh = FileHandle->new();
  open($fh, $file) || logdie "Kann '$file' nicht öffnen!\n";
  # Die Datei "files.sc" hat den Aufbau:
  # [subsys]
  # <fileentry>
  # ...
  # [subsys]: Beginn Beschreibung Subsystem namens "subsys"
  # <fileentry>: <type> <filename> <Zielfilename>
  # <patternblock>
  #
  # Weiteres siehe Doku.
  
  my $subsys = '';
  my ($art, $quelle, $ziel);
  # Den Subsystem-Abschnitt finden
  while(<$fh>)
  {
 ABSCHNITT:
    next if /^\#/;   # Kommentare
    next if /^\s*$/; # Leerzeilen
    if (! /\[(.+)\]/)
    {
      chomp;
      logdie "'[subsystem]' statt '$_' in '$file' Zeile $. erwartet!\n";
    }
    else
    {
      $subsys = $1;
      last;
    }
  }
  # Jetzt im Subsystem-Abschnitt weiterlesen
  while(<$fh>)
  {
    next if /^\#/;   # Kommentare
    next if /^\s*$/; # Leerzeilen
    goto ABSCHNITT if /^\[/;  # Nächster Subsystem-Abschnitt
    /(\S+)\s+(\S+)\s*(\S*)/;
    ($art,$quelle,$ziel) = ($1,$2,($3 ne '' ? $3 : $2));
    # File-Art testen
    unless (defined $artkey{$art})
    {
      logdie "Die File-Art '$art' in '$file' Zeile $. ist ungültig!\n";
    }

    # Templates
    if ($artkey{$art} =~ /template/)
    {
      # Kein absoluter Pfad, dann ist das File im Unterverzeichnis filedir.sc/
      unless ($quelle =~ /^\//)
      {
        $quelle = $sysconfroot.$slash.$bs.$slash.'filedir.sc'.$slash.$quelle;
      }
      # Kein absoluter Pfad beim Ziel, also "/" ergänzen
      $ziel = $slash.$ziel unless ($ziel =~ /^\//);
      # Quellfile lesbar?
      unless (-r $quelle)
      {
        warning "Kann File '$quelle' aus '$file' Zeile $. nicht lesen!\n";
        next;
      }
      $files->Set($bs, $subsys, $artkey{$art}, $quelle, $ziel);

      # Den Patternblock einlesen
      my $pattern = <$fh>;
      unless ($pattern =~ /^beginpattern$/)
      { logdie "'beginpattern' erwartet in '$file' in Zeile $.!\n" }
      my $ganzes_muster = '';
      while( defined ($pattern = <$fh>) )
      {
        last if $pattern =~ /^endpattern$/;
        if ($pattern =~ /^\[/)
        { logdie "Kein schließendes 'endpattern' in '$file' in Zeile $.!\n" }
        $ganzes_muster .= $pattern;
      }
      $templatepattern->Set($bs,$subsys,$quelle,$ziel,$ganzes_muster);
      next;
    }    

    # Prüfung für normale Files
    unless ($artkey{$art} =~ /link|shell/)
    {
      # Kein absoluter Pfad, dann ist das File im Unterverzeichnis filedir.sc/
      unless ($quelle =~ /^\//)
      {
        $quelle = $sysconfroot.$slash.$bs.$slash.'filedir.sc'.$slash.$quelle;
      }
      # Kein absoluter Pfad beim Ziel, also "/" ergänzen
      $ziel = $slash.$ziel unless ($ziel =~ /^\//);
      # Quellfile lesbar?
      unless (-r $quelle)
      {
        warning "Kann File '$quelle' aus '$file' Zeile $. nicht lesen!\n";
        next;
      }
      $files->Set($bs, $subsys, $artkey{$art}, $quelle, $ziel);
    }

    # Links prüfen
    if ($artkey{$art} =~ /link/)
    {
      # Link-Quelle ohne absoulten Pfad
      unless ($quelle =~ /^\//)
      {
        logdie "Link-Quelle nicht absolut angegeben in '$file' Zeile $.!\n";
      }
      # Link-Ziel ohne absoulten Pfad
      unless ($ziel =~ /^\//)
      {
        logdie "Link-Ziel nicht absolut angegeben in '$file' Zeile $.!\n";
      }
      $files->Set($bs, $subsys, $artkey{$art}, $quelle, $ziel);
    }

    # Shellkommandos
    if ($artkey{$art} =~ /shell/)
    {
      /(\S+)\s+(.*)/;
      ($art,$kommando) = ($1,$2);
      $files->Set($bs, $subsys, $artkey{$art}, $kommando, '');
    }

  }
  close $fh;
}


sub KompaktiereSubsystemListe
{
  ###
  ### Subsysteme "zusammenschnurren" lassen, z.B.:
  ###
  #   0 7 8 5 4 8 9 5 4 8 4
  #   wird zu
  #   0 7 8 5 4 9
  #
  # Parameter: Liste von Subsystemen
  # Return:    Liste von Subsystemen

  my @subsys = @_;
  my @result_deps = ();
  my %schon_enthalten = ();
  my $s;
  foreach $s (@subsys)
  {
    next if $schon_enthalten{$s};
    push @result_deps, $s;
    $schon_enthalten{$s} = $TRUE;
  }
  return @result_deps;
}


sub CheckSubsystems
{
  # Überprüfung der Subsystem-Angaben
  # - alle Subsysteme? (ALL)
  # - darf dieser Rechner diese Subsysteme bekommen?
  # - dann stehen in @subsys die gewünschten Subsysteme
  #
  # Parameter: Rechnername, Beschreibungen-Objekt, Parameter-Liste-Objekt
  # Return:    Liste der Subsysteme
  #
  
  my ($rechner,$beschreibung,$param) = @_;
  my @wanted  = $param->Subsysteme;
  my @erlaubt = $beschreibung->Subsysteme($rechner);
  debug "-----\n";
  debug "Rechner:              $rechner\n";
  debug "Subsysteme erlaubt:   ",join(" ",@erlaubt),"\n";
  debug "Subsysteme gewünscht: ",join(" ",@wanted),"\n";

  # Alle Subsysteme
  if ( ($#wanted == 0) && ($wanted[0] eq 'ALL') )
  {
    return @erlaubt;
  }

  my @result = ();
  my %ist_erlaubt = ();
  foreach (@erlaubt) { $ist_erlaubt{$_} = $TRUE; }

  my $subsys;
  foreach $subsys (@wanted)
  {
    if ($ist_erlaubt{$subsys})
    {
      push @result, $subsys;
    }
    else
    {
      # Wenn es eine Klassenbezeichnung ist, dann expandieren
      if (defined $klassendef{$subsys})
      {
        foreach (@{$klassendef{$subsys}})
        {
          # Aber nur, wenn es erlaubt ist
          if ($ist_erlaubt{$_}) { push @result, $_; }
          else { warning "Subsystem '$_' aus Klasse '$subsys' ist für ",
                 "'$rechner' nicht erlaubt!\n"; }
        }
      }
      else
      {      
        warning "Subsystem '$subsys' ist für '$rechner' nicht erlaubt!\n";
      }
    }
  }

  return @result;
}


sub ReadConfigFile
{
  # Setzen von globalen Parametern aus dem Konfigurationsfile
  # Parameter: -
  # Return:    -
  #
  my $file = './sysconfrc';
  $file = "$ENV{HOME}/.sysconfrc" unless -r $file;
  $file = '/etc/sysconfrc' unless -r $file;
  logdie "Kann weder './sysconfrc' noch '$ENV{HOME}/.sysconfrc' noch ".
  "'/etc/sysconfrc' lesen!\n" unless -r $file;
  debug "Verwende Konfigurationsfile '$file'\n";
 
  TesteDateiBesitzer($file);

  my $fh = FileHandle->new();
  open($fh, $file);
  while(<$fh>)
  {
    next if /^\#/; # Kommentare überspringen
    $sysconfroot = $1    if /^SYSCONF_ROOT\s*=\s*(.+)/i;
    $UseSyscheck = $TRUE if /^USE_SYSCHECK\s*=\s*TRUE/i;
    $htmldir     = $1    if /^HTMLDIR\s*=\s*(.+)/i;
    next;
  }
  close $fh;
  logdie "Kein 'SYSCONF_ROOT' in '$file' definiert!\n" if $sysconfroot eq '';
  logdie "Kein 'HTMLDIR' in '$file' definiert!\n"      if $htmldir     eq '';
  $sysconfroot = KillSlashAtEnd($sysconfroot);
  $htmldir     = KillSlashAtEnd($htmldir    );
}


sub RechnerKlassenEinlesen
{
  # Es wird die Datei "classes.sc" eingelesen
  # Parameter: -
  # Return:    Hash: Klassenname -> Liste der Subsysteme

  my $Klassen_File = FileHandle->new();

  open($Klassen_File, "$sysconfroot${slash}classes.sc") || 
  logdie "Kann $sysconfroot${slash}classes.sc nicht öffnen!\n";

  TesteDateiBesitzer("$sysconfroot${slash}classes.sc");

  my %result = ();
  my ($klassendef,$subsysteme);
  {
    # Blockweise einlesen (Leerzeilen trennen)
    local $/ = '';
    while (<$Klassen_File>)
    {
      # Kommentare entfernen (alle Zeilen, die mit '#' beginnen.)
      s/^(\#[^\n]*\n)*//g;
      # Leerzeilen überspringen
      s/^\s*\n//g;
      next if $_ eq '';
      
      unless (
              m/
              CLASSDEF    \s+(\S+)\s*.*?
              SUBSYSTEMS  \s+([^\n]+)\s*.*?
              /sx
             )
      {
        logdie "Fehler in Klassendefinition:\n",$_,"\n";      
      }
      ($klassendef,$subsysteme) = ($1,$2);
      $result{$klassendef} = [ split(/\s+/,$subsysteme) ];
    }
    close $Klassen_File;
  }
  return %result;
}


sub RechnerBeschreibungenEinlesen
{
  # Es wird die Datei "hosts.sc" eingelesen
  # Parameter: -
  # Return:    RechnerBeschreibung-Objekt

  my $beschreibungen = RechnerBeschreibung::new();
  my $Besch_File = FileHandle->new();

  open($Besch_File, "$sysconfroot${slash}hosts.sc") || 
  logdie "Kann $sysconfroot${slash}hosts.sc nicht öffnen!\n";

  TesteDateiBesitzer("$sysconfroot${slash}hosts.sc");

  $/ = ''; # Blockweise einlesen (Leerzeilen trennen)
  while (<$Besch_File>)
  {
    # Kommentare entfernen (alle Zeilen, die mit '#' beginnen.)
     s/^(\#[^\n]*\n)*//g;
    # Leerzeilen überspringen
    s/^\s*\n//g;
    next if $_ eq '';

    unless(
           m/
           HOST        \s+(\S+)\s*.*?
           OS          \s+(\S+)\s*.*?
           CLASSES     \s+([^\n]*)\s*.*?
           SUBSYSTEMS  \s+([^\n]*)\s*.*?
           /sx
          )
    {
      logdie "Fehler in Rechnerdefinition:\n",$_,"\n";      
    }
    my ($rechner,$betriebssystem,$klassen,$subsysteme) = ($1,$2,$3,$4);
    $klassen    = '' unless defined $klassen;
    $subsysteme = '' unless defined $subsysteme;

    my @subsys = split(/\s+/,$subsysteme);
    # Klassen expandieren
    my $klasse;
    foreach $klasse (split(/\s+/,$klassen))
    {
      unless (defined $klassendef{$klasse})
      { logdie "Klasse '$klasse' gibt es nicht!\n" }
      push @subsys, @{$klassendef{$klasse}};
    }
    
    $beschreibungen->SetBetriebssystem($rechner,$betriebssystem);
    $beschreibungen->SetSubsysteme    ($rechner,@subsys);
  }
  close $Besch_File;
  $/ = "\n";
  return $beschreibungen;
}


sub ParameterEinlesen
{
  # Parameter: Kommandozeile des Programms als Liste
  # Return:    ParameterListe-Objekt

  my ($aktion, $subsysteme, $rechner) = @_;
  logdie "Falsche Anzahl Parameter!\n" unless defined $rechner;
  unless ($aktion =~ /^init$|^update$|^remove$|^start$|^stop$|^documentation$/)
  { logdie "Ungültige Aktion '$aktion'!\n" }

  my @subsysteme = split(/,/,$subsysteme);
  my @rechner    = split(/,/,$rechner);

  @rechner = ExpandiereALLRechner() if $rechner[0] eq 'ALL';

  my $result = ParameterListe::new();
  $result->SetAktion($aktion);
  $result->SetSubsysteme(@subsysteme);
  $result->SetRechner(@rechner);
  return $result;
}


sub ExpandiereALLRechner
{
  # Expandiert "ALL" zu einer Lister aller Rechner
  # Parameter: -
  # Return;    -
  #
  my @rechner = ();
  my $fh = FileHandle->new();
  open($fh, "$sysconfroot${slash}hosts.sc") || 
  logdie "Kann $sysconfroot${slash}hosts.sc nicht öffnen!\n";
  TesteDateiBesitzer("$sysconfroot${slash}hosts.sc");
  while(<$fh>)
  {
    next unless /HOST\s+(\S+)\s*.*?/;
    push @rechner, $1;
  }
  close $fh;
  return @rechner;
}


sub TesteRemoteShell
{
  # Parameter: Rechnername, Debug
  # Wenn "Debug"==TRUE, dann werden Debuginformationen ausgegeben.
  # Return: 'ssh', 'rsh', 'ssh-passwd', 'none'
  # Es wird festgestellt, welche Verbindungsmöglichkeit zu einem Rechner
  # besteht. Dazu dieses der Reihe nach versucht:
  # - ssh
  # - rsh
  # - ssh mit Paßwortabfrage
  #
  my $host = shift || die "TesteVerbindung() ohne Parameter aufgrufen!\n";
  my $debug = shift || $FALSE;
  my $null = '2>/dev/null >/dev/null';

  # ssh
  print "Teste ssh...\n" if $debug;
  system("ssh -o 'FallBackToRsh no' -o 'BatchMode yes' $host echo TEST $null");
  if ( ($?>>8) == 0 )
  {
    return 'ssh';
  }

  # rsh
  print "Teste rsh...\n" if $debug;
  system("rsh $host echo TEST $null");
  if ( ($?>>8) == 0 )
  {
    return 'rsh';
  }

  # ssh mit Paßwortabfrage
  print "Teste ssh mit Paßwortabfrage...\n" if $debug;
  system("ssh -o 'FallBackToRsh no' $host echo TEST");
  if ( ($?>>8) == 0 )
  {
    return 'ssh-passwd';
  }

  return 'none';
}


sub TesteRemoteCopy
{
  # Parameter: Rechnername, Debug
  # Wenn "Debug"==TRUE, dann werden Debuginformationen ausgegeben.
  # Return: 'scp ...', 'rcp', 'rsync ...', 'none'
  # (Bei scp und rsync wird gleich eine passende Kommandozeile gebildet.)
  # Es wird festgestellt, welche Kopiermöglichkeit zu einem Rechner
  # besteht. Dazu dieses der Reihe nach versucht:
  # - rsync mit ssh
  # - rsync mit rsh
  # - scp
  # - rcp
  #
  $host = shift || die "TesteVerbindung() ohne Parameter aufgrufen!\n";
  my $debug = shift || $FALSE;
  my $null = '2>/dev/null >/dev/null';

  my $verzeichnisse = ' $HOME/bin/rsync /usr/local/bin/rsync /usr/bin/rsync '.
      '/bin/rsync /usr/local/sbin/rsync /usr/sbin/rsync /root/bin/rsync ';

  # Gibt es lokalen rsync?
  if (which('rsync'))
  {
    # Funktioniert die ssh?
    if ( TesteRemoteShell($host) =~ /^ssh/ )
    {
      # rsync mit ssh
      print "Teste rsync mit ssh...\n" if $debug;
      system("rsync -z -e ssh --dry-run $0 $host:/tmp $null");
      if ( ($?>>8) == 0 )
      {
        return 'rsync -z -e ssh';
      }
      
      # Erfolglos, also erst einmal herausfinden, wo der rsync sich auf dem
      # remote-Rechner befindet
      print "Versuche rsync zu finden...\n" if $debug;
      my @list = `ssh $host 'ls $verzeichnisse 2>/dev/null'`;
      print @list if $debug;
      if (defined $list[0])
      {
        my $pfad = $list[0];
        chomp $pfad;
        # rsync mit ssh und Pfad
        print "Teste rsync mit ssh und Pfad '$pfad'...\n" if $debug;
        system("rsync -z -e ssh --rsync-path=$pfad --dry-run $0 ".
               "$host:/tmp $null");
        if ( ($?>>8) == 0 )
        {
          return "rsync -z -e ssh --rsync-path=$pfad";
        }
      }
    }
    else
    {
      # rsync mit rsh
      print "Teste rsync mit rsh...\n" if $debug;
      system("rsync -z --dry-run $0 $host:/tmp $null");
      if ( ($?>>8) == 0 )
      {
        return 'rsync -z';
      }
      
      # Erfolglos, also erst einmal herausfinden, wo der rsync sich auf dem
      # remote-Rechner befindet
      print "Versuche rsync zu finden...\n" if $debug;
      my @list = `rsh $host 'ls $verzeichnisse 2>/dev/null'`;
      print @list if $debug;
      if (defined $list[0])
      {
        my $pfad = $list[0];
        chomp $pfad;
        # rsync mit rsh und Pfad
        print "Teste rsync mit rsh und Pfad '$pfad'...\n" if $debug;
        system("rsync -z --rsync-path=$pfad --dry-run $0 $host:/tmp $null");
        if ( ($?>>8) == 0 )
        {
          return "rsync -z --rsync-path=$pfad";
        }
      }
    }
  }
  else
  {
    # Ohne rsync
    print "Kann lokal keinen rsync finden!\n" if $debug;
    # Funktioniert die ssh?
    print "Teste scp ...\n" if $debug;
    if ( TesteRemoteShell($host) =~ /^ssh/ )
    {
      return "scp -C -o 'CompressionLevel 9'";
    }
    print "Teste rcp ...\n" if $debug;
    if ( TesteRemoteShell($host) eq 'rsh' )
    {
      return 'rcp';
    }
  }
  return 'none';
}


######################################################################
### Debug, Logging, Exit, ...
######################################################################


sub myexit
{
  # Diese Funktion macht einen normalen exit() mit dem übergebenen
  # Exitcode
  # und erledigt vorher noch Aufräum-Arbeiten, wie LOG-file schließen, ...
  my $error = defined $_[0] ? shift : $NormalExitCode;
  logprint("$appname PID $$ Ende um ",date,"\n");
  close(LOG);
  exit $error;
}


sub logprint
{
  # Schreibt einen Text ins LOG-file
  print LOG @_;
}


sub debug_rsh
{
  # Es werden Debug-Informationen über RSH-Aufrufe erzeugt
  logprint("RSH: ",@_) if ($logLevel >= 4);
}


sub debug
{
  # Es werden Debug-Informationen erzeugt
  logprint("DEBUG: ",@_) if ($logLevel >= 3);
}


sub warning
{
  # Falls Warnings eingeschaltet sind, dann werden Informationen erzeugt
  logprint("WARN:  ",@_) if ($logLevel >= 2);
}


sub error
{
  # Falls Warnings eingeschaltet sind, dann werden Informationen erzeugt
  logprint("ERROR: ",@_) if ($logLevel >= 1);
}


sub loginit
{
  # Schreibt einen kleinen Header ins LOG-file
  my $login = (getpwuid($<))[0] || 'unknown';
  logprint("\n---\n\n$appname $version PID $$ mit Perl $]\nStart um ",date,
           " durch ",$login,"\n");
}


sub logdie
{
  # Schreibt einen Text ins LOG-file und stirbt dann
  print "FATAL: ",@_;
  logprint("FATAL: ",@_);
  myexit($ErrorExitCode);
}


sub catch_signal 
{
  my $signame = shift;
  logdie "Ende von $appname wegen Signal SIG$signame.\n";
}


sub catch_warning
{
  # Abfangen von Laufzeit-Warnungen
  my $warnung = shift;
  warning "INTERNAL: $warnung     (Interne Warnungen deuten auf einen ",
          "möglichen internen Programm-Fehler\n",
          "     hin oder auf eine fehlerhafte Eingabe, die nicht abgefangen ",
          "wurde!)\n";
  print $warnung if ($logLevel < 2);
}


######################################################################
### Kopf und Hilfe
######################################################################


sub Kopf
{
  my $head = "$appname $version   -   von Stephan Löscher";
  return "\n$head\n" . '~' x length($head) . "\n";
}


sub Hilfe
{
  printumlautepaged
  Kopf().
"Syntax: sysconf optionen action subsystem machine

Eine Aktion ist in natürlicher Sprache formuliert von dem Aufbau:
'Führe Aktion X mit Subsysteme Y auf Rechner Z aus.'
Aktionen werden per Kommandozeile übergeben, wobei:

action    := init | update | remove | start | stop | documentation
subsystem := <subsystem-name>{,<subsystem-name>}* | ALL
machine   := <machine-name>{,<machine-name>}*     | ALL
'ALL' steht dabei für 'alle Subsysteme' oder 'alle Rechner'.

Erklärungen:
init:   Subsystem erstmalig installieren (alle Files einschließlich
        'initfiles')
update: Subsystem updaten (Files aus 'initfiles' nicht kopieren!)
        Wenn es das Subsystem nicht gibt, dann 'init' vorher durchführen
remove: Subsystem entfernen.
start:  Subsystem starten.
stop:   Subsystem stoppen.
documentation : Dokumentation erzeugen

Optionen:
-wX: mit X=0-4 gibt den LOG-Level an.
     0: Nur fatale Fehler
     1: zusätzlich alle Fehler (Genaue Fehlerbeschreibungen)
     2: zusätzlich alle Warnungen (ganz informativ)
     3: zusätzlich alle Debug-Informationen (ausführlicher Status, etc.)
     4: Alle Remote-Shell-Aufrufe werden mitprotokolliert

Beispiele:
'Führe einen Update von Sendmail auf tgx045 durch':
sysconf update sendmail tgx045
'Führe einen Update von allen Subsystemen auf tgx002 durch':
sysconf update ALL tgx002
'Verteile alle Subsysteme auf alle Rechner':
sysconf init ALL ALL
'Führe einen Update von Sendmail und Syslog auf tgx045,tgx200,tgx200 durch':
sysconf update sendmail,syslog tgx045,tgx200,tgx200

";
  logprint "Es wird nur Hilfe ausgegeben.\n";
  myexit;
}


sub POD_Ausgabe
{
  # Erstellt Dokumentation aus POD im aktuellen Verzeichnis
  # Parameter: "man" oder "html" oder "latex"
  #
  $art = shift;
  if ($art eq 'man')
  {
    which('pod2man') || die "Leider kein 'pod2man' verfügbar!\n";
    which('nroff') || die "Leider kein 'nroff' verfügbar!\n";
    system("pod2man $0 | nroff -man > \L$appname\E.man");
  }
  if ($art eq 'html')
  {
    which('pod2html') || die "Leider kein 'pod2html' verfügbar!\n";
    system("pod2html $0 > \L$appname\E.html");
    # Nachbesserung: (FIXME)
    system('perl -i -pe \'s/&lt;(.?)EM&gt;/<${1}EM>/g\' '."\L$appname\E.html");
  }
  if ($art eq 'latex')
  {
    which('pod2latex') || die "Leider kein 'pod2latex' verfügbar!\n";
    system("pod2latex \L$appname\E");
    open(FH,"\L$appname\E.tex");
    @tex = <FH>;
    close FH;
    unshift @tex, '\documentclass[9pt]{article}\usepackage{german,a4,t1enc}'.
    '\usepackage[latin1]{inputenc}\begin{document}\def\C++{{\rm C'.
    '\kern-.05em\raise.3ex\hbox{\footnotesize ++}}}\def\underscore'.
    '{\leavevmode\kern.04em\vbox{\hrule width 0.4em height 0.3pt}}'.
    '\setlength{\parindent}{0pt}';
    push @tex, '\end{document}';
    grep(s/\"/\'\'/g, @tex); # Anführungszeichen ersetzen
    open(FH,">\L$appname\E.tex");
    print FH @tex;
    close FH;
  }
}


######################################################################
### ParameterListe-Objekt
######################################################################

package ParameterListe;

sub new
{
  my $daten = {
               aktion      => '',
               subssysteme => [],
               rechner     => [],
              };
  bless $daten, 'ParameterListe';
  return $daten;
}

sub Aktion
{
  my $objekt = shift;
  return $objekt->{aktion};
}

sub SetAktion
{
  my $objekt = shift;
  $objekt->{aktion} = shift;
}

sub Subsysteme
{
  my $objekt = shift;
  return @{$objekt->{subsysteme}};
}

sub SetSubsysteme
{
  my $objekt = shift;
  $objekt->{subsysteme} = [ @_ ];
}

sub Rechner
{
  my $objekt = shift;
  return @{$objekt->{rechner}};
}

sub SetRechner
{
  my $objekt = shift;
  $objekt->{rechner} = [ @_ ];
}


######################################################################
### RechnerBeschreibung-Objekt
######################################################################

package RechnerBeschreibung;

use Carp;

sub new
{
  my $daten = {
# Das sieht so aus:
#              rechnername => {
#                              BS  => Betriebsystem,
#                              SUB => Liste von Subsystemen,
#                              RSH  => Remoteshell,
#                              RCP  => Remotecopy,
#                             }
              };
  bless $daten, 'RechnerBeschreibung';
  return $daten;
}

sub Betriebssystem
{
  my $objekt  = shift;
  my $rechner = shift;
  unless (defined $rechner)
  {
    carp("RechnerBeschreibung::Betriebssystem() ohne Parameter ".
    "aufgerufen!\n");
    main::myexit($main::ErrorExitCode);
  }
  return $objekt->{$rechner}->{BS};
}

sub SetBetriebssystem
{
  my $objekt  = shift;
  my $rechner = shift;
  unless (defined $rechner)
  {
    carp("RechnerBeschreibung::SetBetriebssystem() ohne Parameter ".
    "aufgerufen!\n");
    main::myexit($main::ErrorExitCode);
  }
  $objekt->{$rechner}->{BS} = shift;
}

sub Subsysteme
{
  my $objekt  = shift;
  my $rechner = shift;
  # Ohne Parameter aufgerufen?
  unless (defined $rechner)
  {
    carp("RechnerBeschreibung::Subsysteme() ohne Parameter ".
    "aufgerufen!\n");
    main::myexit($main::ErrorExitCode);
  }
  return @{$objekt->{$rechner}->{SUB}};
}

sub SetSubsysteme
{
  my $objekt  = shift;
  my $rechner = shift;
  unless (defined $rechner)
  {
    carp("RechnerBeschreibung::SetSubsysteme() ohne Parameter ".
    "aufgerufen!\n");
    main::myexit($main::ErrorExitCode);
  }
  $objekt->{$rechner}->{SUB} = [ @_ ];
}

sub GetRSH
{
  my $objekt  = shift;
  my $rechner = shift;
  my $temp;
  unless (defined $rechner)
  {
    carp("RechnerBeschreibung::GetRSH() ohne Parameter aufgerufen!\n");
    main::myexit($main::ErrorExitCode);
  }
  # Wenn die RSH schon bekannt ist, dann gleich zurückgeben
  if (defined $objekt->{$rechner}->{RSH})
  {
    main::debug "Cached RSH: ".$objekt->{$rechner}->{RSH}."\n";
    return $objekt->{$rechner}->{RSH};
  }
  # ansonsten erst ermitteln
  else
  {
    $temp = main::TesteRemoteShell($rechner);
    unless ($temp =~ /^ssh$|^rsh$/)
    {
      carp("RechnerBeschreibung::GetRSH(): Kann weder mit rsh noch mit ssh ".
           "auf den Rechner '$rechner' ohne Paßwort zugreifen!\n");
      main::myexit($main::ErrorExitCode);
    }
    main::debug "Ermittelte RSH: ".$temp."\n";
    $objekt->{$rechner}->{RSH} = $temp;
    return $temp;
  }
}

sub GetRCP
{
  my $objekt  = shift;
  my $rechner = shift;
  my $temp;
  unless (defined $rechner)
  {
    carp("RechnerBeschreibung::GetRCP() ohne Parameter aufgerufen!\n");
    main::myexit($main::ErrorExitCode);
  }
  # Wenn RCP schon bekannt ist, dann gleich zurückgeben
  if (defined $objekt->{$rechner}->{RCP})
  {
    main::debug "Cached RCP: ".$objekt->{$rechner}->{RCP}."\n";
    return $objekt->{$rechner}->{RCP};
  }
  # ansonsten erst ermitteln
  else
  {
    $temp = main::TesteRemoteCopy($rechner);
    if ($temp eq 'none')
    {
      carp("RechnerBeschreibung::GetRCP(): Kann weder mit rcp, scp ".
           "noch rsync auf den Rechner '$rechner' ohne Paßwort zugreifen!\n");
      main::myexit($main::ErrorExitCode);
    }
    main::debug "Ermitteltes RCP: ".$temp."\n";
    $objekt->{$rechner}->{RCP} = $temp;
    return $temp;
  }
}


######################################################################
### Dependencies-Objekt
######################################################################

package Dependencies;

use Carp;

sub new
{
  my $daten = {
# Das sieht so aus:
#              betriebssystem => {
#                                  SUB => Liste von Subsystemen,
#                                }
              };
  bless $daten, 'Dependencies';
  return $daten;
}

sub Get
{
  my $objekt         = shift;
  my $betriebssystem = shift;
  unless (defined $betriebssystem)
  {
    carp("Dependencies::Get() ohne Parameter aufgerufen!\n");
    main::myexit($main::ErrorExitCode);
  }
  # Das return ist nur so umständlich, weil Perl sonst den undef-Wert
  # kritisiert. Kurz: return %{$objekt->{$betriebssystem}};
  return (
          defined %{$objekt->{$betriebssystem}} 
          ? %{$objekt->{$betriebssystem}}
          : undef
         );
}

sub Set
{
  my $objekt         = shift;
  my $betriebssystem = shift;
  my %hash = @_;

  unless (defined $betriebssystem)
  {
    carp("Dependencies::Set() ohne Parameter aufgerufen!\n");
    main::myexit($main::ErrorExitCode);
  }
  $objekt->{$betriebssystem} = { %hash };
}


######################################################################
### Files-Objekt
######################################################################

package Files;

use Carp;

sub new
{
  my $daten = {
# Das sieht so aus:
#     betriebssystem => {
#                         SUB => {
#                                  install      => Hash der Installfiles
#                                  init         => Hash der Initfiles
#                                  inittemplate => Hash der Init-Templates
#                                  file         => Hash der Files
#                                  template     => Hash der Templates
#                                }
#                       }
              };
# in den Hashes sind Listen der Zielfiles!
  bless $daten, 'Files';
  return $daten;
}

sub BereitsEingelesen
{
  # Parameter: Betriebssystem
  # Return: TRUE oder FALSE, je nachdem, ob für dieses Betriebssystem schon
  # die Datei files.sc eingelesen wurde
  #
  my ($objekt, $betriebssystem) = @_;
  unless (defined $betriebssystem)
  {
    carp("Files::BereitsEingelesen() ohne Parameter aufgerufen!\n");
    main::myexit($main::ErrorExitCode);
  }
  return (
          defined %{$objekt->{$betriebssystem}} 
          ? $main::TRUE
          : $main::FALSE
         );
}

sub Get
{
  # Parameter: (Betriebssystem, Subsystem, Art der Liste)
  # Die Art der Liste ist "install", "init", inittemplate", "file" oder
  # "template".
  # Return: Referenz auf Hash
  # Der Hash hat diesen Aufbau:
  # quellfile -> zielfile
  # Beispiel:
  # "var/lib/news/expire.ctl.INN" -> "var/lib/news/expire.ctl"
  #
  # Typischer Zugriff:
  #   my %x = %{$files->Get($bs,"login","install")};
  #   foreach (keys %x)
  #   { print "Eintrag: $_ -> $x{$_}\n"; }

  my ($objekt, $betriebssystem, $subsystem, $art) = @_;
  unless (defined $art)
  {
    carp("Files::Get() mit zuwenig Parameter aufgerufen!\n");
    main::myexit($main::ErrorExitCode);
  }
  unless ( IstArtGueltig($objekt,$art) )
  {
    carp("Files::Get() mit falschem Art-Parameter aufgerufen!\n");
    main::myexit($main::ErrorExitCode);
  }
  return ( \%{$objekt->{$betriebssystem}->{$subsystem}->{$art}} );
}

sub Set
{
  # Parameter: (Betriebssystem, Subsystem, Art des Files, Quellfile, Zielfile)
  # Die Art des Files ist "install", "init", inittemplate", "file" oder
  # "template".
  # Return: -
  #
  my ($objekt, $betriebssystem, $subsystem, $art, $quelle, $ziel) = @_;
  unless (defined $ziel)
  {
    carp("Files::Set() mit zuwenig Parameter aufgerufen!\n");
    main::myexit($main::ErrorExitCode);
  }
  unless ( IstArtGueltig($objekt,$art) )
  {
    carp("Files::Set() mit falschem Art-Parameter aufgerufen!\n");
    main::myexit($main::ErrorExitCode);
  }
  # Der Zielfile-Liste hinzufügen
  push @{$objekt->{$betriebssystem}->{$subsystem}->{$art}->{$quelle}}, $ziel;
}

sub IstArtGueltig
{
  shift;
  my $art = shift;
  return ( $art =~ /^install$|^init$|^inittemplate$|^file$|^template$|
           ^link$|^installlink$|^initlink$|^shell$|^installshell$|
           ^initshell$/sx );
}


######################################################################
### TemplatePattern-Objekt
######################################################################

package TemplatePattern;

use Carp;

sub new
{
  my $daten = {
# Das sieht so aus:
#     betriebssystem => {
#                         SUB => {
#                                  quell => {
#                                              ziel => pattern
#                                           }
#                                }
#                       }
              };
  bless $daten, 'TemplatePattern';
  return $daten;
}

sub Get
{
  # Parameter: (Betriebssystem, Subsystem, Quellfile, Zielfile)
  # Return:    Pattern

  my ($objekt, $betriebssystem, $subsystem, $quelle, $ziel) = @_;
  unless (defined $ziel)
  {
    carp("TemplatePattern::Get() mit zuwenig Parameter aufgerufen!\n");
    main::myexit($main::ErrorExitCode);
  }
  unless (defined $objekt->{$betriebssystem}->{$subsystem}->{$quelle}->{$ziel})
  {
    carp("TemplatePattern::Get() liefert undef()! ".
         "Aufruf war: '$betriebssystem','$subsystem','$quelle','$ziel'\n");
    main::myexit($main::ErrorExitCode);
  }
  return ( $objekt->{$betriebssystem}->{$subsystem}->{$quelle}->{$ziel} );
}

sub Set
{
  # Parameter: (Betriebssystem, Subsystem, Quellfile, Zielfile, Pattern)
  # Return: -
  #
  my ($objekt, $betriebssystem, $subsystem, $quelle, $ziel, $pattern) = @_;
  unless (defined $pattern)
  {
    carp("TemplatePattern::Set() mit zuwenig Parameter aufgerufen!\n");
    main::myexit($main::ErrorExitCode);
  }
  if ( defined $objekt->{$betriebssystem}->{$subsystem}->{$quelle}->{$ziel} )
  {
    carp("TemplatePattern::Set(): Doppelter Eintrag! ".
         "Aufruf war: '$betriebssystem','$subsystem','$quelle','$ziel'\n");
    main::myexit($main::ErrorExitCode);
  }
  $objekt->{$betriebssystem}->{$subsystem}->{$quelle}->{$ziel} = $pattern;
}


######################################################################
### Subsystem-Objekt
######################################################################

package SubsystemObject;

# Alle Kommandos für Subsysteme werden in diesem Objekt als Methoden
# implementiert, z.B.:
# testinstallcmd ist Obj->IsInstalled

use File::Copy;
use Carp;

sub new
{
  my ($object,$bs,$rechner,$subsystem) = @_;
  unless (defined $subsystem)
  {
    carp("SubsystemObject::new(Betriebssystem, Rechner, Bestriebssystem)".
         "mit zu wenig Parameter aufgerufen!\n");
    main::myexit($main::ErrorExitCode);
  }
  my $daten = {
               BS        => $bs,
               RECHNER   => $rechner,
               SUBSYSTEM => $subsystem
              };
  bless $daten, 'SubsystemObject';
  return $daten;
}

sub rsh
{
  # Kopieren des Kommandos auf die Zielmaschine, Ausführung und wieder Löschen
  #
  my ($rechner, $path_to_command, $command, $subsystem) = @_;
  my $kommandofile = "$path_to_command$main::slash$command";

  unless (-r $kommandofile)
  {
    main::logdie "Kann File '$kommandofile' nicht lesen!\n";
  }

  # Dummy-Kommandos: Wenn das Kommando die Filelänge Null hat, dann nicht
  # kopieren und ausführen!
  unless (-s "$path_to_command$main::slash$command")
  {
    print "(Dummy wird nicht nicht ausgeführt.)\n";
    return $main::TRUE;
  }

  unless (-x $kommandofile)
  {
    main::logdie "Kann File '$kommandofile' nicht ausführen!\n".
    "(Sind die Permissions richtig gesetzt?)\n";
  }

  my $rsh = $main::beschreibungen->GetRSH($rechner);
  my $rcp = $main::beschreibungen->GetRCP($rechner);

  # Zur besseren Lesbarkeit ein Beispiel:
  # rcp /var/sysconf/AIX-4.1.5/sendmail/testinstallcmd
  #     tgx987:/tmp/testinstallcmd.3648
  # rsh tgx987 /tmp/testinstallcmd.3648
  # rsh tgx987 rm /tmp/testinstallcmd.3648

  main::debug_rsh "$rcp $path_to_command$main::slash$command ".
  "$rechner:${main::slash}tmp${main::slash}$command.$$\n";
  main::debug_rsh "$rsh $rechner ".
  "${main::slash}tmp${main::slash}$command.$$\n";
  main::debug_rsh "$rsh $rechner rm ${main::slash}tmp".
  "${main::slash}$command.$$\n";

  system("$rcp $path_to_command$main::slash$command ".
         "$rechner:${main::slash}tmp${main::slash}$command.$$");
  main::logdie "RemoteCopy liefert Fehler!\n" if ($?>>8);
  # Rsh liefert nicht den Exitcode des Remote-Prozesses!
  # => Das Programm/Script muß den String "TRUE" zurückgeben
  my $ret = `$rsh $rechner ${main::slash}tmp${main::slash}$command.$$`;
  main::logdie "RemoteShell liefert Fehler!\n" if ($?>>8);
  system("$rsh $rechner rm ${main::slash}tmp${main::slash}$command.$$");
  main::logdie "RemoteShell liefert Fehler!\n" if ($?>>8);

  print ( ($ret =~ /TRUE/) ? "true\n" : "false\n");
  return ( ($ret =~ /TRUE/) ? 1 : 0 );
}


sub ExecuteCommand
{
  # Diese Funktion führt die geforderten Kommandos aus.
  # Das vereinfacht die anderen Funktionen.
  #
  my $objekt  = shift;
  my $command = shift;
  print "Kommando: $command... ";
  return rsh($objekt->{RECHNER},
             "$main::sysconfroot$main::slash$objekt->{BS}$main::slash".
             "$objekt->{SUBSYSTEM}",
             $command,
             $objekt->{SUBSYSTEM});
}


sub GetHTML
{
  # Es werden nicht die Kommandos ausgeführt, sondern eine HTML-Dokumentation
  # ausgegeben.
  #
  my $objekt  = shift;
  my $rechner = shift;
  my $htmldir = shift;
  my $command;
  my $ret = '';
  foreach $command ('testinstallcmd', 'installcmd', 'testruncmd', 'stopcmd',
                    'startcmd', 'removecmd', 'reconfigcmd')
  {
    my $cmdfile = "$main::sysconfroot$main::slash$objekt->{BS}$main::slash".
    "$objekt->{SUBSYSTEM}$main::slash$command";
    # Existiert das cmd-File?
    if (-r $cmdfile)
    {
      # Wenn das cmd-File nicht leer ist
      unless (-z $cmdfile)
      {
        # Wenn das cmd-File eine Textdatei ist
        if (-T $cmdfile)
        {
          $ret .= "    <a href=\"$rechner-sub-$objekt->{SUBSYSTEM}-".
          "$command.txt\">$command</a><br>\n";
          copy("$main::sysconfroot$main::slash$objekt->{BS}$main::slash".
               "$objekt->{SUBSYSTEM}$main::slash$command",
               "$htmldir$main::slash$rechner-sub-$objekt->{SUBSYSTEM}".
               "-$command.txt"
              );
        }
        # Wenn es eine Binär-Datei ist
        else
        {
          $ret .= "    $command (bin&auml;r)<br>\n";
        }
      }
      # Wenn das cmd-File leer ist, dann keinen Link erzeugen
      else
      {
        $ret .= "    $command (leer)<br>\n";
      }
    }
  }
  return $ret;
}

sub IsInstalled
{ return ExecuteCommand(shift,'testinstallcmd') }


sub Install
{ return ExecuteCommand(shift,'installcmd') }


sub IsRunning
{ return ExecuteCommand(shift,'testruncmd') }


sub Stop
{ return ExecuteCommand(shift,'stopcmd') }


sub Start
{ return ExecuteCommand(shift,'startcmd') }


sub Remove
{ return ExecuteCommand(shift,'removecmd') }

sub Reconfigure
{ return ExecuteCommand(shift,'reconfigcmd') }


######################################################################
### Modul zur Textersetzung in Files
######################################################################

package TextModify;

use Carp;

sub debug   { main::debug  (@_); }
sub warning { main::warning(@_); }


# Modul-Globale Variablen initialisieren
sub TextModifyInit
{
  # Quellfile ist globale Variable, da sie sonst immer übergeben werden muß
  @quelle = ();
  $TRUE  = 1;
  $FALSE = 0;
}


sub ErsetzeMuster
{
  # Parameter: Quellfile, Zielfile, Pattern, Referenz auf Variablen-Hash
  # Return:    True bei Erfolg, False bei Fehler

  TextModifyInit(); # Modul-Globale Variablen initialisieren

  my ($quellfile, $zielfile, $pattern, $refvar) = @_;
  croak "Quellfile '$quellfile' nicht lesbar!\n"     unless -r $quellfile;

  my $variablenCode = '';
  foreach (keys %$refvar)
  {
    my $refvarquote = $$refvar{$_}; $refvarquote =~ s/\'/\\\'/g;
    $variablenCode .= "my \$$_='$refvarquote';\n";
  }

  # Quellfile komplett einlesen
  my $quellFH = FileHandle->new();
  open($quellFH, $quellfile);
  @quelle = <$quellFH>;
  close $quellFH;

  @patternarray = split("\n",$pattern);

  # Durch alle Ersatz-Muster durchgehen
  my $zeile;
  my $expr;
  my $Veraenderungen = 0;
  while( defined($zeile = shift @patternarray) )
  {
    next if $zeile =~ /^\s*$/; # Leerzeilen ignorieren

    # CHANGE
    if ($zeile =~ /^\s*change\s+(s.*)/i)
    {
      $expr = $1;
      debug "CHANGE: '$expr'\n";
      next unless IstMusterGueltig($expr);
      foreach (@quelle)
      {
        # Ersetzungen mit Variablen durchführen
        {
          local $FehlerInVariablenErsetzungExpression = $expr;
          local $SIG{__WARN__} = \&FehlerInVariablenErsetzung;
          $Veraenderungen += eval $variablenCode.$expr;
        }
      }
      next;
    }

    # ADD FIRST
    if ($zeile =~ /^\s*add\s+first\s+(.*)/i)
    {
      $expr = $1;
      debug "ADD FIRST: '$expr'\n";
      # Es sollte eigentlich so funktionieren (Fehler in Perl?):
      # eval $variablenCode.'$expr =~ s/\$(\w+)/${$1}/g';
      # Variablenersetzung
      {
        local $FehlerInVariablenErsetzungExpression = $expr;
        local $SIG{__WARN__} = \&FehlerInVariablenErsetzung;
        eval $variablenCode.'$expr =~ s/(\$\w+)/$1/eeg;';
      }
      unshift @quelle, $expr, "\n";
      $Veraenderungen++;
      next;
    }

    # ADD LAST
    if ($zeile =~ /^\s*add\s+last\s+(.*)/i)
    {
      $expr = $1;
      debug "ADD LAST: '$expr'\n";
      # Variablenersetzung
      {
        local $FehlerInVariablenErsetzungExpression = $expr;
        local $SIG{__WARN__} = \&FehlerInVariablenErsetzung;
        eval $variablenCode.'$expr =~ s/(\$\w+)/$1/eeg;';
      }
      push @quelle, $expr, "\n";
      $Veraenderungen++;
      next;
    }

    # ADD match text
    if ($zeile =~ /^\s*add\s+(m.*)/i)
    {
      $expr = $1;
      debug "ADD match: '$expr'\n";
      chomp($neuertext = shift @patternarray);
      debug "ADD neuertext: '$neuertext'\n";
      # Variablenersetzung
      {
        local $FehlerInVariablenErsetzungExpression = $expr;
        local $SIG{__WARN__} = \&FehlerInVariablenErsetzung;
        eval $variablenCode.'$neuertext =~ s/(\$\w+)/$1/eeg;';
      }
      next unless IstMusterGueltig($expr);
      my $i;
      my @ziel = ();
      foreach (@quelle)
      {
        push @ziel, $_;
        next unless (eval $expr);
        # Neue Zeile einfügen
        push @ziel, $neuertext."\n";
        $Veraenderungen++;
      }
      @quelle = @ziel;
      next;
    }

    # "uniq" (Nur ersetzen, wenn es noch nicht vorkommt)
    # ADDUNIQ FIRST
    if ($zeile =~ /^\s*adduniq\s+first\s+(.*)/i)
    {
      $expr = $1;
      debug "ADDUNIQ FIRST: '$expr'\n";
      # Variablenersetzung
      {
        local $FehlerInVariablenErsetzungExpression = $expr;
        local $SIG{__WARN__} = \&FehlerInVariablenErsetzung;
        eval $variablenCode.'$expr =~ s/(\$\w+)/$1/eeg;';
      }
      next if grep(/$expr/, @quelle); # Wenn schon vorhanden, dann nichts tun
      unshift @quelle, $expr, "\n";
      $Veraenderungen++;
      next;
    }

    # ADDUNIQ LAST
    if ($zeile =~ /^adduniq\s+last\s+(.*)/i)
    {
      $expr = $1;
      debug "ADDUNIQ LAST: '$expr'\n";
      # Variablenersetzung
      {
        local $FehlerInVariablenErsetzungExpression = $expr;
        local $SIG{__WARN__} = \&FehlerInVariablenErsetzung;
        eval $variablenCode.'$expr =~ s/(\$\w+)/$1/eeg;';
      }
      next if grep(/$expr/, @quelle); # Wenn schon vorhanden, dann nichts tun
      push @quelle, $expr, "\n";
      $Veraenderungen++;
      next;
    }

    # ADDUNIQ match text
    if ($zeile =~ /^\s*adduniq\s+(m.*)/i)
    {
      $expr = $1;
      debug "ADDUNIQ match: '$expr'\n";
      chomp($neuertext = shift @patternarray);
      debug "ADDUNIQ neuertext: '$neuertext'\n";
      # Variablenersetzung
      {
        local $FehlerInVariablenErsetzungExpression = $expr;
        local $SIG{__WARN__} = \&FehlerInVariablenErsetzung;
        eval $variablenCode.'$neuertext =~ s/(\$\w+)/$1/eeg;';
      }
      next unless IstMusterGueltig($expr);
      # Wenn schon vorhanden, dann nichts tun
      next if grep(/$neuertext/, @quelle);
      my $i;
      my @ziel = ();
      foreach (@quelle)
      {
        push @ziel, $_;
        next unless (eval $expr);
        # Neue Zeile einfügen
        push @ziel, $neuertext."\n";
        $Veraenderungen++;
      }
      @quelle = @ziel;
      next;
    }
    chomp($zeile);
    warning "Ignoriere ungültigen Text:\n'$zeile'\n";
  }
  debug "Es wurden $Veraenderungen Textveränderungen durchgeführt\n";

  # Resultat schreiben
  my $zielFH = FileHandle->new();
  open($zielFH, ">$zielfile") || 
                          croak "Kann Zielfile '$zielfile' nicht schreiben!\n";
  print $zielFH @quelle;
  close $zielFH;

  debug "DIFF:\n".`diff $quellfile $zielfile`;
  return $TRUE;
}


sub IstMusterGueltig
{
  # Testen, ob der Code eine gültige Perl-Expression ist
  # Parameter: Ein Stück Perl-Code
  # Beispiel:  "s/bla/fasel/;"

  my $code = shift;
  {
    local $SIG{__WARN__} = sub {}; # IGNORE geht nicht!?
    eval $code;
    if ($@)
    {
      warning "Fehler in Perl-Expression: ", $@;
      return $FALSE;
    }
  }
  return $TRUE;
}


sub FehlerInVariablenErsetzung
{
  # Signal-Handler für Fehler in Variablenersetzungen
  # Wichtig: Die globale (local) Variable
  #          $FehlerInVariablenErsetzungExpression muß gesetzt sein!
  # Parameter: -
  # Return:    -
  my $fehler = shift;
  if ($fehler =~ /Use of uninitialized value at/)
  {
    warning "Fehler in der Variablenersetzung!\n";
    warning "Sie haben in folgender Zeile eine Variable ".
    "verwendet,\n";
    warning "die Sie aber nicht definiert haben:\n";
    warning "'$FehlerInVariablenErsetzungExpression'\n";
  }
  else
  {
    warning $fehler;
    warning "Nicht abgefangener Fehler in Variablenersetzung!\n";
  }
}


######################################################################
### POD-Dokumentation
######################################################################

__END__

=head1 NAME

sysconf - Systemkonfiguration

=head1 SYNOPSIS

 sysconf [options] action subsystem machine

=head1 DESCRIPTION

Eine Aktion ist in natürlicher Sprache formuliert von dem Aufbau:
'Führe Aktion X mit Subsysteme Y auf Rechner Z aus.'
Aktionen werden per Kommandozeile übergeben, wobei:

 action    := init | update | remove | start | stop | documentation
 subsystem := <subsystem-name>{,<subsystem-name>}* | ALL
 machine   := <machine-name>{,<machine-name>}*     | ALL
 'ALL' steht dabei für 'alle Subsysteme' oder 'alle Rechner'.

 init:   Subsystem erstmalig installieren (alle File einschließlich
         'initfiles')
 update: Subsystem updaten (Files aus 'initfiles' nicht kopieren!)
         Wenn es das Subsystem nicht gibt, dann 'init' vorher durchführen
 remove: Subsystem entfernen.
 start:  Subsystem starten.
 stop:   Subsystem stoppen.
 documentation : Dokumentation erzeugen

 Optionen:
 -wX: mit X=0-4 gibt den LOG-Level an.
      0: Nur fatale Fehler
      1: zusätzlich alle Fehler (Genaue Fehlerbeschreibungen)
      2: zusätzlich alle Warnungen (ganz informativ)
      3: zusätzlich alle Debug-Informationen (ausführlicher Status, etc.)
      4: Alle Remote-Shell-Aufrufe werden mitprotokolliert

 Beispiele:
 'Führe einen Update von Sendmail auf tgx045 durch':
 sysconf update sendmail tgx045
 'Führe einen Update von allen Subsystemen auf tgx002 durch':
 sysconf update ALL tgx002
 'Verteile alle Subsysteme auf alle Rechner':
 sysconf init ALL ALL
 'Führe einen Update von Sendmail und Syslog auf tgx045,tgx200,tgx200 durch':
 sysconf update sendmail,syslog tgx045,tgx200,tgx200

=head2 Hauptkonfigurationsdatei


In der Datei "/etc/sysconfrc" bzw. "./sysconfrc" bzw. "~/.sysconfrc" sind
die Standardeinstellungen gespeichert. Mit SYSCONF_ROOT wird das Verzeichnis
angegeben, in dem sich die Konfigurationsdatenbank befindet. Mit HTMLDIR wird
das Verzeichnis für die HTML-Dokumentation, die mit der Aktion 'documentation'
erstellt wird, festgelegt. USE_SYSCHECK kann die Werte TRUE und FALSE
annehmen und legt fest, ob bei einem Update auf dem Zielrechner Syscheck
gestartet werden soll. Beispiel:

 SYSCONF_ROOT=/var/adm/sysconf
 HTMLDIR=/http/htdocs/sysconf
 USE_SYSCHECK=TRUE

Es wird automatisch ermittelt, welche Kommandos zu Rechneransteuerung
verwendet werden sollen. Zur Auswahl stehen: rsh, ssh, rcp, scp und rsync.

=for html
Links zur erwähnten Software:<br>
<A HREF="http://samba.anu.edu.au/rsync/">rsync</A><br>
<A HREF="http://www.ssh.fi/">SSH</A><br>
<A HREF="http://www.uni-karlsruhe.de/~ig25/ssh-faq/">SSH-FAQ</A><br>


=head2 Konfigurationsdatenbank


Die Konfigurationsdatenbank enthält die Informationen welcher Rechner
welche Files erhält und ist im Dateisystem als Verzeichnis-Struktur
abgebildet:

 hosts.sc
 classes.sc
 variables/
     tgx045.var
     tgx200.var
     ...
 AIX-4.1.5/
    dependencies.sc            (Abhängigkeiten der Subsysteme)
    files.sc                   (Liste der Files pro Subsystem)
    filedir.sc/                (Hier liegen alle Konfigurationsfiles)
       etc/rc.config
       etc/hosts
       etc/issue
       etc/exports
       root/profile
       var/lib/news/active
       var/lib/news/newsgroups
       usr/lib/news/nnrp.access
       usr/lib/news/hosts.nntp
       usr/lib/news/inn.conf
       ...
    patches/                   ??? (noch offen/ungeklärt)
       ...
    syslog/
          installcmd*
          removecmd*
          testinstallcmd*
          startcmd*
          stopcmd*
          reconfigcmd*
          testruncmd*
          checkversion*
    nfsserver/
          installcmd*
          removecmd*
          testinstallcmd*
          startcmd*
          stopcmd*
          reconfigcmd*
          testruncmd*
          checkversion*
    newsserver/
    ...
    sendmail/
    ...


=head2 Dateien F<variables/xxx.var>


Beispielsweise enthält F<tgx045.var> Variablen speziell für den Rechner
"tgx045". Escapes ("\n") in Variablen werden nicht interpretiert! Aufbau:

 variable=wert

oder

 variable include FILENAME1 FILENAME2 ...

Im ersten Fall wird der Variable der Wert zugewiesen. Wenn kein Wert
angegeben ist, dann wird die Variable auf den Leerstring gesetzt.
Im zweiten Fall werden die angegebenen Dateien eingelesen und in der Inhalt
nacheinander in der Variable gespeichert. Diese Files werden im
Variablen-Verzeichnis gesucht.


=head2 Datei F<files.sc>


Beispiel:

 [boot] 
 f etc/rc
 t etc/inittab
  
 [login] 
 f etc/environment
 F etc/passwd
 t etc/motd
  
 [adsmc] 
 f etc/inittab
 f /etc/sendmail.cf
 
 [INN]
 f var/lib/news/expire.ctl.inn /var/lib/news/expire.ctl
 
 [CNEWS]
 f var/lib/news/expire.ctl.cnews /var/lib/news/expire.ctl
 
 [Compiler]
 L /usr/local/bin/rs6000-ibm-aix4.1.4.0-gcc /usr/local/bin/gcc

 [Modem]
 t etc/inittab
 beginpattern
 change s!#PORT!mo:123:respawn:/usr/local/sbin/vgetty $MODEMDEVICE!;
 adduniq m/^5:/
 6:123:respawn:/sbin/mingetty tty6
 endpattern

Die genaue Syntax lautet:

 [subsys]
 <fileentry>
 <patternblock>

 [subsys]:       Beginn Beschreibung Subsystem namens "subsys"
 <fileentry>:    <type> <filename> <zielfilename>
 <patternblock>: hat folgenden Aufbau:
 beginpattern
 <pattern>
 endpattern

Die möglichen <pattern> sind im Abschnitt L<Patternblock> erklärt.

Wenn der <type> kein Template-Typ ist, dann entfällt der <patternblock>

 <type>:
 inst  = I : Installfile   (Nur bei INIT übertragen, vor installcmd)
 initf = F : Initfile      (Nur bei INIT übertragen, nach installcmd)
 initt = T : Init-Template (Bei INIT übertragen und Ersetzungen
                            aus dem Patternblock durchführen)
 f         : File          (Bei UPDATE übertragen)
 t         : Template      (Bei UPDATE übertragen und Ersetzungen
                            aus dem Patternblock durchführen)
 instL     : Install-Link  (Nur bei INIT anlegen, vor installcmd)
 initL     : Init-Link     (Nur bei INIT anlegen, nach installcmd)
 L         : Link          (Bei UPDATE anlegen)
 instS     : Shellkommando (Nur bei INIT ausführen, vor installcmd)
 initS     : Shellkommando (Nur bei INIT ausführen, nach installcmd)
 S         : Shellkommando (Bei UPDATE ausführen)

 <filename>:     Kompletter Pfad des Files. Wenn das ein relativer
                 Pfad ist, dann liegt das File unterhalb von
                 files.sc/, sonst wird der absolute Pfad genommen.
 <zielfilename>: Optional. Kompletter Zielpfad+Name des Files.

Bei den Links wird wie auch bei "ln" üblich "Quelle Ziel" angegeben, es
entsteht also ein Link von "filename" auf "zielfilename".

Die Shellkommandos sind dazu gedacht, daß man Verzeichnisse, Gerätedateien,
Links, etc. anlegen oder entfernen kann. Als Shellkommandos ist alles
möglich, was auch per "rsh" möglich ist, z.B.:

 cd /home/user ; ln -sf ../skel/.profile .

In den Shellkommandos werden die Variablen aus Dateien F<variables/xxx.var>
expandiert.

Der spezielle Subsystemname "GLOBAL" bedeutet, daß diese Files nicht zu
einem bestimmten Subsystem gehören, sondern global sind.
Das sind also die Files und Konfigurationsdateien, die jeder (!) Rechner
mit diesem Betriebssystem erhalten soll.


=head2 Kommando-Files *cmd


Es müssen alle Kommando-Files vorhanden sein.
Wenn ein solches *cmd-Programm die Länge Null hat, dann wird es nicht
verwendet! (Man braucht beispielsweise nicht immer ein startcmd.)
Wenn Files für verschiedene Betriebssysteme identisch sind, dann kann man
einfach einen Link legen.
Alle Programme/Shellscripten müssen TRUE oder FALSE auf
STDOUT ausgeben. Diese Programme werden auf dem Zielrechner ausgeführt.
F<checkversion> überprüft die Versionsnummer des Subssystems und liefert
TRUE/FALSE (Möglicherweise nicht oder schwer realisierbar!)


=head2 Kommentare


In den Dateien

 dependencies.txt
 hosts.sc
 classes.sc

sind Kommentare möglich: Alle Zeilen, die mit '#' beginnen werden ignoriert.


=head2 Dateien F<hosts.sc> und F<classes.sc>


Die Datei F<hosts.sc> beschreibt die einzelnen Rechner:
In dieser Datei wird festgelegt welche Subsysteme die einzelnen Rechner
bekommen sollen. Beispiel:

 HOST         tgx987
 OS           AIX-4.1.5
 CLASSES      Basissystem Client
 SUBSYSTEMS   sendmail tcpwrapper nfsclient adsm
 
 HOST         tgx123
 OS           AIX-4.1.5
 CLASSES      Basissystem Server
 SUBSYSTEMS   newsserver nfsserver
 ...
 
Die Einträge hinter CLASSES und SUBSYSTEMS sind optional.

Dabei können auch Klassen in der Datei F<classes.sc> definiert werden:

 CLASSDEF    Basissystem
 SUBSYSTEMS  syslog tcpwrapper ssh

 CLASSDEF    Server
 SUBSYSTEMS  adsm audit nis
 ...

Dabei sollten die Subsysteme möglichst fein aufgefächert werden, also, z.B.:

 - NIS-Master, NIS-Slave, NIS-Client
 - sendmail f. s_mailout, sendmail f. s_mailbox oder sendmail f. Client
 - ADSM-Server, ADSM-Client
 - Newsserver, Newsclient


=head2 Patternblock

In der Datei F<files.sc> können im Patternblock für Templates
Ersetzungsregeln angegeben werden nach denen die Template-Files verändert
werden bevor sie kopiert werden. In dem Patternblock sind keine Kommentare
möglich! Leerzeilen werden ignoriert.
Die möglichen Syntax-Konstrukte lauten

 change <substitute>

 add FIRST <text>

 add LAST  <text>

 add <patternmatch>
 <text>
 
 adduniq FIRST <text>

 adduniq LAST <text>

 adduniq <patternmatch>
 <text>
 
 <substitute>:   Substitute-Kommando von Perl: s/.../.../
 <patternmatch>: Patternmatch-Kommando von Perl: m/.../.../
 <text>:         Text, der eingefügt werden soll.
 
Zu beachten: nach dem Patternmatch muß eine neue Zeile beginnen!

Die Variablen, die in F<variables/rechnername.var> definiert wurden
können mit vorangestelltem "$" verwendet werden. Escapes ("\n") in
Variablen werden nicht interpretiert, aber man kann diese Funktionsweise
nachbilden: Wenn man eine Variable mit Newlines hat, z.B. 

 VAR=Das\nsind\nviele\nZeilen\ngetrennt\ndurch\nNewlines\n

dann muß man selbst dafür sorgen, daß die Sonderzeichen interpretiert
werden:

 change s/suchtext/($VAR=~s!\\n!\n!g,$VAR)/e;

Beispiele:

 Zeilen verändern:
   change s/#MEINE_IP#/10.135.82.54/
   change s/#MEIN_NAME#/$RECHNER/
   change s/#ANWENDUNG#/MFK-Rechner Testbetrieb/
   change s/^"START_INN=.*/"START_INN=yes"/
   change s/SC_GESCHWINDIGKEIT/$MODEMSPEED/
 Zeilen löschen:
   change s/^netstat\s+stream\s+tcp//
 Zeilen einfügen:
 Vor der ersten Zeile einfügen:
   add FIRST 127.0.0.1       localhost
 Nach der letzten Zeile einfügen:
   add LAST 129.187.13.89 mailhost mail
 Nach der Zeile, die mit "OVERVIEW" beginnt einfügen:
   add m/^OVERVIEW/
   news/newsserver:*,!junk,!control*:Ap,Tf,Wnm:news
 Zeile nur hinzufügen, wenn sie im File noch nicht vorhanden ist:
   adduniq m/^6:123:respawn:/
   mo:123:respawn:/usr/local/sbin/vgetty modem
 

=head2 dependencies.sc

Die Datei "dependencies.sc" hat den Aufbau:

 S : D1 D2 D3 ...

Bedeutung: Subsystem "S" hängt von Subsystemen "D1", "D2", "D3", ... ab.
Beispiele:

 sendmail : syslog
 nfsserver : tcpwrapper portmap inetd


=head1 RETURN


Keine Rückgabewerte.


=head1 AUTHOR


=for text
Sysconf wurde geschrieben von Stephan Löscher,
http://www.leo.org/~loescher/,
loescher@gmx.de, 1998.

=for man
Sysconf wurde geschrieben von Stephan Löscher,
http://www.leo.org/~loescher/,
loescher@gmx.de, 1998.

=for latex
Sysconf wurde geschrieben von Stephan Löscher,
http://www.leo.org/~loescher/,
loescher@gmx.de, 1998.

=for html
Sysconf wurde geschrieben von
<A HREF="http://www.leo.org/~loescher/">Stephan L&ouml;scher</A>,
<A HREF="mailto:loescher@gmx.de">loescher@gmx.de</A>,
1998.

=cut

######################################################################
#
# Warranty and legal notice
# ~~~~~~~~~~~~~~~~~~~~~~~~~
#
# Copyright (c) 1998 by Stephan Löscher  -  all rights reserved
# My Address: Stephan Löscher, Dr.Troll-str. 3, 82194 Gröbenzell, Germany
# Email: loescher@gmx.de
# WWW: http://www.leo.org/~loescher/
#
# This program is freeware.
# It is NOT Public-Domain-Software!
# The author (Stephan Löscher) does NOT give up his copyright, but he 
# reserves his copyright. Usage and copying is free of charge for private
# use, but NOT for commercial use!
# 
# You may and should copy this program free of charge, use it,
# give it to your friends, upload it to a BBS or something similar, under
# the following conditions:
# * Don't charge any money for it. If you upload it to a BBS, make sure that
#    it can be downloaded free (without paying for downloading it, except
#    for usage fees that have to be paid anyway). Small copying fees (up to
#    5 DM or 3 $US) may be charged.
#  * Only distribute the whole original package, with all the files included.
#  * This program may not be part of any commercial product or service without
#    the written permission by the author.
#  * If you want to include this program on a CD-ROM and/or book, please send
#    me a free copy of the CD/book (this is not a must, but I would appreciate
#    it very much).
# 
# Distribution of the program is explicitly desired, provided that the above
# conditions are accepted.
# 
# YOU ARE USING THIS PROGRAM AT YOUR OWN RISK! THE AUTHOR (STEPHAN LÖSCHER)
# IS NOT LIABLE FOR ANY DAMAGE OR DATA-LOSS CAUSED BY THE USE OF THIS PROGRAM
# OR BY THE INABILITY TO USE THIS PROGRAM. IF YOU ARE NOT SURE ABOUT THIS, OR
# IF YOU DON'T ACCEPT THIS, THEN DO NOT USE THIS PROGRAM!
# BECAUSE OF THE VARIOUS HARDWARE AND SOFTWARE ENVIRONMENTS INTO WHICH THIS
# PROGRAM MAY BE PUT, NO WARRANTY OF FITNESS FOR A PARTICULAR PURPOSE IS
# OFFERED.
# GOOD DATA PROCESSING PROCEDURE DICTATES THAT ANY PROGRAM BE THOROUGHLY
# TESTED WITH NON-CRITICAL DATA BEFORE RELYING ON IT.
# 
# No part of the documentation may be reproduced, transmitted, transcribed,
# stored in any retrieval system, or translated into any other language in
# whole or in part, in any form or by any means, whether it be electronic,
# mechanical, magnetic, optical, manual or otherwise, without prior written
# consent of the author, Stephan Löscher.
# 
# You may not make any changes or modifications to this software or this
# manual. You may not decompile, disassemble, or otherwise reverse-engineer
# the software in any way.
# If you got the source, then you are permitted to modify it if you
# contact me and tell me your enhancements.
# You also may include the source as a whole or parts of it into other
# programs, as long as you don't make profit directly out of selling
# the result. If you re-use code of this program then do not remove my name!
# If you include this source-code in your projects, mark it clearly as such
# "... derived from code XXX by Stephan Löscher".
# But don't distribute modified code!
# 
# If you believe your copy of this software has been tampered or altered in
# anyway, shape or form, please contact me immediately! Do not hesitate a
# moment to inform me. Remember, this software should be available to all, in
# the original form, so please do not accept modified or damaged versions of
# my software.
# 
# The author reserves his right for taking legal steps if the copyright or the
# license agreement is violated.
# 
# All product names mentioned in this software are trademarks or registered
# trademarks of their respective owners.
# 
# If you have any questions, ideas, suggestions for improvements or if you find
# bugs (I don't hope so.) then feel free to contact me. (Email is appreciated.)
# 
# I'm not a native english speaker. If you are one and discover some strange
# sounding parts in this documentation or in the program, please, feel free
# to point it out to me and give me suggestions for alteration!
# 
# If the program works for you, and you want to honour my efforts, you are
# invited to donate as much as you want... :)
#
# In any case, if you don't like the restrictions in this license, contact
# me, and we can work something out.
#
######################################################################