Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions playbooks/vm_consumption_report.yml
Original file line number Diff line number Diff line change
@@ -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_migration.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]
...
66 changes: 66 additions & 0 deletions roles/vm_consumption_report/README.md
Original file line number Diff line number Diff line change
@@ -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
```
66 changes: 66 additions & 0 deletions roles/vm_consumption_report/defaults/main.yml
Original file line number Diff line number Diff line change
@@ -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> # Namespace to query (omit for all namespaces)
# names: # Optional list of VM names to include
# - <vm-name>
# label_selectors: # Optional label selectors (cannot be combined with names)
# - <key>=<value>

# 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> # Hostname or IP used by AAP/SSH
# uuid: <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
...
13 changes: 13 additions & 0 deletions roles/vm_consumption_report/meta/main.yml
Original file line number Diff line number Diff line change
@@ -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: []
...
114 changes: 114 additions & 0 deletions roles/vm_consumption_report/tasks/_build_report.yml
Original file line number Diff line number Diff line change
@@ -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([]) }}"
...
18 changes: 18 additions & 0 deletions roles/vm_consumption_report/tasks/_collect_vmis.yml
Original file line number Diff line number Diff line change
@@ -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 }}
...
30 changes: 30 additions & 0 deletions roles/vm_consumption_report/tasks/_collect_vms.yml
Original file line number Diff line number Diff line change
@@ -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 }}"
...
59 changes: 59 additions & 0 deletions roles/vm_consumption_report/tasks/main.yml
Original file line number Diff line number Diff line change
@@ -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 }}"
...
7 changes: 7 additions & 0 deletions roles/vm_consumption_report/templates/report.csv.j2
Original file line number Diff line number Diff line change
@@ -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 %}
Loading