From 7c52b8a5fbcfe1e7442bbd011956b52e303745c5 Mon Sep 17 00:00:00 2001
From: Dmitry Zykov <44090270+zykovd@users.noreply.github.com>
Date: Fri, 12 May 2023 21:48:10 +0300
Subject: [PATCH] Fix KeyError in iproute pformat (#3287)

Reference:https://github.com/canonical/cloud-init/commit/7c52b8a5fbcfe1e7442bbd011956b52e303745c5
Conflict:format diff.

This fixes KeyError on specific network configuration when running
cloud-init on "network" stage. The same problem was mentioned in
#746 and #1041.
---
 cloudinit/netinfo.py                          | 16 +++++++----
 .../route-formatted-output-missing-gateway    | 19 +++++++++++++
 .../sample-iproute-output-v4-missing-gateway  |  1 +
 .../sample-iproute-output-v6-missing-gateway  | 10 +++++++
 cloudinit/tests/test_netinfo.py               | 27 +++++++++++++++++++
 5 files changed, 68 insertions(+), 5 deletions(-)
 create mode 100644 tests/data/netinfo/route-formatted-output-missing-gateway
 create mode 100644 tests/data/netinfo/sample-iproute-output-v4-missing-gateway
 create mode 100644 tests/data/netinfo/sample-iproute-output-v6-missing-gateway

diff --git a/cloudinit/netinfo.py b/cloudinit/netinfo.py
index 628e290..f26399a 100644
--- a/cloudinit/netinfo.py
+++ b/cloudinit/netinfo.py
@@ -460,9 +460,12 @@ def route_pformat():
             tbl_v4 = SimpleTable(fields_v4)
             for (n, r) in enumerate(routes.get('ipv4')):
                 route_id = str(n)
-                tbl_v4.add_row([route_id, r['destination'],
-                                r['gateway'], r['genmask'],
-                                r['iface'], r['flags']])
+                try:
+                    tbl_v4.add_row([route_id, r['destination'],
+                                    r['gateway'], r['genmask'],
+                                    r['iface'], r['flags']])
+                except KeyError as e:
+                    util.logexc(LOG, "Route info formatting error: %s" % e)
             route_s = tbl_v4.get_string()
             max_len = len(max(route_s.splitlines(), key=len))
             header = util.center("Route IPv4 info", "+", max_len)
@@ -475,8 +478,11 @@ def route_pformat():
                 route_id = str(n)
                 if r['iface'] == 'lo':
                     continue
-                tbl_v6.add_row([route_id, r['destination'],
-                                r['gateway'], r['iface'], r['flags']])
+                try:
+                    tbl_v6.add_row([route_id, r['destination'],
+                                    r['gateway'], r['iface'], r['flags']])
+                except KeyError as e:
+                    util.logexc(LOG, "Route info formatting error: %s" % e)
             route_s = tbl_v6.get_string()
             max_len = len(max(route_s.splitlines(), key=len))
             header = util.center("Route IPv6 info", "+", max_len)
diff --git a/tests/data/netinfo/route-formatted-output-missing-gateway b/tests/data/netinfo/route-formatted-output-missing-gateway
new file mode 100644
index 0000000..8bbd819
--- /dev/null
+++ b/tests/data/netinfo/route-formatted-output-missing-gateway
@@ -0,0 +1,19 @@
++++++++++++++++++++++++++++Route IPv4 info+++++++++++++++++++++++++++
++-------+-------------+---------+---------------+-----------+-------+
+| Route | Destination | Gateway |    Genmask    | Interface | Flags |
++-------+-------------+---------+---------------+-----------+-------+
+|   0   | 192.168.2.0 | 0.0.0.0 | 255.255.255.0 |  enp0s25  |   U   |
++-------+-------------+---------+---------------+-----------+-------+
+++++++++++++++++++++++++++Route IPv6 info++++++++++++++++++++++++++
++-------+---------------------------+---------+-----------+-------+
+| Route |        Destination        | Gateway | Interface | Flags |
++-------+---------------------------+---------+-----------+-------+
+|   0   |  2a00:abcd:82ae:cd33::657 |    ::   |  enp0s25  |   Ue  |
+|   1   |  2a00:abcd:82ae:cd33::/64 |    ::   |  enp0s25  |   U   |
+|   2   |  2a00:abcd:82ae:cd33::/56 |    ::   |  enp0s25  |   U   |
+|   3   |     fd81:123f:654::657    |    ::   |  enp0s25  |   U   |
+|   4   |     fd81:123f:654::/64    |    ::   |  enp0s25  |   U   |
+|   5   |     fd81:123f:654::/48    |    ::   |  enp0s25  |   U   |
+|   6   | fe80::abcd:ef12:bc34:da21 |    ::   |  enp0s25  |   U   |
+|   7   |         fe80::/64         |    ::   |  enp0s25  |   U   |
++-------+---------------------------+---------+-----------+-------+
diff --git a/tests/data/netinfo/sample-iproute-output-v4-missing-gateway b/tests/data/netinfo/sample-iproute-output-v4-missing-gateway
new file mode 100644
index 0000000..c1e0b3c
--- /dev/null
+++ b/tests/data/netinfo/sample-iproute-output-v4-missing-gateway
@@ -0,0 +1 @@
+192.168.2.0/24 dev enp0s25 proto kernel scope link src 192.168.2.18 metric 100
diff --git a/tests/data/netinfo/sample-iproute-output-v6-missing-gateway b/tests/data/netinfo/sample-iproute-output-v6-missing-gateway
new file mode 100644
index 0000000..ffab1fa
--- /dev/null
+++ b/tests/data/netinfo/sample-iproute-output-v6-missing-gateway
@@ -0,0 +1,10 @@
+2a00:abcd:82ae:cd33::657 dev enp0s25 proto kernel metric 256 expires 2334sec pref medium
+2a00:abcd:82ae:cd33::/64 dev enp0s25 proto ra metric 100 pref medium
+2a00:abcd:82ae:cd33::/56 dev enp0s25 proto ra metric 100 pref medium
+fd81:123f:654::657 dev enp0s25 proto kernel metric 256 pref medium
+fd81:123f:654::/64 dev enp0s25 proto ra metric 100 pref medium
+fd81:123f:654::/48 dev enp0s25 proto ra metric 100 pref medium
+fe80::abcd:ef12:bc34:da21 dev enp0s25 proto static metric 100 pref medium
+fe80::/64 dev enp0s25 proto kernel metric 256 pref medium
+local ::1 dev lo  table local  proto none  metric 0  pref medium
+local 2600:1f16:b80:ad00:90a:c915:bca6:5ff2 dev lo  table local  proto none  metric 0  pref medium
diff --git a/cloudinit/tests/test_netinfo.py b/cloudinit/tests/test_netinfo.py
index e44b16d..6619e0d 100644
--- a/cloudinit/tests/test_netinfo.py
+++ b/cloudinit/tests/test_netinfo.py
@@ -15,10 +15,19 @@ SAMPLE_FREEBSD_IFCONFIG_OUT = readResource("netinfo/freebsd-ifconfig-output")
 SAMPLE_IPADDRSHOW_OUT = readResource("netinfo/sample-ipaddrshow-output")
 SAMPLE_ROUTE_OUT_V4 = readResource("netinfo/sample-route-output-v4")
 SAMPLE_ROUTE_OUT_V6 = readResource("netinfo/sample-route-output-v6")
+SAMPLE_IPROUTE_OUT_V6_MISSING_GATEWAY = readResource(
+    "netinfo/sample-iproute-output-v6-missing-gateway"
+)
+SAMPLE_IPROUTE_OUT_V4_MISSING_GATEWAY = readResource(
+    "netinfo/sample-iproute-output-v4-missing-gateway"
+)
 SAMPLE_IPROUTE_OUT_V4 = readResource("netinfo/sample-iproute-output-v4")
 SAMPLE_IPROUTE_OUT_V6 = readResource("netinfo/sample-iproute-output-v6")
 NETDEV_FORMATTED_OUT = readResource("netinfo/netdev-formatted-output")
 ROUTE_FORMATTED_OUT = readResource("netinfo/route-formatted-output")
+ROUTE_FORMATTED_OUT_MISSING_GATEWAY = readResource(
+    "netinfo/route-formatted-output-missing-gateway"
+)
 FREEBSD_NETDEV_OUT = readResource("netinfo/freebsd-netdev-formatted-output")


@@ -165,6 +174,24 @@ class TestNetInfo(CiTestCase):
         content = route_pformat()
         self.assertEqual(ROUTE_FORMATTED_OUT, content)

+    @mock.patch("cloudinit.netinfo.subp.which")
+    @mock.patch("cloudinit.netinfo.subp.subp")
+    def test_route_iproute_pformat_missing_gateway(self, m_subp, m_which):
+        """route_pformat properly rendering info with missing gateway."""
+
+        def subp_iproute_selector(*args, **kwargs):
+            if ["ip", "-o", "route", "list"] == args[0]:
+                return (SAMPLE_IPROUTE_OUT_V4_MISSING_GATEWAY, "")
+            v6cmd = ["ip", "--oneline", "-6", "route", "list", "table", "all"]
+            if v6cmd == args[0]:
+                return (SAMPLE_IPROUTE_OUT_V6_MISSING_GATEWAY, "")
+            raise RuntimeError("Unexpected subp call %s" % args[0])
+
+        m_subp.side_effect = subp_iproute_selector
+        m_which.side_effect = lambda x: x if x == "ip" else None
+        content = route_pformat()
+        assert ROUTE_FORMATTED_OUT_MISSING_GATEWAY == content
+
     @mock.patch('cloudinit.netinfo.subp.which')
     @mock.patch('cloudinit.netinfo.subp.subp')
     def test_route_warn_on_missing_commands(self, m_subp, m_which):
-- 
2.33.0


