From 2e6b21b4951512245253219b4744582bd0fb1a69 Mon Sep 17 00:00:00 2001 From: mhorak Date: Thu, 14 Aug 2025 08:16:17 +0000 Subject: [PATCH] Update www-install-win-updates.yaml --- www-install-win-updates.yaml | 144 ++++++++++++++++++++++++++++++++++- 1 file changed, 143 insertions(+), 1 deletion(-) diff --git a/www-install-win-updates.yaml b/www-install-win-updates.yaml index c187465..48570aa 100644 --- a/www-install-win-updates.yaml +++ b/www-install-win-updates.yaml @@ -118,4 +118,146 @@ 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) \ No newline at end of file + - (kb_numbers is not defined or kb_numbers | length == 0) + +- name: Post patching results to SharePoint (Graph) + hosts: windows + 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') }}" + + # Helpful AWX vars (exist in AWX/Controller job context) + job_id: "{{ tower_job_id | default('n/a') }}" + job_name: "{{ tower_job_template_name | default('Patch run') }}" + job_url: "{{ tower_job_url | default('') }}" + status: "{{ (tower_job_failed | default(false)) | ternary('failed','successful') }}" + + # Timestamps (avoid ansible_date_time since gather_facts: false) + 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 }} {{ status }}. + Template={{ job_name }}. + URL={{ job_url }}. + + tasks: + - name: Acquire Graph token (client credentials) + delegate_to: localhost + run_once: true + 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] + + # --- Get recap from AWX and turn it into flat totals --- + - name: Fetch play recap (playbook_on_stats) + delegate_to: localhost + run_once: true + uri: + url: "{{ lookup('env','AWX_API_URL') }}/api/v2/jobs/{{ tower_job_id }}/job_events/?event=playbook_on_stats" + method: GET + headers: + Authorization: "Bearer {{ lookup('env','AWX_API_TOKEN') }}" + Content-Type: "application/json" + return_content: true + status_code: 200 + register: _recap + no_log: true + + - name: Parse recap totals + run_once: true + vars: + stats: "{{ (_recap.json.results | default([])) | first | default({}) }}" + data: "{{ stats.event_data | default({}) }}" + set_fact: + recap_ok: "{{ (data.ok | default({})) | dict2items | map(attribute='value') | map('int') | sum }}" + recap_changed: "{{ (data.changed | default({})) | dict2items | map(attribute='value') | map('int') | sum }}" + recap_failed: "{{ (data.failures | default({})) | dict2items | map(attribute='value') | map('int') | sum }}" + recap_skipped: "{{ (data.skipped | default({})) | dict2items | map(attribute='value') | map('int') | sum }}" + recap_unreach: "{{ (data.dark | default({})) | dict2items | map(attribute='value') | map('int') | sum }}" + + - name: Build SharePoint recap line (store on localhost for later use) + run_once: true + delegate_to: localhost + delegate_facts: true + set_fact: + recap_line: >- + OK={{ recap_ok | default(0) }}, + Changed={{ recap_changed | default(0) }}, + Failed={{ recap_failed | default(0) }}, + Skipped={{ recap_skipped | default(0) }}, + Unreachable={{ recap_unreach | default(0) }} + + - name: Build final status from recap + run_once: true + set_fact: + status_final: "{{ 'failed' if (recap_failed | int > 0 or recap_unreach | int > 0) else 'successful' }}" + + # --- Create list item (no block/rescue; use ignore_errors + diagnostics) --- + - name: Create SharePoint list item (Graph) + delegate_to: localhost + run_once: true + 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 }} + Recap: {{ hostvars['localhost'].recap_line }} + register: sp_create + ignore_errors: true + no_log: true + + - name: Show sanitized Graph error (if any) + run_once: true + 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'))) }} + hint: > + 400: column internal names; 401: scope/audience; 403: permissions; + 404: siteId/listId. + + - name: Fail if SharePoint item was not created + run_once: true + when: sp_create is failed + fail: + msg: "Failed to create SharePoint item (see previous message)." + + - name: Show created list item id + run_once: true + when: sp_create is succeeded + debug: + var: sp_create.json.id \ No newline at end of file