Introduce --hotplug-if-possible option
[ganeti-local] / lib / client / gnt_instance.py
index 8b6be02..e71c0da 100644 (file)
@@ -29,7 +29,6 @@ import copy
 import itertools
 import simplejson
 import logging
-from cStringIO import StringIO
 
 from ganeti.cli import *
 from ganeti import opcodes
@@ -53,21 +52,19 @@ _EXPAND_NODES_SEC_BY_TAGS = "nodes-sec-by-tags"
 _EXPAND_INSTANCES = "instances"
 _EXPAND_INSTANCES_BY_TAGS = "instances-by-tags"
 
-_EXPAND_NODES_TAGS_MODES = frozenset([
+_EXPAND_NODES_TAGS_MODES = compat.UniqueFrozenset([
   _EXPAND_NODES_BOTH_BY_TAGS,
   _EXPAND_NODES_PRI_BY_TAGS,
   _EXPAND_NODES_SEC_BY_TAGS,
   ])
 
-
 #: default list of options for L{ListInstances}
 _LIST_DEF_FIELDS = [
   "name", "hypervisor", "os", "pnode", "status", "oper_ram",
   ]
 
-
 _MISSING = object()
-_ENV_OVERRIDE = frozenset(["list"])
+_ENV_OVERRIDE = compat.UniqueFrozenset(["list"])
 
 _INST_DATA_VAL = ht.TListOf(ht.TDict)
 
@@ -406,6 +403,36 @@ def ReinstallInstance(opts, args):
     return constants.EXIT_FAILURE
 
 
+def SnapshotInstance(opts, args):
+  """Snapshot an instance.
+
+  @param opts: the command line options selected by the user
+  @type args: list
+  @param args: should contain only one element, the name of the
+      instance to be reinstalled
+  @rtype: int
+  @return: the desired exit code
+
+  """
+  instance_name  = args[0]
+  inames = _ExpandMultiNames(_EXPAND_INSTANCES, [instance_name])
+  if not inames:
+    raise errors.OpPrereqError("Selection filter does not match any instances",
+                               errors.ECODE_INVAL)
+  multi_on = len(inames) > 1
+  jex = JobExecutor(verbose=multi_on, opts=opts)
+  for instance_name in inames:
+    op = opcodes.OpInstanceSnapshot(instance_name=instance_name,
+                                    disks=opts.disks)
+    jex.QueueJob(instance_name, op)
+
+  results = jex.WaitOrShow(not opts.submit_only)
+
+  if compat.all(map(compat.fst, results)):
+    return constants.EXIT_SUCCESS
+  else:
+    return constants.EXIT_FAILURE
+
 def RemoveInstance(opts, args):
   """Remove an instance.
 
@@ -432,7 +459,8 @@ def RemoveInstance(opts, args):
 
   op = opcodes.OpInstanceRemove(instance_name=instance_name,
                                 ignore_failures=opts.ignore_failures,
-                                shutdown_timeout=opts.shutdown_timeout)
+                                shutdown_timeout=opts.shutdown_timeout,
+                                keep_disks=opts.keep_disks)
   SubmitOrSend(op, opts, cl=cl)
   return 0
 
@@ -648,6 +676,7 @@ def _ShutdownInstance(name, opts):
 
   """
   return opcodes.OpInstanceShutdown(instance_name=name,
+                                    force=opts.force,
                                     timeout=opts.timeout,
                                     ignore_offline_nodes=opts.ignore_offline,
                                     no_remember=opts.no_remember)
@@ -928,8 +957,8 @@ def _FormatLogicalID(dev_type, logical_id, roman):
                                                             convert=roman))),
       ("nodeB", "%s, minor=%s" % (node_b, compat.TryToRoman(minor_b,
                                                             convert=roman))),
-      ("port", compat.TryToRoman(port, convert=roman)),
-      ("auth key", key),
+      ("port", str(compat.TryToRoman(port, convert=roman))),
+      ("auth key", str(key)),
       ]
   elif dev_type == constants.LD_LV:
     vg_name, lv_name = logical_id
@@ -940,6 +969,10 @@ def _FormatLogicalID(dev_type, logical_id, roman):
   return data
 
 
+def _FormatListInfo(data):
+  return list(str(i) for i in data)
+
+
 def _FormatBlockDevInfo(idx, top_level, dev, roman):
   """Show block device information.
 
@@ -1022,9 +1055,8 @@ def _FormatBlockDevInfo(idx, top_level, dev, roman):
   if isinstance(dev["size"], int):
     nice_size = utils.FormatUnit(dev["size"], "h")
   else:
-    nice_size = dev["size"]
-  d1 = ["- %s: %s, size %s" % (txt, dev["dev_type"], nice_size)]
-  data = []
+    nice_size = str(dev["size"])
+  data = [(txt, "%s, size %s" % (dev["dev_type"], nice_size))]
   if top_level:
     data.append(("access mode", dev["mode"]))
   if dev["logical_id"] is not None:
@@ -1037,8 +1069,7 @@ def _FormatBlockDevInfo(idx, top_level, dev, roman):
     else:
       data.extend(l_id)
   elif dev["physical_id"] is not None:
-    data.append("physical_id:")
-    data.append([dev["physical_id"]])
+    data.append(("physical_id:", _FormatListInfo(dev["physical_id"])))
 
   if dev["pstatus"]:
     data.append(("on primary", helper(dev["dev_type"], dev["pstatus"])))
@@ -1046,41 +1077,126 @@ def _FormatBlockDevInfo(idx, top_level, dev, roman):
   if dev["sstatus"]:
     data.append(("on secondary", helper(dev["dev_type"], dev["sstatus"])))
 
-  if dev["children"]:
-    data.append("child devices:")
-    for c_idx, child in enumerate(dev["children"]):
-      data.append(_FormatBlockDevInfo(c_idx, False, child, roman))
-  d1.append(data)
-  return d1
-
+  data.append(("name", dev["name"]))
+  data.append(("UUID", dev["uuid"]))
 
-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
+  if dev["children"]:
+    data.append(("child devices", [
+      _FormatBlockDevInfo(c_idx, False, child, roman)
+      for c_idx, child in enumerate(dev["children"])
+      ]))
+  return data
 
-  @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 _FormatInstanceNicInfo(idx, nic):
+  """Helper function for L{_FormatInstanceInfo()}"""
+  (name, uuid, ip, mac, mode, link, _, netinfo) = nic
+  network_name = None
+  if netinfo:
+    network_name = netinfo["name"]
+  return [
+    ("nic/%d" % idx, ""),
+    ("MAC", str(mac)),
+    ("IP", str(ip)),
+    ("mode", str(mode)),
+    ("link", str(link)),
+    ("network", str(network_name)),
+    ("UUID", str(uuid)),
+    ("name", str(name)),
+    ]
+
+
+def _FormatInstanceNodesInfo(instance):
+  """Helper function for L{_FormatInstanceInfo()}"""
+  pgroup = ("%s (UUID %s)" %
+            (instance["pnode_group_name"], instance["pnode_group_uuid"]))
+  secs = utils.CommaJoin(("%s (group %s, group UUID %s)" %
+                          (name, group_name, group_uuid))
+                         for (name, group_name, group_uuid) in
+                           zip(instance["snodes"],
+                               instance["snodes_group_names"],
+                               instance["snodes_group_uuids"]))
+  return [
+    [
+      ("primary", instance["pnode"]),
+      ("group", pgroup),
+      ],
+    [("secondaries", secs)],
+    ]
+
+
+def _GetVncConsoleInfo(instance):
+  """Helper function for L{_FormatInstanceInfo()}"""
+  vnc_bind_address = instance["hv_actual"].get(constants.HV_VNC_BIND_ADDRESS,
+                                               None)
+  if vnc_bind_address:
+    port = instance["network_port"]
+    display = int(port) - constants.VNC_BASE_PORT
+    if display > 0 and vnc_bind_address == constants.IP4_ADDRESS_ANY:
+      vnc_console_port = "%s:%s (display %s)" % (instance["pnode"],
+                                                 port,
+                                                 display)
+    elif display > 0 and netutils.IP4Address.IsValid(vnc_bind_address):
+      vnc_console_port = ("%s:%s (node %s) (display %s)" %
+                           (vnc_bind_address, port,
+                            instance["pnode"], display))
+    else:
+      # vnc bind address is a file
+      vnc_console_port = "%s:%s" % (instance["pnode"],
+                                    vnc_bind_address)
+    ret = "vnc to %s" % vnc_console_port
+  else:
+    ret = None
+  return ret
+
+
+def _FormatInstanceInfo(instance, roman_integers):
+  """Format instance information for L{cli.PrintGenericInfo()}"""
+  istate = "configured to be %s" % instance["config_state"]
+  if instance["run_state"]:
+    istate += ", actual state is %s" % instance["run_state"]
+  info = [
+    ("Instance name", instance["name"]),
+    ("UUID", instance["uuid"]),
+    ("Serial number",
+     str(compat.TryToRoman(instance["serial_no"], convert=roman_integers))),
+    ("Creation time", utils.FormatTime(instance["ctime"])),
+    ("Modification time", utils.FormatTime(instance["mtime"])),
+    ("State", istate),
+    ("Nodes", _FormatInstanceNodesInfo(instance)),
+    ("Operating system", instance["os"]),
+    ("Operating system parameters",
+     FormatParamsDictInfo(instance["os_instance"], instance["os_actual"])),
+    ]
+
+  if "network_port" in instance:
+    info.append(("Allocated network port",
+                 str(compat.TryToRoman(instance["network_port"],
+                                       convert=roman_integers))))
+  info.append(("Hypervisor", instance["hypervisor"]))
+  console = _GetVncConsoleInfo(instance)
+  if console:
+    info.append(("console connection", console))
+  # deprecated "memory" value, kept for one version for compatibility
+  # TODO(ganeti 2.7) remove.
+  be_actual = copy.deepcopy(instance["be_actual"])
+  be_actual["memory"] = be_actual[constants.BE_MAXMEM]
+  info.extend([
+    ("Hypervisor parameters",
+     FormatParamsDictInfo(instance["hv_instance"], instance["hv_actual"])),
+    ("Back-end parameters",
+     FormatParamsDictInfo(instance["be_instance"], be_actual)),
+    ("NICs", [
+      _FormatInstanceNicInfo(idx, nic)
+      for (idx, nic) in enumerate(instance["nics"])
+      ]),
+    ("Disk template", instance["disk_template"]),
+    ("Disks", [
+      _FormatBlockDevInfo(idx, True, device, roman_integers)
+      for (idx, device) in enumerate(instance["disks"])
+      ]),
+    ])
+  return info
 
 
 def ShowInstanceConfig(opts, args):
@@ -1111,85 +1227,10 @@ def ShowInstanceConfig(opts, args):
     ToStdout("No instances.")
     return 1
 
-  buf = StringIO()
-  retcode = 0
-  for instance_name in result:
-    instance = result[instance_name]
-    buf.write("Instance name: %s\n" % instance["name"])
-    buf.write("UUID: %s\n" % instance["uuid"])
-    buf.write("Serial number: %s\n" %
-              compat.TryToRoman(instance["serial_no"],
-                                convert=opts.roman_integers))
-    buf.write("Creation time: %s\n" % utils.FormatTime(instance["ctime"]))
-    buf.write("Modification time: %s\n" % utils.FormatTime(instance["mtime"]))
-    buf.write("State: configured to be %s" % instance["config_state"])
-    if instance["run_state"]:
-      buf.write(", actual state is %s" % instance["run_state"])
-    buf.write("\n")
-    ##buf.write("Considered for memory checks in cluster verify: %s\n" %
-    ##          instance["auto_balance"])
-    buf.write("  Nodes:\n")
-    buf.write("    - primary: %s\n" % instance["pnode"])
-    buf.write("      group: %s (UUID %s)\n" %
-              (instance["pnode_group_name"], instance["pnode_group_uuid"]))
-    buf.write("    - secondaries: %s\n" %
-              utils.CommaJoin("%s (group %s, group UUID %s)" %
-                                (name, group_name, group_uuid)
-                              for (name, group_name, group_uuid) in
-                                zip(instance["snodes"],
-                                    instance["snodes_group_names"],
-                                    instance["snodes_group_uuids"])))
-    buf.write("  Operating system: %s\n" % instance["os"])
-    FormatParameterDict(buf, instance["os_instance"], instance["os_actual"],
-                        level=2)
-    if "network_port" in instance:
-      buf.write("  Allocated network port: %s\n" %
-                compat.TryToRoman(instance["network_port"],
-                                  convert=opts.roman_integers))
-    buf.write("  Hypervisor: %s\n" % instance["hypervisor"])
-
-    # custom VNC console information
-    vnc_bind_address = instance["hv_actual"].get(constants.HV_VNC_BIND_ADDRESS,
-                                                 None)
-    if vnc_bind_address:
-      port = instance["network_port"]
-      display = int(port) - constants.VNC_BASE_PORT
-      if display > 0 and vnc_bind_address == constants.IP4_ADDRESS_ANY:
-        vnc_console_port = "%s:%s (display %s)" % (instance["pnode"],
-                                                   port,
-                                                   display)
-      elif display > 0 and netutils.IP4Address.IsValid(vnc_bind_address):
-        vnc_console_port = ("%s:%s (node %s) (display %s)" %
-                             (vnc_bind_address, port,
-                              instance["pnode"], display))
-      else:
-        # vnc bind address is a file
-        vnc_console_port = "%s:%s" % (instance["pnode"],
-                                      vnc_bind_address)
-      buf.write("    - console connection: vnc to %s\n" % vnc_console_port)
-
-    FormatParameterDict(buf, instance["hv_instance"], instance["hv_actual"],
-                        level=2)
-    buf.write("  Hardware:\n")
-    # deprecated "memory" value, kept for one version for compatibility
-    # TODO(ganeti 2.7) remove.
-    be_actual = copy.deepcopy(instance["be_actual"])
-    be_actual["memory"] = be_actual[constants.BE_MAXMEM]
-    FormatParameterDict(buf, instance["be_instance"], be_actual, level=2)
-    # TODO(ganeti 2.7) rework the NICs as well
-    buf.write("    - NICs:\n")
-    for idx, (ip, mac, mode, link, network) in enumerate(instance["nics"]):
-      buf.write("      - nic/%d: MAC: %s, IP: %s,"
-                " mode: %s, link: %s, network: %s\n" %
-                (idx, mac, ip, mode, link, network))
-    buf.write("  Disk template: %s\n" % instance["disk_template"])
-    buf.write("  Disks:\n")
-
-    for idx, device in enumerate(instance["disks"]):
-      _FormatList(buf, _FormatBlockDevInfo(idx, True, device,
-                  opts.roman_integers), 2)
-
-  ToStdout(buf.getvalue().rstrip("\n"))
+  PrintGenericInfo([
+    _FormatInstanceInfo(instance, opts.roman_integers)
+    for instance in result.values()
+    ])
   return retcode
 
 
@@ -1209,23 +1250,17 @@ def _ConvertNicDiskModifications(mods):
   """
   result = []
 
-  for (idx, params) in mods:
-    if idx == constants.DDM_ADD:
+  for (identifier, params) in mods:
+    if identifier == constants.DDM_ADD:
       # Add item as last item (legacy interface)
       action = constants.DDM_ADD
-      idxno = -1
-    elif idx == constants.DDM_REMOVE:
+      identifier = -1
+    elif identifier == constants.DDM_REMOVE:
       # Remove last item (legacy interface)
       action = constants.DDM_REMOVE
-      idxno = -1
+      identifier = -1
     else:
       # Modifications and adding/removing at arbitrary indices
-      try:
-        idxno = int(idx)
-      except (TypeError, ValueError):
-        raise errors.OpPrereqError("Non-numeric index '%s'" % idx,
-                                   errors.ECODE_INVAL)
-
       add = params.pop(constants.DDM_ADD, _MISSING)
       remove = params.pop(constants.DDM_REMOVE, _MISSING)
       modify = params.pop(constants.DDM_MODIFY, _MISSING)
@@ -1253,7 +1288,7 @@ def _ConvertNicDiskModifications(mods):
       raise errors.OpPrereqError("Not accepting parameters on removal",
                                  errors.ECODE_INVAL)
 
-    result.append((action, idxno, params))
+    result.append((action, identifier, params))
 
   return result
 
@@ -1287,7 +1322,8 @@ def SetInstanceParams(opts, args):
   """
   if not (opts.nics or opts.disks or opts.disk_template or
           opts.hvparams or opts.beparams or opts.os or opts.osparams or
-          opts.offline_inst or opts.online_inst or opts.runtime_mem):
+          opts.offline_inst or opts.online_inst or opts.runtime_mem or
+          opts.new_primary_node):
     ToStderr("Please give at least one of the parameters.")
     return 1
 
@@ -1308,6 +1344,14 @@ def SetInstanceParams(opts, args):
                       allowed_values=[constants.VALUE_DEFAULT])
 
   nics = _ConvertNicDiskModifications(opts.nics)
+  for action, _, __ in nics:
+    if action == constants.DDM_MODIFY and opts.hotplug:
+      usertext = ("You are about to hot-modify a NIC. This will be done"
+                  " by removing the exisiting and then adding a new one."
+                  " Network connection might be lost. Continue?")
+      if not AskUser(usertext):
+        return 1
+
   disks = _ParseDiskSizes(_ConvertNicDiskModifications(opts.disks))
 
   if (opts.disk_template and
@@ -1327,8 +1371,12 @@ def SetInstanceParams(opts, args):
   op = opcodes.OpInstanceSetParams(instance_name=args[0],
                                    nics=nics,
                                    disks=disks,
+                                   hotplug=opts.hotplug,
+                                   hotplug_if_possible=opts.hotplug_if_possible,
+                                   keep_disks=opts.keep_disks,
                                    disk_template=opts.disk_template,
                                    remote_node=opts.node,
+                                   pnode=opts.new_primary_node,
                                    hvparams=opts.hvparams,
                                    beparams=opts.beparams,
                                    runtime_mem=opts.runtime_mem,
@@ -1338,6 +1386,7 @@ def SetInstanceParams(opts, args):
                                    force=opts.force,
                                    wait_for_sync=opts.wait_for_sync,
                                    offline=offline,
+                                   conflicts_check=opts.conflicts_check,
                                    ignore_ipolicy=opts.ignore_ipolicy)
 
   # even if here we process the result, we allow submit only
@@ -1347,10 +1396,11 @@ def SetInstanceParams(opts, args):
     ToStdout("Modified instance %s", args[0])
     for param, data in result:
       ToStdout(" - %-5s -> %s", param, data)
-    ToStdout("Please don't forget that most parameters take effect"
-             " only at the next (re)start of the instance initiated by"
-             " ganeti; restarting from within the instance will"
-             " not be enough.")
+    if not opts.hotplug:
+      ToStdout("Please don't forget that most parameters take effect"
+               " only at the next (re)start of the instance initiated by"
+               " ganeti; restarting from within the instance will"
+               " not be enough.")
   return 0
 
 
@@ -1460,7 +1510,7 @@ commands = {
     FailoverInstance, ARGS_ONE_INSTANCE,
     [FORCE_OPT, IGNORE_CONSIST_OPT, SUBMIT_OPT, SHUTDOWN_TIMEOUT_OPT,
      DRY_RUN_OPT, PRIORITY_OPT, DST_NODE_OPT, IALLOCATOR_OPT,
-     IGNORE_IPOLICY_OPT],
+     IGNORE_IPOLICY_OPT, CLEANUP_OPT],
     "[-f] <instance>", "Stops the instance, changes its primary node and"
     " (if it was originally running) starts it on the new node"
     " (the secondary for mirrored instances or any node"
@@ -1505,10 +1555,14 @@ commands = {
      m_pri_node_tags_opt, m_sec_node_tags_opt, m_inst_tags_opt, SELECT_OS_OPT,
      SUBMIT_OPT, DRY_RUN_OPT, PRIORITY_OPT, OSPARAMS_OPT],
     "[-f] <instance>", "Reinstall a stopped instance"),
+  "snapshot": (
+    SnapshotInstance, [ArgInstance(min=1,max=1)],
+    [DISK_OPT, SUBMIT_OPT, DRY_RUN_OPT],
+    "<instance>", "Snapshot an instance's disk(s)"),
   "remove": (
     RemoveInstance, ARGS_ONE_INSTANCE,
     [FORCE_OPT, SHUTDOWN_TIMEOUT_OPT, IGNORE_FAILURES_OPT, SUBMIT_OPT,
-     DRY_RUN_OPT, PRIORITY_OPT],
+     DRY_RUN_OPT, PRIORITY_OPT, KEEPDISKS_OPT],
     "[-f] <instance>", "Shuts down the instance and removes it"),
   "rename": (
     RenameInstance,
@@ -1527,11 +1581,13 @@ commands = {
     [BACKEND_OPT, DISK_OPT, FORCE_OPT, HVOPTS_OPT, NET_OPT, SUBMIT_OPT,
      DISK_TEMPLATE_OPT, SINGLE_NODE_OPT, OS_OPT, FORCE_VARIANT_OPT,
      OSPARAMS_OPT, DRY_RUN_OPT, PRIORITY_OPT, NWSYNC_OPT, OFFLINE_INST_OPT,
-     ONLINE_INST_OPT, IGNORE_IPOLICY_OPT, RUNTIME_MEM_OPT],
+     ONLINE_INST_OPT, IGNORE_IPOLICY_OPT, RUNTIME_MEM_OPT,
+     NOCONFLICTSCHECK_OPT, NEW_PRIMARY_OPT, HOTPLUG_OPT, KEEPDISKS_OPT,
+     HOTPLUG_IF_POSSIBLE_OPT],
     "<instance>", "Alters the parameters of an instance"),
   "shutdown": (
     GenericManyOps("shutdown", _ShutdownInstance), [ArgInstance()],
-    [m_node_opt, m_pri_node_opt, m_sec_node_opt, m_clust_opt,
+    [FORCE_OPT, m_node_opt, m_pri_node_opt, m_sec_node_opt, m_clust_opt,
      m_node_tags_opt, m_pri_node_tags_opt, m_sec_node_tags_opt,
      m_inst_tags_opt, m_inst_opt, m_force_multi, TIMEOUT_OPT, SUBMIT_OPT,
      DRY_RUN_OPT, PRIORITY_OPT, IGNORE_OFFLINE_OPT, NO_REMEMBER_OPT],