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.parsing.yaml.objects import AnsibleUnicode 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, use_task): display.vvv("win_updates: running win_updates module") wrap_async = self._task.async_val result = self._execute_module_with_become(module_name='win_updates', module_args=module_args, task_vars=task_vars, wrap_async=wrap_async, use_task=use_task) return result def _reboot_server(self, task_vars, reboot_timeout, use_task): 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']) # only run this if the user has specified we can only use scheduled # tasks, the win_shell command requires become and will be skipped if # become isn't available to use if use_task: display.vvv("win_updates: skipping WUA is not busy check as " "use_scheduled_task=True is set") else: 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 } try: shell_result = self._execute_module_with_become( module_name='win_shell', module_args=shell_module_args, task_vars=task_vars, wrap_async=False, use_task=use_task ) 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)) 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 _execute_module_with_become(self, module_name, module_args, task_vars, wrap_async, use_task): orig_become = self._play_context.become orig_become_method = self._play_context.become_method orig_become_user = self._play_context.become_user\ if not use_task: 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 orig_become_user == 'root': self._play_context.become_user = 'SYSTEM' try: module_res = self._execute_module(module_name=module_name, module_args=module_args, task_vars=task_vars, wrap_async=wrap_async) finally: self._play_context.become = orig_become self._play_context.become_method = orig_become_method self._play_context.become_user = orig_become_user return module_res 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) del tmp # tmp no longer has any effect category_names = self._task.args.get('category_names', [ 'CriticalUpdates', 'SecurityUpdates', 'UpdateRollups', ]) if isinstance(category_names, AnsibleUnicode): category_names = [cat.strip() for cat in category_names.split(",")] 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) use_task = boolean(self._task.args.get('use_scheduled_task', 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']: 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, use_task) # if the module failed to run at all then changed won't be populated # so we just return the result as is # https://github.com/ansible/ansible/issues/38232 failed = result.get('failed', False) if ("updates" not in result.keys() and self._task.async_val == 0) or failed: result['failed'] = True return result changed = result.get('changed', False) 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) # 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, use_task) 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, use_task) if result.get('failed', False): return result 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']: 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['filtered_updates'] = filtered_updates result['found_update_count'] = found_update_count result['installed_update_count'] = installed_update_count return result