commit 5c947f38e749a1af1ff81590e52380611fa9c9f9
Author: Iustin Pop <iustin@google.com>
Date:   Wed Aug 8 09:37:52 2007 +0000

    Implement tag support for cluster, nodes and instances.
    
    This is only the backend part, from the command line the tags can't be
    read/modified yet.
    
    Reviewed-by: imsnah

diff --git a/lib/cmdlib.py b/lib/cmdlib.py
index 2400bc5..20f7008 100644
--- a/lib/cmdlib.py
+++ b/lib/cmdlib.py
@@ -3506,3 +3506,108 @@ class LUExportInstance(LogicalUnit):
           if not rpc.call_export_remove(node, instance.name):
             logger.Error("could not remove older export for instance %s"
                          " on node %s" % (instance.name, node))
+
+
+class TagsLU(NoHooksLU):
+  """Generic tags LU.
+
+  This is an abstract class which is the parent of all the other tags LUs.
+
+  """
+  def CheckPrereq(self):
+    """Check prerequisites.
+
+    """
+    if self.op.kind == constants.TAG_CLUSTER:
+      self.target = self.cfg.GetClusterInfo()
+    elif self.op.kind == constants.TAG_NODE:
+      name = self.cfg.ExpandNodeName(self.op.name)
+      if name is None:
+        raise errors.OpPrereqError, ("Invalid node name (%s)" %
+                                     (self.op.name,))
+      self.op.name = name
+      self.target = self.cfg.GetNodeInfo(name)
+    elif self.op.kind == constants.TAG_INSTANCE:
+      name = self.cfg.ExpandInstanceName(name)
+      if name is None:
+        raise errors.OpPrereqError, ("Invalid instance name (%s)" %
+                                     (self.op.name,))
+      self.op.name = name
+      self.target = self.cfg.GetInstanceInfo(name)
+    else:
+      raise errors.OpPrereqError, ("Wrong tag type requested (%s)" %
+                                   str(self.op.kind))
+
+
+class LUGetTags(TagsLU):
+  """Returns the tags of a given object.
+
+  """
+  _OP_REQP = ["kind", "name"]
+
+  def Exec(self, feedback_fn):
+    """Returns the tag list.
+
+    """
+    return self.target.GetTags()
+
+
+class LUAddTag(TagsLU):
+  """Sets a tag on a given object.
+
+  """
+  _OP_REQP = ["kind", "name", "tag"]
+
+  def CheckPrereq(self):
+    """Check prerequisites.
+
+    This checks the type and length of the tag name and value.
+
+    """
+    TagsLU.CheckPrereq(self)
+    objects.TaggableObject.ValidateTag(self.op.tag)
+
+  def Exec(self, feedback_fn):
+    """Sets the tag.
+
+    """
+    try:
+      self.target.AddTag(self.op.tag)
+    except errors.TagError, err:
+      raise errors.OpExecError, ("Error while setting tag: %s" % str(err))
+    try:
+      self.cfg.Update(self.target)
+    except errors.ConfigurationError:
+      raise errors.OpRetryError, ("There has been a modification to the"
+                                  " config file and the operation has been"
+                                  " aborted. Please retry.")
+
+
+class LUDelTag(TagsLU):
+  """Delete a tag from a given object.
+
+  """
+  _OP_REQP = ["kind", "name", "tag"]
+
+  def CheckPrereq(self):
+    """Check prerequisites.
+
+    This checks that we have the given tag.
+
+    """
+    TagsLU.CheckPrereq(self)
+    objects.TaggableObject.ValidateTag(self.op.tag)
+    if self.op.tag not in self.target.GetTags():
+      raise errors.OpPrereqError, ("Tag not found")
+
+  def Exec(self, feedback_fn):
+    """Remove the tag from the object.
+
+    """
+    self.target.RemoveTag(self.op.tag)
+    try:
+      self.cfg.Update(self.target)
+    except errors.ConfigurationError:
+      raise errors.OpRetryError, ("There has been a modification to the"
+                                  " config file and the operation has been"
+                                  " aborted. Please retry.")
diff --git a/lib/constants.py b/lib/constants.py
index 6103e66..09dd712 100644
--- a/lib/constants.py
+++ b/lib/constants.py
@@ -85,6 +85,13 @@ INISECT_INS = "instance"
 # common exit codes
 EXIT_NOTMASTER = 11
 
+# tags
+TAG_CLUSTER = "cluster"
+TAG_NODE = "node"
+TAG_INSTANCE = "instance"
+MAX_TAG_LEN = 128
+MAX_TAGS_PER_OBJ = 4096
+
 # others
 DEFAULT_BRIDGE = "xen-br0"
 SYNC_SPEED = 30 * 1024
diff --git a/lib/errors.py b/lib/errors.py
index 0971907..546ed27 100644
--- a/lib/errors.py
+++ b/lib/errors.py
@@ -136,6 +136,12 @@ class OpExecError(GenericError):
   """
 
 
+class OpRetryError(OpExecError):
+  """Error during OpCode execution, action can be retried.
+
+  """
+
+
 class OpCodeUnknown(GenericError):
   """Unknown opcode submitted.
 
@@ -173,3 +179,11 @@ class UnitParseError(GenericError):
 class SshKeyError(GenericError):
   """Invalid SSH key.
   """
+
+
+class TagError(GenericError):
+  """Generic tag error.
+
+  The argument to this exception will show the exact error.
+
+  """
diff --git a/lib/mcpu.py b/lib/mcpu.py
index 8caf8e2..2bfb79d 100644
--- a/lib/mcpu.py
+++ b/lib/mcpu.py
@@ -82,6 +82,10 @@ class Processor(object):
     # exports lu
     opcodes.OpQueryExports: cmdlib.LUQueryExports,
     opcodes.OpExportInstance: cmdlib.LUExportInstance,
+    # tags lu
+    opcodes.OpGetTags: cmdlib.LUGetTags,
+    opcodes.OpSetTag: cmdlib.LUAddTag,
+    opcodes.OpDelTag: cmdlib.LUDelTag,
     }
 
 
diff --git a/lib/objects.py b/lib/objects.py
index 2fb8969..087b204 100644
--- a/lib/objects.py
+++ b/lib/objects.py
@@ -30,8 +30,10 @@ pass to and from external parties.
 import cPickle
 from cStringIO import StringIO
 import ConfigParser
+import re
 
 from ganeti import errors
+from ganeti import constants
 
 
 __all__ = ["ConfigObject", "ConfigData", "NIC", "Disk", "Instance",
@@ -146,6 +148,59 @@ class ConfigObject(object):
     return ConfigObject.Load(StringIO(data))
 
 
+class TaggableObject(object):
+  """An generic class supporting tags.
+
+  """
+  @staticmethod
+  def ValidateTag(tag):
+    """Check if a tag is valid.
+
+    If the tag is invalid, an errors.TagError will be raised. The
+    function has no return value.
+
+    """
+    if not isinstance(tag, basestring):
+      raise errors.TagError, ("Invalid tag type (not a string)")
+    if len(tag) > constants.MAX_TAG_LEN:
+      raise errors.TagError, ("Tag too long (>%d)" %
+                              constants.MAX_TAG_LEN)
+    if not tag:
+      raise errors.TagError, ("Tags cannot be empty")
+    if not re.match("^[ \w.+*/:-]+$", tag):
+      raise errors.TagError, ("Tag contains invalid characters")
+
+  def GetTags(self):
+    """Return the tags list.
+
+    """
+    tags = getattr(self, "tags", None)
+    if tags is None:
+      tags = self.tags = set()
+    return tags
+
+  def AddTag(self, tag):
+    """Add a new tag.
+
+    """
+    self.ValidateTag(tag)
+    tags = self.GetTags()
+    if len(tags) >= constants.MAX_TAGS_PER_OBJ:
+      raise errors.TagError, ("Too many tags")
+    self.GetTags().add(tag)
+
+  def RemoveTag(self, tag):
+    """Remove a tag.
+
+    """
+    self.ValidateTag(tag)
+    tags = self.GetTags()
+    try:
+      tags.remove(tag)
+    except KeyError:
+      raise errors.TagError, ("Tag not found")
+
+
 class ConfigData(ConfigObject):
   """Top-level config object."""
   __slots__ = ["cluster", "nodes", "instances"]
@@ -231,7 +286,7 @@ class Disk(ConfigObject):
     return result
 
 
-class Instance(ConfigObject):
+class Instance(ConfigObject, TaggableObject):
   """Config object representing an instance."""
   __slots__ = [
     "name",
@@ -243,6 +298,7 @@ class Instance(ConfigObject):
     "nics",
     "disks",
     "disk_template",
+    "tags",
     ]
 
   def _ComputeSecondaryNodes(self):
@@ -337,12 +393,12 @@ class OS(ConfigObject):
     ]
 
 
-class Node(ConfigObject):
+class Node(ConfigObject, TaggableObject):
   """Config object representing a node."""
-  __slots__ = ["name", "primary_ip", "secondary_ip"]
+  __slots__ = ["name", "primary_ip", "secondary_ip", "tags"]
 
 
-class Cluster(ConfigObject):
+class Cluster(ConfigObject, TaggableObject):
   """Config object representing the cluster."""
   __slots__ = [
     "config_version",
@@ -353,8 +409,10 @@ class Cluster(ConfigObject):
     "mac_prefix",
     "volume_group_name",
     "default_bridge",
+    "tags",
     ]
 
+
 class SerializableConfigParser(ConfigParser.SafeConfigParser):
   """Simple wrapper over ConfigParse that allows serialization.
 
diff --git a/lib/opcodes.py b/lib/opcodes.py
index 106839a..a2f1c9a 100644
--- a/lib/opcodes.py
+++ b/lib/opcodes.py
@@ -239,3 +239,22 @@ class OpExportInstance(OpCode):
   """Export an instance."""
   OP_ID = "OP_BACKUP_EXPORT"
   __slots__ = ["instance_name", "target_node", "shutdown"]
+
+
+# Tags opcodes
+class OpGetTags(OpCode):
+  """Returns the tags of the given object."""
+  OP_ID = "OP_TAGS_GET"
+  __slots__ = ["kind", "name"]
+
+
+class OpSetTag(OpCode):
+  """Sets the value of a tag on a given object."""
+  OP_ID = "OP_TAGS_SET"
+  __slots__ = ["kind", "name", "tag"]
+
+
+class OpDelTag(OpCode):
+  """Remove a tag from a given object."""
+  OP_ID = "OP_TAGS_DEL"
+  __slots__ = ["kind", "name", "tag"]
