diff options
author | Max Illfelder <illfelder@users.noreply.github.com> | 2016-07-27 17:42:19 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-07-27 17:42:19 -0700 |
commit | 709a82adbac3b41bf3f7f23dd29ef9112eaf7fe6 (patch) | |
tree | b408572c84656bafc60d106f4d670f50dbac8861 | |
parent | aeadc0511f5cea3a417fabc73d3d386316bbf867 (diff) | |
download | google-compute-image-packages-709a82adbac3b41bf3f7f23dd29ef9112eaf7fe6.tar.gz |
Use MAC address metadata for IP forwarding. (#312)
4 files changed, 235 insertions, 104 deletions
diff --git a/google_compute_engine/ip_forwarding/ip_forwarding_daemon.py b/google_compute_engine/ip_forwarding/ip_forwarding_daemon.py index e1abe4a..b56ffa9 100755 --- a/google_compute_engine/ip_forwarding/ip_forwarding_daemon.py +++ b/google_compute_engine/ip_forwarding/ip_forwarding_daemon.py @@ -41,7 +41,7 @@ LOCKFILE = '/var/lock/google_ip_forwarding.lock' class IpForwardingDaemon(object): """Manage IP forwarding based on changes to forwarded IPs metadata.""" - forwarded_ips = 'instance/network-interfaces/0/forwarded-ips' + network_interfaces = 'instance/network-interfaces' def __init__(self, proto_id=None, debug=False): """Constructor. @@ -60,12 +60,12 @@ class IpForwardingDaemon(object): with file_utils.LockFile(LOCKFILE): self.logger.info('Starting Google IP Forwarding daemon.') self.watcher.WatchMetadata( - self.HandleForwardedIps, metadata_key=self.forwarded_ips, + self.HandleNetworkInterfaces, metadata_key=self.network_interfaces, recursive=True) except (IOError, OSError) as e: self.logger.warning(str(e)) - def _LogForwardedIpChanges(self, configured, desired, to_add, to_remove): + def _LogForwardedIpChanges(self, configured, desired, to_add, to_remove, interface): """Log the planned IP address changes. Args: @@ -73,44 +73,66 @@ class IpForwardingDaemon(object): desired: list, the IP address strings that will be configured. to_add: list, the forwarded IP address strings to configure. to_remove: list, the forwarded IP address strings to delete. + interface: string, the output device to modify. """ if not to_add and not to_remove: return self.logger.info( - 'Changing forwarded IPs from %s to %s by adding %s and removing %s.', - configured or None, desired or None, to_add or None, to_remove or None) + 'Changing %s forwarded IPs from %s to %s by adding %s and removing %s.', + interface, configured or None, desired or None, to_add or None, + to_remove or None) - def _AddForwardedIps(self, forwarded_ips): + def _AddForwardedIps(self, forwarded_ips, interface): """Configure the forwarded IP address on the network interface. Args: forwarded_ips: list, the forwarded IP address strings to configure. + interface: string, the output device to use. """ for address in forwarded_ips: - self.utils.AddForwardedIp(address) + self.utils.AddForwardedIp(address, interface) - def _RemoveForwardedIps(self, forwarded_ips): + def _RemoveForwardedIps(self, forwarded_ips, interface): """Remove the forwarded IP addresses from the network interface. Args: forwarded_ips: list, the forwarded IP address strings to delete. + interface: string, the output device to use. """ for address in forwarded_ips: - self.utils.RemoveForwardedIp(address) + self.utils.RemoveForwardedIp(address, interface) - def HandleForwardedIps(self, result): - """Called when forwarded IPs metadata changes. + def _HandleForwardedIps(self, forwarded_ips, interface): + """Handle changes to the forwarded IPs on a network interface. Args: - result: string, the metadata response with the new forwarded IP addresses. + forwarded_ips: list, the forwarded IP address strings desired. + interface: string, the output device to configure. """ - desired = self.utils.ParseForwardedIps(result) - configured = self.utils.GetForwardedIps() + desired = self.utils.ParseForwardedIps(forwarded_ips) + configured = self.utils.GetForwardedIps(interface) to_add = sorted(set(desired) - set(configured)) to_remove = sorted(set(configured) - set(desired)) - self._LogForwardedIpChanges(configured, desired, to_add, to_remove) - self._AddForwardedIps(to_add) - self._RemoveForwardedIps(to_remove) + self._LogForwardedIpChanges( + configured, desired, to_add, to_remove, interface) + self._AddForwardedIps(to_add, interface) + self._RemoveForwardedIps(to_remove, interface) + + def HandleNetworkInterfaces(self, result): + """Called when network interface metadata changes. + + Args: + result: string, the metadata response with the new network interfaces. + """ + for network_interface in result: + mac_address = network_interface.get('mac') + interface = self.utils.GetNetworkInterface(mac_address) + if interface: + forwarded_ips = network_interface.get('forwardedIps') + self._HandleForwardedIps(forwarded_ips, interface) + else: + message = 'Network interface not found for MAC address: %s.' + self.logger.warning(message, mac_address) def main(): diff --git a/google_compute_engine/ip_forwarding/ip_forwarding_utils.py b/google_compute_engine/ip_forwarding/ip_forwarding_utils.py index dee62f1..913954b 100644 --- a/google_compute_engine/ip_forwarding/ip_forwarding_utils.py +++ b/google_compute_engine/ip_forwarding/ip_forwarding_utils.py @@ -15,6 +15,7 @@ """Utilities for configuring IP address forwarding.""" +import os import re import subprocess @@ -32,14 +33,44 @@ class IpForwardingUtils(object): proto_id: string, the routing protocol identifier for Google IP changes. """ self.logger = logger - self.options = { - 'dev': self._GetDefaultInterface(), - 'proto': proto_id or '66', + self.proto_id = proto_id or '66' + self.interfaces = self._CreateInterfaceMap() + + def _CreateInterfaceMap(self): + """Generate a dictionary mapping MAC address to ethernet interfaces. + + Returns: + dict, string MAC addresses mapped to the string network interface name. + """ + interfaces = {} + for interface in os.listdir('/sys/class/net'): + try: + mac_address = open('/sys/class/net/%s/address' % interface).read().strip() + except (IOError, OSError) as e: + message = 'Unable to determine MAC address for %s. %s.' + self.logger.warning(message, interface, str(e)) + else: + interfaces[mac_address] = interface + return interfaces + + def _CreateRouteOptions(self, **kwargs): + """Create a dictionary of parameters to append to the ip route command. + + Args: + **kwargs: dict, the string parameters to update in the ip route command. + + Returns: + dict, the string parameters to append to the ip route command. + """ + options = { + 'proto': self.proto_id, 'scope': 'host', } + options.update(kwargs) + return options def _RunIpRoute(self, args=None, options=None): - """Run a command with IP route and return the response. + """Run a command with ip route and return the response. Args: args: list, the string ip route command args to execute. @@ -68,19 +99,16 @@ class IpForwardingUtils(object): return stdout return '' - def _GetDefaultInterface(self): - """Get the name of the default network interface. + def GetNetworkInterface(self, mac_address): + """Get the name of the network interface associated with a MAC address. + + Args: + mac_address: string, the hardware address of the network interface. Returns: - string, the name of the default network interface. + string, the network interface associated with a MAC address or None. """ - result = self._RunIpRoute(args=['list']) - for route in result.decode('utf-8').split('\n'): - fields = route.split() - if fields and fields[0] == 'default' and 'dev' in fields: - index = fields.index('dev') + 1 - return fields[index] if index < len(fields) else 'eth0' - return 'eth0' + return self.interfaces.get(mac_address) def ParseForwardedIps(self, forwarded_ips): """Parse and validate forwarded IP addresses. @@ -100,30 +128,38 @@ class IpForwardingUtils(object): self.logger.warning('Could not parse IP address: "%s".', ip) return addresses - def GetForwardedIps(self): + def GetForwardedIps(self, interface): """Retrieve the list of configured forwarded IP addresses. + Args: + interface: string, the output device to query. + Returns: list, the IP address strings. """ args = ['ls', 'table', 'local', 'type', 'local'] - result = self._RunIpRoute(args=args, options=self.options) + options = self._CreateRouteOptions(dev=interface) + result = self._RunIpRoute(args=args, options=options) return self.ParseForwardedIps(result.split()) - def AddForwardedIp(self, address): + def AddForwardedIp(self, address, interface): """Configure a new IP address on the network interface. Args: address: string, the IP address to configure. + interface: string, the output device to use. """ args = ['add', 'to', 'local', '%s/32' % address] - self._RunIpRoute(args=args, options=self.options) + options = self._CreateRouteOptions(dev=interface) + self._RunIpRoute(args=args, options=options) - def RemoveForwardedIp(self, address): + def RemoveForwardedIp(self, address, interface): """Delete an IP address on the network interface. Args: address: string, the IP address to configure. + interface: string, the output device to use. """ args = ['delete', 'to', 'local', '%s/32' % address] - self._RunIpRoute(args=args, options=self.options) + options = self._CreateRouteOptions(dev=interface) + self._RunIpRoute(args=args, options=options) diff --git a/google_compute_engine/ip_forwarding/tests/ip_forwarding_daemon_test.py b/google_compute_engine/ip_forwarding/tests/ip_forwarding_daemon_test.py index 769eebf..0b06b36 100644 --- a/google_compute_engine/ip_forwarding/tests/ip_forwarding_daemon_test.py +++ b/google_compute_engine/ip_forwarding/tests/ip_forwarding_daemon_test.py @@ -46,10 +46,10 @@ class IpForwardingDaemonTest(unittest.TestCase): mocks.attach_mock(mock_logger, 'logger') mocks.attach_mock(mock_utils, 'utils') mocks.attach_mock(mock_watcher, 'watcher') - metadata_key = ip_forwarding_daemon.IpForwardingDaemon.forwarded_ips + metadata_key = ip_forwarding_daemon.IpForwardingDaemon.network_interfaces with mock.patch.object( ip_forwarding_daemon.IpForwardingDaemon, - 'HandleForwardedIps') as mock_handle: + 'HandleNetworkInterfaces') as mock_handle: ip_forwarding_daemon.IpForwardingDaemon(proto_id='66', debug=True) expected_calls = [ mock.call.logger.Logger(name=mock.ANY, debug=True, facility=mock.ANY), @@ -80,7 +80,7 @@ class IpForwardingDaemonTest(unittest.TestCase): mocks.attach_mock(mock_watcher, 'watcher') mock_lock.LockFile.side_effect = IOError('Test Error') with mock.patch.object( - ip_forwarding_daemon.IpForwardingDaemon, 'HandleForwardedIps'): + ip_forwarding_daemon.IpForwardingDaemon, 'HandleNetworkInterfaces'): ip_forwarding_daemon.IpForwardingDaemon() expected_calls = [ mock.call.logger.Logger( @@ -95,47 +95,47 @@ class IpForwardingDaemonTest(unittest.TestCase): def testLogForwardedIpChanges(self): ip_forwarding_daemon.IpForwardingDaemon._LogForwardedIpChanges( - self.mock_setup, [], [], [], []) + self.mock_setup, [], [], [], [], '1') ip_forwarding_daemon.IpForwardingDaemon._LogForwardedIpChanges( - self.mock_setup, ['a'], ['a'], [], []) + self.mock_setup, ['a'], ['a'], [], [], '2') ip_forwarding_daemon.IpForwardingDaemon._LogForwardedIpChanges( - self.mock_setup, ['a'], [], [], ['a']) + self.mock_setup, ['a'], [], [], ['a'], '3') ip_forwarding_daemon.IpForwardingDaemon._LogForwardedIpChanges( - self.mock_setup, ['a', 'b'], ['b'], [], ['a']) + self.mock_setup, ['a', 'b'], ['b'], [], ['a'], '4') ip_forwarding_daemon.IpForwardingDaemon._LogForwardedIpChanges( - self.mock_setup, ['a'], ['b'], ['b'], ['a']) + self.mock_setup, ['a'], ['b'], ['b'], ['a'], '5') expected_calls = [ - mock.call.info(mock.ANY, ['a'], None, None, ['a']), - mock.call.info(mock.ANY, ['a', 'b'], ['b'], None, ['a']), - mock.call.info(mock.ANY, ['a'], ['b'], ['b'], ['a']), + mock.call.info(mock.ANY, '3', ['a'], None, None, ['a']), + mock.call.info(mock.ANY, '4', ['a', 'b'], ['b'], None, ['a']), + mock.call.info(mock.ANY, '5', ['a'], ['b'], ['b'], ['a']), ] self.assertEqual(self.mock_logger.mock_calls, expected_calls) def testAddForwardedIp(self): ip_forwarding_daemon.IpForwardingDaemon._AddForwardedIps( - self.mock_setup, []) + self.mock_setup, [], 'interface') self.assertEqual(self.mock_utils.mock_calls, []) ip_forwarding_daemon.IpForwardingDaemon._AddForwardedIps( - self.mock_setup, ['a', 'b', 'c']) + self.mock_setup, ['a', 'b', 'c'], 'interface') expected_calls = [ - mock.call.AddForwardedIp('a'), - mock.call.AddForwardedIp('b'), - mock.call.AddForwardedIp('c'), + mock.call.AddForwardedIp('a', 'interface'), + mock.call.AddForwardedIp('b', 'interface'), + mock.call.AddForwardedIp('c', 'interface'), ] self.assertEqual(self.mock_utils.mock_calls, expected_calls) def testRemoveForwardedIp(self): ip_forwarding_daemon.IpForwardingDaemon._RemoveForwardedIps( - self.mock_setup, []) + self.mock_setup, [], 'interface') self.assertEqual(self.mock_utils.mock_calls, []) ip_forwarding_daemon.IpForwardingDaemon._RemoveForwardedIps( - self.mock_setup, ['a', 'b', 'c']) + self.mock_setup, ['a', 'b', 'c'], 'interface') expected_calls = [ - mock.call.RemoveForwardedIp('a'), - mock.call.RemoveForwardedIp('b'), - mock.call.RemoveForwardedIp('c'), + mock.call.RemoveForwardedIp('a', 'interface'), + mock.call.RemoveForwardedIp('b', 'interface'), + mock.call.RemoveForwardedIp('c', 'interface'), ] self.assertEqual(self.mock_utils.mock_calls, expected_calls) @@ -147,19 +147,47 @@ class IpForwardingDaemonTest(unittest.TestCase): mocks.attach_mock(self.mock_setup, 'setup') self.mock_utils.ParseForwardedIps.return_value = desired self.mock_utils.GetForwardedIps.return_value = configured - result = 'result' + forwarded_ips = 'forwarded ips' + interface = 'interface' expected_add = ['d'] expected_remove = ['a', 'b'] - ip_forwarding_daemon.IpForwardingDaemon.HandleForwardedIps( - self.mock_setup, result) + ip_forwarding_daemon.IpForwardingDaemon._HandleForwardedIps( + self.mock_setup, forwarded_ips, interface) expected_calls = [ - mock.call.utils.ParseForwardedIps(result), - mock.call.utils.GetForwardedIps(), + mock.call.utils.ParseForwardedIps(forwarded_ips), + mock.call.utils.GetForwardedIps(interface), mock.call.setup._LogForwardedIpChanges( - configured, desired, expected_add, expected_remove), - mock.call.setup._AddForwardedIps(expected_add), - mock.call.setup._RemoveForwardedIps(expected_remove), + configured, desired, expected_add, expected_remove, interface), + mock.call.setup._AddForwardedIps(expected_add, interface), + mock.call.setup._RemoveForwardedIps(expected_remove, interface), + ] + self.assertEqual(mocks.mock_calls, expected_calls) + + def testHandleNetworkInterfaces(self): + mocks = mock.Mock() + mocks.attach_mock(self.mock_utils, 'utils') + mocks.attach_mock(self.mock_setup, 'setup') + self.mock_utils.GetNetworkInterface.side_effect = [ + 'eth0', 'eth1', 'eth2', None] + result = [ + {'mac': '1', 'forwardedIps': 'a'}, + {'mac': '2', 'forwardedIps': 'b'}, + {'mac': '3'}, + {'forwardedIps': 'c'}, + ] + + ip_forwarding_daemon.IpForwardingDaemon.HandleNetworkInterfaces( + self.mock_setup, result) + expected_calls = [ + mock.call.utils.GetNetworkInterface('1'), + mock.call.setup._HandleForwardedIps('a', 'eth0'), + mock.call.utils.GetNetworkInterface('2'), + mock.call.setup._HandleForwardedIps('b', 'eth1'), + mock.call.utils.GetNetworkInterface('3'), + mock.call.setup._HandleForwardedIps(None, 'eth2'), + mock.call.utils.GetNetworkInterface(None), + mock.call.setup.logger.warning(mock.ANY, None), ] self.assertEqual(mocks.mock_calls, expected_calls) diff --git a/google_compute_engine/ip_forwarding/tests/ip_forwarding_utils_test.py b/google_compute_engine/ip_forwarding/tests/ip_forwarding_utils_test.py index 0d32939..7c82dfb 100644 --- a/google_compute_engine/ip_forwarding/tests/ip_forwarding_utils_test.py +++ b/google_compute_engine/ip_forwarding/tests/ip_forwarding_utils_test.py @@ -16,6 +16,7 @@ """Unittest for ip_forwarding_utils.py module.""" from google_compute_engine.ip_forwarding import ip_forwarding_utils +from google_compute_engine.test_compat import builtin from google_compute_engine.test_compat import mock from google_compute_engine.test_compat import unittest @@ -24,9 +25,70 @@ class IpForwardingUtilsTest(unittest.TestCase): def setUp(self): self.mock_logger = mock.Mock() + self.interfaces = {'address': 'interface'} self.options = {'hello': 'world'} self.mock_utils = ip_forwarding_utils.IpForwardingUtils(self.mock_logger) - self.mock_utils.options = self.options + self.mock_utils.interfaces = self.interfaces + self.mock_utils.proto_id = 'proto' + + @mock.patch('google_compute_engine.ip_forwarding.ip_forwarding_utils.os.listdir') + def testCreateInterfaceMap(self, mock_listdir): + mock_open = mock.mock_open() + interface_map = { + '1': 'a', + '2': 'b', + '3': 'c', + } + mock_listdir.return_value = interface_map.values() + + with mock.patch('%s.open' % builtin, mock_open, create=False): + addresses = interface_map.keys() + addresses = ['%s\n' % address for address in addresses] + mock_open().read.side_effect = interface_map.keys() + self.assertEqual(self.mock_utils._CreateInterfaceMap(), interface_map) + + @mock.patch('google_compute_engine.ip_forwarding.ip_forwarding_utils.os.listdir') + def testCreateInterfaceMapError(self, mock_listdir): + mock_open = mock.mock_open() + mock_listdir.return_value = ['a', 'b', 'c'] + + with mock.patch('%s.open' % builtin, mock_open, create=False): + mock_open().read.side_effect = [ + '1', OSError('OSError'), IOError('IOError')] + self.assertEqual(self.mock_utils._CreateInterfaceMap(), {'1': 'a'}) + expected_calls = [ + mock.call.warning(mock.ANY, 'b', 'OSError'), + mock.call.warning(mock.ANY, 'c', 'IOError'), + ] + self.assertEqual(self.mock_logger.mock_calls, expected_calls) + + def testCreateRouteOptions(self): + # Default options. + expected_options = { + 'proto': 'proto', + 'scope': 'host', + } + self.assertEqual(self.mock_utils._CreateRouteOptions(), expected_options) + + # Update dictionary when arguments are specified. + expected_options = { + 'proto': 'proto', + 'scope': 'host', + 'num': 1, + 'string': 'hello world', + } + self.assertEqual( + self.mock_utils._CreateRouteOptions(num=1, string='hello world'), + expected_options) + + # Update the default options. + expected_options = { + 'proto': 'test 1', + 'scope': 'test 2', + } + self.assertEqual( + self.mock_utils._CreateRouteOptions(proto='test 1', scope='test 2'), + expected_options) @mock.patch('google_compute_engine.ip_forwarding.ip_forwarding_utils.subprocess') def testRunIpRoute(self, mock_subprocess): @@ -69,29 +131,10 @@ class IpForwardingUtilsTest(unittest.TestCase): self.mock_logger.warning.assert_called_once_with( mock.ANY, command, 'Test Error') - def testGetDefaultInterface(self): - mock_run = mock.Mock() - mock_run.side_effect = [ - bytes(b''), - bytes(b'invalid route\n'), - bytes(b'default invalid interface\n'), - bytes(b'default dev\n'), - bytes(b'\n\n\ndefault dev interface\n\n\n'), - bytes(b'default via ip dev interface\nip default eth0\n'), - bytes(b'ip default eth0\ndefault via ip dev interface\n'), - ] - self.mock_utils._RunIpRoute = mock_run - - # Invalid routes default to 'eth0'. - self.assertEqual(self.mock_utils._GetDefaultInterface(), 'eth0') - self.assertEqual(self.mock_utils._GetDefaultInterface(), 'eth0') - self.assertEqual(self.mock_utils._GetDefaultInterface(), 'eth0') - self.assertEqual(self.mock_utils._GetDefaultInterface(), 'eth0') - - # Valid routes where the expected response is 'interface'. - self.assertEqual(self.mock_utils._GetDefaultInterface(), 'interface') - self.assertEqual(self.mock_utils._GetDefaultInterface(), 'interface') - self.assertEqual(self.mock_utils._GetDefaultInterface(), 'interface') + def testGetNetworkInterface(self): + self.assertIsNone(self.mock_utils.GetNetworkInterface('invalid')) + self.assertEqual( + self.mock_utils.GetNetworkInterface('address'), 'interface') def testParseForwardedIps(self): self.assertEqual(self.mock_utils.ParseForwardedIps(None), []) @@ -133,41 +176,43 @@ class IpForwardingUtilsTest(unittest.TestCase): self.assertEqual(self.mock_logger.mock_calls, expected_calls) def testGetForwardedIps(self): + mock_options = mock.Mock() + mock_options.return_value = self.options mock_run = mock.Mock() - mock_run.return_value = '' + mock_run.return_value = 'a\nb\n' mock_parse = mock.Mock() mock_parse.return_value = ['Test'] + self.mock_utils._CreateRouteOptions = mock_options self.mock_utils._RunIpRoute = mock_run self.mock_utils.ParseForwardedIps = mock_parse - self.assertEqual(self.mock_utils.GetForwardedIps(), ['Test']) + self.assertEqual(self.mock_utils.GetForwardedIps('interface'), ['Test']) + mock_options.assert_called_once_with(dev='interface') mock_run.assert_called_once_with( args=['ls', 'table', 'local', 'type', 'local'], options=self.options) - mock_parse.assert_called_once_with([]) - - def testGetForwardedIpsSplit(self): - mock_run = mock.Mock() - mock_run.return_value = 'a\nb\n' - mock_parse = mock.Mock() - self.mock_utils._RunIpRoute = mock_run - self.mock_utils.ParseForwardedIps = mock_parse - - self.mock_utils.GetForwardedIps() mock_parse.assert_called_once_with(['a', 'b']) def testAddForwardedIp(self): + mock_options = mock.Mock() + mock_options.return_value = self.options mock_run = mock.Mock() + self.mock_utils._CreateRouteOptions = mock_options self.mock_utils._RunIpRoute = mock_run - self.mock_utils.AddForwardedIp('1.1.1.1') + self.mock_utils.AddForwardedIp('1.1.1.1', 'interface') + mock_options.assert_called_once_with(dev='interface') mock_run.assert_called_once_with( args=['add', 'to', 'local', '1.1.1.1/32'], options=self.options) def testRemoveForwardedIp(self): + mock_options = mock.Mock() + mock_options.return_value = self.options mock_run = mock.Mock() + self.mock_utils._CreateRouteOptions = mock_options self.mock_utils._RunIpRoute = mock_run - self.mock_utils.RemoveForwardedIp('1.1.1.1') + self.mock_utils.RemoveForwardedIp('1.1.1.1', 'interface') + mock_options.assert_called_once_with(dev='interface') mock_run.assert_called_once_with( args=['delete', 'to', 'local', '1.1.1.1/32'], options=self.options) |