win_updates: action plugin-ify it (#33216)

* converted win_updates to an action plugin for automatic reboots

* do not set final result when running under async

* Updated documentation around win_updates with async and become
This commit is contained in:
Jordan Borean 2018-01-11 14:41:52 +10:00 committed by GitHub
commit 557716dc49
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 315 additions and 24 deletions

View file

@ -267,15 +267,15 @@ foreach ($update in $updates_to_install) {
} }
} }
if ($update_fail_count -gt 0) {
Fail-Json -obj $result -msg "Failed to install one or more updates"
}
Write-DebugLog -msg "Performing post-install reboot requirement check..." Write-DebugLog -msg "Performing post-install reboot requirement check..."
$result.reboot_required = Get-RebootStatus $result.reboot_required = Get-RebootStatus
$result.installed_update_count = $update_success_count $result.installed_update_count = $update_success_count
$result.failed_update_count = $update_fail_count $result.failed_update_count = $update_fail_count
if ($update_fail_count -gt 0) {
Fail-Json -obj $result -msg "Failed to install one or more updates"
}
Write-DebugLog -msg "Return value:`r`n$(ConvertTo-Json -InputObject $result -Depth 99)" Write-DebugLog -msg "Return value:`r`n$(ConvertTo-Json -InputObject $result -Depth 99)"
Exit-Json $result Exit-Json $result

View file

@ -1,22 +1,9 @@
#!/usr/bin/python #!/usr/bin/python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# (c) 2015, Matt Davis <mdavis_ansible@rolpdog.com> # Copyright (c) 2015, Matt Davis <mdavis_ansible@rolpdog.com>
# # Copyright (c) 2017 Ansible Project
# This file is part of Ansible # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
# this is a windows documentation stub. actual code lives in the .ps1 # this is a windows documentation stub. actual code lives in the .ps1
# file of the same name # file of the same name
@ -52,6 +39,23 @@ options:
- Tools - Tools
- UpdateRollups - UpdateRollups
- Updates - Updates
reboot:
description:
- Ansible will automatically reboot the remote host if it is required
and continue to install updates after the reboot.
- This can be used instead of using a M(win_reboot) task after this one
and ensures all updates for that category is installed in one go.
- Async does not work when C(reboot=True).
type: bool
default: 'no'
version_added: '2.5'
reboot_timeout:
description:
- The time in seconds to wait until the host is back online from a
reboot.
- This is only used if C(reboot=True) and a reboot is required.
default: 1200
version_added: '2.5'
state: state:
description: description:
- Controls whether found updates are returned as a list or actually installed. - Controls whether found updates are returned as a list or actually installed.
@ -67,9 +71,11 @@ options:
required: false required: false
author: "Matt Davis (@nitzmahone)" author: "Matt Davis (@nitzmahone)"
notes: notes:
- C(win_updates) must be run by a user with membership in the local Administrators group - C(win_updates) must be run by a user with membership in the local Administrators group.
- C(win_updates) will use the default update service configured for the machine (Windows Update, Microsoft Update, WSUS, etc) - C(win_updates) will use the default update service configured for the machine (Windows Update, Microsoft Update, WSUS, etc).
- C(win_updates) does not manage reboots, but will signal when a reboot is required with the reboot_required return value. - By default C(win_updates) does not manage reboots, but will signal when a
reboot is required with the I(reboot_required) return value, as of Ansible 2.5
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). - 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. Performance depends on many factors, including OS version, number of updates, system load, and update server load.
''' '''
@ -91,6 +97,43 @@ EXAMPLES = r'''
category_names: SecurityUpdates category_names: SecurityUpdates
state: searched state: searched
log_path: c:\ansible_wu.txt log_path: c:\ansible_wu.txt
- name: Install all security updates with automatic reboots
win_updates:
category_names:
- 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: Search for Windows updates asynchronously
win_updates:
category_names:
- SecurityUpdates
state: searched
async: 180
poll: 10
register: updates_to_install
become: yes
become_method: runas
become_user: SYSTEM
# Async can also be run in the background in a fire and forget fashion
- name: Search for Windows updates asynchronously (poll and forget)
win_updates:
category_names:
- SecurityUpdates
state: searched
async: 180
poll: 0
register: updates_to_install_async
- name: get status of Windows Update async job
async_status:
jid: '{{ updates_to_install_async.ansible_job_id }}'
register: updates_to_install_result
become: yes
become_method: runas
become_user: SYSTEM
''' '''
RETURN = r''' RETURN = r'''

View file

@ -0,0 +1,248 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import json
from ansible.errors import AnsibleError
from ansible.module_utils._text import to_text
from ansible.module_utils.parsing.convert_bool import boolean
from ansible.plugins.action import ActionBase
try:
from __main__ import display
except ImportError:
from ansible.utils.display import Display
display = Display()
class ActionModule(ActionBase):
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):
display.vvv("win_updates: running win_updates module")
result = self._execute_module(module_name='win_updates',
module_args=module_args,
task_vars=task_vars,
wrap_async=self._task.async_val)
return result
def _reboot_server(self, task_vars, reboot_timeout):
display.vvv("win_updates: rebooting remote host after update install")
reboot_args = {
'reboot_timeout': reboot_timeout
}
reboot_result = self._run_action_plugin('win_reboot', task_vars,
module_args=reboot_args)
if reboot_result.get('failed', False):
raise AnsibleError(reboot_result['msg'])
display.vvv("win_updates: checking WUA is not busy with win_shell "
"command")
# While this always returns False after a reboot it doesn't return a
# value until Windows is actually ready and finished installing updates
# This needs to run with become as WUA doesn't work over WinRM
# Ignore connection errors as another reboot can happen
command = "(New-Object -ComObject Microsoft.Update.Session)." \
"CreateUpdateInstaller().IsBusy"
shell_module_args = {
'_raw_params': command
}
# run win_shell module with become and ignore any errors in case of
# a windows reboot during execution
orig_become = self._play_context.become
orig_become_method = self._play_context.become_method
orig_become_user = self._play_context.become_user
if orig_become is None or orig_become is False:
self._play_context.become = True
if orig_become_method != 'runas':
self._play_context.become_method = 'runas'
if orig_become_user is None or 'root':
self._play_context.become_user = 'SYSTEM'
try:
shell_result = self._execute_module(module_name='win_shell',
module_args=shell_module_args,
task_vars=task_vars)
display.vvv("win_updates: shell wait results: %s"
% json.dumps(shell_result))
except Exception as exc:
display.debug("win_updates: Fatal error when running shell "
"command, attempting to recover: %s" % to_text(exc))
finally:
self._play_context.become = orig_become
self._play_context.become_method = orig_become_method
self._play_context.become_user = orig_become_user
display.vvv("win_updates: ensure the connection is up and running")
# in case Windows needs to reboot again after the updates, we wait for
# the connection to be stable again
wait_for_result = self._run_action_plugin('wait_for_connection',
task_vars)
if wait_for_result.get('failed', False):
raise AnsibleError(wait_for_result['msg'])
def _run_action_plugin(self, plugin_name, task_vars, module_args=None):
# Create new task object and reset the args
new_task = self._task.copy()
new_task.args = {}
if module_args is not None:
for key, value in module_args.items():
new_task.args[key] = value
# run the action plugin and return the results
action = self._shared_loader_obj.action_loader.get(
plugin_name,
task=new_task,
connection=self._connection,
play_context=self._play_context,
loader=self._loader,
templar=self._templar,
shared_loader_obj=self._shared_loader_obj
)
return action.run(task_vars=task_vars)
def _merge_dict(self, original, new):
dict_var = original.copy()
dict_var.update(new)
return dict_var
def run(self, tmp=None, task_vars=None):
self._supports_check_mode = True
self._supports_async = True
result = super(ActionModule, self).run(tmp, task_vars)
category_names = self._task.args.get('category_names', [
'CriticalUpdates',
'SecurityUpdates',
'UpdateRollups',
])
state = self._task.args.get('state', 'installed')
reboot = self._task.args.get('reboot', False)
reboot_timeout = self._task.args.get('reboot_timeout',
self.DEFAULT_REBOOT_TIMEOUT)
# 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']:
result['failed'] = True
result['msg'] = "state must be either installed or searched"
return result
try:
reboot = boolean(reboot)
except TypeError as exc:
result['failed'] = True
result['msg'] = "cannot parse reboot as a boolean: %s" % to_text(exc)
return result
if not isinstance(reboot_timeout, int):
result['failed'] = True
result['msg'] = "reboot_timeout must be an integer"
return result
if reboot and self._task.async_val > 0:
result['failed'] = True
result['msg'] = "async is not supported for this task when " \
"reboot=yes"
return result
# Run the module
new_module_args = self._task.args.copy()
new_module_args.pop('reboot', None)
new_module_args.pop('reboot_timeout', None)
result = self._run_win_updates(new_module_args, task_vars)
changed = result['changed']
updates = result.get('updates', dict())
found_update_count = result.get('found_update_count', 0)
installed_update_count = result.get('installed_update_count', 0)
# Handle automatic reboots if the reboot flag is set
if reboot and state == 'installed' and not \
self._play_context.check_mode:
previously_errored = False
while result['installed_update_count'] > 0 or \
result['found_update_count'] > 0 or \
result['reboot_required'] is True:
display.vvv("win_updates: check win_updates results for "
"automatic reboot: %s" % json.dumps(result))
# check if the module failed, break from the loop if it
# previously failed and return error to the user
if result.get('failed', False):
if previously_errored:
break
previously_errored = True
else:
previously_errored = False
reboot_error = None
# check if a reboot was required before installing the updates
if result.get('msg', '') == "A reboot is required before " \
"more updates can be installed":
reboot_error = "reboot was required before more updates " \
"can be installed"
if result.get('reboot_required', False):
if reboot_error is None:
reboot_error = "reboot was required to finalise " \
"update install"
try:
changed = True
self._reboot_server(task_vars, reboot_timeout)
except AnsibleError as exc:
result['failed'] = True
result['msg'] = "Failed to reboot remote host when " \
"%s: %s" \
% (reboot_error, to_text(exc))
break
result.pop('msg', None)
# rerun the win_updates module after the reboot is complete
result = self._run_win_updates(new_module_args, task_vars)
result_updates = result.get('updates', dict())
updates = self._merge_dict(updates, result_updates)
found_update_count += result.get('found_update_count', 0)
installed_update_count += result.get('installed_update_count', 0)
if result['changed']:
changed = True
# finally create the return dict based on the aggregated execution
# values if we are not in async
if self._task.async_val == 0:
result['changed'] = changed
result['updates'] = updates
result['found_update_count'] = found_update_count
result['installed_update_count'] = installed_update_count
return result

View file

@ -3,7 +3,7 @@
win_updates: win_updates:
state: invalid state: invalid
register: invalid_state register: invalid_state
failed_when: "invalid_state.msg != 'Get-AnsibleParam: Argument state needs to be one of installed,searched but was invalid.'" failed_when: invalid_state.msg != 'state must be either installed or searched'
- name: expect failure with invalid category name - name: expect failure with invalid category name
win_updates: win_updates: