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/.gitignore b/.gitignore index 8012542..b86e7cc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,5 @@ -# Ansible caches -.ansible -.ansible_facts_cache - -# Secrets playbook/group_vars/vault.yml +.ansible_facts_cache/ +.vscode/ +.idea/ .env -*.retry 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/.woodpecker.yml b/.woodpecker.yml new file mode 100644 index 0000000..e97a28f --- /dev/null +++ b/.woodpecker.yml @@ -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] 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 index a30ce43..27026ed 100644 --- a/ansible.cfg +++ b/ansible.cfg @@ -1,9 +1,18 @@ [defaults] -inventory = playbook/inventories +inventory = playbook/servicenow_inventory.yml roles_path = playbook/roles +collections_path = ~/.ansible/collections:./collections +host_key_checking = False retry_files_enabled = False stdout_callback = yaml -host_key_checking = False -force_color = True -forks = 10 -callback_whitelist = profile_tasks +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 index e69de29..050bf3b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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=` 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. 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.sample.yml b/playbook/group_vars/vault.sample.yml new file mode 100644 index 0000000..9a17aa5 --- /dev/null +++ b/playbook/group_vars/vault.sample.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..f7e1872 --- /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..418cab5 --- /dev/null +++ b/playbook/roles/compliance_check/tasks/main.yml @@ -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 diff --git a/playbook/roles/post_upgrade/tasks/main.yml b/playbook/roles/post_upgrade/tasks/main.yml new file mode 100644 index 0000000..c6a06ec --- /dev/null +++ b/playbook/roles/post_upgrade/tasks/main.yml @@ -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 diff --git a/playbook/roles/preflight_check/tasks/main.yml b/playbook/roles/preflight_check/tasks/main.yml new file mode 100644 index 0000000..f3dc0c3 --- /dev/null +++ b/playbook/roles/preflight_check/tasks/main.yml @@ -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 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..b45e8bc --- /dev/null +++ b/playbook/roles/smoke_tests/tasks/main.yml @@ -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 < 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' }}" 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..0e3ccc3 --- /dev/null +++ b/playbook/roles/vmware_snapshot/tasks/main.yml @@ -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 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[@]}"