--- - name: Check Package Updates with Risk Assessment hosts: all gather_facts: true vars: openai_api_key: "{{ lookup('env', 'OPENAI_API_KEY') }}" openai_api_url: "https://api.openai.com/v1/chat/completions" openai_model: "gpt-4o" output_file: "/tmp/update_report_{{ ansible_date_time.iso8601_basic_short }}.json" temp_update_file: "/tmp/available_updates.json" tasks: - name: Validate OpenAI API key is present fail: msg: "OPENAI_API_KEY environment variable is required" when: openai_api_key | length == 0 - name: Detect OS family and set package manager set_fact: pkg_mgr: "{{ 'apt' if ansible_os_family == 'Debian' else 'apk' if ansible_os_family == 'Alpine' else 'unknown' }}" - name: Update package cache (Debian/Ubuntu) apt: update_cache: true cache_valid_time: 3600 when: ansible_os_family == 'Debian' - name: Update package cache (Alpine) apk: update_cache: true when: ansible_os_family == 'Alpine' - name: List upgradable packages (Debian/Ubuntu) shell: apt list --upgradable 2>/dev/null | tail -n +2 | awk -F'/' '{print $1 "\t" $2}' register: upgradable_debian changed_when: false when: ansible_os_family == 'Debian' - name: List upgradable packages (Alpine) shell: apk version -l '<' register: upgradable_alpine changed_when: false when: ansible_os_family == 'Alpine' - name: Parse upgradable packages (Debian/Ubuntu) set_fact: upgradable_packages: >- {{ upgradable_debian.stdout.split('\n') | select('match', '^.+\t.+$') | map('regex_replace', '^(.+?)\\t(.+)$', '{\"name\": \"\\1\", \"new_version\": \"\\2\"}') | map('from_json') | list }} when: ansible_os_family == 'Debian' - name: Parse upgradable packages (Alpine) set_fact: upgradable_packages: >- {{ upgradable_alpine.stdout.split('\n') | select('match', '^.+\s+<\s+.+$') | map('regex_replace', '^(.+?)\\s+<\\s+(.+)$', '{\"name\": \"\\1\", \"new_version\": \"\\2\"}') | map('from_json') | list }} when: ansible_os_family == 'Alpine' - name: Get current versions of upgradable packages (Debian/Ubuntu) shell: dpkg-query -W -f='${Package}\t${Version}\n' {{ item.name }} register: current_versions_debian changed_when: false loop: "{{ upgradable_packages }}" loop_control: loop_var: item when: ansible_os_family == 'Debian' - name: Get current versions of upgradable packages (Alpine) shell: apk info -vv | grep "{{ item.name }}-" | awk '{print $1}' register: current_versions_alpine changed_when: false loop: "{{ upgradable_packages }}" loop_control: loop_var: item when: ansible_os_family == 'Alpine' - name: Build complete package update list (Debian/Ubuntu) set_fact: package_update_list: >- {{ current_versions_debian.results | map(attribute='stdout') | zip(upgradable_packages) | map('regex_replace', '^(.+?)\\t(.+)$', '{\"name\": \"\\1\", \"current_version\": \"\\2\"}') | map('from_json') | product(upgradable_packages) | map('combine') | list }} when: ansible_os_family == 'Debian' - name: Build complete package update list (Alpine) set_fact: package_update_list: >- {{ current_versions_alpine.results | map(attribute='stdout') | zip(upgradable_packages) | map('regex_replace', '^(.+?)-([0-9].+)$', '{\"name\": \"\\1\", \"current_version\": \"\\2\"}') | map('from_json') | product(upgradable_packages) | map('combine') | list }} when: ansible_os_family == 'Alpine' - name: Prepare package list for OpenAI analysis set_fact: packages_for_analysis: >- {{ package_update_list | map('to_json') | join('\n') }} - name: Create OpenAI prompt for risk assessment set_fact: openai_prompt: >- Analyze the following package updates for potential breaking changes or disruptions. Identify which packages might cause issues based on version changes. Return a JSON array with package names and a boolean "risk" field (true if risky, false if safe). Packages: {{ packages_for_analysis }} - name: Send request to OpenAI for risk assessment uri: url: "{{ openai_api_url }}" method: POST headers: Authorization: "Bearer {{ openai_api_key }}" Content-Type: "application/json" body_format: json body: model: "{{ openai_model }}" messages: - role: system content: "You are a package update risk assessment assistant. Analyze package updates and identify potential breaking changes or disruptions. Return only valid JSON." - role: user content: "{{ openai_prompt }}" temperature: 0.3 response_format: { "type": "json_object" } register: openai_response until: openai_response.status == 200 retries: 3 delay: 5 failed_when: openai_response.status != 200 - name: Parse OpenAI risk assessment set_fact: risk_assessment: "{{ openai_response.json.choices[0].message.content | from_json }}" - name: Merge risk assessment with package list set_fact: packages_with_risk: >- {{ package_update_list | map('combine', {'risk': risk_assessment | selectattr('name', 'equalto', item.name) | map(attribute='risk') | first | default(false)}) | list }} loop: "{{ package_update_list }}" loop_control: loop_var: item - name: Separate safe and risky packages set_fact: safe_updates: "{{ packages_with_risk | selectattr('risk', 'equalto', false) | list }}" risky_updates: "{{ packages_with_risk | selectattr('risk', 'equalto', true) | list }}" - name: Generate update report copy: dest: "{{ output_file }}" content: >- { "hostname": "{{ ansible_hostname }}", "ip_address": "{{ ansible_default_ipv4.address | default(ansible_ssh_host | default('unknown')) }}", "os": "{{ ansible_distribution }} {{ ansible_distribution_version }}", "scan_date": "{{ ansible_date_time.iso8601 }}", "total_updatable_packages": {{ packages_with_risk | length }}, "safe_updates_count": {{ safe_updates | length }}, "risky_updates_count": {{ risky_updates | length }}, "safe_updates": {{ safe_updates | to_json }}, "risky_updates": {{ risky_updates | to_json }}, "can_proceed_with_update": {{ risky_updates | length == 0 }} } mode: '0600' - name: Display update summary debug: msg: - "Total upgradable packages: {{ packages_with_risk | length }}" - "Safe updates: {{ safe_updates | length }}" - "Risky updates: {{ risky_updates | length }}" - "Can proceed with automatic update: {{ risky_updates | length == 0 }}" - "Report saved to: {{ output_file }}" - name: Return update findings set_fact: update_report: hostname: ansible_hostname ip_address: ansible_default_ipv4.address | default(ansible_ssh_host | default('unknown')) os: ansible_distribution + ' ' + ansible_distribution_version total_updatable_packages: packages_with_risk | length safe_updates: safe_updates risky_updates: risky_updates can_proceed_with_update: risky_updates | length == 0 scan_date: ansible_date_time.iso8601 report_file: output_file