From 65478ac9a7d9c85ee831e745b1067eb8643fe919 Mon Sep 17 00:00:00 2001 From: Donien <88634789+Donien@users.noreply.github.com> Date: Tue, 30 Sep 2025 17:37:27 +0200 Subject: [PATCH 01/12] Add Icinga for Windows role 'ifw' --- .../fragments/feature_icinga_for_windows.yml | 5 + doc/role-ifw/README.md | 1 + plugins/modules/ifw_backgrounddaemon.ps1 | 125 +++++++++++ plugins/modules/ifw_backgrounddaemon.py | 104 ++++++++++ plugins/modules/ifw_component.ps1 | 177 ++++++++++++++++ plugins/modules/ifw_component.py | 94 +++++++++ plugins/modules/ifw_restapicommand.ps1 | 123 +++++++++++ plugins/modules/ifw_restapicommand.py | 116 +++++++++++ roles/ifw/README.md | 194 ++++++++++++++++++ roles/ifw/defaults/main.yml | 20 ++ roles/ifw/handlers/main.yml | 1 + roles/ifw/meta/argument_specs.yml | 165 +++++++++++++++ roles/ifw/meta/main.yml | 17 ++ roles/ifw/tasks/configure_icinga2.yml | 132 ++++++++++++ roles/ifw/tasks/install_components.yml | 40 ++++ .../tasks/install_powershell_framework.yml | 73 +++++++ roles/ifw/tasks/main.yml | 32 +++ roles/ifw/tasks/manage_repositories.yml | 43 ++++ .../windows/icinga_install_command.j2 | 103 ++++++++++ roles/ifw/vars/main.yml | 14 ++ 20 files changed, 1579 insertions(+) create mode 100644 changelogs/fragments/feature_icinga_for_windows.yml create mode 120000 doc/role-ifw/README.md create mode 100644 plugins/modules/ifw_backgrounddaemon.ps1 create mode 100644 plugins/modules/ifw_backgrounddaemon.py create mode 100644 plugins/modules/ifw_component.ps1 create mode 100644 plugins/modules/ifw_component.py create mode 100644 plugins/modules/ifw_restapicommand.ps1 create mode 100644 plugins/modules/ifw_restapicommand.py create mode 100644 roles/ifw/README.md create mode 100644 roles/ifw/defaults/main.yml create mode 100644 roles/ifw/handlers/main.yml create mode 100644 roles/ifw/meta/argument_specs.yml create mode 100644 roles/ifw/meta/main.yml create mode 100644 roles/ifw/tasks/configure_icinga2.yml create mode 100644 roles/ifw/tasks/install_components.yml create mode 100644 roles/ifw/tasks/install_powershell_framework.yml create mode 100644 roles/ifw/tasks/main.yml create mode 100644 roles/ifw/tasks/manage_repositories.yml create mode 100644 roles/ifw/templates/windows/icinga_install_command.j2 create mode 100644 roles/ifw/vars/main.yml diff --git a/changelogs/fragments/feature_icinga_for_windows.yml b/changelogs/fragments/feature_icinga_for_windows.yml new file mode 100644 index 00000000..8346a4c2 --- /dev/null +++ b/changelogs/fragments/feature_icinga_for_windows.yml @@ -0,0 +1,5 @@ +major_changes: + - "Introduction of role :code:`ifw` - Icinga for Windows: This role allows to install the Icinga PowerShell Framework, manage components and repositories, and install and configure Icinga 2 through Icinga for Windows." + - "Module :code:`ifw_backgrounddaemon`: Registers/unregisters an Icinga for Windows background daemon." + - "Module :code:`ifw_component`: Installs/removes/updates Icinga for Windows components (e.g. :code:`agent`, :code:`plugins`)." + - "Module :code:`ifw_restapicommand`: Adds/removes commands to/from the whitelist/blacklist of the Icinga for Windows REST-Api." diff --git a/doc/role-ifw/README.md b/doc/role-ifw/README.md new file mode 120000 index 00000000..ff888298 --- /dev/null +++ b/doc/role-ifw/README.md @@ -0,0 +1 @@ +../../roles/ifw/README.md \ No newline at end of file diff --git a/plugins/modules/ifw_backgrounddaemon.ps1 b/plugins/modules/ifw_backgrounddaemon.ps1 new file mode 100644 index 00000000..b90a60c7 --- /dev/null +++ b/plugins/modules/ifw_backgrounddaemon.ps1 @@ -0,0 +1,125 @@ +#!powershell + +#AnsibleRequires -CSharpUtil Ansible.Basic + +### Input parameters +$spec = @{ + options = @{ + state = @{ type = "str"; choices = "absent", "present"; default = "present" } + command = @{ type = "str"; required = $true } + arguments = @{ type = "dict"; required = $false; default = @{} } + } + supports_check_mode = $true +} + + +### Module initilization +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + + +### Make use of input parameters +$Changed = $false +$State = $module.Params.state +$Command = $module.Params.command +$Arguments = $module.Params.arguments + +# Sanetize $Arguments, prepend "-" to get " - " +$TmpArguments = @{} +foreach ($Argument in $Arguments.Keys) { + if (-Not $Argument.StartsWith("-")) { + $TmpArguments."-$($Argument)" = $Arguments.$Argument + } +} +$Arguments = $TmpArguments + + +### Main code +# Check if IfW is installed +if (-Not (Get-Command | Where-Object -Property Name -EQ "Show-IcingaRegisteredBackgroundDaemons")) { + throw "Necessary command 'Show-IcingaRegisteredBackgroundDaemons' was not found. Is IfW installed?" +} + +# Check that $Command is valid and available +if (-Not (Get-Command | Where-Object -Property Name -EQ $Command)) { + throw "Necessary command '$($Command)' was not found." +} + + +# Check if BackgroundDaemon for given command exists +function BackgroundDaemon-Exists () { + param( + [String]$Command + ); + + $Exists = (Show-IcingaRegisteredBackgroundDaemons) -Contains $Command + return $Exists +} + +# Check if given command arguments are equal to existing command arguments +function ArgumentsAreEqual () { + param( + $Arguments, + $ExistingArguments + ); + + $ArgumentsAreEqual = $true + foreach ($Key in $Arguments.keys) { + if ($Arguments.$Key -NE $ExistingArguments.$Key) { + $ArgumentsAreEqual = $false + break + } + } + return $ArgumentsAreEqual +} + +# Get existing arguments for given command +function Get-ExistingArguments () { + param( + [String]$Command + ); + + $Arguments = (Read-IcingaPowerShellConfig).BackgroundDaemon.EnabledDaemons."$($Command)".Arguments + + return $Arguments +} + + + +$CommandIsRegistered = BackgroundDaemon-Exists -Command $Command +$ExistingArguments = Get-ExistingArguments -Command $Command +$ArgumentsAreEqual = ArgumentsAreEqual -Arguments $Arguments -ExistingArguments $ExistingArguments + + +# Update if needed +if ($State -EQ "absent" -And $CommandIsRegistered) { + if (-Not $module.CheckMode) { + Unregister-IcingaBackgroundDaemon ` + -BackgroundDaemon $Command | Out-Null + } + $Changed = $true + +} elseif ($State -EQ "present" -And (-Not $CommandIsRegistered -Or -Not $ArgumentsAreEqual)) { + if (-Not $module.CheckMode) { + Register-IcingaBackgroundDaemon ` + -Command $Command ` + -Arguments $Arguments | Out-Null + } + $Changed = $true +} + +$Before = @{ + command = (&{ if ($CommandIsRegistered) { $Command } else { $null } } ) + arguments = (&{ if ($CommandIsRegistered) { $ExistingArguments } else { $null } } ) +} +$After = @{ + command = (&{ if ($State -EQ "present") { $Command } else { $null } } ) + arguments = (&{ if ($State -EQ "present") { $Arguments } else { $null } } ) +} + + + +### Module return +$module.Result.before = $Before +$module.Result.after = $After +$module.Result.changed = $Changed +$module.ExitJson() diff --git a/plugins/modules/ifw_backgrounddaemon.py b/plugins/modules/ifw_backgrounddaemon.py new file mode 100644 index 00000000..b81446e6 --- /dev/null +++ b/plugins/modules/ifw_backgrounddaemon.py @@ -0,0 +1,104 @@ +DOCUMENTATION = ''' +--- +name: ifw_backgrounddaemon +short_description: (Un-)Registers an IfW Background Daemon. +description: + - This module allows you to register/unregister an Icinga for Windows Background Daemon. + - They are used to collect metrics over time or used for the IfW API Check Forwarder. +version_added: 0.1.0 +author: + - Matthias Döhler +seealso: + - name: Icinga for Windows Background Daemons + description: Reference for the Background Daemons. + link: https://icinga.com/docs/icinga-for-windows/latest/doc/110-Installation/05-Background-Daemons/ + - name: Icinga for Windows API Check Forwareder + description: Reference for a possible use case regarding the API Check Forwarder. + link: https://icinga.com/docs/icinga-for-windows/latest/doc/110-Installation/30-API-Check-Forwarder/ +options: + state: + description: + - The state of the Background Daemon. + required: false + default: present + choices: [ "present", "absent" ] + type: str + command: + description: + - The name of a valid command available in the used PowerShell. + - This could be something like C(Start-MyCustomDaemon). + - If O(state=absent), only the O(command) is used to determine which Background Daemon should be removed. + required: true + type: str + arguments: + description: + - Arguments to be passed to O(command). + - Must be key value pairs. + - The leading C(-) is prepended in front of the argument name/key (C(key) becomes C(-key)). + required: false + type: dict +''' + +EXAMPLES = r''' +# The PowerShell equivalent is: +# Register-IcingaBackgroundDaemon ` +# -Command 'Start-MyCustomDaemon' ` +# -Arguments @{ +# '-MySwitchParameter' = $True; +# '-MyIntegerParameter' = 42; +# '-MyStringParameter' = 'Example'; +# }; +- name: Register a Background Daemon for a specific command passing argument flags with values to that command + netways.icinga.ifw_backgrounddaemon: + state: present + command: "Start-MyCustomDaemon" + arguments: + MySwitchParameter: true + MyIntegerParameter: 42 + MyStringParameter: "Example" + +- name: Register the Icinga for Windows RESTApi as a Background Daemon to use API Check Forwarder + netways.icinga.ifw_backgrounddaemon: + state: present + command: "Start-IcingaWindowsRESTApi" +''' + +RETURN = r''' +before: + description: + - Shows information about the previously (un-)registered command and its arguments. + - If no change occurs, will be the same as RV(after). + returned: success + type: dict + contains: + command: + description: The name of the previously (un-)registered command. + returned: success + type: str + sample: + arguments: + description: The arguments used previously for the specified command. + returned: success + type: dict + sample: +after: + description: + - Shows information about the newly (un-)registered command and its arguments. + - If no change occurs, will be the same as RV(before). + returned: success + type: dict + contains: + command: + description: The name of the newly (un-)registered command. + returned: success + type: str + sample: Start-MyCustomDaemon + arguments: + description: The arguments now used for the specified command. + returned: success + type: dict + sample: + -MyIntegerParameter: 42 + -MyStringParameter: "Example" + -MySwitchParameter: true +''' diff --git a/plugins/modules/ifw_component.ps1 b/plugins/modules/ifw_component.ps1 new file mode 100644 index 00000000..b3ceada5 --- /dev/null +++ b/plugins/modules/ifw_component.ps1 @@ -0,0 +1,177 @@ +#!powershell + +#AnsibleRequires -CSharpUtil Ansible.Basic + +### Input parameters +$spec = @{ + options = @{ + state = @{ type = "str"; choices = "absent", "present", "latest"; default = "present" } + name = @{ type = "list"; elements = "str"; required = $true; aliases = "component" } + } + supports_check_mode = $true +} + + +### Module initilization +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + + +### Make use of input parameters +$Changed = $false +$State = $module.Params.state +$Components = $module.Params.name + + +### Main code +# Check if IfW is installed +if (-Not (Get-Command | Where-Object -Property Name -EQ "Install-IcingaComponent")) { + throw "Necessary command 'Install-IcingaComponent' was not found. Is IfW installed?" +} + +# Get list of installed components +function Get-InstalledComponentList () { + param( + ); + + $ComponentList = Get-IcingaInstallation + return $ComponentList +} + +# Get list of available components +function Get-AvailableComponentList () { + param( + ); + + $AvailableComponents = (Get-IcingaComponentList).Components + return $AvailableComponents +} + + +function Install-Component () { + param( + [String]$Component, + [String]$Version, + [Switch]$CheckMode + ); + + if ($CheckMode) { + return + } + + Install-IcingaComponent ` + -Name $Component ` + -Version $Version ` + -Confirm ` + -Force ` + | Out-Null +} + + +function Remove-Component () { + param( + [String]$Component, + [Switch]$CheckMode + ); + + if ($Component -EQ "framework") { + throw "Refusing to remove component '$($Component)'!" + } + + if ($CheckMode) { + return + } + + Uninstall-IcingaComponent ` + -Name $Component ` + -Confirm ` + | Out-Null +} + + + +$Added = @( +) +$Removed = @( +) + +$InstalledComponents = Get-InstalledComponentList +$AvailableComponents = Get-AvailableComponentList + +foreach ($Component in $Components) { + $NeedsInstallation = $false + + # Set version to specified version or to latest if state == latest and no version is specified + $Name, $Version = $Component.ToLower() -Split ":" + + $CurrentVersion = $InstalledComponents.$Name.CurrentVersion + $LatestVersion = $AvailableComponents.$Name + + # Allow shorthand ":" to mean ":latest" + if ($Version -EQ "latest" -Or $Version -EQ "") { + $Version = $LatestVersion + } + + switch ($State) { + "present" { + # If component is not installed or installed in wrong version + if ($InstalledComponents.Keys -NotContains $Name) { + if ($Version -EQ $null) { + $Version = $LatestVersion + } + $NeedsInstallation = $true + + } elseif ($Version -NE $null -And $Version -NE $CurrentVersion) { + $NeedsInstallation = $true + } + + } + "latest" { + # Mark any component with lower current version than 'latest' for install, unless component has a version specified + # If a version is specified, check if component is already installed in that version + if ($Version -EQ $null) { + $Version = $LatestVersion + + } + if ($Version -NE $LatestVersion) { + if ($Version -NE $CurrentVersion) { + $NeedsInstallation = $true + } + } elseif ($CurrentVersion -NE $LatestVersion) { + $Version = $LatestVersion + $NeedsInstallation = $true + } + } + "absent" { + if ($InstalledComponents.Keys -Contains $Name) { + Remove-Component ` + -Component $Name ` + -CheckMode:$CheckMode + $Removed += @{ + name = $Name + version = $CurrentVersion + } + $Changed = $true + } + } + } + # For states "present" and "latest" + if ($NeedsInstallation) { + Install-Component ` + -Component $Name ` + -Version $Version ` + -CheckMode:$CheckMode + $Added += @{ + name = $Name + version = $Version + } + $Changed = $true + } +} + + + +### Module return +$module.Result.added = $Added +$module.Result.removed = $Removed +$module.Result.changed = $Changed +$module.ExitJson() diff --git a/plugins/modules/ifw_component.py b/plugins/modules/ifw_component.py new file mode 100644 index 00000000..edf0c9c0 --- /dev/null +++ b/plugins/modules/ifw_component.py @@ -0,0 +1,94 @@ +DOCUMENTATION = ''' +--- +name: ifw_component +short_description: (Un-)Installs an IfW Component. +description: + - This module allows you to install/uninstall an Icinga for Windows Component. +version_added: 0.1.0 +author: + - Matthias Döhler +seealso: + - name: Icinga for Windows Components + description: Reference for the Components. + link: https://icinga.com/docs/icinga-for-windows/latest/doc/120-Repository-Manager/05-Install-Components/ +options: + state: + description: + - The state of the Component. + - Decides whether the Component will be installed (or its version changed) or removed. + required: false + default: present + choices: [ "present", "latest", "absent" ] + type: str + name: + description: + - A list of Component names that should be installed / removed. + - If O(state=present), a Component will be installed if not already present. + - If O(state=absent), a Component will be removed if present. + - If O(state=latest), a Component will be installed if not already present with the latest version. + - Each list entry can use the syntax of C(:) to install Component C() in version C(). C(:latest) will evaluate to the appropriate version for the given Component. C(:) is the same as C(:latest). + - If O(state=present) or O(state=latest), the above syntax takes precedence. This means that specifying a version should always result in that specific version of the component being installed. Has no impact if O(state=absent). + required: true + type: list + elements: str + aliases: + - "component" +''' + +EXAMPLES = r''' +- name: Install the Component 'plugins' in version '1.11.1' + netways.icinga.ifw_component: + state: present + name: "plugins:1.11.1" + +- name: Install multiple Components while keeping installed Components in their current version + netways.icinga.ifw_component: + state: present + name: + - "plugins" + - "agent" + - "service" + - "mssql" + +- name: Install the latest version of Components 'plugins' and 'mssql' while keeping 'agent' in version '2.14.1' + netways.icinga.ifw_component: + state: latest + name: + - "plugins" + - "mssql" + - "agent:2.14.1" + +- name: Remove Components 'mssql' and 'hyperv' + netways.icinga.ifw_component: + state: absent + name: + - "mssql" + - "hyperv" +''' + +RETURN = r''' +added: + description: + - Shows information about newly added or changed Components and the versions they now have. + returned: success + type: list + elements: dict + sample: + - name: "plugins" + version: "1.12.0" + - name: "mssql" + version: "1.5.0" + - name: "agent" + version: "2.14.1" +removed: + description: + - Shows information about removed Components and the versions they were installed with. + returned: success + type: list + elements: dict + sample: + - name: "hyperv" + version: "1.3.0" + - name: "mssql" + version: "1.5.0" +''' diff --git a/plugins/modules/ifw_restapicommand.ps1 b/plugins/modules/ifw_restapicommand.ps1 new file mode 100644 index 00000000..bb7cbc77 --- /dev/null +++ b/plugins/modules/ifw_restapicommand.ps1 @@ -0,0 +1,123 @@ +#!powershell + +#AnsibleRequires -CSharpUtil Ansible.Basic + +### Input parameters +$spec = @{ + options = @{ + state = @{ type = "str"; choices = "absent", "present"; default = "present" } + command = @{ type = "list"; elements = "str"; required = $true } + list = @{ type = "str"; choices = "whitelist", "blacklist"; default = "whitelist" } + endpoint = @{ type = "str"; choices = "apichecks", "checker"; default = "apichecks" } + purge = @{ type = "bool"; default = $false } + } + supports_check_mode = $true +} + + +### Module initilization +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + + +### Make use of input parameters +$Changed = $false +$State = $module.Params.state +$Commands = $module.Params.command +$List = $module.Params.list +$Endpoint = $module.Params.endpoint +$Purge = $module.Params.purge + +$Blacklist = $false +if ($List -EQ "blacklist") { + $Blacklist = $true +} + + +# Check if RESTApiCommand exists in given endpoint and list +function RESTApiCommand-Exists () { + param( + [String]$Command, + [String]$Endpoint, + [String]$List + ); + + $Exists = (Get-IcingaPowerShellConfig -Path "RESTApi.Commands.$($Endpoint).$($List)") -Contains $Command + return $Exists +} + +function Get-ExistingRESTApiCommand () { + param( + [String]$Endpoint, + [String]$List + ); + + $ExistingCommands = Get-IcingaPowerShellConfig -Path "RESTApi.Commands.$($Endpoint).$($List)" + return $ExistingCommands +} + + +### Main code +# Check if IfW is installed +if (-Not (Get-Command | Where-Object -Property Name -EQ "Add-IcingaRESTApiCommand")) { + throw "Necessary command 'Add-IcingaRESTApiCommand' was not found. Is IfW installed?" +} + +$Added = @() +$Removed = @() + +$ExistingCommands = Get-ExistingRESTApiCommand -Endpoint $Endpoint -List $List + +# Purge every command in endpoint and list +# Simply replace with given list of commands +if ($Purge) { + if ($State -EQ "absent") { + $Removed += $ExistingCommands + } else { + $Removed += ($ExistingCommands | Where-Object { $_ -Notin $Commands }) + $Added += ($Commands | Where-Object { $_ -Notin $ExistingCommands }) + } + if (-Not $module.CheckMode) { + if ($State -EQ "absent") { + Set-IcingaPowerShellConfig -Path "RESTApi.Commands.$($Endpoint).$($List)" -Value @() + } else { + Set-IcingaPowerShellConfig -Path "RESTApi.Commands.$($Endpoint).$($List)" -Value $Commands + } + } + if ($Removed -Or $Added) { + $Changed = $true + } +} else { + foreach ($Command in $Commands) { + $CommandExists = ($ExistingCommands -Contains $Command) + + # Update if needed + if ($State -EQ "absent" -And $CommandExists) { + if (-Not $module.CheckMode) { + Remove-IcingaRESTApiCommand ` + -Command $Command ` + -Endpoint $Endpoint ` + -Blacklist:$Blacklist + } + $Removed += $Command + $Changed = $true + } elseif ($State -EQ "present" -And (-Not $CommandExists)) { + if (-Not $module.CheckMode) { + Add-IcingaRESTApiCommand ` + -Command $Command ` + -Endpoint $Endpoint ` + -Blacklist:$Blacklist + } + $Added += $Command + $Changed = $true + } + } +} + + +### Module return +$module.Result.added = $Added +$module.Result.removed = $Removed +$module.Result.list = $List +$module.Result.endpoint = $Endpoint +$module.Result.changed = $Changed +$module.ExitJson() diff --git a/plugins/modules/ifw_restapicommand.py b/plugins/modules/ifw_restapicommand.py new file mode 100644 index 00000000..54fb8761 --- /dev/null +++ b/plugins/modules/ifw_restapicommand.py @@ -0,0 +1,116 @@ +DOCUMENTATION = ''' +--- +name: ifw_restapicommand +short_description: Adds / Removes Icinga REST-Api Commands. +description: + - This module allows you to add / remove Icinga for Windows REST-Api Commands. + - You can also add Commands to the Blacklist. +version_added: 0.1.0 +author: + - Matthias Döhler +seealso: + - name: Icinga REST-Api Whitelists and Blacklists + description: Reference for how to use Whitelists and Blacklists for the Icinga REST-Api. + link: https://icinga.com/docs/icinga-for-windows/latest/doc/110-Installation/30-API-Check-Forwarder/#whitelist-check-commands +options: + state: + description: + - The state of the REST-Api Command. + required: false + default: present + choices: [ "present", "absent" ] + type: str + command: + description: + - The name of a valid command available in the used PowerShell. + - This could be something like C(Invoke-IcingaCheckCPU). + - The use of C(*) as a wildcard is allowed. + - O(command) is added to or removed from the chosen O(list) within the chosen O(endpoint). + required: true + type: list + elements: str + list: + description: + - The list the command should be added to / removed from. + required: false + default: whitelist + choices: [ "whitelist", "blacklist" ] + type: str + endpoint: + description: + - The endpoint the command should be added to / removed from. + required: false + default: apichecks + choices: [ "apichecks", "checker" ] + type: str + purge: + description: + - If O(purge=true), removes any command not specified in O(command) from O(list) within O(endpoint). If also setting O(state=absent), nothing will remain. + - If O(purge=false), commands in O(command) are added or removed if necessary. Existing commands stay untouched. + required: false + default: false + type: bool +''' + +EXAMPLES = r''' +- name: Allow all "Invoke-IcingaCheck" commands via wildcard + netways.icinga.ifw_restapicommand: + state: present + list: whitelist + endpoint: apichecks + command: "Invoke-IcingaCheck*" + +- name: Prohibit the use of "Invoke-IcingaCheckCPU" specifically + netways.icinga.ifw_restapicommand: + state: present + list: blacklist + command: "Invoke-IcingaCheckCPU" + +- name: Remove all entries from the blacklist + netways.icinga.ifw_restapicommand: + state: absent + list: blacklist + purge: true + command: "Invoke-IcingaCheckCPU" # Some valid command needed + +- name: Allow only two specific commands + netways.icinga.ifw_restapicommand: + state: present + purge: true + command: + - "Invoke-IcingaCheckCPU" + - "Invoke-IcingaCheckDiskHealth" +''' + +RETURN = r''' +added: + description: + - Shows information about newly added commands. + - They were added to RV(list) within RV(endpoint). + returned: success + type: list + elements: str + sample: + - Invoke-IcingaCheckCPU +removed: + description: + - Shows information about now removed commands. + - They were removed from RV(list) within RV(endpoint). + returned: success + type: list + elements: str + sample: + - Invoke-IcingaCheckDiskHealth +list: + description: + - The used list. + returned: success + type: str + sample: whitelist +endpoint: + description: + - The used endpoint. + returned: success + type: str + sample: apichecks +''' diff --git a/roles/ifw/README.md b/roles/ifw/README.md new file mode 100644 index 00000000..6b416b8f --- /dev/null +++ b/roles/ifw/README.md @@ -0,0 +1,194 @@ +# Role netways.icinga.ifw + +This role manages the installation/removal of [Icinga for Windows](https://icinga.com/docs/icinga-for-windows/latest/doc/000-Introduction/) components. +It first installs the [Icinga PowerShell Framework](https://github.com/Icinga/icinga-powershell-framework) in case it is not present yet and then goes on to use the framework's commands to manage other components. + +Tasks it can do: + +* Install the Icinga PowerShell Framework +* Manage repositories (no locally synced repositories yet) +* Configure the Icinga 2 Agent +* Create a valid Icinga 2 certificate + +Table of contents: + +* [Variables](#variables) + * [Getting a Certificate](#getting-a-certificate) +* [Example Playbooks](#example-playbooks) + * [Install Basic Components](#install-basic-components) + * [Install Other Plugins](#install-other-plugins) + * [Add Custom Repositories](#add-custom-repositories) + * [Icinga 2 Setup](#icinga-2-setup) + +## Variables + +- `ifw_framework_url: string` + The URL to the different verions of the Icinga PowerShell Framework. + Default: `https://packages.icinga.com/IcingaForWindows/stable/framework/` + +- `ifw_framework_version: string` + The version of the Icinga PowerShell Framework to install. You can set a specific version here like `1.11.1`. + This is only used for the initial installation of the framework. Updates can be done using `ifw_components`. + Default: `latest` + +- `ifw_framework_path: string` + The path where the Icinga PowerShell Framework should be installed. If unspecified, the role will try using the best available path. + +- `ifw_repositories: list of dictionaries` + Here you can specify additional repositories from which to pull components from. The default Icinga For Windows repository will always be added. + Default: `none` + Example: + ``` + ifw_repositories: + - name: "Custom" + remote_path: "https://example.com/IcingaForWindows/stable/ifw.repo.json" + ``` + +- `ifw_components: list of dictionaries` + Specify which components should be present. Optionally specify which version of the component should be installed. + Components installed but not present within this list will be removed. + Default: `[ { name: "plugins", version: "latest" }, { name: "agent", version: "latest" } ]` + +- `ifw_icinga2_ca_host: string` + The Ansible inventory hostname of your Icinga 2 CA host (master). + This variable is used to sign the certificate for your Windows host using delegated tasks. + If `ifw_icinga2_ticket` is set, the ticket will take precedence. + Default: `none` + +- `ifw_icinga2_ticket: string` + The ticket obtained from your Icinga 2 CA host (master) using `icinga2 pki ticket --cn ""`. + It is used to receive a valid certificate. + Default: `none` + +- `ifw_connection_direction: string` + This variable decides whether your host should connect to its parent(s), its parent(s) to the host, or if connection should be established from both sides. + This influences the firewall rules that are automatically deployed. + In a well structured Icinga environment the master(s) (or satellite(s)) should not connect to the agent, but the agent should connect "up" to its parent(s). + Default: `fromagent` + +- `ifw_force_newcert: boolean` + If set to true, this forces the generation of a new certificate. + Default: `false` + +- `ifw_icinga2_cn: string` + This variable is used to define what common name your host takes within the Icinga hierarchy. Certificates will be generated using that name. + Default: `"{{ inventory_hostname }}"` + +- `ifw_icinga2_port: int` + This number is used for the local port on which your host should listen. + Default: `5665` + +- `ifw_icinga2_global_zones: list of strings` + Here you can specify all global zones your host should be aware of. + Default: `[]` + +- `ifw_icinga2_parents: list of dictionaries` + Here you can specify the parent endpoint(s) of your host's parent zone (O(ifw_icinga2_parent_zone)). + You can specify each parent's `cn`, its `host` attribute and the `port` on which it listens. The `cn` attribute is **required**. + Default: `none` + Example: + ``` + ifw_icinga2_parents: + - cn: parent1 + host: icinga01.example.com + port: 5665 + - cn: parent2 + host: 10.0.0.20 + port: 6556 + ``` + +- `ifw_icinga2_parent_zone: string` + The name of your parent(s) zone. + Default: `none` + +### Getting a Certificate + +If neither `ifw_icinga2_ca_host` nor `ifw_icinga2_ticket` is specified, your target host will connect to the first parent in `ifw_icinga2_parents` and file a CSR. This needs to be signed manually afterwards. + +If `ifw_icinga2_ca_host` is specified, a CSR is created locally and then moved to `ifw_icinga2_ca_host` where it is signed. The resulting certificate is then moved back to your target host. + +If `ifw_icinga2_ticket` is specified, `ifw_icinga2_ca_host` is ignored and the certificate should get signed automatically. `ifw_icinga2_ticket` needs to be acquired beforehand. + +## Example Playbooks + +The examples below showcase different aspects of the Icinga for Windows configuration. In a real scenario the variables from the examples should be used in conjunction. + +### Install Basic Components + +This installs just the Icinga PowerShell Framework, the Agent component and the Icinga PowerShell Plugins. + +``` +- name: Run ifw role + hosts: all + + roles: + - netways.icinga.ifw +``` + +### Install Other Plugins + +This installs the Agent component, the basic plugins and in addition to that the plugins for MSSQL and HyperV. +It also pins the Agent component to a specific version. + +``` +- name: Run ifw role + hosts: all + + vars: + ifw_components: + - name: "agent" + version: "2.14.0" + - name: "plugins" + - name: "mssql" + - name: "hyperv" + + roles: + - netways.icinga.ifw +``` + +### Add Custom Repositories + +This adds more repositories to Icinga for Windows in addition to the default one. +This is useful if you want to host a local mirror. + +``` +- name: Run ifw role + hosts: all + + vars: + ifw_repositories: + - name: "Ansible Managed" + remote_path: "https://example.com/IcingaForWindows/stable/ifw.repo.json" + + roles: + - netways.icinga.ifw +``` + +### Icinga 2 Setup + +This installs the Agent component and sets it up to communicate with both its parents. +It adds the global zone `windows-agents`. + +``` +- name: Run ifw role + hosts: all + + vars: + ifw_icinga2_ca_host: icinga01.example.com + + ifw_icinga2_global_zones: + - "windows-agents" + + ifw_icinga2_parent_zone: main + + ifw_icinga2_parents: + - cn: main01 + host: icinga01.example.com + port: 5665 + - cn: main02 + host: 10.0.0.20 + port: 5665 + + roles: + - netways.icinga.ifw +``` diff --git a/roles/ifw/defaults/main.yml b/roles/ifw/defaults/main.yml new file mode 100644 index 00000000..bcbc8a1c --- /dev/null +++ b/roles/ifw/defaults/main.yml @@ -0,0 +1,20 @@ +--- + +ifw_framework_url: "https://packages.icinga.com/IcingaForWindows/stable/framework/" +ifw_framework_version: "latest" +ifw_framework_path: + +ifw_repositories: [] + +ifw_components: + - name: "plugins" + version: "latest" + - name: "agent" + version: "latest" + +ifw_icinga2_ca_host: +ifw_connection_direction: "fromagent" +ifw_force_newcert: false +ifw_icinga2_cn: "{{ inventory_hostname }}" +ifw_icinga2_port: 5665 +ifw_icinga2_global_zones: [] diff --git a/roles/ifw/handlers/main.yml b/roles/ifw/handlers/main.yml new file mode 100644 index 00000000..ed97d539 --- /dev/null +++ b/roles/ifw/handlers/main.yml @@ -0,0 +1 @@ +--- diff --git a/roles/ifw/meta/argument_specs.yml b/roles/ifw/meta/argument_specs.yml new file mode 100644 index 00000000..80d81ee1 --- /dev/null +++ b/roles/ifw/meta/argument_specs.yml @@ -0,0 +1,165 @@ +--- + +argument_specs: + main: + short_description: Role to manage Icinga for Windows + description: + - Role to install, configure and manage L(Icinga for Windows, https://icinga.com/docs/icinga-for-windows/latest/doc/000-Introduction/). + - The role installs the Icinga PowerShell Framework and can manage components. + author: + - Matthias Döhler + options: + ifw_framework_url: + description: + - The URL to the different verions of the Icinga PowerShell Framework. + - This is where the zip files for the different versions are located, e.g. C(icinga-powershell-framework-1.13.3.zip) and C(icinga-powershell-framework-1.13.2.zip). + - HORIZONTALLINE + - The Icinga PowerShell Framework will be downloaded to your Ansible control node and copied over to the target machine. + type: str + required: false + default: "https://packages.icinga.com/IcingaForWindows/stable/framework/" + ifw_framework_version: + description: + - The version of the Icinga PowerShell Framework to install. You can specify a specific version here like C(1.11.1). + - This is only used for the initial installation of the framework. Updates can be done using O(ifw_components). + type: str + required: false + default: "latest" + ifw_framework_path: + description: + - The path where the Icinga PowerShell Framework should be installed. + - If unspecified, the role will try using the best available path. + type: str + required: false + ifw_repositories: + description: + - Here you can specify additional repositories from which to pull components from. + - The default Icinga For Windows repository will always be added. + type: list + elements: dict + required: false + default: [] + options: + name: + description: + - The name of the repository. + type: str + required: true + remote_path: + description: + - The remote path to the repository. This has to end in C(ifw.repo.json). + - "Example: C(remote_path: https://packages.icinga.com/IcingaForWindows/stable/ifw.repo.json)" + type: str + required: true + ifw_components: + description: + - Specify which components should be present. Optionally specify which version of the component should be installed. + - Components installed but not present within this list will be removed. + - &icinga2_requirements + Icinga 2 is only configured if + O(ifw_components) is set to install the component C(agent), + O(ifw_icinga2_parents) is set, and + O(ifw_icinga2_parent_zone) is set. + type: list + elements: dict + required: false + default: + - name: "plugins" + version: "latest" + - name: "agent" + version: "latest" + options: + state: + description: + - The desired state of the component. + type: str + required: false + default: present + choices: [ present, absent, latest ] + name: + description: + - The name of the component to be installed / removed. + - On the Windows host, a list of available components can be shown using C(Get-IcingaComponentList | Select-Object -Property Components | ConvertTo-Json). + type: str + required: true + version: + description: + - The version of the component to be installed / removed. + type: str + required: false + ifw_icinga2_ca_host: + description: + - The Ansible C(inventory_hostname) of your Icinga 2 CA host (master). + - This variable is used to sign the certificate for your Windows host using delegated tasks. + - If O(ifw_icinga2_ticket) is set, the ticket will take precedence. + type: str + required: false + ifw_icinga2_ticket: + description: + - The ticket obtained from your Icinga 2 CA host (master) using C(icinga2 pki ticket --cn ""). + - It is used to receive a valid certificate. + type: str + required: false + ifw_connection_direction: + description: + - This variable decides whether your host should connect to its parent(s), its parent(s) to the host, or if connection should be established from both sides. + - This influences the firewall rules that are automatically deployed. + - In a well structured Icinga environment the master(s) (or satellite(s)) should not connect to the agent, but the agent should connect "up" to its parent(s). + type: str + required: false + default: fromagent + choices: [ fromagent, fromparent, both ] + ifw_force_newcert: + description: + - If O(ifw_force_newcert=true), this forces the generation of a new certificate. + type: bool + required: false + default: false + ifw_icinga2_cn: + description: + - This variable is used to define what common name your host takes within the Icinga hierarchy. Certificates will be generated using that name. + type: str + required: false + default: "{{ inventory_hostname }}" + ifw_icinga2_port: + description: + - This number is used for the local port on which your host should listen for Icinga 2 cluster communication. + type: int + required: false + default: 5665 + ifw_icinga2_global_zones: + description: + - Here you can specify all global zones your host should be aware of. + type: list + elements: str + required: false + default: [] + ifw_icinga2_parents: + description: + - Here you can specify the parent endpoint(s) of your host's parent zone (O(ifw_icinga2_parent_zone)). + - You can specify each parent's C(cn), its C(host) attribute and the C(port) on which it listens. + - *icinga2_requirements + type: list + elements: dict + required: false + options: + cn: + description: + - The CN of the given host. This is the C(name) of the Icinga 2 Endpoint object. + type: str + required: true + host: + description: + - The hostname or address of the given host. This is the C(host) attribute of the Icinga 2 Endpoint object. + type: str + required: false + port: + description: + - The port of the given host. This is the C(port) attribute of the Icinga 2 Endpoint object. + type: str + required: false + ifw_icinga2_parent_zone: + description: + - The name of the Icinga 2 parent(s) zone. + - *icinga2_requirements + type: list diff --git a/roles/ifw/meta/main.yml b/roles/ifw/meta/main.yml new file mode 100644 index 00000000..34fb8eb7 --- /dev/null +++ b/roles/ifw/meta/main.yml @@ -0,0 +1,17 @@ +galaxy_info: + namespace: netways + role_name: ifw + author: | + - Matthias Döhler + description: Role to install the Icinga PowerShell Framework and manage its components + license: Apache-2.0 + min_ansible_version: "2.9" + platforms: + - name: Windows + galaxy_tags: + - icinga + - monitoring + - icingaforwindows + - ifw + - windows +dependencies: [] diff --git a/roles/ifw/tasks/configure_icinga2.yml b/roles/ifw/tasks/configure_icinga2.yml new file mode 100644 index 00000000..e936201d --- /dev/null +++ b/roles/ifw/tasks/configure_icinga2.yml @@ -0,0 +1,132 @@ +--- + +- name: Get path to Icinga binary + changed_when: false + ansible.windows.win_shell: Get-IcingaAgentBinary + register: _icinga_binary_cmd + +- name: Get Icinga PowerShell Framework path + changed_when: false + ansible.windows.win_shell: Get-IcingaFrameworkRootPath + register: _framework_path + +- name: Get path to Icinga configuration directory + changed_when: false + ansible.windows.win_shell: Get-IcingaAgentConfigDirectory + register: _icinga_config_dir_cmd + +- name: CA certificate + when: not ifw_icinga2_ca_host is none + block: + - name: Get CA certificate + become: true + run_once: true + delegate_to: "{{ ifw_icinga2_ca_host | default('localhost', true) }}" + ansible.builtin.slurp: + src: "/var/lib/icinga2/certs/ca.crt" + register: _icinga2_ca_cert + + - name: Deploy CA certificate + ansible.windows.win_copy: + dest: "{{ _icinga_config_dir_cmd.stdout | trim }}../../var/lib/icinga2/certs/ca.crt" + content: "{{ _icinga2_ca_cert.content | b64decode }}" + +- name: Create private key and signing request + ansible.windows.win_shell: | + & '{{ (_icinga_binary_cmd.stdout | trim) }}' pki new-cert \ + --cn "{{ ifw_icinga2_cn }}" \ + --key "{{ _icinga_config_dir_cmd.stdout | trim }}../../var/lib/icinga2/certs/{{ ifw_icinga2_cn }}.key" \ + --cert "{{ _icinga_config_dir_cmd.stdout | trim }}../../var/lib/icinga2/certs/{{ ifw_icinga2_cn }}.crt" \ + --csr "{{ _icinga_config_dir_cmd.stdout | trim }}../../var/lib/icinga2/certs/{{ ifw_icinga2_cn }}.csr" + args: + creates: "{{ _icinga_config_dir_cmd.stdout | trim }}../../var/lib/icinga2/certs/{{ ifw_icinga2_cn }}.key" + register: _created_private_key + +- name: Handle certificate + when: + - not ifw_icinga2_ca_host is none + - (_created_private_key.skipped is not defined) or (ifw_force_newcert) + block: + - name: Get content of CSR + ansible.builtin.slurp: + src: "{{ _icinga_config_dir_cmd.stdout | trim }}../../var/lib/icinga2/certs/{{ ifw_icinga2_cn }}.csr" + register: _ifw_csr + + - name: Copy CSR to CA + delegate_to: "{{ ifw_icinga2_ca_host | default('localhost', true) }}" + ansible.builtin.copy: + dest: "/tmp/{{ ifw_icinga2_cn }}.csr" + content: "{{ _ifw_csr.content | b64decode }}" + mode: "0664" + + - name: Sign CSR + become: true + delegate_to: "{{ ifw_icinga2_ca_host | default('localhost', true) }}" + changed_when: false + ansible.builtin.shell: | + icinga2 pki sign-csr \ + --csr /tmp/{{ ifw_icinga2_cn }}.csr \ + --cert /tmp/{{ ifw_icinga2_cn }}.crt + + - name: Get host certificate + become: true + delegate_to: "{{ ifw_icinga2_ca_host | default('localhost', true) }}" + ansible.builtin.slurp: + src: "/tmp/{{ ifw_icinga2_cn }}.crt" + register: _icinga2_host_cert + + - name: Deploy host certificate + when: not ifw_icinga2_ca_host is none + ansible.windows.win_copy: + dest: "{{ _icinga_config_dir_cmd.stdout | trim }}../../var/lib/icinga2/certs/{{ ifw_icinga2_cn }}.crt" + content: "{{ _icinga2_host_cert.content | b64decode }}" + +- name: Get current configuration + ansible.builtin.slurp: + src: "{{ _framework_path.stdout | trim }}/config/config.json" + register: _current_icinga_configuration + +- name: Create Icinga install command + vars: + ifw_icinga2_agent_version: "{{ (ifw_components | selectattr('name', 'eq', 'agent')).version | default('latest') }}" + ansible.builtin.set_fact: + _install_command: "{{ lookup('template', role_path + '/templates/windows/icinga_install_command.j2') }}" + +- name: Set facts for comparison + when: + - (_current_icinga_configuration.content | b64decode | from_json).Framework.Config is defined + - (_current_icinga_configuration.content | b64decode | from_json).Framework.Config.Live is defined + vars: + _framework_config_live: "{{ (_current_icinga_configuration.content | b64decode | from_json).Framework.Config.Live }}" + ansible.builtin.set_fact: + _current_ca_server: "{{ _framework_config_live['IfW-CAServer']['Values'][0] | default(none) }}" + _current_global_zones: "{{ _framework_config_live['IfW-CustomZones']['Values'] }}" + _current_port: "{{ _framework_config_live['IfW-Port']['Values'][0] | default(none) }}" + _current_parent_zone: "{{ _framework_config_live['IfW-ParentZone']['Values'][0] | default(none) }}" + _current_parents: "{{ _framework_config_live['IfW-ParentNodes']['Values'] }}" + _current_parents0: "{{ _framework_config_live['IfW-ParentAddress:' + ifw_icinga2_parents[0].cn]['Values'][0] | default(none) }}" + _current_parents1: "{{ (_framework_config_live['IfW-ParentAddress:' + ifw_icinga2_parents[1].cn]['Values'][0] if ifw_icinga2_parents | length > 1 else none) | default(none) }}" # noqa: yaml[line-length] + +- name: Check whether requested and existing configuration is identical + failed_when: false + vars: + _ifw_ca_server: "[{{ ifw_icinga2_parents[0].host | default(ifw_icinga2_parents[0].cn) }}]:{{ ifw_icinga2_parents[0].port | default('5665') }}" + _parent0: "[{{ ifw_icinga2_parents[0].host | default(ifw_icinga2_parents[0].cn) }}]:{{ ifw_icinga2_parents[0].port | default('5665') }}" + _parent1: "[{{ ifw_icinga2_parents[1].host | default(ifw_icinga2_parents[1].cn) | default(none) }}]:{{ ifw_icinga2_parents[1].port | default('5665') | default(none) }}" # noqa: yaml[line-length] + ansible.builtin.assert: + that: + - (_current_icinga_configuration.content | b64decode | from_json).Framework.Config.Live is defined + - (_current_ca_server | default(true, true)) == (_ifw_ca_server) + - _current_global_zones == ifw_icinga2_global_zones + - (_current_port | int) == (ifw_icinga2_port | int) + - _current_parent_zone == ifw_icinga2_parent_zone + - _current_parents == (ifw_icinga2_parents | map(attribute='cn')) + - _current_parents0 == _parent0 + - (_current_parents1 == _parent1 if ifw_icinga2_parents | length > 1 else true) + fail_msg: "Configuration needs an update" + success_msg: "Configuration needs no update" + register: _assertion_result + +- name: Set up Icinga + when: _assertion_result.evaluated_to is defined + ansible.windows.win_shell: "Install-Icinga -InstallCommand \"{{ _install_command }}\"" diff --git a/roles/ifw/tasks/install_components.yml b/roles/ifw/tasks/install_components.yml new file mode 100644 index 00000000..69031e04 --- /dev/null +++ b/roles/ifw/tasks/install_components.yml @@ -0,0 +1,40 @@ +--- + +- name: Get list of available components + changed_when: false + ansible.windows.win_shell: ConvertTo-Json -InputObject ((Get-IcingaComponentList).Components) + register: _available_components + +- name: Get list of installed components + changed_when: false + ansible.windows.win_shell: ConvertTo-Json -InputObject (Get-IcingaInstallation) + register: _installed_components + +- name: Convert available/installed components to json + ansible.builtin.set_fact: + _available_components_json: "{{ _available_components.stdout | from_json | dict2items }}" + _installed_components_json: "{{ _installed_components.stdout | from_json | dict2items }}" + +- name: Validate requested components are installable + loop: "{{ ifw_components }}" + ansible.builtin.assert: + that: "{{ item.name in (_available_components_json | map(attribute='key') | sort) }}" + fail_msg: "'{{ item.name }}' is not an installable component! (Keep component names lowercase)" + +- name: Remove non requested components + vars: + _installed_components_list: "{{ _installed_components_json | map(attribute='key') | difference(['framework']) }}" + _components_to_be_removed: "{{ _installed_components_list | difference(ifw_components | map(attribute='name')) }}" + netways.icinga.ifw_component: + state: absent + name: "{{ _components_to_be_removed }}" + +- name: Combine component name with component version + loop: "{{ ifw_components }}" + ansible.builtin.set_fact: + _components_to_be_installed: "{{ (_components_to_be_installed | default([])) + [item.name + (':' + item.version if item.version is defined else '')] }}" + +- name: Install components + netways.icinga.ifw_component: + state: present + name: "{{ _components_to_be_installed }}" diff --git a/roles/ifw/tasks/install_powershell_framework.yml b/roles/ifw/tasks/install_powershell_framework.yml new file mode 100644 index 00000000..8d049e9a --- /dev/null +++ b/roles/ifw/tasks/install_powershell_framework.yml @@ -0,0 +1,73 @@ +--- + +- name: Get latest Framework version + run_once: true + when: ifw_framework_version == "latest" + block: + - name: Get list of available Framework versions + delegate_to: localhost + ansible.builtin.uri: + url: "{{ ifw_framework_url }}" + return_content: true + register: _repo_html + + - name: Set fact for Framework version + ansible.builtin.set_fact: + ifw_framework_version: "{{ _repo_html.content | regex_findall('[\\d\\.]+\\.zip') | community.general.version_sort | last | replace('.zip', '') }}" + +- name: Determine Icinga PowerShell Framework module path + when: not ifw_framework_path + block: + - name: Check existence of possible module paths + ansible.windows.win_stat: + path: "{{ item }}" + register: _module_path_info + loop: "{{ ifw_framework_path_options }}" + + - name: Set Icinga PowerShell Framework module path + ansible.builtin.set_fact: + ifw_framework_path: "{{ (_module_path_info.results | selectattr('stat', 'defined') | selectattr('stat.exists', 'equalto', true) | first).stat.path }}" + when: (_module_path_info.results | selectattr('stat', 'defined') | selectattr('stat.exists', 'equalto', true) | first).stat.exists + +- name: Download Icinga PowerShell Framework + run_once: true + delegate_to: localhost + ansible.builtin.get_url: + url: "{{ ifw_framework_url }}/icinga-powershell-framework-{{ ifw_framework_version }}.zip" + dest: "/tmp/icinga-powershell-framework.zip" + mode: "0665" + +- name: Unzip Icinga PowerShell Framework + run_once: true + delegate_to: localhost + ansible.builtin.unarchive: + src: "/tmp/icinga-powershell-framework.zip" + dest: "/tmp/" + +- name: Remove previous Icinga PowerShell Framework directory + run_once: true + delegate_to: localhost + changed_when: false + ansible.builtin.file: + state: absent + path: "/tmp/icinga-powershell-framework/" + +- name: Rename Icinga PowerShell Framework directory + run_once: true + delegate_to: localhost + changed_when: false + ansible.builtin.command: "cp -r /tmp/icinga-powershell-framework-{{ ifw_framework_version }}/ /tmp/icinga-powershell-framework" + +- name: Check if Icinga PowerShell Framework needs to be copied + ansible.windows.win_stat: + path: "{{ ifw_framework_path }}/icinga-powershell-framework" + register: _ifw_framework_path_stat + +- name: Copy Icinga PowerShell Framework to Windows hosts + when: not _ifw_framework_path_stat.stat.exists + ansible.windows.win_copy: + src: "/tmp/icinga-powershell-framework/" + dest: "{{ ifw_framework_path }}/icinga-powershell-framework/" + +- name: Run Use-Icinga once + ansible.windows.win_shell: Use-Icinga diff --git a/roles/ifw/tasks/main.yml b/roles/ifw/tasks/main.yml new file mode 100644 index 00000000..c5134c03 --- /dev/null +++ b/roles/ifw/tasks/main.yml @@ -0,0 +1,32 @@ +--- + +- name: Check if Icinga PowerShell Framework is installed + ansible.windows.win_shell: Use-Icinga -Minimal + when: ansible_os_family == "Windows" + register: _use_icinga + changed_when: false + failed_when: false + +- name: Install Icinga PowerShell Framework + ansible.builtin.include_tasks: "{{ role_path }}/tasks/install_powershell_framework.yml" + when: + - ansible_os_family == "Windows" + - _use_icinga.stderr != "" + +- name: Manage repositories + ansible.builtin.include_tasks: "{{ role_path }}/tasks/manage_repositories.yml" + when: ansible_os_family == "Windows" + +- name: Install Icinga Powershell Components + ansible.builtin.include_tasks: "{{ role_path }}/tasks/install_components.yml" + when: + - ansible_os_family == "Windows" + - ifw_components | length > 0 + +- name: Configure Icinga 2 + ansible.builtin.include_tasks: "{{ role_path }}/tasks/configure_icinga2.yml" + when: + - ansible_os_family == "Windows" + - ifw_components | selectattr('name', 'eq', 'agent') | length > 0 + - ifw_icinga2_parents is defined + - ifw_icinga2_parent_zone is defined diff --git a/roles/ifw/tasks/manage_repositories.yml b/roles/ifw/tasks/manage_repositories.yml new file mode 100644 index 00000000..a6e52666 --- /dev/null +++ b/roles/ifw/tasks/manage_repositories.yml @@ -0,0 +1,43 @@ +--- + +- name: Get current repositories + changed_when: false + ansible.windows.win_shell: ConvertTo-Json -InputObject @(Get-IcingaRepositories) + register: _current_repositories + +- name: Convert current repositories to json + ansible.builtin.set_fact: + _current_repositories_json: "{{ _current_repositories.stdout | from_json }}" + +- name: Combine requested repositories and fixed repositories + ansible.builtin.set_fact: + ifw_repositories: "{{ ifw_repositories + ifw_fixed_repositories }}" + +- name: Remove non requested repositories + loop: "{{ _current_repositories_json }}" + when: + - not item is none + - item.Name not in (ifw_repositories | map(attribute='name')) + ansible.windows.win_shell: Remove-IcingaRepository -Name "{{ item.Name }}" + +- name: Update existing repositories + loop: "{{ ifw_repositories }}" + vars: + _current_repository: "{{ _current_repositories_json | selectattr('Name', 'defined') | selectattr('Name', 'eq', item.name) }}" + when: + - _current_repository | length > 0 + - _current_repository[0].Value.RemotePath != item.remote_path + ansible.windows.win_shell: Add-IcingaRepository -Name "{{ item.name }}" -RemotePath "{{ item.remote_path }}" -Force + +- name: Add repositories + loop: "{{ ifw_repositories }}" + vars: + _current_repository: "{{ _current_repositories_json | selectattr('Name', 'defined') | selectattr('Name', 'eq', item.name) }}" + when: + - not _current_repository + ansible.windows.win_shell: Add-IcingaRepository -Name "{{ item.name }}" -RemotePath "{{ item.remote_path }}" + +- name: Push fixed repositories to the end of the list (lowest priority) + changed_when: false + loop: "{{ ifw_fixed_repositories | map(attribute='name') }}" + ansible.windows.win_shell: Push-IcingaRepository -Name "{{ item }}" diff --git a/roles/ifw/templates/windows/icinga_install_command.j2 b/roles/ifw/templates/windows/icinga_install_command.j2 new file mode 100644 index 00000000..b56cff55 --- /dev/null +++ b/roles/ifw/templates/windows/icinga_install_command.j2 @@ -0,0 +1,103 @@ +{ + "IfW-CustomZones": { + "Values": {{ ifw_icinga2_global_zones | tojson }} + }, + {% if ifw_icinga2_ticket is defined %} + {# Use ticket if specified, else resort to contacting ca_host (master or satellite) #} + "IfW-Certificate": { + {# 1 -> Use ticket to obtain certificate #} + "Selection": "1" + }, + "IfW-Ticket": { + "Values": [ + "{{ ifw_icinga2_ticket }}" + ] + }, + {% else %} + "IfW-Certificate": { + {# 0 -> Sign certificate manually on the Icinga CA master #} + "Selection": "0" + }, + "IfW-CAServer": { + "Values": [ + "[{{ ifw_icinga2_parents[0].host | default(ifw_icinga2_parents[0].cn) }}]:{{ ifw_icinga2_parents[0].port | default('5665') }}" + ] + }, + {% endif %} + "IfW-GlobalZones": { + {# 3 -> Don't automatically add suggested global zones #} + "Selection": "3" + }, + "IfW-InstallAgent": { + {# 0 -> Set up for installation. Keeps IMC structured correctly. #} + "Selection": "0" + }, + "IfW-AgentVersion": { + "Values": [ + {# 'release' means 'latest' stable #} + "{{ ifw_icinga2_agent_version if ifw_icinga2_agent_version != 'latest' else 'release' }}" + ] + }, + "IfW-Port": { + "Values": [ + "{{ ifw_icinga2_port | default('5665') }}" + ] + }, + "IfW-ParentNodes": { + "Values": {{ ifw_icinga2_parents | map(attribute='cn') | tojson }} + }, + "IfW-ParentZone": { + "Values": [ "{{ ifw_icinga2_parent_zone }}" ] + }, + "IfW-ParentAddress": { + "Values": { + {% for _parent in ifw_icinga2_parents %} + "{{ _parent.cn }}": [ "[{{ _parent.host | default(_parent.cn) }}]:{{ _parent.port | default('5665') }}" ]{{ ',' if (ifw_icinga2_parents | length == 2) and loop.index == 1 }} + {% endfor %} + } + }, + "IfW-InstallPlugins": { + {# 1 -> Don't automatically install plugins #} + "Selection": "1" + }, + "IfW-Connection": { + {# Connection direction -> Influences firewall rules + 0 -> Connection from agent + 1 -> Connection from parent + 2 -> Connection from both sides + #} + {% if (ifw_connection_direction | lower) == 'fromagent' %} + "Selection": "0" + {% elif (ifw_connection_direction | lower) == 'fromparent' %} + "Selection": "1" + {% elif (ifw_connection_direction | lower) == 'both' %} + "Selection": "2" + {% endif %} + }, + "IfW-InstallService": { + {# 1 -> Don't automatically install service. Handled elsewhere #} + "Selection": "1" + }, + "IfW-InstallJEAProfile": { + "Selection": "2" + }, + "IfW-Hostname": { + {# 6 -> Set hostname manually #} + "Selection": "6" + }, + "IfW-CustomHostname": { + "Values": [ + "{{ ifw_icinga2_cn }}" + ] + }, + "IfW-InstallApiChecks": { + {# 0 -> Don't install IfW Api Check Forwarder #} + "Selection": "0" + }, + "IfW-AgentUser": { + {# TODO WIP - Define user to run service as #} + "Values": [ + "NT Authority\\NetworkService" + ] + } +} diff --git a/roles/ifw/vars/main.yml b/roles/ifw/vars/main.yml new file mode 100644 index 00000000..2059025c --- /dev/null +++ b/roles/ifw/vars/main.yml @@ -0,0 +1,14 @@ +--- + +ifw_repository_json: "https://packages.icinga.com/IcingaForWindows/stable/ifw.repo.json" + +ifw_framework_path_options: + - "C:/Program Files/WindowsPowerShell/Modules" + - "C:/Windows/system32/WindowsPowerShell/v1.0/Modules" + - "C:/Users/{{ ansible_user }}/Documents/WindowsPowerShell/Modules" + +ifw_temp_directory: "%localappdata%/Temp/" + +ifw_fixed_repositories: + - name: "Icinga Stable" + remote_path: "https://packages.icinga.com/IcingaForWindows/stable/ifw.repo.json" From 7d601265d8a78bd062e04cf23561aab22c9557a7 Mon Sep 17 00:00:00 2001 From: Donien <88634789+Donien@users.noreply.github.com> Date: Mon, 20 Oct 2025 16:26:11 +0200 Subject: [PATCH 02/12] Clearify scope of role --- roles/ifw/README.md | 48 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/roles/ifw/README.md b/roles/ifw/README.md index 6b416b8f..d816e2a4 100644 --- a/roles/ifw/README.md +++ b/roles/ifw/README.md @@ -10,6 +10,12 @@ Tasks it can do: * Configure the Icinga 2 Agent * Create a valid Icinga 2 certificate +Tasks it will not do: + +* Management of custom Monitoring Plugins +* Management of firewall rules outside of Icinga for Windows (like allowing ICMP echo request) +* Management of Check Commands (available as Icinga Config or Director Basket) + Table of contents: * [Variables](#variables) @@ -19,6 +25,7 @@ Table of contents: * [Install Other Plugins](#install-other-plugins) * [Add Custom Repositories](#add-custom-repositories) * [Icinga 2 Setup](#icinga-2-setup) +* [Additional Tasks](#additional-tasks) ## Variables @@ -192,3 +199,44 @@ It adds the global zone `windows-agents`. roles: - netways.icinga.ifw ``` + + +## Additional Tasks + +This is meant as a hint for additional tasks you may need but which are not covered by Icinga for Windows and this role. + +This will use [`community.windows.win_firewall_rule`](https://docs.ansible.com/ansible/latest/collections/community/windows/win_firewall_rule_module.html) to allow ICMP (echo request) in all network zones, so default host checks like `hostalive` work. + +``` +- name: Allow ICMP (echo request) in firewall + community.windows.win_firewall_rule: + state: present + name: "{{ item.name }}" + enabled: true + profiles: "{{ item.profiles }}" + action: "{{ item.action }}" + direction: "{{ item.direction }}" + protocol: "{{ item.protocol }}" + icmp_type_code: "{{ item.icmp_type }}" + loop: + - name: "Allow inbound ICMPv4 (echo request)" + direction: "in" + protocol: "icmpv4" + icmp_type: + - "8:*" + action: "allow" + profiles: + - "domain" + - "private" + - "public" + - name: "Allow inbound ICMPv6 (echo request)" + direction: "in" + protocol: "icmpv6" + icmp_type: + - "8:*" + action: "allow" + profiles: + - "domain" + - "private" + - "public" +``` From 7b7f9d755a9da7b993e9bf4c47f9bc214491af16 Mon Sep 17 00:00:00 2001 From: Donien <88634789+Donien@users.noreply.github.com> Date: Mon, 20 Oct 2025 16:40:36 +0200 Subject: [PATCH 03/12] Install default components as 'present' --- roles/ifw/README.md | 2 +- roles/ifw/defaults/main.yml | 4 ++-- roles/ifw/meta/argument_specs.yml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/roles/ifw/README.md b/roles/ifw/README.md index d816e2a4..e57e2d1b 100644 --- a/roles/ifw/README.md +++ b/roles/ifw/README.md @@ -54,7 +54,7 @@ Table of contents: - `ifw_components: list of dictionaries` Specify which components should be present. Optionally specify which version of the component should be installed. Components installed but not present within this list will be removed. - Default: `[ { name: "plugins", version: "latest" }, { name: "agent", version: "latest" } ]` + Default: `[ { name: "plugins", state: "present" }, { name: "agent", state: "present" } ]` - `ifw_icinga2_ca_host: string` The Ansible inventory hostname of your Icinga 2 CA host (master). diff --git a/roles/ifw/defaults/main.yml b/roles/ifw/defaults/main.yml index bcbc8a1c..d87141d3 100644 --- a/roles/ifw/defaults/main.yml +++ b/roles/ifw/defaults/main.yml @@ -8,9 +8,9 @@ ifw_repositories: [] ifw_components: - name: "plugins" - version: "latest" + state: "present" - name: "agent" - version: "latest" + state: "present" ifw_icinga2_ca_host: ifw_connection_direction: "fromagent" diff --git a/roles/ifw/meta/argument_specs.yml b/roles/ifw/meta/argument_specs.yml index 80d81ee1..582ce663 100644 --- a/roles/ifw/meta/argument_specs.yml +++ b/roles/ifw/meta/argument_specs.yml @@ -65,9 +65,9 @@ argument_specs: required: false default: - name: "plugins" - version: "latest" + state: "present" - name: "agent" - version: "latest" + state: "present" options: state: description: From 5d0ea676db2b583b45fd59fa2b3887dd8cebda5b Mon Sep 17 00:00:00 2001 From: Donien <88634789+Donien@users.noreply.github.com> Date: Mon, 20 Oct 2025 17:02:55 +0200 Subject: [PATCH 04/12] Add timeout for Icinga install command As the Icinga install command might drop to an interactive prompt if some option is malformed, this timeout ensures that Ansible does not idle indefinetely. --- roles/ifw/tasks/configure_icinga2.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/roles/ifw/tasks/configure_icinga2.yml b/roles/ifw/tasks/configure_icinga2.yml index e936201d..8c98b1ab 100644 --- a/roles/ifw/tasks/configure_icinga2.yml +++ b/roles/ifw/tasks/configure_icinga2.yml @@ -128,5 +128,6 @@ register: _assertion_result - name: Set up Icinga + timeout: 300 when: _assertion_result.evaluated_to is defined ansible.windows.win_shell: "Install-Icinga -InstallCommand \"{{ _install_command }}\"" From 5a4d42abd3224eb36c69eb70f959c38da89b692e Mon Sep 17 00:00:00 2001 From: Donien <88634789+Donien@users.noreply.github.com> Date: Mon, 20 Oct 2025 17:14:29 +0200 Subject: [PATCH 05/12] Add syntax highlighting --- roles/ifw/README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/roles/ifw/README.md b/roles/ifw/README.md index e57e2d1b..fffddd70 100644 --- a/roles/ifw/README.md +++ b/roles/ifw/README.md @@ -45,7 +45,7 @@ Table of contents: Here you can specify additional repositories from which to pull components from. The default Icinga For Windows repository will always be added. Default: `none` Example: - ``` + ```yaml ifw_repositories: - name: "Custom" remote_path: "https://example.com/IcingaForWindows/stable/ifw.repo.json" @@ -94,7 +94,7 @@ Table of contents: You can specify each parent's `cn`, its `host` attribute and the `port` on which it listens. The `cn` attribute is **required**. Default: `none` Example: - ``` + ```yaml ifw_icinga2_parents: - cn: parent1 host: icinga01.example.com @@ -124,7 +124,7 @@ The examples below showcase different aspects of the Icinga for Windows configur This installs just the Icinga PowerShell Framework, the Agent component and the Icinga PowerShell Plugins. -``` +```yaml - name: Run ifw role hosts: all @@ -137,7 +137,7 @@ This installs just the Icinga PowerShell Framework, the Agent component and the This installs the Agent component, the basic plugins and in addition to that the plugins for MSSQL and HyperV. It also pins the Agent component to a specific version. -``` +```yaml - name: Run ifw role hosts: all @@ -158,7 +158,7 @@ It also pins the Agent component to a specific version. This adds more repositories to Icinga for Windows in addition to the default one. This is useful if you want to host a local mirror. -``` +```yaml - name: Run ifw role hosts: all @@ -176,7 +176,7 @@ This is useful if you want to host a local mirror. This installs the Agent component and sets it up to communicate with both its parents. It adds the global zone `windows-agents`. -``` +```yaml - name: Run ifw role hosts: all @@ -207,7 +207,7 @@ This is meant as a hint for additional tasks you may need but which are not cove This will use [`community.windows.win_firewall_rule`](https://docs.ansible.com/ansible/latest/collections/community/windows/win_firewall_rule_module.html) to allow ICMP (echo request) in all network zones, so default host checks like `hostalive` work. -``` +```yaml - name: Allow ICMP (echo request) in firewall community.windows.win_firewall_rule: state: present From 0983dbe2f38fd149cefb98277799a457a1fdd1f6 Mon Sep 17 00:00:00 2001 From: Donien <88634789+Donien@users.noreply.github.com> Date: Wed, 22 Oct 2025 13:38:09 +0200 Subject: [PATCH 06/12] Add JEA installation --- roles/ifw/README.md | 13 ++++++++++++- roles/ifw/defaults/main.yml | 5 +++++ roles/ifw/meta/argument_specs.yml | 17 +++++++++++++++++ roles/ifw/tasks/configure_icinga2.yml | 6 ++++++ .../windows/icinga_install_command.j2 | 18 +++++++++++++----- 5 files changed, 53 insertions(+), 6 deletions(-) diff --git a/roles/ifw/README.md b/roles/ifw/README.md index fffddd70..e7b6ea63 100644 --- a/roles/ifw/README.md +++ b/roles/ifw/README.md @@ -90,7 +90,7 @@ Table of contents: Default: `[]` - `ifw_icinga2_parents: list of dictionaries` - Here you can specify the parent endpoint(s) of your host's parent zone (O(ifw_icinga2_parent_zone)). + Here you can specify the parent endpoint(s) of your host's parent zone (`ifw_icinga2_parent_zone`). You can specify each parent's `cn`, its `host` attribute and the `port` on which it listens. The `cn` attribute is **required**. Default: `none` Example: @@ -108,6 +108,17 @@ Table of contents: The name of your parent(s) zone. Default: `none` +- `ifw_jea_install: boolean` + Whether to install the Icinga for Windows JEA profile. + If `ifw_jea_managed_user=false`, the JEA will profile will be created and registered. + If `ifw_jea_managed_user=true`, the service user 'icinga' will also be created to run Icinga for Windows as. + [Read more about Icinga for Windows and JEA](https://icinga.com/docs/icinga-for-windows/latest/doc/130-JEA/01-JEA-Profiles/). + Default: `true` + +- `ifw_jea_managed_user: boolean` + Whether to use the Icinga for Windows service user 'icinga' when `ifw_jea_install=true`. + Default: `true` + ### Getting a Certificate If neither `ifw_icinga2_ca_host` nor `ifw_icinga2_ticket` is specified, your target host will connect to the first parent in `ifw_icinga2_parents` and file a CSR. This needs to be signed manually afterwards. diff --git a/roles/ifw/defaults/main.yml b/roles/ifw/defaults/main.yml index d87141d3..4cee64d6 100644 --- a/roles/ifw/defaults/main.yml +++ b/roles/ifw/defaults/main.yml @@ -7,6 +7,8 @@ ifw_framework_path: ifw_repositories: [] ifw_components: + - name: "service" + state: "present" - name: "plugins" state: "present" - name: "agent" @@ -18,3 +20,6 @@ ifw_force_newcert: false ifw_icinga2_cn: "{{ inventory_hostname }}" ifw_icinga2_port: 5665 ifw_icinga2_global_zones: [] + +ifw_jea_install: true +ifw_jea_managed_user: true diff --git a/roles/ifw/meta/argument_specs.yml b/roles/ifw/meta/argument_specs.yml index 582ce663..bf5afad5 100644 --- a/roles/ifw/meta/argument_specs.yml +++ b/roles/ifw/meta/argument_specs.yml @@ -64,6 +64,8 @@ argument_specs: elements: dict required: false default: + - name: "service" + state: "present" - name: "plugins" state: "present" - name: "agent" @@ -163,3 +165,18 @@ argument_specs: - The name of the Icinga 2 parent(s) zone. - *icinga2_requirements type: list + ifw_jea_install: + description: + - Whether to install the Icinga for Windows JEA profile. + If O(ifw_jea_managed_user=false), the JEA will profile will be created and registered. + If O(ifw_jea_managed_user=true), the service user 'icinga' will also be created to run Icinga for Windows as. + L(Read more about Icinga for Windows and JEA, https://icinga.com/docs/icinga-for-windows/latest/doc/130-JEA/01-JEA-Profiles/). + type: bool + required: false + default: true + ifw_jea_managed_user: + description: + - Whether to use the Icinga for Windows service user 'icinga' when O(ifw_jea_install=true). + type: bool + required: false + default: true diff --git a/roles/ifw/tasks/configure_icinga2.yml b/roles/ifw/tasks/configure_icinga2.yml index 8c98b1ab..ed2aaced 100644 --- a/roles/ifw/tasks/configure_icinga2.yml +++ b/roles/ifw/tasks/configure_icinga2.yml @@ -101,11 +101,13 @@ ansible.builtin.set_fact: _current_ca_server: "{{ _framework_config_live['IfW-CAServer']['Values'][0] | default(none) }}" _current_global_zones: "{{ _framework_config_live['IfW-CustomZones']['Values'] }}" + _current_cn: "{{ _framework_config_live['IfW-CustomHostname']['Values'][0] }}" _current_port: "{{ _framework_config_live['IfW-Port']['Values'][0] | default(none) }}" _current_parent_zone: "{{ _framework_config_live['IfW-ParentZone']['Values'][0] | default(none) }}" _current_parents: "{{ _framework_config_live['IfW-ParentNodes']['Values'] }}" _current_parents0: "{{ _framework_config_live['IfW-ParentAddress:' + ifw_icinga2_parents[0].cn]['Values'][0] | default(none) }}" _current_parents1: "{{ (_framework_config_live['IfW-ParentAddress:' + ifw_icinga2_parents[1].cn]['Values'][0] if ifw_icinga2_parents | length > 1 else none) | default(none) }}" # noqa: yaml[line-length] + _current_jea: "{{ _framework_config_live['IfW-InstallJEAProfile']['Selection'] }}" - name: Check whether requested and existing configuration is identical failed_when: false @@ -118,11 +120,15 @@ - (_current_icinga_configuration.content | b64decode | from_json).Framework.Config.Live is defined - (_current_ca_server | default(true, true)) == (_ifw_ca_server) - _current_global_zones == ifw_icinga2_global_zones + - _current_cn == ifw_icinga2_cn - (_current_port | int) == (ifw_icinga2_port | int) - _current_parent_zone == ifw_icinga2_parent_zone - _current_parents == (ifw_icinga2_parents | map(attribute='cn')) - _current_parents0 == _parent0 - (_current_parents1 == _parent1 if ifw_icinga2_parents | length > 1 else true) + - (_current_jea == "0" if (ifw_jea_install and not ifw_jea_managed_user) else true) + - (_current_jea == "1" if (ifw_jea_install and ifw_jea_managed_user) else true) + - (_current_jea == "2" if not ifw_jea_install else true) fail_msg: "Configuration needs an update" success_msg: "Configuration needs no update" register: _assertion_result diff --git a/roles/ifw/templates/windows/icinga_install_command.j2 b/roles/ifw/templates/windows/icinga_install_command.j2 index b56cff55..cd65b9bf 100644 --- a/roles/ifw/templates/windows/icinga_install_command.j2 +++ b/roles/ifw/templates/windows/icinga_install_command.j2 @@ -94,10 +94,18 @@ {# 0 -> Don't install IfW Api Check Forwarder #} "Selection": "0" }, - "IfW-AgentUser": { - {# TODO WIP - Define user to run service as #} - "Values": [ - "NT Authority\\NetworkService" - ] + "IfW-InstallJEAProfile": { + {% if ifw_jea_install %} + {% if not ifw_jea_managed_user %} + {# 0 Install JEA Profile #} + "Selection": "0" + {% else %} + {# 1 Install JEA Profile with managed user "icinga" #} + "Selection": "1" + {% endif %} + {% else %} + {# 2 Do not install JEA Profile #} + "Selection": "2" + {% endif %} } } From 670df12fe1404b1557904d331383cd5a8369da13 Mon Sep 17 00:00:00 2001 From: Donien <88634789+Donien@users.noreply.github.com> Date: Wed, 22 Oct 2025 16:04:04 +0200 Subject: [PATCH 07/12] Add API Check Forwarder feature --- roles/ifw/README.md | 6 ++++++ roles/ifw/defaults/main.yml | 2 ++ roles/ifw/meta/argument_specs.yml | 7 +++++++ roles/ifw/tasks/configure_icinga2.yml | 3 +++ roles/ifw/templates/windows/icinga_install_command.j2 | 9 +++++++++ 5 files changed, 27 insertions(+) diff --git a/roles/ifw/README.md b/roles/ifw/README.md index e7b6ea63..24ec346e 100644 --- a/roles/ifw/README.md +++ b/roles/ifw/README.md @@ -119,6 +119,12 @@ Table of contents: Whether to use the Icinga for Windows service user 'icinga' when `ifw_jea_install=true`. Default: `true` +- `ifw_api_feature: boolean` + Whether to enable the Icinga for Windows API Check Forwarder. + [Read more about the Icinga for Windows API Check Forwarder](https://icinga.com/docs/icinga-for-windows/latest/doc/110-Installation/30-API-Check-Forwarder/). + Default: `true` + + ### Getting a Certificate If neither `ifw_icinga2_ca_host` nor `ifw_icinga2_ticket` is specified, your target host will connect to the first parent in `ifw_icinga2_parents` and file a CSR. This needs to be signed manually afterwards. diff --git a/roles/ifw/defaults/main.yml b/roles/ifw/defaults/main.yml index 4cee64d6..0f0f9599 100644 --- a/roles/ifw/defaults/main.yml +++ b/roles/ifw/defaults/main.yml @@ -23,3 +23,5 @@ ifw_icinga2_global_zones: [] ifw_jea_install: true ifw_jea_managed_user: true + +ifw_api_feature: true diff --git a/roles/ifw/meta/argument_specs.yml b/roles/ifw/meta/argument_specs.yml index bf5afad5..fab0fbae 100644 --- a/roles/ifw/meta/argument_specs.yml +++ b/roles/ifw/meta/argument_specs.yml @@ -180,3 +180,10 @@ argument_specs: type: bool required: false default: true + ifw_api_feature: + description: + - Whether to enable the Icinga for Windows API Check Forwarder. + L(Read more about the Icinga for Windows API Check Forwarder, https://icinga.com/docs/icinga-for-windows/latest/doc/110-Installation/30-API-Check-Forwarder/). + type: bool + required: false + default: true diff --git a/roles/ifw/tasks/configure_icinga2.yml b/roles/ifw/tasks/configure_icinga2.yml index ed2aaced..7525613c 100644 --- a/roles/ifw/tasks/configure_icinga2.yml +++ b/roles/ifw/tasks/configure_icinga2.yml @@ -108,6 +108,7 @@ _current_parents0: "{{ _framework_config_live['IfW-ParentAddress:' + ifw_icinga2_parents[0].cn]['Values'][0] | default(none) }}" _current_parents1: "{{ (_framework_config_live['IfW-ParentAddress:' + ifw_icinga2_parents[1].cn]['Values'][0] if ifw_icinga2_parents | length > 1 else none) | default(none) }}" # noqa: yaml[line-length] _current_jea: "{{ _framework_config_live['IfW-InstallJEAProfile']['Selection'] }}" + _current_api_feature: "{{ _framework_config_live['IfW-InstallApiChecks']['Selection'] }}" - name: Check whether requested and existing configuration is identical failed_when: false @@ -129,6 +130,8 @@ - (_current_jea == "0" if (ifw_jea_install and not ifw_jea_managed_user) else true) - (_current_jea == "1" if (ifw_jea_install and ifw_jea_managed_user) else true) - (_current_jea == "2" if not ifw_jea_install else true) + - (_current_api_feature == "0" if not ifw_api_feature else true) + - (_current_api_feature == "1" if ifw_api_feature else true) fail_msg: "Configuration needs an update" success_msg: "Configuration needs no update" register: _assertion_result diff --git a/roles/ifw/templates/windows/icinga_install_command.j2 b/roles/ifw/templates/windows/icinga_install_command.j2 index cd65b9bf..4e686935 100644 --- a/roles/ifw/templates/windows/icinga_install_command.j2 +++ b/roles/ifw/templates/windows/icinga_install_command.j2 @@ -107,5 +107,14 @@ {# 2 Do not install JEA Profile #} "Selection": "2" {% endif %} + }, + "IfW-InstallApiChecks": { + {% if ifw_api_feature %} + {# 1 -> Install Api-Checks feature #} + "Selection": "1" + {% else %} + {# 0 -> Do not install Api-Checks feature #} + "Selection": "0" + {% endif %} } } From 406260d278ec3ccbc1213acd4cbfe742881523b4 Mon Sep 17 00:00:00 2001 From: Donien <88634789+Donien@users.noreply.github.com> Date: Wed, 22 Oct 2025 21:01:26 +0200 Subject: [PATCH 08/12] Add choice of service user (1/2) Password in case of non-service account still needed --- roles/ifw/README.md | 5 +++++ roles/ifw/defaults/main.yml | 2 ++ roles/ifw/meta/argument_specs.yml | 9 ++++++++- roles/ifw/tasks/configure_icinga2.yml | 2 ++ roles/ifw/templates/windows/icinga_install_command.j2 | 6 ++++++ 5 files changed, 23 insertions(+), 1 deletion(-) diff --git a/roles/ifw/README.md b/roles/ifw/README.md index 24ec346e..48138e0d 100644 --- a/roles/ifw/README.md +++ b/roles/ifw/README.md @@ -56,6 +56,10 @@ Table of contents: Components installed but not present within this list will be removed. Default: `[ { name: "plugins", state: "present" }, { name: "agent", state: "present" } ]` +- `ifw_icinga2_user: string` + The user Icinga 2 runs as. This user is only used if `ifw_jea_managed_user=false`. + Default: `NT Authority\NetworkService` + - `ifw_icinga2_ca_host: string` The Ansible inventory hostname of your Icinga 2 CA host (master). This variable is used to sign the certificate for your Windows host using delegated tasks. @@ -112,6 +116,7 @@ Table of contents: Whether to install the Icinga for Windows JEA profile. If `ifw_jea_managed_user=false`, the JEA will profile will be created and registered. If `ifw_jea_managed_user=true`, the service user 'icinga' will also be created to run Icinga for Windows as. + If both `ifw_jea_install=true` and `ifw_jea_managed_user=true`, `ifw_icinga2_user` will essentially be ignored. [Read more about Icinga for Windows and JEA](https://icinga.com/docs/icinga-for-windows/latest/doc/130-JEA/01-JEA-Profiles/). Default: `true` diff --git a/roles/ifw/defaults/main.yml b/roles/ifw/defaults/main.yml index 0f0f9599..5fbad9fd 100644 --- a/roles/ifw/defaults/main.yml +++ b/roles/ifw/defaults/main.yml @@ -14,6 +14,7 @@ ifw_components: - name: "agent" state: "present" +ifw_icinga2_user: "NT Authority\\NetworkService" ifw_icinga2_ca_host: ifw_connection_direction: "fromagent" ifw_force_newcert: false @@ -21,6 +22,7 @@ ifw_icinga2_cn: "{{ inventory_hostname }}" ifw_icinga2_port: 5665 ifw_icinga2_global_zones: [] + ifw_jea_install: true ifw_jea_managed_user: true diff --git a/roles/ifw/meta/argument_specs.yml b/roles/ifw/meta/argument_specs.yml index fab0fbae..d5f9ee67 100644 --- a/roles/ifw/meta/argument_specs.yml +++ b/roles/ifw/meta/argument_specs.yml @@ -89,6 +89,12 @@ argument_specs: - The version of the component to be installed / removed. type: str required: false + ifw_icinga2_user: + description: + - The user Icinga 2 runs as. This user is only used if O(ifw_jea_managed_user=false). + type: str + required: false + default: "NT Authority\\NetworkService" ifw_icinga2_ca_host: description: - The Ansible C(inventory_hostname) of your Icinga 2 CA host (master). @@ -168,8 +174,9 @@ argument_specs: ifw_jea_install: description: - Whether to install the Icinga for Windows JEA profile. - If O(ifw_jea_managed_user=false), the JEA will profile will be created and registered. + If O(ifw_jea_managed_user=false), the JEA will profile will be created and registered for the user running Icinga for Windows (O(ifw_icinga2_user) by default). If O(ifw_jea_managed_user=true), the service user 'icinga' will also be created to run Icinga for Windows as. + If both O(ifw_jea_install=true) and O(ifw_jea_managed_user=true), O(ifw_icinga2_user) will essentially be ignored. L(Read more about Icinga for Windows and JEA, https://icinga.com/docs/icinga-for-windows/latest/doc/130-JEA/01-JEA-Profiles/). type: bool required: false diff --git a/roles/ifw/tasks/configure_icinga2.yml b/roles/ifw/tasks/configure_icinga2.yml index 7525613c..9d9031c8 100644 --- a/roles/ifw/tasks/configure_icinga2.yml +++ b/roles/ifw/tasks/configure_icinga2.yml @@ -102,6 +102,7 @@ _current_ca_server: "{{ _framework_config_live['IfW-CAServer']['Values'][0] | default(none) }}" _current_global_zones: "{{ _framework_config_live['IfW-CustomZones']['Values'] }}" _current_cn: "{{ _framework_config_live['IfW-CustomHostname']['Values'][0] }}" + _current_agent_user: "{{ _framework_config_live['IfW-AgentUser']['Values'][0] }}" _current_port: "{{ _framework_config_live['IfW-Port']['Values'][0] | default(none) }}" _current_parent_zone: "{{ _framework_config_live['IfW-ParentZone']['Values'][0] | default(none) }}" _current_parents: "{{ _framework_config_live['IfW-ParentNodes']['Values'] }}" @@ -122,6 +123,7 @@ - (_current_ca_server | default(true, true)) == (_ifw_ca_server) - _current_global_zones == ifw_icinga2_global_zones - _current_cn == ifw_icinga2_cn + - _current_agent_user == ifw_icinga2_user - (_current_port | int) == (ifw_icinga2_port | int) - _current_parent_zone == ifw_icinga2_parent_zone - _current_parents == (ifw_icinga2_parents | map(attribute='cn')) diff --git a/roles/ifw/templates/windows/icinga_install_command.j2 b/roles/ifw/templates/windows/icinga_install_command.j2 index 4e686935..18b65aea 100644 --- a/roles/ifw/templates/windows/icinga_install_command.j2 +++ b/roles/ifw/templates/windows/icinga_install_command.j2 @@ -90,6 +90,12 @@ "{{ ifw_icinga2_cn }}" ] }, + "IfW-AgentUser": { + "Values": [ + {# Ensure single backslash becomes double backslash. InstallCommand fails otherwise #} + "{{ ifw_icinga2_user | replace('\\', '\\\\') }}" + ] + }, "IfW-InstallApiChecks": { {# 0 -> Don't install IfW Api Check Forwarder #} "Selection": "0" From 8c1868ca2865fca9685cab8ea1d7e5da24f5ea56 Mon Sep 17 00:00:00 2001 From: Donien <88634789+Donien@users.noreply.github.com> Date: Thu, 23 Oct 2025 09:54:20 +0200 Subject: [PATCH 09/12] Update documentation --- plugins/modules/ifw_backgrounddaemon.py | 2 +- roles/ifw/README.md | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/modules/ifw_backgrounddaemon.py b/plugins/modules/ifw_backgrounddaemon.py index b81446e6..7dcd6ee1 100644 --- a/plugins/modules/ifw_backgrounddaemon.py +++ b/plugins/modules/ifw_backgrounddaemon.py @@ -12,7 +12,7 @@ - name: Icinga for Windows Background Daemons description: Reference for the Background Daemons. link: https://icinga.com/docs/icinga-for-windows/latest/doc/110-Installation/05-Background-Daemons/ - - name: Icinga for Windows API Check Forwareder + - name: Icinga for Windows API Check Forwarder description: Reference for a possible use case regarding the API Check Forwarder. link: https://icinga.com/docs/icinga-for-windows/latest/doc/110-Installation/30-API-Check-Forwarder/ options: diff --git a/roles/ifw/README.md b/roles/ifw/README.md index 48138e0d..15c856de 100644 --- a/roles/ifw/README.md +++ b/roles/ifw/README.md @@ -15,6 +15,8 @@ Tasks it will not do: * Management of custom Monitoring Plugins * Management of firewall rules outside of Icinga for Windows (like allowing ICMP echo request) * Management of Check Commands (available as Icinga Config or Director Basket) +* Management of Background Daemons (available via module `ifw_backgrounddaemon`) +* Management of what commands are allowed by the API Check Forwarder (available via module `ifw_restapicommand`) Table of contents: @@ -29,6 +31,8 @@ Table of contents: ## Variables +The default values for some variables - like the ones for JEA and the API feature - are considered best practice. Though, feel free to adjust them to your needs. + - `ifw_framework_url: string` The URL to the different verions of the Icinga PowerShell Framework. Default: `https://packages.icinga.com/IcingaForWindows/stable/framework/` From 740b459e52e8832070870786c4ef71026ffd2923 Mon Sep 17 00:00:00 2001 From: Donien <88634789+Donien@users.noreply.github.com> Date: Thu, 23 Oct 2025 13:25:50 +0200 Subject: [PATCH 10/12] Update documentation --- roles/ifw/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roles/ifw/README.md b/roles/ifw/README.md index 15c856de..909f409e 100644 --- a/roles/ifw/README.md +++ b/roles/ifw/README.md @@ -118,7 +118,7 @@ The default values for some variables - like the ones for JEA and the API featur - `ifw_jea_install: boolean` Whether to install the Icinga for Windows JEA profile. - If `ifw_jea_managed_user=false`, the JEA will profile will be created and registered. + If `ifw_jea_managed_user=false`, the JEA will profile will be created and registered for the user running Icinga for Windows (`ifw_icinga2_user`) by default). If `ifw_jea_managed_user=true`, the service user 'icinga' will also be created to run Icinga for Windows as. If both `ifw_jea_install=true` and `ifw_jea_managed_user=true`, `ifw_icinga2_user` will essentially be ignored. [Read more about Icinga for Windows and JEA](https://icinga.com/docs/icinga-for-windows/latest/doc/130-JEA/01-JEA-Profiles/). From 9218c10ebb8fb06f79fa40400521e7750ab6d88b Mon Sep 17 00:00:00 2001 From: Donien <88634789+Donien@users.noreply.github.com> Date: Sat, 25 Oct 2025 17:57:48 +0200 Subject: [PATCH 11/12] Ensure ServiceCheckDaemon --- roles/ifw/README.md | 1 + roles/ifw/meta/argument_specs.yml | 1 + roles/ifw/tasks/configure_icinga2.yml | 15 +++++++++++++-- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/roles/ifw/README.md b/roles/ifw/README.md index 909f409e..e4398444 100644 --- a/roles/ifw/README.md +++ b/roles/ifw/README.md @@ -130,6 +130,7 @@ The default values for some variables - like the ones for JEA and the API featur - `ifw_api_feature: boolean` Whether to enable the Icinga for Windows API Check Forwarder. + Be sure to install the `service` component if you want to make use of this feature. [Read more about the Icinga for Windows API Check Forwarder](https://icinga.com/docs/icinga-for-windows/latest/doc/110-Installation/30-API-Check-Forwarder/). Default: `true` diff --git a/roles/ifw/meta/argument_specs.yml b/roles/ifw/meta/argument_specs.yml index d5f9ee67..182808ce 100644 --- a/roles/ifw/meta/argument_specs.yml +++ b/roles/ifw/meta/argument_specs.yml @@ -190,6 +190,7 @@ argument_specs: ifw_api_feature: description: - Whether to enable the Icinga for Windows API Check Forwarder. + Be sure to install the C(service) component if you want to make use of this feature. L(Read more about the Icinga for Windows API Check Forwarder, https://icinga.com/docs/icinga-for-windows/latest/doc/110-Installation/30-API-Check-Forwarder/). type: bool required: false diff --git a/roles/ifw/tasks/configure_icinga2.yml b/roles/ifw/tasks/configure_icinga2.yml index 9d9031c8..314ef331 100644 --- a/roles/ifw/tasks/configure_icinga2.yml +++ b/roles/ifw/tasks/configure_icinga2.yml @@ -138,7 +138,18 @@ success_msg: "Configuration needs no update" register: _assertion_result +- name: Register IcingaServiceCheckDaemon + when: ifw_api_feature or "service" in (ifw_components | map(attribute='name')) + netways.icinga.ifw_backgrounddaemon: + state: present + command: "Start-IcingaServiceCheckDaemon" + - name: Set up Icinga - timeout: 300 + async: 300 + poll: 30 when: _assertion_result.evaluated_to is defined - ansible.windows.win_shell: "Install-Icinga -InstallCommand \"{{ _install_command }}\"" + ansible.windows.win_powershell: + script: "Install-Icinga -InstallCommand \"{{ _install_command }}\"" + arguments: + - "-ExecutionPolicy" + - "ByPass" From ef233020a661aeb45e2335ad3daf36e64887cbff Mon Sep 17 00:00:00 2001 From: Donien <88634789+Donien@users.noreply.github.com> Date: Mon, 27 Oct 2025 08:40:04 +0100 Subject: [PATCH 12/12] Make conditionals explicit booleans --- roles/ifw/tasks/install_powershell_framework.yml | 2 +- roles/ifw/tasks/manage_repositories.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/roles/ifw/tasks/install_powershell_framework.yml b/roles/ifw/tasks/install_powershell_framework.yml index 8d049e9a..035f20a8 100644 --- a/roles/ifw/tasks/install_powershell_framework.yml +++ b/roles/ifw/tasks/install_powershell_framework.yml @@ -16,7 +16,7 @@ ifw_framework_version: "{{ _repo_html.content | regex_findall('[\\d\\.]+\\.zip') | community.general.version_sort | last | replace('.zip', '') }}" - name: Determine Icinga PowerShell Framework module path - when: not ifw_framework_path + when: not ifw_framework_path is none block: - name: Check existence of possible module paths ansible.windows.win_stat: diff --git a/roles/ifw/tasks/manage_repositories.yml b/roles/ifw/tasks/manage_repositories.yml index a6e52666..4da1f479 100644 --- a/roles/ifw/tasks/manage_repositories.yml +++ b/roles/ifw/tasks/manage_repositories.yml @@ -34,7 +34,7 @@ vars: _current_repository: "{{ _current_repositories_json | selectattr('Name', 'defined') | selectattr('Name', 'eq', item.name) }}" when: - - not _current_repository + - _current_repository | length == 0 ansible.windows.win_shell: Add-IcingaRepository -Name "{{ item.name }}" -RemotePath "{{ item.remote_path }}" - name: Push fixed repositories to the end of the list (lowest priority)