--- - 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({}) }}" # event_data keys are dicts {host: count}; sum values safely sumdict: >- {% set ns = namespace(t=0) -%} {%- for v in (item | default({})).values() -%}{%- set ns.t = ns.t + (v|int) -%}{%- endfor -%} {{ ns.t }} set_fact: recap_ok: "{{ sumdict | from_yaml(item=data.ok) }}" recap_changed: "{{ sumdict | from_yaml(item=data.changed) }}" recap_failed: "{{ sumdict | from_yaml(item=data.failures) }}" recap_skipped: "{{ sumdict | from_yaml(item=data.skipped) }}" recap_unreach: "{{ sumdict | from_yaml(item=data.dark) }}" vars_prompt: [] # no prompts; just here to keep YAML tidy - name: Build SharePoint recap line run_once: 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: Verify siteId resolves delegate_to: localhost run_once: true uri: url: "https://graph.microsoft.com/v1.0/sites/{{ site_id }}" method: GET headers: Authorization: "Bearer {{ graph_token.json.access_token }}" return_content: true status_code: 200 register: site_probe no_log: true - name: List lists to confirm listId (name + id) delegate_to: localhost run_once: true uri: url: "https://graph.microsoft.com/v1.0/sites/{{ site_id }}/lists?$select=id,displayName" method: GET headers: Authorization: "Bearer {{ graph_token.json.access_token }}" return_content: true status_code: 200 register: lists_probe no_log: false - name: Show lists (sanitized) run_once: true debug: msg: "{{ (lists_probe.json.value | default([])) | map(attribute='displayName') | list }}" - name: Inspect columns (internal names) delegate_to: localhost run_once: true uri: url: "https://graph.microsoft.com/v1.0/sites/{{ site_id }}/lists/{{ list_id }}/columns?$select=name,displayName" method: GET headers: Authorization: "Bearer {{ graph_token.json.access_token }}" return_content: true status_code: 200 register: cols_probe no_log: false - name: Print internal names run_once: true debug: var: cols_probe.json.value | map(attribute='name') | list - name: Show internal column names (safe) debug: msg: names: "{{ (cols_probe.json.value | default([])) | map(attribute='name') | list }}" - name: Create SharePoint list item (Graph) delegate_to: localhost run_once: true block: - 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 }}" RunStart: "{{ run_start }}" RunEnd: "{{ run_end }}" Notes: |- {{ summary_text }} Recap: {{ recap_line }} register: sp_create no_log: false rescue: - name: Sanitize and print the error run_once: true 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. - fail: msg: "Failed to create SharePoint item (see previous message)." - name: Show created list item id run_once: true debug: var: sp_create.json.id