From nobody Tue Nov 26 13:25:40 2024 Delivered-To: importer@patchew.org Received-SPF: pass (zoho.com: domain of redhat.com designates 207.211.31.120 as permitted sender) client-ip=207.211.31.120; envelope-from=libvir-list-bounces@redhat.com; helo=us-smtp-1.mimecast.com; Authentication-Results: mx.zohomail.com; dkim=pass; spf=pass (zoho.com: domain of redhat.com designates 207.211.31.120 as permitted sender) smtp.mailfrom=libvir-list-bounces@redhat.com; dmarc=pass(p=none dis=none) header.from=redhat.com ARC-Seal: i=1; a=rsa-sha256; t=1573483569; cv=none; d=zoho.com; s=zohoarc; b=dZJam1Hs995CWlJCguEH3UKFhNMB3dVEYMuawFRLsAShrOqFw3IFAVa1s3ZEpNvwfK6Y8k2OWotNs69dlscOyZquTlfQl3sw6mEUv6Cqjxpn+2FxV52v0FjU4YECVnIeA4VIv3Df9SwkEWwj+0eo9GMBY3f9hqzSlBtt/13GBkU= ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=zoho.com; s=zohoarc; t=1573483569; h=Content-Type:Content-Transfer-Encoding:Date:From:In-Reply-To:List-Subscribe:List-Post:List-Id:List-Archive:List-Help:List-Unsubscribe:MIME-Version:Message-ID:References:Sender:Subject:To; bh=PYdiNluJiVDzbNjkZFZ8O9kBexoHJd+4N7yesZbqSos=; b=IykwQKGUDJBG948/aM58Ye9Rdh7zVBVW5J+/jOgqq5hbuikkDQrk4djTPjw0eT60krVOE4pSlw/E5aYlxszsSgiV9MY5PxjxlDniMF58g5Wm5nd8/Cm9Iip2DhsMSh8rSTD9MivZxnpGvL9rvUlCnZ5K/I0+gyH5bYf94gscEw8= ARC-Authentication-Results: i=1; mx.zoho.com; dkim=pass; spf=pass (zoho.com: domain of redhat.com designates 207.211.31.120 as permitted sender) smtp.mailfrom=libvir-list-bounces@redhat.com; dmarc=pass header.from= (p=none dis=none) header.from= Return-Path: Received: from us-smtp-1.mimecast.com (us-smtp-delivery-1.mimecast.com [207.211.31.120]) by mx.zohomail.com with SMTPS id 1573483569209168.7416225310327; Mon, 11 Nov 2019 06:46:09 -0800 (PST) Received: from mimecast-mx01.redhat.com (mimecast-mx01.redhat.com [209.132.183.4]) (Using TLS) by relay.mimecast.com with ESMTP id us-mta-148-e3UlWQHVOLuVD6FsJCArzA-1; Mon, 11 Nov 2019 09:45:21 -0500 Received: from smtp.corp.redhat.com (int-mx06.intmail.prod.int.phx2.redhat.com [10.5.11.16]) (using TLSv1.2 with cipher AECDH-AES256-SHA (256/256 bits)) (No client certificate requested) by mimecast-mx01.redhat.com (Postfix) with ESMTPS id 289AD85EE8B; Mon, 11 Nov 2019 14:45:15 +0000 (UTC) Received: from colo-mx.corp.redhat.com (colo-mx02.intmail.prod.int.phx2.redhat.com [10.5.11.21]) by smtp.corp.redhat.com (Postfix) with ESMTPS id EE75C4B4; Mon, 11 Nov 2019 14:45:14 +0000 (UTC) Received: from lists01.pubmisc.prod.ext.phx2.redhat.com (lists01.pubmisc.prod.ext.phx2.redhat.com [10.5.19.33]) by colo-mx.corp.redhat.com (Postfix) with ESMTP id 912E24E57C; Mon, 11 Nov 2019 14:45:14 +0000 (UTC) Received: from smtp.corp.redhat.com (int-mx04.intmail.prod.int.phx2.redhat.com [10.5.11.14]) by lists01.pubmisc.prod.ext.phx2.redhat.com (8.13.8/8.13.8) with ESMTP id xABEj0DK020443 for ; Mon, 11 Nov 2019 09:45:00 -0500 Received: by smtp.corp.redhat.com (Postfix) id 3BF3C5DD73; Mon, 11 Nov 2019 14:45:00 +0000 (UTC) Received: from catbus.gsslab.fab.redhat.com (dhcp-32.gsslab.fab.redhat.com [10.33.9.32]) by smtp.corp.redhat.com (Postfix) with ESMTP id 9C4595D9C9; Mon, 11 Nov 2019 14:44:59 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; s=mimecast20190719; t=1573483566; h=from:from:sender:sender:reply-to:subject:subject:date:date: message-id:message-id:to:to:cc:mime-version:mime-version: content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references:list-id:list-help: list-unsubscribe:list-subscribe:list-post; bh=PYdiNluJiVDzbNjkZFZ8O9kBexoHJd+4N7yesZbqSos=; b=HYYn4Qr+iOtYrjMgykrnVm/fi9Zts3vsQ8an/yNqXwRebyuVcY54IjgkitcHYR86wl+T1m h1uJuq7nAX28iit0zXGjxVLIy/vsHnkSiRnhl6KGsDxuWPeMWAWPwS9pJ4fFBfayaHTHbh 5YnsTx5TYWOtqFcNMgFcrHe3m5RJpc0= From: =?UTF-8?q?Daniel=20P=2E=20Berrang=C3=A9?= To: libvir-list@redhat.com Date: Mon, 11 Nov 2019 14:38:15 +0000 Message-Id: <20191111143826.16050-13-berrange@redhat.com> In-Reply-To: <20191111143826.16050-1-berrange@redhat.com> References: <20191111143826.16050-1-berrange@redhat.com> MIME-Version: 1.0 X-Scanned-By: MIMEDefang 2.79 on 10.5.11.14 X-loop: libvir-list@redhat.com Subject: [libvirt] [PATCH v5 12/23] src: rewrite ACL rule checker in Python X-BeenThere: libvir-list@redhat.com X-Mailman-Version: 2.1.12 Precedence: junk List-Id: Development discussions about the libvirt library & tools List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Sender: libvir-list-bounces@redhat.com Errors-To: libvir-list-bounces@redhat.com X-Scanned-By: MIMEDefang 2.79 on 10.5.11.16 X-MC-Unique: e3UlWQHVOLuVD6FsJCArzA-1 X-Mimecast-Spam-Score: 0 Content-Transfer-Encoding: quoted-printable X-ZohoMail-DKIM: pass (identity @redhat.com) Content-Type: text/plain; charset="utf-8" As part of an goal to eliminate Perl from libvirt build tools, rewrite the check-aclrules.pl tool in Python. This was a straight conversion, manually going line-by-line to change the syntax from Perl to Python. Thus the overall structure of the file and approach is the same. Signed-off-by: Daniel P. Berrang=C3=A9 Reviewed-by: J=C3=A1n Tomko Tested-by: Cole Robinson --- Makefile.am | 1 + scripts/check-aclrules.py | 263 ++++++++++++++++++++++++++++++++++++++ src/Makefile.am | 4 +- src/check-aclrules.pl | 252 ------------------------------------ 4 files changed, 265 insertions(+), 255 deletions(-) create mode 100755 scripts/check-aclrules.py delete mode 100755 src/check-aclrules.pl diff --git a/Makefile.am b/Makefile.am index 407a664626..f28b07d814 100644 --- a/Makefile.am +++ b/Makefile.am @@ -47,6 +47,7 @@ EXTRA_DIST =3D \ AUTHORS.in \ scripts/augeas-gentest.py \ scripts/check-aclperms.py \ + scripts/check-aclrules.py \ scripts/check-drivername.py \ scripts/check-driverimpls.py \ scripts/check-spacing.py \ diff --git a/scripts/check-aclrules.py b/scripts/check-aclrules.py new file mode 100755 index 0000000000..3fab126a4a --- /dev/null +++ b/scripts/check-aclrules.py @@ -0,0 +1,263 @@ +#!/usr/bin/env python +# +# Copyright (C) 2013-2019 Red Hat, Inc. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see +# . +# +# This script validates that the driver implementation of any +# public APIs contain ACL checks. +# +# As the script reads each source file, it attempts to identify +# top level function names. +# +# When reading the body of the functions, it looks for anything +# that looks like an API called named XXXEnsureACL. It will +# validate that the XXX prefix matches the name of the function +# it occurs in. +# +# When it later finds the virDriverPtr table, for each entry +# point listed, it will validate if there was a previously +# detected EnsureACL call recorded. +# + +from __future__ import print_function + +import re +import sys + +whitelist =3D { + "connectClose": True, + "connectIsEncrypted": True, + "connectIsSecure": True, + "connectIsAlive": True, + "networkOpen": True, + "networkClose": True, + "nwfilterOpen": True, + "nwfilterClose": True, + "secretOpen": True, + "secretClose": True, + "storageOpen": True, + "storageClose": True, + "interfaceOpen": True, + "interfaceClose": True, + "connectURIProbe": True, + "localOnly": True, + "domainQemuAttach": True, +} + +# XXX this vzDomainMigrateConfirm3Params looks +# bogus - determine why it doesn't have a valid +# ACL check. +implwhitelist =3D { + "vzDomainMigrateConfirm3Params": True, +} + +lastfile =3D None + + +def fixup_name(name): + name.replace("Nwfilter", "NWFilter") + name.replace("Pm", "PM") + name.replace("Scsi", "SCSI") + if name.endswith("Xml"): + name =3D name[:-3] + "XML" + elif name.endswith("Uri"): + name =3D name[:-3] + "URI" + elif name.endswith("Uuid"): + name =3D name[:-4] + "UUID" + elif name.endswith("Id"): + name =3D name[:-2] + "ID" + elif name.endswith("Mac"): + name =3D name[:-3] + "MAC" + elif name.endswith("Cpu"): + name =3D name[:-3] + "MAC" + elif name.endswith("Os"): + name =3D name[:-2] + "OS" + elif name.endswith("Nmi"): + name =3D name[:-3] + "NMI" + elif name.endswith("Fstrim"): + name =3D name[:-6] + "FSTrim" + elif name.endswith("Wwn"): + name =3D name[:-3] + "WWN" + + return name + + +def name_to_ProcName(name): + elems =3D [] + if name.find("_") !=3D -1 or name.lower() in ["open", "close"]: + elems =3D [n.lower().capitalize() for n in name.split("_")] + else: + elems =3D [name] + + elems =3D [fixup_name(n) for n in elems] + procname =3D "".join(elems) + + return procname[0:1].lower() + procname[1:] + + +proto =3D sys.argv[1] + +filteredmap =3D {} +with open(proto, "r") as fh: + incomment =3D False + filtered =3D False + + for line in fh: + if line.find("/**") !=3D -1: + incomment =3D True + filtered =3D False + elif incomment: + if line.find("* @aclfilter") !=3D -1: + filtered =3D True + elif filtered: + m =3D re.search(r'''REMOTE_PROC_(.*)\s+=3D\s*\d+''', line) + if m is not None: + api =3D name_to_ProcName(m.group(1)) + # Event filtering is handled in daemon/remote.c + # instead of drivers + if line.find("_EVENT_REGISTER") =3D=3D -1: + filteredmap[api] =3D True + incomment =3D False + + +def process_file(filename): + brace =3D 0 + maybefunc =3D None + intable =3D False + table =3D None + + acls =3D {} + aclfilters =3D {} + errs =3D False + with open(filename, "r") as fh: + lineno =3D 0 + for line in fh: + lineno =3D lineno + 1 + if brace =3D=3D 0: + # Looks for anything which appears to be a function + # body name. Doesn't matter if we pick up bogus stuff + # here, as long as we don't miss valid stuff + m =3D re.search(r'''\b(\w+)\(''', line) + if m is not None: + maybefunc =3D m.group(1) + elif brace > 0: + ensureacl =3D re.search(r'''(\w+)EnsureACL''', line) + checkacl =3D re.search(r'''(\w+)CheckACL''', line) + stub =3D re.search(r'''\b(\w+)\(''', line) + if ensureacl is not None: + # Record the fact that maybefunc contains an + # ACL call, and make sure it is the right call! + func =3D ensureacl.group(1) + if func.startswith("vir"): + func =3D func[3:] + + if maybefunc is None: + print("%s:%d Unexpected check '%s' outside functio= n" % + (filename, lineno, func), file=3Dsys.stderr) + errs =3D True + else: + if not maybefunc.lower().endswith(func.lower()): + print("%s:%d Mismatch check 'vir%sEnsureACL'" + + "for function '%s'" % + (filename, lineno, func, maybefunc), + file=3Dsys.stderr) + errs =3D True + acls[maybefunc] =3D True + elif checkacl: + # Record the fact that maybefunc contains an + # ACL filter call, and make sure it is the right call! + func =3D checkacl.group(1) + if func.startswith("vir"): + func =3D func[3:] + + if maybefunc is None: + print("%s:%d Unexpected check '%s' outside functio= n" % + (filename, lineno, func), file=3Dsys.stderr) + errs =3D True + else: + if not maybefunc.lower().endswith(func.lower()): + print("%s:%d Mismatch check 'vir%sEnsureACL' "= + + "for function '%s'" % + (filename, lineno, func, maybefunc), + file=3Dsys.stderr) + errs =3D True + aclfilters[maybefunc] =3D True + elif stub: + # Handles case where we replaced an API with a new + # one which adds new parameters, and we're left with + # a simple stub calling the new API. + callfunc =3D stub.group(1) + if callfunc in acls: + acls[maybefunc] =3D True + + if callfunc in aclfilters: + aclfilters[maybefunc] =3D True + + # Pass the vir*DriverPtr tables and make sure that + # every func listed there, has an impl which calls + # an ACL function + if intable: + assign =3D re.search(r'''\.(\w+)\s*=3D\s*(\w+),?''', line) + if line.find("}") !=3D -1: + intable =3D False + table =3D None + elif assign is not None: + api =3D assign.group(1) + impl =3D assign.group(2) + + if (impl !=3D "NULL" and + api not in ["no", "name"] and + table !=3D "virStateDriver"): + if (impl not in acls and + api not in whitelist and + impl not in implwhitelist): + print("%s:%d Missing ACL check in " + + "function '%s' for '%s'" % + (filename, lineno, impl, api), + file=3Dsys.stderr) + errs =3D True + + if api in filteredmap and impl not in aclfilters: + print("%s:%d Missing ACL filter in " + + "function '%s' for '%s'" % + (filename, lineno, impl, api), + file=3Dsys.stderr) + errs =3D True + else: + m =3D re.search(r'''^(?:static\s+)?(vir(?:\w+)?Driver)\s+'= '', + line) + if m is not None: + name =3D m.group(1) + if name not in ["virNWFilterCallbackDriver", + "virNWFilterTechDriver", + "virDomainConfNWFilterDriver"]: + intable =3D True + table =3D name + + if line.find("{") !=3D -1: + brace =3D brace + 1 + if line.find("}") !=3D -1: + brace =3D brace - 1 + + return errs + + +status =3D 0 +for filename in sys.argv[2:]: + if process_file(filename): + status =3D 1 + +sys.exit(status) diff --git a/src/Makefile.am b/src/Makefile.am index 62f1d55402..bb63e2486c 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -357,7 +357,7 @@ check-driverimpls: $(filter-out /%,$(DRIVER_SOURCE_FILES)))) =20 check-aclrules: - $(AM_V_GEN)$(PERL) $(srcdir)/check-aclrules.pl \ + $(AM_V_GEN)$(RUNUTF8) $(PYTHON) $(top_srcdir)/scripts/check-aclrules.py \ $(REMOTE_PROTOCOL) \ $(addprefix $(srcdir)/,$(filter-out /%,$(STATEFUL_DRIVER_SOURCE_FILES))) =20 @@ -366,8 +366,6 @@ check-aclperms: $(srcdir)/access/viraccessperm.h \ $(srcdir)/access/viraccessperm.c =20 -EXTRA_DIST +=3D check-aclrules.pl - check-local: check-protocol check-symfile check-symsorting \ check-drivername check-driverimpls check-aclrules \ check-aclperms check-admin diff --git a/src/check-aclrules.pl b/src/check-aclrules.pl deleted file mode 100755 index 0d4cac17ca..0000000000 --- a/src/check-aclrules.pl +++ /dev/null @@ -1,252 +0,0 @@ -#!/usr/bin/env perl -# -# Copyright (C) 2013-2014 Red Hat, Inc. -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library. If not, see -# . -# -# This script validates that the driver implementation of any -# public APIs contain ACL checks. -# -# As the script reads each source file, it attempts to identify -# top level function names. -# -# When reading the body of the functions, it looks for anything -# that looks like an API called named XXXEnsureACL. It will -# validate that the XXX prefix matches the name of the function -# it occurs in. -# -# When it later finds the virDriverPtr table, for each entry -# point listed, it will validate if there was a previously -# detected EnsureACL call recorded. -# -use strict; -use warnings; - -my $status =3D 0; - -my $brace =3D 0; -my $maybefunc; -my $intable =3D 0; -my $table; - -my %acls; -my %aclfilters; - -my %whitelist =3D ( - "connectClose" =3D> 1, - "connectIsEncrypted" =3D> 1, - "connectIsSecure" =3D> 1, - "connectIsAlive" =3D> 1, - "networkOpen" =3D> 1, - "networkClose" =3D> 1, - "nwfilterOpen" =3D> 1, - "nwfilterClose" =3D> 1, - "secretOpen" =3D> 1, - "secretClose" =3D> 1, - "storageOpen" =3D> 1, - "storageClose" =3D> 1, - "interfaceOpen" =3D> 1, - "interfaceClose" =3D> 1, - "connectURIProbe" =3D> 1, - "localOnly" =3D> 1, - "domainQemuAttach" =3D> 1, - ); - -# XXX this vzDomainMigrateConfirm3Params looks -# bogus - determine why it doesn't have a valid -# ACL check. -my %implwhitelist =3D ( - "vzDomainMigrateConfirm3Params" =3D> 1, - ); - -my $lastfile; - -sub fixup_name { - my $name =3D shift; - - $name =3D~ s/Nwfilter/NWFilter/; - $name =3D~ s/Xml$/XML/; - $name =3D~ s/Uri$/URI/; - $name =3D~ s/Uuid$/UUID/; - $name =3D~ s/Id$/ID/; - $name =3D~ s/Mac$/MAC/; - $name =3D~ s/Cpu$/CPU/; - $name =3D~ s/Os$/OS/; - $name =3D~ s/Nmi$/NMI/; - $name =3D~ s/Pm/PM/; - $name =3D~ s/Fstrim$/FSTrim/; - $name =3D~ s/Scsi/SCSI/; - $name =3D~ s/Wwn$/WWN/; - - return $name; -} - -sub name_to_ProcName { - my $name =3D shift; - - my @elems; - if ($name =3D~ /_/ || (lc $name) eq "open" || (lc $name) eq "close") { - @elems =3D split /_/, $name; - @elems =3D map lc, @elems; - @elems =3D map ucfirst, @elems; - } else { - @elems =3D $name; - } - @elems =3D map { fixup_name($_) } @elems; - my $procname =3D join "", @elems; - - $procname =3D~ s/^([A-Z])/lc $1/e; - - return $procname; -} - - -my $proto =3D shift @ARGV; - -open PROTO, "<$proto" or die "cannot read $proto"; - -my %filtered; -my $incomment =3D 0; -my $filtered =3D 0; -while () { - if (m,/\*\*,) { - $incomment =3D 1; - $filtered =3D 0; - } elsif ($incomment) { - if (m,\*\s\@aclfilter,) { - $filtered =3D 1; - } elsif ($filtered && - m,REMOTE_PROC_(.*)\s+=3D\s*\d+,) { - my $api =3D name_to_ProcName($1); - # Event filtering is handled in daemon/remote.c instead of dri= vers - if (! m,_EVENT_REGISTER,) { - $filtered{$api} =3D 1; - } - $incomment =3D 0; - } - } -} - -close PROTO; - -while (<>) { - if (!defined $lastfile || - $lastfile ne $ARGV) { - %acls =3D (); - $brace =3D 0; - $maybefunc =3D undef; - $lastfile =3D $ARGV; - } - if ($brace =3D=3D 0) { - # Looks for anything which appears to be a function - # body name. Doesn't matter if we pick up bogus stuff - # here, as long as we don't miss valid stuff - if (m,\b(\w+)\(,) { - $maybefunc =3D $1; - } - } elsif ($brace > 0) { - if (m,(\w+)EnsureACL,) { - # Record the fact that maybefunc contains an - # ACL call, and make sure it is the right call! - my $func =3D $1; - $func =3D~ s/^vir//; - if (!defined $maybefunc) { - print "$ARGV:$. Unexpected check '$func' outside function\= n"; - $status =3D 1; - } else { - unless ($maybefunc =3D~ /$func$/i) { - print "$ARGV:$. Mismatch check 'vir${func}EnsureACL' f= or function '$maybefunc'\n"; - $status =3D 1; - } - } - $acls{$maybefunc} =3D 1; - } elsif (m,(\w+)CheckACL,) { - # Record the fact that maybefunc contains an - # ACL filter call, and make sure it is the right call! - my $func =3D $1; - $func =3D~ s/^vir//; - if (!defined $maybefunc) { - print "$ARGV:$. Unexpected check '$func' outside function\= n"; - $status =3D 1; - } else { - unless ($maybefunc =3D~ /$func$/i) { - print "$ARGV:$. Mismatch check 'vir${func}CheckACL' fo= r function '$maybefunc'\n"; - $status =3D 1; - } - } - $aclfilters{$maybefunc} =3D 1; - } elsif (m,\b(\w+)\(,) { - # Handles case where we replaced an API with a new - # one which adds new parameters, and we're left with - # a simple stub calling the new API. - my $callfunc =3D $1; - if (exists $acls{$callfunc}) { - $acls{$maybefunc} =3D 1; - } - if (exists $aclfilters{$callfunc}) { - $aclfilters{$maybefunc} =3D 1; - } - } - } - - # Pass the vir*DriverPtr tables and make sure that - # every func listed there, has an impl which calls - # an ACL function - if ($intable) { - if (/\}/) { - $intable =3D 0; - $table =3D undef; - } elsif (/\.(\w+)\s*=3D\s*(\w+),?/) { - my $api =3D $1; - my $impl =3D $2; - - next if $impl eq "NULL"; - - if ($api ne "no" && - $api ne "name" && - $table ne "virStateDriver" && - !exists $acls{$impl} && - !exists $whitelist{$api} && - !exists $implwhitelist{$impl}) { - print "$ARGV:$. Missing ACL check in function '$impl' for = '$api'\n"; - $status =3D 1; - } - - if (exists $filtered{$api} && - !exists $aclfilters{$impl}) { - print "$ARGV:$. Missing ACL filter in function '$impl' for= '$api'\n"; - $status =3D 1; - } - } - } elsif (/^(?:static\s+)?(vir(?:\w+)?Driver)\s+/) { - if ($1 ne "virNWFilterCallbackDriver" && - $1 ne "virNWFilterTechDriver" && - $1 ne "virDomainConfNWFilterDriver") { - $intable =3D 1; - $table =3D $1; - } - } - - - my $count; - $count =3D s/{//g; - $brace +=3D $count; - $count =3D s/}//g; - $brace -=3D $count; -} continue { - close ARGV if eof; -} - -exit $status; --=20 2.21.0 -- libvir-list mailing list libvir-list@redhat.com https://www.redhat.com/mailman/listinfo/libvir-list