Ansible Teil 2 - Facts und Jinja2 Filter

Im ersten Teil der kleinen Ansible Serie ging es um die Installation von RPM Paketen für die Oracle Software. Der nächste Schritt wäre nun der Aufbau einer Datenbank mit dem Database Configuration Assistant (DBCA) der Firma Oracle. Es macht aber Sinn, vorab zum besseren Verständnis die Punkte Ansible Facts und Jinja2 Filter zu behandeln.

In unserem Demo-Beispiel soll es später die Möglichkeit geben, die Hauptspeicher-Grösse der Datenbank-Instanz in Megabyte als Variable zu definieren. Ist kein Wert explizit gesetzt, soll als Default-Wert 50% des verfügbaren Hauptspeichers genutzt werden. Für die praktische Umsetzung gibt es in Ansible Standard-Facts die automatisch vom Zielsystem ermittelt werden. In unserem Fall das Fact ansible_memtotal_mb das wir als Grundlage für eigene Berechnungen nutzen könnten:

- name: calculate totalMemory parameter for dbca
  debug:
    msg: "{{ oracle_memory | default((ansible_memtotal_mb / 2) | round | int) }}"

Zusätzlich möchten wir den DBCA später nur starten, wenn auf dem Zielsystem noch keine entsprechende Datenbank vorhanden ist. Ein Indikator für diese Erkennung ist der Inhalt der Datei /etc/oratab. In diesem Fall ist die Verarbeitung etwas aufwendiger, zunächst könnten wir den Inhalt von /etc/oratab in Ansible verfügbar machen:

- name: Slurp /etc/oratab
  ansible.builtin.slurp:
    src: /etc/oratab
  register: slurped_etc_oratab

Aus diesem Inhalt können wir dann mit Jinja2 Filtern eine Liste der ORACLE_SIDs herausfiltern:

- name: Extract list of Oracle SIDs from slurped /etc/oratab
  ansible.builtin.set_fact:
    oracle_sids: "{{ slurped_etc_oratab.content | 
                     ansible.builtin.b64decode | 
                     ansible.builtin.regex_findall('^((?!#).+):.*:.*$', multiline=True) }}"

Die so ermittelten ORACLE_SIDs könnten wir für Debug-Zwecke in einer Schleife ausgeben lassen:

- name: Iterate over list of Oracle SIDs and display them
  ansible.builtin.debug:
    msg: "{{ item }}"
  loop: "{{ oracle_sids }}"

Um zu verhindern, dass der DBCA gestartet wird, wenn die gewünschte ORACLE_SID schon in der /etc/oratab vorhanden ist, könnten wir den späteren Task mit folgender when-Bedingung versehen:

- name: Only execute if given SID is not in extracted Oracle SID list
  ansible.builtin.debug:
    msg: "Database {{ oracle_sid }} is not in list..."
  when: oracle_sid not in oracle_sids

Ansible Teil 1 - YUM und RPM-Pakete

In einem der letzten Blog-Posts hatte ich gezeigt, wie mit wenigen Schritten ein Yum-Repository für RPM-Pakete auf einem NAS angelegt werden kann. Nun soll es darum gehen, diese Pakete auf einem System mit Rocky Linux 9 via Ansible automatisiert zu installieren.

Einer der Grundbaustein von Ansible sind die Tasks in denen definiert wird, was auf einem System automatisiert werden soll. Glück für uns, für die Konfiguration von Yum-Repositories und die Installation von RPM-Paketen gibt es schon fertige Standard-Module.

Hier ein Beispiel für die YUM-Repository Konfiguration und die Installation der Oracle 23ai Free Edition:

- name: "Handle local YUM Repository"
  ansible.builtin.yum_repository:
    name: "local-yum-repo"
    baseurl: "{{ yum_repo_url }}"
    file: "local_yum_repo"
    description: "Local YUM Repo on NAS"
    enabled: yes
    gpgcheck: no
    proxy: "_none_"
    state: "present"

- name: Install Oracle RPM Packages for 23ai
  ansible.builtin.dnf:
    name: 
      - oracle-database-preinstall-23ai
      - oracle-database-free-23ai
    state: installed

In weiteren Blog-Posts werde ich zeigen, wie sich im Anschluss eine Oracle Datenbank erstellen lässt und welche weiteren Bausteine für eine Ansible Automatisierung notwendig sind.

Lokales YUM-Repository für eigene RPM-Pakete

Speziell für eigene RPM-Pakete bietet sich ein YUM-Repository im LAN an. Damit lässt sich die Installation von RPM-Paketen automatisieren und man spart sich - gerade bei großen Paketen - den Download aus dem Netz.

Für Test- und Entwicklungszwecke hat sich bei mir ein NAS der Firma Synology bewährt. Als Basis dient ein NFS-Share und der "Web Station"-Service. So lassen sich die RPM-Pakete bequem von meiner Workstation auf das NAS schieben. Das YUM-Repository lässt sich im zweiten Schritt mit dem Befehl createrepo anlegen oder aktualisieren.

Zunächst die gewünschen RPM-Pakete auf das NFS-Share verschieben / kopieren:

mv oracle-database-preinstall-23ai-1.0-2.el9.x86_64.rpm /media/web/
mv oracle-database-free-23ai-1.0-1.el9.x86_64.rpm /media/web/

Falls noch kein YUM-Repository existiert:

createrepo /media/web/

Nach diesem Aufruf gibt es ein Unterverzeichnis repodata mit den entsprechenden Metadaten des Repository.

Um die Metadaten nach dem Hinzufügen von Paketen zu aktualisieren reicht ein:

createrepo --update /media/web/

Gitea - Git Versionsverwaltung in einfach

Die Versionsverwaltung Git hat sich über die letzten Jahre als Standard in der Softwareentwicklung etabliert. Die meisten Kunden setzten hierbei auf Gitlab oder Bitbucket von der Firma Atlassian. Gitea bietet eine schlanke Alternative gerade für das Selbsthosting von kleinen Repos.

Nachdem Amazon AWS den Dienst CodeCommit im letzten Jahr eingestellt hat, bot sich der Wechsel zu einem selbstgehosteten Gitea unter Proxmox an. Tipp, die Übernahme von Repos ist direkt über die Web-GUI von Gitea möglich:

In den nachfolgenden Dialogen besteht nun die Möglichkeit für verschiedene Repos eine einfache Migration durchzuführen. Hier als Beispiel der Dialog für CodeCommit:

Über diesen Weg lässt sich zumindest der Source Code inklusive der Commit-History bequem übernehmen. Deutlich mehr Aufwand wäre sicherlich notwendig, wenn angebundene DevOps-Prozesse z.B. für Build-Pipelines in eine neue Umgebung übernommen werden sollen.

Damit bietet Git die passenden Konzepte für die Herausforderungen der nächsten Zeit, wenn es darum geht, von Cloud A zu Cloud B, oder zurück nach On-Premise zu wechseln.

Die Corona-Warn-App - Das cwa-server Backend in einer Docker Umgebung

Da der Code der Corona-Warn-App nun komplett auf GitHub veröffentlicht wurde, ergibt sich nun für uns die Chance Möglichkeiten und Probleme in der Software-Entwicklung an einem sehr aktuellen Projekt live mitzuverfolgen. Mir ist bewusst, dass Tracking-Apps politisch und ethisch enormes Konfliktpotenzial bieten. Meiner Meinung nach wird der Nutzen derartiger Apps stark überschätzt und die entstehenden ethischen Probleme leider ignoriert. Erfolg oder Misserfolg werden sich wohl erst hinterher genau mit Fakten belegen lassen. Es gibt also einiges zu lernen! Im nachfolgenden Post möchte ich Euch zeigen wie Ihr das cwa-server Backend der App in einer Docker Umgebung aufsetzen könnt. Danach werden wir mit einem kleinen Python-Script einen Submission-Key an das Backend schicken und das Ergebnis in der Datenbank überprüfen.

CWA – Das cwa-server Backend in einer Docker Umgebung

[Durch Click auf die Play-Schaltfläche erfolgt ein Verbindungsaufbau zu YouTube/Google und es werden Daten an an diese externen Dienste übertragen]

Um Euch lokal eine passende Entwicklungs-Umgebung aufzubauen empfehle ich folgende Werkzeuge:

Sobald Ihr die Tools installiert habt, könnt Ihr den Code auch schon von GitHub clonen und die Umgebung mit Docker-Compose aufbauen lassen:

mkdir -p /c/cwa
cd /c/cwa
git clone https://github.com/corona-warn-app/cwa-server.git
cd cwa-server
docker-compose build
docker-compose up

Sobald die Docker Container laufen könnt Ihr z.B. mit einem eigenen kleinen Python-Client auf das Backend zugreifen. Der HTTP Body des POST-Request besteht in diesem Fall leider nicht aus klassischem Text oder Daten im JSON-Format. Für die API wird Protobuf von Google genutzt, dabei handelt es sich um ein schlankes binär Format. Für unseren Python-Client müssen wir zunächst mit den .proto-Files aus dem Source Code und dem Protobuf-Compiler von Google passende Python-Klassen bauen. Bei dieser Gelegenheit installieren wir auch gleich noch das Protobuf-Binding für Python. Evtl. müsst Ihr noch die Pfade an Eure Umgebung anpassen:

pip install protobuf
mkdir -p /c/cwa/cwa-client
cd /c/cwa/cwa-client
/c/tools/protoc/bin/protoc.exe --python_out=/c/cwa/cwa-client \
 --proto_path=/c/cwa/cwa-server/common/protocols/src/main/proto \
 app/coronawarn/server/common/protocols/external/exposurenotification/temporary_exposure_key_export.proto \
 app/coronawarn/server/common/protocols/internal/submission_payload.proto

Damit müsstet Ihr dann folgenden Python-Client ausführen können:

import http.client
from datetime import datetime, timedelta
from app.coronawarn.server.common.protocols.internal.submission_payload_pb2 import SubmissionPayload

submission = SubmissionPayload()
key = submission.keys.add()
key.key_data = bytes("FranksDemoKey222", "utf-8")
key.rolling_start_interval_number = int(((datetime.today() - timedelta(days=14)).timestamp())/600)
key.rolling_period = 144
key.transmission_risk_level = 4

conn = http.client.HTTPConnection('localhost', 8000)
headers = {'Content-Type': 'application/x-protobuf',
           'cwa-fake': '0',
           'cwa-authorization': 'edc07f08-a1aa-11ea-bb37-0242ac130002'}
conn.request("POST", "/version/v1/diagnosis-keys", submission.SerializeToString(), headers)
response = conn.getresponse()
print(response.status, response.reason)

Um auf die Postgres Datenbank zugreifen zu können, haben uns die CWA-Entwickler pgAdmin als Web-Oberfläche in einem eigenen Container mitgeliefert. Ihr müsst dort noch die Datenbank-Verbindung einrichten. Danach könnt Ihr mit folgendem SQL Statement prüfen ob unser POST-Request zu einem Eintrag in der Datenbank geführt hat:

SELECT encode(key_data, 'escape')
     , rolling_period
	 , rolling_start_interval_number
	 , submission_timestamp
	 , transmission_risk_level
	FROM public.diagnosis_key
	WHERE key_data like 'FranksDemoKey%';