Konzeptionelle Gedanken Text-Template-Engine rtemple ==================================================== Start: 25.04.2005 Stand: 26.04.2005 Autor: Winfried Mueller, www.reintechnisch.de Oft besteht die Aufgabe, eine Sammlung von Textdateien für einen speziellen Anwendungsfall anzupassen. Typische Bereiche, wo dies nötig ist, sind textbasierte Konfigurationsdateien, wie sie unter Linux üblich sind, oder Web-Projekte, wo HTML, CSS, Steuerungs oder Konfigurations-Dateien angepasst werden müssen. Oft ist es dabei so, dass große Teile der Textdateien unverändert bleiben und nur ein paar wenige Bereiche ausgetauscht werden. Bei dutzenden von Dateien kann dieser Job sehr mühselig und fehleranfällig sein. Dies ist eine zentrale Aufgabe von System-Administratoren: Eine Menge von Systemen einzurichten und zu pflegen, die alle ähnlich und doch nicht gleich sind. Und dies beansprucht oft jede Menge Zeit, weil keine optimalen Werkzeuge für diese Anforderung zur Verfügung stehen. Ein paar grundsätzliche Strategien, wie man diese Problematik in den Griff bekommt: * Zentralisierung der Konfiguration: Anstatt überall verstreut Konfigurationsdateien zu haben, ist es besser, einen zentralen Ort für die gesamte Konfiguration eines bestimmten Systembereiches zu haben. * Hierarchische Konfigurationsstruktur: Um bei einer Vielzahl von Variablen den Überblick zu behalten und schnell konfigurieren zu können, bedarf es eines hierarchischen Systems der Konfigurationsvariablen. Darüber bekommt man schnell einen Überblick und kann Bereiche ausblenden, die man im Augenblick nicht konfigurieren will. * Verbergung von Implementierungsdetails: Obwohl man eigentlich nur konfigurieren möchte, muss man sich in technische Details von Komponenten (Programmpaketen, Skripten) einarbeiten. Jedes Programm bringt z.B. seine eigene Konfigurationssprache (Syntax) mit, manche nutzen gar eine Programmiersprache zur Konfiguration, die man verstehen muss. Das bringt alles oft unnötige Komplexität in den Job der Systemkonfiguration. Solche Details müssen verborgen werden. * Verbergung von unnötigen Konfigurationsparametern: Manche Konfigurationsdatei kommt mit hunderten von Parametern daher, die eingestellt werden können. Ein Großteil davon wird man in seinem konkreten Anwendungsfeld nie brauchen. Jedesmal, wenn man die Konfigdatei jedoch bearbeitet, wird man wieder mit unzähligen Einstellungen belastet und muss erneut abschätzen, was man anpassen muss und was konstant bleibt. Es ist eine gute Idee, all das zu verbergen, was man für das eigene Anwendungsfeld nie verändern wird. Konzept Text-Template-Engine ---------------------------- In einem Template-Verzeichnisbaum werden alle zu konfigurierenden Text- Dateien abgelegt. Und zwar in der gleichen hierarchischen-Struktur und mit den gleichen Namen, wie sie später im Zielverzeichnis existieren sollen. Template Verzeichnis: etc/ hosts resolv.conf apache2/ apache2.conf Zielverzeichnis: etc/ hosts resolv.conf apache2/ apache2.conf Jede dieser Textdateien wird nun präperiert. In der Art, dass überall dort, wo veränderliche Bereiche sind, Variablen eingesetzt werden. Diese Variablen werden später durch die konkrete Ausprägung ersetzt. Damit Variablen eindeutig erkannt werden können, erhalten sie ein bestimmtes Textmuster am Anfang und am Ende, werden sozusagen dort eingebettet. Bsp: -- Bsp TextKonfigurationsdatei # Konfigurationsdatei MailTo = @@_mailto_@@ MailFrom = @@_mailfrom_@@ SmtpServer = @@_mailsmtp_@@ -- Bsp HTMLDatei <html> <body> Dies ist der Server @@_server_name_@@. Nutzen sie bitte folgende Weiterleitung: @@_forward_@@ </body> </html> Hier wird @@_ als Anfangsmuster und _@@ als Endmuster verwendet. Grundsätzlich ist dies frei konfigurierbar, man sollte sich jedoch nach Möglichkeit eines auswählen, was für alle möglichen Projekte passt. Es darf nicht in Konflikt mit dem restlichen Text kommen. Der ursprüngliche Text darf also das Muster nicht enthalten. Für das komplette Projekt gibt es nun eine Konfigurationsdatei in Yaml- Syntax. Dort können wohl strukturiert Konfiguriationsparameter eingetragen werden. Man wählt hier eine Struktur, die aus dem Blickwinkel des Konfigurators sinnvoll ist, anstatt sich an technischen Details der dahinterliegenden Komponenten zu orientieren. Hier ein Beispiel: --- Config: General: LogoName: 'logo4.gif' WikiTitle: 'reintechnisch.de Wiki' BaseUrl: 'http://www.wikidorf.de/reintechnisch/' Color: HeadLine: '#0099FF' BGHead: '#ffcc33' Background: '#fffff2' A: '#003399' AVisited: '#663399' Left: '#000000' BGLeft: '#ffcc33' Pre: '#efefef' Password: Admin: 'StrengGeheim' Edit: 'Geheim' Read: '' Upload: '' Attr: '' Oft wird man nun direkt die Variablen in den Template-Dateien mit dem angegebenen Wert in der Config-Datei ersetzen können. In diesem Fall kann man in den Templates direkt auf die Variable verweisen. Die hierarchische Struktur der Variablen wird syntaktisch durch einen "/" abgebildet. Bsp: <html> <body> Das Wiki ist zu erreichen unter @@_General/BaseUrl_@@. </body> </html> Im einfachsten Fall baut man also zuerst einmal die Configdatei auf und ersetzt dann in allen Text-Template-Dateien die entsprechenden Bereiche durch die Variablen. Damit ist alles vorbereitet, damit rtemple die Zieldateien nach Vorgabe der Config-Datei erzeugen kann. Die Welt ist jedoch oft nicht so einfach. Nehmen wir einmal an, wir wollen eine Reihe von Rechnern in eine etc/hosts Datei eintragen. Eine Config-Datei könnte so aussehen: Config: Hosts: - Name: 'workstation1' Domain: 'intra.net' IP: '192.168.0.10' - Name: 'workstation2' Domain: 'intra.net' IP: '192.168.0.11' Dies ist für den Konfigurator gut lesbar und technische Details über den Aufbau einer Hosts Datei werden verborgen. Die Template Datei könnte nun so aussehen: 127.0.0.1 localhost # Begin local hosts @@_hosts_@@ # End local hosts ::1 ip6-localhost ip6-loopback fe00::0 ip6-localnet ff00::0 ip6-mcastprefix ff02::1 ip6-allnodes ff02::2 ip6-allrouters ff02::3 ip6-allhosts Wir können hier keine direkte Variablensubstitution machen. Wir brauchen vielmehr ein Stück Programmcode, welches aus den Informationen der Config-Datei die entsprechenden Hosts-Zeilen bastelt. Rtemple handhabt das so, dass generell alle Variablen, die kein hierarchischen Trenner "/" enthalten, als lokale Variablen angesehen werden. Und lokale Variablen werden durch Programmcode erzeugt, der in die Template Datei eingefügt wird. Und zwar vor dem eigentlichen Template-Text, mit einem __END_RTEMPLE__ abgeschlossen: localvar('hosts') { r = "" $Config['Hosts'].each do |host| r << host['IP'] + "\t" + host['Name'] + "." + host['Domain'] + "\t" + host['Name'] + "\n" end r } __END_RTEMPLE__ 127.0.0.1 localhost # Begin local hosts @@_hosts_@@ # End local hosts ::1 ip6-localhost ip6-loopback fe00::0 ip6-localnet ff00::0 ip6-mcastprefix ff02::1 ip6-allnodes ff02::2 ip6-allrouters ff02::3 ip6-allhosts Bei der Generierung der Zieldateien schaut rtemple also zuerst einmal, ob es ein __END_RTEMPLE__ findet. Wenn ja, dann wird die Datei in zwei Teile gesplittet. Alles davor ist Generierungs-Code, alles danach ist der eigentliche Template-Teil. Wichtig ist, dass man im Generierungs-Code alles generisch hält und alles, was für die Erzeugung anpassbar sein muss, in der Config-Datei landet. Nur die Config-Datei ist der zentrale Ort aller Einstellungen für die zu erzeugende Instanz (Entität). Die Definition der lokalen Variablen folgt immer dem Schema localvar('hosts') { codeblock }, wobei das Ergebnis des Codeblocks der Wert der Variablen entspricht. Dieses Konzept ist recht flexibel, kann man doch so während der Erzeugung beliebigen Code ausführen. Man könnte z.B. ein Time.new aufrufen, um die aktuelle Uhrzeit irgendwo einzufügen. Oder das Environment nach bestimmten Werten befragen. localvar( "Test1" ) { "Hello World" } localvar( "Test2" ) { Time.new.to_s } __END_RTEMPLE__ Das ist ein kleiner Text, und ich sage mal @@_Test1_@@. Wenn dieser Text erzeugt wird, ist es @@_Test2_@@ Bla Bla Bla Generierung ----------- Nachdem die Template-Files und die Config-Datei entsprechend vorbereitet sind, kann man rtemple aufrufen. In der Config-Datei befinden sich weitere Angaben, die für die Erzeugung des aktuellen Projekts wichtig sind: TmplPath: './tmpl' DstPath: './dst' VarPrePattern: '@@__' VarPostPattern: '__@@' TmplEndMark: '__END_RTEMPLE' In diesem Beispiel würde die Konfigurationsdatei im aktuellen Verzeichnis liegen und unterhalb dieses Verzeichnisses gibt es ./tmpl und ./dst für die Vorlagen und das zu erzeugende Ziel. Ein Aufruf erzeugt dann alles: # rtemple rtemple.conf
Diskussion unter: http://www.rubyforen.de/topic,713.html
Weblog
21.05.2005 Konfigurationsmanagment
Vernünftiges Konfigurationsmanagment wird heutzutage immer wichtiger. Systeme werden immer komplexer und damit steigen auch die Möglichkeiten der Konfiguration und auch der Fehlkonfiguration.
Wenn ich Systeme wie Samba oder Apache konfiguriere, komme ich mir manchmal vor, als müsste ich eine Steuererklärung machen oder bei der Stadt ein kompliziertes Formular ausfüllen. Tausende von Fragen, die gar nicht auf mich zutreffen, die aber auf irgendjemanden in diesem Land zutreffen könnten. Und all diese Felder muss ich erstmal verstehen. Diese Aufgabe ist nervig. Wenn man so ein Formular dann zum zweiten mal ausfüllen muss, ist man froh, eine Vorlage zu haben, wo man fix abschreiben kann. Wehe dem, das Formular hat sich irgendwo kaum sichtbar verändert, dass übersehe ich dann bestimmt.
So in etwa ist das mit der Konfiguration von komplizierten Systemen. Wohin muss gutes Konfigurationsmanagment gehen? Ein paar Ideen:
- Mir sollte als Konfigurator immer nur das zur Konfiguration angeboten werden, was ich in meinem Anwendungskontext brauche. Als Systeme nur ein dutzend Konfigparameter hatten, konnte man da schnell über alles drüberschauen. Heutzutage haben Systeme aber hunderte oder gar tausende von Konfigurationsparametern, besonders wenn sie stark skalierbar sind. Viele sind in meinem Umfeld, in dem ich agiere niemals nötig. Es muss also Mechanismen geben, womit ich alles ausblenden kann, was für meine Anwendungsdomäne nicht erforderlich ist. Ein schönes Praxisbeispiel ist der Browser Firefox, wo man möglichst viel Konfiguration verbirgt. Man hat sozusagen ein Anfängerprofil an Konfigmöglichkeiten als Standard definiert. Manche Programme nutzen auch "Standard" und "Experte", um zwei Ebenen anzubieten. Modernes Konfigmanagment muss oft noch viel weiter gehen, in dem man eigene Konfigprofile anlegt, wo man genau definiert, welche Parameter in meiner Domäne variabel sein müssen und welche fix sind und damit ausgeblendet werden können.
- Konfigurationsobjekte - es gibt oft Teilbereiche einer Konfiguration, die definierte zusammenhängende Konfiguration benötigt. Im Steuerformular wäre das die Anschrift. Name, Adresse, Telefon ist sozusagen eine Aggregation, ein zusammenhängendes Etwas, was in dieser Form öfters gebraucht wird. Solche Konfigurationsobjekte begegnen einem immer wieder - Teilbereiche einer Gesamtkonfiguration, die in einer definierten Art belegt werden müssen. "Wenn wir einen Internetserver für diese Anwendung aufsetzen, muss dieser Abschnitt so konfiguriert werden." oder "Für die Dateifreigabe X braucht es diese Konfiguration in diesem Bereich, für Freigabe Y dagegen diese." Konfigurationswerkzeuge müssen solche Konfigurations-Subsets irgendwie unterstützen.
- Konfiguration muss hierarchisch sein. Das Steuerformular ist wieder ein gutes Beispiel. Wenn ein Formular "Einnahmen aus Vermietung" heißt und ich weiß, dass ich nichts vermiete, dann weiß ich auch, dass ich mir die hundert Fragen dazu nicht im Einzelnen durchlesen brauche. Ich kann gleich alles ausblenden. Mit einer guten Hierarchie ist das ähnlich, ich kann ganze Teilbereiche ausblenden und mich auf das Wesentliche konzentrieren.
- Konfiguration und Programmierung strikt trennen. Mitunter wird Programmierung und Konfiguration vermischt. Programmcode und Teile einer Skriptsprache in Konfigurationsdateien. Das Hauptproblem dabei ist, dass man so in der Rolle des Konfigurators das Wissen eines Programmierers haben muss. Programme sollen aber oft durch Menschen konfiguriert werden, die kein Einblick in die dahinterliegende Programmiersprache haben. Selbst Programmierer können nicht Einblick in sämtliche gängigen Skriptsprachen haben. Auch wird das konfigurieren eines Systems so oft unnötig kompliziert. Durch die Mächtigkeit einer Programmiersprache in der Konfiguration kann man auch mächtigere Konfigurationsfehler machen. Ein Programm kann solche Konfigurationsfehler auch nicht abfangen. Man kann sehr gut ein paar Parameter auf Gültigkeit überprüfen, nicht aber, ob ein Stück Programmcode auch Sinn macht. Ein Programm was ein Konfiguration über Programmiersprache anbietet, wird bald dahin tendieren, dass dort auch komplexe Skripte für bestimmte Funktionalitäten entstehen. Man programmiert sozusagen seine Anwendung im Konfigskript weiter. (PmWiki ist ein gutes Beispiel dafür). Ziel sollte es aber sein, dass wirklich die Anwendung weiter entwickelt wird, um dann eine einfache Konfigurationsschnittstelle anzubieten.
07.05.2005 Erste Experimentierversion
So langsam stabilisiert sich der Code für eine erste Version, mit der ich in den nächsten Tagen einige Praxistests machen werde. Es ist einiges hinzugekommen und einige Schnittstellen haben sich geändert, aber das Grundkonzept ist geblieben. Ein erster Testlauf für eine Wiki-Konfiguration verlief prächtig. Beim Wiki werden dabei ca. 5 Dateien nach Vorgabe der rtemple-Config-Datei angepasst. Hier mal die Beispiel Config:
#PmWiki 1.0 Config --- TmplPath: './tmpl' DstPath: './' TmplPrePattern: '@@-' TmplPostPattern: '-@@' FileDefaultProp: Owner: 'wm:wm' Perm: '640' DirDefaultProp: Owner: 'wm:wm' Perm: '750' Files: - Name: 'pub/skins/wm/wm.css' Perm: '0440' - Name: 'pub/skins/xp/xp.css' Exclude: true Config: General: LogoName: "logo4.gif" WikiTitle: "Test Wiki" BaseUrl: "http://local.intra.net/reintechnisch" RewritePath: "/reintechnisch" Color: HeadLine: '#0099FF' Background: '#fffff2' A: '#003399' AVisited: '#663399' Left: '#000000' BGLeft: '#ffcc33' BGTop: '#ffcc33' BGPre: '#efefef' PreBorder: '#ff6600' Decoration: A: 'none' AVisited: 'none' AHover: 'underline' PreBorder: '1px dashed' Position: LogoMarginTop: '5px' LogoMarginLeft: '10px' LogoHeight: '100px' Password: Admin: '$1$Pes.MzH9$HPbkNCSxzbwim7q6z5P735' Edit: '$1$Pes.MzH9$HPbkNCSxzbwim7q6z5P735' Read: '$1$Pes.MzH9$HPbkNCSxzbwim7q6z5P735' Upload: '$1$Pes.MzH9$HPbkNCSxzbwim7q6z5P735' Attr: '$1$Pes.MzH9$HPbkNCSxzbwim7q6z5P735'
Permissions können jetzt für jede Datei einzeln in der Config angegeben werden. Verzeichnisse, die angelegt werden müssen, können ebenfalls mit definierten Permissions und Owner belegt werden. Auch kann man Files einzeln über die Config excluden.
Eine neue Idee ist hinzugekommen: Manchmal gibt es von bestimmten Konfigurationsdateien völlig unterschiedliche Varianten und man möchte eine Variante auswählen. Möglich wäre dies, in dem das Template mehrere Abschnitte hätte, die Varianten widerspiegeln. Also z.B. so:
#template code #... __END_RTEMPLE__ # Template Variante 1 #... __END_RTEMPLE_VARIANT__ # Template Variante 2 #... __END_RTEMPLE_VARIANT__ # Template Variante 3 #...
26.04.2005 Erster Prototyp
Hier ist ein erster Prototyp mit ein paar Beispieldateien einer Linux-Konfiguration. Einfach auspacken und starten. Getestet unter Windows und Linux mit Ruby 1.8.