Dokumentation als Code mit Ansible und Asciidoctor

Christoph Stoettner

stoeps@vegardit.com

@stoeps@infosec.exchange

Christoph Stoettner (stoeps)

  • Macht seit 30 Jahren was mit Computern

    • Amiga, OS/2, Linux

    • Beruflich auch Windows (wenn es sein muss)

  • Started with Linux / OSS around 1994/1995

    • Linux Kernel < 1.0

    • Slackware

  • mag vi, vim, neovim

    • zu doof für emacs

Mein (langer) Weg zur Dokumentation

  • Inhalt meiner Dokumentation

    • Konfiguration (XML, JSON, httpd.conf, GUI)

    • Teile von Property Dateien

    • Screenshots

  • Benutzte Software

    • MS Excel / Libreoffice (mit Dropbox)

    • MS Word / Libreoffice mit Sharepoint oder Mail

    • Verschiedene Wikis, Evernote

    • LaTex und Docbook    

  • Bild: Geek&Poke, CC-BY-3.0

gap documentation2

Notwendige Eigenschaften meiner Dokumentationslösung

  • Durchsuchbar / Volltextindex

  • Änderungen von allen Endgeräten

    • Smartphone, Tablet, Notebook, Customer Notebook

    • Server per SSH?

  • Vorlagen um Kundenanforderung oder CI umzusetzen

  • Offline Support

  • Versionskontrolle

  • Einstellungen aus XML und Property Dateien

  • Kein manuelles Kopieren von Dateien und Einstellungen

  • Automatisierte Erstellung von Ausgabeformaten

Warum kein Wiki?

  • Editieren mit mobilen Geräten kompliziert

  • Wo dokumentiert man das Wiki

    • Installation

    • Konfiguration

    • Kein Zugriff falls Wiki nicht läuft

  • Kein einheitlicher Syntax

  • Marketing, Projekt oder Kunde fordern of gedruckte Dokumentation mit Vorlage

  • UND jede Menge Copy & Paste

Markdown und reStructuredText

  • Ich habe mit Markdown begonnen

  • Immer mehr Tools unterstützen Markdown Syntax

  • Cool WYSIWIG editors

  • Sehr gut für mobile Notizen

  • Aber etwas zu einfach

    • Dateien einbinden nicht möglich

Wann brauche ich eine Dokumentation

  • Was mache ich?

    • Installation von Software auf Basis von Kubernetes und WebSphere Application Server

    • Jede Menge Pfade und Benutzer

    • Secrets, Passwörter und Datei-Konfiguration

      • XML, JSON, .properties …​

  • Inzwischen mache ich viele Installationen mit Ansible oder automatisiert mit Bash Skripten

  • Manuell sollte ein Kunde allein auch eine Installation neustarten, troubleshooten und administrieren können

    • Die benötigten Log-Pfade, Admin-URL, Kommandos zur Konfiguration sind systemabhänging

Asciidoc / Asciidoctor

Syntax - Überschriften

:numbered: (1)

== H2
=== H3
==== H4

:!numbered: (2)

== H2
=== H3
==== H4
1nummerierte Überschriften
2Überschriften ohne Nummerierung
headings

Heading above blocks and images

.Heading (1)
[source, bash]
----
ls -al
----

.Image (2)
image::test.png[role=fill]
1Heading above Code block
2Image heading
source heading
pngtest

Syntax - Lists

// some comment
def text():
    var = xyz
    x = x +1
==== Bullet Points

* Bullet Points
** Sub Bullet Point

==== Numbered Lists
. Numbered List
.. Sub 1
.. Sub 2
. Numbered 2

==== Definition Lists

CPU:: The brain of the computer.
Hard drive:: Permanent storage for operating system...
listings

Images

image::sunset.jpg[]
image::sunset.jpg[role=right] (1)
image::sunset.jpg[Image of a Sunset]     (2)
image:sunset.jpg[]            (3)
1CSS Rollen werden zum Element hinzugefügt
2Setzen alt-Text
3Inline Image
Ergebnis in HTML
<div class="imageblock"><img src="images/sunset.jpg" alt="sunset"></div>
<div class="imageblock right"><img src="images/sunset.jpg" alt="sunset"></div>
<div class="imageblock"><img src="images/sunset.jpg" alt="Image of a Sunset"></div>
<div class="paragraph"><p><span class="image"><img src="images/sunset.jpg" alt="sunset"></span></p></div>

Tabellen

[cols="1,1"]
|===
|Cell in column 1, row 1
|Cell in column 2, row 1

|Cell in column 1, row 2
|Cell in column 2, row 2

|Cell in column 1, row 3
|Cell in column 2, row 3
|===
[cols="1,1", options=header]
|===
|Cell in column 1, row 1
|Cell in column 2, row 1

|Cell in column 1, row 2
|Cell in column 2, row 2

|Cell in column 1, row 3
|Cell in column 2, row 3
|===

column 1, row 1

column 2, row 1

column 1, row 2

column 2, row 2

column 1, row 3

column 2, row 3

 

column 1, row 1column 2, row 1

column 1, row 2

column 2, row 2

column 1, row 3

column 2, row 3

Use Inline Icons

  • Jedes Font Awesome Icon kann gesetzt werden

  • Muss im Dokument-Kopf aktiviert werden

:icons: font
Some Examples
icon:twitter[] Twitter https://twitter.com[@stoeps]
icon:linux[] Linux Icon
icon:windows[] Windows Icon
icons

Admonition Blocks

  • Admonition Blocks (Warning, Caution, Important, Note, Tip)

  • Font Awesome Icons mit :icons: font

WARNING: This is a warning
This is a warning
CAUTION: This is a caution
This is a caution
IMPORTANT: This is important
This is important

Admonition Blocks in Html and PDF

NOTE: A note

TIP: Here is a tip

IMPORTANT: That's important

WARNING: Warning message

CAUTION: Caution, be careful
admonition html
HTML
admonition pdf
PDF

Menus, Keys and Buttons

:experimental: (1)
1Die folgenden Beispiele sind noch als Experimental markiert
.Copy Text
menu:Edit[Copy Special > Text]

.Button
Press kbd:[OK]

.Keyboard
kbd:[Ctrl+C] to stop this.
experimental
HTML
experimental pdf
PDF

Sourcecode

  • Adding [source]

 .A Python function
 [source, python]
 ----
 def function():
    var x = 10
    return x
 ----
src
HTML
src pdf
PDF

Including Files

  • Split longer documents and include Asciidoc Source

  • Include any type of files in [source]

  • Powerful

  • Include complete files

  • Include only parts

include::path/filename[] (1)
include::path/filename[lines=10..15] (2)
include::path/filename[tags=mytag] (3)
1Include entire file
2Include lines 10-15
3Include area between mytag tags

Example Include

<html><head>
<title>example</title>
<!-- tag::stoeps[] -->
<!-- even comments -->
</head><body>
<h1>Test</h1>
<!-- end::stoeps[] -->
</body>
</html>
Include in our asciidoc source
[source, indent=0]
----
include::test.html[tags=stoeps]
----
Included source
<!-- even comments -->
</head><body>
<h1>Test</h1>

Callouts in Sources

[source]
----
def function:
    x = 'secret' # <1>
    print(secret)
    return 0
----
<1> Hardcoded variable
def function:
    x = 'secret' (1)
    print(secret)
    return 0
1Hardcoded variable
callout html
HTML
callout pdf
PDF

Dokument Header

= Connections Update bei Example Ltd  (1)
:author: Christoph Stoettner          (2)
:email: stoeps@vegardit.com
:lang: de                             (3)
:revnumber: 1.6                       (4)
:revdate: 09.03.2024
:revremark: httpd.conf und TDI
:imagesdir: images                    (5)
:doctype: book
:source-highlighter: rouge            (6)
:icons: font                          (7)
:sectnums:                            (8)
:sectnumlevels: 5
:toc: left                            (9)
:toclevels: 4
1Title
2Autorenname und Mail
3Sprache
4Revision (Nummer, Datum, Info)
5Verzeichnis für Bilder
6Syntax Highlighter
7Font Awesome für Icons
8Nummerierung der Überschriften bis Level 5
9Inhaltsverzeichnis (bis Überschrift-Level 4)
Diagram

Plantuml - https://plantuml.com/

@startuml

skinparam rectangle {
	BackgroundColor DarkSeaGreen
	FontStyle Bold
	FontColor DarkGreen
}
skinparam usecase {
 	BackgroundColor Pink
 	FontColor DarkRed
 	FontStyle Bold
}
skinparam note {
	BackgroundColor LightYellow
	FontColor Black
}

:user: as u
(Robot Framework) as rf
(PSTT) as pstt
(Check Library) as chk

rectangle network as n1{
}

rectangle network as n2{
}

note as tc
	Test Case
	==
	Send...
	OnRecv...
	Get Data
	Check
end note

note as script1
	Script
	==
	Send...
	Recv...
	Get Data
end note

note as script2
	Script
	==
	Send...
	Recv...
	Get Data
end note

note as report1
  Report
  ==
  Data1
  Data2
  Data...
end note

note as report2
  Report
  ==
  Data1
  Data2
  Data...
end note

note as checklist
  Check List
  ==
  Check1
  Check2
end note

note as result
  Result
  ==
  Pass
  Fail
  Error
end note

u - tc
tc -> rf
rf . script1
script1 .> n1
n1 . script2
script2 .> pstt
pstt .. report1
n2 <. report1
report2 . n2
chk <. report2
rf -- checklist
checklist --> chk
result - chk
result --> u

@enduml
Diagram

Ditaa - https://ditaa.sourceforge.net/

                       +-------------+
                       |             |
                       | Exponential |
                       |             |
                       +-------------+
                              |
                       lambda |
                              v
+-------------+        +-------------+           +---------+
|             |   tau  |             |   lambda  |         |
|  Lognormal  |------->|    Gamma    |<----------| Poisson |
|             |        |             |---+       |         |
+-------------+        +-------------+   |       +---------+
      |                      ^ ^         | beta
      |                tau   | |         |
      | tau                  | +---------+
      |                +-------------+
      +--------------->|             |
                       |     Normal  |
                       |             |
                       +-------------+
Diagram

Mermaid - https://mermaid.js.org

gantt
    title A Gantt Diagram
    dateFormat YYYY-MM-DD
    section Section
        A task          :a1, 2024-03-17, 30d
        Another task    :after a1, 20d
    section Another
        Task in Another :2024-03-20, 12d
        another task    :24d
flowchart LR
    Start --> stop
Diagram
Diagram

D2 - https://d2lang.com/

costumes: {
  shape: sql_table
  id: int {constraint: primary_key}
  silliness: int
  monster: int
  last_updated: timestamp
}

monsters: {
  shape: sql_table
  id: int {constraint: primary_key}
  movie: string
  weight: int
  last_updated: timestamp
}

costumes.monster -> monsters.id
Diagram

D2 Flowchart

dogs -> cats -> mice: chase
replica 1 <-> replica 2
a -> b: To err is human, to moo bovine {
  source-arrowhead: 1
  target-arrowhead: * {
    shape: diamond
  }
}
Diagram

Asciidoctor Output

  • Builtin

    • HTML

    • XHTML

    • DocBook

    • Man page

  • Add-ons

    • PDF

    • EPUB3

    • Reveal.js

    • Bespoke

Asciidoctor installieren

  • Ruby Applikation

  • Es gibt aber auch

    • Java und Javascript Implementierungen

Ruby Packing
gem install asciidoctor
Bundler
bundle init
echo "gem 'asciidoctor'" > Gemfile
bundle
  • Installation mit Linux Paketmanager

    • Debian / Ubuntu:

      • sudo apt-get install -y asciidoctor

    • RPM basiert:

      • sudo dnf install -y asciidoctor

  • Docker und Podman

Vorteile des Containers - Enthaltene Applikationen

  • Asciidoctor 2.0.21

  • Asciidoctor Diagram 2.3.0 with ERD and Graphviz integration (supports plantuml and graphiz diagrams)

  • Asciidoctor PDF 2.3.13

  • Asciidoctor EPUB3 2.1.0

  • Asciidoctor FB2 0.7.0

  • Asciidoctor Mathematical 0.3.5

  • Asciidoctor reveal.js 5.1.0

  • AsciiMath

  • Source highlighting using Rouge, CodeRay or Pygments

  • Asciidoctor Confluence 0.0.2

  • Asciidoctor Bibtex 0.9.0

  • Asciidoctor Kroki 0.9.1

  • Asciidoctor Reducer 1.0.2

Beispiel

Einfache Asciidoc Datei example.adoc
= Chemnitzer Linux-Tage

Auch 2024 haben sich die Chemnitzer Linux-Tage einen Platz an einem März-Wochenende gesucht.
Also Kalender gezückt und den 16. und 17. März 2024 dick einkreisen! Es lohnt sich bestimmt.

https://chemnitzer.linux-tage.de/2024/de/info/eintritt/[Eintrittskarten] sind an der Tageskasse
erhältlich.

Wir freuen uns sehr, euch im März vor Ort in Chemnitz in gewohnter Umgebung wiederzusehen.
Über unsere Pressemitteilungen, Social Media könnt ihr euch diesbezüglich auf dem Laufenden halten.

image::images/martin-wettstein-4CVMWrWh3xU-unsplash.jpg[]

HTML erstellen

Konvertierung mit installiertem Asciidoctor
asciidoctor example.adoc
Konvertierung in Docker Container
docker run --rm \
       -u $(id -u):$(id -g) \
       -v $(pwd):/documents/:Z \
       asciidoctor/docker-asciidoctor \
       asciidoctor example.adoc
Podman
podman run --rm \
       -v /var/home/stoeps/devel/asciidoctor-presentations:/documents/:z \
       docker.io/asciidoctor/docker-asciidoctor \
       asciidoctor example.adoc

Ergebnis

2024 03 13 18 02 21

PDF

  • asciidoctor-pdf erstellt eine PDF Version

2024 03 13 18 02 22

Eigene Vorlagen

  • Wir werden uns vor allem auf PDF Ausgaben konzentrieren

  • aber mit pandoc kann aus den gegebenen Formaten praktisch jede Ausgabe erzeugt werden

    • pandoc kann auch in Office Formate mit Vorlagen konvertieren

  • DocBook ist xml und kann daher mittels xslt oder xmlto weiterverarbeitet werden

Bestehende Dokumentation

  • Doc und Docx können zu Asciidoc konvertiert werden

pandoc input.docx -f docx -t asciidoc --wrap=none --markdown-headings=atx \
  --extract-media=extracted-media -o output.adoc
clt pandoc word 2 adoc

PDF

Konvertieren von Asciidoctor zu PDF
asciidoctor-pdf documentation.adoc
Konvertieren mit Diagramm-Erstellung
asciidoctor-pdf -r asciidoctor-diagram documentation.adoc
Im Container
podman run \
      --rm \ (1)
      -v /var/home/stoeps/devel/asciidoctor-presentations:/documents/:z  \ (2)
      -v /var/home/stoeps/.asciidoctor/theme:/theme/:Z \
      docker.io/asciidoctor/docker-asciidoctor \
      asciidoctor-pdf \
      -a pdf-themesdir=/theme \ (3)
      -a pdf-theme=stoeps-theme.yml \
      example.adoc \
      -o example-vegardit-book.pdf
1Container nach beenden löschen
2Volume mounten (Dokumente mit Asciidoc und Theme für das Template)
3-a Dokument-Attribute setzen (könnte auch im Source erfolgen)

PDF mit Template/Theme

asciidoctor-pdf \
  -r asciidoctor-diagram \                   (1)
  -a pdf-themesdir=~/.asciidoctor/theme \    (2)
  -a pdf-theme=stoeps-theme.yml \            (3)
  "document.adoc"
pdftheme tree
1Diagramme erstellen
2PDF Template Verzeichnis
3Template Name

PDF theme

Erweitern des Standard Themes
extends: default
base:
  font-color: #FF0000
Eigenes Theme
page:
  layout: portrait
  margin: 20mm
  margin-inner: 25mm
  margin-outer: 20mm
  size: a4
base:
  font-color: #333333
  font-style: normal
  font-size: 12
  line_height_length: 14
  line_height: $base_line_height_length / $base_font-size

PDF Theme - Title

title-page:
  font-color: ffffff
  background-color: #1d4e89
  align: center
  logo:
    top: 10%
    image: image:images/{logo-image-name}[width=30%, align=center] (1)
  title:
    top: 30%
    font-size: $base_font-size * 4.25
    font-style: bold
    line-height: 0.9
  subtitle:
    font-size: $base_font-size * 2.00
    line-height: 1
  authors:
    margin-top: $base_font-size * 29.25
    font-size: $base_font-size * 1.5
  revision:
    margin-top: $base_font-size * 0.5
1{logo-image-name} ist eine Variable die im Asciidoc definiert wird

Beispieldokument mit Theme

2024 03 13 18 18 55
Default (doctype=article)
2024 03 13 18 28 12
doctype=book
= Chemnitzer Linux-Tage
:author: Christoph Stoettner
:email: stoeps@vegardit.com
:doctype: book
:logo-image-name: vegardit-logo.png

Ansible

  • Templates in rollen-name/templates

    • *.j2

    • vim Syntax highlighting kann z.B. mit Kommentar in Kopf- oder Fusszeile forciert werden

documentation.adoc.j2
// vim: syntax=asciidoc

Variablen in Ansible - yaml Definition

Einfache Variable
variable: Hallo
Liste
users:
  - root
  - stoeps
  - ansible

# alternative Schreibweise
users: ['root', 'stoeps', 'ansible']
Dictionary
stoeps:
  name: Christoph Stoettner
  mail: stoeps@vegardit.com
  group: wheel

# alternative Schreibweise
stoeps: {name: Christoph Stoettner, mail: stoeps@vegardit.com, group: wheel}

Kombination Liste und Dictionary

users:
  - stoeps:
    name: Christoph Stoettner
    mail: stoeps@vegardit.com
    group: wheel
  - ansible:
    name: Ansible User
    mail: ansible@example-domain.home
    group: ansible

Variablen und Default-Werte aus anderen Rollen

  • Variablen sind in environments/prod/group_vars/all/ definiert

    • aber nur die vom Default der Rollen abweichen

    • D.h. wir können aus einer Rolle Dokumentation nicht auf die Default-Variablen anderen Rollen zugreifen

  • Modul: ansible.builtin.include_vars

    • Kann yaml und json Variablen Definitionen einlesen

    • Rekursiv in Verzeichnissen möglich

Import von einzelnen Variablen Dateien aus den Rollen im Verzeichnis vars
- name: Include variables from other roles
  ansible.builtin.include_vars:
    file: '{{ item }}'
  with_items:
    - ../../../roles/hcl/connections/vars/main.yml
    - ../../../roles/third_party/ibm/wasnd/was-dmgr-full-sync-nodes/vars/main.yml
    - ../../../roles/third_party/ibm/wasnd/was-dmgr-config-ldap/vars/main.yml
    - ../../../roles/third_party/ibm/db2-install/vars/main.yml
    - ../../../roles/third_party/ibm/ihs/ibm-http-config-plgwct/vars/main.yml

Jinja2

  • https://jinja.palletsprojects.com

  • Template-Engine für Python

  • Platzhalter werden mit dynamischen Inhalten ersetzt

  • Unterstützt Schleifen, Bedingungen, Variablen, Filter und Funktionen

Jinja2 Syntax - Variablen und Kommentare

  • Einfügen von Variablen und Ausdrücken

    • {{ …​ }}

  • Kommentare

    • {# …​ #}

    • Mehr Lesbarkeit des Codes

Beispiel: Variable mit Jinja2
my_var: Hallo
{{ my_var }} Christoph
Hallo Christoph
Beispielkommentar, multiline
{#
  The code is
  documentation enough
#}

Jinja2 Syntax - Kontrollstrukturen

  • Jinja2 Code (Statements, Control structures)

    • {% …​ %}

for-Schleifen
{% for user in users %}
    {{ users[user].mail }}
{% else %}
    Kein User vorhanden!
{% endfor %}
if-Blöcke
{% if  users|length > 1 %}
    Viele User!
{% elif users|length == 1 %}
    Schon ein User!
{% else %}
    Bisher keine User!
{% endif %}
Variablen Definition
---
# defaults file for clt-jinja2
users:
  stoeps:
    name: Christoph Stoettner
    mail: stoeps@vegardit.com
    group: wheel
  ansible:
    name: Ansible User
    mail: ansible@example-domain.home
    group: ansible
Output for-Schleife
stoeps@vegardit.com
ansible@example-domain.home
Output if-Block
Viele User!

Jinja2 mit Ansible rendern

- name: Generate Asciidoc documentation from Jinja2 template
  ansible.builtin.template:          (1)
    src: documentation.adoc.j2       (2)
    dest: /tmp/documentation.adoc    (3)
    # owner: root
    # group: root
    # mode: '0600'
    # validate: /usr/sbin/sshd -t -f %s
    # backup: yes
1Ansible module
2Name des Quell-Dokuments (im Template Ordner der Rolle)
3Name des Ziel-Dokuments (Jinja2 Variablen ersetzt)

Jinja2 oder Asciidoctor Vorlagen

  • Es macht keinen grossen Unterschied

    • Um Teile von Source Code in die Dokumentation aufzunehmen, nehme ich Asciidoctor include

  • Variablen und Conditionals mache ich normalerweise im Jinja2

Ansible mit Asciidoctor

Erstellen einer neuen Rolle für die Dokumentation
$ ansible-galaxy init clt-documentation
$ tree clt-documentation
clt-documentation
├── defaults
│   └── main.yml
├── files
├── handlers
│   └── main.yml
├── meta
│   └── main.yml
├── README.md
├── tasks
│   └── main.yml
├── templates
├── tests
│   ├── inventory
│   └── test.yml
└── vars
    └── main.yml

defaults/main.yml - Asciidoctor Defaults

  • Definition von Default-Variablen und Option aus group_vars zu lesen

__migration_title: '{{ migration_title | default("Connections Update") }}'
__author_name: '{{ author_name | default("Joe Doe") }}'
__author_email: '{{ author_email | default("joe.doe@vegardit.com") }}'
__author_company: '{{ author_company | default("My Company Ltd") }}'
__customer: '{{ customer | default("Example Ltd.") }}'
__revdate: "{{ revdate | default(now(utc=true,fmt='%d.%m.%Y')) }}"
__revnumber: "{{ revision | default('1.0') }}"
__revhistory: "{{ revhistory }}"
__revremark: "{{ revremark | default('Describe latest change') }}"
__src_highlighter: "{{ src_highlighter | default('rouge') }}"

defaults/main.yml - Sammeln von Variablen aus anderen Rollen

  • Variablen die nicht direkt aus den anderen Rollen gefüllt werden können, kann man hier aufbereiten

  • Die hier verwendeten Variablen kommen aus den eingelesenen vars/main.yaml Dateien

__credentials:
  user1:
    function: database user
    name: lcuser
    password: "{{ __db2_users_password }}"
  user2:
    function: database user
    name: docsuser
    password: "{{ __db2_users_password }}"
  user3:
    function: DMGR Admin
    name: "{{ __was_username }}"
    password: "{{ __was_password }}"
  user4:
    function: HTTP Admin
    name: "{{ __ihs_username }}"
    password: "{{ ihs_password }}"
  user5:
    function: LDAP Bind
    name: "{{ __ldap_bind_user }}"
    password: "{{ __ldap_bind_pass }}"

Task: Verzeichnis für Dokumentation erstellen

Erstelle ~/doc-temp
- name: Create folder with documentation
  ansible.builtin.file:
    path: '{{ ansible_env.HOME }}/doc-temp'
    state: directory
  • Man kann noch abfangen was passieren soll, wenn der Ordner besteht

  • Temp Ordner mit random Name wäre eine Option

  • Backup vorhandener Dateien erstellen?

Task: Konvertierung im Container - Docker

  • Nach Möglichkeit die Module command und shell vermeiden

  • community.docker.docker_container

tasks/main.yml
- name: Generate with docker_container
  community.docker.docker_container:
    name: asciidoctor       (1)
    image: asciidoctor/docker-asciidoctor  (2)
    command: asciidoctor-pdf documentation.adoc -o /documents/container-doc.pdf  (3)
    auto_remove: true  (4)
    volumes:
      - '/{{ ansible_env.HOME }}/doc-temp:/documents:z' (5)
1Name des Containers (nicht relevant, siehe 4)
2Image des Containers
3Ausgeführtes Kommando
4Container nach Ausführung löschen
5Volumes mount

Task: Konvertierung im Container - podman

- name: Generate with podman container
  containers.podman.podman_container:
    name: asciidoctor
    image: docker.io/asciidoctor/docker-asciidoctor
    command: asciidoctor-pdf documentation.adoc -o /documents/podman-doc.pdf
    auto_remove: true
    volumes:
      - '/{{ ansible_env.HOME }}/doc-temp:/documents:z'

Template: Aufbereiten einer vorhandenen Dokumentation mit Jinja2

  • Dokumentkopf

  • Tabellen

  • Listen