win_updates: Add post search category matching to support product matching (#45708)

* win_update: Add post search category matching to support product matching

* win_updates: Return categories of each update

* win_updates: Documentation fix-up

* win_updates: Adjusted documentation to reflect regex vs sub-string match of post-cat strings

* win_updates: Sped up post-category checking

* win_updates: Updated documentation to suggest querying post-category strings

* win_updates: Simplified saving and checking post-categories

* fixed some issues and added filtered categories to return value

* win_updates: Moved all category matching to occur after initial search

* win_updates: Adjustments to satisfy PowerShell lint checks

* win_updates: Dropped category validation from action plugin

* win_updates: Documentation updates

* win_updates: Fixed plugin unit tests
This commit is contained in:
Michael Cassaniti 2018-11-07 20:32:07 +11:00 committed by Jordan Borean
parent 8245441b2e
commit a2f3f16930
6 changed files with 113 additions and 149 deletions

View file

@ -0,0 +1,2 @@
minor_changes:
- win_updates - Reworked filtering updates based on category classification - https://github.com/ansible/ansible/issues/45476

View file

@ -17,25 +17,21 @@ $state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "insta
$blacklist = Get-AnsibleParam -obj $params -name "blacklist" -type "list" $blacklist = Get-AnsibleParam -obj $params -name "blacklist" -type "list"
$whitelist = Get-AnsibleParam -obj $params -name "whitelist" -type "list" $whitelist = Get-AnsibleParam -obj $params -name "whitelist" -type "list"
Function Get-CategoryGuid($category_name) { # For backwards compatibility
$guid = switch -exact ($category_name) { Function Get-CategoryMapping ($category_name) {
"Application" {"5C9376AB-8CE6-464A-B136-22113DD69801"} switch -exact ($category_name) {
"Connectors" {"434DE588-ED14-48F5-8EED-A15E09A991F6"} "CriticalUpdates" {return "Critical Updates"}
"CriticalUpdates" {"E6CF1350-C01B-414D-A61F-263D14D133B4"} "DefinitionUpdates" {return "Definition Updates"}
"DefinitionUpdates" {"E0789628-CE08-4437-BE74-2495B842F43B"} "DeveloperKits" {return "Developer Kits"}
"DeveloperKits" {"E140075D-8433-45C3-AD87-E72345B36078"} "FeaturePacks" {return "Feature Packs"}
"FeaturePacks" {"B54E7D24-7ADD-428F-8B75-90A396FA584F"} "SecurityUpdates" {return "Security Updates"}
"Guidance" {"9511D615-35B2-47BB-927F-F73D8E9260BB"} "ServicePacks" {return "Service Packs"}
"SecurityUpdates" {"0FA1201D-4330-4FA8-8AE9-B877473B6441"} "UpdateRollups" {return "Update Rollups"}
"ServicePacks" {"68C5B0A3-D1A6-4553-AE49-01D3A7827828"} default {return $category_name}
"Tools" {"B4832BD8-E735-4761-8DAF-37F882276DAB"}
"UpdateRollups" {"28BC880E-0592-4CBF-8F95-C79B17911D5F"}
"Updates" {"CD5FFD1E-E932-4E3A-BF74-18BF0B1BBD83"}
default { Fail-Json -message "Unknown category_name $category_name, must be one of (Application,Connectors,CriticalUpdates,DefinitionUpdates,DeveloperKits,FeaturePacks,Guidance,SecurityUpdates,ServicePacks,Tools,UpdateRollups,Updates)" }
} }
return $guid
} }
$category_guids = $category_names | ForEach-Object { Get-CategoryGuid -category_name $_ }
$category_names = $category_names | ForEach-Object { Get-CategoryMapping -category_name $_ }
$common_functions = { $common_functions = {
Function Write-DebugLog($msg) { Function Write-DebugLog($msg) {
@ -59,7 +55,7 @@ $update_script_block = {
Function Start-Updates { Function Start-Updates {
Param( Param(
$category_guids, $category_names,
$log_path, $log_path,
$state, $state,
$blacklist, $blacklist,
@ -90,19 +86,12 @@ $update_script_block = {
return $result return $result
} }
# OR is only allowed at the top-level, so we have to repeat base criteria inside Write-DebugLog -msg "Searching for updates to install"
# FUTURE: change this to client-side filtered?
$criteria_base = "IsInstalled = 0"
$criteria_list = $category_guids | ForEach-Object { "($criteria_base AND CategoryIds contains '$_') " }
$criteria = [string]::Join(" OR", $criteria_list)
Write-DebugLog -msg "Search criteria: $criteria"
Write-DebugLog -msg "Searching for updates to install in category Ids $category_guids..."
try { try {
$search_result = $searcher.Search($criteria) $search_result = $searcher.Search("IsInstalled = 0")
} catch { } catch {
$result.failed = $true $result.failed = $true
$result.msg = "Failed to search for updates with criteria '$criteria': $($_.Exception.Message)" $result.msg = "Failed to search for updates: $($_.Exception.Message)"
return $result return $result
} }
Write-DebugLog -msg "Found $($search_result.Updates.Count) updates" Write-DebugLog -msg "Found $($search_result.Updates.Count) updates"
@ -123,10 +112,10 @@ $update_script_block = {
kb = $update.KBArticleIDs kb = $update.KBArticleIDs
id = $update.Identity.UpdateId id = $update.Identity.UpdateId
installed = $false installed = $false
categories = ($update.Categories | ForEach-Object { $_.Name })
} }
# validate update again blacklist/whitelist # validate update again blacklist/whitelist/post_category_names/hidden
$skipped = $false
$whitelist_match = $false $whitelist_match = $false
foreach ($whitelist_entry in $whitelist) { foreach ($whitelist_entry in $whitelist) {
if ($update_info.title -imatch $whitelist_entry) { if ($update_info.title -imatch $whitelist_entry) {
@ -142,32 +131,51 @@ $update_script_block = {
} }
if ($whitelist.Length -gt 0 -and -not $whitelist_match) { if ($whitelist.Length -gt 0 -and -not $whitelist_match) {
Write-DebugLog -msg "Skipping update $($update_info.id) - $($update_info.title) as it was not found in the whitelist" Write-DebugLog -msg "Skipping update $($update_info.id) - $($update_info.title) as it was not found in the whitelist"
$skipped = $true $update_info.filtered_reason = "whitelist"
}
foreach ($kb in $update_info.kb) {
if ("KB$kb" -imatch $blacklist_entry) {
$kb_match = $true
}
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 $result.filtered_updates[$update_info.id] = $update_info
continue continue
} }
$blacklist_match = $false
foreach ($blacklist_entry in $blacklist) {
if ($update_info.title -imatch $blacklist_entry) {
$blacklist_match = $true
break
}
foreach ($kb in $update_info.kb) {
if ("KB$kb" -imatch $blacklist_entry) {
$blacklist_match = $true
break
}
}
}
if ($blacklist_match) {
Write-DebugLog -msg "Skipping update $($update_info.id) - $($update_info.title) as it was found in the blacklist"
$update_info.filtered_reason = "blacklist"
$result.filtered_updates[$update_info.id] = $update_info
continue
}
if ($update.IsHidden) {
Write-DebugLog -msg "Skipping update $($update_info.title) as it was hidden"
$update_info.filtered_reason = "skip_hidden"
$result.filtered_updates[$update_info.id] = $update_info
continue
}
$category_match = $false
foreach ($match_cat in $category_names) {
if ($update_info.categories -ieq $match_cat) {
$category_match = $true
break
}
}
if ($category_names.Length -gt 0 -and -not $category_match) {
Write-DebugLog -msg "Skipping update $($update_info.id) - $($update_info.title) as it was not found in the category names filter"
$update_info.filtered_reason = "category_names"
$result.filtered_updates[$update_info.id] = $update_info
continue
}
if (-not $update.EulaAccepted) { if (-not $update.EulaAccepted) {
Write-DebugLog -msg "Accepting EULA for $($update_info.id)" Write-DebugLog -msg "Accepting EULA for $($update_info.id)"
@ -180,11 +188,6 @@ $update_script_block = {
} }
} }
if ($update.IsHidden) {
Write-DebugLog -msg "Skipping hidden update $($update_info.title)"
continue
}
Write-DebugLog -msg "Adding update $($update_info.id) - $($update_info.title)" Write-DebugLog -msg "Adding update $($update_info.id) - $($update_info.title)"
$updates_to_install.Add($update) > $null $updates_to_install.Add($update) > $null
@ -275,8 +278,8 @@ $update_script_block = {
} }
Write-DebugLog -msg "Installing updates..." Write-DebugLog -msg "Installing updates..."
# install as a batch so the reboot manager will suppress intermediate reboots # install as a batch so the reboot manager will suppress intermediate reboots
Write-DebugLog -msg "Creating installer object..." Write-DebugLog -msg "Creating installer object..."
try { try {
$installer = $session.CreateUpdateInstaller() $installer = $session.CreateUpdateInstaller()
@ -389,7 +392,7 @@ Function Start-Natively($common_functions, $script) {
# add the update script block and required parameters # add the update script block and required parameters
$ps_pipeline.AddStatement().AddScript($script) > $null $ps_pipeline.AddStatement().AddScript($script) > $null
$ps_pipeline.AddParameter("arguments", @{ $ps_pipeline.AddParameter("arguments", @{
category_guids = $category_guids category_names = $category_names
log_path = $log_path log_path = $log_path
state = $state state = $state
blacklist = $blacklist blacklist = $blacklist
@ -450,7 +453,7 @@ Function Start-AsScheduledTask($common_functions, $script) {
Name = $job_name Name = $job_name
ArgumentList = @( ArgumentList = @(
@{ @{
category_guids = $category_guids category_names = $category_names
log_path = $log_path log_path = $log_path
state = $state state = $state
blacklist = $blacklist blacklist = $blacklist
@ -553,3 +556,4 @@ if ($wua_available) {
} }
Exit-Json -obj $result Exit-Json -obj $result

View file

@ -33,22 +33,14 @@ options:
version_added: '2.5' version_added: '2.5'
category_names: category_names:
description: description:
- A scalar or list of categories to install updates from - A scalar or list of categories to install updates from. To get the list
of categories, run the module with C(state=searched). The category must
be the full category string, but is case insensitive.
- Some possible categories are Application, Connectors, Critical Updates,
Definition Updates, Developer Kits, Feature Packs, Guidance, Security
Updates, Service Packs, Tools, Update Rollups and Updates.
type: list type: list
default: [ CriticalUpdates, SecurityUpdates, UpdateRollups ] default: [ CriticalUpdates, SecurityUpdates, UpdateRollups ]
choices:
- Application
- Connectors
- CriticalUpdates
- DefinitionUpdates
- DeveloperKits
- FeaturePacks
- Guidance
- SecurityUpdates
- ServicePacks
- Tools
- UpdateRollups
- Updates
reboot: reboot:
description: description:
- Ansible will automatically reboot the remote host if it is required - Ansible will automatically reboot the remote host if it is required
@ -191,6 +183,11 @@ updates:
returned: always returned: always
type: boolean type: boolean
sample: True sample: True
categories:
description: A list of category strings for this update
returned: always
type: list of strings
sample: [ 'Critical Updates', 'Windows Server 2012 R2' ]
failure_hresult_code: failure_hresult_code:
description: The HRESULT code from a failed update description: The HRESULT code from a failed update
returned: on install failure returned: on install failure
@ -199,12 +196,17 @@ updates:
filtered_updates: filtered_updates:
description: List of updates that were found but were filtered based on 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(blacklist), I(whitelist) or I(category_names). The return value is in
I(updates). the same form as I(updates), along with I(filtered_reason).
returned: success returned: success
type: complex type: complex
sample: see the updates return value sample: see the updates return value
contains: {} contains:
filtered_reason:
description: The reason why this update was filtered
returned: always
type: string
sample: 'skip_hidden'
found_update_count: found_update_count:
description: The number of updates found needing to be applied description: The number of updates found needing to be applied

View file

@ -20,26 +20,6 @@ class ActionModule(ActionBase):
DEFAULT_REBOOT_TIMEOUT = 1200 DEFAULT_REBOOT_TIMEOUT = 1200
def _validate_categories(self, category_names):
valid_categories = [
'Application',
'Connectors',
'CriticalUpdates',
'DefinitionUpdates',
'DeveloperKits',
'FeaturePacks',
'Guidance',
'SecurityUpdates',
'ServicePacks',
'Tools',
'UpdateRollups',
'Updates'
]
for name in category_names:
if name not in valid_categories:
raise AnsibleError("Unknown category_name %s, must be one of "
"(%s)" % (name, ','.join(valid_categories)))
def _run_win_updates(self, module_args, task_vars, use_task): def _run_win_updates(self, module_args, task_vars, use_task):
display.vvv("win_updates: running win_updates module") display.vvv("win_updates: running win_updates module")
wrap_async = self._task.async_val wrap_async = self._task.async_val
@ -172,14 +152,6 @@ class ActionModule(ActionBase):
use_task = boolean(self._task.args.get('use_scheduled_task', False), use_task = boolean(self._task.args.get('use_scheduled_task', False),
strict=False) strict=False)
# Validate the options
try:
self._validate_categories(category_names)
except AnsibleError as exc:
result['failed'] = True
result['msg'] = to_text(exc)
return result
if state not in ['installed', 'searched']: if state not in ['installed', 'searched']:
result['failed'] = True result['failed'] = True
result['msg'] = "state must be either installed or searched" result['msg'] = "state must be either installed or searched"

View file

@ -5,14 +5,6 @@
register: invalid_state register: invalid_state
failed_when: invalid_state.msg != 'state must be either installed or searched' failed_when: invalid_state.msg != 'state must be either installed or searched'
- name: expect failure with invalid category name
win_updates:
state: searched
category_names:
- Invalid
register: invalid_category_name
failed_when: invalid_category_name.msg != 'Unknown category_name Invalid, must be one of (Application,Connectors,CriticalUpdates,DefinitionUpdates,DeveloperKits,FeaturePacks,Guidance,SecurityUpdates,ServicePacks,Tools,UpdateRollups,Updates)'
- name: ensure log file not present before tests - name: ensure log file not present before tests
win_file: win_file:
path: '{{win_updates_dir}}/update.log' path: '{{win_updates_dir}}/update.log'

View file

@ -16,14 +16,6 @@ from ansible.playbook.task import Task
class TestWinUpdatesActionPlugin(object): class TestWinUpdatesActionPlugin(object):
INVALID_OPTIONS = ( INVALID_OPTIONS = (
(
{"category_names": ["fake category"]},
False,
"Unknown category_name fake category, must be one of (Application,"
"Connectors,CriticalUpdates,DefinitionUpdates,DeveloperKits,"
"FeaturePacks,Guidance,SecurityUpdates,ServicePacks,Tools,"
"UpdateRollups,Updates)"
),
( (
{"state": "invalid"}, {"state": "invalid"},
False, False,