Aus Linux-Magazin 10/2008

Konfigurationsverwaltung mit Puppet

© elgris, fotolia.com

Puppet erhebt den Anspruch, die nächste Generation an Konfigurationswerkzeugen zu repräsentieren. Es bietet eine Sprache, um Konfigurationen unabhängig vom verwendeten Betriebssystem zu beschreiben.

Mit anderen Konfigurationswerkzeugen war der Hauptentwickler von Puppet, Luke Kanies, unzufrieden [1]. Die meisten erlaubten es ihm nicht, die Unterschiede mehrerer Paketformate und Betriebssysteme zu kaschieren. Sein in Ruby [2] komplett neu entwickeltes Puppet sollte folglich verwandte Konzepte abstrahieren und sie transparent für den Systemverwalter im Hintergrund passend für die entsprechende Plattform implementieren. Davon erhofften sich Kanies und die Puppet-Entwickler bessere Portabilität und größeren Austausch von Anwender-Modulen.

Die Bühne zimmern

Puppet eignet sich sowohl für einzelne Rechner als auch für große Rechnerverbünde. Wer nur einen Rechner verwaltet, ruft Puppet dort von der Kommandozeile aus auf. Administratoren größerer Rechnerparks betreiben Puppet im Client-Server-Modus. Der einfach einzurichtende Austausch nutzt ein eigenes, SSL-gesichertes Protokoll. Ein zentraler Host verteilt die Konfigurationen und nimmt die Berichte seiner Clients entgegen.

Diverse Distributionen bieten die Software als Paket an, aktuell und stabil ist die Version 0.24.5. Da es unter dem hier benutzten Gentoo als instabil gekennzeichnet ist, schalteten die Tester es gemeinsam mit dem abhängigen Facter-Paket mit

( echo "dev-ruby/facter ~x86"; echo "app-admin/puppet ~x86" ) >> /etc/portage/package.keywords

frei und installierten es mit »emerge puppet«. Die meisten anderen Distributionen wie Fedora, Open Suse oder Debian nennen das Paket einfach »puppet«. Es gibt auch ein Ruby-Gem.

Der Konfigurationsmanager möchte konfiguriert sein. In »/etc/puppet/puppet.conf« trägt der Systemverwalter Verzeichnispfade ein, die nicht fehlen dürfen (siehe Listing 1). Im »vardir« legt Puppet Backupdateien an, Logdateien landen im »logdir« und das »rundir« enthält die PID-Information des laufenden Puppet-Masterprozesses und des Puppet-Clients.

Listing 1: Konfig von
»puppet.conf«

01 [main]
02   confdir     = /etc/puppet
03   vardir      = /var/lib/puppet
04   logdir      = /var/log/puppet
05   rundir      = /var/run/puppet
06   modulepath  = $confdir/modules
07   manifestdir = $confdir/manifests

Der erste Akt

Will der Admin Konfig-Spezifikationen in Modulen gruppieren, müssen die Direktive »modulepath« und das zugehörige Verzeichnis vorhanden sein. Die Pfade entsprechen der Konfiguration unter Gentoo und können auf anderen Systemen leicht abweichen. Weitere Konfigurationsvariablen und ihre Standardwerte finden Anwender im Wiki [3].

Manifeste nennt Puppet seine Steuerdateien für die Konfigurationen eines Dienstes oder eines Pakets. Das Tool verwendet dazu die Endung ».pp«. Kürzere Manifeste sammelt der Admin typischerweise im »manifestdir«, komplexere legt er als Module im »modulepath« ab.

Als zentrales Manifest dient standardmäßig die Datei »site.pp«. Wer Puppet von der Kommandozeile aus aufruft, gibt den Dateinamen explizit im Kommando »puppet site.pp« an. Listing 2 enthält ein unvermeidliches “Hello World”, das nichts konfiguriert. Abbildung 1 zeigt, dass Puppet in der Ausgabe den Funktionsnamen (»notice«), den Node »default« als Kontext (»Scope(Node[default])«) und schließlich den angegebenen Text »Hello world!« ausgibt.

Abbildung 1: Manuell aufgerufen arbeitet Puppet das Standard-Manifest »site.pp« aus Listing 1 ab und meldet sich mit einem Gruß.

Abbildung 1: Manuell aufgerufen arbeitet Puppet das Standard-Manifest »site.pp« aus Listing 1 ab und meldet sich mit einem Gruß.

Das Listing gibt schon Aufschluss über den Aufbau von Manifesten: Es deklariert in Zeile 1 einen »node«, das Puppet-Synonym für einen einzelnen Rechner. Hier stehen Hostnamen, für die die Inhalte in geschweiften Klammern gelten. Puppet ermittelt beim Start automatisch den Namen des Rechners, um so den passenden Node zu bestimmen und die darin festgelegte Konfiguration umzusetzen. Die Einstellungen des Node mit dem Bezeichner »default« greifen dann, wenn Puppet keinen anderen passenden Node findet. Es ist nun am Administrator, Spezifikationen niederzulegen.

Das Beispiel in Listing 3 konfiguriert einen OpenSSH-Server. Da die Spezifikation autark von der restlichen Systemkonfiguration ist, bietet es sich an, sie in ein eigenständiges Modul auszulagern. Das umfasst mindestens ein Manifest für einen Dienst, darf aber auch Konfigurationsdateien oder -templates mitbringen. Komplexere Module stellen in Ruby geschriebene Plugins bereit und erweitern Puppet damit.

Listing 2: Hello World in
»site.pp«

01 node default {
02   notice('Hello world!')
03 }

Listing 3: Konfiguration des
OpenSSH-Servers

01 class openssh {
02 
03   package {openssh:
04     ensure => 'installed'
05   }
06 
07   user {sshd:
08     home => '/var/empty',
09     shell => '/sbin/nologin',
10     require => Package['openssh'];
11   }
12 
13   file {'/etc/ssh':
14     ensure => directory,
15     mode => 0755,
16     owner => root, group => root,
17     require => Package['openssh'];
18 
19         '/etc/ssh/sshd_config':
20     source =>'puppet:///openssh/sshd_config',
21     mode => 0600,
22     owner => root, group => root,
23     notify => Service[sshd],
24     require => File['/etc/ssh'];
25   }
26 
27   service {sshd:
28     ensure => running,
29     require => [Package['openssh'],
30                 File['/etc/ssh/sshd_config'],
31                 User[sshd]],
32     subscribe => User[sshd]
33   }

Ein ganzes Stück

Module haben eine festgelegte Dateistruktur und liegen im »modulepath«. Jedes Modul hat sein eigenes Verzeichnis. Der Admin legt also das Unterverzeichnis »openssh« an und speichert dort die Konfigurationsdatei des Moduls unter »manifests/init.pp«. Die ganze »/etc/puppet/modules/openssh/manifests/init.pp« findet sich in Listing 3.

In Zeile 1 definiert der Bezeichner »class« die Konfigurationsklasse »openssh«. Strukturell gleichen Klassen den oben angesprochenen Nodes, hängen aber nicht von konkreten Hosts ab. Diese Verbindung stellt die aktualisierte Version von »site.pp« in Listing 4 her. Das Schlüsselwort »import« in Zeile 1 stellt Puppet das neue Modul »openssh« vor. Die Direktive »include« in Zeile 4 bindet anschließend die in Listing 3 definierte Klasse »openssh« in die Definition des »default«-Node ein.

Verpackt der Anwender die Direktiven in Modulen, kann er sie in einzelnen Komponenten frei kombinieren. Möchte er einen neuen Host »www.example.org« als Webserver einrichten, definiert er das Modul »apache« und fügt zum Listing 4 in Zeile 2 die Anweisung »import \’apache\’« und einen neuen »node www.example.org« hinzu.

Listing 4: SSH-Server in
»site.pp« einbinden

01 import 'openssh'
02 
03 node default {
04   include openssh
05 }

Päckchen packen

Die Klassendefinition im Modul aus Listing 3 enthält eine Reihe an Konfigurationsanweisungen, so genannten Typen. Sie steuern das Verhalten des zugehörigen Objekts und sind ein wichtiges Element der Puppet-Konfigurationssprache. Zu ihnen zählen »package«, »user«, »file« und »service«. Eine vollständige Liste verfügbarer Typen listet das Wiki [4].

Hinter dem Typ folgt in geschweiften Klammern eine Liste von Attributen und Werten. Sie ergibt gemeinsam mit dem Typ eine Ressource, die beschreibt, wie sich das zu konfigurierende Objekt laut Spezifikation verhalten soll. Bindet der Admin die Ressource an ein Manifest, so versucht Puppet, das betreffende Objekt nach dieser Spezifikation zu konfigurieren, etwa Berechtigungen anzupassen oder Dienste zu aktivieren.

Die Ressource in Listing 3 ab Zeile 3 veranschaulicht dies: Sie kombiniert den Typ »package« mit dem Namen »openssh« und fügt dem Attribut »ensure« den Wert »installed« hinzu. Der Admin legt damit fest, dass auf jedem zugehörigen Node das Softwarepaket mit dem Namen »openssh« im System installiert sein soll. Puppet überprüft diesen Umstand und installiert das Paket nötigenfalls nach.

Funktionen verstecken

Unterschiedliche Betriebssysteme stellen dazu verschiedene Methoden bereit: Gentoo installiert Pakete mittels »emerge«, während RPM-basierte Distributionen »rpm«, »yum« sowie »zypper« und Debian-Derivate »aptitude« nutzen. Diese Abstraktion ist ein zentrales Element von Puppet: Damit die Definition der Konfigurationsmodule nicht vom Betriebssystem abhängt, stellt ein Typ in Puppet nur ein abstraktes Interface bereit.

Im Hintergrund kümmern sich mitgelieferte Provider darum, das Interface auf den einzelnen Betriebssystemen umzusetzen. So gibt es für den »package«-Typ Provider für »apt«, »rpm«, »yum« und »emerge«. Weitere Provider kümmern sich um BSD oder Mac OS. Diese Abstraktion erspart es dem Administrator, sich mit den Eigenheiten des Betriebssystems zu beschäftigen. Im Detail reicht es Providern nicht aus, zwischen »yum« und »emerge« zu unterscheiden.

Im konkreten Fall des SSH-Servers trägt das Paket unter Gentoo den Namen »openssh«, unter Debian aber »openssh-server« und unter Solaris »SMCossh«. Diese Unterschiede innerhalb von Puppet zu abstrahieren ist derzeit weder möglich noch sinnvoll. Wer die Klassendefinition aus Listing 3 über die Grenzen mehrerer Distributionen verwenden möchte, wandelt die Ressource »package« ab, wie in Listing 5 beschrieben.

Listing 5: OpenSSH für
mehrere Betriebssysteme

01 package {openssh:
02   name => $operatingsystem ? {
03     gentoo  => 'openssh',
04     solaris => 'SMCossh',
05     debian  => 'openssh-server'
06   },
07   ensure => 'installed'
08 }

Fakten, Fakten, Fakten

Normalerweise verwendet das »name«-Attribut eines Pakets automatisch den Namen der Ressource. Das ist die Angabe direkt nach der geschweiften Klammer. Das Beispiel in Listing 5 definiert das Attribut manuell und abhängig vom Betriebssystem. Die Konstruktion zeigt gleich einen weiteren Aspekt von Puppet: Ein vorgestelltes »$«-Zeichen kennzeichnet Variablen. Das Fragezeichen erlaubt Alternativen, aus anderen Programmiersprachen als »switch« oder »case« bekannt. Puppet durchbricht hier die vollständige Abstraktion, der Admin muss sich eben doch mit den einzelnen Distributionen auseinandersetzen.

Das Skript hat die Variable »$operatingsystem« nicht explizit definiert, sie stammt vom Hilfswerkzeug »facter«, der einzigen Abhängigkeit, die grundlegende Fakten über das System an Puppet liefert. Der Befehl

facter architecture kernel operatingsystem hardwareisa

zeigt als Trockenübung einige Attribute, die das Tool plattformspezifisch ausfüllt (siehe Abbildung 2). Erfahrene Puppet-Anwender erweitern es um eigene Parameter und Tests und ersparen sich so, die entsprechenden Werte manuell für jede Maschine anzugeben.

Abbildung 2: Das Ruby-Tool »facter« liefert unabhängig von Puppet Informationen über das System.

Abbildung 2: Das Ruby-Tool »facter« liefert unabhängig von Puppet Informationen über das System.

Grundeinstellungen

Die OpenSSH-Klasse aus Listing 3 definiert noch eine Reihe weiterer Ressourcen. Als zweite folgt die Definition des Benutzers, der den Dienst später betreiben soll. Dessen »home« liegt in »/var/empty«, der Eintrag von »/sbin/nologin« als Shell verhindert einen interaktiven Login für den Funktionsuser.

Da sich die einzelnen Distributionen beim Anlegen von Benutzern nur geringfügig unterscheiden, sind Umwege über Mehrfachauswahlen wie beim Paket-Typ unnötig. Die Ressource ist ohnehin unnötig, wenn der Paketmanager direkt einen entsprechenden Benutzer anlegt. Der Admin ist so jedoch in der Lage, bestimmte Einstellungen der nativen Distributionspakete zu überschreiben, etwa die spezielle »nologin«-Shell, die einen interaktiven Login verhindert.

Dem User folgen Ressourcen vom Typ »file«. Listing 3 definiert zwei Datei-Ressourcen, eine für »/etc/ssh« und eine für »/etc/ssh/sshd_config«. Die beiden Deklarationen trennt ein Semikolon. Die Attribute spezifizieren näher, wie das Konfigurationswerkzeug mit den Dateien umgeht, etwa legt »ensure => directory« fest, dass »/etc/ssh« ein Verzeichnis mit dem Besitzer und der Gruppe »root« sein soll und die Rechte »0755« aufweist.

Beim Pfad »/etc/ssh/sshd_config« (Zeile 19) kommt zum ersten Mal eine Konfigurationsdatei ins Spiel. Sie konfiguriert den OpenSSH-Server. Mit »source => \’puppet:///openssh/sshd_config\’« (Zeile 20) verlangt die Direktive, dass Puppet diesen Pfad mit dem Inhalt der Datei »/etc/puppet/modules/openssh/files/sshd_config« überschreibt. Die Syntax »puppet:///openssh/sshd_config« abstrahiert von diesem Pfad. Das erfordert besonders der Client-Server-Betrieb, wenn verschiedene Nodes unterschiedliche Pfad-Prefixe verwenden.

Der Client greift nicht direkt auf die Konfigurationsmodule des Servers zu, sondern erhält die Dateien über das Netzwerk. Mit »puppet:///openssh/sshd_config« fragt der Client den Puppet-Server nach der Datei »sshd_config« aus dem Modul »openssh«. Ruft der Administrator Puppet über die Kommandozeile auf, konvertiert es die gleiche Syntax in den lokalen Pfad und liest die Datei direkt.

Ein Modul muss jene Dateien, die Puppet über diesen Mechanismus abfragt, innerhalb der Modulstruktur im Verzeichnis »files« bereithalten. So ergibt sich der Pfad »/etc/puppet/modules/openssh/files/sshd_config«. Dort bringt der Admin die Konfiguration des SSH-Servers entsprechend Listing 6 unter.

Listing 6: Konfiguration des
»sshd«

01 Port                             22
02 Protocol                          2
03 PermitRootLogin                  no
04 RSAAuthentication                no
05 PubkeyAuthentication             no
06 PasswordAuthentication          yes
07 ChallengeResponseAuthentication  no
08 UsePAM                          yes
09 X11Forwarding                   yes
10 ClientAliveInterval              30

Vorlagen verwenden

Statt statischer Dateien darf der Admin auch Templates benutzen. Sie erlauben ihm Optionen der Konfigurationsdatei abhängig von Host-spezifischen Parametern zu setzen. Will er flexibel etwa auf »special.example.org« den SSH-Server auf Port 1022 betreiben, schreibt er die Node-Konfiguration um:

node spezial.example.org {
  $openssh_port = 1022
  include openssh
}

Im dazu passenden Template setzt er den definierten Wert durch »Port <%= openssh_port %>« in den Text ein: Die Templates nutzen ERB, die Templating-Sprache, die auch Ruby verwendet Den Platzhalter ersetzt Puppet beim Umsetzen der Konfiguration. Anstatt eine statische Konfigurationsdatei »source => \’puppet:///openssh/sshd_config\’« zu verwenden, modifiziert der Systemverwalter Zeile 20 zu »content => template(\’openssh/sshd_config\’)« als Attribut der Ressource. Das dazu passende Template legt er unter »/etc/puppet/modules/openssh/templates/sshd_config« an. Wünscht er keine weiteren variablen Elemente, entspricht dies Listing 6 bis auf die Port-Angabe.

Die Attribute »notify« und »require«, die in der Definition von »/etc/ssh/sshd_config:« in Listing 3 vorkommen (Zeilen 23 und 24), lassen sich im Kontext des nächsten Eintrags gut verdeutlichen. Der Code »service{sshd: …}« ab Zeile 27 legt fest, dass Puppet den SSH-Server starten soll (»ensure => running«, ab Zeile 28). Voraussetzung dafür sind jedoch drei Bedingungen, die das Attribut »require« (Zeile 29) abbildet: Das Paket »openssh« muss erstens installiert sein (»Package[\’openssh\’]«), die Datei »/etc/ssh/sshd_config« muss definierten Inhalt haben (»File[\’/etc/ssh/sshd_config\’]«) und der User »sshd« muss existieren (»User[sshd]«).

In gleicher Art und Weise definiert die »openssh«-Klasse für die Datei »/etc/ssh/sshd_config« mit »require => File[\’/etc/ssh\’]« (Zeile 24), dass Puppet erst sicherstellen soll, dass »/etc/ssh« als Verzeichnis existiert, bevor es die darin liegende Konfigurationsdatei für den SSH-Server anlegt. Sowohl für das Verzeichnis »/etc/ssh« als auch den User »sshd« fordert die Klasse, dass das »openssh«-Paket installiert sein muss (Zeilen 10 und 17). Puppet arbeitet die Einträge nicht zwangsläufig sequenziell ab, deshalb ist das »require«-Attribut wichtig, um den reibungslosen Ablauf der Konfiguration in der korrekten Reihenfolge zu gewährleisten.

Für die Service-Definition spielen die Attribute »subscribe« und »notify« eine wichtige Rolle. Läuft schon ein SSH-Server, wenn Puppet die Konfiguration umsetzt, soll das Tool ihn neu starten, damit er die geänderte »sshd_config« einliest. Puppet tut dies aber nicht von sich aus, wenn der Dämon schon läuft und der Administrator mit »ensure => running« nur fordert, dass der Dienst läuft. Das Attribut »notify => Service[sshd]« (Zeile 23) veranlasst in dieser Situation, dass Puppet bei einer sich ändernden Konfiguration den Dienst neu startet. Umgekehrt legt der Administrator für den SSH-Service mit »subscribe => User[sshd]« fest, dass die Service-Ressource auf Veränderungen bei der User-Ressource reagiert.

Erfolge ernten

Vorsichtige Admins unterziehen das neue Manifest mit der Option »–noop« einem Test, ohne wirklich Hand an das System zu legen (siehe Abbildung 3). Abbildung 4 zeigt, wie Puppet die in Listing 3 festgelegte Konfiguration umsetzt: Nachdem es das Paket installiert hat, verzichtet es darauf, den Benutzer »sshd« anzulegen, da der Paketmanager dies bereits erledigt hat. Dafür aktualisiert Puppet die vom Paket gelieferte Konfiguration in »/etc/ssh/sshd/sshd_config«. Es meldet den Hashwert der neuen Datei »a15c…« und legt ein Backup der alten Datei unterhalb von »/var/lib/puppet/clientbucket« an. So gehen keine Daten verloren, der SSH-Server ist bereit.

Ein einzelnes, ihm vertrautes Paket konfiguriert ein erfahrener Systemverwalter auf klassischem Wege vermutlich schneller, dennoch leistet Listing 3 Beachtliches: Bringt es doch die dahinterstehenden Befehle aus ihrer flüchtigen in eine handhabbare, versionierbare Form.

Abbildung 3: Puppet informiert mit der Option »--noop« über anstehende Änderungen und Aktivitäten, bevor es sie im System anwendet.

Abbildung 3: Puppet informiert mit der Option »–noop« über anstehende Änderungen und Aktivitäten, bevor es sie im System anwendet.

Abbildung 4: Puppet aktiviert den SSH-Server, der in dem Default-Node »site.pp« steht und den die Klasse in »modules/openssh/manifests/init.pp« konfiguriert.

Abbildung 4: Puppet aktiviert den SSH-Server, der in dem Default-Node »site.pp« steht und den die Klasse in »modules/openssh/manifests/init.pp« konfiguriert.

Lohn der Mühe

Die Konfigurationen lassen sich auch mit anderen Admins austauschen – bei der wachsenden Zahl an Paketen und Rechnern, die jeder verwaltet, zunehmend wichtiger. Derzeit entstehen einige interessante Sammlungen an Puppet-Modulen [5], [6]. Auch komplexe Konfigurationen ganzer Groupware-Server wie Kolab [7] verwaltet Puppet [8].

Einzelne Nodes in einer Datenbank oder einem LDAP-Verzeichnis zu verwalten ist besonders für Netzinstallationen praktisch. Gerade dort spielt das Tool seine Stärken aus, der demonstrierte Aufruf über das Kommandozeilentool »puppet« dient eher zum Testen. Ein Puppet-Master-Host liefert die Konfigurationen für eine Anzahl Clients SSL-gesichert aus. Auf dem Server startet der Admin dazu »/etc/init.d/puppetmaster start« auf dem Client »/etc/init.d/puppetd start«.

Haben sich die Clients das erste Mal beim Master angemeldet, listet »puppetca –list« ausstehende Zertifikatsanfragen. Sind sie authentisch, erzeugt der Admin mit »puppetca –sign Client« Zertifikate.

Guter Mittelweg

Mehrere Werkzeuge nehmen für sich in Anspruch, eine neue Generation des Konfigurationsmanagments zu avisieren. Puppet geht mit seiner Sprache einen Mittelweg zwischen Abstraktion von spezifischen Plattformfragen einerseits und pragmatischer Notation andererseits. Alle Aspekte verschiedener Betriebssysteme lassen sich derzeit noch nicht vollständig abstrahieren, Know-how über den heterogenen Rechnerpark müssen Admins weiter mitbringen. Sie haben mit Puppet jedoch ein Tool zur Hand, das hilft diese Arbeit besser zu strukturieren. Jeder Schritt in Richtung einer gemeinsamen, verständlichen Sprache für die Administration ist wertvoll.

Google auch

Puppet wartet mit einer leicht und schnell erlernbaren Sprache auf, selbst wenn ihre Syntax an machen Stellen anfangs tückische Fallen für Anfänger stellt. Das ist wohl der Tatsache geschuldet, dass die Software noch recht jung ist. Der Funktionsumfang beeindruckt jedoch: Der Betrieb skaliert von wenigen Einzelsystemen bis zu mehreren Tausend verwalteten Rechnern in einem Netzwerk. Auch die Liste der Firmen, die Puppet derzeit verwenden, kann sich sehen lassen [9]. So spricht die Tatsache, dass selbst Google das Werkzeug für die Administration mehrerer Tausend Rechner nutzt, dafür, dass Puppet eine goldene Zukunft hat. (mg)

Infos

[1] Blog des Puppet-Autors:[http://www.madstop.com]

[2] Ruby: [http://www.ruby-lang.org]

[3] Puppet-Wiki: [http://reductivelabs.com/trac/puppet/wiki]

[4] Typen von Puppet: [http://reductivelabs.com/trac/puppet/wiki/TypeReference]

[5] Puppet-Module von David Schmitt:[http://git.black.co.at]

6] Puppet-Rezepte und -Module:[http://reductivelabs.com/trac/puppet/tags/puppet%2Crecipe]

[7] Kolab-Projekt: [http://www.kolab.org]

[8] Kolab-Server-Konfiguration: [http://github.com/wrobel/pardalys/tree/master]

[9] Puppet-Nutzer: [http://reductivelabs.com/trac/puppet/wiki/WhosUsingPuppet]

Der Autor

Gunnar Wrobel ist Gründer des Unternehmens Pardus und bietet Hosting von Kolab-Groupware-Servern an, an denen er aktiv entwickelt. Über seine Lieblingsdistribution hat er das Buch “Gentoo Linux” bei Open Source Press geschrieben.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 5 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben