Drop this at the root of the new repo. Claude Code reads it automatically at the start of every session, so it front-loads the domain knowledge that would otherwise cost debugging cycles to rediscover.
This repo is Invar, a fork of
dalehassinger/VMware-Explore-Hackathon-2026-Live. All work happens here and is
pushed to origin (virtualFrog/Invar). The original repo is wired up as
upstream and is read-only for us; pull from it with git fetch upstream.
A prior working implementation exists at ../VMware-Explore-Hackathon-2026/Tauri/
in the original author's setup. It is not checked out in this working copy, so
treat any reference to it below as conditional on cloning it yourself. Where it is
available, consult it for vCenter property paths, SOAP request shapes and RVTools
column mappings.
- It is reference only. Never modify anything under it.
- Treat it as a source of understanding, not code to copy. Write fresh implementations here.
- It has known defects; the "Improvements required" list in
docs/BUILD-PLAN.mdsays what to do differently. Don't reproduce them.
These live under docs/.
docs/LAB-ENVIRONMENT.md— vCenter host, credentials, what's in the lab, and which empty results are expected rather than bugsdocs/VCENTER-PROPERTY-REFERENCE.md— 89 verified vim25 property paths by object typedocs/RVTOOLS-SHEETS-AND-COLUMNS.md— all 27 RVTools sheets with exact column names, and how much of each the reference implementation coversdocs/RUNNING-ON-WINDOWS.md— prerequisites, commands and troubleshooting for building and running on Windows
A cross-platform desktop app (and optional web service) that pulls VMware vCenter
inventory and presents it as sortable tables, one per object type — a native
alternative to RVTools, which is Windows-only. Data can be exported to a
multi-sheet .xlsx workbook matching RVTools' sheet and column naming.
Stack: Tauri v2 (Rust backend, plain HTML/CSS/vanilla JS frontend — no framework, no build step).
- Never guess a vCenter API field name or XML shape. Query the live vCenter
with
curlfirst, look at the actual response, then write the parsing code. Guessing has produced silent empty tables more than once. - Verify against the live system before declaring something done. Compare row counts to what vCenter itself reports.
- Match RVTools' exact sheet and column names where an equivalent exists. If our units differ (GiB vs MiB), keep RVTools' term but state the real unit — never label GiB values as MiB.
vCenter exposes two APIs and you need both.
- Login:
POST /rest/com/vmware/cis/sessionwith HTTP basic auth → returns{"value": "<token>"}. Send it back as thevmware-api-session-idheader. - Two namespaces exist and differ: legacy
/rest/vcenter/*and newer/api/vcenter/*. Some endpoints only exist in one. Sessions are shared between them — one token works for both. - Good for: host list, VM list, clusters, datastores, resource pools, networks.
- Missing: nearly all hardware detail, per-VM devices, performance stats.
- Endpoint:
POST /sdk,Content-Type: text/xml; charset=utf-8,SOAPAction: urn:vim25/8.0. - Login:
LoginonSessionManager→ grab thevmware_soap_sessioncookie fromSet-Cookieand send it on subsequent calls. - Read properties via
RetrievePropertiesExonpropertyCollector. RetrieveServiceContentonServiceInstanceneeds no auth — handy for version info.
In vim25 SOAP responses, array elements are named after the property's declared field type, not the field name.
config.hardware.device (VirtualDevice[]) → <VirtualDevice xsi:type="VirtualDisk"> NOT <device>
guest.disk (GuestDiskInfo[]) → <GuestDiskInfo> NOT <disk>
snapshot.rootSnapshotList → <VirtualMachineSnapshotTree>
Getting this wrong yields zero rows with no error. If a new SOAP array query returns nothing, check the element names first — dump the raw XML and look.
This applies only to the top-level <val> array. Arrays nested inside a
data object repeat the field name instead. Verified 2026-08-31 against the
lab:
config.hardware.device → <VirtualDevice xsi:type="VirtualDisk"> (type name)
…its storageIOAllocation → <shares><shares>1000</shares>… (field name)
layoutEx.snapshot → <VirtualMachineFileLayoutExSnapshotLayout> (type name)
…its disk chain → <disk><chain><fileKey>3</fileKey>… (field name)
snapshot.rootSnapshotList → <VirtualMachineSnapshotTree> (type name)
…its children → <childSnapshotList> (field name)
vCenter 9.1.0.0300 returned hardware.systemInfo.serialNumber for every host.
9.1.1 returns it for none, on the same hardware, with the hosts still connected
and green. Nothing errors — the column just empties.
cargo run --example property_audit exists for exactly this. It re-checks every
path in data::SHEETS against a live vCenter and names any that returned for
nothing. Run it after any vCenter upgrade, and do not rewrite a mapping
while an upgrade is in flight: a transient absence and a retired field look
identical, and only the second is worth changing code for.
Every policy on a distributed port group is an object carrying inherited plus
the effective value:
securityPolicy/allowPromiscuous/value <- the answer
securityPolicy/allowPromiscuous <- an envelope, no text of its own
Reading the field instead of its value child yields an empty cell and no
error. A standard switch or port group states the same settings directly,
with no envelope, so the two cannot share a reader. vlan is the exception on
the distributed side: it carries vlanId directly, because the field is
polymorphic and a trunk group holds ranges under a different type instead.
Also: a Network container view returns DistributedVirtualPortgroup (a
subclass) but not DistributedVirtualSwitch. If you need switch names, ask
for that type explicitly.
VirtualCdrom, VirtualFloppy and the ethernet cards carry a connectable
block with connected / startConnected inside it. VirtualUSB does not:
it reports connected directly on the device, alongside vendor and product.
Reading connectable/connected on a USB device yields an empty cell and no
error. Verified 2026-09-03 against a real device.
Verified live 2026-09-03, for the inventory path index:
- A moref-valued property carries two type attributes:
<val type="Folder" xsi:type="ManagedObjectReference">group-v4</val>.xsi:typeonly says "this is a reference"; the plaintypeattribute is the managed-object type. Walkparenton the latter — do not infer a type from the moref prefix. CreateContainerViewaccepts a repeating<type>, and oneRetrievePropertiesExcan carry one<propSet>per type against that view. Folder + Datacenter + ComputeResource is one round trip, not three.- A
ComputeResourceview also returnsClusterComputeResource, its subclass. Querying both types is redundant. - A VM reaches its folder via
parent, but its cluster viaruntime.hostand then the host'sparent. Folders and compute are separate branches of the inventory tree; there is no folder path from a VM to its cluster.
- Lab vCenters use self-signed certs → the HTTP client needs
danger_accept_invalid_certs(true). - Escape XML when interpolating credentials into SOAP envelopes. A password
containing
&,<, or>produces malformed XML and a confusing failure where REST-backed views work and SOAP-backed ones don't. - Snapshots nest (a snapshot can have children) — flatten recursively.
- Not everything is available:
vMultiPathandvFileInfoneed datastore file browsing, a different API area entirely. Treat as out of scope unless you have time to spare. vHealthwas previously listed here too, on the belief that it needs alarm/event aggregation. It does not — RVTools computes it from inventory it already has (NTP, NTPD, folder-name, CDROM and snapshot checks), so it is cheap. Only itsZombiecheck needs datastore browsing. Seedocs/RVTOOLS-SHEETS-AND-COLUMNS.md.
vCenter sessions do not clean themselves up promptly — they linger until a ~30 minute idle timeout. Logging in per API call leaks sessions fast; an earlier version of this app accumulated ~300 open sessions in a day of testing.
- Cache and reuse sessions. Key the cache by host + username so multiple vCenters don't evict each other.
- Refresh on a TTL (15 min is safe against the 30 min idle timeout).
- Log out explicitly: REST
DELETE /rest/com/vmware/cis/session, SOAPLogoutonSessionManager. - Clean up on shutdown, handling both SIGINT and SIGTERM —
systemctl stop/restartsends SIGTERM, so Ctrl-C-only handling leaks on every restart.
cargo run --example session_audit does this — RetrievePropertiesEx on
SessionManager / sessionList, counting the <UserSession> elements and
separating the ones belonging to the configured user from everyone else's. Use
it as a bracket: audit, run the thing you suspect, audit again. --all lists
every session, --max <n> makes it exit non-zero so a check can gate on it.
Verified 2026-09-10: the cache works. Eight consecutive invar-export runs
left the count unchanged, so each process logs out cleanly.
Two things that make the count harder to read than it sounds:
- The app sets no user agent, so its sessions are indistinguishable from any other client logged in as the same user. Attribution is by username only.
- A REST login also creates a server-side
vapi-endpointsession from127.0.0.1, so one connection can appear as more than one row.
Learned from the reference implementation — worth adopting from the start rather than refactoring into later.
Each data source is a plain function:
pub async fn fetch_host_data_core(conn: &VCenterConnection) -> Result<Vec<HostInfo>, String>Tauri commands are thin wrappers around these. This is what makes a web-server binary possible later without touching any query logic — retrofitting it meant mechanically rewriting 21 functions.
Sheets are pure functions over an InventorySnapshot (data/snapshot.rs).
The snapshot is fetched once per vCenter with the union of the requested sheets'
property sets; a sheet does no I/O of its own.
- Never add a
retrievecall inside a sheet. Declare the properties in the sheet'sSheetSpecand read them off the snapshot. Sheets used to walk the inventory themselves and five of them cost ten full passes; ten of RVTools' 27 sheets are VM-derived, so that shape does not scale. - A sheet composes shared property groups (
&[VM_CONTEXT_PROPS, VM_PROPS]) rather than restating them, so there is one definition of each group. data::SHEETSis the single registry. Adding a sheet is a new module plus one line there;lib.rsand the frontend need no edit.- Because sheets are pure, they are testable with no vCenter:
snapshot::test_supportbuilds managed objects from XML fragments. Look up columns by RVTools label, never by index.
Config should hold a list of connections, not one. Wrap each per-server
function with an aggregator that loops all servers, concatenates rows, and tags
each row with its source vCenter (RVTools calls this column VI SDK Server).
Bolting this on afterwards touched the config shape, every command, the session
cache, the settings UI, and the export.
When one vCenter is unreachable, return data from the healthy ones plus a visible warning. Never fail everything, and never silently under-report — this is an inventory tool, so a short list that looks complete is the worst outcome.
Define each table's columns once. When adding a cross-cutting column (like
VI SDK Server), append it generically in the table renderer and the xlsx
writer — two places, not once per table. There are ~24 tables; editing them all
by hand is where mistakes creep in.
Avoid let Ok(x) = ... else { continue } in per-object loops. A host that fails
to query should be reported, not silently dropped from the results.
The reference implementation got these wrong; don't inherit them.
- Escape HTML when rendering vCenter data. VM annotations and names are
free-text and become XSS if interpolated into
innerHTML. In a Tauri webview withwithGlobalTauri: true, injected script can reachwindow.__TAURI__and call backend commands. - If you build a web server, authenticate it. Binding
0.0.0.0with no auth and an endpoint that returns stored vCenter credentials in cleartext exposes admin passwords to the whole network. - Don't log credentials.
- Passwords are not in
config.json. They go to the OS credential store viavcenter::secrets.VCenterConnection::passwordis#[serde(default, skip_serializing)], which does double duty: it keeps secrets out of the file and out of the JSONget_confighands to the webview. Removing that attribute reintroduces both holes at once. config::loadreturns connections with blank passwords by design.config::resolveis what fills them in, checkingINVAR_PASSWORD_<n>before the credential store so a headless Linux box with no Secret Service works.- An empty password on save means "keep the stored one", never "erase it". The settings dialog cannot display an existing password, so it cannot resend one; treating blank as erase would wipe a working credential on every edit.
skip_cert_verifydefaults tofalse. An earlier build pinned it totruein the settings UI with no way to turn it off, so every connection the app made was open to interception. It is a per-connection checkbox now. Do not reintroduce a default that skips verification.- CSV is an injection surface. A cell starting
=,+,@or a control character executes when Excel opens the file, and VM names and annotations are free text.export::defuse_formulahandles it.-is deliberately excluded: it starts every negative number.
npm run tauri dev— run the desktop app.npm run tauri build— produce an installer. It only builds for the platform you're on; Windows/Linux installers need to be built on those platforms. Windows setup is written up indocs/RUNNING-ON-WINDOWS.md.cargo run --bin invar-export -- --help— the headless exporter. It sharesconfig.jsonwith the desktop app, andconfig::default_dir()is how a non-Tauri binary finds that directory without anAppHandle.- Adding a second binary breaks
cargo run, whichtauri devuses internally. Fixed bydefault-run = "invar"inCargo.toml's[package], already set. - A second binary also breaks
tauri build --target universal-apple-darwin, and only that target. The bundler copies every binary in the package into the.app, but its universal handling lipos only the main one, so it dies withFailed to copy binary from ".../universal-apple-darwin/release/invar-export". Build both slices andlipothem into that directory before bundling; the release workflow does this. Verified 2026-09-10: plaintauri buildis unaffected, because cargo has already put every binary intarget/release. - CI gates on
cargo clippy --all-targets -- -D warnings. Two lints are allowed inCargo.toml's[lints.clippy]with reasons; don't add more without one.cargo fmtis deliberately not enforced: the codebase predates any rustfmt config and reformatting it would bury real changes in noise. - Releases come from
.github/workflows/release.ymlon av*tag. macOS builds are signed and notarized when theAPPLE_*secrets are present, and produce one universal.dmg. Windows code signing is not wired up. The whole procedure, including where each secret comes from, isdocs/RELEASING.md. Three version fields have to agree:tauri.conf.json,Cargo.toml,package.json. - Keep tests free of absolute machine-specific paths — write to
std::env::temp_dir().