(2.10) Minor changes regarding hotplug support
[ganeti-local] / lib / cli.py
index 7615c73..8e8402c 100644 (file)
@@ -95,6 +95,7 @@ __all__ = [
   "GLOBAL_FILEDIR_OPT",
   "HID_OS_OPT",
   "GLOBAL_SHARED_FILEDIR_OPT",
+  "HOTPLUG_OPT",
   "HVLIST_OPT",
   "HVOPTS_OPT",
   "HYPERVISOR_OPT",
@@ -116,6 +117,7 @@ __all__ = [
   "MASTER_NETMASK_OPT",
   "MC_OPT",
   "MIGRATION_MODE_OPT",
+  "MODIFY_ETCHOSTS_OPT",
   "NET_OPT",
   "NETWORK_OPT",
   "NETWORK6_OPT",
@@ -283,6 +285,7 @@ __all__ = [
   "OPT_COMPL_ONE_OS",
   "OPT_COMPL_ONE_EXTSTORAGE",
   "cli_option",
+  "FixHvParams",
   "SplitNodeOption",
   "CalculateOSNames",
   "ParseFields",
@@ -680,14 +683,17 @@ def _SplitListKeyVal(opt, value):
   return retval
 
 
-def check_list_ident_key_val(_, opt, value):
-  """Custom parser for "ident:key=val,key=val/ident:key=val" options.
+def check_multilist_ident_key_val(_, opt, value):
+  """Custom parser for "ident:key=val,key=val/ident:key=val//ident:.." options.
 
   @rtype: list of dictionary
-  @return: {ident: {key: val, key: val}, ident: {key: val}}
+  @return: [{ident: {key: val, key: val}, ident: {key: val}}, {ident:..}]
 
   """
-  return _SplitListKeyVal(opt, value)
+  retval = []
+  for line in value.split("//"):
+    retval.append(_SplitListKeyVal(opt, line))
+  return retval
 
 
 def check_bool(option, opt, value): # pylint: disable=W0613
@@ -762,7 +768,7 @@ class CliOption(Option):
     "completion_suggest",
     ]
   TYPES = Option.TYPES + (
-    "listidentkeyval",
+    "multilistidentkeyval",
     "identkeyval",
     "keyval",
     "unit",
@@ -771,7 +777,7 @@ class CliOption(Option):
     "maybefloat",
     )
   TYPE_CHECKER = Option.TYPE_CHECKER.copy()
-  TYPE_CHECKER["listidentkeyval"] = check_list_ident_key_val
+  TYPE_CHECKER["multilistidentkeyval"] = check_multilist_ident_key_val
   TYPE_CHECKER["identkeyval"] = check_ident_key_val
   TYPE_CHECKER["keyval"] = check_key_val
   TYPE_CHECKER["unit"] = check_unit
@@ -882,7 +888,7 @@ FILESTORE_DIR_OPT = cli_option("--file-storage-dir", dest="file_storage_dir",
 
 FILESTORE_DRIVER_OPT = cli_option("--file-driver", dest="file_driver",
                                   help="Driver to use for image files",
-                                  default="loop", metavar="<DRIVER>",
+                                  default=None, metavar="<DRIVER>",
                                   choices=list(constants.FILE_DRIVER))
 
 IALLOCATOR_OPT = cli_option("-I", "--iallocator", metavar="<NAME>",
@@ -964,7 +970,7 @@ SPECS_NIC_COUNT_OPT = cli_option("--specs-nic-count", dest="ispecs_nic_count",
 IPOLICY_BOUNDS_SPECS_STR = "--ipolicy-bounds-specs"
 IPOLICY_BOUNDS_SPECS_OPT = cli_option(IPOLICY_BOUNDS_SPECS_STR,
                                       dest="ipolicy_bounds_specs",
-                                      type="listidentkeyval", default=None,
+                                      type="multilistidentkeyval", default=None,
                                       help="Complete instance specs limits")
 
 IPOLICY_STD_SPECS_STR = "--ipolicy-std-specs"
@@ -1084,12 +1090,12 @@ SHOWCMD_OPT = cli_option("--show-cmd", dest="show_command",
 
 CLEANUP_OPT = cli_option("--cleanup", dest="cleanup",
                          default=False, action="store_true",
-                         help="Instead of performing the migration, try to"
-                         " recover from a failed cleanup. This is safe"
+                         help="Instead of performing the migration/failover,"
+                         " try to recover from a failed cleanup. This is safe"
                          " to run even if the instance is healthy, but it"
                          " will create extra replication traffic and "
                          " disrupt briefly the replication (like during the"
-                         " migration")
+                         " migration/failover")
 
 STATIC_OPT = cli_option("-s", "--static", dest="static",
                         action="store_true", default=False,
@@ -1303,6 +1309,12 @@ NOMODIFY_ETCHOSTS_OPT = cli_option("--no-etc-hosts", dest="modify_etc_hosts",
                                    help="Don't modify %s" % pathutils.ETC_HOSTS,
                                    action="store_false", default=True)
 
+MODIFY_ETCHOSTS_OPT = \
+ cli_option("--modify-etc-hosts", dest="modify_etc_hosts", metavar=_YORNO,
+            default=None, type="bool",
+            help="Defines whether the cluster should autonomously modify"
+            " and keep in sync the /etc/hosts file of the nodes")
+
 NOMODIFY_SSH_SETUP_OPT = cli_option("--no-ssh-init", dest="modify_ssh_setup",
                                     help="Don't initialize SSH keys",
                                     action="store_false", default=True)
@@ -1628,6 +1640,10 @@ INCLUDEDEFAULTS_OPT = cli_option("--include-defaults", dest="include_defaults",
                                  default=False, action="store_true",
                                  help="Include default values")
 
+HOTPLUG_OPT = cli_option("--hotplug", dest="hotplug",
+                         action="store_true", default=False,
+                         help="Hotplug supported devices (NICs and Disks)")
+
 #: Options provided by all commands
 COMMON_OPTS = [DEBUG_OPT, REASON_OPT]
 
@@ -2571,6 +2587,21 @@ def ParseNicOption(optvalue):
   return nics
 
 
+def FixHvParams(hvparams):
+  # In Ganeti 2.8.4 the separator for the usb_devices hvparam was changed from
+  # comma to space because commas cannot be accepted on the command line
+  # (they already act as the separator between different hvparams). Still,
+  # RAPI should be able to accept commas for backwards compatibility.
+  # Therefore, we convert spaces into commas here, and we keep the old
+  # parsing logic everywhere else.
+  try:
+    new_usb_devices = hvparams[constants.HV_USB_DEVICES].replace(" ", ",")
+    hvparams[constants.HV_USB_DEVICES] = new_usb_devices
+  except KeyError:
+    #No usb_devices, no modification required
+    pass
+
+
 def GenericInstanceCreate(mode, opts, args):
   """Add an instance to the cluster via either creation or import.
 
@@ -2660,6 +2691,7 @@ def GenericInstanceCreate(mode, opts, args):
 
   utils.ForceDictType(opts.beparams, constants.BES_PARAMETER_COMPAT)
   utils.ForceDictType(hvparams, constants.HVS_PARAMETER_TYPES)
+  FixHvParams(hvparams)
 
   if mode == constants.INSTANCE_CREATE:
     start = opts.start
@@ -3736,13 +3768,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(
@@ -3751,7 +3794,7 @@ def FormatPolicyInfo(custom_ipolicy, eff_ipolicy, iscluster):
       )
 
   ret.append(
-    ("enabled disk templates",
+    ("allowed disk templates",
      _FormatListInfoDefault(custom_ipolicy.get(constants.IPOLICY_DTS),
                             eff_ipolicy[constants.IPOLICY_DTS]))
     )
@@ -3785,12 +3828,17 @@ def PrintIPolicyCommand(buf, ipolicy, isgroup):
     if stdspecs:
       buf.write(" %s " % IPOLICY_STD_SPECS_STR)
       _PrintSpecsParameters(buf, stdspecs)
-  minmax = ipolicy.get("minmax")
-  if minmax:
+  minmaxes = ipolicy.get("minmax", [])
+  first = True
+  for minmax in minmaxes:
     minspecs = minmax.get("min")
     maxspecs = minmax.get("max")
     if minspecs and maxspecs:
-      buf.write(" %s " % IPOLICY_BOUNDS_SPECS_STR)
+      if first:
+        buf.write(" %s " % IPOLICY_BOUNDS_SPECS_STR)
+        first = False
+      else:
+        buf.write("//")
       buf.write("min:")
       _PrintSpecsParameters(buf, minspecs)
       buf.write("/max:")
@@ -3892,13 +3940,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],
@@ -3933,8 +3982,9 @@ def _ParseISpec(spec, keyname, required):
 
 def _GetISpecsInAllowedValues(minmax_ispecs, allowed_values):
   ret = None
-  if minmax_ispecs and allowed_values and len(minmax_ispecs) == 1:
-    for (key, spec) in minmax_ispecs.items():
+  if (minmax_ispecs and allowed_values and len(minmax_ispecs) == 1 and
+      len(minmax_ispecs[0]) == 1):
+    for (key, spec) in minmax_ispecs[0].items():
       # This loop is executed exactly once
       if key in allowed_values and not spec:
         ret = key
@@ -3947,12 +3997,15 @@ def _InitISpecsFromFullOpts(ipolicy_out, minmax_ispecs, std_ispecs,
   if found_allowed is not None:
     ipolicy_out[constants.ISPECS_MINMAX] = found_allowed
   elif minmax_ispecs is not None:
-    minmax_out = {}
-    for (key, spec) in minmax_ispecs.items():
-      if key not in constants.ISPECS_MINMAX_KEYS:
-        msg = "Invalid key in bounds instance specifications: %s" % key
-        raise errors.OpPrereqError(msg, errors.ECODE_INVAL)
-      minmax_out[key] = _ParseISpec(spec, key, True)
+    minmax_out = []
+    for mmpair in minmax_ispecs:
+      mmpair_out = {}
+      for (key, spec) in mmpair.items():
+        if key not in constants.ISPECS_MINMAX_KEYS:
+          msg = "Invalid key in bounds instance specifications: %s" % key
+          raise errors.OpPrereqError(msg, errors.ECODE_INVAL)
+        mmpair_out[key] = _ParseISpec(spec, key, True)
+      minmax_out.append(mmpair_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