From 26288e6803bc2976ce51805f2af43dfd28e32d1d Mon Sep 17 00:00:00 2001
From: Iustin Pop <iustin@google.com>
Date: Fri, 5 Mar 2010 16:55:27 +0100
Subject: [PATCH] Add a function to validate and normalize hostnames

This differs slightly from the specification, by allowing names to start
with digits, not checking the length of individual components, and
allowing underscores.

Signed-off-by: Iustin Pop <iustin@google.com>
Reviewed-by: Michael Hanselmann <hansmi@google.com>
---
 lib/utils.py                  | 23 ++++++++++++++++++++
 test/ganeti.utils_unittest.py | 41 +++++++++++++++++++++++++++++++++--
 2 files changed, 62 insertions(+), 2 deletions(-)

diff --git a/lib/utils.py b/lib/utils.py
index 52b799fe5..131f4c1a7 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 aad16c101..8392ad056 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()
-- 
GitLab