From 41044e04c1fea3630a5129de68a1938b3e3d53d3 Mon Sep 17 00:00:00 2001
From: Bernardo Dal Seno <bdalseno@google.com>
Date: Mon, 15 Apr 2013 11:03:38 +0200
Subject: [PATCH] Add multiple min/max specs in instance policy

Now instance policies can contain more than one min/max specs.  This is the
main element of the "Constrained instance sizes" section in the
"Partitioned Ganeti" design doc.

This is a big patch, but changing the type of a configuration item requires
to change all the code that handles it.

Signed-off-by: Bernardo Dal Seno <bdalseno@google.com>
Reviewed-by: Helga Velroyen <helgav@google.com>
---
 doc/rapi.rst                                  |   2 +-
 lib/cli.py                                    |  38 +++--
 lib/cmdlib.py                                 |  21 ++-
 lib/config.py                                 |   3 +-
 lib/constants.py                              |   2 +-
 lib/objects.py                                |  32 ++--
 man/htools.rst                                |   6 +-
 src/Ganeti/HTools/Backend/Text.hs             |  48 +++++-
 src/Ganeti/HTools/Instance.hs                 |  20 ++-
 src/Ganeti/HTools/Program/Hspace.hs           |  10 +-
 src/Ganeti/HTools/Types.hs                    |  34 ++---
 src/Ganeti/Objects.hs                         |   4 +-
 test/data/htools/clean-nonzero-score.data     |   4 +-
 test/data/htools/common-suffix.data           |   4 +-
 test/data/htools/empty-cluster.data           |   4 +-
 test/data/htools/hail-alloc-drbd.json         |  74 ++++-----
 .../htools/hail-alloc-invalid-twodisks.json   |  36 ++---
 test/data/htools/hail-alloc-twodisks.json     |  36 ++---
 test/data/htools/hail-change-group.json       | 112 +++++++-------
 test/data/htools/hail-node-evac.json          |  38 ++---
 test/data/htools/hail-reloc-drbd.json         |  74 ++++-----
 test/data/htools/hbal-split-insts.data        |   6 +-
 test/data/htools/invalid-node.data            |   4 +-
 test/data/htools/missing-resources.data       |   4 +-
 test/data/htools/multiple-master.data         |   4 +-
 test/data/htools/n1-failure.data              |   6 +-
 test/data/htools/rapi/groups.json             |  38 ++---
 test/data/htools/rapi/info.json               |  36 ++---
 test/data/htools/unique-reboot-order.data     |   4 +-
 test/hs/Test/Ganeti/HTools/Backend/Text.hs    |  10 +-
 test/hs/Test/Ganeti/HTools/Types.hs           |  41 ++++-
 test/hs/Test/Ganeti/TestHTools.hs             |   4 +-
 test/py/ganeti.cli_unittest.py                |  73 +++++----
 test/py/ganeti.cmdlib_unittest.py             | 143 +++++++++++++++---
 test/py/ganeti.config_unittest.py             |  42 ++---
 test/py/ganeti.objects_unittest.py            | 132 +++++++++++++---
 tools/cfgupgrade                              |  20 ++-
 37 files changed, 767 insertions(+), 402 deletions(-)

diff --git a/doc/rapi.rst b/doc/rapi.rst
index eb39719f1..eb115dcf8 100644
--- a/doc/rapi.rst
+++ b/doc/rapi.rst
@@ -251,7 +251,7 @@ The instance policy specification is a dict with the following fields:
 
 
 :pyeval:`constants.ISPECS_MINMAX`
-  A dict with the following two fields:
+  A list of dictionaries, each with the following two fields:
 
   |ispec-min|, |ispec-max|
     A sub- `dict` with the following fields, which sets the limit of the
diff --git a/lib/cli.py b/lib/cli.py
index 7615c7358..b924e89a1 100644
--- a/lib/cli.py
+++ b/lib/cli.py
@@ -3736,13 +3736,24 @@ def FormatPolicyInfo(custom_ipolicy, eff_ipolicy, iscluster):
   if iscluster:
     eff_ipolicy = custom_ipolicy
 
-  custom_minmax = custom_ipolicy.get(constants.ISPECS_MINMAX, {})
-  ret = [
-    (key,
-     FormatParamsDictInfo(custom_minmax.get(key, {}),
-                          eff_ipolicy[constants.ISPECS_MINMAX][key]))
-    for key in constants.ISPECS_MINMAX_KEYS
-    ]
+  minmax_out = []
+  custom_minmax = custom_ipolicy.get(constants.ISPECS_MINMAX)
+  if custom_minmax:
+    for (k, minmax) in enumerate(custom_minmax):
+      minmax_out.append([
+        ("%s/%s" % (key, k),
+         FormatParamsDictInfo(minmax[key], minmax[key]))
+        for key in constants.ISPECS_MINMAX_KEYS
+        ])
+  else:
+    for (k, minmax) in enumerate(eff_ipolicy[constants.ISPECS_MINMAX]):
+      minmax_out.append([
+        ("%s/%s" % (key, k),
+         FormatParamsDictInfo({}, minmax[key]))
+        for key in constants.ISPECS_MINMAX_KEYS
+        ])
+  ret = [("bounds specs", minmax_out)]
+
   if iscluster:
     stdspecs = custom_ipolicy[constants.ISPECS_STD]
     ret.append(
@@ -3787,8 +3798,8 @@ def PrintIPolicyCommand(buf, ipolicy, isgroup):
       _PrintSpecsParameters(buf, stdspecs)
   minmax = ipolicy.get("minmax")
   if minmax:
-    minspecs = minmax.get("min")
-    maxspecs = minmax.get("max")
+    minspecs = minmax[0].get("min")
+    maxspecs = minmax[0].get("max")
     if minspecs and maxspecs:
       buf.write(" %s " % IPOLICY_BOUNDS_SPECS_STR)
       buf.write("min:")
@@ -3892,13 +3903,14 @@ def _InitISpecsFromSplitOpts(ipolicy, ispecs_mem_size, ispecs_cpu_count,
     for key, val in specs.items(): # {min: .. ,max: .., std: ..}
       assert key in ispecs
       ispecs[key][name] = val
-  ipolicy[constants.ISPECS_MINMAX] = {}
+  minmax_out = {}
   for key in constants.ISPECS_MINMAX_KEYS:
     if fill_all:
-      ipolicy[constants.ISPECS_MINMAX][key] = \
+      minmax_out[key] = \
         objects.FillDict(constants.ISPECS_MINMAX_DEFAULTS[key], ispecs[key])
     else:
-      ipolicy[constants.ISPECS_MINMAX][key] = ispecs[key]
+      minmax_out[key] = ispecs[key]
+  ipolicy[constants.ISPECS_MINMAX] = [minmax_out]
   if fill_all:
     ipolicy[constants.ISPECS_STD] = \
         objects.FillDict(constants.IPOLICY_DEFAULTS[constants.ISPECS_STD],
@@ -3953,7 +3965,7 @@ def _InitISpecsFromFullOpts(ipolicy_out, minmax_ispecs, std_ispecs,
         msg = "Invalid key in bounds instance specifications: %s" % key
         raise errors.OpPrereqError(msg, errors.ECODE_INVAL)
       minmax_out[key] = _ParseISpec(spec, key, True)
-    ipolicy_out[constants.ISPECS_MINMAX] = minmax_out
+    ipolicy_out[constants.ISPECS_MINMAX] = [minmax_out]
   if std_ispecs is not None:
     assert not group_ipolicy # This is not an option for gnt-group
     ipolicy_out[constants.ISPECS_STD] = _ParseISpec(std_ispecs, "std", False)
diff --git a/lib/cmdlib.py b/lib/cmdlib.py
index 7ba55d3ed..2c6eccb53 100644
--- a/lib/cmdlib.py
+++ b/lib/cmdlib.py
@@ -828,7 +828,8 @@ def _GetUpdatedIPolicy(old_ipolicy, new_ipolicy, group_policy=False):
     if (not value or value == [constants.VALUE_DEFAULT] or
         value == constants.VALUE_DEFAULT):
       if group_policy:
-        del ipolicy[key]
+        if key in ipolicy:
+          del ipolicy[key]
       else:
         raise errors.OpPrereqError("Can't unset ipolicy attribute '%s'"
                                    " on the cluster'" % key,
@@ -843,8 +844,9 @@ def _GetUpdatedIPolicy(old_ipolicy, new_ipolicy, group_policy=False):
                                      " '%s': '%s', error: %s" %
                                      (key, value, err), errors.ECODE_INVAL)
       elif key == constants.ISPECS_MINMAX:
-        for k in value.keys():
-          utils.ForceDictType(value[k], constants.ISPECS_PARAMETER_TYPES)
+        for minmax in value:
+          for k in minmax.keys():
+            utils.ForceDictType(minmax[k], constants.ISPECS_PARAMETER_TYPES)
         ipolicy[key] = value
       elif key == constants.ISPECS_STD:
         if group_policy:
@@ -1276,10 +1278,15 @@ def _ComputeIPolicySpecViolation(ipolicy, mem_size, cpu_count, disk_count,
     ret.append("Disk template %s is not allowed (allowed templates: %s)" %
                (disk_template, utils.CommaJoin(allowed_dts)))
 
-  minmax = ipolicy[constants.ISPECS_MINMAX]
-  return ret + filter(None,
-                      (_compute_fn(name, qualifier, minmax, value)
-                       for (name, qualifier, value) in test_settings))
+  min_errs = None
+  for minmax in ipolicy[constants.ISPECS_MINMAX]:
+    errs = filter(None,
+                  (_compute_fn(name, qualifier, minmax, value)
+                   for (name, qualifier, value) in test_settings))
+    if min_errs is None or len(errs) < len(min_errs):
+      min_errs = errs
+  assert min_errs is not None
+  return ret + min_errs
 
 
 def _ComputeIPolicyInstanceViolation(ipolicy, instance, cfg,
diff --git a/lib/config.py b/lib/config.py
index 6c829917a..b968fcf00 100644
--- a/lib/config.py
+++ b/lib/config.py
@@ -633,7 +633,8 @@ class ConfigWriter:
         result.append("%s has invalid instance policy: %s" % (owner, err))
       for key, value in ipolicy.items():
         if key == constants.ISPECS_MINMAX:
-          _helper_ispecs(owner, "ipolicy/" + key, value)
+          for k in range(len(value)):
+            _helper_ispecs(owner, "ipolicy/%s[%s]" % (key, k), value[k])
         elif key == constants.ISPECS_STD:
           _helper(owner, "ipolicy/" + key, value,
                   constants.ISPECS_PARAMETER_TYPES)
diff --git a/lib/constants.py b/lib/constants.py
index e439ea826..0e49de634 100644
--- a/lib/constants.py
+++ b/lib/constants.py
@@ -2215,7 +2215,7 @@ ISPECS_MINMAX_DEFAULTS = {
     },
   }
 IPOLICY_DEFAULTS = {
-  ISPECS_MINMAX: ISPECS_MINMAX_DEFAULTS,
+  ISPECS_MINMAX: [ISPECS_MINMAX_DEFAULTS],
   ISPECS_STD: {
     ISPEC_MEM_SIZE: 128,
     ISPEC_CPU_COUNT: 1,
diff --git a/lib/objects.py b/lib/objects.py
index c032c448d..8d809c48d 100644
--- a/lib/objects.py
+++ b/lib/objects.py
@@ -959,20 +959,24 @@ class InstancePolicy(ConfigObject):
     if check_std:
       InstancePolicy._CheckIncompleteSpec(stdspec, constants.ISPECS_STD)
 
-    minmaxspecs = 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
-    if not spec_std_ok:
+    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
diff --git a/man/htools.rst b/man/htools.rst
index b908863ef..22b67bbb7 100644
--- a/man/htools.rst
+++ b/man/htools.rst
@@ -198,8 +198,10 @@ support all options. Some common options are:
   groups, in the following format (separated by ``|``):
 
   - owner (empty if cluster, group name otherwise)
-  - standard, min, max instance specs, containing the following values
-    separated by commas:
+  - standard, min, max instance specs; min and max instance specs are
+    separated between them by a semicolon, and can be specified multiple
+    times (min;max;min;max...); each of the specs contains the following
+    values separated by commas:
     - memory size
     - cpu count
     - disk size
diff --git a/src/Ganeti/HTools/Backend/Text.hs b/src/Ganeti/HTools/Backend/Text.hs
index 579ce77bc..2370eb749 100644
--- a/src/Ganeti/HTools/Backend/Text.hs
+++ b/src/Ganeti/HTools/Backend/Text.hs
@@ -32,12 +32,14 @@ module Ganeti.HTools.Backend.Text
   , loadInst
   , loadNode
   , loadISpec
+  , loadMultipleMinMaxISpecs
   , loadIPolicy
   , serializeInstances
   , serializeNode
   , serializeNodes
   , serializeGroup
   , serializeISpec
+  , serializeMultipleMinMaxISpecs
   , serializeIPolicy
   , serializeCluster
   ) where
@@ -117,6 +119,10 @@ serializeInstances :: Node.List -> Instance.List -> String
 serializeInstances nl =
   unlines . map (serializeInstance nl) . Container.elems
 
+-- | Separator between ISpecs (in MinMaxISpecs).
+iSpecsSeparator :: Char
+iSpecsSeparator = ';'
+
 -- | Generate a spec data from a given ISpec object.
 serializeISpec :: ISpec -> String
 serializeISpec ispec =
@@ -130,15 +136,20 @@ serializeISpec ispec =
 serializeDiskTemplates :: [DiskTemplate] -> String
 serializeDiskTemplates = intercalate "," . map diskTemplateToRaw
 
+-- | Generate min/max instance specs data.
+serializeMultipleMinMaxISpecs :: [MinMaxISpecs] -> String
+serializeMultipleMinMaxISpecs minmaxes =
+  intercalate [iSpecsSeparator] $ foldr serialpair [] minmaxes
+  where serialpair (MinMaxISpecs minspec maxspec) acc =
+          serializeISpec minspec : serializeISpec maxspec : acc
+
 -- | Generate policy data from a given policy object.
 serializeIPolicy :: String -> IPolicy -> String
 serializeIPolicy owner ipol =
   let IPolicy minmax stdspec dts vcpu_ratio spindle_ratio = ipol
-      MinMaxISpecs minspec maxspec = minmax
       strings = [ owner
                 , serializeISpec stdspec
-                , serializeISpec minspec
-                , serializeISpec maxspec
+                , serializeMultipleMinMaxISpecs minmax
                 , serializeDiskTemplates dts
                 , show vcpu_ratio
                 , show spindle_ratio
@@ -255,18 +266,41 @@ loadISpec owner [mem_s, cpu_c, dsk_s, dsk_c, nic_c, su] = do
   return $ ISpec xmem_s xcpu_c xdsk_s xdsk_c xnic_c xsu
 loadISpec owner s = fail $ "Invalid ispec data for " ++ owner ++ ": " ++ show s
 
+-- | Load a single min/max ISpec pair
+loadMinMaxISpecs :: String -> String -> String -> Result MinMaxISpecs
+loadMinMaxISpecs owner minspec maxspec = do
+  xminspec <- loadISpec (owner ++ "/minspec") (commaSplit minspec)
+  xmaxspec <- loadISpec (owner ++ "/maxspec") (commaSplit maxspec)
+  return $ MinMaxISpecs xminspec xmaxspec
+
+-- | Break a list of ispecs strings into a list of (min/max) ispecs pairs
+breakISpecsPairs :: String -> [String] -> Result [(String, String)]
+breakISpecsPairs _ [] =
+  return []
+breakISpecsPairs owner (x:y:xs) = do
+  rest <- breakISpecsPairs owner xs
+  return $ (x, y) : rest
+breakISpecsPairs owner _ =
+  fail $ "Odd number of min/max specs for " ++ owner
+
+-- | Load a list of min/max ispecs pairs
+loadMultipleMinMaxISpecs :: String -> [String] -> Result [MinMaxISpecs]
+loadMultipleMinMaxISpecs owner ispecs = do
+  pairs <- breakISpecsPairs owner ispecs
+  mapM (uncurry $ loadMinMaxISpecs owner) pairs
+
 -- | Loads an ipolicy from a field list.
 loadIPolicy :: [String] -> Result (String, IPolicy)
-loadIPolicy [owner, stdspec, minspec, maxspec, dtemplates,
+loadIPolicy [owner, stdspec, minmaxspecs, dtemplates,
              vcpu_ratio, spindle_ratio] = do
   xstdspec <- loadISpec (owner ++ "/stdspec") (commaSplit stdspec)
-  xminspec <- loadISpec (owner ++ "/minspec") (commaSplit minspec)
-  xmaxspec <- loadISpec (owner ++ "/maxspec") (commaSplit maxspec)
+  xminmaxspecs <- loadMultipleMinMaxISpecs owner $
+                  sepSplit iSpecsSeparator minmaxspecs
   xdts <- mapM diskTemplateFromRaw $ commaSplit dtemplates
   xvcpu_ratio <- tryRead (owner ++ "/vcpu_ratio") vcpu_ratio
   xspindle_ratio <- tryRead (owner ++ "/spindle_ratio") spindle_ratio
   return (owner,
-          IPolicy (MinMaxISpecs xminspec xmaxspec) xstdspec
+          IPolicy xminmaxspecs xstdspec
                 xdts xvcpu_ratio xspindle_ratio)
 loadIPolicy s = fail $ "Invalid ipolicy data: '" ++ show s ++ "'"
 
diff --git a/src/Ganeti/HTools/Instance.hs b/src/Ganeti/HTools/Instance.hs
index 8de100979..4ba3e91bb 100644
--- a/src/Ganeti/HTools/Instance.hs
+++ b/src/Ganeti/HTools/Instance.hs
@@ -279,12 +279,26 @@ instAboveISpec inst ispec
   | vcpus inst < T.iSpecCpuCount ispec = Bad T.FailCPU
   | otherwise = Ok ()
 
+-- | Checks if an instance matches a min/max specs pair
+instMatchesMinMaxSpecs :: Instance -> T.MinMaxISpecs -> T.OpResult ()
+instMatchesMinMaxSpecs inst minmax = do
+  instAboveISpec inst (T.minMaxISpecsMinSpec minmax)
+  instBelowISpec inst (T.minMaxISpecsMaxSpec minmax)
+
+-- | Checks if an instance matches any specs of a policy
+instMatchesSpecs :: Instance -> [T.MinMaxISpecs] -> T.OpResult ()
+ -- Return Ok for no constraints, though this should never happen
+instMatchesSpecs _ [] = Ok ()
+instMatchesSpecs inst (minmax:minmaxes) =
+  foldr eithermatch (instMatchesMinMaxSpecs inst minmax) minmaxes
+  where eithermatch mm (Bad _) = instMatchesMinMaxSpecs inst mm
+        eithermatch _ y@(Ok ()) = y
+--  # See 04f231771
+
 -- | Checks if an instance matches a policy.
 instMatchesPolicy :: Instance -> T.IPolicy -> T.OpResult ()
 instMatchesPolicy inst ipol = do
-  let minmax = T.iPolicyMinMaxISpecs ipol
-  instAboveISpec inst (T.minMaxISpecsMinSpec minmax)
-  instBelowISpec inst (T.minMaxISpecsMaxSpec minmax)
+  instMatchesSpecs inst $ T.iPolicyMinMaxISpecs ipol
   if diskTemplate inst `elem` T.iPolicyDiskTemplates ipol
     then Ok ()
     else Bad T.FailDisk
diff --git a/src/Ganeti/HTools/Program/Hspace.hs b/src/Ganeti/HTools/Program/Hspace.hs
index f4b0836d9..d26d5fe9b 100644
--- a/src/Ganeti/HTools/Program/Hspace.hs
+++ b/src/Ganeti/HTools/Program/Hspace.hs
@@ -446,9 +446,13 @@ main opts args = do
 
   -- Run the tiered allocation
 
-  let minmax = iPolicyMinMaxISpecs ipol
-  let tspec = fromMaybe (rspecFromISpec (minMaxISpecsMaxSpec minmax))
-              (optTieredSpec opts)
+  let minmaxes = iPolicyMinMaxISpecs ipol
+  -- TODO: Go through all min/max specs pairs
+  tspec <- case minmaxes of
+             [] -> exitErr "Empty list of specs received from the cluster"
+             minmax:_ -> return $ fromMaybe
+                         (rspecFromISpec (minMaxISpecsMaxSpec minmax))
+                         (optTieredSpec opts)
 
   (treason, trl_nl, _, spec_map) <-
     runAllocation cdata stop_allocation
diff --git a/src/Ganeti/HTools/Types.hs b/src/Ganeti/HTools/Types.hs
index e579e3634..e9e9badb6 100644
--- a/src/Ganeti/HTools/Types.hs
+++ b/src/Ganeti/HTools/Types.hs
@@ -169,12 +169,12 @@ $(THH.buildObject "ISpec" "iSpec"
 
 -- | The default minimum ispec.
 defMinISpec :: ISpec
-defMinISpec = ISpec { iSpecMemorySize = C.ipolicyDefaultsMinmaxMinMemorySize
-                    , iSpecCpuCount   = C.ipolicyDefaultsMinmaxMinCpuCount
-                    , iSpecDiskSize   = C.ipolicyDefaultsMinmaxMinDiskSize
-                    , iSpecDiskCount  = C.ipolicyDefaultsMinmaxMinDiskCount
-                    , iSpecNicCount   = C.ipolicyDefaultsMinmaxMinNicCount
-                    , iSpecSpindleUse = C.ipolicyDefaultsMinmaxMinSpindleUse
+defMinISpec = ISpec { iSpecMemorySize = C.ispecsMinmaxDefaultsMinMemorySize
+                    , iSpecCpuCount   = C.ispecsMinmaxDefaultsMinCpuCount
+                    , iSpecDiskSize   = C.ispecsMinmaxDefaultsMinDiskSize
+                    , iSpecDiskCount  = C.ispecsMinmaxDefaultsMinDiskCount
+                    , iSpecNicCount   = C.ispecsMinmaxDefaultsMinNicCount
+                    , iSpecSpindleUse = C.ispecsMinmaxDefaultsMinSpindleUse
                     }
 
 -- | The default standard ispec.
@@ -189,12 +189,12 @@ defStdISpec = ISpec { iSpecMemorySize = C.ipolicyDefaultsStdMemorySize
 
 -- | The default max ispec.
 defMaxISpec :: ISpec
-defMaxISpec = ISpec { iSpecMemorySize = C.ipolicyDefaultsMinmaxMaxMemorySize
-                    , iSpecCpuCount   = C.ipolicyDefaultsMinmaxMaxCpuCount
-                    , iSpecDiskSize   = C.ipolicyDefaultsMinmaxMaxDiskSize
-                    , iSpecDiskCount  = C.ipolicyDefaultsMinmaxMaxDiskCount
-                    , iSpecNicCount   = C.ipolicyDefaultsMinmaxMaxNicCount
-                    , iSpecSpindleUse = C.ipolicyDefaultsMinmaxMaxSpindleUse
+defMaxISpec = ISpec { iSpecMemorySize = C.ispecsMinmaxDefaultsMaxMemorySize
+                    , iSpecCpuCount   = C.ispecsMinmaxDefaultsMaxCpuCount
+                    , iSpecDiskSize   = C.ispecsMinmaxDefaultsMaxDiskSize
+                    , iSpecDiskCount  = C.ispecsMinmaxDefaultsMaxDiskCount
+                    , iSpecNicCount   = C.ispecsMinmaxDefaultsMaxNicCount
+                    , iSpecSpindleUse = C.ispecsMinmaxDefaultsMaxSpindleUse
                     }
 
 -- | Minimum and maximum instance specs type.
@@ -204,15 +204,15 @@ $(THH.buildObject "MinMaxISpecs" "minMaxISpecs"
   ])
 
 -- | Defult minimum and maximum instance specs.
-defMinMaxISpecs :: MinMaxISpecs
-defMinMaxISpecs = MinMaxISpecs { minMaxISpecsMinSpec = defMinISpec
-                               , minMaxISpecsMaxSpec = defMaxISpec
-                               }
+defMinMaxISpecs :: [MinMaxISpecs]
+defMinMaxISpecs = [MinMaxISpecs { minMaxISpecsMinSpec = defMinISpec
+                                , minMaxISpecsMaxSpec = defMaxISpec
+                                }]
 
 -- | Instance policy type.
 $(THH.buildObject "IPolicy" "iPolicy"
   [ THH.renameField "MinMaxISpecs" $
-      THH.simpleField C.ispecsMinmax [t| MinMaxISpecs |]
+      THH.simpleField C.ispecsMinmax [t| [MinMaxISpecs] |]
   , THH.renameField "StdSpec" $ THH.simpleField C.ispecsStd [t| ISpec |]
   , THH.renameField "DiskTemplates" $
       THH.simpleField C.ipolicyDts [t| [DiskTemplate] |]
diff --git a/src/Ganeti/Objects.hs b/src/Ganeti/Objects.hs
index 908daecf6..155030954 100644
--- a/src/Ganeti/Objects.hs
+++ b/src/Ganeti/Objects.hs
@@ -512,7 +512,7 @@ $(buildObject "MinMaxISpecs" "mmis"
 -- has a special 2-level inheritance mode.
 $(buildObject "PartialIPolicy" "ipolicy"
   [ optionalField . renameField "MinMaxISpecsP"
-                    $ simpleField C.ispecsMinmax   [t| MinMaxISpecs |]
+                    $ simpleField C.ispecsMinmax   [t| [MinMaxISpecs] |]
   , optionalField . renameField "StdSpecP"
                     $ simpleField "std"            [t| PartialISpecParams |]
   , optionalField . renameField "SpindleRatioP"
@@ -527,7 +527,7 @@ $(buildObject "PartialIPolicy" "ipolicy"
 -- has a special 2-level inheritance mode.
 $(buildObject "FilledIPolicy" "ipolicy"
   [ renameField "MinMaxISpecs"
-    $ simpleField C.ispecsMinmax [t| MinMaxISpecs |]
+    $ simpleField C.ispecsMinmax [t| [MinMaxISpecs] |]
   , renameField "StdSpec" $ simpleField "std" [t| FilledISpecParams |]
   , simpleField "spindle-ratio"  [t| Double |]
   , simpleField "vcpu-ratio"     [t| Double |]
diff --git a/test/data/htools/clean-nonzero-score.data b/test/data/htools/clean-nonzero-score.data
index 72762fa91..28ebdf130 100644
--- a/test/data/htools/clean-nonzero-score.data
+++ b/test/data/htools/clean-nonzero-score.data
@@ -11,5 +11,5 @@ new-3|128|1024|1|running|Y|node-01-003||diskless||1
 new-4|128|1024|1|running|Y|node-01-002||diskless||1
 
 
-|128,1,1024,1,1,1|128,1,1024,1,1,1|32768,8,1048576,16,8,12|diskless,file,sharedfile,plain,blockdev,drbd,rbd,ext|4.0|32.0
-group-01|128,1,1024,1,1,1|128,1,1024,1,1,1|32768,8,1048576,16,8,12|diskless,file,sharedfile,plain,blockdev,drbd,rbd,ext|4.0|32.0
+|128,1,1024,1,1,1|128,1,1024,1,1,1;32768,8,1048576,16,8,12|diskless,file,sharedfile,plain,blockdev,drbd,rbd,ext|4.0|32.0
+group-01|128,1,1024,1,1,1|128,1,1024,1,1,1;32768,8,1048576,16,8,12|diskless,file,sharedfile,plain,blockdev,drbd,rbd,ext|4.0|32.0
diff --git a/test/data/htools/common-suffix.data b/test/data/htools/common-suffix.data
index f7410aed0..1e34c9437 100644
--- a/test/data/htools/common-suffix.data
+++ b/test/data/htools/common-suffix.data
@@ -6,5 +6,5 @@ node2.example.com|1024|0|896|95367|94343|4|N|fake-uuid-01|1
 instance1.example.com|128|1024|1|running|Y|node2.example.com||plain|
 
 
-|128,1,1024,1,1,1|128,1,1024,1,1,1|32768,8,1048576,16,8,1|diskless,file,sharedfile,plain,blockdev,drbd,rbd|4.0|32.0
-default|128,1,1024,1,1,1|128,1,1024,1,1,1|32768,8,1048576,16,8,1|diskless,file,sharedfile,plain,blockdev,drbd,rbd|4.0|32.0
+|128,1,1024,1,1,1|128,1,1024,1,1,1;32768,8,1048576,16,8,1|diskless,file,sharedfile,plain,blockdev,drbd,rbd|4.0|32.0
+default|128,1,1024,1,1,1|128,1,1024,1,1,1;32768,8,1048576,16,8,1|diskless,file,sharedfile,plain,blockdev,drbd,rbd|4.0|32.0
diff --git a/test/data/htools/empty-cluster.data b/test/data/htools/empty-cluster.data
index d25526f5d..9e17bbb0c 100644
--- a/test/data/htools/empty-cluster.data
+++ b/test/data/htools/empty-cluster.data
@@ -3,5 +3,5 @@ group-01|fake-uuid-01|preferred|
 
 
 
-|128,1,1024,1,1,1|128,1,1024,1,1,1|32768,8,1048576,16,8,12|diskless,file,sharedfile,plain,blockdev,drbd,rbd,ext|4.0|32.0
-group-01|128,1,1024,1,1,1|128,1,1024,1,1,1|32768,8,1048576,16,8,12|diskless,file,sharedfile,plain,blockdev,drbd,rbd,ext|4.0|32.0
+|128,1,1024,1,1,1|128,1,1024,1,1,1;32768,8,1048576,16,8,12|diskless,file,sharedfile,plain,blockdev,drbd,rbd,ext|4.0|32.0
+group-01|128,1,1024,1,1,1|128,1,1024,1,1,1;32768,8,1048576,16,8,12|diskless,file,sharedfile,plain,blockdev,drbd,rbd,ext|4.0|32.0
diff --git a/test/data/htools/hail-alloc-drbd.json b/test/data/htools/hail-alloc-drbd.json
index f650fbe60..57e20c3c6 100644
--- a/test/data/htools/hail-alloc-drbd.json
+++ b/test/data/htools/hail-alloc-drbd.json
@@ -14,24 +14,26 @@
           "cpu-count": 1,
           "spindle-use": 1
         },
-        "minmax": {
-	  "min": {
-	    "nic-count": 1,
-	    "disk-size": 128,
-	    "disk-count": 1,
-	    "memory-size": 128,
-	    "cpu-count": 1,
-	    "spindle-use": 1
-	  },
-	  "max": {
-	    "nic-count": 8,
-	    "disk-size": 1048576,
-	    "disk-count": 16,
-	    "memory-size": 32768,
-	    "cpu-count": 8,
-	    "spindle-use": 8
-	  }
-        },
+        "minmax": [
+          {
+            "min": {
+              "nic-count": 1,
+              "disk-size": 128,
+              "disk-count": 1,
+              "memory-size": 128,
+              "cpu-count": 1,
+              "spindle-use": 1
+            },
+            "max": {
+              "nic-count": 8,
+              "disk-size": 1048576,
+              "disk-count": 16,
+              "memory-size": 32768,
+              "cpu-count": 8,
+              "spindle-use": 8
+            }
+          }
+        ],
         "vcpu-ratio": 4.0,
         "disk-templates": [
           "sharedfile",
@@ -58,22 +60,26 @@
       "disk-count": 1,
       "spindle-use": 1
     },
-    "min": {
-      "nic-count": 1,
-      "disk-size": 1024,
-      "memory-size": 128,
-      "cpu-count": 1,
-      "disk-count": 1,
-      "spindle-use": 1
-    },
-    "max": {
-      "nic-count": 8,
-      "disk-size": 1048576,
-      "memory-size": 32768,
-      "cpu-count": 8,
-      "disk-count": 16,
-      "spindle-use": 8
-    },
+    "minmax": [
+      {
+        "min": {
+          "nic-count": 1,
+          "disk-size": 1024,
+          "memory-size": 128,
+          "cpu-count": 1,
+          "disk-count": 1,
+          "spindle-use": 1
+        },
+        "max": {
+          "nic-count": 8,
+          "disk-size": 1048576,
+          "memory-size": 32768,
+          "cpu-count": 8,
+          "disk-count": 16,
+          "spindle-use": 8
+        }
+      }
+    ],
     "vcpu-ratio": 4.0,
     "disk-templates": [
       "sharedfile",
diff --git a/test/data/htools/hail-alloc-invalid-twodisks.json b/test/data/htools/hail-alloc-invalid-twodisks.json
index 790de2a54..4f233c73e 100644
--- a/test/data/htools/hail-alloc-invalid-twodisks.json
+++ b/test/data/htools/hail-alloc-invalid-twodisks.json
@@ -16,24 +16,26 @@
         "disk-templates": [
           "file"
         ],
-        "minmax" : {
-          "max": {
-            "cpu-count": 2,
-            "disk-count": 8,
-            "disk-size": 2048,
-            "memory-size": 12800,
-            "nic-count": 8,
-            "spindle-use": 8
-          },
-          "min": {
-            "cpu-count": 1,
-            "disk-count": 1,
-            "disk-size": 1024,
-            "memory-size": 128,
-            "nic-count": 1,
-            "spindle-use": 1
+        "minmax" : [
+          {
+            "max": {
+              "cpu-count": 2,
+              "disk-count": 8,
+              "disk-size": 2048,
+              "memory-size": 12800,
+              "nic-count": 8,
+              "spindle-use": 8
+            },
+            "min": {
+              "cpu-count": 1,
+              "disk-count": 1,
+              "disk-size": 1024,
+              "memory-size": 128,
+              "nic-count": 1,
+              "spindle-use": 1
+            }
           }
-        },
+        ],
         "spindle-ratio": 32.0,
         "std": {
           "cpu-count": 1,
diff --git a/test/data/htools/hail-alloc-twodisks.json b/test/data/htools/hail-alloc-twodisks.json
index 2a4f63686..b4e72805c 100644
--- a/test/data/htools/hail-alloc-twodisks.json
+++ b/test/data/htools/hail-alloc-twodisks.json
@@ -16,24 +16,26 @@
         "disk-templates": [
           "file"
         ],
-        "minmax": {
-          "max": {
-            "cpu-count": 2,
-            "disk-count": 8,
-            "disk-size": 2048,
-            "memory-size": 12800,
-            "nic-count": 8,
-            "spindle-use": 8
-          },
-          "min": {
-            "cpu-count": 1,
-            "disk-count": 1,
-            "disk-size": 1024,
-            "memory-size": 128,
-            "nic-count": 1,
-            "spindle-use": 1
+        "minmax": [
+          {
+            "max": {
+              "cpu-count": 2,
+              "disk-count": 8,
+              "disk-size": 2048,
+              "memory-size": 12800,
+              "nic-count": 8,
+              "spindle-use": 8
+            },
+            "min": {
+              "cpu-count": 1,
+              "disk-count": 1,
+              "disk-size": 1024,
+              "memory-size": 128,
+              "nic-count": 1,
+              "spindle-use": 1
+            }
           }
-        },
+        ],
         "spindle-ratio": 32.0,
         "std": {
           "cpu-count": 1,
diff --git a/test/data/htools/hail-change-group.json b/test/data/htools/hail-change-group.json
index f698638e8..8cca142dc 100644
--- a/test/data/htools/hail-change-group.json
+++ b/test/data/htools/hail-change-group.json
@@ -14,24 +14,26 @@
           "cpu-count": 1,
           "spindle-use": 1
         },
-        "minmax": {
-	  "min": {
-	    "nic-count": 1,
-	    "disk-size": 128,
-	    "disk-count": 1,
-	    "memory-size": 128,
-	    "cpu-count": 1,
-	    "spindle-use": 1
-	  },
-	  "max": {
-	    "nic-count": 8,
-	    "disk-size": 1048576,
-	    "disk-count": 16,
-	    "memory-size": 32768,
-	    "cpu-count": 8,
-	    "spindle-use": 8
-	  }
-        },
+        "minmax": [
+          {
+            "min": {
+              "nic-count": 1,
+              "disk-size": 128,
+              "disk-count": 1,
+              "memory-size": 128,
+              "cpu-count": 1,
+              "spindle-use": 1
+            },
+            "max": {
+              "nic-count": 8,
+              "disk-size": 1048576,
+              "disk-count": 16,
+              "memory-size": 32768,
+              "cpu-count": 8,
+              "spindle-use": 8
+            }
+          }
+        ],
         "vcpu-ratio": 4.0,
         "disk-templates": [
           "sharedfile",
@@ -58,24 +60,26 @@
           "cpu-count": 1,
           "spindle-use": 1
         },
-        "minmax": {
-	  "min": {
-	    "nic-count": 1,
-	    "disk-size": 128,
-	    "disk-count": 1,
-	    "memory-size": 128,
-	    "cpu-count": 1,
-	    "spindle-use": 1
-	  },
-	  "max": {
-	    "nic-count": 8,
-	    "disk-size": 1048576,
-	    "disk-count": 16,
-	    "memory-size": 32768,
-	    "cpu-count": 8,
-	    "spindle-use": 8
-	  }
-        },
+        "minmax": [
+          {
+            "min": {
+              "nic-count": 1,
+              "disk-size": 128,
+              "disk-count": 1,
+              "memory-size": 128,
+              "cpu-count": 1,
+              "spindle-use": 1
+            },
+            "max": {
+              "nic-count": 8,
+              "disk-size": 1048576,
+              "disk-count": 16,
+              "memory-size": 32768,
+              "cpu-count": 8,
+              "spindle-use": 8
+            }
+          }
+        ],
         "vcpu-ratio": 4.0,
         "disk-templates": [
           "sharedfile",
@@ -102,24 +106,26 @@
       "disk-count": 1,
       "spindle-use": 1
     },
-    "minmax": {
-      "min": {
-	"nic-count": 1,
-	"disk-size": 1024,
-	"memory-size": 128,
-	"cpu-count": 1,
-	"disk-count": 1,
-	"spindle-use": 1
-      },
-      "max": {
-	"nic-count": 8,
-	"disk-size": 1048576,
-	"memory-size": 32768,
-	"cpu-count": 8,
-	"disk-count": 16,
-	"spindle-use": 8
+    "minmax": [
+      {
+        "min": {
+          "nic-count": 1,
+          "disk-size": 1024,
+          "memory-size": 128,
+          "cpu-count": 1,
+          "disk-count": 1,
+          "spindle-use": 1
+        },
+        "max": {
+          "nic-count": 8,
+          "disk-size": 1048576,
+          "memory-size": 32768,
+          "cpu-count": 8,
+          "disk-count": 16,
+          "spindle-use": 8
+        }
       }
-    },
+    ],
     "vcpu-ratio": 4.0,
     "disk-templates": [
       "sharedfile",
diff --git a/test/data/htools/hail-node-evac.json b/test/data/htools/hail-node-evac.json
index 8b80e2534..8fed47789 100644
--- a/test/data/htools/hail-node-evac.json
+++ b/test/data/htools/hail-node-evac.json
@@ -14,24 +14,26 @@
           "cpu-count": 1,
           "spindle-use": 1
         },
-        "minmax": {
-	  "min": {
-	    "nic-count": 1,
-	    "disk-size": 128,
-	    "disk-count": 1,
-	    "memory-size": 128,
-	    "cpu-count": 1,
-	    "spindle-use": 1
-	  },
-	  "max": {
-	    "nic-count": 8,
-	    "disk-size": 1048576,
-	    "disk-count": 16,
-	    "memory-size": 32768,
-	    "cpu-count": 8,
-	    "spindle-use": 8
-	  }
-        },
+        "minmax": [
+          {
+            "min": {
+              "nic-count": 1,
+              "disk-size": 128,
+              "disk-count": 1,
+              "memory-size": 128,
+              "cpu-count": 1,
+              "spindle-use": 1
+            },
+            "max": {
+              "nic-count": 8,
+              "disk-size": 1048576,
+              "disk-count": 16,
+              "memory-size": 32768,
+              "cpu-count": 8,
+              "spindle-use": 8
+            }
+          }
+        ],
         "vcpu-ratio": 4.0,
         "disk-templates": [
           "sharedfile",
diff --git a/test/data/htools/hail-reloc-drbd.json b/test/data/htools/hail-reloc-drbd.json
index ce66041ce..944700e2e 100644
--- a/test/data/htools/hail-reloc-drbd.json
+++ b/test/data/htools/hail-reloc-drbd.json
@@ -14,24 +14,26 @@
           "cpu-count": 1,
           "spindle-use": 1
         },
-        "minmax": {
-	  "min": {
-	    "nic-count": 1,
-	    "disk-size": 128,
-	    "disk-count": 1,
-	    "memory-size": 128,
-	    "cpu-count": 1,
-	    "spindle-use": 1
-	  },
-	  "max": {
-	    "nic-count": 8,
-	    "disk-size": 1048576,
-	    "disk-count": 16,
-	    "memory-size": 32768,
-	    "cpu-count": 8,
-	    "spindle-use": 8
-	  }
-        },
+        "minmax": [
+          {
+            "min": {
+              "nic-count": 1,
+              "disk-size": 128,
+              "disk-count": 1,
+              "memory-size": 128,
+              "cpu-count": 1,
+              "spindle-use": 1
+            },
+            "max": {
+              "nic-count": 8,
+              "disk-size": 1048576,
+              "disk-count": 16,
+              "memory-size": 32768,
+              "cpu-count": 8,
+              "spindle-use": 8
+            }
+          }
+        ],
         "vcpu-ratio": 4.0,
         "disk-templates": [
           "sharedfile",
@@ -58,22 +60,26 @@
       "disk-count": 1,
       "spindle-use": 1
     },
-    "min": {
-      "nic-count": 1,
-      "disk-size": 1024,
-      "memory-size": 128,
-      "cpu-count": 1,
-      "disk-count": 1,
-      "spindle-use": 1
-    },
-    "max": {
-      "nic-count": 8,
-      "disk-size": 1048576,
-      "memory-size": 32768,
-      "cpu-count": 8,
-      "disk-count": 16,
-      "spindle-use": 8
-    },
+    "minmax": [
+      {
+	"min": {
+	  "nic-count": 1,
+	  "disk-size": 1024,
+	  "memory-size": 128,
+	  "cpu-count": 1,
+	  "disk-count": 1,
+	  "spindle-use": 1
+	},
+	"max": {
+	  "nic-count": 8,
+	  "disk-size": 1048576,
+	  "memory-size": 32768,
+	  "cpu-count": 8,
+	  "disk-count": 16,
+	  "spindle-use": 8
+	}
+      }
+    ],
     "vcpu-ratio": 4.0,
     "disk-templates": [
       "sharedfile",
diff --git a/test/data/htools/hbal-split-insts.data b/test/data/htools/hbal-split-insts.data
index 5c5761f31..fe3bb22cd 100644
--- a/test/data/htools/hbal-split-insts.data
+++ b/test/data/htools/hbal-split-insts.data
@@ -140,6 +140,6 @@ new-126|128|1024|1|running|Y|node-01-002|node-01-004|drbd||1
 new-127|128|1024|1|running|Y|node-01-001|node-01-003|drbd||1
 
 
-|128,1,1024,1,1,1|128,1,1024,1,1,1|32768,8,1048576,16,8,12|diskless,file,sharedfile,plain,blockdev,drbd,rbd|4.0|32.0
-group-01|128,1,1024,1,1,1|128,1,1024,1,1,1|32768,8,1048576,16,8,12|diskless,file,sharedfile,plain,blockdev,drbd,rbd|4.0|32.0
-group-02|128,1,1024,1,1,1|128,1,1024,1,1,1|32768,8,1048576,16,8,12|diskless,file,sharedfile,plain,blockdev,drbd,rbd|4.0|32.0
+|128,1,1024,1,1,1|128,1,1024,1,1,1;32768,8,1048576,16,8,12|diskless,file,sharedfile,plain,blockdev,drbd,rbd|4.0|32.0
+group-01|128,1,1024,1,1,1|128,1,1024,1,1,1;32768,8,1048576,16,8,12|diskless,file,sharedfile,plain,blockdev,drbd,rbd|4.0|32.0
+group-02|128,1,1024,1,1,1|128,1,1024,1,1,1;32768,8,1048576,16,8,12|diskless,file,sharedfile,plain,blockdev,drbd,rbd|4.0|32.0
diff --git a/test/data/htools/invalid-node.data b/test/data/htools/invalid-node.data
index f85223ea8..9655f72cd 100644
--- a/test/data/htools/invalid-node.data
+++ b/test/data/htools/invalid-node.data
@@ -6,5 +6,5 @@ node-01-002|1024|0|896|95367|94343|4|N|fake-uuid-01|1
 new-0|128|1024|1|running|Y|no-such-node||plain|
 
 
-|128,1,1024,1,1,1|128,1,1024,1,1,1|32768,8,1048576,16,8,8|diskless,file,sharedfile,plain,blockdev,drbd,rbd|4.0|32.0
-group-01|128,1,1024,1,1,1|128,1,1024,1,1,1|32768,8,1048576,16,8,8|diskless,file,sharedfile,plain,blockdev,drbd,rbd|4.0|32.0
+|128,1,1024,1,1,1|128,1,1024,1,1,1;32768,8,1048576,16,8,8|diskless,file,sharedfile,plain,blockdev,drbd,rbd|4.0|32.0
+group-01|128,1,1024,1,1,1|128,1,1024,1,1,1;32768,8,1048576,16,8,8|diskless,file,sharedfile,plain,blockdev,drbd,rbd|4.0|32.0
diff --git a/test/data/htools/missing-resources.data b/test/data/htools/missing-resources.data
index b12eb4fc5..500576a5d 100644
--- a/test/data/htools/missing-resources.data
+++ b/test/data/htools/missing-resources.data
@@ -5,5 +5,5 @@ node2|1024|0|0|95367|0|4|N|fake-uuid-01|1
 
 
 
-|128,1,1024,1,1,1|128,1,1024,1,1,1|32768,8,1048576,16,8,8|diskless,file,sharedfile,plain,blockdev,drbd,rbd|4.0|32.0
-default|128,1,1024,1,1,1|128,1,1024,1,1,1|32768,8,1048576,16,8,8|diskless,file,sharedfile,plain,blockdev,drbd,rbd|4.0|32.0
+|128,1,1024,1,1,1|128,1,1024,1,1,1;32768,8,1048576,16,8,8|diskless,file,sharedfile,plain,blockdev,drbd,rbd|4.0|32.0
+default|128,1,1024,1,1,1|128,1,1024,1,1,1;32768,8,1048576,16,8,8|diskless,file,sharedfile,plain,blockdev,drbd,rbd|4.0|32.0
diff --git a/test/data/htools/multiple-master.data b/test/data/htools/multiple-master.data
index 35aa1b7bd..5d4fc52f3 100644
--- a/test/data/htools/multiple-master.data
+++ b/test/data/htools/multiple-master.data
@@ -6,5 +6,5 @@ node-01-003|91552|0|91296|953674|953674|16|M|fake-uuid-01|1
 
 
 
-|128,1,1024,1,1,1|128,1,1024,1,1,1|32768,8,1048576,16,8,12|diskless,file,sharedfile,plain,blockdev,drbd,rbd,ext|4.0|32.0
-group-01|128,1,1024,1,1,1|128,1,1024,1,1,1|32768,8,1048576,16,8,12|diskless,file,sharedfile,plain,blockdev,drbd,rbd,ext|4.0|32.0
+|128,1,1024,1,1,1|128,1,1024,1,1,1;32768,8,1048576,16,8,12|diskless,file,sharedfile,plain,blockdev,drbd,rbd,ext|4.0|32.0
+group-01|128,1,1024,1,1,1|128,1,1024,1,1,1;32768,8,1048576,16,8,12|diskless,file,sharedfile,plain,blockdev,drbd,rbd,ext|4.0|32.0
diff --git a/test/data/htools/n1-failure.data b/test/data/htools/n1-failure.data
index 8a90223b2..521875009 100644
--- a/test/data/htools/n1-failure.data
+++ b/test/data/htools/n1-failure.data
@@ -20,6 +20,6 @@ new-6|128|1024|1|running|Y|node-01-004|node-01-002|drbd||1
 new-7|128|1024|1|running|Y|node-01-001|node-01-003|drbd||1
 
 
-|128,1,1024,1,1,1|128,1,1024,1,1,1|32768,8,1048576,16,8,12|diskless,file,sharedfile,plain,blockdev,drbd,rbd,ext|4.0|32.0
-group-01|128,1,1024,1,1,1|128,1,1024,1,1,1|32768,8,1048576,16,8,12|diskless,file,sharedfile,plain,blockdev,drbd,rbd,ext|4.0|32.0
-group-02|128,1,1024,1,1,1|128,1,1024,1,1,1|32768,8,1048576,16,8,12|diskless,file,sharedfile,plain,blockdev,drbd,rbd,ext|4.0|32.0
+|128,1,1024,1,1,1|128,1,1024,1,1,1;32768,8,1048576,16,8,12|diskless,file,sharedfile,plain,blockdev,drbd,rbd,ext|4.0|32.0
+group-01|128,1,1024,1,1,1|128,1,1024,1,1,1;32768,8,1048576,16,8,12|diskless,file,sharedfile,plain,blockdev,drbd,rbd,ext|4.0|32.0
+group-02|128,1,1024,1,1,1|128,1,1024,1,1,1;32768,8,1048576,16,8,12|diskless,file,sharedfile,plain,blockdev,drbd,rbd,ext|4.0|32.0
diff --git a/test/data/htools/rapi/groups.json b/test/data/htools/rapi/groups.json
index dab65698f..8ac08cbfa 100644
--- a/test/data/htools/rapi/groups.json
+++ b/test/data/htools/rapi/groups.json
@@ -11,24 +11,26 @@
         "disk-count": 1,
         "spindle-use": 1
       },
-      "minmax": {
-	"min": {
-	  "cpu-count": 1,
-	  "nic-count": 1,
-	  "disk-size": 1024,
-	  "memory-size": 128,
-	  "disk-count": 1,
-	  "spindle-use": 1
-	},
-	"max": {
-	  "cpu-count": 8,
-	  "nic-count": 8,
-	  "disk-size": 1048576,
-	  "memory-size": 32768,
-	  "disk-count": 16,
-	  "spindle-use": 8
-	}
-      },
+      "minmax": [
+        {
+          "min": {
+            "cpu-count": 1,
+            "nic-count": 1,
+            "disk-size": 1024,
+            "memory-size": 128,
+            "disk-count": 1,
+            "spindle-use": 1
+          },
+          "max": {
+            "cpu-count": 8,
+            "nic-count": 8,
+            "disk-size": 1048576,
+            "memory-size": 32768,
+            "disk-count": 16,
+            "spindle-use": 8
+          }
+        }
+      ],
       "vcpu-ratio": 4.0,
       "disk-templates": [
         "sharedfile",
diff --git a/test/data/htools/rapi/info.json b/test/data/htools/rapi/info.json
index a37d4e52e..20fc0af28 100644
--- a/test/data/htools/rapi/info.json
+++ b/test/data/htools/rapi/info.json
@@ -86,24 +86,26 @@
       "cpu-count": 1,
       "spindle-use": 1
     },
-    "minmax": {
-      "min": {
-	"nic-count": 1,
-	"disk-size": 128,
-	"disk-count": 1,
-	"memory-size": 128,
-	"cpu-count": 1,
-	"spindle-use": 1
-      },
-      "max": {
-	"nic-count": 8,
-	"disk-size": 1048576,
-	"disk-count": 16,
-	"memory-size": 32768,
-	"cpu-count": 8,
-	"spindle-use": 8
+    "minmax": [
+      {
+        "min": {
+          "nic-count": 1,
+          "disk-size": 128,
+          "disk-count": 1,
+          "memory-size": 128,
+          "cpu-count": 1,
+          "spindle-use": 1
+        },
+        "max": {
+          "nic-count": 8,
+          "disk-size": 1048576,
+          "disk-count": 16,
+          "memory-size": 32768,
+          "cpu-count": 8,
+          "spindle-use": 8
+        }
       }
-    },
+    ],
     "vcpu-ratio": 4.0,
     "disk-templates": [
       "sharedfile",
diff --git a/test/data/htools/unique-reboot-order.data b/test/data/htools/unique-reboot-order.data
index d4261c999..a756a05a5 100644
--- a/test/data/htools/unique-reboot-order.data
+++ b/test/data/htools/unique-reboot-order.data
@@ -8,5 +8,5 @@ new-0|128|1152|1|running|Y|node-01-001|node-01-002|drbd||1
 new-1|128|1152|1|running|Y|node-01-002|node-01-003|drbd||1
 
 
-|128,1,1024,1,1,1|128,1,1024,1,1,1|32768,8,1048576,16,8,12|diskless,file,sharedfile,plain,blockdev,drbd,rbd,ext|4.0|32.0
-group-01|128,1,1024,1,1,1|128,1,1024,1,1,1|32768,8,1048576,16,8,12|diskless,file,sharedfile,plain,blockdev,drbd,rbd,ext|4.0|32.0
+|128,1,1024,1,1,1|128,1,1024,1,1,1;32768,8,1048576,16,8,12|diskless,file,sharedfile,plain,blockdev,drbd,rbd,ext|4.0|32.0
+group-01|128,1,1024,1,1,1|128,1,1024,1,1,1;32768,8,1048576,16,8,12|diskless,file,sharedfile,plain,blockdev,drbd,rbd,ext|4.0|32.0
diff --git a/test/hs/Test/Ganeti/HTools/Backend/Text.hs b/test/hs/Test/Ganeti/HTools/Backend/Text.hs
index 5763fa0a6..088a53bbc 100644
--- a/test/hs/Test/Ganeti/HTools/Backend/Text.hs
+++ b/test/hs/Test/Ganeti/HTools/Backend/Text.hs
@@ -7,7 +7,7 @@
 
 {-
 
-Copyright (C) 2009, 2010, 2011, 2012 Google Inc.
+Copyright (C) 2009, 2010, 2011, 2012, 2013 Google Inc.
 
 This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
@@ -158,6 +158,13 @@ prop_ISpecIdempotent ispec =
     Bad msg -> failTest $ "Failed to load ispec: " ++ msg
     Ok ispec' -> ispec ==? ispec'
 
+prop_MultipleMinMaxISpecsIdempotent :: [Types.MinMaxISpecs] -> Property
+prop_MultipleMinMaxISpecsIdempotent minmaxes =
+  case Text.loadMultipleMinMaxISpecs "dummy" . Utils.sepSplit ';' .
+       Text.serializeMultipleMinMaxISpecs $ minmaxes of
+    Bad msg -> failTest $ "Failed to load min/max ispecs: " ++ msg
+    Ok minmaxes' -> minmaxes ==? minmaxes'
+
 prop_IPolicyIdempotent :: Types.IPolicy -> Property
 prop_IPolicyIdempotent ipol =
   case Text.loadIPolicy . Utils.sepSplit '|' $
@@ -210,6 +217,7 @@ testSuite "HTools/Backend/Text"
             , 'prop_Load_NodeFail
             , 'prop_NodeLSIdempotent
             , 'prop_ISpecIdempotent
+            , 'prop_MultipleMinMaxISpecsIdempotent
             , 'prop_IPolicyIdempotent
             , 'prop_CreateSerialise
             ]
diff --git a/test/hs/Test/Ganeti/HTools/Types.hs b/test/hs/Test/Ganeti/HTools/Types.hs
index 3980edb86..1379878ed 100644
--- a/test/hs/Test/Ganeti/HTools/Types.hs
+++ b/test/hs/Test/Ganeti/HTools/Types.hs
@@ -42,6 +42,7 @@ import Test.HUnit
 
 import Control.Applicative
 import Data.List (sort)
+import Control.Monad (replicateM)
 
 import Test.Ganeti.TestHelper
 import Test.Ganeti.TestCommon
@@ -101,19 +102,45 @@ genBiggerISpec imin = do
                      , Types.iSpecSpindleUse = fromIntegral su
                      }
 
+genMinMaxISpecs :: Gen Types.MinMaxISpecs
+genMinMaxISpecs = do
+  imin <- arbitrary
+  imax <- genBiggerISpec imin
+  return Types.MinMaxISpecs { Types.minMaxISpecsMinSpec = imin
+                             , Types.minMaxISpecsMaxSpec = imax
+                             }
+
+instance Arbitrary Types.MinMaxISpecs where
+  arbitrary = genMinMaxISpecs
+
+genMinMaxStdISpecs :: Gen (Types.MinMaxISpecs, Types.ISpec)
+genMinMaxStdISpecs = do
+  imin <- arbitrary
+  istd <- genBiggerISpec imin
+  imax <- genBiggerISpec istd
+  return (Types.MinMaxISpecs { Types.minMaxISpecsMinSpec = imin
+                             , Types.minMaxISpecsMaxSpec = imax
+                             },
+          istd)
+
+genIPolicySpecs :: Gen ([Types.MinMaxISpecs], Types.ISpec)
+genIPolicySpecs = do
+  num_mm <- choose (1, 6) -- 6 is just an arbitrary limit
+  std_compl <- choose (1, num_mm)
+  mm_head <- replicateM (std_compl - 1) genMinMaxISpecs
+  (mm_middle, istd) <- genMinMaxStdISpecs
+  mm_tail <- replicateM (num_mm - std_compl) genMinMaxISpecs
+  return (mm_head ++ (mm_middle : mm_tail), istd)
+
+
 instance Arbitrary Types.IPolicy where
   arbitrary = do
-    imin <- arbitrary
-    istd <- genBiggerISpec imin
-    imax <- genBiggerISpec istd
+    (iminmax, istd) <- genIPolicySpecs
     num_tmpl <- choose (0, length allDiskTemplates)
     dts  <- genUniquesList num_tmpl arbitrary
     vcpu_ratio <- choose (1.0, maxVcpuRatio)
     spindle_ratio <- choose (1.0, maxSpindleRatio)
-    return Types.IPolicy { Types.iPolicyMinMaxISpecs = Types.MinMaxISpecs
-                           { Types.minMaxISpecsMinSpec = imin
-                           , Types.minMaxISpecsMaxSpec = imax
-                           }
+    return Types.IPolicy { Types.iPolicyMinMaxISpecs = iminmax
                          , Types.iPolicyStdSpec = istd
                          , Types.iPolicyDiskTemplates = dts
                          , Types.iPolicyVcpuRatio = vcpu_ratio
diff --git a/test/hs/Test/Ganeti/TestHTools.hs b/test/hs/Test/Ganeti/TestHTools.hs
index 3b0ac6a68..961d7cd0e 100644
--- a/test/hs/Test/Ganeti/TestHTools.hs
+++ b/test/hs/Test/Ganeti/TestHTools.hs
@@ -52,7 +52,7 @@ import qualified Ganeti.HTools.Types as Types
 -- | Null iPolicy, and by null we mean very liberal.
 nullIPolicy :: Types.IPolicy
 nullIPolicy = Types.IPolicy
-  { Types.iPolicyMinMaxISpecs = Types.MinMaxISpecs
+  { Types.iPolicyMinMaxISpecs = [Types.MinMaxISpecs
     { Types.minMaxISpecsMinSpec = Types.ISpec { Types.iSpecMemorySize = 0
                                               , Types.iSpecCpuCount   = 0
                                               , Types.iSpecDiskSize   = 0
@@ -68,7 +68,7 @@ nullIPolicy = Types.IPolicy
       , Types.iSpecNicCount   = C.maxNics
       , Types.iSpecSpindleUse = maxBound
       }
-    }
+    }]
   , Types.iPolicyStdSpec = Types.ISpec { Types.iSpecMemorySize = Types.unitMem
                                        , Types.iSpecCpuCount   = Types.unitCpu
                                        , Types.iSpecDiskSize   = Types.unitDsk
diff --git a/test/py/ganeti.cli_unittest.py b/test/py/ganeti.cli_unittest.py
index 6ad63fcd9..4d89c262f 100755
--- a/test/py/ganeti.cli_unittest.py
+++ b/test/py/ganeti.cli_unittest.py
@@ -1184,7 +1184,8 @@ class TestCreateIPolicyFromOpts(unittest.TestCase):
     # Policies are big, and we want to see the difference in case of an error
     self.maxDiff = None
 
-  def _RecursiveCheckMergedDicts(self, default_pol, diff_pol, merged_pol):
+  def _RecursiveCheckMergedDicts(self, default_pol, diff_pol, merged_pol,
+                                 merge_minmax=False):
     self.assertTrue(type(default_pol) is dict)
     self.assertTrue(type(diff_pol) is dict)
     self.assertTrue(type(merged_pol) is dict)
@@ -1194,6 +1195,12 @@ class TestCreateIPolicyFromOpts(unittest.TestCase):
       if key in diff_pol:
         if type(val) is dict:
           self._RecursiveCheckMergedDicts(default_pol[key], diff_pol[key], val)
+        elif (merge_minmax and key == "minmax" and type(val) is list and
+              len(val) == 1):
+          self.assertEqual(len(default_pol[key]), 1)
+          self.assertEqual(len(diff_pol[key]), 1)
+          self._RecursiveCheckMergedDicts(default_pol[key][0],
+                                          diff_pol[key][0], val[0])
         else:
           self.assertEqual(val, diff_pol[key])
       else:
@@ -1214,16 +1221,18 @@ class TestCreateIPolicyFromOpts(unittest.TestCase):
     self.assertEqual(pol0, constants.IPOLICY_DEFAULTS)
 
     exp_pol1 = {
-      constants.ISPECS_MINMAX: {
-        constants.ISPECS_MIN: {
-          constants.ISPEC_CPU_COUNT: 2,
-          constants.ISPEC_DISK_COUNT: 1,
-          },
-        constants.ISPECS_MAX: {
-          constants.ISPEC_MEM_SIZE: 12*1024,
-          constants.ISPEC_DISK_COUNT: 2,
+      constants.ISPECS_MINMAX: [
+        {
+          constants.ISPECS_MIN: {
+            constants.ISPEC_CPU_COUNT: 2,
+            constants.ISPEC_DISK_COUNT: 1,
+            },
+          constants.ISPECS_MAX: {
+            constants.ISPEC_MEM_SIZE: 12*1024,
+            constants.ISPEC_DISK_COUNT: 2,
+            },
           },
-        },
+        ],
       constants.ISPECS_STD: {
         constants.ISPEC_CPU_COUNT: 2,
         constants.ISPEC_DISK_COUNT: 2,
@@ -1242,18 +1251,20 @@ class TestCreateIPolicyFromOpts(unittest.TestCase):
       fill_all=True
       )
     self._RecursiveCheckMergedDicts(constants.IPOLICY_DEFAULTS,
-                                    exp_pol1, pol1)
+                                    exp_pol1, pol1, merge_minmax=True)
 
     exp_pol2 = {
-      constants.ISPECS_MINMAX: {
-        constants.ISPECS_MIN: {
-          constants.ISPEC_DISK_SIZE: 512,
-          constants.ISPEC_NIC_COUNT: 2,
+      constants.ISPECS_MINMAX: [
+        {
+          constants.ISPECS_MIN: {
+            constants.ISPEC_DISK_SIZE: 512,
+            constants.ISPEC_NIC_COUNT: 2,
+            },
+          constants.ISPECS_MAX: {
+            constants.ISPEC_NIC_COUNT: 3,
+            },
           },
-        constants.ISPECS_MAX: {
-          constants.ISPEC_NIC_COUNT: 3,
-          },
-        },
+        ],
       constants.ISPECS_STD: {
         constants.ISPEC_CPU_COUNT: 2,
         constants.ISPEC_NIC_COUNT: 3,
@@ -1273,7 +1284,7 @@ class TestCreateIPolicyFromOpts(unittest.TestCase):
       fill_all=True
       )
     self._RecursiveCheckMergedDicts(constants.IPOLICY_DEFAULTS,
-                                      exp_pol2, pol2)
+                                      exp_pol2, pol2, merge_minmax=True)
 
     for fill_all in [False, True]:
       exp_pol3 = {
@@ -1294,7 +1305,7 @@ class TestCreateIPolicyFromOpts(unittest.TestCase):
         )
       if fill_all:
         self._RecursiveCheckMergedDicts(constants.IPOLICY_DEFAULTS,
-                                        exp_pol3, pol3)
+                                        exp_pol3, pol3, merge_minmax=True)
       else:
         self.assertEqual(pol3, exp_pol3)
 
@@ -1384,6 +1395,9 @@ class TestCreateIPolicyFromOpts(unittest.TestCase):
       broken_mmspecs[key]["invalid_key"] = None
       self._TestInvalidISpecs(broken_mmspecs, None)
       del broken_mmspecs[key]["invalid_key"]
+    broken_mmspecs["invalid_key"] = None
+    self._TestInvalidISpecs(broken_mmspecs, None)
+    del broken_mmspecs["invalid_key"]
     assert broken_mmspecs == good_mmspecs
 
     good_stdspecs = constants.IPOLICY_DEFAULTS[constants.ISPECS_STD]
@@ -1437,7 +1451,7 @@ class TestCreateIPolicyFromOpts(unittest.TestCase):
       minmax_ispecs = {}
       for (key, spec) in exp_minmax.items():
         minmax_ispecs[key] = self._ConvertSpecToStrings(spec)
-      exp_ipol[constants.ISPECS_MINMAX] = exp_minmax
+      exp_ipol[constants.ISPECS_MINMAX] = [exp_minmax]
     else:
       minmax_ispecs = None
     if exp_std is not None:
@@ -1527,15 +1541,16 @@ class TestPrintIPolicyCommand(unittest.TestCase):
     policies = [
       {},
       {"std": {}},
-      {"minmax": {}},
-      {"minmax": {
+      {"minmax": []},
+      {"minmax": [{}]},
+      {"minmax": [{
         "min": {},
         "max": {},
-        }},
-      {"minmax": {
+        }]},
+      {"minmax": [{
         "min": self._SPECS1,
         "max": {},
-        }},
+        }]},
       ]
     for pol in policies:
       self._CheckPrintIPolicyCommand(pol, False, "")
@@ -1544,10 +1559,10 @@ class TestPrintIPolicyCommand(unittest.TestCase):
     cases = [
       ({"std": self._SPECS1},
        " %s %s" % (cli.IPOLICY_STD_SPECS_STR, self._SPECS1_STR)),
-      ({"minmax": {
+      ({"minmax": [{
         "min": self._SPECS1,
         "max": self._SPECS2,
-        }},
+        }]},
        " %s min:%s/max:%s" % (cli.IPOLICY_BOUNDS_SPECS_STR,
                               self._SPECS1_STR, self._SPECS2_STR)),
       ]
diff --git a/test/py/ganeti.cmdlib_unittest.py b/test/py/ganeti.cmdlib_unittest.py
index 68ee95e27..bc4e7ee4f 100755
--- a/test/py/ganeti.cmdlib_unittest.py
+++ b/test/py/ganeti.cmdlib_unittest.py
@@ -673,7 +673,7 @@ class TestComputeIPolicySpecViolation(unittest.TestCase):
   # Minimal policy accepted by _ComputeIPolicySpecViolation()
   _MICRO_IPOL = {
     constants.IPOLICY_DTS: [constants.DT_PLAIN, constants.DT_DISKLESS],
-    constants.ISPECS_MINMAX: NotImplemented,
+    constants.ISPECS_MINMAX: [NotImplemented],
     }
 
   def test(self):
@@ -719,6 +719,79 @@ class TestComputeIPolicySpecViolation(unittest.TestCase):
     self.assertEqual(ret, ["foo", "bar"])
     self.assertFalse(spec.spec)
 
+  def testWithIPolicy(self):
+    mem_size = 2048
+    cpu_count = 2
+    disk_count = 1
+    disk_sizes = [512]
+    nic_count = 1
+    spindle_use = 4
+    disk_template = "mytemplate"
+    ispec = {
+      constants.ISPEC_MEM_SIZE: mem_size,
+      constants.ISPEC_CPU_COUNT: cpu_count,
+      constants.ISPEC_DISK_COUNT: disk_count,
+      constants.ISPEC_DISK_SIZE: disk_sizes[0],
+      constants.ISPEC_NIC_COUNT: nic_count,
+      constants.ISPEC_SPINDLE_USE: spindle_use,
+      }
+    ipolicy1 = {
+      constants.ISPECS_MINMAX: [{
+        constants.ISPECS_MIN: ispec,
+        constants.ISPECS_MAX: ispec,
+        }],
+      constants.IPOLICY_DTS: [disk_template],
+      }
+    ispec_copy = copy.deepcopy(ispec)
+    ipolicy2 = {
+      constants.ISPECS_MINMAX: [
+        {
+          constants.ISPECS_MIN: ispec_copy,
+          constants.ISPECS_MAX: ispec_copy,
+          },
+        {
+          constants.ISPECS_MIN: ispec,
+          constants.ISPECS_MAX: ispec,
+          },
+        ],
+      constants.IPOLICY_DTS: [disk_template],
+      }
+    ipolicy3 = {
+      constants.ISPECS_MINMAX: [
+        {
+          constants.ISPECS_MIN: ispec,
+          constants.ISPECS_MAX: ispec,
+          },
+        {
+          constants.ISPECS_MIN: ispec_copy,
+          constants.ISPECS_MAX: ispec_copy,
+          },
+        ],
+      constants.IPOLICY_DTS: [disk_template],
+      }
+    def AssertComputeViolation(ipolicy, violations):
+      ret = cmdlib._ComputeIPolicySpecViolation(ipolicy, mem_size, cpu_count,
+                                                disk_count, nic_count,
+                                                disk_sizes, spindle_use,
+                                                disk_template)
+      self.assertEqual(len(ret), violations)
+
+    AssertComputeViolation(ipolicy1, 0)
+    AssertComputeViolation(ipolicy2, 0)
+    AssertComputeViolation(ipolicy3, 0)
+    for par in constants.ISPECS_PARAMETERS:
+      ispec[par] += 1
+      AssertComputeViolation(ipolicy1, 1)
+      AssertComputeViolation(ipolicy2, 0)
+      AssertComputeViolation(ipolicy3, 0)
+      ispec[par] -= 2
+      AssertComputeViolation(ipolicy1, 1)
+      AssertComputeViolation(ipolicy2, 0)
+      AssertComputeViolation(ipolicy3, 0)
+      ispec[par] += 1 # Restore
+    ipolicy1[constants.IPOLICY_DTS] = ["another_template"]
+    AssertComputeViolation(ipolicy1, 1)
+
 
 class _StubComputeIPolicySpecViolation:
   def __init__(self, mem_size, cpu_count, disk_count, nic_count, disk_sizes,
@@ -1733,12 +1806,32 @@ class TestGetUpdatedIPolicy(unittest.TestCase):
   """Tests for cmdlib._GetUpdatedIPolicy()"""
   _OLD_CLUSTER_POLICY = {
     constants.IPOLICY_VCPU_RATIO: 1.5,
-    constants.ISPECS_MINMAX: constants.ISPECS_MINMAX_DEFAULTS,
+    constants.ISPECS_MINMAX: [
+      {
+        constants.ISPECS_MIN: {
+          constants.ISPEC_MEM_SIZE: 32768,
+          constants.ISPEC_CPU_COUNT: 8,
+          constants.ISPEC_DISK_COUNT: 1,
+          constants.ISPEC_DISK_SIZE: 1024,
+          constants.ISPEC_NIC_COUNT: 1,
+          constants.ISPEC_SPINDLE_USE: 1,
+          },
+        constants.ISPECS_MAX: {
+          constants.ISPEC_MEM_SIZE: 65536,
+          constants.ISPEC_CPU_COUNT: 10,
+          constants.ISPEC_DISK_COUNT: 5,
+          constants.ISPEC_DISK_SIZE: 1024 * 1024,
+          constants.ISPEC_NIC_COUNT: 3,
+          constants.ISPEC_SPINDLE_USE: 12,
+          },
+        },
+      constants.ISPECS_MINMAX_DEFAULTS,
+      ],
     constants.ISPECS_STD: constants.IPOLICY_DEFAULTS[constants.ISPECS_STD],
     }
   _OLD_GROUP_POLICY = {
     constants.IPOLICY_SPINDLE_RATIO: 2.5,
-    constants.ISPECS_MINMAX: {
+    constants.ISPECS_MINMAX: [{
       constants.ISPECS_MIN: {
         constants.ISPEC_MEM_SIZE: 128,
         constants.ISPEC_CPU_COUNT: 1,
@@ -1755,11 +1848,11 @@ class TestGetUpdatedIPolicy(unittest.TestCase):
         constants.ISPEC_NIC_COUNT: 3,
         constants.ISPEC_SPINDLE_USE: 12,
         },
-      },
+      }],
     }
 
   def _TestSetSpecs(self, old_policy, isgroup):
-    diff_minmax = {
+    diff_minmax = [{
       constants.ISPECS_MIN: {
         constants.ISPEC_MEM_SIZE: 64,
         constants.ISPEC_CPU_COUNT: 1,
@@ -1776,7 +1869,7 @@ class TestGetUpdatedIPolicy(unittest.TestCase):
         constants.ISPEC_NIC_COUNT: 9,
         constants.ISPEC_SPINDLE_USE: 18,
         },
-      }
+      }]
     diff_std = {
         constants.ISPEC_DISK_COUNT: 10,
         constants.ISPEC_DISK_SIZE: 512,
@@ -1847,6 +1940,16 @@ class TestGetUpdatedIPolicy(unittest.TestCase):
     self.assertRaises(errors.OpPrereqError, cmdlib._GetUpdatedIPolicy,
                       old_policy, diff_policy, group_policy=False)
 
+  def testUnsetEmpty(self):
+    old_policy = {}
+    for key in constants.IPOLICY_ALL_KEYS:
+      diff_policy = {
+        key: constants.VALUE_DEFAULT,
+        }
+    new_policy = cmdlib._GetUpdatedIPolicy(old_policy, diff_policy,
+                                           group_policy=True)
+    self.assertEqual(new_policy, old_policy)
+
   def _TestInvalidKeys(self, old_policy, isgroup):
     INVALID_KEY = "this_key_shouldnt_be_allowed"
     INVALID_DICT = {
@@ -1856,7 +1959,7 @@ class TestGetUpdatedIPolicy(unittest.TestCase):
     self.assertRaises(errors.OpPrereqError, cmdlib._GetUpdatedIPolicy,
                       old_policy, invalid_policy, group_policy=isgroup)
     invalid_ispecs = {
-      constants.ISPECS_MINMAX: INVALID_DICT,
+      constants.ISPECS_MINMAX: [INVALID_DICT],
       }
     self.assertRaises(errors.TypeEnforcementError, cmdlib._GetUpdatedIPolicy,
                       old_policy, invalid_ispecs, group_policy=isgroup)
@@ -1871,19 +1974,21 @@ class TestGetUpdatedIPolicy(unittest.TestCase):
     invalid_policy = {
       constants.ISPECS_MINMAX: invalid_ispecs,
       }
-    for key in constants.ISPECS_MINMAX_KEYS:
-      ispec = invalid_ispecs[key]
-      ispec[INVALID_KEY] = None
-      self.assertRaises(errors.TypeEnforcementError, cmdlib._GetUpdatedIPolicy,
-                        old_policy, invalid_policy, group_policy=isgroup)
-      del ispec[INVALID_KEY]
-      for par in constants.ISPECS_PARAMETERS:
-        oldv = ispec[par]
-        ispec[par] = "this_is_not_good"
+    for minmax in invalid_ispecs:
+      for key in constants.ISPECS_MINMAX_KEYS:
+        ispec = minmax[key]
+        ispec[INVALID_KEY] = None
         self.assertRaises(errors.TypeEnforcementError,
-                          cmdlib._GetUpdatedIPolicy,
-                          old_policy, invalid_policy, group_policy=isgroup)
-        ispec[par] = oldv
+                          cmdlib._GetUpdatedIPolicy, old_policy,
+                          invalid_policy, group_policy=isgroup)
+        del ispec[INVALID_KEY]
+        for par in constants.ISPECS_PARAMETERS:
+          oldv = ispec[par]
+          ispec[par] = "this_is_not_good"
+          self.assertRaises(errors.TypeEnforcementError,
+                            cmdlib._GetUpdatedIPolicy,
+                            old_policy, invalid_policy, group_policy=isgroup)
+          ispec[par] = oldv
     # This is to make sure that no two errors were present during the tests
     cmdlib._GetUpdatedIPolicy(old_policy, invalid_policy, group_policy=isgroup)
 
diff --git a/test/py/ganeti.config_unittest.py b/test/py/ganeti.config_unittest.py
index 7eff2e6d1..cbfa51022 100755
--- a/test/py/ganeti.config_unittest.py
+++ b/test/py/ganeti.config_unittest.py
@@ -461,12 +461,13 @@ class TestConfigRunner(unittest.TestCase):
 
     ispeclist = []
     if constants.ISPECS_MINMAX in ipolicy:
-      ispeclist.extend([
-        (ipolicy[constants.ISPECS_MINMAX][constants.ISPECS_MIN],
-         "%s/%s" % (constants.ISPECS_MINMAX, constants.ISPECS_MIN)),
-        (ipolicy[constants.ISPECS_MINMAX][constants.ISPECS_MAX],
-         "%s/%s" % (constants.ISPECS_MINMAX, constants.ISPECS_MAX)),
-        ])
+      for k in range(len(ipolicy[constants.ISPECS_MINMAX])):
+        ispeclist.extend([
+            (ipolicy[constants.ISPECS_MINMAX][k][constants.ISPECS_MIN],
+             "%s[%s]/%s" % (constants.ISPECS_MINMAX, k, constants.ISPECS_MIN)),
+            (ipolicy[constants.ISPECS_MINMAX][k][constants.ISPECS_MAX],
+             "%s[%s]/%s" % (constants.ISPECS_MINMAX, k, constants.ISPECS_MAX)),
+            ])
     if constants.ISPECS_STD in ipolicy:
       ispeclist.append((ipolicy[constants.ISPECS_STD], constants.ISPECS_STD))
 
@@ -498,23 +499,24 @@ class TestConfigRunner(unittest.TestCase):
 
     if constants.ISPECS_MINMAX in ipolicy:
       # Test partial minmax specs
-      minmax = ipolicy[constants.ISPECS_MINMAX]
-      for key in constants.ISPECS_MINMAX_KEYS:
-        self.assertTrue(key in minmax)
-        ispec = minmax[key]
-        del minmax[key]
-        errs = cfg.VerifyConfig()
-        self.assertTrue(len(errs) >= 1)
-        self.assertTrue(_IsErrorInList("Missing instance specification", errs))
-        minmax[key] = ispec
-        for par in constants.ISPECS_PARAMETERS:
-          oldv = ispec[par]
-          del ispec[par]
+      for minmax in ipolicy[constants.ISPECS_MINMAX]:
+        for key in constants.ISPECS_MINMAX_KEYS:
+          self.assertTrue(key in minmax)
+          ispec = minmax[key]
+          del minmax[key]
           errs = cfg.VerifyConfig()
           self.assertTrue(len(errs) >= 1)
-          self.assertTrue(_IsErrorInList("Missing instance specs parameters",
+          self.assertTrue(_IsErrorInList("Missing instance specification",
                                          errs))
-          ispec[par] = oldv
+          minmax[key] = ispec
+          for par in constants.ISPECS_PARAMETERS:
+            oldv = ispec[par]
+            del ispec[par]
+            errs = cfg.VerifyConfig()
+            self.assertTrue(len(errs) >= 1)
+            self.assertTrue(_IsErrorInList("Missing instance specs parameters",
+                                           errs))
+            ispec[par] = oldv
       errs = cfg.VerifyConfig()
       self.assertFalse(errs)
 
diff --git a/test/py/ganeti.objects_unittest.py b/test/py/ganeti.objects_unittest.py
index 318ef88aa..719a4a12a 100755
--- a/test/py/ganeti.objects_unittest.py
+++ b/test/py/ganeti.objects_unittest.py
@@ -22,6 +22,7 @@
 """Script for unittesting the objects module"""
 
 
+import copy
 import unittest
 
 from ganeti import constants
@@ -414,11 +415,12 @@ class TestInstancePolicy(unittest.TestCase):
 
   def _AssertIPolicyIsFull(self, policy):
     self.assertEqual(frozenset(policy.keys()), constants.IPOLICY_ALL_KEYS)
-    minmax = policy[constants.ISPECS_MINMAX]
-    self.assertEqual(frozenset(minmax.keys()), constants.ISPECS_MINMAX_KEYS)
-    for key in constants.ISPECS_MINMAX_KEYS:
-      self.assertEqual(frozenset(minmax[key].keys()),
-                       constants.ISPECS_PARAMETERS)
+    self.assertTrue(len(policy[constants.ISPECS_MINMAX]) > 0)
+    for minmax in policy[constants.ISPECS_MINMAX]:
+      self.assertEqual(frozenset(minmax.keys()), constants.ISPECS_MINMAX_KEYS)
+      for key in constants.ISPECS_MINMAX_KEYS:
+        self.assertEqual(frozenset(minmax[key].keys()),
+                         constants.ISPECS_PARAMETERS)
     self.assertEqual(frozenset(policy[constants.ISPECS_STD].keys()),
                      constants.ISPECS_PARAMETERS)
 
@@ -427,36 +429,132 @@ class TestInstancePolicy(unittest.TestCase):
                                                 True)
     self._AssertIPolicyIsFull(constants.IPOLICY_DEFAULTS)
 
+  def _AssertPolicyIsBad(self, ipolicy, do_check_std=None):
+    if do_check_std is None:
+      check_std_vals = [False, True]
+    else:
+      check_std_vals = [do_check_std]
+    for check_std in check_std_vals:
+      self.assertRaises(errors.ConfigurationError,
+                        objects.InstancePolicy.CheckISpecSyntax,
+                        ipolicy, check_std)
+
   def testCheckISpecSyntax(self):
     default_stdspec = constants.IPOLICY_DEFAULTS[constants.ISPECS_STD]
     incomplete_ipolicies = [
       {
-         constants.ISPECS_MINMAX: {},
+         constants.ISPECS_MINMAX: [],
          constants.ISPECS_STD: default_stdspec,
          },
       {
-        constants.ISPECS_MINMAX: {
+         constants.ISPECS_MINMAX: [{}],
+         constants.ISPECS_STD: default_stdspec,
+         },
+      {
+        constants.ISPECS_MINMAX: [{
           constants.ISPECS_MIN: NotImplemented,
-          },
+          }],
         constants.ISPECS_STD: default_stdspec,
         },
       {
-        constants.ISPECS_MINMAX: {
+        constants.ISPECS_MINMAX: [{
           constants.ISPECS_MAX: NotImplemented,
-          },
+          }],
         constants.ISPECS_STD: default_stdspec,
         },
       {
-        constants.ISPECS_MINMAX: {
+        constants.ISPECS_MINMAX: [{
           constants.ISPECS_MIN: NotImplemented,
           constants.ISPECS_MAX: NotImplemented,
-          },
+          }],
         },
       ]
     for ipol in incomplete_ipolicies:
       self.assertRaises(errors.ConfigurationError,
                         objects.InstancePolicy.CheckISpecSyntax,
                         ipol, True)
+      oldminmax = ipol[constants.ISPECS_MINMAX]
+      if oldminmax:
+        # Prepending valid specs shouldn't change the error
+        ipol[constants.ISPECS_MINMAX] = ([constants.ISPECS_MINMAX_DEFAULTS] +
+                                         oldminmax)
+        self.assertRaises(errors.ConfigurationError,
+                          objects.InstancePolicy.CheckISpecSyntax,
+                          ipol, True)
+
+    good_ipolicy = {
+      constants.ISPECS_MINMAX: [
+        {
+          constants.ISPECS_MIN: {
+            constants.ISPEC_MEM_SIZE: 64,
+            constants.ISPEC_CPU_COUNT: 1,
+            constants.ISPEC_DISK_COUNT: 2,
+            constants.ISPEC_DISK_SIZE: 64,
+            constants.ISPEC_NIC_COUNT: 1,
+            constants.ISPEC_SPINDLE_USE: 1,
+            },
+          constants.ISPECS_MAX: {
+            constants.ISPEC_MEM_SIZE: 16384,
+            constants.ISPEC_CPU_COUNT: 5,
+            constants.ISPEC_DISK_COUNT: 12,
+            constants.ISPEC_DISK_SIZE: 1024,
+            constants.ISPEC_NIC_COUNT: 9,
+            constants.ISPEC_SPINDLE_USE: 18,
+            },
+          },
+        {
+          constants.ISPECS_MIN: {
+            constants.ISPEC_MEM_SIZE: 32768,
+            constants.ISPEC_CPU_COUNT: 8,
+            constants.ISPEC_DISK_COUNT: 1,
+            constants.ISPEC_DISK_SIZE: 1024,
+            constants.ISPEC_NIC_COUNT: 1,
+            constants.ISPEC_SPINDLE_USE: 1,
+            },
+          constants.ISPECS_MAX: {
+            constants.ISPEC_MEM_SIZE: 65536,
+            constants.ISPEC_CPU_COUNT: 10,
+            constants.ISPEC_DISK_COUNT: 5,
+            constants.ISPEC_DISK_SIZE: 1024 * 1024,
+            constants.ISPEC_NIC_COUNT: 3,
+            constants.ISPEC_SPINDLE_USE: 12,
+            },
+          },
+        ],
+      }
+    good_ipolicy[constants.ISPECS_STD] = copy.deepcopy(
+      good_ipolicy[constants.ISPECS_MINMAX][0][constants.ISPECS_MAX])
+    # Check that it's really good before making it bad
+    objects.InstancePolicy.CheckISpecSyntax(good_ipolicy, True)
+
+    bad_ipolicy = copy.deepcopy(good_ipolicy)
+    for minmax in bad_ipolicy[constants.ISPECS_MINMAX]:
+      for (key, spec) in minmax.items():
+        for param in spec:
+          oldv = spec[param]
+          del spec[param]
+          self._AssertPolicyIsBad(bad_ipolicy)
+          if key == constants.ISPECS_MIN:
+            spec[param] = minmax[constants.ISPECS_MAX][param] + 1
+          self._AssertPolicyIsBad(bad_ipolicy)
+          spec[param] = oldv
+    assert bad_ipolicy == good_ipolicy
+
+    stdspec = bad_ipolicy[constants.ISPECS_STD]
+    for param in stdspec:
+      oldv = stdspec[param]
+      del stdspec[param]
+      self._AssertPolicyIsBad(bad_ipolicy, True)
+      # Note that std spec is the same as a max spec
+      stdspec[param] = oldv + 1
+      self._AssertPolicyIsBad(bad_ipolicy, True)
+      stdspec[param] = oldv
+    assert bad_ipolicy == good_ipolicy
+
+    for minmax in good_ipolicy[constants.ISPECS_MINMAX]:
+      for spec in minmax.values():
+        good_ipolicy[constants.ISPECS_STD] = spec
+        objects.InstancePolicy.CheckISpecSyntax(good_ipolicy, True)
 
   def testCheckISpecParamSyntax(self):
     par = "my_parameter"
@@ -558,12 +656,7 @@ class TestInstancePolicy(unittest.TestCase):
   def _AssertIPolicyMerged(self, default_pol, diff_pol, merged_pol):
     for (key, value) in merged_pol.items():
       if key in diff_pol:
-        if key == constants.ISPECS_MINMAX:
-          self.assertEqual(frozenset(value), constants.ISPECS_MINMAX_KEYS)
-          for k in constants.ISPECS_MINMAX_KEYS:
-            self._AssertISpecsMerged(default_pol[key][k], diff_pol[key][k],
-                                     value[k])
-        elif key == constants.ISPECS_STD:
+        if key == constants.ISPECS_STD:
           self._AssertISpecsMerged(default_pol[key], diff_pol[key], value)
         else:
           self.assertEqual(value, diff_pol[key])
@@ -575,6 +668,9 @@ class TestInstancePolicy(unittest.TestCase):
       {constants.IPOLICY_VCPU_RATIO: 3.14},
       {constants.IPOLICY_SPINDLE_RATIO: 2.72},
       {constants.IPOLICY_DTS: [constants.DT_FILE]},
+      {constants.ISPECS_STD: {constants.ISPEC_DISK_COUNT: 3}},
+      {constants.ISPECS_MINMAX: [constants.ISPECS_MINMAX_DEFAULTS,
+                                 constants.ISPECS_MINMAX_DEFAULTS]}
       ]
     for diff_pol in partial_policies:
       policy = objects.FillIPolicy(constants.IPOLICY_DEFAULTS, diff_pol)
diff --git a/tools/cfgupgrade b/tools/cfgupgrade
index 4d80b18ca..0526b1a17 100755
--- a/tools/cfgupgrade
+++ b/tools/cfgupgrade
@@ -104,8 +104,8 @@ def CheckHostname(path):
 
 def _FillIPolicySpecs(default_ipolicy, ipolicy):
   if "minmax" in ipolicy:
-    for (key, spec) in ipolicy["minmax"].items():
-      for (par, val) in default_ipolicy["minmax"][key].items():
+    for (key, spec) in ipolicy["minmax"][0].items():
+      for (par, val) in default_ipolicy["minmax"][0][key].items():
         if par not in spec:
           spec[par] = val
 
@@ -120,7 +120,7 @@ def UpgradeIPolicy(ipolicy, default_ipolicy, isgroup):
           minmax[key] = ipolicy[key]
         del ipolicy[key]
     if minmax:
-      ipolicy["minmax"] = minmax
+      ipolicy["minmax"] = [minmax]
   if isgroup and "std" in ipolicy:
     del ipolicy["std"]
   _FillIPolicySpecs(default_ipolicy, ipolicy)
@@ -260,13 +260,19 @@ def UpgradeAll(config_data):
   UpgradeInstances(config_data)
 
 
-def DowngradeIPolicy(ipolicy):
+def DowngradeIPolicy(ipolicy, owner):
   # Downgrade IPolicy to 2.7 (stable)
   minmax_keys = ["min", "max"]
   specs_is_split = any((k in ipolicy) for k in minmax_keys)
   if not specs_is_split:
     if "minmax" in ipolicy:
-      minmax = ipolicy["minmax"]
+      if type(ipolicy["minmax"]) is not list:
+        raise Error("Invalid minmax type in %s ipolicy: %s" %
+                    (owner, type(ipolicy["minmax"])))
+      if len(ipolicy["minmax"]) > 1:
+        logging.warning("Discarding some limit specs values from %s policy",
+                        owner)
+      minmax = ipolicy["minmax"][0]
       del ipolicy["minmax"]
     else:
       minmax = {}
@@ -281,7 +287,7 @@ def DowngradeGroups(config_data):
   for group in config_data["nodegroups"].values():
     ipolicy = group.get("ipolicy", None)
     if ipolicy is not None:
-      DowngradeIPolicy(ipolicy)
+      DowngradeIPolicy(ipolicy, "group \"%s\"" % group.get("name"))
 
 
 def DowngradeEnabledTemplates(cluster):
@@ -300,7 +306,7 @@ def DowngradeCluster(config_data):
   DowngradeEnabledTemplates(cluster)
   ipolicy = cluster.get("ipolicy", None)
   if ipolicy:
-    DowngradeIPolicy(ipolicy)
+    DowngradeIPolicy(ipolicy, "cluster")
 
 
 def DowngradeAll(config_data):
-- 
GitLab