From 6ef21c9aa0481f1d779be0de0fdd169349d0d668 Mon Sep 17 00:00:00 2001 From: Mitch <25337396+MitchDrage@users.noreply.github.com> Date: Mon, 9 Nov 2026 04:15:47 +0000 Subject: [PATCH] Fix virtual router nftables restore at boot --- systemvm/debian/opt/cloud/bin/configure.py | 2 +- systemvm/debian/opt/cloud/bin/cs/CsHelper.py | 36 ++++++++++++++++---- systemvm/test/TestCsHelper.py | 31 +++++++++++++++++ 3 files changed, 62 insertions(+), 7 deletions(-) diff --git a/systemvm/debian/opt/cloud/bin/configure.py b/systemvm/debian/opt/cloud/bin/configure.py index cf0b71ab436a..a67c3b6a7aeb 100755 --- a/systemvm/debian/opt/cloud/bin/configure.py +++ b/systemvm/debian/opt/cloud/bin/configure.py @@ -1609,7 +1609,7 @@ def process(self): CsHelper.save_iptables("ip6tables-save", "/etc/iptables/rules.v6") # Save nftables configuration - CsHelper.save_iptables("nft list ruleset", "/etc/iptables/rules.nftables") + CsHelper.save_nftables("/etc/iptables/rules.nftables") def main(argv): # The file we are currently processing, if it is "cmd_line.json" everything will be processed. diff --git a/systemvm/debian/opt/cloud/bin/cs/CsHelper.py b/systemvm/debian/opt/cloud/bin/cs/CsHelper.py index c71bcdac4f42..93170b5c8fbd 100755 --- a/systemvm/debian/opt/cloud/bin/cs/CsHelper.py +++ b/systemvm/debian/opt/cloud/bin/cs/CsHelper.py @@ -210,13 +210,37 @@ def save_iptables(command, iptables_file): """ Execute command """ logging.debug("Saving iptables for %s" % command) - result = execute(command) - fIptables = open(iptables_file, "w+") + write_lines(iptables_file, execute(command)) - for line in result: - fIptables.write(line) - fIptables.write("\n") - fIptables.close() + +def write_lines(path, lines): + with open(path, "w+") as f: + for line in lines: + f.write(line) + f.write("\n") + + +IPTABLES_FAMILIES = ["ip", "ip6", "arp", "bridge"] +IPTABLES_TABLES = ["filter", "nat", "mangle", "raw", "security", "broute"] + + +def save_nftables(nftables_file): + """ Save the tables that only nftables manages, such as the IPv6 firewall and ACLs. The iptables-nft tables are + saved by save_iptables and restored with iptables-restore. nft can't load them, and one of them in the file fails + the whole restore at boot. """ + logging.debug("Saving nftables tables to %s" % nftables_file) + + rules = [] + for line in execute("nft list tables"): + table = line.split() + if len(table) != 3 or table[0] != "table": + continue + family, name = table[1], table[2] + if family in IPTABLES_FAMILIES and name in IPTABLES_TABLES: + continue + rules.extend(execute("nft list table %s %s" % (family, name))) + + write_lines(nftables_file, rules) def execute2(command, wait=True): diff --git a/systemvm/test/TestCsHelper.py b/systemvm/test/TestCsHelper.py index c3be13c62d3d..af3f5a4087c0 100644 --- a/systemvm/test/TestCsHelper.py +++ b/systemvm/test/TestCsHelper.py @@ -15,6 +15,7 @@ # specific language governing permissions and limitations # under the License. +import tempfile import unittest import mock from cs import CsHelper @@ -31,6 +32,36 @@ def test_execute(self, mock_subprocess): result = CsHelper.execute("/bin/false") self.assertTrue(result is not None) + @mock.patch('cs.CsHelper.execute') + def test_save_iptables_writes_the_command_output(self, mock_execute): + mock_execute.return_value = ["*filter", "COMMIT"] + + with tempfile.NamedTemporaryFile(mode="r") as saved: + CsHelper.save_iptables("iptables-save", saved.name) + self.assertEqual("*filter\nCOMMIT\n", saved.read()) + mock_execute.assert_called_once_with("iptables-save") + + @mock.patch('cs.CsHelper.execute') + def test_save_nftables_leaves_out_iptables_tables(self, mock_execute): + tables = ["table ip filter", "table ip nat", "table ip mangle", "table ip6 filter", "table ip6 raw", + "table bridge broute", "table ip6 ip6_firewall", "table ip ip4_firewall", "table inet cloudstack"] + mock_execute.side_effect = lambda command: tables if command == "nft list tables" else \ + ["%s {" % command.replace("nft list ", ""), "}"] + + with tempfile.NamedTemporaryFile(mode="r") as saved: + CsHelper.save_nftables(saved.name) + content = saved.read() + + self.assertEqual("table ip6 ip6_firewall {\n}\ntable ip ip4_firewall {\n}\ntable inet cloudstack {\n}\n", content) + + @mock.patch('cs.CsHelper.execute') + def test_save_nftables_writes_an_empty_file_without_nftables_tables(self, mock_execute): + mock_execute.side_effect = lambda command: ["table ip filter", "table ip nat"] if command == "nft list tables" else ["x"] + + with tempfile.NamedTemporaryFile(mode="r") as saved: + CsHelper.save_nftables(saved.name) + self.assertEqual("", saved.read()) + if __name__ == '__main__': unittest.main()