Fix a bug in command line option parsing code
[ganeti-local] / lib / cli.py
index d539add..95bca46 100644 (file)
@@ -29,6 +29,7 @@ import time
 import logging
 import errno
 import itertools
+import optparse
 from cStringIO import StringIO
 
 from ganeti import utils
@@ -43,7 +44,7 @@ from ganeti import compat
 from ganeti import netutils
 from ganeti import qlang
 
-from optparse import (OptionParser, TitledHelpFormatter,
+from optparse import (TitledHelpFormatter,
                       Option, OptionValueError)
 
 
@@ -162,11 +163,13 @@ __all__ = [
   "SRC_DIR_OPT",
   "SRC_NODE_OPT",
   "SUBMIT_OPT",
+  "STARTUP_PAUSED_OPT",
   "STATIC_OPT",
   "SYNC_OPT",
   "TAG_ADD_OPT",
   "TAG_SRC_OPT",
   "TIMEOUT_OPT",
+  "TO_GROUP_OPT",
   "UIDPOOL_OPT",
   "USEUNITS_OPT",
   "USE_REPL_NET_OPT",
@@ -255,9 +258,12 @@ _PRIONAME_TO_VALUE = dict(_PRIORITY_NAMES)
  QR_UNKNOWN,
  QR_INCOMPLETE) = range(3)
 
+#: Maximum batch size for ChooseJob
+_CHOOSE_BATCH = 25
+
 
 class _Argument:
-  def __init__(self, min=0, max=None): # pylint: disable-msg=W0622
+  def __init__(self, min=0, max=None): # pylint: disable=W0622
     self.min = min
     self.max = max
 
@@ -272,7 +278,7 @@ class ArgSuggest(_Argument):
   Value can be any of the ones passed to the constructor.
 
   """
-  # pylint: disable-msg=W0622
+  # pylint: disable=W0622
   def __init__(self, min=0, max=None, choices=None):
     _Argument.__init__(self, min=min, max=max)
     self.choices = choices
@@ -460,7 +466,7 @@ def RemoveTags(opts, args):
   SubmitOpCode(op, opts=opts)
 
 
-def check_unit(option, opt, value): # pylint: disable-msg=W0613
+def check_unit(option, opt, value): # pylint: disable=W0613
   """OptParsers custom converter for units.
 
   """
@@ -507,7 +513,7 @@ def _SplitKeyVal(opt, data):
   return kv_dict
 
 
-def check_ident_key_val(option, opt, value):  # pylint: disable-msg=W0613
+def check_ident_key_val(option, opt, value):  # pylint: disable=W0613
   """Custom parser for ident:key=val,key=val options.
 
   This will store the parsed values as a tuple (ident, {key: val}). As such,
@@ -515,7 +521,7 @@ def check_ident_key_val(option, opt, value):  # pylint: disable-msg=W0613
 
   """
   if ":" not in value:
-    ident, rest = value, ''
+    ident, rest = value, ""
   else:
     ident, rest = value.split(":", 1)
 
@@ -535,7 +541,7 @@ def check_ident_key_val(option, opt, value):  # pylint: disable-msg=W0613
   return retval
 
 
-def check_key_val(option, opt, value):  # pylint: disable-msg=W0613
+def check_key_val(option, opt, value):  # pylint: disable=W0613
   """Custom parser class for key=val,key=val options.
 
   This will store the parsed values as a dict {key: val}.
@@ -544,7 +550,7 @@ def check_key_val(option, opt, value):  # pylint: disable-msg=W0613
   return _SplitKeyVal(opt, value)
 
 
-def check_bool(option, opt, value): # pylint: disable-msg=W0613
+def check_bool(option, opt, value): # pylint: disable=W0613
   """Custom parser for yes/no options.
 
   This will store the parsed value as either True or False.
@@ -619,7 +625,7 @@ SEP_OPT = cli_option("--separator", default=None,
                            " (defaults to one space)"))
 
 USEUNITS_OPT = cli_option("--units", default=None,
-                          dest="units", choices=('h', 'm', 'g', 't'),
+                          dest="units", choices=("h", "m", "g", "t"),
                           help="Specify units for output (one of h/m/g/t)")
 
 FIELDS_OPT = cli_option("-o", "--output", dest="output", action="store",
@@ -726,9 +732,9 @@ 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")
+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"
@@ -802,7 +808,8 @@ 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", "--node-group",
+NODEGROUP_OPT_NAME = "--node-group"
+NODEGROUP_OPT = cli_option("-g", NODEGROUP_OPT_NAME,
                            dest="nodegroup",
                            help="Node group (name or uuid)",
                            metavar="<nodegroup>",
@@ -978,7 +985,7 @@ VG_NAME_OPT = cli_option("--vg-name", dest="vg_name",
                                " [%s]" % constants.DEFAULT_VG),
                          metavar="VG", default=None)
 
-YES_DOIT_OPT = cli_option("--yes-do-it", dest="yes_do_it",
+YES_DOIT_OPT = cli_option("--yes-do-it", "--ya-rly", dest="yes_do_it",
                           help="Destroy cluster", action="store_true")
 
 NOVOTING_OPT = cli_option("--no-voting", dest="no_voting",
@@ -1223,6 +1230,15 @@ SECONDARY_ONLY_OPT = cli_option("-s", "--secondary-only",
                                      " disk templates, e.g. %s)" %
                                      utils.CommaJoin(constants.DTS_INT_MIRROR))
 
+STARTUP_PAUSED_OPT = cli_option("--paused", dest="startup_paused",
+                                action="store_true", default=False,
+                                help="Pause instance at startup")
+
+TO_GROUP_OPT = cli_option("--to", dest="to", metavar="<group>",
+                          help="Destination node group (name or uuid)",
+                          default=None, action="append",
+                          completion_suggest=OPT_COMPL_ONE_NODEGROUP)
+
 
 #: Options provided by all commands
 COMMON_OPTS = [DEBUG_OPT]
@@ -1318,10 +1334,10 @@ def _ParseArgs(argv, commands, aliases):
     cmd = aliases[cmd]
 
   func, args_def, parser_opts, usage, description = commands[cmd]
-  parser = OptionParser(option_list=parser_opts + COMMON_OPTS,
-                        description=description,
-                        formatter=TitledHelpFormatter(),
-                        usage="%%prog %s %s" % (cmd, usage))
+  parser = CustomOptionParser(option_list=parser_opts + COMMON_OPTS,
+                              description=description,
+                              formatter=TitledHelpFormatter(),
+                              usage="%%prog %s %s" % (cmd, usage))
   parser.disable_interspersed_args()
   options, args = parser.parse_args()
 
@@ -1331,6 +1347,21 @@ def _ParseArgs(argv, commands, aliases):
   return func, options, args
 
 
+class CustomOptionParser(optparse.OptionParser):
+  def _match_long_opt(self, opt):
+    """Override C{OptionParser}'s function for matching long options.
+
+    The default implementation does prefix-based abbreviation matching. We
+    disable such behaviour as it can can lead to confusing conflicts (e.g.
+    C{--force} and C{--force-multi}).
+
+    """
+    if opt in self._long_opt:
+      return opt
+    else:
+      raise optparse.BadOptionError(opt)
+
+
 def _CheckArguments(cmd, args_def, args):
   """Verifies the arguments using the argument definition.
 
@@ -1406,8 +1437,8 @@ def SplitNodeOption(value):
   """Splits the value of a --node option.
 
   """
-  if value and ':' in value:
-    return value.split(':', 1)
+  if value and ":" in value:
+    return value.split(":", 1)
   else:
     return (value, None)
 
@@ -1424,7 +1455,7 @@ def CalculateOSNames(os_name, os_variants):
 
   """
   if os_variants:
-    return ['%s+%s' % (os_name, v) for v in os_variants]
+    return ["%s+%s" % (os_name, v) for v in os_variants]
   else:
     return [os_name]
 
@@ -1466,12 +1497,12 @@ def AskUser(text, choices=None):
 
   """
   if choices is None:
-    choices = [('y', True, 'Perform the operation'),
-               ('n', False, 'Do not perform the operation')]
+    choices = [("y", True, "Perform the operation"),
+               ("n", False, "Do not perform the operation")]
   if not choices or not isinstance(choices, list):
     raise errors.ProgrammerError("Invalid choices argument to AskUser")
   for entry in choices:
-    if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
+    if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == "?":
       raise errors.ProgrammerError("Invalid choices element to AskUser")
 
   answer = choices[-1][1]
@@ -1486,18 +1517,18 @@ def AskUser(text, choices=None):
   try:
     chars = [entry[0] for entry in choices]
     chars[-1] = "[%s]" % chars[-1]
-    chars.append('?')
+    chars.append("?")
     maps = dict([(entry[0], entry[1]) for entry in choices])
     while True:
       f.write(text)
-      f.write('\n')
+      f.write("\n")
       f.write("/".join(chars))
       f.write(": ")
       line = f.readline(2).strip().lower()
       if line in maps:
         answer = maps[line]
         break
-      elif line == '?':
+      elif line == "?":
         for entry in choices:
           f.write(" %s - %s\n" % (entry[0], entry[2]))
         f.write("\n")
@@ -1744,7 +1775,7 @@ class StdioJobPollReportCb(JobPollReportCbBase):
       ToStderr("Job %s is waiting in queue", job_id)
       self.notified_queued = True
 
-    elif status == constants.JOB_STATUS_WAITLOCK and not self.notified_waitlock:
+    elif status == constants.JOB_STATUS_WAITING and not self.notified_waitlock:
       ToStderr("Job %s is trying to acquire all necessary locks", job_id)
       self.notified_waitlock = True
 
@@ -1954,7 +1985,7 @@ def FormatError(err):
     retcode = 0
   else:
     obuf.write("Unhandled exception: %s" % msg)
-  return retcode, obuf.getvalue().rstrip('\n')
+  return retcode, obuf.getvalue().rstrip("\n")
 
 
 def GenericMain(commands, override=None, aliases=None):
@@ -2249,7 +2280,7 @@ class _RunWhileClusterStoppedHelper:
     """
     # Pause watcher by acquiring an exclusive lock on watcher state file
     self.feedback_fn("Blocking watcher")
-    watcher_block = utils.FileLock.Open(constants.WATCHER_STATEFILE)
+    watcher_block = utils.FileLock.Open(constants.WATCHER_LOCK_FILE)
     try:
       # TODO: Currently, this just blocks. There's no timeout.
       # TODO: Should it be a shared lock?
@@ -2351,8 +2382,8 @@ def GenerateTable(headers, fields, separator, data,
   if unitfields is None:
     unitfields = []
 
-  numfields = utils.FieldSet(*numfields)   # pylint: disable-msg=W0142
-  unitfields = utils.FieldSet(*unitfields) # pylint: disable-msg=W0142
+  numfields = utils.FieldSet(*numfields)   # pylint: disable=W0142
+  unitfields = utils.FieldSet(*unitfields) # pylint: disable=W0142
 
   format_fields = []
   for field in fields:
@@ -2370,7 +2401,7 @@ def GenerateTable(headers, fields, separator, data,
 
   if separator is None:
     mlens = [0 for name in fields]
-    format_str = ' '.join(format_fields)
+    format_str = " ".join(format_fields)
   else:
     format_str = separator.replace("%", "%%").join(format_fields)
 
@@ -2409,7 +2440,7 @@ def GenerateTable(headers, fields, separator, data,
   for line in data:
     args = []
     if line is None:
-      line = ['-' for _ in fields]
+      line = ["-" for _ in fields]
     for idx in range(len(fields)):
       if separator is None:
         args.append(mlens[idx])
@@ -2649,18 +2680,7 @@ def GenericList(resource, fields, names, unit, separator, header, cl=None,
   if not names:
     names = None
 
-  if (force_filter or
-      (names and len(names) == 1 and qlang.MaybeFilter(names[0]))):
-    try:
-      (filter_text, ) = names
-    except ValueError:
-      raise errors.OpPrereqError("Exactly one argument must be given as a"
-                                 " filter")
-
-    logging.debug("Parsing '%s' as filter", filter_text)
-    filter_ = qlang.ParseFilter(filter_text)
-  else:
-    filter_ = qlang.MakeSimpleFilter("name", names)
+  filter_ = qlang.MakeFilter(names, force_filter)
 
   response = cl.Query(resource, fields, filter_)
 
@@ -2814,8 +2834,8 @@ def FormatTimestamp(ts):
   @return: a string with the formatted timestamp
 
   """
-  if not isinstance (ts, (tuple, list)) or len(ts) != 2:
-    return '?'
+  if not isinstance(ts, (tuple, list)) or len(ts) != 2:
+    return "?"
   sec, usec = ts
   return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
 
@@ -2838,11 +2858,11 @@ def ParseTimespec(value):
   if not value:
     raise errors.OpPrereqError("Empty time specification passed")
   suffix_map = {
-    's': 1,
-    'm': 60,
-    'h': 3600,
-    'd': 86400,
-    'w': 604800,
+    "s": 1,
+    "m": 60,
+    "h": 3600,
+    "d": 86400,
+    "w": 604800,
     }
   if value[-1] not in suffix_map:
     try:
@@ -2863,7 +2883,7 @@ def ParseTimespec(value):
 
 
 def GetOnlineNodes(nodes, cl=None, nowarn=False, secondary_ips=False,
-                   filter_master=False):
+                   filter_master=False, nodegroup=None):
   """Returns the names of online nodes.
 
   This function will also log a warning on stderr with the names of
@@ -2884,28 +2904,60 @@ def GetOnlineNodes(nodes, cl=None, nowarn=False, secondary_ips=False,
   @param filter_master: if True, do not return the master node in the list
       (useful in coordination with secondary_ips where we cannot check our
       node name against the list)
+  @type nodegroup: string
+  @param nodegroup: If set, only return nodes in this node group
 
   """
   if cl is None:
     cl = GetClient()
 
-  if secondary_ips:
-    name_idx = 2
-  else:
-    name_idx = 0
+  filter_ = []
+
+  if nodes:
+    filter_.append(qlang.MakeSimpleFilter("name", nodes))
+
+  if nodegroup is not None:
+    filter_.append([qlang.OP_OR, [qlang.OP_EQUAL, "group", nodegroup],
+                                 [qlang.OP_EQUAL, "group.uuid", nodegroup]])
 
   if filter_master:
-    master_node = cl.QueryConfigValues(["master_node"])[0]
-    filter_fn = lambda x: x != master_node
+    filter_.append([qlang.OP_NOT, [qlang.OP_TRUE, "master"]])
+
+  if filter_:
+    if len(filter_) > 1:
+      final_filter = [qlang.OP_AND] + filter_
+    else:
+      assert len(filter_) == 1
+      final_filter = filter_[0]
   else:
-    filter_fn = lambda _: True
+    final_filter = None
+
+  result = cl.Query(constants.QR_NODE, ["name", "offline", "sip"], final_filter)
+
+  def _IsOffline(row):
+    (_, (_, offline), _) = row
+    return offline
+
+  def _GetName(row):
+    ((_, name), _, _) = row
+    return name
+
+  def _GetSip(row):
+    (_, _, (_, sip)) = row
+    return sip
+
+  (offline, online) = compat.partition(result.data, _IsOffline)
 
-  result = cl.QueryNodes(names=nodes, fields=["name", "offline", "sip"],
-                         use_locking=False)
-  offline = [row[0] for row in result if row[1]]
   if offline and not nowarn:
-    ToStderr("Note: skipping offline node(s): %s" % utils.CommaJoin(offline))
-  return [row[name_idx] for row in result if not row[1] and filter_fn(row[0])]
+    ToStderr("Note: skipping offline node(s): %s" %
+             utils.CommaJoin(map(_GetName, offline)))
+
+  if secondary_ips:
+    fn = _GetSip
+  else:
+    fn = _GetName
+
+  return map(fn, online)
 
 
 def _ToStream(stream, txt, *args):
@@ -2923,7 +2975,7 @@ def _ToStream(stream, txt, *args):
       stream.write(txt % args)
     else:
       stream.write(txt)
-    stream.write('\n')
+    stream.write("\n")
     stream.flush()
   except IOError, err:
     if err.errno == errno.EPIPE:
@@ -3022,13 +3074,14 @@ class JobExecutor(object):
     """
     assert self.jobs, "_ChooseJob called with empty job list"
 
-    result = self.cl.QueryJobs([i[2] for i in self.jobs], ["status"])
+    result = self.cl.QueryJobs([i[2] for i in self.jobs[:_CHOOSE_BATCH]],
+                               ["status"])
     assert result
 
     for job_data, status in zip(self.jobs, result):
       if (isinstance(status, list) and status and
           status[0] in (constants.JOB_STATUS_QUEUED,
-                        constants.JOB_STATUS_WAITLOCK,
+                        constants.JOB_STATUS_WAITING,
                         constants.JOB_STATUS_CANCELING)):
         # job is still present and waiting
         continue