Skip to content

Commit

Permalink
fix: Report correct guest UUID, when VM Generation 2 is used
Browse files Browse the repository at this point in the history
* When VM is created as Generation 2 in the setup wizard of
  Hyper-V hypervisor, then hypervisor reports UUID of given
  VM using little-endian. Thus it is not necessary to
  convert this UUID as we do for Generation 1.
* We solve this issue by extending SQL query. We also ask
  for VirtualSystemSubType, which contains version of VM
  generation ("Microsoft:Hyper-V:SubType:1" or
  "Microsoft:Hyper-V:SubType:2"). When VM is generation 1,
  then we convert UUID from big-endian to little-endian
  as we have did it in the past. When generation is 2, then
  we do nothing with guest UUID.
* Changed code around little bit, because there was variable
  uuid used for different purpose and it was confusing.
* TODO: hyperv.py deserves more refactoring
* More information about generation 1 or 2 on Hyper-V could be
  found e.g. here:
  * https://learn.microsoft.com/en-us/windows-server/virtualization/hyper-v/get-started/create-a-virtual-machine-in-hyper-v
  * https://learn.microsoft.com/en-us/windows-server/virtualization/hyper-v/plan/should-i-create-a-generation-1-or-2-virtual-machine-in-hyper-v
  • Loading branch information
jirihnidek committed Feb 6, 2025
1 parent 37c4269 commit 934fa7b
Showing 1 changed file with 65 additions and 25 deletions.
90 changes: 65 additions & 25 deletions virtwho/virt/hyperv/hyperv.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,12 +272,15 @@ def post(self, body):
"Content-Type": "application/soap+xml;charset=UTF-8"
}
try:
self.logger.debug("Trying to POST request to Hyper-V")
# self.logger.debug(body)
response = self.connection.post(self.url, body, headers=headers)
except requests.RequestException as e:
raise HyperVException("Unable to connect to Hyper-V server: %s" % str(e))

if response.status_code == requests.codes.ok:
self.logger.debug(f'Received valid response from Hyper-V server: {response.status_code}')
# self.logger.debug(response.text)
return response.content
elif response.status_code == 401:
raise HyperVAuthFailed("Authentication failed")
Expand Down Expand Up @@ -404,6 +407,10 @@ class HyperVCallFailed(HyperVException):
class HyperV(virt.Virt):
CONFIG_TYPE = "hyperv"

#
GEN_VERSION_1 = "Microsoft:Hyper-V:SubType:1"
GEN_VERSION_2 = "Microsoft:Hyper-V:SubType:2"

def __init__(self, logger, config, dest, terminate_event=None,
interval=None, oneshot=False, status=False):
super(HyperV, self).__init__(logger, config, dest,
Expand All @@ -427,17 +434,32 @@ def connect(self):
s.auth = HyperVAuth(self.username, self.password, self.logger)
return s

@classmethod
def decodeWinUUID(cls, uuid):
""" Windows UUID needs to be decoded using following key
def decodeWinUUID(self, uuid, gen_version=None):
"""
Windows UUID needs to be decoded using following key:
From: {78563412-AB90-EFCD-1234-567890ABCDEF}
To: 12345678-90AB-CDEF-1234-567890ABCDEF
when generation of VM is 1, because Windows UUID uses big-endian standard
It seems that Generation 2 of VMs use little-endian standard, and we need
to strip only beginning and trailing brackets in this case.
"""
if uuid[0] == "{":
s = uuid[1:-1]
else:
s = uuid
return s[6:8] + s[4:6] + s[2:4] + s[0:2] + "-" + s[11:13] + s[9:11] + "-" + s[16:18] + s[14:16] + s[18:]

if gen_version is None or gen_version == self.GEN_VERSION_1:
return s[6:8] + s[4:6] + s[2:4] + s[0:2] + "-" + s[11:13] + s[9:11] + "-" + s[16:18] + s[14:16] + s[18:]
elif gen_version == self.GEN_VERSION_2:
return s
else:
self.logger.warning(
f"Unsupported version of VirtualSystemSubType: '{gen_version}', using little-endian UUID"
)
return s

def getVmmsVersion(self, hypervsoap):
"""
Expand All @@ -457,12 +479,12 @@ def getHostGuestMapping(self):
guests = []
connection = self.connect()
hypervsoap = HyperVSoap(self.url, connection, self.logger)
uuid = None
ctx_uuid = None
if not self.useNewApi:
try:
# SettingType == 3 means current setting, 5 is snapshot - we don't want snapshots
uuid = hypervsoap.Enumerate(
"select BIOSGUID, VirtualSystemIdentifier "
ctx_uuid = hypervsoap.Enumerate(
"select BIOSGUID, VirtualSystemIdentifier, VirtualSystemSubType "
"from Msvm_VirtualSystemSettingData "
"where SettingType = 3",
"root/virtualization")
Expand All @@ -474,61 +496,79 @@ def getHostGuestMapping(self):
if self.useNewApi:
# Filter out Planned VMs and snapshots, see
# http://msdn.microsoft.com/en-us/library/hh850257%28v=vs.85%29.aspx
uuid = hypervsoap.Enumerate(
"select BIOSGUID, VirtualSystemIdentifier "
ctx_uuid = hypervsoap.Enumerate(
"select BIOSGUID, VirtualSystemIdentifier, VirtualSystemSubType "
"from Msvm_VirtualSystemSettingData "
"where VirtualSystemType = 'Microsoft:Hyper-V:System:Realized'",
"root/virtualization/v2")

# Get guest states
guest_states = hypervsoap.Invoke_GetSummaryInformation(
"root/virtualization/v2" if self.useNewApi else "root/virtualization")
vmmsVersion = self.getVmmsVersion(hypervsoap)
for instance in hypervsoap.Pull(uuid):

# Try to get information about UUID and state
for instance in hypervsoap.Pull(ctx_uuid):
try:
uuid = instance["BIOSGUID"]
assert uuid is not None
guest_uuid = instance["BIOSGUID"]
assert guest_uuid is not None
except (KeyError, AssertionError):
self.logger.warning("Guest without BIOSGUID found, ignoring")
continue

try:
system_Id = instance["VirtualSystemIdentifier"]
except KeyError:
self.logger.warning("Guest %s is missing VirtualSystemIdentifier", uuid)
self.logger.warning("Guest %s is missing VirtualSystemIdentifier", guest_uuid)
continue

try:
gen_version = instance["VirtualSystemSubType"]
except KeyError:
self.logger.warning(f"Guest {guest_uuid} is missing VirtualSystemSubType")
gen_version = None
else:
self.logger.debug(f"Guest {guest_uuid} has VirtualSystemSubType: '{gen_version}'")

try:
state = guest_states[system_Id]
except KeyError:
self.logger.warning("Unknown state for guest %s", uuid)
self.logger.warning("Unknown state for guest %s", guest_uuid)
state = virt.Guest.STATE_UNKNOWN

guests.append(virt.Guest(HyperV.decodeWinUUID(uuid), self.CONFIG_TYPE, state))
# Get the hostname
little_endian_guest_uuid = self.decodeWinUUID(guest_uuid, gen_version)
guests.append(virt.Guest(little_endian_guest_uuid, self.CONFIG_TYPE, state))

# Try to get the hostname and socket count of hypervisor
hostname = None
socket_count = None
data = hypervsoap.Enumerate("select DNSHostName, NumberOfProcessors from Win32_ComputerSystem", "root/cimv2")
for instance in hypervsoap.Pull(data, "root/cimv2"):
ctx_uuid = hypervsoap.Enumerate("select DNSHostName, NumberOfProcessors from Win32_ComputerSystem", "root/cimv2")
for instance in hypervsoap.Pull(ctx_uuid, "root/cimv2"):
hostname = instance["DNSHostName"]
socket_count = instance["NumberOfProcessors"]

uuid = hypervsoap.Enumerate("select UUID from Win32_ComputerSystemProduct", "root/cimv2")
# Try to get UUID of hypervisor
ctx_uuid = hypervsoap.Enumerate("select UUID from Win32_ComputerSystemProduct", "root/cimv2")
system_uuid = None
for instance in hypervsoap.Pull(uuid, "root/cimv2"):
system_uuid = HyperV.decodeWinUUID(instance["UUID"])
for instance in hypervsoap.Pull(ctx_uuid, "root/cimv2"):
system_uuid = self.decodeWinUUID(uuid=instance["UUID"])

host_id = None
if self.config['hypervisor_id'] == 'uuid':
host = system_uuid
host_id = system_uuid
elif self.config['hypervisor_id'] == 'hostname':
host = hostname
host_id = hostname

vmmsVersion = self.getVmmsVersion(hypervsoap)

facts = {
virt.Hypervisor.CPU_SOCKET_FACT: str(socket_count),
virt.Hypervisor.HYPERVISOR_TYPE_FACT: 'hyperv',
virt.Hypervisor.HYPERVISOR_VERSION_FACT: vmmsVersion,
virt.Hypervisor.SYSTEM_UUID_FACT: system_uuid
}
hypervisor = virt.Hypervisor(hypervisorId=host, name=hostname, guestIds=guests, facts=facts)

hypervisor = virt.Hypervisor(hypervisorId=host_id, name=hostname, guestIds=guests, facts=facts)

return {'hypervisors': [hypervisor]}

def statusConfirmConnection(self):
Expand Down

0 comments on commit 934fa7b

Please sign in to comment.