QAPI patches patches for 2025-07-16

-----BEGIN PGP SIGNATURE-----
 
 iQJGBAABCAAwFiEENUvIs9frKmtoZ05fOHC0AOuRhlMFAmh34AYSHGFybWJydUBy
 ZWRoYXQuY29tAAoJEDhwtADrkYZTiw4P/iFXG7aZvnMC1OwJ0bUKJIIWTwcxrio2
 ELKvxHplktZe+KUHPoH+l+5j9ytSl8k6BKlMZgiqJv6pVVFUXlYmwPZ19Rtwi2o5
 62J2Lq/f33JSFh/ROqgM88gey61nxtL8eK2+Y2CxBGL3rd39VF/qto9HzE48kITM
 iDPKeCzcikDg0QIlnj4afP1wNbJawFBW60JAugaEdHG5VBhjcPQTDI1bMaSLpUJu
 8d+v3bHLTM8FqHnkIdxQWDTnMwI9SVfqovzoDbsVw4sa9Ptt8GdKnUHUbNRtyeNp
 9zRu01ztMV9zuByAwsRm2ECdTwxsOVhVp4nFUxM24u2eTO6Pixe1sgogrtxcUVIf
 tQuorYqeu1AAEyqz2iYFxrxKgsqRkA/etQCzibg+1coKlmLi7UTra4F0LEUWUMxZ
 iXLGlnDZx9iwQq5iVHDb0zSzbBSlAamYK5HX4p8E7skJ7yaSwwZGgfQAr4JfJ7Yu
 73k4nuRHcLYIoyv02KE8npHFE62I0hH5YvJ2G9vQ2GJtw0vbCcVn01G3PYCe/P6/
 RXLSWAZmIyON7FAst1MZd2sF+0QG0beEt81bUrGbKEZY8qhXL4/mABmJvOp+eNJ6
 23Z2bFNBkjCMxsJYQL8h9E9Zg/VAvxFJRoGClEPV+PfjCIy/wZPR8gUeYVPuDya4
 63sn7GO5hu3c
 =+Yt5
 -----END PGP SIGNATURE-----

Merge tag 'pull-qapi-2025-07-16' of https://repo.or.cz/qemu/armbru into staging

QAPI patches patches for 2025-07-16

# -----BEGIN PGP SIGNATURE-----
#
# iQJGBAABCAAwFiEENUvIs9frKmtoZ05fOHC0AOuRhlMFAmh34AYSHGFybWJydUBy
# ZWRoYXQuY29tAAoJEDhwtADrkYZTiw4P/iFXG7aZvnMC1OwJ0bUKJIIWTwcxrio2
# ELKvxHplktZe+KUHPoH+l+5j9ytSl8k6BKlMZgiqJv6pVVFUXlYmwPZ19Rtwi2o5
# 62J2Lq/f33JSFh/ROqgM88gey61nxtL8eK2+Y2CxBGL3rd39VF/qto9HzE48kITM
# iDPKeCzcikDg0QIlnj4afP1wNbJawFBW60JAugaEdHG5VBhjcPQTDI1bMaSLpUJu
# 8d+v3bHLTM8FqHnkIdxQWDTnMwI9SVfqovzoDbsVw4sa9Ptt8GdKnUHUbNRtyeNp
# 9zRu01ztMV9zuByAwsRm2ECdTwxsOVhVp4nFUxM24u2eTO6Pixe1sgogrtxcUVIf
# tQuorYqeu1AAEyqz2iYFxrxKgsqRkA/etQCzibg+1coKlmLi7UTra4F0LEUWUMxZ
# iXLGlnDZx9iwQq5iVHDb0zSzbBSlAamYK5HX4p8E7skJ7yaSwwZGgfQAr4JfJ7Yu
# 73k4nuRHcLYIoyv02KE8npHFE62I0hH5YvJ2G9vQ2GJtw0vbCcVn01G3PYCe/P6/
# RXLSWAZmIyON7FAst1MZd2sF+0QG0beEt81bUrGbKEZY8qhXL4/mABmJvOp+eNJ6
# 23Z2bFNBkjCMxsJYQL8h9E9Zg/VAvxFJRoGClEPV+PfjCIy/wZPR8gUeYVPuDya4
# 63sn7GO5hu3c
# =+Yt5
# -----END PGP SIGNATURE-----
# gpg: Signature made Wed 16 Jul 2025 13:23:18 EDT
# gpg:                using RSA key 354BC8B3D7EB2A6B68674E5F3870B400EB918653
# gpg:                issuer "armbru@redhat.com"
# gpg: Good signature from "Markus Armbruster <armbru@redhat.com>" [full]
# gpg:                 aka "Markus Armbruster <armbru@pond.sub.org>" [full]
# Primary key fingerprint: 354B C8B3 D7EB 2A6B 6867  4E5F 3870 B400 EB91 8653

* tag 'pull-qapi-2025-07-16' of https://repo.or.cz/qemu/armbru:
  python: fix editable installs for modern pip/setuptools
  MAINTAINERS: Add docs/requirements.txt
  docs: Bump sphinx to 6.2.1
  tests/qtest/qom-test: unit test for qom-list-get
  python: use qom-list-get
  qom: qom-list-get

Signed-off-by: Stefan Hajnoczi <stefanha@redhat.com>
This commit is contained in:
Stefan Hajnoczi 2025-07-17 11:18:52 -04:00
commit 3656e761bc
9 changed files with 305 additions and 27 deletions

View file

@ -4428,6 +4428,7 @@ M: Peter Maydell <peter.maydell@linaro.org>
S: Maintained
F: docs/conf.py
F: docs/*/conf.py
F: docs/requirements.txt
F: docs/sphinx/
F: docs/_templates/
F: docs/devel/docs.rst

View file

@ -1,5 +1,5 @@
# Used by readthedocs.io
# Should be in sync with the "installed" key of pythondeps.toml
sphinx==5.3.0
sphinx_rtd_theme==1.1.1
sphinx==6.2.1
sphinx_rtd_theme==1.2.2

View file

@ -68,7 +68,7 @@ $(QEMU_MINVENV_DIR) $(QEMU_MINVENV_DIR)/bin/activate: setup.cfg tests/minreqs.tx
echo "INSTALL -r tests/minreqs.txt $(QEMU_MINVENV_DIR)";\
$(PIP_INSTALL) -r tests/minreqs.txt 1>/dev/null; \
echo "INSTALL -e qemu $(QEMU_MINVENV_DIR)"; \
$(PIP_INSTALL) -e . 1>/dev/null; \
PIP_CONFIG_SETTINGS="editable_mode=compat" $(PIP_INSTALL) -e . 1>/dev/null; \
)
@touch $(QEMU_MINVENV_DIR)
@ -103,7 +103,7 @@ check-dev: dev-venv
.PHONY: develop
develop:
$(PIP_INSTALL) -e .[devel]
PIP_CONFIG_SETTINGS="editable_mode=compat" $(PIP_INSTALL) -e .[devel]
.PHONY: check
check:

View file

@ -31,8 +31,7 @@ QOM commands:
##
import argparse
from qemu.qmp import ExecuteError
from typing import List
from .qom_common import QOMCommand
@ -224,28 +223,34 @@ class QOMTree(QOMCommand):
super().__init__(args)
self.path = args.path
def _list_node(self, path: str) -> None:
print(path)
items = self.qom_list(path)
for item in items:
if item.child:
continue
try:
rsp = self.qmp.cmd('qom-get', path=path,
property=item.name)
print(f" {item.name}: {rsp} ({item.type})")
except ExecuteError as err:
print(f" {item.name}: <EXCEPTION: {err!s}> ({item.type})")
print('')
for item in items:
if not item.child:
continue
def _list_nodes(self, paths: List[str]) -> None:
all_paths_props = self.qom_list_get(paths)
i = 0
for props in all_paths_props:
path = paths[i]
i = i + 1
print(path)
if path == '/':
path = ''
self._list_node(f"{path}/{item.name}")
newpaths = []
for item in props.properties:
if item.child:
newpaths += [f"{path}/{item.name}"]
else:
value = item.value
if value is None:
value = "<EXCEPTION: property could not be read>"
print(f" {item.name}: {value} ({item.type})")
print('')
if newpaths:
self._list_nodes(newpaths)
def run(self) -> int:
self._list_node(self.path)
self._list_nodes([self.path])
return 0

View file

@ -65,6 +65,52 @@ class ObjectPropertyInfo:
return self.type.startswith('link<')
class ObjectPropertyValue:
"""
Represents a property return from e.g. qom-tree-get
"""
def __init__(self, name: str, type_: str, value: object):
self.name = name
self.type = type_
self.value = value
@classmethod
def make(cls, value: Dict[str, Any]) -> 'ObjectPropertyValue':
"""
Build an ObjectPropertyValue from a Dict with an unknown shape.
"""
assert value.keys() >= {'name', 'type'}
assert value.keys() <= {'name', 'type', 'value'}
return cls(value['name'], value['type'], value.get('value'))
@property
def child(self) -> bool:
"""Is this property a child property?"""
return self.type.startswith('child<')
class ObjectPropertiesValues:
"""
Represents the return type from e.g. qom-list-get
"""
# pylint: disable=too-few-public-methods
def __init__(self, properties: List[ObjectPropertyValue]) -> None:
self.properties = properties
@classmethod
def make(cls, value: Dict[str, Any]) -> 'ObjectPropertiesValues':
"""
Build an ObjectPropertiesValues from a Dict with an unknown shape.
"""
assert value.keys() == {'properties'}
props = [ObjectPropertyValue(item['name'],
item['type'],
item.get('value'))
for item in value['properties']]
return cls(props)
CommandT = TypeVar('CommandT', bound='QOMCommand')
@ -145,6 +191,15 @@ class QOMCommand:
assert isinstance(rsp, list)
return [ObjectPropertyInfo.make(x) for x in rsp]
def qom_list_get(self, paths: List[str]) -> List[ObjectPropertiesValues]:
"""
:return: a strongly typed list from the 'qom-list-get' command.
"""
rsp = self.qmp.cmd('qom-list-get', paths=paths)
# qom-list-get returns List[ObjectPropertiesValues]
assert isinstance(rsp, list)
return [ObjectPropertiesValues.make(x) for x in rsp]
@classmethod
def command_runner(
cls: Type[CommandT],

View file

@ -24,8 +24,8 @@ pycotap = { accepted = ">=1.1.0", installed = "1.3.1" }
[docs]
# Please keep the installed versions in sync with docs/requirements.txt
sphinx = { accepted = ">=3.4.3", installed = "5.3.0", canary = "sphinx-build" }
sphinx_rtd_theme = { accepted = ">=0.5", installed = "1.1.1" }
sphinx = { accepted = ">=3.4.3", installed = "6.2.1", canary = "sphinx-build" }
sphinx_rtd_theme = { accepted = ">=0.5", installed = "1.2.2" }
[testdeps]
qemu.qmp = { accepted = ">=0.0.3", installed = "0.0.3" }

View file

@ -47,6 +47,34 @@
'*description': 'str',
'*default-value': 'any' } }
##
# @ObjectPropertyValue:
#
# @name: the name of the property.
#
# @type: the type of the property, as described in `ObjectPropertyInfo`.
#
# @value: the value of the property. Absent when the property cannot
# be read.
#
# Since 10.1
##
{ 'struct': 'ObjectPropertyValue',
'data': { 'name': 'str',
'type': 'str',
'*value': 'any' } }
##
# @ObjectPropertiesValues:
#
# @properties: a list of properties.
#
# Since 10.1
##
{ 'struct': 'ObjectPropertiesValues',
'data': { 'properties': [ 'ObjectPropertyValue' ] }}
##
# @qom-list:
#
@ -124,6 +152,28 @@
'returns': 'any',
'allow-preconfig': true }
##
# @qom-list-get:
#
# List properties and their values for each object path in the input
# list.
#
# @paths: The absolute or partial path for each object, as described
# in `qom-get`.
#
# Errors:
# - If any path is not valid or is ambiguous
#
# Returns: A list where each element is the result for the
# corresponding element of @paths.
#
# Since 10.1
##
{ 'command': 'qom-list-get',
'data': { 'paths': [ 'str' ] },
'returns': [ 'ObjectPropertiesValues' ],
'allow-preconfig': true }
##
# @qom-set:
#

View file

@ -69,6 +69,59 @@ ObjectPropertyInfoList *qmp_qom_list(const char *path, Error **errp)
return props;
}
static void qom_list_add_property_value(Object *obj, ObjectProperty *prop,
ObjectPropertyValueList **props)
{
ObjectPropertyValue *item = g_new0(ObjectPropertyValue, 1);
QAPI_LIST_PREPEND(*props, item);
item->name = g_strdup(prop->name);
item->type = g_strdup(prop->type);
item->value = object_property_get_qobject(obj, prop->name, NULL);
}
static ObjectPropertyValueList *qom_get_property_value_list(const char *path,
Error **errp)
{
Object *obj;
ObjectProperty *prop;
ObjectPropertyIterator iter;
ObjectPropertyValueList *props = NULL;
obj = qom_resolve_path(path, errp);
if (obj == NULL) {
return NULL;
}
object_property_iter_init(&iter, obj);
while ((prop = object_property_iter_next(&iter))) {
qom_list_add_property_value(obj, prop, &props);
}
return props;
}
ObjectPropertiesValuesList *qmp_qom_list_get(strList *paths, Error **errp)
{
ObjectPropertiesValuesList *head = NULL, **tail = &head;
strList *path;
for (path = paths; path; path = path->next) {
ObjectPropertiesValues *item = g_new0(ObjectPropertiesValues, 1);
QAPI_LIST_APPEND(tail, item);
item->properties = qom_get_property_value_list(path->value, errp);
if (!item->properties) {
qapi_free_ObjectPropertiesValuesList(head);
return NULL;
}
}
return head;
}
void qmp_qom_set(const char *path, const char *property, QObject *value,
Error **errp)
{

View file

@ -11,11 +11,119 @@
#include "qobject/qdict.h"
#include "qobject/qlist.h"
#include "qobject/qstring.h"
#include "qemu/cutils.h"
#include "libqtest.h"
#define RAM_NAME "node0"
#define RAM_SIZE 65536
static int verbosity_level;
/*
* Verify that the /object/RAM_NAME 'size' property is RAM_SIZE.
*/
static void test_list_get_value(QTestState *qts)
{
QDict *args = qdict_new();
g_autoptr(QDict) response = NULL;
g_autoptr(QList) paths = qlist_new();
QListEntry *entry, *prop_entry;
const char *prop_name;
QList *properties, *return_list;
QDict *obj;
qlist_append_str(paths, "/objects/" RAM_NAME);
qdict_put_obj(args, "paths", QOBJECT(qlist_copy(paths)));
response = qtest_qmp(qts, "{ 'execute': 'qom-list-get',"
" 'arguments': %p }", args);
g_assert(response);
g_assert(qdict_haskey(response, "return"));
return_list = qobject_to(QList, qdict_get(response, "return"));
entry = QTAILQ_FIRST(&return_list->head);
obj = qobject_to(QDict, qlist_entry_obj(entry));
g_assert(qdict_haskey(obj, "properties"));
properties = qobject_to(QList, qdict_get(obj, "properties"));
QLIST_FOREACH_ENTRY(properties, prop_entry) {
QDict *prop = qobject_to(QDict, qlist_entry_obj(prop_entry));
g_assert(qdict_haskey(prop, "name"));
g_assert(qdict_haskey(prop, "value"));
prop_name = qdict_get_str(prop, "name");
if (!strcmp(prop_name, "type")) {
g_assert_cmpstr(qdict_get_str(prop, "value"), ==,
"memory-backend-ram");
} else if (!strcmp(prop_name, "size")) {
g_assert_cmpint(qdict_get_int(prop, "value"), ==, RAM_SIZE);
}
}
}
static void test_list_get(QTestState *qts, QList *paths)
{
QListEntry *entry, *prop_entry, *path_entry;
g_autoptr(QDict) response = NULL;
QDict *args = qdict_new();
QDict *prop;
QList *return_list;
if (verbosity_level >= 2) {
g_test_message("Obtaining properties for paths:");
QLIST_FOREACH_ENTRY(paths, path_entry) {
QString *qstr = qobject_to(QString, qlist_entry_obj(path_entry));
g_test_message(" %s", qstring_get_str(qstr));
}
}
qdict_put_obj(args, "paths", QOBJECT(qlist_copy(paths)));
response = qtest_qmp(qts, "{ 'execute': 'qom-list-get',"
" 'arguments': %p }", args);
g_assert(response);
g_assert(qdict_haskey(response, "return"));
return_list = qobject_to(QList, qdict_get(response, "return"));
g_assert(!qlist_empty(return_list));
path_entry = QTAILQ_FIRST(&paths->head);
QLIST_FOREACH_ENTRY(return_list, entry) {
QDict *obj = qobject_to(QDict, qlist_entry_obj(entry));
g_assert(qdict_haskey(obj, "properties"));
QList *properties = qobject_to(QList, qdict_get(obj, "properties"));
bool has_child = false;
QLIST_FOREACH_ENTRY(properties, prop_entry) {
prop = qobject_to(QDict, qlist_entry_obj(prop_entry));
g_assert(qdict_haskey(prop, "name"));
g_assert(qdict_haskey(prop, "type"));
has_child |= strstart(qdict_get_str(prop, "type"), "child<", NULL);
}
if (has_child) {
/* build a list of child paths */
QString *qstr = qobject_to(QString, qlist_entry_obj(path_entry));
const char *path = qstring_get_str(qstr);
g_autoptr(QList) child_paths = qlist_new();
QLIST_FOREACH_ENTRY(properties, prop_entry) {
prop = qobject_to(QDict, qlist_entry_obj(prop_entry));
if (strstart(qdict_get_str(prop, "type"), "child<", NULL)) {
g_autofree char *child_path = g_strdup_printf(
"%s/%s", path, qdict_get_str(prop, "name"));
qlist_append_str(child_paths, child_path);
}
}
/* fetch props for all children with one qom-list-get call */
test_list_get(qts, child_paths);
}
path_entry = QTAILQ_NEXT(path_entry, next);
}
}
static void test_properties(QTestState *qts, const char *path, bool recurse)
{
char *child_path;
@ -85,8 +193,10 @@ static void test_machine(gconstpointer data)
const char *machine = data;
QDict *response;
QTestState *qts;
g_autoptr(QList) paths = qlist_new();
qts = qtest_initf("-machine %s", machine);
qts = qtest_initf("-machine %s -object memory-backend-ram,id=%s,size=%d",
machine, RAM_NAME, RAM_SIZE);
if (g_test_slow()) {
/* Make sure we can get the machine class properties: */
@ -101,6 +211,10 @@ static void test_machine(gconstpointer data)
test_properties(qts, "/machine", true);
qlist_append_str(paths, "/machine");
test_list_get(qts, paths);
test_list_get_value(qts);
response = qtest_qmp(qts, "{ 'execute': 'quit' }");
g_assert(qdict_haskey(response, "return"));
qobject_unref(response);