aim-web2.1.0rc9

This commit is contained in:
admin_rb
2026-09-22 19:23:17 +02:00
parent d095887d2e
commit 3dfc80b782
438 changed files with 31613 additions and 1510 deletions
File diff suppressed because it is too large Load Diff
-64
View File
@@ -1,64 +0,0 @@
---
- 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
+75
View File
@@ -0,0 +1,75 @@
---
# PURPOSE: Preview / clean up Checkmk scripts
# DESCRIPTION: List obsolete managed script paths; remove them only with explicit deletion approval.
# TARGETS: linux, windows
# INPUTS (omitted values inherit inventory / role defaults):
# aim_debug [bool]: false
# checkmk_cleanup_enabled [bool]: false
# checkmk_unifi_mode [choice]: auto
# want_linux_check_certificate [bool]: false
# want_windows_citrix [bool]: false
# want_windows_surebackup [bool]: false
# want_windows_backup [bool]: false
# want_windows_nsp_mailqueue [bool]: false
# want_windows_certificate [bool]: false
# want_windows_veeam_cloud_connect [bool]: false
# want_windows_veeam_backup [bool]: false
# AUTH: existing inventory / Vault credentials; no embedded passwords.
# CHANGES: Cleanup only touches the documented managed filenames. Preview is the default; enabling deletion requires another confirmation.
# EXAMPLE: ansible-playbook -i inventories/<customer>/hosts.yml
# playbooks/checkmk_cleanup_scripts.yml --limit <host> --vault-id <customer>@prompt
- name: Checkmk | Preview or clean up linux
hosts: linux
gather_facts: true
pre_tasks:
- name: AIM | Validate diagnostics option
ansible.builtin.assert:
that:
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
'false']
fail_msg: aim_debug must be a YAML/JSON boolean.
quiet: true
- name: AIM | Reject mixed platform membership
ansible.builtin.assert:
that:
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
quiet: true
- name: AIM | Execution context
ansible.builtin.debug:
msg:
host: '{{ inventory_hostname }}'
diagnostics: Enabled; secret values are never included by this task.
when: aim_debug | default(false) | bool
become: true
roles:
- role: system_detect_roles
- role: checkmk_script_plan
- role: checkmk_cleanup_scripts
- name: Checkmk | Preview or clean up windows
hosts: windows
gather_facts: true
pre_tasks:
- name: AIM | Validate diagnostics option
ansible.builtin.assert:
that:
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
'false']
fail_msg: aim_debug must be a YAML/JSON boolean.
quiet: true
- name: AIM | Reject mixed platform membership
ansible.builtin.assert:
that:
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
quiet: true
- name: AIM | Execution context
ansible.builtin.debug:
msg:
host: '{{ inventory_hostname }}'
diagnostics: Enabled; secret values are never included by this task.
when: aim_debug | default(false) | bool
roles:
- role: system_detect_roles
- role: checkmk_script_plan
- role: checkmk_cleanup_scripts
-10
View File
@@ -1,10 +0,0 @@
---
- name: "Checkmk | Deploy agent, scripts and configuration"
hosts: all
gather_facts: true
roles:
- checkmk_agent
- server_role_selection
- checkmk_scripts
- checkmk_agent_config
+136
View File
@@ -0,0 +1,136 @@
# PURPOSE: Install Checkmk agent
# DESCRIPTION: Install staged agent packages, deploy selected checks, render Windows settings and ensure the agent is running.
# TARGETS: linux, windows
# INPUTS (omitted values inherit inventory / role defaults):
# aim_debug [bool]: false
# checkmk_unifi_mode [choice]: auto
# checkmk_unifi_username [text]: bf-monitoring
# checkmk_unifi_password [secret_ref]: vault_checkmk_unifi_password
# checkmk_unifi_baseurl [url]: network: https://127.0.0.1:8443; os: https://127.0.0.1:11443
# checkmk_unifi_curl_options [text]: --insecure --tlsv1.2
# want_linux_check_certificate [bool]: false
# want_windows_citrix [bool]: false
# want_windows_surebackup [bool]: false
# want_windows_backup [bool]: false
# want_windows_nsp_mailqueue [bool]: false
# want_windows_certificate [bool]: false
# want_windows_veeam_cloud_connect [bool]: false
# want_windows_veeam_backup [bool]: false
# checkmk_unifi_status_provisioning [int]: 1
# checkmk_unifi_status_upgrading [int]: 1
# checkmk_unifi_status_upgradable [int]: 0
# checkmk_unifi_status_heartbeat_missed [int]: 1
# checkmk_unifi_status_noautobackup [int]: 0
# checkmk_windows_updates_timeout [int]: 3600
# checkmk_windows_updates_cache [int]: 43200
# checkmk_mk_inventory_timeout [int]: 120
# checkmk_plugins_default_timeout [int]: 120
# checkmk_plugins_default_cache [int]: 600
# checkmk_extra_plugin_patterns [sequence]: []
# AUTH: existing inventory / Vault credentials; no embedded passwords.
# CHANGES: Installs packages and AIM-managed script files. On Windows only the marked plugins section in check_mk.user.yml is replaced; all other user-config sections are preserved. UniFi deployment also removes the alternative UniFi local check; no other cleanup is performed.
# EXAMPLE: ansible-playbook -i inventories/<customer>/hosts.yml
# playbooks/checkmk_install_agent.yml --limit <host> --vault-id <customer>@prompt
- name: Checkmk | Install linux
hosts: linux
gather_facts: true
pre_tasks:
- name: AIM | Validate diagnostics option
ansible.builtin.assert:
that:
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in
['true', 'false']
fail_msg: aim_debug must be a YAML/JSON boolean.
quiet: true
- name: AIM | Reject mixed platform membership
ansible.builtin.assert:
that:
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
quiet: true
- name: AIM | Execution context
ansible.builtin.debug:
msg:
host: '{{ inventory_hostname }}'
diagnostics: Enabled; secret values are never included by this task.
when: aim_debug | default(false) | bool
- name: Load system detect roles
ansible.builtin.include_role:
name: system_detect_roles
- name: Load checkmk script plan
ansible.builtin.include_role:
name: checkmk_script_plan
- name: Checkmk | Preflight scripts and credentials
ansible.builtin.include_role:
name: checkmk_deploy_scripts
tasks_from: preflight
become: true
roles:
- role: checkmk_agent
- role: checkmk_deploy_scripts
- role: checkmk_configure_agent
- role: checkmk_manage_service
post_tasks:
- name: Checkmk | Observe installed agent
ansible.builtin.include_role:
name: checkmk_report
- name: AIM | Publish operation result
ansible.builtin.set_stats:
per_host: true
aggregate: false
data:
aim_output:
protocol: aim_output_v1
schema: checkmk_agent_state_v1
data: '{{ _aim_checkmk_state }}'
- name: Checkmk | Install windows
hosts: windows
gather_facts: true
pre_tasks:
- name: AIM | Validate diagnostics option
ansible.builtin.assert:
that:
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in
['true', 'false']
fail_msg: aim_debug must be a YAML/JSON boolean.
quiet: true
- name: AIM | Reject mixed platform membership
ansible.builtin.assert:
that:
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
quiet: true
- name: AIM | Execution context
ansible.builtin.debug:
msg:
host: '{{ inventory_hostname }}'
diagnostics: Enabled; secret values are never included by this task.
when: aim_debug | default(false) | bool
- name: Load system detect roles
ansible.builtin.include_role:
name: system_detect_roles
- name: Load checkmk script plan
ansible.builtin.include_role:
name: checkmk_script_plan
- name: Checkmk | Preflight scripts and credentials
ansible.builtin.include_role:
name: checkmk_deploy_scripts
tasks_from: preflight
roles:
- role: checkmk_agent
- role: checkmk_deploy_scripts
- role: checkmk_configure_agent
- role: checkmk_manage_service
post_tasks:
- name: Checkmk | Observe installed agent
ansible.builtin.include_role:
name: checkmk_report
- name: AIM | Publish operation result
ansible.builtin.set_stats:
per_host: true
aggregate: false
data:
aim_output:
protocol: aim_output_v1
schema: checkmk_agent_state_v1
data: '{{ _aim_checkmk_state }}'
+115
View File
@@ -0,0 +1,115 @@
# PURPOSE: Read current Windows Checkmk user configuration
# DESCRIPTION: Display the current check_mk.user.yml from Windows without modifying the host.
# TARGETS: windows
# INPUTS (omitted values inherit inventory / playbook defaults):
# aim_debug [bool]: false
# checkmk_windows_user_cfg [text]: C:\ProgramData\checkmk\agent\check_mk.user.yml
# AUTH: existing inventory / Vault credentials; no embedded passwords.
# CHANGES: none; this playbook is read-only.
# EXAMPLE: ansible-playbook -i inventories/<customer>/hosts.yml
# playbooks/checkmk_read_windows_config.yml --limit <host> --vault-id <customer>@prompt
- name: Checkmk | Read Windows user configuration
hosts: windows
gather_facts: false
tasks:
- name: AIM | Validate diagnostics option
ansible.builtin.assert:
that:
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in
['true', 'false']
fail_msg: aim_debug must be a YAML/JSON boolean.
quiet: true
- name: AIM | Reject mixed platform membership
ansible.builtin.assert:
that:
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
quiet: true
- name: AIM | Validate Checkmk configuration path
ansible.builtin.assert:
that:
- (checkmk_windows_user_cfg | default('C:\\ProgramData\\checkmk\\agent\\check_mk.user.yml')) is
string
- (checkmk_windows_user_cfg | default('C:\\ProgramData\\checkmk\\agent\\check_mk.user.yml') | trim
| length) > 0
fail_msg: checkmk_windows_user_cfg must be a non-empty Windows path.
quiet: true
- name: AIM | Execution context
ansible.builtin.debug:
msg:
host: '{{ inventory_hostname }}'
configuration: "{{ checkmk_windows_user_cfg | default('C:\\\\ProgramData\\\\checkmk\\\\agent\\\\check_mk.user.yml')
}}"
mode: Read-only; no Checkmk configuration is modified.
when: aim_debug | default(false) | bool
- name: Windows | Inspect current Checkmk user configuration
ansible.windows.win_stat:
path: "{{ checkmk_windows_user_cfg | default('C:\\ProgramData\\checkmk\\agent\\check_mk.user.yml') }}"
get_checksum: false
register: _checkmk_windows_user_config_stat
changed_when: false
- name: Windows | Require the approved Checkmk user filename
ansible.builtin.assert:
that:
- not (_checkmk_windows_user_config_stat.stat.exists | default(false)) or (_checkmk_windows_user_config_stat.stat.filename | lower) == 'check_mk.user.yml'
- not (_checkmk_windows_user_config_stat.stat.exists | default(false)) or (_checkmk_windows_user_config_stat.stat.isreg | default(false))
- (_checkmk_windows_user_config_stat.stat.size | default(0) | int) <= 524288
fail_msg: Only a regular check_mk.user.yml file up to 512 KiB may be read.
quiet: true
- name: Windows | Read current Checkmk user configuration
ansible.windows.slurp:
src: "{{ checkmk_windows_user_cfg | default('C:\\ProgramData\\checkmk\\agent\\check_mk.user.yml') }}"
register: _checkmk_windows_user_config_slurp
when: _checkmk_windows_user_config_stat.stat.exists | default(false)
no_log: true
- name: Windows | Build Checkmk user configuration result
ansible.builtin.set_fact:
_checkmk_windows_user_config:
exists: '{{ _checkmk_windows_user_config_stat.stat.exists | default(false) | bool }}'
path: "{{ _checkmk_windows_user_config_stat.stat.path | default(checkmk_windows_user_cfg | default('C:\\ProgramData\\checkmk\\agent\\check_mk.user.yml')) }}"
size_bytes: '{{ _checkmk_windows_user_config_stat.stat.size | default(0) | int }}'
last_write_time_utc: >-
{{ (_checkmk_windows_user_config_stat.stat.lastwritetime | aim_epoch_iso_utc)
if (_checkmk_windows_user_config_stat.stat.exists | default(false)) else none }}
content: >-
{{ (_checkmk_windows_user_config_slurp.content | b64decode)
if (_checkmk_windows_user_config_stat.stat.exists | default(false)) else '' }}
changed_when: false
no_log: true
- name: Windows | Report missing Checkmk user configuration
ansible.builtin.debug:
msg: |-
{{ inventory_hostname }}
Checkmk user configuration was not found.
Path: {{ _checkmk_windows_user_config.path }}
when: not (_checkmk_windows_user_config.exists | bool)
- name: Windows | Print current Checkmk user configuration
ansible.builtin.debug:
msg: |-
{{ inventory_hostname }}
Path: {{ _checkmk_windows_user_config.path }}
Size: {{ _checkmk_windows_user_config.size_bytes }} bytes
Last write (UTC): {{ _checkmk_windows_user_config.last_write_time_utc }}
----- BEGIN check_mk.user.yml -----
{{ _checkmk_windows_user_config.content }}
----- END check_mk.user.yml -----
when: _checkmk_windows_user_config.exists | bool
- name: AIM | Publish operation result
ansible.builtin.set_stats:
per_host: true
aggregate: false
data:
aim_output:
protocol: aim_output_v1
schema: checkmk_user_config_v1
data: '{{ _checkmk_windows_user_config | aim_report_checkmk_config }}'
-8
View File
@@ -1,8 +0,0 @@
---
- name: "Checkmk | Update agent configuration"
hosts: all
gather_facts: true
roles:
- server_role_selection
- checkmk_agent_config
-8
View File
@@ -1,8 +0,0 @@
---
- name: "Checkmk | Update monitoring scripts"
hosts: all
gather_facts: true
roles:
- server_role_selection
- checkmk_scripts
+135
View File
@@ -0,0 +1,135 @@
# PURPOSE: Update Checkmk scripts and configuration
# DESCRIPTION: Deploy selected checks and Windows configuration without installing agent packages.
# TARGETS: linux, windows
# INPUTS (omitted values inherit inventory / role defaults):
# aim_debug [bool]: false
# checkmk_unifi_mode [choice]: auto
# checkmk_unifi_username [text]: bf-monitoring
# checkmk_unifi_password [secret_ref]: vault_checkmk_unifi_password
# checkmk_unifi_baseurl [url]: network: https://127.0.0.1:8443; os: https://127.0.0.1:11443
# checkmk_unifi_curl_options [text]: --insecure --tlsv1.2
# want_linux_check_certificate [bool]: false
# want_windows_citrix [bool]: false
# want_windows_surebackup [bool]: false
# want_windows_backup [bool]: false
# want_windows_nsp_mailqueue [bool]: false
# want_windows_certificate [bool]: false
# want_windows_veeam_cloud_connect [bool]: false
# want_windows_veeam_backup [bool]: false
# checkmk_unifi_status_provisioning [int]: 1
# checkmk_unifi_status_upgrading [int]: 1
# checkmk_unifi_status_upgradable [int]: 0
# checkmk_unifi_status_heartbeat_missed [int]: 1
# checkmk_unifi_status_noautobackup [int]: 0
# checkmk_windows_updates_timeout [int]: 3600
# checkmk_windows_updates_cache [int]: 43200
# checkmk_mk_inventory_timeout [int]: 120
# checkmk_plugins_default_timeout [int]: 120
# checkmk_plugins_default_cache [int]: 600
# checkmk_extra_plugin_patterns [sequence]: []
# AUTH: existing inventory / Vault credentials; no embedded passwords.
# CHANGES: AIM-managed script files are updated. On Windows only the marked plugins section in check_mk.user.yml is replaced; all other user-config sections are preserved. Only the opposite UniFi check is removed during a UniFi mode transition.
# EXAMPLE: ansible-playbook -i inventories/<customer>/hosts.yml
# playbooks/checkmk_update_scripts_config.yml --limit <host> --vault-id <customer>@prompt
- name: Checkmk | Update linux
hosts: linux
gather_facts: true
pre_tasks:
- name: AIM | Validate diagnostics option
ansible.builtin.assert:
that:
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in
['true', 'false']
fail_msg: aim_debug must be a YAML/JSON boolean.
quiet: true
- name: AIM | Reject mixed platform membership
ansible.builtin.assert:
that:
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
quiet: true
- name: AIM | Execution context
ansible.builtin.debug:
msg:
host: '{{ inventory_hostname }}'
diagnostics: Enabled; secret values are never included by this task.
when: aim_debug | default(false) | bool
- name: Load system detect roles
ansible.builtin.include_role:
name: system_detect_roles
- name: Load checkmk script plan
ansible.builtin.include_role:
name: checkmk_script_plan
- name: Checkmk | Preflight scripts and credentials
ansible.builtin.include_role:
name: checkmk_deploy_scripts
tasks_from: preflight
become: true
roles:
- role: checkmk_deploy_scripts
- role: checkmk_configure_agent
- role: checkmk_manage_service
post_tasks:
- &id001
name: Checkmk | Collect managed change summary
ansible.builtin.set_fact:
_aim_checkmk_changes: '{{ _aim_check_copies | default({}) | aim_report_checkmk_changes(_checkmk_selected_scripts,
_checkmk_plugins_update | default({}), _aim_unifi_write | default({}), _aim_opposite_remove | default({}),
_checkmk_unifi_effective, ansible_check_mode) }}'
- name: AIM | Publish operation result
ansible.builtin.set_stats:
per_host: true
aggregate: false
data:
aim_output:
protocol: aim_output_v1
schema: checkmk_agent_config_v1
data: '{{ _aim_checkmk_changes }}'
- name: Checkmk | Update windows
hosts: windows
gather_facts: true
pre_tasks:
- name: AIM | Validate diagnostics option
ansible.builtin.assert:
that:
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in
['true', 'false']
fail_msg: aim_debug must be a YAML/JSON boolean.
quiet: true
- name: AIM | Reject mixed platform membership
ansible.builtin.assert:
that:
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
quiet: true
- name: AIM | Execution context
ansible.builtin.debug:
msg:
host: '{{ inventory_hostname }}'
diagnostics: Enabled; secret values are never included by this task.
when: aim_debug | default(false) | bool
- name: Load system detect roles
ansible.builtin.include_role:
name: system_detect_roles
- name: Load checkmk script plan
ansible.builtin.include_role:
name: checkmk_script_plan
- name: Checkmk | Preflight scripts and credentials
ansible.builtin.include_role:
name: checkmk_deploy_scripts
tasks_from: preflight
roles:
- role: checkmk_deploy_scripts
- role: checkmk_configure_agent
- role: checkmk_manage_service
post_tasks:
- *id001
- name: AIM | Publish operation result
ansible.builtin.set_stats:
per_host: true
aggregate: false
data:
aim_output:
protocol: aim_output_v1
schema: checkmk_agent_config_v1
data: '{{ _aim_checkmk_changes }}'
@@ -0,0 +1,96 @@
---
# PURPOSE: Apply customer Sophos configuration
# DESCRIPTION: Apply this customer profile using hostname, network_objects and vlan_interfaces from inventory.
# TARGETS: sophosxgs
# REQUIRED NETWORK KEYS: derz_lan, derz_sslvpn, facility, guest, lan_old, management, office, server, voip
# INPUTS (omitted values inherit inventory / role defaults):
# aim_debug [bool]: false
# AUTH: existing inventory / Vault credentials; no embedded passwords.
# CHANGES: Changes customer firewall configuration. VLAN parent remains Port1 as in the supplied playbooks. Only this customer profile is selected.
# EXAMPLE: ansible-playbook -i inventories/<customer>/hosts.yml
# playbooks/customers/bluuunit/sophos_apply_customer.yml --limit <host> --vault-id <customer>@prompt --ask-pass
- name: Sophos | Apply customer configuration | bluuunit
hosts: sophosxgs
gather_facts: false
any_errors_fatal: false
tasks:
- name: Apply customer firewall policy
ansible.builtin.import_role:
name: sophos_customer_bluuunit
pre_tasks:
- name: AIM | Validate diagnostics option
ansible.builtin.assert:
that:
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
'false']
fail_msg: aim_debug must be a YAML/JSON boolean.
quiet: true
- name: AIM | Reject mixed platform membership
ansible.builtin.assert:
that:
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
quiet: true
- name: AIM | Execution context
ansible.builtin.debug:
msg:
host: '{{ inventory_hostname }}'
diagnostics: Enabled; secret values are never included by this task.
when: aim_debug | default(false) | bool
- name: Sophos | Require customer host configuration
ansible.builtin.assert:
that:
- hostname is defined
- hostname is string
- hostname | length > 0
- network_objects is defined
- network_objects is mapping
- vlan_interfaces is defined
- vlan_interfaces is mapping
fail_msg: Set hostname, network_objects and vlan_interfaces in this host inventory. AIM can prepare
these fields.
quiet: true
- name: Sophos | Require profile network keys
ansible.builtin.assert:
that:
- item in network_objects
fail_msg: A customer-required network_objects key is missing. See the loop item.
quiet: true
loop:
- derz_lan
- derz_sslvpn
- facility
- guest
- lan_old
- management
- office
- server
- voip
- name: Sophos | Validate network object shape
ansible.builtin.assert:
that:
- item.value is mapping
- item.value.name is defined
- item.value.network is defined
- item.value.subnetmask is defined
fail_msg: Every network object requires name, network and subnetmask.
quiet: true
loop: '{{ network_objects | dict2items }}'
loop_control:
label: '{{ item.key }}'
- name: Sophos | Validate VLAN shape
ansible.builtin.assert:
that:
- item.value is mapping
- item.value.name is defined
- item.value.ip_address is defined
- item.value.subnetmask is defined
- item.value.vlan_id is defined
- item.value.zone_name is defined
- item.value.zone_type is defined
- item.value.zone_description is defined
fail_msg: Each VLAN requires its full interface and zone mapping.
quiet: true
loop: '{{ vlan_interfaces | dict2items }}'
loop_control:
label: '{{ item.key }}'
@@ -0,0 +1,91 @@
---
# PURPOSE: Apply customer Sophos configuration
# DESCRIPTION: Apply this customer profile using hostname, network_objects and vlan_interfaces from inventory.
# TARGETS: sophosxgs
# REQUIRED NETWORK KEYS: azuregwc_lan, lan_old, management, office
# INPUTS (omitted values inherit inventory / role defaults):
# aim_debug [bool]: false
# AUTH: existing inventory / Vault credentials; no embedded passwords.
# CHANGES: Changes customer firewall configuration. VLAN parent remains Port1 as in the supplied playbooks. Only this customer profile is selected.
# EXAMPLE: ansible-playbook -i inventories/<customer>/hosts.yml
# playbooks/customers/formicon/sophos_apply_customer.yml --limit <host> --vault-id <customer>@prompt --ask-pass
- name: Sophos | Apply customer configuration | formicon
hosts: sophosxgs
gather_facts: false
any_errors_fatal: false
tasks:
- name: Apply customer firewall policy
ansible.builtin.import_role:
name: sophos_customer_formicon
pre_tasks:
- name: AIM | Validate diagnostics option
ansible.builtin.assert:
that:
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
'false']
fail_msg: aim_debug must be a YAML/JSON boolean.
quiet: true
- name: AIM | Reject mixed platform membership
ansible.builtin.assert:
that:
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
quiet: true
- name: AIM | Execution context
ansible.builtin.debug:
msg:
host: '{{ inventory_hostname }}'
diagnostics: Enabled; secret values are never included by this task.
when: aim_debug | default(false) | bool
- name: Sophos | Require customer host configuration
ansible.builtin.assert:
that:
- hostname is defined
- hostname is string
- hostname | length > 0
- network_objects is defined
- network_objects is mapping
- vlan_interfaces is defined
- vlan_interfaces is mapping
fail_msg: Set hostname, network_objects and vlan_interfaces in this host inventory. AIM can prepare
these fields.
quiet: true
- name: Sophos | Require profile network keys
ansible.builtin.assert:
that:
- item in network_objects
fail_msg: A customer-required network_objects key is missing. See the loop item.
quiet: true
loop:
- azuregwc_lan
- lan_old
- management
- office
- name: Sophos | Validate network object shape
ansible.builtin.assert:
that:
- item.value is mapping
- item.value.name is defined
- item.value.network is defined
- item.value.subnetmask is defined
fail_msg: Every network object requires name, network and subnetmask.
quiet: true
loop: '{{ network_objects | dict2items }}'
loop_control:
label: '{{ item.key }}'
- name: Sophos | Validate VLAN shape
ansible.builtin.assert:
that:
- item.value is mapping
- item.value.name is defined
- item.value.ip_address is defined
- item.value.subnetmask is defined
- item.value.vlan_id is defined
- item.value.zone_name is defined
- item.value.zone_type is defined
- item.value.zone_description is defined
fail_msg: Each VLAN requires its full interface and zone mapping.
quiet: true
loop: '{{ vlan_interfaces | dict2items }}'
loop_control:
label: '{{ item.key }}'
@@ -0,0 +1,91 @@
---
# PURPOSE: Apply customer Sophos configuration
# DESCRIPTION: Apply this customer profile using hostname, network_objects and vlan_interfaces from inventory.
# TARGETS: sophosxgs
# REQUIRED NETWORK KEYS: drucker, guest, office, wlan
# INPUTS (omitted values inherit inventory / role defaults):
# aim_debug [bool]: false
# AUTH: existing inventory / Vault credentials; no embedded passwords.
# CHANGES: Changes customer firewall configuration. VLAN parent remains Port1 as in the supplied playbooks. Only this customer profile is selected.
# EXAMPLE: ansible-playbook -i inventories/<customer>/hosts.yml
# playbooks/customers/gebhardt_stahl/sophos_apply_customer.yml --limit <host> --vault-id <customer>@prompt --ask-pass
- name: Sophos | Apply customer configuration | gebhardt_stahl
hosts: sophosxgs
gather_facts: false
any_errors_fatal: false
tasks:
- name: Apply customer firewall policy
ansible.builtin.import_role:
name: sophos_customer_gebhardt_stahl
pre_tasks:
- name: AIM | Validate diagnostics option
ansible.builtin.assert:
that:
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
'false']
fail_msg: aim_debug must be a YAML/JSON boolean.
quiet: true
- name: AIM | Reject mixed platform membership
ansible.builtin.assert:
that:
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
quiet: true
- name: AIM | Execution context
ansible.builtin.debug:
msg:
host: '{{ inventory_hostname }}'
diagnostics: Enabled; secret values are never included by this task.
when: aim_debug | default(false) | bool
- name: Sophos | Require customer host configuration
ansible.builtin.assert:
that:
- hostname is defined
- hostname is string
- hostname | length > 0
- network_objects is defined
- network_objects is mapping
- vlan_interfaces is defined
- vlan_interfaces is mapping
fail_msg: Set hostname, network_objects and vlan_interfaces in this host inventory. AIM can prepare
these fields.
quiet: true
- name: Sophos | Require profile network keys
ansible.builtin.assert:
that:
- item in network_objects
fail_msg: A customer-required network_objects key is missing. See the loop item.
quiet: true
loop:
- drucker
- guest
- office
- wlan
- name: Sophos | Validate network object shape
ansible.builtin.assert:
that:
- item.value is mapping
- item.value.name is defined
- item.value.network is defined
- item.value.subnetmask is defined
fail_msg: Every network object requires name, network and subnetmask.
quiet: true
loop: '{{ network_objects | dict2items }}'
loop_control:
label: '{{ item.key }}'
- name: Sophos | Validate VLAN shape
ansible.builtin.assert:
that:
- item.value is mapping
- item.value.name is defined
- item.value.ip_address is defined
- item.value.subnetmask is defined
- item.value.vlan_id is defined
- item.value.zone_name is defined
- item.value.zone_type is defined
- item.value.zone_description is defined
fail_msg: Each VLAN requires its full interface and zone mapping.
quiet: true
loop: '{{ vlan_interfaces | dict2items }}'
loop_control:
label: '{{ item.key }}'
@@ -0,0 +1,92 @@
---
# PURPOSE: Apply customer Sophos configuration
# DESCRIPTION: Apply this customer profile using hostname, network_objects and vlan_interfaces from inventory.
# TARGETS: sophosxgs
# REQUIRED NETWORK KEYS: facility, guest, management, office, voip
# INPUTS (omitted values inherit inventory / role defaults):
# aim_debug [bool]: false
# AUTH: existing inventory / Vault credentials; no embedded passwords.
# CHANGES: Changes customer firewall configuration. VLAN parent remains Port1 as in the supplied playbooks. Only this customer profile is selected.
# EXAMPLE: ansible-playbook -i inventories/<customer>/hosts.yml
# playbooks/customers/hungeling_und_toechter/sophos_apply_customer.yml --limit <host> --vault-id <customer>@prompt --ask-pass
- name: Sophos | Apply customer configuration | hungeling_und_toechter
hosts: sophosxgs
gather_facts: false
any_errors_fatal: false
tasks:
- name: Apply customer firewall policy
ansible.builtin.import_role:
name: sophos_customer_hungeling_und_toechter
pre_tasks:
- name: AIM | Validate diagnostics option
ansible.builtin.assert:
that:
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
'false']
fail_msg: aim_debug must be a YAML/JSON boolean.
quiet: true
- name: AIM | Reject mixed platform membership
ansible.builtin.assert:
that:
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
quiet: true
- name: AIM | Execution context
ansible.builtin.debug:
msg:
host: '{{ inventory_hostname }}'
diagnostics: Enabled; secret values are never included by this task.
when: aim_debug | default(false) | bool
- name: Sophos | Require customer host configuration
ansible.builtin.assert:
that:
- hostname is defined
- hostname is string
- hostname | length > 0
- network_objects is defined
- network_objects is mapping
- vlan_interfaces is defined
- vlan_interfaces is mapping
fail_msg: Set hostname, network_objects and vlan_interfaces in this host inventory. AIM can prepare
these fields.
quiet: true
- name: Sophos | Require profile network keys
ansible.builtin.assert:
that:
- item in network_objects
fail_msg: A customer-required network_objects key is missing. See the loop item.
quiet: true
loop:
- facility
- guest
- management
- office
- voip
- name: Sophos | Validate network object shape
ansible.builtin.assert:
that:
- item.value is mapping
- item.value.name is defined
- item.value.network is defined
- item.value.subnetmask is defined
fail_msg: Every network object requires name, network and subnetmask.
quiet: true
loop: '{{ network_objects | dict2items }}'
loop_control:
label: '{{ item.key }}'
- name: Sophos | Validate VLAN shape
ansible.builtin.assert:
that:
- item.value is mapping
- item.value.name is defined
- item.value.ip_address is defined
- item.value.subnetmask is defined
- item.value.vlan_id is defined
- item.value.zone_name is defined
- item.value.zone_type is defined
- item.value.zone_description is defined
fail_msg: Each VLAN requires its full interface and zone mapping.
quiet: true
loop: '{{ vlan_interfaces | dict2items }}'
loop_control:
label: '{{ item.key }}'
@@ -0,0 +1,93 @@
---
# PURPOSE: Apply customer Sophos configuration
# DESCRIPTION: Apply this customer profile using hostname, network_objects and vlan_interfaces from inventory.
# TARGETS: sophosxgs
# REQUIRED NETWORK KEYS: facility, guest, management, office, server, voip
# INPUTS (omitted values inherit inventory / role defaults):
# aim_debug [bool]: false
# AUTH: existing inventory / Vault credentials; no embedded passwords.
# CHANGES: Changes customer firewall configuration. VLAN parent remains Port1 as in the supplied playbooks. Only this customer profile is selected.
# EXAMPLE: ansible-playbook -i inventories/<customer>/hosts.yml
# playbooks/customers/koenig_holding_gmbh/sophos_apply_customer.yml --limit <host> --vault-id <customer>@prompt --ask-pass
- name: Sophos | Apply customer configuration | koenig_holding_gmbh
hosts: sophosxgs
gather_facts: false
any_errors_fatal: false
tasks:
- name: Apply customer firewall policy
ansible.builtin.import_role:
name: sophos_customer_koenig_holding_gmbh
pre_tasks:
- name: AIM | Validate diagnostics option
ansible.builtin.assert:
that:
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
'false']
fail_msg: aim_debug must be a YAML/JSON boolean.
quiet: true
- name: AIM | Reject mixed platform membership
ansible.builtin.assert:
that:
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
quiet: true
- name: AIM | Execution context
ansible.builtin.debug:
msg:
host: '{{ inventory_hostname }}'
diagnostics: Enabled; secret values are never included by this task.
when: aim_debug | default(false) | bool
- name: Sophos | Require customer host configuration
ansible.builtin.assert:
that:
- hostname is defined
- hostname is string
- hostname | length > 0
- network_objects is defined
- network_objects is mapping
- vlan_interfaces is defined
- vlan_interfaces is mapping
fail_msg: Set hostname, network_objects and vlan_interfaces in this host inventory. AIM can prepare
these fields.
quiet: true
- name: Sophos | Require profile network keys
ansible.builtin.assert:
that:
- item in network_objects
fail_msg: A customer-required network_objects key is missing. See the loop item.
quiet: true
loop:
- facility
- guest
- management
- office
- server
- voip
- name: Sophos | Validate network object shape
ansible.builtin.assert:
that:
- item.value is mapping
- item.value.name is defined
- item.value.network is defined
- item.value.subnetmask is defined
fail_msg: Every network object requires name, network and subnetmask.
quiet: true
loop: '{{ network_objects | dict2items }}'
loop_control:
label: '{{ item.key }}'
- name: Sophos | Validate VLAN shape
ansible.builtin.assert:
that:
- item.value is mapping
- item.value.name is defined
- item.value.ip_address is defined
- item.value.subnetmask is defined
- item.value.vlan_id is defined
- item.value.zone_name is defined
- item.value.zone_type is defined
- item.value.zone_description is defined
fail_msg: Each VLAN requires its full interface and zone mapping.
quiet: true
loop: '{{ vlan_interfaces | dict2items }}'
loop_control:
label: '{{ item.key }}'
+62
View File
@@ -0,0 +1,62 @@
# PURPOSE: Detect host roles
# DESCRIPTION: Report detected AD, DHCP, Hyper-V, Veeam and UniFi capabilities without changing inventory memberships.
# TARGETS: linux, windows
# INPUTS (omitted values inherit inventory / role defaults):
# aim_debug [bool]: false
# AUTH: existing inventory / Vault credentials; no embedded passwords.
# EXAMPLE: ansible-playbook -i inventories/<customer>/hosts.yml
# playbooks/debug_detect_host_roles.yml --limit <host> --vault-id <customer>@prompt
- name: Debug | Detected host roles
hosts: linux:windows
gather_facts: true
pre_tasks:
- name: AIM | Validate diagnostics option
ansible.builtin.assert:
that:
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in
['true', 'false']
fail_msg: aim_debug must be a YAML/JSON boolean.
quiet: true
- name: AIM | Reject mixed platform membership
ansible.builtin.assert:
that:
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
quiet: true
- name: AIM | Execution context
ansible.builtin.debug:
msg:
host: '{{ inventory_hostname }}'
diagnostics: Enabled; secret values are never included by this task.
when: aim_debug | default(false) | bool
roles:
- role: system_detect_roles
tasks:
- name: Detection summary
ansible.builtin.debug:
msg:
is_dc: '{{ is_dc | default(false) }}'
is_dhcp_server: '{{ is_dhcp_server | default(false) }}'
is_hyperv_host: '{{ is_hyperv_host | 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_unifi_controller: '{{ is_unifi_controller | default(false) }}'
is_unifi_os_server: '{{ is_unifi_os_server | default(false) }}'
- name: AIM | Publish operation result
ansible.builtin.set_stats:
per_host: true
aggregate: false
data:
aim_output:
protocol: aim_output_v1
schema: host_capabilities_v1
data:
is_dc: '{{ is_dc | default(false) | bool }}'
is_dhcp_server: '{{ is_dhcp_server | default(false) | bool }}'
is_hyperv_host: '{{ is_hyperv_host | default(false) | bool }}'
has_veeam_vbr: '{{ has_veeam_vbr | default(false) | bool }}'
has_veeam_vbo: '{{ has_veeam_vbo | default(false) | bool }}'
has_veeam_em: '{{ has_veeam_em | default(false) | bool }}'
is_unifi_controller: '{{ is_unifi_controller | default(false) | bool }}'
is_unifi_os_server: '{{ is_unifi_os_server | default(false) | bool }}'
-38
View File
@@ -1,38 +0,0 @@
---
- 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
-13
View File
@@ -1,13 +0,0 @@
---
- 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:
-87
View File
@@ -1,87 +0,0 @@
---
- 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"
+185
View File
@@ -0,0 +1,185 @@
# PURPOSE: Show disk usage
# DESCRIPTION: Report attached Windows volumes and common operational Linux mounts.
# TARGETS: windows, linux
# INPUTS (omitted values inherit inventory / role defaults):
# aim_debug [bool]: false
# AUTH: existing inventory / Vault credentials; no embedded passwords.
# NOTES:
# Windows reports attached storage volumes from community.windows.win_disk_facts; mapped/network drives are excluded.
# Linux excludes pseudo/system mounts and reports common operational paths plus network/storage filesystems.
# EXAMPLE: ansible-playbook -i inventories/<customer>/hosts.yml
# playbooks/debug_show_disk_usage.yml --limit <host> --vault-id <customer>@prompt
- name: Debug | Windows disk usage
hosts: windows
gather_facts: false
tasks:
- name: AIM | Validate diagnostics option
ansible.builtin.assert:
that:
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in
['true', 'false']
fail_msg: aim_debug must be a YAML/JSON boolean.
quiet: true
- name: AIM | Reject mixed platform membership
ansible.builtin.assert:
that:
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
quiet: true
- name: AIM | Execution context
ansible.builtin.debug:
msg:
host: '{{ inventory_hostname }}'
diagnostics: Enabled; secret values are never included by this task.
when: aim_debug | default(false) | bool
- name: Windows | Gather attached disk and volume facts
community.windows.win_disk_facts:
filter:
- partitions
- volumes
changed_when: false
- name: Windows | Normalize attached volume usage
ansible.builtin.set_fact:
_windows_disk_report: "{{ ansible_facts.disks | default([]) | aim_report_disks('windows') }}"
- name: Windows | Print disk usage
ansible.builtin.debug:
msg: |-
{{ inventory_hostname }}
{% for d in _windows_disk_report.filesystems | default([]) %}
{% if d.status == 'available' %}
{{ '%-18s' | format(d.name) }} {{ (d.used_bytes / 1073741824) | round(2) }} GB / {{ (d.total_bytes / 1073741824) | round(2) }} GB | {{ d.used_percent | round(1) }}% used | {{ (d.available_bytes / 1073741824) | round(2) }} GB free | {{ d.filesystem_type | default('unknown', true) }} | {{ d.mount }}
{% else %}
{{ '%-18s' | format(d.name) }} unavailable | {{ d.mount }}
{% endif %}
{% endfor %}
- name: AIM | Publish operation result
ansible.builtin.set_stats:
per_host: true
aggregate: false
data:
aim_output:
protocol: aim_output_v1
schema: filesystem_usage_v1
data: "{{ _windows_disk_report }}"
- name: Debug | Linux disk usage
hosts: linux
gather_facts: false
become: false
vars:
_disk_common_mounts:
- /
- /boot
- /boot/efi
- /home
- /var
- /var/log
- /tmp
- /opt
- /srv
_disk_network_storage_fstypes:
- nfs
- nfs4
- cifs
- smb3
- ceph
- glusterfs
- fuse.sshfs
- fuse.glusterfs
_disk_excluded_fstypes:
- proc
- sysfs
- devtmpfs
- tmpfs
- cgroup
- cgroup2
- overlay
- squashfs
- nsfs
- tracefs
- debugfs
- securityfs
- pstore
- configfs
- hugetlbfs
- mqueue
- rpc_pipefs
- autofs
- fusectl
- binfmt_misc
tasks:
- name: AIM | Validate diagnostics option
ansible.builtin.assert:
that:
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in
['true', 'false']
fail_msg: aim_debug must be a YAML/JSON boolean.
quiet: true
- name: AIM | Reject mixed platform membership
ansible.builtin.assert:
that:
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
quiet: true
- name: Linux | Collect mount facts
ansible.builtin.setup:
filter:
- ansible_mounts
gather_subset:
- '!all'
- '!min'
changed_when: false
- name: Linux | Reset selected mount report
ansible.builtin.set_fact:
_linux_disk_mounts: []
- name: Linux | Select common operational mounts
ansible.builtin.set_fact:
_linux_disk_mounts: '{{ (_linux_disk_mounts | default([])) + [item] }}'
loop: '{{ ansible_facts.mounts | default(ansible_mounts | default([])) }}'
loop_control:
label: "{{ item.mount | default('?') }}"
when:
- item.fstype | default('') not in _disk_excluded_fstypes
- >-
(item.mount | default('')) in _disk_common_mounts
or (item.mount | default('')).startswith('/mnt/')
or (item.mount | default('')).startswith('/media/')
or (item.fstype | default('')) in _disk_network_storage_fstypes
- name: Linux | Execution context
ansible.builtin.debug:
msg:
host: '{{ inventory_hostname }}'
selected_mounts: "{{ (_linux_disk_mounts | default([])) | map(attribute='mount') | list }}"
diagnostics: Enabled; pseudo/system mounts are excluded.
when: aim_debug | default(false) | bool
- name: Linux | Print disk usage
ansible.builtin.debug:
msg: |-
{{ inventory_hostname }}
{% for m in (_linux_disk_mounts | default([]) | sort(attribute='mount')) %}
{% set total = m.size_total | default(0) | float %}
{% set free = m.size_available | default(0) | float %}
{% set used = total - free %}
{% set pct = ((used / total) * 100) if total > 0 else 0 %}
{{ '%-18s' | format(m.mount) }} {{ (used / 1073741824) | round(2) }} GB / {{ (total / 1073741824) | round(2) }} GB | {{ pct | round(1) }}% used | {{ (free / 1073741824) | round(2) }} GB free | {{ m.fstype }}
{% endfor %}
- name: AIM | Publish operation result
ansible.builtin.set_stats:
per_host: true
aggregate: false
data:
aim_output:
protocol: aim_output_v1
schema: filesystem_usage_v1
data: "{{ _linux_disk_mounts | aim_report_disks('linux') }}"
+61
View File
@@ -0,0 +1,61 @@
---
# PURPOSE: Test Ansible connection
# DESCRIPTION: Check Ansible manageability using ping or win_ping; this is not an ICMP ping.
# TARGETS: linux, windows
# INPUTS (omitted values inherit inventory / role defaults):
# aim_debug [bool]: false
# AUTH: existing inventory / Vault credentials; no embedded passwords.
# EXAMPLE: ansible-playbook -i inventories/<customer>/hosts.yml
# playbooks/debug_test_connection.yml --limit <host> --vault-id <customer>@prompt
- name: Debug | Windows manageability
hosts: windows
gather_facts: false
pre_tasks:
- name: AIM | Validate diagnostics option
ansible.builtin.assert:
that:
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
'false']
fail_msg: aim_debug must be a YAML/JSON boolean.
quiet: true
- name: AIM | Reject mixed platform membership
ansible.builtin.assert:
that:
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
quiet: true
- name: AIM | Execution context
ansible.builtin.debug:
msg:
host: '{{ inventory_hostname }}'
diagnostics: Enabled; secret values are never included by this task.
when: aim_debug | default(false) | bool
tasks:
- name: Windows | Test WinRM
ansible.windows.win_ping: {}
- name: Debug | Linux manageability
hosts: linux
gather_facts: false
pre_tasks:
- name: AIM | Validate diagnostics option
ansible.builtin.assert:
that:
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
'false']
fail_msg: aim_debug must be a YAML/JSON boolean.
quiet: true
- name: AIM | Reject mixed platform membership
ansible.builtin.assert:
that:
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
quiet: true
- name: AIM | Execution context
ansible.builtin.debug:
msg:
host: '{{ inventory_hostname }}'
diagnostics: Enabled; secret values are never included by this task.
when: aim_debug | default(false) | bool
tasks:
- name: Linux | Test SSH and Python
ansible.builtin.ping: {}
+398
View File
@@ -0,0 +1,398 @@
"""Report normalization owned by the shipped runbooks, not by the Core API.
Only deliberately selected public fields leave these functions. Raw registered
results, exception text, credentials and command lines are never returned.
"""
from __future__ import annotations
import json
import math
import re
from datetime import datetime, timezone
from collections.abc import Mapping
def _bool(value):
if type(value) is bool: return value
if str(value).lower() in ('true','yes','1'): return True
if str(value).lower() in ('false','no','0','none',''): return False
raise ValueError('Report boolean is not a supported literal')
def epoch_iso_utc(value):
return datetime.fromtimestamp(float(value), tz=timezone.utc).isoformat().replace('+00:00','Z')
def capabilities(value):
names = ('is_dc','is_dhcp_server','is_hyperv_host','has_veeam_vbr','has_veeam_vbo',
'has_veeam_em','is_unifi_controller','is_unifi_os_server')
return {k:_bool(value.get(k, False)) for k in names}
def disks(value, platform):
result=[]
if platform == 'windows':
# community.windows.win_disk_facts returns disks -> partitions -> volumes.
# Report attached volumes rather than PowerShell-session/mapped drives.
for disk in value or []:
for partition in disk.get('partitions', []) or []:
drive_letter=partition.get('drive_letter')
for volume in partition.get('volumes', []) or []:
total=volume.get('size')
free=volume.get('size_remaining')
good=isinstance(total, (int, float)) and isinstance(free, (int, float)) and total >= 0 and free >= 0
if good:
total=int(total); free=int(free); used=max(0,total-free)
pct=round(used/total*100,2) if total else 0.0
else:
used=total=free=pct=None
native_path=volume.get('path') or volume.get('object_id') or ''
if drive_letter:
name=f'{drive_letter}:'
mount=f'{drive_letter}:\\'
else:
name=volume.get('label') or native_path or f"volume-disk{disk.get('number','?')}-part{partition.get('number','?')}"
mount=native_path or name
result.append(dict(name=str(name),mount=str(mount),filesystem_type=volume.get('type'),
used_bytes=used,total_bytes=total,available_bytes=free,used_percent=pct,
status='available' if good else 'unavailable'))
else:
for row in value:
total,free=int(row.get('size_total',0)),int(row.get('size_available',0))
used=max(0,total-free); good=total>0
name,mount,fs=row.get('device',row['mount']),row['mount'],row.get('fstype')
if not good: used=total=free=pct=None
else:
used,total,free=(int(x) if x is not None else None for x in (used,total,free))
pct=round(used/total*100,2) if total and used is not None else None
result.append(dict(name=name,mount=mount,filesystem_type=fs,used_bytes=used,total_bytes=total,
available_bytes=free,used_percent=pct,status='available' if good else 'unavailable'))
return {'platform':platform,'filesystems':result}
SERVICE_ERRORS={
5:('permission_denied','Access was denied when starting the service.'),
1053:('start_timeout','The service did not respond to the start request in time.'),
1058:('service_disabled','The service is disabled.'),
1060:('service_not_found','The service no longer exists.'),
1068:('dependency_failed','A required dependency service could not be started.'),
1069:('logon_failed','The service account could not log on.'),
}
def service_error(raw):
code=raw.get('native_code',raw.get('error_code'))
if type(code) is not int: code=None
reason,message=SERVICE_ERRORS.get(code,('start_failed','The service start request failed.'))
# A bounded fallback for the existing win_service module; no raw text export.
text=str(raw.get('msg',''))[:4096].lower()
if code is None:
signatures=[('access is denied',5),('access denied',5),('dependency',1068),
('disabled',1058),('timed out',1053),('does not exist',1060),('logon failure',1069)]
for term,num in signatures:
if term in text:
reason,message=SERVICE_ERRORS[num]; break
return reason,message,code
def services(before, attempts, after, include=(), exclude=(), check=False):
stopped=sorted(s['name'] for s in before if s.get('state')=='stopped')
eligible=sorted(s['name'] for s in before if s.get('state')=='stopped'
and s.get('start_mode') in ('auto','delayed') and (not include or s['name'] in include)
and s['name'] not in exclude)
actual={}
for row in attempts:
if row.get('skipped'): continue
name=row.get('item',{}).get('name')
if name in eligible and not check: actual[name]=row
states={s['name']:s.get('state','unknown') for s in after}
newly=sorted(n for n in stopped if states.get(n)=='started')
still=sorted(n for n in stopped if states.get(n)=='stopped')
absent=sorted(n for n in stopped if n not in states)
failures=[]
for name, row in actual.items():
if states.get(name)=='started': continue
if row.get('failed'):
reason,message,code=service_error(row)
elif name not in states:
reason,message,code='state_unavailable','Post-start service state could not be observed.',None
else:
reason,message,code='not_running_after_start','The service is not running at the post-start observation.',None
failures.append({'name':name,'reason':reason,'message':message,'native_code':code})
return dict(mode='check' if check else 'apply',initially_stopped=stopped,eligible=eligible,
attempted=sorted(actual),excluded=sorted(set(stopped)-set(eligible)),newly_running=newly,
still_stopped=still,unobserved=absent,failed_to_start=failures,
started_count=len(set(newly)&set(actual)),failed_count=len(failures),before_count=len(stopped),
after_observed=bool(after) or not before)
def package_snapshot(raw, platform):
"""Normalize package_facts (preferred) or legacy textual snapshots."""
result={}
if isinstance(raw, Mapping):
for name, rows in raw.items():
if not isinstance(rows, list): continue
for row in rows:
if not isinstance(row, Mapping): continue
arch=str(row.get('arch') or 'unknown')
version=str(row.get('version') or '')
release=row.get('release')
epoch=row.get('epoch')
if release not in (None,''):
version=f'{version}-{release}'
if epoch not in (None,'','0',0):
version=f'{epoch}:{version}'
result.setdefault((str(name),arch),set()).add(version)
return result
for line in str(raw).splitlines():
columns=line.split('\t')
if len(columns)!=4: raise ValueError('Invalid package database record')
name,arch,version,status=columns
if platform=='debian' and status!='installed': continue
result.setdefault((name,arch),set()).add(version)
return result
def _patch_reboot_fields(reboot_required, rebooted, preexisting, reboot_enabled, delay_minutes, blocked_reason=None):
observed = None if reboot_required is None else bool(reboot_required)
performed = bool(rebooted)
final_required = False if performed else observed
deferred = bool(final_required) and not bool(reboot_enabled)
return dict(
reboot_required=final_required,
reboot_required_before=bool(preexisting),
reboot_required_after=final_required,
reboot_performed=performed,
reboot_deferred=deferred,
reboot_delay_minutes=int(delay_minutes),
blocked_reason=blocked_reason,
)
def _reboot_reasons(value):
rows=value if isinstance(value,list) else []
out=[]
for row in rows:
if not isinstance(row,Mapping): continue
source=str(row.get('source','unknown'))[:128]
desc=str(row.get('description',''))[:512]
out.append(dict(source=source,description=desc))
return out[:32]
def patch_blocked(platform, check=False, delay_minutes=0, rescan_after_reboot=False, reboot_reasons=()):
result=dict(platform=str(platform), mode='check' if check else 'apply',
evidence='preflight_reboot_state', complete=False, updates=[], updated_count=0,
installed_count=0, removed_count=0, pending=[], failed_updates=[])
result.update(_patch_reboot_fields(True, False, True, False, delay_minutes,
'preexisting_reboot_required'))
if str(platform) == 'windows':
result.update(rescan_after_reboot=bool(rescan_after_reboot), patch_cycles=0,
continuation_required=True, remaining_updates_known=False,
reboot_reasons_before=_reboot_reasons(reboot_reasons))
return result
def patch_linux(before, after, platform, check=False, complete=True, reboot_required=None, rebooted=False,
preexisting=False, reboot_enabled=True, delay_minutes=0):
old,new=package_snapshot(before,platform),package_snapshot(after,platform)
updates=[]
if not check:
for name,arch in sorted(set(old)|set(new)):
a,b=old.get((name,arch),set()),new.get((name,arch),set())
if a==b: continue
updates.append(dict(name=name,identifier=None,architecture=arch,old_versions=sorted(a),new_versions=sorted(b),
action='updated' if a and b else 'installed' if b else 'removed',kb=[]))
result=dict(platform=platform,mode='check' if check else 'apply',evidence='package_snapshots',complete=bool(complete),
updates=updates,updated_count=sum(x['action']=='updated' for x in updates),
installed_count=sum(x['action']=='installed' for x in updates),removed_count=sum(x['action']=='removed' for x in updates),
pending=[],failed_updates=[])
result.update(_patch_reboot_fields(reboot_required, rebooted, preexisting, reboot_enabled, delay_minutes))
return result
def _hresult_u32(code):
if type(code) is not int: return None
return code & 0xffffffff
WINDOWS_UPDATE_FAILURES={
0x80240009:('operation_in_progress','Another conflicting Windows Update operation is in progress.'),
0x80240016:('install_not_allowed','Windows Update could not install this update because another installation was active or a mandatory reboot was pending.'),
0x80240017:('not_applicable','The update is no longer applicable to this host.'),
0x80240019:('exclusive_install_conflict','Windows Update reported an exclusive-install conflict.'),
0x8024001B:('self_update_in_progress','The Windows Update Agent is updating itself.'),
0x8024001F:('no_connection','Windows Update could not complete because its network connection was unavailable.'),
0x80240021:('timeout','Windows Update did not complete before its operation timeout.'),
}
def _windows_failure(code):
normalized=_hresult_u32(code)
reason,message=WINDOWS_UPDATE_FAILURES.get(normalized,('update_failed','Windows Update failed to install this update.'))
return normalized,reason,message
def windows_update_result_failed(value):
value=value if isinstance(value,Mapping) else {}
if value.get('failed') is True: return True
if int(value.get('failed_update_count',0) or 0)>0: return True
return any(isinstance(row,Mapping) and 'failure_hresult_code' in row for row in value.get('updates',{}).values())
def windows_update_block_reason(value):
value=value if isinstance(value,Mapping) else {}
for row in value.get('updates',{}).values():
if isinstance(row,Mapping) and 'failure_hresult_code' in row:
return _windows_failure(row.get('failure_hresult_code'))[1]
# ansible_failed_result can carry only a generic module failure. Keep this bounded.
return 'update_failed'
def _windows_pending_from_search(value):
value=value if isinstance(value,Mapping) else {}
pending=[]
for ident,row in value.get('updates',{}).items():
if not isinstance(row,Mapping): continue
pending.append(dict(name=str(row.get('title',ident)),identifier=str(ident),
kb=[str(x) for x in row.get('kb',[])]))
return pending
def patch_windows_runs(runs, searches=(), check=False, preexisting=False, rebooted=False,
reboot_enabled=True, delay_minutes=0, rescan_after_reboot=False,
patch_cycles=0, continuation_required=False, remaining_updates_known=False,
reboot_deferred=False, reboot_required_after=False, blocked_reason=None,
complete_override=True, reboot_reasons_before=()):
runs=runs if isinstance(runs,list) else []
searches=searches if isinstance(searches,list) else []
installed={}; failed={}
for entry in runs:
if not isinstance(entry,Mapping): continue
requested=entry.get('requested',{}) if isinstance(entry.get('requested'),Mapping) else {}
value=entry.get('result',{}) if isinstance(entry.get('result'),Mapping) else {}
rows=value.get('updates',{}) if isinstance(value.get('updates'),Mapping) else {}
if not rows and entry.get('task_failed'):
wave=int(entry.get('wave',0) or 0)
ident=str(requested.get('identifier') or ('wave-%s' % wave if wave else 'windows-update-wave'))
failed[ident]=dict(name=str(requested.get('title') or 'Windows Update patch wave'),identifier=ident,
native_code=None,native_code_hex=None,reason='update_failed',
message='Windows Update failed before per-update failure details were available.')
for ident,row in rows.items():
if not isinstance(row,Mapping): continue
ident=str(ident); name=str(row.get('title',requested.get('title',ident)))
kb=[str(x) for x in row.get('kb',requested.get('kb',[]))]
if row.get('installed') is True and not check:
installed[ident]=dict(name=name,identifier=ident,architecture=None,old_versions=[],new_versions=[],
action='updated',kb=kb)
failed.pop(ident,None)
if 'failure_hresult_code' in row:
code,reason,message=_windows_failure(row.get('failure_hresult_code'))
failed[ident]=dict(name=name,identifier=ident,native_code=code,
native_code_hex=(f'0x{code:08X}' if code is not None else None),
reason=reason,message=message)
pending=[]
if remaining_updates_known and searches:
pending=_windows_pending_from_search(searches[-1])
# A final search is authoritative for remaining applicability; do not duplicate
# updates that it says are no longer pending.
final_required=bool(reboot_required_after)
performed=bool(rebooted)
deferred=bool(reboot_deferred) or (final_required and not bool(reboot_enabled))
complete=bool(complete_override) and not bool(failed)
result=dict(platform='windows',mode='check' if check else 'apply',evidence='windows_update_result',
complete=complete,updates=list(installed.values()),updated_count=len(installed),installed_count=0,
removed_count=0,pending=pending,failed_updates=list(failed.values()),
reboot_required=final_required,reboot_required_before=bool(preexisting),
reboot_required_after=final_required,reboot_performed=performed,reboot_deferred=deferred,
reboot_delay_minutes=int(delay_minutes),blocked_reason=blocked_reason,
rescan_after_reboot=bool(rescan_after_reboot),patch_cycles=int(patch_cycles),
continuation_required=bool(continuation_required),remaining_updates_known=bool(remaining_updates_known),
reboot_reasons_before=_reboot_reasons(reboot_reasons_before))
return result
def cleanup(paths, directory, removed, unifi, unifi_result, enabled=False, check=False):
deleted=[r.get('item') for r in removed.get('results',[]) if r.get('changed') and not check]
mode='check' if check else 'apply' if enabled else 'preview'
state='retained'
if unifi=='disabled':
state='candidate' if not enabled or check else 'removed' if unifi_result.get('changed') else 'unchanged'
return dict(mode=mode,directory=directory,candidates=list(paths),removed=deleted,
unifi_configuration=state,complete=True)
_SECRET=re.compile(r'password|passwd|passphrase|secret|token|credential|private.?key|authorization|community',re.I)
_COMMAND={'cmd_line','command','command_line','arguments','args','environment','env','passphrase'}
def checkmk_config(value):
"""Read only the named user config; redact secret/command fields before publication."""
import yaml
path=value['path']
if re.split(r'[\\/]',path)[-1].lower()!='check_mk.user.yml':
raise ValueError('Only check_mk.user.yml can be published')
content=value.get('content','')
if len(content.encode('utf-8'))>512*1024:
raise ValueError('Checkmk user configuration exceeds report limit')
data=yaml.safe_load(content) if value.get('exists') else {}
if data is None: data={}
if not isinstance(data,dict): raise ValueError('Checkmk configuration must be a mapping')
redactions=[]; seen=set(); budget=[0]
def walk(item,path,depth=0):
budget[0]+=1
if depth>18 or budget[0]>50000: raise ValueError('Configuration structure exceeds limit')
if isinstance(item,(dict,list)):
if id(item) in seen: raise ValueError('Recursive configuration aliases are unsupported')
seen.add(id(item))
try:
if isinstance(item,list): return [walk(x,path+[str(i)],depth+1) for i,x in enumerate(item)]
out={}
for key,val in item.items():
if not isinstance(key,str): key=str(key)
here=path+[key]
if _SECRET.search(key) or key.lower() in _COMMAND or (path and path[0].lower()=='mrpe' and key.lower() in ('config','entries')):
out[key]='[REDACTED]'; redactions.append('.'.join(here)); continue
out[key]=walk(val,here,depth+1)
return out
finally:seen.remove(id(item))
if isinstance(item,str):
# Do not expose URL userinfo, inline password assignment, PEM key bodies.
if re.search(r'://[^/\s]+:[^/\s]+@|(?:password|passwd|token|secret)\s*[=:]|-----BEGIN .*PRIVATE KEY',item,re.I):
redactions.append('.'.join(path)); return '[REDACTED]'
# Multiline operational strings are represented by spaces, not terminal controls.
return ' '.join(item.splitlines())
if item is None or type(item) in (bool,int,float): return item
return str(item)
return dict(path=path,exists=bool(value.get('exists')),size_bytes=int(value.get('size_bytes',0)),
last_write_time_utc=value.get('last_write_time_utc'),sections=walk(data,[]),
redacted_paths=redactions,comment_preservation='not_in_structured_output')
def checkmk_changes(copies, selected, plugin_update, unifi_result, opposite, mode, check=False):
changed_checks=[r['item']['filename'] for r in copies.get('results',[]) if r.get('changed') and isinstance(r.get('item'),Mapping)]
removed=[]
if opposite.get('changed') and mode in ('os','network'):
removed=['check_unifi-controller.sh' if mode=='os' else 'check_unifi-os.sh']
changes=[dict(component='check',name=n,action='updated') for n in changed_checks]
changes += [dict(component='check',name=n,action='removed') for n in removed]
if plugin_update.get('changed'):changes.append(dict(component='section',name='plugins',action='updated'))
if unifi_result.get('changed'):changes.append(dict(component='configuration',name='unifi.cfg',action='updated'))
return dict(mode='check' if check else 'apply',changed=bool(changes),managed_sections=['plugins'] if plugin_update else [],
changes=changes,deployed_checks=[s['filename'] for s in selected],changed_checks=changed_checks,
removed_checks=removed if not check else [],unknown_files_policy='untouched_not_enumerated')
def checkmk_state(observed, service_rows, change_report, package_changed=False, check=False):
return dict(mode='check' if check else 'apply',installed=observed.get('installed'),version=observed.get('version'),
version_source=observed.get('version_source','unavailable'),
services=[dict(name=s['name'],state=s.get('state','unknown')) for s in service_rows],
package_changed=bool(package_changed),configuration_updated=any(c['component'] in ('section','configuration') for c in change_report['changes']),
checks_deployed=change_report['deployed_checks'],changed_checks=change_report['changed_checks'],removed_checks=change_report['removed_checks'])
class FilterModule:
def filters(self):
return {'aim_epoch_iso_utc':epoch_iso_utc,'aim_report_capabilities':capabilities,'aim_report_disks':disks,'aim_report_services':services,
'aim_report_patch_linux':patch_linux,'aim_report_patch_windows_runs':patch_windows_runs,
'aim_report_patch_blocked':patch_blocked,
'aim_windows_update_result_failed':windows_update_result_failed,
'aim_windows_update_block_reason':windows_update_block_reason,
'aim_report_cleanup':cleanup,'aim_report_checkmk_config':checkmk_config,
'aim_report_checkmk_changes':checkmk_changes,'aim_report_checkmk_state':checkmk_state}
@@ -1,74 +0,0 @@
---
- 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
@@ -0,0 +1,37 @@
---
# PURPOSE: Export Windows event logs
# DESCRIPTION: Export selected event channels to EVTX files on the target; existing event logs are not cleared.
# TARGETS: windows
# INPUTS (omitted values inherit inventory / role defaults):
# aim_debug [bool]: false
# event_age_days [int]: 45
# export_folder [text]: C:\Logs
# event_log_channels [list]: Application, Security, System, Setup
# AUTH: existing inventory / Vault credentials; no embedded passwords.
# EXAMPLE: ansible-playbook -i inventories/<customer>/hosts.yml
# playbooks/maintenance_export_event_logs.yml --limit <host> --vault-id <customer>@prompt
- name: Maintenance | Export Windows event logs
hosts: windows
gather_facts: false
pre_tasks:
- name: AIM | Validate diagnostics option
ansible.builtin.assert:
that:
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
'false']
fail_msg: aim_debug must be a YAML/JSON boolean.
quiet: true
- name: AIM | Reject mixed platform membership
ansible.builtin.assert:
that:
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
quiet: true
- name: AIM | Execution context
ansible.builtin.debug:
msg:
host: '{{ inventory_hostname }}'
diagnostics: Enabled; secret values are never included by this task.
when: aim_debug | default(false) | bool
roles:
- role: maintenance_export_event_logs
+67 -32
View File
@@ -1,35 +1,70 @@
---
- name: "Maintenance | Patch operating system"
hosts: all
# PURPOSE: Patch operating systems
# DESCRIPTION: Apply updates on Windows, Debian and RedHat-family systems; optionally reboot when required.
# TARGETS: linux, windows
# INPUTS (omitted values inherit inventory / role defaults):
# aim_debug [bool]: false
# os_patching_reboot [bool]: true
# os_patching_windows_categories [list]: SecurityUpdates, CriticalUpdates, UpdateRollups, DefinitionUpdates, Updates
# os_patching_serial [serial]: 100%
# os_patching_reboot_timeout [int]: 600
# os_patching_reboot_delay_minutes [int]: 0
# os_patching_reboot_message [text]: AIM maintenance: operating system patching requires a reboot.
# os_patching_rescan_after_reboot [bool]: false (Windows only; continue with a newly discovered patch wave after reboot)
# AUTH: existing inventory / Vault credentials; no embedded passwords.
# CHANGES: Updates production operating systems. Optional AIM-initiated reboots notify logged-in users and honor the configured delay.
# EXAMPLE: ansible-playbook -i inventories/<customer>/hosts.yml
# playbooks/maintenance_patch_os.yml --limit <host> --vault-id <customer>@prompt
- name: Maintenance | Patch Linux
hosts: linux
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)
pre_tasks:
- name: AIM | Validate diagnostics option
ansible.builtin.assert:
that:
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
'false']
fail_msg: aim_debug must be a YAML/JSON boolean.
quiet: true
- name: AIM | Reject mixed platform membership
ansible.builtin.assert:
that:
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
quiet: true
- name: AIM | Execution context
ansible.builtin.debug:
msg: "Windows updates were installed and a reboot is required. No reboot was performed."
msg:
host: '{{ inventory_hostname }}'
diagnostics: Enabled; secret values are never included by this task.
when: aim_debug | default(false) | bool
become: true
serial: '{{ os_patching_serial | default(''100%'') }}'
roles:
- role: maintenance_patch_os
- name: Maintenance | Patch Windows
hosts: windows
gather_facts: true
pre_tasks:
- name: AIM | Validate diagnostics option
ansible.builtin.assert:
that:
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
'false']
fail_msg: aim_debug must be a YAML/JSON boolean.
quiet: true
- name: AIM | Reject mixed platform membership
ansible.builtin.assert:
that:
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
quiet: true
- name: AIM | Execution context
ansible.builtin.debug:
msg:
host: '{{ inventory_hostname }}'
diagnostics: Enabled; secret values are never included by this task.
when: aim_debug | default(false) | bool
serial: '{{ os_patching_serial | default(''100%'') }}'
roles:
- role: maintenance_patch_os
-15
View File
@@ -1,15 +0,0 @@
---
- 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
+68
View File
@@ -0,0 +1,68 @@
---
# PURPOSE: Reboot hosts
# DESCRIPTION: Reboot selected hosts in batches and wait for management connectivity.
# TARGETS: linux, windows
# INPUTS (omitted values inherit inventory / role defaults):
# aim_debug [bool]: false
# maintenance_reboot_serial [serial]: 10
# maintenance_reboot_timeout [int]: 1800
# maintenance_reboot_message [text]: Reboot initiated by debsansible01.bfmiglabor.lan (Ansible)
# maintenance_reboot_pre_delay [int]: 0
# maintenance_reboot_post_delay [int]: 15
# AUTH: existing inventory / Vault credentials; no embedded passwords.
# CHANGES: Every selected host will be rebooted. No reboot occurs before final confirmation.
# EXAMPLE: ansible-playbook -i inventories/<customer>/hosts.yml
# playbooks/maintenance_reboot_hosts.yml --limit <host> --vault-id <customer>@prompt
- name: Maintenance | Reboot Linux
hosts: linux
gather_facts: true
pre_tasks:
- name: AIM | Validate diagnostics option
ansible.builtin.assert:
that:
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
'false']
fail_msg: aim_debug must be a YAML/JSON boolean.
quiet: true
- name: AIM | Reject mixed platform membership
ansible.builtin.assert:
that:
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
quiet: true
- name: AIM | Execution context
ansible.builtin.debug:
msg:
host: '{{ inventory_hostname }}'
diagnostics: Enabled; secret values are never included by this task.
when: aim_debug | default(false) | bool
become: true
serial: '{{ maintenance_reboot_serial | default(10) }}'
roles:
- role: maintenance_reboot_hosts
- name: Maintenance | Reboot Windows
hosts: windows
gather_facts: false
pre_tasks:
- name: AIM | Validate diagnostics option
ansible.builtin.assert:
that:
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
'false']
fail_msg: aim_debug must be a YAML/JSON boolean.
quiet: true
- name: AIM | Reject mixed platform membership
ansible.builtin.assert:
that:
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
quiet: true
- name: AIM | Execution context
ansible.builtin.debug:
msg:
host: '{{ inventory_hostname }}'
diagnostics: Enabled; secret values are never included by this task.
when: aim_debug | default(false) | bool
serial: '{{ maintenance_reboot_serial | default(10) }}'
roles:
- role: maintenance_reboot_hosts
@@ -1,82 +1,37 @@
---
- 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'
# PURPOSE: Start stopped automatic services
# DESCRIPTION: Start eligible stopped services, apply optional include/exclude lists and report partial failures.
# TARGETS: windows
# INPUTS (omitted values inherit inventory / role defaults):
# aim_debug [bool]: false
# maintenance_service_include [list]: []
# maintenance_service_exclude [list]: []
# maintenance_service_fail_on_error [bool]: true
# AUTH: existing inventory / Vault credentials; no embedded passwords.
# EXAMPLE: ansible-playbook -i inventories/<customer>/hosts.yml
# playbooks/maintenance_start_stopped_services.yml --limit <host> --vault-id <customer>@prompt
- name: Maintenance | Start automatic services
hosts: windows
gather_facts: false
pre_tasks:
- name: AIM | Validate diagnostics option
ansible.builtin.assert:
that:
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
'false']
fail_msg: aim_debug must be a YAML/JSON boolean.
quiet: true
- name: AIM | Reject mixed platform membership
ansible.builtin.assert:
that:
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
quiet: true
- name: AIM | Execution context
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'] }}
msg:
host: '{{ inventory_hostname }}'
diagnostics: Enabled; secret values are never included by this task.
when: aim_debug | default(false) | bool
roles:
- role: maintenance_start_stopped_services
+63
View File
@@ -0,0 +1,63 @@
---
# PURPOSE: Apply bitformer pfSense baseline
# DESCRIPTION: Apply the supplied pfSense baseline without changing its firewall/VPN policy.
# TARGETS: pfsense
# INPUTS (omitted values inherit inventory / role defaults):
# aim_debug [bool]: false
# AUTH: existing inventory / Vault credentials; no embedded passwords.
# CHANGES: Contains the original any-source WAN management rule for ports 22/80/443. The original CA, VPN endpoint and client certificate reference are unchanged; verify them before execution. Requires separately approved pfsensible.core installation.
# EXAMPLE: ansible-playbook -i inventories/<customer>/hosts.yml
# playbooks/pfsense_apply_baseline.yml --limit <host> --vault-id <customer>@prompt
- name: Install pfSense sudo package
hosts: pfsense
tasks:
- name: Apply supplied firewall policy
ansible.builtin.import_role:
name: pfsense_install_prerequisites
pre_tasks:
- name: AIM | Validate diagnostics option
ansible.builtin.assert:
that:
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
'false']
fail_msg: aim_debug must be a YAML/JSON boolean.
quiet: true
- name: AIM | Reject mixed platform membership
ansible.builtin.assert:
that:
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
quiet: true
- name: AIM | Execution context
ansible.builtin.debug:
msg:
host: '{{ inventory_hostname }}'
diagnostics: Enabled; secret values are never included by this task.
when: aim_debug | default(false) | bool
- name: Initial pfSense bitformer config
hosts: pfsense
become: true
tasks:
- name: Apply supplied firewall policy
ansible.builtin.import_role:
name: pfsense_apply_baseline
pre_tasks:
- name: AIM | Validate diagnostics option
ansible.builtin.assert:
that:
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
'false']
fail_msg: aim_debug must be a YAML/JSON boolean.
quiet: true
- name: AIM | Reject mixed platform membership
ansible.builtin.assert:
that:
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
quiet: true
- name: AIM | Execution context
ansible.builtin.debug:
msg:
host: '{{ inventory_hostname }}'
diagnostics: Enabled; secret values are never included by this task.
when: aim_debug | default(false) | bool
@@ -0,0 +1,77 @@
# AIM public operation-data schema. Closed fields except explicitly redacted configuration sections.
type: object
properties:
mode:
type: string
maxLength: 128
enum:
- apply
- check
changed:
type: boolean
managed_sections:
type: array
items:
type: string
maxLength: 1024
maxItems: 20000
changes:
type: array
items:
type: object
properties:
component:
type: string
maxLength: 128
enum:
- section
- check
- configuration
name:
type: string
maxLength: 1024
action:
type: string
maxLength: 128
enum:
- updated
- removed
required:
- component
- name
- action
additionalProperties: false
maxItems: 20000
deployed_checks:
type: array
items:
type: string
maxLength: 1024
maxItems: 20000
changed_checks:
type: array
items:
type: string
maxLength: 1024
maxItems: 20000
removed_checks:
type: array
items:
type: string
maxLength: 1024
maxItems: 20000
unknown_files_policy:
type: string
maxLength: 128
enum:
- untouched_not_enumerated
required:
- mode
- changed
- managed_sections
- changes
- deployed_checks
- changed_checks
- removed_checks
- unknown_files_policy
additionalProperties: false
@@ -0,0 +1,74 @@
# AIM public operation-data schema. Closed fields except explicitly redacted configuration sections.
type: object
properties:
mode:
type: string
maxLength: 128
enum:
- apply
- check
installed:
type:
- boolean
- 'null'
version:
type:
- string
- 'null'
maxLength: 1024
version_source:
type: string
maxLength: 128
enum:
- registry
- package_database
- unavailable
services:
type: array
items:
type: object
properties:
name:
type: string
maxLength: 1024
state:
type: string
maxLength: 1024
required:
- name
- state
additionalProperties: false
maxItems: 20000
package_changed: &id001
type: boolean
configuration_updated: *id001
checks_deployed:
type: array
items:
type: string
maxLength: 1024
maxItems: 20000
changed_checks:
type: array
items:
type: string
maxLength: 1024
maxItems: 20000
removed_checks:
type: array
items:
type: string
maxLength: 1024
maxItems: 20000
required:
- mode
- installed
- version
- version_source
- services
- package_changed
- configuration_updated
- checks_deployed
- changed_checks
- removed_checks
additionalProperties: false
@@ -0,0 +1,40 @@
# AIM public operation-data schema. Closed fields except explicitly redacted configuration sections.
type: object
properties:
path:
type: string
maxLength: 1024
exists:
type: boolean
size_bytes:
type: integer
minimum: 0
last_write_time_utc:
type:
- string
- 'null'
maxLength: 1024
sections:
type: object
properties: {}
additionalProperties: true
redacted_paths:
type: array
items:
type: string
maxLength: 1024
maxItems: 20000
comment_preservation:
type: string
maxLength: 128
enum:
- not_in_structured_output
required:
- path
- exists
- size_bytes
- last_write_time_utc
- sections
- redacted_paths
- comment_preservation
additionalProperties: false
+30
View File
@@ -0,0 +1,30 @@
# AIM public operation-data schema. Closed fields except explicitly redacted configuration sections.
type: object
properties:
mode:
type: string
maxLength: 128
enum:
- apply
- check
days:
type: integer
minimum: 0
files:
type: array
items:
type: string
maxLength: 1024
maxItems: 20000
channels:
type: array
items:
type: string
maxLength: 1024
maxItems: 20000
required:
- mode
- days
- files
- channels
additionalProperties: false
+66
View File
@@ -0,0 +1,66 @@
# AIM public operation-data schema. Closed fields except explicitly redacted configuration sections.
type: object
properties:
platform:
type: string
maxLength: 128
enum:
- windows
- linux
filesystems:
type: array
items:
type: object
properties:
name:
type: string
maxLength: 1024
mount:
type: string
maxLength: 1024
filesystem_type:
type:
- string
- 'null'
maxLength: 1024
used_bytes:
type:
- integer
- 'null'
minimum: 0
total_bytes:
type:
- integer
- 'null'
minimum: 0
available_bytes:
type:
- integer
- 'null'
minimum: 0
used_percent:
type:
- number
- 'null'
minimum: 0
status:
type: string
maxLength: 128
enum:
- available
- unavailable
required:
- name
- mount
- filesystem_type
- used_bytes
- total_bytes
- available_bytes
- used_percent
- status
additionalProperties: false
maxItems: 20000
required:
- platform
- filesystems
additionalProperties: false
@@ -0,0 +1,22 @@
# AIM public operation-data schema. Closed fields except explicitly redacted configuration sections.
type: object
properties:
is_dc: &id001
type: boolean
is_dhcp_server: *id001
is_hyperv_host: *id001
has_veeam_vbr: *id001
has_veeam_vbo: *id001
has_veeam_em: *id001
is_unifi_controller: *id001
is_unifi_os_server: *id001
required:
- is_dc
- is_dhcp_server
- is_hyperv_host
- has_veeam_vbr
- has_veeam_vbo
- has_veeam_em
- is_unifi_controller
- is_unifi_os_server
additionalProperties: false
@@ -0,0 +1,43 @@
# AIM public operation-data schema. Closed fields except explicitly redacted configuration sections.
type: object
properties:
mode:
type: string
maxLength: 128
enum:
- preview
- apply
- check
directory:
type: string
maxLength: 1024
candidates:
type: array
items:
type: string
maxLength: 1024
maxItems: 20000
removed:
type: array
items:
type: string
maxLength: 1024
maxItems: 20000
unifi_configuration:
type: string
maxLength: 128
enum:
- retained
- candidate
- removed
- unchanged
complete:
type: boolean
required:
- mode
- directory
- candidates
- removed
- unifi_configuration
- complete
additionalProperties: false
+190
View File
@@ -0,0 +1,190 @@
# AIM public operation-data schema. Closed fields; no raw package-manager/module output.
type: object
properties:
platform:
type: string
maxLength: 128
enum:
- windows
- debian
- redhat
mode:
type: string
maxLength: 128
enum:
- apply
- check
evidence:
type: string
maxLength: 128
enum:
- package_snapshots
- windows_update_result
- preflight_reboot_state
complete:
type: boolean
updates:
type: array
items:
type: object
properties:
name:
type: string
maxLength: 1024
identifier:
type: [string, 'null']
maxLength: 1024
architecture:
type: [string, 'null']
maxLength: 1024
old_versions:
type: array
items:
type: string
maxLength: 1024
maxItems: 100
new_versions:
type: array
items:
type: string
maxLength: 1024
maxItems: 100
action:
type: string
maxLength: 128
enum: [updated, installed, removed]
kb:
type: array
items:
type: string
maxLength: 1024
maxItems: 100
required: [name, identifier, architecture, old_versions, new_versions, action, kb]
additionalProperties: false
maxItems: 20000
updated_count:
type: integer
minimum: 0
installed_count:
type: integer
minimum: 0
removed_count:
type: integer
minimum: 0
pending:
type: array
items:
type: object
properties:
name:
type: string
maxLength: 1024
identifier:
type: [string, 'null']
maxLength: 1024
kb:
type: array
items:
type: string
maxLength: 1024
maxItems: 100
required: [name, identifier, kb]
additionalProperties: false
maxItems: 20000
failed_updates:
type: array
items:
type: object
properties:
name:
type: string
maxLength: 1024
identifier:
type: [string, 'null']
maxLength: 1024
native_code:
type: [integer, 'null']
minimum: 0
native_code_hex:
type: [string, 'null']
maxLength: 32
reason:
type: string
maxLength: 128
enum: [update_failed, operation_in_progress, install_not_allowed, not_applicable, exclusive_install_conflict, self_update_in_progress, no_connection, timeout]
message:
type: string
maxLength: 512
required: [name, identifier, native_code, native_code_hex, reason, message]
additionalProperties: false
maxItems: 20000
reboot_required:
type: [boolean, 'null']
description: Final observed/predicted pending reboot state after this run.
reboot_required_before:
type: boolean
description: A pending reboot was detected before patching began.
reboot_reasons_before:
type: array
description: Windows only. Native reboot sources reported by ansible.windows.win_reboot_info before patching.
items:
type: object
properties:
source:
type: string
maxLength: 128
description:
type: string
maxLength: 512
required: [source, description]
additionalProperties: false
maxItems: 32
reboot_required_after:
type: [boolean, 'null']
description: Final pending reboot state; false after an AIM-performed successful reboot.
reboot_performed:
type: boolean
reboot_deferred:
type: boolean
description: A reboot remains required because automatic reboot was disabled.
reboot_delay_minutes:
type: integer
minimum: 0
maximum: 1440
blocked_reason:
type: [string, 'null']
maxLength: 128
enum: [preexisting_reboot_required, update_failed, operation_in_progress, install_not_allowed, not_applicable, exclusive_install_conflict, self_update_in_progress, no_connection, timeout, cycle_limit_reached, null]
rescan_after_reboot:
type: boolean
description: Windows only. True permits a newly discovered patch wave after an AIM-performed reboot.
patch_cycles:
type: integer
minimum: 0
maximum: 12
description: Windows discovery/install cycles entered by this run.
continuation_required:
type: boolean
description: Another operator-approved patch run is recommended or required to continue patching.
remaining_updates_known:
type: boolean
description: True only when the report contains an authoritative post-wave discovery in pending.
required:
- platform
- mode
- evidence
- complete
- updates
- updated_count
- installed_count
- removed_count
- pending
- failed_updates
- reboot_required
- reboot_required_before
- reboot_required_after
- reboot_performed
- reboot_deferred
- reboot_delay_minutes
- blocked_reason
additionalProperties: false
@@ -0,0 +1,113 @@
# AIM public operation-data schema. Closed fields except explicitly redacted configuration sections.
type: object
properties:
mode:
type: string
maxLength: 128
enum:
- apply
- check
initially_stopped:
type: array
items:
type: string
maxLength: 1024
maxItems: 20000
eligible:
type: array
items:
type: string
maxLength: 1024
maxItems: 20000
attempted:
type: array
items:
type: string
maxLength: 1024
maxItems: 20000
excluded:
type: array
items:
type: string
maxLength: 1024
maxItems: 20000
newly_running:
type: array
items:
type: string
maxLength: 1024
maxItems: 20000
still_stopped:
type: array
items:
type: string
maxLength: 1024
maxItems: 20000
unobserved:
type: array
items:
type: string
maxLength: 1024
maxItems: 20000
failed_to_start:
type: array
items:
type: object
properties:
name:
type: string
maxLength: 1024
reason:
type: string
maxLength: 128
enum:
- dependency_failed
- permission_denied
- service_disabled
- start_timeout
- service_not_found
- logon_failed
- start_failed
- not_running_after_start
- state_unavailable
message:
type: string
maxLength: 1024
native_code:
type:
- integer
- 'null'
minimum: 0
required:
- name
- reason
- message
- native_code
additionalProperties: false
maxItems: 20000
started_count:
type: integer
minimum: 0
failed_count:
type: integer
minimum: 0
before_count:
type: integer
minimum: 0
after_observed:
type: boolean
required:
- mode
- initially_stopped
- eligible
- attempted
- excluded
- newly_running
- still_stopped
- unobserved
- failed_to_start
- started_count
- failed_count
- before_count
- after_observed
additionalProperties: false
+63
View File
@@ -0,0 +1,63 @@
---
# PURPOSE: Apply bitformer Sophos baseline
# DESCRIPTION: Apply the supplied bitformer firewall baseline. Existing policy values and action order are preserved.
# TARGETS: sophosxgs
# INPUTS (omitted values inherit inventory / role defaults):
# aim_debug [bool]: false
# AUTH: existing inventory / Vault credentials; no embedded passwords.
# CHANGES: Changes firewall management access, objects and rules, including rule removal and a final drop rule. Policy values have NOT been redesigned.
# EXAMPLE: ansible-playbook -i inventories/<customer>/hosts.yml
# playbooks/sophos_apply_baseline.yml --limit <host> --vault-id <customer>@prompt --ask-pass
- name: Grundkonfiguration der Sophos-Firewall nach bitformer Standard
hosts: sophosxgs
gather_facts: false
any_errors_fatal: false
vars:
network_hosts:
- name: bf_spn_network
network: 10.242.176.0
subnetmask: 255.255.255.0
- name: rfc_1918_a
network: 10.0.0.0
subnetmask: 255.0.0.0
- name: rfc_1918_b
network: 172.16.0.0
subnetmask: 255.240.0.0
- name: rfc_1918_c
network: 192.168.0.0
subnetmask: 255.255.0.0
- name: rfc_5735
network: 169.254.0.0
subnetmask: 255.255.0.0
firewall_rules_to_remove:
- '[example] Traffic to Internal Zones'
- '[example] Traffic to WAN'
- '[example] Traffic to DMZ'
wireless_networks_to_remove:
- GuestAP
- Sophos
tasks:
- name: Apply supplied firewall policy
ansible.builtin.import_role:
name: sophos_apply_baseline
tasks_from: main
pre_tasks:
- name: AIM | Validate diagnostics option
ansible.builtin.assert:
that:
- (aim_debug | default(false)) is boolean or (aim_debug | default(false) | string | lower) in ['true',
'false']
fail_msg: aim_debug must be a YAML/JSON boolean.
quiet: true
- name: AIM | Reject mixed platform membership
ansible.builtin.assert:
that:
- (group_names | intersect(['linux', 'windows', 'sophosxgs', 'pfsense']) | length) <= 1
fail_msg: This host belongs to incompatible platform groups. Use globally unique subgroups.
quiet: true
- name: AIM | Execution context
ansible.builtin.debug:
msg:
host: '{{ inventory_hostname }}'
diagnostics: Enabled; secret values are never included by this task.
when: aim_debug | default(false) | bool