diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..359d707 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +**/bak.yml +**/vault.yml \ No newline at end of file diff --git a/README.md b/README.md index e69de29..f29215c 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,76 @@ +# AIM --- Ansible Inventory Manager + +AIM is an operator-focused Python application for managing Ansible +customer inventories, access configuration, Vaults and curated playbook +execution. + +## Source layout + +``` text +/etc/ansible/scripts/ +├── README.md +├── install.md +├── CHANGELOG.md +├── docs/ +├── pyproject.toml +└── src/ + └── aim/ +``` + +AIM uses `/etc/ansible/inventories//hosts.yml` as the +inventory source of truth. + +## Supported platform groups + +- `linux` +- `windows` +- `sophosxgs` +- `pfsense` + +A host can belong to multiple groups/subgroups. Platform groups remain +the top-level groups because they define Ansible connection semantics. + +## Quick start + +See [install.md](install.md) for installation, virtual-environment +setup, authorization-group configuration and recovery of Git-ignored +runtime data. + +Start AIM with: + +``` bash +aim +``` + +Useful UI troubleshooting modes: + +``` bash +aim --no-clear +aim --plain +aim --live-output +``` + +## Documentation + +- [Installation](install.md) +- [Operations](docs/OPERATIONS.md) +- [Inventory](docs/INVENTORY.md) +- [Windows / WinRM / AD](docs/WINDOWS.md) +- [Recovery](docs/RECOVERY.md) +- [Security and sensitive data](docs/SECURITY.md) +- [Development](docs/DEVELOPMENT.md) +- [Changelog](CHANGELOG.md) + +## Important operational rules + +AIM does not use `.hosts.tsv` as inventory state. `hosts.yml` is +authoritative. + +Routine host changes use local YAML validation and do not unnecessarily +prompt for the Vault password. Explicit inventory validation may invoke +`ansible-inventory` and request the customer Vault password when a Vault +exists. + +Existing customer-specific values and arbitrary valid YAML structures +should be preserved unless an operator explicitly requests an operation +that changes them. diff --git a/aim.yml b/aim.yml new file mode 100644 index 0000000..2df302f --- /dev/null +++ b/aim.yml @@ -0,0 +1,12 @@ +root_dir: /etc/ansible +service_user: svc_ansible +required_group: root +platform_groups: +- linux +- windows +- sophosxgs +- pfsense +ui: + clear_screen: true + ascii: false + output: compact diff --git a/inventories/desq_gaming/.aim.yml b/inventories/desq_gaming/.aim.yml new file mode 100644 index 0000000..1ea379b --- /dev/null +++ b/inventories/desq_gaming/.aim.yml @@ -0,0 +1,5 @@ +domain_suffix: desq-gaming.lan +network_address: 192.168.20.0 +netmask: 255.255.255.0 +ad_dns_domain: '' +ad_netbios_domain: '' diff --git a/inventories/desq_gaming/group_vars/all/main.yml b/inventories/desq_gaming/group_vars/all/main.yml new file mode 100644 index 0000000..8b801f4 --- /dev/null +++ b/inventories/desq_gaming/group_vars/all/main.yml @@ -0,0 +1 @@ +# group_vars/all/main.yml for desq_gaming diff --git a/inventories/desq_gaming/group_vars/linux/.ssh/svc_ansible b/inventories/desq_gaming/group_vars/linux/.ssh/svc_ansible new file mode 100644 index 0000000..3111a57 --- /dev/null +++ b/inventories/desq_gaming/group_vars/linux/.ssh/svc_ansible @@ -0,0 +1,8 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABCzzsHfog +Wv9erYAv5gNSMrAAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIFEjnIIVsyXXaoqd +VGOGwSPXthf8vzZ7L//SZP3OuJjxAAAAoPoC23Ps5HmvCpJrlOsubdcAjq0Ol65xhOb8VQ +OolhyCPFPj/eH1z21w/PvXhL1S8tDsBut27qW8+5dSwT2gqjtt/x2SiOQYMHt6EjGGAISu +NNy9L+vRcOFIB4Lo1IE/BMeZRUpgW2GXRFcQH9KgZXh/IWMDqcS5q8GbIT69OUxVUeBg3x +Wa5f3MyCUMEmIMkBcdbJrebWXLjDvVYDI3myE= +-----END OPENSSH PRIVATE KEY----- diff --git a/inventories/desq_gaming/group_vars/linux/.ssh/svc_ansible.pub b/inventories/desq_gaming/group_vars/linux/.ssh/svc_ansible.pub new file mode 100644 index 0000000..7b6b399 --- /dev/null +++ b/inventories/desq_gaming/group_vars/linux/.ssh/svc_ansible.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFEjnIIVsyXXaoqdVGOGwSPXthf8vzZ7L//SZP3OuJjx svc_ansible@desq_gaming diff --git a/inventories/desq_gaming/group_vars/linux/main.yml b/inventories/desq_gaming/group_vars/linux/main.yml new file mode 100644 index 0000000..90f94ab --- /dev/null +++ b/inventories/desq_gaming/group_vars/linux/main.yml @@ -0,0 +1,7 @@ +# Linux / SSH variables +ansible_connection: ssh +ansible_user: svc_ansible +ansible_private_key_file: + /etc/ansible/inventories/desq_gaming/group_vars/linux/.ssh/svc_ansible +ansible_ssh_pass: '{{ ansible_vault_linux_ssh_pass }}' +ansible_become_method: sudo diff --git a/inventories/desq_gaming/group_vars/pfsense/main.yml b/inventories/desq_gaming/group_vars/pfsense/main.yml new file mode 100644 index 0000000..86095a4 --- /dev/null +++ b/inventories/desq_gaming/group_vars/pfsense/main.yml @@ -0,0 +1,2 @@ +# pfSense-specific variables +{} diff --git a/inventories/desq_gaming/group_vars/sophosxgs/main.yml b/inventories/desq_gaming/group_vars/sophosxgs/main.yml new file mode 100644 index 0000000..3bfbf55 --- /dev/null +++ b/inventories/desq_gaming/group_vars/sophosxgs/main.yml @@ -0,0 +1,7 @@ +# SophosXGS-specific variables +#ansible_password: "{{ ansible_password }}" # Passwort wird aus --ask-pass übernommen +ansible_user: admin +ansible_connection: ansible.netcommon.httpapi +ansible_httpapi_validate_certs: false +ansible_httpapi_port: 4444 +ansible_network_os: sophos.sophos_firewall.sfos diff --git a/inventories/desq_gaming/group_vars/windows/main.yml b/inventories/desq_gaming/group_vars/windows/main.yml new file mode 100644 index 0000000..a8bfa4b --- /dev/null +++ b/inventories/desq_gaming/group_vars/windows/main.yml @@ -0,0 +1,7 @@ +# Windows / WinRM variables +ansible_connection: winrm +ansible_port: 5986 +ansible_winrm_transport: ntlm +ansible_winrm_server_cert_validation: ignore +ansible_user: svc_ansible +ansible_password: '{{ vault_windows_ansible_password }}' diff --git a/inventories/desq_gaming/host_vars/dewedns02.desq-gaming.lan/main.yml b/inventories/desq_gaming/host_vars/dewedns02.desq-gaming.lan/main.yml new file mode 100644 index 0000000..e69de29 diff --git a/inventories/desq_gaming/host_vars/dewenpm01.desq-gaming.lan/main.yml b/inventories/desq_gaming/host_vars/dewenpm01.desq-gaming.lan/main.yml new file mode 100644 index 0000000..e69de29 diff --git a/inventories/desq_gaming/host_vars/dewepbs01.desq-gaming.lan/main.yml b/inventories/desq_gaming/host_vars/dewepbs01.desq-gaming.lan/main.yml new file mode 100644 index 0000000..e69de29 diff --git a/inventories/desq_gaming/host_vars/dewepve01.desq-gaming.lan/main.yml b/inventories/desq_gaming/host_vars/dewepve01.desq-gaming.lan/main.yml new file mode 100644 index 0000000..e69de29 diff --git a/inventories/desq_gaming/hosts.aim-session.bak.yml b/inventories/desq_gaming/hosts.aim-session.bak.yml new file mode 100644 index 0000000..dc1aef2 --- /dev/null +++ b/inventories/desq_gaming/hosts.aim-session.bak.yml @@ -0,0 +1,4 @@ +all: + children: + desq_gaming: + children: {} diff --git a/inventories/desq_gaming/hosts.yml b/inventories/desq_gaming/hosts.yml new file mode 100644 index 0000000..3dd7983 --- /dev/null +++ b/inventories/desq_gaming/hosts.yml @@ -0,0 +1,99 @@ +all: + children: + desq_gaming: + children: + linux: + children: + networking: + hosts: + dewenpm01.desq-gaming.lan: + ansible_host: 192.168.20.3 + dewedns02.desq-gaming.lan: + ansible_host: 192.168.20.2 + dewesrv-unifi02.desq-gaming.lan: + ansible_host: 192.168.99.5 + + backup: + hosts: + dewepbs01.desq-gaming.lan: + ansible_host: 192.168.99.32 + + proxmox: + hosts: + dewepbs01.desq-gaming.lan: + ansible_host: 192.168.99.32 + dewepve01.desq-gaming.lan: + ansible_host: 192.168.99.31 + + hosting: + hosts: + dewepve01.desq-gaming.lan: + ansible_host: 192.168.99.31 + + management: + hosts: + dewesrv-ansible01.desq-gaming.lan: + ansible_host: 192.168.20.46 + dewesrv-patch01.desq-gaming.lan: + ansible_host: 192.168.20.45 + + applications: + hosts: + dewesrv-budget02.desq-gaming.lan: + ansible_host: 192.168.20.44 + dewesrv-cache02.desq-gaming.lan: + ansible_host: 192.168.20.24 + dewesrv-cloud01.desq-gaming.lan: + ansible_host: 192.168.20.14 + dewesrv-crafty02.desq-gaming.lan: + ansible_host: 192.168.20.19 + dewesrv-db01.desq-gaming.lan: + ansible_host: 192.168.20.21 + dewesrv-db02.desq-gaming.lan: + ansible_host: 192.168.20.23 + dewesrv-docker02.desq-gaming.lan: + ansible_host: 192.168.20.30 + dewesrv-git01.desq-gaming.lan: + ansible_host: 192.168.20.38 + dewesrv-grafana01.desq-gaming.lan: + ansible_host: 192.168.20.22 + dewesrv-ha01.desq-gaming.lan: + ansible_host: 192.168.30.10 + dewesrv-homarr01.desq-gaming.lan: + ansible_host: 192.168.20.35 + dewesrv-mail01.desq-gaming.lan: + ansible_host: 192.168.20.40 + dewesrv-nodejs01.desq-gaming.lan: + ansible_host: 192.168.20.20 + dewesrv-omv01.desq-gaming.lan: + ansible_host: 192.168.20.15 + dewesrv-overseerr01.desq-gaming.lan: + ansible_host: 192.168.20.18 + dewesrv-plex02.desq-gaming.lan: + ansible_host: 192.168.20.39 + dewesrv-puppet01.desq-gaming.lan: + ansible_host: 192.168.20.25 + dewesrv-recipe01.desq-gaming.lan: + ansible_host: 192.168.20.37 + dewesrv-rust02.desq-gaming.lan: + ansible_host: 192.168.20.27 + dewesrv-speed01.desq-gaming.lan: + ansible_host: 192.168.20.16 + dewesrv-steam01.desq-gaming.lan: + ansible_host: 192.168.20.12 + dewesrv-support01.desq-gaming.lan: + ansible_host: 192.168.20.28 + dewesrv-tautulli01.desq-gaming.lan: + ansible_host: 192.168.20.17 + dewesrv-tv01.desq-gaming.lan: + ansible_host: 192.168.20.43 + dewesrv-uptime01.desq-gaming.lan: + ansible_host: 192.168.20.11 + dewesrv-vault01.desq-gaming.lan: + ansible_host: 192.168.20.34 + dewesrv-wallos01.desq-gaming.lan: + ansible_host: 192.168.20.29 + dewesrv-wazuh01.desq-gaming.lan: + ansible_host: 192.168.20.13 + dewesrv-wiki01.desq-gaming.lan: + ansible_host: 192.168.20.36 \ No newline at end of file diff --git a/playbooks/checkmk_cleanup.yml b/playbooks/checkmk_cleanup.yml new file mode 100644 index 0000000..5a322e3 --- /dev/null +++ b/playbooks/checkmk_cleanup.yml @@ -0,0 +1,64 @@ +--- +- name: "Checkmk | Cleanup agent" + hosts: all + gather_facts: true + + tasks: + - name: "Windows | Remove installed Checkmk agent" + when: ansible_facts['os_family'] == 'Windows' + ansible.windows.win_powershell: + script: | + $Ansible.Changed = $false + $uninstallRoots = @( + 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall', + 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall' + ) + + $products = foreach ($root in $uninstallRoots) { + if (Test-Path -LiteralPath $root) { + Get-ChildItem -LiteralPath $root -ErrorAction SilentlyContinue | + ForEach-Object { + $product = Get-ItemProperty -LiteralPath $_.PSPath -ErrorAction SilentlyContinue + if ($product.DisplayName -like 'Check MK Agent*' -or + $product.DisplayName -like 'Checkmk Agent*') { + [PSCustomObject]@{ + ProductCode = $_.PSChildName + DisplayName = $product.DisplayName + } + } + } + } + } + + foreach ($product in $products) { + if ($product.ProductCode -match '^\{[0-9A-Fa-f-]+\}$') { + $process = Start-Process -FilePath 'msiexec.exe' ` + -ArgumentList "/x $($product.ProductCode) /qn /norestart" ` + -Wait -PassThru + + if ($process.ExitCode -notin @(0, 1605, 1614, 3010)) { + throw "Failed to uninstall $($product.DisplayName). MSI exit code: $($process.ExitCode)" + } + + if ($process.ExitCode -in @(0, 3010)) { + $Ansible.Changed = $true + } + } + } + + $Ansible.Result = @{ + removed_products = @($products.DisplayName) + } + + - name: "Debian | Remove Checkmk agent" + when: ansible_facts['os_family'] == 'Debian' + ansible.builtin.apt: + name: check-mk-agent + state: absent + purge: true + + - name: "RedHat | Remove Checkmk agent" + when: ansible_facts['os_family'] == 'RedHat' + ansible.builtin.dnf: + name: check-mk-agent + state: absent diff --git a/playbooks/checkmk_deploy.yml b/playbooks/checkmk_deploy.yml new file mode 100644 index 0000000..2224998 --- /dev/null +++ b/playbooks/checkmk_deploy.yml @@ -0,0 +1,10 @@ +--- +- name: "Checkmk | Deploy agent, scripts and configuration" + hosts: all + gather_facts: true + + roles: + - checkmk_agent + - server_role_selection + - checkmk_scripts + - checkmk_agent_config diff --git a/playbooks/checkmk_update_config.yml b/playbooks/checkmk_update_config.yml new file mode 100644 index 0000000..5efbb02 --- /dev/null +++ b/playbooks/checkmk_update_config.yml @@ -0,0 +1,8 @@ +--- +- name: "Checkmk | Update agent configuration" + hosts: all + gather_facts: true + + roles: + - server_role_selection + - checkmk_agent_config diff --git a/playbooks/checkmk_update_scripts.yml b/playbooks/checkmk_update_scripts.yml new file mode 100644 index 0000000..0992f22 --- /dev/null +++ b/playbooks/checkmk_update_scripts.yml @@ -0,0 +1,8 @@ +--- +- name: "Checkmk | Update monitoring scripts" + hosts: all + gather_facts: true + + roles: + - server_role_selection + - checkmk_scripts diff --git a/playbooks/debug_disk_usage.yml b/playbooks/debug_disk_usage.yml new file mode 100644 index 0000000..a1c0e55 --- /dev/null +++ b/playbooks/debug_disk_usage.yml @@ -0,0 +1,38 @@ +--- +- name: "Debug | Disk usage" + hosts: all + gather_facts: true + + tasks: + - name: "Linux | Collect filesystem usage" + when: ansible_facts['os_family'] != 'Windows' + ansible.builtin.command: + cmd: df -hP -x tmpfs -x devtmpfs + register: disk_usage_linux + changed_when: false + + - name: "Linux | Show filesystem usage" + when: ansible_facts['os_family'] != 'Windows' + ansible.builtin.debug: + var: disk_usage_linux.stdout_lines + + - name: "Windows | Collect filesystem drive usage" + when: ansible_facts['os_family'] == 'Windows' + ansible.windows.win_powershell: + script: | + Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" | + Select-Object DeviceID, + @{Name='SizeGB';Expression={[math]::Round($_.Size / 1GB, 2)}}, + @{Name='FreeGB';Expression={[math]::Round($_.FreeSpace / 1GB, 2)}}, + @{Name='UsedPercent';Expression={ + if ($_.Size -gt 0) { + [math]::Round((($_.Size - $_.FreeSpace) / $_.Size) * 100, 1) + } else { 0 } + }} + register: disk_usage_windows + changed_when: false + + - name: "Windows | Show filesystem drive usage" + when: ansible_facts['os_family'] == 'Windows' + ansible.builtin.debug: + var: disk_usage_windows.output diff --git a/playbooks/debug_ping.yml b/playbooks/debug_ping.yml new file mode 100644 index 0000000..310489d --- /dev/null +++ b/playbooks/debug_ping.yml @@ -0,0 +1,13 @@ +--- +- name: "Debug | Ping hosts" + hosts: all + gather_facts: false + + tasks: + - name: "Windows | WinRM ping" + when: ansible_connection | default('') == 'winrm' + ansible.windows.win_ping: + + - name: "Linux | Ansible ping" + when: ansible_connection | default('ssh') != 'winrm' + ansible.builtin.ping: diff --git a/playbooks/debug_server_role_selection.yml b/playbooks/debug_server_role_selection.yml new file mode 100644 index 0000000..0a28f59 --- /dev/null +++ b/playbooks/debug_server_role_selection.yml @@ -0,0 +1,87 @@ +--- +- name: "Debug | Server Role Selection" + hosts: all + gather_facts: true + + roles: + - server_role_selection + + tasks: + - name: "Debug | Display detected server roles" + ansible.builtin.debug: + msg: + host: "{{ inventory_hostname }}" + os_family: "{{ ansible_facts['os_family'] | default('unknown') }}" + + windows_roles: + domain_controller: "{{ is_dc | default(false) | bool }}" + dhcp_server: "{{ is_dhcp_server | default(false) | bool }}" + hyperv_host: "{{ is_hyperv_host | default(false) | bool }}" + + veeam: + vbr: "{{ has_veeam_vbr | default(false) | bool }}" + vbo: "{{ has_veeam_vbo | default(false) | bool }}" + enterprise_manager: "{{ has_veeam_em | default(false) | bool }}" + + linux_roles: + unifi_controller: "{{ is_unifi_controller | default(false) | bool }}" + + optional_features: + linux_certificate_check: "{{ want_linux_check_certificate | default(false) | bool }}" + windows_citrix: "{{ want_windows_citrix | default(false) | bool }}" + windows_surebackup: "{{ want_windows_surebackup | default(false) | bool }}" + windows_backup: "{{ want_windows_backup | default(false) | bool }}" + + - name: "Debug | Display Checkmk script deployment decisions" + ansible.builtin.debug: + msg: + linux: + unifi_controller: + deploy: "{{ is_unifi_controller | default(false) | bool }}" + reason: "UniFi Controller detected" + scripts: + - "check_unifi-controller.sh" + - "unifi.cfg" + + certificate_directory: + deploy: "{{ want_linux_check_certificate | default(false) | bool }}" + reason: "Certificate directory monitoring explicitly enabled" + scripts: + - "check_certificate_directory.sh" + + windows: + check_ping: + deploy: "{{ is_dc | default(false) | bool }}" + reason: "Domain Controller" + scripts: + - "check-ping.ps1" + + veeam_config_backup: + deploy: "{{ has_veeam_vbr | default(false) | bool }}" + reason: "Veeam Backup & Replication detected" + scripts: + - "veeam_config_backup_status.ps1" + + veeam_o365: + deploy: "{{ has_veeam_vbo | default(false) | bool }}" + reason: "Veeam Backup for Microsoft 365 detected" + scripts: + - "veeam_o365_status.ps1" + + citrix_sessions: + deploy: "{{ want_windows_citrix | default(false) | bool }}" + reason: "Citrix monitoring explicitly enabled" + scripts: + - "citrix_sessions_customized.ps1" + + veeam_surebackup: + deploy: "{{ want_windows_surebackup | default(false) | bool }}" + reason: "Veeam SureBackup monitoring explicitly enabled" + scripts: + - "veeam_surebackup_status.ps1" + + windows_backup: + deploy: "{{ want_windows_backup | default(false) | bool }}" + reason: "Windows Backup monitoring explicitly enabled" + scripts: + - "windows-backup.ps1" \ No newline at end of file diff --git a/playbooks/maintenance_backup_event_log.yml b/playbooks/maintenance_backup_event_log.yml new file mode 100644 index 0000000..3f4f3ff --- /dev/null +++ b/playbooks/maintenance_backup_event_log.yml @@ -0,0 +1,74 @@ +--- +- name: "Maintenance | Backup system logs" + hosts: all + gather_facts: true + + vars: + windows_event_log_backup_dir: 'C:\\Windows\\Temp\\EventLogBackup' + windows_event_logs_to_backup: + - Application + - System + - Security + + linux_log_backup_dir: /var/backups/system-logs + linux_journal_since: "-24h" + + tasks: + - name: "Windows | Ensure event log backup directory exists" + when: ansible_facts['os_family'] == 'Windows' + ansible.windows.win_file: + path: "{{ windows_event_log_backup_dir }}" + state: directory + + - name: "Windows | Export event logs" + when: ansible_facts['os_family'] == 'Windows' + ansible.windows.win_command: >- + wevtutil epl {{ item }} "{{ windows_event_log_backup_dir }}\\{{ item }}.evtx" /ow:true + loop: "{{ windows_event_logs_to_backup }}" + changed_when: true + + - name: "Linux | Ensure system log backup directory exists" + when: ansible_facts['os_family'] != 'Windows' + become: true + ansible.builtin.file: + path: "{{ linux_log_backup_dir }}" + state: directory + owner: root + group: root + mode: "0750" + + - name: "Linux | Check whether systemd journal is available" + when: ansible_facts['os_family'] != 'Windows' + ansible.builtin.command: + cmd: journalctl --version + register: journalctl_available + changed_when: false + failed_when: false + + - name: "Linux | Export systemd journal" + when: + - ansible_facts['os_family'] != 'Windows' + - journalctl_available.rc == 0 + become: true + ansible.builtin.shell: >- + journalctl --since {{ linux_journal_since | quote }} --no-pager + > {{ (linux_log_backup_dir ~ '/journal.log') | quote }} + args: + executable: /bin/sh + changed_when: true + + - name: "Linux | Backup traditional system logs" + when: + - ansible_facts['os_family'] != 'Windows' + - journalctl_available.rc != 0 + become: true + ansible.builtin.shell: | + set -e + for file in /var/log/syslog /var/log/messages /var/log/auth.log /var/log/secure; do + if [ -f "$file" ]; then + cp -p "$file" "{{ linux_log_backup_dir }}/$(basename "$file")" + fi + done + args: + executable: /bin/sh + changed_when: true diff --git a/playbooks/maintenance_patch_os.yml b/playbooks/maintenance_patch_os.yml new file mode 100644 index 0000000..faf9ab7 --- /dev/null +++ b/playbooks/maintenance_patch_os.yml @@ -0,0 +1,35 @@ +--- +- name: "Maintenance | Patch operating system" + hosts: all + gather_facts: true + + tasks: + - name: "Debian | Update package cache and upgrade packages" + when: ansible_facts['os_family'] == 'Debian' + ansible.builtin.apt: + update_cache: true + upgrade: dist + + - name: "RedHat | Upgrade installed packages" + when: ansible_facts['os_family'] == 'RedHat' + ansible.builtin.dnf: + name: '*' + state: latest + + - name: "Windows | Install available updates" + when: ansible_facts['os_family'] == 'Windows' + ansible.windows.win_updates: + category_names: + - CriticalUpdates + - SecurityUpdates + - UpdateRollups + - Updates + reboot: false + register: windows_updates + + - name: "Windows | Report reboot requirement" + when: + - ansible_facts['os_family'] == 'Windows' + - windows_updates.reboot_required | default(false) + ansible.builtin.debug: + msg: "Windows updates were installed and a reboot is required. No reboot was performed." diff --git a/playbooks/maintenance_reboot.yml b/playbooks/maintenance_reboot.yml new file mode 100644 index 0000000..303958e --- /dev/null +++ b/playbooks/maintenance_reboot.yml @@ -0,0 +1,15 @@ +--- +- name: "Maintenance | Reboot systems" + hosts: all + gather_facts: true + + tasks: + - name: "Windows | Reboot system" + when: ansible_facts['os_family'] == 'Windows' + ansible.windows.win_reboot: + reboot_timeout: 1800 + + - name: "Linux | Reboot system" + when: ansible_facts['os_family'] != 'Windows' + ansible.builtin.reboot: + reboot_timeout: 1800 diff --git a/playbooks/maintenance_start_stopped_services.yml b/playbooks/maintenance_start_stopped_services.yml new file mode 100644 index 0000000..7b75660 --- /dev/null +++ b/playbooks/maintenance_start_stopped_services.yml @@ -0,0 +1,82 @@ +--- +- name: "Maintenance | Start stopped automatic services" + hosts: all + gather_facts: true + + tasks: + - name: "Windows | Start stopped automatic services" + when: ansible_facts['os_family'] == 'Windows' + ansible.windows.win_powershell: + script: | + $Ansible.Changed = $false + $started = @() + + Get-CimInstance Win32_Service | + Where-Object { + $_.StartMode -eq 'Auto' -and + $_.State -ne 'Running' + } | + ForEach-Object { + try { + Start-Service -Name $_.Name -ErrorAction Stop + $started += $_.Name + $Ansible.Changed = $true + } + catch { + Write-Warning "Could not start service $($_.Name): $($_.Exception.Message)" + } + } + + $Ansible.Result = @{ + started_services = $started + } + register: started_services_windows + + - name: "Windows | Show started services" + when: ansible_facts['os_family'] == 'Windows' + ansible.builtin.debug: + var: started_services_windows.result.started_services + + - name: "Linux | Collect service facts" + when: ansible_facts['os_family'] != 'Windows' + ansible.builtin.service_facts: + + - name: "Linux | Start stopped enabled systemd services" + when: + - ansible_facts['os_family'] != 'Windows' + - ansible_facts['service_mgr'] == 'systemd' + - item.value.status | default('') == 'enabled' + - item.value.state | default('') != 'running' + become: true + ansible.builtin.systemd: + name: "{{ item.key }}" + state: started + loop: "{{ ansible_facts.services | dict2items }}" + loop_control: + label: "{{ item.key }}" + register: started_services_linux + failed_when: false + + - name: "Linux | Show services that were started" + when: + - ansible_facts['os_family'] != 'Windows' + - ansible_facts['service_mgr'] == 'systemd' + ansible.builtin.debug: + msg: >- + {{ + started_services_linux.results + | default([]) + | selectattr('changed', 'defined') + | selectattr('changed') + | map(attribute='item.key') + | list + }} + + - name: "Linux | Report unsupported service manager" + when: + - ansible_facts['os_family'] != 'Windows' + - ansible_facts['service_mgr'] != 'systemd' + ansible.builtin.debug: + msg: >- + Automatic stopped-service recovery currently supports systemd hosts only. + Detected service manager: {{ ansible_facts['service_mgr'] }} diff --git a/roles/checkmk_agent/defaults/main.yml b/roles/checkmk_agent/defaults/main.yml new file mode 100644 index 0000000..5b80a83 --- /dev/null +++ b/roles/checkmk_agent/defaults/main.yml @@ -0,0 +1,9 @@ +--- +checkmk_linux_tmp_path: "/tmp" +checkmk_windows_tmp_path: "C:\\Windows\\Temp" +checkmk_deb_filename: "check-mk-agent.deb" +checkmk_rpm_filename: "check-mk-agent.rpm" +checkmk_msi_filename: "check_mk_agent.msi" +checkmk_linux_socket_name: "check-mk-agent.socket" +checkmk_linux_service_name: "check-mk-agent" +checkmk_windows_service_name: "CheckMkService" diff --git a/roles/checkmk_agent/files/README.md b/roles/checkmk_agent/files/README.md new file mode 100644 index 0000000..e8b926b --- /dev/null +++ b/roles/checkmk_agent/files/README.md @@ -0,0 +1,5 @@ +Place the Checkmk agent packages here: + +- `check-mk-agent.deb` +- `check-mk-agent.rpm` +- `check_mk_agent.msi` diff --git a/roles/checkmk_agent/files/check-mk-agent.deb b/roles/checkmk_agent/files/check-mk-agent.deb new file mode 100644 index 0000000..2109890 Binary files /dev/null and b/roles/checkmk_agent/files/check-mk-agent.deb differ diff --git a/roles/checkmk_agent/files/check-mk-agent.rpm b/roles/checkmk_agent/files/check-mk-agent.rpm new file mode 100644 index 0000000..434993a Binary files /dev/null and b/roles/checkmk_agent/files/check-mk-agent.rpm differ diff --git a/roles/checkmk_agent/files/check_mk_agent.msi b/roles/checkmk_agent/files/check_mk_agent.msi new file mode 100644 index 0000000..3ac2f9e Binary files /dev/null and b/roles/checkmk_agent/files/check_mk_agent.msi differ diff --git a/roles/checkmk_agent/handlers/main.yml b/roles/checkmk_agent/handlers/main.yml new file mode 100644 index 0000000..fd756b1 --- /dev/null +++ b/roles/checkmk_agent/handlers/main.yml @@ -0,0 +1,55 @@ +--- +- name: "Enable & start Checkmk socket (if present)" + listen: "checkmk | linux | agent-ensure-running" + when: ansible_facts['os_family'] != "Windows" + ansible.builtin.systemd: + name: "{{ checkmk_linux_socket_name }}" + enabled: true + state: started + daemon_reload: true + failed_when: false + +- name: "Enable & start Checkmk service (fallback/if present)" + listen: "checkmk | linux | agent-ensure-running" + when: ansible_facts['os_family'] != "Windows" + ansible.builtin.systemd: + name: "{{ checkmk_linux_service_name }}" + enabled: true + state: started + daemon_reload: true + failed_when: false + +- name: "Windows | Detect Checkmk service name" + listen: "checkmk | windows | agent-ensure-running" + when: ansible_facts['os_family'] == "Windows" + ansible.windows.win_powershell: + script: | + $candidates = @('CheckMkService','Check_MK_Agent') + foreach ($n in $candidates) { + $svc = Get-Service -Name $n -ErrorAction SilentlyContinue + if ($svc) { $svc.Name; break } + } + register: cmk_detect + failed_when: false + +- name: "Windows | Set detected service name" + listen: "checkmk | windows | agent-ensure-running" + when: ansible_facts['os_family'] == "Windows" + ansible.builtin.set_fact: + cmk_service_name: >- + {{ + (cmk_detect.output[0] | default('') | trim) + if (cmk_detect.output | default([]) | length > 0) + else (checkmk_windows_service_name | default('CheckMkService')) + }} + +- name: "Windows | Ensure Checkmk agent service running" + listen: "checkmk | windows | agent-ensure-running" + when: + - ansible_facts['os_family'] == "Windows" + - (cmk_service_name | default('')) | length > 0 + ansible.windows.win_service: + name: "{{ cmk_service_name }}" + start_mode: auto + state: started + failed_when: false diff --git a/roles/checkmk_agent/meta/main.yml b/roles/checkmk_agent/meta/main.yml new file mode 100644 index 0000000..a8400e1 --- /dev/null +++ b/roles/checkmk_agent/meta/main.yml @@ -0,0 +1,6 @@ +--- +galaxy_info: + role_name: checkmk_agent + description: Install Checkmk agent from local packages + min_ansible_version: "2.18" +dependencies: [] diff --git a/roles/checkmk_agent/tasks/debian.yml b/roles/checkmk_agent/tasks/debian.yml new file mode 100644 index 0000000..5cfe19e --- /dev/null +++ b/roles/checkmk_agent/tasks/debian.yml @@ -0,0 +1,11 @@ +--- +- name: "Debian | Copy Checkmk agent package" + ansible.builtin.copy: + src: "{{ checkmk_deb_filename }}" + dest: "{{ checkmk_linux_tmp_path }}/{{ checkmk_deb_filename }}" + mode: "0644" + +- name: "Debian | Install Checkmk agent" + ansible.builtin.apt: + deb: "{{ checkmk_linux_tmp_path }}/{{ checkmk_deb_filename }}" + notify: "checkmk | linux | agent-ensure-running" diff --git a/roles/checkmk_agent/tasks/main.yml b/roles/checkmk_agent/tasks/main.yml new file mode 100644 index 0000000..cd5e8fc --- /dev/null +++ b/roles/checkmk_agent/tasks/main.yml @@ -0,0 +1,12 @@ +--- +- name: Include Debian installation + ansible.builtin.include_tasks: debian.yml + when: ansible_facts['os_family'] == 'Debian' + +- name: Include RedHat installation + ansible.builtin.include_tasks: redhat.yml + when: ansible_facts['os_family'] == 'RedHat' + +- name: Include Windows installation + ansible.builtin.include_tasks: windows.yml + when: ansible_facts['os_family'] == 'Windows' diff --git a/roles/checkmk_agent/tasks/redhat.yml b/roles/checkmk_agent/tasks/redhat.yml new file mode 100644 index 0000000..bba0eb4 --- /dev/null +++ b/roles/checkmk_agent/tasks/redhat.yml @@ -0,0 +1,12 @@ +--- +- name: "RedHat | Copy Checkmk agent package" + ansible.builtin.copy: + src: "{{ checkmk_rpm_filename }}" + dest: "{{ checkmk_linux_tmp_path }}/{{ checkmk_rpm_filename }}" + mode: "0644" + +- name: "RedHat | Install Checkmk agent" + ansible.builtin.package: + name: "{{ checkmk_linux_tmp_path }}/{{ checkmk_rpm_filename }}" + state: present + notify: "checkmk | linux | agent-ensure-running" diff --git a/roles/checkmk_agent/tasks/windows.yml b/roles/checkmk_agent/tasks/windows.yml new file mode 100644 index 0000000..ee72015 --- /dev/null +++ b/roles/checkmk_agent/tasks/windows.yml @@ -0,0 +1,11 @@ +--- +- name: "Windows | Copy Checkmk agent MSI" + ansible.windows.win_copy: + src: "{{ checkmk_msi_filename }}" + dest: "{{ checkmk_windows_tmp_path }}\\{{ checkmk_msi_filename }}" + +- name: "Windows | Install Checkmk agent from local MSI" + ansible.windows.win_package: + path: "{{ checkmk_windows_tmp_path }}\\{{ checkmk_msi_filename }}" + state: present + notify: "checkmk | windows | agent-ensure-running" diff --git a/roles/checkmk_agent_config/defaults/main.yml b/roles/checkmk_agent_config/defaults/main.yml new file mode 100644 index 0000000..409e815 --- /dev/null +++ b/roles/checkmk_agent_config/defaults/main.yml @@ -0,0 +1,9 @@ +--- +checkmk_windows_user_cfg: "C:\\ProgramData\\checkmk\\agent\\check_mk.user.yml" +checkmk_windows_updates_timeout: 3600 +checkmk_windows_updates_cache: 43200 +checkmk_mk_inventory_timeout: 120 +checkmk_plugins_default_timeout: 120 +checkmk_plugins_default_cache: 600 +checkmk_extra_plugin_patterns: [] +checkmk_extra_local_patterns: [] diff --git a/roles/checkmk_agent_config/meta/main.yml b/roles/checkmk_agent_config/meta/main.yml new file mode 100644 index 0000000..cbb0724 --- /dev/null +++ b/roles/checkmk_agent_config/meta/main.yml @@ -0,0 +1,6 @@ +--- +galaxy_info: + role_name: checkmk_agent_config + description: Render Windows Checkmk agent configuration from detected server roles + min_ansible_version: "2.18" +dependencies: [] diff --git a/roles/checkmk_agent_config/tasks/main.yml b/roles/checkmk_agent_config/tasks/main.yml new file mode 100644 index 0000000..ba50c40 --- /dev/null +++ b/roles/checkmk_agent_config/tasks/main.yml @@ -0,0 +1,20 @@ +--- +- name: "Debug detected role flags" + when: ansible_facts['os_family'] == "Windows" + ansible.builtin.debug: + msg: + dc: "{{ is_dc | default(false) }}" + dhcp: "{{ is_dhcp_server | default(false) }}" + vbr: "{{ has_veeam_vbr | default(false) }}" + vbo: "{{ has_veeam_vbo | default(false) }}" + em: "{{ has_veeam_em | default(false) }}" + hv: "{{ is_hyperv_host | default(false) }}" + timeout_updates: "{{ checkmk_windows_updates_timeout }}" + +- name: "Windows | Render check_mk.user.yml" + when: ansible_facts['os_family'] == "Windows" + ansible.windows.win_template: + src: "windows_check_mk.user.yml.j2" + dest: "{{ checkmk_windows_user_cfg }}" + backup: true + notify: "checkmk | windows | agent-ensure-running" diff --git a/roles/checkmk_agent_config/templates/windows_check_mk.user.yml.j2 b/roles/checkmk_agent_config/templates/windows_check_mk.user.yml.j2 new file mode 100644 index 0000000..e90d70c --- /dev/null +++ b/roles/checkmk_agent_config/templates/windows_check_mk.user.yml.j2 @@ -0,0 +1,130 @@ +# Managed by Ansible (checkmk_agent_config) +# Windows Checkmk Agent user configuration built from detected roles. + +# $CUSTOM_PLUGINS_PATH$ -> ProgramData\checkmk\agent\plugins +# $BUILTIN_PLUGINS_PATH$ -> Program Files (x86)\checkmk\service\plugins +# $CUSTOM_AGENT_PATH$ -> ProgramData\checkmk\agent +# $CUSTOM_LOCAL_PATH$ -> ProgramData\checkmk\agent\local + +global: + _only_from: + _realtime: + enabled: yes + timeout: 90 + port: 6559 + encrypted: no + passphrase: this is my password + run: + - mem + - df + - winperf_processor + +winperf: + counters: + - MSExchangeTransport Queues: msx_queues + +_logfiles: + enabled: no + +fileinfo: + path: [] + +logwatch: + logfile: [] + +plugins: + execution: + # --- Built-in defaults --- + - pattern: '$BUILTIN_PLUGINS_PATH$\windows_updates.vbs' + run: yes + async: yes + timeout: {{ checkmk_windows_updates_timeout }} + cache_age: {{ checkmk_windows_updates_cache }} + retry_count: 0 + + - pattern: '$BUILTIN_PLUGINS_PATH$\mk_inventory.vbs' + run: yes + async: yes + timeout: {{ checkmk_mk_inventory_timeout }} + cache_age: 3600 + + - pattern: '$BUILTIN_PLUGINS_PATH$\mssql.vbs' + run: yes + timeout: {{ checkmk_plugins_default_timeout }} + +{% if has_veeam_vbr | default(false) %} + # --- Veeam Backup & Replication --- + - pattern: '$BUILTIN_PLUGINS_PATH$\veeam_backup_status.ps1' + run: yes + async: yes + timeout: {{ checkmk_plugins_default_timeout }} + cache_age: {{ checkmk_plugins_default_cache }} +{% endif %} + +{% if is_dc | default(false) %} + # --- Domain Controller --- + - pattern: '$BUILTIN_PLUGINS_PATH$\ad_replication.bat' + run: yes + timeout: {{ checkmk_plugins_default_timeout }} +{% endif %} + +{% if is_dhcp_server | default(false) %} + # --- DHCP Server --- + - pattern: '$BUILTIN_PLUGINS_PATH$\win_dhcp_pools.bat' + run: yes + timeout: {{ checkmk_plugins_default_timeout }} +{% endif %} + + # --- Generic patterns / precedence --- + - pattern: '$CUSTOM_PLUGINS_PATH$\*.ps1' + run: yes + async: yes + timeout: {{ checkmk_plugins_default_timeout }} + cache_age: {{ checkmk_plugins_default_cache }} + + - pattern: '$CUSTOM_PLUGINS_PATH$\*.*' + run: yes + timeout: {{ checkmk_plugins_default_timeout }} + + - pattern: '$BUILTIN_PLUGINS_PATH$\*.*' + run: no + timeout: {{ checkmk_plugins_default_timeout }} + + - pattern: '*' + run: no + +{% for p in (checkmk_extra_plugin_patterns | default([])) %} + - pattern: '{{ p.pattern }}' +{% if p.run is defined %} + run: {{ p.run | bool }} +{% endif %} +{% if p.async is defined %} + async: {{ p.async | bool }} +{% endif %} +{% if p.timeout is defined %} + timeout: {{ p.timeout }} +{% endif %} +{% if p.cache_age is defined %} + cache_age: {{ p.cache_age }} +{% endif %} +{% endfor %} + +local: + _execution: + - pattern: '*.*' + run: yes +{% for l in (checkmk_extra_local_patterns | default([])) %} + - pattern: '{{ l.pattern }}' +{% if l.run is defined %} + run: {{ l.run | bool }} +{% endif %} +{% if l.async is defined %} + async: {{ l.async | bool }} +{% endif %} +{% if l.timeout is defined %} + timeout: {{ l.timeout }} +{% endif %} +{% endfor %} + +mrpe: + config: [] diff --git a/roles/checkmk_scripts/defaults/main.yml b/roles/checkmk_scripts/defaults/main.yml new file mode 100644 index 0000000..8803d80 --- /dev/null +++ b/roles/checkmk_scripts/defaults/main.yml @@ -0,0 +1,12 @@ +--- +checkmk_linux_local_dir: "/usr/lib/check_mk_agent/local" +checkmk_linux_config_dir: "/etc/check_mk" +checkmk_windows_local_dir: "C:\\ProgramData\\checkmk\\agent\\local" +checkmk_linux_scripts_dir: "Linux/local" +checkmk_windows_scripts_dir: "Windows/local" + +# Optional checks, disabled until explicitly requested +want_linux_check_certificate: false +want_windows_citrix: false +want_windows_surebackup: false +want_windows_backup: false diff --git a/roles/checkmk_scripts/files/Linux/local/check_certificate_directory.sh.example b/roles/checkmk_scripts/files/Linux/local/check_certificate_directory.sh.example new file mode 100644 index 0000000..e69de29 diff --git a/roles/checkmk_scripts/files/Linux/local/check_unifi-controller.sh.example b/roles/checkmk_scripts/files/Linux/local/check_unifi-controller.sh.example new file mode 100644 index 0000000..e69de29 diff --git a/roles/checkmk_scripts/files/Linux/local/unifi.cfg.example b/roles/checkmk_scripts/files/Linux/local/unifi.cfg.example new file mode 100644 index 0000000..e69de29 diff --git a/roles/checkmk_scripts/files/README.md b/roles/checkmk_scripts/files/README.md new file mode 100644 index 0000000..a92fb3c --- /dev/null +++ b/roles/checkmk_scripts/files/README.md @@ -0,0 +1,16 @@ +Place the actual monitoring scripts in these directories. + +Linux/local/ +- check_certificate_directory.sh +- check_unifi-controller.sh +- unifi.cfg + +Windows/local/ +- check-ping.ps1 +- citrix_sessions_customized.ps1 +- veeam_config_backup_status.ps1 +- veeam_o365_status.ps1 +- veeam_surebackup_status.ps1 +- windows-backup.ps1 + +The ZIP intentionally does not invent script contents that were not provided. diff --git a/roles/checkmk_scripts/files/Windows/local/check-ping.ps1.example b/roles/checkmk_scripts/files/Windows/local/check-ping.ps1.example new file mode 100644 index 0000000..e69de29 diff --git a/roles/checkmk_scripts/files/Windows/local/citrix_sessions_customized.ps1.example b/roles/checkmk_scripts/files/Windows/local/citrix_sessions_customized.ps1.example new file mode 100644 index 0000000..e69de29 diff --git a/roles/checkmk_scripts/files/Windows/local/veeam_config_backup_status.ps1.example b/roles/checkmk_scripts/files/Windows/local/veeam_config_backup_status.ps1.example new file mode 100644 index 0000000..e69de29 diff --git a/roles/checkmk_scripts/files/Windows/local/veeam_o365_status.ps1.example b/roles/checkmk_scripts/files/Windows/local/veeam_o365_status.ps1.example new file mode 100644 index 0000000..e69de29 diff --git a/roles/checkmk_scripts/files/Windows/local/veeam_surebackup_status.ps1.example b/roles/checkmk_scripts/files/Windows/local/veeam_surebackup_status.ps1.example new file mode 100644 index 0000000..e69de29 diff --git a/roles/checkmk_scripts/files/Windows/local/windows-backup.ps1.example b/roles/checkmk_scripts/files/Windows/local/windows-backup.ps1.example new file mode 100644 index 0000000..e69de29 diff --git a/roles/checkmk_scripts/meta/main.yml b/roles/checkmk_scripts/meta/main.yml new file mode 100644 index 0000000..e4e1200 --- /dev/null +++ b/roles/checkmk_scripts/meta/main.yml @@ -0,0 +1,6 @@ +--- +galaxy_info: + role_name: checkmk_scripts + description: Deploy role-specific Checkmk local monitoring scripts + min_ansible_version: "2.18" +dependencies: [] diff --git a/roles/checkmk_scripts/tasks/linux.yml b/roles/checkmk_scripts/tasks/linux.yml new file mode 100644 index 0000000..914212c --- /dev/null +++ b/roles/checkmk_scripts/tasks/linux.yml @@ -0,0 +1,36 @@ +--- +- name: "Linux | Ensure local dir exists" + ansible.builtin.file: + path: "{{ checkmk_linux_local_dir }}" + state: directory + mode: "0755" + +- name: "Linux | Ensure config dir exists" + ansible.builtin.file: + path: "{{ checkmk_linux_config_dir }}" + state: directory + mode: "0755" + +- name: "Linux | Deploy UniFi local check" + when: is_unifi_controller | default(false) | bool + ansible.builtin.copy: + src: "{{ checkmk_linux_scripts_dir }}/check_unifi-controller.sh" + dest: "{{ checkmk_linux_local_dir }}/check_unifi-controller.sh" + mode: "0755" + notify: "checkmk | linux | agent-ensure-running" + +- name: "Linux | Deploy unifi.cfg" + when: is_unifi_controller | default(false) | bool + ansible.builtin.copy: + src: "{{ checkmk_linux_scripts_dir }}/unifi.cfg" + dest: "{{ checkmk_linux_config_dir }}/unifi.cfg" + mode: "0644" + notify: "checkmk | linux | agent-ensure-running" + +- name: "Linux | Deploy certificate directory check" + when: want_linux_check_certificate | default(false) | bool + ansible.builtin.copy: + src: "{{ checkmk_linux_scripts_dir }}/check_certificate_directory.sh" + dest: "{{ checkmk_linux_local_dir }}/check_certificate_directory.sh" + mode: "0755" + notify: "checkmk | linux | agent-ensure-running" diff --git a/roles/checkmk_scripts/tasks/main.yml b/roles/checkmk_scripts/tasks/main.yml new file mode 100644 index 0000000..503ef04 --- /dev/null +++ b/roles/checkmk_scripts/tasks/main.yml @@ -0,0 +1,8 @@ +--- +- name: Include Linux monitoring scripts + ansible.builtin.include_tasks: linux.yml + when: ansible_facts['os_family'] != 'Windows' + +- name: Include Windows monitoring scripts + ansible.builtin.include_tasks: windows.yml + when: ansible_facts['os_family'] == 'Windows' diff --git a/roles/checkmk_scripts/tasks/windows.yml b/roles/checkmk_scripts/tasks/windows.yml new file mode 100644 index 0000000..d9ebb58 --- /dev/null +++ b/roles/checkmk_scripts/tasks/windows.yml @@ -0,0 +1,47 @@ +--- +- name: "Windows | Ensure local dir exists" + ansible.windows.win_file: + path: "{{ checkmk_windows_local_dir }}" + state: directory + +- name: "Windows | Deploy check-ping.ps1 (DC only)" + when: is_dc | default(false) | bool + ansible.windows.win_copy: + src: "{{ checkmk_windows_scripts_dir }}/check-ping.ps1" + dest: "{{ checkmk_windows_local_dir }}\\check-ping.ps1" + notify: "checkmk | windows | agent-ensure-running" + +- name: "Windows | Deploy Veeam configuration backup status check (VBR only)" + when: has_veeam_vbr | default(false) | bool + ansible.windows.win_copy: + src: "{{ checkmk_windows_scripts_dir }}/veeam_config_backup_status.ps1" + dest: "{{ checkmk_windows_local_dir }}\\veeam_config_backup_status.ps1" + notify: "checkmk | windows | agent-ensure-running" + +- name: "Windows | Deploy veeam_o365_status.ps1 (VBO only)" + when: has_veeam_vbo | default(false) | bool + ansible.windows.win_copy: + src: "{{ checkmk_windows_scripts_dir }}/veeam_o365_status.ps1" + dest: "{{ checkmk_windows_local_dir }}\\veeam_o365_status.ps1" + notify: "checkmk | windows | agent-ensure-running" + +- name: "Windows | Deploy Citrix sessions check" + when: want_windows_citrix | default(false) | bool + ansible.windows.win_copy: + src: "{{ checkmk_windows_scripts_dir }}/citrix_sessions_customized.ps1" + dest: "{{ checkmk_windows_local_dir }}\\citrix_sessions_customized.ps1" + notify: "checkmk | windows | agent-ensure-running" + +- name: "Windows | Deploy Veeam SureBackup check" + when: want_windows_surebackup | default(false) | bool + ansible.windows.win_copy: + src: "{{ checkmk_windows_scripts_dir }}/veeam_surebackup_status.ps1" + dest: "{{ checkmk_windows_local_dir }}\\veeam_surebackup_status.ps1" + notify: "checkmk | windows | agent-ensure-running" + +- name: "Windows | Deploy Windows Backup check" + when: want_windows_backup | default(false) | bool + ansible.windows.win_copy: + src: "{{ checkmk_windows_scripts_dir }}/windows-backup.ps1" + dest: "{{ checkmk_windows_local_dir }}\\windows-backup.ps1" + notify: "checkmk | windows | agent-ensure-running" diff --git a/roles/server_role_selection/defaults/main.yml b/roles/server_role_selection/defaults/main.yml new file mode 100644 index 0000000..0dbd25c --- /dev/null +++ b/roles/server_role_selection/defaults/main.yml @@ -0,0 +1,12 @@ +--- +ad_ds_feature_name: "AD-Domain-Services" +dhcp_windows_service_name: "DHCPServer" +veeam_services: + vbr: "VeeamBackupSvc" + vbo: "Veeam.Archiver.Service" + em: "VeeamEnterpriseManagerSvc" +unifi_linux_services: + - unifi + - unifi.service +unifi_linux_packages: + - unifi diff --git a/roles/server_role_selection/meta/main.yml b/roles/server_role_selection/meta/main.yml new file mode 100644 index 0000000..c5d1af7 --- /dev/null +++ b/roles/server_role_selection/meta/main.yml @@ -0,0 +1,6 @@ +--- +galaxy_info: + role_name: server_role_selection + description: Detect server roles used for Checkmk deployment decisions + min_ansible_version: "2.18" +dependencies: [] diff --git a/roles/server_role_selection/tasks/main.yml b/roles/server_role_selection/tasks/main.yml new file mode 100644 index 0000000..4c67e66 --- /dev/null +++ b/roles/server_role_selection/tasks/main.yml @@ -0,0 +1,111 @@ +--- +# ========================== +# WINDOWS DETECTION +# ========================== +- name: "Windows | Detect AD DS feature (DC)" + when: ansible_facts['os_family'] == "Windows" + ansible.windows.win_feature_info: + name: "{{ ad_ds_feature_name }}" + register: _win_dc_feature + failed_when: false + +- name: "Windows | Fallback: check NTDS service (DC)" + when: ansible_facts['os_family'] == "Windows" + ansible.windows.win_service_info: + name: "NTDS" + register: _win_ntds_svc + failed_when: false + +- name: "Windows | Check DHCP service" + when: ansible_facts['os_family'] == "Windows" + ansible.windows.win_service_info: + name: "{{ dhcp_windows_service_name }}" + register: _win_dhcp_svc + failed_when: false + +- name: "Windows | Check Veeam VBR service" + when: ansible_facts['os_family'] == "Windows" + ansible.windows.win_service_info: + name: "{{ veeam_services.vbr }}" + register: _veeam_vbr + failed_when: false + +- name: "Windows | Check Veeam VBO service" + when: ansible_facts['os_family'] == "Windows" + ansible.windows.win_service_info: + name: "{{ veeam_services.vbo }}" + register: _veeam_vbo + failed_when: false + +- name: "Windows | Check Veeam Enterprise Manager service" + when: ansible_facts['os_family'] == "Windows" + ansible.windows.win_service_info: + name: "{{ veeam_services.em }}" + register: _veeam_em + failed_when: false + +- name: "Windows | Check Hyper-V service" + when: ansible_facts['os_family'] == "Windows" + ansible.windows.win_service_info: + name: "vmms" + register: _win_hyperv_svc + failed_when: false + +- name: "Windows | Set detection booleans" + when: ansible_facts['os_family'] == "Windows" + ansible.builtin.set_fact: + is_dc: >- + {{ + (((_win_dc_feature.features | default([])) | selectattr('installed') | list | length) > 0) + or (_win_ntds_svc.exists | default(false)) + }} + is_dhcp_server: "{{ _win_dhcp_svc.exists | default(false) }}" + has_veeam_vbr: "{{ _veeam_vbr.exists | default(false) }}" + has_veeam_vbo: "{{ _veeam_vbo.exists | default(false) }}" + has_veeam_em: "{{ _veeam_em.exists | default(false) }}" + is_hyperv_host: "{{ _win_hyperv_svc.exists | default(false) }}" + +- name: "Windows | Debug summary" + when: ansible_facts['os_family'] == "Windows" + ansible.builtin.debug: + msg: + is_dc: "{{ is_dc }}" + is_dhcp_server: "{{ is_dhcp_server }}" + has_veeam_vbr: "{{ has_veeam_vbr }}" + has_veeam_vbo: "{{ has_veeam_vbo }}" + has_veeam_em: "{{ has_veeam_em }}" + is_hyperv_host: "{{ is_hyperv_host }}" + +# ========================== +# LINUX DETECTION +# ========================== +- name: "Linux | Collect service facts" + when: ansible_facts['os_family'] != "Windows" + ansible.builtin.service_facts: + +- name: "Linux | Collect package facts" + when: ansible_facts['os_family'] != "Windows" + ansible.builtin.package_facts: + manager: auto + +- name: "Linux | Set UniFi flag" + when: ansible_facts['os_family'] != "Windows" + vars: + svcs: "{{ ansible_facts.services | default({}) }}" + pkgs: "{{ ansible_facts.packages | default({}) | list }}" + ansible.builtin.set_fact: + is_unifi_controller: >- + {{ + (unifi_linux_services | select('in', svcs.keys()) | list | length > 0) + or (pkgs | intersect(unifi_linux_packages) | length > 0) + }} + +- name: "Normalize detection booleans" + ansible.builtin.set_fact: + is_dc: "{{ is_dc | default(false) }}" + is_dhcp_server: "{{ is_dhcp_server | default(false) }}" + has_veeam_vbr: "{{ has_veeam_vbr | default(false) }}" + has_veeam_vbo: "{{ has_veeam_vbo | default(false) }}" + has_veeam_em: "{{ has_veeam_em | default(false) }}" + is_hyperv_host: "{{ is_hyperv_host | default(false) }}" + is_unifi_controller: "{{ is_unifi_controller | default(false) }}" diff --git a/scripts/pyproject.toml b/scripts/pyproject.toml index 1834926..ca1118c 100644 --- a/scripts/pyproject.toml +++ b/scripts/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "aim-inventory-manager" -version = "2.1.2" +version = "2.2.2" description = "AIM - Ansible Inventory Manager" requires-python = ">=3.11" dependencies = [ diff --git a/scripts/src/aim/__init__.py b/scripts/src/aim/__init__.py index 4eabd0b..ba51ced 100644 --- a/scripts/src/aim/__init__.py +++ b/scripts/src/aim/__init__.py @@ -1 +1 @@ -__version__ = "2.1.2" +__version__ = "2.2.2" diff --git a/scripts/src/aim/__pycache__/__init__.cpython-313.pyc b/scripts/src/aim/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..9b6bb8f Binary files /dev/null and b/scripts/src/aim/__pycache__/__init__.cpython-313.pyc differ diff --git a/scripts/src/aim/__pycache__/__main__.cpython-313.pyc b/scripts/src/aim/__pycache__/__main__.cpython-313.pyc new file mode 100644 index 0000000..9c33233 Binary files /dev/null and b/scripts/src/aim/__pycache__/__main__.cpython-313.pyc differ diff --git a/scripts/src/aim/__pycache__/auth.cpython-313.pyc b/scripts/src/aim/__pycache__/auth.cpython-313.pyc new file mode 100644 index 0000000..56328c5 Binary files /dev/null and b/scripts/src/aim/__pycache__/auth.cpython-313.pyc differ diff --git a/scripts/src/aim/__pycache__/config.cpython-313.pyc b/scripts/src/aim/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000..39aed3d Binary files /dev/null and b/scripts/src/aim/__pycache__/config.cpython-313.pyc differ diff --git a/scripts/src/aim/__pycache__/exceptions.cpython-313.pyc b/scripts/src/aim/__pycache__/exceptions.cpython-313.pyc new file mode 100644 index 0000000..11392c4 Binary files /dev/null and b/scripts/src/aim/__pycache__/exceptions.cpython-313.pyc differ diff --git a/scripts/src/aim/__pycache__/external.cpython-313.pyc b/scripts/src/aim/__pycache__/external.cpython-313.pyc new file mode 100644 index 0000000..444a78a Binary files /dev/null and b/scripts/src/aim/__pycache__/external.cpython-313.pyc differ diff --git a/scripts/src/aim/__pycache__/locking.cpython-313.pyc b/scripts/src/aim/__pycache__/locking.cpython-313.pyc new file mode 100644 index 0000000..c4d03bf Binary files /dev/null and b/scripts/src/aim/__pycache__/locking.cpython-313.pyc differ diff --git a/scripts/src/aim/__pycache__/permissions.cpython-313.pyc b/scripts/src/aim/__pycache__/permissions.cpython-313.pyc new file mode 100644 index 0000000..4c05f18 Binary files /dev/null and b/scripts/src/aim/__pycache__/permissions.cpython-313.pyc differ diff --git a/scripts/src/aim/__pycache__/structure.cpython-313.pyc b/scripts/src/aim/__pycache__/structure.cpython-313.pyc new file mode 100644 index 0000000..9c50bd9 Binary files /dev/null and b/scripts/src/aim/__pycache__/structure.cpython-313.pyc differ diff --git a/scripts/src/aim/__pycache__/templates.cpython-313.pyc b/scripts/src/aim/__pycache__/templates.cpython-313.pyc new file mode 100644 index 0000000..582a1fe Binary files /dev/null and b/scripts/src/aim/__pycache__/templates.cpython-313.pyc differ diff --git a/scripts/src/aim/backup/__pycache__/__init__.cpython-313.pyc b/scripts/src/aim/backup/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..9520dfe Binary files /dev/null and b/scripts/src/aim/backup/__pycache__/__init__.cpython-313.pyc differ diff --git a/scripts/src/aim/backup/__pycache__/session.cpython-313.pyc b/scripts/src/aim/backup/__pycache__/session.cpython-313.pyc new file mode 100644 index 0000000..94dab77 Binary files /dev/null and b/scripts/src/aim/backup/__pycache__/session.cpython-313.pyc differ diff --git a/scripts/src/aim/config.py b/scripts/src/aim/config.py index 1a044fd..524bda9 100644 --- a/scripts/src/aim/config.py +++ b/scripts/src/aim/config.py @@ -25,6 +25,11 @@ class Config: ui_ascii: bool = False ui_output: str = "compact" + checkmk_agent_base_url: str = "" + checkmk_agent_version: str = "" + checkmk_agent_role_files_dir: Path = Path("/etc/ansible/roles/checkmk_agent/files") + checkmk_monitoring_scripts_dir: Path = Path("/etc/checkmk_monitoring_scripts") + @property def inventory_dir(self) -> Path: return self.root_dir / "inventories" @@ -64,8 +69,19 @@ class Config: for setting in ("clear_screen", "ascii"): if setting in ui and not isinstance(ui[setting], bool): raise ValueError(f"ui.{setting} must be true or false") - return cls(root, service_user, required_group, platform_groups, - ui.get("clear_screen", True), ui.get("ascii", False), output) + maintenance = data.get("maintenance", {}) or {} + if not isinstance(maintenance, dict): + raise ValueError("maintenance must be a mapping") + return cls( + root_dir=root, service_user=service_user, required_group=required_group, + platform_groups=platform_groups, + ui_clear_screen=ui.get("clear_screen", True), + ui_ascii=ui.get("ascii", False), ui_output=output, + checkmk_agent_base_url=str(maintenance.get("checkmk_agent_base_url", "")).strip(), + checkmk_agent_version=str(maintenance.get("checkmk_agent_version", "")).strip(), + checkmk_agent_role_files_dir=Path(str(maintenance.get("checkmk_agent_role_files_dir", "/etc/ansible/roles/checkmk_agent/files"))), + checkmk_monitoring_scripts_dir=Path(str(maintenance.get("checkmk_monitoring_scripts_dir", "/etc/checkmk_monitoring_scripts"))), + ) except Exception as exc: raise ConfigurationError(f"Could not load {path}: {exc}") from exc @@ -86,6 +102,15 @@ class Config: if not isinstance(ui, dict): raise ConfigurationError("Existing ui configuration is not a mapping") ui.update(clear_screen=self.ui_clear_screen, ascii=self.ui_ascii, output=self.ui_output) + maintenance = data.setdefault("maintenance", {}) + if not isinstance(maintenance, dict): + raise ConfigurationError("Existing maintenance configuration is not a mapping") + maintenance.update( + checkmk_agent_base_url=self.checkmk_agent_base_url, + checkmk_agent_version=self.checkmk_agent_version, + checkmk_agent_role_files_dir=str(self.checkmk_agent_role_files_dir), + checkmk_monitoring_scripts_dir=str(self.checkmk_monitoring_scripts_dir), + ) fd, name = tempfile.mkstemp(prefix=".aim-config-", dir=self.root_dir) tmp = Path(name) try: diff --git a/scripts/src/aim/customers/__pycache__/__init__.cpython-313.pyc b/scripts/src/aim/customers/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..875ff51 Binary files /dev/null and b/scripts/src/aim/customers/__pycache__/__init__.cpython-313.pyc differ diff --git a/scripts/src/aim/customers/__pycache__/manager.cpython-313.pyc b/scripts/src/aim/customers/__pycache__/manager.cpython-313.pyc new file mode 100644 index 0000000..745b379 Binary files /dev/null and b/scripts/src/aim/customers/__pycache__/manager.cpython-313.pyc differ diff --git a/scripts/src/aim/inventory/__pycache__/__init__.cpython-313.pyc b/scripts/src/aim/inventory/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..169de44 Binary files /dev/null and b/scripts/src/aim/inventory/__pycache__/__init__.cpython-313.pyc differ diff --git a/scripts/src/aim/inventory/__pycache__/hosts.cpython-313.pyc b/scripts/src/aim/inventory/__pycache__/hosts.cpython-313.pyc new file mode 100644 index 0000000..531b28e Binary files /dev/null and b/scripts/src/aim/inventory/__pycache__/hosts.cpython-313.pyc differ diff --git a/scripts/src/aim/inventory/__pycache__/loader.cpython-313.pyc b/scripts/src/aim/inventory/__pycache__/loader.cpython-313.pyc new file mode 100644 index 0000000..85865b4 Binary files /dev/null and b/scripts/src/aim/inventory/__pycache__/loader.cpython-313.pyc differ diff --git a/scripts/src/aim/inventory/__pycache__/model.cpython-313.pyc b/scripts/src/aim/inventory/__pycache__/model.cpython-313.pyc new file mode 100644 index 0000000..d3cbbb9 Binary files /dev/null and b/scripts/src/aim/inventory/__pycache__/model.cpython-313.pyc differ diff --git a/scripts/src/aim/inventory/__pycache__/validator.cpython-313.pyc b/scripts/src/aim/inventory/__pycache__/validator.cpython-313.pyc new file mode 100644 index 0000000..ef705a6 Binary files /dev/null and b/scripts/src/aim/inventory/__pycache__/validator.cpython-313.pyc differ diff --git a/scripts/src/aim/inventory/__pycache__/writer.cpython-313.pyc b/scripts/src/aim/inventory/__pycache__/writer.cpython-313.pyc new file mode 100644 index 0000000..0dd8e94 Binary files /dev/null and b/scripts/src/aim/inventory/__pycache__/writer.cpython-313.pyc differ diff --git a/scripts/src/aim/maintenance/__init__.py b/scripts/src/aim/maintenance/__init__.py new file mode 100644 index 0000000..e9aebda --- /dev/null +++ b/scripts/src/aim/maintenance/__init__.py @@ -0,0 +1 @@ +"""Controller-local maintenance helpers.""" diff --git a/scripts/src/aim/maintenance/__pycache__/__init__.cpython-313.pyc b/scripts/src/aim/maintenance/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..40b3121 Binary files /dev/null and b/scripts/src/aim/maintenance/__pycache__/__init__.cpython-313.pyc differ diff --git a/scripts/src/aim/maintenance/__pycache__/checkmk.cpython-313.pyc b/scripts/src/aim/maintenance/__pycache__/checkmk.cpython-313.pyc new file mode 100644 index 0000000..8e4eeb6 Binary files /dev/null and b/scripts/src/aim/maintenance/__pycache__/checkmk.cpython-313.pyc differ diff --git a/scripts/src/aim/maintenance/__pycache__/repository.cpython-313.pyc b/scripts/src/aim/maintenance/__pycache__/repository.cpython-313.pyc new file mode 100644 index 0000000..4737c7e Binary files /dev/null and b/scripts/src/aim/maintenance/__pycache__/repository.cpython-313.pyc differ diff --git a/scripts/src/aim/maintenance/checkmk.py b/scripts/src/aim/maintenance/checkmk.py new file mode 100644 index 0000000..8a25a6e --- /dev/null +++ b/scripts/src/aim/maintenance/checkmk.py @@ -0,0 +1,116 @@ +from __future__ import annotations + +from dataclasses import dataclass +import hashlib +import os +import re +from pathlib import Path +import shutil +import tempfile +from urllib.error import HTTPError, URLError +from urllib.request import Request, urlopen + +from aim.exceptions import AIMError + + +@dataclass(frozen=True) +class PackageResult: + label: str + filename: str + source_url: str + size: int + old_sha256: str | None + new_sha256: str + + +class CheckmkAgentUpdater: + """Download and atomically stage Checkmk agent packages on the controller.""" + + def __init__(self, target_dir: Path): + self.target_dir = target_dir + + @staticmethod + def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open('rb') as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b''): + digest.update(chunk) + return digest.hexdigest() + + @staticmethod + def _specs(base_url: str, version: str) -> list[tuple[str, str, str]]: + base = base_url.rstrip('/') + return [ + ('Windows MSI', f'{base}/windows/check_mk_agent.msi', 'check_mk_agent.msi'), + ('Debian / Ubuntu DEB', f'{base}/check-mk-agent_{version}_all.deb', 'check-mk-agent.deb'), + ('RHEL / Alma / Rocky RPM', f'{base}/check-mk-agent-{version}.noarch.rpm', 'check-mk-agent.rpm'), + ] + + def update(self, base_url: str, version: str) -> list[PackageResult]: + base_url = base_url.strip().rstrip('/') + version = version.strip() + if not base_url: + raise AIMError('Checkmk agent base URL is required.') + if not version: + raise AIMError('Checkmk agent version is required.') + if not re.fullmatch(r'\d+\.\d+\.\d+p\d+-\d+', version): + raise AIMError( + 'Checkmk agent version must use the package format ..p- ' + '(example: 2.4.0p21-1).' + ) + if not base_url.startswith(('https://', 'http://')): + raise AIMError('Checkmk agent base URL must start with https:// or http://.') + if not base_url.endswith('/check_mk/agents'): + raise AIMError('Checkmk agent base URL must end with /check_mk/agents.') + + specs = self._specs(base_url, version) + downloaded: list[tuple[str, str, str, Path, int, str]] = [] + with tempfile.TemporaryDirectory(prefix='aim-checkmk-agent-') as tmp_name: + tmp = Path(tmp_name) + for label, url, filename in specs: + destination = tmp / filename + try: + request = Request(url, headers={'User-Agent': 'bitformer-AIM/CheckmkAgentUpdater'}) + with urlopen(request, timeout=60) as response, destination.open('wb') as output: + shutil.copyfileobj(response, output) + except (HTTPError, URLError, TimeoutError, OSError) as exc: + raise AIMError(f'Could not download {label} from {url}: {exc}') from exc + size = destination.stat().st_size + if size <= 0: + raise AIMError(f'Downloaded {label} is empty: {url}') + downloaded.append((label, url, filename, destination, size, self._sha256(destination))) + + try: + self.target_dir.mkdir(parents=True, exist_ok=True) + except OSError as exc: + raise AIMError(f'Could not create Checkmk role files directory {self.target_dir}: {exc}') from exc + + staged: list[tuple[str, str, str, Path, Path, int, str, str | None]] = [] + try: + # Stage every file in the destination filesystem before replacing anything. + for label, url, filename, source, size, new_hash in downloaded: + target = self.target_dir / filename + old_hash = self._sha256(target) if target.exists() else None + fd, stage_name = tempfile.mkstemp(prefix=f'.{filename}.aim-', dir=self.target_dir) + stage = Path(stage_name) + try: + with os.fdopen(fd, 'wb') as output, source.open('rb') as input_stream: + shutil.copyfileobj(input_stream, output) + output.flush() + os.fsync(output.fileno()) + stage.chmod(0o644) + except Exception: + stage.unlink(missing_ok=True) + raise + staged.append((label, url, filename, stage, target, size, new_hash, old_hash)) + + results: list[PackageResult] = [] + for label, url, filename, stage, target, size, new_hash, old_hash in staged: + os.replace(stage, target) + results.append(PackageResult(label, filename, url, size, old_hash, new_hash)) + return results + except OSError as exc: + raise AIMError(f'Could not stage Checkmk agent packages in {self.target_dir}: {exc}') from exc + finally: + for _label, _url, _filename, stage, _target, _size, _new_hash, _old_hash in staged: + stage.unlink(missing_ok=True) diff --git a/scripts/src/aim/maintenance/repository.py b/scripts/src/aim/maintenance/repository.py new file mode 100644 index 0000000..f36d581 --- /dev/null +++ b/scripts/src/aim/maintenance/repository.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path +import subprocess + +from aim.exceptions import AIMError + + +@dataclass(frozen=True) +class RepositoryState: + path: Path + branch: str + upstream: str + head: str + tracked_dirty: bool + untracked_count: int + + +@dataclass(frozen=True) +class SyncResult: + before: str + after: str + upstream: str + changed: bool + stat: str + + +class ShadowRepository: + """Manage a checkout that intentionally mirrors its configured upstream.""" + + def __init__(self, path: Path): + self.path = path + + def _git(self, *args: str, check: bool = True) -> str: + try: + proc = subprocess.run( + ['git', '-C', str(self.path), *args], + text=True, capture_output=True, check=False, + ) + except OSError as exc: + raise AIMError(f'Could not execute git: {exc}') from exc + output = (proc.stdout or '').strip() + error = (proc.stderr or '').strip() + if check and proc.returncode != 0: + detail = error or output or f'exit code {proc.returncode}' + raise AIMError(f'git {" ".join(args)} failed: {detail}') + return output + + def inspect(self) -> RepositoryState: + if not self.path.is_dir(): + raise AIMError(f'Monitoring scripts directory does not exist: {self.path}') + if self._git('rev-parse', '--is-inside-work-tree') != 'true': + raise AIMError(f'Not a Git working tree: {self.path}') + branch = self._git('branch', '--show-current') or '(detached HEAD)' + if branch == '(detached HEAD)': + raise AIMError('The monitoring scripts shadow repository is in detached HEAD state.') + upstream = self._git('rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}') + head = self._git('rev-parse', 'HEAD') + tracked = self._git('status', '--porcelain', '--untracked-files=no') + untracked = self._git('ls-files', '--others', '--exclude-standard') + return RepositoryState(self.path, branch, upstream, head, bool(tracked.strip()), + len([line for line in untracked.splitlines() if line.strip()])) + + def sync(self) -> SyncResult: + before = self._git('rev-parse', 'HEAD') + upstream = self._git('rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}') + self._git('fetch', '--prune') + remote_head = self._git('rev-parse', '@{u}') + stat = '' if before == remote_head else self._git('diff', '--stat', f'{before}..{remote_head}', check=False) + self._git('reset', '--hard', '@{u}') + after = self._git('rev-parse', 'HEAD') + if after != remote_head: + raise AIMError(f'Repository reset did not reach upstream {upstream}. Expected {remote_head}, got {after}.') + return SyncResult(before, after, upstream, before != after, stat) diff --git a/scripts/src/aim/playbooks/__pycache__/__init__.cpython-313.pyc b/scripts/src/aim/playbooks/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..20c2c17 Binary files /dev/null and b/scripts/src/aim/playbooks/__pycache__/__init__.cpython-313.pyc differ diff --git a/scripts/src/aim/playbooks/__pycache__/manager.cpython-313.pyc b/scripts/src/aim/playbooks/__pycache__/manager.cpython-313.pyc new file mode 100644 index 0000000..438f0e7 Binary files /dev/null and b/scripts/src/aim/playbooks/__pycache__/manager.cpython-313.pyc differ diff --git a/scripts/src/aim/playbooks/manager.py b/scripts/src/aim/playbooks/manager.py index d494c27..03e4788 100644 --- a/scripts/src/aim/playbooks/manager.py +++ b/scripts/src/aim/playbooks/manager.py @@ -21,22 +21,114 @@ class PlaybookSpec: PLAYBOOKS: tuple[PlaybookSpec, ...] = ( - PlaybookSpec("checkmk_cleanup", "Cleanup CheckMK", "checkmk_cleanup.yml", "CheckMK", ("linux", "windows"), ("checkmk_cleanup_enabled=true",)), - PlaybookSpec("checkmk_install_agent", "Install CheckMK Agent", "checkmk_install_agent.yml", "CheckMK", ("linux", "windows")), - PlaybookSpec("checkmk_update_config", "Update CheckMK Config", "checkmk_update_config.yml", "CheckMK", ("linux", "windows")), - PlaybookSpec("debug_ping", "Ping", "debug_ping.yml", "Debug", ("linux", "windows")), - PlaybookSpec("debug_server_role_selection", "Server Role Selection", "debug_server_role_selection.yml", "Debug", ("linux", "windows")), - PlaybookSpec("debug_disk_usage", "Disk Usage", "debug_disk_usage.yml", "Debug", ("windows",)), - PlaybookSpec("patch_os", "Patch OS", "patch_os.yml", "Maintenance", ("linux", "windows")), - PlaybookSpec("reboot_system", "Reboot System", "reboot_system.yml", "Maintenance", ("linux", "windows")), - PlaybookSpec("backup_eventlog", "Backup Event Log", "backup_eventlog.yml", "Maintenance", ("windows",)), - PlaybookSpec("start_stopped_services", "Start Stopped Services", "start_stopped_services.yml", "Maintenance", ("windows",)), - PlaybookSpec("configure_sophos_initial", "Initial Bitformer Config", "configure_sophos_initial_bitformer_config.yml", "Sophos XGS", ("sophosxgs",), ask_pass=True, require_vault=True), - PlaybookSpec("configure_sophosxgs", "Configure Sophos XGS", "configure_sophosxgs.yml", "Sophos XGS", ("sophosxgs",), customer_specific=True, ask_pass=True, require_vault=True), + # CheckMK + PlaybookSpec( + "checkmk_cleanup", + "Cleanup CheckMK", + "checkmk_cleanup.yml", + "CheckMK", + ("linux", "windows"), + ("checkmk_cleanup_enabled=true",), + ), + PlaybookSpec( + "checkmk_deploy", + "Deploy CheckMK", + "checkmk_deploy.yml", + "CheckMK", + ("linux", "windows"), + ), + PlaybookSpec( + "checkmk_update_config", + "Update CheckMK Config", + "checkmk_update_config.yml", + "CheckMK", + ("windows",), + ), + PlaybookSpec( + "checkmk_update_scripts", + "Update CheckMK Scripts", + "checkmk_update_scripts.yml", + "CheckMK", + ("linux", "windows"), + ), + + # Debug + PlaybookSpec( + "debug_ping", + "Ping", + "debug_ping.yml", + "Debug", + ("linux", "windows"), + ), + PlaybookSpec( + "debug_server_role_selection", + "Server Role Selection", + "debug_server_role_selection.yml", + "Debug", + ("linux", "windows"), + ), + PlaybookSpec( + "debug_disk_usage", + "Disk Usage", + "debug_disk_usage.yml", + "Debug", + ("linux", "windows"), + ), + + # Maintenance + PlaybookSpec( + "maintenance_patch_os", + "Patch OS", + "maintenance_patch_os.yml", + "Maintenance", + ("linux", "windows"), + ), + PlaybookSpec( + "maintenance_reboot", + "Reboot System", + "maintenance_reboot.yml", + "Maintenance", + ("linux", "windows"), + ), + PlaybookSpec( + "maintenance_backup_event_log", + "Backup System Logs", + "maintenance_backup_event_log.yml", + "Maintenance", + ("linux", "windows"), + ), + PlaybookSpec( + "maintenance_start_stopped_services", + "Start Stopped Services", + "maintenance_start_stopped_services.yml", + "Maintenance", + ("linux", "windows"), + ), + + # Sophos XGS + # PlaybookSpec( + # "configure_sophos_initial", + # "Initial Bitformer Config", + # "configure_sophos_initial_bitformer_config.yml", + # "Sophos XGS", + # ("sophosxgs",), + # ask_pass=True, + # require_vault=True, + # ), + # PlaybookSpec( + # "configure_sophosxgs", + # "Configure Sophos XGS", + # "configure_sophosxgs.yml", + # "Sophos XGS", + # ("sophosxgs",), + # customer_specific=True, + # ask_pass=True, + # require_vault=True, + # ), ) -CATEGORY_ORDER = ("CheckMK", "Debug", "Maintenance", "Sophos XGS") - +# CATEGORY_ORDER = ("CheckMK", "Debug", "Maintenance", "Sophos XGS") +CATEGORY_ORDER = ("CheckMK", "Debug", "Maintenance") class PlaybookManager: def __init__(self, customers, config): diff --git a/scripts/src/aim/sophos/__pycache__/__init__.cpython-313.pyc b/scripts/src/aim/sophos/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..bbc9bcd Binary files /dev/null and b/scripts/src/aim/sophos/__pycache__/__init__.cpython-313.pyc differ diff --git a/scripts/src/aim/sophos/__pycache__/config.cpython-313.pyc b/scripts/src/aim/sophos/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000..c7e72a6 Binary files /dev/null and b/scripts/src/aim/sophos/__pycache__/config.cpython-313.pyc differ diff --git a/scripts/src/aim/ssh/__pycache__/__init__.cpython-313.pyc b/scripts/src/aim/ssh/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..7028d0b Binary files /dev/null and b/scripts/src/aim/ssh/__pycache__/__init__.cpython-313.pyc differ diff --git a/scripts/src/aim/ssh/__pycache__/keys.cpython-313.pyc b/scripts/src/aim/ssh/__pycache__/keys.cpython-313.pyc new file mode 100644 index 0000000..1680320 Binary files /dev/null and b/scripts/src/aim/ssh/__pycache__/keys.cpython-313.pyc differ diff --git a/scripts/src/aim/ui/__pycache__/__init__.cpython-313.pyc b/scripts/src/aim/ui/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..96d41a0 Binary files /dev/null and b/scripts/src/aim/ui/__pycache__/__init__.cpython-313.pyc differ diff --git a/scripts/src/aim/ui/__pycache__/access.cpython-313.pyc b/scripts/src/aim/ui/__pycache__/access.cpython-313.pyc new file mode 100644 index 0000000..c4a99e4 Binary files /dev/null and b/scripts/src/aim/ui/__pycache__/access.cpython-313.pyc differ diff --git a/scripts/src/aim/ui/__pycache__/administration.cpython-313.pyc b/scripts/src/aim/ui/__pycache__/administration.cpython-313.pyc new file mode 100644 index 0000000..576e8f0 Binary files /dev/null and b/scripts/src/aim/ui/__pycache__/administration.cpython-313.pyc differ diff --git a/scripts/src/aim/ui/__pycache__/app.cpython-313.pyc b/scripts/src/aim/ui/__pycache__/app.cpython-313.pyc new file mode 100644 index 0000000..e0f6543 Binary files /dev/null and b/scripts/src/aim/ui/__pycache__/app.cpython-313.pyc differ diff --git a/scripts/src/aim/ui/__pycache__/components.cpython-313.pyc b/scripts/src/aim/ui/__pycache__/components.cpython-313.pyc new file mode 100644 index 0000000..b3018bc Binary files /dev/null and b/scripts/src/aim/ui/__pycache__/components.cpython-313.pyc differ diff --git a/scripts/src/aim/ui/__pycache__/customers.cpython-313.pyc b/scripts/src/aim/ui/__pycache__/customers.cpython-313.pyc new file mode 100644 index 0000000..a7ac06f Binary files /dev/null and b/scripts/src/aim/ui/__pycache__/customers.cpython-313.pyc differ diff --git a/scripts/src/aim/ui/__pycache__/execution.cpython-313.pyc b/scripts/src/aim/ui/__pycache__/execution.cpython-313.pyc new file mode 100644 index 0000000..94a4e0b Binary files /dev/null and b/scripts/src/aim/ui/__pycache__/execution.cpython-313.pyc differ diff --git a/scripts/src/aim/ui/__pycache__/hosts.cpython-313.pyc b/scripts/src/aim/ui/__pycache__/hosts.cpython-313.pyc new file mode 100644 index 0000000..59d3deb Binary files /dev/null and b/scripts/src/aim/ui/__pycache__/hosts.cpython-313.pyc differ diff --git a/scripts/src/aim/ui/__pycache__/maintenance.cpython-313.pyc b/scripts/src/aim/ui/__pycache__/maintenance.cpython-313.pyc new file mode 100644 index 0000000..26c4c1b Binary files /dev/null and b/scripts/src/aim/ui/__pycache__/maintenance.cpython-313.pyc differ diff --git a/scripts/src/aim/ui/__pycache__/playbooks.cpython-313.pyc b/scripts/src/aim/ui/__pycache__/playbooks.cpython-313.pyc new file mode 100644 index 0000000..998648b Binary files /dev/null and b/scripts/src/aim/ui/__pycache__/playbooks.cpython-313.pyc differ diff --git a/scripts/src/aim/ui/__pycache__/selection.cpython-313.pyc b/scripts/src/aim/ui/__pycache__/selection.cpython-313.pyc new file mode 100644 index 0000000..d2b3a28 Binary files /dev/null and b/scripts/src/aim/ui/__pycache__/selection.cpython-313.pyc differ diff --git a/scripts/src/aim/ui/__pycache__/targets.cpython-313.pyc b/scripts/src/aim/ui/__pycache__/targets.cpython-313.pyc new file mode 100644 index 0000000..69cb6ea Binary files /dev/null and b/scripts/src/aim/ui/__pycache__/targets.cpython-313.pyc differ diff --git a/scripts/src/aim/ui/administration.py b/scripts/src/aim/ui/administration.py index 7d559ce..dfdbbe3 100644 --- a/scripts/src/aim/ui/administration.py +++ b/scripts/src/aim/ui/administration.py @@ -235,6 +235,10 @@ class AdministrationScreens: MenuItem('6', 'Screen clearing', 'Enabled' if draft.ui_clear_screen else 'Disabled'), MenuItem('7', 'Character style', 'ASCII' if draft.ui_ascii else 'Unicode (terminal-dependent)'), MenuItem('8', 'Command output', draft.ui_output), + MenuItem('9', 'Checkmk agent base URL', draft.checkmk_agent_base_url or '(not set)'), + MenuItem('10', 'Checkmk agent version', draft.checkmk_agent_version or '(not set)'), + MenuItem('11', 'Checkmk agent role files', str(draft.checkmk_agent_role_files_dir)), + MenuItem('12', 'Checkmk monitoring scripts', str(draft.checkmk_monitoring_scripts_dir)), ]) if choice == '0': if not dirty or self.confirm('Discard unsaved settings and go back?', default=False): @@ -264,9 +268,27 @@ class AdministrationScreens: draft.ui_ascii = not draft.ui_ascii elif choice == '8': draft.ui_output = 'live' if draft.ui_output == 'compact' else 'compact' + elif choice == '9': + draft.checkmk_agent_base_url = self.optional_value('Checkmk agent base URL', draft.checkmk_agent_base_url).rstrip('/') + elif choice == '10': + draft.checkmk_agent_version = self.optional_value('Checkmk agent version', draft.checkmk_agent_version) + elif choice == '11': + value = Path(self.required_value('Checkmk agent role files directory', str(draft.checkmk_agent_role_files_dir))).expanduser() + if not value.is_absolute(): + raise ValueError('Use an absolute Checkmk agent role files path.') + draft.checkmk_agent_role_files_dir = value + elif choice == '12': + value = Path(self.required_value('Checkmk monitoring scripts directory', str(draft.checkmk_monitoring_scripts_dir))).expanduser() + if not value.is_absolute(): + raise ValueError('Use an absolute monitoring scripts path.') + draft.checkmk_monitoring_scripts_dir = value elif choice == '5': self.ui.review('Save application settings', [('File', draft.config_path), ('Root directory', draft.root_dir), - ('Service account', draft.service_user), ('Authorized group', draft.required_group)], + ('Service account', draft.service_user), ('Authorized group', draft.required_group), + ('Checkmk agent source', draft.checkmk_agent_base_url or '(not set)'), + ('Checkmk agent version', draft.checkmk_agent_version or '(not set)'), + ('Checkmk role files', draft.checkmk_agent_role_files_dir), + ('Monitoring scripts', draft.checkmk_monitoring_scripts_dir)], warning='Changing defaults does not migrate existing inventory variables, users or SSH keys.') if self.confirm('Save these settings?', default=False): draft.save() diff --git a/scripts/src/aim/ui/app.py b/scripts/src/aim/ui/app.py index 48e273e..202d368 100644 --- a/scripts/src/aim/ui/app.py +++ b/scripts/src/aim/ui/app.py @@ -34,12 +34,13 @@ from aim.ui.access import AccessScreens from aim.ui.playbooks import PlaybookScreens from aim.ui.administration import AdministrationScreens from aim.ui.targets import TargetScreens +from aim.ui.maintenance import MaintenanceScreens _STYLE = re.compile(r'\[/?(?:bold|dim|red|green|yellow|cyan|magenta)(?: [^\]]+)?\]') class App(CustomerScreens, HostScreens, AccessScreens, PlaybookScreens, - AdministrationScreens, TargetScreens): + AdministrationScreens, TargetScreens, MaintenanceScreens): def __init__(self, config: Config | None = None, *, console: Console | None = None, no_clear: bool = False, plain: bool = False, live_output: bool = False): self.config = config or Config.load() @@ -274,7 +275,8 @@ class App(CustomerScreens, HostScreens, AccessScreens, PlaybookScreens, MenuItem('2', 'Open customer', 'Hosts, access, Vault and playbooks'), MenuItem('3', 'Browse customers', 'Local inventory overview'), MenuItem('4', 'Delete customer', 'Remove the complete customer inventory', True), - MenuItem('5', 'Settings', 'Application and display preferences'), + MenuItem('5', 'Maintenance', 'Controller-local Checkmk artifacts and script synchronization'), + MenuItem('6', 'Settings', 'Application and display preferences'), ], back='Exit') if choice == '0': return 0 @@ -287,6 +289,8 @@ class App(CustomerScreens, HostScreens, AccessScreens, PlaybookScreens, elif choice == '4': self.call(self.delete_customer) elif choice == '5': + self.call(self.maintenance_menu) + elif choice == '6': self.call(self.config_menu) except EOFError: return 0 diff --git a/scripts/src/aim/ui/maintenance.py b/scripts/src/aim/ui/maintenance.py new file mode 100644 index 0000000..01ab53f --- /dev/null +++ b/scripts/src/aim/ui/maintenance.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +from pathlib import Path + +from aim.exceptions import AIMError +from aim.maintenance.checkmk import CheckmkAgentUpdater +from aim.maintenance.repository import ShadowRepository +from aim.ui.components import MenuItem + + +class MaintenanceScreens: + def maintenance_menu(self) -> None: + while True: + self.clear() + self.header('AIM > Maintenance', 'Controller-local maintenance. No customer inventory is modified.') + choice = self.menu([ + MenuItem('1', 'Update Checkmk agent packages', 'Download MSI / DEB / RPM into the checkmk_agent role'), + MenuItem('2', 'Sync Checkmk monitoring scripts', 'Reset the managed shadow checkout to its configured upstream'), + ]) + if choice == '0': + return + if choice == '1': + self.call(self.update_checkmk_agents) + elif choice == '2': + self.call(self.sync_checkmk_scripts) + + def update_checkmk_agents(self) -> None: + self.clear() + self.header('AIM > Maintenance > Checkmk Agent Packages', + 'Downloads all package formats first; role files are replaced only after every download succeeds.') + self.ui.details([ + ('Expected base URL', 'https:////check_mk/agents'), + ('URL ends at', '/check_mk/agents'), + ('Example', 'https://monitoring.example.de/monitoring/check_mk/agents'), + ('Version format', '..p-'), + ('Version example', '2.4.0p21-1'), + ], title='Checkmk agent source') + base_url = self.required_value('Checkmk agent base URL', self.config.checkmk_agent_base_url or None).rstrip('/') + version = self.required_value('Checkmk agent version', self.config.checkmk_agent_version or None) + target = Path(self.config.checkmk_agent_role_files_dir) + self.ui.review('Update Checkmk agent packages', [ + ('Base URL', base_url), + ('Version', version), + ('Target', target), + ('Windows source', f'{base_url}/windows/check_mk_agent.msi'), + ('Debian source', f'{base_url}/check-mk-agent_{version}_all.deb'), + ('RPM source', f'{base_url}/check-mk-agent-{version}.noarch.rpm'), + ], warning='Existing role packages are replaced only after all three downloads have completed successfully.') + if not self.confirm('Update these local Checkmk agent packages?', default=False): + return + + updater = CheckmkAgentUpdater(target) + self.ui.result('Downloading Checkmk agent packages...', level='info') + results = updater.update(base_url, version) + rows = [] + for result in results: + old = result.old_sha256[:12] if result.old_sha256 else '(new file)' + new = result.new_sha256[:12] + rows.append((result.label, f'{result.filename} | {result.size} bytes | {old} -> {new}')) + self.ui.details(rows, title='Updated packages') + self.ui.result('All Checkmk agent packages were updated successfully.', title='Maintenance completed') + if base_url != self.config.checkmk_agent_base_url or version != self.config.checkmk_agent_version: + self.ui.result('Tip: save the base URL/version in Settings to use them as future defaults.', level='info') + self.pause() + + def sync_checkmk_scripts(self) -> None: + self.clear() + self.header('AIM > Maintenance > Checkmk Monitoring Scripts', + 'Managed shadow checkout: tracked local changes are intentionally discarded; untracked files are retained.') + repo = ShadowRepository(Path(self.config.checkmk_monitoring_scripts_dir)) + state = repo.inspect() + self.ui.review('Sync Checkmk monitoring scripts', [ + ('Repository', state.path), + ('Branch', state.branch), + ('Upstream', state.upstream), + ('Current commit', state.head[:12]), + ('Tracked local changes', 'YES - will be discarded' if state.tracked_dirty else 'No'), + ('Untracked files', f'{state.untracked_count} (retained)'), + ('Operation', 'git fetch --prune; git reset --hard @{u}'), + ], warning=('Tracked local modifications will be discarded because this directory is a managed shadow copy.' + if state.tracked_dirty else 'The checkout will be reset exactly to its configured upstream.')) + if not self.confirm('Sync the monitoring scripts shadow copy?', default=False): + return + self.ui.result('Fetching and synchronizing monitoring scripts...', level='info') + result = repo.sync() + self.ui.details([ + ('Upstream', result.upstream), + ('Before', result.before[:12]), + ('After', result.after[:12]), + ('Result', 'Updated' if result.changed else 'Already current'), + ('Changed files', result.stat or 'No upstream file changes'), + ], title='Repository sync') + self.ui.result('Checkmk monitoring scripts are synchronized with the configured upstream.', title='Maintenance completed') + self.pause() diff --git a/scripts/src/aim/variables/__pycache__/__init__.cpython-313.pyc b/scripts/src/aim/variables/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..9780c24 Binary files /dev/null and b/scripts/src/aim/variables/__pycache__/__init__.cpython-313.pyc differ diff --git a/scripts/src/aim/variables/__pycache__/viewer.cpython-313.pyc b/scripts/src/aim/variables/__pycache__/viewer.cpython-313.pyc new file mode 100644 index 0000000..107b6f6 Binary files /dev/null and b/scripts/src/aim/variables/__pycache__/viewer.cpython-313.pyc differ diff --git a/scripts/src/aim/vault/__pycache__/__init__.cpython-313.pyc b/scripts/src/aim/vault/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..4bbfc1b Binary files /dev/null and b/scripts/src/aim/vault/__pycache__/__init__.cpython-313.pyc differ diff --git a/scripts/src/aim/vault/__pycache__/manager.cpython-313.pyc b/scripts/src/aim/vault/__pycache__/manager.cpython-313.pyc new file mode 100644 index 0000000..6ffbd0a Binary files /dev/null and b/scripts/src/aim/vault/__pycache__/manager.cpython-313.pyc differ diff --git a/scripts/src/aim/winrm/__pycache__/__init__.cpython-313.pyc b/scripts/src/aim/winrm/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..e645d32 Binary files /dev/null and b/scripts/src/aim/winrm/__pycache__/__init__.cpython-313.pyc differ diff --git a/scripts/src/aim/winrm/__pycache__/manager.cpython-313.pyc b/scripts/src/aim/winrm/__pycache__/manager.cpython-313.pyc new file mode 100644 index 0000000..847e8cf Binary files /dev/null and b/scripts/src/aim/winrm/__pycache__/manager.cpython-313.pyc differ