Skip to content

Instantly share code, notes, and snippets.

@dmccuk
Last active July 15, 2025 14:51
Show Gist options
  • Save dmccuk/36de8e5b477b1b6625504b1d49ff619a to your computer and use it in GitHub Desktop.
Save dmccuk/36de8e5b477b1b6625504b1d49ff619a to your computer and use it in GitHub Desktop.
- name: Ensure /tmp and /var/tmp do not use noexec
  become: true
  block:

    - name: Remove 'noexec' from /etc/fstab for /tmp
      lineinfile:
        path: /etc/fstab
        regexp: '^([^#].*\s/tmp\s.*)noexec(.*)$'
        line: '\1\2'
        backrefs: yes

    - name: Remove 'noexec' from /etc/fstab for /var/tmp
      lineinfile:
        path: /etc/fstab
        regexp: '^([^#].*\s/var/tmp\s.*)noexec(.*)$'
        line: '\1\2'
        backrefs: yes

    - name: Remount /tmp and /var/tmp
      mount:
        path: "{{ item }}"
        state: remounted
      loop:
        - /tmp
        - /var/tmp

πŸ“‹ Transformation Overview: Morpheus Timeline 🧠 Categories: Management Control Plane – Governance, visibility, policy, integration

Virtual Machines as a Service (VMaaS) – Provisioning, lifecycle, self-service

βœ… Next 1–2 Months: Foundation & Validation πŸ”Ή Management Control Plane Prove Morpheus pilot use cases (e.g., role-based access, auditing, API access)

Complete infrastructure design for Morpheus (e.g., sizing, HA, backup, DR strategy)

Finalise integration points (e.g., AD/LDAP, CMDB, ticketing tools)

Deploy Morpheus DEV/POC instance (pilot environment)

πŸ”Ή VMaaS Onboard 1–2 platforms for test provisioning (e.g., vCenter, AWS, etc.)

Define initial VM blueprints (e.g., Linux/Windows base images, tagging standards)

Test role-based provisioning workflows

Validate lifecycle hooks and automation (e.g., Ansible/Terraform)

βœ… 3–6 Months: Expansion & Stabilisation πŸ”Ή Management Control Plane Build and validate production Morpheus environment (with HA and monitoring)

Build test/staging Morpheus environments (mirror of prod config)

Implement policy and placement rules (e.g., cost center tagging, region rules)

Define and document operational support model (who owns what?)

πŸ”Ή VMaaS Expand support for multiple clouds/hypervisors (e.g., Azure, OpenStack, etc.)

Finalise service catalog (tiers, SLAs, ownership)

Integrate with CMDB and ITSM workflows (e.g., ServiceNow)

Begin onboarding internal teams to use VMaaS self-service

Train service desk / cloud ops on provisioning and support

βœ… 6+ Months: Adoption & Optimisation πŸ”Ή Management Control Plane Convert existing server inventory to be managed via Morpheus

Implement chargeback/showback (e.g., cost reporting per department/project)

Enable automation pipelines (e.g., IAC + Morpheus)

Expand API-driven governance and audits

πŸ”Ή VMaaS Full rollout to business units or development teams

Continuous integration into CI/CD pipelines

VM lifecycle automation (e.g., auto-expiry, archiving, reclamation)

Performance tuning & reporting refinement (e.g., usage heatmaps)

🧩 What You Might Be Missing: πŸ” Across Both Tracks Security integration (SSO, MFA, RBAC enforcement)

Monitoring/logging integration (e.g., sending logs to Splunk, integrating with Prometheus)

End-user feedback loop – plan a round of feedback after rollout of self-service

Decommissioning legacy tooling (e.g., once Morpheus replaces old provisioning tools)

Compliance reporting – aligning provisioning/governance with internal controls

πŸ›  Example Output Table (Simplified): Timeline Management Control Plane VMaaS 1–2 Months Prove pilot use cases, design infra, build dev environment Test provisioning, basic catalog, lifecycle testing 3–6 Months Build prod env, policy rules, test integration, ops model Expand platforms, finalize service catalog 6+ Months Convert legacy, cost reporting, full API/automation adoption


🧩 Virtual Machines as a Service (VMaaS) β€” Roadmap by Timeline βœ… 1–2 Months: Foundation & Initial Enablement Objective: Establish a functional baseline and validate core provisioning capabilities.

Area Key Activities Platform Onboarding Integrate one or two platforms (e.g., vCenter, AWS, or Azure) into Morpheus. Blueprints & Images Create initial Linux/Windows VM blueprints. Define standard images with pre-installed tools. Provisioning Workflow Define and test provisioning workflows (naming conventions, IPAM, domain join, tags). RBAC & User Roles Set up self-service portal access for limited users with role-based permissions. Basic Automation Attach simple automation (e.g., post-provision scripts, email notifications). Pilot Users/Teams Identify early adopter teams for testing and feedback.

βœ… 3–6 Months: Expansion & Service Definition Objective: Mature VMaaS into a repeatable, supportable internal service.

Area Key Activities Service Catalog Define and publish a VMaaS catalog (flavors, tiers, purpose-built templates). Multi-Cloud Support Onboard additional platforms (e.g., Azure, OpenStack, GCP, more vCenters). Advanced Lifecycle Add workflows for resize, snapshot, rebuild, and expiration. ITSM Integration Integrate with ServiceNow (or other) for approvals, tickets, change tracking. CMDB/Inventory Integration Automatically register provisioned VMs in the CMDB. Operations Training Train cloud ops and service desk teams on VMaaS provisioning, lifecycle, and troubleshooting. Chargeback/Showback Planning Start modeling cost metrics and tenant/resource usage reporting.

βœ… 6+ Months: Adoption & Optimization Objective: Scale and optimize the VMaaS offering for widespread use.

Area Key Activities Full Rollout Extend VMaaS to all business units, developers, or project teams. Cost Reporting / Chargeback Implement departmental cost tracking, alerts for over-provisioning, budget reports. Lifecycle Governance Enforce expiration policies, idle VM reporting, and automated cleanup workflows. Self-Healing & Automation Introduce autoscaling, failure recovery actions, and integration with CI/CD. User Feedback & Tuning Collect feedback and adjust catalog, performance, and provisioning SLAs. Retire Legacy Tools Begin decommissioning old provisioning/manual VM request processes.

🎯 Outcome Goals by Phase Phase Outcome 1–2 Months A working PoC VMaaS solution with real provisioning and basic user roles. 3–6 Months A stable service offering across platforms, integrated with operations. 6+ Months A scalable, optimized, cost-aware service with full governance and automation.


---
- name: Loop through host data and display WWNs
  hosts: localhost
  gather_facts: false
  vars:
    hosts:
      - name: ansiblehost01
        wwns:
          - '10:00:00:00:00:00:00:01'
          - '10:00:00:00:00:00:00:02'
      - name: ansiblehost02
        wwns:
          - '10:00:00:00:00:00:00:03'
          - '10:00:00:00:00:00:00:04'

  tasks:
    - name: Display WWNs for each host
      debug:
        msg: "Host {{ item.0.name }} WWN: {{ item.1 }}"
      loop: "{{ hosts | subelements('wwns') }}"
      

Output as a text block:

- name: Build string output
  set_fact:
    host_wwn_strings: "{{ host_wwn_strings | default([]) + [ item.name + ': ' + item.wwns | join(', ') ] }}"
  loop: "{{ hosts }}"

- name: Show combined string
  debug:
    msg: |
      WWNs by Host:
      {{ host_wwn_strings | join('\n') }}
- name: Build string output per host
 set_fact:
   host_wwn_strings: "{{ host_wwn_strings | default([]) + [ item.name ~ ': ' ~ (item.wwns | join(', ')) ] }}"
 loop: "{{ hosts }}"

- name: Print nicely formatted WWN list
 debug:
   msg: |
     WWNs by Host:
     {{ host_wwn_strings | join('\n') }}
| Ansible Version | Control Node OS | Python Required on Control Node | Managed Node OS (RHEL) | Python Required on Managed Node |
| --------------- | --------------- | ------------------------------- | ---------------------- | ------------------------------- |
| 2.9.x (LTS)     | RHEL 7, 8       | Python 2.7 / 3.5+               | RHEL 6, 7, 8           | Python 2.6+ / 2.7+ / 3.5+       |
| 2.10–2.13       | RHEL 7, 8, 9    | Python 3.6+                     | RHEL 7, 8              | Python 2.7+ or 3.5+             |
| 2.14+           | RHEL 8, 9       | Python 3.8+ (3.9 preferred)     | RHEL 7, 8, 9           | Python 3.6+ recommended         |
| 2.16+           | RHEL 8, 9       | Python 3.9+                     | RHEL 8, 9              | Python 3.6+, ideally 3.8+       |

---
- name: Get Morpheus managed servers using Bearer token
  hosts: localhost
  gather_facts: no
  vars:
    morpheus_api_url: "https://morpheus.example.com/api"
    morpheus_token: "your_existing_bearer_token"
  tasks:

    - name: Get list of managed servers
      uri:
        url: "{{ morpheus_api_url }}/servers"
        method: GET
        headers:
          Authorization: "BEARER {{ morpheus_token }}"
        return_content: yes
        validate_certs: no
      register: servers_response

    - name: Show server names
      debug:
        msg: |
          {% for server in servers_response.json.servers %}
          Name: {{ server.name }}
          {% endfor %}
          
---
- name: Launch Morpheus Catalog Item from JSON file
  hosts: localhost
  gather_facts: no
  vars:
    morpheus_api_url: "https://morpheus.example.com/api"
    morpheus_token: "your_token_here"

  tasks:

    - name: Load catalog item JSON from file
      slurp:
        src: "catalog_item.json"
      register: raw_catalog_json

    - name: Convert JSON from base64
      set_fact:
        catalog_payload: "{{ raw_catalog_json.content | b64decode }}"

    - name: Call Morpheus API to create instance
      uri:
        url: "{{ morpheus_api_url }}/instances"
        method: POST
        headers:
          Authorization: "BEARER {{ morpheus_token }}"
          Content-Type: "application/json"
        body: "{{ catalog_payload }}"
        body_format: raw
        return_content: yes
        validate_certs: no
      register: response

    - name: Show API response
      debug:
        var: response.json

Use Case Summary for Morpheus: Task Automation and Dynamic Targeting We are exploring how to better leverage Morpheus to automate operational tasks across our estate of managed servers. We want to ensure we can safely and flexibly target specific subsets of systems based on real-time inventory and metadata without duplicating information or managing external lists.

  1. Dynamic Targeting for Ad-hoc and Scheduled Tasks Goal: Enable operators or automation workflows to run scripts or configuration changes on:

A single server

A group of selected servers

All servers of a specific type (e.g. OS, version, data center, environment)

This should be possible through:

Ad-hoc execution (on demand)

Scheduled tasks (e.g. nightly checks, periodic audits)

API-triggered automation (e.g. Ansible or pipeline-driven)

  1. Example Scenarios Apply a config change to all RHEL 9 servers

Patch a specific vulnerability (e.g. Heartbleed) across affected hosts only

Run a health check or remediation script on all servers in the UK data center

Perform a one-time upgrade on a known subset of servers (e.g. dev + prod web tiers)

  1. Inventory Requirements We want to avoid static server lists. Morpheus already has visibility into managed hosts, so our goal is to:

Query the Morpheus inventory via API or internal tools

Dynamically generate a target list based on:

OS type / version (e.g. all RHEL 9)

Location (e.g. UKDC1)

Tags, labels, or metadata

Morpheus group or cloud membership

This inventory should ideally be available in real time, and integrate into task or workflow targeting.

  1. Labels or Built-in Metadata? We understand Morpheus supports labels and tags, but would prefer to avoid manually maintaining metadata that already exists in the system. For example:

OS version, environment, cloud, group, etc. are already tracked

Can we query this data directly and avoid redundant tagging?

We are open to using labels where needed but would prefer native filters or queries where possible.

  1. What We Need from HP / Morpheus What is the recommended method for targeting a dynamic group of servers (e.g. all RHEL 9) when running a script or task?

How do we schedule tasks to automatically run against qualifying servers, now and in the future?

Is there a way to build a dynamic inventory from Morpheus, usable in Ansible or directly in the Morpheus UI/API?

What metadata is best used to support these operations β€” labels, tags, built-in properties?

Are there best practices or reference implementations for this kind of policy-based task execution?

- name: Run telnet diagnostics for failed targets
  shell: "echo quit | timeout 5 telnet {{ item.ip }} {{ item.port }}"
  register: telnet_result
  loop: "{{ failed_targets | default([]) }}"
  when: failed_targets is defined and failed_targets | length > 0
  loop_control:
    label: "{{ item.ip }}:{{ item.port }}"
  ignore_errors: true

- name: Show telnet result details
  debug:
    msg: |
      Telnet to {{ item.item.ip }}:{{ item.item.port }}
      Output:
      {{ item.stdout_lines | default(item.stderr_lines) }}
  loop: "{{ telnet_result.results }}"
  when: telnet_result is defined
Connectivity Check Playbook
β”œβ”€β”€ Runs on: Remote servers (inventory hosts)
β”‚
β”œβ”€β”€ Step 1: Initialize tracking variables
β”‚   β”œβ”€β”€ had_timeout = false
β”‚   └── failed_targets = []
β”‚
β”œβ”€β”€ Step 2: For each target IP and port
β”‚   β”œβ”€β”€ Use 'wait_for' to test connectivity
β”‚   β”œβ”€β”€ If connection fails with timeout
β”‚   β”‚   β”œβ”€β”€ Add that IP:port to failed_targets
β”‚   β”‚   └── Set had_timeout = true
β”‚   └── If connection succeeds, do nothing
β”‚
β”œβ”€β”€ Step 3: If any timeouts occurred on this host
β”‚   β”œβ”€β”€ Loop over failed_targets
β”‚   β”œβ”€β”€ Run 'telnet' to gather low-level error details
β”‚   └── Capture stderr (e.g. "connection refused", "no route", etc.)
β”‚
└── Step 4: Output
    β”œβ”€β”€ Only the real telnet error messages are shown
    └── No unnecessary Ansible task output clutter
---
- name: Collect and store Morpheus metadata facts locally
  hosts: all
  become: yes
  vars:
    morpheus_url: "https://morpheus.example.com"
    cypher_token_path: "secret/vault/bearer_token"

  tasks:

    # Step 0: Get bearer token from Cypher
    - name: Get Morpheus Bearer token from Cypher
      uri:
        url: "{{ morpheus_url }}/api/cypher/{{ cypher_token_path | urlencode }}"
        method: GET
        headers:
          Authorization: "BEARER {{ lookup('env', 'MORPHEUS_API_TOKEN') }}"
        return_content: yes
        validate_certs: no
      register: cypher_token_result

    - name: Set token as a fact
      set_fact:
        morpheus_token: "{{ cypher_token_result.content }}"

    # Step 1: Download all Morpheus instances
    - name: Download all Morpheus instances
      uri:
        url: "{{ morpheus_url }}/api/instances?max=1000"
        method: GET
        headers:
          Authorization: "BEARER {{ morpheus_token }}"
        return_content: yes
        validate_certs: no
      register: all_instances

    # Step 2: Get local short hostname
    - name: Get local short hostname
      command: hostname -s
      register: hostname_result
      changed_when: false

    - name: Find matching instance by name
      set_fact:
        matching_instance: >-
          {{ all_instances.json.instances
             | selectattr('name', 'equalto', hostname_result.stdout)
             | list | first }}

    - name: Fail if no matching instance found
      fail:
        msg: "Could not find a matching instance for hostname {{ hostname_result.stdout }}"
      when: matching_instance is not defined

    # Step 3: Block of tasks that only run if instance is found
    - block:

        - name: Get full instance detail
          uri:
            url: "{{ morpheus_url }}/api/instances/{{ matching_instance.id }}"
            method: GET
            headers:
              Authorization: "BEARER {{ morpheus_token }}"
            return_content: yes
            validate_certs: no
          register: instance_details

        - name: Ensure facts directory exists
          file:
            path: /etc/ansible/facts.d
            state: directory
            mode: '0755'

        - name: Write morpheus.fact file
          copy:
            dest: /etc/ansible/facts.d/morpheus.fact
            content: |
              {
                "instance_id": {{ instance_details.json.instance.id }},
                "hostname": "{{ instance_details.json.instance.hostname }}",
                "name": "{{ instance_details.json.instance.name }}",
                "source_template": "{{ instance_details.json.instance.containerDetails[0].server.sourceImage.name }}",
                "layout": "{{ instance_details.json.instance.layout.name }}",
                "instance_type": "{{ instance_details.json.instance.instanceType.code }}",
                "requested_by": "{{ instance_details.json.instance.createdBy.username }}",
                "created_date": "{{ instance_details.json.instance.dateCreated }}",
                "group": "{{ instance_details.json.instance.group.name }}",
                "cloud": "{{ instance_details.json.instance.cloud.name }}",
                "site": "{{ instance_details.json.instance.site.name }}",
                "environment": "{{ instance_details.json.instance.instanceContext }}"
              }
            mode: '0644'

      when: matching_instance is defined

#!/bin/bash
# collect_sysinfo_for_csv.sh - Enhanced sysinfo collection for RHEL 9 with safe CSV formatting

output_file="sysinfo_$(hostname -s).txt"
echo "Collecting system info into $output_file..."

{
  echo "hostname=$(hostname -s)"
  echo "fqdn=$(hostname -f)"
  echo "os_release=$(awk -F= '/^PRETTY_NAME/{print $2}' /etc/os-release | tr -d \")"
  echo "kernel=$(uname -r)"
  echo "timezone=$(timedatectl | awk '/Time zone/ {print $3}')"

  # NTP
  echo "ntp_status=$(chronyc tracking 2>/dev/null | grep 'Leap status' | cut -d: -f2 | xargs || echo 'not available')"

  # SELinux
  echo "selinux_status=$(sestatus | grep '^SELinux status:' | cut -d: -f2 | xargs)"
  echo "selinux_mode=$(sestatus | grep '^Current mode:' | cut -d: -f2 | xargs)"
  echo "selinux_policy=$(sestatus | grep '^Loaded policy name:' | cut -d: -f2 | xargs)"
  echo "getenforce=$(getenforce 2>/dev/null || echo 'not available')"

  # Security and system tuning
  echo "firewalld=$(firewall-cmd --state 2>/dev/null || echo 'not running')"
  echo "tuned_profile=$(tuned-adm active 2>/dev/null | awk -F: '{print $2}' | xargs)"
  echo "morpheus_agent=$(systemctl is-active morpheus-node-runsvdir.service 2>/dev/null || echo 'not found')"

  # System resources
  echo "cpu_count=$(nproc)"
  echo "mem_total_kb=$(grep MemTotal /proc/meminfo | awk '{print $2}')"

  # Disks
  echo "disk_list=\"$(lsblk -dn -o NAME | paste -sd ',' -)\""
  echo "disk_count=$(lsblk -dn -o NAME | wc -l)"

  # Mounts
  echo "mounts=\"$(df -hT | awk 'NR>1 {print $7}' | paste -sd ',' -)\""
  echo "mount_count=$(df -hT | tail -n +2 | wc -l)"

  # Networking
  echo "interfaces=\"$(ip -o -4 addr show | awk '{print $2\":\"$4}' | paste -sd ',' -)\""
  echo "interface_count=$(ip -o -4 addr show | wc -l)"
  echo "default_route=$(ip route | awk '/default/ {print $3}')"

  # Users
  echo "users=\"$(getent passwd | awk -F: '$3 >= 1000 && $3 < 65534 {print $1}' | paste -sd ',' -)\""
  echo "user_count=$(getent passwd | awk -F: '$3 >= 1000 && $3 < 65534' | wc -l)"

  # Packages
  echo "packages=\"$(rpm -qa | paste -sd ',' -)\""
  echo "package_count=$(rpm -qa | wc -l)"
} > "$output_file"

echo "βœ… Done. File created: $output_file"

#!/bin/bash
# compare_sysinfo_to_csv.sh - Compare two sysinfo files and produce a CSV-safe output

if [ $# -ne 2 ]; then
  echo "Usage: $0 <sysinfo_file_1> <sysinfo_file_2>"
  exit 1
fi

file1="$1"
file2="$2"

# Get hostnames for column headers
host1=$(grep '^hostname=' "$file1" | cut -d= -f2)
host2=$(grep '^hostname=' "$file2" | cut -d= -f2)

# Output file name
output_file="comparison_${host1}_vs_${host2}.csv"

# Merge both files into a temporary aligned format
join -t= -j 1 <(sort "$file1") <(sort "$file2") > /tmp/compare_merged.txt

# Write header
echo "Metric,$host1,$host2" > "$output_file"

# Process each key=value=value line
while IFS='=' read -r key value1 value2; do
  # Remove leading/trailing whitespace
  value1=$(echo "$value1" | xargs)
  value2=$(echo "$value2" | xargs)

  # Escape double quotes and wrap each value
  value1="\"${value1//\"/\"\"}\""
  value2="\"${value2//\"/\"\"}\""

  echo "$key,$value1,$value2"
done < /tmp/compare_merged.txt >> "$output_file"

# Clean up
rm -f /tmp/compare_merged.txt

echo "βœ… Comparison complete. Output saved to: $output_file"

- name: Convert CSV rows to host_vars files
  hosts: localhost
  gather_facts: no
  vars:
    csv_file: input.csv
    host_vars_dir: host_vars
    hostname_field: VMName  # or ComputerName

  tasks:

    - name: Read CSV data
      community.general.read_csv:
        path: "{{ csv_file }}"
      register: csv_content

    - name: Ensure host_vars directory exists
      file:
        path: "{{ host_vars_dir }}"
        state: directory
        mode: '0755'

    - name: Create host_vars YAML files for each row
      loop: "{{ csv_content.list }}"
      when: item[hostname_field] is defined and item[hostname_field] | length > 0
      vars:
        hostname: "{{ item[hostname_field] }}"
        filtered_vars: >-
          {{
            dict(
              item.items() |
              selectattr('1', 'match', '^(?!\\s*$).+') |
              rejectattr('0', 'equalto', hostname_field)
            )
          }}
      copy:
        dest: "{{ host_vars_dir }}/{{ hostname }}.yml"
        content: "{{ filtered_vars | to_nice_yaml }}"
        mode: '0644'
---
- name: Collect VM info from vCenter and output to CSV
  hosts: localhost
  gather_facts: no
  vars:
    vcenter_hostname: "vcenter.example.com"
    session_token: "your_bearer_token_here"
    vm_names:
      - web01
      - db01
    csv_output_file: "./vm_info.csv"
    csv_headers:
      - vm_name
      - power_state
      - guest_os
      - cluster
      - cpu_count
      - memory_MB
      - datastores
      - disks
      - nics

  tasks:

    - name: Ensure output file is empty before writing
      copy:
        dest: "{{ csv_output_file }}"
        content: "{{ csv_headers | join(',') }}\n"

    - name: Include per-VM task for each VM
      include_tasks: vm_info_tasks.yml
      loop: "{{ vm_names }}"
      loop_control:
        loop_var: vm_name

- name: Get VM ID by name
  uri:
    url: "https://{{ vcenter_hostname }}/rest/vcenter/vm?filter.names={{ vm_name }}"
    method: GET
    headers:
      vmware-api-session-id: "{{ session_token }}"
    validate_certs: no
    return_content: yes
  register: vm_query

- name: Fail if VM not found
  fail:
    msg: "VM '{{ vm_name }}' not found in vCenter"
  when: vm_query.json.value | length == 0

- name: Set VM ID
  set_fact:
    vm_id: "{{ vm_query.json.value[0].vm }}"

- name: Get basic VM info
  uri:
    url: "https://{{ vcenter_hostname }}/rest/vcenter/vm/{{ vm_id }}"
    method: GET
    headers:
      vmware-api-session-id: "{{ session_token }}"
    validate_certs: no
    return_content: yes
  register: vm_summary

- name: Get memory info
  uri:
    url: "https://{{ vcenter_hostname }}/rest/vcenter/vm/{{ vm_id }}/hardware/memory"
    method: GET
    headers:
      vmware-api-session-id: "{{ session_token }}"
    validate_certs: no
    return_content: yes
  register: vm_memory

- name: Format CSV line
  set_fact:
    vm_csv_line: >-
      {{
        [
          vm_name,
          vm_summary.json.value.power_state,
          vm_summary.json.value.guest_OS,
          vm_memory.json.value.size_MiB | default('N/A')
        ] | join(',')
      }}

- name: Append to CSV file
  lineinfile:
    path: "{{ csv_output_file }}"
    line: "{{ vm_csv_line }}"
    create: yes
    insertafter: EOF


# Get list of disk IDs
- name: Get disk list
  uri:
    url: "https://{{ vcenter_hostname }}/rest/vcenter/vm/{{ vm_id }}/hardware/disk"
    method: GET
    headers:
      vmware-api-session-id: "{{ session_token }}"
    validate_certs: no
    return_content: yes
  register: vm_disks

- name: Extract disk IDs
  set_fact:
    disk_ids: "{{ vm_disks.json.value | map(attribute='disk') | list }}"

# Loop through and get full disk details
- name: Gather full disk details
  vars:
    disk_details: []
  block:
    - name: Get detail for each disk
      uri:
        url: "https://{{ vcenter_hostname }}/rest/vcenter/vm/{{ vm_id }}/hardware/disk/{{ item }}"
        method: GET
        headers:
          vmware-api-session-id: "{{ session_token }}"
        validate_certs: no
        return_content: yes
      loop: "{{ disk_ids }}"
      register: disk_detail_results

    - name: Extract usable disk info
      set_fact:
        disk_details: "{{ disk_detail_results.results | map(attribute='json.value') | list }}"

# Format disk summary
- name: Format disk summary
  set_fact:
    disk_summary: >-
      {{
        disk_details
        | map('extract', ['label', 'capacity'])
        | map('regex_replace', '([0-9]+)$', '\1')  # sanitize
        | map('map', 'string')  # ensure string type
        | map('zip', [disk_details | map(attribute='capacity' | int) | map('int') | map('regex_replace', '([0-9]+)', '\1') | map('int') | map('product', [1/1073741824]) | map('int') ])
        | map('join', ':')
        | map('regex_replace', ':([0-9]+)', ':\\1 GB')
        | join('; ')
      }}
- name: Extract disk details from API response
  set_fact:
    disk_details: "{{ vm_disks.json.value | default([]) }}"

- name: Build list of disk sizes in GB
  set_fact:
    disk_sizes_gb: "{{ disk_details | map(attribute='capacity') | map('int') | map('floordiv', 1073741824) | list }}"

- name: Combine disk label and size into CSV-safe summary
  set_fact:
    disk_summary: >-
      {{
        disk_details
        | map(attribute='label')
        | zip(disk_sizes_gb)
        | map('join', ':')
        | map('regex_replace', ':([0-9]+)', ':\\1 GB')
        | join('; ')
      }}

- name: Debug final CSV-formatted disk summary
  debug:
    msg: "{{ disk_summary }}"

---
- name: Collect disk sizes from vCenter VM
  hosts: localhost
  gather_facts: no
  vars:
    vcenter_hostname: "vcenter.example.com"
    session_token: "your_bearer_token_here"
    vm_name: "my-vm-name"

  tasks:

    - name: Get VM ID by name
      uri:
        url: "https://{{ vcenter_hostname }}/rest/vcenter/vm?filter.names={{ vm_name }}"
        method: GET
        headers:
          vmware-api-session-id: "{{ session_token }}"
        validate_certs: no
        return_content: yes
      register: vm_query

    - name: Fail if VM not found
      fail:
        msg: "VM '{{ vm_name }}' not found in vCenter"
      when: vm_query.json.value | length == 0

    - name: Set VM ID
      set_fact:
        vm_id: "{{ vm_query.json.value[0].vm }}"

    - name: Get all disk IDs
      uri:
        url: "https://{{ vcenter_hostname }}/rest/vcenter/vm/{{ vm_id }}/hardware/disk"
        method: GET
        headers:
          vmware-api-session-id: "{{ session_token }}"
        validate_certs: no
        return_content: yes
      register: vm_disks

    - name: Extract list of disk IDs
      set_fact:
        disk_ids: "{{ vm_disks.json.value | map(attribute='disk') | list }}"

    - name: Get full details for each disk
      uri:
        url: "https://{{ vcenter_hostname }}/rest/vcenter/vm/{{ vm_id }}/hardware/disk/{{ item }}"
        method: GET
        headers:
          vmware-api-session-id: "{{ session_token }}"
        validate_certs: no
        return_content: yes
      loop: "{{ disk_ids }}"
      register: disk_detail_results

    - name: Collect disk detail values
      set_fact:
        disk_details: "{{ disk_detail_results.results | map(attribute='json.value') | list }}"

    - name: Convert capacity to GB
      set_fact:
        disk_sizes_gb: "{{ disk_details | map(attribute='capacity') | map('int') | map('floordiv', 1073741824) | list }}"

    - name: Combine disk label and size
      set_fact:
        disk_summary: >-
          {{
            disk_details
            | map(attribute='label')
            | zip(disk_sizes_gb)
            | map('join', ':')
            | map('regex_replace', ':([0-9]+)', ':\\1 GB')
            | join('; ')
          }}

    - name: Show final disk summary
      debug:
        msg: "{{ disk_summary }}"
---
- name: Collect disk names from a vCenter VM (robust version)
  hosts: localhost
  gather_facts: no
  vars:
    vcenter_hostname: "vcenter.example.com"
    session_token: "your_bearer_token_here"
    vm_name: "your-vm-name-here"

  tasks:

    - name: Get VM ID by name
      uri:
        url: "https://{{ vcenter_hostname }}/rest/vcenter/vm?filter.names={{ vm_name }}"
        method: GET
        headers:
          vmware-api-session-id: "{{ session_token }}"
        validate_certs: no
        return_content: yes
      register: vm_query

    - name: Fail if VM not found
      fail:
        msg: "VM '{{ vm_name }}' not found in vCenter"
      when: vm_query.json.value | length == 0

    - name: Set VM ID
      set_fact:
        vm_id: "{{ vm_query.json.value[0].vm }}"

    - name: Get list of disk IDs
      uri:
        url: "https://{{ vcenter_hostname }}/rest/vcenter/vm/{{ vm_id }}/hardware/disk"
        method: GET
        headers:
          vmware-api-session-id: "{{ session_token }}"
        validate_certs: no
        return_content: yes
      register: vm_disks

    - name: Extract disk IDs
      set_fact:
        disk_ids: "{{ vm_disks.json.value | map(attribute='disk') | list }}"

    - name: Initialize disk name list
      set_fact:
        disk_names: []

    - name: Fetch each disk's label
      uri:
        url: "https://{{ vcenter_hostname }}/rest/vcenter/vm/{{ vm_id }}/hardware/disk/{{ item }}"
        method: GET
        headers:
          vmware-api-session-id: "{{ session_token }}"
        validate_certs: no
        return_content: yes
      loop: "{{ disk_ids }}"
      register: per_disk_result
      loop_control:
        label: "{{ item }}"

    - name: Extract disk names from results
      set_fact:
        disk_names: "{{ disk_names + [item.json.value.label] }}"
      loop: "{{ per_disk_result.results }}"
      when: item.json.value.label is defined

    - name: Show disk names
      debug:
        var: disk_names

now shell:

#!/bin/bash

VCENTER="vcenter.example.com"
SESSION_ID="your_vmware_session_id"
VM_NAME="your-vm-name"

# Step 1: Get the VM ID
VM_ID=$(curl -sk -H "vmware-api-session-id: $SESSION_ID" \
  "https://${VCENTER}/rest/vcenter/vm?filter.names=${VM_NAME}" \
  | jq -r '.value[0].vm')

if [[ -z "$VM_ID" ]]; then
  echo "VM not found: $VM_NAME"
  exit 1
fi

# Step 2: Get the list of disk IDs
DISK_IDS=$(curl -sk -H "vmware-api-session-id: $SESSION_ID" \
  "https://${VCENTER}/rest/vcenter/vm/${VM_ID}/hardware/disk" \
  | jq -r '.value[].disk')

# Step 3: Loop over each disk and get its label
echo "Disks for $VM_NAME:"
for DISK_ID in $DISK_IDS; do
  LABEL=$(curl -sk -H "vmware-api-session-id: $SESSION_ID" \
    "https://${VCENTER}/rest/vcenter/vm/${VM_ID}/hardware/disk/${DISK_ID}" \
    | jq -r '.value.label')
  echo "- $LABEL"
done

#!/bin/bash

VCENTER="vcenter.example.com"
SESSION_ID="your_vmware_session_id"
VM_NAME="your-vm-name"

# Step 1: Get VM ID
VM_ID=$(curl -sk -H "vmware-api-session-id: $SESSION_ID" \
  "https://${VCENTER}/rest/vcenter/vm?filter.names=${VM_NAME}" \
  | jq -r '.value[0].vm')

if [[ -z "$VM_ID" ]]; then
  echo "VM not found: $VM_NAME"
  exit 1
fi

# Step 2: Get list of disk IDs
DISK_IDS=$(curl -sk -H "vmware-api-session-id: $SESSION_ID" \
  "https://${VCENTER}/rest/vcenter/vm/${VM_ID}/hardware/disk" \
  | jq -r '.value[].disk')

echo "Disks for $VM_NAME:"
for DISK_ID in $DISK_IDS; do
  # Get disk JSON
  DISK_JSON=$(curl -sk -H "vmware-api-session-id: $SESSION_ID" \
    "https://${VCENTER}/rest/vcenter/vm/${VM_ID}/hardware/disk/${DISK_ID}")

  # Extract label and capacity
  LABEL=$(echo "$DISK_JSON" | jq -r '.value.label')
  CAPACITY_BYTES=$(echo "$DISK_JSON" | jq -r '.value.capacity')

  # Convert to GB (rounding down)
  CAPACITY_GB=$(( CAPACITY_BYTES / 1073741824 ))

  echo "- $LABEL: ${CAPACITY_GB} GB"
done
#!/bin/bash

VCENTER="vcenter.example.com"
SESSION_ID="your_vmware_session_id"
VM_NAME="your-vm-name"

# Step 1: Get VM ID
VM_ID=$(curl -sk -H "vmware-api-session-id: $SESSION_ID" \
  "https://${VCENTER}/rest/vcenter/vm?filter.names=${VM_NAME}" \
  | jq -r '.value[0].vm')

if [[ -z "$VM_ID" ]]; then
  echo "VM not found: $VM_NAME"
  exit 1
fi

echo "VM ID: $VM_ID"
echo

# Step 2: Get list of disk IDs
DISK_IDS=$(curl -sk -H "vmware-api-session-id: $SESSION_ID" \
  "https://${VCENTER}/rest/vcenter/vm/${VM_ID}/hardware/disk" \
  | jq -r '.value[].disk')

echo "Disks for $VM_NAME:"
for DISK_ID in $DISK_IDS; do
  echo "Running: curl -sk -H \"vmware-api-session-id: \$SESSION_ID\" \"https://${VCENTER}/rest/vcenter/vm/${VM_ID}/hardware/disk/${DISK_ID}\""

  DISK_JSON=$(curl -sk -H "vmware-api-session-id: $SESSION_ID" \
    "https://${VCENTER}/rest/vcenter/vm/${VM_ID}/hardware/disk/${DISK_ID}")

  # Print raw JSON (optional debug)
  echo "Raw disk JSON:"
  echo "$DISK_JSON" | jq
  echo

  # Extract and convert
  LABEL=$(echo "$DISK_JSON" | jq -r '.value.label')
  CAPACITY_BYTES=$(echo "$DISK_JSON" | jq -r '.value.capacity')
  CAPACITY_GB=$(( CAPACITY_BYTES / 1073741824 ))

  echo "β†’ Disk $DISK_ID: $LABEL - ${CAPACITY_GB} GB"
  echo "----------------------------------------------------"
done
---
- name: Collect disk info for a VM from vCenter
  hosts: localhost
  gather_facts: no
  vars:
    vcenter_hostname: "vcenter.example.com"
    session_token: "your_vmware_session_id"
    vm_name: "your-vm-name"

  tasks:
    - name: Get VM ID from vCenter
      uri:
        url: "https://{{ vcenter_hostname }}/rest/vcenter/vm?filter.names={{ vm_name }}"
        method: GET
        headers:
          vmware-api-session-id: "{{ session_token }}"
        return_content: yes
        validate_certs: no
      register: vm_id_result

    - name: Fail if VM not found
      fail:
        msg: "VM '{{ vm_name }}' not found in vCenter"
      when: vm_id_result.json.value | length == 0

    - name: Set VM ID fact
      set_fact:
        vm_id: "{{ vm_id_result.json.value[0].vm }}"

    - name: Get disk list for VM
      uri:
        url: "https://{{ vcenter_hostname }}/rest/vcenter/vm/{{ vm_id }}/hardware/disk"
        method: GET
        headers:
          vmware-api-session-id: "{{ session_token }}"
        return_content: yes
        validate_certs: no
      register: disk_list_result

    - name: Set list of disk IDs
      set_fact:
        disk_ids: "{{ disk_list_result.json.value | map(attribute='disk') | list }}"

    - name: Initialize disk output list
      set_fact:
        disk_output: []

    - name: Get disk details for each disk
      vars:
        disk_url: "https://{{ vcenter_hostname }}/rest/vcenter/vm/{{ vm_id }}/hardware/disk/{{ item }}"
      uri:
        url: "{{ disk_url }}"
        method: GET
        headers:
          vmware-api-session-id: "{{ session_token }}"
        return_content: yes
        validate_certs: no
      loop: "{{ disk_ids }}"
      register: disk_details_result

    - name: Format disk info
      set_fact:
        disk_output: >-
          {{
            disk_output + [
              {
                'disk_id': item.item,
                'label': item.json.value.label,
                'size_gb': (item.json.value.capacity | int // 1073741824)
              }
            ]
          }}
      loop: "{{ disk_details_result.results }}"

    - name: Display disk info
      debug:
        var: disk_output

- name: Get VM ID by name
  uri:
    url: "https://{{ vcenter_hostname }}/rest/vcenter/vm?filter.names={{ vm_name }}"
    method: GET
    headers:
      vmware-api-session-id: "{{ session_token }}"
    validate_certs: no
    return_content: yes
  register: vm_query

- name: Fail if VM not found
  fail:
    msg: "VM '{{ vm_name }}' not found in vCenter"
  when: vm_query.json.value | length == 0

- name: Set VM ID
  set_fact:
    vm_id: "{{ vm_query.json.value[0].vm }}"

- name: Get basic VM info
  uri:
    url: "https://{{ vcenter_hostname }}/rest/vcenter/vm/{{ vm_id }}"
    method: GET
    headers:
      vmware-api-session-id: "{{ session_token }}"
    validate_certs: no
    return_content: yes
  register: vm_summary

- name: Get memory info
  uri:
    url: "https://{{ vcenter_hostname }}/rest/vcenter/vm/{{ vm_id }}/hardware/memory"
    method: GET
    headers:
      vmware-api-session-id: "{{ session_token }}"
    validate_certs: no
    return_content: yes
  register: vm_memory

- name: Get disk list for VM
  uri:
    url: "https://{{ vcenter_hostname }}/rest/vcenter/vm/{{ vm_id }}/hardware/disk"
    method: GET
    headers:
      vmware-api-session-id: "{{ session_token }}"
    return_content: yes
    validate_certs: no
  register: disk_list_result

- name: Set list of disk IDs
  set_fact:
    disk_ids: "{{ disk_list_result.json.value | map(attribute='disk') | list }}"

- name: Initialize disk output list
  set_fact:
    disk_output: []

- name: Get disk details for each disk
  vars:
    disk_url: "https://{{ vcenter_hostname }}/rest/vcenter/vm/{{ vm_id }}/hardware/disk/{{ item }}"
  uri:
    url: "{{ disk_url }}"
    method: GET
    headers:
      vmware-api-session-id: "{{ session_token }}"
    return_content: yes
    validate_certs: no
  loop: "{{ disk_ids }}"
  register: disk_details_result

- name: Format disk info
  set_fact:
    disk_output: >-
      {{
        disk_output + [
          {
            'label': item.json.value.label,
            'size_gb': (item.json.value.capacity | int // 1073741824)
          }
        ]
      }}
  loop: "{{ disk_details_result.results }}"

- name: Create disk summary string
  set_fact:
    disk_summary: >-
      {{ disk_output | map(attribute='label') | zip(disk_output | map(attribute='size_gb'))
                     | map('join', ':') | map('regex_replace', ':([0-9]+)', ':\1 GB')
                     | join('; ') }}

- name: Format CSV line
  set_fact:
    vm_csv_line: >-
      {{
        [
          vm_name,
          vm_summary.json.value.power_state,
          vm_summary.json.value.guest_OS,
          vm_memory.json.value.size_MiB | default('N/A'),
          disk_summary | default('')
        ] | join(',')
      }}

- name: Append to CSV file
  lineinfile:
    path: "{{ csv_output_file }}"
    line: "{{ vm_csv_line }}"
    create: yes
    insertafter: EOF
- name: Format disk summary
  set_fact:
    disk_summary: >-
      "{{
        disk_details
        | map(attribute='label') 
        | zip(disk_details | map(attribute='capacity') | map('int') | map('floordiv', 1073741824))
        | map('join', ':') 
        | map('regex_replace', ':([0-9]+)', ':\\1 GB') 
        | join('; ')
      }}"
- name: Format IP address summary
  set_fact:
    ip_summary: >-
      {{
        vm_guest_networking.json.value
        | map(attribute='ip_addresses')
        | sum(start=[])
        | select('match', '^[0-9.]+$')
        | list
        | join('; ')
      }}
- name: Gather NIC details
  vars:
    nic_results: []
  set_fact:
    nic_results: "{{ nic_results + [ nic_detail.json.value ] }}"
  loop: "{{ nic_ids }}"
  loop_control:
    loop_var: nic_id
  tasks:
    - name: Get NIC detail for {{ nic_id }}
      uri:
        url: "https://{{ vcenter_hostname }}/rest/vcenter/vm/{{ vm_id }}/hardware/ethernet/{{ nic_id }}"
        method: GET
        headers:
          vmware-api-session-id: "{{ session_token }}"
        validate_certs: no
        return_content: yes
      register: nic_detail

- name: Create NIC summary
  set_fact:
    nic_summary: >-
      "{{
        nic_results
        | map(attribute='mac_address')
        | zip(nic_results | map(attribute='backing.network_name'))
        | map('join', ' (')
        | map('regex_replace', '$', ')')
        | join('; ')
      }}"

- name: Format CSV line
  set_fact:
    vm_csv_line: >-
      {{
        [
          vm_name,
          vm_summary.json.value.power_state,
          vm_summary.json.value.guest_OS,
          vm_summary.json.value.vm,
          vm_summary.json.value.cpu_count | default('N/A'),
          vm_memory.json.value.size_MiB | default('N/A'),
          disk_summary | default(''),
          nic_summary | default('')
        ] | map('regex_replace', ',', ' ') | join(',')
      }}

- name: Initialize empty NIC results list
  set_fact:
    nic_results: []

- name: Get NIC detail for each NIC ID
  uri:
    url: "https://{{ vcenter_hostname }}/rest/vcenter/vm/{{ vm_id }}/hardware/ethernet/{{ item }}"
    method: GET
    headers:
      vmware-api-session-id: "{{ session_token }}"
    validate_certs: no
    return_content: yes
  loop: "{{ nic_ids }}"
  loop_control:
    loop_var: nic_id
  register: nic_detail_results

- name: Extract NIC values into a list
  set_fact:
    nic_results: "{{ nic_detail_results.results | map(attribute='json.value') | list }}"

# Step 1: Get NIC IDs
- name: Get NIC IDs
  uri:
    url: "https://{{ vcenter_hostname }}/rest/vcenter/vm/{{ vm_id }}/hardware/ethernet"
    method: GET
    headers:
      vmware-api-session-id: "{{ session_token }}"
    validate_certs: no
    return_content: yes
  register: nic_list

# Step 2: Extract NIC IDs
- name: Extract NIC IDs
  set_fact:
    nic_ids: "{{ nic_list.json.value | map(attribute='nic') | list }}"

# Step 3: Get NIC details (looped)
- name: Get NIC details for each NIC
  uri:
    url: "https://{{ vcenter_hostname }}/rest/vcenter/vm/{{ vm_id }}/hardware/ethernet/{{ item }}"
    method: GET
    headers:
      vmware-api-session-id: "{{ session_token }}"
    validate_certs: no
    return_content: yes
  loop: "{{ nic_ids }}"
  register: nic_detail_results

# Step 4: Extract NIC attributes into list
- name: Extract NIC values into a list
  set_fact:
    nic_results: "{{ nic_detail_results.results | map(attribute='json.value') | list }}"

# Step 5: Format NICs for CSV (e.g. MAC addresses)
- name: Format NIC summary
  set_fact:
    nic_summary: "{{ nic_results | map(attribute='mac_address') | join('; ') }}"

- name: Get all networks from vCenter
  uri:
    url: "https://{{ vcenter_hostname }}/rest/vcenter/network"
    method: GET
    headers:
      vmware-api-session-id: "{{ session_token }}"
    validate_certs: no
    return_content: yes
  register: all_networks

- name: Convert networks to dictionary for lookup
  set_fact:
    network_id_name_map: "{{ all_networks.json.value | items2dict(key_name='network', value_name='name') }}"

---

- name: Extract port group names from NICs
  set_fact:
    portgroup_names: >-
      {{
        nic_results
        | map(attribute='backing.network')
        | map('extract', network_id_name_map)
        | map('quote')
        | list
      }}

---

- name: Format port group names for CSV
  set_fact:
    portgroup_summary: "{{ portgroup_names | join('; ') }}"


---

- name: Format CSV line
  set_fact:
    vm_csv_line: >-
      {{
        [
          vm_name,
          vm_summary.json.value.power_state,
          vm_summary.json.value.guest_OS,
          vm_memory.json.value.size_MiB | default('N/A'),
          disk_summary | default(''),
          nic_summary | default(''),
          portgroup_summary | default('')
        ] | join(',')
      }}


- name: Set VM cluster ID
  set_fact:
    vm_cluster_id: "{{ vm_summary.json.value.cluster | default('') }}"

- name: Get cluster name from vCenter
  uri:
    url: "https://{{ vcenter_hostname }}/rest/vcenter/cluster/{{ vm_cluster_id }}"
    method: GET
    headers:
      vmware-api-session-id: "{{ session_token }}"
    validate_certs: no
    return_content: yes
  register: cluster_info
  when: vm_cluster_id != ''
  failed_when: false

- name: Set cluster name fact
  set_fact:
    cluster_name: "{{ cluster_info.json.value.name | default('N/A') }}"
  when: vm_cluster_id != ''

- name: Format CSV line
  set_fact:
    vm_csv_line: >-
      {{
        [
          vm_name,
          vm_id,
          vm_summary.json.value.power_state,
          vm_summary.json.value.guest_OS,
          cluster_name | default('N/A'),          # <--- new field
          cpu_count | default('N/A'),
          vm_memory.json.value.size_MiB | default('N/A'),
          datastore_summary | default(''),
          disk_summary | default(''),
          nic_summary | default(''),
          network_names | default('')
        ] | map('regex_replace', ',', ' ') | join(',')
      }}
- name: Build VM to Cluster Map
  hosts: localhost
  gather_facts: no
  vars:
    vcenter_hostname: "your-vcenter.example.com"
    session_token: "your_token"
  tasks:

    - name: Get all clusters
      uri:
        url: "https://{{ vcenter_hostname }}/rest/vcenter/cluster"
        method: GET
        headers:
          vmware-api-session-id: "{{ session_token }}"
        validate_certs: no
        return_content: yes
      register: cluster_result

    - name: Set cluster list
      set_fact:
        cluster_list: "{{ cluster_result.json.value }}"

    - name: Initialize empty mapping
      set_fact:
        vm_cluster_map: []

    - name: Include per-cluster task to gather VMs
      include_tasks: cluster_vm_map.yml
      loop: "{{ cluster_list }}"
      loop_control:
        loop_var: cluster_data


- name: Get VMs in cluster {{ cluster_data.name }}
  uri:
    url: "https://{{ vcenter_hostname }}/rest/vcenter/vm?filter.clusters={{ cluster_data.cluster }}"
    method: GET
    headers:
      vmware-api-session-id: "{{ session_token }}"
    validate_certs: no
    return_content: yes
  register: vm_lookup

- name: Append VM names with cluster to vm_cluster_map
  set_fact:
    vm_cluster_map: "{{ vm_cluster_map + cluster_vm_list }}"
  vars:
    cluster_vm_list: >-
      {{
        vm_lookup.json.value
        | map(attribute='name')
        | map('regex_replace', '^(.*)$', '\\1::{{ cluster_data.name }}')
        | list
      }}

- name: Add VM-to-cluster entries as dict
  set_fact:
    vm_cluster_map: "{{ vm_cluster_map | combine(new_entries) }}"
  vars:
    new_entries: >-
      {{
        dict(
          vm_lookup.json.value | map(attribute='name') | zip(repeat(cluster_name))
        )
      }}

- name: Add VM-to-cluster entries as dict
  set_fact:
    vm_cluster_map: "{{ vm_cluster_map | combine(new_entries) }}"
  vars:
    new_entries: >-
      {{
        dict(
          vm_lookup.json.value |
          map(attribute='name') |
          zip([cluster_name] * (vm_lookup.json.value | length))
        )
      }}
      
- name: Get VMs in cluster {{ cluster_data.name }}
  uri:
    url: "https://{{ vcenter_hostname }}/rest/vcenter/vm?filter.clusters={{ cluster_data.cluster }}"
    method: GET
    headers:
      vmware-api-session-id: "{{ session_token }}"
    validate_certs: no
    return_content: yes
  register: vm_lookup

- name: Add VM-to-cluster entries as dict
  set_fact:
    vm_cluster_map: >-
      {{
        vm_cluster_map | combine(
          dict(
            vm_lookup.json.value |
            map(attribute='name') |
            zip([cluster_data.name] * (vm_lookup.json.value | length))
          )
        )
      }}



- name: Get all clusters
  uri:
    url: "https://{{ vcenter_hostname }}/rest/vcenter/cluster"
    method: GET
    headers:
      vmware-api-session-id: "{{ session_token }}"
    validate_certs: no
    return_content: yes
  register: cluster_result

- name: Set cluster list
  set_fact:
    cluster_list: "{{ cluster_result.json.value }}"

- name: Initialize empty mapping
  set_fact:
    vm_cluster_map: {}

- name: Include per-cluster task to gather VMs
  include_tasks: cluster_vm_map.yml
  loop: "{{ cluster_list }}"
  loop_control:
    loop_var: cluster_data

---
cluster_vm_map.yml


- name: Get VMs in cluster {{ cluster_data.name }}
  uri:
    url: "https://{{ vcenter_hostname }}/rest/vcenter/vm?filter.clusters={{ cluster_data.cluster }}"
    method: GET
    headers:
      vmware-api-session-id: "{{ session_token }}"
    validate_certs: no
    return_content: yes
  register: vm_lookup

- name: Add VM-to-cluster entries as dict
  set_fact:
    vm_cluster_map: >-
      {{
        vm_cluster_map | combine(
          dict(
            vm_lookup.json.value |
            map(attribute='name') |
            zip([cluster_data.name] * (vm_lookup.json.value | length))
          )
        )
      }}

---

vm_info_tasks.yml
- name: Get cluster name from vm_cluster_map
  set_fact:
    vm_cluster_name: "{{ vm_cluster_map[vm_name] | default('N/A') }}"


- name: Format CSV line
  set_fact:
    vm_csv_line: >-
      {{
        [
          vm_name,
          vm_id,
          vm_summary.json.value.power_state,
          vm_summary.json.value.guest_OS,
          vm_memory.json.value.size_MiB | default('N/A'),
          cpu_info.json.value.count | default('N/A'),
          disk_summary | default(''),
          nic_summary | default(''),
          network_summary | default(''),
          vm_cluster_name <--- ADD THIS LINE
        ] | map('regex_replace', ',', ' ') | join(',')
      }}

How to fix it properly Option A: Revert to the old controller type If you can, temporarily switch the VM’s SCSI controller back to what it was (for example, VMware Paravirtual). The VM should boot again, letting you rebuild the initramfs with support for the new controller.

Option B: Boot in rescue mode and rebuild the initramfs Attach the RHELβ€―9 DVD ISO or boot media to the VM.

Boot the VM from that ISO and choose Troubleshooting β†’ Rescue a Red Hat Enterprise Linux system.

Mount your system and chroot into it:

bash Copy Edit chroot /mnt/sysimage Rebuild the initramfs with all drivers:

bash Copy Edit dracut --force --add-drivers "mpt3sas mpt2sas mptspi mptscsih" /boot/initramfs-$(uname -r).img $(uname -r) (LSI Logic SAS uses the mpt3sas or similar driver, so we add those.)

Exit chroot and reboot:

bash Copy Edit exit reboot Option C: Pre‑build a template with both drivers Before cloning, build the initramfs with additional storage drivers:

bash Copy Edit sudo dracut --force --add-drivers "mpt3sas mpt2sas" /boot/initramfs-$(uname -r).img $(uname -r) Then shut down and convert to a template. Clones can then boot with either VMware Paravirtual or LSI Logic SAS.

Summary Changing SCSI adapter after install without preparing the drivers causes boot failures.

Quick fix: revert to original controller type.

Permanent fix: rebuild initramfs with the needed drivers (via rescue mode or before cloning).

If you want, I can give you a step‑by‑step rescue session command list or help craft the exact dracut command for your kernel. Just let me know.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment