Über unsMediaKontaktImpressum
Jonas Verhoelen 10. April 2018

Implementierung einer Blockchain-Anwendung mit Hyperledger Fabric und Composer

Am Thema Blockchain führt aktuell kein Weg vorbei. Bitcoin ist immer wieder in aller Munde und es erscheinen täglich weitere Namen und Projekte auf der Bildfläche. Doch abgesehen von Währungen gibt es noch mehr und sogar interessantere Anwendungsfälle. Somit entstehen auch viele neue Software-Ökosysteme. So auch das Projekt Hyperledger, das ein beachtlich modulares Blockchain-Framework liefert. Wir schauen uns einmal an, wie einfach Use Cases mit Hyperledger Fabric und Composer implementiert werden können.

Blockchain mit Open Source: Hyperledger

Hyperledger ist der Name eines Regenschirm-Projektes, unter dem Open Source-Blockchain-Ansätze und -Tools kollaborativ entwickelt werden. Es wurde im Jahr 2015 von der Linux Foundation gestartet und erfreut sich reger Beteiligung von Software-Riesen wie IBM, Intel und Fujitsu sowie einer großen Community. Die Repositories der GitHub-Organisation von Hyperledger weisen aktuell eine stärkere Aktivität denn je auf [1]. An der Entwicklung kann potentiell jeder teilnehmen.

Dabei wird nicht nur ein einzelnes Blockchain-Framework bzw. eine einzelne Blockchain-Plattform entwickelt. Vielmehr liegt der Fokus darauf, mehrere Ansätze parallel zu verfolgen, wobei bei der Entwicklung Synergien und wiederverwendbare Komponenten entstehen. Ein Blockchain-Netzwerk aus Sicht von Hyperledger-Konzepten ist nicht mit Vertretern der Kryptowährungen wie Bitcoin oder Ethereum vergleichbar. Vielmehr werden Knoten der Netzwerke bei Hyperledger über die teilnehmenden Organisationen verteilt, wodurch es besonders für Private bzw. Permissioned Networks interessant ist. Gedanken bezüglich Proof of Work (dem klassischen Mining), Proof of Stake und weiteren Konsens-Mechanismen aus der Welt der Public Blockchains können wir zunächst ausblenden. Die involvierten Organisationen validieren einfach gegenseitig ihre Transaktionen und profitieren als Konsortium davon, dass das Netzwerk aufrechterhalten wird. Dies löst auch weitestgehend Skalierbarkeitsprobleme auf (die wir vom Bitcoin-Netzwerk kennen) und macht einen hohen Transaktionsdurchsatz möglich.

Die unterschiedlichen Blockchain-Ansätze des Project Hyperledgers sind Fabric, Burrow, Iroha und Sawtooth. Mit allen davon können Permissioned Blockchains entwickelt werden, jedoch verfolgt jeder dieser Ansätze einen fundamental anderen Weg. Wir werden uns in diesem Artikel näher mit Fabric beschäftigen, da es die aktivste Community hat und die flexibelste der Varianten ist. Fabric ist durch seine starke Modularisierung universell nutzbar. "You could think of Hyperledger Fabric kind of like an Apache Web Server", so Brian Behlendorf, Executive Director von Hyperledger bei der Linux Foundation. Die anderen Ansätze sind eher für die Implementierung von speziellen Fällen in eingeschränkten Kontexten gedacht.

Hyperledger Fabric – Die verteilte Blockchain-Plattform

Mit Fabric als Plattform lassen sich völlig individuelle Distributed-Ledger-Lösungen entwickeln. Fabric beinhaltet Konzepte, die durch Code weitestgehend frei implementierbar sind. Als Basis für ein Blockchain-Netzwerk wird die Modellierung der angestrebten Organisationsstruktur genommen. Jeder Teilnehmer hat eine feste Identität und kann sich durch vergebene Zertifikate identifizieren. Neben der Authentifizierung kann darüber auch Autorisierung abgebildet werden, die in Permissioned Blockchains die Aspekte von Privatsphäre und Vertraulichkeit implementiert. Für die Verwaltung von Zertifikaten und Teilnehmern kann auch die Fabric Certificate Authority (vor Version 1.0 Membership Service Provider [2]) genutzt werden.

Die Definition von Assets (Gegenständen, die auf der Blockchain verwaltet werden sollen) obliegt ganz allein der Anwendung. Diese Assets, z. B. Motorblöcke aus der Automobil-Industrie, werden durch ein Modell aus Schlüssel-Werte-Paaren im JSON- und/oder Binärformat definiert.

Zur Umsetzung von Business-Logik auf Basis der Assets und ihrer Besitzer ist das Konzept des Chaincodes gedacht. Hiermit können in den Programmiersprachen Go, Java oder JavaScript Regeln implementiert werden, die Leserechte oder Modifikationen von Assets definieren. Die Ausführung einer Chaincode-Funktion kann Assets auslesen und zurückgeben und/oder Assets anlegen sowie modifizieren und diese im lokalen Ledger speichern. Nach der lokalen Persistierung der Änderungen auf einem Knoten werden die Änderungen dem Netzwerk vorgeschlagen ("Endorsement") und nach Akzeptanz der anderen Organisationen in die Blockchain eingefügt. Chaincode lässt sich im Kontext von Ethereum mit Smart Contracts vergleichen.

Um verschiedene Bereiche der Privatsphäre zu implementieren, werden Channels genutzt. Im simpelsten Szenario wird der gesamte Chaincode auf einem einzigen Channel deployed, an dem alle Teilnehmer angeschlossen sind. Um aber abgekapselte Bereiche zu erstellen und nur ausgewählte Teilnehmer darin kommunizieren zu lassen, können zusätzlich Channel mit eingeschränkten Teilnehmergruppen konfiguriert werden. Pro Channel kann ebenfalls unterschiedlicher Chaincode deployed werden, sodass auch funktional eine Isolation erreicht werden kann. Zusätzlich kann die Kommunikation im Channel teilweise oder komplett mit AES verschlüsselt werden.

Als Ergebnis wird in jedem Channel ein verteiltes Ledger gepflegt, das man sich gut als Kassenbuch von verketteten Transaktionen vorstellen kann. Jeder Teilnehmer bewahrt für jeden Channel, in dem er Mitglied ist, eine Kopie des Ledgers auf. Somit entsteht korrekterweise eine Blockchain-Datenstruktur pro existierendem Channel im Netzwerk. Wie bei einer Blockchain üblich, werden die Transaktionen in Blöcken eingeschlossen gespeichert, die in einer einfach verketteten Liste zur verschlüsselten Kette werden. Um Client-Anwendungen individuelle Sichten auf die Daten der Ledger zu verschaffen, können jedoch selbst komplexe Leseanfragen gegen das Netzwerk ausgeführt werden. Dies ist aufgrund der genutzten dokumentorientierten Datenbank CouchDB möglich [3]. Somit wird Clients, die sich zu Fabric-Netzwerken verbinden, flexibler Datenzugriff geboten.

Zusätzliche Konzepte durch das Composer-Framework

Composer ist eines der Tools aus dem Hyperledger-Ökosystem [4]. Man kann es sich als Framework für Fabric vorstellen. Es ist praktisch, wenn nicht sogar obligatorisch, wenn man Fabric-Netzwerke entwickeln, aufbauen und administrieren möchte. Es führt auf der Grundlage von Fabric weitere Konzepte ein, um den Umgang mit Fabric stark zu vereinfachen.

Neben Assets kann damit auch das Schema von Netzwerkteilnehmern, Transaktionen und Events in der Composer Modeling Language definiert werden [5]. Transaktionsabläufe für jede Transaktionsart werden auf einer einfachen API durch JavaScript-Code implementiert. Mit Access-Control-Files können die Zugriffsrechte für Teilnehmer auf bestimmte Ressourcen begrenzt werden. Häufig genutzte Queries auf den Daten im Ledger können in der Composer Query Language, einer SQL-ähnlichen Sprache, definiert werden werden [6].

Alle benötigten Dateien müssen anschließend zu einer BND (Business Network Definition) in eine .bna-Datei paketiert werden. Dieses Archiv kann dann auf ein bestehendes Fabric-Netzwerk installiert werden. Der Sourcecode für BNDs kann natürlich lokal im von uns bevorzugten Editor entwickelt sowie getestet und somit über Git versioniert werden. Für Prototyping- und Demo-Zwecke gibt es den Composer Playground. Dieser liefert ein modernes, übersichtliches und intuitiv nutzbares Webinterface, das ebenso auf die Konfigurationen der Composer CLI zugreift. Mit dem Playground kann man also auf komfortable Art und Weise BNDs erstellen, installieren, testen, editieren, importieren und exportieren.

In dem auf BlueMix gehosteten Composer Playground [7] kann man anwenderfreundlich und ohne viel Vorwissen aus Beispielanwendungen (z. B. Fahrzeuglebenszyklus, Automobil-Auktion oder Tracking von Nutztieren) ein neues Business-Network installieren, es modifizieren und testen. Selbiges lässt sich nach Einrichtung der Tools auch lokal durchführen, sodass wir den gehosteten Playground nach kurzem Ausprobieren wieder verlassen können. Der Playground eignet sich hervorragend dafür, Ideen anhand von Prototypen zu validieren und ein Gefühl für das zugrundeliegende Composer- & Fabric-Modell zu bekommen.

Beispiel: Supply-Chain-Tracking von Motorblöcken

Zur Umsetzung eines Private Blockchain-Netzwerkes mit Hyperledger Fabric stellt man sich als Beispiel das Tracking von Motorblöcken aus der Automobilindustrie vor. In diesem Fall gibt es Hersteller und Händler als Netzwerkteilnehmer. Motoren und die Fahrzeuge, in denen sie eingebaut wurden, werden als Assets abgebildet. Die Firmen der Hersteller und Händler werden als Organisationen im Netzwerk eingeführt und identifiziert.

Der Fabric Chaincode soll folgende Funktionalität zur Verfügung stellen:

  1. Herstellung eines Motorblocks mit eindeutiger Seriennummer;
  2. Übertragung eines Motorblocks nach der Produktion an einen Händler;
  3. Aufnahme eines Fahrzeuges mit dessen Seriennummer;
  4. Einbau eines Motorblocks in ein Fahrzeug.

Zunächst installiert man die benötigten Tools und setzt das Projekt auf.

Entwicklungsumgebung einrichten und Projekt erstellen

Zuerst müssen alle Voraussetzungen für Fabric installiert sein, diese werden ausführlich von Hyperledger dokumentiert [8]. Danach installieren wir noch die Voraussetzungen für Composer [9] sowie Composer und seine verwandten Tools selbst [10].

Dann machen wir uns am besten noch schnell mit der neuen Umgebung vertraut. Wenn wir der Anleitung des letzten Links genau gefolgt sind, liegt nun fabric-tools in unserem Home-Directory. Mit den beschriebenen Skripten können wir ein einfaches Fabric-Netzwerk in Docker-Compose starten, uns Peer-Admin-Zugriff verschaffen und es wieder stoppen und löschen. Wir führen zuerst Folgendes aus, um die Docker-Images der Version 1.1 (1.0 ist für dieses Beispiel nicht ausreichend) herunterzuladen und das Netzwerk zu starten:

export FABRIC_VERSION=hlfv11 && ./downloadFabric.sh && ./startFabric.sh

Während das Netzwerk läuft, kann per composer-playground die Composer Web-UI gestartet werden. Sie nutzt alle verwalteten Konfigurationen der composer-cli mit und setzt damit auf dem laufenden Fabric-Netzwerk auf. Ab jetzt sehen wir Fabric eher nur noch als eine konfigurierbare Plattform/ Infrastruktur, deren Zustand wir mit geeigneten Tools verändern.

Umsetzung der Funktionalitäten

Nun legen wir unser BND-Projekt in einem Verzeichnis unserer Wahl an. Für Yeoman, einen Code-Generator zum Aufsetzen von Projekten mittels Templates, vergleichbar mit Maven Archtypes, gibt es auch ein Template (hyperledger-composer:businessnetwork [11]). Ich habe allerdings schon ein Repository vorbereitet, in dem wir nun auch JavaScript ES6 und einige zusätzliche Tools nutzen können [12]. Das Repository verfügt über einen Start-Branch (initial), mit dem wir starten können. Der Master-Branch verfügt über die finale Version, die wir am Ende betrachten können. Wir klonen das Repository und selektieren erst einmal den Start-Branch.

git clone -b initial git@github.com:jverhoelen/fabric-composer-engine-supplychain.git

Nun öffnen wir den Ordner im Editor unserer Wahl. Für Composer eignet sich Visual Studio Code sehr gut, da es über eine installierbare Erweiterung für Syntax Highlighting verfügt. Nach kurzem Anschauen fällt auf, dass es sich um ein NPM-Projekt handelt, also starten wir mit npm install, um alle Abhängigkeiten zu installieren. Mit npm test können wir die Unit-Tests ausführen, mit npm run lint den Codestyle testen, und mit npm run createArchive können wir das .bna-File, unsere Business-Network-Definition, erzeugen. Das probieren wir am besten gleich aus, um zu prüfen, ob alles funktioniert.

Danach machen wir uns mit der Projektstruktur vertraut. Im lib-Ordner liegen die JS-Files, die die Transaction Processor Functions implementieren. Diese Business-Logik wollen wir natürlich testen und legen unsere Unit-Tests dafür im test-Ordner ab. Model-Definitionen (Participants, Assets, Transactions usw.) liegen in models.

Wir wollen das angestrebte Blockchain-Network zuerst modellieren. Dazu löschen wir den Inhalt der Models-Datei und geben ihr danach in der ersten Zeile einen neuen Namespace:

namespace org.acme.enginesupplychain

Die Participants Hersteller und Händler bilden wir als solche ab und nutzen dabei gleich die Vererbung der Composer Modeling Language. Außerdem möchten wir, dass jeder Teilnehmer neben seinem Namen optional eine Adresse angeben kann. Diese lagern wir wiederum in ein Konzept aus:

participant Member identified by memberId {
    o String memberId
    o String name
    o Address address optional
}

participant Manufacturer extends Member {
}

participant Merchant extends Member {
}

concept Address {
    o String country
    o String city
    o String street
    o String streetNo
}

Danach führen wir die Assets unseres Netzwerkes ein, die Motorblöcke und Autos, in denen diese verbaut werden. Hierbei lernen wir, dass Assets und Participants aufeinander referenzieren können. Eine Referenz zeigt immer auf eine bereits existierende Ressource eines beliebigen Typs. Eigenschaften, die wir mit einem kleinen "o" beginnen, hängen immer an der Ressource selbst.

asset Engine identified by engineId {
    o String engineId
    o EngineProperties data

    --> Manufacturer manufacturer
    --> Car currentCar optional
    --> Merchant merchant optional
}

asset Car identified by carId {
    o String carId
    o String legalDocumentId
}

concept EngineProperties {
    o String brand
    o String model
    o Double horsePower
    o Double cubicCapacity
    o Integer cylindersAmount
}

Am Ende der Modellierung implementieren wir die Transaktionstypen, die auf den eingeführten Ressourcen ausgeführt werden können. Zu jedem der folgenden Transaction Models testen und implementieren wir nachher die dahinterliegende Transaktionslogik.

transaction EngineMerchantTransfer {
    --> Engine engine
    --> Merchant merchant
}

transaction EngineCarInstallation {
    --> Engine engine
    --> Car car
}

transaction EngineCreation {
    --> Manufacturer manufacturer
    o EngineProperties data
}

transaction CarCreation {
    o String legalIdDocument
}

Jetzt können wir damit beginnen, die Logik hinter den Transaktionen zu implementieren. Zuerst widmen wir uns der Erstellung eines Engine Assets. Ein Motor soll eine zufällig generierte ID im UUID-Format bekommen und soll immer von Anfang an einem Hersteller zugehörig sein. Wir leeren also die logic.js-Datei und fangen frisch an. Die Konstante modelsNamespace und die Funktion uuid definieren wir, da wir sie noch öfter brauchen werden. Danach folgt die Funktion createEngineAsset. Der Dokumentationsblock über der Funktion ist wichtig, damit Composer beim Paketieren erkennt, welcher Transaktionstyp damit implementiert wird. Diese Form der Dokumentation sollten wir uns für die Funktionen angewöhnen.

/* global getAssetRegistry getFactory */

const modelsNamespace = 'org.acme.enginesupplychain'
function uuid() {
    const s4 = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1)
    return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`
}

/**
* Creation of a Engine asset triggered by physical production.
* @param {org.acme.enginesupplychain.EngineCreation} tx - the transaction to create an engine
* @transaction
*/
async function createEngineAsset(tx) { // eslint-disable-line no-unused-vars
    const engineRegistry = await getAssetRegistry(modelsNamespace + '.Engine')
    const engine = getFactory().newResource(modelsNamespace, 'Engine', uuid())
    const engineData = getFactory().newConcept(modelsNamespace, 'EngineProperties')

    engine.data = Object.assign(engineData, tx.data)
    engine.manufacturer = tx.manufacturer

    await engineRegistry.add(engine)
}

In dieser Manier implementieren wir auch die weiteren Transaktionstypen EngineMerchantTransfer, EngineCarInstallation und CarCreation.

/**
* An engine is transfered to a merchant.
* @param {org.acme.enginesupplychain.EngineMerchantTransfer} tx - the engine transfer transaction
* @transaction
*/
async function transferEngineToMerchant(tx) { // eslint-disable-line no-unused-vars
    const engineRegistry = await getAssetRegistry(modelsNamespace + '.Engine')
    tx.engine.merchant = tx.merchant

    await engineRegistry.update(tx.engine)
}

/**
* An engine is installed in a car.
* @param {org.acme.enginesupplychain.EngineCarInstallation} tx - the engine into car installation transaction
* @transaction
*/
async function installEngineToCar(tx) { // eslint-disable-line no-unused-vars
    const engineRegistry = await getAssetRegistry(modelsNamespace + '.Engine')
    if (tx.car) {
        tx.engine.currentCar = tx.car
        await engineRegistry.update(tx.engine)
    } else {
        return Promise.reject('No target car was set on the transaction!')
    }
}

/**
* A car is created
* @param {org.acme.enginesupplychain.CarCreation} tx - transaction to create a new car
* @transaction
*/
async function createCar(tx) { // eslint-disable-line no-unused-vars
    const carRegistry = await getAssetRegistry(modelsNamespace + '.Car')
    const factory = getFactory()
    const carId = uuid()
    const car = factory.newResource(modelsNamespace, 'Car', carId)
    car.legalDocumentId = tx.legalIdDocument

    await carRegistry.add(car)
}

Das Unit-Testen der Funktionen selbst fällt relativ einfach aus. Lediglich das Bootstrapping der dafür benötigten Objekte wirkt noch etwas mit Boilerplate-Code überladen. Die Tests starten zuerst ein In-Memory-Fabric-Netzwerk, installieren das Business Network darauf und authentifizieren sich dann als Default-Admin dagegen. Hierfür liefert Composer die Libraries composer-admin, composer-client, composer-common und composer-connector-embedded. Nach dem Test-Setup können wir jetzt Testfälle gegen das Embedded-Netzwerk schreiben. Der Code für das Setup wurde aufgrund der Länge nicht ins Listing aufgenommen, kann aber auf dem Master-Branch in test/EngineSupplychainSpec.js angesehen und ausprobiert werden.

Die Unit-Testfälle zum Testen eines Transaktionstyps haben oft ein ähnliches Schema. Sie bauen eine Transaktion mit ihren Eigenschaften und Beziehungen nach, führen sie gegen das Netzwerk aus und überprüfen danach den Datenstand der beteiligten Assets und Participants. Den vorhandenen Testfall für createEngineAsset schauen wir uns stellvertretend einmal an.

describe(‘EngineSupplychainSpec’, () => {

   // Das Setup wird hier im before- und beforeEach Hook durchgeführt
   // Daraus resultieren bnc (BusinessNetworkConnection), namespace
   // Sowie Test-Assets und Participants und zugehörige Registries
   // Siehe ganze Datei: github.com/jverhoelen/fabric-composer-engine-supplychain/blob/master/test/EngineSupplychainSpec.js

   describe('createEngineAsset', () => {
       it('should create an Engine by submitting a valid EngineCreation transaction', async () => {
           const factory = bnc.getBusinessNetwork().getFactory()

           const engineCreationTrans = factory.newTransaction(namespace, 'EngineCreation')
           engineCreationTrans.data = factory.newConcept(namespace, 'EngineProperties')
           engineCreationTrans.data.brand = 'Audi'
           engineCreationTrans.data.model = 'Fancy engine model'
           engineCreationTrans.data.horsePower = 400
           engineCreationTrans.data.cubicCapacity = 4000
           engineCreationTrans.data.cylindersAmount = 10

           const manufacturerRegistry = await bnc.getParticipantRegistry(namespace + '.Manufacturer')
           await manufacturerRegistry.addAll([])
           engineCreationTrans.manufacturer = factory.newRelationship(namespace, 'Manufacturer', testManufacturer.$identifier)

           await bnc.submitTransaction(engineCreationTrans)

           const allEngines = await engineRegistry.getAll()
           allEngines.length.should.equal(2)
       })
   })
})

Die Herangehensweise zur Implementierung von Business Network Definitions in Hyperledger Composer sollte mit diesen Einblicken klar geworden sein. Zusätzlich kann eine BND noch ein paar weitere Dinge für uns definieren. In permissions.acl können mithilfe der Access Control Language noch die erwähnten Zugriffsbeschränkungen festgelegt werden [13]. Auch das Event- sowie das Query-Feature sind sehr nützlich und für viele Anwendungsfälle interessant.

Zum Schluss schauen wir uns den Lösungsbranch "master" an. In ihm sind alle genannten Anforderungen implementiert und getestet. Wir generieren jetzt mit npm run createArchive die fertige .bna-Datei, die danach im dist-Ordner liegt. Diese können wir jetzt zum Ausprobieren auf unserem lokalen Fabric-Netzwerk in den Composer Playground, den wir in der Konsole zuvor gestartet haben, importieren. Der Weg durch die Web-UI sollte dabei selbsterklärend sein, ist aber auch offiziell dokumentiert [14].

Zusammenfassung und Ausblick

Wir haben bereits einen bedeutenden Teil des Hyperledger-Projektes kennengelernt. Konkret wissen wir jetzt, dass Fabric als Blockchain-Plattform mit rudimentären Konzepten fungiert. Composer fügt dem viele wichtige Konzepte hinzu und macht es für Entwickler sehr angenehm, Blockchain-Netzwerke zu implementieren und sie zu verwalten. Durch den implementierten Anwendungsfall über die Produktion und die Verfolgung von Motorblöcken haben wir einen einfachen, aber mächtigen Use Case für Permissioned Corporate Blockchains kennengelernt.

Das resultierende Blockchain-Netzwerk haben wir zunächst nur lokal ausgeführt. Die Konfiguration der Peer-Organisationen und der Order-Organisation haben wir nicht erweitert. Wir könnten aber ohne viel Aufwand weitere Organisationen hinzufügen. Für ein Blockchain-Netzwerk, das über ein echtes Organisations-Konsortium gespannt werden soll, müssen wir noch ein paar Probleme lösen:

  • Wie verwalten wir die Organisationen und Peers?
  • Wie können Organisationen auch automatisiert neue Peer-Knoten zum Netzwerk hinzufügen?
  • Wir erhalten wir ein faires und homogenes Netzwerk, das Ausfällen trotzen kann?
  • Wie können Clients mit dem Netzwerk kommunizieren?

Die noch junge Plattform bietet schon einiges an Funktionsumfang und Komfort. Es sind allerdings noch einige Aufgaben zu bewältigen. Aus Entwicklersicht wirkt besonders der Unit-Test-Code noch recht aufgeblasen. Hier werden sicherlich bald Bibliotheken folgen, mit denen die üblichen Test-Muster einfacher umgesetzt werden können. Wir dürfen gespannt zuschauen, wie Projekte wie Hyperledger die Adaption von Distributed-Ledger-Technologien in der Wirtschaft weiter antreiben werden.

Troubleshooting

Stellen Sie sicher, dass alle Tools kompatibel für Fabric 1.1.x installiert wurden. Das bedeutet, dass alle Docker-Images dieser Version heruntergeladen sein müssen. Der Composer sowie der Composer-Playground sollten in Version v0.18.2 installiert sein. Alle Links zur Fabric- sowie Composer-Dokumentation im Artikel sind bewusst auf Fabric 1.1.x sowie Composer-Next fixiert.

Autor

Jonas Verhoelen

Jonas Verhoelen beschäftigt sich neben Agiler Softwareentwicklung im Microservice-Umfeld, Java, TypeScript, ReactJS und Kubernetes mit Blockchain und Distributed Ledger Technology.
>> Weiterlesen
Das könnte Sie auch interessieren
Kommentare (0)

Neuen Kommentar schreiben