From 34d1b93288d90ce06d498793a150383ec5dcba19 Mon Sep 17 00:00:00 2001 From: mhorak Date: Thu, 14 Aug 2025 09:00:45 +0000 Subject: [PATCH] Update win-updates-troubleshooting.yaml --- win-updates-troubleshooting.yaml | 272 +++++++++++++++++++++++++++++++ 1 file changed, 272 insertions(+) diff --git a/win-updates-troubleshooting.yaml b/win-updates-troubleshooting.yaml index e69de29..890ad3a 100644 --- a/win-updates-troubleshooting.yaml +++ b/win-updates-troubleshooting.yaml @@ -0,0 +1,272 @@ +--- +- name: Windows Update Installation from Assessment Report + hosts: windows + gather_facts: no + + vars: + # ===== Toggle these to TEST failure handling ===== + simulate_failure: false # set to true to mark ALL hosts as failed + simulate_failure_hosts: ['SRW2025AWX03'] # or list specific hosts, e.g. ['SERVER01','SERVER02'] + simulate_failed_update_count: 3 # optional: pretend N failed updates on simulated hosts + # ===========================AWX===================== + + tasks: + - name: Get current timestamp + set_fact: + current_timestamp: "{{ lookup('pipe', 'date +%Y-%m-%dT%H:%M:%S') }}" + + - name: Check if KB updates report file exists + win_stat: + path: 'C:\Temp\windows_updates_with_kb.txt' + register: kb_updates_file + + - name: Fail if updates report file is missing + fail: + msg: 'KB updates report file not found at C:\Temp\windows_updates_with_kb.txt. Please run the assessment playbook first.' + when: not kb_updates_file.stat.exists + + - name: Read KB updates report content + win_shell: Get-Content -Path 'C:\Temp\windows_updates_with_kb.txt' + register: updates_content + when: kb_updates_file.stat.exists + + - name: Extract KB numbers from report file + set_fact: + kb_numbers: "{{ updates_content.stdout_lines + | select('match', '.*KB: .*') + | map('regex_replace', '.*KB: ([0-9,\\s]+).*', '\\1') + | map('split', ',') | flatten | map('trim') + | select('match', '^[0-9]+$') | list | unique }}" + when: + - kb_updates_file.stat.exists + - updates_content.stdout_lines is defined + + - name: Display KB numbers to be installed + debug: + msg: + - "Found {{ kb_numbers | length }} unique KB numbers to install:" + - "{{ kb_numbers | join(', ') }}" + when: + - kb_updates_file.stat.exists + - kb_numbers is defined + - kb_numbers | length > 0 + + # ---- Patch with failure capture (block/rescue) ---- + - name: Install Windows updates by KB numbers (with failure capture) + block: + - name: Install Windows updates by KB numbers + win_updates: + category_names: '*' + state: installed + accept_list: "{{ kb_numbers }}" + log_path: 'C:\Temp\windows_update_installation.log' + register: installation_result + when: + - kb_updates_file.stat.exists + - kb_numbers is defined + - kb_numbers | length > 0 + + - name: Mark host failed if any KB installs failed (partial failures) + set_fact: + patch_failed_host: "{{ (installation_result.failed_update_count | default(0) | int) > 0 }}" + patch_failed_count: "{{ installation_result.failed_update_count | default(0) | int }}" + when: installation_result is defined + + rescue: + - name: Mark host failed because win_updates task error/exception + set_fact: + patch_failed_host: true + patch_failed_count: "{{ (patch_failed_count | default(0) | int) + 1 }}" + + always: + - name: Ensure failure flags exist (default to false/0) + set_fact: + patch_failed_host: "{{ patch_failed_host | default(false) }}" + patch_failed_count: "{{ patch_failed_count | default(0) | int }}" + + # ---- FAILURE SIMULATION (enable via vars above) ---- + - name: Simulate patch failure (all hosts or specific hosts) + set_fact: + patch_failed_host: true + patch_failed_count: >- + {{ (simulate_failed_update_count | int) + if (simulate_failed_update_count | int) > 0 + else ((patch_failed_count | default(0) | int) + 1) }} + when: + - simulate_failure | bool or (inventory_hostname in simulate_failure_hosts) + + - name: Display installation summary + debug: + msg: + - "=== WINDOWS UPDATE INSTALLATION COMPLETE ===" + - "Host: {{ inventory_hostname }}" + - "Updates Found: {{ installation_result.found_update_count | default(0) }}" + - "Updates Installed: {{ installation_result.installed_update_count | default(0) }}" + - "Updates Failed: {{ installation_result.failed_update_count | default(0) }}" + - "Reboot Required: {{ 'Yes' if installation_result.reboot_required | default(false) else 'No' }}" + - "Patch failed flag: {{ patch_failed_host | default(false) }}" + when: + - kb_updates_file.stat.exists + - kb_numbers is defined + - kb_numbers | length > 0 + + - name: Reboot if required + win_reboot: + reboot_timeout: 1800 + when: installation_result is defined and (installation_result.reboot_required | default(false)) + + - name: Create installation report + set_fact: + installation_summary: | + Windows Update Installation Report + ================================= + Host: {{ inventory_hostname }} + Date: {{ current_timestamp }} + + Summary: + -------- + Total Updates Found: {{ installation_result.found_update_count | default(0) }} + Successfully Installed: {{ installation_result.installed_update_count | default(0) }} + Failed Installations: {{ installation_result.failed_update_count | default(0) }} + Reboot Required: {{ installation_result.reboot_required | default('No') }} + + Requested KB Numbers: {{ kb_numbers | default([]) | join(', ') }} + + {% if installation_result is defined and installation_result.updates is defined %} + Installed Updates: + ----------------- + {% for update_id, update_info in installation_result.updates.items() %} + - {{ update_info.title }} + KB: {{ update_info.kb | join(', ') if update_info.kb else 'None' }} + {% endfor %} + {% endif %} + when: kb_updates_file.stat.exists + + - name: Save installation report to file + win_copy: + content: "{{ installation_summary }}" + dest: 'C:\Temp\windows_update_installation_report.txt' + when: installation_summary is defined + + - name: Give a report when no KB numbers were found on updates + debug: + msg: "No valid KB numbers found in the updates report file. Please verify the assessment report." + when: + - kb_updates_file.stat.exists + - (kb_numbers is not defined or kb_numbers | length == 0) + + # ---- Aggregate & publish facts to localhost for next play ---- + - name: Aggregate per-host patch failures and publish to localhost + run_once: true + delegate_to: localhost + delegate_facts: true + set_fact: + failed_hosts_list: [] + + - name: Collect failed hosts (no extract filter) + run_once: true + delegate_to: localhost + delegate_facts: true + set_fact: + failed_hosts_list: "{{ failed_hosts_list + [item] }}" + loop: "{{ ansible_play_hosts_all }}" + when: hostvars[item].patch_failed_host | default(false) + + - name: Publish aggregate flags to localhost + run_once: true + delegate_to: localhost + delegate_facts: true + set_fact: + any_patch_failed: "{{ (failed_hosts_list | length) > 0 }}" + failed_hosts_csv: "{{ failed_hosts_list | join(', ') if failed_hosts_list | length > 0 else 'None' }}" + +# ------------------------------------------------------------------------------ + +- name: Post patching results to SharePoint (Graph) + hosts: localhost + connection: local + gather_facts: false + + vars: + tenant_id: "{{ lookup('env', 'SP_TENANT_ID') }}" + client_id: "{{ lookup('env', 'SP_CLIENT_ID') }}" + client_secret: "{{ lookup('env', 'SP_CLIENT_SECRET') }}" + site_id: "{{ lookup('env', 'SP_SITE_ID') }}" + list_id: "{{ lookup('env', 'SP_LIST_ID') }}" + + job_id: "{{ tower_job_id | default('n/a') }}" + job_name: "{{ tower_job_template_name | default('Patch run') }}" + job_url: "{{ tower_job_url | default('') }}" + + run_start: "{{ lookup('pipe','date -u +%Y-%m-%dT%H:%M:%SZ') }}" + run_end: "{{ lookup('pipe','date -u +%Y-%m-%dT%H:%M:%SZ') }}" + + summary_text: >- + Job {{ job_id }}. + Template={{ job_name }}. + URL={{ job_url }}. + + tasks: + - name: Build final status from published facts + set_fact: + status_final: "{{ 'failed' if (hostvars['localhost'].any_patch_failed | default(false)) else 'successful' }}" + + - name: Acquire Graph token (client credentials) + uri: + url: "https://login.microsoftonline.com/{{ tenant_id }}/oauth2/v2.0/token" + method: POST + headers: + Content-Type: "application/x-www-form-urlencoded" + body: > + client_id={{ client_id }} + &client_secret={{ client_secret | urlencode }} + &scope=https%3A%2F%2Fgraph.microsoft.com%2F.default + &grant_type=client_credentials + register: graph_token + no_log: true + failed_when: graph_token.status not in [200] + + - name: Create SharePoint list item (Graph) + uri: + url: "https://graph.microsoft.com/v1.0/sites/{{ site_id }}/lists/{{ list_id }}/items" + method: POST + headers: + Authorization: "Bearer {{ graph_token.json.access_token }}" + Content-Type: "application/json" + body_format: json + return_content: true + status_code: [200, 201] + body: + fields: + Title: "{{ job_name }} ({{ job_id }})" + Status: "{{ status_final }}" + RunStart: "{{ run_start }}" + RunEnd: "{{ run_end }}" + Notes: |- + {{ summary_text }} + Failed hosts: {{ hostvars['localhost'].failed_hosts_csv | default('None') }} + register: sp_create + ignore_errors: true + no_log: true + + - name: Show sanitized Graph error (if any) + when: sp_create is failed + vars: + _json: "{{ sp_create.json | default({}) }}" + debug: + msg: + status: "{{ sp_create.status | default('n/a') }}" + graph_error: >- + {{ _json.error.message + | default(_json.message + | default(sp_create.msg | default('Unknown error'))) }} + + - name: Fail if SharePoint item was not created + when: sp_create is failed + fail: + msg: "Failed to create SharePoint item (see previous message)." + + - name: Show created list item id + when: sp_create is succeeded + debug: + var: sp_create.json.id