Merge branch 'stable-2.2'
[ganeti-local] / lib / cli.py
index 327026d..1db1d45 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2006, 2007 Google Inc.
+# Copyright (C) 2006, 2007, 2008, 2009, 2010 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
@@ -52,6 +52,7 @@ __all__ = [
   "AUTO_PROMOTE_OPT",
   "AUTO_REPLACE_OPT",
   "BACKEND_OPT",
+  "BLK_OS_OPT",
   "CLEANUP_OPT",
   "CLUSTER_DOMAIN_SECRET_OPT",
   "CONFIRM_OPT",
@@ -62,6 +63,7 @@ __all__ = [
   "DISK_OPT",
   "DISK_TEMPLATE_OPT",
   "DRAINED_OPT",
+  "DRY_RUN_OPT",
   "DRBD_HELPER_OPT",
   "EARLY_RELEASE_OPT",
   "ENABLED_HV_OPT",
@@ -72,6 +74,7 @@ __all__ = [
   "FORCE_OPT",
   "FORCE_VARIANT_OPT",
   "GLOBAL_FILEDIR_OPT",
+  "HID_OS_OPT",
   "HVLIST_OPT",
   "HVOPTS_OPT",
   "HYPERVISOR_OPT",
@@ -83,10 +86,12 @@ __all__ = [
   "IGNORE_REMOVE_FAILURES_OPT",
   "IGNORE_SECONDARIES_OPT",
   "IGNORE_SIZE_OPT",
+  "INTERVAL_OPT",
   "MAC_PREFIX_OPT",
   "MAINTAIN_NODE_HEALTH_OPT",
   "MASTER_NETDEV_OPT",
   "MC_OPT",
+  "MIGRATION_MODE_OPT",
   "NET_OPT",
   "NEW_CLUSTER_CERT_OPT",
   "NEW_CLUSTER_DOMAIN_SECRET_OPT",
@@ -96,6 +101,7 @@ __all__ = [
   "NIC_PARAMS_OPT",
   "NODE_LIST_OPT",
   "NODE_PLACEMENT_OPT",
+  "NODEGROUP_OPT",
   "NODRBD_STORAGE_OPT",
   "NOHDR_OPT",
   "NOIPCHECK_OPT",
@@ -118,11 +124,14 @@ __all__ = [
   "OSPARAMS_OPT",
   "OS_OPT",
   "OS_SIZE_OPT",
+  "PRIMARY_IP_VERSION_OPT",
+  "PRIORITY_OPT",
   "RAPI_CERT_OPT",
   "READD_OPT",
   "REBOOT_TYPE_OPT",
   "REMOVE_INSTANCE_OPT",
   "REMOVE_UIDS_OPT",
+  "RESERVED_LVS_OPT",
   "ROMAN_OPT",
   "SECONDARY_IP_OPT",
   "SELECT_OS_OPT",
@@ -188,15 +197,29 @@ __all__ = [
   "OPT_COMPL_ONE_IALLOCATOR",
   "OPT_COMPL_ONE_INSTANCE",
   "OPT_COMPL_ONE_NODE",
+  "OPT_COMPL_ONE_NODEGROUP",
   "OPT_COMPL_ONE_OS",
   "cli_option",
   "SplitNodeOption",
   "CalculateOSNames",
+  "ParseFields",
   ]
 
 NO_PREFIX = "no_"
 UN_PREFIX = "-"
 
+#: Priorities (sorted)
+_PRIORITY_NAMES = [
+  ("low", constants.OP_PRIO_LOW),
+  ("normal", constants.OP_PRIO_NORMAL),
+  ("high", constants.OP_PRIO_HIGH),
+  ]
+
+#: Priority dictionary for easier lookup
+# TODO: Replace this and _PRIORITY_NAMES with a single sorted dictionary once
+# we migrate to Python 2.6
+_PRIONAME_TO_VALUE = dict(_PRIORITY_NAMES)
+
 
 class _Argument:
   def __init__(self, min=0, max=None): # pylint: disable-msg=W0622
@@ -370,7 +393,7 @@ def AddTags(opts, args):
   if not args:
     raise errors.OpPrereqError("No tags to be added")
   op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
-  SubmitOpCode(op)
+  SubmitOpCode(op, opts=opts)
 
 
 def RemoveTags(opts, args):
@@ -387,7 +410,7 @@ def RemoveTags(opts, args):
   if not args:
     raise errors.OpPrereqError("No tags to be removed")
   op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
-  SubmitOpCode(op)
+  SubmitOpCode(op, opts=opts)
 
 
 def check_unit(option, opt, value): # pylint: disable-msg=W0613
@@ -496,7 +519,8 @@ def check_bool(option, opt, value): # pylint: disable-msg=W0613
  OPT_COMPL_ONE_INSTANCE,
  OPT_COMPL_ONE_OS,
  OPT_COMPL_ONE_IALLOCATOR,
- OPT_COMPL_INST_ADD_NODES) = range(100, 106)
+ OPT_COMPL_INST_ADD_NODES,
+ OPT_COMPL_ONE_NODEGROUP) = range(100, 107)
 
 OPT_COMPL_ALL = frozenset([
   OPT_COMPL_MANY_NODES,
@@ -505,6 +529,7 @@ OPT_COMPL_ALL = frozenset([
   OPT_COMPL_ONE_OS,
   OPT_COMPL_ONE_IALLOCATOR,
   OPT_COMPL_INST_ADD_NODES,
+  OPT_COMPL_ONE_NODEGROUP,
   ])
 
 
@@ -573,11 +598,11 @@ SYNC_OPT = cli_option("--sync", dest="do_locking",
                       help=("Grab locks while doing the queries"
                             " in order to ensure more consistent results"))
 
-_DRY_RUN_OPT = cli_option("--dry-run", default=False,
-                          action="store_true",
-                          help=("Do not execute the operation, just run the"
-                                " check steps and verify it it could be"
-                                " executed"))
+DRY_RUN_OPT = cli_option("--dry-run", default=False,
+                         action="store_true",
+                         help=("Do not execute the operation, just run the"
+                               " check steps and verify it it could be"
+                               " executed"))
 
 VERBOSE_OPT = cli_option("-v", "--verbose", default=False,
                          action="store_true",
@@ -698,6 +723,12 @@ NONLIVE_OPT = cli_option("--non-live", dest="live",
                          " freeze the instance, save the state, transfer and"
                          " only then resume running on the secondary node)")
 
+MIGRATION_MODE_OPT = cli_option("--migration-mode", dest="migration_mode",
+                                default=None,
+                                choices=list(constants.HT_MIGRATION_MODES),
+                                help="Override default migration mode (choose"
+                                " either live or non-live")
+
 NODE_PLACEMENT_OPT = cli_option("-n", "--node", dest="node",
                                 help="Target node and optional secondary node",
                                 metavar="<pnode>[:<snode>]",
@@ -709,6 +740,13 @@ NODE_LIST_OPT = cli_option("-n", "--node", dest="nodes", default=[],
                            " times, if not given defaults to all nodes)",
                            completion_suggest=OPT_COMPL_ONE_NODE)
 
+NODEGROUP_OPT = cli_option("-g", "--nodegroup",
+                           dest="nodegroup",
+                           help="Node group (name or uuid)",
+                           metavar="<nodegroup>",
+                           default=None, type="string",
+                           completion_suggest=OPT_COMPL_ONE_NODEGROUP)
+
 SINGLE_NODE_OPT = cli_option("-n", "--node", dest="node", help="Target node",
                              metavar="<node>",
                              completion_suggest=OPT_COMPL_ONE_NODE)
@@ -919,6 +957,11 @@ SHUTDOWN_TIMEOUT_OPT = cli_option("--shutdown-timeout",
                          default=constants.DEFAULT_SHUTDOWN_TIMEOUT,
                          help="Maximum time to wait for instance shutdown")
 
+INTERVAL_OPT = cli_option("--interval", dest="interval", type="int",
+                          default=None,
+                          help=("Number of seconds between repetions of the"
+                                " command"))
+
 EARLY_RELEASE_OPT = cli_option("--early-release",
                                dest="early_release", default=False,
                                action="store_true",
@@ -994,6 +1037,12 @@ REMOVE_UIDS_OPT = cli_option("--remove-uids", default=None,
                                    " ranges separated by commas, to be"
                                    " removed from the user-id pool"))
 
+RESERVED_LVS_OPT = cli_option("--reserved-lvs", default=None,
+                             action="store", dest="reserved_lvs",
+                             help=("A comma-separated list of reserved"
+                                   " logical volumes names, that will be"
+                                   " ignored by cluster verify"))
+
 ROMAN_OPT = cli_option("--roman",
                        dest="roman_integers", default=False,
                        action="store_true",
@@ -1007,6 +1056,30 @@ NODRBD_STORAGE_OPT = cli_option("--no-drbd-storage", dest="drbd_storage",
                                 action="store_false", default=True,
                                 help="Disable support for DRBD")
 
+PRIMARY_IP_VERSION_OPT = \
+    cli_option("--primary-ip-version", default=constants.IP4_VERSION,
+               action="store", dest="primary_ip_version",
+               metavar="%d|%d" % (constants.IP4_VERSION,
+                                  constants.IP6_VERSION),
+               help="Cluster-wide IP version for primary IP")
+
+PRIORITY_OPT = cli_option("--priority", default=None, dest="priority",
+                          metavar="|".join(name for name, _ in _PRIORITY_NAMES),
+                          choices=_PRIONAME_TO_VALUE.keys(),
+                          help="Priority for opcode processing")
+
+HID_OS_OPT = cli_option("--hidden", dest="hidden",
+                        type="bool", default=None, metavar=_YORNO,
+                        help="Sets the hidden flag on the OS")
+
+BLK_OS_OPT = cli_option("--blacklisted", dest="blacklisted",
+                        type="bool", default=None, metavar=_YORNO,
+                        help="Sets the blacklisted flag on the OS")
+
+
+#: Options provided by all commands
+COMMON_OPTS = [DEBUG_OPT]
+
 
 def _ParseArgs(argv, commands, aliases):
   """Parser for the command line arguments.
@@ -1026,7 +1099,8 @@ def _ParseArgs(argv, commands, aliases):
     binary = argv[0].split("/")[-1]
 
   if len(argv) > 1 and argv[1] == "--version":
-    ToStdout("%s (ganeti) %s", binary, constants.RELEASE_VERSION)
+    ToStdout("%s (ganeti %s) %s", binary, constants.VCS_VERSION,
+             constants.RELEASE_VERSION)
     # Quit right away. That way we don't have to care about this special
     # argument. optparse.py does it the same.
     sys.exit(0)
@@ -1073,7 +1147,7 @@ def _ParseArgs(argv, commands, aliases):
     cmd = aliases[cmd]
 
   func, args_def, parser_opts, usage, description = commands[cmd]
-  parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT, DEBUG_OPT],
+  parser = OptionParser(option_list=parser_opts + COMMON_OPTS,
                         description=description,
                         formatter=TitledHelpFormatter(),
                         usage="%%prog %s %s" % (cmd, usage))
@@ -1184,14 +1258,25 @@ def CalculateOSNames(os_name, os_variants):
     return [os_name]
 
 
-def UsesRPC(fn):
-  def wrapper(*args, **kwargs):
-    rpc.Init()
-    try:
-      return fn(*args, **kwargs)
-    finally:
-      rpc.Shutdown()
-  return wrapper
+def ParseFields(selected, default):
+  """Parses the values of "--field"-like options.
+
+  @type selected: string or None
+  @param selected: User-selected options
+  @type default: list
+  @param default: Default fields
+
+  """
+  if selected is None:
+    return default
+
+  if selected.startswith("+"):
+    return default + selected[1:].split(",")
+
+  return selected.split(",")
+
+
+UsesRPC = rpc.RunWithRPC
 
 
 def AskUser(text, choices=None):
@@ -1503,6 +1588,7 @@ def FormatLogMessage(log_type, log_msg):
   return utils.SafeEncode(log_msg)
 
 
+def PollJob(job_id, cl=None, feedback_fn=None, reporter=None):
   """Function to poll for the result of a job.
 
   @type job_id: job identified
@@ -1515,15 +1601,18 @@ def FormatLogMessage(log_type, log_msg):
   if cl is None:
     cl = GetClient()
 
-  if feedback_fn:
-    reporter = FeedbackFnJobPollReportCb(feedback_fn)
-  else:
-    reporter = StdioJobPollReportCb()
+  if reporter is None:
+    if feedback_fn:
+      reporter = FeedbackFnJobPollReportCb(feedback_fn)
+    else:
+      reporter = StdioJobPollReportCb()
+  elif feedback_fn:
+    raise errors.ProgrammerError("Can't specify reporter and feedback function")
 
   return GenericPollJob(job_id, _LuxiJobPollCb(cl), reporter)
 
 
-def SubmitOpCode(op, cl=None, feedback_fn=None, opts=None):
+def SubmitOpCode(op, cl=None, feedback_fn=None, opts=None, reporter=None):
   """Legacy function to submit an opcode.
 
   This is just a simple wrapper over the construction of the processor
@@ -1536,9 +1625,10 @@ def SubmitOpCode(op, cl=None, feedback_fn=None, opts=None):
 
   SetGenericOpcodeOpts([op], opts)
 
-  job_id = SendJob([op], cl)
+  job_id = SendJob([op], cl=cl)
 
-  op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
+  op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn,
+                       reporter=reporter)
 
   return op_results[0]
 
@@ -1578,8 +1668,11 @@ def SetGenericOpcodeOpts(opcode_list, options):
   if not options:
     return
   for op in opcode_list:
-    op.dry_run = options.dry_run
     op.debug_level = options.debug
+    if hasattr(options, "dry_run"):
+      op.dry_run = options.dry_run
+    if getattr(options, "priority", None) is not None:
+      op.priority = _PRIONAME_TO_VALUE[options.priority]
 
 
 def GetClient():
@@ -1635,7 +1728,7 @@ def FormatError(err):
   elif isinstance(err, errors.HooksFailure):
     obuf.write("Failure: hooks general failure: %s" % msg)
   elif isinstance(err, errors.ResolverError):
-    this_host = netutils.HostInfo.SysName()
+    this_host = netutils.Hostname.GetSysName()
     if err.args[0] == this_host:
       msg = "Failure: can't resolve my own hostname ('%s')"
     else:
@@ -1669,9 +1762,14 @@ def FormatError(err):
   elif isinstance(err, luxi.TimeoutError):
     obuf.write("Timeout while talking to the master daemon. Error:\n"
                "%s" % msg)
+  elif isinstance(err, luxi.PermissionError):
+    obuf.write("It seems you don't have permissions to connect to the"
+               " master daemon.\nPlease retry as a different user.")
   elif isinstance(err, luxi.ProtocolError):
     obuf.write("Unhandled protocol error while talking to the master daemon:\n"
                "%s" % msg)
+  elif isinstance(err, errors.JobLost):
+    obuf.write("Error checking job status: %s" % msg)
   elif isinstance(err, errors.GenericError):
     obuf.write("Unhandled Ganeti error: %s" % msg)
   elif isinstance(err, JobSubmittedException):
@@ -1742,6 +1840,30 @@ def GenericMain(commands, override=None, aliases=None):
   return result
 
 
+def ParseNicOption(optvalue):
+  """Parses the value of the --net option(s).
+
+  """
+  try:
+    nic_max = max(int(nidx[0]) + 1 for nidx in optvalue)
+  except (TypeError, ValueError), err:
+    raise errors.OpPrereqError("Invalid NIC index passed: %s" % str(err))
+
+  nics = [{}] * nic_max
+  for nidx, ndict in optvalue:
+    nidx = int(nidx)
+
+    if not isinstance(ndict, dict):
+      raise errors.OpPrereqError("Invalid nic/%d value: expected dict,"
+                                 " got %s" % (nidx, ndict))
+
+    utils.ForceDictType(ndict, constants.INIC_PARAMS_TYPES)
+
+    nics[nidx] = ndict
+
+  return nics
+
+
 def GenericInstanceCreate(mode, opts, args):
   """Add an instance to the cluster via either creation or import.
 
@@ -1763,17 +1885,7 @@ def GenericInstanceCreate(mode, opts, args):
     hypervisor, hvparams = opts.hypervisor
 
   if opts.nics:
-    try:
-      nic_max = max(int(nidx[0]) + 1 for nidx in opts.nics)
-    except ValueError, err:
-      raise errors.OpPrereqError("Invalid NIC index passed: %s" % str(err))
-    nics = [{}] * nic_max
-    for nidx, ndict in opts.nics:
-      nidx = int(nidx)
-      if not isinstance(ndict, dict):
-        msg = "Invalid nic/%d value: expected dict, got %s" % (nidx, ndict)
-        raise errors.OpPrereqError(msg)
-      nics[nidx] = ndict
+    nics = ParseNicOption(opts.nics)
   elif opts.no_nics:
     # no nics
     nics = []
@@ -2310,12 +2422,13 @@ class JobExecutor(object):
     assert result
 
     for job_data, status in zip(self.jobs, result):
-      if status[0] in (constants.JOB_STATUS_QUEUED,
-                    constants.JOB_STATUS_WAITLOCK,
-                    constants.JOB_STATUS_CANCELING):
-        # job is still waiting
+      if (isinstance(status, list) and status and
+          status[0] in (constants.JOB_STATUS_QUEUED,
+                        constants.JOB_STATUS_WAITLOCK,
+                        constants.JOB_STATUS_CANCELING)):
+        # job is still present and waiting
         continue
-      # good candidate found
+      # good candidate found (either running job or lost job)
       self.jobs.remove(job_data)
       return job_data
 
@@ -2351,6 +2464,11 @@ class JobExecutor(object):
       try:
         job_result = PollJob(jid, cl=self.cl, feedback_fn=self.feedback_fn)
         success = True
+      except errors.JobLost, err:
+        _, job_result = FormatError(err)
+        ToStderr("Job %s for %s has been archived, cannot check its result",
+                 jid, name)
+        success = False
       except (errors.GenericError, luxi.ProtocolError), err:
         _, job_result = FormatError(err)
         success = False