#!/usr/bin/perl -w
# Programm zur Rechnerkonfiguration
# Diplomarbeit von Stephan Löscher
# Bei Rückfragen:
# loescher@gmx.de oder 08142/7257
######################################################################
###
### IMPORTANT!
### Please read the warranty and legal notice
### at the end of this file!
###
######################################################################
require 5.000;
use lib '/usr/local/bin',"$ENV{HOME}/bin",'/usr/stud/loescher/bin';
use lib 'd:/bin','c:/mydos','c:/bin';
use slutil; # Verfügbar unter: http://www.leo.org/~loescher/progdata/slutil.pm
use English;
use FileHandle;
use File::Copy;
use Carp;
######################################################################
### Unterprogramm-Funktionen für interne Zwecke
######################################################################
sub debug_rsh; # Ausgabe von Informationen über remote-shell
sub debug; # Ausgabe von Informationen
sub warning; # Ausgabe von Warnungen
sub error; # Ausgabe von Fehlern
sub myexit; # Exit und Aufräumen
sub loginit; # Schreibt einen kleinen Header ins LOG-file
sub logdie; # Schreibt einen Text ins LOG-file und stirbt dann
sub logprint; # Schreibt einen Text ins LOG-file
######################################################################
### Voreinstellungen
######################################################################
$version = '1.0';
$appname = 'SysConf';
# Wohin soll das logging erfolgen?
$logfile = ">>/tmp/\L$appname\E.log"; # Log in eine Datei
# $logfile = ">&STDERR"; # Log auf STDERR
# Exitcodes beim Beenden
$NormalExitCode = 0;
$ErrorExitCode = 1;
$0 = $0; # Oh, very strange... :-) ... Kommandozeile verbergen.
######################################################################
### Log-File und Signal-Handler
######################################################################
# LOG bereits hier starten, denn Konfigurationsfehler sind entscheidend!
open(LOG, $logfile) || die "Kann Logfile '$logfile' nicht schreiben!\n";
select(LOG); $|=1; select(STDOUT); # Buffer ausschalten
loginit;
# Signal-Handler installieren
$SIG{HUP} = \&catch_signal;
$SIG{INT} = \&catch_signal;
$SIG{QUIT} = \&catch_signal;
$SIG{ABRT} = \&catch_signal;
$SIG{TERM} = \&catch_signal;
$SIG{'__WARN__'} = \&catch_warning;
# Welche Meldungen sollen ausgegeben werden?
if ( (defined $ARGV[0]) && ($ARGV[0] =~ /^-w(\d)/) )
{
$logLevel = $1;
shift;
}
else
{
$logLevel = 0;
}
logprint "Log-Level: $logLevel\n";
######################################################################
### Hauptprogramm
######################################################################
if ($#ARGV<0)
{
logprint "Ohne Parameter aufgerufen.\n";
print "\nSie sollten $appname nicht ohne Parameter aufrufen.
Sie haben zur Auswahl:
1. man-page erzeugen und ins aktuelle Verzeichnis schreiben
2. HTML-Dokumentation ins aktuelle Verzeichnis schreiben
3. LaTeX-Dokumentation ins aktuelle Verzeichnis schreiben
4. Alle Dokumentationen (1 bis 3) erzeugen
5. Kurzhilfe anzeigen
Auswahl: ";
$input = readkey(); print "\n";
POD_Ausgabe('man') if $input =~ /1/;
POD_Ausgabe('html') if $input =~ /2/;
POD_Ausgabe('latex') if $input =~ /3/;
if ($input =~ /4/)
{
POD_Ausgabe('man');
POD_Ausgabe('html');
POD_Ausgabe('latex');
}
&Hilfe if $input =~ /5/;
myexit;
}
printumlaute Kopf();
# Einstellungen aus Hauptkonfigurationsdatei lesen
$sysconfroot = '';
$UseSyscheck = $FALSE;
$htmldir = '';
ReadConfigFile();
debug "SYSCONF_ROOT = $sysconfroot\n";
debug "USE_SYSCHECK = $UseSyscheck\n";
debug "HTMLDIR = $htmldir\n";
$checkpass = ReadWithoutEcho('Syscheck-Paßwort: ','STDIN') if $UseSyscheck;
print "\n";
%klassendef = RechnerKlassenEinlesen();
$beschreibungen = RechnerBeschreibungenEinlesen();
$param = ParameterEinlesen(@ARGV);
debug "Aktion: ", $param->Aktion,"\n";
debug "Subsysteme: ", join(',',$param->Subsysteme), "\n";
debug "Rechner: ", join(',',$param->Rechner), "\n";
$dependencies = Dependencies::new();
$files = Files::new();
$templatepattern = TemplatePattern::new();
Documentation($beschreibungen,$param) if $param->Aktion eq 'documentation';
foreach $rechner ($param->Rechner)
{
# Hier ist es pro Rechner parallelisierbar
# Testen, ob es eine Beschreibung zu dem Rechner gibt
unless (defined $beschreibungen->Betriebssystem($rechner))
{
warning "Zu dem Rechner '$rechner' gibt es keine Breschreibung!\n";
next;
}
# Überprüfen durch CheckSubsystems():
# - alle Subsysteme?
# - darf dieser Rechner diese Subsysteme bekommen?
# - dann stehen in @subsys die gewünschten Subsysteme
@subsys = CheckSubsystems($rechner,$beschreibungen,$param);
unless (@subsys)
{
warning "Keine Subsysteme für '$rechner' zu konfigurieren!\n";
next;
}
%RechnerVars = ReadVariables($rechner);
debug "Rechnervariablen:\n";
foreach (keys %RechnerVars)
{ debug "'$_'='$RechnerVars{$_}'\n"; }
# Datei "files.sc" für dieses Betriebssystem einlesen
ReadFilesSC($beschreibungen->Betriebssystem($rechner));
debug "Nach ReadFilesSC().\n";
SWITCH: {
# Init
if ($param->Aktion eq 'init')
{
@subsys =
CheckAndAddDependencies($beschreibungen->Betriebssystem($rechner),
@subsys);
Init($rechner,$beschreibungen->Betriebssystem($rechner),\%RechnerVars,
@subsys);
last SWITCH;
}
# Update
if ($param->Aktion eq 'update')
{
@subsys =
CheckAndAddDependencies($beschreibungen->Betriebssystem($rechner),
@subsys);
Update($rechner,$beschreibungen->Betriebssystem($rechner),\%RechnerVars,
@subsys);
last SWITCH;
}
# Start
if ($param->Aktion eq 'start')
{
# Hier kein CheckAndAddDependencies(), weil sonst alle möglichen
# Subsysteme beeinflußt werden!
Start($rechner,$beschreibungen->Betriebssystem($rechner),@subsys);
last SWITCH;
}
# Stop
if ($param->Aktion eq 'stop')
{
# Hier kein CheckAndAddDependencies(), weil sonst alle möglichen
# Subsysteme beeinflußt werden!
Stop($rechner,$beschreibungen->Betriebssystem($rechner),@subsys);
last SWITCH;
}
# Remove
if ($param->Aktion eq 'remove')
{
# Hier kein CheckAndAddDependencies(), weil sonst alle möglichen
# Subsysteme entfernt werden!
Remove($rechner,$beschreibungen->Betriebssystem($rechner),@subsys);
last SWITCH;
}
# Dokumentation
if ($param->Aktion eq 'documentation')
{
Documentation($rechner,$beschreibungen->Betriebssystem($rechner),
\%RechnerVars,@subsys);
last SWITCH;
}
logdie "Die Aktion ",$param->Aktion," gibt es nicht!\n";
}
print '-'x70,"\n";
}
myexit($NormalExitCode);
######################################################################
### Unterprogramme
######################################################################
sub Init
{
# Initialisieren eines Rechners
# Parameter: Rechner, Betriebssystem, Referenz auf Hash der Rechnervariablen,
# Liste der Subsysteme
my $rechner = shift;
my $bs = shift;
my $refvar = shift;
my @subsys = @_;
print "Führe Aktion 'init' mit '$rechner' für die ",
"Subsysteme\n",join(',',@subsys), "\nin genau dieser Reihenfolge aus.\n";
# Subsystem-unabhängige Files
TransferFiles($rechner, $bs, 'GLOBAL', 'installshell',$refvar);
TransferFiles($rechner, $bs, 'GLOBAL', 'install', $refvar);
TransferFiles($rechner, $bs, 'GLOBAL', 'installlink', $refvar);
# Init-Files/Templates kopieren
TransferFiles($rechner, $bs, 'GLOBAL', 'initshell', $refvar);
TransferFiles($rechner, $bs, 'GLOBAL', 'init', $refvar);
TransferFiles($rechner, $bs, 'GLOBAL', 'inittemplate',$refvar);
TransferFiles($rechner, $bs, 'GLOBAL', 'initlink', $refvar);
# Rest, den "update' auch überträgt
TransferFiles($rechner, $bs, 'GLOBAL', 'shell', $refvar);
TransferFiles($rechner, $bs, 'GLOBAL', 'file', $refvar);
TransferFiles($rechner, $bs, 'GLOBAL', 'template', $refvar);
TransferFiles($rechner, $bs, 'GLOBAL', 'link', $refvar);
# Für alle Subsysteme
my $subsystem;
foreach $subsystem (@subsys)
{
print "Behandle Subsystem '$subsystem'\n";
my $sub = SubsystemObject->new($bs, $rechner, $subsystem);
# Ist das Subsystem bereits installiert?
if ($sub->IsInstalled)
{
# Läuft das Subsystem gerade?
if ($sub->IsRunning)
{
# Stoppen (damit auch alle abhängigen)
$sub->Stop || warning "Stoppen von '$subsystem' nicht erfolgreich!\n";
}
}
else
{
# Files, die zur Installation benötigt werden übertragen
TransferFiles($rechner, $bs, $subsystem, 'installshell',$refvar);
TransferFiles($rechner, $bs, $subsystem, 'install', $refvar);
TransferFiles($rechner, $bs, $subsystem, 'installlink', $refvar);
# Subsystem installieren
unless ($sub->Install)
{
warning "Install von '$subsystem' nicht erfolgreich!\n";
warning "Breche '$subsystem' ab!\n";
return;
}
}
# Init-Files/Templates kopieren
TransferFiles($rechner, $bs, $subsystem, 'initshell', $refvar);
TransferFiles($rechner, $bs, $subsystem, 'init', $refvar);
TransferFiles($rechner, $bs, $subsystem, 'inittemplate',$refvar);
TransferFiles($rechner, $bs, $subsystem, 'initlink', $refvar);
# Rest, den "update' auch überträgt
TransferFiles($rechner, $bs, $subsystem, 'shell', $refvar);
TransferFiles($rechner, $bs, $subsystem, 'file', $refvar);
TransferFiles($rechner, $bs, $subsystem, 'template', $refvar);
TransferFiles($rechner, $bs, $subsystem, 'link', $refvar);
# Subsystem starten
$sub->Start || warning "Start von '$subsystem' nicht erfolgreich!\n";
}
}
sub Update
{
# Updaten eines Rechners
# Parameter: Rechner, Betriebssystem, Referenz auf Hash der Rechnervariablen,
# Liste der Subsysteme
my $rechner = shift;
my $bs = shift;
my $refvar = shift;
my @subsys = @_;
print "Führe Aktion 'update' mit '$rechner' für die ",
"Subsysteme\n",join(',',@subsys), "\nin genau dieser Reihenfolge aus.\n";
# Subsystem-unabhängige Files
TransferFiles($rechner, $bs, 'GLOBAL', 'installshell',$refvar);
TransferFiles($rechner, $bs, 'GLOBAL', 'install', $refvar);
TransferFiles($rechner, $bs, 'GLOBAL', 'installlink', $refvar);
TransferFiles($rechner, $bs, 'GLOBAL', 'shell', $refvar);
TransferFiles($rechner, $bs, 'GLOBAL', 'file', $refvar);
TransferFiles($rechner, $bs, 'GLOBAL', 'link', $refvar);
TransferFiles($rechner, $bs, 'GLOBAL', 'template', $refvar);
# Für alle Subsysteme
my $subsystem;
foreach $subsystem (@subsys)
{
print "Behandle Subsystem '$subsystem'\n";
my $sub = SubsystemObject->new($bs, $rechner, $subsystem);
# Ist das Subsystem bereits installiert?
if ($sub->IsInstalled)
{
# Läuft das Subsystem gerade?
if ($sub->IsRunning)
{
# Stoppen (damit auch alle abhängigen)
$sub->Stop || warning "Stoppen von '$subsystem' nicht erfolgreich!\n";
}
# Files für Update übertragen
TransferFiles($rechner, $bs, $subsystem, 'shell', $refvar);
TransferFiles($rechner, $bs, $subsystem, 'file', $refvar);
TransferFiles($rechner, $bs, $subsystem, 'template',$refvar);
TransferFiles($rechner, $bs, $subsystem, 'link', $refvar);
$sub->Reconfigure ||
warning "Rekonfigurieren von '$subsystem' nicht erfolgreich!\n";
# Subsystem starten
$sub->Start || warning "Start von '$subsystem' nicht erfolgreich!\n";
}
else
{
# Wenn das Subsystem nicht installiert ist, dann Init() aufrufen
Init($rechner,$bs,$refvar,@subsys);
}
}
}
sub Remove
{
# Parameter: Rechner, Betriebssystem, Liste der Subsysteme
my $rechner = shift;
my $bs = shift;
my @subsys = @_;
print "Führe Aktion 'remove' mit '$rechner' für die ",
"Subsysteme\n",join(',',@subsys), "\nin genau dieser Reihenfolge aus.\n";
# Für alle Subsysteme
my $subsystem;
foreach $subsystem (@subsys)
{
print "Behandle Subsystem '$subsystem'\n";
my $sub = SubsystemObject->new($bs, $rechner, $subsystem);
# Ist das Subsystem überhaupt installiert?
unless ($sub->IsInstalled)
{
print "'$subsystem' ist auf '$rechner' nicht installiert!\n";
next;
}
# Läuft das Subsystem gerade?
if ($sub->IsRunning)
{
# Stoppen (damit auch alle abhängigen)
$sub->Stop || warning "Stoppen von '$subsystem' nicht erfolgreich!\n";
}
# Subsystem de-installieren
$sub->Remove || warning "Remove von '$subsystem' nicht erfolgreich!\n";
}
}
sub Start
{
# Parameter: Rechner, Betriebssystem, Liste der Subsysteme
my $rechner = shift;
my $bs = shift;
my @subsys = @_;
print "Führe Aktion 'start' mit '$rechner' für die ",
"Subsysteme\n",join(',',@subsys), "\nin genau dieser Reihenfolge aus.\n";
# Für alle Subsysteme
my $subsystem;
foreach $subsystem (@subsys)
{
print "Behandle Subsystem '$subsystem'\n";
my $sub = SubsystemObject->new($bs, $rechner, $subsystem);
# Ist das Subsystem überhaupt installiert?
unless ($sub->IsInstalled)
{
print "'$subsystem' ist auf '$rechner' nicht installiert!\n";
next;
}
# Läuft das Subsystem gerade?
if ($sub->IsRunning)
{
print "'$subsystem' läuft auf '$rechner' bereits!\n";
next;
}
# Subsystem starten
$sub->Start || warning "Start von '$subsystem' nicht erfolgreich!\n";
}
}
sub Stop
{
# Parameter: Rechner, Betriebssystem, Liste der Subsysteme
my $rechner = shift;
my $bs = shift;
my @subsys = @_;
print "Führe Aktion 'stop' mit '$rechner' für die ",
"Subsysteme\n",join(',',@subsys), "\nin genau dieser Reihenfolge aus.\n";
# Für alle Subsysteme
my $subsystem;
foreach $subsystem (@subsys)
{
print "Behandle Subsystem '$subsystem'\n";
my $sub = SubsystemObject->new($bs, $rechner, $subsystem);
# Ist das Subsystem überhaupt installiert?
unless ($sub->IsInstalled)
{
print "'$subsystem' ist auf '$rechner' nicht installiert!\n";
next;
}
# Läuft das Subsystem gerade?
if ($sub->IsRunning)
{
# Subsystem stoppen
$sub->Stop || warning "Stoppen von '$subsystem' nicht erfolgreich!\n";
}
else
{
print "'$subsystem' läuft auf '$rechner' gar nicht!\n";
}
}
}
sub TransferFiles
{
# Überträgt Files von der Konfigurationsdatenbank auf Zielrechner
# und führt Textersetzungen durch und startet Shellkommandos
# Parameter: Rechner, Betriebssystem, Subsystem, File-Art,
# Referenz auf Hash mit den Rechnervariablen
# Return: -
my ($rechner, $bs, $subsys, $art, $refvar) = @_;
unless ( $files->IstArtGueltig($art) )
{
carp("TransferFiles() mit falschem Art-Parameter aufgerufen!\n");
myexit($main::ErrorExitCode);
}
my %files = %{$files->Get($bs,$subsys,$art)};
print "Übertrage Files... ($art)\n";
my ($file, $zielfile);
foreach $file (keys %files)
{ # foreach $file
@zielfiles = @{$files{$file}};
foreach $zielfile (@zielfiles)
{ # foreach $zielfile
# Installfiles, Initfiles, Files
if ($art =~ /^install$|^init$|^file$/)
{
RemoteCopy($file,$rechner,$zielfile);
next;
}
# Inittemplates, Templates
if ($art =~ /^inittemplate$|^template$/)
{
# Ersetzungsmuster anwenden
my $pattern = $templatepattern->Get($bs,$subsys,$file,$zielfile);
my $tempfile = "/tmp/sysconf.template.$$";
TextModify::ErsetzeMuster($file, $tempfile, $pattern, $refvar);
# Owner,Gruppe,Permissions auf das neue Tempfile übertragen
my ($mode,$uid,$gid) = (stat($file))[2,4,5];
chmod $mode, $tempfile || logdie "Fehler bei chmod('$tempfile')!\n";
chown $uid,$gid,$tempfile || logdie "Fehler bei chown('$tempfile')!\n";
RemoteCopy($tempfile,$rechner,$zielfile);
unlink $tempfile;
next;
}
# Links
if ($art =~ /^link$|^installlink$|^initlink$/)
{
RemoteCreateLink($file,$rechner,$zielfile);
next;
}
# Shellkommandos
if ($art =~ /^shell$|^installshell$|^initshell$/)
{
my $variablenCode = '';
foreach (keys %$refvar)
{
my $refvarquote = $$refvar{$_}; $refvarquote =~ s/\'/\\\'/g;
$variablenCode .= "my \$$_='$refvarquote';\n";
}
# Variablenersetzung
{
local $FehlerInShellVariablenErsetzung_Kommando = $file;
local $SIG{__WARN__} = \&FehlerInShellVariablenErsetzung;
eval $variablenCode.'$file =~ s/(\$\w+)/$1/eeg;';
}
RemoteShell($rechner,$file);
next;
}
# Sonst
logdie "Interner Fehler: File-Art ist ungültig!\n";
}
}
}
sub Documentation
{
# HTML-Dokumentation erzeugen
# Parameter: Beschreibungen-Objekt, ParameterListe-Objekt
# Return; -
#
my ($beschreibungen,$param) = @_;
my $head = '<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html>
<head>
<title>Systemkonfiguration</title>
</head>
<body>
<h1>Systemkonfiguration</h1>
<table border=1>
<tr>
<td>Rechner</td>
<td>Subsysteme</td>
<td>Variablen</td>
<td>Betriebssystem</td>
</tr>
';
my $tail = ' </table>
<hr>
<address>
Diese Seite wurde automatisch generiert auf dem Rechner '
.`hostname`.
" aus der Datenbank in $sysconfroot durch<br>"
.$appname.' '.$version.' von
<A HREF="http://www.leo.org/~loescher/">Stephan Löscher</A>,
<a href="mailto:loescher@gmx.de">loescher@gmx.de</a>,<br>
'.date.'
</address>
</body>
</html>
';
print "Führe Aktion 'documentation' aus.\n";
my $fh = FileHandle->new();
open($fh, ">$htmldir${slash}index.html") || logdie
"Kann '$htmldir${slash}index.html' nicht zum Schreiben öffnen!\n";
print $fh $head;
my $rechner;
my $bs;
foreach $rechner ($param->Rechner)
{
# Testen, ob es eine Beschreibung zu dem Rechner gibt
unless (defined ($bs = $beschreibungen->Betriebssystem($rechner)) )
{
warning "Zu dem Rechner '$rechner' gibt es keine Breschreibung!\n";
next;
}
my @subsys = CheckSubsystems($rechner,$beschreibungen,$param);
unless (@subsys)
{
warning "Keine Subsysteme für '$rechner' vorhanden!\n";
next;
}
my %RechnerVars = ReadVariables($rechner);
# Datei "files.sc" für dieses Betriebssystem einlesen
ReadFilesSC($beschreibungen->Betriebssystem($rechner));
# Dateien der Variablen-Inhalte erstellen
my ($key,$value);
while (($key,$value) = each %RechnerVars)
{
my $vfh = FileHandle->new();
open($vfh, ">$htmldir${slash}$rechner-$key.txt") || logdie
"Kann '$htmldir${slash}$rechner-$key.txt' nicht zum Schreiben öffnen!\n";
print $vfh $value;
close $vfh;
}
print $fh " <tr>
<td valign=top>$rechner</td>
<td valign=top>\n";
foreach (@subsys)
{
print $fh " <a href=\"$rechner-sub-$_.html\">$_</a><br>\n";
GeneriereSubsystemHTML($rechner, $bs, $_, \%RechnerVars);
}
print $fh " </td>\n <td valign=top>\n";
foreach (keys %RechnerVars)
{
print $fh " <a href=\"$rechner-$_.txt\">$_</a><br>\n";
}
print $fh " </td>
<td valign=top>",$beschreibungen->Betriebssystem($rechner),"</td>
</tr>
";
}
print $fh $tail;
close $fh;
myexit($NormalExitCode);
}
sub GeneriereSubsystemHTML
{
# Erstellt HTML-Seiten zu einem Subsystem analog wie TransferFiles() arbeitet
# Parameter: Rechner, Betriebssystem, Subsystem,
# Referenz auf Hash mit den Rechnervariablen
# Return: -
my ($rechner, $bs, $subsys, $refvar) = @_;
my @artliste = ('install','installlink','installshell',
'init','inittemplate','initlink','initshell',
'file','template','link','shell');
my $art;
my $head = "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML//EN\">
<html>
<head>
<title>Subsystem '$subsys' auf '$rechner'</title>
</head>
<body>
<h2>Subsystemkonfiguration: ".
"Dateien für '$subsys' auf Rechner '$rechner'</h2>
<table border=1>
<tr>
<td><b>Dateiart</b></td>
<td><b>Datei/Inhalt</b></td>
</tr>\n";
my $tail = ' <hr>
<address>
Diese Seite wurde automatisch generiert durch<br>
'.$appname.' '.$version.' von
<A HREF="http://www.leo.org/~loescher/">Stephan Löscher</A>,
<a href="mailto:loescher@gmx.de">loescher@gmx.de</a>,<br>
'.date.'
</address>
</body>
</html>
';
my $fh = FileHandle->new();
open($fh, ">$htmldir${slash}$rechner-sub-$subsys.html") || logdie
"Kann '$htmldir${slash}$rechner-sub-$subsys.html' nicht zum Schreiben ".
"öffnen!\n";
print $fh $head;
# Alle Dateiarten durchlaufen
foreach $art (@artliste)
{ # foreach $art
my %files = %{$files->Get($bs,$subsys,$art)};
my ($file, $zielfile);
print $fh " <tr>\n <td valign=top>$art</td>\n <td>\n";
foreach $file (keys %files)
{ # foreach $file
@zielfiles = @{$files{$file}};
foreach $zielfile (@zielfiles)
{ # foreach $zielfile
my $zielhtml = $zielfile;
$zielhtml =~ s!/!_!g;
# Installfiles, Initfiles, Files
if ($art =~ /^install$|^init$|^file$/)
{
# Nur kopieren, wenn es eine Text-Datei ist
if (-T $file)
{
copy($file, "$htmldir$slash$rechner-sub-$subsys-$zielhtml.txt");
print $fh " <a href=\"$rechner-sub-$subsys-".
"$zielhtml.txt\">$zielfile</a><br>\n";
}
else
{
print $fh " $zielfile (binär)<br>\n";
}
next;
}
# Inittemplates, Templates
if ($art =~ /^inittemplate$|^template$/)
{
# Ersetzungsmuster anwenden
my $pattern = $templatepattern->Get($bs,$subsys,$file,$zielfile);
my $tempfile = "/tmp/sysconf.template.$$";
TextModify::ErsetzeMuster($file, $tempfile, $pattern, $refvar);
copy($tempfile, "$htmldir$slash$rechner-sub-$subsys-$zielhtml.txt");
print $fh " <a href=\"$rechner-sub-$subsys-".
"$zielhtml.txt\">$zielfile</a><br>\n";
unlink $tempfile;
next;
}
# Links
if ($art =~ /^link$|^installlink$|^initlink$/)
{
print $fh " $file<br>\n";
next;
}
# Shellkommandos
if ($art =~ /^shell$|^installshell$|^initshell$/)
{
my $variablenCode = '';
foreach (keys %$refvar)
{
my $refvarquote = $$refvar{$_}; $refvarquote =~ s/\'/\\\'/g;
$variablenCode .= "my \$$_='$refvarquote';\n";
}
# Variablenersetzung
{
local $FehlerInShellVariablenErsetzung_Kommando = $file;
local $SIG{__WARN__} = \&FehlerInShellVariablenErsetzung;
eval $variablenCode.'$file =~ s/(\$\w+)/$1/eeg;';
}
print $fh " <pre>$file</pre>\n";
next;
}
# Sonst
logdie "Interner Fehler: File-Art ist ungültig!\n";
}
}
print $fh " <br>\n </td>\n";
}
print $fh " </tr>\n";
# Kommando-Dateien auflisten
print $fh " </table>\n <br>\n";
print $fh " <b>Kommandos:</b><br>\n";
my $sub = SubsystemObject->new($bs, $rechner, $subsys);
print $fh $sub->GetHTML($rechner,$htmldir);
print $fh $tail;
close $fh;
}
sub FehlerInShellVariablenErsetzung
{
# Signal-Handler für Fehler in Variablenersetzungen in Shell-Kommandos
# Wichtig: Die globale (local) Variable
# $FehlerInShellVariablenErsetzung_Kommando muß gesetzt sein!
# Parameter: -
# Return: -
my $fehler = shift;
if ($fehler =~ /Use of uninitialized value at/)
{
warning "Fehler in der Variablenersetzung!\n";
warning "Sie haben in folgendem Shell-Kommando eine Variable ".
"verwendet,\n";
warning "die Sie aber nicht definiert haben:\n";
warning "'$FehlerInShellVariablenErsetzung_Kommando'\n";
}
else
{
warning $fehler;
warning "Nicht abgefangener Fehler in Shell-Variablenersetzung!\n";
}
}
sub TesteDateiBesitzer
{
# Parameter: Voller Pfad einer Datei
# Kein Returnwert. (Bei Gefahr sofort Abbruch.)
#
# Es wird überprüft, ob eine Konfigurationsdatei nur für den Menschen
# schreibbar ist, der auch sysconf ausführt.
# Sonst könnte irgendjemand die Konfigurationsfiles verändern und Root
# läßt das dann aufs System los.
#
my $file = shift;
if ( (lstat($file))[2] != 33188 ) # "-rw-r--r--"
{
logdie "Sicherheitslücke: Das File '$file' ist nicht Mode 644!\n"
}
unless (-o $file)
{
logdie "Sicherheitslücke: Sie sind nicht Besitzer von '$file'\n"
}
}
sub RemoteMkdir
{
# Erstellt ein Verzeichnis auf einem anderen Rechner
# Parameter: Rechner, Verzeichnisname
my $rechner = shift;
my $verz = shift;
my $rsh = $beschreibungen->GetRSH($rechner);
debug_rsh "$rsh $rechner mkdir -p $verz\n";
system("$rsh $rechner mkdir -p $verz");
logdie "RemoteShell liefert Fehler!\n" if ($?>>8);
}
sub RemoteCopy
{
# Kopiert ein File auf einen anderen Rechner
# Parameter: Quelle, Rechner, Ziel
my $quelle = shift;
my $rechner = shift;
my $ziel = shift;
# Vor dem Kopieren die Verzeichnisse anlegen
$ziel =~ /(.*)$slashsuch[^$slashsuch]+/;
my $pfad = $1;
RemoteMkdir($rechner, $pfad);
my $rsh = $beschreibungen->GetRSH($rechner);
my $rcp = $beschreibungen->GetRCP($rechner);
debug_rsh "$rcp $quelle $rechner:$ziel\n";
system("$rcp -p $quelle $rechner:$ziel");
logdie "RemoteCopy liefert Fehler!\n" if ($?>>8);
# Syscheck starten, wenn erwünscht
StartSyscheck($rsh, $checkpass, $rechner, $ziel) if $UseSyscheck;
# Owner / Gruppe übertragen
my ($uid,$gid) = (stat($quelle))[4,5];
my $user = getpwuid($uid);
my $gruppe = getgrgid($gid);
debug_rsh "$rsh $rechner chown $user.$gruppe $ziel\n";
system("$rsh $rechner chown $user.$gruppe $ziel");
logdie "RemoteShell liefert Fehler!\n" if ($?>>8);
}
sub StartSyscheck
{
# Parameter: Remote-Shell, Syscheck-Paßwort, Rechner, Dateiname
# Return: -
#
my ($rsh, $checkpass, $rechner, $file) = @_;
my $fh = FileHandle->new();
debug_rsh "|$rsh $rechner syscheck -q -w3 update\n";
open($fh, "|$rsh $rechner syscheck -q -w3 update") || logdie
"Kann '|$rsh $rechner' für Syscheck nicht öffnen!\n";
print $fh "$checkpass\n$file\n";
close $fh;
}
sub RemoteCreateLink
{
# Erstellt einen Link auf einem anderen Rechner
# Parameter: Quelle, Rechner, Ziel
my $quelle = shift;
my $rechner = shift;
my $ziel = shift;
# Vor dem Link anlegen die Verzeichnisse anlegen
$ziel =~ /(.*)$slashsuch[^$slashsuch]+/;
my $pfad = $1;
RemoteMkdir($rechner, $pfad);
my $rsh = $beschreibungen->GetRSH($rechner);
debug_rsh "$rsh $rechner ln -sf $quelle $ziel\n";
system("$rsh $rechner ln -sf $quelle $ziel");
logdie "RemoteShell liefert Fehler!\n" if ($?>>8);
}
sub RemoteShell
{
# Startet einen RemoteShell
# Parameter: Rechner, Kommando
my $rechner = shift;
my $kommando = shift;
my $rsh = $beschreibungen->GetRSH($rechner);
debug_rsh "$rsh $rechner $kommando\n";
system("$rsh $rechner $kommando");
logdie "RemoteShell liefert Fehler!\n" if ($?>>8);
}
sub ReadVariables
{
# Einlesen der Variablen pro Rechner, welche in
# $sysconfroot/variables/rechnername.var
# stehen.
# Parameter: Rechnername
# Return: Hash mit den Variablen
my $rechner = shift;
my %hash = ();
# Gibt es das Verzeichnis für die Variablen?
unless (-d "$sysconfroot${slash}variables")
{
logdie "Das Verzeichnis '$sysconfroot${slash}variables' existiert nicht!\n";
}
my $file = "$sysconfroot${slash}variables$slash$rechner.var";
# Ist das File lesbar?
unless (-r $file)
{
logdie "Das File '$file' existiert nicht oder ist nicht lesbar!\n";
}
TesteDateiBesitzer($file);
my $fh = FileHandle->new();
open($fh, $file) || logdie "Kann '$file' nicht öffnen!\n";
# Die Variablen-Dateien haben den Aufbau:
# variable=wert
# oder
# variable=
# oder
# variable include FILENAME
while(<$fh>)
{
next if /^\#/; # Kommentare
next if /^\s*$/; # Leerzeilen
my ($var,$zuweisung,$wert);
SWITCH:
{
if (/^(\S+)\s*=(.*)/)
{
($var,$zuweisung,$wert) = ($1,'=',(defined $2 ? $2 : ''));
last SWITCH;
}
if (/^(\S+)\s*include\s+(.+)/)
{
($var,$zuweisung,$wert) = ($1,'include',$2);
last SWITCH;
}
logdie "Fehler in den Variablen im File '$file' ",
"in Zeile $.:\n",$_,"\n";
}
if (defined $hash{$var})
{
logdie "Variable '$var' doppelt deklariert im File '$file' ",
"in Zeile $.!\n";
}
# Direkte Zuweisung
if ($zuweisung eq '=')
{
$hash{$var} = $wert;
}
# Zuweisung eines File-Inhalts
else
{
# In $wert steht eine Liste von Dateinamen getrennt durch Leerzeichen
foreach $einzelfile (split(/\s+/,$wert))
{
my $varfile = "$sysconfroot${slash}variables$slash$einzelfile";
logdie "File '$einzelfile' kann in '$sysconfroot${slash}variables' ".
"nicht gelesen/gefunden werden!\n" unless -r $varfile;
my $fh = FileHandle->new();
open($fh, $varfile) || logdie "Fehler beim Öffnen von '$varfile'!\n";
{
local $/ = undef; # In einem Stück einlesen
$hash{$var} .= <$fh>;
}
close $fh;
}
}
}
return %hash;
}
sub CheckAndAddDependencies
{
# Überprüfung der Abhängigkeiten der Subsysteme und Hinzufügen fehlender
# Subsysteme
# Parameter: Betriebssystem, Liste der Subsysteme
# Return: Ergänzte Liste der Subsysteme in richtiger Reihenfolge
my $betriebssystem = shift;
my @subsysteme = @_;
ReadDependencies($betriebssystem);
my %dep = ( defined $dependencies->Get($betriebssystem) ?
$dependencies->Get($betriebssystem) : () );
my @alle_subs = ();
foreach (@subsysteme)
{
print "Gewünschtes Subsys: $_\n";
unless (defined $dep{$_})
{
push @alle_subs, $_;
next;
}
push @alle_subs, @{$dep{$_}}, $_;
}
@alle_subs = KompaktiereSubsystemListe(@alle_subs);
return @alle_subs;
}
sub ReadDependencies
{
# File mit den Abhängigkeiten der Subsysteme einlesen
# Es wird das globale Dependencies-Objekt gesetzt
# Parameter: Betriebssystem
# Return: -
my $depfilename = 'dependencies.sc';
my $bs = shift;
# Wenn für dieses Betriebssystem die Abhängigkeiten schon eingelesen sind
# dann nichts tun.
return if defined ($dependencies->Get($bs));
# Gibt es das Verzeichnis für das Betriebssystem?
unless (-d "$sysconfroot$slash$bs")
{
logdie "Das Verzeichnis '$sysconfroot$slash$bs' existiert nicht!\n";
}
my $depfile = "$sysconfroot$slash$bs$slash$depfilename";
unless (-r $depfile)
{
logdie "Das File '$depfile' existiert nicht oder ist nicht lesbar!\n";
}
TesteDateiBesitzer($depfile);
my %dep = ();
###
### Abhängigkeiten einlesen
###
my $fh = FileHandle->new();
open($fh, $depfile) || logdie "Kann '$depfile' nicht öffnen!\n";
# Die Datei "dependencies.sc" hat den Aufbau:
# S : D1 D2 D3 ...
# Bedeutung: Subsystem "S" hängt von Subsystemen "D1", "D2", "D3", ... ab.
while(<$fh>)
{
next if /^\#/; # Kommentare
next if /^\s*$/; # Leerzeilen
unless (/^(\S+)\s*:\s*(.+)/)
{
logdie "Fehler in den Abhängikeiten in Zeile $.:\n",$_,"\n";
}
my ($sub,$deps) = ($1,$2);
if (defined $dep{$sub})
{
logdie "Abhängigkeiten für '$sub' mehrfach definiert in Zeile $.!\n";
}
$dep{$sub} = [ split(/\s+/,$deps) ];
}
###
### Rekursive Abhängigkeiten auflösen
###
my @deps = ();
my @result_deps = ();
NOCHMAL:
my $neuer_durchlauf_notwendig = $FALSE;
my $subsys;
foreach $subsys (keys %dep)
{
@result_deps = ();
@deps = @{$dep{$subsys}};
my $d;
foreach $d (@deps)
{
if ($d eq $subsys)
{
logdie "Zyklische Abhängigkeit für Subsystem '$d' in '$depfile'!\n";
}
if (defined $dep{$d})
{
# Abhängigkeit auflösen
push @result_deps, @{$dep{$d}};
$neuer_durchlauf_notwendig = $TRUE;
}
else
{
# Subsystem direkt übernhemen
push @result_deps, $d;
}
}
$dep{$subsys} = [ @result_deps ];
}
goto NOCHMAL if $neuer_durchlauf_notwendig;
###
### Subsysteme "zusammenschnurren" lassen, z.B.:
###
# 0 7 8 5 4 8 9 5 4 8 4
# wird zu
# 0 7 8 5 4 9
foreach $subsys (keys %dep)
{
@result_deps = KompaktiereSubsystemListe( @{$dep{$subsys}} );
$dep{$subsys} = [ @result_deps ];
}
debug "-----\n";
debug "Abhängigkeiten:\n";
foreach (keys %dep) { debug "$_ -> @{$dep{$_}}\n"; }
close $fh;
$dependencies->Set($bs,%dep);
}
sub ReadFilesSC
{
# File "files.sc" mit den Files für die Subsysteme einlesen
# Es wird das globale Files-Objekt gesetzt
# Parameter: Betriebssystem
# Return: -
my $files_sc = 'files.sc';
my $bs = shift;
# Zuordung von Schlüsselwörtern zu Objekt-Parametern
my %artkey = (
inst => 'install',
I => 'install',
initf => 'init',
F => 'init',
initt => 'inittemplate',
T => 'inittemplate',
f => 'file',
t => 'template',
# Links
L => 'link',
instL => 'installlink',
initL => 'initlink',
# Shell-Kommandos
S => 'shell',
instS => 'installshell',
initS => 'initshell',
);
# Wenn für dieses Betriebssystem die "files.sc" schon eingelesen sind
# dann nichts tun.
return if $files->BereitsEingelesen($bs);
# Gibt es das Verzeichnis für das Betriebssystem?
unless (-d "$sysconfroot$slash$bs")
{
logdie "Das Verzeichnis '$sysconfroot$slash$bs' existiert nicht!\n";
}
my $file = "$sysconfroot$slash$bs$slash$files_sc";
unless (-r $file)
{
logdie "Das File '$file' existiert nicht oder ist nicht lesbar!\n";
}
TesteDateiBesitzer($file);
###
### "files.sc" einlesen
###
my $fh = FileHandle->new();
open($fh, $file) || logdie "Kann '$file' nicht öffnen!\n";
# Die Datei "files.sc" hat den Aufbau:
# [subsys]
# <fileentry>
# ...
# [subsys]: Beginn Beschreibung Subsystem namens "subsys"
# <fileentry>: <type> <filename> <Zielfilename>
# <patternblock>
#
# Weiteres siehe Doku.
my $subsys = '';
my ($art, $quelle, $ziel);
# Den Subsystem-Abschnitt finden
while(<$fh>)
{
ABSCHNITT:
next if /^\#/; # Kommentare
next if /^\s*$/; # Leerzeilen
if (! /\[(.+)\]/)
{
chomp;
logdie "'[subsystem]' statt '$_' in '$file' Zeile $. erwartet!\n";
}
else
{
$subsys = $1;
last;
}
}
# Jetzt im Subsystem-Abschnitt weiterlesen
while(<$fh>)
{
next if /^\#/; # Kommentare
next if /^\s*$/; # Leerzeilen
goto ABSCHNITT if /^\[/; # Nächster Subsystem-Abschnitt
/(\S+)\s+(\S+)\s*(\S*)/;
($art,$quelle,$ziel) = ($1,$2,($3 ne '' ? $3 : $2));
# File-Art testen
unless (defined $artkey{$art})
{
logdie "Die File-Art '$art' in '$file' Zeile $. ist ungültig!\n";
}
# Templates
if ($artkey{$art} =~ /template/)
{
# Kein absoluter Pfad, dann ist das File im Unterverzeichnis filedir.sc/
unless ($quelle =~ /^\//)
{
$quelle = $sysconfroot.$slash.$bs.$slash.'filedir.sc'.$slash.$quelle;
}
# Kein absoluter Pfad beim Ziel, also "/" ergänzen
$ziel = $slash.$ziel unless ($ziel =~ /^\//);
# Quellfile lesbar?
unless (-r $quelle)
{
warning "Kann File '$quelle' aus '$file' Zeile $. nicht lesen!\n";
next;
}
$files->Set($bs, $subsys, $artkey{$art}, $quelle, $ziel);
# Den Patternblock einlesen
my $pattern = <$fh>;
unless ($pattern =~ /^beginpattern$/)
{ logdie "'beginpattern' erwartet in '$file' in Zeile $.!\n" }
my $ganzes_muster = '';
while( defined ($pattern = <$fh>) )
{
last if $pattern =~ /^endpattern$/;
if ($pattern =~ /^\[/)
{ logdie "Kein schließendes 'endpattern' in '$file' in Zeile $.!\n" }
$ganzes_muster .= $pattern;
}
$templatepattern->Set($bs,$subsys,$quelle,$ziel,$ganzes_muster);
next;
}
# Prüfung für normale Files
unless ($artkey{$art} =~ /link|shell/)
{
# Kein absoluter Pfad, dann ist das File im Unterverzeichnis filedir.sc/
unless ($quelle =~ /^\//)
{
$quelle = $sysconfroot.$slash.$bs.$slash.'filedir.sc'.$slash.$quelle;
}
# Kein absoluter Pfad beim Ziel, also "/" ergänzen
$ziel = $slash.$ziel unless ($ziel =~ /^\//);
# Quellfile lesbar?
unless (-r $quelle)
{
warning "Kann File '$quelle' aus '$file' Zeile $. nicht lesen!\n";
next;
}
$files->Set($bs, $subsys, $artkey{$art}, $quelle, $ziel);
}
# Links prüfen
if ($artkey{$art} =~ /link/)
{
# Link-Quelle ohne absoulten Pfad
unless ($quelle =~ /^\//)
{
logdie "Link-Quelle nicht absolut angegeben in '$file' Zeile $.!\n";
}
# Link-Ziel ohne absoulten Pfad
unless ($ziel =~ /^\//)
{
logdie "Link-Ziel nicht absolut angegeben in '$file' Zeile $.!\n";
}
$files->Set($bs, $subsys, $artkey{$art}, $quelle, $ziel);
}
# Shellkommandos
if ($artkey{$art} =~ /shell/)
{
/(\S+)\s+(.*)/;
($art,$kommando) = ($1,$2);
$files->Set($bs, $subsys, $artkey{$art}, $kommando, '');
}
}
close $fh;
}
sub KompaktiereSubsystemListe
{
###
### Subsysteme "zusammenschnurren" lassen, z.B.:
###
# 0 7 8 5 4 8 9 5 4 8 4
# wird zu
# 0 7 8 5 4 9
#
# Parameter: Liste von Subsystemen
# Return: Liste von Subsystemen
my @subsys = @_;
my @result_deps = ();
my %schon_enthalten = ();
my $s;
foreach $s (@subsys)
{
next if $schon_enthalten{$s};
push @result_deps, $s;
$schon_enthalten{$s} = $TRUE;
}
return @result_deps;
}
sub CheckSubsystems
{
# Überprüfung der Subsystem-Angaben
# - alle Subsysteme? (ALL)
# - darf dieser Rechner diese Subsysteme bekommen?
# - dann stehen in @subsys die gewünschten Subsysteme
#
# Parameter: Rechnername, Beschreibungen-Objekt, Parameter-Liste-Objekt
# Return: Liste der Subsysteme
#
my ($rechner,$beschreibung,$param) = @_;
my @wanted = $param->Subsysteme;
my @erlaubt = $beschreibung->Subsysteme($rechner);
debug "-----\n";
debug "Rechner: $rechner\n";
debug "Subsysteme erlaubt: ",join(" ",@erlaubt),"\n";
debug "Subsysteme gewünscht: ",join(" ",@wanted),"\n";
# Alle Subsysteme
if ( ($#wanted == 0) && ($wanted[0] eq 'ALL') )
{
return @erlaubt;
}
my @result = ();
my %ist_erlaubt = ();
foreach (@erlaubt) { $ist_erlaubt{$_} = $TRUE; }
my $subsys;
foreach $subsys (@wanted)
{
if ($ist_erlaubt{$subsys})
{
push @result, $subsys;
}
else
{
# Wenn es eine Klassenbezeichnung ist, dann expandieren
if (defined $klassendef{$subsys})
{
foreach (@{$klassendef{$subsys}})
{
# Aber nur, wenn es erlaubt ist
if ($ist_erlaubt{$_}) { push @result, $_; }
else { warning "Subsystem '$_' aus Klasse '$subsys' ist für ",
"'$rechner' nicht erlaubt!\n"; }
}
}
else
{
warning "Subsystem '$subsys' ist für '$rechner' nicht erlaubt!\n";
}
}
}
return @result;
}
sub ReadConfigFile
{
# Setzen von globalen Parametern aus dem Konfigurationsfile
# Parameter: -
# Return: -
#
my $file = './sysconfrc';
$file = "$ENV{HOME}/.sysconfrc" unless -r $file;
$file = '/etc/sysconfrc' unless -r $file;
logdie "Kann weder './sysconfrc' noch '$ENV{HOME}/.sysconfrc' noch ".
"'/etc/sysconfrc' lesen!\n" unless -r $file;
debug "Verwende Konfigurationsfile '$file'\n";
TesteDateiBesitzer($file);
my $fh = FileHandle->new();
open($fh, $file);
while(<$fh>)
{
next if /^\#/; # Kommentare überspringen
$sysconfroot = $1 if /^SYSCONF_ROOT\s*=\s*(.+)/i;
$UseSyscheck = $TRUE if /^USE_SYSCHECK\s*=\s*TRUE/i;
$htmldir = $1 if /^HTMLDIR\s*=\s*(.+)/i;
next;
}
close $fh;
logdie "Kein 'SYSCONF_ROOT' in '$file' definiert!\n" if $sysconfroot eq '';
logdie "Kein 'HTMLDIR' in '$file' definiert!\n" if $htmldir eq '';
$sysconfroot = KillSlashAtEnd($sysconfroot);
$htmldir = KillSlashAtEnd($htmldir );
}
sub RechnerKlassenEinlesen
{
# Es wird die Datei "classes.sc" eingelesen
# Parameter: -
# Return: Hash: Klassenname -> Liste der Subsysteme
my $Klassen_File = FileHandle->new();
open($Klassen_File, "$sysconfroot${slash}classes.sc") ||
logdie "Kann $sysconfroot${slash}classes.sc nicht öffnen!\n";
TesteDateiBesitzer("$sysconfroot${slash}classes.sc");
my %result = ();
my ($klassendef,$subsysteme);
{
# Blockweise einlesen (Leerzeilen trennen)
local $/ = '';
while (<$Klassen_File>)
{
# Kommentare entfernen (alle Zeilen, die mit '#' beginnen.)
s/^(\#[^\n]*\n)*//g;
# Leerzeilen überspringen
s/^\s*\n//g;
next if $_ eq '';
unless (
m/
CLASSDEF \s+(\S+)\s*.*?
SUBSYSTEMS \s+([^\n]+)\s*.*?
/sx
)
{
logdie "Fehler in Klassendefinition:\n",$_,"\n";
}
($klassendef,$subsysteme) = ($1,$2);
$result{$klassendef} = [ split(/\s+/,$subsysteme) ];
}
close $Klassen_File;
}
return %result;
}
sub RechnerBeschreibungenEinlesen
{
# Es wird die Datei "hosts.sc" eingelesen
# Parameter: -
# Return: RechnerBeschreibung-Objekt
my $beschreibungen = RechnerBeschreibung::new();
my $Besch_File = FileHandle->new();
open($Besch_File, "$sysconfroot${slash}hosts.sc") ||
logdie "Kann $sysconfroot${slash}hosts.sc nicht öffnen!\n";
TesteDateiBesitzer("$sysconfroot${slash}hosts.sc");
$/ = ''; # Blockweise einlesen (Leerzeilen trennen)
while (<$Besch_File>)
{
# Kommentare entfernen (alle Zeilen, die mit '#' beginnen.)
s/^(\#[^\n]*\n)*//g;
# Leerzeilen überspringen
s/^\s*\n//g;
next if $_ eq '';
unless(
m/
HOST \s+(\S+)\s*.*?
OS \s+(\S+)\s*.*?
CLASSES \s+([^\n]*)\s*.*?
SUBSYSTEMS \s+([^\n]*)\s*.*?
/sx
)
{
logdie "Fehler in Rechnerdefinition:\n",$_,"\n";
}
my ($rechner,$betriebssystem,$klassen,$subsysteme) = ($1,$2,$3,$4);
$klassen = '' unless defined $klassen;
$subsysteme = '' unless defined $subsysteme;
my @subsys = split(/\s+/,$subsysteme);
# Klassen expandieren
my $klasse;
foreach $klasse (split(/\s+/,$klassen))
{
unless (defined $klassendef{$klasse})
{ logdie "Klasse '$klasse' gibt es nicht!\n" }
push @subsys, @{$klassendef{$klasse}};
}
$beschreibungen->SetBetriebssystem($rechner,$betriebssystem);
$beschreibungen->SetSubsysteme ($rechner,@subsys);
}
close $Besch_File;
$/ = "\n";
return $beschreibungen;
}
sub ParameterEinlesen
{
# Parameter: Kommandozeile des Programms als Liste
# Return: ParameterListe-Objekt
my ($aktion, $subsysteme, $rechner) = @_;
logdie "Falsche Anzahl Parameter!\n" unless defined $rechner;
unless ($aktion =~ /^init$|^update$|^remove$|^start$|^stop$|^documentation$/)
{ logdie "Ungültige Aktion '$aktion'!\n" }
my @subsysteme = split(/,/,$subsysteme);
my @rechner = split(/,/,$rechner);
@rechner = ExpandiereALLRechner() if $rechner[0] eq 'ALL';
my $result = ParameterListe::new();
$result->SetAktion($aktion);
$result->SetSubsysteme(@subsysteme);
$result->SetRechner(@rechner);
return $result;
}
sub ExpandiereALLRechner
{
# Expandiert "ALL" zu einer Lister aller Rechner
# Parameter: -
# Return; -
#
my @rechner = ();
my $fh = FileHandle->new();
open($fh, "$sysconfroot${slash}hosts.sc") ||
logdie "Kann $sysconfroot${slash}hosts.sc nicht öffnen!\n";
TesteDateiBesitzer("$sysconfroot${slash}hosts.sc");
while(<$fh>)
{
next unless /HOST\s+(\S+)\s*.*?/;
push @rechner, $1;
}
close $fh;
return @rechner;
}
sub TesteRemoteShell
{
# Parameter: Rechnername, Debug
# Wenn "Debug"==TRUE, dann werden Debuginformationen ausgegeben.
# Return: 'ssh', 'rsh', 'ssh-passwd', 'none'
# Es wird festgestellt, welche Verbindungsmöglichkeit zu einem Rechner
# besteht. Dazu dieses der Reihe nach versucht:
# - ssh
# - rsh
# - ssh mit Paßwortabfrage
#
my $host = shift || die "TesteVerbindung() ohne Parameter aufgrufen!\n";
my $debug = shift || $FALSE;
my $null = '2>/dev/null >/dev/null';
# ssh
print "Teste ssh...\n" if $debug;
system("ssh -o 'FallBackToRsh no' -o 'BatchMode yes' $host echo TEST $null");
if ( ($?>>8) == 0 )
{
return 'ssh';
}
# rsh
print "Teste rsh...\n" if $debug;
system("rsh $host echo TEST $null");
if ( ($?>>8) == 0 )
{
return 'rsh';
}
# ssh mit Paßwortabfrage
print "Teste ssh mit Paßwortabfrage...\n" if $debug;
system("ssh -o 'FallBackToRsh no' $host echo TEST");
if ( ($?>>8) == 0 )
{
return 'ssh-passwd';
}
return 'none';
}
sub TesteRemoteCopy
{
# Parameter: Rechnername, Debug
# Wenn "Debug"==TRUE, dann werden Debuginformationen ausgegeben.
# Return: 'scp ...', 'rcp', 'rsync ...', 'none'
# (Bei scp und rsync wird gleich eine passende Kommandozeile gebildet.)
# Es wird festgestellt, welche Kopiermöglichkeit zu einem Rechner
# besteht. Dazu dieses der Reihe nach versucht:
# - rsync mit ssh
# - rsync mit rsh
# - scp
# - rcp
#
$host = shift || die "TesteVerbindung() ohne Parameter aufgrufen!\n";
my $debug = shift || $FALSE;
my $null = '2>/dev/null >/dev/null';
my $verzeichnisse = ' $HOME/bin/rsync /usr/local/bin/rsync /usr/bin/rsync '.
'/bin/rsync /usr/local/sbin/rsync /usr/sbin/rsync /root/bin/rsync ';
# Gibt es lokalen rsync?
if (which('rsync'))
{
# Funktioniert die ssh?
if ( TesteRemoteShell($host) =~ /^ssh/ )
{
# rsync mit ssh
print "Teste rsync mit ssh...\n" if $debug;
system("rsync -z -e ssh --dry-run $0 $host:/tmp $null");
if ( ($?>>8) == 0 )
{
return 'rsync -z -e ssh';
}
# Erfolglos, also erst einmal herausfinden, wo der rsync sich auf dem
# remote-Rechner befindet
print "Versuche rsync zu finden...\n" if $debug;
my @list = `ssh $host 'ls $verzeichnisse 2>/dev/null'`;
print @list if $debug;
if (defined $list[0])
{
my $pfad = $list[0];
chomp $pfad;
# rsync mit ssh und Pfad
print "Teste rsync mit ssh und Pfad '$pfad'...\n" if $debug;
system("rsync -z -e ssh --rsync-path=$pfad --dry-run $0 ".
"$host:/tmp $null");
if ( ($?>>8) == 0 )
{
return "rsync -z -e ssh --rsync-path=$pfad";
}
}
}
else
{
# rsync mit rsh
print "Teste rsync mit rsh...\n" if $debug;
system("rsync -z --dry-run $0 $host:/tmp $null");
if ( ($?>>8) == 0 )
{
return 'rsync -z';
}
# Erfolglos, also erst einmal herausfinden, wo der rsync sich auf dem
# remote-Rechner befindet
print "Versuche rsync zu finden...\n" if $debug;
my @list = `rsh $host 'ls $verzeichnisse 2>/dev/null'`;
print @list if $debug;
if (defined $list[0])
{
my $pfad = $list[0];
chomp $pfad;
# rsync mit rsh und Pfad
print "Teste rsync mit rsh und Pfad '$pfad'...\n" if $debug;
system("rsync -z --rsync-path=$pfad --dry-run $0 $host:/tmp $null");
if ( ($?>>8) == 0 )
{
return "rsync -z --rsync-path=$pfad";
}
}
}
}
else
{
# Ohne rsync
print "Kann lokal keinen rsync finden!\n" if $debug;
# Funktioniert die ssh?
print "Teste scp ...\n" if $debug;
if ( TesteRemoteShell($host) =~ /^ssh/ )
{
return "scp -C -o 'CompressionLevel 9'";
}
print "Teste rcp ...\n" if $debug;
if ( TesteRemoteShell($host) eq 'rsh' )
{
return 'rcp';
}
}
return 'none';
}
######################################################################
### Debug, Logging, Exit, ...
######################################################################
sub myexit
{
# Diese Funktion macht einen normalen exit() mit dem übergebenen
# Exitcode
# und erledigt vorher noch Aufräum-Arbeiten, wie LOG-file schließen, ...
my $error = defined $_[0] ? shift : $NormalExitCode;
logprint("$appname PID $$ Ende um ",date,"\n");
close(LOG);
exit $error;
}
sub logprint
{
# Schreibt einen Text ins LOG-file
print LOG @_;
}
sub debug_rsh
{
# Es werden Debug-Informationen über RSH-Aufrufe erzeugt
logprint("RSH: ",@_) if ($logLevel >= 4);
}
sub debug
{
# Es werden Debug-Informationen erzeugt
logprint("DEBUG: ",@_) if ($logLevel >= 3);
}
sub warning
{
# Falls Warnings eingeschaltet sind, dann werden Informationen erzeugt
logprint("WARN: ",@_) if ($logLevel >= 2);
}
sub error
{
# Falls Warnings eingeschaltet sind, dann werden Informationen erzeugt
logprint("ERROR: ",@_) if ($logLevel >= 1);
}
sub loginit
{
# Schreibt einen kleinen Header ins LOG-file
my $login = (getpwuid($<))[0] || 'unknown';
logprint("\n---\n\n$appname $version PID $$ mit Perl $]\nStart um ",date,
" durch ",$login,"\n");
}
sub logdie
{
# Schreibt einen Text ins LOG-file und stirbt dann
print "FATAL: ",@_;
logprint("FATAL: ",@_);
myexit($ErrorExitCode);
}
sub catch_signal
{
my $signame = shift;
logdie "Ende von $appname wegen Signal SIG$signame.\n";
}
sub catch_warning
{
# Abfangen von Laufzeit-Warnungen
my $warnung = shift;
warning "INTERNAL: $warnung (Interne Warnungen deuten auf einen ",
"möglichen internen Programm-Fehler\n",
" hin oder auf eine fehlerhafte Eingabe, die nicht abgefangen ",
"wurde!)\n";
print $warnung if ($logLevel < 2);
}
######################################################################
### Kopf und Hilfe
######################################################################
sub Kopf
{
my $head = "$appname $version - von Stephan Löscher";
return "\n$head\n" . '~' x length($head) . "\n";
}
sub Hilfe
{
printumlautepaged
Kopf().
"Syntax: sysconf optionen action subsystem machine
Eine Aktion ist in natürlicher Sprache formuliert von dem Aufbau:
'Führe Aktion X mit Subsysteme Y auf Rechner Z aus.'
Aktionen werden per Kommandozeile übergeben, wobei:
action := init | update | remove | start | stop | documentation
subsystem := <subsystem-name>{,<subsystem-name>}* | ALL
machine := <machine-name>{,<machine-name>}* | ALL
'ALL' steht dabei für 'alle Subsysteme' oder 'alle Rechner'.
Erklärungen:
init: Subsystem erstmalig installieren (alle Files einschließlich
'initfiles')
update: Subsystem updaten (Files aus 'initfiles' nicht kopieren!)
Wenn es das Subsystem nicht gibt, dann 'init' vorher durchführen
remove: Subsystem entfernen.
start: Subsystem starten.
stop: Subsystem stoppen.
documentation : Dokumentation erzeugen
Optionen:
-wX: mit X=0-4 gibt den LOG-Level an.
0: Nur fatale Fehler
1: zusätzlich alle Fehler (Genaue Fehlerbeschreibungen)
2: zusätzlich alle Warnungen (ganz informativ)
3: zusätzlich alle Debug-Informationen (ausführlicher Status, etc.)
4: Alle Remote-Shell-Aufrufe werden mitprotokolliert
Beispiele:
'Führe einen Update von Sendmail auf tgx045 durch':
sysconf update sendmail tgx045
'Führe einen Update von allen Subsystemen auf tgx002 durch':
sysconf update ALL tgx002
'Verteile alle Subsysteme auf alle Rechner':
sysconf init ALL ALL
'Führe einen Update von Sendmail und Syslog auf tgx045,tgx200,tgx200 durch':
sysconf update sendmail,syslog tgx045,tgx200,tgx200
";
logprint "Es wird nur Hilfe ausgegeben.\n";
myexit;
}
sub POD_Ausgabe
{
# Erstellt Dokumentation aus POD im aktuellen Verzeichnis
# Parameter: "man" oder "html" oder "latex"
#
$art = shift;
if ($art eq 'man')
{
which('pod2man') || die "Leider kein 'pod2man' verfügbar!\n";
which('nroff') || die "Leider kein 'nroff' verfügbar!\n";
system("pod2man $0 | nroff -man > \L$appname\E.man");
}
if ($art eq 'html')
{
which('pod2html') || die "Leider kein 'pod2html' verfügbar!\n";
system("pod2html $0 > \L$appname\E.html");
# Nachbesserung: (FIXME)
system('perl -i -pe \'s/<(.?)EM>/<${1}EM>/g\' '."\L$appname\E.html");
}
if ($art eq 'latex')
{
which('pod2latex') || die "Leider kein 'pod2latex' verfügbar!\n";
system("pod2latex \L$appname\E");
open(FH,"\L$appname\E.tex");
@tex = <FH>;
close FH;
unshift @tex, '\documentclass[9pt]{article}\usepackage{german,a4,t1enc}'.
'\usepackage[latin1]{inputenc}\begin{document}\def\C++{{\rm C'.
'\kern-.05em\raise.3ex\hbox{\footnotesize ++}}}\def\underscore'.
'{\leavevmode\kern.04em\vbox{\hrule width 0.4em height 0.3pt}}'.
'\setlength{\parindent}{0pt}';
push @tex, '\end{document}';
grep(s/\"/\'\'/g, @tex); # Anführungszeichen ersetzen
open(FH,">\L$appname\E.tex");
print FH @tex;
close FH;
}
}
######################################################################
### ParameterListe-Objekt
######################################################################
package ParameterListe;
sub new
{
my $daten = {
aktion => '',
subssysteme => [],
rechner => [],
};
bless $daten, 'ParameterListe';
return $daten;
}
sub Aktion
{
my $objekt = shift;
return $objekt->{aktion};
}
sub SetAktion
{
my $objekt = shift;
$objekt->{aktion} = shift;
}
sub Subsysteme
{
my $objekt = shift;
return @{$objekt->{subsysteme}};
}
sub SetSubsysteme
{
my $objekt = shift;
$objekt->{subsysteme} = [ @_ ];
}
sub Rechner
{
my $objekt = shift;
return @{$objekt->{rechner}};
}
sub SetRechner
{
my $objekt = shift;
$objekt->{rechner} = [ @_ ];
}
######################################################################
### RechnerBeschreibung-Objekt
######################################################################
package RechnerBeschreibung;
use Carp;
sub new
{
my $daten = {
# Das sieht so aus:
# rechnername => {
# BS => Betriebsystem,
# SUB => Liste von Subsystemen,
# RSH => Remoteshell,
# RCP => Remotecopy,
# }
};
bless $daten, 'RechnerBeschreibung';
return $daten;
}
sub Betriebssystem
{
my $objekt = shift;
my $rechner = shift;
unless (defined $rechner)
{
carp("RechnerBeschreibung::Betriebssystem() ohne Parameter ".
"aufgerufen!\n");
main::myexit($main::ErrorExitCode);
}
return $objekt->{$rechner}->{BS};
}
sub SetBetriebssystem
{
my $objekt = shift;
my $rechner = shift;
unless (defined $rechner)
{
carp("RechnerBeschreibung::SetBetriebssystem() ohne Parameter ".
"aufgerufen!\n");
main::myexit($main::ErrorExitCode);
}
$objekt->{$rechner}->{BS} = shift;
}
sub Subsysteme
{
my $objekt = shift;
my $rechner = shift;
# Ohne Parameter aufgerufen?
unless (defined $rechner)
{
carp("RechnerBeschreibung::Subsysteme() ohne Parameter ".
"aufgerufen!\n");
main::myexit($main::ErrorExitCode);
}
return @{$objekt->{$rechner}->{SUB}};
}
sub SetSubsysteme
{
my $objekt = shift;
my $rechner = shift;
unless (defined $rechner)
{
carp("RechnerBeschreibung::SetSubsysteme() ohne Parameter ".
"aufgerufen!\n");
main::myexit($main::ErrorExitCode);
}
$objekt->{$rechner}->{SUB} = [ @_ ];
}
sub GetRSH
{
my $objekt = shift;
my $rechner = shift;
my $temp;
unless (defined $rechner)
{
carp("RechnerBeschreibung::GetRSH() ohne Parameter aufgerufen!\n");
main::myexit($main::ErrorExitCode);
}
# Wenn die RSH schon bekannt ist, dann gleich zurückgeben
if (defined $objekt->{$rechner}->{RSH})
{
main::debug "Cached RSH: ".$objekt->{$rechner}->{RSH}."\n";
return $objekt->{$rechner}->{RSH};
}
# ansonsten erst ermitteln
else
{
$temp = main::TesteRemoteShell($rechner);
unless ($temp =~ /^ssh$|^rsh$/)
{
carp("RechnerBeschreibung::GetRSH(): Kann weder mit rsh noch mit ssh ".
"auf den Rechner '$rechner' ohne Paßwort zugreifen!\n");
main::myexit($main::ErrorExitCode);
}
main::debug "Ermittelte RSH: ".$temp."\n";
$objekt->{$rechner}->{RSH} = $temp;
return $temp;
}
}
sub GetRCP
{
my $objekt = shift;
my $rechner = shift;
my $temp;
unless (defined $rechner)
{
carp("RechnerBeschreibung::GetRCP() ohne Parameter aufgerufen!\n");
main::myexit($main::ErrorExitCode);
}
# Wenn RCP schon bekannt ist, dann gleich zurückgeben
if (defined $objekt->{$rechner}->{RCP})
{
main::debug "Cached RCP: ".$objekt->{$rechner}->{RCP}."\n";
return $objekt->{$rechner}->{RCP};
}
# ansonsten erst ermitteln
else
{
$temp = main::TesteRemoteCopy($rechner);
if ($temp eq 'none')
{
carp("RechnerBeschreibung::GetRCP(): Kann weder mit rcp, scp ".
"noch rsync auf den Rechner '$rechner' ohne Paßwort zugreifen!\n");
main::myexit($main::ErrorExitCode);
}
main::debug "Ermitteltes RCP: ".$temp."\n";
$objekt->{$rechner}->{RCP} = $temp;
return $temp;
}
}
######################################################################
### Dependencies-Objekt
######################################################################
package Dependencies;
use Carp;
sub new
{
my $daten = {
# Das sieht so aus:
# betriebssystem => {
# SUB => Liste von Subsystemen,
# }
};
bless $daten, 'Dependencies';
return $daten;
}
sub Get
{
my $objekt = shift;
my $betriebssystem = shift;
unless (defined $betriebssystem)
{
carp("Dependencies::Get() ohne Parameter aufgerufen!\n");
main::myexit($main::ErrorExitCode);
}
# Das return ist nur so umständlich, weil Perl sonst den undef-Wert
# kritisiert. Kurz: return %{$objekt->{$betriebssystem}};
return (
defined %{$objekt->{$betriebssystem}}
? %{$objekt->{$betriebssystem}}
: undef
);
}
sub Set
{
my $objekt = shift;
my $betriebssystem = shift;
my %hash = @_;
unless (defined $betriebssystem)
{
carp("Dependencies::Set() ohne Parameter aufgerufen!\n");
main::myexit($main::ErrorExitCode);
}
$objekt->{$betriebssystem} = { %hash };
}
######################################################################
### Files-Objekt
######################################################################
package Files;
use Carp;
sub new
{
my $daten = {
# Das sieht so aus:
# betriebssystem => {
# SUB => {
# install => Hash der Installfiles
# init => Hash der Initfiles
# inittemplate => Hash der Init-Templates
# file => Hash der Files
# template => Hash der Templates
# }
# }
};
# in den Hashes sind Listen der Zielfiles!
bless $daten, 'Files';
return $daten;
}
sub BereitsEingelesen
{
# Parameter: Betriebssystem
# Return: TRUE oder FALSE, je nachdem, ob für dieses Betriebssystem schon
# die Datei files.sc eingelesen wurde
#
my ($objekt, $betriebssystem) = @_;
unless (defined $betriebssystem)
{
carp("Files::BereitsEingelesen() ohne Parameter aufgerufen!\n");
main::myexit($main::ErrorExitCode);
}
return (
defined %{$objekt->{$betriebssystem}}
? $main::TRUE
: $main::FALSE
);
}
sub Get
{
# Parameter: (Betriebssystem, Subsystem, Art der Liste)
# Die Art der Liste ist "install", "init", inittemplate", "file" oder
# "template".
# Return: Referenz auf Hash
# Der Hash hat diesen Aufbau:
# quellfile -> zielfile
# Beispiel:
# "var/lib/news/expire.ctl.INN" -> "var/lib/news/expire.ctl"
#
# Typischer Zugriff:
# my %x = %{$files->Get($bs,"login","install")};
# foreach (keys %x)
# { print "Eintrag: $_ -> $x{$_}\n"; }
my ($objekt, $betriebssystem, $subsystem, $art) = @_;
unless (defined $art)
{
carp("Files::Get() mit zuwenig Parameter aufgerufen!\n");
main::myexit($main::ErrorExitCode);
}
unless ( IstArtGueltig($objekt,$art) )
{
carp("Files::Get() mit falschem Art-Parameter aufgerufen!\n");
main::myexit($main::ErrorExitCode);
}
return ( \%{$objekt->{$betriebssystem}->{$subsystem}->{$art}} );
}
sub Set
{
# Parameter: (Betriebssystem, Subsystem, Art des Files, Quellfile, Zielfile)
# Die Art des Files ist "install", "init", inittemplate", "file" oder
# "template".
# Return: -
#
my ($objekt, $betriebssystem, $subsystem, $art, $quelle, $ziel) = @_;
unless (defined $ziel)
{
carp("Files::Set() mit zuwenig Parameter aufgerufen!\n");
main::myexit($main::ErrorExitCode);
}
unless ( IstArtGueltig($objekt,$art) )
{
carp("Files::Set() mit falschem Art-Parameter aufgerufen!\n");
main::myexit($main::ErrorExitCode);
}
# Der Zielfile-Liste hinzufügen
push @{$objekt->{$betriebssystem}->{$subsystem}->{$art}->{$quelle}}, $ziel;
}
sub IstArtGueltig
{
shift;
my $art = shift;
return ( $art =~ /^install$|^init$|^inittemplate$|^file$|^template$|
^link$|^installlink$|^initlink$|^shell$|^installshell$|
^initshell$/sx );
}
######################################################################
### TemplatePattern-Objekt
######################################################################
package TemplatePattern;
use Carp;
sub new
{
my $daten = {
# Das sieht so aus:
# betriebssystem => {
# SUB => {
# quell => {
# ziel => pattern
# }
# }
# }
};
bless $daten, 'TemplatePattern';
return $daten;
}
sub Get
{
# Parameter: (Betriebssystem, Subsystem, Quellfile, Zielfile)
# Return: Pattern
my ($objekt, $betriebssystem, $subsystem, $quelle, $ziel) = @_;
unless (defined $ziel)
{
carp("TemplatePattern::Get() mit zuwenig Parameter aufgerufen!\n");
main::myexit($main::ErrorExitCode);
}
unless (defined $objekt->{$betriebssystem}->{$subsystem}->{$quelle}->{$ziel})
{
carp("TemplatePattern::Get() liefert undef()! ".
"Aufruf war: '$betriebssystem','$subsystem','$quelle','$ziel'\n");
main::myexit($main::ErrorExitCode);
}
return ( $objekt->{$betriebssystem}->{$subsystem}->{$quelle}->{$ziel} );
}
sub Set
{
# Parameter: (Betriebssystem, Subsystem, Quellfile, Zielfile, Pattern)
# Return: -
#
my ($objekt, $betriebssystem, $subsystem, $quelle, $ziel, $pattern) = @_;
unless (defined $pattern)
{
carp("TemplatePattern::Set() mit zuwenig Parameter aufgerufen!\n");
main::myexit($main::ErrorExitCode);
}
if ( defined $objekt->{$betriebssystem}->{$subsystem}->{$quelle}->{$ziel} )
{
carp("TemplatePattern::Set(): Doppelter Eintrag! ".
"Aufruf war: '$betriebssystem','$subsystem','$quelle','$ziel'\n");
main::myexit($main::ErrorExitCode);
}
$objekt->{$betriebssystem}->{$subsystem}->{$quelle}->{$ziel} = $pattern;
}
######################################################################
### Subsystem-Objekt
######################################################################
package SubsystemObject;
# Alle Kommandos für Subsysteme werden in diesem Objekt als Methoden
# implementiert, z.B.:
# testinstallcmd ist Obj->IsInstalled
use File::Copy;
use Carp;
sub new
{
my ($object,$bs,$rechner,$subsystem) = @_;
unless (defined $subsystem)
{
carp("SubsystemObject::new(Betriebssystem, Rechner, Bestriebssystem)".
"mit zu wenig Parameter aufgerufen!\n");
main::myexit($main::ErrorExitCode);
}
my $daten = {
BS => $bs,
RECHNER => $rechner,
SUBSYSTEM => $subsystem
};
bless $daten, 'SubsystemObject';
return $daten;
}
sub rsh
{
# Kopieren des Kommandos auf die Zielmaschine, Ausführung und wieder Löschen
#
my ($rechner, $path_to_command, $command, $subsystem) = @_;
my $kommandofile = "$path_to_command$main::slash$command";
unless (-r $kommandofile)
{
main::logdie "Kann File '$kommandofile' nicht lesen!\n";
}
# Dummy-Kommandos: Wenn das Kommando die Filelänge Null hat, dann nicht
# kopieren und ausführen!
unless (-s "$path_to_command$main::slash$command")
{
print "(Dummy wird nicht nicht ausgeführt.)\n";
return $main::TRUE;
}
unless (-x $kommandofile)
{
main::logdie "Kann File '$kommandofile' nicht ausführen!\n".
"(Sind die Permissions richtig gesetzt?)\n";
}
my $rsh = $main::beschreibungen->GetRSH($rechner);
my $rcp = $main::beschreibungen->GetRCP($rechner);
# Zur besseren Lesbarkeit ein Beispiel:
# rcp /var/sysconf/AIX-4.1.5/sendmail/testinstallcmd
# tgx987:/tmp/testinstallcmd.3648
# rsh tgx987 /tmp/testinstallcmd.3648
# rsh tgx987 rm /tmp/testinstallcmd.3648
main::debug_rsh "$rcp $path_to_command$main::slash$command ".
"$rechner:${main::slash}tmp${main::slash}$command.$$\n";
main::debug_rsh "$rsh $rechner ".
"${main::slash}tmp${main::slash}$command.$$\n";
main::debug_rsh "$rsh $rechner rm ${main::slash}tmp".
"${main::slash}$command.$$\n";
system("$rcp $path_to_command$main::slash$command ".
"$rechner:${main::slash}tmp${main::slash}$command.$$");
main::logdie "RemoteCopy liefert Fehler!\n" if ($?>>8);
# Rsh liefert nicht den Exitcode des Remote-Prozesses!
# => Das Programm/Script muß den String "TRUE" zurückgeben
my $ret = `$rsh $rechner ${main::slash}tmp${main::slash}$command.$$`;
main::logdie "RemoteShell liefert Fehler!\n" if ($?>>8);
system("$rsh $rechner rm ${main::slash}tmp${main::slash}$command.$$");
main::logdie "RemoteShell liefert Fehler!\n" if ($?>>8);
print ( ($ret =~ /TRUE/) ? "true\n" : "false\n");
return ( ($ret =~ /TRUE/) ? 1 : 0 );
}
sub ExecuteCommand
{
# Diese Funktion führt die geforderten Kommandos aus.
# Das vereinfacht die anderen Funktionen.
#
my $objekt = shift;
my $command = shift;
print "Kommando: $command... ";
return rsh($objekt->{RECHNER},
"$main::sysconfroot$main::slash$objekt->{BS}$main::slash".
"$objekt->{SUBSYSTEM}",
$command,
$objekt->{SUBSYSTEM});
}
sub GetHTML
{
# Es werden nicht die Kommandos ausgeführt, sondern eine HTML-Dokumentation
# ausgegeben.
#
my $objekt = shift;
my $rechner = shift;
my $htmldir = shift;
my $command;
my $ret = '';
foreach $command ('testinstallcmd', 'installcmd', 'testruncmd', 'stopcmd',
'startcmd', 'removecmd', 'reconfigcmd')
{
my $cmdfile = "$main::sysconfroot$main::slash$objekt->{BS}$main::slash".
"$objekt->{SUBSYSTEM}$main::slash$command";
# Existiert das cmd-File?
if (-r $cmdfile)
{
# Wenn das cmd-File nicht leer ist
unless (-z $cmdfile)
{
# Wenn das cmd-File eine Textdatei ist
if (-T $cmdfile)
{
$ret .= " <a href=\"$rechner-sub-$objekt->{SUBSYSTEM}-".
"$command.txt\">$command</a><br>\n";
copy("$main::sysconfroot$main::slash$objekt->{BS}$main::slash".
"$objekt->{SUBSYSTEM}$main::slash$command",
"$htmldir$main::slash$rechner-sub-$objekt->{SUBSYSTEM}".
"-$command.txt"
);
}
# Wenn es eine Binär-Datei ist
else
{
$ret .= " $command (binär)<br>\n";
}
}
# Wenn das cmd-File leer ist, dann keinen Link erzeugen
else
{
$ret .= " $command (leer)<br>\n";
}
}
}
return $ret;
}
sub IsInstalled
{ return ExecuteCommand(shift,'testinstallcmd') }
sub Install
{ return ExecuteCommand(shift,'installcmd') }
sub IsRunning
{ return ExecuteCommand(shift,'testruncmd') }
sub Stop
{ return ExecuteCommand(shift,'stopcmd') }
sub Start
{ return ExecuteCommand(shift,'startcmd') }
sub Remove
{ return ExecuteCommand(shift,'removecmd') }
sub Reconfigure
{ return ExecuteCommand(shift,'reconfigcmd') }
######################################################################
### Modul zur Textersetzung in Files
######################################################################
package TextModify;
use Carp;
sub debug { main::debug (@_); }
sub warning { main::warning(@_); }
# Modul-Globale Variablen initialisieren
sub TextModifyInit
{
# Quellfile ist globale Variable, da sie sonst immer übergeben werden muß
@quelle = ();
$TRUE = 1;
$FALSE = 0;
}
sub ErsetzeMuster
{
# Parameter: Quellfile, Zielfile, Pattern, Referenz auf Variablen-Hash
# Return: True bei Erfolg, False bei Fehler
TextModifyInit(); # Modul-Globale Variablen initialisieren
my ($quellfile, $zielfile, $pattern, $refvar) = @_;
croak "Quellfile '$quellfile' nicht lesbar!\n" unless -r $quellfile;
my $variablenCode = '';
foreach (keys %$refvar)
{
my $refvarquote = $$refvar{$_}; $refvarquote =~ s/\'/\\\'/g;
$variablenCode .= "my \$$_='$refvarquote';\n";
}
# Quellfile komplett einlesen
my $quellFH = FileHandle->new();
open($quellFH, $quellfile);
@quelle = <$quellFH>;
close $quellFH;
@patternarray = split("\n",$pattern);
# Durch alle Ersatz-Muster durchgehen
my $zeile;
my $expr;
my $Veraenderungen = 0;
while( defined($zeile = shift @patternarray) )
{
next if $zeile =~ /^\s*$/; # Leerzeilen ignorieren
# CHANGE
if ($zeile =~ /^\s*change\s+(s.*)/i)
{
$expr = $1;
debug "CHANGE: '$expr'\n";
next unless IstMusterGueltig($expr);
foreach (@quelle)
{
# Ersetzungen mit Variablen durchführen
{
local $FehlerInVariablenErsetzungExpression = $expr;
local $SIG{__WARN__} = \&FehlerInVariablenErsetzung;
$Veraenderungen += eval $variablenCode.$expr;
}
}
next;
}
# ADD FIRST
if ($zeile =~ /^\s*add\s+first\s+(.*)/i)
{
$expr = $1;
debug "ADD FIRST: '$expr'\n";
# Es sollte eigentlich so funktionieren (Fehler in Perl?):
# eval $variablenCode.'$expr =~ s/\$(\w+)/${$1}/g';
# Variablenersetzung
{
local $FehlerInVariablenErsetzungExpression = $expr;
local $SIG{__WARN__} = \&FehlerInVariablenErsetzung;
eval $variablenCode.'$expr =~ s/(\$\w+)/$1/eeg;';
}
unshift @quelle, $expr, "\n";
$Veraenderungen++;
next;
}
# ADD LAST
if ($zeile =~ /^\s*add\s+last\s+(.*)/i)
{
$expr = $1;
debug "ADD LAST: '$expr'\n";
# Variablenersetzung
{
local $FehlerInVariablenErsetzungExpression = $expr;
local $SIG{__WARN__} = \&FehlerInVariablenErsetzung;
eval $variablenCode.'$expr =~ s/(\$\w+)/$1/eeg;';
}
push @quelle, $expr, "\n";
$Veraenderungen++;
next;
}
# ADD match text
if ($zeile =~ /^\s*add\s+(m.*)/i)
{
$expr = $1;
debug "ADD match: '$expr'\n";
chomp($neuertext = shift @patternarray);
debug "ADD neuertext: '$neuertext'\n";
# Variablenersetzung
{
local $FehlerInVariablenErsetzungExpression = $expr;
local $SIG{__WARN__} = \&FehlerInVariablenErsetzung;
eval $variablenCode.'$neuertext =~ s/(\$\w+)/$1/eeg;';
}
next unless IstMusterGueltig($expr);
my $i;
my @ziel = ();
foreach (@quelle)
{
push @ziel, $_;
next unless (eval $expr);
# Neue Zeile einfügen
push @ziel, $neuertext."\n";
$Veraenderungen++;
}
@quelle = @ziel;
next;
}
# "uniq" (Nur ersetzen, wenn es noch nicht vorkommt)
# ADDUNIQ FIRST
if ($zeile =~ /^\s*adduniq\s+first\s+(.*)/i)
{
$expr = $1;
debug "ADDUNIQ FIRST: '$expr'\n";
# Variablenersetzung
{
local $FehlerInVariablenErsetzungExpression = $expr;
local $SIG{__WARN__} = \&FehlerInVariablenErsetzung;
eval $variablenCode.'$expr =~ s/(\$\w+)/$1/eeg;';
}
next if grep(/$expr/, @quelle); # Wenn schon vorhanden, dann nichts tun
unshift @quelle, $expr, "\n";
$Veraenderungen++;
next;
}
# ADDUNIQ LAST
if ($zeile =~ /^adduniq\s+last\s+(.*)/i)
{
$expr = $1;
debug "ADDUNIQ LAST: '$expr'\n";
# Variablenersetzung
{
local $FehlerInVariablenErsetzungExpression = $expr;
local $SIG{__WARN__} = \&FehlerInVariablenErsetzung;
eval $variablenCode.'$expr =~ s/(\$\w+)/$1/eeg;';
}
next if grep(/$expr/, @quelle); # Wenn schon vorhanden, dann nichts tun
push @quelle, $expr, "\n";
$Veraenderungen++;
next;
}
# ADDUNIQ match text
if ($zeile =~ /^\s*adduniq\s+(m.*)/i)
{
$expr = $1;
debug "ADDUNIQ match: '$expr'\n";
chomp($neuertext = shift @patternarray);
debug "ADDUNIQ neuertext: '$neuertext'\n";
# Variablenersetzung
{
local $FehlerInVariablenErsetzungExpression = $expr;
local $SIG{__WARN__} = \&FehlerInVariablenErsetzung;
eval $variablenCode.'$neuertext =~ s/(\$\w+)/$1/eeg;';
}
next unless IstMusterGueltig($expr);
# Wenn schon vorhanden, dann nichts tun
next if grep(/$neuertext/, @quelle);
my $i;
my @ziel = ();
foreach (@quelle)
{
push @ziel, $_;
next unless (eval $expr);
# Neue Zeile einfügen
push @ziel, $neuertext."\n";
$Veraenderungen++;
}
@quelle = @ziel;
next;
}
chomp($zeile);
warning "Ignoriere ungültigen Text:\n'$zeile'\n";
}
debug "Es wurden $Veraenderungen Textveränderungen durchgeführt\n";
# Resultat schreiben
my $zielFH = FileHandle->new();
open($zielFH, ">$zielfile") ||
croak "Kann Zielfile '$zielfile' nicht schreiben!\n";
print $zielFH @quelle;
close $zielFH;
debug "DIFF:\n".`diff $quellfile $zielfile`;
return $TRUE;
}
sub IstMusterGueltig
{
# Testen, ob der Code eine gültige Perl-Expression ist
# Parameter: Ein Stück Perl-Code
# Beispiel: "s/bla/fasel/;"
my $code = shift;
{
local $SIG{__WARN__} = sub {}; # IGNORE geht nicht!?
eval $code;
if ($@)
{
warning "Fehler in Perl-Expression: ", $@;
return $FALSE;
}
}
return $TRUE;
}
sub FehlerInVariablenErsetzung
{
# Signal-Handler für Fehler in Variablenersetzungen
# Wichtig: Die globale (local) Variable
# $FehlerInVariablenErsetzungExpression muß gesetzt sein!
# Parameter: -
# Return: -
my $fehler = shift;
if ($fehler =~ /Use of uninitialized value at/)
{
warning "Fehler in der Variablenersetzung!\n";
warning "Sie haben in folgender Zeile eine Variable ".
"verwendet,\n";
warning "die Sie aber nicht definiert haben:\n";
warning "'$FehlerInVariablenErsetzungExpression'\n";
}
else
{
warning $fehler;
warning "Nicht abgefangener Fehler in Variablenersetzung!\n";
}
}
######################################################################
### POD-Dokumentation
######################################################################
__END__
=head1 NAME
sysconf - Systemkonfiguration
=head1 SYNOPSIS
sysconf [options] action subsystem machine
=head1 DESCRIPTION
Eine Aktion ist in natürlicher Sprache formuliert von dem Aufbau:
'Führe Aktion X mit Subsysteme Y auf Rechner Z aus.'
Aktionen werden per Kommandozeile übergeben, wobei:
action := init | update | remove | start | stop | documentation
subsystem := <subsystem-name>{,<subsystem-name>}* | ALL
machine := <machine-name>{,<machine-name>}* | ALL
'ALL' steht dabei für 'alle Subsysteme' oder 'alle Rechner'.
init: Subsystem erstmalig installieren (alle File einschließlich
'initfiles')
update: Subsystem updaten (Files aus 'initfiles' nicht kopieren!)
Wenn es das Subsystem nicht gibt, dann 'init' vorher durchführen
remove: Subsystem entfernen.
start: Subsystem starten.
stop: Subsystem stoppen.
documentation : Dokumentation erzeugen
Optionen:
-wX: mit X=0-4 gibt den LOG-Level an.
0: Nur fatale Fehler
1: zusätzlich alle Fehler (Genaue Fehlerbeschreibungen)
2: zusätzlich alle Warnungen (ganz informativ)
3: zusätzlich alle Debug-Informationen (ausführlicher Status, etc.)
4: Alle Remote-Shell-Aufrufe werden mitprotokolliert
Beispiele:
'Führe einen Update von Sendmail auf tgx045 durch':
sysconf update sendmail tgx045
'Führe einen Update von allen Subsystemen auf tgx002 durch':
sysconf update ALL tgx002
'Verteile alle Subsysteme auf alle Rechner':
sysconf init ALL ALL
'Führe einen Update von Sendmail und Syslog auf tgx045,tgx200,tgx200 durch':
sysconf update sendmail,syslog tgx045,tgx200,tgx200
=head2 Hauptkonfigurationsdatei
In der Datei "/etc/sysconfrc" bzw. "./sysconfrc" bzw. "~/.sysconfrc" sind
die Standardeinstellungen gespeichert. Mit SYSCONF_ROOT wird das Verzeichnis
angegeben, in dem sich die Konfigurationsdatenbank befindet. Mit HTMLDIR wird
das Verzeichnis für die HTML-Dokumentation, die mit der Aktion 'documentation'
erstellt wird, festgelegt. USE_SYSCHECK kann die Werte TRUE und FALSE
annehmen und legt fest, ob bei einem Update auf dem Zielrechner Syscheck
gestartet werden soll. Beispiel:
SYSCONF_ROOT=/var/adm/sysconf
HTMLDIR=/http/htdocs/sysconf
USE_SYSCHECK=TRUE
Es wird automatisch ermittelt, welche Kommandos zu Rechneransteuerung
verwendet werden sollen. Zur Auswahl stehen: rsh, ssh, rcp, scp und rsync.
=for html
Links zur erwähnten Software:<br>
<A HREF="http://samba.anu.edu.au/rsync/">rsync</A><br>
<A HREF="http://www.ssh.fi/">SSH</A><br>
<A HREF="http://www.uni-karlsruhe.de/~ig25/ssh-faq/">SSH-FAQ</A><br>
=head2 Konfigurationsdatenbank
Die Konfigurationsdatenbank enthält die Informationen welcher Rechner
welche Files erhält und ist im Dateisystem als Verzeichnis-Struktur
abgebildet:
hosts.sc
classes.sc
variables/
tgx045.var
tgx200.var
...
AIX-4.1.5/
dependencies.sc (Abhängigkeiten der Subsysteme)
files.sc (Liste der Files pro Subsystem)
filedir.sc/ (Hier liegen alle Konfigurationsfiles)
etc/rc.config
etc/hosts
etc/issue
etc/exports
root/profile
var/lib/news/active
var/lib/news/newsgroups
usr/lib/news/nnrp.access
usr/lib/news/hosts.nntp
usr/lib/news/inn.conf
...
patches/ ??? (noch offen/ungeklärt)
...
syslog/
installcmd*
removecmd*
testinstallcmd*
startcmd*
stopcmd*
reconfigcmd*
testruncmd*
checkversion*
nfsserver/
installcmd*
removecmd*
testinstallcmd*
startcmd*
stopcmd*
reconfigcmd*
testruncmd*
checkversion*
newsserver/
...
sendmail/
...
=head2 Dateien F<variables/xxx.var>
Beispielsweise enthält F<tgx045.var> Variablen speziell für den Rechner
"tgx045". Escapes ("\n") in Variablen werden nicht interpretiert! Aufbau:
variable=wert
oder
variable include FILENAME1 FILENAME2 ...
Im ersten Fall wird der Variable der Wert zugewiesen. Wenn kein Wert
angegeben ist, dann wird die Variable auf den Leerstring gesetzt.
Im zweiten Fall werden die angegebenen Dateien eingelesen und in der Inhalt
nacheinander in der Variable gespeichert. Diese Files werden im
Variablen-Verzeichnis gesucht.
=head2 Datei F<files.sc>
Beispiel:
[boot]
f etc/rc
t etc/inittab
[login]
f etc/environment
F etc/passwd
t etc/motd
[adsmc]
f etc/inittab
f /etc/sendmail.cf
[INN]
f var/lib/news/expire.ctl.inn /var/lib/news/expire.ctl
[CNEWS]
f var/lib/news/expire.ctl.cnews /var/lib/news/expire.ctl
[Compiler]
L /usr/local/bin/rs6000-ibm-aix4.1.4.0-gcc /usr/local/bin/gcc
[Modem]
t etc/inittab
beginpattern
change s!#PORT!mo:123:respawn:/usr/local/sbin/vgetty $MODEMDEVICE!;
adduniq m/^5:/
6:123:respawn:/sbin/mingetty tty6
endpattern
Die genaue Syntax lautet:
[subsys]
<fileentry>
<patternblock>
[subsys]: Beginn Beschreibung Subsystem namens "subsys"
<fileentry>: <type> <filename> <zielfilename>
<patternblock>: hat folgenden Aufbau:
beginpattern
<pattern>
endpattern
Die möglichen <pattern> sind im Abschnitt L<Patternblock> erklärt.
Wenn der <type> kein Template-Typ ist, dann entfällt der <patternblock>
<type>:
inst = I : Installfile (Nur bei INIT übertragen, vor installcmd)
initf = F : Initfile (Nur bei INIT übertragen, nach installcmd)
initt = T : Init-Template (Bei INIT übertragen und Ersetzungen
aus dem Patternblock durchführen)
f : File (Bei UPDATE übertragen)
t : Template (Bei UPDATE übertragen und Ersetzungen
aus dem Patternblock durchführen)
instL : Install-Link (Nur bei INIT anlegen, vor installcmd)
initL : Init-Link (Nur bei INIT anlegen, nach installcmd)
L : Link (Bei UPDATE anlegen)
instS : Shellkommando (Nur bei INIT ausführen, vor installcmd)
initS : Shellkommando (Nur bei INIT ausführen, nach installcmd)
S : Shellkommando (Bei UPDATE ausführen)
<filename>: Kompletter Pfad des Files. Wenn das ein relativer
Pfad ist, dann liegt das File unterhalb von
files.sc/, sonst wird der absolute Pfad genommen.
<zielfilename>: Optional. Kompletter Zielpfad+Name des Files.
Bei den Links wird wie auch bei "ln" üblich "Quelle Ziel" angegeben, es
entsteht also ein Link von "filename" auf "zielfilename".
Die Shellkommandos sind dazu gedacht, daß man Verzeichnisse, Gerätedateien,
Links, etc. anlegen oder entfernen kann. Als Shellkommandos ist alles
möglich, was auch per "rsh" möglich ist, z.B.:
cd /home/user ; ln -sf ../skel/.profile .
In den Shellkommandos werden die Variablen aus Dateien F<variables/xxx.var>
expandiert.
Der spezielle Subsystemname "GLOBAL" bedeutet, daß diese Files nicht zu
einem bestimmten Subsystem gehören, sondern global sind.
Das sind also die Files und Konfigurationsdateien, die jeder (!) Rechner
mit diesem Betriebssystem erhalten soll.
=head2 Kommando-Files *cmd
Es müssen alle Kommando-Files vorhanden sein.
Wenn ein solches *cmd-Programm die Länge Null hat, dann wird es nicht
verwendet! (Man braucht beispielsweise nicht immer ein startcmd.)
Wenn Files für verschiedene Betriebssysteme identisch sind, dann kann man
einfach einen Link legen.
Alle Programme/Shellscripten müssen TRUE oder FALSE auf
STDOUT ausgeben. Diese Programme werden auf dem Zielrechner ausgeführt.
F<checkversion> überprüft die Versionsnummer des Subssystems und liefert
TRUE/FALSE (Möglicherweise nicht oder schwer realisierbar!)
=head2 Kommentare
In den Dateien
dependencies.txt
hosts.sc
classes.sc
sind Kommentare möglich: Alle Zeilen, die mit '#' beginnen werden ignoriert.
=head2 Dateien F<hosts.sc> und F<classes.sc>
Die Datei F<hosts.sc> beschreibt die einzelnen Rechner:
In dieser Datei wird festgelegt welche Subsysteme die einzelnen Rechner
bekommen sollen. Beispiel:
HOST tgx987
OS AIX-4.1.5
CLASSES Basissystem Client
SUBSYSTEMS sendmail tcpwrapper nfsclient adsm
HOST tgx123
OS AIX-4.1.5
CLASSES Basissystem Server
SUBSYSTEMS newsserver nfsserver
...
Die Einträge hinter CLASSES und SUBSYSTEMS sind optional.
Dabei können auch Klassen in der Datei F<classes.sc> definiert werden:
CLASSDEF Basissystem
SUBSYSTEMS syslog tcpwrapper ssh
CLASSDEF Server
SUBSYSTEMS adsm audit nis
...
Dabei sollten die Subsysteme möglichst fein aufgefächert werden, also, z.B.:
- NIS-Master, NIS-Slave, NIS-Client
- sendmail f. s_mailout, sendmail f. s_mailbox oder sendmail f. Client
- ADSM-Server, ADSM-Client
- Newsserver, Newsclient
=head2 Patternblock
In der Datei F<files.sc> können im Patternblock für Templates
Ersetzungsregeln angegeben werden nach denen die Template-Files verändert
werden bevor sie kopiert werden. In dem Patternblock sind keine Kommentare
möglich! Leerzeilen werden ignoriert.
Die möglichen Syntax-Konstrukte lauten
change <substitute>
add FIRST <text>
add LAST <text>
add <patternmatch>
<text>
adduniq FIRST <text>
adduniq LAST <text>
adduniq <patternmatch>
<text>
<substitute>: Substitute-Kommando von Perl: s/.../.../
<patternmatch>: Patternmatch-Kommando von Perl: m/.../.../
<text>: Text, der eingefügt werden soll.
Zu beachten: nach dem Patternmatch muß eine neue Zeile beginnen!
Die Variablen, die in F<variables/rechnername.var> definiert wurden
können mit vorangestelltem "$" verwendet werden. Escapes ("\n") in
Variablen werden nicht interpretiert, aber man kann diese Funktionsweise
nachbilden: Wenn man eine Variable mit Newlines hat, z.B.
VAR=Das\nsind\nviele\nZeilen\ngetrennt\ndurch\nNewlines\n
dann muß man selbst dafür sorgen, daß die Sonderzeichen interpretiert
werden:
change s/suchtext/($VAR=~s!\\n!\n!g,$VAR)/e;
Beispiele:
Zeilen verändern:
change s/#MEINE_IP#/10.135.82.54/
change s/#MEIN_NAME#/$RECHNER/
change s/#ANWENDUNG#/MFK-Rechner Testbetrieb/
change s/^"START_INN=.*/"START_INN=yes"/
change s/SC_GESCHWINDIGKEIT/$MODEMSPEED/
Zeilen löschen:
change s/^netstat\s+stream\s+tcp//
Zeilen einfügen:
Vor der ersten Zeile einfügen:
add FIRST 127.0.0.1 localhost
Nach der letzten Zeile einfügen:
add LAST 129.187.13.89 mailhost mail
Nach der Zeile, die mit "OVERVIEW" beginnt einfügen:
add m/^OVERVIEW/
news/newsserver:*,!junk,!control*:Ap,Tf,Wnm:news
Zeile nur hinzufügen, wenn sie im File noch nicht vorhanden ist:
adduniq m/^6:123:respawn:/
mo:123:respawn:/usr/local/sbin/vgetty modem
=head2 dependencies.sc
Die Datei "dependencies.sc" hat den Aufbau:
S : D1 D2 D3 ...
Bedeutung: Subsystem "S" hängt von Subsystemen "D1", "D2", "D3", ... ab.
Beispiele:
sendmail : syslog
nfsserver : tcpwrapper portmap inetd
=head1 RETURN
Keine Rückgabewerte.
=head1 AUTHOR
=for text
Sysconf wurde geschrieben von Stephan Löscher,
http://www.leo.org/~loescher/,
loescher@gmx.de, 1998.
=for man
Sysconf wurde geschrieben von Stephan Löscher,
http://www.leo.org/~loescher/,
loescher@gmx.de, 1998.
=for latex
Sysconf wurde geschrieben von Stephan Löscher,
http://www.leo.org/~loescher/,
loescher@gmx.de, 1998.
=for html
Sysconf wurde geschrieben von
<A HREF="http://www.leo.org/~loescher/">Stephan Löscher</A>,
<A HREF="mailto:loescher@gmx.de">loescher@gmx.de</A>,
1998.
=cut
######################################################################
#
# Warranty and legal notice
# ~~~~~~~~~~~~~~~~~~~~~~~~~
#
# Copyright (c) 1998 by Stephan Löscher - all rights reserved
# My Address: Stephan Löscher, Dr.Troll-str. 3, 82194 Gröbenzell, Germany
# Email: loescher@gmx.de
# WWW: http://www.leo.org/~loescher/
#
# This program is freeware.
# It is NOT Public-Domain-Software!
# The author (Stephan Löscher) does NOT give up his copyright, but he
# reserves his copyright. Usage and copying is free of charge for private
# use, but NOT for commercial use!
#
# You may and should copy this program free of charge, use it,
# give it to your friends, upload it to a BBS or something similar, under
# the following conditions:
# * Don't charge any money for it. If you upload it to a BBS, make sure that
# it can be downloaded free (without paying for downloading it, except
# for usage fees that have to be paid anyway). Small copying fees (up to
# 5 DM or 3 $US) may be charged.
# * Only distribute the whole original package, with all the files included.
# * This program may not be part of any commercial product or service without
# the written permission by the author.
# * If you want to include this program on a CD-ROM and/or book, please send
# me a free copy of the CD/book (this is not a must, but I would appreciate
# it very much).
#
# Distribution of the program is explicitly desired, provided that the above
# conditions are accepted.
#
# YOU ARE USING THIS PROGRAM AT YOUR OWN RISK! THE AUTHOR (STEPHAN LÖSCHER)
# IS NOT LIABLE FOR ANY DAMAGE OR DATA-LOSS CAUSED BY THE USE OF THIS PROGRAM
# OR BY THE INABILITY TO USE THIS PROGRAM. IF YOU ARE NOT SURE ABOUT THIS, OR
# IF YOU DON'T ACCEPT THIS, THEN DO NOT USE THIS PROGRAM!
# BECAUSE OF THE VARIOUS HARDWARE AND SOFTWARE ENVIRONMENTS INTO WHICH THIS
# PROGRAM MAY BE PUT, NO WARRANTY OF FITNESS FOR A PARTICULAR PURPOSE IS
# OFFERED.
# GOOD DATA PROCESSING PROCEDURE DICTATES THAT ANY PROGRAM BE THOROUGHLY
# TESTED WITH NON-CRITICAL DATA BEFORE RELYING ON IT.
#
# No part of the documentation may be reproduced, transmitted, transcribed,
# stored in any retrieval system, or translated into any other language in
# whole or in part, in any form or by any means, whether it be electronic,
# mechanical, magnetic, optical, manual or otherwise, without prior written
# consent of the author, Stephan Löscher.
#
# You may not make any changes or modifications to this software or this
# manual. You may not decompile, disassemble, or otherwise reverse-engineer
# the software in any way.
# If you got the source, then you are permitted to modify it if you
# contact me and tell me your enhancements.
# You also may include the source as a whole or parts of it into other
# programs, as long as you don't make profit directly out of selling
# the result. If you re-use code of this program then do not remove my name!
# If you include this source-code in your projects, mark it clearly as such
# "... derived from code XXX by Stephan Löscher".
# But don't distribute modified code!
#
# If you believe your copy of this software has been tampered or altered in
# anyway, shape or form, please contact me immediately! Do not hesitate a
# moment to inform me. Remember, this software should be available to all, in
# the original form, so please do not accept modified or damaged versions of
# my software.
#
# The author reserves his right for taking legal steps if the copyright or the
# license agreement is violated.
#
# All product names mentioned in this software are trademarks or registered
# trademarks of their respective owners.
#
# If you have any questions, ideas, suggestions for improvements or if you find
# bugs (I don't hope so.) then feel free to contact me. (Email is appreciated.)
#
# I'm not a native english speaker. If you are one and discover some strange
# sounding parts in this documentation or in the program, please, feel free
# to point it out to me and give me suggestions for alteration!
#
# If the program works for you, and you want to honour my efforts, you are
# invited to donate as much as you want... :)
#
# In any case, if you don't like the restrictions in this license, contact
# me, and we can work something out.
#
######################################################################