Repository: Azure/WALinuxAgent
Pull request #1221 (2018-06-20 22:56:02)

Add tooling to capture route table as a list of RouteEntry objects
This duplicates other functionality in osutil. The goal is to log the route table at startup and when it changes; this PR, and the ones to follow, will deliberately not perturb existing code so as to minimize the possibility of breakage in paths that are known to work. Cleanup issues will be filed to use the common mechanism.


Patch:
diff --git a/azurelinuxagent/common/osutil/default.py b/azurelinuxagent/common/osutil/default.py
index 34a049aa..9c0f0ae1 100644
--- a/azurelinuxagent/common/osutil/default.py
+++ b/azurelinuxagent/common/osutil/default.py
@@ -36,6 +36,7 @@ import time
 import azurelinuxagent.common.logger as logger
 import azurelinuxagent.common.conf as conf
 import azurelinuxagent.common.utils.fileutil as fileutil
+import azurelinuxagent.common.utils.networkutil as networkutil
 import azurelinuxagent.common.utils.shellutil as shellutil
 import azurelinuxagent.common.utils.textutil as textutil
 
@@ -761,6 +762,65 @@ class DefaultOSUtil(object):
 
         return '', ''
 
+    @staticmethod
+    def _build_route_list(proc_net_route):
+        """
+        Construct a list of network route entries
+        :param list(str) proc_net_route: List of lines in /proc/net/route, containing at least one route
+        :return: List of network route objects
+        :rtype: list(networkutil.RouteEntry)
+        """
+        idx = 0
+        column_index = {}
+        header_line = proc_net_route[0]
+        for header in filter(lambda h: len(h) > 0, header_line.split("\t")):
+            column_index[header.strip()] = idx
+            idx += 1
+        try:
+            idx_iface = column_index["Iface"]
+            idx_dest = column_index["Destination"]
+            idx_gw = column_index["Gateway"]
+            idx_flags = column_index["Flags"]
+            idx_metric = column_index["Metric"]
+            idx_mask = column_index["Mask"]
+        except KeyError:
+            msg = "/proc/net/route is missing key information; headers are [{0}]".format(header_line)
+            logger.periodic(logger.EVERY_HALF_DAY, msg)
+            return []
+
+        route_list = []
+        for entry in proc_net_route[1:]:
+            route = entry.split("\t")
+            if len(route) > 0:
+                route_obj = networkutil.RouteEntry(route[idx_iface], route[idx_dest], route[idx_gw], route[idx_mask],
+                                                   route[idx_flags], route[idx_metric])
+                route_list.append(route_obj)
+        return route_list
+
+    def get_route_table(self):
+        """
+        Construct a list of all network routes known to this system.
+
+        :return: a list of network routes
+        :rtype: list(networkutil.RouteEntry)
+        """
+        route_list = []
+        try:
+            with open('/proc/net/route') as routing_table:
+                proc_net_route = list(map(str.strip, routing_table.readlines()))
+        except OSError as e:
+            logger.periodic(logger.EVERY_HALF_DAY, "Can't read route table. {0}", e)
+            return route_list
+
+        count = len(proc_net_route)
+
+        if count < 1:
+            logger.periodic(logger.EVERY_HALF_DAY, "/proc/net/route is missing headers")
+        elif count == 1:
+            logger.periodic(logger.EVERY_HALF_DAY, "/proc/net/route contains no routes")
+        else:
+            route_list = DefaultOSUtil._build_route_list(proc_net_route)
+        return route_list
 
     def get_primary_interface(self):
         """
diff --git a/azurelinuxagent/common/utils/networkutil.py b/azurelinuxagent/common/utils/networkutil.py
new file mode 100644
index 00000000..f1cd0078
--- /dev/null
+++ b/azurelinuxagent/common/utils/networkutil.py
@@ -0,0 +1,58 @@
+#
+# Copyright 2018 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.6+ and Openssl 1.0+
+#
+
+
+class RouteEntry(object):
+    """
+    Represents a single route. The destination, gateway, and mask members are hex representations of the IPv4 address in
+    network byte order.
+    """
+    def __init__(self, interface, destination, gateway, mask, flags, metric):
+        self.interface = interface
+        self.destination = destination
+        self.gateway = gateway
+        self.mask = mask
+        self.flags = int(flags, 16)
+        self.metric = int(metric)
+
+    @staticmethod
+    def _net_hex_to_dotted_quad(value):
+        if len(value) != 8:
+            raise Exception("String to dotted quad conversion must be 8 characters")
+        octets = []
+        for idx in range(6, -2, -2):
+            octets.append(str(int(value[idx:idx+2], 16)))
+        return ".".join(octets)
+
+    def destination_quad(self):
+        return self._net_hex_to_dotted_quad(self.destination)
+
+    def gateway_quad(self):
+        return self._net_hex_to_dotted_quad(self.gateway)
+
+    def mask_quad(self):
+        return self._net_hex_to_dotted_quad(self.mask)
+
+    def __str__(self):
+        return "Iface: {0}\tDestination: {1}\tGateway: {2}\tMask: {3}\tFlags: {4}\tMetric: {5}" \
+               .format(self.interface, self.destination_quad(), self.gateway_quad(), self.mask_quad(),
+                       self.flags, self.metric)
+
+    def __repr__(self):
+        return 'RouteEntry("{0}", "{1}", "{2}", "{3}", "{4}", "{5}")'\
+            .format(self.interface, self.destination, self.gateway, self.mask, self.flags, self.metric)


Test patch:
diff --git a/tests/common/osutil/test_default.py b/tests/common/osutil/test_default.py
index 7db80271..0481cfc4 100644
--- a/tests/common/osutil/test_default.py
+++ b/tests/common/osutil/test_default.py
@@ -28,6 +28,8 @@ from azurelinuxagent.common.osutil import get_osutil
 from tests.tools import *
 
 
+actual_get_proc_net_route = 'azurelinuxagent.common.osutil.default.DefaultOSUtil._get_proc_net_route'
+
 def fake_is_loopback(_, iface):
     return iface.startswith('lo')
 
@@ -91,6 +93,52 @@ class TestOSUtil(AgentTestCase):
                         self.assertTrue(msg in ustr(ose))
                         self.assertTrue(patch_run.call_count == 6)
 
+    def test_empty_proc_net_route(self):
+        routing_table = ""
+
+        mo = mock.mock_open(read_data=routing_table)
+        with patch(open_patch(), mo):
+            self.assertEqual(len(osutil.DefaultOSUtil().get_route_table()), 0)
+
+    def test_no_routes(self):
+        routing_table = 'Iface\tDestination\tGateway \tFlags\tRefCnt\tUse\tMetric\tMask\t\tMTU\tWindow\tIRTT        \n'
+
+        mo = mock.mock_open(read_data=routing_table)
+        with patch(open_patch(), mo):
+            self.assertEqual(len(osutil.DefaultOSUtil().get_route_table()), 0)
+
+    def test_bogus_proc_net_route(self):
+        routing_table = 'Iface\tDestination\tGateway \tFlags\t\tUse\tMetric\t\neth0\t00000000\t00000000\t0001\t\t0\t0\n'
+
+        mo = mock.mock_open(read_data=routing_table)
+        with patch(open_patch(), mo):
+            self.assertEqual(len(osutil.DefaultOSUtil().get_route_table()), 0)
+
+    def test_valid_routes(self):
+        routing_table = \
+            'Iface\tDestination\tGateway \tFlags\tRefCnt\tUse\tMetric\tMask\t\tMTU\tWindow\tIRTT   \n' \
+            'eth0\t00000000\tC1BB910A\t0003\t0\t0\t0\t00000000\t0\t0\t0    \n' \
+            'eth0\tC0BB910A\t00000000\t0001\t0\t0\t0\tC0FFFFFF\t0\t0\t0    \n' \
+            'eth0\t10813FA8\tC1BB910A\t000F\t0\t0\t0\tFFFFFFFF\t0\t0\t0    \n' \
+            'eth0\tFEA9FEA9\tC1BB910A\t0007\t0\t0\t0\tFFFFFFFF\t0\t0\t0    \n' \
+            'docker0\t002BA8C0\t00000000\t0001\t0\t0\t10\t00FFFFFF\t0\t0\t0    \n'
+
+        mo = mock.mock_open(read_data=routing_table)
+        with patch(open_patch(), mo):
+            route_list = osutil.DefaultOSUtil().get_route_table()
+
+        self.assertEqual(len(route_list), 5)
+        self.assertEqual(route_list[0].gateway_quad(), '10.145.187.193')
+        self.assertEqual(route_list[1].gateway_quad(), '0.0.0.0')
+        self.assertEqual(route_list[1].mask_quad(), '255.255.255.192')
+        self.assertEqual(route_list[2].destination_quad(), '168.63.129.16')
+        self.assertEqual(route_list[1].flags, 1)
+        self.assertEqual(route_list[2].flags, 15)
+        self.assertEqual(route_list[3].flags, 7)
+        self.assertEqual(route_list[3].metric, 0)
+        self.assertEqual(route_list[4].metric, 10)
+        self.assertEqual(route_list[0].interface, 'eth0')
+        self.assertEqual(route_list[4].interface, 'docker0')
 
     @patch('azurelinuxagent.common.osutil.default.DefaultOSUtil.get_primary_interface', return_value='eth0')
     @patch('azurelinuxagent.common.osutil.default.DefaultOSUtil._get_all_interfaces', return_value={'eth0':'10.0.0.1'})
diff --git a/tests/utils/test_network_util.py b/tests/utils/test_network_util.py
new file mode 100644
index 00000000..53af2bf7
--- /dev/null
+++ b/tests/utils/test_network_util.py
@@ -0,0 +1,43 @@
+# Copyright 2018 Microsoft Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Requires Python 2.6+ and Openssl 1.0+
+#
+
+import errno
+from socket import htonl
+
+
+import azurelinuxagent.common.utils.fileutil as fileutil
+import azurelinuxagent.common.utils.networkutil as networkutil
+
+
+from azurelinuxagent.common.future import ustr
+from tests.tools import *
+
+
+class TestNetworkOperations(AgentTestCase):
+    def test_route_entry(self):
+        interface = "eth0"
+        mask = "C0FFFFFF"    # 255.255.255.192
+        destination = "C0BB910A"    #
+        gateway = "C1BB910A"
+        flags = "1"
+        metric = "0"
+
+        expected = 'Iface: eth0\tDestination: 10.145.187.192\tGateway: 10.145.187.193\tMask: 255.255.255.192\tFlags: 1\tMetric: 0'
+
+        entry = networkutil.RouteEntry(interface, destination, gateway, mask, flags, metric)
+
+        self.assertEqual(str(entry), expected)