Support reseting arbitrary params of ext disks
[ganeti-local] / lib / objects.py
index e2f9a1a..358f108 100644 (file)
@@ -82,23 +82,17 @@ def FillDict(defaults_dict, custom_dict, skip_keys=None):
   return ret_dict
 
 
-def FillIPolicy(default_ipolicy, custom_ipolicy, skip_keys=None):
+def FillIPolicy(default_ipolicy, custom_ipolicy):
   """Fills an instance policy with defaults.
 
   """
   assert frozenset(default_ipolicy.keys()) == constants.IPOLICY_ALL_KEYS
-  ret_dict = {}
-  for key in constants.IPOLICY_ISPECS:
-    ret_dict[key] = FillDict(default_ipolicy[key],
-                             custom_ipolicy.get(key, {}),
-                             skip_keys=skip_keys)
-  # list items
-  for key in [constants.IPOLICY_DTS]:
-    ret_dict[key] = list(custom_ipolicy.get(key, default_ipolicy[key]))
-  # other items which we know we can directly copy (immutables)
-  for key in constants.IPOLICY_PARAMETERS:
-    ret_dict[key] = custom_ipolicy.get(key, default_ipolicy[key])
-
+  ret_dict = copy.deepcopy(custom_ipolicy)
+  for key in default_ipolicy:
+    if key not in ret_dict:
+      ret_dict[key] = copy.deepcopy(default_ipolicy[key])
+    elif key == constants.ISPECS_STD:
+      ret_dict[key] = FillDict(default_ipolicy[key], ret_dict[key])
   return ret_dict
 
 
@@ -186,11 +180,7 @@ def MakeEmptyIPolicy():
   """Create empty IPolicy dictionary.
 
   """
-  return dict([
-    (constants.ISPECS_MIN, {}),
-    (constants.ISPECS_MAX, {}),
-    (constants.ISPECS_STD, {}),
-    ])
+  return {}
 
 
 class ConfigObject(outils.ValidatedSlots):
@@ -463,11 +453,43 @@ class ConfigData(ConfigObject):
       self.networks = {}
     for network in self.networks.values():
       network.UpgradeConfig()
+    self._UpgradeEnabledDiskTemplates()
+
+  def _UpgradeEnabledDiskTemplates(self):
+    """Upgrade the cluster's enabled disk templates by inspecting the currently
+       enabled and/or used disk templates.
+
+    """
+    # enabled_disk_templates in the cluster config were introduced in 2.8.
+    # Remove this code once upgrading from earlier versions is deprecated.
+    if not self.cluster.enabled_disk_templates:
+      template_set = \
+        set([inst.disk_template for inst in self.instances.values()])
+      # Add drbd and plain, if lvm is enabled (by specifying a volume group)
+      if self.cluster.volume_group_name:
+        template_set.add(constants.DT_DRBD8)
+        template_set.add(constants.DT_PLAIN)
+      # FIXME: Adapt this when dis/enabling at configure time is removed.
+      # Enable 'file' and 'sharedfile', if they are enabled, even though they
+      # might currently not be used.
+      if constants.ENABLE_FILE_STORAGE:
+        template_set.add(constants.DT_FILE)
+      if constants.ENABLE_SHARED_FILE_STORAGE:
+        template_set.add(constants.DT_SHARED_FILE)
+      # Set enabled_disk_templates to the inferred disk templates. Order them
+      # according to a preference list that is based on Ganeti's history of
+      # supported disk templates.
+      self.cluster.enabled_disk_templates = []
+      for preferred_template in constants.DISK_TEMPLATE_PREFERENCE:
+        if preferred_template in template_set:
+          self.cluster.enabled_disk_templates.append(preferred_template)
+          template_set.remove(preferred_template)
+      self.cluster.enabled_disk_templates.extend(list(template_set))
 
 
 class NIC(ConfigObject):
   """Config object representing a network card."""
-  __slots__ = ["mac", "ip", "network", "nicparams", "netinfo"]
+  __slots__ = ["name", "mac", "ip", "network", "nicparams", "netinfo"] + _UUID
 
   @classmethod
   def CheckParameterSyntax(cls, nicparams):
@@ -490,8 +512,8 @@ class NIC(ConfigObject):
 
 class Disk(ConfigObject):
   """Config object representing a block device."""
-  __slots__ = ["dev_type", "logical_id", "physical_id",
-               "children", "iv_name", "size", "mode", "params"]
+  __slots__ = ["name", "dev_type", "logical_id", "physical_id",
+               "children", "iv_name", "size", "mode", "params"] + _UUID
 
   def CreateOnSecondary(self):
     """Test if this device needs to be created on a secondary node."""
@@ -806,9 +828,18 @@ class Disk(ConfigObject):
         child.UpgradeConfig()
 
     # FIXME: Make this configurable in Ganeti 2.7
-    self.params = {}
+    # Params should be an empty dict that gets filled any time needed
+    # In case of ext template we allow arbitrary params that should not
+    # be overrided during a config reload/upgrade.
+    if not self.params or not isinstance(self.params, dict):
+      self.params = {}
+
     # add here config upgrade for this disk
 
+    # If the file driver is empty, fill it up with the default value
+    if self.dev_type == constants.LD_FILE and self.physical_id[0] is None:
+      self.physical_id[0] = constants.FD_DEFAULT
+
   @staticmethod
   def ComputeLDParams(disk_template, disk_params):
     """Computes Logical Disk parameters from Disk Template parameters.
@@ -881,7 +912,6 @@ class Disk(ConfigObject):
 class InstancePolicy(ConfigObject):
   """Config object representing instance policy limits dictionary.
 
-
   Note that this object is not actually used in the config, it's just
   used as a placeholder for a few functions.
 
@@ -890,9 +920,14 @@ class InstancePolicy(ConfigObject):
   def CheckParameterSyntax(cls, ipolicy, check_std):
     """ Check the instance policy for validity.
 
+    @type ipolicy: dict
+    @param ipolicy: dictionary with min/max/std specs and policies
+    @type check_std: bool
+    @param check_std: Whether to check std value or just assume compliance
+    @raise errors.ConfigurationError: when the policy is not legal
+
     """
-    for param in constants.ISPECS_PARAMETERS:
-      InstancePolicy.CheckISpecSyntax(ipolicy, param, check_std)
+    InstancePolicy.CheckISpecSyntax(ipolicy, check_std)
     if constants.IPOLICY_DTS in ipolicy:
       InstancePolicy.CheckDiskTemplates(ipolicy[constants.IPOLICY_DTS])
     for key in constants.IPOLICY_PARAMETERS:
@@ -904,44 +939,100 @@ class InstancePolicy(ConfigObject):
                                       utils.CommaJoin(wrong_keys))
 
   @classmethod
-  def CheckISpecSyntax(cls, ipolicy, name, check_std):
-    """Check the instance policy for validity on a given key.
+  def _CheckIncompleteSpec(cls, spec, keyname):
+    missing_params = constants.ISPECS_PARAMETERS - frozenset(spec.keys())
+    if missing_params:
+      msg = ("Missing instance specs parameters for %s: %s" %
+             (keyname, utils.CommaJoin(missing_params)))
+      raise errors.ConfigurationError(msg)
 
-    We check if the instance policy makes sense for a given key, that is
-    if ipolicy[min][name] <= ipolicy[std][name] <= ipolicy[max][name].
+  @classmethod
+  def CheckISpecSyntax(cls, ipolicy, check_std):
+    """Check the instance policy specs for validity.
 
     @type ipolicy: dict
-    @param ipolicy: dictionary with min, max, std specs
+    @param ipolicy: dictionary with min/max/std specs
+    @type check_std: bool
+    @param check_std: Whether to check std value or just assume compliance
+    @raise errors.ConfigurationError: when specs are not valid
+
+    """
+    if constants.ISPECS_MINMAX not in ipolicy:
+      # Nothing to check
+      return
+
+    if check_std and constants.ISPECS_STD not in ipolicy:
+      msg = "Missing key in ipolicy: %s" % constants.ISPECS_STD
+      raise errors.ConfigurationError(msg)
+    stdspec = ipolicy.get(constants.ISPECS_STD)
+    if check_std:
+      InstancePolicy._CheckIncompleteSpec(stdspec, constants.ISPECS_STD)
+
+    if not ipolicy[constants.ISPECS_MINMAX]:
+      raise errors.ConfigurationError("Empty minmax specifications")
+    std_is_good = False
+    for minmaxspecs in ipolicy[constants.ISPECS_MINMAX]:
+      missing = constants.ISPECS_MINMAX_KEYS - frozenset(minmaxspecs.keys())
+      if missing:
+        msg = "Missing instance specification: %s" % utils.CommaJoin(missing)
+        raise errors.ConfigurationError(msg)
+      for (key, spec) in minmaxspecs.items():
+        InstancePolicy._CheckIncompleteSpec(spec, key)
+
+      spec_std_ok = True
+      for param in constants.ISPECS_PARAMETERS:
+        par_std_ok = InstancePolicy._CheckISpecParamSyntax(minmaxspecs, stdspec,
+                                                           param, check_std)
+        spec_std_ok = spec_std_ok and par_std_ok
+      std_is_good = std_is_good or spec_std_ok
+    if not std_is_good:
+      raise errors.ConfigurationError("Invalid std specifications")
+
+  @classmethod
+  def _CheckISpecParamSyntax(cls, minmaxspecs, stdspec, name, check_std):
+    """Check the instance policy specs for validity on a given key.
+
+    We check if the instance specs makes sense for a given key, that is
+    if minmaxspecs[min][name] <= stdspec[name] <= minmaxspec[max][name].
+
+    @type minmaxspecs: dict
+    @param minmaxspecs: dictionary with min and max instance spec
+    @type stdspec: dict
+    @param stdspec: dictionary with standard instance spec
     @type name: string
     @param name: what are the limits for
     @type check_std: bool
     @param check_std: Whether to check std value or just assume compliance
-    @raise errors.ConfigureError: when specs for given name are not valid
+    @rtype: bool
+    @return: C{True} when specs are valid, C{False} when standard spec for the
+        given name is not valid
+    @raise errors.ConfigurationError: when min/max specs for the given name
+        are not valid
 
     """
-    min_v = ipolicy[constants.ISPECS_MIN].get(name, 0)
+    minspec = minmaxspecs[constants.ISPECS_MIN]
+    maxspec = minmaxspecs[constants.ISPECS_MAX]
+    min_v = minspec[name]
+    max_v = maxspec[name]
 
-    if check_std:
-      std_v = ipolicy[constants.ISPECS_STD].get(name, min_v)
-      std_msg = std_v
-    else:
-      std_v = min_v
-      std_msg = "-"
-
-    max_v = ipolicy[constants.ISPECS_MAX].get(name, std_v)
-    err = ("Invalid specification of min/max/std values for %s: %s/%s/%s" %
-           (name,
-            ipolicy[constants.ISPECS_MIN].get(name, "-"),
-            ipolicy[constants.ISPECS_MAX].get(name, "-"),
-            std_msg))
-    if min_v > std_v or std_v > max_v:
+    if min_v > max_v:
+      err = ("Invalid specification of min/max values for %s: %s/%s" %
+             (name, min_v, max_v))
       raise errors.ConfigurationError(err)
+    elif check_std:
+      std_v = stdspec.get(name, min_v)
+      return std_v >= min_v and std_v <= max_v
+    else:
+      return True
 
   @classmethod
   def CheckDiskTemplates(cls, disk_templates):
     """Checks the disk templates for validity.
 
     """
+    if not disk_templates:
+      raise errors.ConfigurationError("Instance policy must contain" +
+                                      " at least one disk template")
     wrong = frozenset(disk_templates).difference(constants.DISK_TEMPLATES)
     if wrong:
       raise errors.ConfigurationError("Invalid disk template(s) %s" %
@@ -975,6 +1066,7 @@ class Instance(TaggableObject):
     "nics",
     "disks",
     "disk_template",
+    "disks_active",
     "network_port",
     "serial_no",
     ] + _TIMESTAMPS + _UUID
@@ -1140,6 +1232,8 @@ class Instance(TaggableObject):
     if self.osparams is None:
       self.osparams = {}
     UpgradeBeParams(self.beparams)
+    if self.disks_active is None:
+      self.disks_active = self.admin_state == constants.ADMINST_UP
 
 
 class OS(ConfigObject):
@@ -1434,6 +1528,7 @@ class Cluster(TaggableObject):
   __slots__ = [
     "serial_no",
     "rsahostkeypub",
+    "dsahostkeypub",
     "highest_used_port",
     "tcpudp_port_pool",
     "mac_prefix",
@@ -1471,6 +1566,7 @@ class Cluster(TaggableObject):
     "prealloc_wipe_disks",
     "hv_state_static",
     "disk_state_static",
+    "enabled_disk_templates",
     ] + _TIMESTAMPS + _UUID
 
   def UpgradeConfig(self):
@@ -1578,6 +1674,12 @@ class Cluster(TaggableObject):
       # we can either make sure to upgrade the ipolicy always, or only
       # do it in some corner cases (e.g. missing keys); note that this
       # will break any removal of keys from the ipolicy dict
+      wrongkeys = frozenset(self.ipolicy.keys()) - constants.IPOLICY_ALL_KEYS
+      if wrongkeys:
+        # These keys would be silently removed by FillIPolicy()
+        msg = ("Cluster instance policy contains spurious keys: %s" %
+               utils.CommaJoin(wrongkeys))
+        raise errors.ConfigurationError(msg)
       self.ipolicy = FillIPolicy(constants.IPOLICY_DEFAULTS, self.ipolicy)
 
   @property