RVTools consolidation scenarios
Answers "how many target servers does this VMware estate need, and how many fewer if we
consolidate?" from the customer's own RVTools exports. Every powered-on VM is actually
bin-packed; nothing is a ratio estimate.
Two scripts, standard library only (Python 3.9+, no pip install):
scripts/consolidate.py -- reads the exports, packs each scenario, writes scenarios.json
and cluster_mapping.csv (every source cluster and the target pool it lands in per scenario).
scripts/render_report.py -- turns scenarios.json into a two-page branded HTML report and,
with headless Chrome/Chromium available, a PDF.
What you need before running
| Input |
Where it comes from |
One RVTools export per vCenter (.xlsx, or a folder of per-sheet CSVs) |
Customer. Needs the vInfo, vHost and vMemory sheets. One export = one site; pools never span sites. |
| Target node: RAM, physical cores, threads |
Customer's standard build or the proposed server. Ask if not given -- it drives every number. |
| Which clusters are licensing or tenant boundaries |
Infer from cluster names, then confirm (see step 3). |
| Server unit prices (optional) |
Reseller quote. Without one, give a sensitivity range and say so. |
Workflow
Run once with defaults to see the estate and how clusters classify:
python3 scripts/consolidate.py \
--site "Chicago=Chicago RVTools_export_all.xlsx" \
--site "London=London RVTools_export_all.xlsx" \
--out out/
The first printed line is the estate. Check it against the RVTools totals the customer
quoted (VM count, vCPU, RAM). If they disagree, stop and find out why before going on.
Read out/cluster_mapping.csv. Every cluster name, its hosts, VMs and target pool per
scenario. This is where misclassification shows up.
Write a config (start from assets/example-config.json). Set the target node, then
the islands -- clusters whose workloads must never share nodes with anything else:
"kind": "license" -- the boundary exists for software licensing (Oracle, SQL Server,
IBM, Windows Datacenter). Kept 1:1 in the moderate scenario; merged by class per site
only in the aggressive one. Oracle VMs never go into a general pool -- Oracle
licenses every physical core a VM could run on.
"kind": "isolation" -- tenant-dedicated clusters, firewall A/B pairs, DMZ, PCI.
Merged per class per site from the moderate scenario on, never mixed with others.
Islands match the uppercase cluster name as a regex, first match wins, so order specific
before general (Oracle EE before Oracle SE). Match loosely -- real estates misspell names.
Run with the config and read the summary: totals per scenario, net-new servers beyond
the target model already owned, servers avoided versus the first scenario, and which pools
are CPU-bound.
Verify before it leaves the building. For customer-facing numbers, have a second agent
on a different model recompute A/B/C from the CSVs using only the rules in "Scenario
rules" below -- not this script -- and compare totals per scenario and per site. An agent
re-running the same script proves nothing.
Render:
python3 scripts/render_report.py out/scenarios.json --customer "Acme" \
--data-date 8/20/26 --html out/Acme_Consolidation.html --pdf out/Acme_Consolidation.pdf
It warns if the PDF is not exactly 2 pages. Chrome is found via $CHROME, then
google-chrome/chromium on PATH, then a puppeteer-cached Chrome, then the macOS app.
Look at both pages before sending.
Scenario rules (the defaults)
Common to every scenario:
- Only powered-on, non-template VMs. Clusters with nothing powered on retire with vSphere.
- Pack first-fit decreasing by memory, second dimension allocated vCPU at 4 per hardware thread.
- Measured host CPU (cores x
CPU usage %) must fit in 70% of the pool's target cores. When
this binds, the pool is marked CPU-bound.
- +0.25 GiB KubeVirt overhead per VM.
- +1 spare node per target cluster, 3-node minimum per cluster, and no cluster larger than
the largest cluster in the estate today (split, with a spare each, if exceeded).
- Holding clusters (empty name = standalone hosts,
ESXi_Upgrade, or 2 or fewer VMs) are not
workload boundaries: their VMs join the site's largest general-purpose cluster/pool.
|
A -- Cluster-for-cluster |
B -- Moderate |
C -- Aggressive |
| General-purpose clusters |
each stays its own cluster |
merged per site into prod and non-prod |
all merged per site |
| License islands |
as-is |
as-is |
merged by class per site |
| Isolation islands |
as-is |
merged by class per site |
merged by class per site |
| VM memory |
configured |
configured |
Consumed x 1.2, never above configured |
| Memory fill per node |
80% |
80% |
90% |
Non-prod is a token match on DEV, NP, NONPROD, TST, TEST, QA, UAT, STG, STAGE, DRTEST, PSR,
PERF, BUILD, SANDBOX, LAB. Everything else general-purpose counts as prod.
The engine also re-runs the last scenario one lever at a time (merging clusters, then
right-sizing, then higher fill) and reports how many servers each lever saves. Lead with the
merging number: it needs no application owner's approval.
Config reference
Any key you omit keeps its default (see DEFAULTS at the top of consolidate.py). Lists
(islands, scenarios) replace the default list entirely.
node: name (shown in the report), ram_gib, cores, threads, model_match (regex on
the vHost Model column, to count target-model servers already owned).
limits: vcpu_per_thread, cpu_measured_cap, vm_overhead_gib, min_nodes,
max_nodes (null = largest cluster today), spares_per_cluster, holding_max_vms.
islands: list of {name, match, kind}.
holding, nonprod: regexes on the uppercase cluster name.
scenarios: list of {key, name, general: cluster|env|site, islands: cluster|merge-isolation|merge, fill, rightsize: null|factor}.
The report shows the first three and compares everything to the first.
pricing: unit_prices (list, drives the cost rows) and currency.
Gotchas that have bitten
- RVTools
vInfo repeats column names (Datacenter, Environment). The engine reads the
last Cluster column; if a customer's export differs, check cluster_mapping.csv.
- Misspelled cluster names split islands. One real estate spelled a tenant's name one way
at one site and transposed two letters at another; a tight regex silently dropped 1.2 TB
database VMs into a general pool. Grep the mapping CSV for every island name.
- Holding clusters inflate cluster-for-cluster. Without the holding rule, three near-empty
staging clusters became nine phantom nodes.
- Maintenance mode means the estate is mid-change. A whole 28-host cluster can be in
maintenance with nothing on it. Report hosts-not-in-maintenance alongside hosts.
CPU usage % is a point-in-time reading. Firewall and packet-processing VMs pin CPU, so
their pools are usually CPU-bound -- that is correct, not a bug.
Consumed is a snapshot too, and for Windows guests it is close to configured (the OS
zeroes pages at boot), so right-sizing savings come mostly from Linux. Right-sizing needs VM
owner sign-off or KubeVirt memory overcommit; say so wherever the aggressive number appears.
- "1.7 TB" is not a GiB figure. ESXi may report 1,789 GiB for a server sold as 1.7 TB.
Pick one planning number, stay consistent with any earlier assessment, and state it.
- Scope mismatches. A customer RFP may size "the top 80% of VMs"; this engine sizes every
powered-on VM. The largest 80% usually holds ~98% of RAM, so the gap is small -- note it
rather than re-running.
- Never present the dollar rows as a quote. They are servers avoided x an assumed price.
Swap in the reseller's number and re-render; it is one command.
Delivering it
Send the PDF plus cluster_mapping.csv (the reseller and the customer's migration planners
use the mapping). In the covering note: the three totals, the merging-only saving, what still
needs confirming (boundaries, right-sizing, price), and nothing else.
1---2name: rvtools-consolidation3description: Bin-pack a VMware estate from RVTools exports onto a target server spec and show how many servers each consolidation level needs -- cluster-for-cluster, moderate, aggressive -- with N+1 spares, licensing and tenant boundaries kept, memory right-sizing, servers avoided and a hardware-cost range, rendered as a two-page Spectro Cloud branded PDF. Use when someone asks things like can it do bin packing, how many servers or nodes do we need, how few hosts can we land this on, condense or consolidate the clusters, save the N+1 servers, cluster for cluster versus consolidated, size the target hardware from RVTools, turn the host savings into dollars, right-size the VMs for the migration. Pairs with a VMware migration readiness assessment; this skill owns the host-count and consolidation math, not OS readiness.4---56# RVTools consolidation scenarios78Answers "how many target servers does this VMware estate need, and how many fewer if we9consolidate?" from the customer's own RVTools exports. Every powered-on VM is actually10bin-packed; nothing is a ratio estimate.1112Two scripts, standard library only (Python 3.9+, no pip install):1314- `scripts/consolidate.py` -- reads the exports, packs each scenario, writes `scenarios.json`15 and `cluster_mapping.csv` (every source cluster and the target pool it lands in per scenario).16- `scripts/render_report.py` -- turns `scenarios.json` into a two-page branded HTML report and,17 with headless Chrome/Chromium available, a PDF.1819## What you need before running2021| Input | Where it comes from |22|---|---|23| One RVTools export per vCenter (`.xlsx`, or a folder of per-sheet CSVs) | Customer. Needs the `vInfo`, `vHost` and `vMemory` sheets. One export = one site; pools never span sites. |24| Target node: RAM, physical cores, threads | Customer's standard build or the proposed server. Ask if not given -- it drives every number. |25| Which clusters are licensing or tenant boundaries | Infer from cluster names, then confirm (see step 3). |26| Server unit prices (optional) | Reseller quote. Without one, give a sensitivity range and say so. |2728## Workflow29301. **Run once with defaults** to see the estate and how clusters classify:3132 ```bash33 python3 scripts/consolidate.py \34 --site "Chicago=Chicago RVTools_export_all.xlsx" \35 --site "London=London RVTools_export_all.xlsx" \36 --out out/37 ```3839 The first printed line is the estate. Check it against the RVTools totals the customer40 quoted (VM count, vCPU, RAM). If they disagree, stop and find out why before going on.41422. **Read `out/cluster_mapping.csv`.** Every cluster name, its hosts, VMs and target pool per43 scenario. This is where misclassification shows up.44453. **Write a config** (start from `assets/example-config.json`). Set the target node, then46 the islands -- clusters whose workloads must never share nodes with anything else:47 - `"kind": "license"` -- the boundary exists for software licensing (Oracle, SQL Server,48 IBM, Windows Datacenter). Kept 1:1 in the moderate scenario; merged by class per site49 only in the aggressive one. **Oracle VMs never go into a general pool** -- Oracle50 licenses every physical core a VM could run on.51 - `"kind": "isolation"` -- tenant-dedicated clusters, firewall A/B pairs, DMZ, PCI.52 Merged per class per site from the moderate scenario on, never mixed with others.5354 Islands match the uppercase cluster name as a regex, first match wins, so order specific55 before general (Oracle EE before Oracle SE). Match loosely -- real estates misspell names.56574. **Run with the config** and read the summary: totals per scenario, net-new servers beyond58 the target model already owned, servers avoided versus the first scenario, and which pools59 are CPU-bound.60615. **Verify before it leaves the building.** For customer-facing numbers, have a second agent62 on a different model recompute A/B/C from the CSVs using only the rules in "Scenario63 rules" below -- not this script -- and compare totals per scenario and per site. An agent64 re-running the same script proves nothing.65666. **Render:**6768 ```bash69 python3 scripts/render_report.py out/scenarios.json --customer "Acme" \70 --data-date 8/20/26 --html out/Acme_Consolidation.html --pdf out/Acme_Consolidation.pdf71 ```7273 It warns if the PDF is not exactly 2 pages. Chrome is found via `$CHROME`, then74 `google-chrome`/`chromium` on PATH, then a puppeteer-cached Chrome, then the macOS app.75 Look at both pages before sending.7677## Scenario rules (the defaults)7879Common to every scenario:8081- Only powered-on, non-template VMs. Clusters with nothing powered on retire with vSphere.82- Pack first-fit decreasing by memory, second dimension allocated vCPU at 4 per hardware thread.83- Measured host CPU (cores x `CPU usage %`) must fit in 70% of the pool's target cores. When84 this binds, the pool is marked CPU-bound.85- +0.25 GiB KubeVirt overhead per VM.86- +1 spare node per target cluster, 3-node minimum per cluster, and no cluster larger than87 the largest cluster in the estate today (split, with a spare each, if exceeded).88- Holding clusters (empty name = standalone hosts, `ESXi_Upgrade`, or 2 or fewer VMs) are not89 workload boundaries: their VMs join the site's largest general-purpose cluster/pool.9091| | A -- Cluster-for-cluster | B -- Moderate | C -- Aggressive |92|---|---|---|---|93| General-purpose clusters | each stays its own cluster | merged per site into prod and non-prod | all merged per site |94| License islands | as-is | as-is | merged by class per site |95| Isolation islands | as-is | merged by class per site | merged by class per site |96| VM memory | configured | configured | Consumed x 1.2, never above configured |97| Memory fill per node | 80% | 80% | 90% |9899Non-prod is a token match on DEV, NP, NONPROD, TST, TEST, QA, UAT, STG, STAGE, DRTEST, PSR,100PERF, BUILD, SANDBOX, LAB. Everything else general-purpose counts as prod.101102The engine also re-runs the last scenario one lever at a time (merging clusters, then103right-sizing, then higher fill) and reports how many servers each lever saves. Lead with the104merging number: it needs no application owner's approval.105106## Config reference107108Any key you omit keeps its default (see `DEFAULTS` at the top of `consolidate.py`). Lists109(`islands`, `scenarios`) replace the default list entirely.110111- `node`: `name` (shown in the report), `ram_gib`, `cores`, `threads`, `model_match` (regex on112 the vHost `Model` column, to count target-model servers already owned).113- `limits`: `vcpu_per_thread`, `cpu_measured_cap`, `vm_overhead_gib`, `min_nodes`,114 `max_nodes` (null = largest cluster today), `spares_per_cluster`, `holding_max_vms`.115- `islands`: list of `{name, match, kind}`.116- `holding`, `nonprod`: regexes on the uppercase cluster name.117- `scenarios`: list of `{key, name, general: cluster|env|site, islands: cluster|merge-isolation|merge, fill, rightsize: null|factor}`.118 The report shows the first three and compares everything to the first.119- `pricing`: `unit_prices` (list, drives the cost rows) and `currency`.120121## Gotchas that have bitten122123- **RVTools `vInfo` repeats column names** (`Datacenter`, `Environment`). The engine reads the124 last `Cluster` column; if a customer's export differs, check `cluster_mapping.csv`.125- **Misspelled cluster names split islands.** One real estate spelled a tenant's name one way126 at one site and transposed two letters at another; a tight regex silently dropped 1.2 TB127 database VMs into a general pool. Grep the mapping CSV for every island name.128- **Holding clusters inflate cluster-for-cluster.** Without the holding rule, three near-empty129 staging clusters became nine phantom nodes.130- **Maintenance mode means the estate is mid-change.** A whole 28-host cluster can be in131 maintenance with nothing on it. Report hosts-not-in-maintenance alongside hosts.132- **`CPU usage %` is a point-in-time reading.** Firewall and packet-processing VMs pin CPU, so133 their pools are usually CPU-bound -- that is correct, not a bug.134- **`Consumed` is a snapshot too**, and for Windows guests it is close to configured (the OS135 zeroes pages at boot), so right-sizing savings come mostly from Linux. Right-sizing needs VM136 owner sign-off or KubeVirt memory overcommit; say so wherever the aggressive number appears.137- **"1.7 TB" is not a GiB figure.** ESXi may report 1,789 GiB for a server sold as 1.7 TB.138 Pick one planning number, stay consistent with any earlier assessment, and state it.139- **Scope mismatches.** A customer RFP may size "the top 80% of VMs"; this engine sizes every140 powered-on VM. The largest 80% usually holds ~98% of RAM, so the gap is small -- note it141 rather than re-running.142- **Never present the dollar rows as a quote.** They are servers avoided x an assumed price.143 Swap in the reseller's number and re-render; it is one command.144145## Delivering it146147Send the PDF plus `cluster_mapping.csv` (the reseller and the customer's migration planners148use the mapping). In the covering note: the three totals, the merging-only saving, what still149needs confirming (boundaries, right-sizing, price), and nothing else.