- 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.
- 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)
- 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)
- 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.
- 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.
- 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.