From 375312365901324c753fd221d3210fd8a4f4e0fc Mon Sep 17 00:00:00 2001 From: Andrea Spadaccini <spadaccio@google.com> Date: Thu, 11 Aug 2011 11:30:47 +0100 Subject: [PATCH] Added helper functions in netutils and related constants Added the following functions to netutils: - IsValidInterface - GetInterfaceIpAddresses - _GetIpAddressesFromIpOutput Added the following static methods to netutils.IPAddress: - GetAddressFamilyFromVersion - GetVersionFromAddressFamily Added unit tests for the new methods in netutils.IPAddress, for the IP address search regex and for GetInterfaceIpAddresses Signed-off-by: Andrea Spadaccini <spadaccio@google.com> Signed-off-by: Michael Hanselmann <hansmi@google.com> Reviewed-by: Michael Hanselmann <hansmi@google.com> --- lib/netutils.py | 107 ++++++++++++++++++++- test/data/ip-addr-show-dummy0.txt | 5 + test/data/ip-addr-show-lo-ipv4.txt | 2 + test/data/ip-addr-show-lo-ipv6.txt | 3 + test/data/ip-addr-show-lo-oneline-ipv4.txt | 1 + test/data/ip-addr-show-lo-oneline-ipv6.txt | 1 + test/data/ip-addr-show-lo-oneline.txt | 3 + test/data/ip-addr-show-lo.txt | 5 + test/ganeti.netutils_unittest.py | 82 ++++++++++++++++ 9 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 test/data/ip-addr-show-dummy0.txt create mode 100644 test/data/ip-addr-show-lo-ipv4.txt create mode 100644 test/data/ip-addr-show-lo-ipv6.txt create mode 100644 test/data/ip-addr-show-lo-oneline-ipv4.txt create mode 100644 test/data/ip-addr-show-lo-oneline-ipv6.txt create mode 100644 test/data/ip-addr-show-lo-oneline.txt create mode 100644 test/data/ip-addr-show-lo.txt diff --git a/lib/netutils.py b/lib/netutils.py index 8b51e466f..11d515010 100644 --- a/lib/netutils.py +++ b/lib/netutils.py @@ -28,13 +28,16 @@ the command line scripts. import errno +import os import re import socket import struct import IN +import logging from ganeti import constants from ganeti import errors +from ganeti import utils # Structure definition for getsockopt(SOL_SOCKET, SO_PEERCRED, ...): # struct ucred { pid_t pid; uid_t uid; gid_t gid; }; @@ -48,6 +51,38 @@ from ganeti import errors _STRUCT_UCRED = "iII" _STRUCT_UCRED_SIZE = struct.calcsize(_STRUCT_UCRED) +# Regexes used to find IP addresses in the output of ip. +_IP_RE_TEXT = r"[.:a-z0-9]+" # separate for testing purposes +_IP_FAMILY_RE = re.compile(r"(?P<family>inet6?)\s+(?P<ip>%s)/" % _IP_RE_TEXT, + re.IGNORECASE) + +# Dict used to convert from a string representing an IP family to an IP +# version +_NAME_TO_IP_VER = { + "inet": constants.IP4_VERSION, + "inet6": constants.IP6_VERSION, + } + + +def _GetIpAddressesFromIpOutput(ip_output): + """Parses the output of the ip command and retrieves the IP addresses and + version. + + @param ip_output: string containing the output of the ip command; + @rtype: dict; (int, list) + @return: a dict having as keys the IP versions and as values the + corresponding list of addresses found in the IP output. + + """ + addr = dict((i, []) for i in _NAME_TO_IP_VER.values()) + + for row in ip_output.splitlines(): + match = _IP_FAMILY_RE.search(row) + if match and IPAddress.IsValid(match.group("ip")): + addr[_NAME_TO_IP_VER[match.group("family")]].append(match.group("ip")) + + return addr + def GetSocketCredentials(sock): """Returns the credentials of the foreign process connected to a socket. @@ -62,6 +97,39 @@ def GetSocketCredentials(sock): return struct.unpack(_STRUCT_UCRED, peercred) +def IsValidInterface(ifname): + """Validate an interface name. + + @type ifname: string + @param ifname: Name of the network interface + @return: boolean indicating whether the interface name is valid or not. + + """ + return os.path.exists(utils.PathJoin("/sys/class/net", ifname)) + + +def GetInterfaceIpAddresses(ifname): + """Returns the IP addresses associated to the interface. + + @type ifname: string + @param ifname: Name of the network interface + @return: A dict having for keys the IP version (either + L{constants.IP4_VERSION} or L{constants.IP6_VERSION}) and for + values the lists of IP addresses of the respective version + associated to the interface + + """ + result = utils.RunCmd([constants.IP_COMMAND_PATH, "-o", "addr", "show", + ifname]) + + if result.failed: + logging.error("Error running the ip command while getting the IP" + " addresses of %s", ifname) + return None + + return _GetIpAddressesFromIpOutput(result.output) + + def GetHostname(name=None, family=None): """Returns a Hostname object. @@ -366,7 +434,7 @@ class IPAddress(object): @type address: str @param address: ip address whose family will be returned @rtype: int - @return: socket.AF_INET or socket.AF_INET6 + @return: C{socket.AF_INET} or C{socket.AF_INET6} @raise errors.GenericError: for invalid addresses """ @@ -382,6 +450,43 @@ class IPAddress(object): raise errors.IPAddressError("Invalid address '%s'" % address) + @staticmethod + def GetVersionFromAddressFamily(family): + """Convert an IP address family to the corresponding IP version. + + @type family: int + @param family: IP address family, one of socket.AF_INET or socket.AF_INET6 + @return: an int containing the IP version, one of L{constants.IP4_VERSION} + or L{constants.IP6_VERSION} + @raise errors.ProgrammerError: for unknown families + + """ + if family == socket.AF_INET: + return constants.IP4_VERSION + elif family == socket.AF_INET6: + return constants.IP6_VERSION + + raise errors.ProgrammerError("%s is not a valid IP address family" % family) + + @staticmethod + def GetAddressFamilyFromVersion(version): + """Convert an IP version to the corresponding IP address family. + + @type version: int + @param version: IP version, one of L{constants.IP4_VERSION} or + L{constants.IP6_VERSION} + @return: an int containing the IP address family, one of C{socket.AF_INET} + or C{socket.AF_INET6} + @raise errors.ProgrammerError: for unknown IP versions + + """ + if version == constants.IP4_VERSION: + return socket.AF_INET + elif version == constants.IP6_VERSION: + return socket.AF_INET6 + + raise errors.ProgrammerError("%s is not a valid IP version" % version) + @classmethod def IsLoopback(cls, address): """Determine whether it is a loopback address. diff --git a/test/data/ip-addr-show-dummy0.txt b/test/data/ip-addr-show-dummy0.txt new file mode 100644 index 000000000..786e9aed7 --- /dev/null +++ b/test/data/ip-addr-show-dummy0.txt @@ -0,0 +1,5 @@ +7: dummy0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN + link/ether 06:d2:06:24:99:dc brd ff:ff:ff:ff:ff:ff + inet 192.0.2.1/32 scope global dummy0 + inet6 2001:db8:85a3::8a2e:370:7334/128 scope global + valid_lft forever preferred_lft forever diff --git a/test/data/ip-addr-show-lo-ipv4.txt b/test/data/ip-addr-show-lo-ipv4.txt new file mode 100644 index 000000000..71064609d --- /dev/null +++ b/test/data/ip-addr-show-lo-ipv4.txt @@ -0,0 +1,2 @@ +1: lo: <LOOPBACK,UP,LOWER_UP> mtu 16436 qdisc noqueue state UNKNOWN + inet 127.0.0.1/8 scope host lo diff --git a/test/data/ip-addr-show-lo-ipv6.txt b/test/data/ip-addr-show-lo-ipv6.txt new file mode 100644 index 000000000..5aef079e9 --- /dev/null +++ b/test/data/ip-addr-show-lo-ipv6.txt @@ -0,0 +1,3 @@ +1: lo: <LOOPBACK,UP,LOWER_UP> mtu 16436 + inet6 ::1/128 scope host + valid_lft forever preferred_lft forever diff --git a/test/data/ip-addr-show-lo-oneline-ipv4.txt b/test/data/ip-addr-show-lo-oneline-ipv4.txt new file mode 100644 index 000000000..105e2a309 --- /dev/null +++ b/test/data/ip-addr-show-lo-oneline-ipv4.txt @@ -0,0 +1 @@ +1: lo inet 127.0.0.1/8 scope host lo diff --git a/test/data/ip-addr-show-lo-oneline-ipv6.txt b/test/data/ip-addr-show-lo-oneline-ipv6.txt new file mode 100644 index 000000000..3cf19a89f --- /dev/null +++ b/test/data/ip-addr-show-lo-oneline-ipv6.txt @@ -0,0 +1 @@ +1: lo inet6 ::1/128 scope host \ valid_lft forever preferred_lft forever diff --git a/test/data/ip-addr-show-lo-oneline.txt b/test/data/ip-addr-show-lo-oneline.txt new file mode 100644 index 000000000..e0a665bb6 --- /dev/null +++ b/test/data/ip-addr-show-lo-oneline.txt @@ -0,0 +1,3 @@ +1: lo: <LOOPBACK,UP,LOWER_UP> mtu 16436 qdisc noqueue state UNKNOWN \ link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 +1: lo inet 127.0.0.1/8 scope host lo +1: lo inet6 ::1/128 scope host \ valid_lft forever preferred_lft forever diff --git a/test/data/ip-addr-show-lo.txt b/test/data/ip-addr-show-lo.txt new file mode 100644 index 000000000..afa4e5de7 --- /dev/null +++ b/test/data/ip-addr-show-lo.txt @@ -0,0 +1,5 @@ +1: lo: <LOOPBACK,UP,LOWER_UP> mtu 16436 qdisc noqueue state UNKNOWN + link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 + inet 127.0.0.1/8 scope host lo + inet6 ::1/128 scope host + valid_lft forever preferred_lft forever diff --git a/test/ganeti.netutils_unittest.py b/test/ganeti.netutils_unittest.py index e9ed0dbae..dd7cd479d 100755 --- a/test/ganeti.netutils_unittest.py +++ b/test/ganeti.netutils_unittest.py @@ -22,6 +22,7 @@ """Script for unittesting the netutils module""" import os +import re import shutil import socket import tempfile @@ -171,6 +172,27 @@ class TestIPAddress(unittest.TestCase): self.assertFalse(netutils.IPAddress.Own("192.0.2.1"), "Should not own IP address 192.0.2.1") + def testFamilyVersionConversions(self): + # IPAddress.GetAddressFamilyFromVersion + self.assertEqual( + netutils.IPAddress.GetAddressFamilyFromVersion(constants.IP4_VERSION), + socket.AF_INET) + self.assertEqual( + netutils.IPAddress.GetAddressFamilyFromVersion(constants.IP6_VERSION), + socket.AF_INET6) + self.assertRaises(errors.ProgrammerError, + netutils.IPAddress.GetAddressFamilyFromVersion, 3) + + # IPAddress.GetVersionFromAddressFamily + self.assertEqual( + netutils.IPAddress.GetVersionFromAddressFamily(socket.AF_INET), + constants.IP4_VERSION) + self.assertEqual( + netutils.IPAddress.GetVersionFromAddressFamily(socket.AF_INET6), + constants.IP6_VERSION) + self.assertRaises(errors.ProgrammerError, + netutils.IPAddress.GetVersionFromAddressFamily, socket.AF_UNIX) + class TestIP4Address(unittest.TestCase): def testGetIPIntFromString(self): @@ -421,6 +443,66 @@ class TestFormatAddress(unittest.TestCase): self.assertRaises(errors.ParameterError, netutils.FormatAddress, ("::1"), family=socket.AF_INET ) +class TestIpParsing(testutils.GanetiTestCase): + """Test the code that parses the ip command output""" + + def testIp4(self): + valid_addresses = [constants.IP4_ADDRESS_ANY, + constants.IP4_ADDRESS_LOCALHOST, + "192.0.2.1", # RFC5737, IPv4 address blocks for docs + "198.51.100.1", + "203.0.113.1", + ] + for addr in valid_addresses: + self.failUnless(re.search(netutils._IP_RE_TEXT, addr)) + + def testIp6(self): + valid_addresses = [constants.IP6_ADDRESS_ANY, + constants.IP6_ADDRESS_LOCALHOST, + "0:0:0:0:0:0:0:1", # other form for IP6_ADDRESS_LOCALHOST + "0:0:0:0:0:0:0:0", # other form for IP6_ADDRESS_ANY + "2001:db8:85a3::8a2e:370:7334", # RFC3849 IP6 docs block + "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + "0:0:0:0:0:FFFF:192.0.2.1", # IPv4-compatible IPv6 + "::FFFF:192.0.2.1", + "0:0:0:0:0:0:203.0.113.1", # IPv4-mapped IPv6 + "::203.0.113.1", + ] + for addr in valid_addresses: + self.failUnless(re.search(netutils._IP_RE_TEXT, addr)) + + def testParseIpCommandOutput(self): + # IPv4-only, fake loopback interface + tests = ["ip-addr-show-lo-ipv4.txt", "ip-addr-show-lo-oneline-ipv4.txt"] + for test_file in tests: + data = self._ReadTestData(test_file) + addr = netutils._GetIpAddressesFromIpOutput(data) + self.failUnless(len(addr[4]) == 1 and addr[4][0] == "127.0.0.1" and not + addr[6]) + + # IPv6-only, fake loopback interface + tests = ["ip-addr-show-lo-ipv6.txt", "ip-addr-show-lo-ipv6.txt"] + for test_file in tests: + data = self._ReadTestData(test_file) + addr = netutils._GetIpAddressesFromIpOutput(data) + self.failUnless(len(addr[6]) == 1 and addr[6][0] == "::1" and not addr[4]) + + # IPv4 and IPv6, fake loopback interface + tests = ["ip-addr-show-lo.txt", "ip-addr-show-lo-oneline.txt"] + for test_file in tests: + data = self._ReadTestData(test_file) + addr = netutils._GetIpAddressesFromIpOutput(data) + self.failUnless(len(addr[6]) == 1 and addr[6][0] == "::1" and + len(addr[4]) == 1 and addr[4][0] == "127.0.0.1") + + # IPv4 and IPv6, dummy interface + data = self._ReadTestData("ip-addr-show-dummy0.txt") + addr = netutils._GetIpAddressesFromIpOutput(data) + self.failUnless(len(addr[6]) == 1 and + addr[6][0] == "2001:db8:85a3::8a2e:370:7334" and + len(addr[4]) == 1 and + addr[4][0] == "192.0.2.1") + if __name__ == "__main__": testutils.GanetiTestProgram() -- GitLab