Repository: Azure/WALinuxAgent
Pull request #1222 (2018-06-21 18:21:27)

Log network routes if changed
Check the sha1 hash of the route table (/proc/net/route); if it changes, emit the route table as a JSON array of route structures, to the log.


Patch:
diff --git a/azurelinuxagent/common/osutil/default.py b/azurelinuxagent/common/osutil/default.py
index 9c0f0ae1..d07919c5 100644
--- a/azurelinuxagent/common/osutil/default.py
+++ b/azurelinuxagent/common/osutil/default.py
@@ -22,6 +22,7 @@ import datetime
 import errno
 import fcntl
 import glob
+import hashlib
 import multiprocessing
 import os
 import platform
@@ -766,7 +767,7 @@ class DefaultOSUtil(object):
     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
+        :param list(str) proc_net_route: Route table lines, including headers, containing at least one route
         :return: List of network route objects
         :rtype: list(networkutil.RouteEntry)
         """
@@ -797,29 +798,39 @@ class DefaultOSUtil(object):
                 route_list.append(route_obj)
         return route_list
 
-    def get_route_table(self):
+    def read_route_table(self):
         """
-        Construct a list of all network routes known to this system.
+        Return a list of strings comprising the route table, including column headers. Each line is stripped of leading
+        or trailing whitespace but is otherwise unmolested.
 
-        :return: a list of network routes
-        :rtype: list(networkutil.RouteEntry)
+        :return: Entries in the text route table
+        :rtype: list(str)
         """
-        route_list = []
         try:
             with open('/proc/net/route') as routing_table:
-                proc_net_route = list(map(str.strip, routing_table.readlines()))
+                return 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)
+        return []
+
+    def get_list_of_routes(self, route_table):
+        """
+        Construct a list of all network routes known to this system.
+
+        :param list(str) route_table: List of text entries from route table, including headers
+        :return: a list of network routes
+        :rtype: list(networkutil.RouteEntry)
+        """
+        route_list = []
+        count = len(route_table)
 
         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)
+            route_list = DefaultOSUtil._build_route_list(route_table)
         return route_list
 
     def get_primary_interface(self):
diff --git a/azurelinuxagent/common/utils/networkutil.py b/azurelinuxagent/common/utils/networkutil.py
index f1cd0078..2c086711 100644
--- a/azurelinuxagent/common/utils/networkutil.py
+++ b/azurelinuxagent/common/utils/networkutil.py
@@ -48,11 +48,16 @@ class RouteEntry(object):
     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(),
+    def to_json(self):
+        f = '{{"Iface": "{0}", "Destination": "{1}", "Gateway": "{2}", "Mask": "{3}", "Flags": "{4:#06x}", "Metric": "{5}"}}'
+        return f.format(self.interface, self.destination_quad(), self.gateway_quad(), self.mask_quad(),
                        self.flags, self.metric)
 
+    def __str__(self):
+        f = "Iface: {0}\tDestination: {1}\tGateway: {2}\tMask: {3}\tFlags: {4:#06x}\tMetric: {5}"
+        return f.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}")'\
+        return 'RouteEntry("{0}", "{1}", "{2}", "{3}", "{4:#04x}", "{5}")'\
             .format(self.interface, self.destination, self.gateway, self.mask, self.flags, self.metric)
diff --git a/azurelinuxagent/common/utils/textutil.py b/azurelinuxagent/common/utils/textutil.py
index 16c6ed58..0e92b548 100644
--- a/azurelinuxagent/common/utils/textutil.py
+++ b/azurelinuxagent/common/utils/textutil.py
@@ -18,6 +18,7 @@
 
 import base64
 import crypt
+import hashlib
 import random
 import re
 import string
@@ -365,3 +366,16 @@ def parse_json(json_str):
 
 def is_str_none_or_whitespace(s):
     return s is None or len(s) == 0 or s.isspace()
+
+
+def hash_strings(string_list):
+    """
+    Compute a cryptographic hash of a list of strings
+
+    :param string_list: The strings to be hashed
+    :return: The cryptographic hash (digest) of the strings in the order provided
+    """
+    sha1_hash = hashlib.sha1()
+    for item in string_list:
+        sha1_hash.update(item.encode())
+    return sha1_hash.digest()
diff --git a/azurelinuxagent/ga/monitor.py b/azurelinuxagent/ga/monitor.py
index 3005588a..b7e7998a 100644
--- a/azurelinuxagent/ga/monitor.py
+++ b/azurelinuxagent/ga/monitor.py
@@ -40,8 +40,9 @@ from azurelinuxagent.common.protocol.restapi import TelemetryEventParam, \
                                                     TelemetryEventList, \
                                                     TelemetryEvent, \
                                                     set_properties
+import azurelinuxagent.common.utils.networkutil as networkutil
 from azurelinuxagent.common.utils.restutil import IOErrorCounter
-from azurelinuxagent.common.utils.textutil import parse_doc, findall, find, getattrib
+from azurelinuxagent.common.utils.textutil import parse_doc, findall, find, getattrib, hash_strings
 from azurelinuxagent.common.version import DISTRO_NAME, DISTRO_VERSION, \
             DISTRO_CODE_NAME, AGENT_LONG_VERSION, \
             AGENT_NAME, CURRENT_AGENT, CURRENT_VERSION
@@ -118,6 +119,7 @@ class MonitorHandler(object):
         self.last_imds_heartbeat = None
         self.protocol = None
         self.health_service = None
+        self.last_route_table_hash = b''
 
         self.counter = 0
         self.sysinfo = []
@@ -261,6 +263,7 @@ class MonitorHandler(object):
             self.collect_and_send_events()
             self.send_host_plugin_heartbeat()
             self.send_imds_heartbeat()
+            self.log_route_table()
             time.sleep(min_delta)
 
     def add_sysinfo(self, event):
@@ -416,3 +419,11 @@ class MonitorHandler(object):
                 logger.warn("Monitor: updating tracked extensions raised {0}: {1}", e, traceback.format_exc())
 
             self.last_cgroup_telemetry = datetime.datetime.utcnow()
+
+    def log_route_table(self):
+        raw_route_list = self.osutil.read_route_table()
+        digest = hash_strings(raw_route_list)
+        if digest != self.last_route_table_hash:
+            self.last_route_table_hash = digest
+            route_list = self.osutil.get_list_of_routes(raw_route_list)
+            logger.info("Route table: [{0}]".format(",".join(map(networkutil.RouteEntry.to_json, route_list))))


Test patch:
diff --git a/tests/common/osutil/test_default.py b/tests/common/osutil/test_default.py
index 0481cfc4..7b2645a6 100644
--- a/tests/common/osutil/test_default.py
+++ b/tests/common/osutil/test_default.py
@@ -22,6 +22,7 @@ import traceback
 
 import azurelinuxagent.common.osutil.default as osutil
 import azurelinuxagent.common.utils.shellutil as shellutil
+import azurelinuxagent.common.utils.textutil as textutil
 from azurelinuxagent.common.exception import OSUtilError
 from azurelinuxagent.common.future import ustr
 from azurelinuxagent.common.osutil import get_osutil
@@ -98,21 +99,25 @@ class TestOSUtil(AgentTestCase):
 
         mo = mock.mock_open(read_data=routing_table)
         with patch(open_patch(), mo):
-            self.assertEqual(len(osutil.DefaultOSUtil().get_route_table()), 0)
+            self.assertEqual(len(osutil.DefaultOSUtil().read_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)
+            raw_route_list = osutil.DefaultOSUtil().read_route_table()
+
+        self.assertEqual(len(osutil.DefaultOSUtil().get_list_of_routes(raw_route_list)), 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)
+            raw_route_list = osutil.DefaultOSUtil().read_route_table()
+
+        self.assertEqual(len(osutil.DefaultOSUtil().get_list_of_routes(raw_route_list)), 0)
 
     def test_valid_routes(self):
         routing_table = \
@@ -122,10 +127,16 @@ class TestOSUtil(AgentTestCase):
             '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'
+        known_sha1_hash = b'\x1e\xd1k\xae[\xf8\x9b\x1a\x13\xd0\xbbT\xa4\xe3Y\xa3\xdd\x0b\xbd\xa9'
 
         mo = mock.mock_open(read_data=routing_table)
         with patch(open_patch(), mo):
-            route_list = osutil.DefaultOSUtil().get_route_table()
+            raw_route_list = osutil.DefaultOSUtil().read_route_table()
+
+        self.assertEqual(len(raw_route_list), 6)
+        self.assertEqual(textutil.hash_strings(raw_route_list), known_sha1_hash)
+
+        route_list = osutil.DefaultOSUtil().get_list_of_routes(raw_route_list)
 
         self.assertEqual(len(route_list), 5)
         self.assertEqual(route_list[0].gateway_quad(), '10.145.187.193')
diff --git a/tests/utils/test_network_util.py b/tests/utils/test_network_util.py
index 53af2bf7..d6dc1ce3 100644
--- a/tests/utils/test_network_util.py
+++ b/tests/utils/test_network_util.py
@@ -36,8 +36,10 @@ class TestNetworkOperations(AgentTestCase):
         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'
+        expected = 'Iface: eth0\tDestination: 10.145.187.192\tGateway: 10.145.187.193\tMask: 255.255.255.192\tFlags: 0x0001\tMetric: 0'
+        expected_json = '{"Iface": "eth0", "Destination": "10.145.187.192", "Gateway": "10.145.187.193", "Mask": "255.255.255.192", "Flags": "0x0001", "Metric": "0"}'
 
         entry = networkutil.RouteEntry(interface, destination, gateway, mask, flags, metric)
 
         self.assertEqual(str(entry), expected)
+        self.assertEqual(entry.to_json(), expected_json)
diff --git a/tests/utils/test_text_util.py b/tests/utils/test_text_util.py
index 719210ac..a3c2dfa6 100644
--- a/tests/utils/test_text_util.py
+++ b/tests/utils/test_text_util.py
@@ -18,6 +18,8 @@
 from distutils.version import LooseVersion as Version
 from tests.tools import *
 
+import hashlib
+
 import azurelinuxagent.common.utils.textutil as textutil
 
 from azurelinuxagent.common.future import ustr
@@ -142,6 +144,21 @@ class TestTextUtil(AgentTestCase):
         result = textutil.compress('[stdout]\nHello World\n\n[stderr]\n\n')
         self.assertEqual('eJyLLi5JyS8tieXySM3JyVcIzy/KSeHiigaKphYVxXJxAQDAYQr2', result)
 
+    def test_hash_empty_list(self):
+        result = textutil.hash_strings([])
+        self.assertEqual(b'\xda9\xa3\xee^kK\r2U\xbf\xef\x95`\x18\x90\xaf\xd8\x07\t', result)
+
+    def test_hash_list(self):
+        test_list = ["abc", "123"]
+        result_from_list = textutil.hash_strings(test_list)
+
+        test_string = "".join(test_list)
+        hash_from_string = hashlib.sha1()
+        hash_from_string.update(test_string.encode())
+
+        self.assertEqual(result_from_list, hash_from_string.digest())
+        self.assertEqual(hash_from_string.hexdigest(), '6367c48dd193d56ea7b0baad25b19455e529f5ee')
+
 
 if __name__ == '__main__':
     unittest.main()