From 73a1424d0beb2c1ba7e872046e4d7ec0b7de91dd Mon Sep 17 00:00:00 2001 From: sfulmer Date: Wed, 15 Apr 2026 10:55:31 -0400 Subject: [PATCH 01/14] feat(vm-consumption-report): add VM consumption reporting role MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move from openshift_virtualization_migration repo — this is day 2 ops functionality for generating consumption reports of running OCP VMs. Co-Authored-By: Claude Opus 4.6 --- playbooks/vm_consumption_report.yml | 15 +++ roles/vm_consumption_report/README.md | 66 ++++++++++ roles/vm_consumption_report/defaults/main.yml | 66 ++++++++++ roles/vm_consumption_report/meta/main.yml | 13 ++ .../tasks/_build_report.yml | 114 ++++++++++++++++++ .../tasks/_collect_vmis.yml | 18 +++ .../tasks/_collect_vms.yml | 30 +++++ roles/vm_consumption_report/tasks/main.yml | 59 +++++++++ .../templates/report.csv.j2 | 7 ++ 9 files changed, 388 insertions(+) create mode 100644 playbooks/vm_consumption_report.yml create mode 100644 roles/vm_consumption_report/README.md create mode 100644 roles/vm_consumption_report/defaults/main.yml create mode 100644 roles/vm_consumption_report/meta/main.yml create mode 100644 roles/vm_consumption_report/tasks/_build_report.yml create mode 100644 roles/vm_consumption_report/tasks/_collect_vmis.yml create mode 100644 roles/vm_consumption_report/tasks/_collect_vms.yml create mode 100644 roles/vm_consumption_report/tasks/main.yml create mode 100644 roles/vm_consumption_report/templates/report.csv.j2 diff --git a/playbooks/vm_consumption_report.yml b/playbooks/vm_consumption_report.yml new file mode 100644 index 0000000..c489f58 --- /dev/null +++ b/playbooks/vm_consumption_report.yml @@ -0,0 +1,15 @@ +--- + +- name: Generate VM Consumption Report + hosts: localhost + connection: local + gather_facts: false + tasks: + - name: Invoke VM Consumption Report Role + ansible.builtin.include_role: + name: infra.openshift_virtualization_ops.vm_consumption_report + vars: + openshift_host: "{{ lookup('ansible.builtin.env', 'K8S_AUTH_HOST', default=Undefined) | default('', True) }}" + openshift_api_key: "{{ lookup('ansible.builtin.env', 'K8S_AUTH_API_KEY', default=Undefined) | default('', True) }}" # noqa: yaml[line-length] + openshift_verify_ssl: "{{ lookup('ansible.builtin.env', 'K8S_AUTH_VERIFY_SSL', default='') | default(false) | bool }}" # noqa: yaml[line-length] +... diff --git a/roles/vm_consumption_report/README.md b/roles/vm_consumption_report/README.md new file mode 100644 index 0000000..7bb173a --- /dev/null +++ b/roles/vm_consumption_report/README.md @@ -0,0 +1,66 @@ +# vm_consumption_report + +Generate consumption reports for OpenShift Virtualization VMs managed +via the `redhat.openshift_virtualization` collection. The report +distinguishes between **directly managed hosts** (contacted via SSH or +WinRM) and **indirectly managed VMs** (automated via the Kubernetes +API), and supports **reconciliation** of hosts that are managed through +both methods. + +## Requirements + +- `kubernetes.core` collection +- OpenShift API access with permission to list VirtualMachine and + VirtualMachineInstance resources + +## Role Variables + +| Variable | Required | Default | Description | +|---|---|---|---| +| `vm_consumption_report_request` | yes | `[]` | List of query entries (namespace, names, label_selectors) | +| `vm_consumption_report_direct_hosts` | no | `[]` | List of directly managed hosts for reconciliation | +| `vm_consumption_report_openshift_host` | yes | `{{ openshift_host }}` | OpenShift API endpoint | +| `vm_consumption_report_openshift_api_key` | yes | `{{ openshift_api_key }}` | OpenShift API token | +| `vm_consumption_report_openshift_verify_ssl` | yes | `{{ openshift_verify_ssl }}` | Verify SSL certificates | +| `vm_consumption_report_output_format` | no | `json` | Output format: `json` or `csv` | +| `vm_consumption_report_output_path` | no | `""` | File path to write the report | +| `vm_consumption_report_include_status` | no | `true` | Query VMIs for running status and guest OS info | + +## Output + +The role sets `vm_consumption_report_result` containing: + +- `summary` — Counts of total VMs, indirect-only, direct-only, and reconciled +- `indirect_vms` — List of OpenShift VMs with UUID, namespace, status, guest hostname, and management type +- `direct_only_hosts` — Hosts that are directly managed but not matched to any OpenShift VM + +## Reconciliation + +When `vm_consumption_report_direct_hosts` is provided, the role +matches direct hosts to OpenShift VMs by: + +1. **UUID match** — Direct host UUID matches VM `metadata.uid` +2. **Hostname match** — Direct host hostname matches the guest OS + hostname reported by the VMI +3. **IP match** — Direct host hostname matches an IP address on a VMI + network interface + +Matched entries are marked `management_type: reconciled`. + +## Example + +```yaml +vm_consumption_report_request: + - namespace: production-vms + - namespace: staging-vms + label_selectors: + - app=web + +vm_consumption_report_direct_hosts: + - hostname: web-server-01.example.com + - hostname: db-server-02.example.com + uuid: 5a3f8c2e-1234-5678-9abc-def012345678 + +vm_consumption_report_output_format: json +vm_consumption_report_output_path: /tmp/consumption_report.json +``` diff --git a/roles/vm_consumption_report/defaults/main.yml b/roles/vm_consumption_report/defaults/main.yml new file mode 100644 index 0000000..d17e731 --- /dev/null +++ b/roles/vm_consumption_report/defaults/main.yml @@ -0,0 +1,66 @@ +--- +# defaults file for vm_consumption_report + +# title: Consumption Report Request +# required: True +# description: >- +# List of namespaces (or all namespaces) to include in the +# consumption report. Each entry may filter by namespace, names, +# or label selectors. +vm_consumption_report_request: [] +# - namespace: # Namespace to query (omit for all namespaces) +# names: # Optional list of VM names to include +# - +# label_selectors: # Optional label selectors (cannot be combined with names) +# - = + +# title: Directly Managed Hosts +# required: False +# description: >- +# List of directly managed hosts (contacted via SSH/WinRM) with +# their hostnames for reconciliation against indirectly managed +# OpenShift VMs. Each entry should include hostname and optionally +# a UUID if known. +vm_consumption_report_direct_hosts: [] +# - hostname: # Hostname or IP used by AAP/SSH +# uuid: # Optional UUID if already known + +# title: OpenShift host +# required: True +# description: OpenShift host +vm_consumption_report_openshift_host: "{{ openshift_host }}" + +# title: OpenShift API Key +# required: True +# description: OpenShift API Key +vm_consumption_report_openshift_api_key: "{{ openshift_api_key }}" + +# title: Verify SSL Certificate +# required: True +# description: Variable to enable SSL verification +vm_consumption_report_openshift_verify_ssl: "{{ openshift_verify_ssl }}" + +# title: KubeVirt API Version +# required: True +# description: KubeVirt API Version +vm_consumption_report_kubevirt_api_version: kubevirt.io/v1 + +# title: Report Output Format +# required: True +# description: Output format for the consumption report (json or csv) +vm_consumption_report_output_format: json + +# title: Report Output Path +# required: False +# description: >- +# File path to write the consumption report. If not set, the report +# is returned as a variable only. +vm_consumption_report_output_path: "" + +# title: Include Running Status +# required: True +# description: >- +# When true, queries VirtualMachineInstance resources to determine +# current running state and guest OS info for reconciliation. +vm_consumption_report_include_status: true +... diff --git a/roles/vm_consumption_report/meta/main.yml b/roles/vm_consumption_report/meta/main.yml new file mode 100644 index 0000000..c194d05 --- /dev/null +++ b/roles/vm_consumption_report/meta/main.yml @@ -0,0 +1,13 @@ +--- +galaxy_info: + author: "" + description: >- + Generate consumption reports for OpenShift Virtualization VMs, + distinguishing directly and indirectly managed hosts with + UUID-based reconciliation. + company: Red Hat + license: GPL-3.0-only + min_ansible_version: 2.15.0 + galaxy_tags: [] +dependencies: [] +... diff --git a/roles/vm_consumption_report/tasks/_build_report.yml b/roles/vm_consumption_report/tasks/_build_report.yml new file mode 100644 index 0000000..5b24ec2 --- /dev/null +++ b/roles/vm_consumption_report/tasks/_build_report.yml @@ -0,0 +1,114 @@ +--- + +- name: Build VM Entries + ansible.builtin.set_fact: + _vm_consumption_report_entries: >- + {{ _vm_consumption_report_entries | default([]) + [_entry] }} + vars: + _vmi_match: >- + {{ _vm_consumption_report_vmis + | selectattr('metadata.namespace', 'equalto', _vm.metadata.namespace) + | selectattr('metadata.name', 'equalto', _vm.metadata.name) + | list | first | default({}) }} + _vm_uuid: "{{ _vm.metadata.uid }}" + _guest_hostname: >- + {{ _vmi_match.status.guestOSInfo.hostname | default('') }} + _guest_ip_addresses: >- + {{ _vmi_match.status.interfaces + | default([]) + | map(attribute='ipAddress') + | select('defined') + | list }} + _direct_match: >- + {{ vm_consumption_report_direct_hosts + | selectattr('uuid', 'defined') + | selectattr('uuid', 'equalto', _vm_uuid) + | list + + (vm_consumption_report_direct_hosts + | selectattr('hostname', 'defined') + | selectattr('hostname', 'equalto', _guest_hostname) + | list if _guest_hostname | length > 0 else []) + + (vm_consumption_report_direct_hosts + | selectattr('hostname', 'defined') + | selectattr('hostname', 'in', _guest_ip_addresses) + | list if _guest_ip_addresses | length > 0 else []) }} + _management_type: >- + {{ 'reconciled' if _direct_match | length > 0 + else 'indirect' }} + _entry: + name: "{{ _vm.metadata.name }}" + namespace: "{{ _vm.metadata.namespace }}" + uuid: "{{ _vm_uuid }}" + management_type: "{{ _management_type }}" + running: "{{ _vm.spec.running | default(_vm.spec.runStrategy | default('Unknown')) }}" + created_at: "{{ _vm.metadata.creationTimestamp }}" + last_transition: >- + {{ _vm.status.conditions + | default([]) + | sort(attribute='lastTransitionTime') + | map(attribute='lastTransitionTime') + | list | last | default(_vm.metadata.creationTimestamp) }} + guest_hostname: "{{ _guest_hostname }}" + ip_addresses: "{{ _guest_ip_addresses }}" + direct_host_match: "{{ _direct_match | first | default({}) }}" + labels: "{{ _vm.metadata.labels | default({}) }}" + loop: "{{ _vm_consumption_report_vms }}" + loop_control: + loop_var: _vm + label: "{{ _vm.metadata.namespace }}/{{ _vm.metadata.name }}" + +- name: Build Direct-Only Entries + ansible.builtin.set_fact: + _vm_consumption_report_direct_only: >- + {{ _vm_consumption_report_direct_only | default([]) + [_direct_entry] }} + vars: + _matched_uuids: >- + {{ (_vm_consumption_report_entries | default([])) + | selectattr('management_type', 'equalto', 'reconciled') + | map(attribute='direct_host_match') + | select('mapping') + | map(attribute='hostname', default='') + | list }} + _matched_hosts: >- + {{ (_vm_consumption_report_entries | default([])) + | selectattr('management_type', 'equalto', 'reconciled') + | map(attribute='direct_host_match') + | select('mapping') + | map(attribute='hostname', default='') + | list }} + _direct_entry: + hostname: "{{ _host.hostname }}" + uuid: "{{ _host.uuid | default('') }}" + management_type: "direct" + loop: "{{ vm_consumption_report_direct_hosts }}" + loop_control: + loop_var: _host + label: "{{ _host.hostname }}" + when: + - _host.hostname not in _matched_hosts + - (_host.uuid | default('')) not in ((_vm_consumption_report_entries | default([])) | map(attribute='uuid') | list) + +- name: Compile Final Report + ansible.builtin.set_fact: + vm_consumption_report_result: + generated_at: "{{ now(utc=true, fmt='%Y-%m-%dT%H:%M:%SZ') }}" + cluster: "{{ vm_consumption_report_openshift_host }}" + summary: + total_vms: "{{ (_vm_consumption_report_entries | default([])) | length }}" + indirect_only: >- + {{ (_vm_consumption_report_entries | default([])) + | selectattr('management_type', 'equalto', 'indirect') + | list | length }} + direct_only: "{{ (_vm_consumption_report_direct_only | default([])) | length }}" + reconciled: >- + {{ (_vm_consumption_report_entries | default([])) + | selectattr('management_type', 'equalto', 'reconciled') + | list | length }} + total_managed_hosts: >- + {{ (_vm_consumption_report_entries | default([])) + | length + + (_vm_consumption_report_direct_only | default([])) + | length }} + indirect_vms: "{{ _vm_consumption_report_entries | default([]) }}" + direct_only_hosts: "{{ _vm_consumption_report_direct_only | default([]) }}" +... diff --git a/roles/vm_consumption_report/tasks/_collect_vmis.yml b/roles/vm_consumption_report/tasks/_collect_vmis.yml new file mode 100644 index 0000000..f6d686c --- /dev/null +++ b/roles/vm_consumption_report/tasks/_collect_vmis.yml @@ -0,0 +1,18 @@ +--- + +- name: "Query VirtualMachineInstances — {{ _vm_consumption_report_query.namespace | default('all-namespaces') }}" + kubernetes.core.k8s_info: + api_key: "{{ vm_consumption_report_openshift_api_key }}" + host: "{{ vm_consumption_report_openshift_host }}" + api_version: "{{ vm_consumption_report_kubevirt_api_version }}" + kind: VirtualMachineInstance + namespace: "{{ _vm_consumption_report_query.namespace | default(omit) }}" + label_selectors: "{{ _vm_consumption_report_query.label_selectors | default(omit) }}" + validate_certs: "{{ vm_consumption_report_openshift_verify_ssl }}" + register: _vm_consumption_report_vmi_response + +- name: Append VMIs to Collection + ansible.builtin.set_fact: + _vm_consumption_report_vmis: >- + {{ _vm_consumption_report_vmis + _vm_consumption_report_vmi_response.resources }} +... diff --git a/roles/vm_consumption_report/tasks/_collect_vms.yml b/roles/vm_consumption_report/tasks/_collect_vms.yml new file mode 100644 index 0000000..86a1a3b --- /dev/null +++ b/roles/vm_consumption_report/tasks/_collect_vms.yml @@ -0,0 +1,30 @@ +--- + +- name: "Query VirtualMachines — {{ _vm_consumption_report_query.namespace | default('all-namespaces') }}" + kubernetes.core.k8s_info: + api_key: "{{ vm_consumption_report_openshift_api_key }}" + host: "{{ vm_consumption_report_openshift_host }}" + api_version: "{{ vm_consumption_report_kubevirt_api_version }}" + kind: VirtualMachine + namespace: "{{ _vm_consumption_report_query.namespace | default(omit) }}" + label_selectors: "{{ _vm_consumption_report_query.label_selectors | default(omit) }}" + validate_certs: "{{ vm_consumption_report_openshift_verify_ssl }}" + register: _vm_consumption_report_vm_response + +- name: Filter VMs by Name (if specified) + ansible.builtin.set_fact: + _vm_consumption_report_filtered: >- + {{ _vm_consumption_report_vm_response.resources + | selectattr('metadata.name', 'in', _vm_consumption_report_query.names) + | list }} + when: "'names' in _vm_consumption_report_query" + +- name: Use All Returned VMs (no name filter) + ansible.builtin.set_fact: + _vm_consumption_report_filtered: "{{ _vm_consumption_report_vm_response.resources }}" + when: "'names' not in _vm_consumption_report_query" + +- name: Append VMs to Collection + ansible.builtin.set_fact: + _vm_consumption_report_vms: "{{ _vm_consumption_report_vms + _vm_consumption_report_filtered }}" +... diff --git a/roles/vm_consumption_report/tasks/main.yml b/roles/vm_consumption_report/tasks/main.yml new file mode 100644 index 0000000..8ad6808 --- /dev/null +++ b/roles/vm_consumption_report/tasks/main.yml @@ -0,0 +1,59 @@ +--- + +- name: Verify Request Variable Provided + ansible.builtin.assert: + that: + - vm_consumption_report_request | default([], true) | length > 0 + fail_msg: "'vm_consumption_report_request' must contain at least one entry" + quiet: true + +- name: Initialize Report Data + ansible.builtin.set_fact: + _vm_consumption_report_vms: [] + _vm_consumption_report_vmis: [] + +- name: Collect VirtualMachines + ansible.builtin.include_tasks: _collect_vms.yml + loop: "{{ vm_consumption_report_request }}" + loop_control: + loop_var: _vm_consumption_report_query + label: "{{ _vm_consumption_report_query.namespace | default('all-namespaces') }}" + +- name: Collect VirtualMachineInstances for Running Status + ansible.builtin.include_tasks: _collect_vmis.yml + loop: "{{ vm_consumption_report_request }}" + loop_control: + loop_var: _vm_consumption_report_query + label: "{{ _vm_consumption_report_query.namespace | default('all-namespaces') }}" + when: vm_consumption_report_include_status | bool + +- name: Build Consumption Report + ansible.builtin.include_tasks: _build_report.yml + +- name: Write Report to File + ansible.builtin.copy: + content: "{{ vm_consumption_report_result | to_nice_json }}" + dest: "{{ vm_consumption_report_output_path }}" + mode: "0644" + when: + - vm_consumption_report_output_path | default("", true) | length > 0 + - vm_consumption_report_output_format == "json" + +- name: Write CSV Report to File + ansible.builtin.template: + src: report.csv.j2 + dest: "{{ vm_consumption_report_output_path }}" + mode: "0644" + when: + - vm_consumption_report_output_path | default("", true) | length > 0 + - vm_consumption_report_output_format == "csv" + +- name: Display Report Summary + ansible.builtin.debug: + msg: + total_vms: "{{ vm_consumption_report_result.summary.total_vms }}" + indirect_only: "{{ vm_consumption_report_result.summary.indirect_only }}" + direct_only: "{{ vm_consumption_report_result.summary.direct_only }}" + reconciled: "{{ vm_consumption_report_result.summary.reconciled }}" + report_generated: "{{ vm_consumption_report_result.generated_at }}" +... diff --git a/roles/vm_consumption_report/templates/report.csv.j2 b/roles/vm_consumption_report/templates/report.csv.j2 new file mode 100644 index 0000000..2d52ec7 --- /dev/null +++ b/roles/vm_consumption_report/templates/report.csv.j2 @@ -0,0 +1,7 @@ +type,name,namespace,uuid,management_type,running,created_at,last_transition,guest_hostname,ip_addresses +{% for vm in vm_consumption_report_result.indirect_vms %} +vm,{{ vm.name }},{{ vm.namespace }},{{ vm.uuid }},{{ vm.management_type }},{{ vm.running }},{{ vm.created_at }},{{ vm.last_transition }},{{ vm.guest_hostname }},"{{ vm.ip_addresses | join(';') }}" +{% endfor %} +{% for host in vm_consumption_report_result.direct_only_hosts %} +host,{{ host.hostname }},,{{ host.uuid }},{{ host.management_type }},,,,{{ host.hostname }}, +{% endfor %} From f4688c7dbe20d876b2451b8c3ea407a966879432 Mon Sep 17 00:00:00 2001 From: sfulmer Date: Wed, 15 Apr 2026 12:34:28 -0400 Subject: [PATCH 02/14] fix(vm-consumption-report): add name[prefix] to included task files Add filename prefix to task names in _collect_vms.yml, _collect_vmis.yml, and _build_report.yml to satisfy ansible-lint name[prefix] rule. Co-Authored-By: Claude Opus 4.6 --- roles/vm_consumption_report/tasks/_build_report.yml | 6 +++--- roles/vm_consumption_report/tasks/_collect_vmis.yml | 4 ++-- roles/vm_consumption_report/tasks/_collect_vms.yml | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/roles/vm_consumption_report/tasks/_build_report.yml b/roles/vm_consumption_report/tasks/_build_report.yml index 5b24ec2..ae1a0b4 100644 --- a/roles/vm_consumption_report/tasks/_build_report.yml +++ b/roles/vm_consumption_report/tasks/_build_report.yml @@ -1,6 +1,6 @@ --- -- name: Build VM Entries +- name: _build_report | Build VM Entries ansible.builtin.set_fact: _vm_consumption_report_entries: >- {{ _vm_consumption_report_entries | default([]) + [_entry] }} @@ -57,7 +57,7 @@ loop_var: _vm label: "{{ _vm.metadata.namespace }}/{{ _vm.metadata.name }}" -- name: Build Direct-Only Entries +- name: _build_report | Build Direct-Only Entries ansible.builtin.set_fact: _vm_consumption_report_direct_only: >- {{ _vm_consumption_report_direct_only | default([]) + [_direct_entry] }} @@ -88,7 +88,7 @@ - _host.hostname not in _matched_hosts - (_host.uuid | default('')) not in ((_vm_consumption_report_entries | default([])) | map(attribute='uuid') | list) -- name: Compile Final Report +- name: _build_report | Compile Final Report ansible.builtin.set_fact: vm_consumption_report_result: generated_at: "{{ now(utc=true, fmt='%Y-%m-%dT%H:%M:%SZ') }}" diff --git a/roles/vm_consumption_report/tasks/_collect_vmis.yml b/roles/vm_consumption_report/tasks/_collect_vmis.yml index f6d686c..bca7e70 100644 --- a/roles/vm_consumption_report/tasks/_collect_vmis.yml +++ b/roles/vm_consumption_report/tasks/_collect_vmis.yml @@ -1,6 +1,6 @@ --- -- name: "Query VirtualMachineInstances — {{ _vm_consumption_report_query.namespace | default('all-namespaces') }}" +- name: "_collect_vmis | Query VirtualMachineInstances — {{ _vm_consumption_report_query.namespace | default('all-namespaces') }}" kubernetes.core.k8s_info: api_key: "{{ vm_consumption_report_openshift_api_key }}" host: "{{ vm_consumption_report_openshift_host }}" @@ -11,7 +11,7 @@ validate_certs: "{{ vm_consumption_report_openshift_verify_ssl }}" register: _vm_consumption_report_vmi_response -- name: Append VMIs to Collection +- name: _collect_vmis | Append VMIs to Collection ansible.builtin.set_fact: _vm_consumption_report_vmis: >- {{ _vm_consumption_report_vmis + _vm_consumption_report_vmi_response.resources }} diff --git a/roles/vm_consumption_report/tasks/_collect_vms.yml b/roles/vm_consumption_report/tasks/_collect_vms.yml index 86a1a3b..2b83968 100644 --- a/roles/vm_consumption_report/tasks/_collect_vms.yml +++ b/roles/vm_consumption_report/tasks/_collect_vms.yml @@ -1,6 +1,6 @@ --- -- name: "Query VirtualMachines — {{ _vm_consumption_report_query.namespace | default('all-namespaces') }}" +- name: "_collect_vms | Query VirtualMachines — {{ _vm_consumption_report_query.namespace | default('all-namespaces') }}" kubernetes.core.k8s_info: api_key: "{{ vm_consumption_report_openshift_api_key }}" host: "{{ vm_consumption_report_openshift_host }}" @@ -11,7 +11,7 @@ validate_certs: "{{ vm_consumption_report_openshift_verify_ssl }}" register: _vm_consumption_report_vm_response -- name: Filter VMs by Name (if specified) +- name: _collect_vms | Filter VMs by Name (if specified) ansible.builtin.set_fact: _vm_consumption_report_filtered: >- {{ _vm_consumption_report_vm_response.resources @@ -19,12 +19,12 @@ | list }} when: "'names' in _vm_consumption_report_query" -- name: Use All Returned VMs (no name filter) +- name: _collect_vms | Use All Returned VMs (no name filter) ansible.builtin.set_fact: _vm_consumption_report_filtered: "{{ _vm_consumption_report_vm_response.resources }}" when: "'names' not in _vm_consumption_report_query" -- name: Append VMs to Collection +- name: _collect_vms | Append VMs to Collection ansible.builtin.set_fact: _vm_consumption_report_vms: "{{ _vm_consumption_report_vms + _vm_consumption_report_filtered }}" ... From c5422c5368f06e6c03f9ea2fe5a872dabfb0a5af Mon Sep 17 00:00:00 2001 From: sfulmer Date: Sat, 2 May 2026 09:47:08 -0400 Subject: [PATCH 03/14] fix(vm-consumption-report): wrap long task names to pass yaml[line-length] --- roles/vm_consumption_report/tasks/_collect_vmis.yml | 4 +++- roles/vm_consumption_report/tasks/_collect_vms.yml | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/roles/vm_consumption_report/tasks/_collect_vmis.yml b/roles/vm_consumption_report/tasks/_collect_vmis.yml index bca7e70..5052425 100644 --- a/roles/vm_consumption_report/tasks/_collect_vmis.yml +++ b/roles/vm_consumption_report/tasks/_collect_vmis.yml @@ -1,6 +1,8 @@ --- -- name: "_collect_vmis | Query VirtualMachineInstances — {{ _vm_consumption_report_query.namespace | default('all-namespaces') }}" +- name: >- + _collect_vmis | Query VMIs — + {{ _vm_consumption_report_query.namespace | default('all-namespaces') }} kubernetes.core.k8s_info: api_key: "{{ vm_consumption_report_openshift_api_key }}" host: "{{ vm_consumption_report_openshift_host }}" diff --git a/roles/vm_consumption_report/tasks/_collect_vms.yml b/roles/vm_consumption_report/tasks/_collect_vms.yml index 2b83968..3919868 100644 --- a/roles/vm_consumption_report/tasks/_collect_vms.yml +++ b/roles/vm_consumption_report/tasks/_collect_vms.yml @@ -1,6 +1,8 @@ --- -- name: "_collect_vms | Query VirtualMachines — {{ _vm_consumption_report_query.namespace | default('all-namespaces') }}" +- name: >- + _collect_vms | Query VMs — + {{ _vm_consumption_report_query.namespace | default('all-namespaces') }} kubernetes.core.k8s_info: api_key: "{{ vm_consumption_report_openshift_api_key }}" host: "{{ vm_consumption_report_openshift_host }}" From 95f76d4d421034f08c8611862a20c7494fe789cb Mon Sep 17 00:00:00 2001 From: sfulmer Date: Sat, 2 May 2026 09:51:56 -0400 Subject: [PATCH 04/14] fix: add missing allowlist_externals for tox-ansible environments --- tox-ansible.ini | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tox-ansible.ini b/tox-ansible.ini index 3e1b6d9..27d44ea 100644 --- a/tox-ansible.ini +++ b/tox-ansible.ini @@ -31,6 +31,14 @@ allowlist_externals = mkdir ln bash + echo + git + ansible-galaxy + ansible-test + ansible-doc + pytest + mkdir + ln commands = mkdir -p "{env:HOME}/.ansible/collections/ansible_collections" ansible-galaxy collection install "{toxinidir}" -p '{env:HOME}/.ansible/collections/ansible_collections' --force @@ -50,6 +58,14 @@ change_dir = {env:HOME}/.ansible/collections/ansible_collections/infra/openshift skip_install = false allowlist_externals = bash + echo + git + ansible-galaxy + ansible-test + ansible-doc + pytest + mkdir + ln commands = bash -c 'git init --initial-branch=main .' bash -c 'ansible-test integration' From 2a0d6f498240b558431af63a7b319b3eb6eb43e2 Mon Sep 17 00:00:00 2001 From: sfulmer Date: Sat, 2 May 2026 10:00:34 -0400 Subject: [PATCH 05/14] fix: let tox-ansible manage test commands instead of overriding --- tox-ansible.ini | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/tox-ansible.ini b/tox-ansible.ini index 27d44ea..5c79a79 100644 --- a/tox-ansible.ini +++ b/tox-ansible.ini @@ -46,15 +46,13 @@ commands = [testenv] set_env = - COLLECTIONS_PATH = "{env:HOME}/.ansible/collections/ansible_collections" + COLLECTIONS_PATH = {env:HOME}/.ansible/collections/ansible_collections FORCE_COLOR = 1 passenv = HOME ANSIBLE_COLLECTIONS_PATH + ANSIBLE_GALAXY_* no_package = true -deps = - -r tests/integration/requirements.txt -change_dir = {env:HOME}/.ansible/collections/ansible_collections/infra/openshift_virtualization_ops skip_install = false allowlist_externals = bash @@ -66,6 +64,5 @@ allowlist_externals = pytest mkdir ln -commands = - bash -c 'git init --initial-branch=main .' - bash -c 'ansible-test integration' + sh + ade From b80d3de7f32e4e4a5ad50ab6491a23637aab62c3 Mon Sep 17 00:00:00 2001 From: sfulmer Date: Sat, 2 May 2026 10:11:18 -0400 Subject: [PATCH 06/14] fix: add distlib to integration requirements for manifest support --- tests/integration/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/requirements.txt b/tests/integration/requirements.txt index 8b3ac1e..04708bc 100644 --- a/tests/integration/requirements.txt +++ b/tests/integration/requirements.txt @@ -1 +1,2 @@ # Add python packages that are required for integration testing +distlib From 4fb2a1a4a2a5018fefab543e5e58f460d0b99025 Mon Sep 17 00:00:00 2001 From: sfulmer Date: Sat, 2 May 2026 10:22:21 -0400 Subject: [PATCH 07/14] fix: add distlib to testenv deps for galaxy/sanity manifest support --- tox-ansible.ini | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tox-ansible.ini b/tox-ansible.ini index 5c79a79..e02eef9 100644 --- a/tox-ansible.ini +++ b/tox-ansible.ini @@ -31,14 +31,6 @@ allowlist_externals = mkdir ln bash - echo - git - ansible-galaxy - ansible-test - ansible-doc - pytest - mkdir - ln commands = mkdir -p "{env:HOME}/.ansible/collections/ansible_collections" ansible-galaxy collection install "{toxinidir}" -p '{env:HOME}/.ansible/collections/ansible_collections' --force @@ -54,6 +46,8 @@ passenv = ANSIBLE_GALAXY_* no_package = true skip_install = false +deps = + distlib allowlist_externals = bash echo From eebcb0dfc058112a73fbafd7e368ae4a0bb3f036 Mon Sep 17 00:00:00 2001 From: sfulmer Date: Sat, 2 May 2026 10:26:52 -0400 Subject: [PATCH 08/14] =?UTF-8?q?fix:=20remove=20testenv=20overrides=20?= =?UTF-8?q?=E2=80=94=20only=20set=20allowlist=5Fexternals,=20let=20tox-ans?= =?UTF-8?q?ible=20manage=20deps/commands?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tox-ansible.ini | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/tox-ansible.ini b/tox-ansible.ini index e02eef9..5e0966b 100644 --- a/tox-ansible.ini +++ b/tox-ansible.ini @@ -37,17 +37,6 @@ commands = ansible-galaxy collection install -r "{toxinidir}/requirements-dev.yml" [testenv] -set_env = - COLLECTIONS_PATH = {env:HOME}/.ansible/collections/ansible_collections - FORCE_COLOR = 1 -passenv = - HOME - ANSIBLE_COLLECTIONS_PATH - ANSIBLE_GALAXY_* -no_package = true -skip_install = false -deps = - distlib allowlist_externals = bash echo From cc7ba8176ec47a0f89957ea22f7226066dec470f Mon Sep 17 00:00:00 2001 From: sfulmer Date: Sat, 2 May 2026 10:37:43 -0400 Subject: [PATCH 09/14] fix: add distlib to galaxy tox env for manifest support --- tox-ansible.ini | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tox-ansible.ini b/tox-ansible.ini index 5e0966b..9348a75 100644 --- a/tox-ansible.ini +++ b/tox-ansible.ini @@ -49,3 +49,8 @@ allowlist_externals = ln sh ade + +[testenv:galaxy] +deps = + galaxy-importer>=0.4.31 + distlib From 6293498d680617cb2ff1ad1b912e6b5d1552112c Mon Sep 17 00:00:00 2001 From: sfulmer Date: Mon, 4 May 2026 08:23:39 -0400 Subject: [PATCH 10/14] fix: restore original testenv config, only add allowlist_externals --- tox-ansible.ini | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/tox-ansible.ini b/tox-ansible.ini index 9348a75..89248d9 100644 --- a/tox-ansible.ini +++ b/tox-ansible.ini @@ -37,20 +37,29 @@ commands = ansible-galaxy collection install -r "{toxinidir}/requirements-dev.yml" [testenv] +set_env = + COLLECTIONS_PATH = "{env:HOME}/.ansible/collections/ansible_collections" + FORCE_COLOR = 1 +passenv = + HOME + ANSIBLE_COLLECTIONS_PATH +no_package = true +deps = + -r tests/integration/requirements.txt +change_dir = {env:HOME}/.ansible/collections/ansible_collections/infra/openshift_virtualization_ops +skip_install = false allowlist_externals = bash echo git + mkdir + sh + ade ansible-galaxy ansible-test ansible-doc pytest - mkdir ln - sh - ade - -[testenv:galaxy] -deps = - galaxy-importer>=0.4.31 - distlib +commands = + bash -c 'git init --initial-branch=main .' + bash -c 'ansible-test integration' From 9c30035edb6a1473f3e93a7673b603229b372567 Mon Sep 17 00:00:00 2001 From: sfulmer Date: Mon, 4 May 2026 08:36:41 -0400 Subject: [PATCH 11/14] fix: pin tox-ansible<26.2.2 to match main (26.3.0 requires ade) --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 9d3d4f9..0061feb 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -5,7 +5,7 @@ pytest-cov coverage molecule tox -tox-ansible +tox-ansible<26.2.2 black jmespath ansible-core From 89241f34985209e3af903f505e55d9e6e6321a07 Mon Sep 17 00:00:00 2001 From: sfulmer Date: Tue, 5 May 2026 09:52:33 -0400 Subject: [PATCH 12/14] fix: filter null lastTransitionTime before sort Conditions with undefined or null lastTransitionTime values cause sort(attribute='lastTransitionTime') to fail. Filter them out with selectattr/rejectattr before sorting. --- roles/vm_consumption_report/tasks/_build_report.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/roles/vm_consumption_report/tasks/_build_report.yml b/roles/vm_consumption_report/tasks/_build_report.yml index ae1a0b4..9be9492 100644 --- a/roles/vm_consumption_report/tasks/_build_report.yml +++ b/roles/vm_consumption_report/tasks/_build_report.yml @@ -45,6 +45,7 @@ last_transition: >- {{ _vm.status.conditions | default([]) + | selectattr('lastTransitionTime', 'defined') | rejectattr('lastTransitionTime', 'none') | sort(attribute='lastTransitionTime') | map(attribute='lastTransitionTime') | list | last | default(_vm.metadata.creationTimestamp) }} From 6b84fa59a1624c80ff56cbad8667bb8382a7c77f Mon Sep 17 00:00:00 2001 From: sfulmer Date: Tue, 5 May 2026 10:04:20 -0400 Subject: [PATCH 13/14] fix: remove invalid guestOSInfo.hostname reference guestOSInfo does not expose a hostname field. Set _guest_hostname to empty string, disabling hostname-based reconciliation. UUID and IP address matching remain functional. --- roles/vm_consumption_report/tasks/_build_report.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/roles/vm_consumption_report/tasks/_build_report.yml b/roles/vm_consumption_report/tasks/_build_report.yml index 9be9492..4d0d2f9 100644 --- a/roles/vm_consumption_report/tasks/_build_report.yml +++ b/roles/vm_consumption_report/tasks/_build_report.yml @@ -11,8 +11,7 @@ | selectattr('metadata.name', 'equalto', _vm.metadata.name) | list | first | default({}) }} _vm_uuid: "{{ _vm.metadata.uid }}" - _guest_hostname: >- - {{ _vmi_match.status.guestOSInfo.hostname | default('') }} + _guest_hostname: '' _guest_ip_addresses: >- {{ _vmi_match.status.interfaces | default([]) From 214419bdad75f6dac215ee4cc2f03048941d9911 Mon Sep 17 00:00:00 2001 From: sfulmer Date: Fri, 15 May 2026 11:48:41 -0400 Subject: [PATCH 14/14] Derive guest hostname from VM spec instead of VMI Use .spec.template.spec.hostname from the VM object with fallback to metadata.name rather than relying on VMI guestOSInfo. VMI does not exist when a VM is stopped, making it unreliable for reporting. --- roles/vm_consumption_report/tasks/_build_report.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/roles/vm_consumption_report/tasks/_build_report.yml b/roles/vm_consumption_report/tasks/_build_report.yml index 4d0d2f9..6083006 100644 --- a/roles/vm_consumption_report/tasks/_build_report.yml +++ b/roles/vm_consumption_report/tasks/_build_report.yml @@ -11,7 +11,9 @@ | selectattr('metadata.name', 'equalto', _vm.metadata.name) | list | first | default({}) }} _vm_uuid: "{{ _vm.metadata.uid }}" - _guest_hostname: '' + _guest_hostname: >- + {{ _vm.spec.template.spec.hostname + | default(_vm.metadata.name) }} _guest_ip_addresses: >- {{ _vmi_match.status.interfaces | default([])