#!/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. # ######################################################################