[PATCH] hyperv: Implement virDomainGetGuestInfo()

Michal Privoznik via Devel posted 1 patch 12 hours ago
Patches applied successfully (tree, apply log)
git fetch https://github.com/patchew-project/libvirt tags/patchew/3f5f750efadbc99d069928cf99b5b78ec81f0825.1775031736.git.mprivozn@redhat.com
src/hyperv/hyperv_driver.c            | 262 ++++++++++++++++++++++++++
src/hyperv/hyperv_wmi_generator.input |  43 +++++
2 files changed, 305 insertions(+)
[PATCH] hyperv: Implement virDomainGetGuestInfo()
Posted by Michal Privoznik via Devel 12 hours ago
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/tools/hv/hv_kvp_daemon.c
2: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/tools/hv/hv_vss_daemon.c
3: https://learn.microsoft.com/en-us/windows/win32/hyperv_v2/msvm-kvpexchangecomponent
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