guest@6admin.io:~$

log0u7.hyperv: the second dynamic inventory, this time from the hypervisor

The DNS inventory reads the network to guess where hosts are. Its sibling does not guess: log0u7.hyperv (v1.0.0, published on Ansible Galaxy , MIT) asks the hypervisor. It is an Ansible inventory plugin (the collection log0u7.hyperv, plugin log0u7.hyperv.inventory) that builds the inventory from the virtual machines running on one or more Hyper-V hosts, queried over WinRM or PSRP. Second episode of the inventory series: same goal (the truth lives somewhere and Ansible should read it), opposite read path.

The plugin, in 182 lines

plugins/inventory/inventory.py is 182 lines and most of the design decisions are the two words after the class declaration: it inherits BaseInventoryPlugin, Cacheable and Constructable, the native Ansible mixins. No homemade caching (the inventory_cache fragment plus any cache plugin just work), no homemade grouping (compose, groups and keyed_groups from the constructed fragment apply to the inventory like any other).

The actual Hyper-V query lives in plugins/module_utils/hyperv.py (360 lines): an embedded PowerShell block, foreach ($vm in @(Get-VM)) walking VMs and their network adapters, serialized back to Python. The transport choice (winrm default, psrp alternative) selects pywinrm or pypsrp on the control node; ports 5985/5986 do the rest.

What each VM becomes

Hostvars carry the facts that matter for automation: ansible_host (primary IPv4, the connection target), hyperv_state (running, stopped…), hyperv_cpu_count, hyperv_memory_mb (rounded down from bytes), hyperv_network_adapters (MAC list), hyperv_notes (the VM description field, nullable), hyperv_host and hyperv_custom_tags (user-defined VM tags, flattened into a dict).

Groups come in three kinds, all built automatically: one per state (running…), one per Hyper-V host (hyperv_host_hv01), and, when group_by_tags: true, one per custom tag key (tag_<key>). A fleet’s “which VMs run on which hypervisor, which are stopped, which carry tag prod” questions become group names.

Credentials ride lookup('env', 'ANSIBLE_USER') / ANSIBLE_PASSWORD: the inventory source stays commitable, the secret never does. The docs ship a docs/security_guardrails.md alongside, the same honest-caveat discipline as the DNS collection.

What differs from the DNS inventory

Same series, so the deltas are the interesting part:

  • Freshness vs authority. DNS answers “what does the network say the fleet is”; Hyper-V answers “what actually exists right now, powered on or not”. Stopped VMs appear with hyperv_state: stopped; DNS cannot see a machine that is off.
  • Query cost. DNS is trivially parallel with per-thread resolvers; Hyper-V is one PSRP session per host, sequential per host but parallel across hosts. The cache matters more here: a 5-minute cache_timeout turns every Ansible run into one WinRM round trip per host.
  • Trust model. DNS metadata is public-by-default (with the guardrails documented); Hyper-V credentials are private-by-default and travel only through env lookups and encrypted 5986.
  • Distribution. The DNS collection ships a zone-file converter for migration; this one needs none: the hypervisor is the inventory source, there is nothing to convert.

The hypervisor knows too

  • The Cacheable/Constructable mixins are the whole contract. Inherit them and the plugin behaves like a first-class citizen; skip them and you will re-implement caching badly.
  • Query the thing that owns the truth. For VM fleets, that is the hypervisor, not a network protocol that may or may not have seen the machine boot.
  • Env lookups are the credentials convention. Ansible’s inventory sources get committed; the plugin must make that safe by construction.
  • A 182-line inventory plugin is enough when the mixins carry the framework work. The 360 lines of PowerShell in module_utils are where the domain actually lives.

[edit: 2026] The Ansible implementation of all this: the hyperv role .