Agent
...
Hypervisor Discovery
Hyper-V VM Inventory
5 min
will work partially using localsystem, but will not work for vm files on network drives (nas etc) for this, the script will have to be run as an administrator who has fill access to hyper v and the network shares holding the vhds powershell \#requires runasadministrator <# synopsis retrieves detailed hyper v virtual machine inventory information using wmi/cim description queries the local hyper v host for vms and gathers details including \ basic vm info (name, id, state) \ configuration (memory, cpu) \ guest info via kvp (os, hostname, ip addresses, integration services version) \ network adapters (mac, switch) \ virtual hard drives (path, type, configured size, actual size, access status) outputs the collected data as a json object includes enhanced handling for vhd file access, providing status notes especially when running as localsystem encountering network share permissions issues notes author based on user script and ai suggestions date 2025 04 07 requires powershell v3+, hyper v role installed, administrator privileges the get vhd cmdlet (part of hyper v powershell module) is used for vhd type detection running context designed to run locally on the hyper v host if run as localsystem, network vhd file access requires the computername$ account to have permissions on the target network share and file system \#> \# define the hyper v wmi namespace $namespace = "root\virtualization\v2" \# define a mapping for enabledstate numerical values to readable strings $enabledstatemap = @{ 0 = 'unknown' 2 = 'running' # enabled 3 = 'off' # disabled 32768 = 'paused' 32769 = 'suspended' 32770 = 'starting' 32771 = 'snapshotting' 32773 = 'saving' 32774 = 'stopping' 32776 = 'pausing' 32777 = 'resuming' } write output "attempting to retrieve vms from wmi namespace '$namespace' " \# retrieve guest vms (msvm computersystem where caption equals "virtual machine") try { \# use a filter for potentially better performance than where object after retrieval $vms = get ciminstance namespace $namespace classname msvm computersystem filter 'caption = "virtual machine"' erroraction stop } catch { write error "failed to retrieve vms error $($ exception message)" write error "ensure hyper v role is installed, the service is running, and you have necessary permissions (run as administrator) " exit 1 } \# check if any vms were found if ( not $vms) { write output "no vms found ensure hyper v is installed, running, and vms exist " exit } write output "found $($vms count) vms processing each " \# initialize an array to store details for all vms $allvminfo = @() \# loop through each vm found foreach ($vm in $vms) { $vmname = $vm elementname $vmid = $vm name # typically the vm's guid write host "processing vm $vmname ($vmid)" try { \# basic vm details $statenumber = $vm enabledstate $enabledstatestring = $enabledstatemap\[$statenumber] # look up the readable state if ( not $enabledstatestring) { $enabledstatestring = "unknown state ($statenumber)" } # handle unknown codes $creationtime = $vm creationtime \# retrieve vm settings (msvm virtualsystemsettingdata) \# use association msvm settingsdefinestate links msvm computersystem to msvm virtualsystemsettingdata $vmsettings = get cimassociatedinstance inputobject $vm resultclassname msvm virtualsystemsettingdata erroraction stop | select object first 1 \# note assumes the first associated setting data is the active one complex snapshot scenarios might require more filtering $vmnotes = if($vmsettings) {$vmsettings notes} else {$null} \# memory configuration $memorystartupmb = if ($vmsettings) { $vmsettings memorystartup } else { $null } # in mb $dynamicmemoryenabled = if ($vmsettings) { $vmsettings dynamicmemoryenabled } else { $null } $memoryminimummb = if ($vmsettings and $dynamicmemoryenabled) { $vmsettings memoryminimum } else { $null } # in mb $memorymaximummb = if ($vmsettings and $dynamicmemoryenabled) { $vmsettings memorymaximum } else { $null } # in mb \# processor configuration $procsettings = $null if ($vmsettings) { \# use association msvm virtualsystemsettingdata to msvm processorsettingdata $procsettings = get cimassociatedinstance inputobject $vmsettings resultclassname msvm processorsettingdata erroraction silentlycontinue | select object first 1 } $cpucount = if ($procsettings) { $procsettings virtualquantity } else { $null } $cpulimit = if ($procsettings) { $procsettings limit } else { $null } # cpu limit % (100000 = 100%) $cpureservation = if ($procsettings) { $procsettings reservation } else { $null } # cpu reservation % (not % based, matches virtualquantity units) $cpuweight = if ($procsettings) { $procsettings weight } else { $null } # relative weight \# retrieve kvp guest data (integration services data exchange) $kvpdata = @{} $guestos = $null; $guestosversion = $null; $guesthostname = $null; $integrationversion = $null; $networkaddressipv4 = $null; $networkaddressipv6 = $null try { \# use association msvm computersystem to msvm kvpexchangecomponentdata $kvpcomponentdata = get cimassociatedinstance inputobject $vm resultclassname msvm kvpexchangecomponentdata erroraction silentlycontinue | select object first 1 if ($kvpcomponentdata and $kvpcomponentdata guestintrinsicexchangeitems) { \# data is often xml containing key value pairs within guestintrinsicexchangeitems $xmldata = \[xml]$kvpcomponentdata guestintrinsicexchangeitems $xmldata objs obj property | foreach object { if ($ name and $ '#') { # ensure name and value exist $kvpdata\[$ name] = $ '#' } } \# extract common kvp keys safely $guestos = if ($kvpdata containskey('osname')) { $kvpdata\['osname'] } else { $null } $guestosversion = if ($kvpdata containskey('osversion')) { $kvpdata\['osversion'] } else { $null } $guesthostname = if ($kvpdata containskey('fullyqualifieddomainname')) { $kvpdata\['fullyqualifieddomainname'] } elseif ($kvpdata containskey('computername')) { $kvpdata\['computername'] } else { $null } # fqdn preferred $integrationversion = if ($kvpdata containskey('integrationservicesversion')) { $kvpdata\['integrationservicesversion'] } else { $null } $networkaddressipv4 = if ($kvpdata containskey('networkaddressipv4')) { $kvpdata\['networkaddressipv4'] } else { $null } # often semi colon separated $networkaddressipv6 = if ($kvpdata containskey('networkaddressipv6')) { $kvpdata\['networkaddressipv6'] } else { $null } # often semi colon separated } else { write warning "vm $vmname kvp guestintrinsicexchangeitems not found or empty integration services might be off, malfunctioning, or guest os doesn't support " } } catch { write warning "vm $vmname could not parse kvp data error $($ exception message) integration services might be off or guest os doesn't support " } \# retrieve network adapter details $nicinfoarray = @() if ($vmsettings) { \# use association from $vmsettings > msvm resourceallocationsettingdata (rasd) > msvm syntheticethernetportsettingdata $ethernetrasds = get cimassociatedinstance inputobject $vmsettings resultclassname msvm resourceallocationsettingdata erroraction silentlycontinue | where object { $ resourcetype eq 10 } # resourcetype 10 = ethernet adapter if ($ethernetrasds) { foreach ($rasd in $ethernetrasds) { \# get the specific nic settings associated with this resource allocation $nicsetting = get cimassociatedinstance inputobject $rasd resultclassname msvm syntheticethernetportsettingdata erroraction silentlycontinue | select object first 1 if ($nicsetting) { \# get connected switch name via msvm ethernetportallocationsettingdata association $portallocation = get cimassociatedinstance inputobject $rasd resultclassname msvm ethernetportallocationsettingdata erroraction silentlycontinue | select object first 1 $switchname = $null if ($portallocation and $portallocation hostresource) { \# hostresource often contains the switch id like 'microsoft\ hyper v\ virtual ethernet switch\\{guid}' $switchname = $portallocation elementname # often descriptive, like the nic name + switch name \# could parse hostresource for guid and look up msvm virtualethernetswitch if elementname isn't sufficient } $nicinfoarray += \[pscustomobject]@{ adaptername = $nicsetting elementname macaddress = $nicsetting address isstaticmac = $nicsetting staticmacaddress switchname = $switchname # name of the vswitch connected to (or associated port allocation name) } } } } } \# retrieve virtual hard drive details (enhanced handling) \# use association from $vmsettings > msvm resourceallocationsettingdata (rasd) > msvm storageallocationsettingdata (sasd) $virtualharddrives = @() if ($vmsettings) { \# find resource allocation settings associated with storage $diskrasds = get cimassociatedinstance inputobject $vmsettings resultclassname msvm resourceallocationsettingdata erroraction silentlycontinue | where object { $ resourcesubtype like " virtual hard disk " or $ resourcetype in (17, 21, 31) } # check resourcesubtype first, then common resourcetypes if ($diskrasds) { foreach ($rasd in $diskrasds) { \# get storage allocation details associated with this resource $storagesetting = get cimassociatedinstance inputobject $rasd resultclassname msvm storageallocationsettingdata erroraction silentlycontinue | select object first 1 \# ensure it's specifically a virtual hard disk type we are interested in if ($storagesetting and $storagesetting resourcesubtype eq "microsoft\ hyper v\ virtual hard disk") { $filepath = $null \# get path from wmi configuration (doesn't require file access permission) if ($storagesetting hostresource and $storagesetting hostresource count gt 0) { $filepath = $storagesetting hostresource\[0] # primary location for path } if ( not $filepath and $rasd connection and $rasd connection count gt 0) { $filepath = $rasd connection\[0] # fallback location for path } \# initialize variables for details and status $configuredsizegb = if ($storagesetting virtualquantity) { \[math] round($storagesetting virtualquantity / 1gb, 2) } else { $null } # max size from wmi $actualfilesizegb = $null # actual size on disk, null initially $vhdtype = $null # type (fixed, dynamic, differencing) $accessstatus = "unknown" # default file access status $parentpath = $rasd parent # check for differencing disk via parent property in wmi \# proceed only if we have a path from wmi configuration if ($filepath) { $isnetworkpath = $filepath startswith("\\\\") $pathexistsoraccessible = $false # assume not accessible initially \# 1 attempt test path (checks existence and accessibility at a basic level) try { \# use pathtype leaf to ensure it checks for a file, not just the directory path $pathexistsoraccessible = test path path $filepath pathtype leaf erroraction stop \# if test path is $true, we likely have some level of access (e g , read attributes) } catch { \# test path failed, could be permissions, offline share, typo, or truly non existent $pathexistsoraccessible = $false $errormessage = $ exception message if ($isnetworkpath) { $accessstatus = "path inaccessible (network path check permissions/connectivity)" write warning "vm $vmname cannot access network vhd path '$filepath' likely permission issue for computer account '$($env\ computername)$' or offline share error $errormessage" } else { $accessstatus = "path not found or inaccessible (local path)" write warning "vm $vmname cannot access local vhd path '$filepath' error $errormessage" } } \# 2 attempt to get vhd type (only if path seems accessible or it's differencing) if ($parentpath) { $vhdtype = "differencing" \# no need to call get vhd if we know it's differencing based on parent property from wmi \# set status based on test path result if it wasn't already set to an error if ($accessstatus eq "unknown") { $accessstatus = if ($pathexistsoraccessible) {"ok (differencing)"} else {"path inaccessible (differencing)"} } elseif ($accessstatus like " inaccessible ") { \# keep inaccessible status but clarify type $accessstatus += " (differencing)" } } elseif ($pathexistsoraccessible) { \# path seems accessible, try get vhd for type (fixed/dynamic) requires hyper v module try { $vhdinfo = get vhd path $filepath erroraction stop if ($vhdinfo) { $vhdtype = $vhdinfo vhdtype # e g , fixed, dynamic \# only set status to ok if get vhd succeeded and we haven't hit other errors if ($accessstatus eq 'unknown') { $accessstatus = "ok" } } else { $vhdtype = "unknown (get vhd returned no data)" if ($accessstatus eq 'unknown') { $accessstatus = "read warning (get vhd)" } } } catch { $vhdtype = "unknown (get vhd failed)" $errormessage = $ exception message write warning "vm $vmname get vhd failed for '$filepath' may require hyper v ps module loaded and sufficient file permissions error $errormessage" if ($accessstatus eq 'unknown') { $accessstatus = "read error (get vhd)" if ($isnetworkpath) { $accessstatus += " (network path?)"} } } } else { \# path is not accessible/found, cannot determine type beyond differencing check above if ($vhdtype ne "differencing") { # don't overwrite if already identified as differencing $vhdtype = "unknown (path not accessible)" } \# accessstatus should already be set from test path failure } \# 3 attempt to get actual file size (only if path seems accessible via test path) if ($pathexistsoraccessible) { try { $fileitem = get item $filepath erroraction stop $actualfilesizegb = \[math] round($fileitem length / 1gb, 2) \# set status to ok only if this succeeds and no prior error set status if ($accessstatus eq 'unknown' or $accessstatus like "ok ") { $accessstatus = "ok" } } catch { $errormessage = $ exception message write warning "vm $vmname error reading file size for '$filepath' using get item error $errormessage" $actualfilesizegb = $null # ensure size is null on error \# update status if not already an error, or if was previously ok but get item failed if ($accessstatus eq 'unknown' or $accessstatus like "ok ") { $accessstatus = "read error (get item)" if ($isnetworkpath) { $accessstatus += " (network path?)"} } } } else { \# path not accessible/found, cannot get actual size $actualfilesizegb = $null \# accessstatus should already be set from test path failure } } else { \# no file path could be determined from wmi configuration for this storage resource $accessstatus = "path not configured in wmi" write warning "vm $vmname could not determine vhd file path from wmi for a disk resource (rasd $($rasd instanceid)) " } \# add the gathered vhd info, including the file access status $virtualharddrives += \[pscustomobject]@{ filepath = $filepath # the configured path from wmi parentpath = $parentpath # path to parent disk if differencing (from wmi) vhdtype = $vhdtype # best guess type (differencing, dynamic, fixed, or unknown/error) configuredsizegb = $configuredsizegb # max size from wmi (bytes converted to gb) actualfilesizegb = $actualfilesizegb # actual size on disk (gb), or $null if inaccessible/error addressonparent = $rasd addressonparent # controller location (e g , 0, 1) from wmi accessstatus = $accessstatus # status of attempts to access the file ("ok", "inaccessible (network path?)", "read error", etc ) } } # end if ($storagesetting and type is vhd) } # end foreach ($rasd in $diskrasds) } # end if ($diskrasds) } # end if ($vmsettings) \# compose the final object for this vm $vmdetail = \[pscustomobject]@{ vmname = $vmname vmid = $vmid state = $enabledstatestring creationtime = $creationtime notes = $vmnotes memorystartupmb = $memorystartupmb dynamicmemoryenabled = $dynamicmemoryenabled memoryminimummb = $memoryminimummb # only relevant if dynamic memory is on memorymaximummb = $memorymaximummb # only relevant if dynamic memory is on cpucount = $cpucount cpulimitpercent = if($cpulimit) { $cpulimit / 1000 } else {$null} # convert 100000 > 100% cpureservation = $cpureservation # matches virtualquantity units, not percentage based like limit usually correlates to #cpus cpuweight = $cpuweight # relative weight for cpu scheduler guestos = $guestos guestosversion = $guestosversion guesthostname = $guesthostname integrationversion = $integrationversion networkaddressipv4 = $networkaddressipv4 # semicolon separated if multiple ips reported by kvp networkaddressipv6 = $networkaddressipv6 # semicolon separated if multiple ips reported by kvp networkadapters = $nicinfoarray # array of nic objects virtualharddrives = $virtualharddrives # array of vhd objects with enhanced status hypervhost = $env\ computername # record which host this was run on } \# add the details for this vm to the main array $allvminfo += $vmdetail } catch { \# catch errors during processing of a single vm write error "failed to process vm '$vmname' ($vmid) error at $($ invocationinfo scriptlinenumber) $($ exception message)" \# add a placeholder object indicating the failure for this vm $allvminfo += \[pscustomobject]@{ vmname = $vmname vmid = $vmid state = "error processing" error = $ exception message hypervhost = $env\ computername } } } # end foreach vm loop write output "finished processing vms " \# output generation \# convert the final results array to json format \# increase depth if necessary, 5 is usually sufficient for this structure $jsonoutput = $allvminfo | convertto json depth 5 compress # use compress for smaller output string \# output the json string to the console (or pipeline) write output $jsonoutput \# optional save json to a file \# $jsonfilepath = "c \programdata\hypervinventory\\$(get date format 'yyyymmddhhmmss') hypervinventory json" \# # ensure directory exists \# $jsondirpath = split path $jsonfilepath parent \# if ( not (test path $jsondirpath)) { \# new item itemtype directory path $jsondirpath force | out null \# } \# try { \# $jsonoutput | out file filepath $jsonfilepath encoding utf8 erroraction stop \# write host "inventory json saved to $jsonfilepath" \# } catch { \# write error "failed to save json to '$jsonfilepath' error $($ exception message)" \# } \# database insertion (conceptual example needs sqlite module like pssqlite) \# this section remains commented out as it requires external modules and setup \# the json output above can be used as input for a separate database insertion process \# $dbpath = "c \programdata\hypervinventory\hypervinventory sqlite" \# $sqlitemodule = "pssqlite" # or microsoft data sqlite \# if (get command invoke sqlitequery module $sqlitemodule erroraction silentlycontinue) { \# # (code for table creation if not exists recommended) \# # (code for looping through $allvminfo) \# # (code for upsert into vms table) \# # (code for delete/insert into vm networkadapters table) \# # (code for delete/insert into vm vhds table, using the $vhd accessstatus field) \# write host "database operations would occur here " \# } else { \# write warning "sqlite module ('$sqlitemodule') not found or command 'invoke sqlitequery' unavailable skipping direct database operations " \# } write host "script finished " database tables for vm inventory table vms stores core information for each discovered virtual machine (this table is the target of foreign keys) column name data type constraints description vmid text primary key not null unique guid of the virtual machine vmname text user defined name of the vm state text current operational state (e g , "running", "off") creationtime datetime timestamp when the vm was created notes text vm notes/description field from hyper v settings memorystartupmb integer assigned startup memory in megabytes dynamicmemoryenabled integer boolean (0 or 1) indicating if dynamic memory is enabled memoryminimummb integer minimum dynamic memory in mb (if enabled) memorymaximummb integer maximum dynamic memory in mb (if enabled) cpucount integer number of virtual cpus assigned cpulimitpercent real cpu usage limit as a percentage (e g , 100 0, 75 5) cpureservation integer reserved cpu resources (matches vcpu units, not percentage) cpuweight integer relative cpu scheduling weight guestos text guest operating system name reported by kvp (if available) guestosversion text guest os version reported by kvp (if available) guesthostname text guest hostname/fqdn reported by kvp (if available) integrationversion text integration services version reported by kvp (if available) networkaddressipv4 text ipv4 addresses reported by kvp (semi colon separated if multiple) networkaddressipv6 text ipv6 addresses reported by kvp (semi colon separated if multiple) hypervhost text not null name of the hyper v host where the vm was found lastinventorytimestamp datetime not null timestamp when this record was last inserted or updated table vm networkadapters stores information about each network interface controller (nic) associated with a vm column name data type constraints description macaddress text primary key not null mac address of the virtual nic (usually unique per host) vmid text not null, foreign key(vmid) references vms(vmid) links this nic back to the specific vm in the vms table adaptername text name of the network adapter within the vm settings isstaticmac integer boolean (0 or 1) indicating if the mac address is statically set switchname text name of the virtual switch or port allocation this nic connects to note using macaddress as a primary key assumes mac addresses are unique across all inventoried vms on a host if duplicates are possible in your environment (e g , disconnected nics with same static mac on different vms), you might consider a composite primary key like (vmid, macaddress) or (vmid, adaptername) table vm vhds stores information about each virtual hard drive (vhd/vhdx) associated with a vm column name data type constraints description filepath text primary key not null configured path to the vhd/vhdx file vmid text not null, foreign key(vmid) references vms(vmid) links this vhd back to the specific vm in the vms table parentpath text path to the parent disk (for differencing disks only) vhdtype text type of vhd (e g , "fixed", "dynamic", "differencing", "unknown ") configuredsizegb real maximum configured size of the vhd in gigabytes actualfilesizegb real actual size of the vhd file on disk in gigabytes (if accessible) addressonparent text location on the controller (e g , "0,0", "1,0") accessstatus text status indicating accessibility ("ok", "inaccessible (network path?)", "read error") note using filepath as a primary key assumes file paths are unique for configured vhds across the host inventory if multiple vms could genuinely point to the exact same file path string (unlikely but possible), consider a composite key like (vmid, filepath) general note consider adding indexes on foreign key columns ( vmid in vm networkadapters and vm vhds ) and other frequently queried columns (e g , vmname , hypervhost in vms ) to optimize query performance