397dc2
From 5f6723e71e3765d1d43bfa9ba1c66e0e05e11a48 Mon Sep 17 00:00:00 2001
397dc2
Message-Id: <5f6723e71e3765d1d43bfa9ba1c66e0e05e11a48@dist-git>
397dc2
From: Michal Privoznik <mprivozn@redhat.com>
397dc2
Date: Mon, 9 Nov 2020 17:22:32 +0100
397dc2
Subject: [PATCH] Allow NUMA nodes without vCPUs
397dc2
MIME-Version: 1.0
397dc2
Content-Type: text/plain; charset=UTF-8
397dc2
Content-Transfer-Encoding: 8bit
397dc2
397dc2
QEMU allows creating NUMA nodes that have memory only.
397dc2
These are somehow important for HMAT.
397dc2
397dc2
With check done in qemuValidateDomainDef() for QEMU 2.7 or newer
397dc2
(checked via QEMU_CAPS_NUMA), we can be sure that the vCPUs are
397dc2
fully assigned to NUMA nodes in domain XML.
397dc2
397dc2
Signed-off-by: Michal Privoznik <mprivozn@redhat.com>
397dc2
Reviewed-by: Daniel Henrique Barboza <danielhb413@gmail.com>
397dc2
(cherry picked from commit a26f61ee0cffa421b87ef568002b684dd8025432)
397dc2
397dc2
Resolves: https://bugzilla.redhat.com/show_bug.cgi?id=1749518
397dc2
397dc2
Conflicts:
397dc2
- src/qemu/qemu_validate.c: This file doesn't exist in downstream
397dc2
yet, so I've moved the change that original patch would do to
397dc2
qemu_domain.c where the validator lives.
397dc2
397dc2
Signed-off-by: Michal Privoznik <mprivozn@redhat.com>
397dc2
Message-Id: <365508c75e579e9037ad555d6c372068ccd50c95.1604938867.git.mprivozn@redhat.com>
397dc2
Reviewed-by: Ján Tomko <jtomko@redhat.com>
397dc2
---
397dc2
 docs/formatdomain.html.in                     |  2 +
397dc2
 docs/schemas/cputypes.rng                     |  8 ++-
397dc2
 src/conf/numa_conf.c                          | 59 ++++++++++---------
397dc2
 src/libxl/xen_xl.c                            | 10 ++--
397dc2
 src/qemu/qemu_command.c                       | 26 ++++----
397dc2
 src/qemu/qemu_domain.c                        | 22 +++----
397dc2
 tests/qemuxml2argvdata/numatune-no-vcpu.args  | 33 +++++++++++
397dc2
 tests/qemuxml2argvdata/numatune-no-vcpu.xml   | 42 +++++++++++++
397dc2
 tests/qemuxml2argvtest.c                      |  1 +
397dc2
 tests/qemuxml2xmloutdata/numatune-no-vcpu.xml |  1 +
397dc2
 tests/qemuxml2xmltest.c                       |  1 +
397dc2
 11 files changed, 149 insertions(+), 56 deletions(-)
397dc2
 create mode 100644 tests/qemuxml2argvdata/numatune-no-vcpu.args
397dc2
 create mode 100644 tests/qemuxml2argvdata/numatune-no-vcpu.xml
397dc2
 create mode 120000 tests/qemuxml2xmloutdata/numatune-no-vcpu.xml
397dc2
397dc2
diff --git a/docs/formatdomain.html.in b/docs/formatdomain.html.in
397dc2
index 76799f5ffc..4b8d312596 100644
397dc2
--- a/docs/formatdomain.html.in
397dc2
+++ b/docs/formatdomain.html.in
397dc2
@@ -1783,6 +1783,8 @@
397dc2
       cpus specifies the CPU or range of CPUs that are
397dc2
       part of the node. memory specifies the node memory
397dc2
       in kibibytes (i.e. blocks of 1024 bytes).
397dc2
+      Since 6.6.0 the cpus attribute
397dc2
+      is optional and if omitted a CPU-less NUMA node is created.
397dc2
       Since 1.2.11 one can use an additional 
397dc2
           href="#elementsMemoryAllocation">unit attribute to
397dc2
       define units in which memory is specified.
397dc2
diff --git a/docs/schemas/cputypes.rng b/docs/schemas/cputypes.rng
397dc2
index e2744acad3..a1682a1003 100644
397dc2
--- a/docs/schemas/cputypes.rng
397dc2
+++ b/docs/schemas/cputypes.rng
397dc2
@@ -115,9 +115,11 @@
397dc2
           <ref name="unsignedInt"/>
397dc2
         </attribute>
397dc2
       </optional>
397dc2
-      <attribute name="cpus">
397dc2
-        <ref name="cpuset"/>
397dc2
-      </attribute>
397dc2
+      <optional>
397dc2
+        <attribute name="cpus">
397dc2
+          <ref name="cpuset"/>
397dc2
+        </attribute>
397dc2
+      </optional>
397dc2
       <attribute name="memory">
397dc2
         <ref name="memoryKB"/>
397dc2
       </attribute>
397dc2
diff --git a/src/conf/numa_conf.c b/src/conf/numa_conf.c
397dc2
index c9cc8ac22e..a805336d16 100644
397dc2
--- a/src/conf/numa_conf.c
397dc2
+++ b/src/conf/numa_conf.c
397dc2
@@ -889,32 +889,28 @@ virDomainNumaDefParseXML(virDomainNumaPtr def,
397dc2
         }
397dc2
         VIR_FREE(tmp);
397dc2
 
397dc2
-        if (def->mem_nodes[cur_cell].cpumask) {
397dc2
+        if (def->mem_nodes[cur_cell].mem) {
397dc2
             virReportError(VIR_ERR_XML_ERROR,
397dc2
                            _("Duplicate NUMA cell info for cell id '%u'"),
397dc2
                            cur_cell);
397dc2
             goto cleanup;
397dc2
         }
397dc2
 
397dc2
-        if (!(tmp = virXMLPropString(nodes[i], "cpus"))) {
397dc2
-            virReportError(VIR_ERR_XML_ERROR, "%s",
397dc2
-                           _("Missing 'cpus' attribute in NUMA cell"));
397dc2
-            goto cleanup;
397dc2
-        }
397dc2
+        if ((tmp = virXMLPropString(nodes[i], "cpus"))) {
397dc2
+            g_autoptr(virBitmap) cpumask = NULL;
397dc2
 
397dc2
-        if (virBitmapParse(tmp, &def->mem_nodes[cur_cell].cpumask,
397dc2
-                           VIR_DOMAIN_CPUMASK_LEN) < 0)
397dc2
-            goto cleanup;
397dc2
+            if (virBitmapParse(tmp, &cpumask, VIR_DOMAIN_CPUMASK_LEN) < 0)
397dc2
+                goto cleanup;
397dc2
 
397dc2
-        if (virBitmapIsAllClear(def->mem_nodes[cur_cell].cpumask)) {
397dc2
-            virReportError(VIR_ERR_CONFIG_UNSUPPORTED,
397dc2
-                          _("NUMA cell %d has no vCPUs assigned"), cur_cell);
397dc2
-            goto cleanup;
397dc2
+            if (!virBitmapIsAllClear(cpumask))
397dc2
+                def->mem_nodes[cur_cell].cpumask = g_steal_pointer(&cpumask);
397dc2
+            VIR_FREE(tmp);
397dc2
         }
397dc2
-        VIR_FREE(tmp);
397dc2
 
397dc2
         for (j = 0; j < n; j++) {
397dc2
-            if (j == cur_cell || !def->mem_nodes[j].cpumask)
397dc2
+            if (j == cur_cell ||
397dc2
+                !def->mem_nodes[j].cpumask ||
397dc2
+                !def->mem_nodes[cur_cell].cpumask)
397dc2
                 continue;
397dc2
 
397dc2
             if (virBitmapOverlaps(def->mem_nodes[j].cpumask,
397dc2
@@ -976,7 +972,6 @@ virDomainNumaDefFormatXML(virBufferPtr buf,
397dc2
 {
397dc2
     virDomainMemoryAccess memAccess;
397dc2
     virTristateBool discard;
397dc2
-    char *cpustr;
397dc2
     size_t ncells = virDomainNumaGetNodeCount(def);
397dc2
     size_t i;
397dc2
 
397dc2
@@ -986,17 +981,22 @@ virDomainNumaDefFormatXML(virBufferPtr buf,
397dc2
     virBufferAddLit(buf, "<numa>\n");
397dc2
     virBufferAdjustIndent(buf, 2);
397dc2
     for (i = 0; i < ncells; i++) {
397dc2
+        virBitmapPtr cpumask = virDomainNumaGetNodeCpumask(def, i);
397dc2
         int ndistances;
397dc2
 
397dc2
         memAccess = virDomainNumaGetNodeMemoryAccessMode(def, i);
397dc2
         discard = virDomainNumaGetNodeDiscard(def, i);
397dc2
 
397dc2
-        if (!(cpustr = virBitmapFormat(virDomainNumaGetNodeCpumask(def, i))))
397dc2
-            return -1;
397dc2
-
397dc2
         virBufferAddLit(buf, "
397dc2
         virBufferAsprintf(buf, " id='%zu'", i);
397dc2
-        virBufferAsprintf(buf, " cpus='%s'", cpustr);
397dc2
+
397dc2
+        if (cpumask) {
397dc2
+            g_autofree char *cpustr = virBitmapFormat(cpumask);
397dc2
+
397dc2
+            if (!cpustr)
397dc2
+                return -1;
397dc2
+            virBufferAsprintf(buf, " cpus='%s'", cpustr);
397dc2
+        }
397dc2
         virBufferAsprintf(buf, " memory='%llu'",
397dc2
                           virDomainNumaGetNodeMemorySize(def, i));
397dc2
         virBufferAddLit(buf, " unit='KiB'");
397dc2
@@ -1032,8 +1032,6 @@ virDomainNumaDefFormatXML(virBufferPtr buf,
397dc2
             virBufferAdjustIndent(buf, -2);
397dc2
             virBufferAddLit(buf, "</cell>\n");
397dc2
         }
397dc2
-
397dc2
-        VIR_FREE(cpustr);
397dc2
     }
397dc2
     virBufferAdjustIndent(buf, -2);
397dc2
     virBufferAddLit(buf, "</numa>\n");
397dc2
@@ -1048,8 +1046,12 @@ virDomainNumaGetCPUCountTotal(virDomainNumaPtr numa)
397dc2
     size_t i;
397dc2
     unsigned int ret = 0;
397dc2
 
397dc2
-    for (i = 0; i < numa->nmem_nodes; i++)
397dc2
-        ret += virBitmapCountBits(virDomainNumaGetNodeCpumask(numa, i));
397dc2
+    for (i = 0; i < numa->nmem_nodes; i++) {
397dc2
+        virBitmapPtr cpumask = virDomainNumaGetNodeCpumask(numa, i);
397dc2
+
397dc2
+        if (cpumask)
397dc2
+            ret += virBitmapCountBits(cpumask);
397dc2
+    }
397dc2
 
397dc2
     return ret;
397dc2
 }
397dc2
@@ -1061,11 +1063,14 @@ virDomainNumaGetMaxCPUID(virDomainNumaPtr numa)
397dc2
     unsigned int ret = 0;
397dc2
 
397dc2
     for (i = 0; i < numa->nmem_nodes; i++) {
397dc2
+        virBitmapPtr cpumask = virDomainNumaGetNodeCpumask(numa, i);
397dc2
         int bit;
397dc2
 
397dc2
-        bit = virBitmapLastSetBit(virDomainNumaGetNodeCpumask(numa, i));
397dc2
-        if (bit > ret)
397dc2
-            ret = bit;
397dc2
+        if (cpumask) {
397dc2
+            bit = virBitmapLastSetBit(cpumask);
397dc2
+            if (bit > ret)
397dc2
+                ret = bit;
397dc2
+        }
397dc2
     }
397dc2
 
397dc2
     return ret;
397dc2
diff --git a/src/libxl/xen_xl.c b/src/libxl/xen_xl.c
397dc2
index edea30a86a..752fa925ec 100644
397dc2
--- a/src/libxl/xen_xl.c
397dc2
+++ b/src/libxl/xen_xl.c
397dc2
@@ -1443,19 +1443,21 @@ xenFormatXLVnuma(virConfValuePtr list,
397dc2
 {
397dc2
     int ret = -1;
397dc2
     size_t i;
397dc2
-
397dc2
     virBuffer buf = VIR_BUFFER_INITIALIZER;
397dc2
     virConfValuePtr numaVnode, tmp;
397dc2
-
397dc2
+    virBitmapPtr cpumask = virDomainNumaGetNodeCpumask(numa, node);
397dc2
     size_t nodeSize = virDomainNumaGetNodeMemorySize(numa, node) / 1024;
397dc2
-    char *nodeVcpus = virBitmapFormat(virDomainNumaGetNodeCpumask(numa, node));
397dc2
+    g_autofree char *nodeVcpus = NULL;
397dc2
 
397dc2
-    if (VIR_ALLOC(numaVnode) < 0)
397dc2
+    if (!cpumask ||
397dc2
+        VIR_ALLOC(numaVnode) < 0)
397dc2
         goto cleanup;
397dc2
 
397dc2
     numaVnode->type = VIR_CONF_LIST;
397dc2
     numaVnode->list = NULL;
397dc2
 
397dc2
+    nodeVcpus = virBitmapFormat(cpumask);
397dc2
+
397dc2
     /* pnode */
397dc2
     virBufferAsprintf(&buf, "pnode=%zu", node);
397dc2
     xenFormatXLVnode(numaVnode, &buf;;
397dc2
diff --git a/src/qemu/qemu_command.c b/src/qemu/qemu_command.c
397dc2
index 1a573c2817..ac63d18a42 100644
397dc2
--- a/src/qemu/qemu_command.c
397dc2
+++ b/src/qemu/qemu_command.c
397dc2
@@ -7364,8 +7364,6 @@ qemuBuildNumaCommandLine(virQEMUDriverConfigPtr cfg,
397dc2
     size_t i, j;
397dc2
     virQEMUCapsPtr qemuCaps = priv->qemuCaps;
397dc2
     g_auto(virBuffer) buf = VIR_BUFFER_INITIALIZER;
397dc2
-    char *cpumask = NULL;
397dc2
-    char *tmpmask = NULL;
397dc2
     char *next = NULL;
397dc2
     virBufferPtr nodeBackends = NULL;
397dc2
     bool needBackend = false;
397dc2
@@ -7400,9 +7398,7 @@ qemuBuildNumaCommandLine(virQEMUDriverConfigPtr cfg,
397dc2
         goto cleanup;
397dc2
 
397dc2
     for (i = 0; i < ncells; i++) {
397dc2
-        VIR_FREE(cpumask);
397dc2
-        if (!(cpumask = virBitmapFormat(virDomainNumaGetNodeCpumask(def->numa, i))))
397dc2
-            goto cleanup;
397dc2
+        virBitmapPtr cpumask = virDomainNumaGetNodeCpumask(def->numa, i);
397dc2
 
397dc2
         if (needBackend) {
397dc2
             virCommandAddArg(cmd, "-object");
397dc2
@@ -7412,11 +7408,19 @@ qemuBuildNumaCommandLine(virQEMUDriverConfigPtr cfg,
397dc2
         virCommandAddArg(cmd, "-numa");
397dc2
         virBufferAsprintf(&buf, "node,nodeid=%zu", i);
397dc2
 
397dc2
-        for (tmpmask = cpumask; tmpmask; tmpmask = next) {
397dc2
-            if ((next = strchr(tmpmask, ',')))
397dc2
-                *(next++) = '\0';
397dc2
-            virBufferAddLit(&buf, ",cpus=");
397dc2
-            virBufferAdd(&buf, tmpmask, -1);
397dc2
+        if (cpumask) {
397dc2
+            g_autofree char *cpumaskStr = NULL;
397dc2
+            char *tmpmask;
397dc2
+
397dc2
+            if (!(cpumaskStr = virBitmapFormat(cpumask)))
397dc2
+                goto cleanup;
397dc2
+
397dc2
+            for (tmpmask = cpumaskStr; tmpmask; tmpmask = next) {
397dc2
+                if ((next = strchr(tmpmask, ',')))
397dc2
+                    *(next++) = '\0';
397dc2
+                virBufferAddLit(&buf, ",cpus=");
397dc2
+                virBufferAdd(&buf, tmpmask, -1);
397dc2
+            }
397dc2
         }
397dc2
 
397dc2
         if (needBackend)
397dc2
@@ -7447,8 +7451,6 @@ qemuBuildNumaCommandLine(virQEMUDriverConfigPtr cfg,
397dc2
     ret = 0;
397dc2
 
397dc2
  cleanup:
397dc2
-    VIR_FREE(cpumask);
397dc2
-
397dc2
     if (nodeBackends) {
397dc2
         for (i = 0; i < ncells; i++)
397dc2
             virBufferFreeAndReset(&nodeBackends[i]);
397dc2
diff --git a/src/qemu/qemu_domain.c b/src/qemu/qemu_domain.c
397dc2
index 35b536868a..be25790f12 100644
397dc2
--- a/src/qemu/qemu_domain.c
397dc2
+++ b/src/qemu/qemu_domain.c
397dc2
@@ -5373,7 +5373,7 @@ qemuDomainDefValidateNuma(const virDomainDef *def,
397dc2
     }
397dc2
 
397dc2
     for (i = 0; i < ncells; i++) {
397dc2
-        g_autofree char * cpumask = NULL;
397dc2
+        virBitmapPtr cpumask = virDomainNumaGetNodeCpumask(def->numa, i);
397dc2
 
397dc2
         if (!hasMemoryCap &&
397dc2
             virDomainNumaGetNodeMemoryAccessMode(def->numa, i)) {
397dc2
@@ -5383,17 +5383,19 @@ qemuDomainDefValidateNuma(const virDomainDef *def,
397dc2
             return -1;
397dc2
         }
397dc2
 
397dc2
-        if (!(cpumask = virBitmapFormat(virDomainNumaGetNodeCpumask(def->numa, i))))
397dc2
-            return -1;
397dc2
+        if (cpumask) {
397dc2
+            g_autofree char * cpumaskStr = NULL;
397dc2
+            if (!(cpumaskStr = virBitmapFormat(cpumask)))
397dc2
+                return -1;
397dc2
 
397dc2
-        if (strchr(cpumask, ',') &&
397dc2
-            !virQEMUCapsGet(qemuCaps, QEMU_CAPS_NUMA)) {
397dc2
-            virReportError(VIR_ERR_CONFIG_UNSUPPORTED, "%s",
397dc2
-                           _("disjoint NUMA cpu ranges are not supported "
397dc2
-                             "with this QEMU"));
397dc2
-            return -1;
397dc2
+            if (strchr(cpumaskStr, ',') &&
397dc2
+                !virQEMUCapsGet(qemuCaps, QEMU_CAPS_NUMA)) {
397dc2
+                virReportError(VIR_ERR_CONFIG_UNSUPPORTED, "%s",
397dc2
+                               _("disjoint NUMA cpu ranges are not supported "
397dc2
+                                 "with this QEMU"));
397dc2
+                return -1;
397dc2
+            }
397dc2
         }
397dc2
-
397dc2
     }
397dc2
 
397dc2
     if (virDomainNumaNodesDistancesAreBeingSet(def->numa) &&
397dc2
diff --git a/tests/qemuxml2argvdata/numatune-no-vcpu.args b/tests/qemuxml2argvdata/numatune-no-vcpu.args
397dc2
new file mode 100644
397dc2
index 0000000000..a1f1ee044e
397dc2
--- /dev/null
397dc2
+++ b/tests/qemuxml2argvdata/numatune-no-vcpu.args
397dc2
@@ -0,0 +1,33 @@
397dc2
+LC_ALL=C \
397dc2
+PATH=/bin \
397dc2
+HOME=/tmp/lib/domain--1-QEMUGuest \
397dc2
+USER=test \
397dc2
+LOGNAME=test \
397dc2
+XDG_DATA_HOME=/tmp/lib/domain--1-QEMUGuest/.local/share \
397dc2
+XDG_CACHE_HOME=/tmp/lib/domain--1-QEMUGuest/.cache \
397dc2
+XDG_CONFIG_HOME=/tmp/lib/domain--1-QEMUGuest/.config \
397dc2
+QEMU_AUDIO_DRV=none \
397dc2
+/usr/bin/qemu-system-x86_64 \
397dc2
+-name QEMUGuest \
397dc2
+-S \
397dc2
+-machine pc,accel=tcg,usb=off,dump-guest-core=off \
397dc2
+-m 12288 \
397dc2
+-realtime mlock=off \
397dc2
+-smp 12,sockets=12,cores=1,threads=1 \
397dc2
+-numa node,nodeid=0,cpus=0-3,mem=2048 \
397dc2
+-numa node,nodeid=1,cpus=4-7,mem=2048 \
397dc2
+-numa node,nodeid=2,cpus=8-11,mem=2048 \
397dc2
+-numa node,nodeid=3,mem=2048 \
397dc2
+-numa node,nodeid=4,mem=2048 \
397dc2
+-numa node,nodeid=5,mem=2048 \
397dc2
+-uuid c7a5fdb2-cdaf-9455-926a-d65c16db1809 \
397dc2
+-display none \
397dc2
+-no-user-config \
397dc2
+-nodefaults \
397dc2
+-chardev socket,id=charmonitor,path=/tmp/lib/domain--1-QEMUGuest/monitor.sock,\
397dc2
+server,nowait \
397dc2
+-mon chardev=charmonitor,id=monitor,mode=control \
397dc2
+-rtc base=utc \
397dc2
+-no-shutdown \
397dc2
+-usb \
397dc2
+-device virtio-balloon-pci,id=balloon0,bus=pci.0,addr=0x3
397dc2
diff --git a/tests/qemuxml2argvdata/numatune-no-vcpu.xml b/tests/qemuxml2argvdata/numatune-no-vcpu.xml
397dc2
new file mode 100644
397dc2
index 0000000000..f25a07d7ed
397dc2
--- /dev/null
397dc2
+++ b/tests/qemuxml2argvdata/numatune-no-vcpu.xml
397dc2
@@ -0,0 +1,42 @@
397dc2
+<domain type='qemu'>
397dc2
+  <name>QEMUGuest</name>
397dc2
+  <uuid>c7a5fdb2-cdaf-9455-926a-d65c16db1809</uuid>
397dc2
+  <memory unit='KiB'>12582912</memory>
397dc2
+  <currentMemory unit='KiB'>12582912</currentMemory>
397dc2
+  <vcpu placement='static'>12</vcpu>
397dc2
+  <os>
397dc2
+    <type arch='x86_64' machine='pc'>hvm</type>
397dc2
+    <boot dev='hd'/>
397dc2
+  </os>
397dc2
+  <features>
397dc2
+    <acpi/>
397dc2
+    <apic/>
397dc2
+    <pae/>
397dc2
+  </features>
397dc2
+  <cpu>
397dc2
+    <numa>
397dc2
+      <cell id='0' cpus='0-3' memory='2097152' unit='KiB'/>
397dc2
+      <cell id='1' cpus='4-7' memory='2097152' unit='KiB'/>
397dc2
+      <cell id='2' cpus='8-11' memory='2097152' unit='KiB'/>
397dc2
+      <cell id='3' memory='2097152' unit='KiB'/>
397dc2
+      <cell id='4' memory='2097152' unit='KiB'/>
397dc2
+      <cell id='5' memory='2097152' unit='KiB'/>
397dc2
+    </numa>
397dc2
+  </cpu>
397dc2
+  <clock offset='utc'/>
397dc2
+  <on_poweroff>destroy</on_poweroff>
397dc2
+  <on_reboot>restart</on_reboot>
397dc2
+  <on_crash>restart</on_crash>
397dc2
+  <devices>
397dc2
+    <emulator>/usr/bin/qemu-system-x86_64</emulator>
397dc2
+    <controller type='usb' index='0'>
397dc2
+      <address type='pci' domain='0x0000' bus='0x00' slot='0x01' function='0x2'/>
397dc2
+    </controller>
397dc2
+    <controller type='pci' index='0' model='pci-root'/>
397dc2
+    <input type='mouse' bus='ps2'/>
397dc2
+    <input type='keyboard' bus='ps2'/>
397dc2
+    <memballoon model='virtio'>
397dc2
+      <address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x0'/>
397dc2
+    </memballoon>
397dc2
+  </devices>
397dc2
+</domain>
397dc2
diff --git a/tests/qemuxml2argvtest.c b/tests/qemuxml2argvtest.c
397dc2
index ff92af606d..49699e495d 100644
397dc2
--- a/tests/qemuxml2argvtest.c
397dc2
+++ b/tests/qemuxml2argvtest.c
397dc2
@@ -1812,6 +1812,7 @@ mymain(void)
397dc2
     DO_TEST_PARSE_ERROR("numatune-memnode-no-memory", NONE);
397dc2
 
397dc2
     DO_TEST("numatune-distances", QEMU_CAPS_NUMA, QEMU_CAPS_NUMA_DIST);
397dc2
+    DO_TEST("numatune-no-vcpu", NONE);
397dc2
 
397dc2
     DO_TEST("numatune-auto-nodeset-invalid", NONE);
397dc2
     DO_TEST("numatune-auto-prefer", QEMU_CAPS_OBJECT_MEMORY_RAM,
397dc2
diff --git a/tests/qemuxml2xmloutdata/numatune-no-vcpu.xml b/tests/qemuxml2xmloutdata/numatune-no-vcpu.xml
397dc2
new file mode 120000
397dc2
index 0000000000..f213032685
397dc2
--- /dev/null
397dc2
+++ b/tests/qemuxml2xmloutdata/numatune-no-vcpu.xml
397dc2
@@ -0,0 +1 @@
397dc2
+../qemuxml2argvdata/numatune-no-vcpu.xml
397dc2
\ No newline at end of file
397dc2
diff --git a/tests/qemuxml2xmltest.c b/tests/qemuxml2xmltest.c
397dc2
index 6c3f5c4a9e..1ddeba30f0 100644
397dc2
--- a/tests/qemuxml2xmltest.c
397dc2
+++ b/tests/qemuxml2xmltest.c
397dc2
@@ -1105,6 +1105,7 @@ mymain(void)
397dc2
     DO_TEST("numatune-memnode", QEMU_CAPS_NUMA, QEMU_CAPS_OBJECT_MEMORY_FILE);
397dc2
     DO_TEST("numatune-memnode-no-memory", QEMU_CAPS_OBJECT_MEMORY_FILE);
397dc2
     DO_TEST("numatune-distances", QEMU_CAPS_NUMA, QEMU_CAPS_NUMA_DIST);
397dc2
+    DO_TEST("numatune-no-vcpu", QEMU_CAPS_NUMA);
397dc2
 
397dc2
     DO_TEST("bios-nvram", NONE);
397dc2
     DO_TEST("bios-nvram-os-interleave", NONE);
397dc2
-- 
397dc2
2.29.2
397dc2