Add ParseCpuMask() utility function
[ganeti-local] / lib / objects.py
index 969901c..be6f2d7 100644 (file)
@@ -26,6 +26,12 @@ pass to and from external parties.
 
 """
 
 
 """
 
+# pylint: disable-msg=E0203,W0201
+
+# E0203: Access to member %r before its definition, since we use
+# objects.py which doesn't explicitely initialise its members
+
+# W0201: Attribute '%s' defined outside __init__
 
 import ConfigParser
 import re
 
 import ConfigParser
 import re
@@ -42,19 +48,28 @@ __all__ = ["ConfigObject", "ConfigData", "NIC", "Disk", "Instance",
 _TIMESTAMPS = ["ctime", "mtime"]
 _UUID = ["uuid"]
 
 _TIMESTAMPS = ["ctime", "mtime"]
 _UUID = ["uuid"]
 
-def FillDict(defaults_dict, custom_dict):
+
+def FillDict(defaults_dict, custom_dict, skip_keys=None):
   """Basic function to apply settings on top a default dict.
 
   @type defaults_dict: dict
   @param defaults_dict: dictionary holding the default values
   @type custom_dict: dict
   @param custom_dict: dictionary holding customized value
   """Basic function to apply settings on top a default dict.
 
   @type defaults_dict: dict
   @param defaults_dict: dictionary holding the default values
   @type custom_dict: dict
   @param custom_dict: dictionary holding customized value
+  @type skip_keys: list
+  @param skip_keys: which keys not to fill
   @rtype: dict
   @return: dict with the 'full' values
 
   """
   ret_dict = copy.deepcopy(defaults_dict)
   ret_dict.update(custom_dict)
   @rtype: dict
   @return: dict with the 'full' values
 
   """
   ret_dict = copy.deepcopy(defaults_dict)
   ret_dict.update(custom_dict)
+  if skip_keys:
+    for k in skip_keys:
+      try:
+        del ret_dict[k]
+      except KeyError:
+        pass
   return ret_dict
 
 
   return ret_dict
 
 
@@ -95,16 +110,27 @@ class ConfigObject(object):
       setattr(self, k, v)
 
   def __getattr__(self, name):
       setattr(self, k, v)
 
   def __getattr__(self, name):
-    if name not in self.__slots__:
+    if name not in self._all_slots():
       raise AttributeError("Invalid object attribute %s.%s" %
                            (type(self).__name__, name))
     return None
 
   def __setstate__(self, state):
       raise AttributeError("Invalid object attribute %s.%s" %
                            (type(self).__name__, name))
     return None
 
   def __setstate__(self, state):
+    slots = self._all_slots()
     for name in state:
     for name in state:
-      if name in self.__slots__:
+      if name in slots:
         setattr(self, name, state[name])
 
         setattr(self, name, state[name])
 
+  @classmethod
+  def _all_slots(cls):
+    """Compute the list of all declared slots for a class.
+
+    """
+    slots = []
+    for parent in cls.__mro__:
+      slots.extend(getattr(parent, "__slots__", []))
+    return slots
+
   def ToDict(self):
     """Convert to a dict holding only standard python types.
 
   def ToDict(self):
     """Convert to a dict holding only standard python types.
 
@@ -116,7 +142,7 @@ class ConfigObject(object):
 
     """
     result = {}
 
     """
     result = {}
-    for name in self.__slots__:
+    for name in self._all_slots():
       value = getattr(self, name, None)
       if value is not None:
         result[name] = value
       value = getattr(self, name, None)
       if value is not None:
         result[name] = value
@@ -141,7 +167,7 @@ class ConfigObject(object):
       raise errors.ConfigurationError("Invalid object passed to FromDict:"
                                       " expected dict, got %s" % type(val))
     val_str = dict([(str(k), v) for k, v in val.iteritems()])
       raise errors.ConfigurationError("Invalid object passed to FromDict:"
                                       " expected dict, got %s" % type(val))
     val_str = dict([(str(k), v) for k, v in val.iteritems()])
-    obj = cls(**val_str)
+    obj = cls(**val_str) # pylint: disable-msg=W0142
     return obj
 
   @staticmethod
     return obj
 
   @staticmethod
@@ -209,10 +235,11 @@ class TaggableObject(ConfigObject):
   """An generic class supporting tags.
 
   """
   """An generic class supporting tags.
 
   """
-  __slots__ = ConfigObject.__slots__ + ["tags"]
+  __slots__ = ["tags"]
+  VALID_TAG_RE = re.compile("^[\w.+*/:@-]+$")
 
 
-  @staticmethod
-  def ValidateTag(tag):
+  @classmethod
+  def ValidateTag(cls, tag):
     """Check if a tag is valid.
 
     If the tag is invalid, an errors.TagError will be raised. The
     """Check if a tag is valid.
 
     If the tag is invalid, an errors.TagError will be raised. The
@@ -226,7 +253,7 @@ class TaggableObject(ConfigObject):
                             constants.MAX_TAG_LEN)
     if not tag:
       raise errors.TagError("Tags cannot be empty")
                             constants.MAX_TAG_LEN)
     if not tag:
       raise errors.TagError("Tags cannot be empty")
-    if not re.match("^[\w.+*/:-]+$", tag):
+    if not cls.VALID_TAG_RE.match(tag):
       raise errors.TagError("Tag contains invalid characters")
 
   def GetTags(self):
       raise errors.TagError("Tag contains invalid characters")
 
   def GetTags(self):
@@ -313,6 +340,21 @@ class ConfigData(ConfigObject):
     obj.instances = cls._ContainerFromDicts(obj.instances, dict, Instance)
     return obj
 
     obj.instances = cls._ContainerFromDicts(obj.instances, dict, Instance)
     return obj
 
+  def HasAnyDiskOfType(self, dev_type):
+    """Check if in there is at disk of the given type in the configuration.
+
+    @type dev_type: L{constants.LDS_BLOCK}
+    @param dev_type: the type to look for
+    @rtype: boolean
+    @return: boolean indicating if a disk of the given type was found or not
+
+    """
+    for instance in self.instances.values():
+      for disk in instance.disks:
+        if disk.IsBasedOnDiskType(dev_type):
+          return True
+    return False
+
   def UpgradeConfig(self):
     """Fill defaults for missing configuration values.
 
   def UpgradeConfig(self):
     """Fill defaults for missing configuration values.
 
@@ -322,6 +364,12 @@ class ConfigData(ConfigObject):
       node.UpgradeConfig()
     for instance in self.instances.values():
       instance.UpgradeConfig()
       node.UpgradeConfig()
     for instance in self.instances.values():
       instance.UpgradeConfig()
+    if self.cluster.drbd_usermode_helper is None:
+      # To decide if we set an helper let's check if at least one instance has
+      # a DRBD disk. This does not cover all the possible scenarios but it
+      # gives a good approximation.
+      if self.HasAnyDiskOfType(constants.LD_DRBD8):
+        self.cluster.drbd_usermode_helper = constants.DEFAULT_DRBD_HELPER
 
 
 class NIC(ConfigObject):
 
 
 class NIC(ConfigObject):
@@ -341,7 +389,7 @@ class NIC(ConfigObject):
       err = "Invalid nic mode: %s" % nicparams[constants.NIC_MODE]
       raise errors.ConfigurationError(err)
 
       err = "Invalid nic mode: %s" % nicparams[constants.NIC_MODE]
       raise errors.ConfigurationError(err)
 
-    if (nicparams[constants.NIC_MODE] is constants.NIC_MODE_BRIDGED and
+    if (nicparams[constants.NIC_MODE] == constants.NIC_MODE_BRIDGED and
         not nicparams[constants.NIC_LINK]):
       err = "Missing bridged nic link"
       raise errors.ConfigurationError(err)
         not nicparams[constants.NIC_LINK]):
       err = "Missing bridged nic link"
       raise errors.ConfigurationError(err)
@@ -356,7 +404,8 @@ class NIC(ConfigObject):
         self.nicparams[constants.NIC_MODE] = constants.NIC_MODE_BRIDGED
         self.nicparams[constants.NIC_LINK] = self.bridge
     # bridge is no longer used it 2.1. The slot is left there to support
         self.nicparams[constants.NIC_MODE] = constants.NIC_MODE_BRIDGED
         self.nicparams[constants.NIC_LINK] = self.bridge
     # bridge is no longer used it 2.1. The slot is left there to support
-    # upgrading, but will be removed in 2.2
+    # upgrading, but can be removed once upgrades to the current version
+    # straight from 2.0 are deprecated.
     if self.bridge is not None:
       self.bridge = None
 
     if self.bridge is not None:
       self.bridge = None
 
@@ -385,6 +434,9 @@ class Disk(ConfigObject):
     irrespective of their status. For such devices, we return this
     path, for others we return None.
 
     irrespective of their status. For such devices, we return this
     path, for others we return None.
 
+    @warning: The path returned is not a normalized pathname; callers
+        should check that it is a valid path.
+
     """
     if self.dev_type == constants.LD_LV:
       return "/dev/%s/%s" % (self.logical_id[0], self.logical_id[1])
     """
     if self.dev_type == constants.LD_LV:
       return "/dev/%s/%s" % (self.logical_id[0], self.logical_id[1])
@@ -406,6 +458,21 @@ class Disk(ConfigObject):
       return 0
     return -1
 
       return 0
     return -1
 
+  def IsBasedOnDiskType(self, dev_type):
+    """Check if the disk or its children are based on the given type.
+
+    @type dev_type: L{constants.LDS_BLOCK}
+    @param dev_type: the type to look for
+    @rtype: boolean
+    @return: boolean indicating if a device of the given type was found or not
+
+    """
+    if self.children:
+      for child in self.children:
+        if child.IsBasedOnDiskType(dev_type):
+          return True
+    return self.dev_type == dev_type
+
   def GetNodes(self, node):
     """This function returns the nodes this device lives on.
 
   def GetNodes(self, node):
     """This function returns the nodes this device lives on.
 
@@ -468,7 +535,7 @@ class Disk(ConfigObject):
     actual algorithms from bdev.
 
     """
     actual algorithms from bdev.
 
     """
-    if self.dev_type == constants.LD_LV:
+    if self.dev_type == constants.LD_LV or self.dev_type == constants.LD_FILE:
       self.size += amount
     elif self.dev_type == constants.LD_DRBD8:
       if self.children:
       self.size += amount
     elif self.dev_type == constants.LD_DRBD8:
       if self.children:
@@ -620,13 +687,14 @@ class Disk(ConfigObject):
 
 class Instance(TaggableObject):
   """Config object representing an instance."""
 
 class Instance(TaggableObject):
   """Config object representing an instance."""
-  __slots__ = TaggableObject.__slots__ + [
+  __slots__ = [
     "name",
     "primary_node",
     "os",
     "hypervisor",
     "hvparams",
     "beparams",
     "name",
     "primary_node",
     "os",
     "hypervisor",
     "hvparams",
     "beparams",
+    "osparams",
     "admin_up",
     "nics",
     "disks",
     "admin_up",
     "nics",
     "disks",
@@ -732,11 +800,13 @@ class Instance(TaggableObject):
     try:
       idx = int(idx)
       return self.disks[idx]
     try:
       idx = int(idx)
       return self.disks[idx]
-    except ValueError, err:
-      raise errors.OpPrereqError("Invalid disk index: '%s'" % str(err))
+    except (TypeError, ValueError), err:
+      raise errors.OpPrereqError("Invalid disk index: '%s'" % str(err),
+                                 errors.ECODE_INVAL)
     except IndexError:
       raise errors.OpPrereqError("Invalid disk index: %d (instace has disks"
     except IndexError:
       raise errors.OpPrereqError("Invalid disk index: %d (instace has disks"
-                                 " 0 to %d" % (idx, len(self.disks)))
+                                 " 0 to %d" % (idx, len(self.disks)),
+                                 errors.ECODE_INVAL)
 
   def ToDict(self):
     """Instance-specific conversion to standard python types.
 
   def ToDict(self):
     """Instance-specific conversion to standard python types.
@@ -774,10 +844,24 @@ class Instance(TaggableObject):
       nic.UpgradeConfig()
     for disk in self.disks:
       disk.UpgradeConfig()
       nic.UpgradeConfig()
     for disk in self.disks:
       disk.UpgradeConfig()
+    if self.hvparams:
+      for key in constants.HVC_GLOBALS:
+        try:
+          del self.hvparams[key]
+        except KeyError:
+          pass
+    if self.osparams is None:
+      self.osparams = {}
 
 
 class OS(ConfigObject):
 
 
 class OS(ConfigObject):
-  """Config object representing an operating system."""
+  """Config object representing an operating system.
+
+  @type supported_parameters: list
+  @ivar supported_parameters: a list of tuples, name and description,
+      containing the supported parameters by this OS
+
+  """
   __slots__ = [
     "name",
     "path",
   __slots__ = [
     "name",
     "path",
@@ -786,13 +870,15 @@ class OS(ConfigObject):
     "export_script",
     "import_script",
     "rename_script",
     "export_script",
     "import_script",
     "rename_script",
+    "verify_script",
     "supported_variants",
     "supported_variants",
+    "supported_parameters",
     ]
 
 
 class Node(TaggableObject):
   """Config object representing a node."""
     ]
 
 
 class Node(TaggableObject):
   """Config object representing a node."""
-  __slots__ = TaggableObject.__slots__ + [
+  __slots__ = [
     "name",
     "primary_ip",
     "secondary_ip",
     "name",
     "primary_ip",
     "secondary_ip",
@@ -805,13 +891,14 @@ class Node(TaggableObject):
 
 class Cluster(TaggableObject):
   """Config object representing the cluster."""
 
 class Cluster(TaggableObject):
   """Config object representing the cluster."""
-  __slots__ = TaggableObject.__slots__ + [
+  __slots__ = [
     "serial_no",
     "rsahostkeypub",
     "highest_used_port",
     "tcpudp_port_pool",
     "mac_prefix",
     "volume_group_name",
     "serial_no",
     "rsahostkeypub",
     "highest_used_port",
     "tcpudp_port_pool",
     "mac_prefix",
     "volume_group_name",
+    "drbd_usermode_helper",
     "default_bridge",
     "default_hypervisor",
     "master_node",
     "default_bridge",
     "default_hypervisor",
     "master_node",
@@ -821,17 +908,24 @@ class Cluster(TaggableObject):
     "file_storage_dir",
     "enabled_hypervisors",
     "hvparams",
     "file_storage_dir",
     "enabled_hypervisors",
     "hvparams",
+    "os_hvp",
     "beparams",
     "beparams",
+    "osparams",
     "nicparams",
     "candidate_pool_size",
     "modify_etc_hosts",
     "modify_ssh_setup",
     "nicparams",
     "candidate_pool_size",
     "modify_etc_hosts",
     "modify_ssh_setup",
+    "maintain_node_health",
+    "uid_pool",
+    "default_iallocator",
     ] + _TIMESTAMPS + _UUID
 
   def UpgradeConfig(self):
     """Fill defaults for missing configuration values.
 
     """
     ] + _TIMESTAMPS + _UUID
 
   def UpgradeConfig(self):
     """Fill defaults for missing configuration values.
 
     """
+    # pylint: disable-msg=E0203
+    # because these are "defined" via slots, not manually
     if self.hvparams is None:
       self.hvparams = constants.HVC_DEFAULTS
     else:
     if self.hvparams is None:
       self.hvparams = constants.HVC_DEFAULTS
     else:
@@ -839,6 +933,13 @@ class Cluster(TaggableObject):
         self.hvparams[hypervisor] = FillDict(
             constants.HVC_DEFAULTS[hypervisor], self.hvparams[hypervisor])
 
         self.hvparams[hypervisor] = FillDict(
             constants.HVC_DEFAULTS[hypervisor], self.hvparams[hypervisor])
 
+    if self.os_hvp is None:
+      self.os_hvp = {}
+
+    # osparams added before 2.2
+    if self.osparams is None:
+      self.osparams = {}
+
     self.beparams = UpgradeGroupedParams(self.beparams,
                                          constants.BEC_DEFAULTS)
     migrate_default_bridge = not self.nicparams
     self.beparams = UpgradeGroupedParams(self.beparams,
                                          constants.BEC_DEFAULTS)
     migrate_default_bridge = not self.nicparams
@@ -855,17 +956,29 @@ class Cluster(TaggableObject):
       self.modify_ssh_setup = True
 
     # default_bridge is no longer used it 2.1. The slot is left there to
       self.modify_ssh_setup = True
 
     # default_bridge is no longer used it 2.1. The slot is left there to
-    # support auto-upgrading, but will be removed in 2.2
+    # support auto-upgrading. It can be removed once we decide to deprecate
+    # upgrading straight from 2.0.
     if self.default_bridge is not None:
       self.default_bridge = None
 
     if self.default_bridge is not None:
       self.default_bridge = None
 
-    # default_hypervisor is just the first enabled one in 2.1
+    # default_hypervisor is just the first enabled one in 2.1. This slot and
+    # code can be removed once upgrading straight from 2.0 is deprecated.
     if self.default_hypervisor is not None:
       self.enabled_hypervisors = ([self.default_hypervisor] +
         [hvname for hvname in self.enabled_hypervisors
          if hvname != self.default_hypervisor])
       self.default_hypervisor = None
 
     if self.default_hypervisor is not None:
       self.enabled_hypervisors = ([self.default_hypervisor] +
         [hvname for hvname in self.enabled_hypervisors
          if hvname != self.default_hypervisor])
       self.default_hypervisor = None
 
+    # maintain_node_health added after 2.1.1
+    if self.maintain_node_health is None:
+      self.maintain_node_health = False
+
+    if self.uid_pool is None:
+      self.uid_pool = []
+
+    if self.default_iallocator is None:
+      self.default_iallocator = ""
+
   def ToDict(self):
     """Custom function for cluster.
 
   def ToDict(self):
     """Custom function for cluster.
 
@@ -884,21 +997,82 @@ class Cluster(TaggableObject):
       obj.tcpudp_port_pool = set(obj.tcpudp_port_pool)
     return obj
 
       obj.tcpudp_port_pool = set(obj.tcpudp_port_pool)
     return obj
 
-  def FillHV(self, instance):
-    """Fill an instance's hvparams dict.
+  def GetHVDefaults(self, hypervisor, os_name=None, skip_keys=None):
+    """Get the default hypervisor parameters for the cluster.
+
+    @param hypervisor: the hypervisor name
+    @param os_name: if specified, we'll also update the defaults for this OS
+    @param skip_keys: if passed, list of keys not to use
+    @return: the defaults dict
+
+    """
+    if skip_keys is None:
+      skip_keys = []
+
+    fill_stack = [self.hvparams.get(hypervisor, {})]
+    if os_name is not None:
+      os_hvp = self.os_hvp.get(os_name, {}).get(hypervisor, {})
+      fill_stack.append(os_hvp)
+
+    ret_dict = {}
+    for o_dict in fill_stack:
+      ret_dict = FillDict(ret_dict, o_dict, skip_keys=skip_keys)
+
+    return ret_dict
+
+  def SimpleFillHV(self, hv_name, os_name, hvparams, skip_globals=False):
+    """Fill a given hvparams dict with cluster defaults.
+
+    @type hv_name: string
+    @param hv_name: the hypervisor to use
+    @type os_name: string
+    @param os_name: the OS to use for overriding the hypervisor defaults
+    @type skip_globals: boolean
+    @param skip_globals: if True, the global hypervisor parameters will
+        not be filled
+    @rtype: dict
+    @return: a copy of the given hvparams with missing keys filled from
+        the cluster defaults
+
+    """
+    if skip_globals:
+      skip_keys = constants.HVC_GLOBALS
+    else:
+      skip_keys = []
+
+    def_dict = self.GetHVDefaults(hv_name, os_name, skip_keys=skip_keys)
+    return FillDict(def_dict, hvparams, skip_keys=skip_keys)
+
+  def FillHV(self, instance, skip_globals=False):
+    """Fill an instance's hvparams dict with cluster defaults.
 
     @type instance: L{objects.Instance}
     @param instance: the instance parameter to fill
 
     @type instance: L{objects.Instance}
     @param instance: the instance parameter to fill
+    @type skip_globals: boolean
+    @param skip_globals: if True, the global hypervisor parameters will
+        not be filled
     @rtype: dict
     @return: a copy of the instance's hvparams with missing keys filled from
         the cluster defaults
 
     """
     @rtype: dict
     @return: a copy of the instance's hvparams with missing keys filled from
         the cluster defaults
 
     """
-    return FillDict(self.hvparams.get(instance.hypervisor, {}),
-                         instance.hvparams)
+    return self.SimpleFillHV(instance.hypervisor, instance.os,
+                             instance.hvparams, skip_globals)
+
+  def SimpleFillBE(self, beparams):
+    """Fill a given beparams dict with cluster defaults.
+
+    @type beparams: dict
+    @param beparams: the dict to fill
+    @rtype: dict
+    @return: a copy of the passed in beparams with missing keys filled
+        from the cluster defaults
+
+    """
+    return FillDict(self.beparams.get(constants.PP_DEFAULT, {}), beparams)
 
   def FillBE(self, instance):
 
   def FillBE(self, instance):
-    """Fill an instance's beparams dict.
+    """Fill an instance's beparams dict with cluster defaults.
 
     @type instance: L{objects.Instance}
     @param instance: the instance parameter to fill
 
     @type instance: L{objects.Instance}
     @param instance: the instance parameter to fill
@@ -907,8 +1081,39 @@ class Cluster(TaggableObject):
         the cluster defaults
 
     """
         the cluster defaults
 
     """
-    return FillDict(self.beparams.get(constants.PP_DEFAULT, {}),
-                          instance.beparams)
+    return self.SimpleFillBE(instance.beparams)
+
+  def SimpleFillNIC(self, nicparams):
+    """Fill a given nicparams dict with cluster defaults.
+
+    @type nicparams: dict
+    @param nicparams: the dict to fill
+    @rtype: dict
+    @return: a copy of the passed in nicparams with missing keys filled
+        from the cluster defaults
+
+    """
+    return FillDict(self.nicparams.get(constants.PP_DEFAULT, {}), nicparams)
+
+  def SimpleFillOS(self, os_name, os_params):
+    """Fill an instance's osparams dict with cluster defaults.
+
+    @type os_name: string
+    @param os_name: the OS name to use
+    @type os_params: dict
+    @param os_params: the dict to fill with default values
+    @rtype: dict
+    @return: a copy of the instance's osparams with missing keys filled from
+        the cluster defaults
+
+    """
+    name_only = os_name.split("+", 1)[0]
+    # base OS
+    result = self.osparams.get(name_only, {})
+    # OS with variant
+    result = FillDict(result, self.osparams.get(os_name, {}))
+    # specified params
+    return FillDict(result, os_params)
 
 
 class BlockDevStatus(ConfigObject):
 
 
 class BlockDevStatus(ConfigObject):
@@ -924,6 +1129,38 @@ class BlockDevStatus(ConfigObject):
     ]
 
 
     ]
 
 
+class ImportExportStatus(ConfigObject):
+  """Config object representing the status of an import or export."""
+  __slots__ = [
+    "recent_output",
+    "listen_port",
+    "connected",
+    "progress_mbytes",
+    "progress_throughput",
+    "progress_eta",
+    "progress_percent",
+    "exit_status",
+    "error_message",
+    ] + _TIMESTAMPS
+
+
+class ImportExportOptions(ConfigObject):
+  """Options for import/export daemon
+
+  @ivar key_name: X509 key name (None for cluster certificate)
+  @ivar ca_pem: Remote peer CA in PEM format (None for cluster certificate)
+  @ivar compress: Compression method (one of L{constants.IEC_ALL})
+  @ivar magic: Used to ensure the connection goes to the right disk
+
+  """
+  __slots__ = [
+    "key_name",
+    "ca_pem",
+    "compress",
+    "magic",
+    ]
+
+
 class ConfdRequest(ConfigObject):
   """Object holding a confd request.
 
 class ConfdRequest(ConfigObject):
   """Object holding a confd request.
 
@@ -972,10 +1209,10 @@ class SerializableConfigParser(ConfigParser.SafeConfigParser):
     self.write(buf)
     return buf.getvalue()
 
     self.write(buf)
     return buf.getvalue()
 
-  @staticmethod
-  def Loads(data):
+  @classmethod
+  def Loads(cls, data):
     """Load data from a string."""
     buf = StringIO(data)
     """Load data from a string."""
     buf = StringIO(data)
-    cfp = SerializableConfigParser()
+    cfp = cls()
     cfp.readfp(buf)
     return cfp
     cfp.readfp(buf)
     return cfp