Unify the “--no-ip-check” option
[ganeti-local] / lib / cli.py
index ab3a1d5..c81bf54 100644 (file)
@@ -38,27 +38,168 @@ from ganeti import luxi
 from ganeti import ssconf
 from ganeti import rpc
 
-from optparse import (OptionParser, make_option, TitledHelpFormatter,
+from optparse import (OptionParser, TitledHelpFormatter,
                       Option, OptionValueError)
 
 
-__all__ = ["DEBUG_OPT", "NOHDR_OPT", "SEP_OPT", "GenericMain",
-           "SubmitOpCode", "GetClient",
-           "cli_option",
-           "GenerateTable", "AskUser",
-           "ARGS_NONE", "ARGS_FIXED", "ARGS_ATLEAST", "ARGS_ANY", "ARGS_ONE",
-           "USEUNITS_OPT", "FIELDS_OPT", "FORCE_OPT", "SUBMIT_OPT",
-           "ListTags", "AddTags", "RemoveTags", "TAG_SRC_OPT",
-           "FormatError", "SplitNodeOption", "SubmitOrSend",
-           "JobSubmittedException", "FormatTimestamp", "ParseTimespec",
-           "ToStderr", "ToStdout", "UsesRPC",
-           "GetOnlineNodes", "JobExecutor", "SYNC_OPT", "CONFIRM_OPT",
-           ]
+__all__ = [
+  # Command line options
+  "BACKEND_OPT",
+  "CONFIRM_OPT",
+  "DEBUG_OPT",
+  "DEBUG_SIMERR_OPT",
+  "DISK_TEMPLATE_OPT",
+  "FIELDS_OPT",
+  "FILESTORE_DIR_OPT",
+  "FILESTORE_DRIVER_OPT",
+  "HVLIST_OPT",
+  "HVOPTS_OPT",
+  "HYPERVISOR_OPT",
+  "IALLOCATOR_OPT",
+  "FORCE_OPT",
+  "NOHDR_OPT",
+  "NOIPCHECK_OPT",
+  "NONICS_OPT",
+  "NWSYNC_OPT",
+  "OS_OPT",
+  "SEP_OPT",
+  "SUBMIT_OPT",
+  "SYNC_OPT",
+  "TAG_SRC_OPT",
+  "USEUNITS_OPT",
+  "VERBOSE_OPT",
+  # Generic functions for CLI programs
+  "GenericMain",
+  "GetClient",
+  "GetOnlineNodes",
+  "JobExecutor",
+  "JobSubmittedException",
+  "ParseTimespec",
+  "SubmitOpCode",
+  "SubmitOrSend",
+  "UsesRPC",
+  # Formatting functions
+  "ToStderr", "ToStdout",
+  "FormatError",
+  "GenerateTable",
+  "AskUser",
+  "FormatTimestamp",
+  # Tags functions
+  "ListTags",
+  "AddTags",
+  "RemoveTags",
+  # command line options support infrastructure
+  "ARGS_MANY_INSTANCES",
+  "ARGS_MANY_NODES",
+  "ARGS_NONE",
+  "ARGS_ONE_INSTANCE",
+  "ARGS_ONE_NODE",
+  "ArgChoice",
+  "ArgCommand",
+  "ArgFile",
+  "ArgHost",
+  "ArgInstance",
+  "ArgJobId",
+  "ArgNode",
+  "ArgSuggest",
+  "ArgUnknown",
+  "OPT_COMPL_INST_ADD_NODES",
+  "OPT_COMPL_MANY_NODES",
+  "OPT_COMPL_ONE_IALLOCATOR",
+  "OPT_COMPL_ONE_INSTANCE",
+  "OPT_COMPL_ONE_NODE",
+  "OPT_COMPL_ONE_OS",
+  "cli_option",
+  "SplitNodeOption",
+  ]
 
 NO_PREFIX = "no_"
 UN_PREFIX = "-"
 
 
+class _Argument:
+  def __init__(self, min=0, max=None):
+    self.min = min
+    self.max = max
+
+  def __repr__(self):
+    return ("<%s min=%s max=%s>" %
+            (self.__class__.__name__, self.min, self.max))
+
+
+class ArgSuggest(_Argument):
+  """Suggesting argument.
+
+  Value can be any of the ones passed to the constructor.
+
+  """
+  def __init__(self, min=0, max=None, choices=None):
+    _Argument.__init__(self, min=min, max=max)
+    self.choices = choices
+
+  def __repr__(self):
+    return ("<%s min=%s max=%s choices=%r>" %
+            (self.__class__.__name__, self.min, self.max, self.choices))
+
+
+class ArgChoice(ArgSuggest):
+  """Choice argument.
+
+  Value can be any of the ones passed to the constructor. Like L{ArgSuggest},
+  but value must be one of the choices.
+
+  """
+
+
+class ArgUnknown(_Argument):
+  """Unknown argument to program (e.g. determined at runtime).
+
+  """
+
+
+class ArgInstance(_Argument):
+  """Instances argument.
+
+  """
+
+
+class ArgNode(_Argument):
+  """Node argument.
+
+  """
+
+class ArgJobId(_Argument):
+  """Job ID argument.
+
+  """
+
+
+class ArgFile(_Argument):
+  """File path argument.
+
+  """
+
+
+class ArgCommand(_Argument):
+  """Command argument.
+
+  """
+
+
+class ArgHost(_Argument):
+  """Host argument.
+
+  """
+
+
+ARGS_NONE = []
+ARGS_MANY_INSTANCES = [ArgInstance()]
+ARGS_MANY_NODES = [ArgNode()]
+ARGS_ONE_INSTANCE = [ArgInstance(min=1, max=1)]
+ARGS_ONE_NODE = [ArgNode(min=1, max=1)]
+
+
+
 def _ExtractTagsObject(opts, args):
   """Extract the tag type object.
 
@@ -161,68 +302,6 @@ def RemoveTags(opts, args):
   SubmitOpCode(op)
 
 
-DEBUG_OPT = make_option("-d", "--debug", default=False,
-                        action="store_true",
-                        help="Turn debugging on")
-
-NOHDR_OPT = make_option("--no-headers", default=False,
-                        action="store_true", dest="no_headers",
-                        help="Don't display column headers")
-
-SEP_OPT = make_option("--separator", default=None,
-                      action="store", dest="separator",
-                      help="Separator between output fields"
-                      " (defaults to one space)")
-
-USEUNITS_OPT = make_option("--units", default=None,
-                           dest="units", choices=('h', 'm', 'g', 't'),
-                           help="Specify units for output (one of hmgt)")
-
-FIELDS_OPT = make_option("-o", "--output", dest="output", action="store",
-                         type="string", help="Comma separated list of"
-                         " output fields",
-                         metavar="FIELDS")
-
-FORCE_OPT = make_option("-f", "--force", dest="force", action="store_true",
-                        default=False, help="Force the operation")
-
-CONFIRM_OPT = make_option("--yes", dest="confirm", action="store_true",
-                          default=False, help="Do not require confirmation")
-
-TAG_SRC_OPT = make_option("--from", dest="tags_source",
-                          default=None, help="File with tag names")
-
-SUBMIT_OPT = make_option("--submit", dest="submit_only",
-                         default=False, action="store_true",
-                         help="Submit the job and return the job ID, but"
-                         " don't wait for the job to finish")
-
-SYNC_OPT = make_option("--sync", dest="do_locking",
-                       default=False, action="store_true",
-                       help="Grab locks while doing the queries"
-                       " in order to ensure more consistent results")
-
-_DRY_RUN_OPT = make_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")
-
-
-def ARGS_FIXED(val):
-  """Macro-like function denoting a fixed number of arguments"""
-  return -val
-
-
-def ARGS_ATLEAST(val):
-  """Macro-like function denoting a minimum number of arguments"""
-  return val
-
-
-ARGS_NONE = None
-ARGS_ONE = ARGS_FIXED(1)
-ARGS_ANY = ARGS_ATLEAST(0)
-
-
 def check_unit(option, opt, value):
   """OptParsers custom converter for units.
 
@@ -307,10 +386,32 @@ def check_key_val(option, opt, value):
   return _SplitKeyVal(opt, value)
 
 
+# completion_suggestion is normally a list. Using numeric values not evaluating
+# to False for dynamic completion.
+(OPT_COMPL_MANY_NODES,
+ OPT_COMPL_ONE_NODE,
+ OPT_COMPL_ONE_INSTANCE,
+ OPT_COMPL_ONE_OS,
+ OPT_COMPL_ONE_IALLOCATOR,
+ OPT_COMPL_INST_ADD_NODES) = range(100, 106)
+
+OPT_COMPL_ALL = frozenset([
+  OPT_COMPL_MANY_NODES,
+  OPT_COMPL_ONE_NODE,
+  OPT_COMPL_ONE_INSTANCE,
+  OPT_COMPL_ONE_OS,
+  OPT_COMPL_ONE_IALLOCATOR,
+  OPT_COMPL_INST_ADD_NODES,
+  ])
+
+
 class CliOption(Option):
   """Custom option class for optparse.
 
   """
+  ATTRS = Option.ATTRS + [
+    "completion_suggest",
+    ]
   TYPES = Option.TYPES + (
     "identkeyval",
     "keyval",
@@ -326,6 +427,120 @@ class CliOption(Option):
 cli_option = CliOption
 
 
+DEBUG_OPT = cli_option("-d", "--debug", default=False,
+                       action="store_true",
+                       help="Turn debugging on")
+
+NOHDR_OPT = cli_option("--no-headers", default=False,
+                       action="store_true", dest="no_headers",
+                       help="Don't display column headers")
+
+SEP_OPT = cli_option("--separator", default=None,
+                     action="store", dest="separator",
+                     help=("Separator between output fields"
+                           " (defaults to one space)"))
+
+USEUNITS_OPT = cli_option("--units", default=None,
+                          dest="units", choices=('h', 'm', 'g', 't'),
+                          help="Specify units for output (one of hmgt)")
+
+FIELDS_OPT = cli_option("-o", "--output", dest="output", action="store",
+                        type="string", metavar="FIELDS",
+                        help="Comma separated list of output fields")
+
+FORCE_OPT = cli_option("-f", "--force", dest="force", action="store_true",
+                       default=False, help="Force the operation")
+
+CONFIRM_OPT = cli_option("--yes", dest="confirm", action="store_true",
+                         default=False, help="Do not require confirmation")
+
+TAG_SRC_OPT = cli_option("--from", dest="tags_source",
+                         default=None, help="File with tag names")
+
+SUBMIT_OPT = cli_option("--submit", dest="submit_only",
+                        default=False, action="store_true",
+                        help=("Submit the job and return the job ID, but"
+                              " don't wait for the job to finish"))
+
+SYNC_OPT = cli_option("--sync", dest="do_locking",
+                      default=False, action="store_true",
+                      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"))
+
+VERBOSE_OPT = cli_option("-v", "--verbose", default=False,
+                         action="store_true",
+                         help="Increase the verbosity of the operation")
+
+DEBUG_SIMERR_OPT = cli_option("--debug-simulate-errors", default=False,
+                              action="store_true", dest="simulate_errors",
+                              help="Debugging option that makes the operation"
+                              " treat most runtime checks as failed")
+
+NWSYNC_OPT = cli_option("--no-wait-for-sync", dest="wait_for_sync",
+                        default=True, action="store_false",
+                        help="Don't wait for sync (DANGEROUS!)")
+
+DISK_TEMPLATE_OPT = cli_option("-t", "--disk-template", dest="disk_template",
+                               help="Custom disk setup (diskless, file,"
+                               " plain or drbd)",
+                               default=None, metavar="TEMPL",
+                               choices=list(constants.DISK_TEMPLATES))
+
+NONICS_OPT = cli_option("--no-nics", default=False, action="store_true",
+                        help="Do not create any network cards for"
+                        " the instance")
+
+FILESTORE_DIR_OPT = cli_option("--file-storage-dir", dest="file_storage_dir",
+                               help="Relative path under default cluster-wide"
+                               " file storage dir to store file-based disks",
+                               default=None, metavar="<DIR>")
+
+FILESTORE_DRIVER_OPT = cli_option("--file-driver", dest="file_driver",
+                                  help="Driver to use for image files",
+                                  default="loop", metavar="<DRIVER>",
+                                  choices=list(constants.FILE_DRIVER))
+
+IALLOCATOR_OPT = cli_option("-I", "--iallocator", metavar="<NAME>",
+                            help="Select nodes for the instance automatically"
+                            " using the <NAME> iallocator plugin",
+                            default=None, type="string",
+                            completion_suggest=OPT_COMPL_ONE_IALLOCATOR)
+
+OS_OPT = cli_option("-o", "--os-type", dest="os", help="What OS to run",
+                    metavar="<os>",
+                    completion_suggest=OPT_COMPL_ONE_OS)
+
+BACKEND_OPT = cli_option("-B", "--backend-parameters", dest="beparams",
+                         type="keyval", default={},
+                         help="Backend parameters")
+
+HVOPTS_OPT =  cli_option("-H", "--hypervisor-parameters", type="keyval",
+                         default={}, dest="hvparams",
+                         help="Hypervisor parameters")
+
+HYPERVISOR_OPT = cli_option("-H", "--hypervisor-parameters", dest="hypervisor",
+                            help="Hypervisor and hypervisor options, in the"
+                            " format hypervisor:option=value,option=value,...",
+                            default=None, type="identkeyval")
+
+HVLIST_OPT = cli_option("-H", "--hypervisor-parameters", dest="hvparams",
+                        help="Hypervisor and hypervisor options, in the"
+                        " format hypervisor:option=value,option=value,...",
+                        default=[], action="append", type="identkeyval")
+
+NOIPCHECK_OPT = cli_option("--no-ip-check", dest="ip_check", default=True,
+                           action="store_false",
+                           help="Don't check that the instance's IP"
+                           " is alive")
+
+
+
 def _ParseArgs(argv, commands, aliases):
   """Parser for the command line arguments.
 
@@ -390,27 +605,91 @@ def _ParseArgs(argv, commands, aliases):
 
     cmd = aliases[cmd]
 
-  func, nargs, parser_opts, usage, description = commands[cmd]
+  func, args_def, parser_opts, usage, description = commands[cmd]
   parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT],
                         description=description,
                         formatter=TitledHelpFormatter(),
                         usage="%%prog %s %s" % (cmd, usage))
   parser.disable_interspersed_args()
   options, args = parser.parse_args()
-  if nargs is None:
-    if len(args) != 0:
-      ToStderr("Error: Command %s expects no arguments", cmd)
-      return None, None, None
-  elif nargs < 0 and len(args) != -nargs:
-    ToStderr("Error: Command %s expects %d argument(s)", cmd, -nargs)
-    return None, None, None
-  elif nargs >= 0 and len(args) < nargs:
-    ToStderr("Error: Command %s expects at least %d argument(s)", cmd, nargs)
+
+  if not _CheckArguments(cmd, args_def, args):
     return None, None, None
 
   return func, options, args
 
 
+def _CheckArguments(cmd, args_def, args):
+  """Verifies the arguments using the argument definition.
+
+  Algorithm:
+
+    1. Abort with error if values specified by user but none expected.
+
+    1. For each argument in definition
+
+      1. Keep running count of minimum number of values (min_count)
+      1. Keep running count of maximum number of values (max_count)
+      1. If it has an unlimited number of values
+
+        1. Abort with error if it's not the last argument in the definition
+
+    1. If last argument has limited number of values
+
+      1. Abort with error if number of values doesn't match or is too large
+
+    1. Abort with error if user didn't pass enough values (min_count)
+
+  """
+  if args and not args_def:
+    ToStderr("Error: Command %s expects no arguments", cmd)
+    return False
+
+  min_count = None
+  max_count = None
+  check_max = None
+
+  last_idx = len(args_def) - 1
+
+  for idx, arg in enumerate(args_def):
+    if min_count is None:
+      min_count = arg.min
+    elif arg.min is not None:
+      min_count += arg.min
+
+    if max_count is None:
+      max_count = arg.max
+    elif arg.max is not None:
+      max_count += arg.max
+
+    if idx == last_idx:
+      check_max = (arg.max is not None)
+
+    elif arg.max is None:
+      raise errors.ProgrammerError("Only the last argument can have max=None")
+
+  if check_max:
+    # Command with exact number of arguments
+    if (min_count is not None and max_count is not None and
+        min_count == max_count and len(args) != min_count):
+      ToStderr("Error: Command %s expects %d argument(s)", cmd, min_count)
+      return False
+
+    # Command with limited number of arguments
+    if max_count is not None and len(args) > max_count:
+      ToStderr("Error: Command %s expects only %d argument(s)",
+               cmd, max_count)
+      return False
+
+  # Command with some required arguments
+  if min_count is not None and len(args) < min_count:
+    ToStderr("Error: Command %s expects at least %d argument(s)",
+             cmd, min_count)
+    return False
+
+  return True
+
+
 def SplitNodeOption(value):
   """Splits the value of a --node option.
 
@@ -579,6 +858,7 @@ def PollJob(job_id, cl=None, feedback_fn=None):
       if status == constants.OP_STATUS_SUCCESS:
         has_ok = True
       elif status == constants.OP_STATUS_ERROR:
+        errors.MaybeRaise(msg)
         if has_ok:
           raise errors.OpExecError("partial failure (opcode %d): %s" %
                                    (idx, msg))