utils.text: Function to verify MAC address prefix
authorMichael Hanselmann <hansmi@google.com>
Mon, 3 Dec 2012 16:08:42 +0000 (17:08 +0100)
committerMichael Hanselmann <hansmi@google.com>
Tue, 4 Dec 2012 09:22:08 +0000 (10:22 +0100)
The network management code needs to verify a MAC address prefix.
Instead of (ab)using NormalizeAndValidateMac, clean code should be used.
Unit tests for NormalizeAndValidateMac are updated and new ones for
NormalizeAndValidateThreeOctetMacPrefix are added.

Signed-off-by: Michael Hanselmann <hansmi@google.com>
Reviewed-by: Guido Trotter <ultrotter@google.com>

lib/utils/text.py
test/ganeti.utils.text_unittest.py

index 84de0bb..6174caf 100644 (file)
@@ -37,15 +37,15 @@ _PARSEUNIT_REGEX = re.compile(r"^([.\d]+)\s*([a-zA-Z]+)?$")
 #: Characters which don't need to be quoted for shell commands
 _SHELL_UNQUOTED_RE = re.compile("^[-.,=:/_+@A-Za-z0-9]+$")
 
-#: MAC checker regexp
-_MAC_CHECK_RE = re.compile("^([0-9a-f]{2}:){5}[0-9a-f]{2}$", re.I)
-
 #: Shell param checker regexp
 _SHELLPARAM_REGEX = re.compile(r"^[-a-zA-Z0-9._+/:%@]+$")
 
 #: ASCII equivalent of unicode character 'HORIZONTAL ELLIPSIS' (U+2026)
 _ASCII_ELLIPSIS = "..."
 
+#: MAC address octet
+_MAC_ADDR_OCTET_RE = r"[0-9a-f]{2}"
+
 
 def MatchNameComponent(key, name_list, case_sensitive=True):
   """Try to match a name against a list.
@@ -301,25 +301,75 @@ def GenerateSecret(numbytes=20):
   return os.urandom(numbytes).encode("hex")
 
 
-def NormalizeAndValidateMac(mac):
-  """Normalizes and check if a MAC address is valid.
+def _MakeMacAddrRegexp(octets):
+  """Builds a regular expression for verifying MAC addresses.
 
-  Checks whether the supplied MAC address is formally correct, only
-  accepts colon separated format. Normalize it to all lower.
+  @type octets: integer
+  @param octets: How many octets to expect (1-6)
+  @return: Compiled regular expression
 
-  @type mac: str
-  @param mac: the MAC to be validated
-  @rtype: str
-  @return: returns the normalized and validated MAC.
+  """
+  assert octets > 0
+  assert octets <= 6
+
+  return re.compile("^%s$" % ":".join([_MAC_ADDR_OCTET_RE] * octets),
+                    re.I)
+
+
+#: Regular expression for full MAC address
+_MAC_CHECK_RE = _MakeMacAddrRegexp(6)
+
+#: Regular expression for half a MAC address
+_MAC_PREFIX_CHECK_RE = _MakeMacAddrRegexp(3)
+
+
+def _MacAddressCheck(check_re, mac, msg):
+  """Checks a MAC address using a regular expression.
+
+  @param check_re: Compiled regular expression as returned by C{re.compile}
+  @type mac: string
+  @param mac: MAC address to be validated
+  @type msg: string
+  @param msg: Error message (%s will be replaced with MAC address)
+
+  """
+  if check_re.match(mac):
+    return mac.lower()
+
+  raise errors.OpPrereqError(msg % mac, errors.ECODE_INVAL)
 
-  @raise errors.OpPrereqError: If the MAC isn't valid
+
+def NormalizeAndValidateMac(mac):
+  """Normalizes and check if a MAC address is valid and contains six octets.
+
+  Checks whether the supplied MAC address is formally correct. Accepts
+  colon-separated format only. Normalize it to all lower case.
+
+  @type mac: string
+  @param mac: MAC address to be validated
+  @rtype: string
+  @return: Normalized and validated MAC address
+  @raise errors.OpPrereqError: If the MAC address isn't valid
 
   """
-  if not _MAC_CHECK_RE.match(mac):
-    raise errors.OpPrereqError("Invalid MAC address '%s'" % mac,
-                               errors.ECODE_INVAL)
+  return _MacAddressCheck(_MAC_CHECK_RE, mac, "Invalid MAC address '%s'")
 
-  return mac.lower()
+
+def NormalizeAndValidateThreeOctetMacPrefix(mac):
+  """Normalizes a potential MAC address prefix (three octets).
+
+  Checks whether the supplied string is a valid MAC address prefix consisting
+  of three colon-separated octets. The result is normalized to all lower case.
+
+  @type mac: string
+  @param mac: Prefix to be validated
+  @rtype: string
+  @return: Normalized and validated prefix
+  @raise errors.OpPrereqError: If the MAC address prefix isn't valid
+
+  """
+  return _MacAddressCheck(_MAC_PREFIX_CHECK_RE, mac,
+                          "Invalid MAC address prefix '%s'")
 
 
 def SafeEncode(text):
index 181e2f6..c01bcf2 100755 (executable)
@@ -356,14 +356,28 @@ class TestShellWriter(unittest.TestCase):
 
 class TestNormalizeAndValidateMac(unittest.TestCase):
   def testInvalid(self):
-    self.assertRaises(errors.OpPrereqError,
-                      utils.NormalizeAndValidateMac, "xxx")
+    for i in ["xxx", "00:11:22:33:44:55:66", "zz:zz:zz:zz:zz:zz"]:
+      self.assertRaises(errors.OpPrereqError, utils.NormalizeAndValidateMac, i)
 
   def testNormalization(self):
     for mac in ["aa:bb:cc:dd:ee:ff", "00:AA:11:bB:22:cc"]:
       self.assertEqual(utils.NormalizeAndValidateMac(mac), mac.lower())
 
 
+class TestNormalizeAndValidateThreeOctetMacPrefix(unittest.TestCase):
+  def testInvalid(self):
+    for i in ["xxx", "00:11:22:33:44:55:66", "zz:zz:zz:zz:zz:zz",
+              "aa:bb:cc:dd:ee:ff", "00:AA:11:bB:22:cc",
+              "00:11:"]:
+      self.assertRaises(errors.OpPrereqError,
+                        utils.NormalizeAndValidateThreeOctetMacPrefix, i)
+
+  def testNormalization(self):
+    for mac in ["aa:bb:cc", "00:AA:11"]:
+      self.assertEqual(utils.NormalizeAndValidateThreeOctetMacPrefix(mac),
+                       mac.lower())
+
+
 class TestSafeEncode(unittest.TestCase):
   """Test case for SafeEncode"""