[PATCH v3 0/8] API introspection and prevent accidental disk shrink via 'virDomainBlockResize'
v2 got: Reviewed-by: Ján Tomko <jtomko@redhat.com> but I've made the following changes so I'm sending it out for review once again: - rebased on top of Laine's API - added a typed parameter argument for future expansion - added documentation for the XML Patches 1/8 and 8/8 are new in the series. The rest is v3 of the last posting with aforementioned changes and rebase. Peter Krempa (8): qemu: driver: Fix coding style after 'qemuDomainAnnounceInterface' API: Introduce 'virConnectGetIntrospection' virsh: Introduce 'introspection' command which uses 'virConnectGetIntrospection' scripts: Introduce 'getintrospection' script qemu: Implement 'virConnectGetIntrospection' introspection: Add introspection of input typed parameters virsh: blockresize: Use VIR_DOMAIN_BLOCK_RESIZE_EXTEND when available and introduce --allow-shrink docs: Add documentation for introspection XML docs/docs.rst | 1 + docs/formatintrospection.rst | 90 +++++++++ docs/manpages/virsh.rst | 30 ++- docs/meson.build | 1 + include/libvirt/libvirt-host.h | 5 + scripts/genintrospection.py | 345 +++++++++++++++++++++++++++++++++ scripts/meson.build | 1 + src/driver-hypervisor.h | 7 + src/libvirt-host.c | 48 +++++ src/libvirt_private.syms | 5 + src/libvirt_public.syms | 1 + src/qemu/meson.build | 18 ++ src/qemu/qemu_driver.c | 29 ++- src/remote/remote_driver.c | 1 + src/remote/remote_protocol.x | 21 +- src/remote_protocol-structs | 11 ++ src/rpc/gendispatch.pl | 4 +- src/util/meson.build | 1 + src/util/virintrospection.c | 82 ++++++++ src/util/virintrospection.h | 20 ++ tools/virsh-domain.c | 59 ++++++ tools/virsh-host.c | 43 ++++ 22 files changed, 817 insertions(+), 6 deletions(-) create mode 100644 docs/formatintrospection.rst create mode 100755 scripts/genintrospection.py create mode 100644 src/util/virintrospection.c create mode 100644 src/util/virintrospection.h -- 2.54.0
From: Peter Krempa <pkrempa@redhat.com> Too many extra lines were added and the virHypervisorDriver entry was missing a trailing comma which would require patch adding an API to add it. Fixes: 7191d2932601ed500ad02de95dee05d622860584 Signed-off-by: Peter Krempa <pkrempa@redhat.com> --- src/qemu/qemu_driver.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/qemu/qemu_driver.c b/src/qemu/qemu_driver.c index b4cc09c81a..6f8025be53 100644 --- a/src/qemu/qemu_driver.c +++ b/src/qemu/qemu_driver.c @@ -20930,8 +20930,6 @@ qemuDomainDelThrottleGroup(virDomainPtr dom, } - - static int qemuDomainAnnounceInterface(virDomainPtr dom, const char *device, @@ -21262,7 +21260,7 @@ static virHypervisorDriver qemuHypervisorDriver = { .domainSetAutostartOnce = qemuDomainSetAutostartOnce, /* 11.2.0 */ .domainSetThrottleGroup = qemuDomainSetThrottleGroup, /* 11.2.0 */ .domainDelThrottleGroup = qemuDomainDelThrottleGroup, /* 11.2.0 */ - .domainAnnounceInterface = qemuDomainAnnounceInterface /* 12.5.0 */ + .domainAnnounceInterface = qemuDomainAnnounceInterface, /* 12.5.0 */ }; -- 2.54.0
From: Peter Krempa <pkrempa@redhat.com> The API will provide a central point to query for runtime information about support of APIs, flags, typed parameters and in future possibly other information for current connection object. The intofmation can be used to e.g. see which flags are supported for which API. Signed-off-by: Peter Krempa <pkrempa@redhat.com> --- include/libvirt/libvirt-host.h | 5 ++++ src/driver-hypervisor.h | 7 +++++ src/libvirt-host.c | 48 ++++++++++++++++++++++++++++++++++ src/libvirt_public.syms | 1 + src/remote/remote_driver.c | 1 + src/remote/remote_protocol.x | 21 ++++++++++++++- src/remote_protocol-structs | 11 ++++++++ src/rpc/gendispatch.pl | 4 ++- 8 files changed, 96 insertions(+), 2 deletions(-) diff --git a/include/libvirt/libvirt-host.h b/include/libvirt/libvirt-host.h index 5b448e7954..d2c038a2d3 100644 --- a/include/libvirt/libvirt-host.h +++ b/include/libvirt/libvirt-host.h @@ -1041,5 +1041,10 @@ int virNodeAllocPages(virConnectPtr conn, unsigned int cellCount, unsigned int flags); +char *virConnectGetIntrospection(virConnectPtr conn, + virTypedParameterPtr params, + int nparams, + unsigned int flags); + #endif /* LIBVIRT_HOST_H */ diff --git a/src/driver-hypervisor.h b/src/driver-hypervisor.h index 0add95de96..9c90482f19 100644 --- a/src/driver-hypervisor.h +++ b/src/driver-hypervisor.h @@ -1480,6 +1480,12 @@ typedef int int nparams, unsigned int flags); +typedef char * +(*virDrvConnectGetIntrospection)(virConnectPtr conn, + virTypedParameterPtr params, + int nparams, + unsigned int flags); + typedef struct _virHypervisorDriver virHypervisorDriver; /** @@ -1758,4 +1764,5 @@ struct _virHypervisorDriver { virDrvDomainSetThrottleGroup domainSetThrottleGroup; virDrvDomainDelThrottleGroup domainDelThrottleGroup; virDrvDomainAnnounceInterface domainAnnounceInterface; + virDrvConnectGetIntrospection connectGetIntrospection; }; diff --git a/src/libvirt-host.c b/src/libvirt-host.c index 6b4345b09d..951da4f89d 100644 --- a/src/libvirt-host.c +++ b/src/libvirt-host.c @@ -142,6 +142,54 @@ virConnectSupportsFeature(virConnectPtr conn, int feature) } +/** + * virConnectGetIntrospection: + * @conn: pointer to the hypervisor connection + * @params: typed parameters (currently unused) + * @nparams: number of paramters in @params + * @flags: currently unused, pass 0 + * + * Request a XML containing introspection information for the current + * connection. The introspection XML contains information about supported APIs, + * flags, and other information which depends on what the current driver + * associated with the connection supports. + * + * Additional information may be later passed in via @params, but it's currently + * unused. + * + * Returns: XML string containing the introspection data. Caller is responsible + * for freeing the associated memory. On error NULL is returned. + * + * Since: 12.5.0 + */ +char * +virConnectGetIntrospection(virConnectPtr conn, + virTypedParameterPtr params, + int nparams, + unsigned int flags) +{ + VIR_DEBUG("conn=%p, flags=0x%x", conn, flags); + VIR_TYPED_PARAMS_DEBUG(params, nparams); + + virResetLastError(); + + virCheckConnectReturn(conn, NULL); + + if (conn->driver->connectGetIntrospection) { + char *ret = conn->driver->connectGetIntrospection(conn, params, nparams, flags); + if (!ret) + goto error; + return ret; + } + + virReportUnsupportedError(); + + error: + virDispatchError(conn); + return NULL; +} + + /** * virConnectGetType: * @conn: pointer to the hypervisor connection diff --git a/src/libvirt_public.syms b/src/libvirt_public.syms index 64ed641b7f..5ce85bde76 100644 --- a/src/libvirt_public.syms +++ b/src/libvirt_public.syms @@ -959,6 +959,7 @@ LIBVIRT_11.2.0 { LIBVIRT_12.5.0 { global: virDomainAnnounceInterface; + virConnectGetIntrospection; } LIBVIRT_11.2.0; # .... define new API here using predicted next version number .... diff --git a/src/remote/remote_driver.c b/src/remote/remote_driver.c index 5c358d58c4..43f194acde 100644 --- a/src/remote/remote_driver.c +++ b/src/remote/remote_driver.c @@ -8027,6 +8027,7 @@ static virHypervisorDriver hypervisor_driver = { .domainSetThrottleGroup = remoteDomainSetThrottleGroup, /* 11.2.0 */ .domainDelThrottleGroup = remoteDomainDelThrottleGroup, /* 11.2.0 */ .domainAnnounceInterface = remoteDomainAnnounceInterface, /* 12.5.0 */ + .connectGetIntrospection = remoteConnectGetIntrospection, /* 12.5.0 */ }; static virNetworkDriver network_driver = { diff --git a/src/remote/remote_protocol.x b/src/remote/remote_protocol.x index 32185fde2f..8f113e8f17 100644 --- a/src/remote/remote_protocol.x +++ b/src/remote/remote_protocol.x @@ -298,6 +298,11 @@ const REMOTE_DOMAIN_MESSAGES_MAX = 2048; */ const REMOTE_DOMAIN_ANNOUNCE_INTERFACE_PARAMS_MAX = 16; +/* + * Upper limit on number of API introspection parameters + */ +const REMOTE_CONNECT_GET_INTROSPECTION_PARAMS_MAX = 16; + /* UUID. VIR_UUID_BUFLEN definition comes from libvirt.h */ typedef opaque remote_uuid[VIR_UUID_BUFLEN]; @@ -4034,6 +4039,14 @@ struct remote_domain_announce_interface_args { unsigned int flags; }; +struct remote_connect_get_introspection_args { + remote_typed_param params<REMOTE_CONNECT_GET_INTROSPECTION_PARAMS_MAX>; + unsigned int flags; +}; + +struct remote_connect_get_introspection_ret { + remote_nonnull_string xml; +}; /*----- Protocol. -----*/ /* Define the program number, protocol version and procedure numbers here. */ @@ -7163,5 +7176,11 @@ enum remote_procedure { * @generate: both * @acl: domain:write */ - REMOTE_PROC_DOMAIN_ANNOUNCE_INTERFACE = 456 + REMOTE_PROC_DOMAIN_ANNOUNCE_INTERFACE = 456, + + /** + * @generate: both + * @acl: connect:read + */ + REMOTE_PROC_CONNECT_GET_INTROSPECTION = 457 }; diff --git a/src/remote_protocol-structs b/src/remote_protocol-structs index 6093a85c98..de6031d71f 100644 --- a/src/remote_protocol-structs +++ b/src/remote_protocol-structs @@ -3358,6 +3358,16 @@ struct remote_domain_announce_interface_args { } params; u_int flags; }; +struct remote_connect_get_introspection_args { + struct { + u_int params_len; + remote_typed_param * params_val; + } params; + u_int flags; +}; +struct remote_connect_get_introspection_ret { + remote_nonnull_string xml; +}; enum remote_procedure { REMOTE_PROC_CONNECT_OPEN = 1, REMOTE_PROC_CONNECT_CLOSE = 2, @@ -3815,4 +3825,5 @@ enum remote_procedure { REMOTE_PROC_DOMAIN_EVENT_VCPU_REMOVED = 454, REMOTE_PROC_DOMAIN_EVENT_CALLBACK_CHANNEL_LIFECYCLE = 455, REMOTE_PROC_DOMAIN_ANNOUNCE_INTERFACE = 456, + REMOTE_PROC_CONNECT_GET_INTROSPECTION = 457, }; diff --git a/src/rpc/gendispatch.pl b/src/rpc/gendispatch.pl index 8cb7d7eda2..1d8d00b469 100755 --- a/src/rpc/gendispatch.pl +++ b/src/rpc/gendispatch.pl @@ -633,7 +633,9 @@ elsif ($mode eq "server") { # NB: if your new API starts with remote_typed_params, enter it here if you need # the conn arg to be passed first! - if ($call->{ProcName} eq "NodeSetMemoryParameters" || $call->{ProcName} eq "DomainRestoreParams") { + if ($call->{ProcName} eq "NodeSetMemoryParameters" || + $call->{ProcName} eq "DomainRestoreParams" || + $call->{ProcName} eq "ConnectGetIntrospection") { push(@args_list, $conn_var); } push(@args_list, "$1"); -- 2.54.0
From: Peter Krempa <pkrempa@redhat.com> Signed-off-by: Peter Krempa <pkrempa@redhat.com> --- docs/manpages/virsh.rst | 19 ++++++++++++++++++ tools/virsh-host.c | 43 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/docs/manpages/virsh.rst b/docs/manpages/virsh.rst index a10d29e0ea..e5e5c98f44 100644 --- a/docs/manpages/virsh.rst +++ b/docs/manpages/virsh.rst @@ -1104,6 +1104,25 @@ on the host are reported. The option *--all* will report every CPU model known to the hypervisor, including ones that are not supported on the hypervisor (e.g. newer generation models). +introspection +------------- + +**Syntax:** + +:: + + introspection [--xpath EXPRESSION] [--wrap] + +Output the libvirt connection feature introspection XML. + +If the **--xpath** argument provides an XPath expression, it will be +evaluated against the output XML and only those matching nodes will +be printed. The default behaviour is to print each matching node as +a standalone document, however, for ease of additional processing, +the **--wrap** argument will cause the matching node to be wrapped +in a common root node. + + DOMAIN COMMANDS =============== diff --git a/tools/virsh-host.c b/tools/virsh-host.c index 0c18db67c4..f3fc26a875 100644 --- a/tools/virsh-host.c +++ b/tools/virsh-host.c @@ -1871,6 +1871,43 @@ cmdHypervisorCPUModelNames(vshControl *ctl, } +static const vshCmdInfo info_introspection = { + .help = N_("get XML containing connection introspection data"), + .desc = N_("get XML containing connection introspection data"), +}; + +static const vshCmdOptDef opts_introspection[] = { + {.name = "xpath", + .type = VSH_OT_STRING, + .completer = vshCompleteEmpty, + .help = N_("xpath expression to filter the XML document") + }, + {.name = "wrap", + .type = VSH_OT_BOOL, + .help = N_("wrap xpath results in an common root element"), + }, + {.name = NULL} +}; + +static bool +cmdIntrospection(vshControl *ctl, + const vshCmd *cmd) +{ + g_autofree char *xml = NULL; + virshControl *priv = ctl->privData; + bool wrap = vshCommandOptBool(cmd, "wrap"); + const char *xpath = NULL; + + if (vshCommandOptStringQuiet(ctl, cmd, "xpath", &xpath) < 0) + return false; + + if (!(xml = virConnectGetIntrospection(priv->conn, NULL, 0, 0))) + return false; + + return virshDumpXML(ctl, xml, "domain", xpath, wrap); +} + + const vshCmdDef hostAndHypervisorCmds[] = { {.name = "allocpages", .handler = cmdAllocpages, @@ -2010,5 +2047,11 @@ const vshCmdDef hostAndHypervisorCmds[] = { .info = &info_version, .flags = 0 }, + {.name = "introspection", + .handler = cmdIntrospection, + .opts = opts_introspection, + .info = &info_introspection, + .flags = 0 + }, {.name = NULL} }; -- 2.54.0
From: Peter Krempa <pkrempa@redhat.com> The script analyzes the driver implementation source file and generates an include file which describes the analyzed driver in terms of: - supported APIs - flags supported for the API (by looking at virCheckFlags) The generated structure then will be used to generate the introspection XML. The script goes through the 'virHypervisorDriver' struct, finds all callbacks corresponding to public APIs and then goes through the functions finding the 'virCheckFlags' to collect supported flags per API. Since the migration APIs are public but use internal functions which don't map directly, the script tries to find the best matching internal API and then infers the flags for the public migration APIs from the detected flags. The script works only with the contemporary coding style for functions due to regex usage so any driver impl file needs to be modernized first. As first example, introspection of qemu driver is generated. An excerpt from the generated data (which is for internal use, and will be used to generate XML): static const virIntrospectionData driver_api_introspection[] = { { .api = "virConnectBaselineCPU", .flags_arg = true, .flags = VIR_CONNECT_BASELINE_CPU_EXPAND_FEATURES | VIR_CONNECT_BASELINE_CPU_MIGRATABLE, }, { .api = "virConnectBaselineHypervisorCPU", .flags_arg = true, .flags = VIR_CONNECT_BASELINE_CPU_EXPAND_FEATURES | VIR_CONNECT_BASELINE_CPU_MIGRATABLE | VIR_CONNECT_BASELINE_CPU_IGNORE_HOST, }, { .api = "virConnectClose", .flags_arg = false, }, Signed-off-by: Peter Krempa <pkrempa@redhat.com> --- scripts/genintrospection.py | 220 ++++++++++++++++++++++++++++++++++++ scripts/meson.build | 1 + src/qemu/meson.build | 18 +++ 3 files changed, 239 insertions(+) create mode 100755 scripts/genintrospection.py diff --git a/scripts/genintrospection.py b/scripts/genintrospection.py new file mode 100755 index 0000000000..c3ac5c940a --- /dev/null +++ b/scripts/genintrospection.py @@ -0,0 +1,220 @@ +#!/usr/bin/env python3 +# +# SPDX-License-Identifier: LGPL-2.1-or-later + +import argparse +import re +import sys + + +# driver callbacks needed to infer the introspection for public migration APIs +# which do not map directly to any driver API callback. We need a representative +# sample of APIs supporting typed parameters and flags +# The APIs are ordered acco +migration_driver_symbols = [ + "domainMigrateBegin3Params", + "domainMigrateBegin3", + "domainMigratePrepare2", + "domainMigratePrepare", +] + +migration_public_api = { + "virDomainMigrate": {}, + "virDomainMigrateToURI": {}, + "virDomainMigrate2": {}, + "virDomainMigrateToURI2": {}, + "virDomainMigrate3": {"params": True}, + "virDomainMigrateToURI3": {"params": True}, +} + + +def load_public_symbols(filename): + """load the public symbol file and return all APIs""" + symbols = [] + + with open(filename, "r") as symfile: + while True: + line = symfile.readline() + + if not line: + break + + m = re.match(r"\s+(?P<sym>vir\w+);", line) + + if not m: + continue + + symbols.append(m.group("sym")) + + return symbols + + +def parse_api(filename, syms): + with open(filename, "r") as f: + content = f.read() + + # parse the definition of current driver's virHypervisorDriver struct + driverdef = re.search( + r"virHypervisorDriver.*?=\s+{(?P<apis>[^}]+)", + content, + flags=re.DOTALL | re.MULTILINE, + ) + + # parse all functions + # requires that the input file is well formed: + # - types of return value are on separate line including pointer symbol + # - function name starts on separate line followed by opening parenthesis for arguments + # - opening brace for body is on start of a separate line + # - closing brace for body is on start of a separate line + funciter = re.finditer( + r"^(?P<name>\w+)\((?P<args>[^)]*)\)\n^{(?P<impl>.*?)^}", + content, + flags=re.DOTALL | re.MULTILINE, + ) + + if not driverdef: + raise Exception(f"'virHypervisorDriver' definition not found in '{filename}'") + + # create mapping from public API names to local driver's callback names + apis = {} + # .connectOpen = qemuConnectOpen -> apis['qemuConnectOpen'] = 'virConnectOpen' + for api in re.finditer( + r"\.(?P<field>\w+)\s+=\s+(?P<callback>\w+)", driverdef.group("apis") + ): + # skip hypervisor driver name definition + if api.group("field") == "name": + continue + + # skip APIs that have had their implementation removed + if api.group("callback") == "NULL": + continue + + # connectOpen -> virConnectOpen + # for migration API we use the name in virHypervisorDriver + name = api.group("field") + apiname = None + + if name in migration_driver_symbols: + apis[api.group("callback")] = name + else: + apiname = "vir" + name[0:1].upper() + name[1:] + apis[api.group("callback")] = apiname + + flagmap = {} + migr_data = {} + # parse exported functions, presence of 'flags' argument and the 'virCheckFlags' expression + for f in funciter: + apiname = apis.get(f.group("name"), None) + migrationapi = False + + # process exported public APIs or migration handlers; migration handlers + # will be at the end handled separately + if not apiname: + continue + + if apiname in migration_driver_symbols: + migrationapi = True + elif apiname not in syms: + continue + + data = { + "callback": f.group("name"), + "flags_arg": False, + "flags_supported": None, + } + + flagarg = re.search(r"\bflags\b", f.group("args")) + if flagarg: + data["flags_arg"] = True + + flagcheck = re.search( + r"virCheckFlags(?:Goto)?\((?P<flags>[^,]+),", + f.group("impl"), + flags=re.DOTALL | re.MULTILINE, + ) + + if flagcheck: + data["flags_supported"] = re.sub(r"\s+", " ", flagcheck.group("flags")) + + if migrationapi: + migr_data[apiname] = data + else: + flagmap[apiname] = data + + # populate migration API data. To do this we want to find which internal + # APIs are supported and then populate the list based on the detected + # information + for m in migration_driver_symbols: + if m in migr_data: + for apiname, pub in migration_public_api.items(): + data = migr_data[m].copy() + flagmap[apiname] = data + + break + + return flagmap + + +parser = argparse.ArgumentParser( + description="Tool to generate data for 'virConnectIntrospect'" +) +parser.add_argument( + "--symfile", action="append", required=True, help="public symbol file(s)" +) +parser.add_argument("--driverfile", required=True, help="driver implementation file") +parser.add_argument("--output", required=True, help="output file") +args = parser.parse_args() + +syms = [] +for sf in args.symfile: + syms += load_public_symbols(sf) + +introspection = parse_api(args.driverfile, syms) + +fail = False + +with open(args.output, "w") as outfile: + outfile.write( + f"""/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * Generated data for introspection of '{args.driverfile}'. + * This file is generated by '{parser.prog}' + */ +""" + ) + + outfile.write( + """ +static const virIntrospectionData driver_api_introspection[] = +{ +""" + ) + + for api in sorted(introspection.keys()): + data = introspection[api] + + outfile.write(f' {{ .api = "{api}",\n') + if data.get("flags_arg", False): + outfile.write(" .flags_arg = true,\n") + outfile.write(f" .flags = {data.get('flags_supported', 0)},\n") + else: + outfile.write(" .flags_arg = false,\n") + + outfile.write(" },\n") + + epilogue = """ { .api = NULL } +}; +""" + outfile.write(epilogue) + +for api, data in introspection.items(): + if ( + data.get("flags_arg", False) is True + and data.get("flags_supported", None) is None + ): + print(f"failed to parse flags for '{api}' in '{data.get('callback', '')}'") + fail = True + +if fail: + sys.exit(1) diff --git a/scripts/meson.build b/scripts/meson.build index 15a23e8738..bfb3eb7bf4 100644 --- a/scripts/meson.build +++ b/scripts/meson.build @@ -13,6 +13,7 @@ scripts = [ 'dtrace2systemtap.py', 'esx_vi_generator.py', 'genaclperms.py', + 'genintrospection.py', 'genpolkit.py', 'gensystemtap.py', 'group-qemu-caps.py', diff --git a/src/qemu/meson.build b/src/qemu/meson.build index a94bc4f9a8..4fdfc6c3e3 100644 --- a/src/qemu/meson.build +++ b/src/qemu/meson.build @@ -45,6 +45,23 @@ qemu_driver_sources = [ 'qemu_vnc.c', ] +introspection_files = custom_target( + 'qemu_introspection.inc.h', + output: 'qemu_introspection.inc.h', + input: [ + '../libvirt_public.syms', + '../libvirt_qemu.syms', + 'qemu_driver.c', + ], + command: [ + meson_python_prog, genintrospection_prog, + '--symfile', meson.project_source_root() / 'src' / 'libvirt_public.syms', + '--symfile', meson.project_source_root() / 'src' / 'libvirt_qemu.syms', + '--driverfile', meson.project_source_root() / 'src' / 'qemu' / 'qemu_driver.c', + '--output', '@OUTPUT@' + ] +) + driver_source_files += files(qemu_driver_sources) stateful_driver_source_files += files(qemu_driver_sources) @@ -95,6 +112,7 @@ if conf.has('WITH_QEMU') [ qemu_driver_sources, qemu_dtrace_gen_headers, + introspection_files, ], dependencies: [ access_dep, -- 2.54.0
From: Peter Krempa <pkrempa@redhat.com> Add general infrastructure for converting the generated introspection into XML which can be returned via 'virConnectGetIntrospection' and use it in the qemu driver. Example of the generated introspection XML: $ virsh introspection <libvirt-introspection> <hypervisor> <api name='virConnectBaselineCPU'> <flags dec='3' hex='0x3'/> </api> <api name='virConnectBaselineHypervisorCPU'> <flags dec='7' hex='0x7'/> </api> <api name='virConnectClose'/> [...] The XML has the provisions to add introspection for sub-drivers as well as can be extended in the future with other dynamic parameters such as the names and types of virTypedParameter input fields. Signed-off-by: Peter Krempa <pkrempa@redhat.com> --- src/libvirt_private.syms | 5 +++++ src/qemu/qemu_driver.c | 25 +++++++++++++++++++++++++ src/util/meson.build | 1 + src/util/virintrospection.c | 37 +++++++++++++++++++++++++++++++++++++ src/util/virintrospection.h | 17 +++++++++++++++++ 5 files changed, 85 insertions(+) create mode 100644 src/util/virintrospection.c create mode 100644 src/util/virintrospection.h diff --git a/src/libvirt_private.syms b/src/libvirt_private.syms index c76e5cb08a..838feca8e8 100644 --- a/src/libvirt_private.syms +++ b/src/libvirt_private.syms @@ -2700,6 +2700,11 @@ virInhibitorRelease; virInitctlFifos; virInitctlSetRunLevel; + +# util/virintrospection.h +virIntrospectionGetXML; + + # util/viriommufd.h virIOMMUFDOpenDevice; virIOMMUFDSupported; diff --git a/src/qemu/qemu_driver.c b/src/qemu/qemu_driver.c index 6f8025be53..f8774d40e4 100644 --- a/src/qemu/qemu_driver.c +++ b/src/qemu/qemu_driver.c @@ -106,6 +106,7 @@ #include "virdomaincheckpointobjlist.h" #include "virutil.h" #include "backup_conf.h" +#include "virintrospection.h" #define VIR_FROM_THIS VIR_FROM_QEMU @@ -21006,6 +21007,29 @@ qemuDomainAnnounceInterface(virDomainPtr dom, } +#include "qemu_introspection.inc.h" + +static char * +qemuConnectGetIntrospection(virConnectPtr conn G_GNUC_UNUSED, + virTypedParameterPtr params, + int nparams, + unsigned int flags) +{ + virCheckFlags(0, NULL); + + /* BEWARE: This function allows read-only connections. If typed parameters + * are ever introduced it must be ensured that they are properly interpreted + * based on the connection permissions */ + if (virTypedParamsValidate(params, nparams, + NULL) < 0) + + if (virConnectGetIntrospectionEnsureACL(conn) < 0) + return NULL; + + return virIntrospectionGetXML(driver_api_introspection); +} + + static virHypervisorDriver qemuHypervisorDriver = { .name = QEMU_DRIVER_NAME, .connectURIProbe = qemuConnectURIProbe, @@ -21261,6 +21285,7 @@ static virHypervisorDriver qemuHypervisorDriver = { .domainSetThrottleGroup = qemuDomainSetThrottleGroup, /* 11.2.0 */ .domainDelThrottleGroup = qemuDomainDelThrottleGroup, /* 11.2.0 */ .domainAnnounceInterface = qemuDomainAnnounceInterface, /* 12.5.0 */ + .connectGetIntrospection = qemuConnectGetIntrospection, /* 12.5.0 */ }; diff --git a/src/util/meson.build b/src/util/meson.build index 9fb0aa0fe7..542f8a1b9b 100644 --- a/src/util/meson.build +++ b/src/util/meson.build @@ -46,6 +46,7 @@ util_sources = [ 'viridentity.c', 'virinhibitor.c', 'virinitctl.c', + 'virintrospection.c', 'viriommufd.c', 'viriscsi.c', 'virjson.c', diff --git a/src/util/virintrospection.c b/src/util/virintrospection.c new file mode 100644 index 0000000000..b9fb5beda8 --- /dev/null +++ b/src/util/virintrospection.c @@ -0,0 +1,37 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#include <config.h> + +#include "virintrospection.h" +#include "virxml.h" +#include "virbuffer.h" + +char * +virIntrospectionGetXML(const virIntrospectionData *d) +{ + g_auto(virBuffer) xml = VIR_BUFFER_INITIALIZER; + g_auto(virBuffer) sections = VIR_BUFFER_INIT_CHILD(&xml); + g_auto(virBuffer) apis = VIR_BUFFER_INIT_CHILD(§ions); + size_t i; + + for (i = 0; d[i].api != NULL; i++) { + g_auto(virBuffer) api_attr = VIR_BUFFER_INITIALIZER; + g_auto(virBuffer) api_elem = VIR_BUFFER_INIT_CHILD(&apis); + + virBufferAsprintf(&api_attr, " name='%s'", d[i].api); + + if (d[i].flags_arg) { + virBufferAsprintf(&api_elem, "<flags dec='%u' hex='0x%x'/>\n", + d[i].flags, d[i].flags); + } + + virXMLFormatElement(&apis, "api", &api_attr, &api_elem); + } + + virXMLFormatElement(§ions, "hypervisor", NULL, &apis); + virXMLFormatElementEmpty(&xml, "libvirt-introspection", NULL, §ions); + + return virBufferContentAndReset(&xml); +} diff --git a/src/util/virintrospection.h b/src/util/virintrospection.h new file mode 100644 index 0000000000..f996ce0f07 --- /dev/null +++ b/src/util/virintrospection.h @@ -0,0 +1,17 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#include <stdbool.h> + +struct _virIntrospectionData { + const char *api; + bool flags_arg; + unsigned int flags; +}; +typedef struct _virIntrospectionData virIntrospectionData; + +char * +virIntrospectionGetXML(const virIntrospectionData *d); -- 2.54.0
From: Peter Krempa <pkrempa@redhat.com> Extract information about typed parameters from calls to 'virTypedParamsValidate'/'virTypedParamsValidateTemplate' and expose them in the introspection XML: <api name='virDomainMigrate3'> <flags dec='2097151' hex='0x1fffff'/> <typed-parameters type='input' name='params'> <param name='migrate_uri' type='string'/> <param name='destination_name' type='string'/> <param name='destination_xml' type='string'/> <param name='bandwidth' type='ullong'/> <param name='graphics_uri' type='string'/> <param name='listen_address' type='string'/> <param name='migrate_disks' type='string' multiple='yes'/> <param name='migrate_disks_detect_zeroes' type='string' multiple='yes'/> <param name='migrate_disks_target_zero' type='string' multiple='yes'/> <param name='disks_port' type='int'/> <param name='compression' type='string' multiple='yes'/> <param name='compression.mt.level' type='int'/> <param name='compression.mt.threads' type='int'/> <param name='compression.mt.dthreads' type='int'/> <param name='compression.xbzrle.cache' type='ullong'/> <param name='persistent_xml' type='string'/> <param name='auto_converge.initial' type='int'/> <param name='auto_converge.increment' type='int'/> <param name='bandwidth.postcopy' type='ullong'/> <param name='parallel.connections' type='int'/> <param name='compression.zlib.level' type='int'/> <param name='compression.zstd.level' type='int'/> <param name='tls.destination' type='string'/> <param name='disks_uri' type='string'/> <param name='bandwidth.avail.switchover' type='ullong'/> </typed-parameters> </api> Migration APIs once again required special handling as some typed params are supported even if the backing APIs using typed parameters arend supported because they can be converted to legacy parameters for the lesser APIs. Signed-off-by: Peter Krempa <pkrempa@redhat.com> --- scripts/genintrospection.py | 125 ++++++++++++++++++++++++++++++++++++ src/util/virintrospection.c | 45 +++++++++++++ src/util/virintrospection.h | 3 + 3 files changed, 173 insertions(+) diff --git a/scripts/genintrospection.py b/scripts/genintrospection.py index c3ac5c940a..95469fc15a 100755 --- a/scripts/genintrospection.py +++ b/scripts/genintrospection.py @@ -7,6 +7,21 @@ import re import sys +# APIs which fill a user-supplied virTypedParameter pointer -- use it as output +input_params_exceptions = [ + "virDomainGetBlkioParameters", + "virDomainGetMemoryParameters", + "virDomainGetNumaParameters", + "virDomainGetSchedulerParametersFlags", + "virDomainGetSchedulerParameters", + "virDomainBlockStatsFlags", + "virDomainGetInterfaceParameters", + "virDomainGetBlockIoTune", + "virDomainGetCPUStats", + "virNodeGetMemoryParameters", +] + + # driver callbacks needed to infer the introspection for public migration APIs # which do not map directly to any driver API callback. We need a representative # sample of APIs supporting typed parameters and flags @@ -27,6 +42,15 @@ migration_public_api = { "virDomainMigrateToURI3": {"params": True}, } +# list of parameters supported by 'virDomainMigrate3'/'virDomainMigrateToURI3' +# if any migration protocol version is supported by extracting the parameters +migration_public_api_fallback_params = [ + ("VIR_MIGRATE_PARAM_URI", "VIR_TYPED_PARAM_STRING"), + ("VIR_MIGRATE_PARAM_DEST_NAME", "VIR_TYPED_PARAM_STRING"), + ("VIR_MIGRATE_PARAM_DEST_XML", "VIR_TYPED_PARAM_STRING"), + ("VIR_MIGRATE_PARAM_BANDWIDTH", "VIR_TYPED_PARAM_ULLONG"), +] + def load_public_symbols(filename): """load the public symbol file and return all APIs""" @@ -121,8 +145,13 @@ def parse_api(filename, syms): "callback": f.group("name"), "flags_arg": False, "flags_supported": None, + "input_params": False, + "input_params_supported": None, + "input_params_template": None, } + # find APIs having 'flags' argument and find the corresponding + # virCheckFlags flagarg = re.search(r"\bflags\b", f.group("args")) if flagarg: data["flags_arg"] = True @@ -136,6 +165,54 @@ def parse_api(filename, syms): if flagcheck: data["flags_supported"] = re.sub(r"\s+", " ", flagcheck.group("flags")) + # find APIs supporting typed parameters as input and find the + # corresponding supported flags by matching 'virTypedParamsValidate' + # We're looking for APIs which take typed parameters as input, which + # excludes any API taking a double-pointer. From the rest + # the 'input_params_exceptions' array has APIs which output typed + # parameters into a pre-allocated array and thus are excluded + paramarg = re.search( + r"virTypedParameter(?P<ptrtype>Ptr)?(?P<ptrstar>[ *]+)", f.group("args") + ) + param_nptrs = 0 + + if paramarg: + if paramarg.group("ptrtype"): + param_nptrs += 1 + + for c in paramarg.group("ptrstar"): + if c == "*": + param_nptrs += 1 + + if param_nptrs == 1 and apiname not in input_params_exceptions: + data["input_params"] = True + + paramscheck = re.search( + r"virTypedParamsValidate(?P<template>Template)?(?:Deferred)?\([^,]+,\s*[^,]+,\s*(?P<params>[^)]+)\)", + f.group("impl"), + flags=re.DOTALL | re.MULTILINE, + ) + + if paramscheck: + if paramscheck.group("template"): + data["input_params_template"] = paramscheck.group("params") + + else: + data["input_params_supported"] = [] + + idx = 0 + params = re.sub(r"\s+", "", paramscheck.group("params")).split(",") + + while idx < len(params): + if params[idx] == "NULL": + break + + data["input_params_supported"].append( + (params[idx], params[idx + 1]) + ) + + idx += 2 + if migrationapi: migr_data[apiname] = data else: @@ -148,6 +225,18 @@ def parse_api(filename, syms): if m in migr_data: for apiname, pub in migration_public_api.items(): data = migr_data[m].copy() + + if pub.get("params", False): + if not data["input_params"]: + data["input_params"] = True + data["input_params_supported"] = ( + migration_public_api_fallback_params + ) + else: + data["input_params"] = False + data["input_params_supported"] = None + data["input_params_template"] = None + flagmap[apiname] = data break @@ -184,6 +273,27 @@ with open(args.output, "w") as outfile: """ ) + for api in sorted(introspection.keys()): + data = introspection[api] + + if data.get("input_params_supported", None) is None: + continue + + outfile.write( + f""" +const virTypedParamValidationTemplate {data["callback"]}InputParamValidation[] = {{ +""" + ) + + for param, flag in data["input_params_supported"]: + outfile.write(f" {{ {param}, {flag} }},\n") + + outfile.write( + """ { "", 0 } +}; +""" + ) + outfile.write( """ static const virIntrospectionData driver_api_introspection[] = @@ -201,6 +311,13 @@ static const virIntrospectionData driver_api_introspection[] = else: outfile.write(" .flags_arg = false,\n") + if data.get("input_params_supported", None) is not None: + outfile.write( + f" .input_params = {data['callback']}InputParamValidation,\n" + ) + elif data.get("input_params_template", None) is not None: + outfile.write(f" .input_params = {data['input_params_template']},\n") + outfile.write(" },\n") epilogue = """ { .api = NULL } @@ -216,5 +333,13 @@ for api, data in introspection.items(): print(f"failed to parse flags for '{api}' in '{data.get('callback', '')}'") fail = True + if data.get("input_params", False) is True and ( + data.get("input_params_supported", None) is None + and data.get("input_params_template", None) is None + ): + print( + f"failed to parse typed params for '{api}' in '{data.get('callback', '')}'" + ) + fail = True if fail: sys.exit(1) diff --git a/src/util/virintrospection.c b/src/util/virintrospection.c index b9fb5beda8..2862e60924 100644 --- a/src/util/virintrospection.c +++ b/src/util/virintrospection.c @@ -8,6 +8,24 @@ #include "virxml.h" #include "virbuffer.h" +/* These strings are exported int he XML */ +VIR_ENUM_DECL(virIntrospectionTypedParam); + +VIR_ENUM_IMPL(virIntrospectionTypedParam, + VIR_TYPED_PARAM_UNSIGNED + 1, + "", + "int", + "uint", + "llong", + "ullong", + "double", + "boolean", + "string", + "", /* VIR_TYPED_PARAM_LAST */ + "ullong", /* VIR_TYPED_PARAM_UNSIGNED */ +); + + char * virIntrospectionGetXML(const virIntrospectionData *d) { @@ -27,6 +45,33 @@ virIntrospectionGetXML(const virIntrospectionData *d) d[i].flags, d[i].flags); } + if (d[i].input_params) { + g_auto(virBuffer) typedparam_attr = VIR_BUFFER_INITIALIZER; + g_auto(virBuffer) typedparam_elem = VIR_BUFFER_INIT_CHILD(&api_elem); + size_t j; + + virBufferAddLit(&typedparam_attr, " type='input' name='params'"); + + for (j = 0; d[i].input_params[j].name[0] != '\0'; j++) { + unsigned int typeval = d[i].input_params[j].typeflags & ~VIR_TYPED_PARAM_MULTIPLE; + const char *type = virIntrospectionTypedParamTypeToString(typeval); + + virBufferAsprintf(&typedparam_elem, "<param name='%s'", + d[i].input_params[j].name); + + if (type && type[0] != '\0') + virBufferAsprintf(&typedparam_elem, " type='%s'", type); + + if (d[i].input_params[j].typeflags & VIR_TYPED_PARAM_MULTIPLE) + virBufferAddLit(&typedparam_elem, " multiple='yes'"); + + virBufferAddLit(&typedparam_elem, "/>\n"); + } + + virXMLFormatElement(&api_elem, "typed-parameters", + &typedparam_attr, &typedparam_elem); + } + virXMLFormatElement(&apis, "api", &api_attr, &api_elem); } diff --git a/src/util/virintrospection.h b/src/util/virintrospection.h index f996ce0f07..8d259e36ce 100644 --- a/src/util/virintrospection.h +++ b/src/util/virintrospection.h @@ -6,10 +6,13 @@ #include <stdbool.h> +#include "virtypedparam.h" + struct _virIntrospectionData { const char *api; bool flags_arg; unsigned int flags; + const virTypedParamValidationTemplate *input_params; }; typedef struct _virIntrospectionData virIntrospectionData; -- 2.54.0
From: Peter Krempa <pkrempa@redhat.com> Use the new VIR_DOMAIN_BLOCK_RESIZE_PROBE_FLAGS flag to probe for VIR_DOMAIN_BLOCK_RESIZE_EXTEND and use it if available. This will give users the same protection as with --extend in the default case. IMPORTANT: This patch changes behaviour for users who want to shrink their VM's block device knowingly. Such users, when using a newer daemon will be required to pass --allow-shrink now. I've contemplated either renaming the whole command to preserve functionality for the existing one and deprecating 'virsh blockresize' or adding just the '--extend', but unfortunately neither of those in the end looked appealing to me. As shrinking filesystems is much more invloved and much less common the benefit of preventing accidental data loss should outweigh the breakage from the change. To aleviate issues with scripts virsh now also provides the '--probe-arguments' option to allow safe probe if virsh supports this new argument. Closes: https://gitlab.com/libvirt/libvirt/-/work_items/789 Signed-off-by: Peter Krempa <pkrempa@redhat.com> --- docs/manpages/virsh.rst | 11 +++++++- tools/virsh-domain.c | 59 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/docs/manpages/virsh.rst b/docs/manpages/virsh.rst index e5e5c98f44..35fc8bf338 100644 --- a/docs/manpages/virsh.rst +++ b/docs/manpages/virsh.rst @@ -1668,7 +1668,7 @@ blockresize :: - blockresize domain path ([size] | [--capacity]) [--extend] + blockresize domain path ([size] | [--capacity]) [--extend] [--allow-shrink] Resize a block device of domain while the domain is running, *path* specifies the absolute path of the block device; it corresponds @@ -1681,6 +1681,15 @@ smaller than the old size, thus prevents (accidental) shrinking of the block device which may lead to data loss. Users are encouraged to always use this flag unless communicating with an older hypervisor. +As of libvirt 12.4.0 the 'virsh blockresize' command now probes whether the +hypervisor supports enforcing that the device is not shrunk accidentally and +if it is supported this feature will be used as a precaution from accidental +data loss even at the cost of change of behaviour. With older hypervisors +which do not support the feature the protection will not be avaliable. +Users wanting to shrink the block device must use the *--allow-shrink* +flag. In scripts it's possible to look for presence of *--allow-shrink* in +output of ``virsh help blockresize`` to determine if the flag is supported. + For image formats without metadata (raw) stored inside fixed-size storage (e.g. block devices) the --capacity flag can be used to resize the device to the full size of the backing device. diff --git a/tools/virsh-domain.c b/tools/virsh-domain.c index 8a6f868e34..74ef281b42 100644 --- a/tools/virsh-domain.c +++ b/tools/virsh-domain.c @@ -3313,6 +3313,51 @@ cmdBlockpull(vshControl *ctl, const vshCmd *cmd) return ret; } + +/** + * virshIntrospectFlags: + * @ctl: virsh control data + * @api: name of API to introspect flags + * @flags: filled with flags supported by @api on success + * + * Uses the 'virConnectIntrospection' to introspect @api and return supported + * flags. + * + * Returns 0 on success (@flags are populated), -1 on error. No errors are + * reported by this function. + */ +static int +virshIntrospectFlags(vshControl *ctl, + const char *api, + unsigned int *flags) +{ + virshControl *priv = ctl->privData; + g_autofree char *introspectionxml = NULL; + g_autoptr(xmlDoc) xmldoc = NULL; + g_autoptr(xmlXPathContext) ctxt = NULL; + g_autofree char *xpath = g_strdup_printf("string(//api[@name='%s']/flags/@dec)", + api); + + *flags = 0; + + if (!(introspectionxml = virConnectGetIntrospection(priv->conn, NULL, 0, 0))) { + vshResetLibvirtError(); + return -1; + } + + if (!(xmldoc = virXMLParseStringCtxt(introspectionxml, "(introspection xml)", + &ctxt))) + return -1; + + if (virXPathUInt(xpath, ctxt, flags) < 0) + return -1; + + vshDebug(ctl, VSH_ERR_DEBUG, "api:'%s' flags='0x%x'", api, *flags); + + return 0; +} + + /* * "blockresize" command */ @@ -3343,6 +3388,10 @@ static const vshCmdOptDef opts_blockresize[] = { .type = VSH_OT_BOOL, .help = N_("ensure that the new size is larger than actual capacity (prevent shrink)") }, + {.name = "allow-shrink", + .type = VSH_OT_BOOL, + .help = N_("disable size checks so that device can be shrunk") + }, {.name = NULL} }; @@ -3378,6 +3427,16 @@ cmdBlockresize(vshControl *ctl, const vshCmd *cmd) if (!(dom = virshCommandOptDomain(ctl, cmd, NULL))) return false; + /* probe for support of VIR_DOMAIN_BLOCK_RESIZE_EXTEND and use it if it's + * supported to prevent mishaps even at the cost of usability change */ + if (!vshCommandOptBool(cmd, "allow-shrink")) { + unsigned int supportedflags = 0; + + if (virshIntrospectFlags(ctl, "virDomainBlockResize", &supportedflags) == 0 && + supportedflags & VIR_DOMAIN_BLOCK_RESIZE_EXTEND) + flags |= VIR_DOMAIN_BLOCK_RESIZE_EXTEND; + } + if (virDomainBlockResize(dom, path, size, flags) < 0) { vshError(ctl, _("Failed to resize block device '%1$s'"), path); return false; -- 2.54.0
From: Peter Krempa <pkrempa@redhat.com> Briefly describe what the introspection API returns. Signed-off-by: Peter Krempa <pkrempa@redhat.com> --- docs/docs.rst | 1 + docs/formatintrospection.rst | 90 ++++++++++++++++++++++++++++++++++++ docs/meson.build | 1 + 3 files changed, 92 insertions(+) create mode 100644 docs/formatintrospection.rst diff --git a/docs/docs.rst b/docs/docs.rst index 09939c1201..110afba1fc 100644 --- a/docs/docs.rst +++ b/docs/docs.rst @@ -100,6 +100,7 @@ Application development * `snapshots <formatsnapshot.html>`__ * `checkpoints <formatcheckpoint.html>`__ * `backup jobs <formatbackup.html>`__ + * `introspection XML <formatintrospection.html>`__ `Language bindings and API modules <bindings.html>`__ Bindings of the libvirt API for diff --git a/docs/formatintrospection.rst b/docs/formatintrospection.rst new file mode 100644 index 0000000000..80244a7917 --- /dev/null +++ b/docs/formatintrospection.rst @@ -0,0 +1,90 @@ +.. role:: since + +============================ +API introspection XML format +============================ + +.. contents:: + +Overview +-------- + +The API parameter introspection API +`virConnectGetIntrospection <html/libvirt-libvirt-host.html#virConnectGetIntrospection>`__ +allows callers probing which flags and parameters are supported by given API on +given hypervisor. + +The XML has following structure:: + + <libvirt-introspection> + <hypervisor> + <api name='virDomainMigrateToURI3'> + <flags dec='2097151' hex='0x1fffff'/> + <typed-parameters type='input' name='params'> + <param name='migrate_uri' type='string'/> + <param name='destination_name' type='string'/> + <param name='destination_xml' type='string'/> + <param name='bandwidth' type='ullong'/> + <param name='graphics_uri' type='string'/> + <param name='listen_address' type='string'/> + <param name='migrate_disks' type='string' multiple='yes'/> + </typed-parameters> + </api> + + [...] + + </hypervisor> + </libvirt-introspection> + +API groups +---------- + +`virConnectGetIntrospection <html/libvirt-libvirt-host.html#virConnectGetIntrospection>`__ +reports the supported APIs grouped by which driver kind they were queried for. + +Currently only hypervisor drivers are queried and the APIs supported by the +hypervisor driver the connection connects to are reported under ``<hypervisor>`` +sub-element. + + +API element +----------- + +The ``<api>`` element is an entry for an individual API identified by the name +attribute. The value refers to the C function name of the given API. Sub +elements describe possible values for some of the arguments. + +API flags +~~~~~~~~~ + +If the ``<api>`` element has a ``<flags>`` sub-elements, the API accepts a +'flags' parameter. Both ``dec`` and ``hex`` attributes then contain a bitmask +of accepted flag bits by given API in decimal and hexadecimal encoding. + + +``virTypedParams`` argument +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``<typed-parameters>`` element + +The ``name`` attribute specifies the C parameter name that the typed parameters +are passed as. + +The ``type`` attribute specifies whether the typed parameters are arguments to +the API (``input``) or are filled by the API and returned (``output``). + +The ``param`` sub-elenents of ``<typed-parameters>`` then list each supported +parameter name and the type. Following ``type`` values are supported along with +the corresponding `virTypedParameterType <html/libvirt-libvirt-common.html#virTypedParameterType>`__ +value: + + * ``int`` -> ``VIR_TYPED_PARAM_INT`` + * ``uint`` -> ``VIR_TYPED_PARAM_UINT`` + * ``llong`` -> ``VIR_TYPED_PARAM_LLONG`` + * ``ullong`` -> ``VIR_TYPED_PARAM_ULLONG`` + * ``double`` -> ``VIR_TYPED_PARAM_DOUBLE`` + * ``boolean`` -> ``VIR_TYPED_PARAM_BOOLEAN`` + * ``string`` -> ``VIR_TYPED_PARAM_STRING`` + +For typed parameter values which can be specified multiple times with given API +the ``param`` element has a ``multiple='yes'`` attribute. diff --git a/docs/meson.build b/docs/meson.build index 0e74f9c4bf..485356e08b 100644 --- a/docs/meson.build +++ b/docs/meson.build @@ -67,6 +67,7 @@ docs_rst_files = [ 'formatcheckpoint', 'formatdomain', 'formatdomaincaps', + 'formatintrospection', 'formatnetwork', 'formatnetworkport', 'formatnode', -- 2.54.0
participants (1)
-
Peter Krempa