[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

Bug#1013755: bullseye-pu: package ganeti/3.0.2-1~deb11u1



Package: release.debian.org
Severity: normal
Tags: bullseye
User: release.debian.org@packages.debian.org
Usertags: pu

Dear SRMs,

I would like to update Ganeti to the current upstream bugfix version
(3.0.2) - including all Debian packaging fixes currently in unstable - 
and I seek your approval.

3.0.2 was released a while back[1] as a bugfix-only release. Due to my 
involvement upstream, I had full oversight of the release process and I 
can confirm it solves important issues, the vast majority of which affect
Bullseye, while it does not introduce any breaking changes in behavior.  
Note that every commit since v3.0 has been tested against Debian Stable 
and Testing upstream using an automated CI/CD pipeline. I believe 
bumping to 3.0.2 is much safer and cleaner than cherry-picking at
least a dozen of commits as patches on top of 3.0.1.

Apart from upstream fixes, this p-u also includes fixes for Debian bugs #993559
and #1000040 affecting Debian packaging, as well as the removal of an unnecessary
dependency on bridge-utils. Note that I might revert the latter for the stable
update to avoid breaking any custom scripts (e.g. hooks) that still rely on
brctl mid-release.

I'm attaching a full source debdiff against 3.0.1-2. The following 
information is for ease of review, please let me know if there is any 
additional information I can provide.

[1] https://github.com/ganeti/ganeti/releases/tag/v3.0.2

Cheers,
Apollon

Annotated upstream commit list since 3.0.1
------------------------------------------
$ git log --oneline --no-merges v3.0.1..v3.0.2
6f97ee783 Prepare for the 3.0.2 release
  Version bump & documentation changes

4e35c0093 Fix lvcreate for newer lvm versions (#1586)
  Already fixed in Bullseye (#982960), patch dropped in this p-u.

7b89f42fd KVM: handle asynchronous events during QMP connect
	Reliability fix, issue present in Bullseye. Documented as affecting Bullseye
  in https://github.com/ganeti/ganeti/issues/1649

2d04d0190 Fix NIC hotplug with `vhost_net=True`
	Reliability fix, issue present in Bullseye, see
  https://github.com/ganeti/ganeti/issues/1651

f1e6d7ee1 relax VLAN check with VETHs
  Slight behavior change, backwards compatible

9e7210a0b doc: Remove duplicate index entry.
  FTBFS, fixed post-bullseye in our 3.0.1-3 (#997053)

c5bb09744 work around dynamic auto-ro for live migration
  Compatibility fix for QEMU >=4.0, affects bullseye

06a043ad8 KVM: use same code to generate aio/cache parameters for KVM start and hotplugging
59994617c KVM hypervisor: only accept valid combinations of disk_aio and disk_cache
cb1810cf9 Refactor: Move KVM parameter check/validation code to separate methods
	Slight code refactoring to properly sanitize disk parameters using the same
  rules when the instance starts up and when disks are hotplugged. Fixes disk
  hotplugging under certain scenarios, see https://github.com/ganeti/ganeti/issues/1645.
  Note that these commits are responsible for the majority of the diffstat in this p-u.

c851eb73b make check-local happy: fix end-of-line whitespace
  Linting, irrelevant for Debian

543c9e243 allow Ganeti project copyright and year 202x
  Documentation change

432bab2fa add `check-local` to the github CI
  CI, irrelevant for Debian

d512200d7 Qemu-Guest-Agent (QGA): use implicit PCI slot
	Fixes disk/NIC hotplugging when using the QEMU guest agent, see
  https://github.com/ganeti/ganeti/issues/1620. Affects Bullseye.

3951ba628 Do not load ROM file for NICs during hotplug when QEMU is running in chroot mode
  Fixes hotplugging issue, affects Bulsseye

185481da9 Warn users that changing DRBD parameters does not affect running instances
  CLI output change

c4a603b6e Fix live migration of xen instances (#1582)
  Included as patch in Bullseye, dropped in this p-u

d4e89d2f4 RAPI: Correctly return HTTP 400 on request parse error
  Leftover bug from the Python 3 conversion, affects Bullseye

2df1f2cd3 fix building docs on Debian Bullseye (#1602)
	FTBS, affects Bullseye, fixed in Debian in 3.0.1-3, patch dropped in this
  p-u.

d845e7d68 Adjust for Pyparsing 3.0.
  Bugfix, affects Bookworm but does not break Bullseye.

a5ad39397 Fix Byte vs String comparison
	Leftover bug from the Python 3 conversion, affects some CLI commands in
  Bullseye.

95125c644 Loosen Cabal version constraints.
  Releng
  
8f8e82933 Adjust for TupE type change in Template Haskell 2.16.
  GHC 8.10 compatibility using conditional code, noop under Bullseye.

f0189ae15 fix dictionary usage bug leftover from 2to3 migration
  Leftover bug from the Python 3 conversion

c562509fe Fix unsupported keymap include in >=qemu-4.0 (#1612)
  QEMU 4.0 compatibility fix, affects Bullseye
diff -Nru ganeti-3.0.1/autotools/check-header ganeti-3.0.2/autotools/check-header
--- ganeti-3.0.1/autotools/check-header	2021-02-03 10:24:16.000000000 +0200
+++ ganeti-3.0.2/autotools/check-header	2022-02-28 22:51:21.000000000 +0200
@@ -76,10 +76,11 @@
 
 
 _SHEBANG = re.compile(r"^#(?:|!(?:/usr/bin/python3?(?:| -u)|/bin/(?:|ba)sh))$")
-_COPYRIGHT_YEAR = r"20[01][0-9]"
-_COPYRIGHT = re.compile(r"# Copyright \(C\) (%s(?:, %s)*) Google Inc\.$" %
+_COPYRIGHT_YEAR = r"20[012][0-9]"
+_COPYRIGHT = re.compile(r"# Copyright \(C\) (%s(?:, %s)*) "
+                         "(Google Inc\.|the Ganeti project)$" %
                         (_COPYRIGHT_YEAR, _COPYRIGHT_YEAR))
-_COPYRIGHT_DESC = "Copyright (C) <year>[, <year> ...] Google Inc."
+_COPYRIGHT_DESC = "Copyright (C) <year>[, <year> ...] the Ganeti project"
 _AUTOGEN = "# This file is automatically generated, do not edit!"
 
 
diff -Nru ganeti-3.0.1/cabal/ganeti.template.cabal ganeti-3.0.2/cabal/ganeti.template.cabal
--- ganeti-3.0.1/cabal/ganeti.template.cabal	2021-02-03 10:24:16.000000000 +0200
+++ ganeti-3.0.2/cabal/ganeti.template.cabal	2022-02-28 22:51:21.000000000 +0200
@@ -64,7 +64,7 @@
     , utf8-string                   >= 0.3.7
 
     , attoparsec                    >= 0.10.1.1   && < 0.14
-    , base64-bytestring             >= 1.0.0.1    && < 1.1
+    , base64-bytestring             >= 1.0.0.1    && < 1.2
     , case-insensitive              >= 0.4.0.1    && < 1.3
     , curl                          >= 1.3.7      && < 1.4
     , hinotify                      >= 0.3.2      && < 0.5
@@ -95,7 +95,7 @@
   if flag(htest)
     build-depends:
         HUnit                         >= 1.2.4.2    && < 1.7
-      , QuickCheck                    >= 2.8        && < 2.14
+      , QuickCheck                    >= 2.8        && < 2.15
       , test-framework                >= 0.6        && < 0.9
       , test-framework-hunit          >= 0.2.7      && < 0.4
       , test-framework-quickcheck2    >= 0.2.12.1   && < 0.4
diff -Nru ganeti-3.0.1/configure.ac ganeti-3.0.2/configure.ac
--- ganeti-3.0.1/configure.ac	2021-02-03 10:24:16.000000000 +0200
+++ ganeti-3.0.2/configure.ac	2022-02-28 22:51:21.000000000 +0200
@@ -1,7 +1,7 @@
 # Configure script for Ganeti
 m4_define([gnt_version_major], [3])
 m4_define([gnt_version_minor], [0])
-m4_define([gnt_version_revision], [1])
+m4_define([gnt_version_revision], [2])
 m4_define([gnt_version_suffix], [])
 m4_define([gnt_version_full],
           m4_format([%d.%d.%d%s],
diff -Nru ganeti-3.0.1/debian/changelog ganeti-3.0.2/debian/changelog
--- ganeti-3.0.1/debian/changelog	2021-03-02 15:13:17.000000000 +0200
+++ ganeti-3.0.2/debian/changelog	2022-06-25 13:48:16.000000000 +0300
@@ -1,3 +1,36 @@
+ganeti (3.0.2-1~deb11u1) bullseye; urgency=medium
+
+  * Rebuild for bullseye.
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Sat, 25 Jun 2022 13:48:16 +0300
+
+ganeti (3.0.2-1) unstable; urgency=medium
+
+  * New upstream release (closes: #1006003, #993920)
+  * Drop patches merged upstream
+  * d/copyright: adjust years and upstream copyright info
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Sat, 12 Mar 2022 23:23:27 +0200
+
+ganeti (3.0.1-4) unstable; urgency=medium
+
+  * Drop ganeti-3.0's dependency on bridge-utils (Ganeti 3.0 uses iproute2).
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Mon, 31 Jan 2022 08:14:57 +0200
+
+ganeti (3.0.1-3) unstable; urgency=medium
+
+  * postrm: remove diversion only on package removal (Closes: #993559)
+    * Restore the diversion on postinst in case it was accidentally removed
+      due to #993559.
+  * Fix FTBFS by removing duplicate index entry.
+    Thanks to Marius Bakke (Closes: #997053)
+  * d/control: remove unnecessary B-D on libpcre3-dev (Closes: #1000040)
+  * Fix FTBFS with sphinx >= 2.1.
+    Thanks to Sascha Lucas
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Thu, 16 Dec 2021 14:22:41 +0200
+
 ganeti (3.0.1-2) unstable; urgency=medium
 
   * Ignore signatures while creating LVs (Closes: #982960)
diff -Nru ganeti-3.0.1/debian/control ganeti-3.0.2/debian/control
--- ganeti-3.0.1/debian/control	2021-03-02 14:42:23.000000000 +0200
+++ ganeti-3.0.2/debian/control	2022-06-25 13:48:16.000000000 +0300
@@ -36,7 +36,6 @@
  libghc-test-framework-hunit-dev,
  libghc-temporary-dev,
  libghc-old-time-dev,
- libpcre3-dev,
  libcurl4-openssl-dev,
  python3-simplejson,
  python3-pyparsing,
@@ -97,7 +96,6 @@
  lvm2,
  openssh-client,
  openssh-server,
- bridge-utils,
  iproute2,
  iputils-arping,
  libcap2-bin,
diff -Nru ganeti-3.0.1/debian/control.in ganeti-3.0.2/debian/control.in
--- ganeti-3.0.1/debian/control.in	2021-03-02 14:40:51.000000000 +0200
+++ ganeti-3.0.2/debian/control.in	2022-06-25 13:46:01.000000000 +0300
@@ -36,7 +36,6 @@
  libghc-test-framework-hunit-dev,
  libghc-temporary-dev,
  libghc-old-time-dev,
- libpcre3-dev,
  libcurl4-openssl-dev,
  python3-simplejson,
  python3-pyparsing,
@@ -97,7 +96,6 @@
  lvm2,
  openssh-client,
  openssh-server,
- bridge-utils,
  iproute2,
  iputils-arping,
  libcap2-bin,
diff -Nru ganeti-3.0.1/debian/copyright ganeti-3.0.2/debian/copyright
--- ganeti-3.0.1/debian/copyright	2019-12-16 22:12:01.000000000 +0200
+++ ganeti-3.0.2/debian/copyright	2022-06-25 13:46:01.000000000 +0300
@@ -4,11 +4,12 @@
 
 Files: *
 Copyright: Copyright (c) 2006-2015 Google Inc.
+           Copyright (c) 2018, 2021 Ganeti Project Contributors
 License: BSD-2-Clause
 
 Files: debian/*
 Copyright: Copyright (c) 2007 Leonardo Rodrigues de Mello <l@lmello.eu.org>
-           Copyright (c) 2007-2017 Debian Ganeti Team <pkg-ganeti@lists.alioth.debian.org>
+           Copyright (c) 2007-2022 Debian Ganeti Team <pkg-ganeti@lists.alioth.debian.org>
 License: GPL-2+
 
 License: BSD-2-Clause
diff -Nru ganeti-3.0.1/debian/ganeti-3.0.postinst ganeti-3.0.2/debian/ganeti-3.0.postinst
--- ganeti-3.0.1/debian/ganeti-3.0.postinst	2021-03-02 14:42:23.000000000 +0200
+++ ganeti-3.0.2/debian/ganeti-3.0.postinst	2022-06-25 13:48:16.000000000 +0300
@@ -4,4 +4,17 @@
 
 . /usr/share/debconf/confmodule
 
+# Restore the 2.16 version.py diversion in case it was accidentally removed
+# during upgrade. See #993559
+
+if [ -z "$(dpkg-divert --list /usr/share/ganeti/2.16/ganeti/utils/version.py)" ]; then
+	echo "Restoring diversion of /usr/share/ganeti/2.16/ganeti/utils/version.py)"
+	dpkg-divert --add --rename --package ganeti-3.0 \
+		--divert /usr/share/ganeti/2.16/ganeti/utils/version.py.orig \
+		/usr/share/ganeti/2.16/ganeti/utils/version.py
+fi
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
 #DEBHELPER#
diff -Nru ganeti-3.0.1/debian/ganeti-3.0.postrm ganeti-3.0.2/debian/ganeti-3.0.postrm
--- ganeti-3.0.1/debian/ganeti-3.0.postrm	2021-03-02 14:42:23.000000000 +0200
+++ ganeti-3.0.2/debian/ganeti-3.0.postrm	2022-06-25 13:48:16.000000000 +0300
@@ -2,7 +2,7 @@
 
 set -e
 
-if [ "$1" != "purge" ]; then
+if [ "$1" = "remove" ]; then
 	dpkg-divert --remove --rename --package ganeti-3.0 \
 		--divert /usr/share/ganeti/2.16/ganeti/utils/version.py.orig \
 		/usr/share/ganeti/2.16/ganeti/utils/version.py
diff -Nru ganeti-3.0.1/debian/patches/0002-remove-hardcoded-libc-linux-constants.patch ganeti-3.0.2/debian/patches/0002-remove-hardcoded-libc-linux-constants.patch
--- ganeti-3.0.1/debian/patches/0002-remove-hardcoded-libc-linux-constants.patch	2021-03-02 15:11:18.000000000 +0200
+++ ganeti-3.0.2/debian/patches/0002-remove-hardcoded-libc-linux-constants.patch	2022-06-25 13:46:01.000000000 +0300
@@ -50,10 +50,10 @@
  create mode 100644 autotools/HeaderConstants.hsc
 
 diff --git a/Makefile.am b/Makefile.am
-index d489264..414bbfc 100644
+index aa92d2e..3a0825d 100644
 --- a/Makefile.am
 +++ b/Makefile.am
-@@ -2324,6 +2324,9 @@ src/Ganeti/Hs2Py/ListConstants.hs: src/Ganeti/Hs2Py/ListConstants.hs.in \
+@@ -2326,6 +2326,9 @@ src/Ganeti/Hs2Py/ListConstants.hs: src/Ganeti/Hs2Py/ListConstants.hs.in \
  src/Ganeti/Curl/Internal.hs: src/Ganeti/Curl/Internal.hsc | stamp-directories
  	hsc2hs -o $@ $<
  
@@ -63,7 +63,7 @@
  test/hs/Test/Ganeti/TestImports.hs: test/hs/Test/Ganeti/TestImports.hs.in \
  	$(built_base_sources)
  	set -e; \
-@@ -2340,7 +2343,7 @@ lib/_constants.py: Makefile src/hs2py lib/_constants.py.in | stamp-directories
+@@ -2342,7 +2345,7 @@ lib/_constants.py: Makefile src/hs2py lib/_constants.py.in | stamp-directories
  
  lib/constants.py: lib/_constants.py
  
@@ -72,7 +72,7 @@
  	       | $(built_base_sources)
  	@echo "m4 ... >" $@
  	@m4 -DPACKAGE_VERSION="$(PACKAGE_VERSION)" \
-@@ -2418,7 +2421,7 @@ src/AutoConf.hs: Makefile src/AutoConf.hs.in $(PRINT_PY_CONSTANTS) \
+@@ -2420,7 +2423,7 @@ src/AutoConf.hs: Makefile src/AutoConf.hs.in $(PRINT_PY_CONSTANTS) \
  			    done)" \
  	    -DAF_INET4="$$(PYTHONPATH=. $(PYTHON) $(PRINT_PY_CONSTANTS) AF_INET4)" \
  	    -DAF_INET6="$$(PYTHONPATH=. $(PYTHON) $(PRINT_PY_CONSTANTS) AF_INET6)" \
@@ -205,10 +205,10 @@
 +tungetfeatures :: Integer
 +tungetfeatures = AutoConf.tungetfeatures
 diff --git a/test/py/ganeti.hypervisor.hv_kvm_unittest.py b/test/py/ganeti.hypervisor.hv_kvm_unittest.py
-index 9c3c90a..274a0bd 100755
+index 4e05121..1549018 100755
 --- a/test/py/ganeti.hypervisor.hv_kvm_unittest.py
 +++ b/test/py/ganeti.hypervisor.hv_kvm_unittest.py
-@@ -426,7 +426,7 @@ class TestGetTunFeatures(unittest.TestCase):
+@@ -889,7 +889,7 @@ class TestGetTunFeatures(unittest.TestCase):
      self.assertTrue(result is None)
  
    def _FakeIoctl(self, features, fd, request, buf):
diff -Nru ganeti-3.0.1/debian/patches/0003-Fix-lvcreate-for-newer-lvm-versions-1586.patch ganeti-3.0.2/debian/patches/0003-Fix-lvcreate-for-newer-lvm-versions-1586.patch
--- ganeti-3.0.1/debian/patches/0003-Fix-lvcreate-for-newer-lvm-versions-1586.patch	2021-03-02 15:11:18.000000000 +0200
+++ ganeti-3.0.2/debian/patches/0003-Fix-lvcreate-for-newer-lvm-versions-1586.patch	1970-01-01 02:00:00.000000000 +0200
@@ -1,30 +0,0 @@
-From: Valentin <47387140+vali-um@users.noreply.github.com>
-Date: Sun, 21 Feb 2021 11:24:31 +0100
-Subject: Fix lvcreate for newer lvm versions (#1586)
-
-When run non-interactively, newer LVM versions (>= 2.03.10)
-will fail (unless `--yes` is specified) when an existing filesystem
-signature is encountered while creating a new LV. Using `-Wn`
-disables this check.
-
-(cherry picked from commit f8d6260b3762960a342aff716ae5fc6759c5d086)
----
- lib/storage/bdev.py | 5 ++++-
- 1 file changed, 4 insertions(+), 1 deletion(-)
-
-diff --git a/lib/storage/bdev.py b/lib/storage/bdev.py
-index 56ac19a..901a804 100644
---- a/lib/storage/bdev.py
-+++ b/lib/storage/bdev.py
-@@ -215,7 +215,10 @@ class LogicalVolume(base.BlockDev):
-     # create an optimally-striped volume; in that case, we want to try
-     # with N, N-1, ..., 2, and finally 1 (non-stripped) number of
-     # stripes
--    cmd = ["lvcreate", "-L%dm" % size, "-n%s" % lv_name]
-+    # When run non-interactively, newer LVM versions will fail (unless
-+    # `--yes` is specified) when an existing filesystem signature is
-+    # encountered while creating a new LV. Using `-Wn` disables this check.
-+    cmd = ["lvcreate", "-Wn", "-L%dm" % size, "-n%s" % lv_name]
-     for stripes_arg in range(stripes, 0, -1):
-       result = utils.RunCmd(cmd + ["-i%d" % stripes_arg] + [vg_name] + pvlist)
-       if not result.failed:
diff -Nru ganeti-3.0.1/debian/patches/0004-Fix-live-migration-of-xen-instances-1582.patch ganeti-3.0.2/debian/patches/0004-Fix-live-migration-of-xen-instances-1582.patch
--- ganeti-3.0.1/debian/patches/0004-Fix-live-migration-of-xen-instances-1582.patch	2021-03-02 15:13:14.000000000 +0200
+++ ganeti-3.0.2/debian/patches/0004-Fix-live-migration-of-xen-instances-1582.patch	1970-01-01 02:00:00.000000000 +0200
@@ -1,71 +0,0 @@
-From: Wurzelmann <Wurzelmann@users.noreply.github.com>
-Date: Sat, 6 Feb 2021 20:19:35 +0100
-Subject: Fix live migration of xen instances (#1582)
-
-This PR fixes issue #1574. Config deletion has been unified (setting the config path first and deleting the file, if it exists and creating it) and an improved runtime check has been introduced.
-
-The test used before just took the cpu usage in seconds into account, which lead to problems on our new cluster due to its quite beefy AMD cpus; just migrating an idle instance did not increase the number of cpu seconds above 0.
-
-(cherry picked from commit e7dcff35607a6241409f3cfcacd124584c642d94)
----
- lib/hypervisor/hv_xen.py | 23 +++++++++++++++++------
- 1 file changed, 17 insertions(+), 6 deletions(-)
-
-diff --git a/lib/hypervisor/hv_xen.py b/lib/hypervisor/hv_xen.py
-index 2590c08..1344d73 100644
---- a/lib/hypervisor/hv_xen.py
-+++ b/lib/hypervisor/hv_xen.py
-@@ -180,6 +180,16 @@ def _InstanceDomID(info):
-   return info[1]
- 
- 
-+def _InstanceRunning(info):
-+  """Get instance runtime from instance info tuple.
-+  @type info: tuple
-+  @param info: instance info as parsed by _ParseInstanceList()
-+
-+  @return: bool
-+  """
-+  return info[4] == hv_base.HvInstanceState.RUNNING
-+
-+
- def _InstanceRuntime(info):
-   """Get instance runtime from instance info tuple.
-   @type info: tuple
-@@ -780,10 +790,11 @@ class XenHypervisor(hv_base.BaseHypervisor):
-     This version of the function just writes the config file from static data.
- 
-     """
-+    cfg_file = self._ConfigFileName(instance_name)
-+    
-     # just in case it exists
-     utils.RemoveFile(utils.PathJoin(self._cfgdir, "auto", instance_name))
- 
--    cfg_file = self._ConfigFileName(instance_name)
-     try:
-       utils.WriteFile(cfg_file, data=data)
-     except EnvironmentError as err:
-@@ -1065,10 +1076,10 @@ class XenHypervisor(hv_base.BaseHypervisor):
-     if not force:
-       self._ShutdownInstance(name, hvparams, timeout)
- 
--    # TODO: Xen does always destroy the instnace after trying a gracefull
--    # shutdown. That means doing another attempt with force=True will not make
--    # any difference. This differs in behaviour from other hypervisors and
--    # should be cleaned up.
-+    # TODO: Xen always destroys the instance after trying a graceful shutdown.
-+    # That means doing another attempt with force=True will not make any
-+    # difference. This differs in behaviour from other hypervisors and should
-+    # be cleaned up.
-     result = self._DestroyInstanceIfAlive(name, hvparams)
-     if result is not None and result.failed and \
-           self.GetInstanceInfo(name, hvparams=hvparams) is not None:
-@@ -1295,7 +1306,7 @@ class XenHypervisor(hv_base.BaseHypervisor):
-     # We should recreate the config file if the domain is present and running,
-     # regardless if we think the migration succeeded or not.
-     info = self.GetInstanceInfo(instance.name, hvparams=instance.hvparams)
--    if info and _InstanceRuntime(info) != 0:
-+    if info and _InstanceRunning(info):
-       self._WriteConfigFile(instance.name, config)
- 
-     if not success:
diff -Nru ganeti-3.0.1/debian/patches/series ganeti-3.0.2/debian/patches/series
--- ganeti-3.0.1/debian/patches/series	2021-03-02 15:13:14.000000000 +0200
+++ ganeti-3.0.2/debian/patches/series	2022-06-25 13:46:01.000000000 +0300
@@ -1,4 +1,2 @@
 0001-verify-warn-about-weak-certs.patch
 0002-remove-hardcoded-libc-linux-constants.patch
-0003-Fix-lvcreate-for-newer-lvm-versions-1586.patch
-0004-Fix-live-migration-of-xen-instances-1582.patch
diff -Nru ganeti-3.0.1/debian/rules ganeti-3.0.2/debian/rules
--- ganeti-3.0.1/debian/rules	2021-03-02 14:40:51.000000000 +0200
+++ ganeti-3.0.2/debian/rules	2022-06-25 13:46:01.000000000 +0300
@@ -29,11 +29,6 @@
 	$(error Empty VCS version, the target must be run within the packaging git repository)
 else
 debian/control: debian/control.in
-	for file in debian/templates/*-VER*; do \
-		fname="$$(echo $$file | sed -e 's#VER#$(VCS_VER)#' -e 's#templates/##')"; \
-		sed -e 's/@version@/$(VCS_VER)/g' $$file >"$$fname"; \
-	done
-	
 	for package in ganeti-haskell-$(VCS_VER) ganeti-htools-$(VCS_VER) ganeti-$(VCS_VER); do \
 		for file in debian/templates/versioned.*; do \
 			kind="$$(echo $$file | sed -r 's#.*\.(.*)#\1#')"; \
@@ -43,6 +38,11 @@
 		done; \
 	done
 	
+	for file in debian/templates/*-VER*; do \
+		fname="$$(echo $$file | sed -e 's#VER#$(VCS_VER)#' -e 's#templates/##')"; \
+		sed -e 's/@version@/$(VCS_VER)/g' $$file >"$$fname"; \
+	done
+	
 	sed -s 's/#VER#/$(VCS_VER)/g' debian/control.in > debian/control
 endif
 
diff -Nru ganeti-3.0.1/debian/templates/ganeti-VER.postinst ganeti-3.0.2/debian/templates/ganeti-VER.postinst
--- ganeti-3.0.1/debian/templates/ganeti-VER.postinst	1970-01-01 02:00:00.000000000 +0200
+++ ganeti-3.0.2/debian/templates/ganeti-VER.postinst	2022-06-25 13:46:01.000000000 +0300
@@ -0,0 +1,20 @@
+#!/bin/sh
+
+set -e
+
+. /usr/share/debconf/confmodule
+
+# Restore the 2.16 version.py diversion in case it was accidentally removed
+# during upgrade. See #993559
+
+if [ -z "$(dpkg-divert --list /usr/share/ganeti/2.16/ganeti/utils/version.py)" ]; then
+	echo "Restoring diversion of /usr/share/ganeti/2.16/ganeti/utils/version.py)"
+	dpkg-divert --add --rename --package ganeti-@version@ \
+		--divert /usr/share/ganeti/2.16/ganeti/utils/version.py.orig \
+		/usr/share/ganeti/2.16/ganeti/utils/version.py
+fi
+
+# dh_installdeb will replace this with shell code automatically
+# generated by other debhelper scripts.
+
+#DEBHELPER#
diff -Nru ganeti-3.0.1/debian/templates/ganeti-VER.postrm ganeti-3.0.2/debian/templates/ganeti-VER.postrm
--- ganeti-3.0.1/debian/templates/ganeti-VER.postrm	2021-01-14 10:21:47.000000000 +0200
+++ ganeti-3.0.2/debian/templates/ganeti-VER.postrm	2022-06-25 13:46:01.000000000 +0300
@@ -2,7 +2,7 @@
 
 set -e
 
-if [ "$1" != "purge" ]; then
+if [ "$1" = "remove" ]; then
 	dpkg-divert --remove --rename --package ganeti-@version@ \
 		--divert /usr/share/ganeti/2.16/ganeti/utils/version.py.orig \
 		/usr/share/ganeti/2.16/ganeti/utils/version.py
diff -Nru ganeti-3.0.1/doc/index.rst ganeti-3.0.2/doc/index.rst
--- ganeti-3.0.1/doc/index.rst	2021-02-03 10:24:16.000000000 +0200
+++ ganeti-3.0.2/doc/index.rst	2022-02-28 22:51:21.000000000 +0200
@@ -119,7 +119,6 @@
    design-hotplug.rst
    design-internal-shutdown.rst
    design-kvmd.rst
-   design-location.rst
    design-linuxha.rst
    design-location.rst
    design-lu-generated-jobs.rst
diff -Nru ganeti-3.0.1/.github/workflows/ci.yml ganeti-3.0.2/.github/workflows/ci.yml
--- ganeti-3.0.1/.github/workflows/ci.yml	2021-02-03 10:24:16.000000000 +0200
+++ ganeti-3.0.2/.github/workflows/ci.yml	2022-02-28 22:51:21.000000000 +0200
@@ -35,6 +35,11 @@
       - name: Build
         run: make -j 2
 
+      - name: Check Local
+        run: mv .github/ /tmp/
+             && LC_ALL=C make check-local
+             && mv /tmp/.github/ .
+
       - name: Python tests
         run: make py-tests
 
diff -Nru ganeti-3.0.1/INSTALL ganeti-3.0.2/INSTALL
--- ganeti-3.0.1/INSTALL	2021-02-03 10:24:16.000000000 +0200
+++ ganeti-3.0.2/INSTALL	2022-02-28 22:51:21.000000000 +0200
@@ -33,7 +33,7 @@
 - `Python OpenSSL bindings <https://www.pyopenssl.org/>`_
 - `simplejson Python module <https://simplejson.readthedocs.io/>`_
 - `pyparsing Python module <https://pyparsing-docs.readthedocs.io/>`_, version
-  1.4.6 or above
+  1.5.7 or above
 - `pyinotify Python module <https://github.com/seb-m/pyinotify>`_
 - `PycURL Python module <http://pycurl.io/>`_
 - `socat <http://www.dest-unreach.org/socat/>`_, see :ref:`note
diff -Nru ganeti-3.0.1/lib/build/shell_example_lexer.py ganeti-3.0.2/lib/build/shell_example_lexer.py
--- ganeti-3.0.1/lib/build/shell_example_lexer.py	2021-02-03 10:24:16.000000000 +0200
+++ ganeti-3.0.2/lib/build/shell_example_lexer.py	2022-02-28 22:51:21.000000000 +0200
@@ -41,6 +41,7 @@
 
 from pygments.lexer import RegexLexer, bygroups, include
 from pygments.token import Name, Text, Generic, Comment
+import sphinx
 
 
 class ShellExampleLexer(RegexLexer):
@@ -79,4 +80,8 @@
 
 
 def setup(app):
-  app.add_lexer("shell-example", ShellExampleLexer())
+  version = tuple(map(int, sphinx.__version__.split('.')))
+  if version >= (2, 1, 0):
+    app.add_lexer("shell-example", ShellExampleLexer)
+  else:
+    app.add_lexer("shell-example", ShellExampleLexer())
diff -Nru ganeti-3.0.1/lib/build/sphinx_ext.py ganeti-3.0.2/lib/build/sphinx_ext.py
--- ganeti-3.0.1/lib/build/sphinx_ext.py	2021-02-03 10:24:16.000000000 +0200
+++ ganeti-3.0.2/lib/build/sphinx_ext.py	2022-02-28 22:51:21.000000000 +0200
@@ -360,7 +360,7 @@
 
   # Force custom title
   kwargs["refexplicit"] = True
-  kwargs["refdomain"] = None
+  kwargs["refdomain"] = "std"
 
   return sphinx.addnodes.pending_xref(*args, **kwargs)
 
diff -Nru ganeti-3.0.1/lib/cmdlib/cluster/__init__.py ganeti-3.0.2/lib/cmdlib/cluster/__init__.py
--- ganeti-3.0.1/lib/cmdlib/cluster/__init__.py	2021-02-03 10:24:16.000000000 +0200
+++ ganeti-3.0.2/lib/cmdlib/cluster/__init__.py	2022-02-28 22:51:21.000000000 +0200
@@ -1272,6 +1272,16 @@
     self._CheckDrbdHelper(vm_capable_node_uuids,
                           drbd_enabled, drbd_gets_enabled)
 
+    if (self.op.diskparams is not None and
+            constants.DT_DRBD8 in self.op.diskparams):
+      self.LogWarning("Changing DRBD parameters only affects devices created "
+                      "in the future, not existing ones")
+      self.LogWarning("You need to shutdown and start (not reboot!) existing "
+                      "instances to adopt the changes")
+      self.LogWarning("Alternatively you can swap the secondary node by "
+                      "running `gnt-instance replace-disks --new-secondary "
+                      "$instance`")
+
     # validate params changes
     if self.op.beparams:
       objects.UpgradeBeParams(self.op.beparams)
diff -Nru ganeti-3.0.1/lib/http/server.py ganeti-3.0.2/lib/http/server.py
--- ganeti-3.0.1/lib/http/server.py	2021-02-03 10:24:16.000000000 +0200
+++ ganeti-3.0.2/lib/http/server.py	2022-02-28 22:51:21.000000000 +0200
@@ -158,8 +158,7 @@
     # message-body, [...]"
 
     return (http.HttpMessageWriter.HasMessageBody(self) and
-            (request_method is not None and
-             request_method != http.HTTP_HEAD) and
+            request_method != http.HTTP_HEAD and
             response_code >= http.HTTP_OK and
             response_code not in (http.HTTP_NO_CONTENT,
                                   http.HTTP_NOT_MODIFIED))
@@ -314,6 +313,8 @@
         _HandleServerRequestInner(self._handler, request_msg, req_msg_reader)
     except http.HttpException as err:
       self._SetError(self.responses, self._handler, response_msg, err)
+      request_msg = http.HttpMessage()
+      req_msg_reader = None
     else:
       # Only wait for client to close if we didn't have any exception.
       force_close = False
diff -Nru ganeti-3.0.1/lib/hypervisor/hv_kvm/__init__.py ganeti-3.0.2/lib/hypervisor/hv_kvm/__init__.py
--- ganeti-3.0.1/lib/hypervisor/hv_kvm/__init__.py	2021-02-03 10:24:16.000000000 +0200
+++ ganeti-3.0.2/lib/hypervisor/hv_kvm/__init__.py	2022-02-28 22:51:21.000000000 +0200
@@ -72,21 +72,20 @@
                                              MonitorSocket
 from ganeti.hypervisor.hv_kvm.netdev import OpenTap
 
+from ganeti.hypervisor.hv_kvm.validation import check_boot_parameters, \
+                                                check_console_parameters, \
+                                                check_disk_cache_parameters, \
+                                                check_security_model,\
+                                                check_spice_parameters, \
+                                                check_vnc_parameters, \
+                                                validate_machine_version, \
+                                                validate_security_model, \
+                                                validate_spice_parameters, \
+                                                validate_vnc_parameters
 
 _KVM_NETWORK_SCRIPT = pathutils.CONF_DIR + "/kvm-vif-bridge"
 _KVM_START_PAUSED_FLAG = "-S"
 
-#: SPICE parameters which depend on L{constants.HV_KVM_SPICE_BIND}
-_SPICE_ADDITIONAL_PARAMS = frozenset([
-  constants.HV_KVM_SPICE_IP_VERSION,
-  constants.HV_KVM_SPICE_PASSWORD_FILE,
-  constants.HV_KVM_SPICE_LOSSLESS_IMG_COMPR,
-  constants.HV_KVM_SPICE_JPEG_IMG_COMPR,
-  constants.HV_KVM_SPICE_ZLIB_GLZ_IMG_COMPR,
-  constants.HV_KVM_SPICE_STREAMING_VIDEO_DETECTION,
-  constants.HV_KVM_SPICE_USE_TLS,
-  ])
-
 # below constants show the format of runtime file
 # the nics are in second possition, while the disks in 4th (last)
 # moreover disk entries are stored as a list of in tuples
@@ -488,7 +487,6 @@
   _CTRL_DIR = _ROOT_DIR + "/ctrl" # contains instances control sockets
   _CONF_DIR = _ROOT_DIR + "/conf" # contains instances startup data
   _NICS_DIR = _ROOT_DIR + "/nic" # contains instances nic <-> tap associations
-  _KEYMAP_DIR = _ROOT_DIR + "/keymap" # contains instances keymaps
   # KVM instances with chroot enabled are started in empty chroot directories.
   _CHROOT_DIR = _ROOT_DIR + "/chroot" # for empty chroot directories
   # After an instance is stopped, its chroot directory is removed.
@@ -498,7 +496,7 @@
   # a separate directory, called 'chroot-quarantine'.
   _CHROOT_QUARANTINE_DIR = _ROOT_DIR + "/chroot-quarantine"
   _DIRS = [_ROOT_DIR, _PIDS_DIR, _UIDS_DIR, _CTRL_DIR, _CONF_DIR, _NICS_DIR,
-           _CHROOT_DIR, _CHROOT_QUARANTINE_DIR, _KEYMAP_DIR]
+           _CHROOT_DIR, _CHROOT_QUARANTINE_DIR]
 
   PARAMETERS = {
     constants.HV_KVM_PATH: hv_base.REQ_FILE_CHECK,
@@ -609,11 +607,9 @@
   _CPU_INFO_CMD = "info cpus"
 
   _DEFAULT_MACHINE_VERSION_RE = re.compile(r"^(\S+).*\(default\)", re.M)
-  _CHECK_MACHINE_VERSION_RE = \
-    staticmethod(lambda x: re.compile(r"^(%s)[ ]+.*PC" % x, re.M))
 
   _QMP_RE = re.compile(r"^-qmp\s", re.M)
-  _SPICE_RE = re.compile(r"^-spice\s", re.M)
+
   _VHOST_RE = re.compile(r"^-netdev\stap.*,vhost=on\|off", re.M | re.S)
   _VIRTIO_NET_QUEUES_RE = re.compile(r"^-netdev\stap.*,fds=x:y:...:z", re.M)
   _ENABLE_KVM_RE = re.compile(r"^-enable-kvm\s", re.M)
@@ -629,6 +625,10 @@
   # different than -drive is starting)
   _BOOT_RE = re.compile(r"^-drive\s([^-]|(?<!^)-)*,boot=on\|off", re.M | re.S)
   _UUID_RE = re.compile(r"^-uuid\s", re.M)
+  # The auto-read-only option is on the -blockdev, Ganeti uses this at -drive
+  _AUTO_RO_RE = \
+    re.compile(r"^-blockdev\s([^-]|(?<!^)-)*,auto-read-only=on\|off",
+               re.M | re.S)
 
   _INFO_VERSION_RE = \
     re.compile(r'^QEMU (\d+)\.(\d+)(\.(\d+))?.*monitor.*', re.M)
@@ -868,13 +868,6 @@
     return utils.PathJoin(cls._InstanceNICDir(instance_name), str(seq))
 
   @classmethod
-  def _InstanceKeymapFile(cls, instance_name):
-    """Returns the name of the file containing the keymap for a given instance
-
-    """
-    return utils.PathJoin(cls._KEYMAP_DIR, instance_name)
-
-  @classmethod
   def _TryReadUidFile(cls, uid_file):
     """Try to read a uid file
 
@@ -900,7 +893,6 @@
     utils.RemoveFile(cls._InstanceQmpMonitor(instance_name))
     utils.RemoveFile(cls._InstanceQemuGuestAgentMonitor(instance_name))
     utils.RemoveFile(cls._InstanceKVMRuntime(instance_name))
-    utils.RemoveFile(cls._InstanceKeymapFile(instance_name))
     uid_file = cls._InstanceUidFile(instance_name)
     uid = cls._TryReadUidFile(uid_file)
     utils.RemoveFile(uid_file)
@@ -1141,6 +1133,40 @@
         data.append(info)
     return data
 
+  @staticmethod
+  def _GenerateDiskAioCacheParameters(disk_aio, disk_cache, dev_type):
+    """Generate appropriate aio/cache parameters for QEMU
+
+    @type disk_aio: string
+    @param disk_aio: the instance's AIO parameter
+    @type disk_cache: string
+    @param disk_cache: the instance's disk cache parameter
+    @type dev_type: string
+    @param dev_type: the disk type in use
+    @rtype: string
+    @return: parameter string suitable for QEMU drive parameters
+
+    """
+
+    if (dev_type in constants.DTS_EXT_MIRROR
+            and dev_type != constants.DT_RBD):
+      logging.warning("KVM: overriding disk_cache setting '%s' with 'none'"
+                      " to prevent shared storage corruption on migration",
+                      disk_cache)
+      disk_cache = constants.HT_CACHE_NONE
+
+    if not disk_aio:
+      # QEMU defaults to 'threads', so do we
+      disk_aio = constants.HT_KVM_AIO_THREADS
+
+    if disk_aio == constants.HT_KVM_AIO_NATIVE:
+      return ",aio=native,cache=none"
+    else:
+      if disk_cache == constants.HT_CACHE_DEFAULT:
+        return ",aio=threads"
+      else:
+        return ",aio=threads,cache=%s" % disk_cache
+
   def _GenerateKVMBlockDevicesOptions(self, up_hvp, kvm_disks,
                                       kvmhelp, devlist):
     """Generate KVM options regarding instance's block devices.
@@ -1185,12 +1211,6 @@
       if_val = ",if=%s" % iface # for the -drive option
       device_driver = None # without -device option
 
-    # AIO mode
-    aio_mode = up_hvp[constants.HV_KVM_DISK_AIO]
-    if aio_mode == constants.HT_KVM_AIO_NATIVE:
-      aio_val = ",aio=%s" % aio_mode
-    else:
-      aio_val = ""
     # discard mode
     discard_mode = up_hvp[constants.HV_DISK_DISCARD]
     if discard_mode == constants.HT_DISCARD_DEFAULT:
@@ -1200,24 +1220,9 @@
     # Cache mode
     disk_cache = up_hvp[constants.HV_DISK_CACHE]
     for cfdev, link_name, uri in kvm_disks:
-      if (cfdev.dev_type in constants.DTS_EXT_MIRROR
-          and cfdev.dev_type != constants.DT_RBD):
-        if disk_cache != "none":
-          # TODO: make this a hard error, instead of a silent overwrite
-          logging.warning("KVM: overriding disk_cache setting '%s' with 'none'"
-                          " to prevent shared storage corruption on migration",
-                          disk_cache)
-        cache_val = ",cache=none"
-      elif aio_mode == constants.HT_KVM_AIO_NATIVE and disk_cache != "none":
-        # TODO: make this a hard error, instead of a silent overwrite
-        logging.warning("KVM: overriding disk_cache setting '%s' with 'none'"
-                        " to prevent QEMU failures in version 2.6+",
-                        disk_cache)
-        cache_val = ",cache=none"
-      elif disk_cache != constants.HT_CACHE_DEFAULT:
-        cache_val = ",cache=%s" % disk_cache
-      else:
-        cache_val = ""
+      aio_cache_val = self._GenerateDiskAioCacheParameters(
+        up_hvp[constants.HV_KVM_DISK_AIO], up_hvp[constants.HV_DISK_CACHE],
+        cfdev.dev_type)
       if cfdev.mode != constants.DISK_RDWR:
         raise errors.HypervisorError("Instance has read-only disks which"
                                      " are not supported by KVM")
@@ -1231,8 +1236,8 @@
 
       drive_uri = _GetDriveURI(cfdev, link_name, uri)
 
-      drive_val = "file=%s,format=raw%s%s%s%s%s" % \
-                  (drive_uri, if_val, boot_val, cache_val, aio_val, discard_val)
+      drive_val = "file=%s,format=raw%s%s%s%s" % \
+                  (drive_uri, if_val, boot_val, aio_cache_val, discard_val)
 
       # virtio-blk-pci case
       if device_driver is not None:
@@ -1246,6 +1251,14 @@
         dev_val += ",drive=%s" % kvm_devid
         dev_opts.extend(["-device", dev_val])
 
+      # QEMU 4.0 introduced dynamic auto-read-only for file-backed drives. This
+      # is unhandled in Ganeti and breaks live migration with
+      # security_model=user|pool, disable it here. See also
+      # HotAddDevice/drive_add_fn which solves a similar problem for hotpluged
+      # disks
+      if self._AUTO_RO_RE.search(kvmhelp):
+        drive_val += ",auto-read-only=off"
+
       dev_opts.extend(["-drive", drive_val])
 
     return dev_opts
@@ -1662,15 +1675,13 @@
 
     # Add guest agent socket
     if hvp[constants.HV_USE_GUEST_AGENT]:
-      qga_addr = utils.GetFreeSlot(bus_slots[_PCI_BUS], reserve=True)
-      qga_pci_info = "bus=%s,addr=%s" % (_PCI_BUS, hex(qga_addr))
       qga_path = self._InstanceQemuGuestAgentMonitor(instance.name)
       logging.info("KVM: Guest Agent available at %s", qga_path)
       # The 'qga0' identified can change, but the 'org.qemu.guest_agent.0'
       # string is the default expected by the Guest Agent.
       kvm_cmd.extend([
         "-chardev", "socket,path=%s,server,nowait,id=qga0" % qga_path,
-        "-device", "virtio-serial,id=qga0,%s" % qga_pci_info,
+        "-device", "virtio-serial,id=qga0",
         "-device", "virtserialport,chardev=qga0,name=org.qemu.guest_agent.0",
         ])
 
@@ -1896,15 +1907,10 @@
     if security_model == constants.HT_SM_USER:
       kvm_cmd.extend(["-runas", conf_hvp[constants.HV_SECURITY_DOMAIN]])
 
+    # the VNC keymap
     keymap = conf_hvp[constants.HV_KEYMAP]
     if keymap:
-      keymap_path = self._InstanceKeymapFile(name)
-      # If a keymap file is specified, KVM won't use its internal defaults. By
-      # first including the "en-us" layout, an error on loading the actual
-      # layout (e.g. because it can't be found) won't lead to a non-functional
-      # keyboard. A keyboard with incorrect keys is still better than none.
-      utils.WriteFile(keymap_path, data="include en-us\ninclude %s\n" % keymap)
-      kvm_cmd.extend(["-k", keymap_path])
+      kvm_cmd.extend(["-k", keymap])
 
     # We have reasons to believe changing something like the nic driver/type
     # upon migration won't exactly fly with the instance kernel, so for nic
@@ -2296,10 +2302,9 @@
           cmd += ",auto-read-only=off"
         # When hot plugging a disk, parameters should match the current runtime.
         # I.e. for live migration, the cache mode is critical.
-        if up_hvp[constants.HV_DISK_CACHE] != constants.HT_CACHE_DEFAULT:
-          cmd += ",cache=%s" % up_hvp[constants.HV_DISK_CACHE]
-        if up_hvp[constants.HV_KVM_DISK_AIO] == constants.HT_KVM_AIO_NATIVE:
-          cmd += ",aio=%s" % up_hvp[constants.HV_KVM_DISK_AIO]
+        cmd += self._GenerateDiskAioCacheParameters(
+          up_hvp[constants.HV_KVM_DISK_AIO], up_hvp[constants.HV_DISK_CACHE],
+          device_type)
         if up_hvp[constants.HV_DISK_DISCARD] != constants.HT_DISCARD_DEFAULT:
           cmd += ",discard=%s" % up_hvp[constants.HV_DISK_DISCARD]
         self._CallMonitorCommand(instance.name, cmd)
@@ -2313,12 +2318,14 @@
       self.qmp.HotAddDisk(device, kvm_devid, uri, drive_add_fn)
     elif dev_type == constants.HOTPLUG_TARGET_NIC:
       kvmpath = instance.hvparams[constants.HV_KVM_PATH]
+      is_chrooted = instance.hvparams[constants.HV_KVM_USE_CHROOT]
       kvmhelp = self._GetKVMOutput(kvmpath, self._KVMOPT_HELP)
       devlist = self._GetKVMOutput(kvmpath, self._KVMOPT_DEVICELIST)
       features, _, _ = self._GetNetworkDeviceFeatures(up_hvp, devlist, kvmhelp)
       (tap, tapfds, vhostfds) = OpenTap(features=features)
       self._ConfigureNIC(instance, seq, device, tap)
-      self.qmp.HotAddNic(device, kvm_devid, tapfds, vhostfds, features)
+      self.qmp.HotAddNic(device, kvm_devid, tapfds, vhostfds, features,
+                         is_chrooted)
       utils.WriteFile(self._InstanceNICFile(instance.name, seq), data=tap)
 
     self._VerifyHotplugCommand(instance, kvm_devid, True)
@@ -2789,67 +2796,12 @@
     """
     super(KVMHypervisor, cls).CheckParameterSyntax(hvparams)
 
-    kernel_path = hvparams[constants.HV_KERNEL_PATH]
-    if kernel_path:
-      if not hvparams[constants.HV_ROOT_PATH]:
-        raise errors.HypervisorError("Need a root partition for the instance,"
-                                     " if a kernel is defined")
-
-    if (hvparams[constants.HV_VNC_X509_VERIFY] and
-        not hvparams[constants.HV_VNC_X509]):
-      raise errors.HypervisorError("%s must be defined, if %s is" %
-                                   (constants.HV_VNC_X509,
-                                    constants.HV_VNC_X509_VERIFY))
-
-    if hvparams[constants.HV_SERIAL_CONSOLE]:
-      serial_speed = hvparams[constants.HV_SERIAL_SPEED]
-      valid_speeds = constants.VALID_SERIAL_SPEEDS
-      if not serial_speed or serial_speed not in valid_speeds:
-        raise errors.HypervisorError("Invalid serial console speed, must be"
-                                     " one of: %s" %
-                                     utils.CommaJoin(valid_speeds))
-
-    boot_order = hvparams[constants.HV_BOOT_ORDER]
-    if (boot_order == constants.HT_BO_CDROM and
-        not hvparams[constants.HV_CDROM_IMAGE_PATH]):
-      raise errors.HypervisorError("Cannot boot from cdrom without an"
-                                   " ISO path")
-
-    security_model = hvparams[constants.HV_SECURITY_MODEL]
-    if security_model == constants.HT_SM_USER:
-      if not hvparams[constants.HV_SECURITY_DOMAIN]:
-        raise errors.HypervisorError("A security domain (user to run kvm as)"
-                                     " must be specified")
-    elif (security_model == constants.HT_SM_NONE or
-          security_model == constants.HT_SM_POOL):
-      if hvparams[constants.HV_SECURITY_DOMAIN]:
-        raise errors.HypervisorError("Cannot have a security domain when the"
-                                     " security model is 'none' or 'pool'")
-
-    spice_bind = hvparams[constants.HV_KVM_SPICE_BIND]
-    spice_ip_version = hvparams[constants.HV_KVM_SPICE_IP_VERSION]
-    if spice_bind:
-      if spice_ip_version != constants.IFACE_NO_IP_VERSION_SPECIFIED:
-        # if an IP version is specified, the spice_bind parameter must be an
-        # IP of that family
-        if (netutils.IP4Address.IsValid(spice_bind) and
-            spice_ip_version != constants.IP4_VERSION):
-          raise errors.HypervisorError("SPICE: Got an IPv4 address (%s), but"
-                                       " the specified IP version is %s" %
-                                       (spice_bind, spice_ip_version))
-
-        if (netutils.IP6Address.IsValid(spice_bind) and
-            spice_ip_version != constants.IP6_VERSION):
-          raise errors.HypervisorError("SPICE: Got an IPv6 address (%s), but"
-                                       " the specified IP version is %s" %
-                                       (spice_bind, spice_ip_version))
-    else:
-      # All the other SPICE parameters depend on spice_bind being set. Raise an
-      # error if any of them is set without it.
-      for param in _SPICE_ADDITIONAL_PARAMS:
-        if hvparams[param]:
-          raise errors.HypervisorError("SPICE: %s requires %s to be set" %
-                                       (param, constants.HV_KVM_SPICE_BIND))
+    check_boot_parameters(hvparams)
+    check_security_model(hvparams)
+    check_console_parameters(hvparams)
+    check_vnc_parameters(hvparams)
+    check_spice_parameters(hvparams)
+    check_disk_cache_parameters(hvparams)
 
   @classmethod
   def ValidateParameters(cls, hvparams):
@@ -2862,56 +2814,16 @@
     """
     super(KVMHypervisor, cls).ValidateParameters(hvparams)
 
+    validate_security_model(hvparams)
+    validate_vnc_parameters(hvparams)
+
     kvm_path = hvparams[constants.HV_KVM_PATH]
 
-    security_model = hvparams[constants.HV_SECURITY_MODEL]
-    if security_model == constants.HT_SM_USER:
-      username = hvparams[constants.HV_SECURITY_DOMAIN]
-      try:
-        pwd.getpwnam(username)
-      except KeyError:
-        raise errors.HypervisorError("Unknown security domain user %s"
-                                     % username)
-    vnc_bind_address = hvparams[constants.HV_VNC_BIND_ADDRESS]
-    if vnc_bind_address:
-      bound_to_addr = (netutils.IP4Address.IsValid(vnc_bind_address) or
-                       netutils.IP6Address.IsValid(vnc_bind_address))
-      is_interface = netutils.IsValidInterface(vnc_bind_address)
-      is_path = utils.IsNormAbsPath(vnc_bind_address)
-      if not bound_to_addr and not is_interface and not is_path:
-        raise errors.HypervisorError("VNC: The %s parameter must be either"
-                                     " a valid IP address, an interface name,"
-                                     " or an absolute path" %
-                                     constants.HV_VNC_BIND_ADDRESS)
+    kvm_output = cls._GetKVMOutput(kvm_path, cls._KVMOPT_HELP)
+    validate_spice_parameters(hvparams, kvm_output)
 
-    spice_bind = hvparams[constants.HV_KVM_SPICE_BIND]
-    if spice_bind:
-      # only one of VNC and SPICE can be used currently.
-      if hvparams[constants.HV_VNC_BIND_ADDRESS]:
-        raise errors.HypervisorError("Both SPICE and VNC are configured, but"
-                                     " only one of them can be used at a"
-                                     " given time")
-
-      # check that KVM supports SPICE
-      kvmhelp = cls._GetKVMOutput(kvm_path, cls._KVMOPT_HELP)
-      if not cls._SPICE_RE.search(kvmhelp):
-        raise errors.HypervisorError("SPICE is configured, but it is not"
-                                     " supported according to 'kvm --help'")
-
-      # if spice_bind is not an IP address, it must be a valid interface
-      bound_to_addr = (netutils.IP4Address.IsValid(spice_bind) or
-                       netutils.IP6Address.IsValid(spice_bind))
-      if not bound_to_addr and not netutils.IsValidInterface(spice_bind):
-        raise errors.HypervisorError("SPICE: The %s parameter must be either"
-                                     " a valid IP address or interface name" %
-                                     constants.HV_KVM_SPICE_BIND)
-
-    machine_version = hvparams[constants.HV_KVM_MACHINE_VERSION]
-    if machine_version:
-      output = cls._GetKVMOutput(kvm_path, cls._KVMOPT_MLIST)
-      if not cls._CHECK_MACHINE_VERSION_RE(machine_version).search(output):
-        raise errors.HypervisorError("Unsupported machine version: %s" %
-                                     machine_version)
+    kvm_output = cls._GetKVMOutput(kvm_path, cls._KVMOPT_MLIST)
+    validate_machine_version(hvparams, kvm_output)
 
   @classmethod
   def PowercycleNode(cls, hvparams=None):
diff -Nru ganeti-3.0.1/lib/hypervisor/hv_kvm/monitor.py ganeti-3.0.2/lib/hypervisor/hv_kvm/monitor.py
--- ganeti-3.0.1/lib/hypervisor/hv_kvm/monitor.py	2021-02-03 10:24:16.000000000 +0200
+++ ganeti-3.0.2/lib/hypervisor/hv_kvm/monitor.py	2022-02-28 22:51:21.000000000 +0200
@@ -260,6 +260,8 @@
 
   def __init__(self, monitor_filename):
     super(QmpConnection, self).__init__(monitor_filename)
+    self.version = None
+    self.package = None
     self._buf = b""
     self.supported_commands = None
 
@@ -281,13 +283,21 @@
 
     """
     super(QmpConnection, self).connect()
-    # Check if we receive a correct greeting message from the server
-    # (As per the QEMU Protocol Specification 0.1 - section 2.2)
-    greeting = self._Recv()
-    if not greeting[self._FIRST_MESSAGE_KEY]:
-      self._connected = False
-      raise errors.HypervisorError("kvm: QMP communication error (wrong"
-                                   " server greeting")
+    # sometimes we receive asynchronous events instead of the intended greeting
+    # message - we ignore these for now. However, only 5 times to not get stuck
+    # in an endless connect() loop.
+    for x in range(0, 4):
+      # Check if we receive a correct greeting message from the server
+      # (As per the QEMU Protocol Specification 0.1 - section 2.2)
+      greeting = self._Recv()
+      if greeting[self._EVENT_KEY]:
+        continue
+      if not greeting[self._FIRST_MESSAGE_KEY]:
+        self._connected = False
+        raise errors.HypervisorError("kvm: QMP communication error (wrong"
+                                     " server greeting)")
+      else:
+        break
 
     # Extract the version info from the greeting and make it available to users
     # of the monitor.
@@ -465,7 +475,8 @@
     return ret
 
   @_ensure_connection
-  def HotAddNic(self, nic, devid, tapfds=None, vhostfds=None, features=None):
+  def HotAddNic(self, nic, devid, tapfds=None, vhostfds=None, features=None,
+                is_chrooted=False):
     """Hot-add a NIC
 
     First pass the tapfds, then netdev_add and then device_add
@@ -492,6 +503,7 @@
       "id": devid,
       "fds": ":".join(fdnames),
     }
+
     if enable_vhost:
       fdnames = []
       for i, fd in enumerate(vhostfds):
@@ -500,7 +512,7 @@
         fdnames.append(fdname)
 
       arguments.update({
-        "vhost": "on",
+        "vhost": True,
         "vhostfds": ":".join(fdnames),
         })
     self.Execute("netdev_add", arguments)
@@ -509,6 +521,11 @@
       "netdev": devid,
       "mac": nic.mac,
     }
+    if is_chrooted:
+      # do not try to load a rom file when we are running qemu chrooted
+      arguments.update({
+        "romfile": "",
+      })
     # Note that hvinfo that _GenerateDeviceHVInfo() creates
     # should include *only* the driver, id, bus, and addr keys
     arguments.update(self._filter_hvinfo(nic.hvinfo))
diff -Nru ganeti-3.0.1/lib/hypervisor/hv_kvm/validation.py ganeti-3.0.2/lib/hypervisor/hv_kvm/validation.py
--- ganeti-3.0.1/lib/hypervisor/hv_kvm/validation.py	1970-01-01 02:00:00.000000000 +0200
+++ ganeti-3.0.2/lib/hypervisor/hv_kvm/validation.py	2022-02-28 22:51:21.000000000 +0200
@@ -0,0 +1,206 @@
+#
+#
+
+# Copyright (C) 2022 the Ganeti project
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+"""KVM hypervisor parameter/syntax validation helpers
+
+"""
+
+import re
+import pwd
+
+from ganeti import constants
+from ganeti import netutils
+from ganeti import errors
+from ganeti import utils
+
+#: SPICE parameters which depend on L{constants.HV_KVM_SPICE_BIND}
+_SPICE_ADDITIONAL_PARAMS = frozenset([
+  constants.HV_KVM_SPICE_IP_VERSION,
+  constants.HV_KVM_SPICE_PASSWORD_FILE,
+  constants.HV_KVM_SPICE_LOSSLESS_IMG_COMPR,
+  constants.HV_KVM_SPICE_JPEG_IMG_COMPR,
+  constants.HV_KVM_SPICE_ZLIB_GLZ_IMG_COMPR,
+  constants.HV_KVM_SPICE_STREAMING_VIDEO_DETECTION,
+  constants.HV_KVM_SPICE_USE_TLS,
+  ])
+
+_SPICE_RE = re.compile(r"^-spice\s", re.M)
+_CHECK_MACHINE_VERSION_RE = [lambda x: re.compile(r"^(%s)[ ]+.*PC" % x, re.M)]
+
+
+def check_spice_parameters(hvparams):
+    spice_bind = hvparams[constants.HV_KVM_SPICE_BIND]
+    spice_ip_version = hvparams[constants.HV_KVM_SPICE_IP_VERSION]
+    if spice_bind:
+      if spice_ip_version != constants.IFACE_NO_IP_VERSION_SPECIFIED:
+        # if an IP version is specified, the spice_bind parameter must be an
+        # IP of that family
+        if (netutils.IP4Address.IsValid(spice_bind) and
+            spice_ip_version != constants.IP4_VERSION):
+          raise errors.HypervisorError("SPICE: Got an IPv4 address (%s), but"
+                                       " the specified IP version is %s" %
+                                       (spice_bind, spice_ip_version))
+
+        if (netutils.IP6Address.IsValid(spice_bind) and
+            spice_ip_version != constants.IP6_VERSION):
+          raise errors.HypervisorError("SPICE: Got an IPv6 address (%s), but"
+                                       " the specified IP version is %s" %
+                                       (spice_bind, spice_ip_version))
+    else:
+      # All the other SPICE parameters depend on spice_bind being set. Raise an
+      # error if any of them is set without it.
+      for param in _SPICE_ADDITIONAL_PARAMS:
+        if hvparams[param]:
+          raise errors.HypervisorError("SPICE: %s requires %s to be set" %
+                                       (param, constants.HV_KVM_SPICE_BIND))
+    return True
+
+
+def validate_spice_parameters(hvparams, kvm_help_output):
+    spice_bind = hvparams[constants.HV_KVM_SPICE_BIND]
+    if spice_bind:
+      # only one of VNC and SPICE can be used currently.
+      if hvparams[constants.HV_VNC_BIND_ADDRESS]:
+        raise errors.HypervisorError("Both SPICE and VNC are configured, but"
+                                     " only one of them can be used at a"
+                                     " given time")
+
+      # check that KVM supports SPICE
+
+      if not _SPICE_RE.search(kvm_help_output):
+        raise errors.HypervisorError("SPICE is configured, but it is not"
+                                     " supported according to 'kvm --help'")
+
+      # if spice_bind is not an IP address, it must be a valid interface
+      bound_to_addr = (netutils.IP4Address.IsValid(spice_bind) or
+                       netutils.IP6Address.IsValid(spice_bind))
+      if not bound_to_addr and not netutils.IsValidInterface(spice_bind):
+        raise errors.HypervisorError("SPICE: The %s parameter must be either"
+                                     " a valid IP address or interface name" %
+                                     constants.HV_KVM_SPICE_BIND)
+    return True
+
+
+def check_vnc_parameters(hvparams):
+    if (hvparams[constants.HV_VNC_X509_VERIFY] and
+        not hvparams[constants.HV_VNC_X509]):
+      raise errors.HypervisorError("%s must be defined, if %s is" %
+                                   (constants.HV_VNC_X509,
+                                    constants.HV_VNC_X509_VERIFY))
+    return True
+
+
+def validate_vnc_parameters(hvparams):
+    vnc_bind_address = hvparams[constants.HV_VNC_BIND_ADDRESS]
+    if vnc_bind_address:
+      bound_to_addr = (netutils.IP4Address.IsValid(vnc_bind_address) or
+                       netutils.IP6Address.IsValid(vnc_bind_address))
+      is_interface = netutils.IsValidInterface(vnc_bind_address)
+      is_path = utils.IsNormAbsPath(vnc_bind_address)
+      if not bound_to_addr and not is_interface and not is_path:
+        raise errors.HypervisorError("VNC: The %s parameter must be either"
+                                     " a valid IP address, an interface name,"
+                                     " or an absolute path" %
+                                     constants.HV_VNC_BIND_ADDRESS)
+    return True
+
+
+def check_security_model(hvparams):
+    security_model = hvparams[constants.HV_SECURITY_MODEL]
+    if security_model == constants.HT_SM_USER:
+        if not hvparams[constants.HV_SECURITY_DOMAIN]:
+            raise errors.HypervisorError(
+                "A security domain (user to run kvm as)"
+                " must be specified")
+    elif (security_model == constants.HT_SM_NONE or
+          security_model == constants.HT_SM_POOL):
+        if hvparams[constants.HV_SECURITY_DOMAIN]:
+            raise errors.HypervisorError(
+                "Cannot have a security domain when the"
+                " security model is 'none' or 'pool'")
+    return True
+
+
+def validate_security_model(hvparams):
+    security_model = hvparams[constants.HV_SECURITY_MODEL]
+    if security_model == constants.HT_SM_USER:
+        username = hvparams[constants.HV_SECURITY_DOMAIN]
+        try:
+            pwd.getpwnam(username)
+        except KeyError:
+            raise errors.HypervisorError("Unknown security domain user %s"
+                                         % username)
+    return True
+
+
+def check_boot_parameters(hvparams):
+    boot_order = hvparams[constants.HV_BOOT_ORDER]
+    if (boot_order == constants.HT_BO_CDROM and
+        not hvparams[constants.HV_CDROM_IMAGE_PATH]):
+      raise errors.HypervisorError("Cannot boot from cdrom without an"
+                                   " ISO path")
+    kernel_path = hvparams[constants.HV_KERNEL_PATH]
+    if kernel_path:
+      if not hvparams[constants.HV_ROOT_PATH]:
+        raise errors.HypervisorError("Need a root partition for the instance,"
+                                     " if a kernel is defined")
+    return True
+
+
+def check_console_parameters(hvparams):
+    if hvparams[constants.HV_SERIAL_CONSOLE]:
+      serial_speed = hvparams[constants.HV_SERIAL_SPEED]
+      valid_speeds = constants.VALID_SERIAL_SPEEDS
+      if not serial_speed or serial_speed not in valid_speeds:
+        raise errors.HypervisorError("Invalid serial console speed, must be"
+                                     " one of: %s" %
+                                     utils.CommaJoin(valid_speeds))
+    return True
+
+
+def validate_machine_version(hvparams, kvm_machine_output):
+    machine_version = hvparams[constants.HV_KVM_MACHINE_VERSION]
+    if machine_version:
+      for test in _CHECK_MACHINE_VERSION_RE:
+        if not test(machine_version).search(kvm_machine_output):
+            raise errors.HypervisorError("Unsupported machine version: %s" %
+                                         machine_version)
+    return True
+
+
+def check_disk_cache_parameters(hvparams):
+    disk_aio = hvparams[constants.HV_KVM_DISK_AIO]
+    disk_cache = hvparams[constants.HV_DISK_CACHE]
+    if disk_aio == constants.HT_KVM_AIO_NATIVE and \
+            disk_cache != constants.HT_CACHE_NONE:
+        raise errors.HypervisorError("When 'disk_aio' is set to 'native', the "
+                                     "only supported value for 'disk_cache' is "
+                                     "'none'.")
+    return True
diff -Nru ganeti-3.0.1/lib/hypervisor/hv_xen.py ganeti-3.0.2/lib/hypervisor/hv_xen.py
--- ganeti-3.0.1/lib/hypervisor/hv_xen.py	2021-02-03 10:24:16.000000000 +0200
+++ ganeti-3.0.2/lib/hypervisor/hv_xen.py	2022-02-28 22:51:21.000000000 +0200
@@ -180,6 +180,16 @@
   return info[1]
 
 
+def _InstanceRunning(info):
+  """Get instance runtime from instance info tuple.
+  @type info: tuple
+  @param info: instance info as parsed by _ParseInstanceList()
+
+  @return: bool
+  """
+  return info[4] == hv_base.HvInstanceState.RUNNING
+
+
 def _InstanceRuntime(info):
   """Get instance runtime from instance info tuple.
   @type info: tuple
@@ -780,10 +790,11 @@
     This version of the function just writes the config file from static data.
 
     """
+    cfg_file = self._ConfigFileName(instance_name)
+
     # just in case it exists
     utils.RemoveFile(utils.PathJoin(self._cfgdir, "auto", instance_name))
 
-    cfg_file = self._ConfigFileName(instance_name)
     try:
       utils.WriteFile(cfg_file, data=data)
     except EnvironmentError as err:
@@ -1065,10 +1076,10 @@
     if not force:
       self._ShutdownInstance(name, hvparams, timeout)
 
-    # TODO: Xen does always destroy the instnace after trying a gracefull
-    # shutdown. That means doing another attempt with force=True will not make
-    # any difference. This differs in behaviour from other hypervisors and
-    # should be cleaned up.
+    # TODO: Xen always destroys the instance after trying a graceful shutdown.
+    # That means doing another attempt with force=True will not make any
+    # difference. This differs in behaviour from other hypervisors and should
+    # be cleaned up.
     result = self._DestroyInstanceIfAlive(name, hvparams)
     if result is not None and result.failed and \
           self.GetInstanceInfo(name, hvparams=hvparams) is not None:
@@ -1295,7 +1306,7 @@
     # We should recreate the config file if the domain is present and running,
     # regardless if we think the migration succeeded or not.
     info = self.GetInstanceInfo(instance.name, hvparams=instance.hvparams)
-    if info and _InstanceRuntime(info) != 0:
+    if info and _InstanceRunning(info):
       self._WriteConfigFile(instance.name, config)
 
     if not success:
diff -Nru ganeti-3.0.1/lib/qlang.py ganeti-3.0.2/lib/qlang.py
--- ganeti-3.0.1/lib/qlang.py	2021-02-03 10:24:16.000000000 +0200
+++ ganeti-3.0.2/lib/qlang.py	2022-02-28 22:51:21.000000000 +0200
@@ -221,7 +221,7 @@
                glob_cond ^ not_glob_cond)
 
   # Associativity operators
-  filter_expr = pyp.operatorPrecedence(condition, [
+  filter_expr = pyp.infixNotation(condition, [
     (pyp.Keyword("not").suppress(), 1, pyp.opAssoc.RIGHT,
      lambda toks: [[OP_NOT, toks[0][0]]]),
     (pyp.Keyword("and").suppress(), 2, pyp.opAssoc.LEFT,
diff -Nru ganeti-3.0.1/lib/storage/bdev.py ganeti-3.0.2/lib/storage/bdev.py
--- ganeti-3.0.1/lib/storage/bdev.py	2021-02-03 10:24:16.000000000 +0200
+++ ganeti-3.0.2/lib/storage/bdev.py	2022-02-28 22:51:21.000000000 +0200
@@ -215,7 +215,10 @@
     # create an optimally-striped volume; in that case, we want to try
     # with N, N-1, ..., 2, and finally 1 (non-stripped) number of
     # stripes
-    cmd = ["lvcreate", "-L%dm" % size, "-n%s" % lv_name]
+    # When run non-interactively, newer LVM versions will fail (unless
+    # `--yes` is specified) when an existing filesystem signature is
+    # encountered while creating a new LV. Using `-Wn` disables this check.
+    cmd = ["lvcreate", "-Wn", "-L%dm" % size, "-n%s" % lv_name]
     for stripes_arg in range(stripes, 0, -1):
       result = utils.RunCmd(cmd + ["-i%d" % stripes_arg] + [vg_name] + pvlist)
       if not result.failed:
diff -Nru ganeti-3.0.1/lib/tools/common.py ganeti-3.0.2/lib/tools/common.py
--- ganeti-3.0.1/lib/tools/common.py	2021-02-03 10:24:16.000000000 +0200
+++ ganeti-3.0.2/lib/tools/common.py	2022-02-28 22:51:21.000000000 +0200
@@ -99,7 +99,7 @@
   cert_encoded = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM,
                                                  cert)
   complete_cert_encoded = key_encoded + cert_encoded
-  if not cert_pem == complete_cert_encoded:
+  if not cert_pem == complete_cert_encoded.decode('ascii'):
     logging.error("The certificate differs after being reencoded. Please"
                   " renew the certificates cluster-wide to prevent future"
                   " inconsistencies.")
diff -Nru ganeti-3.0.1/Makefile.am ganeti-3.0.2/Makefile.am
--- ganeti-3.0.1/Makefile.am	2021-02-03 10:24:16.000000000 +0200
+++ ganeti-3.0.2/Makefile.am	2022-02-28 22:51:21.000000000 +0200
@@ -545,7 +545,8 @@
 hypervisor_hv_kvm_PYTHON = \
 	lib/hypervisor/hv_kvm/__init__.py \
 	lib/hypervisor/hv_kvm/monitor.py \
-	lib/hypervisor/hv_kvm/netdev.py
+	lib/hypervisor/hv_kvm/netdev.py \
+	lib/hypervisor/hv_kvm/validation.py
 
 jqueue_PYTHON = \
 	lib/jqueue/__init__.py \
@@ -1846,6 +1847,7 @@
 	test/data/kvm_0.9.1_help_boot_test.txt \
 	test/data/kvm_1.0_help.txt \
 	test/data/kvm_1.1.2_help.txt \
+	test/data/kvm_6.0.0_machine.txt \
 	test/data/kvm_runtime.json \
 	test/data/lvs_lv.txt \
 	test/data/NEWS_OK.txt \
diff -Nru ganeti-3.0.1/man/gnt-instance.rst ganeti-3.0.2/man/gnt-instance.rst
--- ganeti-3.0.1/man/gnt-instance.rst	2021-02-03 10:24:16.000000000 +0200
+++ ganeti-3.0.2/man/gnt-instance.rst	2022-02-28 22:51:21.000000000 +0200
@@ -904,6 +904,18 @@
 
     Path to the userspace KVM (or qemu) program.
 
+vhost\_net
+    Valid for the KVM hypervisor.
+
+    This boolean option determines whether the tap devices used by the KVM
+    paravirtual nics (virtio-net) will use accelerated data plane, passing
+    network packets directly between host and guest kernel, without going
+    through userspace emulation layer (qemu).
+
+    Historically it is set to ``false`` by default. New Clusters created
+    with Ganeti-3.1 and newer defaults to ``true``. Everyone is encouraged to
+    enable it.
+
 vnet\_hdr
     Valid for the KVM hypervisor.
 
diff -Nru ganeti-3.0.1/NEWS ganeti-3.0.2/NEWS
--- ganeti-3.0.1/NEWS	2021-02-03 10:24:16.000000000 +0200
+++ ganeti-3.0.2/NEWS	2022-02-28 22:51:21.000000000 +0200
@@ -2,6 +2,39 @@
 ====
 
 
+Version 3.0.2
+-------------
+
+*(Released Mon, 28 Feb 2022)*
+
+Changes since 3.0.1
+~~~~~~~~~~~~~~~~~~~
+
+This release contains the following bug- and compatibility fixes:
+
+ - KVM: fix NIC hotplugging with ``vhost_net=True`` (#1651),
+   ``use_chroot=True`` (#1644) and ``use_guest_agent=True`` (#1620).
+ - KVM: fix asynchronous events breaking QMP handshakes (#1649)
+ - KVM: handle ``disk_cache`` consistently between boot and hotplugging
+   (#1645)
+ - KVM: fix live migration with non-root / chrooted QEMU (dynamic auto-ro)
+   (#1603)
+ - KVM: fix unsupported keymap include in >=qemu-4.0 (#1612)
+ - XEN: fix live migration of xen instances (#1582)
+ - NET: relax VLAN check with veth devices (#1533)
+ - LVM: fix lvcreate for newer lvm versions (#1586)
+ - DRBD: warn users that altered DRBD parameters do not affect existing
+   devices (#781)
+ - Node-Add: byte/string comparison causes false-positive warning (#1635)
+ - RAPI: return HTTP 400 on request parse error (#1610)
+ - build: fix building docs on Debian Bullseye (#1602)
+ - build: adjust for Pyparsing 3.0 (#1638)
+ - build: adjust for TupE type change in Template Haskell 2.16 (#1613)
+ - build: permit base64-bytestring 1.1 and QuickCheck 2.14 (#1613)
+ - tools: fix 2to3 leftover for move-instance (#1616)
+ - Docs: fix building on recent sphinx versions (#1602)
+
+
 Version 3.0.1
 -------------
 
diff -Nru ganeti-3.0.1/src/Ganeti/THH/Compat.hs ganeti-3.0.2/src/Ganeti/THH/Compat.hs
--- ganeti-3.0.1/src/Ganeti/THH/Compat.hs	2021-02-03 10:24:16.000000000 +0200
+++ ganeti-3.0.2/src/Ganeti/THH/Compat.hs	2022-02-28 22:51:21.000000000 +0200
@@ -6,7 +6,7 @@
 
 {-
 
-Copyright (C) 2018 Ganeti Project Contributors.
+Copyright (C) 2018, 2021 Ganeti Project Contributors.
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without
@@ -39,6 +39,7 @@
   , gntDataD
   , extractDataDConstructors
   , myNotStrict
+  , nonUnaryTupE
   ) where
 
 import Language.Haskell.TH
@@ -104,3 +105,12 @@
 #else
 myNotStrict = NotStrict
 #endif
+
+-- | TupE changed from '[Exp] -> Exp' to '[Maybe Exp] -> Exp'.
+-- Provide the old signature for compatibility.
+nonUnaryTupE :: [Exp] -> Exp
+#if MIN_VERSION_template_haskell(2,16,0)
+nonUnaryTupE es = TupE $ map Just es
+#else
+nonUnaryTupE es = TupE $ es
+#endif
diff -Nru ganeti-3.0.1/src/Ganeti/THH/Types.hs ganeti-3.0.2/src/Ganeti/THH/Types.hs
--- ganeti-3.0.1/src/Ganeti/THH/Types.hs	2021-02-03 10:24:16.000000000 +0200
+++ ganeti-3.0.2/src/Ganeti/THH/Types.hs	2022-02-28 22:51:21.000000000 +0200
@@ -49,6 +49,7 @@
 import Control.Monad (liftM, replicateM)
 import Language.Haskell.TH
 import qualified Text.JSON as J
+import Ganeti.THH.Compat (nonUnaryTupE)
 
 -- | This fills the gap between @()@ and @(,)@, providing a wrapper for
 -- 1-element tuples. It's needed for RPC, where arguments for a function are
@@ -123,4 +124,4 @@
   f <- newName "f"
   ps <- replicateM n (newName "x")
   return $ LamE (VarP f : map VarP ps)
-             (AppE (VarE f) (TupE $ map VarE ps))
+             (AppE (VarE f) (nonUnaryTupE $ map VarE ps))
diff -Nru ganeti-3.0.1/test/data/kvm_6.0.0_machine.txt ganeti-3.0.2/test/data/kvm_6.0.0_machine.txt
--- ganeti-3.0.1/test/data/kvm_6.0.0_machine.txt	1970-01-01 02:00:00.000000000 +0200
+++ ganeti-3.0.2/test/data/kvm_6.0.0_machine.txt	2022-02-28 22:51:21.000000000 +0200
@@ -0,0 +1,96 @@
+Supported machines are:
+microvm              microvm (i386)
+pc-i440fx-zesty      Ubuntu 17.04 PC (i440FX + PIIX, 1996)
+pc-i440fx-yakkety    Ubuntu 16.10 PC (i440FX + PIIX, 1996)
+pc-i440fx-xenial     Ubuntu 16.04 PC (i440FX + PIIX, 1996)
+pc-i440fx-wily       Ubuntu 15.04 PC (i440FX + PIIX, 1996)
+pc-i440fx-trusty     Ubuntu 14.04 PC (i440FX + PIIX, 1996)
+ubuntu               Ubuntu 21.10 PC (i440FX + PIIX, 1996) (alias of pc-i440fx-impish)
+pc-i440fx-impish     Ubuntu 21.10 PC (i440FX + PIIX, 1996) (default)
+pc-i440fx-impish-hpb Ubuntu 21.10 PC (i440FX + PIIX +host-phys-bits=true, 1996)
+pc-i440fx-hirsute    Ubuntu 21.04 PC (i440FX + PIIX, 1996)
+pc-i440fx-hirsute-hpb Ubuntu 21.04 PC (i440FX + PIIX +host-phys-bits=true, 1996)
+pc-i440fx-groovy     Ubuntu 20.10 PC (i440FX + PIIX, 1996)
+pc-i440fx-groovy-hpb Ubuntu 20.10 PC (i440FX + PIIX +host-phys-bits=true, 1996)
+pc-i440fx-focal      Ubuntu 20.04 PC (i440FX + PIIX, 1996)
+pc-i440fx-focal-hpb  Ubuntu 20.04 PC (i440FX + PIIX +host-phys-bits=true, 1996)
+pc-i440fx-eoan       Ubuntu 19.10 PC (i440FX + PIIX, 1996)
+pc-i440fx-eoan-hpb   Ubuntu 19.10 PC (i440FX + PIIX +host-phys-bits=true, 1996)
+pc-i440fx-disco      Ubuntu 19.04 PC (i440FX + PIIX, 1996)
+pc-i440fx-disco-hpb  Ubuntu 19.04 PC (i440FX + PIIX +host-phys-bits=true, 1996)
+pc-i440fx-cosmic     Ubuntu 18.10 PC (i440FX + PIIX, 1996)
+pc-i440fx-cosmic-hpb Ubuntu 18.10 PC (i440FX + PIIX +host-phys-bits=true, 1996)
+pc-i440fx-bionic     Ubuntu 18.04 PC (i440FX + PIIX, 1996)
+pc-i440fx-bionic-hpb Ubuntu 18.04 PC (i440FX + PIIX, +host-phys-bits=true, 1996)
+pc-i440fx-artful     Ubuntu 17.10 PC (i440FX + PIIX, 1996)
+pc                   Standard PC (i440FX + PIIX, 1996) (alias of pc-i440fx-6.0)
+pc-i440fx-6.0        Standard PC (i440FX + PIIX, 1996)
+pc-i440fx-5.2        Standard PC (i440FX + PIIX, 1996)
+pc-i440fx-5.1        Standard PC (i440FX + PIIX, 1996)
+pc-i440fx-5.0        Standard PC (i440FX + PIIX, 1996)
+pc-i440fx-4.2        Standard PC (i440FX + PIIX, 1996)
+pc-i440fx-4.1        Standard PC (i440FX + PIIX, 1996)
+pc-i440fx-4.0        Standard PC (i440FX + PIIX, 1996)
+pc-i440fx-3.1        Standard PC (i440FX + PIIX, 1996)
+pc-i440fx-3.0        Standard PC (i440FX + PIIX, 1996)
+pc-i440fx-2.9        Standard PC (i440FX + PIIX, 1996)
+pc-i440fx-2.8        Standard PC (i440FX + PIIX, 1996)
+pc-i440fx-2.7        Standard PC (i440FX + PIIX, 1996)
+pc-i440fx-2.6        Standard PC (i440FX + PIIX, 1996)
+pc-i440fx-2.5        Standard PC (i440FX + PIIX, 1996)
+pc-i440fx-2.4        Standard PC (i440FX + PIIX, 1996)
+pc-i440fx-2.3        Standard PC (i440FX + PIIX, 1996)
+pc-i440fx-2.2        Standard PC (i440FX + PIIX, 1996)
+pc-i440fx-2.12       Standard PC (i440FX + PIIX, 1996)
+pc-i440fx-2.11       Standard PC (i440FX + PIIX, 1996)
+pc-i440fx-2.10       Standard PC (i440FX + PIIX, 1996)
+pc-i440fx-2.1        Standard PC (i440FX + PIIX, 1996)
+pc-i440fx-2.0        Standard PC (i440FX + PIIX, 1996)
+pc-i440fx-1.7        Standard PC (i440FX + PIIX, 1996)
+pc-i440fx-1.6        Standard PC (i440FX + PIIX, 1996)
+pc-i440fx-1.5        Standard PC (i440FX + PIIX, 1996)
+pc-i440fx-1.4        Standard PC (i440FX + PIIX, 1996)
+pc-q35-zesty         Ubuntu 17.04 PC (Q35 + ICH9, 2009)
+pc-q35-yakkety       Ubuntu 16.10 PC (Q35 + ICH9, 2009)
+pc-q35-xenial        Ubuntu 16.04 PC (Q35 + ICH9, 2009)
+ubuntu-q35           Ubuntu 21.10 PC (Q35 + ICH9, 2009) (alias of pc-q35-impish)
+pc-q35-impish        Ubuntu 21.10 PC (Q35 + ICH9, 2009)
+pc-q35-impish-hpb    Ubuntu 21.10 PC (Q35 + ICH9, +host-phys-bits=true, 2009)
+pc-q35-hirsute       Ubuntu 21.04 PC (Q35 + ICH9, 2009)
+pc-q35-hirsute-hpb   Ubuntu 21.04 PC (Q35 + ICH9, +host-phys-bits=true, 2009)
+pc-q35-groovy        Ubuntu 20.10 PC (Q35 + ICH9, 2009)
+pc-q35-groovy-hpb    Ubuntu 20.10 PC (Q35 + ICH9, +host-phys-bits=true, 2009)
+pc-q35-focal         Ubuntu 20.04 PC (Q35 + ICH9, 2009)
+pc-q35-focal-hpb     Ubuntu 20.04 PC (Q35 + ICH9, +host-phys-bits=true, 2009)
+pc-q35-eoan          Ubuntu 19.10 PC (Q35 + ICH9, 2009)
+pc-q35-eoan-hpb      Ubuntu 19.10 PC (Q35 + ICH9, +host-phys-bits=true, 2009)
+pc-q35-disco         Ubuntu 19.04 PC (Q35 + ICH9, 2009)
+pc-q35-disco-hpb     Ubuntu 19.04 PC (Q35 + ICH9, +host-phys-bits=true, 2009)
+pc-q35-cosmic        Ubuntu 18.10 PC (Q35 + ICH9, 2009)
+pc-q35-cosmic-hpb    Ubuntu 18.10 PC (Q35 + ICH9, +host-phys-bits=true, 2009)
+pc-q35-bionic        Ubuntu 18.04 PC (Q35 + ICH9, 2009)
+pc-q35-bionic-hpb    Ubuntu 18.04 PC (Q35 + ICH9, +host-phys-bits=true, 2009)
+pc-q35-artful        Ubuntu 17.10 PC (Q35 + ICH9, 2009)
+q35                  Standard PC (Q35 + ICH9, 2009) (alias of pc-q35-6.0)
+pc-q35-6.0           Standard PC (Q35 + ICH9, 2009)
+pc-q35-5.2           Standard PC (Q35 + ICH9, 2009)
+pc-q35-5.1           Standard PC (Q35 + ICH9, 2009)
+pc-q35-5.0           Standard PC (Q35 + ICH9, 2009)
+pc-q35-4.2           Standard PC (Q35 + ICH9, 2009)
+pc-q35-4.1           Standard PC (Q35 + ICH9, 2009)
+pc-q35-4.0.1         Standard PC (Q35 + ICH9, 2009)
+pc-q35-4.0           Standard PC (Q35 + ICH9, 2009)
+pc-q35-3.1           Standard PC (Q35 + ICH9, 2009)
+pc-q35-3.0           Standard PC (Q35 + ICH9, 2009)
+pc-q35-2.9           Standard PC (Q35 + ICH9, 2009)
+pc-q35-2.8           Standard PC (Q35 + ICH9, 2009)
+pc-q35-2.7           Standard PC (Q35 + ICH9, 2009)
+pc-q35-2.6           Standard PC (Q35 + ICH9, 2009)
+pc-q35-2.5           Standard PC (Q35 + ICH9, 2009)
+pc-q35-2.4           Standard PC (Q35 + ICH9, 2009)
+pc-q35-2.12          Standard PC (Q35 + ICH9, 2009)
+pc-q35-2.11          Standard PC (Q35 + ICH9, 2009)
+pc-q35-2.10          Standard PC (Q35 + ICH9, 2009)
+isapc                ISA-only PC
+none                 empty machine
+x-remote             Experimental remote machine
\ No newline at end of file
diff -Nru ganeti-3.0.1/test/py/ganeti.hypervisor.hv_kvm_unittest.py ganeti-3.0.2/test/py/ganeti.hypervisor.hv_kvm_unittest.py
--- ganeti-3.0.1/test/py/ganeti.hypervisor.hv_kvm_unittest.py	2021-02-03 10:24:16.000000000 +0200
+++ ganeti-3.0.2/test/py/ganeti.hypervisor.hv_kvm_unittest.py	2022-02-28 22:51:21.000000000 +0200
@@ -49,6 +49,7 @@
 from ganeti.hypervisor import hv_kvm
 import ganeti.hypervisor.hv_kvm.netdev as netdev
 import ganeti.hypervisor.hv_kvm.monitor as monitor
+import ganeti.hypervisor.hv_kvm.validation as validation
 
 import mock
 import testutils
@@ -149,6 +150,468 @@
     self.socket.close()
 
 
+class TestParameterCheck(testutils.GanetiTestCase):
+  def testInvalidVncParameters(self):
+    invalid_data = {
+      constants.HV_VNC_X509_VERIFY: True,
+      constants.HV_VNC_X509: None
+    }
+
+    self.assertRaises(errors.HypervisorError,
+                      validation.check_vnc_parameters, invalid_data)
+
+  def testValidVncParameters(self):
+    valid_data = {
+      constants.HV_VNC_X509_VERIFY: True,
+      constants.HV_VNC_X509: "mycert.pem"
+    }
+
+    self.assertTrue(validation.check_vnc_parameters(valid_data))
+
+  def testInvalidSecurityModel(self):
+    invalid_data = {
+      constants.HV_SECURITY_MODEL: constants.HT_SM_USER,
+      constants.HV_SECURITY_DOMAIN: None
+    }
+
+    self.assertRaises(errors.HypervisorError,
+                      validation.check_security_model, invalid_data)
+
+    invalid_data = {
+      constants.HV_SECURITY_MODEL: constants.HT_SM_NONE,
+      constants.HV_SECURITY_DOMAIN: "secure_user"
+    }
+
+    self.assertRaises(errors.HypervisorError,
+                      validation.check_security_model, invalid_data)
+
+    invalid_data = {
+      constants.HV_SECURITY_MODEL: constants.HT_SM_POOL,
+      constants.HV_SECURITY_DOMAIN: "secure_user"
+    }
+
+    self.assertRaises(errors.HypervisorError,
+                      validation.check_security_model, invalid_data)
+
+  def testValidSecurityModel(self):
+    valid_data = {
+      constants.HV_SECURITY_MODEL: constants.HT_SM_USER,
+      constants.HV_SECURITY_DOMAIN: "secure_user"
+    }
+
+    self.assertTrue(validation.check_security_model(valid_data))
+
+    valid_data = {
+      constants.HV_SECURITY_MODEL: constants.HT_SM_POOL,
+      constants.HV_SECURITY_DOMAIN: None
+    }
+
+    self.assertTrue(validation.check_security_model(valid_data))
+
+    valid_data = {
+      constants.HV_SECURITY_MODEL: constants.HT_SM_NONE,
+      constants.HV_SECURITY_DOMAIN: None
+    }
+
+    self.assertTrue(validation.check_security_model(valid_data))
+
+  def testInvalidBootParameters(self):
+    invalid_data = {
+      constants.HV_BOOT_ORDER: constants.HT_BO_CDROM,
+      constants.HV_CDROM_IMAGE_PATH: None,
+      constants.HV_KERNEL_PATH: "/some/path",
+      constants.HV_ROOT_PATH: "/"
+    }
+
+    self.assertRaises(errors.HypervisorError,
+                      validation.check_boot_parameters, invalid_data)
+
+    invalid_data = {
+      constants.HV_BOOT_ORDER: constants.HT_BO_CDROM,
+      constants.HV_CDROM_IMAGE_PATH: "/cd.iso",
+      constants.HV_KERNEL_PATH: "/some/path",
+      constants.HV_ROOT_PATH: None
+    }
+
+    self.assertRaises(errors.HypervisorError,
+                      validation.check_boot_parameters, invalid_data)
+
+  def testValidBootParameters(self):
+    valid_data = {
+      constants.HV_BOOT_ORDER: constants.HT_BO_CDROM,
+      constants.HV_CDROM_IMAGE_PATH: "/cd.iso",
+      constants.HV_KERNEL_PATH: "/some/path",
+      constants.HV_ROOT_PATH: "/"
+    }
+
+    self.assertTrue(validation.check_boot_parameters(valid_data))
+
+    valid_data = {
+      constants.HV_BOOT_ORDER: constants.HT_BO_DISK,
+      constants.HV_CDROM_IMAGE_PATH: None,
+      constants.HV_KERNEL_PATH: "/some/path",
+      constants.HV_ROOT_PATH: "/"
+    }
+
+    self.assertTrue(validation.check_boot_parameters(valid_data))
+
+    valid_data = {
+      constants.HV_BOOT_ORDER: constants.HT_BO_DISK,
+      constants.HV_CDROM_IMAGE_PATH: None,
+      constants.HV_KERNEL_PATH: None,
+      constants.HV_ROOT_PATH: None
+    }
+
+    self.assertTrue(validation.check_boot_parameters(valid_data))
+
+  def testInvalidConsoleParameters(self):
+    invalid_data = {
+      constants.HV_SERIAL_CONSOLE: True,
+      constants.HV_SERIAL_SPEED: None,
+    }
+
+    self.assertRaises(errors.HypervisorError,
+                      validation.check_console_parameters, invalid_data)
+
+    invalid_data = {
+      constants.HV_SERIAL_CONSOLE: True,
+      constants.HV_SERIAL_SPEED: 1,
+    }
+
+    self.assertRaises(errors.HypervisorError,
+                      validation.check_console_parameters, invalid_data)
+
+  def testValidConsoleParameters(self):
+    valid_data = {
+      constants.HV_SERIAL_CONSOLE: False
+    }
+
+    self.assertTrue(validation.check_console_parameters(valid_data))
+
+    for speed in constants.VALID_SERIAL_SPEEDS:
+      valid_data = {
+        constants.HV_SERIAL_CONSOLE: True,
+        constants.HV_SERIAL_SPEED: speed
+      }
+
+      self.assertTrue(validation.check_console_parameters(valid_data),
+                      "Testing serial console speed %d" % speed)
+
+  def testInvalidSpiceParameters(self):
+    invalid_data = {
+      constants.HV_KVM_SPICE_BIND: "0.0.0.0",
+      constants.HV_KVM_SPICE_IP_VERSION: constants.IP6_VERSION
+    }
+
+    self.assertRaises(errors.HypervisorError,
+                      validation.check_spice_parameters, invalid_data)
+
+    invalid_data = {
+      constants.HV_KVM_SPICE_BIND: "::",
+      constants.HV_KVM_SPICE_IP_VERSION: constants.IP4_VERSION
+    }
+
+    self.assertRaises(errors.HypervisorError,
+                      validation.check_spice_parameters, invalid_data)
+
+    invalid_data = {
+      constants.HV_KVM_SPICE_BIND: None,
+      constants.HV_KVM_SPICE_IP_VERSION: None,
+      constants.HV_KVM_SPICE_PASSWORD_FILE: "password.txt",
+      constants.HV_KVM_SPICE_LOSSLESS_IMG_COMPR: None,
+      constants.HV_KVM_SPICE_JPEG_IMG_COMPR: None,
+      constants.HV_KVM_SPICE_ZLIB_GLZ_IMG_COMPR: None,
+      constants.HV_KVM_SPICE_STREAMING_VIDEO_DETECTION: None,
+      constants.HV_KVM_SPICE_USE_TLS: True
+    }
+
+    self.assertRaises(errors.HypervisorError,
+                      validation.check_spice_parameters, invalid_data)
+
+  def testValidSpiceParameters(self):
+    valid_data = {
+      constants.HV_KVM_SPICE_BIND: None,
+      constants.HV_KVM_SPICE_IP_VERSION: None,
+      constants.HV_KVM_SPICE_PASSWORD_FILE: None,
+      constants.HV_KVM_SPICE_LOSSLESS_IMG_COMPR: None,
+      constants.HV_KVM_SPICE_JPEG_IMG_COMPR: None,
+      constants.HV_KVM_SPICE_ZLIB_GLZ_IMG_COMPR: None,
+      constants.HV_KVM_SPICE_STREAMING_VIDEO_DETECTION: None,
+      constants.HV_KVM_SPICE_USE_TLS: None
+    }
+
+    self.assertTrue(validation.check_spice_parameters(valid_data))
+
+    valid_data = {
+      constants.HV_KVM_SPICE_BIND: "0.0.0.0",
+      constants.HV_KVM_SPICE_IP_VERSION: constants.IP4_VERSION,
+      constants.HV_KVM_SPICE_PASSWORD_FILE: "password.txt",
+      constants.HV_KVM_SPICE_LOSSLESS_IMG_COMPR: "glz",
+      constants.HV_KVM_SPICE_JPEG_IMG_COMPR: "never",
+      constants.HV_KVM_SPICE_ZLIB_GLZ_IMG_COMPR: "never",
+      constants.HV_KVM_SPICE_STREAMING_VIDEO_DETECTION: "off",
+      constants.HV_KVM_SPICE_USE_TLS: True
+    }
+
+    self.assertTrue(validation.check_spice_parameters(valid_data))
+
+    valid_data = {
+      constants.HV_KVM_SPICE_BIND: "::",
+      constants.HV_KVM_SPICE_IP_VERSION: constants.IP6_VERSION,
+      constants.HV_KVM_SPICE_PASSWORD_FILE: "password.txt",
+      constants.HV_KVM_SPICE_LOSSLESS_IMG_COMPR: "glz",
+      constants.HV_KVM_SPICE_JPEG_IMG_COMPR: "never",
+      constants.HV_KVM_SPICE_ZLIB_GLZ_IMG_COMPR: "never",
+      constants.HV_KVM_SPICE_STREAMING_VIDEO_DETECTION: "off",
+      constants.HV_KVM_SPICE_USE_TLS: True
+    }
+
+    self.assertTrue(validation.check_spice_parameters(valid_data))
+
+  def testInvalidDiskCacheParameters(self):
+    invalid_data = {
+      constants.HV_KVM_DISK_AIO: constants.HT_KVM_AIO_NATIVE,
+      constants.HV_DISK_CACHE: constants.HT_CACHE_WBACK
+    }
+
+    self.assertRaises(errors.HypervisorError,
+                      validation.check_disk_cache_parameters, invalid_data)
+
+    invalid_data = {
+      constants.HV_KVM_DISK_AIO: constants.HT_KVM_AIO_NATIVE,
+      constants.HV_DISK_CACHE: constants.HT_CACHE_WTHROUGH
+    }
+
+    self.assertRaises(errors.HypervisorError,
+                      validation.check_disk_cache_parameters, invalid_data)
+
+    invalid_data = {
+      constants.HV_KVM_DISK_AIO: constants.HT_KVM_AIO_NATIVE,
+      constants.HV_DISK_CACHE: constants.HT_CACHE_DEFAULT
+    }
+
+    self.assertRaises(errors.HypervisorError,
+                      validation.check_disk_cache_parameters, invalid_data)
+
+  def testValidDiskCacheParameters(self):
+    valid_data = {
+      constants.HV_KVM_DISK_AIO: constants.HT_KVM_AIO_THREADS,
+      constants.HV_DISK_CACHE: constants.HT_CACHE_WBACK
+    }
+
+    self.assertTrue(validation.check_disk_cache_parameters(valid_data))
+
+    valid_data = {
+      constants.HV_KVM_DISK_AIO: constants.HT_KVM_AIO_THREADS,
+      constants.HV_DISK_CACHE: constants.HT_CACHE_WTHROUGH
+    }
+
+    self.assertTrue(validation.check_disk_cache_parameters(valid_data))
+
+    valid_data = {
+      constants.HV_KVM_DISK_AIO: constants.HT_KVM_AIO_THREADS,
+      constants.HV_DISK_CACHE: constants.HT_CACHE_DEFAULT
+    }
+
+    self.assertTrue(validation.check_disk_cache_parameters(valid_data))
+
+    valid_data = {
+      constants.HV_KVM_DISK_AIO: constants.HT_KVM_AIO_NATIVE,
+      constants.HV_DISK_CACHE: constants.HT_CACHE_NONE
+    }
+
+    self.assertTrue(validation.check_disk_cache_parameters(valid_data))
+
+
+class TestParameterValidation(testutils.GanetiTestCase):
+  def testInvalidVncParameters(self):
+    # invalid IPv4 address
+    invalid_data = {
+      constants.HV_VNC_BIND_ADDRESS: "192.0.2.5.5",
+    }
+
+    self.assertRaises(errors.HypervisorError,
+                      validation.validate_vnc_parameters, invalid_data)
+
+    # invalid network interface
+    invalid_data = {
+      constants.HV_VNC_BIND_ADDRESS: "doesnotexist0",
+    }
+
+    self.assertRaises(errors.HypervisorError,
+                      validation.validate_vnc_parameters, invalid_data)
+
+  def testValidVncParameters(self):
+    valid_data = {
+      constants.HV_VNC_BIND_ADDRESS: "127.0.0.1"
+    }
+
+    self.assertTrue(validation.validate_vnc_parameters(valid_data))
+
+    valid_data = {
+      constants.HV_VNC_BIND_ADDRESS: "lo"
+    }
+
+    self.assertTrue(validation.validate_vnc_parameters(valid_data))
+
+  def testInvalidSecurityModelParameters(self):
+    invalid_data = {
+      constants.HV_SECURITY_MODEL: constants.HT_SM_USER,
+      constants.HV_SECURITY_DOMAIN: "really-non-existing-user"
+    }
+
+    self.assertRaises(errors.HypervisorError,
+                      validation.validate_security_model, invalid_data)
+
+  def testValidSecurityModelParameters(self):
+    valid_data = {
+      constants.HV_SECURITY_MODEL: constants.HT_SM_NONE
+    }
+
+    self.assertTrue(validation.validate_security_model(valid_data))
+
+    valid_data = {
+      constants.HV_SECURITY_MODEL: constants.HT_SM_POOL
+    }
+
+    self.assertTrue(validation.validate_security_model(valid_data))
+
+    valid_data = {
+      constants.HV_SECURITY_MODEL: constants.HT_SM_USER,
+      constants.HV_SECURITY_DOMAIN: "root"
+    }
+
+    self.assertTrue(validation.validate_security_model(valid_data))
+
+  def testInvalidMachineVersion(self):
+    kvm_machine_output = testutils.ReadTestData("kvm_6.0.0_machine.txt")
+    invalid_data = {
+      constants.HV_KVM_MACHINE_VERSION: "some-invalid-machine-type"
+    }
+    self.assertRaises(errors.HypervisorError,
+                      validation.validate_machine_version, invalid_data,
+                      kvm_machine_output)
+
+  def testValidMachineVersion(self):
+    kvm_machine_output = testutils.ReadTestData("kvm_6.0.0_machine.txt")
+    valid_data = {
+      constants.HV_KVM_MACHINE_VERSION: "pc-i440fx-6.0"
+    }
+    self.assertTrue(validation.validate_machine_version(valid_data,
+                                                        kvm_machine_output))
+
+  def testInvalidSpiceParameters(self):
+    kvm_help_too_old = testutils.ReadTestData("kvm_0.9.1_help.txt")
+    kvm_help_working = testutils.ReadTestData("kvm_1.1.2_help.txt")
+
+    invalid_data = {
+      constants.HV_KVM_SPICE_BIND: "0.0.0.0",
+      constants.HV_VNC_BIND_ADDRESS: "0.0.0.0"
+    }
+
+    self.assertRaises(errors.HypervisorError,
+                      validation.validate_spice_parameters, invalid_data,
+                      kvm_help_working)
+
+    invalid_data = {
+      constants.HV_KVM_SPICE_BIND: "0.0.0.0",
+      constants.HV_VNC_BIND_ADDRESS: None
+    }
+
+    self.assertRaises(errors.HypervisorError,
+                      validation.validate_spice_parameters, invalid_data,
+                      kvm_help_too_old)
+
+    invalid_data = {
+      constants.HV_KVM_SPICE_BIND: "invalid-interface0",
+      constants.HV_VNC_BIND_ADDRESS: None
+    }
+
+    self.assertRaises(errors.HypervisorError,
+                      validation.validate_spice_parameters, invalid_data,
+                      kvm_help_working)
+
+  def testValidSpiceParameters(self):
+    kvm_help_working = testutils.ReadTestData("kvm_1.1.2_help.txt")
+
+    valid_data = {
+      constants.HV_KVM_SPICE_BIND: "0.0.0.0",
+      constants.HV_VNC_BIND_ADDRESS: None
+    }
+
+    self.assertTrue(validation.validate_spice_parameters(valid_data,
+                                                         kvm_help_working))
+
+    valid_data = {
+      constants.HV_KVM_SPICE_BIND: "::",
+      constants.HV_VNC_BIND_ADDRESS: None
+    }
+
+    self.assertTrue(validation.validate_spice_parameters(valid_data,
+                                                         kvm_help_working))
+
+    valid_data = {
+      constants.HV_KVM_SPICE_BIND: "lo",
+      constants.HV_VNC_BIND_ADDRESS: None
+    }
+
+    self.assertTrue(validation.validate_spice_parameters(valid_data,
+                                                         kvm_help_working))
+
+
+class TestDiskParameters(testutils.GanetiTestCase):
+  def testGenerateDiskAioCacheParameters(self):
+    test_cases = {
+      "aio_threaded_safe_storage_default_cache": {
+        "disk_aio": constants.HT_KVM_AIO_THREADS,
+        "disk_cache": constants.HT_CACHE_DEFAULT,
+        "dev_type": constants.DT_DRBD8,
+        "expected_string": ",aio=threads"
+      },
+      "aio_threaded_unsafe_storage_default_cache": {
+        "disk_aio": constants.HT_KVM_AIO_THREADS,
+        "disk_cache": constants.HT_CACHE_DEFAULT,
+        "dev_type": constants.DT_SHARED_FILE,
+        "expected_string": ",aio=threads,cache=none"
+      },
+      "aio_threaded_safe_storage_writeback_cache": {
+        "disk_aio": constants.HT_KVM_AIO_THREADS,
+        "disk_cache": constants.HT_CACHE_WBACK,
+        "dev_type": constants.DT_RBD,
+        "expected_string": ",aio=threads,cache=writeback"
+      },
+      "aio_native_safe_storage_none_cache": {
+        "disk_aio": constants.HT_KVM_AIO_NATIVE,
+        "disk_cache": constants.HT_CACHE_NONE,
+        "dev_type": constants.DT_DRBD8,
+        "expected_string": ",aio=native,cache=none"
+      },
+      "aio_native_safe_storage_writethrough_cache": {
+        "disk_aio": constants.HT_KVM_AIO_NATIVE,
+        "disk_cache": constants.HT_CACHE_WTHROUGH,
+        "dev_type": constants.DT_DRBD8,
+        "expected_string": ",aio=native,cache=none"
+      },
+      "aio_native_unsafe_storage_writethrough_cache": {
+        "disk_aio": constants.HT_KVM_AIO_NATIVE,
+        "disk_cache": constants.HT_CACHE_WTHROUGH,
+        "dev_type": constants.DT_GLUSTER,
+        "expected_string": ",aio=native,cache=none"
+      },
+      "aio_unset_safe_storage_none_cache": {
+        "disk_aio": None,
+        "disk_cache": constants.HT_CACHE_NONE,
+        "dev_type": constants.DT_DRBD8,
+        "expected_string": ",aio=threads,cache=none"
+      }
+    }
+
+    for name, data in test_cases.items():
+      self.assertEqual(hv_kvm.KVMHypervisor._GenerateDiskAioCacheParameters(
+        data["disk_aio"], data["disk_cache"], data["dev_type"]),
+        data["expected_string"], name)
+
+
 class TestQmpMessage(testutils.GanetiTestCase):
   def testSerialization(self):
     test_data = {
@@ -382,7 +845,7 @@
     fixed = set([
         constants.HV_KVM_SPICE_BIND, constants.HV_KVM_SPICE_TLS_CIPHERS,
         constants.HV_KVM_SPICE_USE_VDAGENT, constants.HV_KVM_SPICE_AUDIO_COMPR])
-    self.assertEqual(hv_kvm._SPICE_ADDITIONAL_PARAMS, params - fixed)
+    self.assertEqual(hv_kvm.validation._SPICE_ADDITIONAL_PARAMS, params - fixed)
 
 
 class TestHelpRegexps(testutils.GanetiTestCase):
@@ -569,6 +1032,7 @@
   def __repr__(self):
     return "<Postfix %s>" % self.string
 
+
 class TestKvmRuntime(testutils.GanetiTestCase):
   """The _ExecuteKvmRuntime is at the core of all KVM operations."""
 
@@ -606,8 +1070,8 @@
         (PostfixMatcher('/run/ganeti/kvm-hypervisor/conf'), 0o775),
         (PostfixMatcher('/run/ganeti/kvm-hypervisor/nic'), 0o775),
         (PostfixMatcher('/run/ganeti/kvm-hypervisor/chroot'), 0o775),
-        (PostfixMatcher('/run/ganeti/kvm-hypervisor/chroot-quarantine'), 0o775),
-        (PostfixMatcher('/run/ganeti/kvm-hypervisor/keymap'), 0o775)])
+        (PostfixMatcher('/run/ganeti/kvm-hypervisor/chroot-quarantine'), 0o775)
+        ])
 
   def testStartInstance(self):
     hypervisor = hv_kvm.KVMHypervisor()
@@ -627,5 +1091,6 @@
     self.mocks['run_cmd'].side_effect = RunCmd
     hypervisor.StartInstance(self.instance, [], False)
 
+
 if __name__ == "__main__":
   testutils.GanetiTestProgram()
diff -Nru ganeti-3.0.1/tools/move-instance ganeti-3.0.2/tools/move-instance
--- ganeti-3.0.1/tools/move-instance	2021-02-03 10:24:16.000000000 +0200
+++ ganeti-3.0.2/tools/move-instance	2022-02-28 22:51:21.000000000 +0200
@@ -761,7 +761,7 @@
     job_id = cl.GetInstanceInfo(name, static=True)
     result = poll_job_fn(cl, job_id)
     assert len(result[0].keys()) == 1
-    return result[0][result[0].keys()[0]]
+    return result[0][list(result[0].keys())[0]]
 
   @staticmethod
   def _PrepareExport(cl, poll_job_fn, name):
diff -Nru ganeti-3.0.1/tools/net-common.in ganeti-3.0.2/tools/net-common.in
--- ganeti-3.0.1/tools/net-common.in	2021-02-03 10:24:16.000000000 +0200
+++ ganeti-3.0.2/tools/net-common.in	2022-02-28 22:51:21.000000000 +0200
@@ -76,9 +76,15 @@
   # tap on the VLAN aware bridge will not receive traffic
   if [ -r /proc/net/vlan/config ]; then
     local vlan_interface="$(awk -F '[| ]*' -v vlan="^${VID}$" 'match($2, vlan) { print $1 }' /proc/net/vlan/config)"
+    local lower_devs="$(awk -F '[| ]*' -v vlan="^${VID}$" 'match($2, vlan) { print $3 }' /proc/net/vlan/config)"
     if [ -n "${vlan_interface}" ]; then
-      echo "VLAN ${VID} is in use by interface ${vlan_interface}"
-      exit 1
+      for i in ${lower_devs}; do
+        # allow bridge stacking and vlan overlap for veth devices
+        if [ "$(ip -o link show dev ${i} type veth | wc -l)" -ne 1 ]; then
+          echo "VLAN ${VID} is in use by lower interface ${i}"
+          exit 1
+        fi
+      done
     fi
   fi
 

Reply to: