From e5c6708d39d0d8ae193296a4812696c5da337a3e Mon Sep 17 00:00:00 2001 From: Jordan Borean Date: Fri, 19 Jan 2018 10:29:46 +1000 Subject: [PATCH] win_updates: added blacklist and whitelist filtering of updates (#34907) * win_updates: added blacklist and whitelist filtering of updates * fixed sanity issue with docs * fix docs typos --- lib/ansible/modules/windows/win_updates.ps1 | 78 +++++++++++++++------ lib/ansible/modules/windows/win_updates.py | 53 +++++++++++++- lib/ansible/plugins/action/win_updates.py | 5 ++ 3 files changed, 115 insertions(+), 21 deletions(-) diff --git a/lib/ansible/modules/windows/win_updates.ps1 b/lib/ansible/modules/windows/win_updates.ps1 index 5743dd4d5b6..26b3d270e3b 100644 --- a/lib/ansible/modules/windows/win_updates.ps1 +++ b/lib/ansible/modules/windows/win_updates.ps1 @@ -22,11 +22,13 @@ $check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "b $category_names = Get-AnsibleParam -obj $params -name "category_names" -type "list" -default @("CriticalUpdates", "SecurityUpdates", "UpdateRollups") $log_path = Get-AnsibleParam -obj $params -name "log_path" -type "path" $state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "installed" -validateset "installed", "searched" -# TODO: blacklist and whitelist +$blacklist = Get-AnsibleParam -obj $params -name "blacklist" -type "list" +$whitelist = Get-AnsibleParam -obj $params -name "whitelist" -type "list" $result = @{ changed = $false updates = @{} + filtered_updates = @{} } Function Write-DebugLog($msg) { @@ -106,32 +108,67 @@ try { Fail-Json -obj $result -message "Failed to create update collection object: $($_.Exception.Message)" } -# FUTURE: add further filtering options (whitelist/blacklist) foreach ($update in $search_result.Updates) { - if (-not $update.EulaAccepted) { - Write-DebugLog -msg "Accepting EULA for $($update.Identity.UpdateID)" - try { - $update.AcceptEula() - } catch { - Fail-Json -obj $result -message "Failed to accept EULA for update $($update.Identity.UpdateID) - $($update.Title)" - } - } - - if ($update.IsHidden) { - Write-DebugLog -msg "Skipping hidden update $($update.Title)" - continue - } - - Write-DebugLog -msg "Adding update $($update.Identity.UpdateID) - $($update.Title)" - $updates_to_install.Add($update) > $null - - $result.updates[$update.Identity.UpdateId] = @{ + $update_info = @{ title = $update.Title # TODO: pluck the first KB out (since most have just one)? kb = $update.KBArticleIDs id = $update.Identity.UpdateId installed = $false } + + # validate update again blacklist/whitelist + $skipped = $false + foreach ($whitelist_entry in $whitelist) { + $kb_match = $false + foreach ($kb in $update_info.kb) { + if ("KB$kb" -imatch $whitelist_entry) { + $kb_match = $true + } + } + if (-not ($kb_match -or $update_info.title -imatch $whitelist_entry)) { + Write-DebugLog -msg "Skipping update $($update_info.id) - $($update_info.title) as it was not found in the whitelist" + $skipped = $true + break + } + } + foreach ($blacklist_entry in $blacklist) { + $kb_match = $false + foreach ($kb in $update_info.kb) { + if ("KB$kb" -imatch $blacklist_entry) { + $kb_match = $true + } + } + if ($kb_match -or $update_info.title -imatch $blacklist_entry) { + Write-DebugLog -msg "Skipping update $($update_info.id) - $($update_info.title) as it was found in the blacklist" + $skipped = $true + break + } + } + if ($skipped) { + $result.filtered_updates[$update_info.id] = $update_info + continue + } + + + if (-not $update.EulaAccepted) { + Write-DebugLog -msg "Accepting EULA for $($update_info.id)" + try { + $update.AcceptEula() + } catch { + Fail-Json -obj $result -message "Failed to accept EULA for update $($update_info.id) - $($update_info.title)" + } + } + + if ($update.IsHidden) { + Write-DebugLog -msg "Skipping hidden update $($update_info.title)" + continue + } + + Write-DebugLog -msg "Adding update $($update_info.id) - $($update_info.title)" + $updates_to_install.Add($update) > $null + + $result.updates[$update_info.id] = $update_info } Write-DebugLog -msg "Calculating pre-install reboot requirement..." @@ -279,3 +316,4 @@ if ($update_fail_count -gt 0) { Write-DebugLog -msg "Return value:`r`n$(ConvertTo-Json -InputObject $result -Depth 99)" Exit-Json $result + diff --git a/lib/ansible/modules/windows/win_updates.py b/lib/ansible/modules/windows/win_updates.py index 04a1fd032c5..3d73c789ddb 100644 --- a/lib/ansible/modules/windows/win_updates.py +++ b/lib/ansible/modules/windows/win_updates.py @@ -21,6 +21,16 @@ short_description: Download and install Windows updates description: - Searches, downloads, and installs Windows updates synchronously by automating the Windows Update client options: + blacklist: + description: + - A list of update titles or KB numbers that can be used to specify + which updates are to be excluded from installation. + - If an available update does match one of the entries, then it is + skipped and not installed. + - Each entry can either be the KB article or Update title as a regex + according to the PowerShell regex rules. + required: false + version_added: '2.5' category_names: description: - A scalar or list of categories to install updates from @@ -69,6 +79,19 @@ options: description: - If set, C(win_updates) will append update progress to the specified file. The directory must already exist. required: false + whitelist: + description: + - A list of update titles or KB numbers that can be used to specify + which updates are to be searched or installed. + - If an available update does not match one of the entries, then it + is skipped and not installed. + - Each entry can either be the KB article or Update title as a regex + according to the PowerShell regex rules. + - The whitelist is only validated on updates that were found based on + I(category_names). It will not force the module to install an update + if it was not in the category specified. + required: false + version_added: '2.5' author: "Matt Davis (@nitzmahone)" notes: - C(win_updates) must be run by a user with membership in the local Administrators group. @@ -78,6 +101,8 @@ notes: C(reboot) can be used to reboot the host if required in the one task. - C(win_updates) can take a significant amount of time to complete (hours, in some cases). Performance depends on many factors, including OS version, number of updates, system load, and update server load. +- More information about PowerShell and how it handles RegEx strings can be + found at U(https://technet.microsoft.com/en-us/library/2007.11.powershell.aspx). ''' EXAMPLES = r''' @@ -104,7 +129,24 @@ EXAMPLES = r''' - SecurityUpdates reboot: yes -# Note async on works on Windows Server 2012 or newer - become must be explicitly set on the task for this to work +- name: Install only particular updates based on the KB numbers + win_updates: + category_name: + - SecurityUpdates + whitelist: + - KB4056892 + - KB4073117 + +- name: Exlude updates based on the update title + win_updates: + category_name: + - SecurityUpdates + - CriticalUpdates + blacklist: + - Windows Malicious Software Removal Tool for Windows + - \d{4}-\d{2} Cumulative Update for Windows Server 2016 + +# Note async works on Windows Server 2012 or newer - become must be explicitly set on the task for this to work - name: Search for Windows updates asynchronously win_updates: category_names: @@ -175,6 +217,15 @@ updates: type: boolean sample: 2147942402 +filtered_updates: + description: List of updates that were found but were filtered based on + I(blacklist) or I(whitelist). The return value is in the same form as + I(updates). + returned: success + type: complex + sample: see the updates return value + contains: {} + found_update_count: description: The number of updates found needing to be applied returned: success diff --git a/lib/ansible/plugins/action/win_updates.py b/lib/ansible/plugins/action/win_updates.py index 1fb37c4bbe7..4ee48f056d7 100644 --- a/lib/ansible/plugins/action/win_updates.py +++ b/lib/ansible/plugins/action/win_updates.py @@ -183,6 +183,7 @@ class ActionModule(ActionBase): changed = result['changed'] updates = result.get('updates', dict()) + filtered_updates = result.get('filtered_updates', dict()) found_update_count = result.get('found_update_count', 0) installed_update_count = result.get('installed_update_count', 0) @@ -231,7 +232,10 @@ class ActionModule(ActionBase): result = self._run_win_updates(new_module_args, task_vars) result_updates = result.get('updates', dict()) + result_filtered_updates = result.get('filtered_updates', dict()) updates = self._merge_dict(updates, result_updates) + filtered_updates = self._merge_dict(filtered_updates, + result_filtered_updates) found_update_count += result.get('found_update_count', 0) installed_update_count += result.get('installed_update_count', 0) if result['changed']: @@ -242,6 +246,7 @@ class ActionModule(ActionBase): if self._task.async_val == 0: result['changed'] = changed result['updates'] = updates + result['filtered_updates'] = filtered_updates result['found_update_count'] = found_update_count result['installed_update_count'] = installed_update_count