diff --git a/lib/utils.py b/lib/utils.py index 52b799fe5387e974a1a5d4da6657d6ff286772f2..131f4c1a7645bbe99f855befeaf252caed62c2e1 100644 --- a/lib/utils.py +++ b/lib/utils.py @@ -615,6 +615,8 @@ class HostInfo: """Class implementing resolver and hostname functionality """ + _VALID_NAME_RE = re.compile("^[a-z0-9._-]{1,255}$") + def __init__(self, name=None): """Initialize the host name object. @@ -665,6 +667,27 @@ class HostInfo: return result + @classmethod + def NormalizeName(cls, hostname): + """Validate and normalize the given hostname. + + @attention: the validation is a bit more relaxed than the standards + require; most importantly, we allow underscores in names + @raise errors.OpPrereqError: when the name is not valid + + """ + hostname = hostname.lower() + if (not cls._VALID_NAME_RE.match(hostname) or + # double-dots, meaning empty label + ".." in hostname or + # empty initial label + hostname.startswith(".")): + raise errors.OpPrereqError("Invalid hostname '%s'" % hostname, + errors.ECODE_INVAL) + if hostname.endswith("."): + hostname = hostname.rstrip(".") + return hostname + def GetHostInfo(name=None): """Lookup host name and raise an OpPrereqError for failures""" diff --git a/test/ganeti.utils_unittest.py b/test/ganeti.utils_unittest.py index aad16c101df4b556a8c0a9c19ee35dab862e09dd..8392ad056cc90b009509dd0ec518178a4aacbb29 100755 --- a/test/ganeti.utils_unittest.py +++ b/test/ganeti.utils_unittest.py @@ -47,10 +47,10 @@ from ganeti.utils import IsProcessAlive, RunCmd, \ ShellQuote, ShellQuoteArgs, TcpPing, ListVisibleFiles, \ SetEtcHostsEntry, RemoveEtcHostsEntry, FirstFree, OwnIpAddress, \ TailFile, ForceDictType, SafeEncode, IsNormAbsPath, FormatTime, \ - UnescapeAndSplit, RunParts, PathJoin + UnescapeAndSplit, RunParts, PathJoin, HostInfo from ganeti.errors import LockError, UnitParseError, GenericError, \ - ProgrammerError + ProgrammerError, OpPrereqError class TestIsProcessAlive(unittest.TestCase): @@ -1284,5 +1284,42 @@ class TestPathJoin(unittest.TestCase): self.failUnlessRaises(ValueError, PathJoin, "/a", "/b") +class TestHostInfo(unittest.TestCase): + """Testing case for HostInfo""" + + def testUppercase(self): + data = "AbC.example.com" + self.failUnlessEqual(HostInfo.NormalizeName(data), data.lower()) + + def testTooLongName(self): + data = "a.b." + "c" * 255 + self.failUnlessRaises(OpPrereqError, HostInfo.NormalizeName, data) + + def testTrailingDot(self): + data = "a.b.c" + self.failUnlessEqual(HostInfo.NormalizeName(data + "."), data) + + def testInvalidName(self): + data = [ + "a b", + "a/b", + ".a.b", + "a..b", + ] + for value in data: + self.failUnlessRaises(OpPrereqError, HostInfo.NormalizeName, value) + + def testValidName(self): + data = [ + "a.b", + "a-b", + "a_b", + "a.b.c", + ] + for value in data: + HostInfo.NormalizeName(value) + + + if __name__ == '__main__': testutils.GanetiTestProgram()