#!/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";
# Verfügbar unter: http://www.loescher-online.de/progdata/slutil.pm
use slutil 2017.1128;
use English;
use FileHandle;
use File::Copy;
use Carp;
use Net::Ping;

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

# 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
sub msg;        # Ausgabe von Meldung in verschiedenen Sprachen

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

$version = '2025.0127';
$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)
# 1.3.50:
# - bessere Fehlermeldung in TransferFiles()
# 1.3.51:
# - /tmp/ durch Variablen ersetzt
# 1.3.52:
# - Einträge der hosts.sc können auch als Einzel-Dateien in hostsdir.sc liegen
# 1.3.53:
# - Code in UTF-8 konvertiert
# - Alle Meldungen in eine msg()-Funktion verlagert z.B. für Übersetzungen
# - Englische Übersetzungen aller Meldungen
# 1.3.54:
# - Anleitung ergänzt
# - Warnung von TesteDateiBesitzer() in Info gewandelt.
# 1.3.55:
# - Kleine Korrektur bei einem msg()-Call
# 1.3.56:
# - Kleine Korrektur beim Einlesen von hostsdir.sc/*.inc
# 1.3.57:
# - Negierung von Subsystemen mit "!" möglich.
# 1.3.58:
# - Die häufigsten Fehler mit einem Identifier versehen.
# 1.3.59:
# - Fehler bei der Abhängigkeitsauflösung HARD/SOFT behoben.
# 1.3.60:
# - Abhängigkeiten von Abhängigkeiten werden bei der Abhängigkeitsauflösung
#   berücksichtigt.
# 1.3.61:
# - ssh-Option "BatchMode yes" ergänzt.
# 1.3.62:
# - Fehlercode 01-DES ergänzt.
# 1.3.63:
# - Einführung des File-Typs instt (Install-Template) in files.sc
# - irritierende File-Typ Abkürzungen für inst initf und initt enternt
# - Änderung des File-Typs inst auf instf für Konsistenz
# 1.3.64:
# - goto-Statements entfernt
# 1.3.65:
# - Interne Variable SYSCONF_REVISION hinzugefügt
# 1.3.66:
# - temporäre Dateien beinhalten nun den Namen des Subsystems, was ggf. die
#   Fehlersuche erleichtert
# 1.3.67:
# - Anleitung ergänzt.
# 1.3.68:
# - Kleine Verbesserung in Funktion TesteDateiBesitzer()
# 1.3.69:
# - Korrektur in der Logik beim Auflösen von LINKs für files.sc
# 1.3.70:
# - Neue Option "--list_variable=..."
# 1.3.71:
# - temporäre Dateien enthalten zufälligen String
# 1.3.72:
# - erste Anpassungen, um per Sysconf-Client auch Windows anbinden zu können
# 1.3.73:
# - Integration von RemoteClientSSL
# 1.3.74:
# - Unterstützung für SE Linux
# 1.3.75:
# - englische Anleitung
# 1.3.76:
# - SSH_KEY-Option: Es können mehrere angegeben werden
# 1.3.77:
# - Übersetzungen
# 1.3.78:
# - Neue Aktion "listfiles"
# 1.3.79:
# - kleiner Fix für Windows
# 2019.1023:
# - neue Versionsnummern und kleine Anpassung für Perl >5.10
# 2020.0124:
# - Interne Variable SYSCONF_ROOT hinzugefügt.
# 2020.0701:
# - Erklärung zu dependencies.sc und dependencies-soft.sc verbessert
# 2021.0830:
# - Better PING error message
# 2021.0901:
# - check, if hostname (or IP) can be resolved
# 2021.1115:
# - improved PING check
# 2021.1126:
# - additional error message tagging
# 2022.1220:
# - allow LINKs for variable files and their includes
# 2024.0617:
# - update manpage workflow
# 2024.1010:
# - error handling for single '!' as subsystem
# 2025.0127:
# - new internal variables:
#   SYSCONF_HOST, SYSCONF_CURRENT_SUBSYSTEM, SYSCONF_ACTION
# - pre_localshell and post_localshell are executed before/after REMOVE

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

$tmp_local  = '/tmp'; # Temporäres Verzeichnis auf Sysconf-Master
$tmp_remote = '/tmp'; # Temporäres Verzeichnis auf Sysconf-Client
$tmp_rand_pid = RandomString(20) . '.' . $$; # Zufälliger String + PID

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

# Sprache einstellen
$language = GetLanguage();

# 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 msg('Elogwr', $logfile);
}
else
{
  open(LOG, ">>$logfile") || die msg('Elogwr', $logfile);
}
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;

%Rechner_mit_Fehlern = ();

$logLevel = 7; # Standardmässig Fehler, Warnungen und Infos ausgeben

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

$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 msg('Eoption', $ARGV[0]);
  }
}

# Soll nur ein "Trockenlauf" durchgeführt werden?
$dry_run = $FALSE;
if (defined $options{"dry-run"})
{
  $dry_run = $TRUE;
  logprint msg('Tdry');
}

if ($backup)
{
  logprint msg('TbackT', $backup_endung);
}
else
{
  logprint msg('TbackF');
}

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

foreach (keys %options)
{
  logprint msg('Topt', $_);
  if (defined $options{$_})
  { logprint " = '$options{$_}'\n"; }
  else
  { logprint "\n"; }
}


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

if ($#ARGV<0)
{
  logprint msg('Twopara');
  print msg('Tauswahl');
  $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;
$client_port_ssl  = 0;
$client_ssl_cert_file = '';
$client_ssl_key_file  = '';

ReadConfigFile();

# Sind alle Optionen für SSL vorhanden?
if ($client_port_ssl)
{
  if ( $client_ssl_cert_file eq '' )
  {
    info msg('Tclnosslopt','SSL_CERT_FILE');
    $client_port_ssl = 0;
  }
  if ( $client_ssl_key_file eq '')
  {
    info msg('Tclnosslopt','SSL_KEY_FILE');
    $client_port_ssl = 0;
  }
}

logprint msg('Tll', $logLevel);

$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";
debug "CLIENT_PORT_SSL  = $client_port_ssl\n";
debug "SSL_CERT_FILE    = $client_ssl_cert_file\n";
debug "SSL_KEY_FILE     = $client_ssl_key_file\n";
debug "LANGUAGE         = $language\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 msg('Tclno');
  $client_port = 0;
}

# Wenn client_port_ssl definiert ist, aber das RemoteClientSSL.pm nicht
# geladen ist, dann den client_port_ssl deaktivieren.
if ( ($client_port_ssl) && (! defined $RemoteClientSSL::VERSION) )
{
  info msg('Tclsslno');
  $client_port_ssl = 0;
}

# Falls weder $client_port noch $client_port_ssl definiert ist, die Variablen
# vorbelegen, um später undef Warnungen zu vermeiden.
$RC_OK    = 0;
$RC_DEBUG = 0;
$RC_INFO  = 0;
$RC_WARN  = 0;
$RC_ERR   = 0;
$RC_FATAL = 0;

if ($client_port)
{
  # Importieren der Fehlercodes von RemoteClient
  # (Das ... += 0 ist nur zum Warnungen-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 msg('Tclver', $RemoteClient::VERSION);

if ($client_port_ssl)
{
  # Importieren der Fehlercodes von RemoteClientSSL
  # (Das ... += 0 ist nur zum Warnungen-Unterdrücken, falls RemoteClientSSL.pm
  #  nicht geladen ist.)
  $RC_OK    = $RemoteClientSSL::RC_OK;    $RemoteClientSSL::RC_OK   +=0;$RC_OK   +=0;
  $RC_DEBUG = $RemoteClientSSL::RC_DEBUG; $RemoteClientSSL::RC_DEBUG+=0;$RC_DEBUG+=0;
  $RC_INFO  = $RemoteClientSSL::RC_INFO;  $RemoteClientSSL::RC_INFO +=0;$RC_INFO +=0;
  $RC_WARN  = $RemoteClientSSL::RC_WARN;  $RemoteClientSSL::RC_WARN +=0;$RC_WARN +=0;
  $RC_ERR   = $RemoteClientSSL::RC_ERR;   $RemoteClientSSL::RC_ERR  +=0;$RC_ERR  +=0;
  $RC_FATAL = $RemoteClientSSL::RC_FATAL; $RemoteClientSSL::RC_FATAL+=0;$RC_FATAL+=0;
  # Debug-Unterfunktion in das Remote-Modul einhängen
  RemoteClientSSL::SetDebugFunction(\&debug_rsh);
}
debug msg('Tclsslver', $RemoteClientSSL::VERSION);

%klassendef = RechnerKlassenEinlesen();

@ALLHOSTS = (); # Wird auch durch RechnerBeschreibungenEinlesen() gefüllt!

$beschreibungen = RechnerBeschreibungenEinlesen();

$param = ParameterEinlesen(@ARGV);
logprint msg('Taction', $param->Aktion);
if ( ! ($param->Aktion eq 'listreposfiles') )
{
  logprint msg('Tsubsys', join(',',$param->Subsysteme));
  logprint msg('Thosts',  join(',',$param->Rechner)   );
}
else
{
  logprint msg('Tos', $param->Betriebssystem());
}

$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 von Variablen-Inhalten der Rechner
ListVariable($beschreibungen,$param,$options{list_variable}) if defined $options{list_variable};

# 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;
}

# Liste aller Files pro Rechner ausgeben
ListFiles($beschreibungen,$param) if $param->Aktion eq 'listfiles';

# 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, msg('Wnodesc');
    next;
  }

  # Testen, ob der Rechner erreichbar ist
  unless ( RechnerErreichbar( $beschreibungen->Interface($rechner) ) )
   {
    error $rechner, msg('Enoping', $beschreibungen->Interface($rechner));
    next;
  }

  my $socket;
  if (! $dry_run)
  {
    # Testen, ob der Rechner per Sysconf-Client erreichbar ist
    $socket = $beschreibungen->Socket($rechner);
    if (defined $socket)
    {
      debug msg('Tclok');
    }
    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, msg('Enossh', $beschreibungen->Interface($rechner));
	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, msg('Wnosubsys');
    next;
  }

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

  # Datei "files.sc" für dieses Betriebssystem einlesen
  ReadFilesSC($beschreibungen->Betriebssystem($rechner));
  debug msg('Darfsc');

 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 msg('Tsubsysad', $rechner, join(',',@subsys));
      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 msg('Tsubsysad', $rechner, join(',',@subsys));
      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 msg('Tsubsysad', $rechner, join(',',@subsys));
      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 msg('Tsubsysad', $rechner, join(',',@subsys));
      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 msg('Tsubsysad', $rechner, join(',',@subsys));
      Remove($rechner, $beschreibungen->Betriebssystem($rechner), @subsys);
      last SWITCH;
    }

    logdie msg('Eaction', $param->Aktion);
  }

  # 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;
  if ($tmp == 1)
  {
    info msg('Iruntime1', $rechner, $tmp);
  }
  else
  {
    info msg('Iruntime2', $rechner, $tmp);
  }
  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 msg('Dactionsub', 'init', $rechner, join(',',@subsys));

  # Für alle Subsysteme
  my $subsystem;
  foreach $subsystem (@subsys)
  {
    debug msg('Dsubsys', $subsystem);

    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, msg('Estop', $subsystem);
      }
    }
    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, 'installtemplate',$refvar);
      TransferFiles($rechner, $bs, $subsystem, 'installlink',    $refvar);
      # Subsystem installieren
      unless ($sub->Install)
      {
        error $rechner, msg('Einstall', $subsystem, $subsystem);
        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, msg('Ereconf', $subsystem);

    # Subsystem starten
    $sub->Start || error $rechner, msg('Estart', $subsystem);

    foreach $tmp ('install','init','installtemplate','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 msg('Dactionsub', 'update', $rechner, join(',',@subsys));

  # Für alle Subsysteme
  my $subsystem;
  foreach $subsystem (@subsys)
  {
    debug msg('Dsubsys', $subsystem);

    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, msg('Estop', $subsystem);
      }
      # 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, msg('Ereconf', $subsystem);

      # Subsystem starten
      $sub->Start || error $rechner, msg('Estart', $subsystem);

      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 msg('Dactionsub', 'remove', $rechner, join(',',@subsys));

  # Für alle Subsysteme
  my $subsystem;
  foreach $subsystem (@subsys)
  {
    debug msg('Dsubsys', $subsystem);

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

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

    # Ist das Subsystem überhaupt installiert?
    unless ($sub->IsInstalled)
    {
      debug msg('Dsubni', $subsystem, $rechner);
      next;
    }

    # Läuft das Subsystem gerade?
    if ($sub->IsRunning)
    {
      # Stoppen (damit auch alle abhängigen)
      $sub->Stop || error $rechner, msg('Estop', $subsystem);
    }
    # Subsystem de-installieren
    $sub->Remove || error $rechner, msg('Eremove', $subsystem);

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


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

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

  debug msg('Dactionsub', 'start', $rechner, join(',',@subsys));

  # Für alle Subsysteme
  my $subsystem;
  foreach $subsystem (@subsys)
  {
    debug msg('Dsubsys', $subsystem);

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

    # Ist das Subsystem überhaupt installiert?
    unless ($sub->IsInstalled)
    {
      debug msg('Dsubni', $subsystem, $rechner);
      next;
    }

    # Läuft das Subsystem gerade?
    if ($sub->IsRunning)
    {
      debug msg('Dsubisr', $subsystem, $rechner);
      next;
    }

    # Subsystem starten
    $sub->Start || error $rechner, msg('Estart', $subsystem);
  }
}


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

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

  debug msg('Dactionsub', 'stop', $rechner, join(',',@subsys));

  # Für alle Subsysteme
  my $subsystem;
  foreach $subsystem (@subsys)
  {
    debug msg('Dsubsys', $subsystem);

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

    # Ist das Subsystem überhaupt installiert?
    unless ($sub->IsInstalled)
    {
      debug msg('Dsubni', $subsystem, $rechner);
      next;
    }

    # Läuft das Subsystem gerade?
    if ($sub->IsRunning)
    {
      # Subsystem stoppen
      $sub->Stop || error $rechner, msg('Estop', $subsystem);
    }
    else
    {
      debug msg('Dsubisnr', $subsystem, $rechner);
    }
  }
}


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() called with wrong 'Art'-parameter!\n");
    myexit($main::FatalExitCode);
  }

  my @files = @{$files->Get($bs,$subsys,$art)};
  debug msg('Dtrans', $art);
  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');
    $seccon     = $fileobj->Get('seccon');

    # Installfiles, Initfiles, Files
    if ($art =~ /^install$|^init$|^file$|^modify$/)
    {
      RemoteCopy($file,$rechner,$zielfile,$owner,$group,$permission,$seccon);
      progress;
      next;
    }
    # Installtemplates, Inittemplates, Templates
    if ($art =~ /^installtemplate$|^inittemplate$|^template$/)
    {
      # Ersetzungsmuster anwenden
      my $pattern = $templatepattern->Get($bs,$subsys,$file,$zielfile);
      my $tempfile = "$tmp_local/sysconf.template.$subsys.$tmp_rand_pid";
      TextModify::ErsetzeMuster($file, $tempfile, $pattern, $refvar, $subsys);

      # Owner,Gruppe,Permissions,SecCon auf das neue Tempfile übertragen
      my ($mode,$uid,$gid) = (stat($file))[2,4,5];
      chmod ($mode,    $tempfile) || logdie msg('Etransmod', $mode, $tempfile, $!);
      chown ($uid,$gid,$tempfile) || logdie msg('Etransown', $uid, $gid, $tempfile, $!);

      RemoteCopy($tempfile,$rechner,$zielfile,$owner,$group,$permission,$seccon);
      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 msg('Eintart', $art, 'TransferFiles()');
  }
}


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() called with wrong 'Art'-parameter!\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 msg('Dremove', $art);
  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_local/master-sysconf-diff-$tmp_rand_pid.pl";
    $tmp_sc_diff       = "$tmp_remote/sysconf-diff-$tmp_rand_pid.pl";
    if ($art =~ /^installlink$|^initlink$|^link$|^modify$|^install$|^init$|^file$|^installtemplate$|^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 msg('Efilenowr', $local_tmp_sc_diff);
      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 msg('Efileclose', $local_tmp_sc_diff);
      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 msg('Eintart', $art, 'RemoveBackupFiles()');
  }
}


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>'.msg('Hsysconf').'</title>
' . $style .
"  </head>

  <body>
    <h1>".msg('Hsysconf')."</h1>

    <a href=\"files.txt\">".msg('Hlist')."</a><br>
    <br>

    ".msg('Hdetail').":<br>
    <br>

";
  my $tail = '    <br>
    <hr>
    <address>
      '.msg('Hfooter', `hostname`, $sysconfroot, $appname, $version).'
      <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 msg('Daction', 'documentation');
  my $fh = FileHandle->new();
  open($fh, ">$htmldir${slash}index0.html") || logdie msg('Efilenowr', "$htmldir${slash}index0.html");
  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, msg('Wnodesc');
      next;
    }

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

    my @subsys = CheckSubsystems($rechner,$beschreibungen,$param);
    unless (@subsys)
    {
      warning $rechner, msg('Wnosubsysexist');
      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 msg('Emkdir', $verz, $!);
    }
    my $rfh = FileHandle->new();
    open($rfh, ">$verz${slash}$rechner.html") || logdie msg('Efilenowr', "$verz${slash}$rechner.html");
    print $rfh '<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html>
  <head>
    <title>'.msg('Hsysconf').' - ' . $rechner . '</title>
' . $style .
'  </head>

  <body>
    <h1>'.msg('Hsysconf')." - $rechner</h1>
    <table border=1>
      <tr>
        <th$class>".msg('Hhost')."</th>
        <th$class>".msg('Hsubsys')."</th>
        <th$class>".msg('Hvar')."</th>
        <th$class>".msg('Hos')."</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 msg('Efilenowr', "$verz${slash}$rechner-$key.txt");
      print $vfh $value;
      close($vfh) || logdie msg('Efileclose', "$verz${slash}$rechner-$key.txt");
    }
    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 msg('Efileclose', "$htmldir${slash}index0.html");
  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 msg('Daction', 'list_hosts_per_os');
  my $rechner;
  foreach $rechner ( ExpandiereALLRechner() )
  {
    # Testen, ob es eine Beschreibung zu dem Rechner gibt
    unless (defined ($bs = $beschreibungen->Betriebssystem($rechner)) )
    {
      warning $rechner, msg('Wnodesc');
      next;
    }
    print "$rechner\n" if $query_os eq $bs;
  }
  myexit;
}


sub ListVariable
{
  # Ausgabe von Variablen-Inhalten der Rechner
  # Parameter: Beschreibungen-Objekt, ParameterListe-Objekt, Variablenname
  # Return:    -
  #
  my ($beschreibungen,$param,$query_variable) = @_;
  debug msg('Daction', 'list_variable');
  my $rechner;
  my $tmp;
  foreach $rechner ( $param->Rechner )
  {
    # Testen, ob es eine Beschreibung zu dem Rechner gibt
    unless (defined ($bs = $beschreibungen->Betriebssystem($rechner)) )
    {
      warning $rechner, msg ('Wnodesc');
      next;
    }
    %RechnerVars = ReadVariables($rechner,$beschreibungen);
    $tmp = $RechnerVars{$query_variable} || 'UNDEF';
    print "$rechner:",$tmp,"\n";
  }
  myexit;
}


sub ListHostsSubsys
{
  # Ausgabe einer Liste der Rechner mit Subsystemen
  # Parameter: Beschreibungen-Objekt, ParameterListe-Objekt
  # Return:    -
  #
  my ($beschreibungen,$param) = @_;
  debug msg('Daction', 'list_hosts_subsys');
  my $rechner;
  foreach $rechner ( $param->Rechner )
  {
    # Testen, ob es eine Beschreibung zu dem Rechner gibt
    unless (defined ($bs = $beschreibungen->Betriebssystem($rechner)) )
    {
      warning $rechner, msg ('Wnodesc');
      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 msg('Daction', 'list_subsys_hosts');
  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, msg ('Wnodesc');
      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 msg('Daction', 'listhosts');

  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, msg ('Wnodesc');
      next;
    }
    print "$rechner\n";
  }
  myexit;
}


sub ListHostsWithRevision
{
  # Liste aller Rechner mit Revisionsnummer ausgeben
  # Parameter: Beschreibungen-Objekt, ParameterListe-Objekt
  # Return:    -
  #
  my ($beschreibungen,$param) = @_;
  debug msg('Daction', 'listrevision');

  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, msg ('Wnodesc');
      next;
    }
    print "$rechner:",$beschreibungen->Revision($rechner),"\n";
  }
  myexit;
}


sub ListInterfaces
{
  # Liste aller Interfaces ausgeben
  # Parameter: Beschreibungen-Objekt, ParameterListe-Objekt
  # Return:    -
  #
  my ($beschreibungen,$param) = @_;
  debug msg('Daction', 'listinterfaces');

  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, msg ('Wnodesc');
      next;
    }
    print $beschreibungen->Interface($rechner),"\n";
  }
  myexit;
}


sub ListFiles
{
  # Ausgabe einer Liste aller Dateien pro Rechner
  # Parameter: Beschreibungen-Objekt, ParameterListe-Objekt
  # Return:    -
  #
  my ($beschreibungen,$param) = @_;
  my $bs;
  my $rechner;
  my $subsys;

  debug msg('Daction', 'listfiles');

  foreach $rechner ($param->Rechner)
  {
    logprint "$rechner ";
    print "$rechner:\n";
    # Testen, ob es eine Beschreibung zu dem Rechner gibt
    unless (defined ($bs = $beschreibungen->Betriebssystem($rechner)) )
    {
      warning $rechner, msg('Wnodesc');
      next;
    }
    my @subsys = CheckSubsystems($rechner,$beschreibungen,$param);
    unless (@subsys)
    {
      warning $rechner, msg('Wnosubsysexist');
      next;
    }
    unshift @subsys, 'GLOBAL';
    push @subsys, 'LAST';
    @subsys = CheckAndAddDependencies($rechner,$beschreibungen,@subsys);
    @subsys = CheckExclusions($rechner,$beschreibungen,@subsys);
    # Datei "files.sc" für dieses Betriebssystem einlesen
    ReadFilesSC($beschreibungen->Betriebssystem($rechner));
    foreach $subsys (sort @subsys)
    {
      my @artliste = ('install','installtemplate','installlink','installshell',
		      'init','inittemplate','initlink','initshell',
		      'file','template','link','shell','modify');
      my $art;
      # Alle Dateiarten durchlaufen
      foreach $art (@artliste)
      { # foreach $art
	my @files = @{$files->Get($bs,$subsys,$art)};
	my ($fileobj, $file, $zielfile);
	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 "$zielfile\n";
	    next;
	  }
	  # Installfiles, Initfiles, Files
	  if ($art =~ /^install$|^init$|^file$/)
	  {
	    print "$zielfile\n";
	    next;
	  }
	  # Modify-"Files"
	  if ($art =~ /^modify$/)
	  {
	    print "$zielfile\n";
	    next;
	  }
	  # Inittemplates, Templates
	  if ($art =~ /^installtemplate$|^inittemplate$|^template$/)
	  {
	    print "$zielfile\n";
	    next;
	  }
	  # Links
	  if ($art =~ /^link$|^installlink$|^initlink$/)
	  {
	    print "$zielfile\n";
	    next;
	  }
	  # Shellkommandos
	  if ($art =~ /^shell$|^installshell$|^initshell$/)
	  {
	    print "$kommando\n";
	    next;
	  }
	  # Sonst
	  logdie msg('Eintart', $art, 'ListFiles()');
	}
      }
    }
  } # Ende der Schleife über alle Rechner
  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','installtemplate','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>".msg('Hsub', $subsys, $rechner)."</title>
" . $style .
"  </head>

  <body>
    <h2>".msg('Hsubconf', $subsys, $rechner)."</h2>

    <table border=1>
      <tr>
        <th$class><b>".msg('Hfiletype')."</b></th>
        <th$class><b>".msg('Hfilecontent')."</b></th>
      </tr>\n";

  my $tail = '    <hr>
    <address>
      '.msg('Hfooter', `hostname`, $sysconfroot, $appname, $version).'
      <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 msg('Efilenowr', "$htmldir${slash}files0.txt");

  my $fh = FileHandle->new();
  open($fh, ">$htmldir${slash}$rechner${slash}$rechner-sub-$subsys.html") || logdie msg('Efilenowr', "$htmldir${slash}$rechner${slash}$rechner-sub-$subsys.html");
  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 (".msg('Hhidden').")<br>\n";
	print $fh_file_list "$zielfile\n"; # Ausgabe für Gesamt-File-Liste
	next;
      }

      my $zielhtml = $zielfile;
      $zielhtml =~ s![/\\:]!_!g;  # '/', '\', ':' durch '_' ersetzen
      # 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 (".msg('Hbin').")<br>\n";
	}
	next;
      }
      # Modify-"Files"
      if ($art =~ /^modify$/)
      {
	print $fh_file_list "$zielfile\n"; # Ausgabe für Gesamt-File-Liste
	print $fh "          $zielfile (".msg('Hmod').")<br>\n";
	next;
      }
      # Inittemplates, Templates
      if ($art =~ /^installtemplate$|^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_local/sysconf.template.$tmp_rand_pid";
	TextModify::ErsetzeMuster($file, $tempfile, $pattern, $refvar, $subsys);
	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 msg('Eintart', $art, 'GeneriereSubsystemHTML()');
    }
    print $fh "          <br>\n        </td>\n";
  }
  print $fh "      </tr>\n";

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

  print $fh $tail;
  close($fh) || logdie msg('Efileclose', "$htmldir${slash}$rechner${slash}$rechner-sub-$subsys.html");
}


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, msg('Evar', $fehler, $FehlerInShellVariablenErsetzung_Kommando);
  }
  else
  {
    error undef, $fehler . msg('Evarunk');
  }
}


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;
  # Mode des Files einlesen. Wenn File nicht existiert -> Fehler.
  my $tmp = (stat($file))[2] || logdie msg('Efilene', $file);
  my $fmode = $tmp & 07777;
  if ( ($fmode & ~0644) > 0 ) # mehr Rechte als "-rw-r--r--"
  {
    logdie msg('Esec', $file);
  }
  unless (-o $file)
  {
    info msg('Isecown', $file);
  }
}


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 $leading_slash = '/';
  my $verz_esc;

  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 msg('Drshscmd')."\"mkdir -p $verz\"\n";
      $verz_esc = $verz;
      if (IsWindowsPath($verz))
      {
	# Backslashes im Verzeichnisnamen durch Slashes ersetzen,
	# denn Windows kann auch sowas: mkdir d:/temp/bla
	$verz_esc =~ s/\\/\//g;
	# ohne führenden Slash
	$leading_slash = '';
      }
      # Das "-p" beim mkdir ist in Perl etwas länglich...
      $tmp = "unless (-d '$verz_esc') { my \$path = '$leading_slash'; foreach (split('/','$verz_esc')) { next if /^\\s*\$/; \$path .=  \"\$_/\"; mkdir \$path;} }";
      ($err, $result) = $socket->Perl($tmp);
      if ($err != $RC_OK)
      {
	logdie msg('Esc', $rechner, "RemoteMkdir($verz)", $result);
      }
    }
  }
  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 msg('Esc', $rechner, "unlink $file", $result);
      }
    }
  }
  else
  {
    $tmp = "$rm $file";
    RemoteShell($rechner,$tmp);
  }
}


sub RemoteCopy
{
  # Kopiert ein File auf einen anderen Rechner
  # Parameter: Quelle, Rechner, Ziel, Owner, Group, Permission, SecCon
  # 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 $seccon     = shift;
  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";

  # Zu wenig Parameter?
  unless (defined $seccon)
  {
    logdie msg('Eintpara', 'RemoteCopy()');
  }

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

  if (! $modify_by_cmd)
  {
    my $pfad;
    # Vor dem Kopieren die Verzeichnisse anlegen
    if (IsWindowsPath($ziel))
    {
      $ziel =~ /^([a-zA-z]\:\\.*\\)[^\\]+/;
      $pfad = $1;
    }
    else
    {
      $ziel =~ /(.*$slashsuch)[^$slashsuch]+/;
      $pfad = $1;
      if ($pfad eq '')
      {
	error $rechner, msg('Ercpath', $ziel);
      }
    }
    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 $chcon= $commands->Get($beschreibungen->Betriebssystem($rechner),'CHCON');
  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) || $gid; # Use group name, fallback numeric 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_remote übertragen
      $tmp = "FILE $quelle $tmp_remote/sysconf.$tmp_rand_pid permission=$permission";
      debug_rsh "Sysconf-Client: $tmp\n";
      my ($err, $result) = $socket->File($quelle, "$tmp_remote/sysconf.$tmp_rand_pid", permission=>$permission);
      if ($err != $RC_OK)
      {
	logdie msg('Esc', $rechner, $tmp, $result);
      }
      # Owner / Gruppe übertragen
      $tmp = "$chown $owner:$group $tmp_remote/sysconf.$tmp_rand_pid";
      RemoteShell($rechner,$tmp);
      if ($permission ne '')
      {
	$tmp = "$chmod $permission $tmp_remote/sysconf.$tmp_rand_pid";
	RemoteShell($rechner,$tmp);
      }
      if ($seccon ne '')
      {
	$tmp = "$chcon $seccon $tmp_remote/sysconf.$tmp_rand_pid";
	RemoteShell($rechner,$tmp);
      }
      # Dann per sudo an den korrekten Ort verschieben
      $tmp = "$mv $tmp_remote/sysconf.$tmp_rand_pid $ziel";
      RemoteShell($rechner,$tmp);
    }
    else
    {
      $tmp = "FILE $quelle $ziel owner=$owner group=$group permission=$permission seccon=$seccon";
      debug_rsh "Sysconf-Client: $tmp\n";
      my ($err, $result) = $socket->File($quelle, $ziel, owner=>$owner, group=>$group, permission=>$permission, seccon=>$seccon);
      if ($err != $RC_OK)
      {
	logdie msg('Esc', $rechner, $tmp, $result);
      }
    }
  }
  else
  ###
  ### Über rsh/ssh ...
  ###
  {
    my $rcp = $beschreibungen->GetRCP($rechner);
    logdie msg('Ercno', $rechner) 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_remote übertragen
	$tmp = "$rcp -p $quelle $user\@$interface:$tmp_remote/sysconf.$tmp_rand_pid 2>&1";
      }
      debug_rsh "$tmp\n";
      if (! $dry_run)
      {
	$output = `$tmp`;
	logdie msg('Ersh', $rechner, $tmp, $output) if ($?>>8);
      }
      # Dann ggf. per sudo an den korrekten Ort verschieben
      if ($use_sudo)
      {
	# Owner / Gruppe übertragen
	$tmp = "$chown $owner:$group $tmp_remote/sysconf.$tmp_rand_pid";
	RemoteShell($rechner,$tmp);
	if ($permission ne '')
	{
	  $tmp = "$chmod $permission $tmp_remote/sysconf.$tmp_rand_pid";
	  RemoteShell($rechner,$tmp);
	}
	if ($seccon ne '')
	{
	  $tmp = "$chcon $seccon $tmp_remote/sysconf.$tmp_rand_pid";
	  RemoteShell($rechner,$tmp);
	}
	# Und jetzt erst verschieben
	$tmp = "$mv $tmp_remote/sysconf.$tmp_rand_pid $ziel";
	RemoteShell($rechner,$tmp);
      }
    }
    else # Interface ist "localhost"
    {
      $tmp = "$sudo$cp $quelle $ziel";
      debug_rsh "$tmp\n";
      if (! $dry_run)
      {
	system($tmp);
	logdie msg('Ersh', $rechner, $tmp, ($?>>8)) if ($?>>8);
      }
    }

    # Owner / Gruppe übertragen
    $tmp = "$chown $owner:$group $ziel";
    RemoteShell($rechner,$tmp);
    if ($permission ne '')
    {
      $tmp = "$chmod $permission $ziel";
      RemoteShell($rechner,$tmp);
    }
    if ($seccon ne '')
    {
      $tmp = "$chcon $seccon $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 msg('Esc', $rechner, $tmp, $result);
      }
    }
  }
  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 msg('Esc', $rechner, $kommando, $result);
    }
  }
  else
  {
    my $rsh = $beschreibungen->GetRSH($rechner);
    logdie msg('Ersno', $rechner) 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 msg('Ersh', $rechner, $kommando, $result) 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 msg('Elsh', $kommando, $result) 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
  # SYSCONF_REVISION            entspricht dem REVISION-Eintrag  in hosts.sc
  # SYSCONF_ROOT                entspricht dem SYSCONF_ROOT in .sysconfrc
  # SYSCONF_HOST                entspricht dem HOST Eintrag in hosts.sc
  #
  # 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 $revision            = $beschreibungen->Revision($rechner);
  my %hash  = ();
  my $tmp;

  # Gibt es das Verzeichnis für die Variablen?
  unless (-d "$sysconfroot${slash}variables")
  {
   logdie msg('Edirne', "$sysconfroot${slash}variables");
  }

  my $file = "$sysconfroot${slash}variables$slash$rechner.var";
  # Ist das File lesbar?
  unless (-r $file)
  {
    $tmp = LinksAufloesen($file);
    if (! defined $tmp)
    {
      logdie msg('Efilene', $file);
    }
    $file = $tmp;
  }

  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);
  $hash{SYSCONF_REVISION}            = $revision;
  $hash{SYSCONF_ROOT}                = $sysconfroot;
  $hash{SYSCONF_HOST}                = $rechner;
  $hash{SYSCONF_ACTION}              = $param->Aktion;

  # 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 msg('Efileno', $file);
  # 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 msg('Evarerr', $file, $., $_);
    }
    if (defined $hash{$var})
    {
      logdie msg('Evardup', $var, $file, $.);
    }
    # 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";
	if (! -r $varfile)
	{
	  $tmp = LinksAufloesen($varfile);
	  if (! defined $tmp)
	  {
	    logdie msg('Evarinc', $var, $einzelfile, $file, "$sysconfroot${slash}variables");
	  }
	  $varfile = $tmp;
	}
        my $fh = FileHandle->new();
        open($fh, $varfile) || logdie msg('Efileno', $varfile);
        {
          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);

  debug "CheckAndAddDependencies(): ",join(" ",@subsysteme),"\n";

  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) : () );

  # Wir müssen ggf. das Hinzufügen von Abhängigkeiten mehrmals durchführen,
  # da es sein, dass eine hinzugefügte Abhängigkeit eine weitere benötigt!

  my $subs_old;
  my @alle_subs = ();
  my $sub;
  my $sub_soft;
  my $pos;

  my $NOCHMAL = $TRUE;
  while($NOCHMAL)
  {
    $NOCHMAL = $FALSE;

    # nur für den Vorher-Nacher-Vergleich einen String daraus machen
    $subs_old = join('',@subsysteme);

    foreach $sub (@subsysteme)
    {
      debug msg('Ddepwish', $sub);
      # Weiche Abhängigkeiten
      if (defined $dep_soft{$sub}) # Wenn es weiche Abhängigkeiten gibt...
      {
	foreach $sub_soft (@{$dep_soft{$sub}})
	{
	  debug msg('Ddepsoft', $sub, $sub_soft);
	  if ($ist_erlaubt{$sub_soft}) # ... und diese auch noch erlaubt sind ...
	  {
	    debug msg('Ddepsok');
	    # ... dann dazunehmen!
	    push @alle_subs, $sub_soft;
	  }
	  else
	  {
	    debug msg('Ddepsnok');
	  }
	}
      }

      # Harte Abhängigkeiten
      unless (defined $dep{$sub})
      {
	# Wenn es keine Abhängigkeit gibt, dann das Subsystem einfach nehmen
	debug msg('Ddephardno',$sub);
	push @alle_subs, $sub;
	next;
      }
      # Ansonsten die Abängigkeiten und das Subsystem nehmen
      debug msg('Ddephardyes', $sub, join(',',@{$dep{$sub}}).",$sub");
      # Wir müssen hier erst einmal schauen, ob $sub schon in der Liste ist
      # (von den weichen Abhängigkeiten) und dann das erste Vorkommen von $sub
      # durch "@{$dep{$sub}}, $sub" ersetzen, sonst würde es passieren, dass
      # die Reihenfolge nicht mehr stimmt!
      $pos = GetPosInArray($sub, @alle_subs);
      if (defined $pos)
      {
	splice(@alle_subs, $pos, 1, @{$dep{$sub}},$sub);
      }
      else
      {
	# ansonsten können wir das einfach hinter dranhängen
	push @alle_subs, @{$dep{$sub}}, $sub;
      }
    }

    @alle_subs = KompaktiereSubsystemListe(@alle_subs);

    # nur für den Vorher-Nacher-Vergleich einen String daraus machen
    my $subs_new = join('',@alle_subs);

    # Wenn neue Subsysteme hinzugekommen sind, dann den Code für Hinzufügen
    # nochmals durchlaufen
    if ($subs_old ne $subs_new)
    {
      debug "CheckAndAddDependencies(): Repetition needed...\n";
      @subsysteme = @alle_subs;
      $NOCHMAL = $TRUE;
    }
  } # Ende der NOCHMAL-Schleife

  # 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, msg('Wdepsub', $subsys);
    }
  }

  return @result;
}


sub GetPosInArray
{
  # Parameter: Suchstring, Array
  # Return: Position im Array
  # Es wird im Array nach dem Substring gesucht und dessen Position
  # zurückgegeben. Wenn es nicht gefunden wird, dann undef.
  # Beispiel:
  # Suchstring: X
  # Liste: A B C X D E F
  # Return: 3

  my $such = shift;
  my @arr  = @_;
  my $i = 0;
  foreach (@arr)
  {
    return $i if $such eq $_;
    $i++;
  }
  return undef;
}


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)
  {
    logdie msg('Eintpara', 'ReadDependencies()');
  }
  my $bs = shift;
  my $tmp;

  debug "ReadDependencies($hard_soft,$bs)\n";

  # 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));
  }

  debug "ReadDependencies() Start...\n";

  # Gibt es das Verzeichnis für das Betriebssystem?
  unless (-d "$sysconfroot$slash$bs")
  {
    logdie msg('Edirne', "$sysconfroot$slash$bs");
  }

  our $depfile = "$sysconfroot$slash$bs$slash$depfilename";
  unless (-r $depfile)
  {
    $tmp = LinksAufloesen($depfile);
    if (! defined $tmp)
    {
      logdie msg('Efilene', $depfile);
    }
    $depfile = $tmp;
  }

  TesteDateiBesitzer($depfile);

  our %dep = ();

  ###
  ### Abhängigkeiten einlesen
  ###
  my $fh = FileHandle->new();
  open($fh, $depfile) || logdie msg('Efileno', $depfile);
  # 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 msg('Edep', $., $_);
    }
    my ($sub,$deps) = ($1,$2);
    if (defined $dep{$sub})
    {
      logdie msg('Edepmulti', $sub, $.);
    }
    $dep{$sub} = [ split(/\s+/,$deps) ];
  }
  close $fh; # Nur lesender open() => Kein Fehler möglich.

  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 msg('Edepcyc', $d, $depfile);
	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 übernehmen
        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 msg('Ddep',$hard_soft);
  foreach (sort keys %dep) { debug "$_ -> @{$dep{$_}}\n"; }
  debug "-----\n";

  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, msg('Wex', $_, $sub);
	  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 msg('Edirne', "$sysconfroot$slash$bs");
  }

  my $exclfile = "$sysconfroot$slash$bs$slash$exclfilename";
  unless (-r $exclfile)
  {
    $tmp = LinksAufloesen($exclfile);
    if (! defined $tmp)
    {
      logdie msg('Efilene', $exclfile);
    }
    $exclfile = $tmp;
  }

  TesteDateiBesitzer($exclfile);

  my %excl = ();

  ###
  ### Ausschlüsse einlesen
  ###
  my $fh = FileHandle->new();
  open($fh, $exclfile) || logdie msg('Efileno');
  # 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 msg('Eexerr', $., $_);
    }
    push @{$excl{$1}}, $2;
  }

  debug "-----\n";
  debug msg('Dex');
  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',
	      CHCON        => 'chcon',
	      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 msg('Edirne', "$sysconfroot$slash$bs");
  }

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

  if (! defined $commandsfile)
  {
    warning undef, msg('Wcomm', "$sysconfroot$slash$bs$slash$commandfilename");
  }
  else
  {
    TesteDateiBesitzer($commandsfile);

    ###
    ### Kommandos einlesen
    ###
    my $fh = FileHandle->new();
    open($fh, $commandsfile) || logdie msg('Efileno', $commandsfile);
    # 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
    # CHCON=/usr/bin/chcon
    # 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{CHCON}       = $1 if /^CHCON\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 msg('Drc');
  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 msg('Eintpara', 'ReadFileAndExpandIncludes()');
  my $include_files_history = '';

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

  #
  # Falls der Pfad ein aufgelöster LINK ist, dann würde im falschen Verzeichnis
  # nach Subsystemen und INCLUDEs gesucht werden.
  # Daher Entfernen der "../"-Teile. Beispiel:
  # Vorher Pfad mit aufgelöstem LINK:
  # sysconf/Linux-RedHat-RHEL-5/../Linux-RedHat-RHEL-4/
  # nachher:
  # sysconf/Linux-RedHat-RHEL-5/
  #
  $pfad =~ s/^(.+?)\.\.\/.*$/$1/;

  my $fh = FileHandle->new();
  open($fh, $file) || logdie 'ReadFileAndExpandIncludes()'.msg('Efilene', $file);
  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(): '.msg('Efilenelink', $include_file, "$include_file.LINK");
	}
	$include_file = $tmp;
      }
      my $fhinc = FileHandle->new();
      $include_files_history .= "$include_file\n";
      open($fhinc, $include_file) || logdie 'ReadFileAndExpandIncludes(): '.msg('Efileno', $include_file);
      {
	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()-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(): '.msg('Efilenelink', $include_file, "$include_file.LINK");
	  }
	  $include_file = $tmp;
	}
	my $fhinc = FileHandle->new();
	$include_files_history .= "$include_file\n";
	open($fhinc, $include_file) || logdie 'ReadFileAndExpandIncludes(): '.msg('Efileno', $include_file);
	{
	  local $/ = undef;
	  $tmp = <$fhinc>;
	}
	$inhalt = $inhalt . $tmp;
	close $fhinc;
      }
      else
      {
	$inhalt = $inhalt . $zeile . "\n";
      }
    }
  }

  if ($rekursions_level <= 0)
  {
    logdie msg('Erfaeierr', $max_rekursions_level, $include_files_history);
  }

  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 = (
		# Install
                instf => 'install',
		instt => 'installtemplate',
                instL => 'installlink',
                instS => 'installshell',
		# Init
                initf => 'init',
                initt => 'inittemplate',
                initL => 'initlink',
                initS => 'initshell',
		# Update
                f     => 'file',
                t     => 'template',
                L     => 'link',
		# cmd-Files
		c     => 'cmdtemplate',
		# Modify
		m     => 'modify',
                # Shell-Kommandos
                S     => 'shell',
               );
  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 msg('Edirne', "$sysconfroot$slash$bs");
  }

  my $file = "$sysconfroot$slash$bs$slash$files_sc";
  unless (-r $file)
  {
    $tmp = LinksAufloesen($file);
    if (! defined $tmp)
    {
      logdie msg('Efilenelink', $file, "$file.LINK");
    }
    $file = $tmp;
  }

  TesteDateiBesitzer($file);

  ###
  ### "files.sc" einlesen
  ###
  my $fh = FileHandle->new();
  my $files_sc_inhalt = ReadFileAndExpandIncludes($file);
  debug msg('Dfscexp', $files_sc_inhalt);
  # Variable wie ein File oeffnen. Das geht ab Perl v5.8.0
  open($fh, '<', \$files_sc_inhalt) || logdie msg('Eimv');

  # 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, $seccon);
  my @zeile;
  # Den ersten Subsystem-Abschnitt finden
  while(<$fh>)
  {
    next if /^\#/;   # Kommentare
    next if /^\s*$/; # Leerzeilen
    if (! /^\[(.+)\]/)
    {
      chomp;
      logdie msg('Efscsub', $_, $file, $.);
    }
    else
    {
      $subsys = $1;
      last;
    }
  }
  # Jetzt im Subsystem-Abschnitt weiterlesen
  while(<$fh>)
  {
    next if /^\#/;   # Kommentare
    next if /^\s*$/; # Leerzeilen
    # Nächster Subsystem-Abschnitt?
    if (/^\[(.+)\]/)
    {
      $subsys = $1;
      next;
    }
    # Zeile parsen. Folgender Aufbau z.B.:
    # f usr/local/bin/adm /tmp/adm permission=750 owner=g0014 group=users
    #                                   seccon=system_u:object_r:etc_t:s0
    # Die ersten beiden Parameter sind fest, die restlichen optional.
    @zeile  = split(' ',$_);
    $art    = shift @zeile;
    $quelle = shift @zeile;
    $ziel   = $quelle;
    $owner  = '';
    $group  = '';
    $perm   = '';
    $seccon = '';
    $hidden = $FALSE;
    foreach (@zeile)
    {
      if    (/owner=(.+)/)      { $owner  = $1; next; }
      elsif (/group=(.+)/)      { $group  = $1; next; }
      elsif (/permission=(.+)/) { $perm   = $1; next; }
      elsif (/seccon=(.+)/)     { $seccon = $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",
    "SecCon: '"  ,defined $seccon ? $seccon : 'undef',"'\n",
    "Hidden: '"  ,defined $hidden ? $hidden : 'undef',"'\n";

    # File-Art testen
    unless (defined $artkey{$art})
    {
      logdie msg('Eart', $art, $file, $.);
    }

    # Templates
    if ($artkey{$art} =~ /^template$|^installtemplate$|^inittemplate$/)
    {
      # Kein absoluter Pfad, dann ist das File im Unterverzeichnis filedir.sc/
      unless ($quelle =~ /^\//)
      {
        $quelle = $sysconfroot.$slash.$bs.$slash.'filedir.sc'.$slash.$quelle;
      }

      if (IsWindowsPath($ziel))
      {
	debug "Target '$ziel' seems to be Windows volume. Not adding leading slash.\n";
      }
      else # Unix
      {
	# 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, msg('Efscnofile', $tmp, "$tmp.LINK", $file, $zeilennummer);
	  next;
	}
      }
      $files->Set($bs, $subsys, $artkey{$art}, 'quelle' => $quelle,
		  'ziel' => $ziel, 'owner' => $owner, 'group' => $group,
		  'permission' => $perm, 'seccon' => $seccon,
		  'hidden' => $hidden);

      # Den Patternblock einlesen
      my $pattern = <$fh>;
      unless ($pattern =~ /^beginpattern$/)
      {
	logdie msg('Efscbpat', $file, $.);
      }
      my $ganzes_muster = '';
      while( defined ($pattern = <$fh>) )
      {
        last if $pattern =~ /^endpattern$/;
        if ($pattern =~ /^\[/)
        {
	  logdie msg('Efscnoend', $file, $.);
	}
        $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
      # removecmd
      # 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 'removecmd')      &&
	  ($quelle ne 'pre_localshell') &&
	  ($quelle ne 'post_localshell')
	)
      {
	error undef, msg('Efscerr', $., $file);
	next;
      }

      # Den Patternblock einlesen
      my $pattern = <$fh>;
      unless ($pattern =~ /^beginpattern$/)
      {
	logdie msg('Efscbpat', $file, $.);
      }
      my $ganzes_muster = '';
      while( defined ($pattern = <$fh>) )
      {
        last if $pattern =~ /^endpattern$/;
        if ($pattern =~ /^\[/)
        {
	  logdie msg('Efscnoend', $file, $.);
	}
        $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, 'seccon' => $seccon,
		  '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;
      }

      if (IsWindowsPath($ziel))
      {
	debug "Target '$ziel' seems to be Windows volume. Not adding leading slash.\n";
      }
      else # Unix
      {
	# 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, msg('Efscnofile', $tmp, "$tmp.LINK", $file, $zeilennummer);
	  next;
	}
      }
      $files->Set($bs, $subsys, $artkey{$art}, 'quelle' => $quelle,
		  'ziel' => $ziel, 'owner' => $owner, 'group' => $group,
		  'permission' => $perm, 'seccon' => $seccon,
		  'hidden' => $hidden);
    }

    # Links prüfen
    if ($artkey{$art} =~ /link/)
    {
      # Link-Quelle ohne absoluten Pfad
      unless ($quelle =~ /^\//)
      {
        logdie msg('Efsclinks', $file, $.);
      }
      # Link-Ziel ohne absoluten Pfad
      unless ($ziel =~ /^\//)
      {
        logdie msg('Efsclinkt', $file, $.);
      }
      $files->Set($bs, $subsys, $artkey{$art}, 'quelle' => $quelle,
		  'ziel' => $ziel, 'owner' => $owner, 'group' => $group,
		  'permission' => $perm, 'seccon' => $seccon,
		  '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' (from Cache)\n";
    return $tmp;
  }
  debug "LinksAufloesen() (not in 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, msg('Elinki', $linkfile);
	  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, msg('Elinke', $tmp, $linkfile);
	    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 ReadFile
{
  # Parameter: Filename
  # Return:    Inhalt des Files als Scalar
  #
  my $file = shift;
  my $fh = FileHandle->new();
  open($fh, $file) || logdie msg('Efileno', $file);
  {
    local $/ = undef; # In einem Stück einlesen
    return <$fh>;
  }
}


sub ReadFileAsList
{
  # Parameter: Filename
  # Return:    Inhalt des Files als Array

  my $file = shift;
  my $fh = FileHandle->new();
  open($fh, $file) || logdie msg('Efileno', $file);
  {
    return <$fh>;
  }
}


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, msg('Efileno', $file);
  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 = @_;

  # Tivial-Fall: Nur ein Subsystem angegeben
  unless (defined $subsys[1])
  {
    return $subsys[0];
  }

  debug 'KompaktiereSubsystemListe(' . join(' ',@subsys) . ")\n";
  my @result_deps = ();
  my %schon_enthalten = ();
  my $s;
  foreach $s (@subsys)
  {
    next if $schon_enthalten{$s};
    push @result_deps, $s;
    $schon_enthalten{$s} = $TRUE;
  }
  debug '   -> ' . join(' ',@result_deps) . "\n";
  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 msg('Thosts', "             $rechner");
  debug msg('Tsubsysok', '  '.join(' ',@erlaubt));
  debug msg('Tsubsys', join(" ",@wanted));

  # 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, msg('Wsknok', $_, $subsys);
	  }
        }
      }
      else
      {
        warning $rechner, msg('Wsubnok', $subsys);
      }
    }
  }

  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_local/\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_local/\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 GetLanguage
{
  # Es wird versucht einen eventuellen LANGUAGES-Eintrag aus
  # sysconfrc zu lesen.
  # Ohne Fehlerbehandlung, da diese Funktion sehr früh aufgerufen wird.
  #
  # Parameter: -
  # Return:    Sprache, z.B, "de_DE"
  #
  my $language = 'de_DE'; # Standardeinstellung

  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
      $language = $1 if /^LANGUAGE\s*=\s*(.+)/i;
    }
    close $fh; # Nur lesender open() => Kein Fehler möglich.
  }

  return $language;
}


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 msg('Econfig', $ENV{HOME}) unless -r $file;
  logprint msg('Trcfu', $file);

  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;
    $client_port_ssl  = $1     if /^CLIENT_PORT_SSL\s*=\s*(\d+)/i;
    $client_ssl_cert_file = $1 if /^SSL_CERT_FILE\s*=\s*(.+)/i;
    $client_ssl_key_file  = $1 if /^SSL_KEY_FILE\s*=\s*(.+)/i;
    $language         = $1     if /^LANGUAGE\s*=\s*(.+)/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 msg('Econfigerr', 'SYSCONF_ROOT', $file) if $sysconfroot eq '';
  logdie msg('Econfigerr', 'HTMLDIR',      $file) 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);

  if (! defined $classes_sc_file)
  {
    logdie msg('Efilenelink', $classes_sc, "$classes_sc.LINK");
  }

  open($Klassen_File, $classes_sc_file) || logdie msg('Efilenelink3', $classes_sc, "$classes_sc.LINK", $classes_sc_file);

  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 msg('Eclass', $_);
      }
      ($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
  # Es wird die globale Variable @ALLHOSTS mit allen Rechner-Namen gefüllt

  my $beschreibungen = RechnerBeschreibung::new();
  my $fh = FileHandle->new();
  my ($rechner,$interface,$betriebssystem,$klassen,$subsysteme,$revision);
  my @input = ();
  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 msg('Efilenelink3', $hosts_sc, "$hosts_sc.LINK",  $hosts_sc_file);
  TesteDateiBesitzer($hosts_sc_file);
  @input = <$fh>; # Beschreibungsfile einlesen
  close($fh); # Nur lesender open() => Kein Fehler möglich.

  # Alle Dateien im optionalen Verzeichnis hostsdir.sc einlesen
  my $hostsdir_sc = "$sysconfroot${slash}hostsdir.sc";
  if ( (-e $hostsdir_sc) || (-e "${hostsdir_sc}.LINK") )
  {
    $hostsdir_sc = LinksAufloesen($hostsdir_sc);
    debug msg('Drbefiles', $hostsdir_sc);
    my $fhdir = FileHandle->new();
    opendir($fhdir, $hostsdir_sc) || logdie msg('Efilenelink', $hostsdir_sc, "$hostsdir_sc.LINK");
    my @files = readdir($fhdir);
    closedir($fhdir);
    my $file;
    foreach $file (@files)
    {
      debug "RechnerBeschreibungenEinlesen(): $file\n";
      # Name muss alphanumerisch beginnen und auf ".inc" enden.
      next unless $file =~ /^[a-zA-Z0-9].*\.inc$/;
      $file = $hostsdir_sc . $slash . $file;
      debug msg('Drbefile', $file);
      TesteDateiBesitzer($file);
      # Dateiinhalt an das Input-Array einfach anhängen
      push @input, ReadFileAsList($file);
    }
  }

  # jetzt steht in @input die hosts.sc und alles aus hostsdir.sc/*.inc

  foreach (@input)
  {
    next if /^\#/;   # Kommentare überspringen
    s/\#.*$//g;      # Kommentare entfernen (alles, was hinter '#' steht.)
    next if /^\s*$/; # Leerzeilen überspringen
    push @Besch_File, $_;
  }

  $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 msg('Ehostdef', $zeile);
	}
      }
    }
    else
    {
      logdie msg('Ehostdef', $zeile);
    }
    $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 msg('Eclassne', $klasse) }
      push @subsys, @{$klassendef{$klasse}};
      # und Liste aller Klassennamen speichern für SYSCONF_CLASSES:
      push @klassen_liste, $klasse;
    }

    # Einzelne Subsystem können durch führendes "!" auch ausgeschlossen
    # werden, also z.B. "!login"
    # Diese aus der Liste wieder entfernen:
    my @subsys_final = ();
    my %subsystems_to_remove = ();
    foreach (@subsys)
    {
      if ($_ eq '!')
      {
	error $rechner, msg('Esubdef',$rechner);
	next;
      }
      if ( (/^!(.+)/) || defined $subsystems_to_remove{$_} )
      {
	debug msg('Dremsub', $1);
	# Merken, dass dieses Subsystem nicht gewünscht ist
	$subsystems_to_remove{$1}++;
	next;
      }
      else
      {
	push @subsys_final, $_;
      }
    }

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


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

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

  logdie msg('Eintpara', 'ParameterEinlesen()') unless defined $rechner;

  unless ($aktion =~ /^init$|^update$|^remove$|^start$|^stop$|^documentation$|^listhosts$|^listinterfaces$|^listreposfiles$|^listrevision$|^listfiles$|^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:    Liste aller Rechner
  # Es erfolgt lesender Zugriff auf die globale Variable @ALLHOSTS
  #

  return @ALLHOSTS;
}


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;

  # check, if hostname (or IP) can be resolved
  my (undef,undef,undef,undef,@addrs) = gethostbyname($rechner);
  return $FALSE if ! defined $addrs[0];

  if ($UID == 0)
  {
    $ping = Net::Ping->new('icmp'); # ICMP/Echo (root only!)
    $erfolgreich = $ping->ping($rechner);
    return $TRUE if $erfolgreich;
  }

  # if this was not successful or we are not running as root
  $ping = Net::Ping->new('tcp'); # TCP/Echo
  $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 "----------\n".msg('Terrsum', $logfile);
    foreach (keys %Rechner_mit_Fehlern)
    {
      $temp = "$_:\n" if ($_ ne '');
      print "$temp$Rechner_mit_Fehlern{$_}";
    }
  }

  if (keys %rechner_noch_nicht_abgearbeitet)
  {
    logprint "----------\n" . msg('Terrns');
    print "----------\n"    . msg('Terrns');
    logprint join(",",sort keys %rechner_noch_nicht_abgearbeitet),"\n";
    print join(",",sort keys %rechner_noch_nicht_abgearbeitet),"\n";
  }

  # 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;
  }

  logprint("$appname PID $$ ".msg('Tendat', date, $error));
  close(LOG);

  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 Error-Meldungen 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 $]\n".msg('Tstartat', date, $login));
}


sub catch_signal
{
  my $signame = shift;
  logdie msg('Eendsig', $appname, $signame);
}


sub msg
{
  my $msgid     = shift;
  my @variables = @_;

  my %map = (
	     # fixme: Man sollte hier klassifizieren: W==1, E==2, etc.
	  Eendsig => {
		     'de_DE' => "02-SIG: Ende von %s wegen Signal SIG%s.\n",
		     'en_US' => "02-SIG: End of %s caused by Signal SIG%s.\n",
		    },
	  Elogwr => {
		     'de_DE' => "Kann Logfile '%s' nicht schreiben!\n",
		     'en_US' => "Can't write to logfile '%s'\n",
		    },
	  Eoption => {
		      'de_DE' => "Option '%s' gibt es nicht!\n",
		      'en_US' => "Option '%s' is not valid!\n",
		     },
	  Eart => {
		   'de_DE' => "Die File-Art '%s' in '%s' Zeile %s ist ungültig!\n",
		   'en_US' => "The file-type '%s' in '%s' line %s is not valid!\n",
		  },
	     Enoping => {
			 'de_DE' => "02-PIN: Per Ping (ICMP/echo, TCP/echo, TCP/SSH) ueber Interface '%s' nicht erreichbar!\n",
			 'en_US' => "02-PIN: Ping (ICMP/echo, TCP/echo, TCP/SSH) via interface '%s' not possible!\n",
			},
	     Enossh => {
			'de_DE' => "02-SSH: Per RSH/SSH ohne Kennwort ueber Interface '%s' nicht erreichbar!\n",
			'en_US' => "02-SSH: RSH/SSH-access without password via interface '%s' not possible!\n",
		    },
	  Eaction => {
		      'de_DE' => "Die Aktion %s gibt es nicht!\n",
		      'en_US' => "Action %s does not exist!\n",
		     },
	  Ereconf => {
		      'de_DE' => "02-REC: Rekonfigurieren von '%s' nicht erfolgreich!\n",
		      'en_US' => "02-REC: Reconfiguration of '%s' not successful!\n",
		     },
	  Einstall => {
		       'de_DE' => "02-INS: Install von '%s' nicht erfolgreich!\nBreche '%s' ab!\n",
		       'en_US' => "02-INS: Install of '%s' not successful!\nAborting '%s'!\n",
		      },
	  Estart => {
		     'de_DE' => "Start von '%s' nicht erfolgreich!\n",
		     'en_US' => "Start of '%s' not successful!\n",
		    },
	  Estop => {
		    'de_DE' => "Stoppen von '%s' nicht erfolgreich!\n",
		    'en_US' => "Stop of '%s' not successful!\n",
		   },
	  Eremove => {
		      'de_DE' => "Remove von '%s' nicht erfolgreich!\n",
		      'en_US' => "Remove of '%s' not successful!\n",
		     },
	  Etransmod => {
			'de_DE' => "Fehler in TransferFiles() bei chmod(%s,'%s'): %s\n",
			'en_US' => "Error in TransferFiles() at chmod(%s,'%s'): %s\n",
		       },
	  Etransown => {
			'de_DE' => "Fehler in TransferFiles() bei chown(%s,%s,'%s'): %s\n",
			'en_US' => "Error in TransferFiles() at chown(%s,%s,'%s'): %s\n",
		       },
	  Eintart => {
		      'de_DE' => "Interner Fehler: File-Art '%s' fuer %s ist ungueltig!\n",
		      'en_US' => "Internal error: file-type '%s' for %s is not valid!\n",
		     },
	  Efilenowr => {
			'de_DE' => "Kann '%s' nicht zum Schreiben oeffnen!\n",
			'en_US' => "Can not open '%s' for writing!\n",
		       },
	  Efileno => {
		      'de_DE' => "Kann '%s' nicht zum Lesen oeffnen!\n",
		      'en_US' => "Can not open '%s' for reading!\n",
		     },
	  Efileclose => {
			 'de_DE' => "Fehler beim Schliessen von '%s'.\n",
			 'en_US' => "Error while closing '%s'.\n",
			},
	  Emkdir => {
		     'de_DE' => "Kann Verzeichnis '%s' nicht anlegen (%s)!\n",
		     'en_US' => "Can not create directory '%s': %s!\n",
		    },
	  Evar => {
		   'de_DE' => "Fehler in der Shell-Variablenersetzung:\n'%s'\nSie haben in folgendem Shell-Kommando eine Variable verwendet, die Sie aber nicht definiert haben:\n'%s'\n",
		   'en_US' => "Error in shell-variables-replacement:\n'%s'\nYou used a variable in the following shell-command, which you did not define before:\n'%s'\n",
		  },
	  Etextvar => {
		       'de_DE' => "Fehler in der Variablenersetzung:\n'%s'\nSie haben in folgender Zeile eine Variable verwendet, die Sie aber nicht definiert haben:\n'%s'\n",
		       'en_US' => "Error in variables-replacement:\n'%s'\nYou used a variable in the following line, which you did not define before:\n'%s'\n",
		      },
	  Evarunk => {
		      'de_DE' => "Nicht abgefangener Fehler in Shell-Variablenersetzung!\n",
		      'en_US' => "Unexpected error in shell-variables-replacement!\n",
		     },
	  Etextvarunk => {
			  'de_DE' => "Nicht abgefangener Fehler in Variablenersetzung!\n",
			  'en_US' => "Unexpected error in shell-variables-replacement!\n",
			 },
	  Esec => {
		   'de_DE' => "Sicherheitslücke: Das File '%s' ist für andere Benutzer schreibbar!\n",
		   'en_US' => "Security-hole: The file '%s' is writeable for other users!\n",
		  },
	  Esc => {
		  'de_DE' => "02-SCE: %s: Sysconf-Client liefert Fehler bei '%s': %s\n",
		  'en_US' => "02-SCE: %s: Sysconf-Client returns error at '%s': %s\n",
		 },
	  Ersh => {
		   'de_DE' => "02-RSE: %s: RemoteShell() liefert Fehler bei '%s': %s\n",
		   'en_US' => "02-RSE: %s: RemoteShell() returns error at '%s': %s\n",
		  },
	  Elsh => {
		   'de_DE' => "LocalShell() liefert Fehler bei '%s': %s\n",
		   'en_US' => "LocalShell() returns error at '%s': %s\n",
		  },
	  Eintpara => {
		       'de_DE' => "%s ohne oder mit zuwenig Parametern aufgerufen!\n",
		       'en_US' => "%s called without or with too less parameters!\n",
		      },
	  Ercpath => {
		      'de_DE' => "Das Ziel '%s' muss ein absoluter Dateiname mit Pfad sein!\n",
		      'en_US' => "The target '%s' has to be a absolute filename with path!\n",
		     },
	  Ercno => {
		    'de_DE' => "%s: Kein Remote-Copy-Zugriff moeglich!\n",
		    'en_US' => "%s: No Remote-Copy-access possibleaccess possible!\n",
		   },
	  Ersno => {
		    'de_DE' => "%s: Kein Remote-Shell-Zugriff moeglich!\n",
		    'en_US' => "%s: No Remote-Shell-access possible!\n",
		   },
	  Edirne => {
		     'de_DE' => "Das Verzeichnis '%s' existiert nicht!\n",
		     'en_US' => "Directory '%s' does not exist!\n",
		    },
	  Efilene => {
		      'de_DE' => "Das File '%s' existiert nicht oder ist nicht lesbar!\n",
		      'en_US' => "File '%s' does not exist or is not readable!\n",
		     },
	  Efilenelink => {
			  'de_DE' => "Das File '%s' bzw. '%s' existiert nicht oder ist nicht lesbar!\n",
			  'en_US' => "File '%s' or '%s' does not exist or is not readable!\n",
			 },
	  Efilenelink3 => {
			   'de_DE' => "Das File '%s' bzw. '%s' bzw. '%s' existiert nicht oder ist nicht lesbar!\n",
			   'en_US' => "File '%s' or '%s' or '%s' does not exist or is not readable!\n",
			  },
	  Evarerr => {
		      'de_DE' => "Fehler in den Variablen im File '%s' in Zeile %s:\n%s\n",
		      'en_US' => "Error in the variables in file '%s' in line %s:\n%s\n",
		     },
	  Evardup => {
		      'de_DE' => "Variable '%s' doppelt deklariert im File '%s' in Zeile %s!\n",
		      'en_US' => "Variable '%s' was declared twice in file '%s' line %s!\n",
		     },
	  Evarinc => {
		      'de_DE' => "Das Include-File für die Variable '%s' namens '%s' im Variablenfile '%s' kann im Verzeichnis '%s' nicht gelesen/gefunden werden!\n",
		      'en_US' => "Include-file for variable '%s' named '%s' in variablesfile '%s' can not be read or found in directory '%s'!\n",
		     },
	  Edep => {
		   'de_DE' => "Fehler in den Abhängikeiten in Zeile %s:\n%s\n",
		   'en_US' => "Error in dependencies in line %s:\n%s\n",
		  },
	  Edepmulti => {
			'de_DE' => "Abhängigkeiten für '%s' mehrfach definiert in Zeile %s!\n",
			'en_US' => "Dependencies for '%s' defined multiple times in line %s!\n",
		       },
	  Edepcyc => {
		      'de_DE' => "Zyklische Abhängigkeit für Subsystem '%s' in '%s'!\n",
		      'en_US' => "Cyclic dependency for subsystem '%s' in '%s'!\n",
		     },
	  Eexerr => {
		     'de_DE' => "Fehler in den Ausschlüssen in Zeile %s:\n%s\n",
		     'en_US' => "Error in exclusions in line %s:\n%s\n",
		    },
	  Erfaeierr => {
			'de_DE' => "ReadFileAndExpandIncludes(): Mehr als %s ineinander verschachtelter INCLUDES. Wahrscheinlich fehlerhafter Zyklus, d.h. es includen sich wohl zwei Include-Files gegenseitig! Backtrace zur Fehlersuche:\n%s\n",
			'en_US' => "ReadFileAndExpandIncludes(): More than %s nested INCLUDES. Perhaps this is a wrong cycle, i.e. two include-files include themselves! Backtrace for error-analysis:\n%s\n",
		       },
	  Eimv => {
		   'de_DE' => "Kann in-memory-variable 'files_sc_inhalt' nicht zum Lesen oeffnen!\n",
		   'en_US' => "Can not open in-memory-variable 'files_sc_inhalt' for reading!\n",
		  },
	  Efscsub => {
		      'de_DE' => "'[subsystem]' statt '%s' in '%s' Zeile %s erwartet!\n",
		      'en_US' => "'[subsystem]' instead of '%s' in '%s' line %s expected!\n",
		     },
	  Efscnofile => {
			 'de_DE' => "Kann File '%s' oder '%s' aus '%s' Zeile %s nicht lesen!\n",
			 'en_US' => "Can not read file '%s' or '%s' from '%s' line %s!\n",
			},
	  Efscbpat => {
		       'de_DE' => "'beginpattern' erwartet in '%s' in Zeile %s!\n",
		       'en_US' => "'beginpattern' expected in '%s' in line %s!\n",
		      },
	  Efscnoend => {
			'de_DE' => "Kein schliessendes 'endpattern' in '%s' in Zeile %s!\n",
			'en_US' => "No closing 'endpattern' in '%s' in line %s!\n",
		       },
	  Efscerr => {
		      'de_DE' => "Fehler in Zeile %s in '%s': Quellfile-Angabe beim Typ 'c' muss eines aus diesen sein: 'installcmd', 'reconfigcmd', 'startcmd', 'stopcmd', 'testinstallcmd', 'testruncmd', 'pre_localshell', 'post_localshell'!\n",
		      'en_US' => "Error in line %s in '%s': sourcefile-specification with tye 'c' has to be one of that: 'installcmd', 'reconfigcmd', 'startcmd', 'stopcmd', 'testinstallcmd', 'testruncmd', 'pre_localshell', 'post_localshell'!\n",
		     },
	  Efsclinks => {
			'de_DE' => "Link-Quelle nicht mit absolutem Pfad angegeben in '%s' Zeile %s!\n",
			'en_US' => "Link-source not specified with absolute path in '%s' line %s!\n",
		       },
	  Efsclinkt => {
			'de_DE' => "Link-Ziel nicht mit absolutem Pfad angegeben in '%s' Zeile %s!\n",
			'en_US' => "Link-target not specified with absolute path in '%s' Zeile %s!\n",
		       },
	  Elinki => {
		     'de_DE' => "Ungültiger Inhalt des Linkfiles '%s'!\n",
		     'en_US' => "Invalid content of linkfile '%s'!\n",
		    },
	  Elinke => {
		     'de_DE' => "Link '%s' im File '%s' existiert nicht!\n",
		     'en_US' => "Link '%s' in file '%s' does not exist!\n",
		    },
	  Econfig => {
		      'de_DE' => "Kann weder './sysconfrc' noch '%s/.sysconfrc' noch '/etc/sysconfrc' lesen!\n",
		      'en_US' => "Can not read one of this: './sysconfrc', '%s/.sysconfrc', '/etc/sysconfrc'!\n",
		     },
	  Econfigerr => {
			 'de_DE' => "Kein '%s' in '%s' definiert!\n",
			 'en_US' => "No '%s' in '%s' defined!\n",
			},
	  Eclass => {
		     'de_DE' => "Fehler in Klassendefinition:\n%s\n",
		     'en_US' => "Error in class-definition:\n%s\n",
		    },
	  Ehostdef => {
		       'de_DE' => "Fehler in Rechnerdefinition:\n%s\n",
		       'en_US' => "Error in host-definition:\n%s\n",
		      },
          Esubdef => {
		      'de_DE' => "Subsystem Negierung '!' ohne Subsystem bei %s!\n",
		      'en_US' => "Subsystem negation '!' without subsystem for %s!\n",
		     },
	  Eclassne => {
		       'de_DE' => "Klasse '%s' gibt es nicht!\n",
		       'en_US' => "Class '%s' does not exist!\n",
		      },
	  Eintw => {
		    'de_DE' => "INTERNAL: %s (Interne Warnungen deuten auf einen moeglichen internen Programm-Fehler hin oder auf eine fehlerhafte Eingabe, die nicht abgefangen wurde!)\n",
		    'en_US' => "INTERNAL: %s (Internal warnings are likely to be internal program-errors or wrong input, which were not handled correctly!)\n",
		   },
	  Etime => {
		    'de_DE' => "02-TIM: Timeout-Signal im Prozess %s!\n",
		    'en_US' => "02-TIM: Timeout-signal in process %s!\n",
		   },
	  Enoexe => {
		     'de_DE' => "Leider kein '%s' verfügbar!\n",
		     'en_US' => "Sorry, but no '%s' available!\n",
		    },
	  Erbnossh => {
		       'de_DE' => "RechnerBeschreibung::GetRSH(): Kann weder mit rsh noch mit ssh auf den Rechner '%s' ueber das Interface '%s' ohne Kennwort zugreifen!\n",
		       'en_US' => "RechnerBeschreibung::GetRSH(): Can not access host '%s' by rsh or ssh via interface '%s' without password!\n",
		      },
	  Erbnoscp => {
		       'de_DE' => "RechnerBeschreibung::GetRCP(): Kann weder mit rcp, scp noch rsync auf den Rechner '%s' ueber das Interface '%s' ohne Kennwort zugreifen!\n",
		       'en_US' => "RechnerBeschreibung::GetRCP(): Can not access host '%s' by rcp, scp or rsync via interface '%s' without password!\n",
		      },
	  Eoutssh => {
		      'de_DE' => "Ausgaben der RemoteShell: '%s'\n",
		      'en_US' => "Output of RemoteShell: '%s'\n",
		     },
	  Eoutpssh => {
		       'de_DE' => "Ausgaben der %s: '%s'\n",
		       'en_US' => "Output of %s: '%s'\n",
		      },
	     Eperlre => {
		      'de_DE' => "Fehler in Perl-Expression: ",
		      'en_US' => "Error in Perl-expression: ",
		     },
	  Tdry => {
		   'de_DE' => "Es findet nur ein Trockenlauf (--dry-run) statt!\n",
		   'en_US' => "Only a dry-run (--dry-run) is done!\n",
		  },
	  TbackT => {
		     'de_DE' => "Backup ist aktiv (Endung %s).\n",
		     'en_US' => "Backup is active (extension %s).\n",
		    },
	  TbackF => {
		     'de_DE' => "Backup ist nicht aktiviert.\n",
		     'en_US' => "Backup is not activated.\n",
		    },
	  Trcfu => {
		     'de_DE' => "verwende Konfigurationsdatei: %s\n",
		     'en_US' => "using configurationfile: %s.\n",
		    },
	  Topt => {
		   'de_DE' => "Option: %s",
		   'en_US' => "Option: %s",
		  },
	  Twopara => {
		      'de_DE' => "Ohne Parameter aufgerufen.\n",
		      'en_US' => "Started without parameter.\n",
		     },
	  Tauswahl => {
		       'de_DE' => "\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: ",
		       'en_US' => "\nYou should not start $appname without parameter.
Your choices:
  1. write man-page into current directory
  2. write HTML-documentation into current directory
  3. write LaTeX-documentation into current directory
  4. create all documentations (1 to 3)
  5. display a short help
Choice: ",
		      },
	  Tll => {
		  'de_DE' => "Log-Level: %s\n",
		  'en_US' => "Log-level: %s\n",
		 },
	  Tclno => {
		    'de_DE' => "CLIENT_PORT ist definiert, aber kein RemoteClient.pm geladen!\nSysconf-Client-Funktionen werden deshalb deaktiviert!\n",
		    'en_US' => "CLIENT_PORT is defined, but no RemoteClient.pm loaded!\nSysconf-Client-functions will be disabled!\n",
		   },
	     Tclsslno => {
			  'de_DE' => "CLIENT_PORT_SSL ist definiert, aber kein RemoteClientSSL.pm geladen!\nSysconf-Client-SSL-Funktionen werden deshalb deaktiviert!\n",
			  'en_US' => "CLIENT_PORT_SSL is defined, but no RemoteClientSSL.pm loaded!\nSysconf-Client-SSL-functions will be disabled!\n",
		   },
	     Tclnosslopt => {
			     'de_DE' => "CLIENT_PORT_SSL ist definiert, aber die Option %s fehlt!\nSysconf-Client-SSL-Funktionen werden deshalb deaktiviert!\n",
			     'en_US' => "CLIENT_PORT_SSL is defined, but option %s is missing!\nSysconf-Client-SSL-functions will be disabled!\n",
		   },
	  Tclver => {
		     'de_DE' => "Client-Protokoll-Version: %s\n",
		     'en_US' => "Client-protocol-version: %s\n",
		    },
	  Tclsslver => {
			'de_DE' => "Client-SSL-Protokoll-Version: %s\n",
			'en_US' => "Client-SSL-protocol-version: %s\n",
		       },
	  Tclok => {
		    'de_DE' => "Rechner ueber Sysconf-Client erreichbar.\n",
		    'en_US' => "Host access via Sysconf-Client possible.\n",
		   },
	  Taction => {
		      'de_DE' => "Aktion: %s\n",
		      'en_US' => "Action: %s\n",
		     },
	  Tsubsys => {
		      'de_DE' => "Subsysteme gewuenscht: %s\n",
		      'en_US' => "Subsystems desired: %s\n",
		     },
	  Tsubsysok => {
			'de_DE' => "Subsysteme erlaubt: %s\n",
			'en_US' => "Subsystems allowed: %s\n",
		       },
	  Thosts => {
		     'de_DE' => "Rechner: %s\n",
		     'en_US' => "Hosts: %s\n",
		    },
	  Tos => {
		  'de_DE' => "Betriebssystem: %s\n",
		  'en_US' => "Operatingsystem: %s\n",
		 },
	  Wnodesc => {
		      'de_DE' => "01-DES: Keine Beschreibung in hosts.sc/hostsdir.sc vorhanden! (Server existiert nicht? Schreibfehler? Groß-/Kleinschreibung?)\n",
		      'en_US' => "01-DES: No description in hosts.sc/hostsdir.sc available! (Server does not exist? Typing error? Uppercase/Lowercase mismatch?)\n",
		     },
	  Wnosubsys => {
			'de_DE' => "Keine Subsysteme zu konfigurieren!\n",
			'en_US' => "No subsystems to configure!\n",
		       },
	  Wnosubsysexist => {
			     'de_DE' => "Keine Subsysteme vorhanden!\n",
			     'en_US' => "No subsystems!\n",
			    },
	  Dhostvars => {
			'de_DE' => "Rechnervariablen:\n",
			'en_US' => "Hostvariables:\n",
		       },
	  Darfsc => {
		     'de_DE' => "Nach ReadFilesSC().\n",
		     'en_US' => "After ReadFilesSC().\n",
		    },
	  Tsubsysad => {
			'de_DE' => "Subsysteme nach Abhaengigkeitsaufloesung fuer %s: %s\n",
			'en_US' => "Subsystems after resolution of dependencies for %s: %s\n",
		       },
	  Iruntime1 => {
			'de_DE' => "Laufzeit %s: %s Sekunde\n",
			'en_US' => "Runtime %s: %s second\n",
		       },
	  Iruntime2 => {
			'de_DE' => "Laufzeit %s: %s Sekunden\n",
			'en_US' => "Runtime %s: %s seconds\n",
		       },
	  Daction => {
		      'de_DE' => "Führe Aktion '%s' aus.\n",
		      'en_US' => "Starting action '%s'.\n",
		     },
	  Dactionsub => {
			 'de_DE' => "Führe Aktion '%s' mit '%s' für die Subsysteme\n%s\nin genau dieser Reihenfolge aus.\n",
			 'en_US' => "Starting action '%s' with '%s' for the subsystems\n%s\nin exactly this order.\n",
			},
	  Dsubsys => {
		      'de_DE' => "Behandle Subsystem '%s'\n",
		      'en_US' => "Processing subsystem '%s'\n",
		     },
	  Dsubni => {
		     'de_DE' => "Subsystem '%s' ist auf '%s' nicht installiert!\n",
		     'en_US' => "Subsystem '%s' is not installed on '%s'!\n",
		    },
	  Dsubisr => {
		      'de_DE' => "Subsystem '%s' läuft auf '%s' bereits!\n",
		      'en_US' => "Subsystem '%s' ist already running on '%s'!\n",
		     },
	  Dsubisnr => {
		       'de_DE' => "Subsystem '%s' läuft auf '%s' nicht!\n",
		       'en_US' => "Subsystem '%s' is not running on '%s'!\n",
		      },
	  Dtrans => {
		     'de_DE' => "Uebertrage Files... (%s)\n",
		     'en_US' => "Transferring files... (%s)\n",
		    },
	  Dremove => {
		      'de_DE' => "Entferne Backup-Files... (%s)\n",
		      'en_US' => "Removing backup-files... (%s)\n",
		     },
	  Hsysconf => {
		       'de_DE' => "Systemkonfiguration",
		       'en_US' => "Systemconfiguration",
		      },
	  Hlist => {
		    'de_DE' => "Liste aller Files, die Sysconf verwaltet.",
		    'en_US' => "Listing of all Sysconf-managed files.",
		   },
	  Hdetail => {
		      'de_DE' => "und die Details je Rechner",
		      'en_US' => "and the details per host",
		     },
	  Hfooter => {
		      'de_DE' => "Diese Seite wurde automatisch generiert auf dem Rechner %s aus der Datenbank in %s durch<br>%s %s von",
		      'en_US' => "This page was created automatically on host %s from the database in %s by<br>%s %s by",
		     },
	  Hhost => {
		    'de_DE' => "Rechner",
		    'en_US' => "Host",
		   },
	  Hsubsys => {
		      'de_DE' => "Subsysteme",
		      'en_US' => "Subsystems",
		     },
	  Hvar => {
		   'de_DE' => "Variablen",
		   'en_US' => "Variables",
		  },
	  Hos => {
		  'de_DE' => "Betriebssystem",
		  'en_US' => "Operatingsystem",
		 },
	  Hsub => {
		   'de_DE' => "Subsystem '%s' auf '%s'",
		   'en_US' => "Subsystem '%s' on '%s'",
		  },
	  Hsubconf => {
		       'de_DE' => "Subsystemkonfiguration: Dateien f&uuml;r '%s' auf Rechner '%s'",
		       'en_US' => "Subsystemconfiguration: files for '%s' on host '%s'",
		      },
	  Hhidden => {
		      'de_DE' => "versteckt",
		      'en_US' => "hidden",
		     },
	  Hbin => {
		   'de_DE' => "bin&auml;r",
		   'en_US' => "binary",
		  },
	  Hempty => {
		     'de_DE' => "leer",
		     'en_US' => "empty",
		    },
	  Hmod => {
		   'de_DE' => "wird durch *cmd modifiziert",
		   'en_US' => "gets modified by *cmd",
		  },
	  Hcomm => {
		    'de_DE' => "Kommandos",
		    'en_US' => "Commands",
		   },
	     Hfiletype => {
			   'de_DE' => "Dateiart",
			   'en_US' => "Filetype",
			  },
	     Hfilecontent => {
			      'de_DE' => "Datei/Inhalt",
			      'en_US' => "File/Content",
			     },
             Isecown => {
			 'de_DE' => "Moegliche Sicherheitsluecke: Sie sind nicht Besitzer von '%s'\n",
			 'en_US' => "Possible security-hole: You are not owner of '%s'\n",
			},
	  Drshscmd => {
		       'de_DE' => "Sysconf-Client: schlaue PERL-Variante von",
		       'en_US' => "Sysconf-Client: smart PERL variation of",
		      },
	  Ddepwish => {
		       'de_DE' => "Gewuenschtes Subsys: %s\n",
		       'en_US' => "Desired Subsys: %s\n",
		      },
	  Ddepsoft => {
		       'de_DE' => "Weiche Abhängigkeit für %s: %s\n",
		       'en_US' => "Soft dependency for %s: %s\n",
		      },
	  Ddepsok => {
		      'de_DE' => "... ist erlaubt und wird hinzugefügt.\n",
		      'en_US' => "... is allowed and is being added.\n",
		     },
	  Ddepsnok => {
		       'de_DE' => "... ist nicht erlaubt!\n",
		       'en_US' => "... is not allowed!\n",
		      },
	  Ddephardno => {
		       'de_DE' => "Keine harte Abhängigkeit für %s. Übernehme Subsystem.\n",
		       'en_US' => "No hard dependency for %s. Adding subsystem.\n",
		      },
	  Ddephardyes => {
		       'de_DE' => "Harte Abhängigkeit für %s. Übernehme Subsysteme: %s\n",
		       'en_US' => "Hard dependency for %s. Adding subsystems: %s\n",
		      },
	  Wdepsub => {
		      'de_DE' => "Subsystem '%s' ist nicht erlaubt!\nFolglich besteht eine nicht aufgeloeste Abhaengigkeit!\n",
		      'en_US' => "Subsystem '%s' is not allowed!\nThis causes an unresolved dependency!\n",
		     },
	  Ddep => {
		   'de_DE' => "Abhängigkeiten (%s):\n",
		   'en_US' => "Dependencies (%s):\n",
		  },
	  Wex => {
		  'de_DE' => "Subsysteme %s und %s schliessen sich gegenseitig aus!\nDiese beiden Subsysteme werden deshalb nicht verteilt.\n",
		  'en_US' => "Subsystems %s and %s are mutually exclusive!\nThis causes this two subsystems not to be distributed.\n",
		 },
	  Dex => {
		  'de_DE' => "Ausschlüsse:\n",
		  'en_US' => "Exclusions:\n",
		 },
	  Wcomm => {
		    'de_DE' => "Das File '%s' existiert nicht oder ist nicht lesbar!\nEs werden Remote-Kommandos ohne absolute Pfade verwendet!\n",
		    'en_US' => "File '%s' does not exist or can not be read!\nSo remote-commands without absolute paths are used!\n",
		   },
	  Drc => {
		  'de_DE' => "Remote-Kommandos:\n",
		  'en_US' => "Remote-commands:\n",
		 },
	  Dfscexp => {
		      'de_DE' => "Inhalt der Datei files.sc nach ReadFileAndExpandIncludes():\n'%s'\nEnde von files.sc\n",
		      'en_US' => "Content of files.sc after ReadFileAndExpandIncludes(): '%s'\nEnd of files.sc\n",
		     },
	  Wsknok => {
		     'de_DE' => "Subsystem '%s' aus Klasse '%s' ist nicht erlaubt!\n",
		     'en_US' => "Subsystem '%s' from class '%s' is not allowed!\n",
		    },
	  Wsubnok => {
		      'de_DE' => "Subsystem '%s' ist nicht erlaubt!\n",
		      'en_US' => "Subsystem '%s' is not allowed!\n",
		     },
	  Drbefiles => {
			'de_DE' => "Verarbeite Dateien in %s\n",
			'en_US' => "Processing files in %s\n",
		       },
	  Drbefile => {
		       'de_DE' => "Einlesen von %s\n",
		       'en_US' => "Reading %s\n",
		      },
	  Terrsum => {
		      'de_DE' => "Fehlerzusammenfassung (siehe auch %s):\n",
		      'en_US' => "Summary of errors (see also %s):\n",
		     },
	  Terrns => {
		     'de_DE' => "ACHTUNG: Diese Rechner wurden NICHT erfolgreich konfiguriert:\n",
		     'en_US' => "ATTENTION: This hosts were NOT configured successfully:\n",
		    },
	  Tendat => {
		     'de_DE' => "Ende um %s mit Exitcode %s.\n",
		     'en_US' => "end at %s with exitcode %s.\n",
		    },
	  Tstartat => {
		       'de_DE' => "Start um %s durch %s\n",
		       'en_US' => "start at %s by %s\n",
		      },
	  Tby => {
		  'de_DE' => "von",
		  'en_US' => "by",
		 },
	  Thelp => {
		    'de_DE' => "Es wird nur Hilfe ausgegeben.\n",
		    'en_US' => "Only help is displayed.\n",
		   },
	  Iaccess => {
		      'de_DE' => "Zugriffsmethode %s auf Rechner '%s' wird ermittelt...\n",
		      'en_US' => "Access-method %s to host '%s' is discovered...\n",
		     },
	  Iaccesssshok => {
			   'de_DE' => "Zugriff auf Rechner '%s' durch '%s'\n",
			   'en_US' => "Access to host '%s' by '%s'\n",
			  },
	  Dscp => {
		   'de_DE' => "Ermitteltes RCP: %s\n",
		   'en_US' => "Discovered RCP: %s\n",
		  },
	  Dsockscno => {
			'de_DE' => "Verbindungsaufbau zu Sysconf-Client nicht moeglich: %s\n",
			'en_US' => "Connection to Sysconf-Client not possible: %s\n",
		       },
	  Dsockscnoport => {
			    'de_DE' => "Verbindungsaufbau zu Sysconf-Client wird nicht versucht, da kein Client-Port angegeben ist.\n",
			    'en_US' => "Connection to Sysconf-Client will not be tried, because no client-port given.\n",
			   },
	  Dsockok => {
		      'de_DE' => "Verbindung zu Sysconf-Client funktioniert.\n",
		      'en_US' => "Connection to Sysconf-Client running.\n",
		     },
	  Dsockver => {
		       'de_DE' => "Die Sysconf-Client-Protokoll-Version von Client und Server (auf %s) stimmen nicht ueberein! Server: %s, Client: %s. Das kann zu Problemen fuehren.\n",
		       'en_US' => "Sysconf-Client-protocol-version of client and server (on %s) are different! Server: %s, Client: %s. This can cause problems.\n",
		      },
	  Isocknp => {
		      'de_DE' => "Verbindung zu Sysconf-Client auf '%s' nicht moeglich: %s\n",
		      'en_US' => "Connection to Sysconf-Client to '%s' not possible: %s\n",
		     },
	  Ddummy => {
		     'de_DE' => "(Dummy wird nicht ausgeführt.)\n",
		     'en_US' => "(Dummy not executed.)\n",
		    },
	  Dtextrep => {
		       'de_DE' => "Textersetzung in cmd-File: '%s'\n",
		       'en_US' => "Text-replacement in cmd-file: '%s'\n",
		      },
	  Icomm => {
		    'de_DE' => "Kommando '%s' liefert '%s'\n",
		    'en_US' => "Command '%s' returns '%s'\n",
		   },
	  Dcomm => {
		    'de_DE' => "Kommando",
		    'en_US' => "Command",
		   },
	  Dlcomm => {
		     'de_DE' => "Lokales Kommando",
		     'en_US' => "Local command",
		    },
	  Dnocomm => {
		      'de_DE' => "(Optionales '%s' ist nicht vorhanden/lesbar.)\n",
		      'en_US' => "(Optional '%s' does not exist or is not readable.)\n",
		     },
	  Wtextign => {
		       'de_DE' => "Ignoriere ungültigen Text:\n'%s'\n",
		       'en_US' => "Ignoring invalid text:\n'%s'\n",
		      },
	  Dtextyes => {
		       'de_DE' => "Es wurden %s Textveränderungen durchgeführt\n",
		       'en_US' => "%s text-replacements done\n",
		      },
	     Wmsgtlp => {
			 'de_DE' => "msg() mit zuwenig Parametern aufgerufen! Message-Code war '%s'.\n",
			 'en_US' => "msg() call with too less parameters! Message-Code was '%s'.\n",
			},
	     Dremsub => {
			 'de_DE' => "Entferne Subsystem '%s'.\n",
			 'en_US' => "Removing subsystem '%s'.\n",
			},
	    );
  my $out;
  if (! defined $map{$msgid})
  {
    $out = "ERROR: No message text for ID '$msgid' available!\n";
    error '(internal)', "No message text for ID '$msgid' available!\n";
  }
  else
  {
    if ( (! defined $map{$msgid}{$language}) ||
	 ($map{$msgid}{$language} eq '') )
    {
      $out = $map{$msgid}{de_DE};
    }
    else
    {
      $out = $map{$msgid}{$language};
    }
  }
  foreach (@variables)
  {
    $out =~ s/\%s/$_/;
  }
  # Wenn jetzt noch ein "%s" im Text ist, dann wurden zuwenig Parameter
  # angegeben!
  if ($out =~ /\%s/)
  {
    warning(msg('Wmsgtlp', $msgid));
  }
  return $out;
}


sub catch_warning
{
  # Abfangen von Laufzeit-Warnungen
  my $warnung = shift;
  error '(internal)', msg('Eintw', $warnung);
}


sub catch_timeout
{
  ResetTimeout(0);
  print msg('Etime', $$);
  logprint msg('Etime', $$);
  myexit($ErrorExitCode);
}


sub RandomString
{
  my $length = shift || 10;
  # 64 mögliche Zeichen
  # Bei einer String-Länge von 10 Zeichen ergeben sich damit
  # 64^10 = 1.15292150460685e+18 Möglichkeiten
  my @chars = ('A'..'Z', 'a'..'z', '0'..'9', '+', '_', '.');
  my $string;
  $string .= $chars[rand @chars] for 1..$length;
  return $string;
}


sub IsWindowsPath
{
  # Liefert TRUE, wenn der übergebene Pfad ein Windows-Laufwerk ist, also
  # z.B. d:\temp\test.txt
  my $input = shift;
  if ($input =~ /^[a-zA-Z]\:\\/)
  {
    return $TRUE;
  }
  else
  {
    return $FALSE;
  }
}


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   -   ".msg('Tby')." Stephan Löscher";
  return "\n$head\n" . '~' x length($head) . "\n";
}


sub Hilfe
{
  printumlautepaged
  Kopf().
"With Sysconf, one or several computers can be configured using a
database. Everything that can be saved as a file by Unix can be
included in the database.

Sysconf transfers the configuration from a central system to the target
systems. To do this so called \"actions\" must be carried out.

An action is formulated in natural language: \"Carry out action X with
subsystem Y on computer Z.\" Actions are handover using the command
line, whereby:

action    := init | update | remove | start | stop | documentation |
             listhosts | listinterfaces | lisreposfiles |
             listrevision | listfiles | none
subsystem := <subsystem-name>{,<subsystem-name>}* | ALL
machine   := <machine-name>{,<machine-name>}*     | ALL
'ALL' stands for 'all Subsystems' or 'all computers'.
Please note the uppercase letters.

init:           Install Subsystem for the first time (all files
                including 'initfiles')
update:         Update Subsystem (files from 'initfiles' are not
                copied!) If the Subsystem does not exist, then carry
                out 'init' first
remove:         Remove Subsystem
start:          Start Subsystem
stop:           Stop Subsystem
documentation:  Create documentation
listhosts :     Create a list of all computers (from the hosts.sc).
                Here the SUBSYSTEM-parameter is ignored and the MACHINE
                must be ALL, also: sysconf listhosts xxx ALL
listinterfaces: Create as list of all of the interfaces of the computer
                (from hosts.sc).
                Here the SUBSYSTEM-parameter is ignored and the MACHINE
                must be ALL, also: sysconf listinterfaces xxx ALL
listreposfiles: List all files in the repository for the stated
                operatingsystem. Here the MACHINE-parameter is ignored.
                Example: sysconf listreposfiles AIX-5.1 xxx
listrevision:   Create a list of all computers (from hosts.sc with field
                REVISION) with the revisionnumber.
                Here the SUBSYSTEM-Parameter is ignored and MACHINE
                must be ALL, also: sysconf listrevision xxx ALL
listfiles:      Create a list of all files after resolving subsystem
                dependencies. SUBSYSTEM- and MACHINE-parameter can be used as
                desired.
none:           No Action.

Options:
-wX: with X=1-31 states the LOG-level. X is calculated as the sum of the
     following possible values (Bitmask):
      1:  all errors (Precise error description)
      2:  all warnings (one should take note of)
      4:  all information (very informative)
      8:  all debug-information (detailed status, etc.)
      16: all remote-shell-calls and Sysconf-Client-calls
      (Default is 7, i.e. 1+2+4)
-b : Creates backup files on the target computer when changes have
     occurred. The backup file names end with a time stamp:
      '.YYYYMMDDHHMMSS.SC' z.B. '.20011019121531.SC'
--logfile=FILENAME : Logging takes place in the file LOGFILE.
                     A pipe can also be used e.g.:
                    --logfile=\"|/bin/cronolog /var/log/sc/\%Y/\%m/\%d\"
--sysconf_root=DIRNAME : Location of the directory containing the
                         Sysconf-repository.
--dry-run : \"Dry run\", i.e. everything will be carried out but not the
            client access, i.e. neither ssh-, rsh- or
            Sysconf-Client-Access.
--list_hosts_per_os=OS : Lists all comuputers with the stated OS, e.g.:
                         --list_hosts_per_os=Gentoo2007 none none none
--list_hosts_subsys : Lists all computers with the allocated Subsystems.
                      e.g.: --list_hosts_subsys none ALL ALL
--list_subsys_hosts : Lists all subsystems and computers that receive
                      them, e.g.: --list_subsys_hosts none ALL ALL
--list_variable=VAR : Lists the variables for all the specified
                      computers, e.g.:
                       --list_variable=SYSCONF_INTERFACE none none ALL

Examples:
'Carry out an update from sendmail on the computer zeus:
sysconf update sendmail zeus
'Carry out an update of all Subsystems on the computer osiris':
sysconf update ALL osiris
'Distribute all Subsystems to all computers':
sysconf update ALL ALL
'Initialise all Subsystems on computer neptun':
sysconf init ALL neptun
'Carry out an update of sendmail and syslog on the computers zeus,
osiris and neptun':
sysconf update sendmail,syslog zeus,osiris,neptun
'Test the consistency of the repository with out changing client
computers:
sysconf --dry-run update ALL ALL

";
  logprint msg('Thelp');
  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 msg('Enoexe', 'pod2man');
    which('nroff') || die msg('Enoexe', 'nroff');
    system("pod2man $0 | nroff -man > \L$appname\E.man");
  }
  if ($art eq 'html')
  {
    which('pod2html') || die msg('Enoexe', 'pod2html');
    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 msg('Enoexe', 'pod2latex');
    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 msg('Efileclose', "\L$appname\E.tex");
  }
}


######################################################################
### 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(main::msg('Eintpara', 'RechnerBeschreibung::Betriebssystem()'));
    main::myexit($main::FatalExitCode);
  }
  return $objekt->{$rechner}->{BS};
}

sub SetBetriebssystem
{
  my $objekt  = shift;
  my $rechner = shift;
  unless (defined $rechner)
  {
    carp(main::msg('Eintpara', 'RechnerBeschreibung::SetBetriebssystem()'));
    main::myexit($main::FatalExitCode);
  }
  $objekt->{$rechner}->{BS} = shift;
}

sub Interface
{
  my $objekt  = shift;
  my $rechner = shift;
  unless (defined $rechner)
  {
    carp(main::msg('Eintpara', 'RechnerBeschreibung::Interface()'));
    main::myexit($main::FatalExitCode);
  }
  return $objekt->{$rechner}->{INTERFACE};
}

sub Revision
{
  my $objekt  = shift;
  my $rechner = shift;
  unless (defined $rechner)
  {
    carp(main::msg('Eintpara', 'RechnerBeschreibung::Revision()'));
    main::myexit($main::FatalExitCode);
  }
  return $objekt->{$rechner}->{REVISION};
}

sub SetInterface
{
  my $objekt  = shift;
  my $rechner = shift;
  unless (defined $rechner)
  {
    carp(main::msg('Eintpara', 'RechnerBeschreibung::SetInterface()'));
    main::myexit($main::FatalExitCode);
  }
  $objekt->{$rechner}->{INTERFACE} = shift;
}

sub SetRevision
{
  my $objekt  = shift;
  my $rechner = shift;
  unless (defined $rechner)
  {
    carp(main::msg('Eintpara', 'RechnerBeschreibung::SetRevision()'));
    main::myexit($main::FatalExitCode);
  }
  $objekt->{$rechner}->{REVISION} = shift;
}

sub Subsysteme
{
  my $objekt  = shift;
  my $rechner = shift;
  # Ohne Parameter aufgerufen?
  unless (defined $rechner)
  {
    carp(main::msg('Eintpara', 'RechnerBeschreibung::Subsysteme()'));
    main::myexit($main::FatalExitCode);
  }
  return @{$objekt->{$rechner}->{SUB}};
}

sub SetSubsysteme
{
  my $objekt  = shift;
  my $rechner = shift;
  unless (defined $rechner)
  {
    carp(main::msg('Eintpara', 'RechnerBeschreibung::SetSubsysteme()'));
    main::myexit($main::FatalExitCode);
  }
  $objekt->{$rechner}->{SUB} = [ @_ ];
}

sub HostsScSubsysteme
{
  my $objekt  = shift;
  my $rechner = shift;
  # Ohne Parameter aufgerufen?
  unless (defined $rechner)
  {
    carp(main::msg('Eintpara', 'RechnerBeschreibung::HostsScSubsysteme()'));
    main::myexit($main::FatalExitCode);
  }
  return @{$objekt->{$rechner}->{HOSTS_SC_SUB}};
}

sub SetHostsScSubsysteme
{
  my $objekt  = shift;
  my $rechner = shift;
  unless (defined $rechner)
  {
    carp(main::msg('Eintpara', 'RechnerBeschreibung::SetHostsScSubsysteme()'));
    main::myexit($main::FatalExitCode);
  }
  $objekt->{$rechner}->{HOSTS_SC_SUB} = [ @_ ];
}

sub Klassen
{
  my $objekt  = shift;
  my $rechner = shift;
  # Ohne Parameter aufgerufen?
  unless (defined $rechner)
  {
    carp(main::msg('Eintpara', 'RechnerBeschreibung::Klassen()'));
    main::myexit($main::FatalExitCode);
  }
  return @{$objekt->{$rechner}->{KLASSEN}};
}

sub SetKlassen
{
  my $objekt  = shift;
  my $rechner = shift;
  unless (defined $rechner)
  {
    carp(main::msg('Eintpara', 'RechnerBeschreibung::SetKlassen()'));
    main::myexit($main::FatalExitCode);
  }
  $objekt->{$rechner}->{KLASSEN} = [ @_ ];
}

sub GetRSH
{
  my $objekt    = shift;
  my $rechner   = shift;
  my $interface = '';
  my $temp;
  unless (defined $rechner)
  {
    carp(main::msg('Eintpara', 'RechnerBeschreibung::GetRSH()'));
    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(main::msg('Iaccess', 'SHELL', $rechner));
    $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(main::msg('Erbnossh', $rechner, $interface));
      }
      if ($ssh_key ne '')
      {
	#
	# Umwandlung von ggf. einer Liste von Keys, also:
	# /home/a/.ssh/key1
	# wird zu:
	# '-i /home/a/.ssh/key1'
	# und
	# /home/a/.ssh/key1 /home/b/.ssh/key2 /home/c/.ssh/key3
	# wird zu: '-i /home/a/.ssh/key1'
	# '-i /home/a/.ssh/key1 -i /home/b/.ssh/key2 -i /home/c/.ssh/key3'
	#
	$ssh_key = '-i ' . join(' -i ', split(/\s+/, $ssh_key)) . ' ';
      }
      if ($temp =~ /^ssh$/)
      {
	$temp .= " ${ssh_key}-x -o 'BatchMode yes'";
      }
    }
    main::debug "Ermittelte RSH: ".$temp."\n";
    $objekt->{$rechner}->{RSH} = $temp;
    main::info(main::msg('Iaccesssshok', $rechner, $temp));
    return $temp;
  }
}

sub GetRCP
{
  my $objekt    = shift;
  my $rechner   = shift;
  my $interface = '';
  my $temp;
  unless (defined $rechner)
  {
    carp(main::msg('Eintpara', 'RechnerBeschreibung::GetRCP()'));
    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(main::msg('Iaccess', 'COPY', $rechner));
    $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(main::msg('Erbnoscp', $rechner, $interface));
	main::myexit($main::FatalExitCode);
      }
    }
    main::debug(main::msg('Dscp', $temp));
    $objekt->{$rechner}->{RCP} = $temp;
    main::info(main::msg('Iaccesssshok', $rechner, $temp));
    return $temp;
  }
}


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

  unless (defined $rechner)
  {
    carp(main::msg('Eintpara', 'RechnerBeschreibung::Socket()'));
    main::myexit($main::FatalExitCode);
  }
  # Bei Trockenlauf 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(main::msg('Iaccess', 'CLIENT', $rechner));
    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')
    {
      # Wenn Sysconf-Client-SSL-Port definiert ist
      if ($main::client_port_ssl)
      {
	($err, $remote) = RemoteClientSSL::new($interface,
					       $main::client_port_ssl,
					       $main::client_ssl_cert_file,
					       $main::client_ssl_key_file
					      );
	if ($err != $main::RC_OK)
	{
	  main::debug_rsh(main::msg('Dsockscno', $remote));
	}
	else
	{
	  $client_name = 'Sysconf-Client-SSL';
	}
      }
      else
      {
	# Kein SSL Port, also Fehler setzen für nachfolgende Tests
	$err = 255;
      }

      # Wenn Sysconf-Client-SSL nicht möglich war und Client-Port definiert ist
      if ( ($err != $main::RC_OK) && $main::client_port )
      {
	($err, $remote) = RemoteClient::new($interface, $main::client_port);
	if ($err != $main::RC_OK)
	{
	  main::debug_rsh(main::msg('Dsockscno', $remote));
	  return undef;
	}
	$client_name = 'Sysconf-Client';
      }

      # Wenn gar kein Client-Port definiert ist
      if ( ! $main::client_port_ssl && ! $main::client_port )
      {
	main::debug_rsh(main::msg('Dsockscnoport'));
	return undef;
      }

      ($err, $temp) = $remote->Version();
      if ($err == $main::RC_OK)
      {
	main::debug_rsh(main::msg('Dsockok'));
	if ($main::client_port)     { $my_version = $RemoteClient::VERSION    }
	if ($main::client_port_ssl) { $my_version = $RemoteClientSSL::VERSION }
	if ($my_version != $temp)
	{
	  main::info(main::msg('Dsockver', $rechner, $temp, $my_version));
	}
      }
      else
      {
	# An dieser Stelle interessiert die Meldung nicht sonderlich, also
	# aus allen Fehler-Levels nur ein DEBUG machen:
	main::info(main::msg('Isocknp', $rechner, $temp));
	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(main::msg('Iaccesssshok', $rechner, $client_name));
      }
    }
    return $remote;
  }
}


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

  unless (defined $rechner)
  {
    carp(main::msg('Eintpara', 'RechnerBeschreibung::Socket_Destroy()'));
    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(main::msg('Eintpara', 'Dependencies::Get()'));
    main::myexit($main::FatalExitCode);
  }
  # Das return ist nur so umständlich, weil Perl sonst den undef-Wert
  # kritisiert. Kurz: return %{$objekt->{$betriebssystem}};
  if (keys %{$objekt->{$betriebssystem}})
  {
    return %{$objekt->{$betriebssystem}}
  }
  return undef;
}

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

  unless (defined $betriebssystem)
  {
    carp(main::msg('Eintpara', 'Dependencies::Set()'));
    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(main::msg('Eintpara', 'Exclusions::Get()'));
    main::myexit($main::FatalExitCode);
  }
  # Das return ist nur so umständlich, weil Perl sonst den undef-Wert
  # kritisiert. Kurz: return %{$objekt->{$betriebssystem}};
  if (keys %{$objekt->{$betriebssystem}})
  {
    return %{$objekt->{$betriebssystem}};
  }
  return undef;
}

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

  unless (defined $betriebssystem)
  {
    carp(main::msg('Eintpara', 'Exclusions::Set()'));
    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",
#                                  CHCON       => "/usr/bin/chcon",
#                                  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(main::msg('Eintpara', 'Commands::Get()'));
    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(main::msg('Eintpara', 'Commands::Set()'));
    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 => ...
#	       seccon     => ...
#	       kommando   => ...
#	       hidden     => ...
	      };
  bless $daten, 'SingleFile';
  return $daten;
}


sub Get
{
  # Parameter: key

  my ($objekt, $key) = @_;
  unless (defined $key)
  {
    carp(main::msg('Eintpara', 'SingleFile::Get()'));
    main::myexit($main::FatalExitCode);
  }
  return $objekt->{$key};
}

sub Set
{
  # Parameter: (key, value)

  my ($objekt, $key, $value) = @_;
  unless (defined $value)
  {
    carp(main::msg('Eintpara', 'SingleFile::Set()'));
    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(main::msg('Eintpara', 'Files::BereitsEingelesen()'));
    main::myexit($main::FatalExitCode);
  }
  if (keys %{$objekt->{$betriebssystem}})
  {
    return $main::TRUE
  }
  return $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(main::msg('Eintpara', 'Files::Get()'));
    main::myexit($main::FatalExitCode);
  }
  unless ( IstArtGueltig($objekt,$art) )
  {
    carp("Files::Get() called with wrong 'Art'-parameter!\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 => ...
  # seccon     => ...
  # 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() called without 'quelle' or 'kommando'!\n");
    main::myexit($main::FatalExitCode);
  }
  if ( (defined $param{quelle}) && (! defined $param{ziel}) )
  {
    carp("Files::Set() called with 'quelle' but without 'ziel'!\n");
    main::myexit($main::FatalExitCode);
  }
  unless ( IstArtGueltig($objekt,$art) )
  {
    carp("Files::Set() called with wrong 'Art'-parameter!\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(main::msg('Eintpara', 'Files::GetFileList()'));
    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$|^installtemplate$|^installlink$|^installshell$|
		    ^init$|^inittemplate$|^initlink$|^initshell$|
		    ^file$|^template$|^link$|^shell$|
		    ^modify$/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(main::msg('Eintpara', 'TemplatePattern::Get()'));
    main::myexit($main::FatalExitCode);
  }
  unless (defined $objekt->{$betriebssystem}->{$subsystem}->{$quelle}->{$ziel})
  {
    carp("TemplatePattern::Get() returns undef()! ".
         "Call was: '$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(main::msg('Eintpara', 'TemplatePattern::Get()'));
    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(main::msg('Eintpara', 'TemplatePattern::Set()'));
    main::myexit($main::FatalExitCode);
  }
  if ( defined $objekt->{$betriebssystem}->{$subsystem}->{$quelle}->{$ziel} )
  {
    carp("TemplatePattern::Set(): Duplicate entry! ".
         "Call was: '$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 debug   { main::debug  (@_); }
sub error   { main::error  (@_); }
sub warning { main::warning(@_); }
sub myexit  { main::myexit (@_); }
sub logdie  { main::logdie (@_); }
sub msg     { main::msg    (@_); }

sub new
{
  my ($object,$bs,$rechner,$subsystem) = @_;
  unless (defined $subsystem)
  {
    carp(msg('Eintpara', "SubsystemObject::new(Betriebssystem, Rechner, Subsystem)"));
    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::tmp_remote$main::slash$command.$subsystem.$main::tmp_rand_pid";
  my $tmp;

  unless (-r $kommandofile)
  {
    $tmp = main::LinksAufloesen($kommandofile);
    if (! defined $tmp)
    {
      logdie(msg('Efilenelink', $kommandofile, "$kommandofile.LINK"));
    }
    $kommandofile = $tmp;
  }

  # Dummy-Kommandos: Wenn das Kommando die Filelänge Null hat, dann nicht
  # kopieren und ausführen!
  unless (-s $kommandofile)
  {
    debug(msg('Ddummy'));
    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(msg('Dtextrep', $kommandofile));
    my $tempfile = "$main::tmp_local/sysconf.template.$main::tmp_rand_pid";
    TextModify::ErsetzeMuster($kommandofile, $tempfile, $pattern, \%main::RechnerVars, $subsystem);
    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
  my $ret = main::RemoteShell($rechner, $remote_tmp_file);
  main::RemoteRm($rechner, $remote_tmp_file);

  unless ( ($ret =~ /TRUE/) || ($ret =~ /FALSE/) )
  {
    main::info(msg('Icomm', $kommandofile, $ret));
  }

  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(msg('Dcomm').": $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(msg('Dlcomm').": $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(msg('Dnocomm', $kommandofile));
      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(msg('Ddummy'));
    return $main::TRUE;
  }

  my $tempfile = "$main::tmp_local/sysconf.template.$main::tmp_rand_pid";

  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(msg('Dtextrep', $kommandofile));
    TextModify::ErsetzeMuster($kommandofile, $tempfile, $pattern, \%main::RechnerVars, $subsystem);
  }
  else
  {
    copy($kommandofile, $tempfile);
  }

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

  unless ( ($ret =~ /TRUE/) || ($ret =~ /FALSE/) )
  {
    main::info(msg('Icomm', $kommandofile, $ret));
  }

  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(msg('Dtextrep', $cmdfile));
	    my $tempfile = "$main::tmp_local/sysconf.template.$main::tmp_rand_pid";
	    TextModify::ErsetzeMuster($cmdfile, $tempfile, $pattern, \%main::RechnerVars, $objekt->{SUBSYSTEM});

	    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 (".msg('Hbin').")<br>\n";
        }
      }
      # Wenn das cmd-File leer ist, dann keinen Link erzeugen
      else
      {
        $ret .= "    $command (".msg('Hempty').")<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},msg('Eoutssh', $ret));
  }
  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},msg('Eoutssh', $ret));
  }
  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},msg('Eoutssh', $ret));
  }
  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},msg('Eoutssh', $ret));
  }
  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},msg('Eoutssh', $ret));
  }
  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},msg('Eoutpssh', 'pre_localshell', $ret));
  }
  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},msg('Eoutpssh', 'post_localshell', $ret));
  }
  return $ok;
}


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

package TextModify;

use Carp;

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


# 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,
  #            Name-des-Subsystems
  # Return:    True bei Erfolg, False bei Fehler

  TextModifyInit(); # Modul-Globale Variablen initialisieren

  my ($quellfile, $zielfile, $pattern, $refvar, $subsys) = @_;
  croak "Sourcefile '$quellfile' not readable!\n" unless -r $quellfile;

  my $variablenCode = '';
  foreach (keys %$refvar)
  {
    my $refvarquote = $$refvar{$_}; $refvarquote =~ s/\'/\\\'/g;
    $variablenCode .= "my \$$_='$refvarquote';\n";
  }
  $variablenCode .= "my \$SYSCONF_CURRENT_SUBSYSTEM='$subsys';\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, msg('Wtextign', $zeile);
  }
  debug msg('Dtextyes', $Veraenderungen);

  # Resultat schreiben
  my $zielFH = FileHandle->new();
  open($zielFH, ">$zielfile") || 
                          croak msg('Efilenowr', $zielfile);
  print $zielFH @quelle;
  close($zielFH) || croak msg('Efileclose', $zielfile);

  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, msg('Eperlre'), $@;
      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, msg('Etextvar', $fehler, $FehlerInVariablenErsetzungExpression);
  }
  else
  {
    error undef,$fehler . msg('Etextvarunk');
  }
}


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

__END__

=encoding utf8

=head1 NAME

sysconf - System Configuration

=head1 SYNOPSIS

sysconf [options] action subsystem machine

=head1 DESCRIPTION

=head2 Overview

With Sysconf, one or several computers can be configured using a database. Everything that can be saved as a file by Unix can be included in the database.

Sysconf transfers the configuration from a central system to the target systems. To do this so called "actions" must be carried out.

An action is formulated in natural language:
"Carry out action X with Subsystem Y on computer Z."
Actions are handover using the command line, whereby:

 action    := init | update | remove | start | stop | documentation |
              listhosts | listinterfaces | lisreposfiles |
              listrevision | listfiles | none
 subsystem := <subsystem-name>{,<subsystem-name>}* | ALL
 machine   := <machine-name>{,<machine-name>}*     | ALL
 'ALL' stands for 'all Subsystems' or 'all computers'.
 Please note the uppercase letters.

 init:           Install Subsystem for the first time (all files
                 including 'initfiles')
 update:         Update Subsystem (files from 'initfiles' are not
                 copied!) If the Subsystem does not exist, then carry
                 out 'init' first
 remove:         Remove Subsystem
 start:          Start Subsystem
 stop:           Stop Subsystem
 documentation:  Create documentation
 listhosts :     Create a list of all computers (from the hosts.sc).
                 Here the SUBSYSTEM-parameter is ignored and the MACHINE
                 must be ALL, also: sysconf listhosts xxx ALL
 listinterfaces: Create as list of all of the interfaces of the computer
                 (from hosts.sc).
                 Here the SUBSYSTEM-parameter is ignored and the MACHINE
                 must be ALL, also: sysconf listinterfaces xxx ALL
 listreposfiles: List all files in the repository for the stated
                 operatingsystem. Here the MACHINE-parameter is ignored.
                 Example: sysconf listreposfiles AIX-5.1 xxx
 listrevision:   Create a list of all computers (from hosts.sc with field
                 REVISION) with the revisionnumber.
                 Here the SUBSYSTEM-Parameter is ignored and MACHINE
                 must be ALL, also: sysconf listrevision xxx ALL
 listfiles:      Create a list of all files after resolving subsystem
                 dependencies. SUBSYSTEM- and MACHINE-parameter can be used as
                 desired.
 none:           No Action.

 Options:
 -wX: with X=1-31 states the LOG-level. X is calculated as the sum of the
      following possible values (Bitmask):
       1:  all errors (Precise error description)
       2:  all warnings (one should take note of)
       4:  all information (very informative)
       8:  all debug-information (detailed status, etc.)
       16: all remote-shell-calls and Sysconf-Client-calls
       (Default is 7, i.e. 1+2+4)
 -b : Creates backup files on the target computer when changes have
      occurred. The backup file names end with a time stamp:
       '.YYYYMMDDHHMMSS.SC' z.B. '.20011019121531.SC'
 --logfile=FILENAME : Logging takes place in the file LOGFILE.
                      A pipe can also be used e.g.:
                     --logfile="|/bin/cronolog /var/log/sysconf/%Y/%m/%d"
 --sysconf_root=DIRNAME : Location of the directory containing the
                          Sysconf-repository.
 --dry-run : "Dry run", i.e. everything will be carried out but not the
             client access, i.e. neither ssh-, rsh- or
             Sysconf-Client-Access.
 --list_hosts_per_os=OS : Lists all comuputers with the stated OS, e.g.:
                          --list_hosts_per_os=Gentoo2007 none none none
 --list_hosts_subsys : Lists all computers with the allocated Subsystems.
                       e.g.: --list_hosts_subsys none ALL ALL
 --list_subsys_hosts : Lists all subsystems and computers that receive
                       them, e.g.: --list_subsys_hosts none ALL ALL
 --list_variable=VAR : Lists the variables for all the specified
                       computers, e.g.:
                        --list_variable=SYSCONF_INTERFACE none none ALL

 Examples:
 'Carry out an update from sendmail on the computer zeus:
 sysconf update sendmail zeus
 'Carry out an update of all Subsystems on the computer osiris':
 sysconf update ALL osiris
 'Distribute all Subsystems to all computers':
 sysconf update ALL ALL
 'Initialise all Subsystems on computer neptun':
 sysconf init ALL neptun
 'Carry out an update of sendmail and syslog on the computers zeus,
 osiris and neptun':
 sysconf update sendmail,syslog zeus,osiris,neptun
 'Test the consistency of the repository with out changing client
 computers:
 sysconf --dry-run update ALL ALL

=head2 Main configuration file

Either the file "/etc/sysconfrc" or "./sysconfrc" or "~/.sysconfrc" are used to store the standard settings. Using SYSCONF_ROOT, the directory is specified in which the L</Configuration database> is located. SYSCONF_ROOT can also be specified in the command line by "--sysconf_root=DIRNAME".
The command HTMLDIR is used to specify the directory in which the HTML-Documentation is saved using the action 'documentation'.

The following settings are optional:

With STYLESHEET and with STYLESHEET_CLASS a stylesheet and style sheet class can be specified for the HTML documentation.

STOP_ON_ERROR=TRUE or STOP_ON_ERROR=FALSE can be used to set if Sysconf should stop in case of an error or not.

Using LOGLEVEL, the LOG-Level can be set analogue to the command line parameter w, the standard is 7, this means that all errors, warning and information will be listed. However the input specified via the command line has priority.
The LOG-Level is displayed as a bitmask with the possible bits:

 1:  all errors (Precise error description)
 2:  all warnings (should be heeded)
 4:  all Information (very informative)
 8:  all debug information (full status etc.)
 16: all remote shell calls and Sysconf client calls
 (Default is 7, i.e. 1+2+4)

So for example, 16 generates only rsh messages, 2 warnings only and 18
(=2+16) rsh- messages and warnings. 9 (=1+8) would only display errors and debug messages and 31 (=1+2+4+8+16) displays everything.

The location of the logfile can be determined using LOGFILE.
(Standard: /tmp/sysconf.log)
This setting can also be entered in the command line using the option "--logfile=FILENAME", whereby the usage of the command line has a higher priority.
A pipe can also be used e.g.
--logfile="|/bin/cronolog /var/log/sysconf/%Y/%m/%d"

The option CLIENT_PORT specifies the number of the port that Sysconf should use to try to contact the optional sysconf client programme.

The option CLIENT_PORT_SSL PORT specifies the number of the port that Sysconf should use to try to contact the optional sysconf-client-ssl programme.
To use this the following SSL options must also be entered:
 SSL_CERT_FILE
 SSL_KEY_FILE

The option LANGUAGE can be used to swap between the following languages:

 de_DE
 en_US

Examples for a main configuration file:

 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
 LANGUAGE=de_DE

Which commands should be used to control a computer are determined automatically.
The following are available for use: sysconf-client-ssl, sysconf-client, rsh/rcp, ssh/scp, rsync/rsh and rsync/ssh.

Sysconf-Client is the optimum method. The programme sysconf-client only needs to be started on the computers that are administrated by Sysconf.
When using sysconf-client, the computer that is running Sysconf, must be specified as an authorised computer and of course have the same port number as specifed when using CLIENT_PORT.

There is also an SSL variant of Sysconf-Client. Please use these options:
 CLIENT_PORT_SSL
 SSL_CERT_FILE
 SSL_KEY_FILE

Each time Sysconf starts, it automatically tries all possibilities to contact a computer in this order:
sysconf-client-ssl, sysconf-client, rsync/ssh, rsync/rsh, ssh/scp and rsh/rcp.
Using the switch -w7 you can observe which method is being used.
rsh and ssh only function when there is no password entry needed to access the other computer. The following command should work as long as no user credentials are needed:
"ssh targethost date".

If the computer that is being administrated by sysconf is called "localhost" then no Remote-Copy/Shell is used, instead the actions take place directly on the computer that is running sysconf.

=for html
Links to mentioned 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<Configuration database>

The configuration database contains the information about which computers receive which files and is organized like a filesystem with directory structure:

 hosts.sc
 classes.sc
 variables/
      zeus.var
      osiris.var
      neptun.var
      ...
 AIX-4.1.5/
     dependencies.sc            (Dependencies of the Subsystems)
     dependencies-soft.sc       ("Soft" dependencies of the Subsystems)
     exclusions.sc              (Mutual exclusions of the Subsystems)
     files.sc                   (List of the files per Subsystem)
     commands.sc                (Optional, remote commands with full
                                 path details)
     filedir.sc/                (All configuration files are stored here)
        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            (Dependencies of the Subsystems)
     dependencies-soft.sc       ("Soft" dependencies of the Subsystems)
     exclusions.sc              (Mutual exclusions of the Subsystems)
     files.sc                   (List of the files per Subsystem)
     commands.sc                (Optional, remote commands with full
                                 path details)
     filedir.sc/                (All configuration files are stored here)
        etc/rc.config
        etc/hosts
        ...
     syslog/
           installcmd
           removecmd
           ...
 Linux-SuSE-6.4/
     dependencies.sc            (Dependencies of the Subsystems)
     dependencies-soft.sc       ("Soft" dependencies of the Subsystems)
     exclusions.sc              (Mutual exclusions of the Subsystems)
     files.sc                   (List of the files per Subsystem)
     commands.sc                (Optional, remote commands with full
                                 path details)
     filedir.sc/                (All configuration files are stored here)
        etc/rc.config
        etc/hosts
        ...
     syslog/
           installcmd
           removecmd
           ...

Symbolic links can of course also be created in this directory tree. This serves to avoid duplicating data storage. This can mean that if the file such as /etc/sudoers under Linux, AIX and Solaris should look identical, this would be then as follows e.g.:

 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
 lrwxrwxrwx 1 root root 46 Oct 20  2003 Solaris-10/filedir.sc/etc/sudoers -> ../../../OS_independent/filedir.sc/etc/sudoers

As can be seen, such a link can also have multiple levels or tiers.

Optionally Sysconf also supports links via special files.
If you normally have a link from a SOURCE to a TARGET, instead of creating SOURCE you create file called SOURCE.LINK, that has as its content the text for the TARGET.
The Sysconf logic as follows, if the file with the name XYZ does not exist, then it searches for a file called XYZ.LINK and then will interpret its content.
The content of XYZ.LINK will be interpreted as a new file name. This can of course be relative path names. This also works for directories. Links with multiple levels or tiers are possible. By not using symbolic links, CVS and data storage in a filesystem that does not support symbolic links can be used.

Example:

 AIX_common/filedir.sc/etc/sudoers.LINK
 With the content:
 ../../../OS_independent/filedir.sc/etc/sudoers
 AIX-5.1/filedir.sc/etc/sudoers with the content:
 ../../../AIX_common/filedir.sc/etc/sudoers
 Linux-2.6/filedir.sc/etc/sudoers with the content:
 ../../../OS_independent/filedir.sc/etc/sudoers
 and OS_independent/filedir.sc/etc/ is where the actual file is stored.

=head2 Variable files F<variables/xxx.var>

The variable files named HOSTNAME.var for each Sysconf host contains variables  which are only used for exactly the computer HOSTNAME.
Escapes ("\n") in the variables will not be interpreted! The entries can be build by:

variable=value

or

variable include FILENAME1 FILENAME2 ...

In the first case the variable is allocated a value, if no value is defined then the variable is set with an empty string.
In the second case, the specified files are interpreted and the contents and are saved sequentially in the variable. The search for these files takes place in the variables directory.

The following internal variabls are defined as standard. The options SYSCONF_BACKUP and SYSCONF_BACKUP_EXTENSION are influenced by the Backup-Option. These can be for example used in *cmd files.

 SYSCONF_HOST                same as the HOST entry value
 SYSCONF_ACTION              contains the ACTION, e.g. "update" or "remove"
 SYSCONF_BACKUP              with the values TRUE and FALSE
 SYSCONF_BACKUP_EXTENSION    contains the backup file ending
 SYSCONF_OS                  contains the Sysconf operating system from hosts.sc
 SYSCONF_INTERFACE           contains the interface from hosts.sc
 SYSCONF_CLASSES             contains the Sysconf-classes from hosts.sc
 SYSCONF_HOSTS_SC_SUBSYSTEMS contains the Sysconf-Subsystems from hosts.sc
 SYSCONF_SUBSYSTEMS          contains all Subsystems
 SYSCONF_CURRENT_SUBSYSTEM   contains the name of currently running Subsystem
 SYSCONF_VARFILE             contains the content of the variable files
 SYSCONF_REVISION            contains the Revision from hosts.sc
 SYSCONF_ROOT                contains the path to the Sysconf repository and eqals SYSCONF_ROOT from .sysconfrc

All variables that start with SYSCONF_ are reserved for use in the future and so should not be used.

Example:

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

=head2 File F<files.sc>

In the file files.sc all Subsystems for this OS are defined.

Example:

 [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

 [nscd]
 f etc/nscd.conf seccon=system_u:object_r:etc_t:s0

 [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

 The syntax is:

 [subsys]
 <fileentry>
 <patternblock>

 [subsys]:       Begin description of Subsystem namend "subsys"
 <fileentry>:    <type> <filename> <targetfilename> <attributes ...>
 <patternblock>: with following structure:
 beginpattern
 <pattern>
 endpattern

The possible <pattern> are explained in the L</Patternblock> section.

If the <type> is not a Template Type then the <patternblock> is not applicable.

 <type>:
 instf : Installfile   (Only transferred with INIT, before installcmd)
 instt : Install-Template (Only transferred with INIT, before installcmd
                       and carrying out substitutions from the pattern
                       block)
 instL : Install-Link  (Only created by INIT, before installcmd)
 instS : Shell command (Only executed with INIT, before installcmd)
 initf : Initfile      (Only transferred with INIT, after installcmd)
 initt : Init-Template (By INIT when carrying out transfer and replace
                       actions from the Pattern block)
 initL : Init-Link     (Only created by INIT, after installcmd)
 initS : Shell command (Only execute by INIT, after installcmd)
 f     : File          (Transfer with UPDATE)
 t     : Template      (By UPDATE when carrying out transfer and replace
                       actions from the Pattern block)
 L     : Link          (Create with UPDATE)
 S     : Shell command (Execute with UPDATE)
 c     : Command template (Replaces the text pattern in command files)
 lS    : local shell command (Execute with UPDATE+INIT)
 m     : Modify

 <filename>:       Complete path of the file, when it is a relative path,
                   the file can be found under files.sc/,
                   otherwise the absolute file name is used.
 <targetfilename>: Optional. Complete target path + name of the file.
 <attribute>:      Optional. The following attributes are possible:
                   owner=<User name>
                   group=<Group name>
                   permission=<octal file rights as for chmod>
                   seccon=<Security Context, e.g. for SELinux and chcon>
                   hidden=<TRUE or FALSE>. Default is FALSE.
                   With setting hidden=TRUE this prevents that the file
                   appears in the HTML documentation
                   If no attributes are specified then the attributes of
                   the files in the sysconf database are used

With the links, as for the Unix "ln" command normal "source target" are specified. Therefore link is created from the "file name" to the "target file name".

Shell commands are intended to be used for creating or removing directories, device files and links etc. As a shell command everything that is possible per "rsh" is also possible e.g.:

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

Variables from the file F<variables/xxx.var> are expanded in the shell commands.

The command templates are used to make sure that you can carry out text replacements in the command files *cmd (see further below) before transmission to the target computers.
This way it is possible to add the content of variables to for example the reconfigcmd.

The "Modify type" serves firstly as a documentation for files that have been created or changed by e.g. a command (reconfigcmd) and secondly for consideration for the backup option. These modify files do not exist in the repository.

The special subsystem name "GLOBAL" means that these files do not belong to a specific subsystem, but are global. These are therefore the files and configuration files that every (!) computer with this operating system should receive. This subsystem is the first subsystem to be deployed on all computers.

Analogue to GLOBAL there is also the special subsystem "LAST", that all computers receives as the last subsystem to be deployed.

Using the instruction INCLUDE followed by file name relative to the location of the file "files.sc", you can expand the files.sc by adding further files to it. In extreme cases the files.sc only contains INCLUDE instructions and the subsystem definitions are all stored separately in individual files.


=head2 Command files *cmd

The following command files are executed during a sysconf run

 pre_localshell
 testinstallcmd
 installcmd
 testruncmd
 stopcmd
 reconfigcmd
 startcmd
 post_localshell

All command files (except the *_localshell files) must be present.
The code in this file can be script code or binary program code. It must be executable on the intended target system.
If such a *cmd script/program has a length of 0 then it will not be used!
If you for example do not need a startcmd, then create it with size 0 (leave it empty) and Sysconf will skip it silently.
If files for different operating systems are identical, you can simply create a link.
All scripts/programs must return the string "TRUE" or "FALSE" to STDOUT, which means "success" or "failure".
These scripts/programs (except the *_localshell files) are executed on the target computers.
pre_localshell and post_localshell are executed direct locally on the server, where Sysconf is executed. This can be used some some local preparation or post cleanup tasks.

Independent of the TRUE/FALSE return string, the return code ("exit code") is relevant for error classification:
If such a script/program sends a return code that is greater than 0, then Syconf stops with a "FATAL" error.
When FALSE on STDOUT is sent as return code and 0 as exit code then only an "ERROR" is recorded.

If both of the options CMD_USER and CMD_GROUP are used in the configuration file "commands.sc"(see below), you can determine which user and group is used to create the subsystems installcmd, reconfigcmd, etc. Standard here is the current user and group in effect that has executed sysconf.

The command files can be linked to the data in the L</Configuration database> using *.LINK-Files.

For UPDATE the *cmd files are executed in the following order:

 pre_localshell
    if TRUE, then continue with next command
    if FALSE, then: ERROR
 testinstallcmd
   if FALSE, then continue with INIT
   if TRUE, then continue with next command
 testruncmd
    if TRUE, then: stopcmd
    if FALSE, then continue with next command
 stopcmd
    if TRUE, then continue with next step
    if FALSE, then: ERROR
 Transferfiles()
    if OK, then continue with next command
    if FALSE, then: ERROR
 reconfigcmd
    if TRUE, then continue with next command
    if FALSE, then: ERROR
 startcmd
    if TRUE, then continue with next command
    if FALSE, then: ERROR
 post_localshell
    if TRUE, then END.
    if FALSE, then: ERROR

For INIT the *cmd files are executed in the following order:

 pre_localshell
    if TRUE, then continue with next command
    if FALSE, then: ERROR
 testinstallcmd
    if FALSE, then Transferfiles(install*), then: installcmd
    if TRUE, Transferfiles(init*)
 if installcmd TRUE, then Transferfiles(init*)
 testruncmd
    if TRUE, then: stopcmd
    if FALSE, then continue with Transferfiles()
 stopcmd
    if TRUE, then continue with next step
    if FALSE, then: ERROR
 Transferfiles()
    if OK, then continue with next command
    if FALSE, then: ERROR
 reconfigcmd
    if TRUE, then continue with next command
    if FALSE, then: ERROR
 startcmd
    if TRUE, then continue with next command
    if FALSE, then: ERROR
 post_localshell
    if TRUE, then END.
    if FALSE, then: ERROR

For START the *cmd files are executed in the following order:

 testinstallcmd
    if FALSE, then END.
    if TRUE, then continue with next command
 testruncmd
    if TRUE, then END.
    if FALSE, then: startcmd
 startcmd
    if TRUE, then END.
    if FALSE, then: ERROR

For STOP the *cmd files are executed in the following order:

 testinstallcmd
    if FALSE, then END.
    if TRUE, then continue with next command
 testruncmd
    if FALSE, then END.
    if TRUE, then: stopcmd
 stopcmd
    if TRUE, then END.
    if FALSE, then: ERROR

For REMOVE the *cmd files are executed in the following order:

 pre_localshell
    if TRUE, then continue with next command
    if FALSE, then: ERROR
 testinstallcmd
    if FALSE, then END.
    if TRUE, then continue with next command
 testruncmd
    if TRUE, then: stopcmd
    if FALSE, then: removecmd
 stopcmd
    if TRUE, then continue with next command
    if FALSE, then: ERROR
 removecmd
    if TRUE, then continue with next command
    if FALSE, then: ERROR
 post_localshell
    if TRUE, then END.
    if FALSE, then: ERROR

=head2 Comments

In the files:

 dependencies.sc
 dependencies-soft.sc
 exclusions.sc
 hosts.sc
 classes.sc

it is possible to create comments: All lines that begin with '#' will be ignored.


=head2 files F<hosts.sc> and F<classes.sc>


The file F<hosts.sc> describes the individual computers: This file
designates which subystems should be deployed to which computers. This
file is constructed in the form of stanzas (paragraphs). Here each
stanza must start with the keyword HOST. To maintain a clear overview
all of the stanzas should be separated by an empty line (carriage
return) HOST is used to specify the computer name. This name must match
the names of the variable in the subdirectory variables/. OS is used to
specify the operating system. This is a freely selectable designation.
There must however be a subdirectory with the same name in the Sysconf
main directory. Any entries made after CLASSES and SUBSYSTEMS are
optional. This designates which subsystems the computers should receive.
In the SUBSYSTEMS entry, individual subsystems can be negated if you put
an "!" in front of them. All subsystems of the specified classes and
subsystems will be installed on this computer except for the subsystem
or subsystems that are marked with "!". Optionally you can use
INTERFACE to specify the interface that should be used Example: Computer
abc123 has under the hostname abc123giga a gigabit Ethernet
interface. So you can Specify as HOST abc123 and under INTERFACE specify
abc123giga. Sysconf will then install on the Computer via the
Gigabit-Ethernet interface. This is very useful for HACMP clusters,
because in this case you should not specify the productive hostname but
hostname/adapter that is not designated as a resource and so will not
"wander". This way you always specify the same computer by using for
example the adapter that is part of the internal administration network.
When specifying "localhost" for HOST or INTERFACE then the computer will
not be accessed remotely, instead the computer that is running sysconf
will be addressed. Instead of using hostnames you can also use
IP-addresses. With REVISION a string or number can be defined, this is
useful for example when operating a version constraint or control system
on the sysconf repository such as CVS or Subversion for you need to
document certain versions for certain computers. Sysconf itself does not
use the field REVISION at all. This value can be displayed using the
command sysconf listrevision xxx ALL You can also save individual
stanzas from the "hosts.sc" as individual files in the directory
hostsdir.sc The filenames must start alphanumerically and end in ".inc".

Example:

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

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

 HOST         neptun
 REVISION     72356
 OS           Linux-SuSE-7.0
 CLASSES      Development basic system
 SUBSYSTEMS   inittab gcc2723 gmake libXpm mailclient adsm-client

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

Optionally, classes can also be defined in the file F<classes.sc>:

 CLASSDEF    Basic ystem
 SUBSYSTEMS  syslog tcpwrapper ssh

 CLASSDEF    Server
 SUBSYSTEMS  adsm audit nis

The pupose of classes is to combine subsystems. This means that instead of doing:

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

You can define this class:

 CLASSDEF    Entwicklung
 SUBSYSTEMS  gcc2723 gmake ddd lex yacc perl

And keep entries in the hosts.sc to a minimum:

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

Negating a subsystem (everything except "dddd") would then look like this:

 HOST         neptun
 OS           Linux-SuSE-7.0
 CLASSES      Development
 SUBSYSTEMS   mailclient !ddd adsm-client

Whereby the subsystems should be spread out as much as possible e.g.:

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

Substitution rules can be specified in the file F<files.sc> in the
pattern block for templates. These rules specify how the template files
are changed before they are copied. Note in the pattern block no
comments are possible! Empy lines will be ignored: The possible syntax
constructions are as follows:

 change <substitute>

 add FIRST <text>

 add LAST  <text>

 add <patternmatch>
 <text>

 adduniq FIRST <text>

 adduniq LAST <text>

 adduniq <patternmatch>
 <text>

 <substitute>:   Substitute command from Perl: s/.../.../
 <patternmatch>: Pattern match command from Perl: m/.../.../
 <text>:         Text, that should be entered.

Note: a new line must be started after the Patternmatch!

The variables that have been defined in F<variables/rechnername.var> can
be used with a preceding "$". Escapes ("\n") in variables will not be
interpreted, however you can reproduce this functionality: If you have a
variable with Newlines e.g.

VAR=This\nare\nlot\nof\nlines\nseparated\nby\nnewlines\n

Then you yourself must to ensure that the special characters are interpreted correctly:

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

However, if you want to be certain that for example that variable does not contain any Newlines, then 
you can use the following line of code:

  change s/ROOTDISK/($ROOTDISK=~s:\n::g,$ROOTDISK)/e

It should also be mentioned that variables can also be evaluated in the "e"-Variations (s/xxx/xxx/e)
if you use the command "eval", e.g.:

change s/suchtext/eval $MY_SPECIAL_PERL_CODE/e

Here the content of the variables $MY_SPECIAL_PERL_CODE will be executed
as code and the result then replaces the "search text".

Examples:

Change lines:
   change s/#MEINE_IP#/10.135.82.54/
   change s/#MEIN_NAME#/$RECHNER/
   change s/#ANWENDUNG#/Rechner Testbetrieb/
   change s/^"START_INN=.*/"START_INN=yes"/
   change s/SC_GESCHWINDIGKEIT/$MODEMSPEED/

Delete lines:
   change s/^netstat\s+stream\s+tcp//

Insert Lines:
Insert in front of the first line:
   add FIRST 127.0.0.1       localhost

Insert after the last line:
   add LAST 129.187.13.89 mailhost mail

Insert after the line that starts with "OVERVIEW":
   add m/^OVERVIEW/
   news/newsserver:*,!junk,!control*:Ap,Tf,Wnm:news

Insert lines only when they are not already present in the file:
   adduniq m/^6:123:respawn:/
   mo:123:respawn:/usr/local/sbin/vgetty modem

Completely delete lines that start with "TCPWindowsize":
   change s/^TCPWindowsize.*\n//


=head2 dependencies.sc

The file "dependencies.sc" is built up as follows:

S : D1 D2 D3 ...

Meaning: Subsystem "S" is dependent on the subsystems "D1", "D2", "D3",
etc. This means that the subsystems D1, D2, D3 are added by Sysconf to
the list of subsystems and are deployed BEFORE "S". If one of the
subsystems D1, D2 or D3 are not allowed for a specific target host, then
they are not added and an error is thrown!

Examples:

 sendmail : syslog
 nfsserver : tcpwrapper portmap inetd


=head2 dependencies-soft.sc

The file "dependencies-soft.sc" is constructed as follows:

S : D1 D2 D3 ...

Meaning: Subsystem "S" has a "soft" dependency on the subsystems "D1",
"D2" and "D3". This means that if the subsystems D1, D2 and D3 are
allowed for a specific target host, then they will be added by Sysconf
to the list of subsystems and are deployed BEFORE "S". If one of the
subsystems D1, D2 or D3 are not allowed for a specific target host, then
they are not added and NO error is thrown.

Examples:

 ssh : ldap


=head2 exclusions.sc

The file "exclusions.sc" is constructed as follows:

S1 S2

Means: Subsystem "S1" and Subsystem "S2" are mutually exclusive.

Examples:

 mailclient mailserver
 gcc2723 egcs1111
 sendmail qmail


=head2 commands.sc

The file "commands.sc" describes which commands are to be executed remotely.
This file is optional, is however recommended for security reasons.
If this file is missing, then the commands will be executed without an absolute path.
If individual commands are not listed in this file, then the will be executed without a path.

It is especially recommended that this file be used for the "cp" command, because "cp" needs different switches in the different unices in order to copy symbolic links correctly. 
I recommend for Linux "cp -pd", and for  AIX "cp -ph" and for Solaris 10 onwards "cp -rpP".

With the options CMD_USER and CMD_GROUP you can determine which user and
group the command for the subsystems (look at L</Command files *cmd>)
e.g. installcmd, reconfigcmd can be created. Standard here is the user
and group that has started Sysconf.

By using the option REMOTE_USER, you can determine which user context
should be used for rsh/ssh/rsync access actions.  (Default: here is the
user and group that has started Sysconf)

By using the option USE_SUDO (Default FALSE) you can activate an
intermediate step on the client this is only really sensible when
Sysconf is not running as "Root" or the Sysconf-Client was not started
as "Root".  Normally the Remote Copy and Remote Shell functions run in
the context of the user that started Sysconf. For example when Sysconf
is running in the Root context on the client, a shell command will be
executed as Root. When however, for security reasons, Sysconf should not
be run in the Root context but must still change files that are owned by
"Root" then you can use USE_SUDO=TRUE to call sudo between the
routines. (The name of the sudo programme can of course be set in the
commands.sc file).

Normally a remote copy is carried out as follows: "scp sendmail.cf
client:/etc/sendmail.cf" A remote shell as follows: "ssh client
command".

With sudo this is then as follows: "scp sendmail.cf
client:/tmp ; ssh client sudo mv /tmp/sendmail.cf /etc/sendmail.cf"
respectively "ssh client sudo command".

In /etc/sudoers you need to make the following sort of entry:
"mysysonfuser ALL=NOPASSWD:ALL"

Using the option SSH_KEY you can specify the location of an alternative
SSH-Key. This is always needed when you have specified a REMOTE_USER.
You can specify the full absolute path to the SSH key. You can also enter
a whitespace separated list of SSH keys, e.g.:

 SSH_KEY=/home/myotheruser/.ssh/id_dsa /home/someuser/.ssh/other_key

Example for 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
 CHCON=/usr/bin/chcon
 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 end with the following return codes:

 0 = No errors occurred
 1 = One or more warnings (WARN) occurred
 2 = One or more errors (ERROR) occurred
 3 = One or more fatal errors (FATAL) occurred

No response value.

=head1 KNOWN BUGS AND PROBLEMS

With AIX, when trying to replace (write over the top of) an executable file that is currently being executed, the following error message will be displayed:
"Cannot open or remove a file containing a running program."
or "Text file busy".
This can only be avoided by using start and stop scripts the ensure that the affected programmes
are stopped by Sysconf.
This is not practical (e.g. with Demons) I would recommend to not directly replace the file, but to 
create a temporary file then to copy it using a reconfigcmd, also using the files.sc:

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

And then in the reconfigcmd something like:

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

This sort of problem can be avoided by using rsync.


=head1 AUTHOR


=for text
Sysconf was written by Stephan Löscher,
http://www.loescher-online.de/,
loescher@gmx.de, 1998.
The idea for Sysconf was inspired by: GNU CFEngine, rdist, Michael Mattes (michael.mattes@iname.com), Software Update Protocol (SUP) of the Carnegie-Mellon University (CMU). A similar software is either Puppet or Chef.
I would like to thank the following people for correcting errors and supplying new features:
Michael Mattes (michael.mattes@iname.com)
Andreas Bussjäger (Andreas.Bussjaeger@partner.bmw.de, Andreas.Bussjaeger@t-systems.com)

=for man
Sysconf was written by Stephan Löscher,
http://www.loescher-online.de/,
loescher@gmx.de, 1998.
The idea for Sysconf was inspired by: GNU CFEngine, rdist, Michael Mattes (michael.mattes@iname.com), Software Update Protocol (SUP) of the Carnegie-Mellon University (CMU). A similar software is either Puppet or Chef.
I would like to thank the following people for correcting errors and supplying new features:
Michael Mattes (michael.mattes@iname.com)
Andreas Bussjäger (Andreas.Bussjaeger@partner.bmw.de, Andreas.Bussjaeger@t-systems.com)

=for latex
Sysconf was written by Stephan Löscher,
http://www.loescher-online.de/,
loescher@gmx.de, 1998.
The idea for Sysconf was inspired by: GNU CFEngine, rdist, Michael Mattes (michael.mattes@iname.com), Software Update Protocol (SUP) of the Carnegie-Mellon University (CMU). A similar software is either Puppet or Chef.
I would like to thank the following people for correcting errors and supplying new features:
Michael Mattes (michael.mattes@iname.com)
Andreas Bussjäger (Andreas.Bussjaeger@partner.bmw.de, Andreas.Bussjaeger@t-systems.com)

=for html
Sysconf was written by
<A HREF="http://www.loescher-online.de/">Stephan L&ouml;scher</A>,
<A HREF="mailto:loescher@gmx.de">loescher@gmx.de</A>,
1998.<br>
The idea for Sysconf was inspired by:
<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>. A similar software is either <a href="http://www.puppetlabs.com/">Puppet</a> or <a href="http://community.opscode.com/">Chef</a>.<br>
I would like to thank the following people for correcting errors and supplying new features:<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 for private use.
# 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 can request a license for commercial use. Please contact me.
#
# You may and should copy this program free of charge, use it,
# give it to your friends, upload it to a BBS, FTP-site or something similar,
# under the following conditions:
# * Don't charge any money for it. If you upload it to a download-site, make
#    sure that it can be downloaded free (without paying for downloading it,
#    except for usage fees for the download-site that have to be paid anyway).
#  * 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.
#
######################################################################
