Compare commits

..

6 Commits

32 changed files with 1525 additions and 11 deletions

7
.ansible-lint Normal file
View File

@ -0,0 +1,7 @@
---
rules: {}
parser:
ansible: true
warn_list: []
skip_list:
- yaml

27
.github-workflows-ci.yml Normal file
View File

@ -0,0 +1,27 @@
name: CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: pip install ansible ansible-lint
- run: ansible-lint -v
dryrun:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: pip install ansible
- run: ansible-playbook playbook/playbook.yml --check --list-tasks

8
.github/CODEOWNERS vendored Normal file
View File

@ -0,0 +1,8 @@
# CODEOWNERS definiert die Reviewer/Verantwortlichen für Verzeichnisse und Dateien
# Syntax: Pfad @owner1 @owner2
* @linux-admins @devops-team
playbook/roles/** @linux-admins
playbook/group_vars/** @linux-admins
docs/** @tech-writer
scripts/** @devops-team

9
.gitignore vendored
View File

@ -1,8 +1,5 @@
# Ansible caches
.ansible
.ansible_facts_cache
# Secrets
playbook/group_vars/vault.yml playbook/group_vars/vault.yml
.ansible_facts_cache/
.vscode/
.idea/
.env .env
*.retry

20
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,20 @@
image: python:3.11
stages:
- lint
- dryrun
variables:
PIP_DISABLE_PIP_VERSION_CHECK: '1'
lint:
stage: lint
script:
- pip install ansible ansible-lint
- ansible-lint -v
dryrun:
stage: dryrun
script:
- pip install ansible
- ansible-playbook playbook/playbook.yml --check --list-tasks

11
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,11 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- repo: https://github.com/ansible-community/ansible-lint
rev: v6.22.1
hooks:
- id: ansible-lint

44
.woodpecker.yml Normal file
View File

@ -0,0 +1,44 @@
branches:
include:
- main
steps:
lint:
image: python:3.11-slim
commands:
- pip install --no-cache-dir ansible ansible-lint
- ansible-lint -v
dryrun:
image: python:3.11-slim
commands:
- pip install --no-cache-dir ansible
- echo "$ANSIBLE_VAULT_PASSWORD" > .vault_pass
- ansible-galaxy collection install -r playbook/requirements.yml --force
- ansible-playbook playbook/playbook.yml --check --list-tasks \
--vault-password-file .vault_pass -i 'localhost,' -c local -e 'gather_facts=false'
secrets: [ANSIBLE_VAULT_PASSWORD]
run_preflight:
when:
event: [manual]
image: python:3.11-slim
commands:
- pip install --no-cache-dir ansible
- echo "$ANSIBLE_VAULT_PASSWORD" > .vault_pass
- ansible-galaxy collection install -r playbook/requirements.yml --force
- ansible-playbook playbook/playbook.yml --tags preflight -l pdp-portal \
--vault-password-file .vault_pass
secrets: [ANSIBLE_VAULT_PASSWORD]
run_patch:
when:
event: [manual]
image: python:3.11-slim
commands:
- pip install --no-cache-dir ansible
- echo "$ANSIBLE_VAULT_PASSWORD" > .vault_pass
- ansible-galaxy collection install -r playbook/requirements.yml --force
- ansible-playbook playbook/playbook.yml -l pdp-portal -e "target_clm_version=prod-2024-06" \
--vault-password-file .vault_pass
secrets: [ANSIBLE_VAULT_PASSWORD]

24
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,24 @@
# CONTRIBUTING
Danke für deinen Beitrag! Bitte beachte folgende Richtlinien:
## Branch/Commit
- Feature-Branches: `feature/<kurzbeschreibung>`
- Fix-Branches: `fix/<kurzbeschreibung>`
- Commit Messages nach Conventional Commits (feat:, fix:, docs:, chore:, refactor:, perf:, test:)
## Code-Style
- Ansible: ansible-lint muss grün sein (`make lint`)
- YAML: 2 Spaces, keine Tabs
## Tests
- Dry-Run: `ansible-playbook playbook/playbook.yml --check --list-tasks`
- App-spezifisch: `-l <app-gruppe>`
## Security
- Keine Secrets im Klartext, nutze `make vault-encrypt`
- PRs werden automatisch per CI geprüft (Linting, Dry-Run)
## Review
- CODEOWNERS regelt Reviewer
- Mindestens 1 Approval erforderlich

18
Makefile Normal file
View File

@ -0,0 +1,18 @@
SHELL := /bin/bash
.PHONY: deps run lint vault-encrypt vault-decrypt
deps:
./scripts/install_collections.sh
run:
./scripts/run_patch.sh $(APP) $(CLM) "$(EXTRA)"
lint:
ansible-lint -v
vault-encrypt:
ansible-vault encrypt group_vars/vault.yml
vault-decrypt:
ansible-vault decrypt group_vars/vault.yml

View File

@ -1,9 +1,18 @@
[defaults] [defaults]
inventory = playbook/inventories inventory = playbook/servicenow_inventory.yml
roles_path = playbook/roles roles_path = playbook/roles
collections_path = ~/.ansible/collections:./collections
host_key_checking = False
retry_files_enabled = False retry_files_enabled = False
stdout_callback = yaml stdout_callback = yaml
host_key_checking = False bin_ansible_callbacks = True
force_color = True forks = 25
forks = 10 interpreter_python = auto_silent
callback_whitelist = profile_tasks fact_caching = jsonfile
fact_caching_connection = .ansible_facts_cache
fact_caching_timeout = 86400
callbacks_enabled = profile_tasks, timer
[ssh_connection]
pipelining = True
ssh_args = -o ControlMaster=auto -o ControlPersist=60s

6
docs/CHANGELOG.md Normal file
View File

@ -0,0 +1,6 @@
# CHANGELOG
Alle Änderungen und Patchläufe werden hier automatisch dokumentiert.
Beispiel:
2024-06-01T12:00:00Z: Patch/Upgrade auf pdp-portal-server1.example.com (FQDN: pdp-portal-server1.example.com) durchgeführt. Ergebnis: OK

View File

@ -0,0 +1,148 @@
## Enterprise Auto-Upgrade Playbook für SLES & RHEL
### Übersicht
Modulares, Enterprise-taugliches Ansible-Playbook für automatisierte Upgrades/Patching von SLES und RHEL. Enthält:
- Automatische OS-Erkennung und OS-spezifische Rollen (RHEL/SLES)
- Upgrade nach Hersteller-Best-Practices (dnf/yum, zypper)
- VMware-Snapshots für Backup/Rollback (vCenter API)
- SUSE Manager API: dynamische CLM-Zuweisung via `target_clm_version`
- Wartungsfenster-Handling, Preflight-Checks
- Smoke-Tests (HTTP/Port/DB, inkl. Oracle SID/Listener-Erkennung)
- Self-Healing (Service-Restarts, Cleanup, Netzwerk)
- Compliance-Checks (OpenSCAP, Lynis)
- Logging, Mail-Benachrichtigung (mailx), optional Slack
- CI/CD via Gitea + Woodpecker (OAuth), Collections-Lint + Dry-Run
### Verzeichnisstruktur (Auszug)
```
os-upgrade-automation/
├── ansible.cfg
├── docs/
│ ├── README.md
│ ├── CHANGELOG.md
│ └── Runbook_SelfService.md
├── playbook/
│ ├── group_vars/
│ │ ├── all.yml
│ │ └── vault.yml # Mit Ansible Vault verschlüsseln
│ ├── host_vars/
│ ├── playbook.yml
│ ├── requirements.yml
│ ├── servicenow_inventory.yml # Dynamic Inventory (ServiceNow)
│ └── roles/
│ ├── common/
│ ├── preflight_check/
│ ├── vmware_snapshot/
│ ├── suma_api_assign_clm/
│ ├── rhel_upgrade/
│ ├── sles_upgrade/
│ ├── post_upgrade/
│ ├── smoke_tests/
│ ├── self_healing/
│ └── compliance_check/
├── scripts/
│ ├── install_collections.sh
│ └── run_patch.sh
└── .woodpecker.yml
```
### Voraussetzungen
- Ansible (empfohlen ≥ 2.14)
- Collections: `community.vmware`, `servicenow.servicenow`, `community.general`
- vCenter-Zugriff (API), SUSE Manager API-Zugang
- Zielsysteme: SLES oder RHEL (ggf. via SUSE Manager, venv-salt-minion)
- Ansible Vault für Secrets (zwingend empfohlen) [[Speicherpräferenz]]
Installation Collections:
```
make deps
```
### Konfiguration
1) Secrets sicher ablegen (Vault)
- `playbook/group_vars/vault.yml` befüllen (ServiceNow, SUSE Manager, vCenter, SMTP, Slack, DB)
- Datei sofort verschlüsseln:
```
ansible-vault encrypt playbook/group_vars/vault.yml
```
2) Globale Variablen prüfen
- `playbook/group_vars/all.yml` enthält Standardwerte (u.a. Skip-Flags, Mail-Empfänger, Wartungsfenster, Log-Verzeichnis).
3) Dynamic Inventory (ServiceNow)
- `playbook/servicenow_inventory.yml` nutzt den ServiceNow-Inventory-Plugin.
- Erwartet in Vault: `servicenow_instance`, `servicenow_user`, `servicenow_pass`.
### Nutzung (lokal)
- Lint:
```
make lint
```
- Playbook ausführen (Beispiele):
```
# App-Gruppe patchen, Ziel-CLM setzen
make run APP=pdp-portal CLM=prod-2024-06 EXTRA="debug_mode=true"
# Direkter Aufruf
./scripts/run_patch.sh pdp-portal prod-2024-06 "skip_compliance=true skip_smoke_tests=false"
```
Wichtige Extra-Variablen / Skip-Flags:
- `target_clm_version` (z.B. `prod-2024-06`)
- `debug_mode=true|false`
- `skip_vmware_snapshot`, `skip_suma_api`, `skip_post_upgrade`
- `skip_smoke_tests`, `skip_self_healing`, `skip_compliance`
- `upgrade_security_only=true|false`
### Rollenüberblick
- `preflight_check`: Diskspace, Wartungsfenster, Erreichbarkeit, Channel-Sync, pyVmomi-Check
- `vmware_snapshot`: Snapshot anlegen, optional Revert, optional Cleanup
- `suma_api_assign_clm`: System dynamisch CLM-Channel zuordnen
- `rhel_upgrade` / `sles_upgrade`: OS-spezifisches Upgrade, Kernel-Erkennung, Reboot-Flag
- `post_upgrade`: Reboot (optional), Health-Check kritischer Dienste
- `smoke_tests`: HTTP/Port/DB, Oracle (SIDs/Listener dynamisch erkannt)
- `self_healing`: Dienst-Restarts, Disk-Cleanup, Netzwerk-Remediation
- `compliance_check`: OpenSCAP, Lynis, Reporting & Mail
- `common`: Logging, mailx-Konfiguration, Admin-Mail inkl. Log-Anhänge, Slack (optional)
### VMware Snapshot & Rollback
- Vor dem Upgrade wird ein Snapshot mit Zeitstempel erstellt.
- Bei Fehlern kann automatisch ein Rollback getriggert werden (`rollback: true`).
- Optionales automatisches Löschen alter Snapshots nach erfolgreichem Lauf (`snapshot_cleanup`).
### Logging & Benachrichtigung
- Logs in `log_dir` (Standard: `/var/log/auto-upgrade`).
- Admin-Mail an `linux_admins_mail` mit Log-Summary + Anhängen.
- Failsafe-Mail an App- und Host-Kontakte bei Fehler.
- Optional Slack-Benachrichtigung (wenn `slack_enabled` und `slack_token`).
### CI/CD (Gitea + Woodpecker)
1) Repo in Woodpecker aktivieren
- In `https://ci.pp1l.de` auf “Repos” → “Sync” → `os-upgrade-automation` (oder `PurePowerPh1l/os-upgrade-automation`) “Enable”.
2) Secrets in Woodpecker setzen
- Mindestens `ANSIBLE_VAULT_PASSWORD` (für CI-Läufe ohne Interaktion).
- Optional: `SERVICENOW_*`, `SUMA_*`, `VCENTER_*`, `SLACK_TOKEN`, SMTP-Variablen, DB-Testdaten.
3) Pipelines
- `.woodpecker.yml` enthält: `lint`, `dryrun`, manuell `run_preflight`, `run_patch`.
- Hinweis: Interaktive Prompts (`--ask-vault-pass`) funktionieren in CI nicht. Nutze stattdessen eine Secret-basierte Lösung (z.B. Secret als Datei schreiben und `--vault-password-file` verwenden). Passe die Pipeline bei Bedarf an.
4) OAuth/Troubleshooting
- “Unregistered Redirect URI”: Stelle sicher, dass die Gitea-OAuth-App `https://ci.pp1l.de/authorize` als Redirect-URI nutzt.
- “PKCE is required for public clients”: Gitea-OAuth-App als Confidential Client anlegen.
- “registration_closed”: In Woodpecker `WOODPECKER_OPEN=false`, erlaubte Nutzer via `WOODPECKER_ADMIN=<user1,user2>` whitelisten.
### Sicherheit
- Keine Secrets im Klartext: Alles in `playbook/group_vars/vault.yml` ablegen und per Vault verschlüsseln [[Speicherpräferenz]].
- Zugriffsdaten in CI ausschließlich als Secrets verwalten.
### Nützliche Links
- Gitea: `https://git.pp1l.de`
- Woodpecker CI: `https://ci.pp1l.de`
- Ansible VMware Snapshot Modul: `https://docs.ansible.com/ansible/latest/collections/community/vmware/vmware_guest_snapshot_module.html`
- SUSE Manager: `https://documentation.suse.com/suma/`
Fragen oder Wünsche? Gerne melden.

View File

@ -0,0 +1,28 @@
# Self-Service Runbook für App-Owner
## Ziel
Dieses Runbook beschreibt, wie App-Owner das Auto-Upgrade-Playbook für ihre Systeme selbstständig ausführen können.
## Voraussetzungen
- Zugang zum Ansible-Server (SSH)
- Berechtigung für die gewünschte App-Gruppe im Inventory
- Vault-Passwort für verschlüsselte Variablen
## Schritt-für-Schritt-Anleitung
1. **Login auf dem Ansible-Server**
2. **Playbook für die eigene App-Gruppe ausführen:**
```bash
ansible-playbook -i inventory_apps playbook.yml -l <app-gruppe> --ask-vault-pass -e "target_clm_version=<channel>"
```
Beispiel für pdp-portal:
```bash
ansible-playbook -i inventory_apps playbook.yml -l pdp-portal --ask-vault-pass -e "target_clm_version=prod-2024-06"
```
3. **Ergebnis prüfen:**
- E-Mail-Benachrichtigung abwarten
- Logfiles im angegebenen Verzeichnis prüfen
## Hinweise
- Bei Fehlern wird automatisch ein Failsafe-Mail an die App- und Linux-Admins gesendet.
- Bei kritischen Fehlern erfolgt ein automatischer Rollback (VMware-Snapshot).
- Für Fragen oder Freigaben bitte an die Linux-Admins wenden.

View File

@ -0,0 +1,33 @@
upgrade_dry_run: false
reboot_after_upgrade: true
log_dir: /var/log/auto-upgrade
rollback: false
mail_to: "root@localhost"
# SMTP-Konfiguration für mailx (optional)
mail_smtp_host: "smtp.example.com"
mail_smtp_port: 587
mail_smtp_user: "user@example.com"
mail_smtp_pass: "dein_passwort"
vcenter_hostname: "vcenter.example.com"
vcenter_user: "administrator@vsphere.local"
vcenter_password: "dein_passwort"
vcenter_datacenter: "DeinDatacenter"
vcenter_folder: "/"
linux_admins_mail: "linux-admins@example.com"
maintenance_window_start: "22:00"
maintenance_window_end: "04:00"
# Upgrade-Optionen
upgrade_security_only: false # true = nur Security-Updates
# Skip-Flags für optionale Schritte
skip_smoke_tests: false
skip_compliance: false
skip_self_healing: false
skip_vmware_snapshot: false
skip_suma_api: false
skip_post_upgrade: false

View File

@ -0,0 +1,32 @@
# ServiceNow API
servicenow_instance: "https://mycompany.service-now.com"
servicenow_user: "ansible_api"
servicenow_pass: "SuperSicheresServiceNowPasswort123!"
# SUSE Manager API
suma_api_url: "https://susemanager.example.com/rpc/api"
suma_api_user: "suma_admin"
suma_api_pass: "NochSichereresSumaPasswort456!"
# vCenter/VMware
vcenter_hostname: "vcenter.example.com"
vcenter_user: "administrator@vsphere.local"
vcenter_password: "MegaSicheresVcenterPasswort789!"
vcenter_datacenter: "Datacenter1"
vcenter_folder: "/"
# Mail/SMTP
mail_smtp_host: "smtp.example.com"
mail_smtp_port: 587
mail_smtp_user: "mailuser@example.com"
mail_smtp_pass: "MailPasswort123!"
# Slack
slack_token: "xoxb-1234567890-abcdefghijklmnopqrstuvwx"
slack_enabled: true
# Datenbank Smoke-Tests
smoke_test_db_host: "db.example.com"
smoke_test_db_user: "dbuser"
smoke_test_db_pass: "DBPasswort456!"
smoke_test_db_name: "appdb"

29
playbook/inventory_apps Normal file
View File

@ -0,0 +1,29 @@
[pdp-portal]
pdp-portal-server1.example.com ansible_host=10.0.1.11 ansible_user=deploy host_email=admin-pdp@example.com
pdp-portal-server2.example.com ansible_host=10.0.1.12 host_email=admin-pdp2@example.com
[pdp-portal:vars]
app_mail=pdp-portal-app@example.com
[confluence]
confluence-server1.example.com ansible_host=10.0.2.21 ansible_user=confluence host_email=confluence-admin@example.com
confluence-server2.example.com ansible_host=10.0.2.22 host_email=confluence-admin2@example.com
[confluence:vars]
app_mail=confluence-app@example.com
[git]
git-server1.example.com ansible_host=10.0.3.31 ansible_user=gitadmin host_email=git-admin@example.com
git-server2.example.com ansible_host=10.0.3.32 host_email=git-admin2@example.com
[git:vars]
app_mail=git-app@example.com
# Optional: Gruppen für Umgebungen
#[dev:children]
#pdp-portal
#git
#[prod:children]
#confluence
#pdp-portal

81
playbook/playbook.yml Normal file
View File

@ -0,0 +1,81 @@
---
- name: Enterprise Auto-Upgrade für SLES und RHEL
hosts: all
gather_facts: false
become: yes
serial: 5
vars_files:
- group_vars/all.yml
- group_vars/vault.yml
vars:
target_clm_version: "" # Kann beim Aufruf überschrieben werden
debug_mode: false # Kann beim Aufruf überschrieben werden
skip_smoke_tests: false
skip_compliance: false
skip_self_healing: false
skip_vmware_snapshot: false
skip_suma_api: false
skip_post_upgrade: false
pre_tasks:
- name: Sammle gezielt Netzwerk- und Hardware-Fakten
setup:
gather_subset:
- network
- hardware
tags: always
- name: ServiceNow Change öffnen (optional)
import_role:
name: servicenow_tickets
tags: snow
- name: Preflight-Check: Prüfe Diskspace, Erreichbarkeit, Channel, Snapshots
import_role:
name: preflight_check
tags: preflight
- name: Setze Ziel-CLM-Version falls übergeben
set_fact:
target_clm_version: "{{ target_clm_version | default('') }}"
tags: always
- name: Debug: Zeige alle relevanten Variablen und Fakten
debug:
msg:
inventory_hostname: "{{ inventory_hostname }}"
ansible_os_family: "{{ ansible_facts['os_family'] }}"
ansible_distribution: "{{ ansible_facts['distribution'] }}"
ansible_distribution_version: "{{ ansible_facts['distribution_version'] }}"
target_clm_version: "{{ target_clm_version }}"
rollback: "{{ rollback }}"
mail_to: "{{ mail_to }}"
vcenter_hostname: "{{ vcenter_hostname }}"
suma_api_url: "{{ suma_api_url }}"
when: debug_mode | bool
tags: debug
- name: Erstelle VMware Snapshot vor Upgrade (optional)
import_role:
name: vmware_snapshot
when: not skip_vmware_snapshot
tags: snapshot
- name: Weise System per SUSE Manager API dem gewünschten CLM-Channel zu (optional)
import_role:
name: suma_api_assign_clm
when: target_clm_version != "" and not skip_suma_api
tags: suma
roles:
- role: common
tags: common
- role: rhel_upgrade
when: ansible_facts['os_family'] == "RedHat"
tags: rhel
- role: sles_upgrade
when: ansible_facts['os_family'] == "Suse"
tags: sles
- role: post_upgrade
when: not skip_post_upgrade
tags: post

View File

@ -0,0 +1,4 @@
collections:
- name: community.vmware
- name: servicenow.servicenow
- name: community.general

View File

@ -0,0 +1,160 @@
---
- name: Prüfe OS-Typ und Version
debug:
msg: "OS: {{ ansible_facts['os_family'] }} Version: {{ ansible_facts['distribution_version'] }}"
- name: Erstelle Log-Verzeichnis
file:
path: "{{ log_dir }}"
state: directory
mode: '0755'
register: logdir_result
ignore_errors: true
- name: Breche ab, wenn Log-Verzeichnis nicht erstellt werden kann
fail:
msg: "Log-Verzeichnis konnte nicht erstellt werden: {{ logdir_result.msg | default('Unbekannter Fehler') }}"
when: logdir_result is failed
- name: Konfiguriere mailx (Absender)
lineinfile:
path: /etc/mail.rc
line: "set from=auto-upgrade@{{ inventory_hostname }}"
create: yes
state: present
become: true
register: mailx_from_result
ignore_errors: true
- name: Logge Fehler bei mailx-Konfiguration (Absender)
copy:
content: "mailx-Konfigurations-Fehler: {{ mailx_from_result.msg | default('Unbekannter Fehler') }}"
dest: "{{ log_dir }}/mailx_error_{{ inventory_hostname }}.log"
when: mailx_from_result is failed
- name: Konfiguriere mailx für externen SMTP-Server (optional)
blockinfile:
path: /etc/mail.rc
block: |
set smtp=smtp://{{ mail_smtp_host }}:{{ mail_smtp_port }}
set smtp-auth=login
set smtp-auth-user={{ mail_smtp_user }}
set smtp-auth-password={{ mail_smtp_pass }}
set ssl-verify=ignore
set nss-config-dir=/etc/pki/nssdb
when: mail_smtp_host is defined and mail_smtp_user is defined and mail_smtp_pass is defined
become: true
register: mailx_smtp_result
ignore_errors: true
- name: Logge Fehler bei mailx-Konfiguration (SMTP)
copy:
content: "mailx-SMTP-Konfigurations-Fehler: {{ mailx_smtp_result.msg | default('Unbekannter Fehler') }}"
dest: "{{ log_dir }}/mailx_error_{{ inventory_hostname }}.log"
when: mailx_smtp_result is failed
- name: Sende Failsafe-Mail an app_mail und host_email bei Fehler
mail:
host: "localhost"
port: 25
to: |
{{ app_mail | default('') }}{{ ',' if app_mail is defined and app_mail != '' else '' }}{{ host_email | default(mail_to) }}
subject: "[FAILSAFE] Fehler beim Patch/Upgrade auf {{ inventory_hostname }}"
body: |
Es ist ein Fehler beim Patch/Upgrade auf {{ inventory_hostname }} (FQDN: {{ ansible_fqdn }}) aufgetreten.
Siehe Log-Verzeichnis: {{ log_dir }}
Zeit: {{ ansible_date_time.iso8601 }}
when: (ansible_failed_result is defined and ansible_failed_result is not none) or (rollback is defined and rollback)
ignore_errors: true
- name: Extrahiere Log-Summary für Admin-Mail
shell: |
tail -n 20 {{ log_dir }}/rhel_upgrade_check.log 2>/dev/null; tail -n 20 {{ log_dir }}/sles_upgrade_check.log 2>/dev/null; tail -n 20 {{ log_dir }}/rhel_upgrade_error_{{ inventory_hostname }}.log 2>/dev/null; tail -n 20 {{ log_dir }}/sles_upgrade_error_{{ inventory_hostname }}.log 2>/dev/null
register: log_summary
changed_when: false
ignore_errors: true
- name: Setze dynamische Liste der Log-Attachments
set_fact:
log_attachments: >-
{{
[
log_dir + '/rhel_upgrade_check.log',
log_dir + '/sles_upgrade_check.log',
log_dir + '/rhel_upgrade_error_' + inventory_hostname + '.log',
log_dir + '/sles_upgrade_error_' + inventory_hostname + '.log',
log_dir + '/snapshot_error_' + inventory_hostname + '.log',
log_dir + '/suma_api_error_' + inventory_hostname + '.log',
log_dir + '/mailx_error_' + inventory_hostname + '.log',
log_dir + '/package_report_' + inventory_hostname + '.log'
] | select('fileexists') | list
}}
- name: Sende Log an Linux-Admins (immer, mit Anhang und Summary)
mail:
host: "localhost"
port: 25
to: "{{ linux_admins_mail }}"
subject: "[LOG] Patch/Upgrade-Log für {{ inventory_hostname }} am {{ ansible_date_time.iso8601 }}"
body: |
Patch/Upgrade-Log für {{ inventory_hostname }} (FQDN: {{ ansible_fqdn }})
Zeit: {{ ansible_date_time.iso8601 }}
---
Log-Summary:
{{ log_summary.stdout | default('Keine Logdaten gefunden.') }}
---
Siehe Anhang für Details.
attach: "{{ log_attachments }}"
ignore_errors: true
- name: Slack-Benachrichtigung bei kritischen Fehlern (optional)
slack:
token: "{{ slack_token | default('xoxb-...') }}"
msg: "[CRITICAL] Fehler beim Patch/Upgrade auf {{ inventory_hostname }}: {{ ansible_failed_result.msg | default('Unbekannter Fehler') }}"
channel: "#linux-admins"
when: slack_enabled | default(false) and (ansible_failed_result is defined and ansible_failed_result is not none)
ignore_errors: true
- name: Dokumentiere Änderung im CHANGELOG
lineinfile:
path: "{{ playbook_dir }}/../CHANGELOG.md"
line: "{{ ansible_date_time.iso8601 }}: Patch/Upgrade auf {{ inventory_hostname }} (FQDN: {{ ansible_fqdn }}) durchgeführt. Ergebnis: {{ 'OK' if (ansible_failed_result is not defined or ansible_failed_result is none) else 'FEHLER' }}"
create: yes
delegate_to: localhost
ignore_errors: true
- name: Erfasse installierte Paketversionen (RHEL)
shell: rpm -qa --qf '%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}\n'
register: rpm_list
when: ansible_facts['os_family'] == 'RedHat'
changed_when: false
ignore_errors: true
- name: Erfasse installierte Paketversionen (SLES)
shell: rpm -qa --qf '%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}\n'
register: rpm_list
when: ansible_facts['os_family'] == 'Suse'
changed_when: false
ignore_errors: true
- name: Schreibe Paket-Report ins Log
copy:
content: "{{ rpm_list.stdout | default('Keine Paketdaten gefunden.') }}"
dest: "{{ log_dir }}/package_report_{{ inventory_hostname }}.log"
when: rpm_list is defined
ignore_errors: true
- name: Sende Paket-Report an Linux-Admins
mail:
host: "localhost"
port: 25
to: "{{ linux_admins_mail }}"
subject: "[REPORT] Paketversionen nach Patch für {{ inventory_hostname }} am {{ ansible_date_time.iso8601 }}"
body: |
Paket-Report für {{ inventory_hostname }} (FQDN: {{ ansible_fqdn }})
Zeit: {{ ansible_date_time.iso8601 }}
Siehe Anhang für Details.
attach:
- "{{ log_dir }}/package_report_{{ inventory_hostname }}.log"
when: rpm_list is defined
ignore_errors: true

View File

@ -0,0 +1,32 @@
---
- name: "Compliance-Check: Führe OpenSCAP-Scan durch (sofern installiert)"
ansible.builtin.shell: >-
oscap xccdf eval --profile xccdf_org.ssgproject.content_profile_standard
--results {{ log_dir }}/oscap_result_{{ inventory_hostname }}.xml
/usr/share/xml/scap/ssg/content/ssg-$(lsb_release -si | tr '[:upper:]' '[:lower:]')-ds.xml
register: oscap_result
ignore_errors: true
changed_when: false
- name: "Compliance-Check: Führe Lynis-Scan durch (sofern installiert)"
ansible.builtin.shell: lynis audit system --quiet --logfile {{ log_dir }}/lynis_{{ inventory_hostname }}.log
register: lynis_result
ignore_errors: true
changed_when: false
- name: Sende Compliance-Report an Linux-Admins
community.general.mail:
host: "localhost"
port: 25
to: "{{ linux_admins_mail }}"
subject: "[COMPLIANCE] Report für {{ inventory_hostname }} am {{ ansible_date_time.iso8601 }}"
body: |
Compliance-Report für {{ inventory_hostname }} (FQDN: {{ ansible_fqdn }})
Zeit: {{ ansible_date_time.iso8601 }}
OpenSCAP-Exit: {{ oscap_result.rc | default('N/A') }}
Lynis-Exit: {{ lynis_result.rc | default('N/A') }}
Siehe Anhang für Details.
attach:
- "{{ log_dir }}/oscap_result_{{ inventory_hostname }}.xml"
- "{{ log_dir }}/lynis_{{ inventory_hostname }}.log"
ignore_errors: true

View File

@ -0,0 +1,39 @@
---
- name: Reboot nach Upgrade (optional)
ansible.builtin.reboot:
msg: "Reboot nach Auto-Upgrade"
pre_reboot_delay: 60
when: reboot_after_upgrade
tags: reboot
- name: "Health-Check: Prüfe, ob kritische Dienste laufen"
ansible.builtin.service_facts:
tags: health
- name: Prüfe Status der kritischen Dienste
ansible.builtin.assert:
that:
- "(services[item].state == 'running') or (services[item].state == 'started')"
fail_msg: "Kritischer Dienst {{ item }} läuft nicht!"
success_msg: "Dienst {{ item }} läuft."
loop: "{{ critical_services | default(['sshd','cron']) }}"
when: item in services
tags: health
- name: Führe automatisierte Smoke-Tests durch (optional)
import_role:
name: smoke_tests
when: not skip_smoke_tests
tags: smoke
- name: Führe Self-Healing/Remediation durch (optional)
import_role:
name: self_healing
when: not skip_self_healing
tags: selfheal
- name: Führe Compliance-Checks durch (optional)
import_role:
name: compliance_check
when: not skip_compliance
tags: compliance

View File

@ -0,0 +1,131 @@
---
- name: "Prüfe, ob aktueller Zeitpunkt im Maintenance-Window liegt"
ansible.builtin.set_fact:
now: "{{ lookup('pipe', 'date +%H:%M') }}"
window_start: "{{ maintenance_window_start }}"
window_end: "{{ maintenance_window_end }}"
changed_when: false
tags: preflight
- name: "Maintenance-Window-Check (Abbruch, wenn außerhalb)"
ansible.builtin.fail:
msg: "Aktuelle Zeit {{ now }} liegt außerhalb des Maintenance-Windows ({{ window_start }} - {{ window_end }}). Upgrade wird abgebrochen!"
when: >-
(
(window_start < window_end and (now < window_start or now > window_end))
or
(window_start > window_end and (now < window_start and now > window_end))
)
tags: preflight
- name: "Prüfe freien Speicherplatz auf / (mind. 5GB empfohlen)"
ansible.builtin.stat:
path: /
register: root_stat
tags: preflight
- name: Warnung bei zu wenig Speicherplatz
ansible.builtin.assert:
that:
- root_stat.stat.avail_bytes > 5368709120
fail_msg: "Wenig freier Speicherplatz auf /: {{ root_stat.stat.avail_bytes | human_readable }} (mind. 5GB empfohlen)"
success_msg: "Genügend Speicherplatz auf /: {{ root_stat.stat.avail_bytes | human_readable }}"
tags: preflight
- name: Prüfe Erreichbarkeit von SUSE Manager
ansible.builtin.uri:
url: "{{ suma_api_url }}"
method: GET
validate_certs: no
timeout: 10
register: suma_reachable
ignore_errors: true
retries: 3
delay: 5
tags: preflight
- name: Warnung, wenn SUSE Manager nicht erreichbar
ansible.builtin.assert:
that:
- suma_reachable.status is defined and suma_reachable.status == 200
fail_msg: "SUSE Manager API nicht erreichbar!"
success_msg: "SUSE Manager API erreichbar."
tags: preflight
- name: Prüfe, ob VMware-Snapshot-Modul verfügbar ist
ansible.builtin.shell: "python3 -c 'import pyVmomi'"
register: pyvmomi_check
ignore_errors: true
changed_when: false
tags: preflight
- name: Warnung, wenn pyVmomi nicht installiert ist
ansible.builtin.assert:
that:
- pyvmomi_check.rc == 0
fail_msg: "pyVmomi (VMware-Modul) nicht installiert!"
success_msg: "pyVmomi ist installiert."
tags: preflight
- name: Prüfe, ob aktueller SUSE Manager Channel synchronisiert ist
ansible.builtin.uri:
url: "{{ suma_api_url }}"
method: POST
body_format: json
headers:
Content-Type: application/json
body: |
{
"method": "auth.login",
"params": ["{{ suma_api_user }}", "{{ suma_api_pass }}"],
"id": 1
}
validate_certs: no
timeout: 20
register: suma_api_login
ignore_errors: true
retries: 3
delay: 10
async: 60
poll: 0
tags: preflight
- name: Hole Channel-Details für Ziel-CLM-Version
ansible.builtin.uri:
url: "{{ suma_api_url }}"
method: POST
body_format: json
headers:
Content-Type: application/json
body: |
{
"method": "channel.software.getDetails",
"params": ["{{ suma_api_login.json.result }}", "{{ target_clm_version }}"],
"id": 2
}
validate_certs: no
timeout: 20
register: suma_channel_details
ignore_errors: true
retries: 3
delay: 10
async: 60
poll: 0
tags: preflight
- name: Prüfe Channel-Sync-Status
ansible.builtin.assert:
that:
- suma_channel_details.json.result.last_sync is defined
fail_msg: "Channel {{ target_clm_version }} ist nicht synchronisiert!"
success_msg: "Channel {{ target_clm_version }} wurde zuletzt synchronisiert am {{ suma_channel_details.json.result.last_sync }}."
tags: preflight
- name: Slack-Benachrichtigung bei kritischen Fehlern (Beispiel)
community.general.slack:
token: "{{ slack_token | default('xoxb-...') }}"
msg: "[CRITICAL] Fehler beim Preflight-Check auf {{ inventory_hostname }}: {{ ansible_failed_result.msg | default('Unbekannter Fehler') }}"
channel: "#linux-admins"
when: slack_enabled | default(false) and (ansible_failed_result is defined and ansible_failed_result is not none)
ignore_errors: true
tags: preflight

View File

@ -0,0 +1,81 @@
---
- name: Prüfe, ob dnf verfügbar ist (RHEL 8+)
stat:
path: /usr/bin/dnf
register: dnf_exists
- name: Pre-Upgrade-Check (yum/dnf)
shell: |
if [ -x /usr/bin/dnf ]; then
dnf check-update || true
else
yum check-update || true
fi
register: rhel_check
changed_when: false
- name: Kernel-Version vor Upgrade sichern
shell: uname -r
register: kernel_before
changed_when: false
- name: Upgrade durchführen (dnf/yum, security-only optional)
package:
name: "*"
state: latest
register: upgrade_result
when: not upgrade_dry_run and not upgrade_security_only
ignore_errors: true
- name: Upgrade durchführen (dnf/yum, nur Security-Updates)
dnf:
name: "*"
state: latest
security: yes
register: upgrade_result
when: not upgrade_dry_run and upgrade_security_only and dnf_exists.stat.exists
ignore_errors: true
- name: Upgrade durchführen (yum-plugin-security Fallback)
command: yum -y --security update
register: upgrade_result
when: not upgrade_dry_run and upgrade_security_only and not dnf_exists.stat.exists
ignore_errors: true
- name: Logge Fehler beim Upgrade (RHEL)
copy:
content: "Upgrade-Fehler: {{ upgrade_result.stderr | default(upgrade_result.msg | default('Unbekannter Fehler')) }}"
dest: "{{ log_dir }}/rhel_upgrade_error_{{ inventory_hostname }}.log"
when: upgrade_result is failed
- name: Setze Rollback-Flag, falls Upgrade fehlschlägt
set_fact:
rollback: true
when: upgrade_result is failed
- name: Breche Playbook ab, wenn Upgrade fehlschlägt
fail:
msg: "Upgrade fehlgeschlagen, Rollback wird empfohlen! Siehe Log: {{ log_dir }}/rhel_upgrade_error_{{ inventory_hostname }}.log"
when: upgrade_result is failed
- name: Logge Upgrade-Output (RHEL)
copy:
content: "{{ rhel_check.stdout }}"
dest: "{{ log_dir }}/rhel_upgrade_check.log"
when: upgrade_result is not failed
- name: Kernel-Version nach Upgrade sichern
shell: uname -r
register: kernel_after
changed_when: false
when: upgrade_result is not failed
- name: Prüfe, ob Kernel-Upgrade erfolgt ist und setze Reboot nötig
set_fact:
reboot_after_upgrade: true
when: upgrade_result is not failed and (kernel_before.stdout != kernel_after.stdout)
- name: Hinweis auf EUS/Leapp (nur RHEL 7/8)
debug:
msg: "Für Major Upgrades (z.B. 7->8) empfiehlt Red Hat das Tool 'leapp' oder EUS-Strategien. Siehe https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html-single/upgrading_from_rhel_7_to_rhel_8/index.html"
when: ansible_facts['distribution_major_version']|int >= 7

View File

@ -0,0 +1,56 @@
---
- name: Self-Healing: Starte kritische Dienste neu, falls sie nicht laufen
service:
name: "{{ item }}"
state: restarted
register: restart_result
loop: "{{ critical_services | default(['sshd','cron']) }}"
when: item in services and (services[item].state != 'running' and services[item].state != 'started')
ignore_errors: true
- name: Prüfe, ob Restart erfolgreich war
service_facts:
- name: Logge Self-Healing-Resultate
copy:
content: |
Self-Healing-Report für {{ inventory_hostname }}
Zeit: {{ ansible_date_time.iso8601 }}
Kritische Dienste: {{ critical_services | default(['sshd','cron']) }}
Restart-Resultate: {{ restart_result.results | default(restart_result) | to_nice_json }}
Service-Status nach Restart:
{% for item in critical_services | default(['sshd','cron']) %}
- {{ item }}: {{ services[item].state | default('unbekannt') }}
{% endfor %}
dest: "{{ log_dir }}/self_healing_{{ inventory_hostname }}.log"
ignore_errors: true
- name: Eskaliere per Mail, wenn Restart fehlschlägt
mail:
host: "localhost"
port: 25
to: "{{ linux_admins_mail }}"
subject: "[SELF-HEALING-FAIL] Dienst konnte nicht neu gestartet werden auf {{ inventory_hostname }}"
body: |
Self-Healing konnte einen oder mehrere kritische Dienste nicht erfolgreich neu starten!
Siehe Log: {{ log_dir }}/self_healing_{{ inventory_hostname }}.log
Zeit: {{ ansible_date_time.iso8601 }}
when: >-
restart_result is defined and (
(restart_result.results is defined and (restart_result.results | selectattr('state', 'ne', 'running') | list | length > 0))
or
(restart_result.state is defined and restart_result.state != 'running')
)
ignore_errors: true
- name: Self-Healing: Bereinige /tmp, /var/tmp, /var/log/alt bei wenig Speicherplatz
shell: rm -rf /tmp/* /var/tmp/* /var/log/alt/*
when: ansible_mounts[0].size_available < 10737418240 # <10GB frei
ignore_errors: true
- name: Self-Healing: Netzwerkdienst neu starten bei Netzwerkproblemen
service:
name: network
state: restarted
when: ansible_default_ipv4 is not defined or ansible_default_ipv4['address'] is not defined
ignore_errors: true

View File

@ -0,0 +1,54 @@
---
- name: Erstelle/aktualisiere Change in ServiceNow (vor Patch)
community.general.snow_record:
instance: "{{ servicenow_instance }}"
username: "{{ servicenow_user }}"
password: "{{ servicenow_pass }}"
state: present
table: change_request
data:
short_description: "OS Patch/Upgrade {{ inventory_hostname }}"
description: "Automatisiertes Upgrade via Ansible"
category: "Software"
risk: "2"
impact: "2"
priority: "3"
work_start: "{{ ansible_date_time.iso8601 }}"
requested_by: "{{ servicenow_requested_by | default('ansible_automation') }}"
register: snow_change
ignore_errors: true
- name: Dokumentiere Change-Nummer
debug:
msg: "ServiceNow Change: {{ snow_change.record.number | default('N/A') }}"
- name: Erstelle Incident bei Fehlern (optional)
community.general.snow_record:
instance: "{{ servicenow_instance }}"
username: "{{ servicenow_user }}"
password: "{{ servicenow_pass }}"
state: present
table: incident
data:
short_description: "Patch/Upgrade FAILED auf {{ inventory_hostname }}"
description: "Siehe Logs unter {{ log_dir }}. Zeitpunkt: {{ ansible_date_time.iso8601 }}"
severity: "2"
urgency: "2"
impact: "2"
when: ansible_failed_result is defined and ansible_failed_result is not none
ignore_errors: true
- name: Aktualisiere Change (Abschluss)
community.general.snow_record:
instance: "{{ servicenow_instance }}"
username: "{{ servicenow_user }}"
password: "{{ servicenow_pass }}"
state: present
table: change_request
number: "{{ snow_change.record.number | default(omit) }}"
data:
work_end: "{{ ansible_date_time.iso8601 }}"
close_notes: "Upgrade abgeschlossen auf {{ inventory_hostname }}"
state: "3"
when: snow_change is defined and snow_change.record is defined
ignore_errors: true

View File

@ -0,0 +1,62 @@
---
- name: Pre-Upgrade-Check (zypper)
shell: zypper list-updates || true
register: sles_check
changed_when: false
- name: Kernel-Version vor Upgrade sichern
shell: uname -r
register: kernel_before
changed_when: false
- name: Upgrade durchführen (zypper, full)
zypper:
name: '*'
state: latest
extra_args: '--non-interactive'
register: upgrade_result
when: not upgrade_dry_run and not upgrade_security_only
ignore_errors: true
- name: Upgrade durchführen (zypper, nur Security)
command: zypper --non-interactive patch --category security
register: upgrade_result
when: not upgrade_dry_run and upgrade_security_only
ignore_errors: true
- name: Logge Fehler beim Upgrade (SLES)
copy:
content: "Upgrade-Fehler: {{ upgrade_result.stderr | default(upgrade_result.msg | default('Unbekannter Fehler')) }}"
dest: "{{ log_dir }}/sles_upgrade_error_{{ inventory_hostname }}.log"
when: upgrade_result is failed
- name: Setze Rollback-Flag, falls Upgrade fehlschlägt
set_fact:
rollback: true
when: upgrade_result is failed
- name: Breche Playbook ab, wenn Upgrade fehlschlägt
fail:
msg: "Upgrade fehlgeschlagen, Rollback wird empfohlen! Siehe Log: {{ log_dir }}/sles_upgrade_error_{{ inventory_hostname }}.log"
when: upgrade_result is failed
- name: Logge Upgrade-Output (SLES)
copy:
content: "{{ sles_check.stdout }}"
dest: "{{ log_dir }}/sles_upgrade_check.log"
when: upgrade_result is not failed
- name: Kernel-Version nach Upgrade sichern
shell: uname -r
register: kernel_after
changed_when: false
when: upgrade_result is not failed
- name: Prüfe, ob Kernel-Upgrade erfolgt ist und setze Reboot nötig
set_fact:
reboot_after_upgrade: true
when: upgrade_result is not failed and (kernel_before.stdout != kernel_after.stdout)
- name: Hinweis auf SLE-Upgrade-Tool
debug:
msg: "Für Major Upgrades (z.B. SLES 12->15) empfiehlt SUSE das Tool 'SUSEConnect' und 'zypper migration'. Siehe https://documentation.suse.com/sles/15-SP4/html/SLES-all/cha-upgrade.html"

View File

@ -0,0 +1,108 @@
- name: Prüfe, ob HTTP-Service installiert ist (Apache/Nginx)
ansible.builtin.stat:
path: /usr/sbin/httpd
register: apache_check
ignore_errors: true
- name: Prüfe, ob Nginx installiert ist
ansible.builtin.stat:
path: /usr/sbin/nginx
register: nginx_check
ignore_errors: true
- name: "Smoke-Test: Prüfe HTTP-Endpoint (nur wenn Web-Server installiert)"
ansible.builtin.uri:
url: "{{ smoke_test_url | default('http://localhost') }}"
status_code: 200
return_content: false
register: http_result
ignore_errors: true
when: apache_check.stat.exists or nginx_check.stat.exists
- name: "Smoke-Test: Prüfe offenen Port (optional)"
ansible.builtin.wait_for:
port: "{{ smoke_test_port | default(80) }}"
host: "{{ smoke_test_host | default('localhost') }}"
timeout: 5
register: port_result
ignore_errors: true
- name: Prüfe, ob MySQL/MariaDB installiert ist
ansible.builtin.stat:
path: /usr/bin/mysql
register: mysql_check
ignore_errors: true
- name: "Smoke-Test: Prüfe Datenbankverbindung (nur wenn MySQL installiert)"
ansible.builtin.shell: "echo 'select 1' | mysql -h {{ smoke_test_db_host | default('localhost') }} -u {{ smoke_test_db_user | default('root') }} --password={{ smoke_test_db_pass | default('') }} {{ smoke_test_db_name | default('') }}"
register: db_result
ignore_errors: true
when: mysql_check.stat.exists and smoke_test_db_host is defined
- name: Prüfe, ob Oracle installiert ist
ansible.builtin.stat:
path: /u01/app/oracle/product
register: oracle_check
ignore_errors: true
- name: "Oracle DB: Finde alle Oracle SIDs (nur wenn Oracle installiert)"
ansible.builtin.shell: |
ps -ef | grep -E "ora_pmon_[A-Z0-9_]+" | grep -v grep | awk '{print $NF}' | sed 's/ora_pmon_//'
register: oracle_sids
changed_when: false
ignore_errors: true
when: oracle_check.stat.exists
- name: "Oracle DB: Finde alle Oracle Listener (nur wenn Oracle installiert)"
ansible.builtin.shell: |
ps -ef | grep -E "tnslsnr" | grep -v grep | awk '{print $NF}' | sed 's/tnslsnr//'
register: oracle_listeners
changed_when: false
ignore_errors: true
when: oracle_check.stat.exists
- name: "Oracle DB: Prüfe alle gefundenen SIDs (nur wenn Oracle installiert)"
ansible.builtin.shell: |
export ORACLE_HOME={{ item.oracle_home | default('/u01/app/oracle/product/19.0.0/dbhome_1') }}
export PATH=$ORACLE_HOME/bin:$PATH
export ORACLE_SID={{ item.sid }}
sqlplus -S / as sysdba <<EOF
select 'OK' as status from dual;
exit;
EOF
register: oracle_sid_check
loop: "{{ oracle_sids.stdout_lines | map('regex_replace', '^(.+)$', '{\"sid\": \"\\1\", \"oracle_home\": \"/u01/app/oracle/product/19.0.0/dbhome_1\"}') | map('from_json') | list }}"
ignore_errors: true
when: oracle_check.stat.exists and oracle_sids.stdout_lines | length > 0
- name: "Oracle DB: Prüfe alle gefundenen Listener (nur wenn Oracle installiert)"
ansible.builtin.shell: |
export ORACLE_HOME={{ item.oracle_home | default('/u01/app/oracle/product/19.0.0/dbhome_1') }}
export PATH=$ORACLE_HOME/bin:$PATH
lsnrctl status {{ item.listener }}
register: oracle_listener_check
loop: "{{ oracle_listeners.stdout_lines | map('regex_replace', '^(.+)$', '{\"listener\": \"\\1\", \"oracle_home\": \"/u01/app/oracle/product/19.0.0/dbhome_1\"}') | map('from_json') | list }}"
ignore_errors: true
when: oracle_check.stat.exists and oracle_listeners.stdout_lines | length > 0
- name: "Oracle DB: Logge Oracle-Check-Ergebnisse (nur wenn Oracle installiert)"
ansible.builtin.copy:
content: |
Oracle DB Check für {{ inventory_hostname }}
Zeit: {{ ansible_date_time.iso8601 }}
Gefundene SIDs: {{ oracle_sids.stdout_lines | default([]) }}
Gefundene Listener: {{ oracle_listeners.stdout_lines | default([]) }}
SID-Check-Resultate: {{ oracle_sid_check.results | default([]) | to_nice_json }}
Listener-Check-Resultate: {{ oracle_listener_check.results | default([]) | to_nice_json }}
dest: "{{ log_dir }}/oracle_check_{{ inventory_hostname }}.log"
ignore_errors: true
when: oracle_check.stat.exists
- name: Smoke-Test Ergebnis zusammenfassen
ansible.builtin.debug:
msg:
- "HTTP-Test: {{ http_result.status | default('NOT INSTALLED') }}"
- "Port-Test: {{ port_result.state | default('FAILED') }}"
- "DB-Test: {{ db_result.rc | default('NOT INSTALLED') }}"
- "Oracle SIDs gefunden: {{ oracle_sids.stdout_lines | length | default(0) if oracle_check.stat.exists else 'NOT INSTALLED' }}"
- "Oracle Listener gefunden: {{ oracle_listeners.stdout_lines | length | default(0) if oracle_check.stat.exists else 'NOT INSTALLED' }}"

View File

@ -0,0 +1,151 @@
---
- name: Setze Variablen für SUSE Manager API
set_fact:
suma_api_url: "{{ suma_api_url }}"
suma_api_user: "{{ suma_api_user }}"
suma_api_pass: "{{ suma_api_pass }}"
- name: Hole System-ID aus SUSE Manager
uri:
url: "{{ suma_api_url }}"
method: POST
body_format: json
headers:
Content-Type: application/json
body: |
{
"method": "auth.login",
"params": ["{{ suma_api_user }}", "{{ suma_api_pass }}"],
"id": 1
}
validate_certs: no
register: suma_api_login
ignore_errors: true
- name: Logge Fehler bei API-Login
copy:
content: "API-Login-Fehler: {{ suma_api_login.msg | default('Unbekannter Fehler') }}"
dest: "{{ log_dir }}/suma_api_error_{{ inventory_hostname }}.log"
when: suma_api_login.failed
- name: Breche Playbook ab, wenn API-Login fehlschlägt
fail:
msg: "SUSE Manager API-Login fehlgeschlagen! Siehe Log: {{ log_dir }}/suma_api_error_{{ inventory_hostname }}.log"
when: suma_api_login.failed
- name: Setze Session-ID
set_fact:
suma_session: "{{ suma_api_login.json.result }}"
- name: Suche System-ID anhand Hostname
uri:
url: "{{ suma_api_url }}"
method: POST
body_format: json
headers:
Content-Type: application/json
body: |
{
"method": "system.getId",
"params": ["{{ suma_session }}", "{{ inventory_hostname }}"],
"id": 2
}
validate_certs: no
register: suma_system_id
ignore_errors: true
- name: Logge Fehler bei System-ID-Suche
copy:
content: "System-ID-Fehler: {{ suma_system_id.msg | default('Unbekannter Fehler') }}"
dest: "{{ log_dir }}/suma_api_error_{{ inventory_hostname }}.log"
when: suma_system_id.failed
- name: Breche Playbook ab, wenn System-ID nicht gefunden
fail:
msg: "System-ID nicht gefunden! Siehe Log: {{ log_dir }}/suma_api_error_{{ inventory_hostname }}.log"
when: suma_system_id.failed
- name: Suche Channel-ID anhand Ziel-CLM-Version
uri:
url: "{{ suma_api_url }}"
method: POST
body_format: json
headers:
Content-Type: application/json
body: |
{
"method": "channel.software.listAllChannels",
"params": ["{{ suma_session }}"],
"id": 3
}
validate_certs: no
register: suma_channels
ignore_errors: true
- name: Logge Fehler bei Channel-Suche
copy:
content: "Channel-Such-Fehler: {{ suma_channels.msg | default('Unbekannter Fehler') }}"
dest: "{{ log_dir }}/suma_api_error_{{ inventory_hostname }}.log"
when: suma_channels.failed
- name: Breche Playbook ab, wenn Channel-Suche fehlschlägt
fail:
msg: "Channel-Suche fehlgeschlagen! Siehe Log: {{ log_dir }}/suma_api_error_{{ inventory_hostname }}.log"
when: suma_channels.failed
- name: Finde Channel-ID für Ziel-CLM-Version
set_fact:
target_channel_label: "{{ item.label }}"
loop: "{{ suma_channels.json.result }}"
when: item.name is search(target_clm_version)
loop_control:
label: "{{ item.label }}"
- name: Breche ab, wenn kein passender Channel gefunden wurde
fail:
msg: "Kein passender CLM-Channel für '{{ target_clm_version }}' gefunden!"
when: target_channel_label is not defined
- name: Weise System dem Channel zu
uri:
url: "{{ suma_api_url }}"
method: POST
body_format: json
headers:
Content-Type: application/json
body: |
{
"method": "system.setBaseChannel",
"params": ["{{ suma_session }}", {{ suma_system_id.json.result[0].id }}, "{{ target_channel_label }}"],
"id": 4
}
validate_certs: no
register: suma_assign_result
ignore_errors: true
- name: Logge Fehler bei Channel-Zuweisung
copy:
content: "Channel-Zuweisungs-Fehler: {{ suma_assign_result.msg | default('Unbekannter Fehler') }}"
dest: "{{ log_dir }}/suma_api_error_{{ inventory_hostname }}.log"
when: suma_assign_result.failed
- name: Breche Playbook ab, wenn Channel-Zuweisung fehlschlägt
fail:
msg: "Channel-Zuweisung fehlgeschlagen! Siehe Log: {{ log_dir }}/suma_api_error_{{ inventory_hostname }}.log"
when: suma_assign_result.failed
- name: Logout von der SUSE Manager API
uri:
url: "{{ suma_api_url }}"
method: POST
body_format: json
headers:
Content-Type: application/json
body: |
{
"method": "auth.logout",
"params": ["{{ suma_session }}"],
"id": 5
}
validate_certs: no
when: suma_session is defined

View File

@ -0,0 +1,65 @@
---
- name: Erstelle VMware Snapshot vor Upgrade
community.vmware.vmware_guest_snapshot:
hostname: "{{ vcenter_hostname }}"
username: "{{ vcenter_user }}"
password: "{{ vcenter_password }}"
validate_certs: no
datacenter: "{{ vcenter_datacenter }}"
folder: "{{ vcenter_folder | default('/') }}"
name: "{{ inventory_hostname }}"
state: present
snapshot_name: "pre-upgrade-{{ inventory_hostname }}-{{ ansible_date_time.iso8601_basic }}"
description: "Snapshot vor Auto-Upgrade"
memory: yes
quiesce: yes
delegate_to: localhost
register: snapshot_result
failed_when: snapshot_result.failed is defined and snapshot_result.failed
retries: 3
delay: 10
- name: Logge Fehler bei Snapshot-Erstellung
ansible.builtin.copy:
content: "Snapshot-Fehler: {{ snapshot_result.msg | default('Unbekannter Fehler') }}"
dest: "{{ log_dir }}/snapshot_error_{{ inventory_hostname }}.log"
when: snapshot_result is failed
- name: Setze Rollback-Flag, falls Snapshot-Erstellung fehlschlägt
ansible.builtin.set_fact:
rollback: true
when: snapshot_result is failed
- name: Breche Playbook ab, wenn Snapshot-Erstellung fehlschlägt
ansible.builtin.fail:
msg: "Snapshot-Erstellung fehlgeschlagen, Upgrade wird abgebrochen! Siehe Log: {{ log_dir }}/snapshot_error_{{ inventory_hostname }}.log"
when: snapshot_result is failed
- name: "Rollback: Setze VM auf Snapshot zurück (nur bei Fehler und wenn aktiviert)"
community.vmware.vmware_guest_snapshot:
hostname: "{{ vcenter_hostname }}"
username: "{{ vcenter_user }}"
password: "{{ vcenter_password }}"
validate_certs: no
datacenter: "{{ vcenter_datacenter }}"
folder: "{{ vcenter_folder | default('/') }}"
name: "{{ inventory_hostname }}"
state: revert
snapshot_name: "pre-upgrade-{{ inventory_hostname }}-{{ ansible_date_time.iso8601_basic }}"
when: rollback is defined and rollback
delegate_to: localhost
- name: Lösche VMware Snapshot nach erfolgreichem Patchlauf (optional)
community.vmware.vmware_guest_snapshot:
hostname: "{{ vcenter_hostname }}"
username: "{{ vcenter_user }}"
password: "{{ vcenter_password }}"
validate_certs: no
datacenter: "{{ vcenter_datacenter }}"
folder: "{{ vcenter_folder | default('/') }}"
name: "{{ inventory_hostname }}"
state: absent
snapshot_name: "pre-upgrade-{{ inventory_hostname }}-{{ ansible_date_time.iso8601_basic }}"
delegate_to: localhost
when: (upgrade_result is defined and upgrade_result is not failed) and (snapshot_cleanup | default(true))
ignore_errors: true

View File

@ -0,0 +1,19 @@
plugin: servicenow.servicenow.now
instance: "{{ servicenow_instance }}"
username: "{{ servicenow_user }}"
password: "{{ servicenow_pass }}"
table: 'cmdb_ci_server'
fields:
- fqdn
- name
- u_app_group
- u_app_mail
- u_host_email
keyed_groups:
- key: u_app_group
prefix: ''
separator: ''
compose:
ansible_host: fqdn
app_mail: u_app_mail
host_email: u_host_email

4
scripts/install_collections.sh Executable file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
ansible-galaxy collection install -r playbook/requirements.yml --force

26
scripts/run_patch.sh Executable file
View File

@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail
APP_GROUP=${1:-}
TARGET_CLM=${2:-}
EXTRA_VARS=${3:-}
if [[ -z "$APP_GROUP" ]]; then
echo "Usage: $0 <app-group> [target_clm_version] [extra_vars]" >&2
exit 1
fi
CMD=(ansible-playbook playbook/playbook.yml -l "$APP_GROUP" --ask-vault-pass)
if [[ -n "$TARGET_CLM" ]]; then
CMD+=( -e "target_clm_version=$TARGET_CLM" )
fi
if [[ -n "$EXTRA_VARS" ]]; then
CMD+=( -e "$EXTRA_VARS" )
fi
# Tags können bei Bedarf angepasst werden, z.B. nur preflight+upgrade
# CMD+=( --tags preflight,common,rhel,sles,post )
exec "${CMD[@]}"