Sort the instance names in batcher
[ganeti-local] / scripts / gnt-instance
index 5af186c..80afc41 100755 (executable)
@@ -375,13 +375,12 @@ def BatchCreate(opts, args):
   in the form::
 
     {"instance-name":{
-      "disk_size": 25,
-      "swap_size": 1024,
+      "disk_size": [20480],
       "template": "drbd",
       "backend": {
         "memory": 512,
         "vcpus": 1 },
-      "os": "etch-image",
+      "os": "debootstrap",
       "primary_node": "firstnode",
       "secondary_node": "secondnode",
       "iallocator": "dumb"}
@@ -397,8 +396,7 @@ def BatchCreate(opts, args):
   @return: the desired exit code
 
   """
-  _DEFAULT_SPECS = {"disk_size": 20 * 1024,
-                    "swap_size": 4 * 1024,
+  _DEFAULT_SPECS = {"disk_size": [20 * 1024],
                     "backend": {},
                     "iallocator": None,
                     "primary_node": None,
@@ -449,7 +447,9 @@ def BatchCreate(opts, args):
   # Iterate over the instances and do:
   #  * Populate the specs with default value
   #  * Validate the instance specs
-  for (name, specs) in instance_data.iteritems():
+  i_names = utils.NiceSort(instance_data.keys())
+  for name in i_names:
+    specs = instance_data[name]
     specs = _PopulateWithDefaults(specs)
     _Validate(specs)
 
@@ -458,19 +458,29 @@ def BatchCreate(opts, args):
     if specs['hypervisor']:
       hypervisor, hvparams = specs['hypervisor'].iteritems()
 
+    disks = []
+    for elem in specs['disk_size']:
+      try:
+        size = utils.ParseUnit(elem)
+      except ValueError, err:
+        raise errors.OpPrereqError("Invalid disk size '%s' for"
+                                   " instance %s: %s" %
+                                   (elem, name, err))
+      disks.append({"size": size})
+
+    nic0 = {'ip': specs['ip'], 'bridge': specs['bridge'], 'mac': specs['mac']}
+
     op = opcodes.OpCreateInstance(instance_name=name,
-                                  disk_size=specs['disk_size'],
-                                  swap_size=specs['swap_size'],
+                                  disks=disks,
                                   disk_template=specs['template'],
                                   mode=constants.INSTANCE_CREATE,
                                   os_type=specs['os'],
                                   pnode=specs['primary_node'],
                                   snode=specs['secondary_node'],
-                                  ip=specs['ip'], bridge=specs['bridge'],
+                                  nics=[nic0],
                                   start=specs['start'],
                                   ip_check=specs['ip_check'],
                                   wait_for_sync=True,
-                                  mac=specs['mac'],
                                   iallocator=specs['iallocator'],
                                   hypervisor=hypervisor,
                                   hvparams=hvparams,
@@ -772,16 +782,18 @@ def ReplaceDisks(opts, args):
       disks = [int(i) for i in opts.disks.split(",")]
     except ValueError, err:
       raise errors.OpPrereqError("Invalid disk index passed: %s" % str(err))
-  if opts.on_primary == opts.on_secondary: # no -p or -s passed, or both passed
-    mode = constants.REPLACE_DISK_ALL
-  elif opts.on_primary: # only on primary:
+  cnt = [opts.on_primary, opts.on_secondary,
+         new_2ndary is not None, iallocator is not None].count(True)
+  if cnt != 1:
+    raise errors.OpPrereqError("One and only one of the -p, -s, -n and -i"
+                               " options must be passed")
+  elif opts.on_primary:
     mode = constants.REPLACE_DISK_PRI
-    if new_2ndary is not None or iallocator is not None:
-      raise errors.OpPrereqError("Can't change secondary node on primary disk"
-                                 " replacement")
-  elif opts.on_secondary is not None or iallocator is not None:
-    # only on secondary
+  elif opts.on_secondary:
     mode = constants.REPLACE_DISK_SEC
+  elif new_2ndary is not None or iallocator is not None:
+    # replace secondary
+    mode = constants.REPLACE_DISK_CHG
 
   op = opcodes.OpReplaceDisks(instance_name=args[0], disks=disks,
                               remote_node=new_2ndary, mode=mode,
@@ -819,6 +831,41 @@ def FailoverInstance(opts, args):
   return 0
 
 
+def MigrateInstance(opts, args):
+  """Migrate an instance.
+
+  The migrate is done without shutdown.
+
+  @param opts: the command line options selected by the user
+  @type args: list
+  @param args: should contain only one element, the instance name
+  @rtype: int
+  @return: the desired exit code
+
+  """
+  instance_name = args[0]
+  force = opts.force
+
+  if not force:
+    if opts.cleanup:
+      usertext = ("Instance %s will be recovered from a failed migration."
+                  " Note that the migration procedure (including cleanup)" %
+                  (instance_name,))
+    else:
+      usertext = ("Instance %s will be migrated. Note that migration" %
+                  (instance_name,))
+    usertext += (" is **experimental** in this version."
+                " This might impact the instance if anything goes wrong."
+                " Continue?")
+    if not AskUser(usertext):
+      return 1
+
+  op = opcodes.OpMigrateInstance(instance_name=instance_name, live=opts.live,
+                                 cleanup=opts.cleanup)
+  SubmitOpCode(op)
+  return 0
+
+
 def ConnectToInstanceConsole(opts, args):
   """Connect to the console of an instance.
 
@@ -845,99 +892,161 @@ def ConnectToInstanceConsole(opts, args):
       os._exit(1)
 
 
-def _FormatBlockDevInfo(buf, dev, indent_level, static):
+def _FormatLogicalID(dev_type, logical_id):
+  """Formats the logical_id of a disk.
+
+  """
+  if dev_type == constants.LD_DRBD8:
+    node_a, node_b, port, minor_a, minor_b, key = logical_id
+    data = [
+      ("nodeA", "%s, minor=%s" % (node_a, minor_a)),
+      ("nodeB", "%s, minor=%s" % (node_b, minor_b)),
+      ("port", port),
+      ("auth key", key),
+      ]
+  elif dev_type == constants.LD_LV:
+    vg_name, lv_name = logical_id
+    data = ["%s/%s" % (vg_name, lv_name)]
+  else:
+    data = [str(logical_id)]
+
+  return data
+
+
+def _FormatBlockDevInfo(idx, top_level, dev, static):
   """Show block device information.
 
   This is only used by L{ShowInstanceConfig}, but it's too big to be
   left for an inline definition.
 
-  @type buf: StringIO
-  @param buf: buffer that will accumulate the output
+  @type idx: int
+  @param idx: the index of the current disk
+  @type top_level: boolean
+  @param top_level: if this a top-level disk?
   @type dev: dict
   @param dev: dictionary with disk information
-  @type indent_level: int
-  @param indent_level: the indendation level we are at, used for
-      the layout of the device tree
   @type static: boolean
   @param static: wheter the device information doesn't contain
       runtime information but only static data
+  @return: a list of either strings, tuples or lists
+      (which should be formatted at a higher indent level)
 
   """
-  def helper(buf, dtype, status):
+  def helper(dtype, status):
     """Format one line for physical device status.
 
-    @type buf: StringIO
-    @param buf: buffer that will accumulate the output
     @type dtype: str
     @param dtype: a constant from the L{constants.LDS_BLOCK} set
     @type status: tuple
     @param status: a tuple as returned from L{backend.FindBlockDevice}
+    @return: the string representing the status
 
     """
     if not status:
-      buf.write("not active\n")
+      return "not active"
+    txt = ""
+    (path, major, minor, syncp, estt, degr, ldisk) = status
+    if major is None:
+      major_string = "N/A"
     else:
-      (path, major, minor, syncp, estt, degr, ldisk) = status
-      if major is None:
-        major_string = "N/A"
-      else:
-        major_string = str(major)
+      major_string = str(major)
 
-      if minor is None:
-        minor_string = "N/A"
-      else:
-        minor_string = str(minor)
-
-      buf.write("%s (%s:%s)" % (path, major_string, minor_string))
-      if dtype in (constants.LD_DRBD8, ):
-        if syncp is not None:
-          sync_text = "*RECOVERING* %5.2f%%," % syncp
-          if estt:
-            sync_text += " ETA %ds" % estt
-          else:
-            sync_text += " ETA unknown"
-        else:
-          sync_text = "in sync"
-        if degr:
-          degr_text = "*DEGRADED*"
-        else:
-          degr_text = "ok"
-        if ldisk:
-          ldisk_text = " *MISSING DISK*"
-        else:
-          ldisk_text = ""
-        buf.write(" %s, status %s%s" % (sync_text, degr_text, ldisk_text))
-      elif dtype == constants.LD_LV:
-        if ldisk:
-          ldisk_text = " *FAILED* (failed drive?)"
+    if minor is None:
+      minor_string = "N/A"
+    else:
+      minor_string = str(minor)
+
+    txt += ("%s (%s:%s)" % (path, major_string, minor_string))
+    if dtype in (constants.LD_DRBD8, ):
+      if syncp is not None:
+        sync_text = "*RECOVERING* %5.2f%%," % syncp
+        if estt:
+          sync_text += " ETA %ds" % estt
         else:
-          ldisk_text = ""
-        buf.write(ldisk_text)
-      buf.write("\n")
-
-  if dev["iv_name"] is not None:
-    data = "  - %s, " % dev["iv_name"]
+          sync_text += " ETA unknown"
+      else:
+        sync_text = "in sync"
+      if degr:
+        degr_text = "*DEGRADED*"
+      else:
+        degr_text = "ok"
+      if ldisk:
+        ldisk_text = " *MISSING DISK*"
+      else:
+        ldisk_text = ""
+      txt += (" %s, status %s%s" % (sync_text, degr_text, ldisk_text))
+    elif dtype == constants.LD_LV:
+      if ldisk:
+        ldisk_text = " *FAILED* (failed drive?)"
+      else:
+        ldisk_text = ""
+      txt += ldisk_text
+    return txt
+
+  # the header
+  if top_level:
+    if dev["iv_name"] is not None:
+      txt = dev["iv_name"]
+    else:
+      txt = "disk %d" % idx
   else:
-    data = "  - "
-  data += "access mode: %s, " % dev["mode"]
-  data += "type: %s" % dev["dev_type"]
+    txt = "child %d" % idx
+  d1 = ["- %s: %s" % (txt, dev["dev_type"])]
+  data = []
+  if top_level:
+    data.append(("access mode", dev["mode"]))
   if dev["logical_id"] is not None:
-    data += ", logical_id: %s" % (dev["logical_id"],)
+    try:
+      l_id = _FormatLogicalID(dev["dev_type"], dev["logical_id"])
+    except ValueError:
+      l_id = [str(dev["logical_id"])]
+    if len(l_id) == 1:
+      data.append(("logical_id", l_id[0]))
+    else:
+      data.extend(l_id)
   elif dev["physical_id"] is not None:
-    data += ", physical_id: %s" % (dev["physical_id"],)
-  buf.write("%*s%s\n" % (2*indent_level, "", data))
+    data.append("physical_id:")
+    data.append([dev["physical_id"]])
   if not static:
-    buf.write("%*s    primary:   " % (2*indent_level, ""))
-    helper(buf, dev["dev_type"], dev["pstatus"])
-
+    data.append(("on primary", helper(dev["dev_type"], dev["pstatus"])))
   if dev["sstatus"] and not static:
-    buf.write("%*s    secondary: " % (2*indent_level, ""))
-    helper(buf, dev["dev_type"], dev["sstatus"])
+    data.append(("on secondary", helper(dev["dev_type"], dev["sstatus"])))
 
   if dev["children"]:
-    for child in dev["children"]:
-      _FormatBlockDevInfo(buf, child, indent_level+1, static)
+    data.append("child devices:")
+    for c_idx, child in enumerate(dev["children"]):
+      data.append(_FormatBlockDevInfo(c_idx, False, child, static))
+  d1.append(data)
+  return d1
+
+
+def _FormatList(buf, data, indent_level):
+  """Formats a list of data at a given indent level.
+
+  If the element of the list is:
+    - a string, it is simply formatted as is
+    - a tuple, it will be split into key, value and the all the
+      values in a list will be aligned all at the same start column
+    - a list, will be recursively formatted
 
+  @type buf: StringIO
+  @param buf: the buffer into which we write the output
+  @param data: the list to format
+  @type indent_level: int
+  @param indent_level: the indent level to format at
+
+  """
+  max_tlen = max([len(elem[0]) for elem in data
+                 if isinstance(elem, tuple)] or [0])
+  for elem in data:
+    if isinstance(elem, basestring):
+      buf.write("%*s%s\n" % (2*indent_level, "", elem))
+    elif isinstance(elem, tuple):
+      key, value = elem
+      spacer = "%*s" % (max_tlen - len(key), "")
+      buf.write("%*s%s:%s %s\n" % (2*indent_level, "", key, spacer, value))
+    elif isinstance(elem, list):
+      _FormatList(buf, elem, indent_level+1)
 
 def ShowInstanceConfig(opts, args):
   """Compute instance run-time status.
@@ -1020,10 +1129,10 @@ def ShowInstanceConfig(opts, args):
     for idx, (mac, ip, bridge) in enumerate(instance["nics"]):
       buf.write("      - nic/%d: MAC: %s, IP: %s, bridge: %s\n" %
                 (idx, mac, ip, bridge))
-    buf.write("  Block devices:\n")
+    buf.write("  Disks:\n")
 
-    for device in instance["disks"]:
-      _FormatBlockDevInfo(buf, device, 1, opts.static)
+    for idx, device in enumerate(instance["disks"]):
+      _FormatList(buf, _FormatBlockDevInfo(idx, True, device, opts.static), 2)
 
   ToStdout(buf.getvalue().rstrip('\n'))
   return retcode
@@ -1046,9 +1155,20 @@ def SetInstanceParams(opts, args):
     ToStderr("Please give at least one of the parameters.")
     return 1
 
-  if constants.BE_MEMORY in opts.beparams:
-    opts.beparams[constants.BE_MEMORY] = utils.ParseUnit(
-      opts.beparams[constants.BE_MEMORY])
+  for param in opts.beparams:
+    if opts.beparams[param].lower() == "default":
+      opts.beparams[param] = constants.VALUE_DEFAULT
+    elif opts.beparams[param].lower() == "none":
+      opts.beparams[param] = constants.VALUE_NONE
+    elif param == constants.BE_MEMORY:
+      opts.beparams[constants.BE_MEMORY] = \
+        utils.ParseUnit(opts.beparams[constants.BE_MEMORY])
+
+  for param in opts.hypervisor:
+    if opts.hypervisor[param].lower() == "default":
+      opts.hypervisor[param] = constants.VALUE_DEFAULT
+    elif opts.hypervisor[param].lower() == "none":
+      opts.hypervisor[param] = constants.VALUE_NONE
 
   for idx, (nic_op, nic_dict) in enumerate(opts.nics):
     try:
@@ -1127,12 +1247,6 @@ add_opts = [
   make_option("-n", "--node", dest="node",
               help="Target node and optional secondary node",
               metavar="<pnode>[:<snode>]"),
-  cli_option("-s", "--os-size", dest="size", help="Disk size, in MiB unless"
-             " a suffix is used",
-             default=20 * 1024, type="unit", metavar="<size>"),
-  cli_option("--swap-size", dest="swap", help="Swap size, in MiB unless a"
-             " suffix is used",
-             default=4 * 1024, type="unit", metavar="<size>"),
   os_opt,
   keyval_option("-B", "--backend", dest="beparams",
                 type="keyval", default={},
@@ -1164,7 +1278,7 @@ add_opts = [
               metavar="<DIR>"),
   make_option("--file-driver", dest="file_driver", help="Driver to use"
               " for image files", default="loop", metavar="<DRIVER>"),
-  make_option("--iallocator", metavar="<NAME>",
+  make_option("-I", "--iallocator", metavar="<NAME>",
               help="Select nodes for the instance automatically using the"
               " <NAME> iallocator plugin", default=None, type="string"),
   ikv_option("-H", "--hypervisor", dest="hypervisor",
@@ -1200,6 +1314,26 @@ commands = {
                "[-f] <instance>",
                "Stops the instance and starts it on the backup node, using"
                " the remote mirror (only for instances of type drbd)"),
+  'migrate': (MigrateInstance, ARGS_ONE,
+               [DEBUG_OPT, FORCE_OPT,
+                make_option("--non-live", dest="live",
+                            default=True, action="store_false",
+                            help="Do a non-live migration (this usually means"
+                            " freeze the instance, save the state,"
+                            " transfer and only then resume running on the"
+                            " secondary node)"),
+                make_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"
+                            " to run even if the instance is healthy, but it"
+                            " will create extra replication traffic and "
+                            " disrupt briefly the replication (like during the"
+                            " migration"),
+                ],
+               "[-f] <instance>",
+               "Migrate instance to its secondary node"
+               " (only for instances of type drbd)"),
   'info': (ShowInstanceConfig, ARGS_ANY,
            [DEBUG_OPT,
             make_option("-s", "--static", dest="static",
@@ -1249,7 +1383,8 @@ commands = {
                     [DEBUG_OPT,
                      make_option("-n", "--new-secondary", dest="new_secondary",
                                  help=("New secondary node (for secondary"
-                                       " node change)"), metavar="NODE"),
+                                       " node change)"), metavar="NODE",
+                                 default=None),
                      make_option("-p", "--on-primary", dest="on_primary",
                                  default=False, action="store_true",
                                  help=("Replace the disk(s) on the primary"
@@ -1262,7 +1397,7 @@ commands = {
                                  help=("Comma-separated list of disks"
                                        " to replace (e.g. sda) (optional,"
                                        " defaults to all disks")),
-                     make_option("--iallocator", metavar="<NAME>",
+                     make_option("-i", "--iallocator", metavar="<NAME>",
                                  help="Select new secondary for the instance"
                                  " automatically using the"
                                  " <NAME> iallocator plugin (enables"