[PATCH] hyperv: Implement virDomainGetGuestInfo()
From: Michal Privoznik <mprivozn@redhat.com> The hyperv hypervisor also has guest agent, in fact multiple ones [1][2]. The first one, KVP, is for storing Key-Value Pairs and in fact it's already used by our hyperv driver when querying domifaddr (see v12.1.0-rc1~148 for more info). Anyhow, the KVP service is capable of more, it can provide guest OS info, guest FQDN and others. These informations are exposed via GuestIntrinsicExchangeItems member of the Msvm_KvpExchangeComponent struct [3]. You may have noticed the member is an array of strings, well those strings are in fact XML documents. For instance: <INSTANCE CLASSNAME="Msvm_KvpExchangeDataItem"> <PROPERTY NAME="Caption" TYPE="string"/> <PROPERTY NAME="Data" TYPE="string"> <VALUE>6.12.61-1-lts</VALUE> </PROPERTY> <PROPERTY NAME="Description" TYPE="string"/> <PROPERTY NAME="ElementName" TYPE="string"/> <PROPERTY NAME="InstanceID" TYPE="string"/> <PROPERTY NAME="Name" TYPE="string"> <VALUE>OSBuildNumber</VALUE> </PROPERTY> <PROPERTY NAME="Source" TYPE="uint16"> <VALUE>2</VALUE> </PROPERTY> </INSTANCE> This is a bit messy to work with, because it's not like in QEMU's world where each type of guest info (virDomainGuestInfoTypes) corresponds 1:1 to a guest agent command. Hence the lookupTable in hypervGetServicesProcessOne(). NB, the original jira issue asks for exposing plain fact whether KVP daemon is running inside the guest and this commit implements seemingly different feature. Well, thing is, in case of QEMU there's a domain XML part where guest agent is configured and where we expose whether there's somebody listening inside the guest. But in case of hyperv there's no <channel/> to be configured as communication with KVP daemon happens through vmbus [4]. Users are advised to call the virDomainGetGuestInfo() API with non-zero 'types' argument and if they get an error with VIR_ERR_AGENT_UNRESPONSIVE code then the KVP daemon is not running. 1: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/tool... 2: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/tool... 3: https://learn.microsoft.com/en-us/windows/win32/hyperv_v2/msvm-kvpexchangeco... 4: https://docs.kernel.org/virt/hyperv/vmbus.html Resolves: https://redhat.atlassian.net/browse/RHEL-147661 Signed-off-by: Michal Privoznik <mprivozn@redhat.com> --- src/hyperv/hyperv_driver.c | 262 ++++++++++++++++++++++++++ src/hyperv/hyperv_wmi_generator.input | 43 +++++ 2 files changed, 305 insertions(+) diff --git a/src/hyperv/hyperv_driver.c b/src/hyperv/hyperv_driver.c index 18e06f0e2a..7ad80bc8ec 100644 --- a/src/hyperv/hyperv_driver.c +++ b/src/hyperv/hyperv_driver.c @@ -4379,6 +4379,267 @@ hypervDomainSnapshotGetParent(virDomainSnapshotPtr snapshot, } +static int +hypervGetServicesProcessOne(const char *xmlStr, + unsigned int supportedTypes, + unsigned int *processedTypes, + virTypedParamList *ifaceAddrList, + virTypedParamList *list) +{ + g_autoptr(xmlDoc) xml = NULL; + g_autoptr(xmlXPathContext) ctxt = NULL; + g_autofree char *name = NULL; + g_autofree char *val = NULL; + const struct { + const char *name; + const char *key; + unsigned int type; + } lookupTable[] = { + { "FullyQualifiedDomainName", VIR_DOMAIN_GUEST_INFO_HOSTNAME_HOSTNAME, + VIR_DOMAIN_GUEST_INFO_HOSTNAME }, + { "OSBuildNumber", VIR_DOMAIN_GUEST_INFO_OS_KERNEL_RELEASE, + VIR_DOMAIN_GUEST_INFO_OS }, + { "OSName", VIR_DOMAIN_GUEST_INFO_OS_NAME, + VIR_DOMAIN_GUEST_INFO_OS}, + { "OSVersion", VIR_DOMAIN_GUEST_INFO_OS_VERSION, + VIR_DOMAIN_GUEST_INFO_OS}, + { "OSMajorVersion", VIR_DOMAIN_GUEST_INFO_OS_VERSION_ID, + VIR_DOMAIN_GUEST_INFO_OS}, + { "ProcessorArchitecture", VIR_DOMAIN_GUEST_INFO_OS_MACHINE, + VIR_DOMAIN_GUEST_INFO_OS}, + }; + size_t i; + + VIR_DEBUG("xmlStr=%s", xmlStr); + + if (!(xml = virXMLParseStringCtxt(xmlStr, _("guest intrinsic exchange data"), &ctxt))) { + virReportError(VIR_ERR_INTERNAL_ERROR, "%s", + _("cannot parse guest intrinsic exchange data")); + return -1; + } + + name = virXPathString("string(//INSTANCE/PROPERTY[@NAME='Name']/VALUE/text())", ctxt); + if (!name) { + VIR_DEBUG("Skipping unexpected guest intrinsic XML (no name): %s", xmlStr); + return 0; + } + + val = virXPathString("string(//INSTANCE/PROPERTY[@NAME='Data']/VALUE/text())", ctxt); + if (!val) { + VIR_DEBUG("Skipping unexpected guest intrinsic XML (no value): %s", xmlStr); + return 0; + } + + for (i = 0; i < G_N_ELEMENTS(lookupTable); i++) { + if (supportedTypes & lookupTable[i].type && + STREQ(name, lookupTable[i].name)) { + virTypedParamListAddString(list, val, "%s", lookupTable[i].key); + *processedTypes |= lookupTable[i].type; + return 1; + } + } + + if (supportedTypes & VIR_DOMAIN_GUEST_INFO_INTERFACES && + (STREQ(name, "NetworkAddressIPv4") || STREQ(name, "NetworkAddressIPv6"))) { + g_auto(GStrv) tmp = NULL; + unsigned int newAddrCount = 0; + size_t addrIndex = 0; + const char *addrType = "ipv4"; + + if (STREQ(name, "NetworkAddressIPv6")) + addrType = "ipv6"; + + if (virTypedParamListFetch(ifaceAddrList, NULL, &addrIndex) < 0) + return -1; + + /* Per Microsoft docs: + * - NetworkAddressIPv4 + * A string that contains a semicolon-delimited list of the IPv4 + * addresses currently assigned to the guest virtual machine. + * - NetworkAddressIPv6 + * A string that contains a semicolon-delimited list of the IPv6 + * addresses currently assigned to the guest virtual machine. + */ + + tmp = g_strsplit(val, ";", 0); + newAddrCount = g_strv_length(tmp); + + for (i = 0; i < newAddrCount; i++) { + virTypedParamListAddString(ifaceAddrList, addrType, + VIR_DOMAIN_GUEST_INFO_IF_PREFIX "0" VIR_DOMAIN_GUEST_INFO_IF_SUFFIX_ADDR_PREFIX "%zu" VIR_DOMAIN_GUEST_INFO_IF_SUFFIX_ADDR_SUFFIX_TYPE, + addrIndex + i); + virTypedParamListAddString(ifaceAddrList, tmp[i], + VIR_DOMAIN_GUEST_INFO_IF_PREFIX "0" VIR_DOMAIN_GUEST_INFO_IF_SUFFIX_ADDR_PREFIX "%zu" VIR_DOMAIN_GUEST_INFO_IF_SUFFIX_ADDR_SUFFIX_ADDR, + addrIndex + i); + *processedTypes |= VIR_DOMAIN_GUEST_INFO_INTERFACES; + } + + return 1; + } + + return 0; +} + + +static int +hypervDomainGetGuestInfoImpl(hypervPrivate *priv, + const char *uuid, + unsigned int supportedTypes, + bool reportUnsupported, + virTypedParamList *list) +{ + g_autoptr(Msvm_KvpExchangeComponent) kvpList = NULL; + Msvm_KvpExchangeComponent_Data *data = NULL; + g_auto(virBuffer) query = VIR_BUFFER_INITIALIZER; + g_autoptr(virTypedParamList) ifaceAddrList = virTypedParamListNew(); + unsigned int processedTypes = 0; + size_t nIfaceAddresses = 0; + size_t i; + + virBufferEscapeSQL(&query, + MSVM_KVPEXCHANGECOMPONENT_WQL_SELECT + "WHERE SystemName = '%s'", + uuid); + + if (hypervGetWmiClass(Msvm_KvpExchangeComponent, &kvpList) < 0) + return -1; + + if (!kvpList) { + virReportError(VIR_ERR_OPERATION_FAILED, "%s", + _("Empty reply from guest. Maybe KVP service is not running?")); + return -1; + } + + data = kvpList->data; + + for (i = 0; i < data->GuestIntrinsicExchangeItems.count; i++) { + const char *xml = ((const char **)data->GuestIntrinsicExchangeItems.data)[i]; + + if (hypervGetServicesProcessOne(xml, supportedTypes, + &processedTypes, ifaceAddrList, list) < 0) { + return -1; + } + } + + if (virTypedParamListFetch(ifaceAddrList, NULL, &nIfaceAddresses) < 0) + return -1; + + if (nIfaceAddresses > 0) { + virTypedParamListAddUInt(list, 1, VIR_DOMAIN_GUEST_INFO_IF_COUNT); + virTypedParamListAddUInt(list, nIfaceAddresses, + VIR_DOMAIN_GUEST_INFO_IF_PREFIX "0" VIR_DOMAIN_GUEST_INFO_IF_SUFFIX_ADDR_COUNT); + virTypedParamListConcat(list, &ifaceAddrList); + } + + if (processedTypes != supportedTypes && + reportUnsupported) { + uint16_t status = ((uint16_t *)kvpList->data->OperationalStatus.data)[0]; + + /* Per Microsoft doc, the status value can be: + * 2 - OK, + * 3 - Degraded, + * 7 - Non-recoverable error, + * 12 - No contact, + * 13 - Lost communication. + */ + switch (status) { + case 3: + virReportError(VIR_ERR_AGENT_UNRESPONSIVE, "%s", + _("Unable to fetch all requested information. Data Exchange service (KVP) is degraded")); + break; + case 7: + virReportError(VIR_ERR_AGENT_UNRESPONSIVE, "%s", + _("Unable to fetch all requested information. Data Exchange service (KVP) encountered a non-recoverable error")); + break; + case 12: + case 13: + virReportError(VIR_ERR_AGENT_UNRESPONSIVE, "%s", + _("Unable to fetch all requested information. Data Exchange service (KVP) is not running")); + break; + default: + virReportError(VIR_ERR_AGENT_UNRESPONSIVE, "%s", + _("Unable to fetch all requested information")); + break; + } + return -1; + } + + return 0; +} + + +static int +hypervDomainGetGuestInfoCheckSupport(unsigned int types, + unsigned int *supportedTypes) +{ + const unsigned int hypervSupportedTypes = + VIR_DOMAIN_GUEST_INFO_OS | + VIR_DOMAIN_GUEST_INFO_HOSTNAME | + VIR_DOMAIN_GUEST_INFO_INTERFACES; + + if (types == 0) { + *supportedTypes = hypervSupportedTypes; + return 0; + } + + *supportedTypes = types & hypervSupportedTypes; + + if (types != *supportedTypes) { + virReportError(VIR_ERR_INVALID_ARG, + _("unsupported guest information types '0x%1$x'"), + types & ~hypervSupportedTypes); + return -1; + } + + return 0; +} + + +static int +hypervDomainGetGuestInfo(virDomainPtr domain, + unsigned int types, + virTypedParameterPtr *params, + int *nparams, + unsigned int flags) +{ + hypervPrivate *priv = NULL; + char uuid_string[VIR_UUID_STRING_BUFLEN]; + unsigned int supportedTypes = 0; + bool reportUnsupported = types != 0; + g_autoptr(Msvm_ComputerSystem) computerSystem = NULL; + g_autoptr(virTypedParamList) list = virTypedParamListNew(); + + virCheckFlags(0, -1); + + if (hypervDomainGetGuestInfoCheckSupport(types, &supportedTypes) < 0) + return -1; + + if (hypervMsvmComputerSystemFromDomain(domain, &computerSystem) < 0) + return -1; + + if (!hypervIsMsvmComputerSystemActive(computerSystem, NULL)) { + virReportError(VIR_ERR_OPERATION_INVALID, "%s", + _("domain is not running")); + + return -1; + } + + priv = domain->conn->privateData; + virUUIDFormat(domain->uuid, uuid_string); + + if (hypervDomainGetGuestInfoImpl(priv, + uuid_string, supportedTypes, + reportUnsupported, list) < 0) { + return -1; + } + + if (virTypedParamListSteal(list, params, nparams) < 0) + return -1; + + return 0; +} + + static virHypervisorDriver hypervHypervisorDriver = { .name = "Hyper-V", .connectOpen = hypervConnectOpen, /* 0.9.5 */ @@ -4452,6 +4713,7 @@ static virHypervisorDriver hypervHypervisorDriver = { .domainHasCurrentSnapshot = hypervDomainHasCurrentSnapshot, /* 12.2.0 */ .domainSnapshotCurrent = hypervDomainSnapshotCurrent, /* 12.2.0 */ .domainSnapshotGetParent = hypervDomainSnapshotGetParent, /* 12.2.0 */ + .domainGetGuestInfo = hypervDomainGetGuestInfo, /* 12.3.0 */ }; diff --git a/src/hyperv/hyperv_wmi_generator.input b/src/hyperv/hyperv_wmi_generator.input index b3cd9d19fb..1f8d4258e1 100644 --- a/src/hyperv/hyperv_wmi_generator.input +++ b/src/hyperv/hyperv_wmi_generator.input @@ -1261,3 +1261,46 @@ class Msvm_SecuritySettingData boolean EncryptStateAndVmMigrationTraffic boolean VirtualizationBasedSecurityOptOut end + +class Msvm_KvpExchangeComponent + string InstanceID + string Caption + string Description + string ElementName + datetime InstallDate + string Name + uint16 OperationalStatus[] + string StatusDescriptions[] + string Status + uint16 HealthState + uint16 CommunicationStatus + uint16 DetailedStatus + uint16 OperatingStatus + uint16 PrimaryStatus + uint16 EnabledState + string OtherEnabledState + uint16 RequestedState + uint16 EnabledDefault + datetime TimeOfLastStateChange + uint16 AvailableRequestedStates[] + uint16 TransitioningToState + string SystemCreationClassName + string SystemName + string CreationClassName + string DeviceID + boolean PowerManagementSupported + uint16 PowerManagementCapabilities[] + uint16 Availability + uint16 StatusInfo + uint32 LastErrorCode + string ErrorDescription + boolean ErrorCleared + string OtherIdentifyingInfo[] + uint64 PowerOnHours + uint64 TotalPowerOnHours + string IdentifyingDescriptions[] + uint16 AdditionalAvailability[] + uint64 MaxQuiesceTime + string GuestExchangeItems[] + string GuestIntrinsicExchangeItems[] +end -- 2.52.0
participants (1)
-
Michal Privoznik