cluster-verify checks uniformity of PV sizes
[ganeti-local] / lib / cli.py
index 91ea9f6..950578e 100644 (file)
@@ -54,6 +54,7 @@ __all__ = [
   # Command line options
   "ABSOLUTE_OPT",
   "ADD_UIDS_OPT",
+  "ADD_RESERVED_IPS_OPT",
   "ALLOCATABLE_OPT",
   "ALLOC_POLICY_OPT",
   "ALL_OPT",
@@ -81,12 +82,15 @@ __all__ = [
   "EARLY_RELEASE_OPT",
   "ENABLED_HV_OPT",
   "ERROR_CODES_OPT",
+  "FAILURE_ONLY_OPT",
   "FIELDS_OPT",
   "FILESTORE_DIR_OPT",
   "FILESTORE_DRIVER_OPT",
   "FORCE_FILTER_OPT",
   "FORCE_OPT",
   "FORCE_VARIANT_OPT",
+  "GATEWAY_OPT",
+  "GATEWAY6_OPT",
   "GLOBAL_FILEDIR_OPT",
   "HID_OS_OPT",
   "GLOBAL_SHARED_FILEDIR_OPT",
@@ -111,6 +115,9 @@ __all__ = [
   "MC_OPT",
   "MIGRATION_MODE_OPT",
   "NET_OPT",
+  "NETWORK_OPT",
+  "NETWORK6_OPT",
+  "NETWORK_TYPE_OPT",
   "NEW_CLUSTER_CERT_OPT",
   "NEW_CLUSTER_DOMAIN_SECRET_OPT",
   "NEW_CONFD_HMAC_KEY_OPT",
@@ -118,6 +125,7 @@ __all__ = [
   "NEW_SECONDARY_OPT",
   "NEW_SPICE_CERT_OPT",
   "NIC_PARAMS_OPT",
+  "NOCONFLICTSCHECK_OPT",
   "NODE_FORCE_JOIN_OPT",
   "NODE_LIST_OPT",
   "NODE_PLACEMENT_OPT",
@@ -160,6 +168,7 @@ __all__ = [
   "READD_OPT",
   "REBOOT_TYPE_OPT",
   "REMOVE_INSTANCE_OPT",
+  "REMOVE_RESERVED_IPS_OPT",
   "REMOVE_UIDS_OPT",
   "RESERVED_LVS_OPT",
   "RUNTIME_MEM_OPT",
@@ -169,6 +178,7 @@ __all__ = [
   "SELECT_OS_OPT",
   "SEP_OPT",
   "SHOWCMD_OPT",
+  "SHOW_MACHINE_OPT",
   "SHUTDOWN_TIMEOUT_OPT",
   "SINGLE_NODE_OPT",
   "SPECS_CPU_COUNT_OPT",
@@ -235,11 +245,13 @@ __all__ = [
   "ARGS_MANY_INSTANCES",
   "ARGS_MANY_NODES",
   "ARGS_MANY_GROUPS",
+  "ARGS_MANY_NETWORKS",
   "ARGS_NONE",
   "ARGS_ONE_INSTANCE",
   "ARGS_ONE_NODE",
   "ARGS_ONE_GROUP",
   "ARGS_ONE_OS",
+  "ARGS_ONE_NETWORK",
   "ArgChoice",
   "ArgCommand",
   "ArgFile",
@@ -247,8 +259,10 @@ __all__ = [
   "ArgHost",
   "ArgInstance",
   "ArgJobId",
+  "ArgNetwork",
   "ArgNode",
   "ArgOs",
+  "ArgExtStorage",
   "ArgSuggest",
   "ArgUnknown",
   "OPT_COMPL_INST_ADD_NODES",
@@ -257,7 +271,9 @@ __all__ = [
   "OPT_COMPL_ONE_INSTANCE",
   "OPT_COMPL_ONE_NODE",
   "OPT_COMPL_ONE_NODEGROUP",
+  "OPT_COMPL_ONE_NETWORK",
   "OPT_COMPL_ONE_OS",
+  "OPT_COMPL_ONE_EXTSTORAGE",
   "cli_option",
   "SplitNodeOption",
   "CalculateOSNames",
@@ -301,6 +317,17 @@ TISPECS_CLUSTER_TYPES = {
   constants.ISPECS_STD: constants.VTYPE_INT,
   }
 
+#: User-friendly names for query2 field types
+_QFT_NAMES = {
+  constants.QFT_UNKNOWN: "Unknown",
+  constants.QFT_TEXT: "Text",
+  constants.QFT_BOOL: "Boolean",
+  constants.QFT_NUMBER: "Number",
+  constants.QFT_UNIT: "Storage size",
+  constants.QFT_TIMESTAMP: "Timestamp",
+  constants.QFT_OTHER: "Custom",
+  }
+
 
 class _Argument:
   def __init__(self, min=0, max=None): # pylint: disable=W0622
@@ -355,6 +382,12 @@ class ArgNode(_Argument):
   """
 
 
+class ArgNetwork(_Argument):
+  """Network argument.
+
+  """
+
+
 class ArgGroup(_Argument):
   """Node group argument.
 
@@ -391,11 +424,19 @@ class ArgOs(_Argument):
   """
 
 
+class ArgExtStorage(_Argument):
+  """ExtStorage argument.
+
+  """
+
+
 ARGS_NONE = []
 ARGS_MANY_INSTANCES = [ArgInstance()]
+ARGS_MANY_NETWORKS = [ArgNetwork()]
 ARGS_MANY_NODES = [ArgNode()]
 ARGS_MANY_GROUPS = [ArgGroup()]
 ARGS_ONE_INSTANCE = [ArgInstance(min=1, max=1)]
+ARGS_ONE_NETWORK = [ArgNetwork(min=1, max=1)]
 ARGS_ONE_NODE = [ArgNode(min=1, max=1)]
 # TODO
 ARGS_ONE_GROUP = [ArgGroup(min=1, max=1)]
@@ -412,9 +453,10 @@ def _ExtractTagsObject(opts, args):
     raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
   kind = opts.tag_type
   if kind == constants.TAG_CLUSTER:
-    retval = kind, kind
+    retval = kind, None
   elif kind in (constants.TAG_NODEGROUP,
                 constants.TAG_NODE,
+                constants.TAG_NETWORK,
                 constants.TAG_INSTANCE):
     if not args:
       raise errors.OpPrereqError("no arguments passed to the command",
@@ -638,16 +680,20 @@ def check_maybefloat(option, opt, value): # pylint: disable=W0613
  OPT_COMPL_ONE_NODE,
  OPT_COMPL_ONE_INSTANCE,
  OPT_COMPL_ONE_OS,
+ OPT_COMPL_ONE_EXTSTORAGE,
  OPT_COMPL_ONE_IALLOCATOR,
+ OPT_COMPL_ONE_NETWORK,
  OPT_COMPL_INST_ADD_NODES,
- OPT_COMPL_ONE_NODEGROUP) = range(100, 107)
+ OPT_COMPL_ONE_NODEGROUP) = range(100, 109)
 
-OPT_COMPL_ALL = frozenset([
+OPT_COMPL_ALL = compat.UniqueFrozenset([
   OPT_COMPL_MANY_NODES,
   OPT_COMPL_ONE_NODE,
   OPT_COMPL_ONE_INSTANCE,
   OPT_COMPL_ONE_OS,
+  OPT_COMPL_ONE_EXTSTORAGE,
   OPT_COMPL_ONE_IALLOCATOR,
+  OPT_COMPL_ONE_NETWORK,
   OPT_COMPL_INST_ADD_NODES,
   OPT_COMPL_ONE_NODEGROUP,
   ])
@@ -734,7 +780,7 @@ SYNC_OPT = cli_option("--sync", dest="do_locking",
 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"
+                               " check steps and verify if it could be"
                                " executed"))
 
 VERBOSE_OPT = cli_option("-v", "--verbose", default=False,
@@ -1174,7 +1220,7 @@ GLOBAL_SHARED_FILEDIR_OPT = cli_option(
   metavar="SHAREDDIR", default=pathutils.DEFAULT_SHARED_FILE_STORAGE_DIR)
 
 NOMODIFY_ETCHOSTS_OPT = cli_option("--no-etc-hosts", dest="modify_etc_hosts",
-                                   help="Don't modify /etc/hosts",
+                                   help="Don't modify %s" % pathutils.ETC_HOSTS,
                                    action="store_false", default=True)
 
 NOMODIFY_SSH_SETUP_OPT = cli_option("--no-ssh-init", dest="modify_ssh_setup",
@@ -1335,9 +1381,30 @@ PRIMARY_IP_VERSION_OPT = \
                                   constants.IP6_VERSION),
                help="Cluster-wide IP version for primary IP")
 
+SHOW_MACHINE_OPT = cli_option("-M", "--show-machine-names", default=False,
+                              action="store_true",
+                              help="Show machine name for every line in output")
+
+FAILURE_ONLY_OPT = cli_option("--failure-only", default=False,
+                              action="store_true",
+                              help=("Hide successful results and show failures"
+                                    " only (determined by the exit code)"))
+
+
+def _PriorityOptionCb(option, _, value, parser):
+  """Callback for processing C{--priority} option.
+
+  """
+  value = _PRIONAME_TO_VALUE[value]
+
+  setattr(parser.values, option.dest, value)
+
+
 PRIORITY_OPT = cli_option("--priority", default=None, dest="priority",
                           metavar="|".join(name for name, _ in _PRIORITY_NAMES),
                           choices=_PRIONAME_TO_VALUE.keys(),
+                          action="callback", type="choice",
+                          callback=_PriorityOptionCb,
                           help="Priority for opcode processing")
 
 HID_OS_OPT = cli_option("--hidden", dest="hidden",
@@ -1440,6 +1507,44 @@ ABSOLUTE_OPT = cli_option("--absolute", dest="absolute",
                           help="Marks the grow as absolute instead of the"
                           " (default) relative mode")
 
+NETWORK_OPT = cli_option("--network",
+                         action="store", default=None, dest="network",
+                         help="IP network in CIDR notation")
+
+GATEWAY_OPT = cli_option("--gateway",
+                         action="store", default=None, dest="gateway",
+                         help="IP address of the router (gateway)")
+
+ADD_RESERVED_IPS_OPT = cli_option("--add-reserved-ips",
+                                  action="store", default=None,
+                                  dest="add_reserved_ips",
+                                  help="Comma-separated list of"
+                                  " reserved IPs to add")
+
+REMOVE_RESERVED_IPS_OPT = cli_option("--remove-reserved-ips",
+                                     action="store", default=None,
+                                     dest="remove_reserved_ips",
+                                     help="Comma-delimited list of"
+                                     " reserved IPs to remove")
+
+NETWORK_TYPE_OPT = cli_option("--network-type",
+                              action="store", default=None, dest="network_type",
+                              help="Network type: private, public, None")
+
+NETWORK6_OPT = cli_option("--network6",
+                          action="store", default=None, dest="network6",
+                          help="IP network in CIDR notation")
+
+GATEWAY6_OPT = cli_option("--gateway6",
+                          action="store", default=None, dest="gateway6",
+                          help="IP6 address of the router (gateway)")
+
+NOCONFLICTSCHECK_OPT = cli_option("--no-conflicts-check",
+                                  dest="conflicts_check",
+                                  default=True,
+                                  action="store_false",
+                                  help="Don't check for conflicting IPs")
+
 #: Options provided by all commands
 COMMON_OPTS = [DEBUG_OPT]
 
@@ -1456,6 +1561,7 @@ COMMON_CREATE_OPTS = [
   NET_OPT,
   NODE_PLACEMENT_OPT,
   NOIPCHECK_OPT,
+  NOCONFLICTSCHECK_OPT,
   NONAMECHECK_OPT,
   NONICS_OPT,
   NWSYNC_OPT,
@@ -1480,68 +1586,60 @@ INSTANCE_POLICY_OPTS = [
   ]
 
 
-def _ParseArgs(argv, commands, aliases, env_override):
+class _ShowUsage(Exception):
+  """Exception class for L{_ParseArgs}.
+
+  """
+  def __init__(self, exit_error):
+    """Initializes instances of this class.
+
+    @type exit_error: bool
+    @param exit_error: Whether to report failure on exit
+
+    """
+    Exception.__init__(self)
+    self.exit_error = exit_error
+
+
+class _ShowVersion(Exception):
+  """Exception class for L{_ParseArgs}.
+
+  """
+
+
+def _ParseArgs(binary, argv, commands, aliases, env_override):
   """Parser for the command line arguments.
 
   This function parses the arguments and returns the function which
   must be executed together with its (modified) arguments.
 
-  @param argv: the command line
-  @param commands: dictionary with special contents, see the design
-      doc for cmdline handling
-  @param aliases: dictionary with command aliases {'alias': 'target, ...}
+  @param binary: Script name
+  @param argv: Command line arguments
+  @param commands: Dictionary containing command definitions
+  @param aliases: dictionary with command aliases {"alias": "target", ...}
   @param env_override: list of env variables allowed for default args
+  @raise _ShowUsage: If usage description should be shown
+  @raise _ShowVersion: If version should be shown
 
   """
   assert not (env_override - set(commands))
+  assert not (set(aliases.keys()) & set(commands.keys()))
 
-  if len(argv) == 0:
-    binary = "<command>"
+  if len(argv) > 1:
+    cmd = argv[1]
   else:
-    binary = argv[0].split("/")[-1]
-
-  if len(argv) > 1 and argv[1] == "--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)
-
-  if len(argv) < 2 or not (argv[1] in commands or
-                           argv[1] in aliases):
-    # let's do a nice thing
-    sortedcmds = commands.keys()
-    sortedcmds.sort()
-
-    ToStdout("Usage: %s {command} [options...] [argument...]", binary)
-    ToStdout("%s <command> --help to see details, or man %s", binary, binary)
-    ToStdout("")
-
-    # compute the max line length for cmd + usage
-    mlen = max([len(" %s" % cmd) for cmd in commands])
-    mlen = min(60, mlen) # should not get here...
-
-    # and format a nice command list
-    ToStdout("Commands:")
-    for cmd in sortedcmds:
-      cmdstr = " %s" % (cmd,)
-      help_text = commands[cmd][4]
-      help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
-      ToStdout("%-*s - %s", mlen, cmdstr, help_lines.pop(0))
-      for line in help_lines:
-        ToStdout("%-*s   %s", mlen, "", line)
-
-    ToStdout("")
+    # No option or command given
+    raise _ShowUsage(exit_error=True)
 
-    return None, None, None
+  if cmd == "--version":
+    raise _ShowVersion()
+  elif cmd == "--help":
+    raise _ShowUsage(exit_error=False)
+  elif not (cmd in commands or cmd in aliases):
+    raise _ShowUsage(exit_error=True)
 
   # get command, unalias it, and look it up in commands
-  cmd = argv.pop(1)
   if cmd in aliases:
-    if cmd in commands:
-      raise errors.ProgrammerError("Alias '%s' overrides an existing"
-                                   " command" % cmd)
-
     if aliases[cmd] not in commands:
       raise errors.ProgrammerError("Alias '%s' maps to non-existing"
                                    " command '%s'" % (cmd, aliases[cmd]))
@@ -1552,7 +1650,7 @@ def _ParseArgs(argv, commands, aliases, env_override):
     args_env_name = ("%s_%s" % (binary.replace("-", "_"), cmd)).upper()
     env_args = os.environ.get(args_env_name)
     if env_args:
-      argv = utils.InsertAtPos(argv, 1, shlex.split(env_args))
+      argv = utils.InsertAtPos(argv, 2, shlex.split(env_args))
 
   func, args_def, parser_opts, usage, description = commands[cmd]
   parser = OptionParser(option_list=parser_opts + COMMON_OPTS,
@@ -1560,7 +1658,7 @@ def _ParseArgs(argv, commands, aliases, env_override):
                         formatter=TitledHelpFormatter(),
                         usage="%%prog %s %s" % (cmd, usage))
   parser.disable_interspersed_args()
-  options, args = parser.parse_args(args=argv[1:])
+  options, args = parser.parse_args(args=argv[2:])
 
   if not _CheckArguments(cmd, args_def, args):
     return None, None, None
@@ -1568,6 +1666,31 @@ def _ParseArgs(argv, commands, aliases, env_override):
   return func, options, args
 
 
+def _FormatUsage(binary, commands):
+  """Generates a nice description of all commands.
+
+  @param binary: Script name
+  @param commands: Dictionary containing command definitions
+
+  """
+  # compute the max line length for cmd + usage
+  mlen = min(60, max(map(len, commands)))
+
+  yield "Usage: %s {command} [options...] [argument...]" % binary
+  yield "%s <command> --help to see details, or man %s" % (binary, binary)
+  yield ""
+  yield "Commands:"
+
+  # and format a nice command list
+  for (cmd, (_, _, _, _, help_text)) in sorted(commands.items()):
+    help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
+    yield " %-*s - %s" % (mlen, cmd, help_lines.pop(0))
+    for line in help_lines:
+      yield " %-*s   %s" % (mlen, "", line)
+
+  yield ""
+
+
 def _CheckArguments(cmd, args_def, args):
   """Verifies the arguments using the argument definition.
 
@@ -2080,7 +2203,7 @@ def SetGenericOpcodeOpts(opcode_list, options):
     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]
+      op.priority = options.priority
 
 
 def GetClient(query=False):
@@ -2242,7 +2365,20 @@ def GenericMain(commands, override=None, aliases=None,
     aliases = {}
 
   try:
-    func, options, args = _ParseArgs(sys.argv, commands, aliases, env_override)
+    (func, options, args) = _ParseArgs(binary, sys.argv, commands, aliases,
+                                       env_override)
+  except _ShowVersion:
+    ToStdout("%s (ganeti %s) %s", binary, constants.VCS_VERSION,
+             constants.RELEASE_VERSION)
+    return constants.EXIT_SUCCESS
+  except _ShowUsage, err:
+    for line in _FormatUsage(binary, commands):
+      ToStdout(line)
+
+    if err.exit_error:
+      return constants.EXIT_FAILURE
+    else:
+      return constants.EXIT_SUCCESS
   except errors.ParameterError, err:
     result, err_msg = FormatError(err)
     ToStderr(err_msg)
@@ -2420,6 +2556,7 @@ def GenericInstanceCreate(mode, opts, args):
                                 disks=disks,
                                 disk_template=opts.disk_template,
                                 nics=nics,
+                                conflicts_check=opts.conflicts_check,
                                 pnode=pnode, snode=snode,
                                 ip_check=opts.ip_check,
                                 name_check=opts.name_check,
@@ -2488,7 +2625,8 @@ class _RunWhileClusterStoppedHelper:
       # No need to use SSH
       result = utils.RunCmd(cmd)
     else:
-      result = self.ssh.Run(node_name, "root", utils.ShellQuoteArgs(cmd))
+      result = self.ssh.Run(node_name, constants.SSH_LOGIN_USER,
+                            utils.ShellQuoteArgs(cmd))
 
     if result.failed:
       errmsg = ["Failed to run command %s" % result.cmd]
@@ -2947,6 +3085,21 @@ def GenericList(resource, fields, names, unit, separator, header, cl=None,
   return constants.EXIT_SUCCESS
 
 
+def _FieldDescValues(fdef):
+  """Helper function for L{GenericListFields} to get query field description.
+
+  @type fdef: L{objects.QueryFieldDefinition}
+  @rtype: list
+
+  """
+  return [
+    fdef.name,
+    _QFT_NAMES.get(fdef.kind, fdef.kind),
+    fdef.title,
+    fdef.doc,
+    ]
+
+
 def GenericListFields(resource, fields, separator, header, cl=None):
   """Generic implementation for listing fields for a resource.
 
@@ -2971,11 +3124,12 @@ def GenericListFields(resource, fields, separator, header, cl=None):
 
   columns = [
     TableColumn("Name", str, False),
+    TableColumn("Type", str, False),
     TableColumn("Title", str, False),
     TableColumn("Description", str, False),
     ]
 
-  rows = [[fdef.name, fdef.title, fdef.doc] for fdef in response.fields]
+  rows = map(_FieldDescValues, response.fields)
 
   for line in FormatTable(rows, columns, header, separator):
     ToStdout(line)