#!/usr/bin/perl -w

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

require 5.008;
use lib '/usr/local/bin',"$ENV{HOME}/bin",'/usr/stud/loescher/bin';
# Verfügbar unter: http://www.loescher-online.de/progdata/slutil.pm
use slutil 2006.0413;
use English;
use FileHandle;
use File::Copy;
use Carp;
use Net::Ping;

# Verfügbar unter http://www.loescher-online.de/progdata/RemoteClient.pm
# Sysconf funktioniert auch ohne RemoteClient. Einfach auskommentieren.
# (Aber dann in der .sysconfrc bitte auch keinen Port angeben. :-) )
use RemoteClient 2005.0620;

# use Data::Dumper; # Zum Debuggen

######################################################################
### 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
sub progress;   # Zeichnet eine kleine Fortschritts-Animation
sub info;       # Ausgabe von Informationen

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

$version = '1.3.49';
$appname = 'SysConf';

# Changelog:
# 1.09:
# - Kleine Schönheitskorrekturen bei HTML-Ausgabe.
# - RemoteCreateLink(): Statt "ln -sf" jetzt "rm" und "ln -s" wg. Solaris.
# - Rechner werden erst angepingt und nur betankt, wenn erreichbar.
# - Diverse warning()s zu error()s geändert.
# - Fehlerausgaben sind optimiert und um Rechnername ergänzt.
# - Fehlerzusammenfassung am Ende des Sysconf-Laufs.
# - Neu in sysconfrc: STOP_ON_ERROR, LOGLEVEL, LOGFILE
#   Mit STOP_ON_ERROR=FALSE wird bei Fehlern nicht abgebrochen.
# - Komplett neuer Parser für hosts.sc (Kleiner Fehler damit beseitigt.)
# - Mit exclusions.sc können sich gegenseitig ausschliessende Subsysteme
#   definiert werden.
# 1.091:
# - Fehler bei Init-Links behoben.
# 1.092:
# - Ausführlichere Fehlermeldungen bei Remote-Kommandos
# 1.1.0:
# - neue sysconf-Versionsnummer :-)
#   a.b.c, mit a=Hauptversion, b=Featureversion und c=Bugfixversion
# - in files.sc können "owner", "group" und "permission" angegeben werden.
# - ssh-Fehlermeldung bei fehlendem xauth unterdrückt.
# 1.1.1:
# - Fehlende Dokumentation zu exclusions.sc nachgetragen.
# 1.1.2:
# - Zielfilenamen wie "/myfile" wurden nicht als Files akzeptiert.
# - Neue Aktion "listhosts" implementiert. (Siehe Anleitung)
# 1.2:
# - Neues Kommando "lS" für ein lokales Shell-Kommando in files.sc möglich.
# - Backup-File-Existenz-Check korrigiert, so dass es Shell-unabhängig ist.
# 1.2.1:
# - CheckExclusions() korrigiert. (Hat Subsysteme fälschlicherweise umsortiert)
# 1.2.2:
# - Die Subsystem-Abhängigkeiten werden jetzt korrekt (auch mehrfach)
#   expandiert.
# 1.2.3:
# - require-Statement auf 5.6.0 korrigiert.
# 1.2.4:
# - Reihenfolge der Subsystem-Abhängigkeiten korrigiert.
# 1.2.5:
# - Prüfung, ob ein Rechner per SSH/RSH erreichbar ist direkt nach dem Ping-
#   Check. Wenn das fehlschlägt, dann wird nicht mehr abgebrochen, sondern
#   der betroffene Rechner übersprungen.
# 1.2.6:
# - Kleine Fehler in der Dokumentation beseitigt
# 1.2.7:
# - In hosts.sc kann jetzt optional über INTERFACE das zu verwendende Interface
#   angegeben werden. Beispiel: Der Rechner abc123 hat unter dem Hostnamen
#   abc123giga ein Gigabit-Ethernet. Dann kann man als HOST abc123 angeben
#   unter INTERFACE abc123giga angeben und Sysconf betankt den Rechner über
#   das Gigabit-Ethernet. Das ist auch sehr nützlich bei HACMP-Clustern.
#   Da sollte man nicht den produktiven Hostnamen angeben, sondern
#   Hostnamen/Adapter, der nicht als Ressource "wandert", sondern immer auf
#   den selben Rechner verweist.
# - Analog zur Aktion "listhosts" gibt es jetzt auch "listinterfaces"
# - Die Remote-Kommandos werden jetzt alle in '' eingeschlossen.
# 1.3:
# - Zusätzlich zum bisherigen rsh/ssh/rsync gibt es einen sysconf-client,
#   mit dem sysconf direkt über TCP/IP kommuniziert.
#   ACHTUNG:
#   Es fehlt dabei jegliche Verschlüsselung und Authentifizierung.
#   Und das nicht mangels besseren Wissens, sondern aus Zeitmangel.
#   (Das kommt aber noch in einer der nächsten Versionen...)
# - Die Loglevels wurden (inkompatibel) geändert. Siehe Anleitung.
#   Alt: -w2, Neu: -w3
#   Alt: -w3, Neu: -w15
#   Alt: -w4, Neu: -w31
# 1.3.1:
# - Man kann jetzt in commands.sc angeben mit welchem Benutzer/Gruppe die
#   Kommando-Files (installcmd, reconfigcmd, ...) angelegt werden sollen.
#   (CMD_USER und CMD_GROUP)
# - In commands.sc kann angegeben werden, unter welchem Benutzer der
#   Remote-Zugriff per rsh/ssh erfolgen soll. (REMOTE_USER)
# - Fehlermeldungen der Kommandos (installcmd, etc.) werden korrekt ausgegeben.
# - Sysconf muss nicht mehr als Root laufen. Auf Client-Seite kann alles per
#   sudo laufen.
# - Kleiner Geschwindigkeits-Vergleich der einzelnen Methoden:
#   sysconf-client als Root, kein sudo:  3 Minuten
#   sysconf-client als User, mit sudo:   7 Minuten
#   rsh/ssh als Root, kein sudo:        51 Minuten
#   rsh/ssh als User, mit sudo:         51 Minuten
# - Die V 1.3.1 hat so viele Features, dass es fast eine 1.4 geworden wäre :-)
# 1.3.2:
# - Korrekturen in slutil.pm: TesteRemoteCopy()
# - RemoteMkdir() erheblich beschleunigt. (30% Geschwindigkeits-Steigerung)
# - Wenn ein Rechner abgearbeitet ist, dann wird die Client-Verbindung
#   wieder geschlossen.
# 1.3.3:
# - Korrektur: Es wurden per rsh/ssh keine Permissions der Files übertragen.
# 1.3.4:
# - Verbesserung in der Fehlerausgabe, wenn eine Variable nicht definiert ist.
# 1.3.5:
# - Symbolische Unix-Links können jetzt in *.LINK-Files nachgebildet werden.
#   Das ermöglicht das Sysconf-Repository in CVS einzuchecken oder auf
#   ein Filesystem zu legen, das keine symbolischen Links unterstützt.
# - Neue Aktion "listreposfiles", um sich alle Files im Repository für ein
#   Betriebssystem auflisten lassen zu können, z.B.
#   sysconf listreposfiles Linux-2.5.6 xxx
# 1.3.6:
# - Mit der Option "--logfile=..." kann man den Ort des Logfiles angeben.
# - Mit der Option "--dry-run" kann ein "Trockenlauf" durchgeführt werden.
#   Dabei wird alles durchgeführt, ausser die Client-Zugriffe,
#   d.h. ssh/rsh/Sysconf-Client sind deaktiviert.
# 1.3.7:
# - Es kann als LOGFILE auch ein Pipe angegeben werden.
# - Variablenersetzung jetzt auch in den *cmd-Files z.B. reconfigcmd möglich.
# - Die *cmd-Files des Subsystems GLOBAL werden jetzt korrekt ausgeführt.
#   (Bisher wurde nicht mal das Fehlen dieser Files bemerkt...)
# - In files.sc kann mit dem Attribut hidden=TRUE verhindert werden, dass ein
#   File in der HTML-Dokumentation auftaucht.
# 1.3.8:
# - Das Subsystem GLOBAL wird jetzt nicht mehr fälschlicherweise doppelt
#   verteilt
# - Die Behandlung der Backupfiles, die mit "-b" angelegt werden, wurde
#   verbessert: Die Backupfiles werden erst nach allen Aktionen (Kommando-Files
#   *cmd) gelöscht. Dadurch ist es jetzt auch gefahrlos möglich z.B. im
#   reconfigcmd eine Datei zu modifizieren, ohne den Backup-Schutz zu verlieren
# 1.3.9:
# - Bei ssh die Option "-o StrictHostKeyChecking=no" in slutil.pm hinzugefügt.
# - Problem bei Files mit s-Bit und sysconf-client behoben.
# 1.3.10:
# - Wenn ping als Benutzer (nicht Root) nicht funktioniert, dann wird noch ein
#   ping per system() versucht.
# 1.3.11:
# - Mit der Option "--sysconf_root=..." kann man den Ort des Sysconf-
#   Repositorys angeben.
# - Bei Sysconf-Abbrüchen wird eine Liste aller Rechner ausgegeben, die noch
#   nicht behandelt worden sind.
# 1.3.12:
# - Liste aller Rechner, nicht bei Abbruch nicht behandelt worden sind, wird
#   komma-getrennt ausgegeben.
# - Wenn gar kein ping geht, dann wird Verbindung auf Port 22 versucht.
# 1.3.13:
# - hosts.sc und classes.sc können jetzt auch über den .LINK-Mechanismus
#   problemlos verlagert werden.
# 1.3.14:
# - In files.sc können mit der INCLUDE-Anweisung weitere Dateien geladen
#   werden.
# 1.3.15:
# - in hosts.sc kann mit REVISION eine Revisionsnummer angegeben werden und
#   mit listrevision gibt Sysconf diese aus.
# 1.3.16:
# - korrekte Ausgabe der Kommando-Files bei einem documentation-Lauf
# 1.3.17:
# - Timeout von Ping auf 10 Sekunden gesetzt.
# 1.3.18:
# - Return-Codes wurden erweitert. Es gibt jetzt statt 0 und 1 diese:
#   $NormalExitCode = 0;
#   $WarnExitCode   = 1;
#   $ErrorExitCode  = 2;
#   $FatalExitCode  = 3;
# 1.3.19:
# - INCLUDE-Anweisung können nun auch verschachtelt sein, d.h. ein includiertes
#   File kann wiederum INCLUDE-Anweiungen enthalten. Maximale Include-Tiefe ist
#   auf 20 beschränkt.
# 1.3.20:
# - Die Option USE_SUDO wurde von der Hauptkonfigurationsdatei in die Datei
#   commands.sc verlagert. Dadurch kann man pro Betriebssystem entscheiden,
#   ob man sudo verwenden will oder nicht.
#   Ausserdem kann man in commands.sc mit SSH_KEY den Ort des zu verwendenden
#   SSH-Keys angeben.
# 1.3.21:
# - Bessere Fehlermeldung bei Fehlern in Variablenersetzung
# 1.3.22:
# - Bei "documentation" wird nun auch ein File aller Sysconf-verwalteten Files
#   im HTMLDIR namens files.txt erzeugt.
# 1.3.23:
# - Verbesserte Fehlerausgabe bei Fehlern im Variablen-File
# 1.3.24:
# - Korrektur bei Erstellung der files.sc
# - Neue Aktion "none".
# - Neue Option "--list_hosts_per_os=..."
# 1.3.25:
# - Zusätzliche Infos über Zugriffsversuch im Logfile
# 1.3.26:
# - Es werden zwei Sysconf-interne Variablen definiert, um z.B.
#   in *cmd-Files darauf zugreifen zu können:
#   SYSCONF_BACKUP mit den Werten TRUE und FALSE (Entspricht $backup)
#   SYSCONF_BACKUP_EXTENSION als String (Entspricht $backup_endung)
# 1.3.27:
# - Neuer Typ in files.sc: "m" für Files, die z.B. durch reconfigcmd verändert
#   werden und deshalb bei der Backup-Option berücksichtigt werden müssen.
# - Für Links "L" in files.sc werden nun auch Backups erstellt.
# - Ergänzung der Dokumentation für commands.sc um die empfohlenen cp-Optionen.
# 1.3.28:
# - Interne Variable SYSCONF_OS hinzugefügt.
# - Dokumenations-Ausgabe in Unterverzeichnisse verlagert.
# 1.3.29:
# - RemoteCreateLink() verbessert. (Es werden jetzt auch bestehende Links
#   korrekt behandelt.)
# 1.3.30:
# - Ausgabe der Laufzeit pro Rechner im Logfile und Liste der nicht erfolgreich
#   betankten Rechner.
# 1.3.31:
# - Mit dependencies-soft.sc können "weiche" Abhängigkeiten angegeben werden.
# 1.3.32:
# - interner Timeout implementiert
# 1.3.33:
# - verbessertes Löschen von überflüssigen Backup-Files
# 1.3.34:
# - "localshell" in files.sc wurde entfernt. Dafür gibt es nun zwei neue
#   Kommando-Files: "pre_localshell" und "post_localshell"
# - Syscheck-Aufrufe wurden entfernt, da so eine Funktion auch mit
#   "pre_localshell" und "post_localshell" möglich ist
# - jeder Rechner bekommt das spezielle Subsystem "LAST" als letztes
# 1.3.35:
# - Löschen eines temporären Templates korrigiert.
# 1.3.36:
# - Unterdrückung eventueller scp- oder rsync-Ausgaben und Ausgabe nur im
#   Fehlerfall.
# 1.3.37:
# - deutsches "scharfes S" entfernt und durch Doppel-S ersetzt.
# 1.3.38:
# - Interne Variable SYSCONF_INTERFACE hinzugefügt.
# 1.3.39:
# - Interne Variablen SYSCONF_CLASSES, SYSCONF_SUBSYSTEMS,
#   SYSCONF_HOSTS_SC_SUBSYSTEMS und SYSCONF_VARFILE hinzugefügt.
# 1.3.40:
# - Vollständiges Listing aller von Sysconf modifizierter Links.
# 1.3.41:
# - Umlaute ersetzt
# 1.3.42:
# - weitere Umlaute ersetzt
# 1.3.43:
# - Neue Option "--list_hosts_subsys"
# 1.3.44:
# - Neue Option "--list_subsys_hosts"
# 1.3.45:
# - Handling von Whitespace beim INCLUDE in files.sc verbessert
# 1.3.46:
# - Umlaute in der HTML-Doku korrigiert.
# 1.3.47:
# - Bei "documentation" korrekte Ausgabe der HIDDEN-Files
# 1.3.48:
# - HTML-Doku auf einzelne Seiten aufgeteilt.
# 1.3.49:
# - Beim mkdir nicht versuchen '/' anzulegen. (Fix für Solaris 6)
#

# Alle Optionen "--option=wert" in einen globalen Hash verlagern. Dabei wird
# ARGV verändert.
%options = ReadOptions();

# Wohin soll das logging erfolgen?
$logfile = SetLogfile();

# Exitcodes beim Beenden
$NormalExitCode = 0;
$WarnExitCode   = 1;
$ErrorExitCode  = 2;
$FatalExitCode  = 3;

# Merker, ob Warnings oder Errors aufgetreten sind
$Warning_aufgetreten = $FALSE;
$Error_aufgetreten = $FALSE;

$0 = $0; # Kommandozeile verbergen.

$EUID_USER = getpwuid($>);
$EGID_USER = getgrgid($));

%LinksAufloesenCache = ();

# Timeout in Sekunden für rsh, ssh und sysconf-client und was sonst noch
# hängen könnte
$timeout = 60*60; # 1 Stunde

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

# LOG bereits hier starten, denn Konfigurationsfehler sind entscheidend!
# Wenn $logfile mit "|" beginnt, dann nicht mit ">>" öfnnen.
if ($logfile =~ /^\|/)
{
  open(LOG, "$logfile") || die "Kann Logfile '$logfile' nicht schreiben!\n";
}
else
{
  open(LOG, ">>$logfile") || die "Kann Logfile '$logfile' nicht schreiben!\n";
}
select(LOG); $|=1; select(STDOUT); $|=1; # 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;
$SIG{ALRM} = \&catch_timeout;

$logLevel = 7; # Standardmässig Fehler, Warnungen und Infos ausgeben
$logLevel_bereits_gesetzt = $FALSE;
$backup = $FALSE;
while ( (defined $ARGV[0]) && ($ARGV[0] =~ /^-/) )
{
 OPTION:
  {
    if ($ARGV[0] =~ /^-w(\d+)/)
    {
      $logLevel = $1; $logLevel_bereits_gesetzt=$TRUE; shift @ARGV; last OPTION
    }
    if ($ARGV[0] eq '-b')
    {
      $backup_endung = '.'.longdate().'.SC';
      $backup = $TRUE; shift @ARGV; last OPTION
    }
    # Sonst:
    die "Option $ARGV[0] gibt es nicht!\n";
  }
}

# Soll nur ein "Trockenlauf" durchgeführt werden?
$dry_run = $FALSE;
if (defined $options{"dry-run"})
{
  $dry_run = $TRUE;
  logprint "Es findet nur ein Trockenlauf (--dry-run) statt!\n";
}

logprint "Backup ist " .
($backup ? "aktiv (Endung $backup_endung)" : 'nicht aktiviert').".\n";

# Ist STDOUT mit einem Terminal verbunden?
$stdout_is_terminal = (-t STDOUT);

foreach (keys %options)
{
  logprint "Option: $_";
  if (defined $options{$_})
  { logprint " = '$options{$_}'\n"; }
  else
  { logprint "\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;
}

# Einstellungen aus Hauptkonfigurationsdatei lesen
$sysconfroot = (defined $options{sysconf_root}) ? $options{sysconf_root} : '';
$htmldir          = '';
$stylesheet       = '';
$stylesheet_class = '';
$stop_on_error    = $TRUE;
$client_port      = 0;
ReadConfigFile();

logprint "Log-Level: $logLevel\n";

$logLevelError  = ( ($logLevel |  1) == $logLevel ) ? $TRUE : $FALSE;
$logLevelWarn   = ( ($logLevel |  2) == $logLevel ) ? $TRUE : $FALSE;
$logLevelInfo   = ( ($logLevel |  4) == $logLevel ) ? $TRUE : $FALSE;
$logLevelDebug  = ( ($logLevel |  8) == $logLevel ) ? $TRUE : $FALSE;
$logLevelRemote = ( ($logLevel | 16) == $logLevel ) ? $TRUE : $FALSE;

debug "SYSCONF_ROOT     = $sysconfroot\n";
debug "HTMLDIR          = $htmldir\n";
debug "STYLESHEET       = $stylesheet\n";
debug "STYLESHEET_CLASS = $stylesheet_class\n";
debug "STOP_ON_ERROR    = $stop_on_error\n";
debug "CLIENT_PORT      = $client_port\n";

# Wenn client_port definiert ist, aber das RemoteClient.pm nicht geladen ist,
# dann den client_port deaktivieren.
if ( ($client_port) && (! defined $RemoteClient::VERSION) )
{
  info("CLIENT_PORT ist definiert, aber kein RemoteClient.pm geladen!\n");
  info("Sysconf-Client-Funktionen werden deshalb deaktiviert!\n");
  $client_port = 0;
}

if ($client_port)
{
  # Importieren der Fehlercodes von RemoteClient
  # (Das ... += 0 ist nur zum Warungen-Unterdrücken, falls RemoteClient.pm
  #  nicht geladen ist.)
  $RC_OK    = $RemoteClient::RC_OK;    $RemoteClient::RC_OK   +=0;$RC_OK   +=0;
  $RC_DEBUG = $RemoteClient::RC_DEBUG; $RemoteClient::RC_DEBUG+=0;$RC_DEBUG+=0;
  $RC_INFO  = $RemoteClient::RC_INFO;  $RemoteClient::RC_INFO +=0;$RC_INFO +=0;
  $RC_WARN  = $RemoteClient::RC_WARN;  $RemoteClient::RC_WARN +=0;$RC_WARN +=0;
  $RC_ERR   = $RemoteClient::RC_ERR;   $RemoteClient::RC_ERR  +=0;$RC_ERR  +=0;
  $RC_FATAL = $RemoteClient::RC_FATAL; $RemoteClient::RC_FATAL+=0;$RC_FATAL+=0;
  # Debug-Unterfunktion in das Remote-Modul einhängen
  RemoteClient::SetDebugFunction(\&debug_rsh);
}
debug "Client-Protokoll-Version: ",$RemoteClient::VERSION,"\n";

%Rechner_mit_Fehlern = ();

%klassendef = RechnerKlassenEinlesen();

$beschreibungen = RechnerBeschreibungenEinlesen();

$param = ParameterEinlesen(@ARGV);
logprint "Aktion: ", $param->Aktion,"\n";
if ( ! ($param->Aktion eq 'listreposfiles') )
{
  logprint "Subsysteme gewuenscht: ", join(',',$param->Subsysteme), "\n";
  logprint "Rechner: ", join(',',$param->Rechner),    "\n";
}
else
{
  logprint "Betriebssystem: ", $param->Betriebssystem(), "\n";
}

$dependencies        = Dependencies::new();
$dependencies_soft   = Dependencies::new();
$exclusions          = Exclusions::new();
$commands            = Commands::new();
$files               = Files::new();
$templatepattern     = TemplatePattern::new(); # Pattern für Files
$cmd_templatepattern = TemplatePattern::new(); # Pattern nur für *cmd-Files

# Ausgabe einer Liste der Rechner nach Betriebssystem
ListHostsPerOS($beschreibungen,$options{list_hosts_per_os}) if defined $options{list_hosts_per_os};

# Ausgabe einer Liste der Rechner mit Subsystemen
ListHostsSubsys($beschreibungen,$param) if defined $options{list_hosts_subsys};

# Ausgabe einer Liste der Subsystemen und welcher Rechner sie bekommt
ListSubsysHosts($beschreibungen,$param) if defined $options{list_subsys_hosts};

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

# Liste aller Rechner ausgeben
ListHosts($beschreibungen,$param) if $param->Aktion eq 'listhosts';

# Liste aller Interfaces ausgeben
ListInterfaces($beschreibungen,$param) if $param->Aktion eq 'listinterfaces';

# Liste aller Rechner mit Revisionsnummer ausgeben
ListHostsWithRevision($beschreibungen,$param) if $param->Aktion eq 'listrevision';

# Liste aller Files im Repository pro Betriebssystem ausgeben
if ($param->Aktion eq 'listreposfiles')
{
  $tmp_bs = $param->Betriebssystem();
  ReadFilesSC($tmp_bs);
  @tmp_files = @{$files->GetFileList($tmp_bs)};
  foreach $fileobj (@tmp_files)
  {
    print $fileobj->Get('quelle'),"\n";
  }
  myexit;
}

# Wir merken uns, welche Rechner noch nicht betankt worden sind, um eine
# "Rest-Liste" bei fatalen Abbrüchen ausgeben zu können.
%rechner_noch_nicht_abgearbeitet = ();
foreach $rechner ($param->Rechner)
{
  $rechner_noch_nicht_abgearbeitet{$rechner}++;
}

foreach $rechner ($param->Rechner)
{
  # Hier ist es pro Rechner parallelisierbar

  # Nur zur Info die Uhrzeit merken, um ausgeben zu können, wie lange der
  # Sysconf-Lauf für diesen Rechner benötigt hat
  $time_start = time();

  # Testen, ob es eine Beschreibung zu dem Rechner gibt
  unless (defined $beschreibungen->Betriebssystem($rechner))
  {
    warning $rechner,"Keine Beschreibung in hosts.sc vorhanden!\n";
    next;
  }

  # Testen, ob der Rechner erreichbar ist
  unless ( RechnerErreichbar( $beschreibungen->Interface($rechner) ) )
   {
    error $rechner,"Per Ping ueber Interface '",
    $beschreibungen->Interface($rechner),"' nicht erreichbar!\n";
    next;
  }

  my $socket;
  if (! $dry_run)
  {
    # Testen, ob der Rechner per Sysconf-Client erreichbar ist
    $socket = $beschreibungen->Socket($rechner);
    if (defined $socket)
    {
      debug "Rechner ueber Sysconf-Client erreichbar.\n";
    }
    else
    {
      # Testen, ob der Rechner per RSH/SSH erreichbar ist
      my $temprsh = $beschreibungen->GetRSH($rechner);
      debug "GetRSH: '$temprsh'\n";
      if ($temprsh eq 'none')
      {
	error $rechner,"Per RSH/SSH ohne Passwort ueber Interface '",
	$beschreibungen->Interface($rechner),"' nicht erreichbar!\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);

  # Das Subsystem "GLOBAL" bekommt jeder Rechner als erstes Subsystem!
  unshift @subsys, 'GLOBAL';

  # Das Subsystem "LAST" bekommt jeder Rechner als letztes Subsystem!
  push @subsys, 'LAST';

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

  %RechnerVars = ReadVariables($rechner,$beschreibungen);
  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')
    {
      ReadCommands($beschreibungen->Betriebssystem($rechner));
      @subsys = CheckAndAddDependencies($rechner,$beschreibungen,@subsys);
      @subsys = CheckExclusions($rechner,$beschreibungen,@subsys);
      print "$rechner\n";
      logprint "Subsysteme nach Abhaengigkeitsaufloesung fuer $rechner: ",
      join(',',@subsys), "\n";
      Init($rechner, $beschreibungen->Betriebssystem($rechner),
	   \%RechnerVars, @subsys);
      last SWITCH;
    }

    # Update
    if ($param->Aktion eq 'update')
    {
      ReadCommands($beschreibungen->Betriebssystem($rechner));
      @subsys = CheckAndAddDependencies($rechner,$beschreibungen,@subsys);
      @subsys = CheckExclusions($rechner,$beschreibungen,@subsys);
      print "$rechner\n";
      logprint "Subsysteme nach Abhaengigkeitsaufloesung fuer $rechner: ",
      join(',',@subsys), "\n";
      Update($rechner, $beschreibungen->Betriebssystem($rechner),
	     \%RechnerVars,@subsys);
      last SWITCH;
    }

    # Start
    if ($param->Aktion eq 'start')
    {
      ReadCommands($beschreibungen->Betriebssystem($rechner));
      # Hier kein CheckAndAddDependencies(), weil sonst alle möglichen
      # Subsysteme beeinflusst werden!
      @subsys = CheckExclusions($rechner,$beschreibungen,@subsys);
      print "$rechner\n";
      logprint "Subsysteme nach Abhaengigkeitsaufloesung fuer $rechner: ",
      join(',',@subsys), "\n";
      Start($rechner, $beschreibungen->Betriebssystem($rechner), @subsys);
      last SWITCH;
    }

    # Stop
    if ($param->Aktion eq 'stop')
    {
      ReadCommands($beschreibungen->Betriebssystem($rechner));
      # Hier kein CheckAndAddDependencies(), weil sonst alle möglichen
      # Subsysteme beeinflusst werden!
      @subsys = CheckExclusions($rechner,$beschreibungen,@subsys);
      print "$rechner\n";
      logprint "Subsysteme nach Abhaengigkeitsaufloesung fuer $rechner: ",
      join(',',@subsys), "\n";
      Stop($rechner, $beschreibungen->Betriebssystem($rechner), @subsys);
      last SWITCH;
    }

    # Remove
    if ($param->Aktion eq 'remove')
    {
      ReadCommands($beschreibungen->Betriebssystem($rechner));
      # Hier kein CheckAndAddDependencies(), weil sonst alle möglichen
      # Subsysteme entfernt werden!
      print "$rechner\n";
      logprint "Subsysteme nach Abhaengigkeitsaufloesung fuer $rechner: ",
      join(',',@subsys), "\n";
      Remove($rechner, $beschreibungen->Betriebssystem($rechner), @subsys);
      last SWITCH;
    }

    logdie "Die Aktion ",$param->Aktion," gibt es nicht!\n";
  }

  # Rechner aus der Liste entfernen, da jetzt abgearbeitet
  delete $rechner_noch_nicht_abgearbeitet{$rechner};

  # Wenn eine Socket-Verbindung bestand, dann wieder schliessen
  if (defined $socket)
  {
    $beschreibungen->Socket_Destroy($rechner);
  }

  $tmp = time() - $time_start;
  info "Laufzeit $rechner: $tmp ", ($tmp == 1 ? "Sekunde\n" : "Sekunden\n");
  debug '-'x60,"\n";
}

myexit;


######################################################################
### 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    = @_;
  my $tmp;

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

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

    my $sub = SubsystemObject->new($bs, $rechner, $subsystem);

    # Zuerst das pre_localshell für dieses Subsystem starten
    $sub->pre_localshell;

    # Modify-Files "übertragen", also sicherstellen, dass für diese Files
    # Backups angelegt werden.
    TransferFiles($rechner, $bs, $subsystem, 'modify', $refvar);

    # Ist das Subsystem bereits installiert?
    if ($sub->IsInstalled)
    {
      # Läuft das Subsystem gerade?
      if ($sub->IsRunning)
      {
        # Stoppen (damit auch alle abhängigen)
        $sub->Stop || error $rechner,"Stoppen von '$subsystem' nicht erfolgreich!\n";
      }
    }
    else # Wenn es nicht installiert ist, dann...
    {
      # 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)
      {
        error $rechner,"Install von '$subsystem' nicht erfolgreich!\n";
        error $rechner,"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 rekonfigurieren
    $sub->Reconfigure ||
    error $rechner,"Rekonfigurieren von '$subsystem' nicht erfolgreich!\n";

    # Subsystem starten
    $sub->Start || error $rechner,"Start von '$subsystem' nicht erfolgreich!\n";

    foreach $tmp ('install','init','inittemplate','file','template','modify',
		 'installlink','initlink','link')
    {
      RemoveBackupFiles($rechner, $bs, $subsystem, $tmp);
    }

    # Zu letzt das post_localshell für dieses Subsystem starten
    $sub->post_localshell;
  }
}


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  = @_;
  my $tmp;

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

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

    my $sub = SubsystemObject->new($bs, $rechner, $subsystem);

    # Zuerst das pre_localshell für dieses Subsystem starten
    $sub->pre_localshell;

    # Ist das Subsystem bereits installiert?
    if ($sub->IsInstalled)
    {
      # Läuft das Subsystem gerade?
      if ($sub->IsRunning)
      {
        # Stoppen (damit auch alle abhängigen)
        $sub->Stop || error $rechner,"Stoppen von '$subsystem' nicht erfolgreich!\n";
      }
      # Modify-Files "übertragen", also sicherstellen, dass für diese Files
      # Backups angelegt werden.
      TransferFiles($rechner, $bs, $subsystem, 'modify',     $refvar);
      # 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 ||
      error $rechner,"Rekonfigurieren von '$subsystem' nicht erfolgreich!\n";
      # Subsystem starten
      $sub->Start || error $rechner,"Start von '$subsystem' nicht erfolgreich!\n";

      foreach $tmp ('file','template','modify','link')
      {
	RemoveBackupFiles($rechner, $bs, $subsystem, $tmp);
      }

      # Zu letzt das post_localshell für dieses Subsystem starten
      $sub->post_localshell;
    }
    else
    {
      # Wenn das Subsystem nicht installiert ist, dann Init() aufrufen
      Init($rechner,$bs,$refvar,$subsystem);
    }
  }
}


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

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

  debug "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)
  {
    debug "Behandle Subsystem '$subsystem'\n";

    my $sub = SubsystemObject->new($bs, $rechner, $subsystem);

    # Ist das Subsystem überhaupt installiert?
    unless ($sub->IsInstalled)
    {
      debug "'$subsystem' ist auf '$rechner' nicht installiert!\n";
      next;
    }

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


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

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

  debug "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)
  {
    debug "Behandle Subsystem '$subsystem'\n";

    my $sub = SubsystemObject->new($bs, $rechner, $subsystem);

    # Ist das Subsystem überhaupt installiert?
    unless ($sub->IsInstalled)
    {
      debug "'$subsystem' ist auf '$rechner' nicht installiert!\n";
      next;
    }

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

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


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

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

  debug "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)
  {
    debug "Behandle Subsystem '$subsystem'\n";

    my $sub = SubsystemObject->new($bs, $rechner, $subsystem);

    # Ist das Subsystem überhaupt installiert?
    unless ($sub->IsInstalled)
    {
      debug "'$subsystem' ist auf '$rechner' nicht installiert!\n";
      next;
    }

    # Läuft das Subsystem gerade?
    if ($sub->IsRunning)
    {
      # Subsystem stoppen
      $sub->Stop || error $rechner,"Stoppen von '$subsystem' nicht erfolgreich!\n";
    }
    else
    {
      debug "'$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) = @_;

  my $use_sudo = $commands->Get($beschreibungen->Betriebssystem($rechner),'USE_SUDO');
  debug "USE_SUDO = $use_sudo\n";

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

  my @files = @{$files->Get($bs,$subsys,$art)};
  debug "Uebertrage Files... ($art)\n";
  progress;
  my ($fileobj, $file, $zielfile);

  foreach $fileobj (@files)
  {
    $file       = $fileobj->Get('quelle');
    $zielfile   = $fileobj->Get('ziel');
    $kommando   = $fileobj->Get('kommando');
    $owner      = $fileobj->Get('owner');
    $group      = $fileobj->Get('group');
    $permission = $fileobj->Get('permission');

    # Installfiles, Initfiles, Files
    if ($art =~ /^install$|^init$|^file$|^modify$/)
    {
      RemoteCopy($file,$rechner,$zielfile,$owner,$group,$permission);
      progress;
      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,$owner,$group,$permission);
      unlink $tempfile;
      progress;
      next;
    }
    # Links
    if ($art =~ /^link$|^installlink$|^initlink$/)
    {
      RemoteCreateLink($file,$rechner,$zielfile);
      progress;
      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 = $kommando;
	local $SIG{__WARN__} = \&FehlerInShellVariablenErsetzung;
	eval $variablenCode.'$kommando =~ s/(\$\w+)/$1/eeg;';
      }
      if ($use_sudo)
      {
	$kommando = '/bin/sh -c "'.$kommando.'"';
      }
      RemoteShell($rechner,$kommando);
      progress;
      next;
    }
    # Sonst
    logdie "Interner Fehler: File-Art ist ungültig!\n";
  }
}


sub RemoveBackupFiles
{
  # Entfernt Backup-Files, wenn Backup und neue Datei identisch sind.
  # Parameter: Rechner, Betriebssystem, Subsystem, File-Art
  # Return: -

  # Wenn die Backup-Option nicht aktiv ist, dann gibt es keine Backup-Files
  # und folglich nichts zu tun.
  return unless $main::backup;

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

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

  my $diff  = $commands->Get($bs,'DIFF'     );
  my $rm    = $commands->Get($bs,'RM'       );
  my $user  = $commands->Get($bs,'CMD_USER' );
  my $group = $commands->Get($bs,'CMD_GROUP');

  my @files = @{$files->Get($bs,$subsys,$art)};
  debug "Entferne Backup-Files... ($art)\n";
  progress;
  my ($fileobj, $file, $zielfile, $backupfile, $fh, $tmp_sc_diff, $local_tmp_sc_diff);

  foreach $fileobj (@files)
  {
    $zielfile   = $fileobj->Get('ziel');
    $backupfile = $zielfile.$backup_endung;
    $local_tmp_sc_diff = "/tmp/master-sysconf-diff-$$.pl";
    $tmp_sc_diff       = "/tmp/sysconf-diff-$$.pl";
    if ($art =~ /^installlink$|^initlink$|^link$|^modify$|^install$|^init$|^file$|^inittemplate$|^template$/)
    {
      # Wenn File und Kopie identisch sind, dann Kopie wieder löschen.
      # Dazu ein etwas besseres "diff"-Programm erstellen und übertragen...
      $fh = FileHandle->new();
      open($fh, ">$local_tmp_sc_diff") || logdie "Kann '$local_tmp_sc_diff' nicht zum Schreiben oeffnen!\n";
      print $fh '#!/usr/bin/perl -w

$file   = \'' . $zielfile   . '\';
$backup = \'' . $backupfile . '\';

if ((-l $file) && (-l $backup))
{
  # Link-Ziele vergleichen
  unlink $backup if (readlink($file) eq readlink($backup));
  exit;
}

# "-f" liefert auch True, wenn es ein Link auf ein File ist!
if ( ((! -l $file)&&(-f $file)) && ((! -l $backup)&&(-f $backup)) )
{
  # Files mit diff vergleichen
  system("' . $diff . ' $file $backup >/dev/null 2>/dev/null");
  unlink $backup if ( ($?>>8) == 0 );
  exit;
}
';
      close($fh) || logdie "Fehler beim Schliessen von '$local_tmp_sc_diff'.\n";
      progress;
      RemoteCopy($local_tmp_sc_diff, $rechner, $tmp_sc_diff, $user, $group, 700);
      progress;
      unlink $local_tmp_sc_diff;
      progress;
      RemoteShell($rechner,$tmp_sc_diff);
      progress;
      RemoteRm($rechner,$tmp_sc_diff);
      next;
    }

    # Sonst
    logdie "Interner Fehler: File-Art '$art' für RemoveBackupFiles() ist ungültig!\n";
  }
}


sub Documentation
{
  # HTML-Dokumentation erzeugen
  # Parameter: Beschreibungen-Objekt, ParameterListe-Objekt
  # Return:    -
  #
  my ($beschreibungen,$param) = @_;

  my ($style,$class);
  $style = '';
  if ($stylesheet ne '')
  {
    $style = "    <meta http-equiv=\"Content-Style-Type\" content=\"text/css\">\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"$stylesheet\">\n";
  }
  $class = '';
  if ($stylesheet_class ne '')
  {
    $class = " class=\"$stylesheet_class\"";
  }

  my $head = '<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html>
  <head>
    <title>Systemkonfiguration</title>
' . $style .
"  </head>

  <body>
    <h1>Systemkonfiguration</h1>

    <a href=\"files.txt\">Liste aller Files, die Sysconf verwaltet.</a><br>
    <br>

    und die Details je Rechner:<br>
    <br>

";
  my $tail = '    <br>
    <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.loescher-online.de/">Stephan L&ouml;scher</a>,
      <a href="mailto:loescher@gmx.de">loescher@gmx.de</a>,<br>
      '.date.'
    </address>
  </body>
</html>
';

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

  my $rechner;
  my $bs;
  my $verz;
  foreach $rechner ($param->Rechner)
  {
    logprint "$rechner ";
    # Testen, ob es eine Beschreibung zu dem Rechner gibt
    unless (defined ($bs = $beschreibungen->Betriebssystem($rechner)) )
    {
      warning $rechner,"Keine Beschreibung in hosts.sc vorhanden!\n";
      next;
    }

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

    my @subsys = CheckSubsystems($rechner,$beschreibungen,$param);
    unless (@subsys)
    {
      warning $rechner,"Keine Subsysteme vorhanden!\n";
      next;
    }
    unshift @subsys, 'GLOBAL';
    push @subsys, 'LAST';
    @subsys = CheckAndAddDependencies($rechner,$beschreibungen,@subsys);
    @subsys = CheckExclusions($rechner,$beschreibungen,@subsys);

    my %RechnerVars = ReadVariables($rechner,$beschreibungen);
    # Datei "files.sc" für dieses Betriebssystem einlesen
    ReadFilesSC($beschreibungen->Betriebssystem($rechner));

    print $fh "    <a href=\"$rechner/$rechner.html\">$rechner</a><br>\n";

    # HTML-Seite für diesen Rechner anlegen
    $verz = "$htmldir${slash}$rechner";
    if (! -d $verz)
    {
      mkdir($verz) || logdie "Kann Verzeichnis '$verz' nicht anlegen ($!)!\n";
    }
    my $rfh = FileHandle->new();
    open($rfh, ">$verz${slash}$rechner.html") || logdie
      "Kann '$verz${slash}$rechner.html' nicht zum Schreiben oeffnen!\n";
    print $rfh '<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html>
  <head>
    <title>Systemkonfiguration - ' . $rechner . '</title>
' . $style .
"  </head>

  <body>
    <h1>Systemkonfiguration - $rechner</h1>
    <table border=1>
      <tr>
        <th$class>Rechner</th>
        <th$class>Subsysteme</th>
        <th$class>Variablen</th>
        <th$class>Betriebssystem</th>
      </tr>
";

    # Dateien der Variablen-Inhalte erstellen
    my ($key,$value);
    while (($key,$value) = each %RechnerVars)
    {
      my $vfh = FileHandle->new();
      open($vfh, ">$verz${slash}$rechner-$key.txt") || logdie
      "Kann '$verz${slash}$rechner-$key.txt' nicht zum Schreiben oeffnen!\n";
      print $vfh $value;
      close($vfh) || logdie "Fehler beim Schliessen von '$verz${slash}$rechner-$key.txt'!\n";
    }
    print $rfh "      <tr>
        <td valign=top$class>$rechner</td>
        <td valign=top$class>\n";
    foreach (sort @subsys)
    {
      print $rfh "          <a href=\"$rechner-sub-$_.html\">$_</a><br>\n";
      GeneriereSubsystemHTML($rechner, $bs, $_, \%RechnerVars);
    }
    print $rfh "        </td>\n        <td valign=top$class>\n";
    foreach (sort keys %RechnerVars)
    {
      print $rfh "          <a href=\"$rechner-$_.txt\">$_</a><br>\n";
    }
    print $rfh "        </td>
        <td valign=top$class>",$beschreibungen->Betriebssystem($rechner),"</td>
      </tr>
";
    print $rfh "    </table>\n";
    print $rfh $tail;
    close $rfh;
  } # Ende der Schleife über alle Rechner

  logprint "\n";
  print $fh $tail;
  close($fh) || logdie "Fehler beim Schliessen von '$htmldir${slash}index0.html'!\n";
  unlink "$htmldir${slash}index.html";
  rename "$htmldir${slash}index0.html", "$htmldir${slash}index.html";

  # Liste aller Files
  system("sort $htmldir${slash}files0.txt | uniq > $htmldir${slash}files.txt");
  unlink("$htmldir${slash}files0.txt");

  myexit;
}


sub ListHostsPerOS
{
  # Ausgabe einer Liste der Rechner nach Betriebssystem
  # Parameter: Beschreibungen-Objekt, Betriebssystemname
  # Return:    -
  #
  my ($beschreibungen,$query_os) = @_;
  debug "Führe Aktion 'list_hosts_per_os' aus.\n";
  my $rechner;
  foreach $rechner ( ExpandiereALLRechner() )
  {
    # Testen, ob es eine Beschreibung zu dem Rechner gibt
    unless (defined ($bs = $beschreibungen->Betriebssystem($rechner)) )
    {
      warning $rechner,"Keine Beschreibung in hosts.sc vorhanden!\n";
      next;
    }
    print "$rechner\n" if $query_os eq $bs;
  }
  myexit;
}


sub ListHostsSubsys
{
  # Ausgabe einer Liste der Rechner mit Subsystemen
  # Parameter: Beschreibungen-Objekt, ParameterListe-Objekt
  # Return:    -
  #
  my ($beschreibungen,$param) = @_;
  debug "Führe Aktion 'list_hosts_subsys' aus.\n";
  my $rechner;
  foreach $rechner ( $param->Rechner )
  {
    # Testen, ob es eine Beschreibung zu dem Rechner gibt
    unless (defined ($bs = $beschreibungen->Betriebssystem($rechner)) )
    {
      warning $rechner,"Keine Beschreibung in hosts.sc vorhanden!\n";
      next;
    }
    my @subsys = CheckSubsystems($rechner,$beschreibungen,$param);
    unshift @subsys, 'GLOBAL';
    push @subsys, 'LAST';
    @subsys = CheckAndAddDependencies($rechner,$beschreibungen,@subsys);
    @subsys = CheckExclusions($rechner,$beschreibungen,@subsys);
    print "$rechner: ",join(',',@subsys),"\n";
  }
  myexit;
}


sub ListSubsysHosts
{
  # Ausgabe einer Liste aller Subsysteme und welche Rechner sie bekommen
  # Parameter: Beschreibungen-Objekt, ParameterListe-Objekt
  # Return:    -
  #
  my ($beschreibungen,$param) = @_;
  debug "Führe Aktion 'list_subsys_hosts' aus.\n";
  my $rechner;
  my %subsys_to_host;
  foreach $rechner ( $param->Rechner )
  {
    # Testen, ob es eine Beschreibung zu dem Rechner gibt
    unless (defined ($bs = $beschreibungen->Betriebssystem($rechner)) )
    {
      warning $rechner,"Keine Beschreibung in hosts.sc vorhanden!\n";
      next;
    }
    my @subsys = CheckSubsystems($rechner,$beschreibungen,$param);
    unshift @subsys, 'GLOBAL';
    push @subsys, 'LAST';
    @subsys = CheckAndAddDependencies($rechner,$beschreibungen,@subsys);
    @subsys = CheckExclusions($rechner,$beschreibungen,@subsys);
    foreach (@subsys)
    {
      if (defined $subsys_to_host{$_})
      {
	$subsys_to_host{$_} = $subsys_to_host{$_} . ',' . $rechner;
      }
      else
      {
	$subsys_to_host{$_} = $rechner;
      }
    }
  }
  foreach (sort keys %subsys_to_host)
  {
    print "$_: $subsys_to_host{$_}\n";
  }
  myexit;
}


sub ListHosts
{
  # Liste aller Rechner ausgeben
  # Parameter: Beschreibungen-Objekt, ParameterListe-Objekt
  # Return:    -
  #
  my ($beschreibungen,$param) = @_;
  debug "Führe Aktion 'listhosts' aus.\n";

  my $bs;
  my $rechner;
  foreach $rechner ($param->Rechner)
  {
    # Testen, ob es eine Beschreibung zu dem Rechner gibt
    unless (defined ($bs = $beschreibungen->Betriebssystem($rechner)) )
    {
      warning $rechner,"Keine Beschreibung in hosts.sc vorhanden!\n";
      next;
    }
    print "$rechner\n";
  }
  myexit;
}


sub ListHostsWithRevision
{
  # Liste aller Rechner mit Revisionsnummer ausgeben
  # Parameter: Beschreibungen-Objekt, ParameterListe-Objekt
  # Return:    -
  #
  my ($beschreibungen,$param) = @_;
  debug "Führe Aktion 'listrevision' aus.\n";

  my $bs;
  my $rechner;
  foreach $rechner ($param->Rechner)
  {
    # Testen, ob es eine Beschreibung zu dem Rechner gibt
    unless (defined ($bs = $beschreibungen->Betriebssystem($rechner)) )
    {
      warning $rechner,"Keine Beschreibung in hosts.sc vorhanden!\n";
      next;
    }
    print "$rechner:",$beschreibungen->Revision($rechner),"\n";
  }
  myexit;
}


sub ListInterfaces
{
  # Liste aller Interfaces ausgeben
  # Parameter: Beschreibungen-Objekt, ParameterListe-Objekt
  # Return:    -
  #
  my ($beschreibungen,$param) = @_;
  debug "Führe Aktion 'listinterfaces' aus.\n";

  my $bs;
  my $rechner;
  foreach $rechner ($param->Rechner)
  {
    # Testen, ob es eine Beschreibung zu dem Rechner gibt
    unless (defined ($bs = $beschreibungen->Betriebssystem($rechner)) )
    {
      warning $rechner,"Keine Beschreibung in hosts.sc vorhanden!\n";
      next;
    }
    print $beschreibungen->Interface($rechner),"\n";
  }
  myexit;
}


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','modify');
  my $art;

  my ($style,$class);
  $style = '';
  if ($stylesheet ne '')
  {
    $style = "    <meta http-equiv=\"Content-Style-Type\" content=\"text/css\">\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"$stylesheet\">\n";
  }
  $class = '';
  if ($stylesheet_class ne '')
  {
    $class = " class=\"$stylesheet_class\"";
  }

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

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

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

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

  # Ausgabe-Datei für die Liste aller Files, die Sysconf verwaltet
  my $fh_file_list = FileHandle->new();
  open($fh_file_list, ">>$htmldir${slash}files0.txt") || logdie
  "Kann '$htmldir${slash}files0.txt' nicht zum Schreiben ".
  "oeffnen!\n";

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

  # Alle Dateiarten durchlaufen
  foreach $art (@artliste)
  { # foreach $art
    my @files = @{$files->Get($bs,$subsys,$art)};
    my ($fileobj, $file, $zielfile);

    print $fh "      <tr>\n        <td valign=top$class>$art</td>\n        <td$class>\n";

    foreach $fileobj (@files)
    { # foreach $fileobj
      $file       = $fileobj->Get('quelle');
      $zielfile   = $fileobj->Get('ziel') || '';
      $kommando   = $fileobj->Get('kommando');
      $hidden     = $fileobj->Get('hidden');

      if($hidden) # Wenn das File nicht ausgegeben werden soll...
      {
	print $fh "          $zielfile (versteckt)<br>\n";
	print $fh_file_list "$zielfile\n"; # Ausgabe für Gesamt-File-Liste
	next;
      }

      my $zielhtml = $zielfile;
      $zielhtml =~ s!/!_!g;
      # Installfiles, Initfiles, Files
      if ($art =~ /^install$|^init$|^file$/)
      {
	print $fh_file_list "$zielfile\n"; # Ausgabe für Gesamt-File-Liste

	# Nur kopieren, wenn es eine Text-Datei ist
	if (-T $file)
	{
	  copy($file, "$htmldir$slash$rechner${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;
      }
      # Modify-"Files"
      if ($art =~ /^modify$/)
      {
	print $fh_file_list "$zielfile\n"; # Ausgabe für Gesamt-File-Liste
	print $fh "          $zielfile (wird durch *cmd modifiziert)<br>\n";
	next;
      }
      # Inittemplates, Templates
      if ($art =~ /^inittemplate$|^template$/)
      {
	print $fh_file_list "$zielfile\n"; # Ausgabe für Gesamt-File-Liste

	# 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${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_list "$zielfile\n"; # Ausgabe für Gesamt-File-Liste

	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 = $kommando;
	  local $SIG{__WARN__} = \&FehlerInShellVariablenErsetzung;
	  eval $variablenCode.'$kommando =~ s/(\$\w+)/$1/eeg;';
	}
	print $fh_file_list "$kommando\n"; # Ausgabe für Gesamt-File-Liste

	print $fh "          <pre>$kommando</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) || logdie "Fehler beim Schliessen von '$htmldir${slash}$rechner${slash}$rechner-sub-$subsys.html'!\n";
}


sub FehlerInShellVariablenErsetzung
{
  # Signal-Handler für Fehler in Variablenersetzungen in Shell-Kommandos
  # Wichtig: Die globale (local) Variable
  #          $FehlerInShellVariablenErsetzung_Kommando muss gesetzt sein!
  # Parameter: -
  # Return:    -
  my $fehler = shift;
  if ($fehler =~ /Use of uninitialized value /)
  {
    error undef,"Fehler in der Variablenersetzung:\n".
    "'". $fehler . "'\n" .
    "Sie haben in folgendem Shell-Kommando eine Variable ".
    "verwendet,\n".
    "die Sie aber nicht definiert haben:\n".
    "'$FehlerInShellVariablenErsetzung_Kommando'\n";
  }
  else
  {
    error undef,$fehler .
    "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ässt das dann aufs System los.
  #
  my $file = shift;
  my $fmode = (stat($file))[2] & 07777;
  if ( ($fmode & ~0644) > 0 ) # mehr Rechte als "-rw-r--r--"
  {
    logdie "Sicherheitslücke: Das File '$file' ist für andere Benutzer schreibbar!\n"
  }
  unless (-o $file)
  {
    warning "Moegliche Sicherheitsluecke: Sie sind nicht Besitzer von '$file'\n"
  }
}


sub RemoteMkdir
{
  # Erstellt ein Verzeichnis auf einem anderen Rechner
  # Parameter: Rechner, Verzeichnisname
  # Seiteneffekte: Im globalen %__RemoteMkdir_Bereits_Erledigt wird vermerkt
  #                welche Verzeichnisse bereits angelegt wurden.

  my $rechner = shift;
  my $verz    = shift;
  my $tmp;
  my $sudo = '';
  my ($err, $result);

  my $use_sudo = $commands->Get($beschreibungen->Betriebssystem($rechner),'USE_SUDO');
  debug "USE_SUDO = $use_sudo\n";

  # Ist das Verzeichnis auf diesem Rechner bereits angelegt worden?
  return if defined $__RemoteMkdir_Bereits_Erledigt{"$rechner:$verz"};

  # Wenn das Verzeichnis "/" angelegt werden soll, dann nichts tun, denn dieses
  # gibt es ja überall bereits
  return if ($verz eq '/');

  my $mkdir= $commands->Get($beschreibungen->Betriebssystem($rechner),'MKDIR');

  my $socket = $beschreibungen->Socket($rechner);
  if (defined $socket)
  {
    if ($use_sudo)
    {
      $tmp = "$mkdir -p $verz";
      RemoteShell($rechner,$tmp);
    }
    else
    {
      debug_rsh "Sysconf-Client: PERL smart variation of \"mkdir -p $verz\"\n";
      # Das "-p" beim mkdir ist in Perl etwas länglich...
      $tmp = "unless (-d '$verz') { my \$path = '/'; foreach (split('/','$verz')) { next if /^\\s*\$/; \$path .=  \"\$_/\"; mkdir \$path;} }";
      ($err, $result) = $socket->Perl($tmp);
      if ($err != $RC_OK)
      {
	logdie "$rechner: Sysconf-Client liefert Fehler bei RemoteMkdir '$verz': $result\n";
      }
    }
  }
  else
  {
    $tmp = "$mkdir -p $verz";
    RemoteShell($rechner,$tmp);
  }
  # Wir merken uns, dass auf diesem Rechner dieses Verzeichnis schon angelegt
  # wurde. Das spart eine Menge Doppelarbeit ein!
  $__RemoteMkdir_Bereits_Erledigt{"$rechner:$verz"} = $TRUE;
}


sub RemoteRm
{
  # Löscht ein File auf einem anderen Rechner
  # Parameter: Rechner, Filename

  my $rechner = shift;
  my $file    = shift;
  my $tmp;
  my $sudo = '';
  my ($err, $result);

  my $use_sudo = $commands->Get($beschreibungen->Betriebssystem($rechner),'USE_SUDO');
  debug "USE_SUDO = $use_sudo\n";

  my $rm = $commands->Get($beschreibungen->Betriebssystem($rechner), 'RM');

  my $socket = $beschreibungen->Socket($rechner);
  if (defined $socket)
  {
    if ($use_sudo)
    {
      $tmp = "$rm $file";
      RemoteShell($rechner,$tmp);
    }
    else
    {
      debug_rsh "Sysconf-Client: PERL unlink '$file';\n";
      ($err, $result) = $socket->Perl("unlink '$file';");
      if ($err != $RC_OK)
      {
	logdie "$rechner: Sysconf-Client liefert Fehler bei 'unlink $file': $result\n";
      }
    }
  }
  else
  {
    $tmp = "$rm $file";
    RemoteShell($rechner,$tmp);
  }
}


sub RemoteCopy
{
  # Kopiert ein File auf einen anderen Rechner
  # Parameter: Quelle, Rechner, Ziel, Owner, Group, Permission
  # ZIEL muss mit einem Slash beginnen!

  my $quelle     = shift;
  my $rechner    = shift;
  my $ziel       = shift;
  my $owner      = shift;
  my $group      = shift;
  my $permission = shift;
  my $fehler     = "$rechner: RemoteShell liefert Fehler!";
  my $tmp;
  my $sudo = '';
  my $output = '';

  my $modify_by_cmd = $FALSE;
  $modify_by_cmd = $TRUE if $quelle eq '';

  my $use_sudo = $commands->Get($beschreibungen->Betriebssystem($rechner),'USE_SUDO');
  debug "USE_SUDO = $use_sudo\n";

  unless (defined $permission)
  {
    logdie "RemoteCopy mit zuwenig Parametern aufgerufen!\n";
  }

  if ($use_sudo)
  {
    $sudo = $commands->Get($beschreibungen->Betriebssystem($rechner),'SUDO');
    $sudo = $sudo . ' ';
  }

  if (! $modify_by_cmd)
  {
    # Vor dem Kopieren die Verzeichnisse anlegen
    $ziel =~ /(.*$slashsuch)[^$slashsuch]+/;
    my $pfad = $1;
    if ($pfad eq '')
    {
      error $rechner,"Das Ziel '$ziel' muss ein absoluter Dateiname mit Pfad sein!\n"
    }
    RemoteMkdir($rechner, $pfad);
  }

  my $cp   = $commands->Get($beschreibungen->Betriebssystem($rechner),'CP'   );
  my $diff = $commands->Get($beschreibungen->Betriebssystem($rechner),'DIFF' );
  my $rm   = $commands->Get($beschreibungen->Betriebssystem($rechner),'RM'   );
  my $chown= $commands->Get($beschreibungen->Betriebssystem($rechner),'CHOWN');
  my $chmod= $commands->Get($beschreibungen->Betriebssystem($rechner),'CHMOD');
  my $mv   = $commands->Get($beschreibungen->Betriebssystem($rechner),'MV'   );

  if ($backup)
  {
    # Sicherungskopie erstellen
    $tmp="/bin/sh -c \"$cp $ziel $ziel$backup_endung 2>/dev/null; true\"";
    RemoteShell($rechner,$tmp);
  }

  # Wenn es gar kein echtes File ist, sondern nur ein Merker, dass ein *cmd-
  # Kommando das File evtl. verändert, dann hier enden.
  return if $modify_by_cmd;

  # Wenn kein User angegeben ist, dann den User des Quellfiles nehmen
  if ($owner eq '')
  {
    my $uid = (stat($quelle))[4];
    $owner  = getpwuid($uid);
  }
  # Wenn keine Gruppe angegeben ist, dann die Gruppe des Quellfiles nehmen
  if ($group eq '')
  {
    my $gid = (stat($quelle))[5];
    $group  = getgrgid($gid);
  }

  # Dann File kopieren...

  ###
  ### Über Sysconf-Client
  ###
  my $socket = $beschreibungen->Socket($rechner);
  if (defined $socket)
  {
    if ($permission eq '')
    {
      # Permissions vom Quellfile lesen und auf Octal umwandeln
      $permission = sprintf "%lo", ( (lstat($quelle))[2] & 07777 );
    }
    # File übertragen
    if ($use_sudo)
    {
      # File erst nach /tmp übertragen
      $tmp = "FILE $quelle /tmp/sysconf.$$ permission=$permission";
      debug_rsh "Sysconf-Client: $tmp\n";
      my ($err, $result) = $socket->File($quelle, "/tmp/sysconf.$$", permission=>$permission);
      if ($err != $RC_OK)
      {
	logdie "$rechner: Sysconf-Client liefert Fehler bei '$tmp': $result\n";
      }
      # Owner / Gruppe übertragen
      $tmp = "$chown $owner:$group /tmp/sysconf.$$";
      RemoteShell($rechner,$tmp);
      if ($permission ne '')
      {
	$tmp = "$chmod $permission /tmp/sysconf.$$";
	RemoteShell($rechner,$tmp);
      }
      # Dann per sudo an den korrekten Ort verschieben
      $tmp = "$mv /tmp/sysconf.$$ $ziel";
      RemoteShell($rechner,$tmp);
    }
    else
    {
      $tmp = "FILE $quelle $ziel owner=$owner group=$group permission=$permission";
      debug_rsh "Sysconf-Client: $tmp\n";
      my ($err, $result) = $socket->File($quelle, $ziel, owner=>$owner, group=>$group, permission=>$permission);
      if ($err != $RC_OK)
      {
	logdie "$rechner: Sysconf-Client liefert Fehler bei '$tmp': $result\n";
      }
    }
  }
  else
  ###
  ### Über rsh/ssh ...
  ###
  {
    my $rcp = $beschreibungen->GetRCP($rechner);
    logdie "$rechner: Kein Remote-Copy-Zugriff moeglich!\n" if $rcp eq 'none';
    my $interface = $beschreibungen->Interface($rechner);
    my $user = $commands->Get($beschreibungen->Betriebssystem($rechner),'REMOTE_USER');

    # File übertragen
    if ($interface ne 'localhost')
    {
      $tmp = "$rcp -p $quelle $user\@$interface:$ziel 2>&1";
      if ($use_sudo)
      {
	# File erst nach /tmp übertragen
	$tmp = "$rcp -p $quelle $user\@$interface:/tmp/sysconf.$$ 2>&1";
      }
      debug_rsh "$tmp\n";
      if (! $dry_run)
      {
	$output = `$tmp`;
	logdie "$fehler bei '$tmp': '$output'\n" if ($?>>8);
      }
      # Dann ggf. per sudo an den korrekten Ort verschieben
      if ($use_sudo)
      {
	# Owner / Gruppe übertragen
	$tmp = "$chown $owner:$group /tmp/sysconf.$$";
	RemoteShell($rechner,$tmp);
	if ($permission ne '')
	{
	  $tmp = "$chmod $permission /tmp/sysconf.$$";
	  RemoteShell($rechner,$tmp);
	}
	# Und jetzt erst verschieben
	$tmp = "$mv /tmp/sysconf.$$ $ziel";
	RemoteShell($rechner,$tmp);
      }
    }
    else # Interface ist "localhost"
    {
      $tmp = "$sudo$cp $quelle $ziel";
      debug_rsh "$tmp\n";
      if (! $dry_run)
      {
	system($tmp);
	logdie "$fehler ('$tmp')\n" if ($?>>8);
      }
    }

    # Owner / Gruppe übertragen
    $tmp = "$chown $owner:$group $ziel";
    RemoteShell($rechner,$tmp);
    if ($permission ne '')
    {
      $tmp = "$chmod $permission $ziel";
      RemoteShell($rechner,$tmp);
    }
  }

  # Das Löschen der Backupfiles passiert seit Version 1.3.8 in der Funktion
  # RemoveBackupFiles(), die NACH allen *cmd-Files erst aufgerufen wird.
}


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

  my $quelle  = shift;
  my $rechner = shift;
  my $ziel    = shift;
  my $tmp;
  my $sudo = '';
  my ($err, $result);

  my $use_sudo = $commands->Get($beschreibungen->Betriebssystem($rechner),'USE_SUDO');
  debug "USE_SUDO = $use_sudo\n";

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

  my $ln = $commands->Get($beschreibungen->Betriebssystem($rechner), 'LN');
  my $rm = $commands->Get($beschreibungen->Betriebssystem($rechner), 'RM');
  my $cp = $commands->Get($beschreibungen->Betriebssystem($rechner), 'CP');

  if ($backup)
  {
    # Sicherungskopie erstellen
    $tmp="/bin/sh -c \"$cp $ziel $ziel$backup_endung 2>/dev/null; true\"";
    RemoteShell($rechner,$tmp);
  }

  my $socket = $beschreibungen->Socket($rechner);
  if (defined $socket)
  {
    if ($use_sudo)
    {
      # älteres Solaris kann kein "ln -sf". Deshalb "rm" und "ln -s" getrennt.
      $tmp = "/bin/sh -c \"$rm $ziel 2>/dev/null || true\"";
      RemoteShell($rechner,$tmp);
      $tmp = "$ln -s $quelle $ziel";
      RemoteShell($rechner,$tmp);
    }
    else
    {
      $tmp = "unlink '$ziel'; symlink '$quelle', '$ziel';";
      debug_rsh "Sysconf-Client: PERL $tmp\n";
      ($err, $result) = $socket->Perl($tmp);
      if ($err != $RC_OK)
      {
	logdie "$rechner: Sysconf-Client liefert Fehler bei '$tmp': $result\n";
      }
    }
  }
  else
  {
    # älteres Solaris kann kein "ln -sf". Deshalb "rm" und "ln -s" getrennt.
    $tmp = "/bin/sh -c \"$rm $ziel 2>/dev/null || true\"";
    RemoteShell($rechner,$tmp);
    $tmp = "$ln -s $quelle $ziel";
    RemoteShell($rechner,$tmp);
  }
}


sub RemoteShell
{
  # Startet eine RemoteShell
  # Parameter: Rechner, Kommando
  # Return:    Ausgabe des Shell-Kommandos (STDOUT)

  my $rechner  = shift;
  my $kommando = shift;
  my $err;
  my $result;
  my $sudo = '';

  my $use_sudo = $commands->Get($beschreibungen->Betriebssystem($rechner),'USE_SUDO');
  debug "USE_SUDO = $use_sudo\n";

  if ($use_sudo)
  {
    $sudo=$commands->Get($beschreibungen->Betriebssystem($rechner),'SUDO');
    # STDIN für sudo von /dev/null, da sudo sonst evtl. nach dem Kennwort
    # fragt, wenn die Einträge in der sudoers nicht passen.
    # Dann würde Sysconf an dieser Stelle hängen bleiben.
    $kommando = $sudo . ' ' . $kommando . '</dev/null';
  }

  my $socket = $beschreibungen->Socket($rechner);
  if (defined $socket)
  {
    debug_rsh "Sysconf-Client: COMMAND $kommando\n";
    ($err, $result) = $socket->Command($kommando);
    if ($err != $RC_OK)
    {
      logdie "$rechner: Sysconf-Client liefert Fehler bei '$kommando': $result\n";
    }
  }
  else
  {
    my $rsh = $beschreibungen->GetRSH($rechner);
    logdie "$rechner: Kein Remote-Shell-Zugriff moeglich!\n" if $rsh eq 'none';
    my $interface = $beschreibungen->Interface($rechner);
    my $user = $commands->Get($beschreibungen->Betriebssystem($rechner),'REMOTE_USER');

    my $rcommand = "$rsh $interface -l $user";
    $rcommand = '/bin/sh -c ' if ($interface eq 'localhost'); # Lokale Shell

    debug_rsh "$rcommand '$kommando' 2>&1\n";
    if (! $dry_run)
    {
      $result = `$rcommand '$kommando' 2>&1`;
      logdie "$rechner: RemoteShell liefert Fehler bei '$kommando'! Ausgaben des Skripts: '$result'\n" if ($?>>8);
    }
    else
    {
      $result = 'TRUE';
    }
  }
  return $result;
}


sub LocalShell
{
  # Startet eine LocalShell
  # Parameter: Kommando

  my $kommando = shift;
  my $result;

  debug_rsh $kommando."\n";
  if (! $dry_run)
  {
    $result = `$kommando 2>&1`;
    logdie "LocalShell liefert Fehler bei '$kommando'! Ausgaben des Skripts: '$result'\n" if ($?>>8);
  }
  else
  {
    $result = 'TRUE';
  }
  return $result;
}


sub ReadVariables
{
  # Einlesen der Variablen pro Rechner, welche in
  # $sysconfroot/variables/rechnername.var
  # stehen.
  #
  # Zusätzliche werden Sysconf-interne Variablen definiert, um z.B.
  # in *cmd-Files darauf zugreifen zu können:
  # SYSCONF_BACKUP mit den Werten TRUE und FALSE (Entspricht $backup)
  # SYSCONF_BACKUP_EXTENSION als String (Entspricht $backup_endung)
  # SYSCONF_OS                  entspricht dem OS-Eintrag         in hosts.sc
  # SYSCONF_INTERFACE           entspricht dem INTERFACE-Eintrag  in hosts.sc
  # SYSCONF_CLASSES             entspricht dem CLASSES-Eintrag    in hosts.sc
  # SYSCONF_HOSTS_SC_SUBSYSTEMS entspricht dem SUBSYSTEMS-Eintrag in hosts.sc
  # SYSCONF_SUBSYSTEMS          alle Subsysteme für diesen Rechner
  # SYSCONF_VARFILE             entspricht dem Inhalt des Variablen-Files
  #
  # Parameter: Rechnername, Beschreibungen-Objekt
  # Return:    Hash mit den Variablen

  my $rechner = shift;
  my $beschreibungen = shift;
  my $betriebssystem      = $beschreibungen->Betriebssystem($rechner);
  my $interface           = $beschreibungen->Interface($rechner);
  my @hosts_sc_subsysteme = $beschreibungen->HostsScSubsysteme($rechner);
  my @subsysteme          = $beschreibungen->Subsysteme($rechner);
  my @klassen             = $beschreibungen->Klassen($rechner);
  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);

  # Vorbelegung mit Sysconf-internen Variablen
  $hash{SYSCONF_BACKUP} = 'FALSE';
  $hash{SYSCONF_BACKUP} = 'TRUE' if $backup;
  $hash{SYSCONF_BACKUP_EXTENSION} = '';
  $hash{SYSCONF_BACKUP_EXTENSION} = $backup_endung if defined $backup_endung;
  $hash{SYSCONF_OS}                  = $betriebssystem;
  $hash{SYSCONF_INTERFACE}           = $interface;
  $hash{SYSCONF_CLASSES}             = join(' ', @klassen);
  $hash{SYSCONF_HOSTS_SC_SUBSYSTEMS} = join(' ', @hosts_sc_subsysteme);
  $hash{SYSCONF_SUBSYSTEMS}          = join(' ', @subsysteme);

  # dann noch in einem Hash alle Variablen (und zwar nicht die aufgelösten Includes!) speichern:
  $hash{SYSCONF_VARFILE} = '';

  my $fh = FileHandle->new();
  open($fh, $file) || logdie "Kann '$file' nicht oeffnen!\n";
  # Die Variablen-Dateien haben den Aufbau:
  # variable=wert
  # oder
  # variable=
  # oder
  # variable include FILENAME
  while(<$fh>)
  {
    next if /^\#/;   # Kommentare
    next if /^\s*$/; # Leerzeilen
    $hash{SYSCONF_VARFILE} = $hash{SYSCONF_VARFILE} . $_;
    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 "Das Include-File für die Variable '$var' namens '$einzelfile' im Variablenfile '$file' kann im Verzeichnis '$sysconfroot${slash}variables' nicht gelesen/gefunden werden!\n" unless -r $varfile;
        my $fh = FileHandle->new();
        open($fh, $varfile) || logdie "Fehler beim Oeffnen von '$varfile'!\n";
        {
          local $/ = undef; # In einem Stück einlesen
          $hash{$var} .= <$fh>;
        }
        close($fh); # Nur lesender open() => Kein Fehler möglich.
      }
    }
  }
  return %hash;
}


sub CheckAndAddDependencies
{
  # Überprüfung der Abhängigkeiten der Subsysteme und Hinzufügen fehlender
  # Subsysteme
  # Parameter:     Rechnername, Beschreibungen-Objekt, Liste der Subsysteme
  # Seiteneffekte: Zugriff auf globale Variable $dependencies!
  # Return:        Ergänzte Liste der Subsysteme in richtiger Reihenfolge

  my $rechner        = shift;
  my $beschreibungen = shift;
  my @subsysteme     = @_;
  my @erlaubt        = $beschreibungen->Subsysteme($rechner);
  my $betriebssystem = $beschreibungen->Betriebssystem($rechner);

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

  ReadDependencies('HARD',$betriebssystem);
  ReadDependencies('SOFT',$betriebssystem);

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

  my @alle_subs = ();
  my $sub;
  my $sub_soft;
  foreach $sub (@subsysteme)
  {
    debug "Gewuenschtes Subsys: $sub\n";
    # Weiche Abhängigkeiten
    if (defined $dep_soft{$sub}) # Wenn es weiche Abhängigkeiten gibt...
    {
      foreach $sub_soft (@{$dep_soft{$sub}})
      {
	debug "Weiche Abhängigkeit für $sub: $sub_soft\n";
	if ($ist_erlaubt{$sub_soft}) # ... und diese auch noch erlaubt sind
	{
	  debug "... ist erlaubt und wird hinzugefügt.\n";
	  push @alle_subs, $sub_soft;
	}
	else
	{
	  debug "... ist nicht erlaubt!\n";
	}
      }
    }

    # Harte Abhängigkeiten
    unless (defined $dep{$sub})
    {
      # Wenn es keine Abhängigkeit gibt, dann das Subsystem einfach nehmen
      push @alle_subs, $sub;
      next;
    }
    # Ansonsten die Abängigkeiten und das Subsystem nehmen
    push @alle_subs, @{$dep{$sub}}, $sub;
  }

  @alle_subs = KompaktiereSubsystemListe(@alle_subs);

  # Noch testen, ob auch wirklich alle Subsysteme erlaubt sind
  my @result = ();
  my $subsys;
  foreach $subsys (@alle_subs)
  {
    if (
	($ist_erlaubt{$subsys}) ||
	($subsys eq 'GLOBAL')   ||
	($subsys eq 'LAST')
       )
    {
      push @result, $subsys;
    }
    else
    {
      warning $rechner, "Subsystem '$subsys' ist nicht erlaubt!\n";
      warning $rechner, "Folglich besteht eine nicht aufgeloeste Abhaengigkeit!\n";
    }
  }

  return @result;
}


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

  my $hard_soft = shift; # Harte oder weiche Abhängigkeiten?
  my $depfilename;
  if ($hard_soft eq 'HARD')
  {
    $depfilename = 'dependencies.sc';
  }
  if ($hard_soft eq 'SOFT')
  {
    $depfilename = 'dependencies-soft.sc';
  }
  unless (defined $depfilename)
  {
    die "ReadDependencies(): Bitte als ersten Parameter HARD oder SOFT angeben!\n";
  }
  my $bs = shift;
  my $tmp;

  # Wenn für dieses Betriebssystem die Abhängigkeiten schon eingelesen sind
  # dann nichts tun.
  if ($hard_soft eq 'HARD')
  {
    return if defined ($dependencies->Get($bs));
  }
  if ($hard_soft eq 'SOFT')
  {
    return if defined ($dependencies_soft->Get($bs));
  }

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

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

  TesteDateiBesitzer($depfile);

  our %dep = ();

  ###
  ### Abhängigkeiten einlesen
  ###
  my $fh = FileHandle->new();
  open($fh, $depfile) || logdie "Kann '$depfile' nicht oeffnen!\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) ];
  }

  sub __ErsetzeSubsysteme
  {
    # Hilfsfunktion zum Auflösen der Subsystem-Abhängigkeiten
    my $subsysname = shift;
    my @deps = @_;

    my @result_deps = ();
    my $d;

    foreach $d (@deps)
    {
      if ($d eq $subsysname)
      {
	no warnings;
	logdie "Zyklische Abhängigkeit für Subsystem '$d' in '$depfile'!\n";
	use warnings;
      }
      no warnings;
      if (defined $dep{$d})
      {
	use warnings;
        # Abhängigkeit auflösen
        push @result_deps, __ErsetzeSubsysteme($d, @{$dep{$d}}), $d;
      }
      else
      {
	use warnings;
        # Subsystem direkt übernhemen
        push @result_deps, $d;
      }
      use warnings;
    }
    return @result_deps;
  }

  ###
  ### Rekursive Abhängigkeiten auflösen
  ###
  my @deps        = ();
  my @result_deps = ();
  my $subsys;
  foreach $subsys (keys %dep)
  {
    @result_deps = __ErsetzeSubsysteme( $subsys, @{$dep{$subsys}} );
    $dep{$subsys} = [ @result_deps ];
  }

  ###
  ### 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; # Nur lesender open() => Kein Fehler möglich.
  if ($hard_soft eq 'HARD')
  {
    $dependencies->Set($bs,%dep);
  }
  if ($hard_soft eq 'SOFT')
  {
    $dependencies_soft->Set($bs,%dep);
  }
}


sub CheckExclusions
{
  # Überprüfung der Ausschlüsse der Subsysteme
  # Parameter: Rechnername, Beschreibungen-Objekt, Liste der Subsysteme
  # Return:    Liste der Subsysteme ohne verbotene Kombinationen

  my $rechner        = shift;
  my $beschreibungen = shift;
  my @subsysteme     = @_;
  my $betriebssystem = $beschreibungen->Betriebssystem($rechner);

  ReadExclusions($betriebssystem);

  my %excl = ( defined $exclusions->Get($betriebssystem) ? 
              $exclusions->Get($betriebssystem) : () );

  # Alle gewünschten Subsysteme in ein Hash zum schnelleren Zugriff
  my %subsys = ();
  foreach (@subsysteme)
  {
    $subsys{$_}++;
  }

  my $sub;
  foreach $sub (@subsysteme) # Alle gewünschten Subsysteme
  {
    if (defined $excl{$sub}) # Wenn dieses Subsystem in der Ausschlussliste ist
    {
      foreach (@{$excl{$sub}}) # Welche Subsysteme sind ausgeschlossen?
      {
	if (defined $subsys{$_})
	{
	  warning $rechner,"Subsysteme $_ und $sub schliessen sich ",
	  "gegenseitig aus!\n";
	  warning $rechner,"Diese beiden Subsysteme werden deshalb nicht ",
	  "verteilt.\n";
	  delete $subsys{$_};
	  delete $subsys{$sub};
	}
      }
    }
  }
  # Das wäre zu einfach:
  #    return keys %subsys;
  # Da dann die Reihenfolge durcheinander käme. Also:
  my @result = ();
  foreach (@subsysteme)
  {
    push @result, $_ if defined $subsys{$_};
  }
  return @result;
}


sub ReadExclusions
{
  # File mit den gegenseitigen Ausschlüssen der Subsysteme einlesen
  # Es wird das globale Exclusions-Objekt gesetzt
  # Parameter: Betriebssystem
  # Return:    -

  my $exclfilename = 'exclusions.sc';
  my $bs = shift;
  my $tmp;

  # Wenn für dieses Betriebssystem die Ausschlüsse schon eingelesen sind
  # dann nichts tun.
  return if defined ($exclusions->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 $exclfile = "$sysconfroot$slash$bs$slash$exclfilename";
  unless (-r $exclfile)
  {
    $tmp = LinksAufloesen($exclfile);
    if (! defined $tmp)
    {
      logdie "Das File '$exclfile' existiert nicht oder ist nicht lesbar!\n";
    }
    $exclfile = $tmp;
  }

  TesteDateiBesitzer($exclfile);

  my %excl = ();

  ###
  ### Ausschlüsse einlesen
  ###
  my $fh = FileHandle->new();
  open($fh, $exclfile) || logdie "Kann '$exclfile' nicht oeffnen!\n";
  # Die Datei "exclusions.sc" hat den Aufbau:
  # S1 S2
  # S45 S27
  # ...
  # Bedeutung: Subsystem "S45" schliesst Subsystem "S27" aus.
  while(<$fh>)
  {
    next if /^\#/;   # Kommentare
    next if /^\s*$/; # Leerzeilen
    unless (/^(\S+)\s+(\S+)/)
    {
      logdie "Fehler in den Ausschlüssen in Zeile $.:\n",$_,"\n";
    }
    push @{$excl{$1}}, $2;
  }

  debug "-----\n";
  debug "Ausschlüsse:\n";
  foreach (keys %excl) { debug "$_ -> @{$excl{$_}}\n"; }

  close $fh; # Nur lesender open() => Kein Fehler möglich.
  $exclusions->Set($bs,%excl);
}


sub ReadCommands
{
  # File mit den Kommandos für das Ziel-Betriebssystem einlesen.
  # Alle remote-Kommandos werden dann mit korrektem absoluten Pfad aufgerufen.
  # Also z.B. /bin/cp unter Linux und /usr/bin/cp unter AIX.
  # Es wird das globale Commands-Objekt gesetzt.
  # Parameter: Betriebssystem
  # Return:    -

  my $bs = shift;
  my $commandfilename = 'commands.sc';
  # Default vorbelegen
  my %comm = (
	      CP           => 'cp',
	      RM           => 'rm',
	      LN           => 'ln',
	      DIFF         => 'diff',
	      CHOWN        => 'chown',
	      CHMOD        => 'chmod',
	      MKDIR        => 'mkdir',
	      MV           => 'mv',
	      SUDO         => 'sudo -H',
	      CMD_USER     => $main::EUID_USER,
	      CMD_GROUP    => $main::EGID_USER,
	      REMOTE_USER  => $main::EUID_USER,
              USE_SUDO     => $main::FALSE,
	      SSH_KEY      => '',
	     );
  my $tmp;

  # Wenn für dieses Betriebssystem die Kommandos schon eingelesen sind
  # dann nichts tun.
  return if defined ($commands->Get($bs,'CP'));

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

  my $commandsfile = "$sysconfroot$slash$bs$slash$commandfilename";
  unless (-r $commandsfile)
  {
    $commandsfile = main::LinksAufloesen($commandsfile);
  }

  if (! defined $commandsfile)
  {
    warning undef,"Das File '$sysconfroot$slash$bs$slash$commandfilename' existiert nicht oder ist nicht lesbar!\n";
    warning undef,"Es werden Remote-Kommandos ohne absolute Pfade verwendet!\n";
  }
  else
  {
    TesteDateiBesitzer($commandsfile);

    ###
    ### Kommandos einlesen
    ###
    my $fh = FileHandle->new();
    open($fh, $commandsfile) || logdie "Kann '$commandsfile' nicht oeffnen!\n";
    # Die Datei "commands.sc" hat den Aufbau:
    # CP=/usr/bin/cp
    # RM=/usr/bin/rm
    # LN=/usr/bin/ln
    # DIFF=/usr/bin/diff
    # CHOWN=/usr/bin/chown
    # CHMOD=/usr/bin/chmod
    # MKDIR=/usr/bin/mkdir
    # MV=/usr/bin/mv
    # SUDO=/usr/bin/sudo -H
    # CMD_USER=root
    # CMD_GROUP=system
    # REMOTE_USER=root
    # USE_SUDO=FALSE
    # SSH_KEY=

    while(<$fh>)
    {
      next if /^\#/;   # Kommentare
      next if /^\s*$/; # Leerzeilen

      $comm{CP}          = $1 if /^CP\s*=\s*(.+)/i;
      $comm{RM}          = $1 if /^RM\s*=\s*(.+)/i;
      $comm{LN}          = $1 if /^LN\s*=\s*(.+)/i;
      $comm{DIFF}        = $1 if /^DIFF\s*=\s*(.+)/i;
      $comm{CHOWN}       = $1 if /^CHOWN\s*=\s*(.+)/i;
      $comm{CHMOD}       = $1 if /^CHMOD\s*=\s*(.+)/i;
      $comm{MKDIR}       = $1 if /^MKDIR\s*=\s*(.+)/i;
      $comm{MV}          = $1 if /^MV\s*=\s*(.+)/i;
      $comm{SUDO}        = $1 if /^SUDO\s*=\s*(.+)/i;
      $comm{CMD_USER}    = $1 if /^CMD_USER\s*=\s*(.+)/i;
      $comm{CMD_GROUP}   = $1 if /^CMD_GROUP\s*=\s*(.+)/i;
      $comm{REMOTE_USER} = $1 if /^REMOTE_USER\s*=\s*(.+)/i;
      $comm{USE_SUDO}    = $main::TRUE  if /^USE_SUDO\s*=\s*TRUE/i;
      $comm{SSH_KEY}     = $1 if /^SSH_KEY\s*=\s*(.+)/i;
    }
    close $fh; # Nur lesender open() => Kein Fehler möglich.
  }

  debug "-----\n";
  debug "Remote-Kommandos:\n";
  foreach (keys %comm)
  {
    $commands->Set($bs,$_,$comm{$_});
    debug "$_ -> $comm{$_}\n";
  }
}


sub ReadFileAndExpandIncludes
{
  # Es wird das File, dessen Name als Parameter angegeben wird eingelesen und
  # der Inhalt als Return-Wert zurückgegeben. Dabei werden in dem File
  # enthaltene Include-Anweisungen aufgelöst.
  # Parameter: Name des Files
  # Return: Inhalt des Files nach Auflösen der Include-Anweisungen
  # Syntax für die Include-Anweisungen:
  # INCLUDE filename

  my $file = shift || logdie "ReadFileAndExpandIncludes() ohne Parameter aufgerufen!\n";
  my $include_files_history = '';

  my $pfad = $file;
  $pfad =~ s/files.sc$//;

  my $fh = FileHandle->new();
  open($fh, $file) || logdie "ReadFileAndExpandIncludes(): Kann '$file' nicht oeffnen!\n";
  my $inhalt = '';
  my $zeile;
  my $include_file;
  my $tmp;
  while ($zeile = <$fh>)
  {
    chomp($zeile);
    if ($zeile =~ /^INCLUDE\s+(\S+)\s*$/)
    {
      $include_file = $pfad . $1;
      unless (-r $include_file)
      {
	$tmp = LinksAufloesen($include_file);
	if (! defined $tmp)
	{
	  logdie "ReadFileAndExpandIncludes(): Das File '$include_file' bzw. '$include_file.LINK' existiert nicht oder ist nicht lesbar!\n";
	}
	$include_file = $tmp;
      }
      my $fhinc = FileHandle->new();
      $include_files_history .= "$include_file\n";
      open($fhinc, $include_file) || logdie "ReadFileAndExpandIncludes(): Kann '$include_file' nicht oeffnen!\n";
      {
	local $/ = undef;
	$tmp = <$fhinc>;
      }
      $inhalt = $inhalt . $tmp;
      close $fhinc;
    }
    else
    {
      $inhalt = $inhalt . $zeile . "\n";
    }
  }
  close $fh;

  # Jetzt könnten im Ergebnis noch INCLUDE-Anweisungen drin sein, die
  # durch die Includes dazugekommen sind. Diese müssen auch noch aufgelöst
  # werden.

  # Bei mehr als 20 verschachtelter Includes gehen wir von einem Zyklus aus
  # und brechen ab.
  my $max_rekursions_level = 20;
  my $rekursions_level = $max_rekursions_level;
  my $weitere_includes_vorhanden = $TRUE;
  while( ($rekursions_level-- > 0) && ($weitere_includes_vorhanden) )
  {
    debug "ReadFileAndExpandIncludes()-Rekursions-Level: $rekursions_level\n";
    $weitere_includes_vorhanden = $FALSE;
    $tmp = $inhalt;
    $inhalt = '';
    foreach $zeile (split(/^/m, $tmp)) # In einzelne Zeilen splitten
    {
      chomp($zeile);
      if ($zeile =~ /^INCLUDE\s+(\S+)\s*$/)
      {
	$weitere_includes_vorhanden = $TRUE;
	$include_file = $pfad . $1;
	unless (-r $include_file)
	{
	  $tmp = LinksAufloesen($include_file);
	  if (! defined $tmp)
	  {
	    logdie "ReadFileAndExpandIncludes(): Das File '$include_file' bzw. '$include_file.LINK' existiert nicht oder ist nicht lesbar!\n";
	  }
	  $include_file = $tmp;
	}
	my $fhinc = FileHandle->new();
	$include_files_history .= "$include_file\n";
	open($fhinc, $include_file) || logdie "ReadFileAndExpandIncludes(): Kann '$include_file' nicht oeffnen!\n";
	{
	  local $/ = undef;
	  $tmp = <$fhinc>;
	}
	$inhalt = $inhalt . $tmp;
	close $fhinc;
      }
      else
      {
	$inhalt = $inhalt . $zeile . "\n";
      }
    }
  }

  if ($rekursions_level <= 0)
  {
    logdie "ReadFileAndExpandIncludes(): Mehr als $max_rekursions_level ineinander verschachtelter INCLUDES. Wahrscheinlich fehlerhafter Zyklus, d.h. es includen sich wohl zwei Include-Files gegenseitig! Backtrace zur Fehlersuche:\n$include_files_history\n";
  }

  return $inhalt;
}


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',
		c     => 'cmdtemplate',
		m     => 'modify',
                # Links
                L     => 'link',
                instL => 'installlink',
                initL => 'initlink',
                # Shell-Kommandos
                S     => 'shell',
                instS => 'installshell',
                initS => 'initshell',
               );
  my $tmp;
  my $zeilennummer;

  # 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)
  {
    $tmp = LinksAufloesen($file);
    if (! defined $tmp)
    {
      logdie "Das File '$file' bzw. '$file.LINK' existiert nicht oder ist nicht lesbar!\n";
    }
    $file = $tmp;
  }

  TesteDateiBesitzer($file);

  ###
  ### "files.sc" einlesen
  ###
  my $fh = FileHandle->new();
  my $files_sc_inhalt = ReadFileAndExpandIncludes($file);
  debug "Inhalt von files.sc nach ReadFileAndExpandIncludes(): '$files_sc_inhalt'\nEnde von files.sc\n";
  # Variable wie ein File oeffnen. Das geht ab Perl v5.8.0
  open($fh, '<', \$files_sc_inhalt) || logdie "Kann in-memory-variable 'files_sc_inhalt' nicht zum Lesen oeffnen!\n";

  # Die Datei "files.sc" hat den Aufbau:
  # [subsys]
  # <fileentry>
  # ...
  # [subsys]: Beginn Beschreibung Subsystem namens "subsys"
  # <fileentry>: <type> <filename> <Zielfilename> <Optionales>
  # <patternblock>
  #
  # Weiteres siehe Doku.

  my $subsys = '';
  my ($art, $quelle, $ziel, $owner, $group, $perm);
  my @zeile;
  # 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
    # Zeile parsen. Folgender Aufbau z.B.:
    # f usr/local/bin/adm /tmp/adm permission=750 owner=g0014 group=users
    # Die ersten beiden Parameter sind fest, die restlichen optional.
    @zeile  = split(' ',$_);
    $art    = shift @zeile;
    $quelle = shift @zeile;
    $ziel   = $quelle;
    $owner  = '';
    $group  = '';
    $perm   = '';
    $hidden = $FALSE;
    foreach (@zeile)
    {
      if    (/owner=(.+)/)      { $owner  = $1; next; }
      elsif (/group=(.+)/)      { $group  = $1; next; }
      elsif (/permission=(.+)/) { $perm   = $1; next; }
      elsif (/hidden=(.+)/)     { $hidden = $1 eq 'TRUE' ? $TRUE : $FALSE; next; }
      else  { $ziel = $_; }
    }
    debug
    "Art: '"     ,defined $art    ? $art    : 'undef',"', ",
    "Quelle: '"  ,defined $quelle ? $quelle : 'undef',"', ",
    "Ziel: '"    ,defined $ziel   ? $ziel   : 'undef',"', ",
    "Owner: '"   ,defined $owner  ? $owner  : 'undef',"', ",
    "Group: '"   ,defined $group  ? $group  : 'undef',"', ",
    "Perm: '"    ,defined $perm   ? $perm   : 'undef',"'\n",
    "Hidden: '"  ,defined $hidden ? $hidden : 'undef',"'\n";

    # File-Art testen
    unless (defined $artkey{$art})
    {
      logdie "Die File-Art '$art' in '$file' Zeile $. ist ungültig!\n";
    }

    # Templates
    if ($artkey{$art} =~ /^template$|^inittemplate$/)
    {
      # 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) && (! -d $quelle) )
      {
	$tmp = $quelle;
	$zeilennummer = $.;
	$quelle = LinksAufloesen($quelle);
	if(! defined $quelle)
	{
	  error undef,"Kann File '$tmp' oder '$tmp.LINK' aus '$file' Zeile $zeilennummer nicht lesen!\n";
	  next;
	}
      }
      $files->Set($bs, $subsys, $artkey{$art}, 'quelle' => $quelle,
		   'ziel' => $ziel, 'owner' => $owner, 'group' => $group,
		   'permission' => $perm, 'hidden' => $hidden);

      # 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 schliessendes 'endpattern' in '$file' in Zeile $.!\n" }
        $ganzes_muster .= $pattern;
      }
      $templatepattern->Set($bs,$subsys,$quelle,$ziel,$ganzes_muster);
      next;
    }

    # Template-Pattern für *cmd-Files
    if ($artkey{$art} =~ /cmdtemplate/)
    {
      # $quelle sollte eines von diesen sein:
      # installcmd
      # reconfigcmd
      # startcmd
      # stopcmd
      # testinstallcmd
      # testruncmd
      # pre_localshell
      # post_localshell
      if( ($quelle ne 'installcmd')     &&
	  ($quelle ne 'reconfigcmd')    &&
	  ($quelle ne 'startcmd')       &&
	  ($quelle ne 'stopcmd')        &&
	  ($quelle ne 'testinstallcmd') &&
	  ($quelle ne 'testruncmd')     &&
	  ($quelle ne 'pre_localshell') &&
	  ($quelle ne 'post_localshell')
	)
      {
	error undef,"Fehler in Zeile $. in '$file': Quellfile-Angabe beim Typ 'c' muss eines aus diesen sein: 'installcmd', 'reconfigcmd', 'startcmd', 'stopcmd', 'testinstallcmd', 'testruncmd', 'pre_localshell', 'post_localshell'!\n";
	next;
      }

      # 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 schliessendes 'endpattern' in '$file' in Zeile $.!\n" }
        $ganzes_muster .= $pattern;
      }
      $cmd_templatepattern->Set($bs,$subsys,$quelle,'',$ganzes_muster);
      next;
    }

    # Modify-Marker werden fast wie normale Files behandelt, aber es existert
    # dazu kein echtes Quelle im Respository, also quelle=''!
    if ($artkey{$art} eq 'modify')
    {
      # Kein absoluter Pfad beim Ziel, also "/" ergänzen
      $ziel = $slash.$ziel unless ($ziel =~ /^\//);
      $files->Set($bs, $subsys, $artkey{$art}, 'quelle' => '',
		   'ziel' => $ziel, 'owner' => $owner, 'group' => $group,
		   'permission' => $perm, 'hidden' => $hidden);
    }

    # Prüfung für normale Files "file" etc.
    unless ($artkey{$art} =~ /link|shell|modify/)
    {
      # 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) && (! -d $quelle) )
      {
	$tmp = $quelle;
	$zeilennummer = $.;
	$quelle = LinksAufloesen($quelle);
	if(! defined $quelle)
	{
	  error undef,"Kann File '$tmp' oder '$tmp.LINK' aus '$file' Zeile $zeilennummer nicht lesen!\n";
	  next;
	}
      }
      $files->Set($bs, $subsys, $artkey{$art}, 'quelle' => $quelle,
		   'ziel' => $ziel, 'owner' => $owner, 'group' => $group,
		   'permission' => $perm, 'hidden' => $hidden);
    }

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

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

  }
  close $fh; # Nur lesender open() => Kein Fehler möglich.
}


sub LinksAufloesen
{
  ###
  ### Es werden die Links, die optional in *.LINK-Dateien angegeben werden
  ### können, aufgelöst.
  # Beispiel:
  # Filename: /etc/mail/sendmail.cf
  # Wenn /etc/mail/sendmail.cf nicht existiert, dann wird nach
  # /etc/mail/sendmail.cf.LINK gesucht und der Inhalt dieser Datei
  # ausgewertet. In dieser LINK-Datei muss der relative oder absolute Pfad
  # zu der eigentlichen Datei stehen. Das gilt auch für Verzeichnisse.
  #
  my $tmp;
  my $file = shift;
  debug "LinksAufloesen INPUT: '$file'\n";

  # Erst schauen, ob dieser Link schon einmal aufgelöst wurde (Cache)
  if (defined $LinksAufloesenCache{$file})
  {
    $tmp = $LinksAufloesenCache{$file};
    debug "LinksAufloesen OUTPUT: '$tmp' (aus dem Cache)\n";
    return $tmp;
  }
  debug "LinksAufloesen (nicht im Cache)\n";

  my @namensteile = split("/",$file);
  # In $namensteile[0] sollte immer '' sein.
  shift @namensteile; # Das erste '' entfernen.

  my $teil;
  my $link;
  my $linkfile;
  my $ergebnis = '';
  foreach $teil (@namensteile)
  {
    $tmp = $ergebnis . '/' . $teil;
    if (-e $tmp) # Existiert das?
    {
      # Übernehmen und fertig mit diesem Teil.
      $ergebnis = $tmp;
      next;
    }
    else # Wenns nicht existiert, dann nach *.LINK suchen
    {
      if (-r "$tmp.LINK")
      {
	$linkfile = "$tmp.LINK";
	$link = ReadLinkFile($linkfile);
	if (! defined $link)
	{
	  error undef,"Ungültiger Inhalt des Linkfiles '$linkfile'!\n";
	  return undef;
	}
	$tmp = $ergebnis . '/' . $link;
	if (-e $tmp)
	{
	  # Übernehmen und fertig mit diesem Teil.
	  $ergebnis = $tmp;
	  next;
	}
	else # Falsche Angabe im LINK-File oder zeigt es wieder auf LINK-File?
	{
	  # Ein weiterer Link?
	  if (-e "$tmp.LINK")
	  {
	    $ergebnis = LinksAufloesen($tmp);
	  }
	  else
	  {
	    error undef,"Link '$tmp' im File '$linkfile' existiert nicht!\n";
	    return undef;
	  }
	}
      }
      else # Fehler
      {
	# Hier keine Ausgabe der Fehlermeldung.
	return undef;
      }
    }
  }
  # Ergebnis im Cache ablegen, um beim nächsten Mal Zeit zu sparen
  $LinksAufloesenCache{$file} = $ergebnis;
  debug "LinksAufloesen OUTPUT:'$ergebnis'\n";
  return $ergebnis;
}


sub ReadLinkFile
{
  # Parameter: Filename des *.LINK-Files
  # Return:    Inhalt des Files (also der Link)
  #
  my $file = shift;
  my $fh = FileHandle->new();
  open($fh, $file) || error undef,"Kann '$file' nicht zum Lesen oeffnen!\n";
  my $inhalt = <$fh>; # Nur erste Zeile interessiert.
  chomp($inhalt);
  return ( $inhalt eq '' ? undef : $inhalt );
}


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 gewuenscht: ",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 $rechner, "Subsystem '$_' aus Klasse '$subsys' ist ",
                 "nicht erlaubt!\n"; }
        }
      }
      else
      {
        warning $rechner, "Subsystem '$subsys' ist nicht erlaubt!\n";
      }
    }
  }

  return @result;
}


sub ReadOptions
{
  # Es werden alle Kommandozeilen-Optionen, die mit "--" beginnen in einem Hash
  # zurückgegeben und aus ARGV entfernt.
  my @new_ARGV = ();
  my %options = ();
  foreach (@ARGV)
  {
    if (/^--([^=]+)$/) # z.B. --backup
    {
      $options{$1}++;
      next;
    }
    if (/^--([^=]+)=(.+)$/) # z.B. --logfile=/tmp/test.log
    {
      $options{$1} = $2;
      next;
    }
    # Die restlichen Parameter aufheben
    push @new_ARGV, $_;
  }
  @ARGV = @new_ARGV;
  return %options;
}


sub SetLogfile
{
  # Wenn die Logfile-Option angegeben ist, dann wird dieses Logfile verwendet.
  # Wenn nicht, dann wird versucht einen eventuellen LOGFILE-Eintrag aus
  # sysconfrc zu lesen.
  # Ohne Fehlerbehandlung, da diese Funktion sehr früh aufgerufen wird.
  # Andernfalls wird nach "/tmp/\L$appname\E.log" geloggt.
  #
  # Parameter: -
  # Return:    Filename des Logfiles
  #
  my $logfile = "/tmp/\L$appname\E.log"; # Standardeinstellung

  if (defined $options{logfile})
  {
    return $options{logfile};
  }

  # Wenn nicht als root aufgerufen, dann Usernamen in den Logfilenamen
  if ($UID != 0)
  {
    my $login = (getpwuid($UID))[0] || $UID;
    $logfile = "/tmp/\L$appname\E.$login.log";
  }

  my $file = './sysconfrc';
  $file = "$ENV{HOME}/.sysconfrc" unless -r $file;
  $file = '/etc/sysconfrc' unless -r $file;

  if(-r $file)
  {
    my $fh = FileHandle->new();
    open($fh, $file);
    while(<$fh>)
    {
      next if /^\#/; # Kommentare überspringen
      $logfile = $1 if /^LOGFILE\s*=\s*(.+)/i;
    }
    close $fh; # Nur lesender open() => Kein Fehler möglich.
  }

  return $logfile;
}


sub ReadConfigFile
{
  # Setzen von globalen Parametern aus dem Konfigurationsfile
  # Parameter: -
  # Return:    -
  #
  my $file = './sysconfrc';
  my $temp;
  $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
    if ( $sysconfroot eq '') # Wenns noch nicht bereits belegt worden ist.
    {
      $sysconfroot      = $1     if /^SYSCONF_ROOT\s*=\s*(.+)/i;
    }
    $htmldir          = $1     if /^HTMLDIR\s*=\s*(.+)/i;
    $stylesheet       = $1     if /^STYLESHEET\s*=\s*(.+)/i;
    $stylesheet_class = $1     if /^STYLESHEET_CLASS\s*=\s*(.+)/i;
    $stop_on_error    = $FALSE if /^STOP_ON_ERROR\s*=\s*FALSE/i;
    $client_port      = $1     if /^CLIENT_PORT\s*=\s*(\d+)/i;
    $temp             = $1     if /^LOGLEVEL\s*=\s*(\d+)/i;
    if ( !$logLevel_bereits_gesetzt && (defined $temp) )
    {
      $logLevel = $temp;
      $logLevel_bereits_gesetzt = $TRUE;
    }
    next;
  }
  close $fh; # Nur lesender open() => Kein Fehler möglich.
  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();

  my $classes_sc      = "$sysconfroot${slash}classes.sc";
  my $classes_sc_file = LinksAufloesen($classes_sc);

  open($Klassen_File, $classes_sc_file) || logdie "Kann $classes_sc bzw. $classes_sc.LINK bzw. $classes_sc_file nicht oeffnen!\n";

  TesteDateiBesitzer($classes_sc_file);

  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; # Nur lesender open() => Kein Fehler möglich.
  }
  return %result;
}


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

  my $beschreibungen = RechnerBeschreibung::new();
  my $fh = FileHandle->new();
  my ($rechner,$interface,$betriebssystem,$klassen,$subsysteme,$revision);
  my @Besch_File;
  my $zeile;

  my $hosts_sc      = "$sysconfroot${slash}hosts.sc";
  my $hosts_sc_file = LinksAufloesen($hosts_sc);

  open($fh, $hosts_sc_file) || logdie "Kann $hosts_sc bzw. $hosts_sc.LINK bzw. $hosts_sc_file nicht oeffnen!\n";

  TesteDateiBesitzer($hosts_sc_file);

  while (<$fh>) # Beschreibungsfile einlesen und bereinigen
  {
    next if /^\#/;   # Kommentare überspringen
    s/\#.*$//g;      # Kommentare entfernen (alles, was hinter '#' steht.)
    next if /^\s*$/; # Leerzeilen überspringen
    push @Besch_File, $_;
  }

  close $fh; # Nur lesender open() => Kein Fehler möglich.

  $zeile = shift @Besch_File;
  while (defined $zeile)
  {
    if ($zeile =~ m/^HOST\s+(\S+)\s*/i)
    {
      $rechner = $1;
      $zeile = shift @Besch_File; # Nächste Zeile lesen
      $interface  = '';
      $klassen    = '';
      $subsysteme = '';
      $revision   = '';
      # Solange kein neuer Block mit HOST beginnt
      while ((defined $zeile) && !($zeile =~ m/^HOST/))
      {
	if ($zeile =~ m/^(\S+)(\s+(.+)\s*)*/i)
	{
	  if ($1 eq 'INTERFACE' ) { $interface      = $3; }
	  if ($1 eq 'OS'        ) { $betriebssystem = $3; }
	  if ($1 eq 'CLASSES'   ) { $klassen        = $3; }
	  if ($1 eq 'SUBSYSTEMS') { $subsysteme     = $3; }
	  if ($1 eq 'REVISION')   { $revision       = $3; }
	  $zeile = shift @Besch_File;
	}
	else
	{
	  logdie "Fehler in Rechnerdefinition:\n",$zeile,"\n";
	}
      }
    }
    else
    {
      logdie "Fehler in Rechnerdefinition:\n",$zeile,"\n";
    }
    $interface  = $rechner if $interface eq '';
    $klassen    = ''       unless defined $klassen;
    $subsysteme = ''       unless defined $subsysteme;
    $revision   = ''       unless defined $revision;

    debug "host       = '$rechner'\n";
    debug "interface  = '$interface'\n";
    debug "os         = '$betriebssystem'\n";
    debug "classes    = '$klassen'\n";
    debug "subsystems = '$subsysteme'\n";
    debug "revision   = '$revision'\n";

    my @subsys = split(/\s+/,$subsysteme);
    $beschreibungen->SetHostsScSubsysteme($rechner,@subsys);

    my @klassen_liste = ();
    # Klassen expandieren
    my $klasse;
    foreach $klasse (split(/\s+/,$klassen))
    {
      unless (defined $klassendef{$klasse})
      { logdie "Klasse '$klasse' gibt es nicht!\n" }
      push @subsys, @{$klassendef{$klasse}};
      # und Liste aller Klassennamen speichern für SYSCONF_CLASSES:
      push @klassen_liste, $klasse;
    }

    $beschreibungen->SetBetriebssystem($rechner,$betriebssystem);
    $beschreibungen->SetInterface     ($rechner,$interface);
    $beschreibungen->SetSubsysteme    ($rechner,@subsys);
    $beschreibungen->SetRevision      ($rechner,$revision);
    # und Liste aller Klassennamen speichern für SYSCONF_CLASSES:
    $beschreibungen->SetKlassen       ($rechner,@klassen_liste);
  }
  return $beschreibungen;
}


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

  my ($aktion, $subsysteme, $rechner) = @_;
  my $result = ParameterListe::new();

  logdie "Falsche Anzahl Parameter!\n" unless defined $rechner;

  unless ($aktion =~ /^init$|^update$|^remove$|^start$|^stop$|^documentation$|^listhosts$|^listinterfaces$|^listreposfiles$|^listrevision$|^none$/)
  { logdie "Ungültige Aktion '$aktion'!\n" }

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

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

  $result->SetAktion($aktion);
  if ($aktion eq 'listreposfiles')
  {
    $result->SetBetriebssystem(@subsysteme);
  }
  else
  {
    $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();

  my $hosts_sc      = "$sysconfroot${slash}hosts.sc";
  my $hosts_sc_file = LinksAufloesen($hosts_sc);

  open($fh, $hosts_sc_file) || logdie "Kann $hosts_sc bzw. $hosts_sc.LINK bzw. $hosts_sc_file nicht oeffnen!\n";
  TesteDateiBesitzer($hosts_sc_file);
  while(<$fh>)
  {
    next if /^\#/; # Kommentare überspringen
    next unless /HOST\s+(\S+)\s*.*?/;
    push @rechner, $1;
  }
  close $fh; # Nur lesender open() => Kein Fehler möglich.
  return @rechner;
}


sub RechnerErreichbar
{
  # Test, ob ein Rechner anpingbar ist
  # Parameter: Rechnername
  # Return:    True oder False
  #

  return $TRUE if $dry_run; # Bei Trockenlauf ein dummy-TRUE zurückgeben.

  my $rechner = shift;
  my $ping;
  my $erfolgreich;

  if ($UID == 0)
  {
    $ping = Net::Ping->new('icmp');
    $erfolgreich = $ping->ping($rechner);
  }
  else
  {
    $ping = Net::Ping->new('tcp');
    $erfolgreich = $ping->ping($rechner);
    # Wenn ping nicht funktioniert, kann es auch sein, dass eine Firewall einen
    # tcp-ping nicht durchlässt. Daher probieren wir dann einfach einen ping
    # mit dem system()-Aufruf:
    unless ($erfolgreich)
    {
      # Ping mit Timeout 10 Sekunden
      system("ping -c 1 -w 10 $rechner >/dev/null");
      $erfolgreich = ( ($?>>8) > 0 ) ? $FALSE : $TRUE;
    }
    # Wenn gar kein Ping geht, dann Versuch auf Port 22 den Rechner zu
    # erreichen. (Manche Firewalls lassen nur TCP auf Port 22 durch und sonst
    # gar nichts.)
    unless ($erfolgreich)
    {
      system("nc -w 3 -z $rechner 22 2>/dev/null");
      $erfolgreich = ( ($?>>8) > 0 ) ? $FALSE : $TRUE;
    }
  }
  return ( $erfolgreich ? $TRUE : $FALSE );
}


######################################################################
### 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 schliessen, ...
  my $temp = '';
  my $error = defined $_[0] ? shift : $NormalExitCode;

  # Ausgabe der Fehlerzusammenfassung
  if (keys %Rechner_mit_Fehlern)
  {
    print "----------\nFehlerzusammenfassung (siehe auch $logfile):\n";
    foreach (keys %Rechner_mit_Fehlern)
    {
      $temp = "$_:\n" if ($_ ne '');
      print "$temp$Rechner_mit_Fehlern{$_}";
    }
  }

  if (keys %rechner_noch_nicht_abgearbeitet)
  {
    logprint "----------\nACHTUNG: Diese Rechner wurden NICHT erfolgreich konfiguriert:\n";
    print "----------\nACHTUNG: Diese Rechner wurden NICHT erfolgreich konfiguriert:\n";
    logprint join(",",sort keys %rechner_noch_nicht_abgearbeitet),"\n";
    print join(",",sort keys %rechner_noch_nicht_abgearbeitet),"\n";
  }

  logprint("$appname PID $$ Ende um ",date,"\n");
  close(LOG);

  # Fehlercode ggf. erhöhen, falls es Warnings oder Error gegeben hat
  if ( $Warning_aufgetreten && ($error < $WarnExitCode) )
  {
    $error = $WarnExitCode;
  }
  if ( $Error_aufgetreten && ($error < $ErrorExitCode) )
  {
    $error = $ErrorExitCode;
  }

  exit $error;
}


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


sub info
{
  # Es werden Informationen erzeugt
  logprint("INFO: ",@_) if $logLevelInfo;
}


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


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


sub warning
{
  # Parameter: Rechnername (oder undef für allgemeine Warnungen), Fehlermeldung
  # Return:    -
  #
  my $rechner = shift || '';
  $rechner .= ': ' unless $rechner eq '';
  # Falls Warnings eingeschaltet sind, dann werden Informationen erzeugt
  if ($logLevelWarn)
  {
    print    "WARN: $rechner",@_;
    logprint("WARN: $rechner",@_);
    $Warning_aufgetreten = $TRUE;
    $Rechner_mit_Fehlern{$rechner} .= 'WARN: '.join('',@_);
  }
}


sub error
{
  # Parameter: Rechnername (oder undef für allgemeine Fehler), Fehlermeldung
  # Return:    -
  #
  my $rechner = shift || '';
  $rechner .= ': ' unless $rechner eq '';
  # Falls Warnings eingeschaltet sind, dann werden Informationen erzeugt
  if ($logLevelError)
  {
    print    "ERROR: $rechner",@_;
    logprint("ERROR: $rechner",@_);
    $Error_aufgetreten = $TRUE;
  }

  $Rechner_mit_Fehlern{$rechner} .= 'ERROR: '.join('',@_);

  myexit($ErrorExitCode) if $stop_on_error;
}


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

  $Rechner_mit_Fehlern{'internal'} .= 'FATAL: '.join('',@_);

  myexit($FatalExitCode);
}


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


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;
  error '(internal)',"INTERNAL: $warnung     (Interne Warnungen deuten auf einen ",
  "moeglichen internen Programm-Fehler\n",
  "     hin oder auf eine fehlerhafte Eingabe, die nicht abgefangen ",
  "wurde!)\n";
}


sub catch_timeout
{
  ResetTimeout(0);
  print "Timeout-Signal im Prozess $$!\n";
  logprint "Timeout-Signal im Prozess $$!\n";
  myexit($ErrorExitCode);
}


sub ResetTimeout
{
  # Setzt den Alarm für den Timeout
  alarm($timeout);
}


sub BEGIN {
  my $i = 0;
  sub progress
  {
    ResetTimeout();
    my @zeichen = ('-','\\','|','/');
    print "$zeichen[$i++]\r" if $stdout_is_terminal;
    $i = 0 if $i==4;
  }
}


######################################################################
### 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 |
             listhosts | listinterfaces | listreposfiles | listrevision | none

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 einschliesslich
               '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
listhosts:     Liste aller Rechner (aus hosts.sc) ausgeben.
               Dabei wird der SUBSYSTEM-Parameter ignoriert und MACHINE muss
               ALL sein, also: sysconf listhosts xxx ALL
listinterfaces: Liste aller Interfaces der Rechner (aus hosts.sc) ausgeben.
               Dabei wird der SUBSYSTEM-Parameter ignoriert und MACHINE muss
               ALL sein, also: sysconf listinterfaces xxx ALL
listreposfiles: Liste aller Files im Repository für das angegebene Betriebs-
               system. Dabei wird der MACHINE-Parameter ignoriert. Beispiel:
               sysconf listreposfiles AIX-5.1 xxx
listrevision:  Liste aller Rechner (aus hosts.sc das Feld REVISION) mit der
               Revisionsnummer ausgeben.
               Dabei wird der SUBSYSTEM-Parameter ignoriert und MACHINE muss
               ALL sein, also: sysconf listrevision xxx ALL
none:          Keine Aktion

Optionen:
-wX: mit X=1-31 gibt den LOG-Level an. X ergibt sich als Summe der folgenden
     möglichen Werte (Bitmaske):
     1:  alle Fehler (Genaue Fehlerbeschreibungen)
     2:  alle Warnungen (sollte man beachten)
     4:  alle Informationen (ganz informativ)
     8:  alle Debug-Informationen (ausführlicher Status, etc.)
     16: alle Remote-Shell-Aufrufe und Sysconf-Client-Aufrufe
     (Default ist 7, also 1+2+4)
-b : Erstellt Backup-Dateien auf dem Zielrechner, wenn Änderungen stattgefunden
     haben. Die Backup-Dateien enden mit einem Zeitstempel:
     '.JJJJMMTTHHMMSS.SC' z.B. '.19991019121531.SC'
--logfile=FILENAME : Logging erfolgt in die Datei LOGFILE.
                     Es kann auch ein Pipe angegeben werden, z.B.
                     --logfile=\"|/bin/cronolog /var/log/sysconf/\%Y/\%m/\%d\"
--sysconf_root=DIRNAME : Angabe des Verzeichnisses in dem sich das Sysconf-
                         Repository befindet.
--dry-run : Trockenlauf, d.h. es wird alles ausgeführt nur die Client-Zugriffe
            nicht, also weder ssh-, rsh- noch Sysconf-Client-Zugriffe.
--list_hosts_per_os=OS : Listet alle Rechner mit dem Betriebssystem OS auf.
                         z.B.: --list_hosts_per_os=Gentoo2007 none none none
--list_hosts_subsys : Listet alle Rechner mit den zugeordneten Subsystemen auf.
                      z.B.: --list_hosts_subsys none ALL ALL
--list_subsys_hosts : Listet alle Subsysteme und welche Rechner sie bekommen.
                      z.B.: --list_subsys_hosts none ALL ALL

Beispiele:
'Führe einen Update von Sendmail auf Rechner zeus durch':
sysconf update sendmail zeus
'Führe einen Update von allen Subsystemen auf Rechner osiris durch':
sysconf update ALL osiris
'Verteile alle Subsysteme auf alle Rechner':
sysconf update ALL ALL
'Initialisiere alle Subsysteme auf Rechner hugo':
sysconf init ALL hugo
'Führe einen Update von Sendmail und Syslog auf den Rechnern zeus, osiris und
hugo durch':
sysconf update sendmail,syslog zeus,osiris,hugo
Repository auf Konsistenz testen, ohne die Client-Rechner zu verändern:
sysconf --dry-run update ALL ALL

";
  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:
    system('perl -i -pe \'s/&lt;(.?)EM&gt;/<${1}EM>/g; s/ä/&auml;/g; s/ö/&ouml;/g; s/ü/&uuml;/g; s/Ä/&Auml;/g; s/Ö/&Ouml;/g; s/Ü/&Uuml;/g; s/ß/&szlig;/g; \' '."\L$appname\E.html");
  }
  if ($art eq 'latex')
  {
    which('pod2latex') || die "Leider kein 'pod2latex' verfügbar!\n";
    system("pod2latex < $0");
    rename '.tex', 'sysconf.tex';
    open(FH,"\L$appname\E.tex");
    @tex = <FH>;
    close(FH); # Nur lesender open() => Kein Fehler möglich.
    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) || logdie "Fehler beim Schliessen von '\L$appname\E.tex'!\n";
  }
}


######################################################################
### 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} = [ @_ ];
}

sub Betriebssystem # Nur für die Aktion "listreposfiles" gültig!
{
  my $objekt = shift;
  return $objekt->{betriebssystem};
}

sub SetBetriebssystem # Nur für die Aktion "listreposfiles" gültig!
{
  my $objekt = shift;
  $objekt->{betriebssystem} = shift;
}


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

package RechnerBeschreibung;

use Carp;

sub new
{
  my $daten = {
# Das sieht so aus:
#              rechnername => {
#                              BS         => Betriebsystem,
#                              INTERFACE  => Interface,
#                              HOSTS_SC_SUB => Subsystem-Liste aus hosts.sc,
#                              SUB        => Liste von Subsystemen,
#                              RSH        => Remoteshell,
#                              RCP        => Remotecopy,
#                              USE_SOCKET => True, wenn SOCKET-Kommunikation
#	                                     möglich ist,
#                              SOCKET     => RemoteClient-Objekt,
#                              KLASSEN    => Liste aller Klassen,
#                             }
              };
  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::FatalExitCode);
  }
  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::FatalExitCode);
  }
  $objekt->{$rechner}->{BS} = shift;
}

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

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

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

sub SetRevision
{
  my $objekt  = shift;
  my $rechner = shift;
  unless (defined $rechner)
  {
    carp("RechnerBeschreibung::SetRevision() ohne Parameter ".
    "aufgerufen!\n");
    main::myexit($main::FatalExitCode);
  }
  $objekt->{$rechner}->{REVISION} = 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::FatalExitCode);
  }
  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::FatalExitCode);
  }
  $objekt->{$rechner}->{SUB} = [ @_ ];
}

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

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

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

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

sub GetRSH
{
  my $objekt    = shift;
  my $rechner   = shift;
  my $interface = '';
  my $temp;
  unless (defined $rechner)
  {
    carp("RechnerBeschreibung::GetRSH() ohne Parameter aufgerufen!\n");
    main::myexit($main::FatalExitCode);
  }
  # 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
  {
    main::info("Zugriffsmethode SHELL auf Rechner '$rechner' wird ermittelt...\n");
    $temp = '';
    if (defined $objekt->{$rechner}->{INTERFACE})
    {
      $interface = $objekt->{$rechner}->{INTERFACE};
    }
    else
    {
      $interface = $rechner;
    }
    unless ($interface eq 'localhost')
    {
      my $user;
      $user = $main::commands->Get($main::beschreibungen->Betriebssystem($rechner),'REMOTE_USER') || do
      {
	# Erst noch die commands.sc einlesen...
	main::ReadCommands($main::beschreibungen->Betriebssystem($rechner));
	$user = $main::commands->Get($main::beschreibungen->Betriebssystem($rechner),'REMOTE_USER');
      };
      my $ssh_key = $main::commands->Get($main::beschreibungen->Betriebssystem($rechner),'SSH_KEY');
      main::debug_rsh("TesteRemoteShell($interface, remote_user=>$user, ssh_key=>$ssh_key)\n");
      if (! $main::dry_run)
      {
	$temp = main::TesteRemoteShell($interface, remote_user=>$user,
				       ssh_key=>$ssh_key);
      }
      else
      {
	$temp = 'ONLY_DRY_RUN'; # Beim Trockenlauf mit Dummy-Wert belegen.
      }
      unless ($temp =~ /^ssh$|^rsh$|^ONLY_DRY_RUN$/)
      {
        carp("RechnerBeschreibung::GetRSH(): Kann weder mit rsh noch mit ssh ".
             "auf den Rechner '$rechner' ueber das Interface '$interface' ohne".
	     " Passwort zugreifen!\n");
      }
      if ($ssh_key ne '')
      {
	$ssh_key = "-i $ssh_key ";
      }
      if ($temp =~ /^ssh$/)
      {
	$temp .= " ${ssh_key}-x";
      }
    }
    main::debug "Ermittelte RSH: ".$temp."\n";
    $objekt->{$rechner}->{RSH} = $temp;
    main::info("Zugriff auf Rechner '$rechner' durch '",$temp,"'\n");
    return $temp;
  }
}

sub GetRCP
{
  my $objekt    = shift;
  my $rechner   = shift;
  my $interface = '';
  my $temp;
  unless (defined $rechner)
  {
    carp("RechnerBeschreibung::GetRCP() ohne Parameter aufgerufen!\n");
    main::myexit($main::FatalExitCode);
  }
  # 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
  {
    main::info("Zugriffsmethode COPY auf Rechner '$rechner' wird ermittelt...\n");
    $temp = '';
    if (defined $objekt->{$rechner}->{INTERFACE})
    {
      $interface = $objekt->{$rechner}->{INTERFACE};
    }
    else
    {
      $interface = $rechner;
    }
    unless ($interface eq 'localhost')
    {
      my $user;
      $user = $main::commands->Get($main::beschreibungen->Betriebssystem($rechner),'REMOTE_USER') || do
      {
	# Erst noch die commands.sc einlesen...
	main::ReadCommands($main::beschreibungen->Betriebssystem($rechner));
	$user = $main::commands->Get($main::beschreibungen->Betriebssystem($rechner),'REMOTE_USER');
      };
      my $ssh_key = $main::commands->Get($main::beschreibungen->Betriebssystem($rechner),'SSH_KEY');
      main::debug_rsh("TesteRemoteCopy($interface, remote_user=>$user, ssh_key=>$ssh_key)\n");
      if (! $main::dry_run)
      {
	$temp = main::TesteRemoteCopy($interface, remote_user=>$user,
				      ssh_key=>$ssh_key);
      }
      else
      {
	$temp = 'ONLY_DRY_RUN'; # Beim Trockenlauf mit Dummy-Wert belegen.
      }

      if ($temp eq 'none')
      {
	carp("RechnerBeschreibung::GetRCP(): Kann weder mit rcp, scp ".
	     "noch rsync auf den Rechner '$rechner' ueber das Interface '$interface' ohne Passwort zugreifen!\n");
	main::myexit($main::FatalExitCode);
      }
    }
    main::debug "Ermitteltes RCP: ".$temp."\n";
    $objekt->{$rechner}->{RCP} = $temp;
    main::info("Zugriff auf Rechner '$rechner' durch '",$temp,"'\n");
    return $temp;
  }
}

sub Socket
{
  # Return: RemoteClient-Objekt wenn Verbindunsaufbau erfolgreich, sonst undef
  #
  my $objekt    = shift;
  my $rechner   = shift;
  my $interface = '';
  my $remote;
  my $temp;

  unless (defined $rechner)
  {
    carp("RechnerBeschreibung::Socket() ohne Parameter aufgerufen!\n");
    main::myexit($main::FatalExitCode);
  }
  # Bei Trocklauf gleich abbrechen
  if ($main::dry_run)
  {
    return undef;
  }
  # Wenn das Socket schon bekannt ist, dann gleich zurückgeben
  if (defined $objekt->{$rechner}->{SOCKET})
  {
    main::debug_rsh "Cached SOCKET: ".$objekt->{$rechner}->{SOCKET}."\n";
    return $objekt->{$rechner}->{SOCKET};
  }
  # Wenn die Verbindung schon getestet wurde, aber nicht möglich war, dann
  # steht in USE_SOCKET $FALSE!
  elsif ( defined ($objekt->{$rechner}->{USE_SOCKET}) &&
	  ($objekt->{$rechner}->{USE_SOCKET}) == 0)
  {
    return undef;
  }
  # ansonsten erst versuchen, die Verbindung aufzubauen
  else
  {
    main::info("Zugriffsmethode CLIENT auf Rechner '$rechner' wird ermittelt...\n");
    if (defined $objekt->{$rechner}->{INTERFACE})
    {
      $interface = $objekt->{$rechner}->{INTERFACE};
    }
    else
    {
      $interface = $rechner;
    }
    $objekt->{$rechner}->{SOCKET} = undef;
    $objekt->{$rechner}->{USE_SOCKET} = 0; # FALSE
    unless ($interface eq 'localhost')
    {
      if ($main::client_port)
      {
	($err, $remote) = RemoteClient::new($interface, $main::client_port);
	if ($err != $main::RC_OK)
	{
	  main::debug_rsh("Verbindungsaufbau zu Sysconf-Client nicht moeglich: $remote\n");
	  return undef;
	}
      }
      else # Wenn gar kein Client-Port definiert ist
      {
	main::debug_rsh("Verbindungsaufbau zu Sysconf-Client wird nicht versucht, da kein Client-Port angegeben ist.\n");
	return undef;
      }
      ($err, $temp) = $remote->Version();
      if ($err == $main::RC_OK)
      {
	main::debug_rsh("Verbindung zu Sysconf-Client funktioniert.\n");
	if ($RemoteClient::VERSION != $temp)
	{
	  main::info("Die Sysconf-Client-Protokoll-Version von Client und\n");
	  main::info("Server (auf $rechner) stimmen nicht ueberein!\n");
	  main::info("Server: $temp, Client: $RemoteClient::VERSION. Das kann zu Problemen fuehren.\n");
	}
      }
      else
      {
	# An dieser Stelle interessiert die Meldung nicht sonderlich, also
	# aus allen Fehler-Levels nur ein DEBUG machen:
	main::info("Verbindung zu Sysconf-Client auf '$rechner' nicht moeglich: $temp\n");
	undef $remote;
      }
      if (defined $remote)
      {
	$objekt->{$rechner}->{SOCKET} = $remote;
	# Vermerken, dass in SOCKET ein gültiges Filehandle steht:
	$objekt->{$rechner}->{USE_SOCKET} = 1; # TRUE
	main::info("Zugriff auf Rechner '$rechner' durch Sysconf-Client.\n");
      }
    }
    return $remote;
  }
}

sub Socket_Destroy
{
  # Beendet die Socket-Verbindung.
  # Return: -
  #
  my $objekt    = shift;
  my $rechner   = shift;

  unless (defined $rechner)
  {
    carp("RechnerBeschreibung::Socket_Destroy() ohne Parameter aufgerufen!\n");
    main::myexit($main::FatalExitCode);
  }
  # Nur wenn das Socket schon bekannt ist, dann Verbindung beenden.
  if (defined $objekt->{$rechner}->{SOCKET})
  {
    main::debug_rsh "Socket_Destroy($rechner)\n";
    $objekt->{$rechner}->{SOCKET}->Quit();
    $objekt->{$rechner}->{SOCKET} = undef;
    $objekt->{$rechner}->{USE_SOCKET} = 0; # FALSE
  }
}

######################################################################
### 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::FatalExitCode);
  }
  # 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::FatalExitCode);
  }
  $objekt->{$betriebssystem} = { %hash };
}


######################################################################
### Exclusions-Objekt
######################################################################

package Exclusions;

use Carp;

sub new
{
  my $daten = {
# Das sieht so aus:
#  betriebssystem => {
#                     Subsystem => Liste von damit unverträglichen Subsystemen,
#                     Subsystem => Liste von damit unverträglichen Subsystemen,
#                     Subsystem => Liste von damit unverträglichen Subsystemen,
#                    }
              };
  bless $daten, 'Exclusions';
  return $daten;
}

sub Get
{
  my $objekt         = shift;
  my $betriebssystem = shift;
  unless (defined $betriebssystem)
  {
    carp("Exclusions::Get() ohne Parameter aufgerufen!\n");
    main::myexit($main::FatalExitCode);
  }
  # 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("Exclusions::Set() ohne Parameter aufgerufen!\n");
    main::myexit($main::FatalExitCode);
  }
  $objekt->{$betriebssystem} = { %hash };
}


######################################################################
### Commands-Objekt
######################################################################

package Commands;

use Carp;

sub new
{
  my $daten = {
# Das sieht so aus:
#              betriebssystem => {
#                                  CP          => "/usr/bin/cp",
#                                  RM          => "/usr/bin/rm",
#                                  LN          => "/usr/bin/ln",
#                                  DIFF        => "/usr/bin/diff",
#                                  CHOWN       => "/usr/bin/chown",
#                                  CHMOD       => "/usr/bin/chmod",
#                                  MKDIR       => "/usr/bin/mkdir",
#                                  MV          => "/usr/bin/mv",
#                                  SUDO        => "/usr/bin/sudo -H",
#                                  CMD_USER    => "root",
#                                  CMD_GROUP   => "system",
#                                  REMOTE_USER => "root",
#                                  USE_SUDO    => "FALSE",
#                                  SSH_KEY     => "",
#                                }
              };
  bless $daten, 'Commands';
  return $daten;
}

sub Get
{
  my $objekt         = shift;
  my $betriebssystem = shift;
  my $kommando       = shift;
  unless (defined $kommando)
  {
    carp("Commands::Get() ohne Parameter aufgerufen!\n");
    main::myexit($main::FatalExitCode);
  }
  return $objekt->{$betriebssystem}->{$kommando};
}

sub Set
{
  my $objekt         = shift;
  my $betriebssystem = shift;
  my $kommando       = shift;
  my $wert           = shift;

  unless (defined $wert)
  {
    carp("Commands::Set() ohne Parameter aufgerufen!\n");
    main::myexit($main::FatalExitCode);
  }
  $objekt->{$betriebssystem}->{$kommando} = $wert;
}

######################################################################
### SingleFile-Objekt
######################################################################

# zur Speicherung eines einzelnen Files bzw. Kommandos

package SingleFile;

use Carp;

sub new
{
  my $daten = {
#	       quelle     => ...
#	       ziel       => ...
#	       owner      => ...
#	       group      => ...
#	       permission => ...
#	       kommando   => ...
#	       hidden     => ...
	      };
  bless $daten, 'SingleFile';
  return $daten;
}


sub Get
{
  # Parameter: key

  my ($objekt, $key) = @_;
  unless (defined $key)
  {
    carp("SingleFile::Get() mit zuwenig Parameter aufgerufen!\n");
    main::myexit($main::FatalExitCode);
  }
  return $objekt->{$key};
}

sub Set
{
  # Parameter: (key, value)

  my ($objekt, $key, $value) = @_;
  unless (defined $value)
  {
    carp("SingleFile::Set() mit zuwenig Parameter aufgerufen!\n");
    main::myexit($main::FatalExitCode);
  }
  $objekt->{$key} = $value;
}

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

package Files;

use Carp;

sub new
{
  my $daten = {
# Das sieht so aus:
#     betriebssystem => {
#                         SUB => {
#                                  install      => Liste von SingleFiles
#                                  init         => Liste von SingleFiles
#                                  inittemplate => Liste von SingleFiles
#                                  file         => Liste von SingleFiles
#                                  template     => Liste von SingleFiles
#                                }
#                       }
              };
  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::FatalExitCode);
  }
  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 @files = @{$files->Get($bs,$subsys,$art)};
  # dabei stehen in @files dann lauter SingleFile-Objekte

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

sub Set
{
  # Parameter: (Betriebssystem, Subsystem, Art des Files, ParameterHash)
  # Die Art des Files ist "install", "init", inittemplate", "file" oder
  # "template".
  # Der ParameterHash kann so aussehen:
  # quelle     => ...
  # ziel       => ...
  # kommando   => ...
  # owner      => ...
  # group      => ...
  # permission => ...
  # hidden     => ...
  # wobei "quelle" (bzw. "kommando") und "ziel" Pflichtfelder sind.
  # Return: -
  #
  my ($objekt, $betriebssystem, $subsystem, $art, %param) = @_;
  unless ( (defined $param{quelle}) || (defined $param{kommando}) )
  {
    carp("Files::Set() ohne 'quelle' oder 'kommando' aufgerufen!\n");
    main::myexit($main::FatalExitCode);
  }
  if ( (defined $param{quelle}) && (! defined $param{ziel}) )
  {
    carp("Files::Set() mit 'quelle' aber ohne 'ziel' aufgerufen!\n");
    main::myexit($main::FatalExitCode);
  }
  unless ( IstArtGueltig($objekt,$art) )
  {
    carp("Files::Set() mit falschem Art-Parameter aufgerufen!\n");
    main::myexit($main::FatalExitCode);
  }
  my $singlefile = SingleFile::new();

  my $temp = $param{quelle} || $param{kommando};
  # Der Zielfile-Liste hinzufügen
  foreach (keys %param)

  {
    $singlefile->Set($_, $param{$_});
  }
  push @{$objekt->{$betriebssystem}->{$subsystem}->{$art}}, $singlefile;
}


sub GetFileList
{
  # Liefert eine komplette Liste aller Files im Repository.
  # Links und Shellkommandos werden nicht mit aufgelistet!
  # Parameter: Betriebssystem
  # Return: Referenz auf Liste von Singlefile-Objekten
  #
  # Typischer Zugriff:
  #   my @files = @{$files->Get($bs,$subsys,$art)};
  # dabei stehen in @files dann lauter SingleFile-Objekte

  my ($objekt, $betriebssystem) = @_;
  unless (defined $betriebssystem)
  {
    carp("Files::GetFileList() mit zuwenig Parameter aufgerufen!\n");
    main::myexit($main::FatalExitCode);
  }
  my $subsystem;
  my $art;
  my @result_list = ();
  foreach $subsystem (keys %{$objekt->{$betriebssystem}})
  {
    foreach $art (keys %{$objekt->{$betriebssystem}->{$subsystem}})
    {
      next if $art eq 'link';
      next if $art eq 'initlink';
      next if $art eq 'shell';
      next if $art eq 'installshell';
      next if $art eq 'initshell';
      push @result_list, @{$objekt->{$betriebssystem}->{$subsystem}->{$art}};
    }
  }
  return \@result_list;
}


sub IstArtGueltig
{
  shift;
  my $art = shift;
  # "cmdtemplate" ist hier bewusst nicht enthalten, da das kein normales File
  # ist.
  return ( $art =~ /^install$|^init$|^inittemplate$|^file$|^modify$|^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::FatalExitCode);
  }
  unless (defined $objekt->{$betriebssystem}->{$subsystem}->{$quelle}->{$ziel})
  {
    carp("TemplatePattern::Get() liefert undef()! ".
         "Aufruf war: '$betriebssystem','$subsystem','$quelle','$ziel'\n");
    main::myexit($main::FatalExitCode);
  }
  return ( $objekt->{$betriebssystem}->{$subsystem}->{$quelle}->{$ziel} );
}

sub Get_nocheck
{
  # Parameter: (Betriebssystem, Subsystem, Quellfile, Zielfile)
  # Return:    Pattern
  # Es wird die Existenz nicht geprüft, sondern ggf. undef() zurückgegeben.

  my ($objekt, $betriebssystem, $subsystem, $quelle, $ziel) = @_;
  unless (defined $ziel)
  {
    carp("TemplatePattern::Get() mit zuwenig Parameter aufgerufen!\n");
    main::myexit($main::FatalExitCode);
  }
  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::FatalExitCode);
  }
  if ( defined $objekt->{$betriebssystem}->{$subsystem}->{$quelle}->{$ziel} )
  {
    carp("TemplatePattern::Set(): Doppelter Eintrag! ".
         "Aufruf war: '$betriebssystem','$subsystem','$quelle','$ziel'\n");
    main::myexit($main::FatalExitCode);
  }
  $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, Subsystem)".
         "mit zu wenig Parameter aufgerufen!\n");
    main::myexit($main::FatalExitCode);
  }
  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";
  my $remote_tmp_file = "${main::slash}tmp${main::slash}$command.$$";
  my $tmp;

  unless (-r $kommandofile)
  {
    $tmp = main::LinksAufloesen($kommandofile);
    if (! defined $tmp)
    {
      main::logdie "Kann File '$kommandofile' bzw. '$kommandofile.LINK' nicht lesen!\n";
    }
    $kommandofile = $tmp;
  }

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

  # Zur besseren Lesbarkeit ein Beispiel des Ablaufs:
  # rcp /var/sysconf/AIX-4/sudo/testinstallcmd zeus:/tmp/testinstallcmd.3648
  # rsh zeus /tmp/testinstallcmd.3648
  # rsh zeus rm /tmp/testinstallcmd.3648

  my $bs = $main::beschreibungen->Betriebssystem($rechner);
  my $user  = $main::commands->Get($bs,'CMD_USER');
  my $group = $main::commands->Get($bs,'CMD_GROUP');

  # Ersetzungsmuster auf *cmd-Files anwenden
  my $pattern = $main::cmd_templatepattern->Get_nocheck($bs,$subsystem,$command,'');
  if (defined $pattern)
  {
    main::debug "Textersetzung in cmd-File: '$kommandofile'\n";
    my $tempfile = "/tmp/sysconf.template.$$";
    TextModify::ErsetzeMuster($kommandofile, $tempfile, $pattern, \%main::RechnerVars);
    main::RemoteCopy($tempfile,
		     $rechner, $remote_tmp_file, $user, $group, 700);
    unlink $tempfile;
  }
  else
  {
    main::RemoteCopy($kommandofile,
		     $rechner, $remote_tmp_file, $user, $group, 700);
  }

  # Rsh liefert nicht den Exitcode des Remote-Prozesses!
  # => Das Programm/Script muss den String "TRUE" zurückgeben
  $ret = main::RemoteShell($rechner, $remote_tmp_file);
  main::RemoteRm($rechner, $remote_tmp_file);

  unless ( ($ret =~ /TRUE/) || ($ret =~ /FALSE/) )
  {
    main::info("Kommando '$kommandofile' liefert '$ret'\n");
  }

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


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


sub ExecuteLocalCommand
{
  # Diese Funktion führt das geforderte Kommando auf "localhost" aus.
  # Das vereinfacht die anderen Funktionen.
  #
  my $objekt  = shift;
  my $command = shift;
  main::debug "Lokales Kommando: $command...\n";

  my $rechner         = $objekt->{RECHNER};
  my $path_to_command = "$main::sysconfroot$main::slash$objekt->{BS}$main::slash$objekt->{SUBSYSTEM}";
  my $subsystem       = $objekt->{SUBSYSTEM};

  my $kommandofile = "$path_to_command${main::slash}$command";
  my $tmp;

  unless (-r $kommandofile)
  {
    $tmp = main::LinksAufloesen($kommandofile);
    if (! defined $tmp)
    {
      # die Kommandos pre_localshell und post_localshell dürfen fehlen, da
      # optional!
      main::debug "(Optionales '$kommandofile' ist nicht vorhanden/lesbar.)\n";
      return $main::TRUE;
    }
    $kommandofile = $tmp;
  }

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

  my $tempfile = "/tmp/sysconf.template.$$";

  my $bs = $main::beschreibungen->Betriebssystem($rechner);
  # Ersetzungsmuster auf *cmd-Files anwenden
  my $pattern = $main::cmd_templatepattern->Get_nocheck($bs,$subsystem,$command,'');
  if (defined $pattern)
  {
    main::debug "Textersetzung in cmd-File: '$kommandofile'\n";
    TextModify::ErsetzeMuster($kommandofile, $tempfile, $pattern, \%main::RechnerVars);
  }
  else
  {
    copy($kommandofile, $tempfile);
  }

  chmod 0700, $tempfile;
  $ret = main::LocalShell($tempfile);
  unlink $tempfile;

  unless ( ($ret =~ /TRUE/) || ($ret =~ /FALSE/) )
  {
    main::info("Kommando '$kommandofile' liefert '$ret'\n");
  }

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


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 = '';
  my $tmp;

  foreach $command ('testinstallcmd', 'installcmd', 'testruncmd', 'stopcmd',
                    'startcmd', 'removecmd', 'reconfigcmd', 'pre_localshell',
		    'post_localshell')
  {
    my $cmdfile = "$main::sysconfroot$main::slash$objekt->{BS}$main::slash".
    "$objekt->{SUBSYSTEM}$main::slash$command";

    # Existiert das cmd-File?
    unless (-r $cmdfile)
    {
      $tmp = main::LinksAufloesen($cmdfile);
      if (! defined $tmp)
      {
	# Wenn es das File nicht gibt, dann einfach mit dem nächsten weiter...
	next;
      }
      $cmdfile = $tmp;
    }

    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";

	  # Ersetzungsmuster auf *cmd-Files anwenden
	  my $pattern = $main::cmd_templatepattern->Get_nocheck($objekt->{BS},$objekt->{SUBSYSTEM},$command,'');
	  if (defined $pattern)
	  {
	    main::debug "Textersetzung in cmd-File: '$cmdfile'\n";
	    my $tempfile = "/tmp/sysconf.template.$$";
	    TextModify::ErsetzeMuster($cmdfile, $tempfile, $pattern, \%main::RechnerVars);

	    copy($tempfile,
		 "$htmldir$main::slash$rechner$main::slash$rechner-sub-$objekt->{SUBSYSTEM}".
		 "-$command.txt"
		);
	    unlink $tempfile;
	  }
	  else
	  {
	    copy($cmdfile,
		 "$htmldir$main::slash$rechner$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
{
  my ($ok, $ret) = ExecuteCommand(shift,'testinstallcmd');
  return $ok;
}


sub IsRunning
{
  my ($ok, $ret) = ExecuteCommand(shift,'testruncmd');
  return $ok;
}


sub Install
{
  my $objekt  = shift;
  my ($ok, $ret) = ExecuteCommand($objekt,'installcmd');
  # Wenn der Returnwert "FALSE" ist, dann interessiert die Fehlermeldung!
  unless ($ok)
  {
    main::error($objekt->{RECHNER},"Ausgaben der RemoteShell: '$ret'\n");
  }
  return $ok;
}


sub Stop
{
  my $objekt  = shift;
  my ($ok, $ret) = ExecuteCommand($objekt,'stopcmd');
  # Wenn der Returnwert "FALSE" ist, dann interessiert die Fehlermeldung!
  unless ($ok)
  {
    main::error($objekt->{RECHNER},"Ausgaben der RemoteShell: '$ret'\n");
  }
  return $ok;
}


sub Start
{
  my $objekt  = shift;
  my ($ok, $ret) = ExecuteCommand($objekt,'startcmd');
  # Wenn der Returnwert "FALSE" ist, dann interessiert die Fehlermeldung!
  unless ($ok)
  {
    main::error($objekt->{RECHNER},"Ausgaben der RemoteShell: '$ret'\n");
  }
  return $ok;
}


sub Remove
{
  my $objekt  = shift;
  my ($ok, $ret) = ExecuteCommand($objekt,'removecmd');
  # Wenn der Returnwert "FALSE" ist, dann interessiert die Fehlermeldung!
  unless ($ok)
  {
    main::error($objekt->{RECHNER},"Ausgaben der RemoteShell: '$ret'\n");
  }
  return $ok;
}


sub Reconfigure
{
  my $objekt  = shift;
  my ($ok, $ret) = ExecuteCommand($objekt,'reconfigcmd');
  # Wenn der Returnwert "FALSE" ist, dann interessiert die Fehlermeldung!
  unless ($ok)
  {
    main::error($objekt->{RECHNER},"Ausgaben der RemoteShell: '$ret'\n");
  }
  return $ok;
}


sub pre_localshell
{
  my $objekt  = shift;
  my ($ok, $ret) = ExecuteLocalCommand($objekt,'pre_localshell');
  # Wenn der Returnwert "FALSE" ist, dann interessiert die Fehlermeldung!
  unless ($ok)
  {
    main::error($objekt->{RECHNER},"Ausgaben der pre_localshell: '$ret'\n");
  }
  return $ok;
}


sub post_localshell
{
  my $objekt  = shift;
  my ($ok, $ret) = ExecuteLocalCommand($objekt,'post_localshell');
  # Wenn der Returnwert "FALSE" ist, dann interessiert die Fehlermeldung!
  unless ($ok)
  {
    main::error($objekt->{RECHNER},"Ausgaben der post_localshell: '$ret'\n");
  }
  return $ok;
}


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

package TextModify;

use Carp;

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


# Modul-Globale Variablen initialisieren
sub TextModifyInit
{
  # Quellfile ist globale Variable, da sie sonst immer übergeben werden muss
  @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; # Nur lesender open() => Kein Fehler möglich.

  @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);
      # Ersetzungen mit Variablen durchführen
      {
	local $FehlerInVariablenErsetzungExpression = $expr;
	local $SIG{__WARN__} = \&FehlerInVariablenErsetzung;
	$Veraenderungen += eval
	$variablenCode.'
        $tmp = 0; # Um die Anzahl der Textveränderungen zu zählen
        foreach (@quelle)
        {
          $tmp += '.$expr.'
        }
        $tmp;';
      }
      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 undef,"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) || croak "Fehler beim Schliessen des Zielfiles '$zielfile'!\n";

  if ($main::logLevelDebug)
  {
    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 ($@)
    {
      error undef,"Fehler in Perl-Expression: ", $@;
      return $FALSE;
    }
  }
  return $TRUE;
}


sub FehlerInVariablenErsetzung
{
  # Signal-Handler für Fehler in Variablenersetzungen
  # Wichtig: Die globale (local) Variable
  #          $FehlerInVariablenErsetzungExpression muss gesetzt sein!
  # Parameter: -
  # Return:    -
  my $fehler = shift;
  if ($fehler =~ /Use of uninitialized value /)
  {
    error undef,"Fehler in der Variablenersetzung:\n".
    "'". $fehler . "'\n" .
    "Sie haben in folgender Zeile eine Variable ".
    "verwendet,\n".
    "die Sie aber nicht definiert haben:\n".
    "'$FehlerInVariablenErsetzungExpression'\n";
  }
  else
  {
    error undef,$fehler .
    "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 |
              listhosts | listinterfaces | lisreposfiles | listrevision | none
 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 einschliesslich
                '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
 listhosts:     Liste aller Rechner (aus hosts.sc) ausgeben.
                Dabei wird der SUBSYSTEM-Parameter ignoriert und MACHINE muss
                ALL sein, also: sysconf listhosts xxx ALL
 listinterfaces: Liste aller Interfaces der Rechner (aus hosts.sc) ausgeben.
                Dabei wird der SUBSYSTEM-Parameter ignoriert und MACHINE muss
                ALL sein, also: sysconf listinterfaces xxx ALL
 listreposfiles: Liste aller Files im Repository für das angegebene Betriebs-
                system. Dabei wird der MACHINE-Parameter ignoriert. Beispiel:
                sysconf listreposfiles AIX-5.1 xxx
 listrevision:  Liste aller Rechner (aus hosts.sc das Feld REVISION) mit der
                Revisionsnummer ausgeben.
                Dabei wird der SUBSYSTEM-Parameter ignoriert und MACHINE muss
                ALL sein, also: sysconf listrevision xxx ALL
 none:          Keine Aktion.

 Optionen:
 -wX: mit X=1-31 gibt den LOG-Level an. X ergibt sich als Summe der folgenden
      möglichen Werte (Bitmaske):
      1:  alle Fehler (Genaue Fehlerbeschreibungen)
      2:  alle Warnungen (sollte man beachten)
      4:  alle Informationen (ganz informativ)
      8:  alle Debug-Informationen (ausführlicher Status, etc.)
      16: alle Remote-Shell-Aufrufe und Sysconf-Client-Aufrufe
      (Default ist 7, also 1+2+4)
 -b : Erstellt Backup-Dateien auf dem Zielrechner, wenn Änderungen
      stattgefunden haben. Die Backup-Dateien enden mit einem Zeitstempel:
      '.JJJJMMTTHHMMSS.SC' z.B. '.20011019121531.SC'
 --logfile=FILENAME : Logging erfolgt in die Datei LOGFILE.
                      Es kann auch ein Pipe angegeben werden, z.B.
                      --logfile=\"|/bin/cronolog /var/log/sysconf/\%Y/\%m/\%d\"
 --sysconf_root=DIRNAME : Angabe des Verzeichnisses in dem sich das Sysconf-
                          Repository befindet.
 --dry-run : Trockenlauf, d.h. es wird alles ausgeführt nur die Client-Zugriffe
             nicht, also weder ssh-, rsh- noch Sysconf-Client-Zugriffe.
 --list_hosts_per_os=OS : Listet alle Rechner mit dem Betriebssystem OS auf.
                          z.B.: --list_hosts_per_os=Gentoo2007 none none none
 --list_hosts_subsys : Listet alle Rechner mit den zugeordneten Subsystemen auf.
                       z.B.: --list_hosts_subsys none ALL ALL
 --list_subsys_hosts : Listet alle Subsysteme und welche Rechner sie bekommen.
                       z.B.: --list_subsys_hosts none ALL ALL

 Beispiele:
 'Führe einen Update von Sendmail auf den Rechner zeus durch':
 sysconf update sendmail zeus
 'Führe einen Update von allen Subsystemen auf den Rechner osiris durch':
 sysconf update ALL osiris
 'Verteile alle Subsysteme auf alle Rechner':
 sysconf update ALL ALL
 'Initialisiere alle Subsysteme auf hugo':
 sysconf init ALL hugo
 'Führe einen Update von Sendmail und Syslog auf die Rechner zeus, osiris und
 hugo durch':
 sysconf update sendmail,syslog zeus,osiris,hugo
 Repository auf Konsistenz testen, ohne die Client-Rechner zu verändern:
 sysconf --dry-run update ALL ALL


=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 L</Konfigurationsdatenbank> befindet. SYSCONF_ROOT kann
auch an der Kommandozeile durch "--sysconf_root=DIRNAME" angegeben werden.
Mit HTMLDIR wird das Verzeichnis für die HTML-Dokumentation, die mit der
Aktion 'documentation' erstellt wird, festgelegt.

Die nachfolgenden Einstellungen können optional angegeben werden:

Mit STYLESHEET kann ein Stylesheet und mit STYLESHEET_CLASS eine
Stylesheet-Klasse für die HTML-Dokumentation angegeben werden.

Durch STOP_ON_ERROR=TRUE oder STOP_ON_ERROR=FALSE kann eingestellt werden,
ob Sysconf bei einem Fehler abbrechen soll (Standard) oder nicht.

Mit LOGLEVEL kann der LOG-Level analog zum Kommandozeilenparameter w
angegeben werden (Standard ist 7, d.h. alle Fehler, Warnungen und Infos
ausgeben) aber die Angabe auf der Kommandozeile hat Vorrang.
Der LOG-Level ergibt sich als Bitmaske mit den möglichen Bits:

 1:  alle Fehler (Genaue Fehlerbeschreibungen)
 2:  alle Warnungen (sollte man beachten)
 4:  alle Informationen (ganz informativ)
 8:  alle Debug-Informationen (ausführlicher Status, etc.)
 16: alle Remote-Shell-Aufrufe und Sysconf-Client-Aufrufe
 (Default ist 7, also 1+2+4)

So gibt beispielsweise 16 nur rsh-Meldungen aus, 2 nur Warnungen und 18
(=2+16) rsh-Meldungen und Warnungen. 9 (=1+8) würde z.B. nur Fehler und
Debug-Meldungen ausgeben und 31 (=1+2+4+8+16) gibt alles aus.

Durch LOGFILE kann der Ort des Logfiles bestimmt werden.
(Standard: /tmp/sysconf.log)
Diese Einstellung kann auch per Option "--logfile=FILENAME" auf der
Kommandozeile angegeben werden, wobei die Angabe auf der Kommandozeile höhere
Priorität hat.
Es kann auch ein Pipe angegeben werden, z.B.
--logfile="|/bin/cronolog /var/log/sysconf/%Y/%m/%d"

Die Option CLIENT_PORT gibt die Portnummer an, über die Sysconf versuchen soll,
zu dem optionalen sysconf-client-Programm Kontakt aufzunehmen.

Beispiel für die Hauptkonfigurationsdatei:

 SYSCONF_ROOT=/var/adm/sysconf
 HTMLDIR=/http/htdocs/sysconf
 STYLESHEET=/styles/corporate-design.css
 STYLESHEET_CLASS=text
 STOP_ON_ERROR=TRUE
 LOGLEVEL=7
 LOGFILE=/var/log/sysconf.log
 CLIENT_PORT=3456

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

Sysconf-client ist die optimale Methode. Dazu muss man nur das Programm
sysconf-client auf den Rechnern starten, die von Sysconf verwaltet werden.
Für sysconf-client muss der Rechner, auf dem Sysconf läuft, als authorisierter
Rechner angegeben werden und natürlich die gleiche Portnummer wie bei Sysconf
in CLIENT_PORT.

Sysconf probiert bei jedem Start alle Möglichkeiten Kontakt zu einem Rechner
herzustellen automatisch durch und zwar in dieser Reihenfolge:
sysconf-client, rsync/ssh, rsync/rsh, ssh/scp, und rsh/rcp.
Mit dem Schalter -w7 kann man beobachten, welche Methode verwendet wird.
rsh und ssh funktionieren nur, wenn es ohne Kennworteingabe möglich ist auf
den anderen Rechner zuzugreifen. Es sollte also soetwas ohne Benutzereingaben
funktionieren: "ssh rechner date".

Wenn der Rechner, der mit sysconf verwaltet wird, "localhost" heisst, dann
wird kein Remote-Copy/Shell verwendet, sondern direkt auf dem Rechner
gearbeitet, auf dem sysconf läuft.

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


=head2 F<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/
     zeus.var
     osiris.var
     hugo.var
     ...
 AIX-4.1.5/
    dependencies.sc            (Abhängigkeiten der Subsysteme)
    dependencies-soft.sc       ("Weiche" Abhängigkeiten der Subsysteme)
    exclusions.sc              (Gegenseitige Ausschlüsse der Subsysteme)
    files.sc                   (Liste der Files pro Subsystem)
    commands.sc                (Optional, Remote-Kommandos mit vollen Pfaden)
    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
       ...
    syslog/
          installcmd
          removecmd
          testinstallcmd
          startcmd
          stopcmd
          reconfigcmd
          testruncmd
          pre_localshell
          post_localshell
    nfsserver/
          installcmd
          removecmd
          testinstallcmd
          startcmd
          stopcmd
          reconfigcmd
          testruncmd
          pre_localshell
          post_localshell
    newsserver/
    ...
    sendmail/
    ...
 AIX-4.3/
    dependencies.sc            (Abhängigkeiten der Subsysteme)
    dependencies-soft.sc       ("Weiche" Abhängigkeiten der Subsysteme)
    exclusions.sc              (Gegenseitige Ausschlüsse der Subsysteme)
    files.sc                   (Liste der Files pro Subsystem)
    commands.sc                (Optional, Remote-Kommandos mit vollen Pfaden)
    filedir.sc/                (Hier liegen alle Konfigurationsfiles)
       etc/rc.config
       etc/hosts
       ...
    syslog/
          installcmd
          removecmd
          ...
 Linux-SuSE-6.4/
    dependencies.sc            (Abhängigkeiten der Subsysteme)
    dependencies-soft.sc       ("Weiche" Abhängigkeiten der Subsysteme)
    exclusions.sc              (Gegenseitige Ausschlüsse der Subsysteme)
    files.sc                   (Liste der Files pro Subsystem)
    commands.sc                (Optional, Remote-Kommandos mit vollen Pfaden)
    filedir.sc/                (Hier liegen alle Konfigurationsfiles)
       etc/rc.config
       etc/hosts
       ...
    syslog/
          installcmd
          removecmd
          ...

In diesem Verzeichnisbaum können natürlich auch symbolische Links angelegt
werden. Das dient dazu, doppelte Datenhaltung zu vermeiden. So kann sicherlich
ein /etc/sudoers unter Linux, AIX und Solaris identisch aussehen. Das wäre dann
z.B. so:

 lrwxrwxrwx 1 root root 46 Feb 15  2001 AIX_common/filedir.sc/etc/sudoers -> ../../../OS_independent/filedir.sc/etc/sudoers
 lrwxrwxrwx 1 root root 39 Sep 10  2002 AIX-5.1/filedir.sc/etc/sudoers -> ../../../AIX_common/filedir.sc/etc/sudoers
 lrwxrwxrwx 1 root root 46 Oct 20  2003 Linux-2.6/filedir.sc/etc/sudoers -> ../../../OS_independent/filedir.sc/etc/sudoers

Wie man sieht kann eine solche Verlinkung auch mehrstufig sein.

Optional unterstützt Sysconf auch Links in speziellen Dateien.
Wenn man normalerweise einen Link von QUELLE nach ZIEL hat, dann legt man
einfach statt QUELLE eine Datei QUELLE.LINK an, die als Inhalt den Text ZIEL
hat. Die Logik im Sysconf ist folgende: Wenn eine Datei namens XYZ nicht
existiert, dann wird nach XYZ.LINK gesucht und deren Inhalt ausgewertet.
Der Inhalt von XYZ.LINK wird als neuer Dateiname interpretiert. Das können
natürlich auch relative Pfadangaben sein. Das funktioniert auch für
Verzeichnisse. Auch mehrstufige Links sind möglich. Durch diesen Verzicht auf
symbolische Links wird eine Benutzung von CVS möglich oder die Ablage auf
einem Filesystem, das keine symbolischen Links unterstützt.
Beispiel:

 AIX_common/filedir.sc/etc/sudoers.LINK
 mit dem Inhalt:
 ../../../OS_independent/filedir.sc/etc/sudoers
 AIX-5.1/filedir.sc/etc/sudoers mit dem Inhalt:
 ../../../AIX_common/filedir.sc/etc/sudoers
 Linux-2.6/filedir.sc/etc/sudoers mit dem Inhalt:
 ../../../OS_independent/filedir.sc/etc/sudoers
 und in OS_independent/filedir.sc/etc/ liegt das eigentliche File.


=head2 Dateien F<variables/xxx.var>


Beispielsweise enthält F<zeus.var> Variablen speziell für den Rechner
"zeus". 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.

Nachfolgende interne Variablen sind standardmässig definiert. Die Optionen
SYSCONF_BACKUP und SYSCONF_BACKUP_EXTENSION werden durch die Backup-Option
beeinflusst. Diese können z.B. in *cmd-Files verwendet werden.

 SYSCONF_BACKUP              mit den Werten TRUE und FALSE
 SYSCONF_BACKUP_EXTENSION    enthält die Backup-File-Endung
 SYSCONF_OS                  enthält das Sysconf-Betriebssystem aus hosts.sc
 SYSCONF_INTERFACE           enthält das Interface aus hosts.sc
 SYSCONF_CLASSES             enthält die Sysconf-Klassen aus hosts.sc
 SYSCONF_HOSTS_SC_SUBSYSTEMS enthält die Sysconf-Subsysteme aus hosts.sc
 SYSCONF_SUBSYSTEMS          enthält alle Subsysteme
 SYSCONF_VARFILE             enthält den Inhalt des Variablen-Files

Alle mit SYSCONF_ beginnenden Variablen sind für zukünftige Verwendung
reserviert und sollten daher nicht benutzt werden.

Beispiel:

 HOSTNAME        =zeus
 HOSTPURPOSE     =NFS-Server
 INITTAB         include inittab/tsm-client.inc
 IPFORWARDING    =0
 CRONTAB         include crontab/node.inc crontab/mksysb.inc

=head2 Datei F<files.sc>


Beispiel:

 [boot]
 f etc/rc
 f etc/inittab

 [login]
 f etc/environment owner=root group=system
 F etc/passwd permission=644
 f etc/motd
 c reconfigcmd
 beginpattern
   change s!#BACKUP_SERVER!$BACKUPSERVER!;
 endpattern

 [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

 INCLDUE files.sc.erweiterung

 [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> <attribute ...>
 <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)
 c         : Kommando-Template (Ersetzt Textmuster in Kommandofiles)
 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)
 lS        : lokales Shellkommando (Bei UPDATE+INIT ausführen)
 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.
 <attribute>:    Optional. Es sind folgende Attribute möglich:
                 owner=<Benutzername>
                 group=<Gruppenname>
                 permission=<oktale Datei-Rechte wie bei chmod>
                 hidden=<TRUE oder FALSE>. Default ist FALSE.
                 Mit hidden=TRUE wird verhindert, dass die Datei in der HTML-
                 Dokumentation auftaucht.
                 Wenn keine Attribute angegeben werden, dann werden die
                 Attribute des Files in der Sysconf-Datenbank verwendet.

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, dass 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.

Die Kommandotemplates dienen dazu, dass man in den Kommando-Files *cmd (siehe
weiter unten) auch Textersetzungen vor der Übertragung durchführen kann.
Auf diese Art und Weise ist es möglich Variablen-Inhalte z.B. in das
reconfigcmd einzubauen.

Der spezielle Subsystemname "GLOBAL" bedeutet, dass 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.
Dieses Subsystem bekommen alle Rechner als erstes Subsystem.

Analog zu GLOBAL existiert das spezielle Subsystem "LAST", das alle Rechner
als letztes Subsystem bekommen.

Mit der Anweisung INCLUDE gefolgt von einem Dateinamen relativ zum Ort der
Datei files.sc kann man die files.sc um weitere Dateien erweitern. Im
Extremfall besteht die files.sc nur aus INCLUDE-Anweisungen und die
Subsystem-Definitionen sind alle in einzelne Dateien ausgelagert.


=head2 Kommando-Files *cmd

Es gibt folgende Kommando-Files, die beim Syscon-Lauf für ein Zielsystem
ausgeführt werden:

  pre_localshell
  testinstallcmd
  installcmd
  testruncmd
  stopcmd
  reconfigcmd
  startcmd
  post_localshell

Es müssen alle Kommando-Files vorhanden sein. (Ausnahme sind die *_localshell)
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.

Wenn ein solches Programm/Shellscript einen Returncode ungleich 0 liefert,
dann bricht Sysconf mit einem "FATAL"-Fehler ab.
Bei Ausgabe von FALSE auf STDOUT und Returncode 0 bei z.B. dem installcmd
wird nur ein "ERROR"-Fehler protokolliert.

Mit den beiden Optionen CMD_USER und CMD_GROUP im Konfigurationsfile
commands.sc (siehe unten) kann festgelegt werden, mit welchem Benutzer und
Gruppe die Kommandos der
Subsysteme (installcmd, reconfigcmd, etc.) angelegt werden. (Standard ist der
effektive User und Gruppe, der Sysconf aufgerufen hat.)

Die Kommando-Files können analog zu den Daten in der L</Konfigurationsdatenbank>
mit *.LINK-Files verlinkt werden.


=head2 Kommentare


In den Dateien

 dependencies.sc
 dependencies-soft.sc
 exclusions.sc
 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.
Diese Datei ist in Strophenform aufgebaut, wobei eine Strophe
immer mit dem Schlüsselwort HOST beginnen muss. Zur Übersichtlichkeit sollte
man die Strophen durch eine Leerzeile trennen.
Mit HOST wird der Rechnername angegeben. Dieser Name muss mit dem Namen der
Variablendatei im Unterverzeichnis variables/ übereinstimmen.
Durch OS wird das Betriebssystem festgelegt. Das ist eine frei wählbare
Bezeichnung. Es muss ein gleichnamiges Unterverzeichnis im
Sysconf-Hauptverzeichnis geben.
Die Einträge hinter CLASSES und SUBSYSTEMS sind optional. Damit wird bestimmt,
welche Subsysteme der Rechner erhalten soll.
Optional kann auch mittels INTERFACE das zu verwendende Interface angegeben
werden. Beispiel: Der Rechner abc123 hat unter dem Hostnamen
abc123giga ein Gigabit-Ethernet. Dann kann man als HOST abc123 angeben
unter INTERFACE abc123giga angeben und Sysconf betankt den Rechner über
das Gigabit-Ethernet. Das ist auch sehr nützlich bei HACMP-Clustern.
Da sollte man nicht den produktiven Hostnamen angeben, sondern den
Hostnamen/Adapter, der nicht als Ressource "wandert", sondern immer auf
den selben Rechner verweist, wie zum Beispiel ein Adapter im internen
Verwaltungsnetz.
Wenn man als HOST oder INTERFACE nur localhost angibt, dann wird der Rechner
nicht remote angesteuert, sondern es wird direkt der Rechner angesprochen,
auf dem Sysconf läuft.
Statt Hostnamen können natürlich auch IP-Adressen angegeben werden.
Optional kann auch noch mit REVISION ein String oder eine Zahl angegeben
werden. Das ist z.B. dazu nützlich, wenn man das Sysconf-Repository unter einer
Versionskontrolle wie CVS oder Subversion betreinbt und für bestimmte Rechner
bestimmte Versions-Stände dokumentieren will. Sysconf selbst verwendet das Feld
REVISION überhaupt nicht. Dieser Wert kann nur mittels
sysconf listrevision xxx ALL
ausgegeben werden.

Beispiel:

 HOST         zeus
 OS           AIX-4.1.5
 CLASSES      Basissystem Client
 SUBSYSTEMS   sendmail tcpwrapper nfsclient adsm

 HOST         osiris
 INTERFACE    osirisgigabit
 OS           AIX-4.1.5
 CLASSES      Basissystem Server
 SUBSYSTEMS   mailclient newsserver nfsserver

 HOST         neptun
 REVISION     72356
 OS           Linux-SuSE-7.0
 CLASSES      Entwicklung Basissystem
 SUBSYSTEMS   inittab gcc2723 gmake libXpm mailclient adsm-client

 HOST         pinguin
 INTERFACE    localhost
 OS           Linux-SuSE-8.1
 CLASSES
 SUBSYSTEMS   inittab gcc295

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

 CLASSDEF    Basissystem
 SUBSYSTEMS  syslog tcpwrapper ssh

 CLASSDEF    Server
 SUBSYSTEMS  adsm audit nis

Die Klassen dienen dazu, Subsysteme zusammenzufassen. So kann man z.B. statt

 HOST         neptun
 OS           Linux-SuSE-7.0
 CLASSES
 SUBSYSTEMS   gcc2723 gmake ddd lex yacc perl mailclient adsm-client

diese Klasse definieren:

 CLASSDEF    Entwicklung
 SUBSYSTEMS  gcc2723 gmake ddd lex yacc perl

und sich dann in der hosts.sc entsprechend kurz fassen:

 HOST         neptun
 OS           Linux-SuSE-7.0
 CLASSES      Entwicklung
 SUBSYSTEMS   mailclient adsm-client

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 F<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 muss 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 muss man selbst dafür sorgen, dass die Sonderzeichen interpretiert
werden:

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

Wenn man aber z.B. sicher sein will, dass eine Variable keine Newlines enthält,
dann erreicht man das mit dieser Zeile:

  change s/ROOTDISK/($ROOTDISK=~s:\n::g,$ROOTDISK)/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
 Zeile, die mit TCPWindowsize beginnt komplett löschen:
   change s/^TCPWindowsize.*\n//


=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.
Das heisst, dass die Subsysteme D1, D2, D3, ... noch in die Liste der Subsysteme mit dazugenommen und noch vor S verteilt werden.
Beispiele:

 sendmail : syslog
 nfsserver : tcpwrapper portmap inetd


=head2 dependencies-soft.sc

Die Datei "dependencies-soft.sc" hat den Aufbau:

 S : D1 D2 D3 ...

Bedeutung: Subsystem "S" hängt "weich" von Subsystemen "D1", "D2", "D3", ... ab.
Das heisst, dass wenn die Subsysteme D1, D2, D3, ... für diesen Rechner erlaubt sind, dann werden sie noch in die Liste der Subsysteme mit dazugenommen und noch vor S verteilt.
Beispiele:

 ssh : ldap


=head2 exclusions.sc

Die Datei "exclusions.sc" hat den Aufbau:

 S1 S2

Bedeutung: Subsystem "S1" und Subsystem "S2" schliessen sich gegenseitig aus.
Beispiele:

 mailclient mailserver
 gcc2723 egcs1111
 sendmail qmail


=head2 commands.sc

Die Datei "commands.sc" beschreibt, welche Kommandos remote auzuführen sind.
Diese Datei ist optional, aber aus Sicherheitsgründen zu empfehlen.
Wenn diese Datei fehlt, dann werden die Kommandos ohne absoluten Pfad
ausgeführt. Wenn einzelne Kommandos in dieser Datei nicht aufgeführt sind,
werden auch diese ohne Pfad aufgerufen.

Speziell für den cp-Befehl empfiehlt es sich diese Datei zu verwenden, denn
cp benötigt unter den verschiedenen Unices unterschiedliche Schalter, um
symbolische Links korrekt zu kopieren. Ich empfehle unter Linux "cp -pd",
unter AIX "cp -ph" und ab Solaris 10 "cp -rpP".

Mit den beiden Optionen CMD_USER und CMD_GROUP kann festgelegt werden, mit
welchem Benutzer und Gruppe die Kommandos der Subsysteme
(siehe L</Kommando-Files *cmd>) wie z.B. installcmd, reconfigcmd, etc. angelegt
werden. (Standard ist der effektive User und Gruppe, der Sysconf aufgerufen
hat.)

Durch die Option REMOTE_USER kann bestimmt werden, unter welchem Benutzer die
rsh/ssh/rsync-Zugriffe erfolgen sollen.
(Default: Der Benutzer, unter dem Sysconf gestartet wurde)

Mit der Option USE_SUDO (Default FALSE) kann man einen Zwischenschritt auf dem
Client aktivieren. Das ist eigentlich nur sinnvoll, wenn Sysconf nicht als
Root läuft oder der Sysconf-Client nicht unter Root gestartet wurde.
Normalerweise laufen die Remote-Copy- und Remote-Shell-Funktionen unter dem
User, der Sysconf startet. Es wird also beispielsweise, wenn Sysconf als Root
läuft, auf dem Client ein Shell-Kommando auch als Root ausgeführt.
Wenn man nun aber aus Sicherheitsgründen Sysconf nicht als Root laufen lassen
will, aber auf dem Client trotzdem Files verändern muss, die Root gehören, dann
kann man mit USE_SUDO=TRUE den Aufruf von sudo zwischenschalten. (Wie das
sudo-Programm mit vollem Pfad heisst, kann man natürlich in commands.sc
einstellen.) Normalerweise wird ein Remote-Copy so ausgeführt: "rcp
sendmail.cf client:/etc/sendmail.cf" und eine Remote-Shell so: "rsh client
command". Mit sudo läuft es so: "rcp sendmail.cf client:/tmp ; rsh client sudo
mv /tmp/sendmail.cf /etc/sendmail.cf" respektive "rsh client sudo commando".
In /etc/sudoers muss man etwas in dieser Art eintragen:
"mysysonfuser ALL=NOPASSWD:ALL"

Mit der Option SSH_KEY kann man den Ort eines alternativen SSH-Key angeben.
Das braucht man in der Regel immer dann, wenn man auch einen REMOTE_USER
angegeben hat.

Beispiel für AIX:

 CP=/usr/bin/cp -ph
 RM=/usr/bin/rm
 LN=/usr/bin/ln
 DIFF=/usr/bin/diff
 CHOWN=/usr/bin/chown
 CHMOD=/usr/bin/chmod
 MKDIR=/usr/bin/mkdir
 MV=/usr/bin/mv
 SUDO=/usr/bin/sudo -H
 CMD_USER=root
 CMD_GROUP=system
 REMOTE_USER=root
 USE_SUDO=FALSE
 SSH_KEY=/home/myotheruser/.ssh/id_dsa


=head1 RETURN

Sysconf beendet sich mit folgenden Returncodes:

 0 = kein Fehler aufgetreten
 1 = es sind Warnungen (WARN) aufgetreten
 2 = es sind Fehler (ERROR) aufgetreten
 3 = es sind fatale Fehler (FATAL) aufgetreten


Keine Rückgabewerte.

=head1 KNOWN BUGS AND PROBLEMS

Unter AIX kommt es beim Versuch ein ausführbares File, das gerade ausgeführt
wird, zu überschreiben zu dieser Fehlermeldung:
"Cannot open or remove a file containing a running program."
oder "Text file busy".
Das lässt sich nur umgehen, indem man durch start- und stop-Skripte
sicherstellt, dass das betroffene Progamm durch Sysconf beendet wird.
Wenn das, wie bei Dämons, nicht praktikabel ist, so würde ich empfehlen,
das File nicht direkt zu überschreiben, sondern ein temporäres File anzulegen
und dieses dann durch reconfigcmd umzukopieren, also in files.sc:

  f usr/local/sbin/mydaemon /usr/local/sbin/mydaemon.tmp

und im reconfigcmd etwas in der Art:

  rm /usr/local/sbin/mydaemon && \
  mv /usr/local/sbin/mydaemon.tmp /usr/local/sbin/mydaemon

Umgehen kann man diese Problematik durch die Verwendung von rsync.


=head1 AUTHOR


=for text
Sysconf wurde geschrieben von Stephan Löscher,
http://www.loescher-online.de/,
loescher@gmx.de, 1998.
Die Idee zu Sysconf wurde angeregt durch: GNU CFEngine, rdist, Michael Mattes (michael.mattes@iname.com), Software Update Protocol (SUP) der Carnegie-Mellon Universität (CMU). Eine ähnliche Software ist Puppet.
Mein Dank für Fehlerkorrekturen und neue Features geht an:
Michael Mattes (michael.mattes@iname.com)
Andreas Bussjäger (Andreas.Bussjaeger@partner.bmw.de, Andreas.Bussjaeger@t-systems.com)

=for man
Sysconf wurde geschrieben von Stephan Löscher,
http://www.loescher-online.de/,
loescher@gmx.de, 1998.
Die Idee zu Sysconf wurde angeregt durch: GNU CFEngine, rdist, Michael Mattes (michael.mattes@iname.com), Software Update Protocol (SUP) der Carnegie-Mellon Universität (CMU). Eine ähnliche Software ist Puppet.
Mein Dank für Fehlerkorrekturen und neue Features geht an:
Michael Mattes (michael.mattes@iname.com)
Andreas Bussjäger (Andreas.Bussjaeger@partner.bmw.de, Andreas.Bussjaeger@t-systems.com)

=for latex
Sysconf wurde geschrieben von Stephan Löscher,
http://www.loescher-online.de/,
loescher@gmx.de, 1998.\\
Die Idee zu Sysconf wurde angeregt durch: GNU CFEngine, rdist, Michael Mattes (michael.mattes@iname.com), Software Update Protocol (SUP) der Carnegie-Mellon Universität (CMU). Eine ähnliche Software ist Puppet.\\
Mein Dank für Fehlerkorrekturen und neue Features geht an:\\
Michael Mattes (michael.mattes@iname.com)\\
Andreas Bussjäger (Andreas.Bussjaeger@partner.bmw.de, Andreas.Bussjaeger@t-systems.com)

=for html
Sysconf wurde geschrieben von
<A HREF="http://www.loescher-online.de/">Stephan L&ouml;scher</A>,
<A HREF="mailto:loescher@gmx.de">loescher@gmx.de</A>,
1998.<br>
Die Idee zu Sysconf wurde angeregt durch:
<a href="http://www.gnu.org/gnulist/production/cfengine.html">GNU CFEngine</a>,
<a href="http://freshmeat.net/projects/freerdist/">rdist</a>,
<a href="mailto:michael.mattes@iname.com">Michael Mattes</a>,
<a href="ftp://ftp.cs.cmu.edu/afs/cs/project/mach/public/sup/">Software Update Protocol (SUP) der Carnegie-Mellon Universität (CMU)</a>. Eine ähnliche Software ist <a href="http://www.puppetlabs.com/">Puppet</a>.<br>
Mein Dank für Fehlerkorrekturen und neue Features geht an:<br>
<a href="mailto:michael.mattes@iname.com">Michael Mattes</a><br>
<a href="mailto:Andreas.Bussjaeger@t-systems.com">Andreas Bu&szlig;j&auml;ger</a>

=cut

######################################################################
#
# Warranty and legal notice
# ~~~~~~~~~~~~~~~~~~~~~~~~~
#
# Copyright (c) 1998 by Stephan Löscher  -  all rights reserved
# My Address: Stephan Löscher, Dr.Troll-str. 110, 82194 Gröbenzell, Germany
# Email: loescher@gmx.de
# WWW: http://www.loescher-online.de/
#
# 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.
#
######################################################################

