From 15c9b4f1e42f154498b4487e33e52d950b41b2c6 Mon Sep 17 00:00:00 2001 From: Automation Admin Date: Thu, 7 Aug 2025 22:24:32 +0000 Subject: [PATCH 1/5] feat: initial import os-upgrade-automation enterprise setup --- .ansible-lint | 7 + .github-workflows-ci.yml | 27 +++ .github/CODEOWNERS | 8 + .gitlab-ci.yml | 20 +++ .pre-commit-config.yaml | 11 ++ CONTRIBUTING.md | 24 +++ Makefile | 18 ++ ansible.cfg | 18 ++ docs/CHANGELOG.md | 6 + docs/README.md | 135 +++++++++++++++ docs/Runbook_SelfService.md | 28 +++ playbook/group_vars/all.yml | 33 ++++ playbook/group_vars/vault.yml | 32 ++++ playbook/inventory_apps | 29 ++++ playbook/playbook.yml | 81 +++++++++ playbook/requirements.yml | 4 + playbook/roles/common/tasks/main.yml | 160 ++++++++++++++++++ .../roles/compliance_check/tasks/main.yml | 29 ++++ playbook/roles/post_upgrade/tasks/main.yml | 39 +++++ playbook/roles/preflight_check/tasks/main.yml | 131 ++++++++++++++ playbook/roles/rhel_upgrade/tasks/main.yml | 81 +++++++++ playbook/roles/self_healing/tasks/main.yml | 56 ++++++ .../roles/servicenow_tickets/tasks/main.yml | 54 ++++++ playbook/roles/sles_upgrade/tasks/main.yml | 62 +++++++ playbook/roles/smoke_tests/tasks/main.yml | 109 ++++++++++++ .../roles/suma_api_assign_clm/tasks/main.yml | 151 +++++++++++++++++ playbook/roles/vmware_snapshot/tasks/main.yml | 65 +++++++ playbook/servicenow_inventory.yml | 19 +++ scripts/install_collections.sh | 4 + scripts/run_patch.sh | 26 +++ 30 files changed, 1467 insertions(+) create mode 100644 .ansible-lint create mode 100644 .github-workflows-ci.yml create mode 100644 .github/CODEOWNERS create mode 100644 .gitlab-ci.yml create mode 100644 .pre-commit-config.yaml create mode 100644 CONTRIBUTING.md create mode 100644 Makefile create mode 100644 ansible.cfg create mode 100644 docs/CHANGELOG.md create mode 100644 docs/README.md create mode 100644 docs/Runbook_SelfService.md create mode 100644 playbook/group_vars/all.yml create mode 100644 playbook/group_vars/vault.yml create mode 100644 playbook/inventory_apps create mode 100644 playbook/playbook.yml create mode 100644 playbook/requirements.yml create mode 100644 playbook/roles/common/tasks/main.yml create mode 100644 playbook/roles/compliance_check/tasks/main.yml create mode 100644 playbook/roles/post_upgrade/tasks/main.yml create mode 100644 playbook/roles/preflight_check/tasks/main.yml create mode 100644 playbook/roles/rhel_upgrade/tasks/main.yml create mode 100644 playbook/roles/self_healing/tasks/main.yml create mode 100644 playbook/roles/servicenow_tickets/tasks/main.yml create mode 100644 playbook/roles/sles_upgrade/tasks/main.yml create mode 100644 playbook/roles/smoke_tests/tasks/main.yml create mode 100644 playbook/roles/suma_api_assign_clm/tasks/main.yml create mode 100644 playbook/roles/vmware_snapshot/tasks/main.yml create mode 100644 playbook/servicenow_inventory.yml create mode 100755 scripts/install_collections.sh create mode 100755 scripts/run_patch.sh diff --git a/.ansible-lint b/.ansible-lint new file mode 100644 index 0000000..c825d95 --- /dev/null +++ b/.ansible-lint @@ -0,0 +1,7 @@ +--- +rules: {} +parser: + ansible: true +warn_list: [] +skip_list: + - yaml diff --git a/.github-workflows-ci.yml b/.github-workflows-ci.yml new file mode 100644 index 0000000..04e8bd6 --- /dev/null +++ b/.github-workflows-ci.yml @@ -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 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..46f9542 --- /dev/null +++ b/.github/CODEOWNERS @@ -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 diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..05d7958 --- /dev/null +++ b/.gitlab-ci.yml @@ -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 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..f1b0faf --- /dev/null +++ b/.pre-commit-config.yaml @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..98af3ca --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,24 @@ +# CONTRIBUTING + +Danke für deinen Beitrag! Bitte beachte folgende Richtlinien: + +## Branch/Commit +- Feature-Branches: `feature/` +- Fix-Branches: `fix/` +- 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 ` + +## 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 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b2d6b24 --- /dev/null +++ b/Makefile @@ -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 diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..448b7b3 --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,18 @@ +[defaults] +inventory = playbook/servicenow_inventory.yml +roles_path = playbook/roles +collections_paths = ~/.ansible/collections:./collections +host_key_checking = False +retry_files_enabled = False +stdout_callback = yaml +bin_ansible_callbacks = True +forks = 25 +interpreter_python = auto_silent +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 diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md new file mode 100644 index 0000000..73d45c7 --- /dev/null +++ b/docs/CHANGELOG.md @@ -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 diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..077519a --- /dev/null +++ b/docs/README.md @@ -0,0 +1,135 @@ +# Enterprise Auto-Upgrade Playbook für SLES & RHEL + +## Übersicht +Dieses Projekt bietet ein modulares, Enterprise-taugliches Ansible-Playbook für automatisierte Upgrades und Patch-Management von SLES (SUSE Linux Enterprise Server) und RHEL (Red Hat Enterprise Linux) Systemen. Es unterstützt: +- Automatische OS-Erkennung +- Upgrade nach Hersteller-Best-Practice +- **VMware-Snapshots für Backup/Rollback** +- Logging +- Mail-Benachrichtigung (lokal & extern via mailx) +- Dynamische Zuweisung von CLM-Channels via SUSE Manager API + +## Verzeichnisstruktur +``` +playbook/ +├── group_vars/ +│ ├── all.yml # Globale Variablen +│ └── vault.yml # Verschlüsselte Zugangsdaten (Vault) +├── host_vars/ # (Optional) Host-spezifische Variablen +├── inventories/ # (Optional) Inventare +├── playbook.yml # Haupt-Playbook +├── README.md # Diese Datei +└── roles/ + ├── common/ # Gemeinsame Tasks (z.B. Logging, mailx) + ├── rhel_upgrade/ # RHEL-spezifische Upgrade-Tasks + ├── sles_upgrade/ # SLES-spezifische Upgrade-Tasks + ├── post_upgrade/ # Reboot etc. + ├── suma_api_assign_clm/ # SUSE Manager API Integration + └── vmware_snapshot/ # VMware Snapshot Handling +``` + +## Voraussetzungen +- Ansible >= 2.9 +- python3-pyvmomi auf dem Ansible-Host (für VMware) +- Zielsysteme: SLES oder RHEL, angebunden an SUSE Manager (ggf. venv-salt-minion) +- Zugang zur SUSE Manager API (XML-RPC, meist Port 443) +- Zugang zum vCenter (API) +- Optional: Ansible Vault für sichere Zugangsdaten + +## Nutzung +1. **Zugangsdaten für SUSE Manager & vCenter sicher speichern** + - Erstelle eine Datei `vault.yml` in `group_vars`: + ```yaml + suma_api_url: "https://susemanager.example.com/rpc/api" + suma_api_user: "admin" + suma_api_pass: "geheim" + vcenter_hostname: "vcenter.example.com" + vcenter_user: "administrator@vsphere.local" + vcenter_password: "dein_passwort" + vcenter_datacenter: "DeinDatacenter" + vcenter_folder: "/" + ``` + - Verschlüssele die Datei mit Ansible Vault: + ```bash + ansible-vault encrypt playbook/group_vars/vault.yml + ``` + - Passe `group_vars/all.yml` an: + ```yaml + # ... bestehende Variablen ... + suma_api_url: "{{ vault_suma_api_url }}" + suma_api_user: "{{ vault_suma_api_user }}" + suma_api_pass: "{{ vault_suma_api_pass }}" + vcenter_hostname: "{{ vault_vcenter_hostname }}" + vcenter_user: "{{ vault_vcenter_user }}" + vcenter_password: "{{ vault_vcenter_password }}" + vcenter_datacenter: "{{ vault_vcenter_datacenter }}" + vcenter_folder: "{{ vault_vcenter_folder }}" + ``` + - Lade die Vault-Datei im Playbook: + ```yaml + vars_files: + - group_vars/all.yml + - group_vars/vault.yml + ``` + - Playbook-Aufruf mit Vault-Passwort: + ```bash + ansible-playbook playbook.yml --ask-vault-pass -e "target_clm_version=prod-2024-06" + ``` + +2. **VMware Snapshot Handling** + - Vor jedem Upgrade wird automatisch ein Snapshot erstellt. + - Bei aktiviertem Rollback (Variable `rollback: true`) wird die VM auf den Snapshot zurückgesetzt. + - Die Snapshot-Tasks laufen auf dem Ansible-Host (`delegate_to: localhost`). + +3. **Upgrade auf bestimmte CLM-Version** + - Beim Playbook-Aufruf die gewünschte Version angeben: + ```bash + ansible-playbook playbook.yml -e "target_clm_version=prod-2024-06" + ``` + - Das System wird per SUSE Manager API dem passenden Channel zugewiesen. + +4. **Rollback aktivieren (optional)** + - In `group_vars/all.yml`: + ```yaml + rollback: true + ``` + - Das Playbook setzt die VM dann auf den Snapshot zurück. + +5. **Mail-Benachrichtigung konfigurieren** + - Lokale Mail: `mail_to: "root@localhost"` + - Externer SMTP: + ```yaml + mail_smtp_host: "smtp.example.com" + mail_smtp_port: 587 + mail_smtp_user: "user@example.com" + mail_smtp_pass: "dein_passwort" + ``` + +## Wichtige Variablen (group_vars/all.yml) +- `upgrade_dry_run`: true/false (Simulation) +- `reboot_after_upgrade`: true/false +- `log_dir`: Logverzeichnis +- `rollback`: true/false +- `mail_to`: Empfängeradresse +- `mail_smtp_*`: SMTP-Parameter (optional) +- `target_clm_version`: Ziel-CLM-Channel (z.B. prod-2024-06) +- `suma_api_url`, `suma_api_user`, `suma_api_pass`: SUSE Manager API (empfohlen: Vault) +- `vcenter_hostname`, `vcenter_user`, `vcenter_password`, `vcenter_datacenter`, `vcenter_folder`: VMware/vCenter (empfohlen: Vault) + +## Sicherheitshinweis +**Lege Zugangsdaten (API, Mail, vCenter) niemals im Klartext ab!** Nutze immer Ansible Vault für sensible Daten. + +## Erweiterungsideen +- Integration mit Monitoring/Alerting +- Approval-Workflows +- Reporting +- Zusätzliche OS-Unterstützung + +## Support & Doku +- [SUSE Manager API Doku](https://documentation.suse.com/suma/) +- [Red Hat Upgrade Guide](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/) +- [SLES Upgrade Guide](https://documentation.suse.com/sles/) +- [Ansible VMware Doku](https://docs.ansible.com/ansible/latest/collections/community/vmware/vmware_guest_snapshot_module.html) + +--- +**Fragen oder Wünsche? Einfach melden!** diff --git a/docs/Runbook_SelfService.md b/docs/Runbook_SelfService.md new file mode 100644 index 0000000..fe9c490 --- /dev/null +++ b/docs/Runbook_SelfService.md @@ -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 --ask-vault-pass -e "target_clm_version=" + ``` + 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. diff --git a/playbook/group_vars/all.yml b/playbook/group_vars/all.yml new file mode 100644 index 0000000..38d5141 --- /dev/null +++ b/playbook/group_vars/all.yml @@ -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 diff --git a/playbook/group_vars/vault.yml b/playbook/group_vars/vault.yml new file mode 100644 index 0000000..9a17aa5 --- /dev/null +++ b/playbook/group_vars/vault.yml @@ -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" diff --git a/playbook/inventory_apps b/playbook/inventory_apps new file mode 100644 index 0000000..4a53391 --- /dev/null +++ b/playbook/inventory_apps @@ -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 diff --git a/playbook/playbook.yml b/playbook/playbook.yml new file mode 100644 index 0000000..6aecb56 --- /dev/null +++ b/playbook/playbook.yml @@ -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 diff --git a/playbook/requirements.yml b/playbook/requirements.yml new file mode 100644 index 0000000..6f5e63c --- /dev/null +++ b/playbook/requirements.yml @@ -0,0 +1,4 @@ +collections: + - name: community.vmware + - name: servicenow.servicenow + - name: community.general diff --git a/playbook/roles/common/tasks/main.yml b/playbook/roles/common/tasks/main.yml new file mode 100644 index 0000000..597ad39 --- /dev/null +++ b/playbook/roles/common/tasks/main.yml @@ -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 diff --git a/playbook/roles/compliance_check/tasks/main.yml b/playbook/roles/compliance_check/tasks/main.yml new file mode 100644 index 0000000..bd76e38 --- /dev/null +++ b/playbook/roles/compliance_check/tasks/main.yml @@ -0,0 +1,29 @@ +--- +- name: Compliance-Check: Führe OpenSCAP-Scan durch (sofern installiert) + 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) + 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 + 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 diff --git a/playbook/roles/post_upgrade/tasks/main.yml b/playbook/roles/post_upgrade/tasks/main.yml new file mode 100644 index 0000000..cb48ff2 --- /dev/null +++ b/playbook/roles/post_upgrade/tasks/main.yml @@ -0,0 +1,39 @@ +--- +- name: Reboot nach Upgrade (optional) + 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 + service_facts: + tags: health + +- name: Prüfe Status der kritischen Dienste + 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 diff --git a/playbook/roles/preflight_check/tasks/main.yml b/playbook/roles/preflight_check/tasks/main.yml new file mode 100644 index 0000000..7f3c41c --- /dev/null +++ b/playbook/roles/preflight_check/tasks/main.yml @@ -0,0 +1,131 @@ +--- +- name: Prüfe, ob aktueller Zeitpunkt im Maintenance-Window liegt + 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) + 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) + stat: + path: / + register: root_stat + tags: preflight + +- name: Warnung bei zu wenig Speicherplatz + 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 + 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 + 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 + shell: "python3 -c 'import pyVmomi'" + register: pyvmomi_check + ignore_errors: true + changed_when: false + tags: preflight + +- name: Warnung, wenn pyVmomi nicht installiert ist + 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 + 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 + 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 + 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) + 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 diff --git a/playbook/roles/rhel_upgrade/tasks/main.yml b/playbook/roles/rhel_upgrade/tasks/main.yml new file mode 100644 index 0000000..c07e9b3 --- /dev/null +++ b/playbook/roles/rhel_upgrade/tasks/main.yml @@ -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 diff --git a/playbook/roles/self_healing/tasks/main.yml b/playbook/roles/self_healing/tasks/main.yml new file mode 100644 index 0000000..1c223e2 --- /dev/null +++ b/playbook/roles/self_healing/tasks/main.yml @@ -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 diff --git a/playbook/roles/servicenow_tickets/tasks/main.yml b/playbook/roles/servicenow_tickets/tasks/main.yml new file mode 100644 index 0000000..7fb70cf --- /dev/null +++ b/playbook/roles/servicenow_tickets/tasks/main.yml @@ -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 diff --git a/playbook/roles/sles_upgrade/tasks/main.yml b/playbook/roles/sles_upgrade/tasks/main.yml new file mode 100644 index 0000000..1f299b6 --- /dev/null +++ b/playbook/roles/sles_upgrade/tasks/main.yml @@ -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" diff --git a/playbook/roles/smoke_tests/tasks/main.yml b/playbook/roles/smoke_tests/tasks/main.yml new file mode 100644 index 0000000..c85b2bc --- /dev/null +++ b/playbook/roles/smoke_tests/tasks/main.yml @@ -0,0 +1,109 @@ +--- +- name: Prüfe, ob HTTP-Service installiert ist (Apache/Nginx) + stat: + path: /usr/sbin/httpd + register: apache_check + ignore_errors: true + +- name: Prüfe, ob Nginx installiert ist + stat: + path: /usr/sbin/nginx + register: nginx_check + ignore_errors: true + +- name: Smoke-Test: Prüfe HTTP-Endpoint (nur wenn Web-Server installiert) + uri: + url: "{{ smoke_test_url | default('http://localhost') }}" + status_code: 200 + return_content: no + register: http_result + ignore_errors: true + when: apache_check.stat.exists or nginx_check.stat.exists + +- name: Smoke-Test: Prüfe offenen Port (optional) + 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 + stat: + path: /usr/bin/mysql + register: mysql_check + ignore_errors: true + +- name: Smoke-Test: Prüfe Datenbankverbindung (nur wenn MySQL installiert) + 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 + stat: + path: /u01/app/oracle/product + register: oracle_check + ignore_errors: true + +- name: Oracle DB: Finde alle Oracle SIDs (nur wenn Oracle installiert) + 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) + 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) + 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 < 0 + +- name: Oracle DB: Prüfe alle gefundenen Listener (nur wenn Oracle installiert) + 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) + 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 + 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' }}" diff --git a/playbook/roles/suma_api_assign_clm/tasks/main.yml b/playbook/roles/suma_api_assign_clm/tasks/main.yml new file mode 100644 index 0000000..13ea14b --- /dev/null +++ b/playbook/roles/suma_api_assign_clm/tasks/main.yml @@ -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 diff --git a/playbook/roles/vmware_snapshot/tasks/main.yml b/playbook/roles/vmware_snapshot/tasks/main.yml new file mode 100644 index 0000000..b62d9e4 --- /dev/null +++ b/playbook/roles/vmware_snapshot/tasks/main.yml @@ -0,0 +1,65 @@ +--- +- name: Erstelle VMware Snapshot vor Upgrade + 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 + 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 + set_fact: + rollback: true + when: snapshot_result is failed + +- name: Breche Playbook ab, wenn Snapshot-Erstellung fehlschlägt + 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) + 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) + 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 diff --git a/playbook/servicenow_inventory.yml b/playbook/servicenow_inventory.yml new file mode 100644 index 0000000..03989cf --- /dev/null +++ b/playbook/servicenow_inventory.yml @@ -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 diff --git a/scripts/install_collections.sh b/scripts/install_collections.sh new file mode 100755 index 0000000..a72e5a8 --- /dev/null +++ b/scripts/install_collections.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." +ansible-galaxy collection install -r playbook/requirements.yml --force diff --git a/scripts/run_patch.sh b/scripts/run_patch.sh new file mode 100755 index 0000000..066c8be --- /dev/null +++ b/scripts/run_patch.sh @@ -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 [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[@]}" From fbbf4e20891246d6a7c73180bea8ce440771bce5 Mon Sep 17 00:00:00 2001 From: Automation Admin Date: Thu, 7 Aug 2025 22:26:18 +0000 Subject: [PATCH 2/5] chore: ignore real vault.yml; add vault.sample.yml and tooling ignores --- .gitignore | 5 +++++ playbook/group_vars/{vault.yml => vault.sample.yml} | 0 2 files changed, 5 insertions(+) create mode 100644 .gitignore rename playbook/group_vars/{vault.yml => vault.sample.yml} (100%) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b86e7cc --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +playbook/group_vars/vault.yml +.ansible_facts_cache/ +.vscode/ +.idea/ +.env diff --git a/playbook/group_vars/vault.yml b/playbook/group_vars/vault.sample.yml similarity index 100% rename from playbook/group_vars/vault.yml rename to playbook/group_vars/vault.sample.yml From 65b4fa97171039971bae18bedbc926aead7eba47 Mon Sep 17 00:00:00 2001 From: Automation Admin Date: Fri, 8 Aug 2025 00:01:20 +0000 Subject: [PATCH 3/5] ci: add woodpecker pipeline (lint, dryrun, manual preflight/patch) --- .woodpecker.yml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .woodpecker.yml diff --git a/.woodpecker.yml b/.woodpecker.yml new file mode 100644 index 0000000..b33d751 --- /dev/null +++ b/.woodpecker.yml @@ -0,0 +1,33 @@ +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 + - ansible-playbook playbook/playbook.yml --check --list-tasks + + run_preflight: + when: + event: [manual] + image: python:3.11-slim + commands: + - pip install --no-cache-dir ansible + - ansible-playbook playbook/playbook.yml --tags preflight -l pdp-portal --ask-vault-pass + + run_patch: + when: + event: [manual] + image: python:3.11-slim + commands: + - pip install --no-cache-dir ansible + - ansible-galaxy collection install -r playbook/requirements.yml --force + - ansible-playbook playbook/playbook.yml -l pdp-portal -e "target_clm_version=prod-2024-06" --ask-vault-pass From 3b17e5b4058f77f3deb30216ad78d078c205cb9f Mon Sep 17 00:00:00 2001 From: Automation Admin Date: Fri, 8 Aug 2025 01:04:20 +0000 Subject: [PATCH 4/5] docs: README aktualisiert (CI/CD, OAuth, Secrets, Nutzung) --- docs/README.md | 253 ++++++++++++++++++++++++++----------------------- 1 file changed, 133 insertions(+), 120 deletions(-) diff --git a/docs/README.md b/docs/README.md index 077519a..050bf3b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,135 +1,148 @@ -# Enterprise Auto-Upgrade Playbook für SLES & RHEL +## Enterprise Auto-Upgrade Playbook für SLES & RHEL -## Übersicht -Dieses Projekt bietet ein modulares, Enterprise-taugliches Ansible-Playbook für automatisierte Upgrades und Patch-Management von SLES (SUSE Linux Enterprise Server) und RHEL (Red Hat Enterprise Linux) Systemen. Es unterstützt: -- Automatische OS-Erkennung -- Upgrade nach Hersteller-Best-Practice -- **VMware-Snapshots für Backup/Rollback** -- Logging -- Mail-Benachrichtigung (lokal & extern via mailx) -- Dynamische Zuweisung von CLM-Channels via SUSE Manager API +### Ü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 +### Verzeichnisstruktur (Auszug) ``` -playbook/ -├── group_vars/ -│ ├── all.yml # Globale Variablen -│ └── vault.yml # Verschlüsselte Zugangsdaten (Vault) -├── host_vars/ # (Optional) Host-spezifische Variablen -├── inventories/ # (Optional) Inventare -├── playbook.yml # Haupt-Playbook -├── README.md # Diese Datei -└── roles/ - ├── common/ # Gemeinsame Tasks (z.B. Logging, mailx) - ├── rhel_upgrade/ # RHEL-spezifische Upgrade-Tasks - ├── sles_upgrade/ # SLES-spezifische Upgrade-Tasks - ├── post_upgrade/ # Reboot etc. - ├── suma_api_assign_clm/ # SUSE Manager API Integration - └── vmware_snapshot/ # VMware Snapshot Handling +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 >= 2.9 -- python3-pyvmomi auf dem Ansible-Host (für VMware) -- Zielsysteme: SLES oder RHEL, angebunden an SUSE Manager (ggf. venv-salt-minion) -- Zugang zur SUSE Manager API (XML-RPC, meist Port 443) -- Zugang zum vCenter (API) -- Optional: Ansible Vault für sichere Zugangsdaten +### 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]] -## Nutzung -1. **Zugangsdaten für SUSE Manager & vCenter sicher speichern** - - Erstelle eine Datei `vault.yml` in `group_vars`: - ```yaml - suma_api_url: "https://susemanager.example.com/rpc/api" - suma_api_user: "admin" - suma_api_pass: "geheim" - vcenter_hostname: "vcenter.example.com" - vcenter_user: "administrator@vsphere.local" - vcenter_password: "dein_passwort" - vcenter_datacenter: "DeinDatacenter" - vcenter_folder: "/" - ``` - - Verschlüssele die Datei mit Ansible Vault: - ```bash - ansible-vault encrypt playbook/group_vars/vault.yml - ``` - - Passe `group_vars/all.yml` an: - ```yaml - # ... bestehende Variablen ... - suma_api_url: "{{ vault_suma_api_url }}" - suma_api_user: "{{ vault_suma_api_user }}" - suma_api_pass: "{{ vault_suma_api_pass }}" - vcenter_hostname: "{{ vault_vcenter_hostname }}" - vcenter_user: "{{ vault_vcenter_user }}" - vcenter_password: "{{ vault_vcenter_password }}" - vcenter_datacenter: "{{ vault_vcenter_datacenter }}" - vcenter_folder: "{{ vault_vcenter_folder }}" - ``` - - Lade die Vault-Datei im Playbook: - ```yaml - vars_files: - - group_vars/all.yml - - group_vars/vault.yml - ``` - - Playbook-Aufruf mit Vault-Passwort: - ```bash - ansible-playbook playbook.yml --ask-vault-pass -e "target_clm_version=prod-2024-06" - ``` +Installation Collections: +``` +make deps +``` -2. **VMware Snapshot Handling** - - Vor jedem Upgrade wird automatisch ein Snapshot erstellt. - - Bei aktiviertem Rollback (Variable `rollback: true`) wird die VM auf den Snapshot zurückgesetzt. - - Die Snapshot-Tasks laufen auf dem Ansible-Host (`delegate_to: localhost`). +### 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 +``` -3. **Upgrade auf bestimmte CLM-Version** - - Beim Playbook-Aufruf die gewünschte Version angeben: - ```bash - ansible-playbook playbook.yml -e "target_clm_version=prod-2024-06" - ``` - - Das System wird per SUSE Manager API dem passenden Channel zugewiesen. +2) Globale Variablen prüfen +- `playbook/group_vars/all.yml` enthält Standardwerte (u.a. Skip-Flags, Mail-Empfänger, Wartungsfenster, Log-Verzeichnis). -4. **Rollback aktivieren (optional)** - - In `group_vars/all.yml`: - ```yaml - rollback: true - ``` - - Das Playbook setzt die VM dann auf den Snapshot zurück. +3) Dynamic Inventory (ServiceNow) +- `playbook/servicenow_inventory.yml` nutzt den ServiceNow-Inventory-Plugin. +- Erwartet in Vault: `servicenow_instance`, `servicenow_user`, `servicenow_pass`. -5. **Mail-Benachrichtigung konfigurieren** - - Lokale Mail: `mail_to: "root@localhost"` - - Externer SMTP: - ```yaml - mail_smtp_host: "smtp.example.com" - mail_smtp_port: 587 - mail_smtp_user: "user@example.com" - mail_smtp_pass: "dein_passwort" - ``` +### Nutzung (lokal) +- Lint: +``` +make lint +``` -## Wichtige Variablen (group_vars/all.yml) -- `upgrade_dry_run`: true/false (Simulation) -- `reboot_after_upgrade`: true/false -- `log_dir`: Logverzeichnis -- `rollback`: true/false -- `mail_to`: Empfängeradresse -- `mail_smtp_*`: SMTP-Parameter (optional) -- `target_clm_version`: Ziel-CLM-Channel (z.B. prod-2024-06) -- `suma_api_url`, `suma_api_user`, `suma_api_pass`: SUSE Manager API (empfohlen: Vault) -- `vcenter_hostname`, `vcenter_user`, `vcenter_password`, `vcenter_datacenter`, `vcenter_folder`: VMware/vCenter (empfohlen: Vault) +- Playbook ausführen (Beispiele): +``` +# App-Gruppe patchen, Ziel-CLM setzen +make run APP=pdp-portal CLM=prod-2024-06 EXTRA="debug_mode=true" -## Sicherheitshinweis -**Lege Zugangsdaten (API, Mail, vCenter) niemals im Klartext ab!** Nutze immer Ansible Vault für sensible Daten. +# Direkter Aufruf +./scripts/run_patch.sh pdp-portal prod-2024-06 "skip_compliance=true skip_smoke_tests=false" +``` -## Erweiterungsideen -- Integration mit Monitoring/Alerting -- Approval-Workflows -- Reporting -- Zusätzliche OS-Unterstützung +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` -## Support & Doku -- [SUSE Manager API Doku](https://documentation.suse.com/suma/) -- [Red Hat Upgrade Guide](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/) -- [SLES Upgrade Guide](https://documentation.suse.com/sles/) -- [Ansible VMware Doku](https://docs.ansible.com/ansible/latest/collections/community/vmware/vmware_guest_snapshot_module.html) +### 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) ---- -**Fragen oder Wünsche? Einfach melden!** +### 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=` 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. From ca24d66e7a8680b80c22a35e464f0bfe5ec31265 Mon Sep 17 00:00:00 2001 From: Automation Admin Date: Fri, 8 Aug 2025 01:58:30 +0000 Subject: [PATCH 5/5] ci: use vault secret file and local dryrun; fix YAML/FQCN in roles; inventory tweaks --- .woodpecker.yml | 17 +++++-- ansible.cfg | 2 +- playbook/inventory_apps | 2 +- .../roles/compliance_check/tasks/main.yml | 13 +++--- playbook/roles/post_upgrade/tasks/main.yml | 8 ++-- playbook/roles/preflight_check/tasks/main.yml | 30 ++++++------- playbook/roles/smoke_tests/tasks/main.yml | 45 +++++++++---------- playbook/roles/vmware_snapshot/tasks/main.yml | 14 +++--- 8 files changed, 72 insertions(+), 59 deletions(-) diff --git a/.woodpecker.yml b/.woodpecker.yml index b33d751..e97a28f 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -13,7 +13,11 @@ steps: image: python:3.11-slim commands: - pip install --no-cache-dir ansible - - ansible-playbook playbook/playbook.yml --check --list-tasks + - 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: @@ -21,7 +25,11 @@ steps: image: python:3.11-slim commands: - pip install --no-cache-dir ansible - - ansible-playbook playbook/playbook.yml --tags preflight -l pdp-portal --ask-vault-pass + - 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: @@ -29,5 +37,8 @@ steps: 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" --ask-vault-pass + - ansible-playbook playbook/playbook.yml -l pdp-portal -e "target_clm_version=prod-2024-06" \ + --vault-password-file .vault_pass + secrets: [ANSIBLE_VAULT_PASSWORD] diff --git a/ansible.cfg b/ansible.cfg index 448b7b3..27026ed 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -1,7 +1,7 @@ [defaults] inventory = playbook/servicenow_inventory.yml roles_path = playbook/roles -collections_paths = ~/.ansible/collections:./collections +collections_path = ~/.ansible/collections:./collections host_key_checking = False retry_files_enabled = False stdout_callback = yaml diff --git a/playbook/inventory_apps b/playbook/inventory_apps index 4a53391..f7e1872 100644 --- a/playbook/inventory_apps +++ b/playbook/inventory_apps @@ -22,7 +22,7 @@ app_mail=git-app@example.com # Optional: Gruppen für Umgebungen #[dev:children] #pdp-portal -git +#git #[prod:children] #confluence diff --git a/playbook/roles/compliance_check/tasks/main.yml b/playbook/roles/compliance_check/tasks/main.yml index bd76e38..418cab5 100644 --- a/playbook/roles/compliance_check/tasks/main.yml +++ b/playbook/roles/compliance_check/tasks/main.yml @@ -1,18 +1,21 @@ --- -- name: Compliance-Check: Führe OpenSCAP-Scan durch (sofern installiert) - 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 +- 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) - shell: lynis audit system --quiet --logfile {{ log_dir }}/lynis_{{ inventory_hostname }}.log +- 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 - mail: + community.general.mail: host: "localhost" port: 25 to: "{{ linux_admins_mail }}" diff --git a/playbook/roles/post_upgrade/tasks/main.yml b/playbook/roles/post_upgrade/tasks/main.yml index cb48ff2..c6a06ec 100644 --- a/playbook/roles/post_upgrade/tasks/main.yml +++ b/playbook/roles/post_upgrade/tasks/main.yml @@ -1,17 +1,17 @@ --- - name: Reboot nach Upgrade (optional) - reboot: + 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 - service_facts: +- name: "Health-Check: Prüfe, ob kritische Dienste laufen" + ansible.builtin.service_facts: tags: health - name: Prüfe Status der kritischen Dienste - assert: + ansible.builtin.assert: that: - "(services[item].state == 'running') or (services[item].state == 'started')" fail_msg: "Kritischer Dienst {{ item }} läuft nicht!" diff --git a/playbook/roles/preflight_check/tasks/main.yml b/playbook/roles/preflight_check/tasks/main.yml index 7f3c41c..f3dc0c3 100644 --- a/playbook/roles/preflight_check/tasks/main.yml +++ b/playbook/roles/preflight_check/tasks/main.yml @@ -1,14 +1,14 @@ --- -- name: Prüfe, ob aktueller Zeitpunkt im Maintenance-Window liegt - set_fact: +- 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) - fail: +- 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: >- ( @@ -18,14 +18,14 @@ ) tags: preflight -- name: Prüfe freien Speicherplatz auf / (mind. 5GB empfohlen) - stat: +- name: "Prüfe freien Speicherplatz auf / (mind. 5GB empfohlen)" + ansible.builtin.stat: path: / register: root_stat tags: preflight - name: Warnung bei zu wenig Speicherplatz - assert: + 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)" @@ -33,7 +33,7 @@ tags: preflight - name: Prüfe Erreichbarkeit von SUSE Manager - uri: + ansible.builtin.uri: url: "{{ suma_api_url }}" method: GET validate_certs: no @@ -45,7 +45,7 @@ tags: preflight - name: Warnung, wenn SUSE Manager nicht erreichbar - assert: + ansible.builtin.assert: that: - suma_reachable.status is defined and suma_reachable.status == 200 fail_msg: "SUSE Manager API nicht erreichbar!" @@ -53,14 +53,14 @@ tags: preflight - name: Prüfe, ob VMware-Snapshot-Modul verfügbar ist - shell: "python3 -c 'import pyVmomi'" + 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 - assert: + ansible.builtin.assert: that: - pyvmomi_check.rc == 0 fail_msg: "pyVmomi (VMware-Modul) nicht installiert!" @@ -68,7 +68,7 @@ tags: preflight - name: Prüfe, ob aktueller SUSE Manager Channel synchronisiert ist - uri: + ansible.builtin.uri: url: "{{ suma_api_url }}" method: POST body_format: json @@ -91,7 +91,7 @@ tags: preflight - name: Hole Channel-Details für Ziel-CLM-Version - uri: + ansible.builtin.uri: url: "{{ suma_api_url }}" method: POST body_format: json @@ -114,7 +114,7 @@ tags: preflight - name: Prüfe Channel-Sync-Status - assert: + ansible.builtin.assert: that: - suma_channel_details.json.result.last_sync is defined fail_msg: "Channel {{ target_clm_version }} ist nicht synchronisiert!" @@ -122,7 +122,7 @@ tags: preflight - name: Slack-Benachrichtigung bei kritischen Fehlern (Beispiel) - slack: + 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" diff --git a/playbook/roles/smoke_tests/tasks/main.yml b/playbook/roles/smoke_tests/tasks/main.yml index c85b2bc..b45e8bc 100644 --- a/playbook/roles/smoke_tests/tasks/main.yml +++ b/playbook/roles/smoke_tests/tasks/main.yml @@ -1,27 +1,26 @@ ---- - name: Prüfe, ob HTTP-Service installiert ist (Apache/Nginx) - stat: + ansible.builtin.stat: path: /usr/sbin/httpd register: apache_check ignore_errors: true - name: Prüfe, ob Nginx installiert ist - stat: + 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) - uri: +- 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: no + 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) - wait_for: +- 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 @@ -29,41 +28,41 @@ ignore_errors: true - name: Prüfe, ob MySQL/MariaDB installiert ist - stat: + ansible.builtin.stat: path: /usr/bin/mysql register: mysql_check ignore_errors: true -- name: Smoke-Test: Prüfe Datenbankverbindung (nur wenn MySQL installiert) - 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('') }}" +- 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 - stat: + 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) - shell: | +- 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) - shell: | +- 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) - shell: | +- 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 }} @@ -76,8 +75,8 @@ 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) - shell: | +- 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 }} @@ -86,8 +85,8 @@ 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) - copy: +- 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 }} @@ -100,7 +99,7 @@ when: oracle_check.stat.exists - name: Smoke-Test Ergebnis zusammenfassen - debug: + ansible.builtin.debug: msg: - "HTTP-Test: {{ http_result.status | default('NOT INSTALLED') }}" - "Port-Test: {{ port_result.state | default('FAILED') }}" diff --git a/playbook/roles/vmware_snapshot/tasks/main.yml b/playbook/roles/vmware_snapshot/tasks/main.yml index b62d9e4..0e3ccc3 100644 --- a/playbook/roles/vmware_snapshot/tasks/main.yml +++ b/playbook/roles/vmware_snapshot/tasks/main.yml @@ -1,6 +1,6 @@ --- - name: Erstelle VMware Snapshot vor Upgrade - vmware_guest_snapshot: + community.vmware.vmware_guest_snapshot: hostname: "{{ vcenter_hostname }}" username: "{{ vcenter_user }}" password: "{{ vcenter_password }}" @@ -20,23 +20,23 @@ delay: 10 - name: Logge Fehler bei Snapshot-Erstellung - copy: + 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 - set_fact: + ansible.builtin.set_fact: rollback: true when: snapshot_result is failed - name: Breche Playbook ab, wenn Snapshot-Erstellung fehlschlägt - fail: + 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) - vmware_guest_snapshot: +- 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 }}" @@ -50,7 +50,7 @@ delegate_to: localhost - name: Lösche VMware Snapshot nach erfolgreichem Patchlauf (optional) - vmware_guest_snapshot: + community.vmware.vmware_guest_snapshot: hostname: "{{ vcenter_hostname }}" username: "{{ vcenter_user }}" password: "{{ vcenter_password }}"