From e9c62bedfebd5c8e1564085c05300ee2b2ae0020 Mon Sep 17 00:00:00 2001 From: Aaron Rainbolt Date: Tue, 4 Mar 2025 21:21:19 -0600 Subject: [PATCH] Add boot mode support Boot modes provide a way for VMs to advertise that they can have their behavior changed by booting them with a predefined combination of additional kernel parameters set. This allows qubes to offer features like a secure system maintenance mode in a way that works seemlessly with Qubes OS. This commit adds boot mode support to qubes-core-admin, providing API support for boot modes and allowing VMs to be booted with special kernel parameters specified by boot modes. --- qubes/ext/core_features.py | 80 +++ qubes/tests/ext.py | 1113 ++++++++++++++++++++++++++++++++++++ qubes/tests/vm/qubesvm.py | 44 ++ qubes/vm/qubesvm.py | 66 +++ qubes/vm/templatevm.py | 30 + templates/libvirt/xen.xml | 8 +- 6 files changed, 1337 insertions(+), 4 deletions(-) diff --git a/qubes/ext/core_features.py b/qubes/ext/core_features.py index 92920d109..ddf1296c1 100644 --- a/qubes/ext/core_features.py +++ b/qubes/ext/core_features.py @@ -104,6 +104,86 @@ async def qubes_features_request(self, vm, event, untrusted_features): untrusted_value = untrusted_features["qubes-agent-version"] if _version_re.fullmatch(untrusted_value): vm.features["qubes-agent-version"] = untrusted_value + + # handle boot mode advertisement + old_bootmode_info = {} + for feature_key, feature_val in vm.features.items(): + if feature_key.startswith( + "boot-mode.kernelopts." + ) or feature_key.startswith("boot-mode.name."): + old_bootmode_info[feature_key] = feature_val + new_bootmode_info = {} + new_bootmode_names = [] + for ( + untrusted_feature_key, + untrusted_feature_value, + ) in untrusted_features.items(): + if untrusted_feature_key.startswith("boot-mode.kernelopts."): + bootmode_key_parts = untrusted_feature_key.split(".") + if len(bootmode_key_parts) != 3: + # Boot mode key contains unexpected data, reject it + continue + bootmode_name = bootmode_key_parts[2] + if bootmode_name == "": + continue + if bootmode_name == "default": + # "default" is reserved, cannot set kernelopts for it + continue + bootmode_feature = untrusted_feature_key + bootmode_value = untrusted_feature_value + new_bootmode_info[bootmode_feature] = bootmode_value + for ( + untrusted_feature_key, + untrusted_feature_value, + ) in untrusted_features.items(): + if untrusted_feature_key.startswith("boot-mode.name."): + bootmode_key_parts = untrusted_feature_key.split(".") + if len(bootmode_key_parts) != 3: + # Boot mode key contains unexpected data, reject it + continue + bootmode_name = bootmode_key_parts[2] + if bootmode_name == "": + continue + if ( + f"boot-mode.kernelopts.{bootmode_name}" + not in new_bootmode_info + ) and bootmode_name != "default": + continue + bootmode_feature = untrusted_feature_key + bootmode_value = untrusted_feature_value + new_bootmode_info[bootmode_feature] = bootmode_value + new_bootmode_names.append(bootmode_value) + if ( + # Disallow duplicate boot mode names + len(new_bootmode_names) == len(set(new_bootmode_names)) + # Don't allow more than 64 boot modes + and len(new_bootmode_info) <= 64 + # Don't allow wiping all boot modes + and len(new_bootmode_info) > 0 + ): + for feature_key in old_bootmode_info: + if feature_key not in new_bootmode_info: + del vm.features[feature_key] + for feature_key, feature_val in new_bootmode_info.items(): + vm.features[feature_key] = feature_val + if "boot-mode.active" in untrusted_features: + untrusted_feature_value = untrusted_features["boot-mode.active"] + if ( + f"boot-mode.kernelopts.{untrusted_feature_value}" in vm.features + or untrusted_feature_value == "default" + ): + bootmode_value = untrusted_feature_value + vm.features["boot-mode.active"] = bootmode_value + if "boot-mode.appvm-default" in untrusted_features: + untrusted_feature_value = untrusted_features[ + "boot-mode.appvm-default" + ] + if ( + f"boot-mode.kernelopts.{untrusted_feature_value}" in vm.features + or untrusted_feature_value == "default" + ) and hasattr(vm, "appvm_default_bootmode"): + bootmode_value = untrusted_feature_value + vm.features["boot-mode.appvm-default"] = bootmode_value del untrusted_features # default user for qvm-run etc diff --git a/qubes/tests/ext.py b/qubes/tests/ext.py index 7bf07ae19..7606d9281 100644 --- a/qubes/tests/ext.py +++ b/qubes/tests/ext.py @@ -72,6 +72,7 @@ def test_010_notify_tools(self): self.assertListEqual( self.vm.mock_calls, [ + ("features.items", (), {}), ("features.get", ("qrexec", False), {}), ("features.__contains__", ("qrexec",), {}), ("features.__setitem__", ("qrexec", True), {}), @@ -101,6 +102,7 @@ def test_011_notify_tools_uninstall(self): self.assertListEqual( self.vm.mock_calls, [ + ("features.items", (), {}), ("features.get", ("qrexec", False), {}), ("features.__contains__", ("qrexec",), {}), ("features.__setitem__", ("qrexec", False), {}), @@ -126,6 +128,7 @@ def test_012_notify_tools_uninstall2(self): self.assertListEqual( self.vm.mock_calls, [ + ("features.items", (), {}), ("features.get", ("qrexec", False), {}), ("features.get", ("qrexec", False), {}), ], @@ -147,6 +150,7 @@ def test_013_notify_tools_no_version(self): self.assertListEqual( self.vm.mock_calls, [ + ("features.items", (), {}), ("features.get", ("qrexec", False), {}), ("features.__contains__", ("qrexec",), {}), ("features.__setitem__", ("qrexec", True), {}), @@ -174,6 +178,7 @@ def test_015_notify_tools_invalid_value_qrexec(self): self.assertEqual( self.vm.mock_calls, [ + ("features.items", (), {}), ("features.get", ("qrexec", False), {}), ("features.__contains__", ("gui",), {}), ("features.__setitem__", ("gui", True), {}), @@ -198,6 +203,7 @@ def test_016_notify_tools_invalid_value_gui(self): self.assertListEqual( self.vm.mock_calls, [ + ("features.items", (), {}), ("features.get", ("qrexec", False), {}), ("features.__contains__", ("qrexec",), {}), ("features.__setitem__", ("qrexec", True), {}), @@ -250,6 +256,7 @@ def test_018_notify_tools_already_installed(self): self.assertListEqual( self.vm.mock_calls, [ + ("features.items", (), {}), ("features.get", ("qrexec", False), {}), ("features.__contains__", ("qrexec",), {}), ("features.__contains__", ("gui",), {}), @@ -270,6 +277,7 @@ def test_20_version(self): self.vm.mock_calls, [ ("features.__setitem__", ("qubes-agent-version", "4.1"), {}), + ("features.items", (), {}), ("features.get", ("qrexec", False), {}), ], ) @@ -287,6 +295,7 @@ def test_21_version_invalid(self): self.assertListEqual( self.vm.mock_calls, [ + ("features.items", (), {}), ("features.get", ("qrexec", False), {}), ], ) @@ -301,6 +310,7 @@ def test_21_version_invalid(self): self.assertListEqual( self.vm.mock_calls, [ + ("features.items", (), {}), ("features.get", ("qrexec", False), {}), ], ) @@ -315,6 +325,7 @@ def test_21_version_invalid(self): self.assertListEqual( self.vm.mock_calls, [ + ("features.items", (), {}), ("features.get", ("qrexec", False), {}), ], ) @@ -329,6 +340,7 @@ def test_21_version_invalid(self): self.assertListEqual( self.vm.mock_calls, [ + ("features.items", (), {}), ("features.get", ("qrexec", False), {}), ], ) @@ -354,6 +366,7 @@ def test_30_distro_meta(self): ("features.__setitem__", ("os-distribution", "debian"), {}), ("features.__setitem__", ("os-version", "12"), {}), ("features.__setitem__", ("os-eol", "2026-06-10"), {}), + ("features.items", (), {}), ("features.get", ("qrexec", False), {}), ], ) @@ -385,6 +398,7 @@ def test_031_distro_meta_ubuntu(self): ), ("features.__setitem__", ("os-version", "22.04"), {}), ("features.__setitem__", ("os-eol", "2027-06-01"), {}), + ("features.items", (), {}), ("features.get", ("qrexec", False), {}), ], ) @@ -416,6 +430,7 @@ def test_032_distro_meta_invalid(self): ), ("log.warning", unittest.mock.ANY, {}), ("log.warning", unittest.mock.ANY, {}), + ("features.items", (), {}), ("features.get", ("qrexec", False), {}), ], ) @@ -447,6 +462,7 @@ def test_033_distro_meta_invalid2(self): ), ("log.warning", unittest.mock.ANY, {}), ("log.warning", unittest.mock.ANY, {}), + ("features.items", (), {}), ("features.get", ("qrexec", False), {}), ], ) @@ -470,6 +486,1103 @@ def test_034_distro_meta_empty(self): self.assertListEqual( self.vm.mock_calls, [ + ("features.items", (), {}), + ("features.get", ("qrexec", False), {}), + ], + ) + + def test_040_bootmode_noname(self): + del self.vm.template + self.loop.run_until_complete( + self.ext.qubes_features_request( + self.vm, + "features-request", + untrusted_features={ + "boot-mode.kernelopts.vmreq": "vmreq1 vmreq2", + }, + ) + ) + self.assertListEqual( + self.vm.mock_calls, + [ + ("features.items", (), {}), + ( + "features.__setitem__", + ("boot-mode.kernelopts.vmreq", "vmreq1 vmreq2"), + {}, + ), + ("features.get", ("qrexec", False), {}), + ("features.get", ("qrexec", False), {}), + ], + ) + + def test_041_bootmode_withname(self): + del self.vm.template + self.loop.run_until_complete( + self.ext.qubes_features_request( + self.vm, + "features-request", + untrusted_features={ + "boot-mode.name.vmreq": "VMReq", + "boot-mode.kernelopts.vmreq": "vmreq1 vmreq2", + }, + ) + ) + self.assertListEqual( + self.vm.mock_calls, + [ + ("features.items", (), {}), + ( + "features.__setitem__", + ("boot-mode.kernelopts.vmreq", "vmreq1 vmreq2"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.name.vmreq", "VMReq"), + {}, + ), + ("features.get", ("qrexec", False), {}), + ("features.get", ("qrexec", False), {}), + ], + ) + + def test_042_bootmode_extraname(self): + del self.vm.template + self.loop.run_until_complete( + self.ext.qubes_features_request( + self.vm, + "features-request", + untrusted_features={ + "boot-mode.name.vmreq1": "VMReq1", + "boot-mode.kernelopts.vmreq1": "vmreq1-1 vmreq1-2", + "boot-mode.name.bad": "Bad Entry", + "boot-mode.name.vmreq2": "VMReq2", + "boot-mode.kernelopts.vmreq2": "vmreq2-1 vmreq2-2", + }, + ) + ) + self.assertListEqual( + self.vm.mock_calls, + [ + ("features.items", (), {}), + ( + "features.__setitem__", + ("boot-mode.kernelopts.vmreq1", "vmreq1-1 vmreq1-2"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.vmreq2", "vmreq2-1 vmreq2-2"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.name.vmreq1", "VMReq1"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.name.vmreq2", "VMReq2"), + {}, + ), + ("features.get", ("qrexec", False), {}), + ("features.get", ("qrexec", False), {}), + ], + ) + + def test_043_bootmode_extrakernelopts(self): + del self.vm.template + self.loop.run_until_complete( + self.ext.qubes_features_request( + self.vm, + "features-request", + untrusted_features={ + "boot-mode.name.vmreq1": "VMReq1", + "boot-mode.kernelopts.vmreq1": "vmreq1-1 vmreq1-2", + "boot-mode.kernelopts.extra": "extra-opts", + "boot-mode.name.vmreq2": "VMReq2", + "boot-mode.kernelopts.vmreq2": "vmreq2-1 vmreq2-2", + }, + ) + ) + self.assertListEqual( + self.vm.mock_calls, + [ + ("features.items", (), {}), + ( + "features.__setitem__", + ("boot-mode.kernelopts.vmreq1", "vmreq1-1 vmreq1-2"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.extra", "extra-opts"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.vmreq2", "vmreq2-1 vmreq2-2"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.name.vmreq1", "VMReq1"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.name.vmreq2", "VMReq2"), + {}, + ), + ("features.get", ("qrexec", False), {}), + ("features.get", ("qrexec", False), {}), + ], + ) + + def test_044_bootmode_discard_old(self): + del self.vm.template + self.features["boot-mode.name.old"] = "Old" + self.features["boot-mode.kernelopts.old"] = "old1 old2" + self.loop.run_until_complete( + self.ext.qubes_features_request( + self.vm, + "features-request", + untrusted_features={ + "boot-mode.name.vmreq1": "VMReq1", + "boot-mode.kernelopts.vmreq1": "vmreq1-1 vmreq1-2", + }, + ) + ) + self.assertListEqual( + self.vm.mock_calls, + [ + ("features.items", (), {}), + ("features.__delitem__", ("boot-mode.name.old",), {}), + ("features.__delitem__", ("boot-mode.kernelopts.old",), {}), + ( + "features.__setitem__", + ("boot-mode.kernelopts.vmreq1", "vmreq1-1 vmreq1-2"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.name.vmreq1", "VMReq1"), + {}, + ), + ("features.get", ("qrexec", False), {}), + ("features.get", ("qrexec", False), {}), + ], + ) + + def test_045_bootmode_update_old(self): + del self.vm.template + self.features["boot-mode.name.old1"] = "Old1" + self.features["boot-mode.kernelopts.old1"] = "old1-1 old1-2" + self.features["boot-mode.name.old2"] = "Old2" + self.features["boot-mode.kernelopts.old2"] = "old2-1 old2-2" + self.loop.run_until_complete( + self.ext.qubes_features_request( + self.vm, + "features-request", + untrusted_features={ + "boot-mode.name.vmreq1": "VMReq1", + "boot-mode.kernelopts.vmreq1": "vmreq1-1 vmreq1-2", + "boot-mode.kernelopts.old1": "old-newer1-1 old-newer1-2", + }, + ) + ) + self.assertListEqual( + self.vm.mock_calls, + [ + ("features.items", (), {}), + ("features.__delitem__", ("boot-mode.name.old1",), {}), + ("features.__delitem__", ("boot-mode.name.old2",), {}), + ("features.__delitem__", ("boot-mode.kernelopts.old2",), {}), + ( + "features.__setitem__", + ("boot-mode.kernelopts.vmreq1", "vmreq1-1 vmreq1-2"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.old1", "old-newer1-1 old-newer1-2"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.name.vmreq1", "VMReq1"), + {}, + ), + ("features.get", ("qrexec", False), {}), + ("features.get", ("qrexec", False), {}), + ], + ) + + def test_046_bootmode_flood1(self): + del self.vm.template + self.loop.run_until_complete( + self.ext.qubes_features_request( + self.vm, + "features-request", + untrusted_features={ + "boot-mode.kernelopts.garbage1": "garbage1", + "boot-mode.kernelopts.garbage2": "garbage2", + "boot-mode.kernelopts.garbage3": "garbage3", + "boot-mode.kernelopts.garbage4": "garbage4", + "boot-mode.kernelopts.garbage5": "garbage5", + "boot-mode.kernelopts.garbage6": "garbage6", + "boot-mode.kernelopts.garbage7": "garbage7", + "boot-mode.kernelopts.garbage8": "garbage8", + "boot-mode.kernelopts.garbage9": "garbage9", + "boot-mode.kernelopts.garbage10": "garbage10", + "boot-mode.kernelopts.garbage11": "garbage11", + "boot-mode.kernelopts.garbage12": "garbage12", + "boot-mode.kernelopts.garbage13": "garbage13", + "boot-mode.kernelopts.garbage14": "garbage14", + "boot-mode.kernelopts.garbage15": "garbage15", + "boot-mode.kernelopts.garbage16": "garbage16", + "boot-mode.kernelopts.garbage17": "garbage17", + "boot-mode.kernelopts.garbage18": "garbage18", + "boot-mode.kernelopts.garbage19": "garbage19", + "boot-mode.kernelopts.garbage20": "garbage20", + "boot-mode.kernelopts.garbage21": "garbage21", + "boot-mode.kernelopts.garbage22": "garbage22", + "boot-mode.kernelopts.garbage23": "garbage23", + "boot-mode.kernelopts.garbage24": "garbage24", + "boot-mode.kernelopts.garbage25": "garbage25", + "boot-mode.kernelopts.garbage26": "garbage26", + "boot-mode.kernelopts.garbage27": "garbage27", + "boot-mode.kernelopts.garbage28": "garbage28", + "boot-mode.kernelopts.garbage29": "garbage29", + "boot-mode.kernelopts.garbage30": "garbage30", + "boot-mode.kernelopts.garbage31": "garbage31", + "boot-mode.kernelopts.garbage32": "garbage32", + "boot-mode.kernelopts.garbage33": "garbage33", + "boot-mode.kernelopts.garbage34": "garbage34", + "boot-mode.kernelopts.garbage35": "garbage35", + "boot-mode.kernelopts.garbage36": "garbage36", + "boot-mode.kernelopts.garbage37": "garbage37", + "boot-mode.kernelopts.garbage38": "garbage38", + "boot-mode.kernelopts.garbage39": "garbage39", + "boot-mode.kernelopts.garbage40": "garbage40", + "boot-mode.kernelopts.garbage41": "garbage41", + "boot-mode.kernelopts.garbage42": "garbage42", + "boot-mode.kernelopts.garbage43": "garbage43", + "boot-mode.kernelopts.garbage44": "garbage44", + "boot-mode.kernelopts.garbage45": "garbage45", + "boot-mode.kernelopts.garbage46": "garbage46", + "boot-mode.kernelopts.garbage47": "garbage47", + "boot-mode.kernelopts.garbage48": "garbage48", + "boot-mode.kernelopts.garbage49": "garbage49", + "boot-mode.kernelopts.garbage50": "garbage50", + "boot-mode.kernelopts.garbage51": "garbage51", + "boot-mode.kernelopts.garbage52": "garbage52", + "boot-mode.kernelopts.garbage53": "garbage53", + "boot-mode.kernelopts.garbage54": "garbage54", + "boot-mode.kernelopts.garbage55": "garbage55", + "boot-mode.kernelopts.garbage56": "garbage56", + "boot-mode.kernelopts.garbage57": "garbage57", + "boot-mode.kernelopts.garbage58": "garbage58", + "boot-mode.kernelopts.garbage59": "garbage59", + "boot-mode.kernelopts.garbage60": "garbage60", + "boot-mode.kernelopts.garbage61": "garbage61", + "boot-mode.kernelopts.garbage62": "garbage62", + "boot-mode.kernelopts.garbage63": "garbage63", + "boot-mode.kernelopts.garbage64": "garbage64", + "boot-mode.kernelopts.garbage65": "garbage65", + }, + ) + ) + self.assertListEqual( + self.vm.mock_calls, + [ + ("features.items", (), {}), + ("features.get", ("qrexec", False), {}), + ("features.get", ("qrexec", False), {}), + ], + ) + + def test_047_bootmode_flood2(self): + del self.vm.template + self.features["boot-mode.kernelopts.garbage1"] = "garbage1" + self.features["boot-mode.kernelopts.garbage2"] = "garbage2" + self.features["boot-mode.kernelopts.garbage3"] = "garbage3" + self.features["boot-mode.kernelopts.garbage4"] = "garbage4" + self.features["boot-mode.kernelopts.garbage5"] = "garbage5" + self.features["boot-mode.kernelopts.garbage6"] = "garbage6" + self.features["boot-mode.kernelopts.garbage7"] = "garbage7" + self.features["boot-mode.kernelopts.garbage8"] = "garbage8" + self.features["boot-mode.kernelopts.garbage9"] = "garbage9" + self.features["boot-mode.kernelopts.garbage10"] = "garbage10" + self.loop.run_until_complete( + self.ext.qubes_features_request( + self.vm, + "features-request", + untrusted_features={ + "boot-mode.kernelopts.garbage11": "garbage11", + "boot-mode.kernelopts.garbage12": "garbage12", + "boot-mode.kernelopts.garbage13": "garbage13", + "boot-mode.kernelopts.garbage14": "garbage14", + "boot-mode.kernelopts.garbage15": "garbage15", + "boot-mode.kernelopts.garbage16": "garbage16", + "boot-mode.kernelopts.garbage17": "garbage17", + "boot-mode.kernelopts.garbage18": "garbage18", + "boot-mode.kernelopts.garbage19": "garbage19", + "boot-mode.kernelopts.garbage20": "garbage20", + "boot-mode.kernelopts.garbage21": "garbage21", + "boot-mode.kernelopts.garbage22": "garbage22", + "boot-mode.kernelopts.garbage23": "garbage23", + "boot-mode.kernelopts.garbage24": "garbage24", + "boot-mode.kernelopts.garbage25": "garbage25", + "boot-mode.kernelopts.garbage26": "garbage26", + "boot-mode.kernelopts.garbage27": "garbage27", + "boot-mode.kernelopts.garbage28": "garbage28", + "boot-mode.kernelopts.garbage29": "garbage29", + "boot-mode.kernelopts.garbage30": "garbage30", + "boot-mode.kernelopts.garbage31": "garbage31", + "boot-mode.kernelopts.garbage32": "garbage32", + "boot-mode.kernelopts.garbage33": "garbage33", + "boot-mode.kernelopts.garbage34": "garbage34", + "boot-mode.kernelopts.garbage35": "garbage35", + "boot-mode.kernelopts.garbage36": "garbage36", + "boot-mode.kernelopts.garbage37": "garbage37", + "boot-mode.kernelopts.garbage38": "garbage38", + "boot-mode.kernelopts.garbage39": "garbage39", + "boot-mode.kernelopts.garbage40": "garbage40", + "boot-mode.kernelopts.garbage41": "garbage41", + "boot-mode.kernelopts.garbage42": "garbage42", + "boot-mode.kernelopts.garbage43": "garbage43", + "boot-mode.kernelopts.garbage44": "garbage44", + "boot-mode.kernelopts.garbage45": "garbage45", + "boot-mode.kernelopts.garbage46": "garbage46", + "boot-mode.kernelopts.garbage47": "garbage47", + "boot-mode.kernelopts.garbage48": "garbage48", + "boot-mode.kernelopts.garbage49": "garbage49", + "boot-mode.kernelopts.garbage50": "garbage50", + "boot-mode.kernelopts.garbage51": "garbage51", + "boot-mode.kernelopts.garbage52": "garbage52", + "boot-mode.kernelopts.garbage53": "garbage53", + "boot-mode.kernelopts.garbage54": "garbage54", + "boot-mode.kernelopts.garbage55": "garbage55", + "boot-mode.kernelopts.garbage56": "garbage56", + "boot-mode.kernelopts.garbage57": "garbage57", + "boot-mode.kernelopts.garbage58": "garbage58", + "boot-mode.kernelopts.garbage59": "garbage59", + "boot-mode.kernelopts.garbage60": "garbage60", + "boot-mode.kernelopts.garbage61": "garbage61", + "boot-mode.kernelopts.garbage62": "garbage62", + "boot-mode.kernelopts.garbage63": "garbage63", + "boot-mode.kernelopts.garbage64": "garbage64", + "boot-mode.kernelopts.garbage65": "garbage65", + }, + ) + ) + self.assertListEqual( + self.vm.mock_calls, + [ + ("features.items", (), {}), + ( + "features.__delitem__", + ("boot-mode.kernelopts.garbage1",), + {}, + ), + ( + "features.__delitem__", + ("boot-mode.kernelopts.garbage2",), + {}, + ), + ( + "features.__delitem__", + ("boot-mode.kernelopts.garbage3",), + {}, + ), + ( + "features.__delitem__", + ("boot-mode.kernelopts.garbage4",), + {}, + ), + ( + "features.__delitem__", + ("boot-mode.kernelopts.garbage5",), + {}, + ), + ( + "features.__delitem__", + ("boot-mode.kernelopts.garbage6",), + {}, + ), + ( + "features.__delitem__", + ("boot-mode.kernelopts.garbage7",), + {}, + ), + ( + "features.__delitem__", + ("boot-mode.kernelopts.garbage8",), + {}, + ), + ( + "features.__delitem__", + ("boot-mode.kernelopts.garbage9",), + {}, + ), + ( + "features.__delitem__", + ("boot-mode.kernelopts.garbage10",), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage11", "garbage11"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage12", "garbage12"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage13", "garbage13"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage14", "garbage14"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage15", "garbage15"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage16", "garbage16"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage17", "garbage17"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage18", "garbage18"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage19", "garbage19"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage20", "garbage20"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage21", "garbage21"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage22", "garbage22"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage23", "garbage23"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage24", "garbage24"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage25", "garbage25"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage26", "garbage26"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage27", "garbage27"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage28", "garbage28"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage29", "garbage29"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage30", "garbage30"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage31", "garbage31"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage32", "garbage32"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage33", "garbage33"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage34", "garbage34"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage35", "garbage35"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage36", "garbage36"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage37", "garbage37"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage38", "garbage38"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage39", "garbage39"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage40", "garbage40"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage41", "garbage41"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage42", "garbage42"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage43", "garbage43"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage44", "garbage44"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage45", "garbage45"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage46", "garbage46"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage47", "garbage47"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage48", "garbage48"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage49", "garbage49"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage50", "garbage50"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage51", "garbage51"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage52", "garbage52"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage53", "garbage53"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage54", "garbage54"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage55", "garbage55"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage56", "garbage56"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage57", "garbage57"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage58", "garbage58"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage59", "garbage59"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage60", "garbage60"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage61", "garbage61"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage62", "garbage62"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage63", "garbage63"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage64", "garbage64"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.garbage65", "garbage65"), + {}, + ), + ("features.get", ("qrexec", False), {}), + ("features.get", ("qrexec", False), {}), + ], + ) + + def test_048_bootmode_defaultname(self): + del self.vm.template + self.loop.run_until_complete( + self.ext.qubes_features_request( + self.vm, + "features-request", + untrusted_features={ + "boot-mode.name.vmreq": "VMReq", + "boot-mode.kernelopts.vmreq": "vmreq1 vmreq2", + "boot-mode.name.default": "No Params", + }, + ) + ) + self.assertListEqual( + self.vm.mock_calls, + [ + ("features.items", (), {}), + ( + "features.__setitem__", + ("boot-mode.kernelopts.vmreq", "vmreq1 vmreq2"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.name.vmreq", "VMReq"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.name.default", "No Params"), + {}, + ), + ("features.get", ("qrexec", False), {}), + ("features.get", ("qrexec", False), {}), + ], + ) + + def test_049_bootmode_multidots(self): + del self.vm.template + self.loop.run_until_complete( + self.ext.qubes_features_request( + self.vm, + "features-request", + untrusted_features={ + "boot-mode.name.vmreq": "VMReq", + "boot-mode.kernelopts.vmreq": "vmreq1 vmreq2", + "boot-mode.name.vmreq.two": "VMReq Two", + "boot-mode.kernelopts.vmreq.two": "vmreqtwo1 vmreqtwo2", + }, + ) + ) + self.assertListEqual( + self.vm.mock_calls, + [ + ("features.items", (), {}), + ( + "features.__setitem__", + ("boot-mode.kernelopts.vmreq", "vmreq1 vmreq2"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.name.vmreq", "VMReq"), + {}, + ), + ("features.get", ("qrexec", False), {}), + ("features.get", ("qrexec", False), {}), + ], + ) + + def test_050_bootmode_noid(self): + del self.vm.template + self.loop.run_until_complete( + self.ext.qubes_features_request( + self.vm, + "features-request", + untrusted_features={ + "boot-mode.name.vmreq": "VMReq", + "boot-mode.kernelopts.vmreq": "vmreq1 vmreq2", + "boot-mode.name.": "Ghost", + "boot-mode.kernelopts.": "ghost", + }, + ) + ) + self.assertListEqual( + self.vm.mock_calls, + [ + ("features.items", (), {}), + ( + "features.__setitem__", + ("boot-mode.kernelopts.vmreq", "vmreq1 vmreq2"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.name.vmreq", "VMReq"), + {}, + ), + ("features.get", ("qrexec", False), {}), + ("features.get", ("qrexec", False), {}), + ], + ) + + def test_051_bootmode_defaultkernelopts(self): + del self.vm.template + self.loop.run_until_complete( + self.ext.qubes_features_request( + self.vm, + "features-request", + untrusted_features={ + "boot-mode.name.vmreq": "VMReq", + "boot-mode.kernelopts.vmreq": "vmreq1 vmreq2", + "boot-mode.kernelopts.default": "default_kernelopts", + }, + ) + ) + self.assertListEqual( + self.vm.mock_calls, + [ + ("features.items", (), {}), + ( + "features.__setitem__", + ("boot-mode.kernelopts.vmreq", "vmreq1 vmreq2"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.name.vmreq", "VMReq"), + {}, + ), + ("features.get", ("qrexec", False), {}), + ("features.get", ("qrexec", False), {}), + ], + ) + + def test_052_bootmode_active_appvm_default(self): + del self.vm.template + self.vm.bootmode = "" + self.vm.appvm_default_bootmode = "" + self.loop.run_until_complete( + self.ext.qubes_features_request( + self.vm, + "features-request", + untrusted_features={ + "boot-mode.name.vmreq1": "VMReq1", + "boot-mode.kernelopts.vmreq1": "vmreq1-1 vmreq1-2", + "boot-mode.name.vmreq2": "VMReq2", + "boot-mode.kernelopts.vmreq2": "vmreq2-1 vmreq2-2", + "boot-mode.active": "vmreq1", + "boot-mode.appvm-default": "vmreq2", + }, + ) + ) + self.assertListEqual( + self.vm.mock_calls, + [ + ("features.items", (), {}), + ( + "features.__setitem__", + ("boot-mode.kernelopts.vmreq1", "vmreq1-1 vmreq1-2"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.vmreq2", "vmreq2-1 vmreq2-2"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.name.vmreq1", "VMReq1"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.name.vmreq2", "VMReq2"), + {}, + ), + ( + "features.__contains__", + ("boot-mode.kernelopts.vmreq1",), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.active", "vmreq1"), + {}, + ), + ( + "features.__contains__", + ("boot-mode.kernelopts.vmreq2",), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.appvm-default", "vmreq2"), + {}, + ), + ("features.get", ("qrexec", False), {}), + ("features.get", ("qrexec", False), {}), + ], + ) + + def test_053_bootmode_active_appvm_default_mod(self): + del self.vm.template + self.vm.bootmode = "" + self.vm.appvm_default_bootmode = "" + self.features["boot-mode.kernelopts.old1"] = "oldopts1" + self.features["boot-mode.active"] = "old1" + self.features["boot-mode.kernelopts.old2"] = "oldopts2" + self.features["boot-mode.appvm-default"] = "old2" + self.loop.run_until_complete( + self.ext.qubes_features_request( + self.vm, + "features-request", + untrusted_features={ + "boot-mode.name.vmreq1": "VMReq1", + "boot-mode.kernelopts.vmreq1": "vmreq1-1 vmreq1-2", + "boot-mode.name.vmreq2": "VMReq2", + "boot-mode.kernelopts.vmreq2": "vmreq2-1 vmreq2-2", + "boot-mode.active": "vmreq1", + "boot-mode.appvm-default": "vmreq2", + }, + ) + ) + self.assertListEqual( + self.vm.mock_calls, + [ + ("features.items", (), {}), + ( + "features.__delitem__", + ("boot-mode.kernelopts.old1",), + {}, + ), + ( + "features.__delitem__", + ("boot-mode.kernelopts.old2",), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.vmreq1", "vmreq1-1 vmreq1-2"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.vmreq2", "vmreq2-1 vmreq2-2"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.name.vmreq1", "VMReq1"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.name.vmreq2", "VMReq2"), + {}, + ), + ( + "features.__contains__", + ("boot-mode.kernelopts.vmreq1",), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.active", "vmreq1"), + {}, + ), + ( + "features.__contains__", + ("boot-mode.kernelopts.vmreq2",), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.appvm-default", "vmreq2"), + {}, + ), + ("features.get", ("qrexec", False), {}), + ("features.get", ("qrexec", False), {}), + ], + ) + + def test_054_bootmode_active_appvm_default_wrong(self): + del self.vm.template + self.vm.bootmode = "" + self.vm.appvm_default_bootmode = "" + self.loop.run_until_complete( + self.ext.qubes_features_request( + self.vm, + "features-request", + untrusted_features={ + "boot-mode.name.vmreq": "VMReq", + "boot-mode.kernelopts.vmreq": "vmreq1 vmreq2", + "boot-mode.active": "nonexistent1", + "boot-mode.appvm-default": "nonexistent2", + }, + ) + ) + self.assertListEqual( + self.vm.mock_calls, + [ + ("features.items", (), {}), + ( + "features.__setitem__", + ("boot-mode.kernelopts.vmreq", "vmreq1 vmreq2"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.name.vmreq", "VMReq"), + {}, + ), + ( + "features.__contains__", + ("boot-mode.kernelopts.nonexistent1",), + {}, + ), + ( + "features.__contains__", + ("boot-mode.kernelopts.nonexistent2",), + {}, + ), + ("features.get", ("qrexec", False), {}), + ("features.get", ("qrexec", False), {}), + ], + ) + + def test_055_bootmode_preserve_oldvals(self): + del self.vm.template + self.features["boot-mode.name.old1"] = "Old1" + self.features["boot-mode.kernelopts.old1"] = "oldopts1" + self.features["boot-mode.name.old2"] = "Old2" + self.features["boot-mode.kernelopts.old2"] = "oldopts2" + self.loop.run_until_complete( + self.ext.qubes_features_request( + self.vm, + "features-request", + untrusted_features={ + "boot-mode.name.old1": "Old1", + "boot-mode.kernelopts.old1": "oldopts1", + "boot-mode.name.old2": "Old2", + "boot-mode.kernelopts.old2": "oldopts2", + }, + ) + ) + self.assertListEqual( + self.vm.mock_calls, + [ + ("features.items", (), {}), + ( + "features.__setitem__", + ("boot-mode.kernelopts.old1", "oldopts1"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.kernelopts.old2", "oldopts2"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.name.old1", "Old1"), + {}, + ), + ( + "features.__setitem__", + ("boot-mode.name.old2", "Old2"), + {}, + ), + ("features.get", ("qrexec", False), {}), ("features.get", ("qrexec", False), {}), ], ) diff --git a/qubes/tests/vm/qubesvm.py b/qubes/tests/vm/qubesvm.py index 42b7fec12..09bc4f36f 100644 --- a/qubes/tests/vm/qubesvm.py +++ b/qubes/tests/vm/qubesvm.py @@ -3095,3 +3095,47 @@ def test_801_ordering(self): assert qubes.vm.qubesvm.QubesVM( self.app, None, qid=1, name="bogus" ) > qubes.vm.adminvm.AdminVM(self.app, None) + + def test_810_bootmode_kernelopts(self): + vm = self.get_vm(cls=qubes.vm.appvm.AppVM) + vm.template = self.get_vm(cls=qubes.vm.templatevm.TemplateVM) + vm.bootmode = qubes.property.DEFAULT + self.assertEqual(vm.bootmode_kernelopts, "") + vm.features["boot-mode.kernelopts.testmode1"] = "abc def" + vm.bootmode = "testmode1" + self.assertEqual(vm.bootmode_kernelopts, " abc def") + del vm.features["boot-mode.kernelopts.testmode1"] + self.assertEqual(vm.bootmode_kernelopts, "") + vm.template.features["boot-mode.kernelopts.testmode2"] = "ghi jkl" + vm.template.appvm_default_bootmode = "testmode2" + vm.bootmode = "nonexistent" + self.assertEqual(vm.bootmode_kernelopts, " ghi jkl") + del vm.template.features["boot-mode.kernelopts.testmode2"] + self.assertEqual(vm.bootmode_kernelopts, "") + + def test_811_default_bootmode(self): + vm = self.get_vm(cls=qubes.vm.appvm.AppVM) + vm.template = self.get_vm(cls=qubes.vm.templatevm.TemplateVM) + vm.bootmode = qubes.property.DEFAULT + self.assertEqual(vm.bootmode, "default") + vm.features["boot-mode.active"] = "default" + self.assertEqual(vm.bootmode, "default") + vm.features["boot-mode.active"] = "testmode1" + vm.template.features["boot-mode.kernelopts.testmode1"] = "abc def" + self.assertEqual(vm.bootmode, "testmode1") + vm.features["boot-mode.active"] = "testmode1" + self.assertEqual(vm.bootmode, "testmode1") + del vm.template.features["boot-mode.kernelopts.testmode1"] + self.assertEqual(vm.bootmode, "default") + vm.template.features["boot-mode.appvm-default"] = "testmode2" + vm.template.features["boot-mode.kernelopts.testmode2"] = "ghi jkl" + self.assertEqual(vm.bootmode, "testmode2") + vm.template.features["boot-mode.appvm-default"] = "testmode2" + self.assertEqual(vm.bootmode, "testmode2") + del vm.template.features["boot-mode.kernelopts.testmode2"] + self.assertEqual(vm.bootmode, "default") + vm.template.features["boot-mode.kernelopts.testmode3"] = "mno pqr" + vm.template.appvm_default_bootmode = "testmode3" + self.assertEqual(vm.bootmode, "testmode3") + del vm.template.features["boot-mode.kernelopts.testmode3"] + self.assertEqual(vm.bootmode, "default") diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py index c9c159c78..f8afe1681 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -216,6 +216,36 @@ def _setter_kbd_layout(self, prop, value): return value +def _default_bootmode(self): + """ + Return the VM's default bootmode. If that doesn't exist, return the + template's default bootmode for AppVMs, and if that doesn't exist return + the special value "default". + """ + if "boot-mode.active" in self.features: + bootmode_value = self.features["boot-mode.active"] + if bootmode_value == "default": + return "default" + kernelopts = self.features.check_with_template( + f"boot-mode.kernelopts.{bootmode_value}", None + ) + if kernelopts is not None: + return bootmode_value + subject = self + while hasattr(subject, "template"): + if hasattr(subject.template, "appvm_default_bootmode"): + bootmode_value = subject.template.appvm_default_bootmode + if bootmode_value == "default": + return "default" + kernelopts = subject.features.check_with_template( + f"boot-mode.kernelopts.{bootmode_value}", None + ) + if kernelopts is not None: + return bootmode_value + subject = subject.template + return "default" + + def _default_virt_mode(self): if list(self.devices["pci"].get_assigned_devices()): return "hvm" @@ -660,6 +690,14 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM): # # properties loaded from XML # + bootmode = qubes.property( + "bootmode", + type=str, + load_stage=4, + default=_default_bootmode, + doc="Active boot mode for this domain", + ) + guivm = qubes.VMProperty( "guivm", load_stage=4, @@ -954,6 +992,24 @@ def attached_volumes(self): return result + list(self.volumes.values()) + @property + def bootmode_kernelopts(self): + if self.bootmode == "default": + return "" + kernelopts = self.features.check_with_template( + f"boot-mode.kernelopts.{self.bootmode}", None + ) + if kernelopts is None: + default_bootmode = _default_bootmode(self) + if default_bootmode == "default": + return "" + kernelopts = self.features.check_with_template( + f"boot-mode.kernelopts.{default_bootmode}", None + ) + if kernelopts is None: + return "" + return f" {kernelopts}" + @property def libvirt_domain(self): """Libvirt domain object from libvirt. @@ -1147,6 +1203,16 @@ def on_domain_init_loaded(self, event): self._domain_stopped_event_received = False self._domain_stopped_event_handled = False + @qubes.events.handler("domain-feature-set:boot-mode.active") + def on_feature_bootmode_active_set( + self, event, feature, value, oldvalue=None + ): + # pylint: disable=unused-argument + if value == oldvalue: + return + if self.property_is_default("bootmode"): + self.fire_event("property-reset:bootmode", name="bootmode") + @qubes.events.handler("property-set:label") def on_property_set_label(self, event, name, newvalue, oldvalue=None): # pylint: disable=unused-argument diff --git a/qubes/vm/templatevm.py b/qubes/vm/templatevm.py index 0561cf11b..3ff93637a 100644 --- a/qubes/vm/templatevm.py +++ b/qubes/vm/templatevm.py @@ -29,6 +29,12 @@ from qubes.vm.qubesvm import QubesVM +def _default_appvm_default_bootmode(self): + if "boot-mode.appvm-default" in self.features: + return self.features["boot-mode.appvm-default"] + return "default" + + class TemplateVM(QubesVM): """Template for AppVM""" @@ -43,6 +49,14 @@ def appvms(self): if hasattr(vm, "template") and vm.template is self: yield vm + appvm_default_bootmode = qubes.property( + "appvm_default_bootmode", + type=str, + load_stage=4, + default=_default_appvm_default_bootmode, + doc="Default active bootmode for AppVMs based on this template", + ) + netvm = qubes.VMProperty( "netvm", load_stage=4, @@ -94,6 +108,22 @@ def __init__(self, *args, **kwargs): } super().__init__(*args, **kwargs) + @qubes.events.handler("domain-feature-set:boot-mode.appvm-default") + def on_feature_bootmode_appvm_set( + self, event, feature, value, oldvalue=None + ): + # pylint: disable=unused-argument + if value == oldvalue: + return + if self.property_is_default("appvm_default_bootmode"): + self.fire_event( + "property-reset:appvm_default_bootmode", + name="appvm_default_bootmode", + ) + for appvm in getattr(self, "appvms", []): + if appvm.property_is_default("bootmode"): + appvm.fire_event("property-reset:bootmode", name="bootmode") + @qubes.events.handler( "property-set:default_user", "property-set:kernel", diff --git a/templates/libvirt/xen.xml b/templates/libvirt/xen.xml index 3d18136e5..ec56214c3 100644 --- a/templates/libvirt/xen.xml +++ b/templates/libvirt/xen.xml @@ -62,13 +62,13 @@ {% endif %} {% if vm.kernel %} {% if vm.features.check_with_template('no-default-kernelopts', False) -%} - {{ vm.kernelopts }} + {{ vm.kernelopts }}{{ vm.bootmode_kernelopts }} {% elif vm.features.check_with_template('apparmor', '0') == '1' -%} - {{ vm.kernelopts_common }}{{ vm.kernelopts }} apparmor=1 security=apparmor + {{ vm.kernelopts_common }}{{ vm.kernelopts }} apparmor=1 security=apparmor{{ vm.bootmode_kernelopts }} {% elif vm.features.check_with_template('selinux', '0') == '1' -%} - {{ vm.kernelopts_common }}{{ vm.kernelopts }} selinux=1 security=selinux + {{ vm.kernelopts_common }}{{ vm.kernelopts }} selinux=1 security=selinux{{ vm.bootmode_kernelopts }} {% else -%} - {{ vm.kernelopts_common }}{{ vm.kernelopts }} + {{ vm.kernelopts_common }}{{ vm.kernelopts }}{{ vm.bootmode_kernelopts }} {% endif -%} {% endif %} {% endblock %}