From nobody Wed Jan 15 13:08:52 2025 Delivered-To: importer@patchew.org Received-SPF: none (zohomail.com: 8.43.85.245 is neither permitted nor denied by domain of lists.libvirt.org) client-ip=8.43.85.245; envelope-from=devel-bounces@lists.libvirt.org; helo=lists.libvirt.org; Authentication-Results: mx.zohomail.com; spf=none (zohomail.com: 8.43.85.245 is neither permitted nor denied by domain of lists.libvirt.org) smtp.mailfrom=devel-bounces@lists.libvirt.org; dmarc=fail(p=none dis=none) header.from=redhat.com Return-Path: Received: from lists.libvirt.org (lists.libvirt.org [8.43.85.245]) by mx.zohomail.com with SMTPS id 1705422333809214.97421610441847; Tue, 16 Jan 2024 08:25:33 -0800 (PST) Received: by lists.libvirt.org (Postfix, from userid 996) id A97791E15; Tue, 16 Jan 2024 11:25:32 -0500 (EST) Received: from lists.libvirt.org.85.43.8.in-addr.arpa (localhost [IPv6:::1]) by lists.libvirt.org (Postfix) with ESMTP id 9F99E1E42; Tue, 16 Jan 2024 11:13:35 -0500 (EST) Received: by lists.libvirt.org (Postfix, from userid 996) id 4A4261D27; Tue, 16 Jan 2024 11:12:59 -0500 (EST) Received: from us-smtp-delivery-124.mimecast.com (us-smtp-delivery-124.mimecast.com [170.10.129.124]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by lists.libvirt.org (Postfix) with ESMTPS id 8B93A1C7D for ; Tue, 16 Jan 2024 11:12:56 -0500 (EST) Received: from mimecast-mx02.redhat.com (mimecast-mx02.redhat.com [66.187.233.88]) by relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384) id us-mta-47-kfRtMyULMcqMNTEfI50pjw-1; Tue, 16 Jan 2024 11:12:54 -0500 Received: from smtp.corp.redhat.com (int-mx07.intmail.prod.int.rdu2.redhat.com [10.11.54.7]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by mimecast-mx02.redhat.com (Postfix) with ESMTPS id 5C5C4185A781 for ; Tue, 16 Jan 2024 16:12:54 +0000 (UTC) Received: from speedmetal.lan (unknown [10.45.242.25]) by smtp.corp.redhat.com (Postfix) with ESMTP id C82C21C060AF for ; Tue, 16 Jan 2024 16:12:53 +0000 (UTC) X-Spam-Checker-Version: SpamAssassin 3.4.4 (2020-01-24) on lists.libvirt.org X-Spam-Level: X-Spam-Status: No, score=-0.8 required=5.0 tests=HEADER_FROM_DIFFERENT_DOMAINS, MAILING_LIST_MULTI,RCVD_IN_DNSWL_NONE,RCVD_IN_MSPIKE_H4, RCVD_IN_MSPIKE_WL,SPF_HELO_NONE,T_SCC_BODY_TEXT_LINE autolearn=unavailable autolearn_force=no version=3.4.4 X-MC-Unique: kfRtMyULMcqMNTEfI50pjw-1 From: Peter Krempa To: devel@lists.libvirt.org Subject: [PATCH 07/10] qemu-replies-tool: Add validation of known fields in 'query-qmp-schema' Date: Tue, 16 Jan 2024 17:12:41 +0100 Message-ID: In-Reply-To: References: MIME-Version: 1.0 X-Scanned-By: MIMEDefang 3.4.1 on 10.11.54.7 X-Mimecast-Spam-Score: 0 X-Mimecast-Originator: redhat.com Message-ID-Hash: BKLTQWOSRNEO25CBG7AT367JMC7TK5EO X-Message-ID-Hash: BKLTQWOSRNEO25CBG7AT367JMC7TK5EO X-MailFrom: pkrempa@redhat.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; emergency; loop; banned-address; member-moderation; header-match-config-1; header-match-config-2; header-match-config-3; header-match-devel.lists.libvirt.org-0; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; suspicious-header X-Mailman-Version: 3.2.2 Precedence: list List-Id: Development discussions about the libvirt library & tools Archived-At: List-Archive: List-Help: List-Post: List-Subscribe: List-Unsubscribe: Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable X-ZM-MESSAGEID: 1705422335920100001 If the schema itself is extended in qemu we need to have a notification to add appropriate handling to ensure that we have full coverage of all fields. Add validation that only fields that libvirt currently knows about are present in the schema. Signed-off-by: Peter Krempa --- scripts/qemu-replies-tool.py | 145 +++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) diff --git a/scripts/qemu-replies-tool.py b/scripts/qemu-replies-tool.py index 57bb26a356..c5dee9a66a 100755 --- a/scripts/qemu-replies-tool.py +++ b/scripts/qemu-replies-tool.py @@ -15,6 +15,10 @@ class qrtException(Exception): pass +class qmpSchemaException(Exception): + pass + + # Load the 'replies' file into a list of (command, reply) tuples of parsed= JSON def qemu_replies_load(filename): conv =3D [] @@ -153,17 +157,157 @@ def modify_replies(conv): conv.insert(idx, (cmd, reply_unsupp)) +# Validates that 'entry' (an member of the QMP schema): +# - checks that it's a Dict (imported from a JSON object) +# - checks that all 'mandatory' fields are present and their types match +# - checks the types of all 'optional' fields +# - checks that no unknown fields are present +def validate_qmp_schema_check_keys(entry, mandatory, optional): + keys =3D set(entry.keys()) + + for k, t in mandatory: + try: + keys.remove(k) + except KeyError: + raise qmpSchemaException("missing mandatory key '%s' in schema= '%s'" % (k, entry)) + + if not isinstance(entry[k], t): + raise qmpSchemaException("key '%s' is not of the expected type= '%s' in schema '%s'" % (k, t, entry)) + + for k, t in optional: + if k in keys: + keys.discard(k) + + if t is not None: + if not isinstance(entry[k], t): + raise qmpSchemaException("key '%s' is not of the expec= ted type '%s' in schema '%s'" % (k, t, entry)) + + if len(keys) > 0: + raise qmpSchemaException("unhandled keys '%s' in schema '%s'" % ('= ,'.join(list(keys)), entry)) + + +# Validates the optional 'features' and that they consist only of strings +def validate_qmp_schema_check_features_list(entry): + for f in entry.get('features', []): + if not isinstance(f, str): + raise qmpSchemaException("broken 'features' list in schema ent= ry '%s'" % entry) + + +# Validate that the passed schema has only members supported by this scrip= t and +# by the libvirt internals. This is useful to stay up to date with any cha= nges +# to the schema. +def validate_qmp_schema(schemalist): + for entry in schemalist: + if not isinstance(entry, dict): + raise qmpSchemaException("schema entry '%s' is not a JSON Obje= ct (dict)" % (entry)) + + match entry.get('meta-type', None): + case 'command': + validate_qmp_schema_check_keys(entry, + mandatory=3D[('name', str), + ('meta-type', st= r), + ('arg-type', str= ), + ('ret-type', str= )], + optional=3D[('features', li= st), + ('allow-oob', boo= l)]) + + validate_qmp_schema_check_features_list(entry) + + case 'event': + validate_qmp_schema_check_keys(entry, + mandatory=3D[('name', str), + ('meta-type', st= r), + ('arg-type', str= )], + optional=3D[('features', li= st)]) + + validate_qmp_schema_check_features_list(entry) + + case 'object': + validate_qmp_schema_check_keys(entry, + mandatory=3D[('name', str), + ('meta-type', st= r), + ('members', list= )], + optional=3D[('tag', str), + ('variants', list= ), + ('features', list= )]) + + validate_qmp_schema_check_features_list(entry) + + for m in entry.get('members', []): + validate_qmp_schema_check_keys(m, + mandatory=3D[('name', s= tr), + ('type', str= )], + optional=3D[('default',= None), + ('features', = list)]) + validate_qmp_schema_check_features_list(m) + + for m in entry.get('variants', []): + validate_qmp_schema_check_keys(m, + mandatory=3D[('case', s= tr), + ('type', str= )], + optional=3D[]) + + case 'array': + validate_qmp_schema_check_keys(entry, + mandatory=3D[('name', str), + ('meta-type', st= r), + ('element-type',= str)], + optional=3D[]) + + case 'enum': + validate_qmp_schema_check_keys(entry, + mandatory=3D[('name', str), + ('meta-type', st= r)], + optional=3D[('members', lis= t), + ('values', list)]) + + for m in entry.get('members', []): + validate_qmp_schema_check_keys(m, + mandatory=3D[('name', s= tr)], + optional=3D[('features'= , list)]) + validate_qmp_schema_check_features_list(m) + + case 'alternate': + validate_qmp_schema_check_keys(entry, + mandatory=3D[('name', str), + ('meta-type', st= r), + ('members', list= )], + optional=3D[]) + + for m in entry.get('members', []): + validate_qmp_schema_check_keys(m, + mandatory=3D[('type', s= tr)], + optional=3D[]) + + case 'builtin': + validate_qmp_schema_check_keys(entry, + mandatory=3D[('name', str), + ('meta-type', st= r), + ('json-type', st= r)], + optional=3D[]) + + case _: + raise qmpSchemaException("unknown or missing 'meta-type' i= n schema entry '%s'" % entry) + + def process_one(filename, args): try: conv =3D qemu_replies_load(filename) modify_replies(conv) + for (cmd, rep) in conv: + if cmd['execute'] =3D=3D 'query-qmp-schema': + validate_qmp_schema(rep['return']) + qemu_replies_compare_or_replace(filename, conv, args.regenerate) except qrtException as e: print("'%s' ... FAIL\n%s" % (filename, e)) return False + except qmpSchemaException as qe: + print("'%s' ... FAIL\nqmp schema error: %s" % (filename, qe)) + return False print("'%s' ... OK" % filename) return True @@ -181,6 +325,7 @@ The default mode is validation which checks the followi= ng: - each command has a reply and both are valid JSON - numbering of the 'id' field is as expected - the input file has the expected JSON formatting + - the QMP schema from qemu is fully covered by libvirt's code The tool can be also used to programmaticaly modify the '.replies' file by editting the 'modify_replies' method directly in the source, or for --=20 2.43.0 _______________________________________________ Devel mailing list -- devel@lists.libvirt.org To unsubscribe send an email to devel-leave@lists.libvirt.org