Added the following functions to netutils: - IsValidInterface - GetInterfaceIpAddresses - _GetIpAddressFromIpOutputLine
Added the following static methods to netutils.IPAddress: - GetAddressFamilyFromVersion - GetVersionFromAddressFamily Added unit tests for the new methods in netutils.IPAddress and for _GetIpAddressFromIpOutputLine Signed-off-by: Andrea Spadaccini <[email protected]> --- lib/netutils.py | 111 +++++++++++++++++++++++++++++++++++++- test/ganeti.netutils_unittest.py | 59 ++++++++++++++++++++ 2 files changed, 169 insertions(+), 1 deletions(-) diff --git a/lib/netutils.py b/lib/netutils.py index 8b51e46..322d3fb 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,32 @@ from ganeti import errors _STRUCT_UCRED = "iII" _STRUCT_UCRED_SIZE = struct.calcsize(_STRUCT_UCRED) +# Regex used to find IP addresses in the output of ip +_IP_RE = re.compile(r"(?P<family>inet6?)\s+(?P<ip>[.:a-z0-9]+)/", 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 _GetIpAddressFromIpOutputLine(output_row): + """Parses a row of the ip command output and retrieves the IP address and + version. + + @param output_row: a row of the output of the ip command; + @rtype: tuple; (string, int) + @return: the retrieved IP address and version + + """ + result = (None, None) + match = _IP_RE.search(output_row) + if match and IPAddress.IsValid(match.group("ip")): + result = (match.group("ip"), _NAME_TO_IP_VER[match.group("family")]) + + return result def GetSocketCredentials(sock): """Returns the credentials of the foreign process connected to a socket. @@ -62,6 +91,48 @@ 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 + + # We use "inet" and "inet6" as keys because this is what we get from the + # regex. Will later be converted to constants.IP_* + addresses = { + constants.IP4_VERSION: [], + constants.IP6_VERSION: [] + } + for row in result.output.splitlines(): + ip, version = _GetIpAddressFromIpOutputLine(row) + if ip: + addresses[version].append(ip) + + return addresses + def GetHostname(name=None, family=None): """Returns a Hostname object. @@ -366,7 +437,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 +453,44 @@ 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/ganeti.netutils_unittest.py b/test/ganeti.netutils_unittest.py index e9ed0db..35eadb2 100755 --- a/test/ganeti.netutils_unittest.py +++ b/test/ganeti.netutils_unittest.py @@ -171,6 +171,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 +442,44 @@ class TestFormatAddress(unittest.TestCase): self.assertRaises(errors.ParameterError, netutils.FormatAddress, ("::1"), family=socket.AF_INET ) +class TestIpParsing(unittest.TestCase): + """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: + fake_ip_output = " inet %s/8 scope host lo" % addr + (ip, version) = netutils._GetIpAddressFromIpOutputLine(fake_ip_output) + self.failUnless(ip == addr and version == constants.IP4_VERSION) + + 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: + fake_ip_output = " inet6 %s/8 scope host lo" % addr + (ip, version) = netutils._GetIpAddressFromIpOutputLine(fake_ip_output) + self.failUnless(ip == addr and version == constants.IP6_VERSION) + + # Check for text that should not be matched, like the MAC address + invalid_output = ["link/ether ea:95:ae:6b:b7:c1 brd ff:ff:ff:ff:ff:ff"] + for out in invalid_output: + (ip, version) = netutils._GetIpAddressFromIpOutputLine(out) + self.failIf(ip or version) + if __name__ == "__main__": testutils.GanetiTestProgram() -- 1.7.3.1
