Add a generic write file function
[ganeti-local] / lib / cli.py
index fbc7f12..d1b6925 100644 (file)
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#
 #
 
 # Copyright (C) 2006, 2007 Google Inc.
 #
 
 # Copyright (C) 2006, 2007 Google Inc.
@@ -26,12 +26,14 @@ import sys
 import textwrap
 import os.path
 import copy
 import textwrap
 import os.path
 import copy
+from cStringIO import StringIO
 
 from ganeti import utils
 from ganeti import logger
 from ganeti import errors
 from ganeti import mcpu
 from ganeti import constants
 
 from ganeti import utils
 from ganeti import logger
 from ganeti import errors
 from ganeti import mcpu
 from ganeti import constants
+from ganeti import opcodes
 
 from optparse import (OptionParser, make_option, TitledHelpFormatter,
                       Option, OptionValueError, SUPPRESS_HELP)
 
 from optparse import (OptionParser, make_option, TitledHelpFormatter,
                       Option, OptionValueError, SUPPRESS_HELP)
@@ -39,7 +41,113 @@ from optparse import (OptionParser, make_option, TitledHelpFormatter,
 __all__ = ["DEBUG_OPT", "NOHDR_OPT", "SEP_OPT", "GenericMain", "SubmitOpCode",
            "cli_option", "GenerateTable", "AskUser",
            "ARGS_NONE", "ARGS_FIXED", "ARGS_ATLEAST", "ARGS_ANY", "ARGS_ONE",
 __all__ = ["DEBUG_OPT", "NOHDR_OPT", "SEP_OPT", "GenericMain", "SubmitOpCode",
            "cli_option", "GenerateTable", "AskUser",
            "ARGS_NONE", "ARGS_FIXED", "ARGS_ATLEAST", "ARGS_ANY", "ARGS_ONE",
-           "USEUNITS_OPT", "FIELDS_OPT", "FORCE_OPT"]
+           "USEUNITS_OPT", "FIELDS_OPT", "FORCE_OPT",
+           "ListTags", "AddTags", "RemoveTags", "TAG_SRC_OPT",
+           "FormatError",
+           ]
+
+
+def _ExtractTagsObject(opts, args):
+  """Extract the tag type object.
+
+  Note that this function will modify its args parameter.
+
+  """
+  if not hasattr(opts, "tag_type"):
+    raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
+  kind = opts.tag_type
+  if kind == constants.TAG_CLUSTER:
+    retval = kind, kind
+  elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
+    if not args:
+      raise errors.OpPrereqError("no arguments passed to the command")
+    name = args.pop(0)
+    retval = kind, name
+  else:
+    raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
+  return retval
+
+
+def _ExtendTags(opts, args):
+  """Extend the args if a source file has been given.
+
+  This function will extend the tags with the contents of the file
+  passed in the 'tags_source' attribute of the opts parameter. A file
+  named '-' will be replaced by stdin.
+
+  """
+  fname = opts.tags_source
+  if fname is None:
+    return
+  if fname == "-":
+    new_fh = sys.stdin
+  else:
+    new_fh = open(fname, "r")
+  new_data = []
+  try:
+    # we don't use the nice 'new_data = [line.strip() for line in fh]'
+    # because of python bug 1633941
+    while True:
+      line = new_fh.readline()
+      if not line:
+        break
+      new_data.append(line.strip())
+  finally:
+    new_fh.close()
+  args.extend(new_data)
+
+
+def ListTags(opts, args):
+  """List the tags on a given object.
+
+  This is a generic implementation that knows how to deal with all
+  three cases of tag objects (cluster, node, instance). The opts
+  argument is expected to contain a tag_type field denoting what
+  object type we work on.
+
+  """
+  kind, name = _ExtractTagsObject(opts, args)
+  op = opcodes.OpGetTags(kind=kind, name=name)
+  result = SubmitOpCode(op)
+  result = list(result)
+  result.sort()
+  for tag in result:
+    print tag
+
+
+def AddTags(opts, args):
+  """Add tags on a given object.
+
+  This is a generic implementation that knows how to deal with all
+  three cases of tag objects (cluster, node, instance). The opts
+  argument is expected to contain a tag_type field denoting what
+  object type we work on.
+
+  """
+  kind, name = _ExtractTagsObject(opts, args)
+  _ExtendTags(opts, args)
+  if not args:
+    raise errors.OpPrereqError("No tags to be added")
+  op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
+  SubmitOpCode(op)
+
+
+def RemoveTags(opts, args):
+  """Remove tags from a given object.
+
+  This is a generic implementation that knows how to deal with all
+  three cases of tag objects (cluster, node, instance). The opts
+  argument is expected to contain a tag_type field denoting what
+  object type we work on.
+
+  """
+  kind, name = _ExtractTagsObject(opts, args)
+  _ExtendTags(opts, args)
+  if not args:
+    raise errors.OpPrereqError("No tags to be removed")
+  op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
+  SubmitOpCode(op)
+
 
 DEBUG_OPT = make_option("-d", "--debug", default=False,
                         action="store_true",
 
 DEBUG_OPT = make_option("-d", "--debug", default=False,
                         action="store_true",
@@ -68,6 +176,8 @@ FORCE_OPT = make_option("-f", "--force", dest="force", action="store_true",
 _LOCK_OPT = make_option("--lock-retries", default=None,
                         type="int", help=SUPPRESS_HELP)
 
 _LOCK_OPT = make_option("--lock-retries", default=None,
                         type="int", help=SUPPRESS_HELP)
 
+TAG_SRC_OPT = make_option("--from", dest="tags_source",
+                          default=None, help="File with tag names")
 
 def ARGS_FIXED(val):
   """Macro-like function denoting a fixed number of arguments"""
 
 def ARGS_FIXED(val):
   """Macro-like function denoting a fixed number of arguments"""
@@ -201,7 +311,7 @@ def AskUser(text, choices=None):
     new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
   text = "\n".join(new_text)
   try:
     new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
   text = "\n".join(new_text)
   try:
-    f = file("/dev/tty", "r+")
+    f = file("/dev/tty", "a+")
   except IOError:
     return answer
   try:
   except IOError:
     return answer
   try:
@@ -228,7 +338,7 @@ def AskUser(text, choices=None):
   return answer
 
 
   return answer
 
 
-def SubmitOpCode(op):
+def SubmitOpCode(op, proc=None, feedback_fn=None):
   """Function to submit an opcode.
 
   This is just a simple wrapper over the construction of the processor
   """Function to submit an opcode.
 
   This is just a simple wrapper over the construction of the processor
@@ -236,15 +346,71 @@ def SubmitOpCode(op):
   interaction functions.
 
   """
   interaction functions.
 
   """
-  proc = mcpu.Processor()
-  return proc.ExecOpCode(op, logger.ToStdout)
+  if proc is None:
+    proc = mcpu.Processor()
+  if feedback_fn is None:
+    feedback_fn = logger.ToStdout
+  return proc.ExecOpCode(op, feedback_fn)
+
+
+def FormatError(err):
+  """Return a formatted error message for a given error.
+
+  This function takes an exception instance and returns a tuple
+  consisting of two values: first, the recommended exit code, and
+  second, a string describing the error message (not
+  newline-terminated).
+
+  """
+  retcode = 1
+  obuf = StringIO()
+  if isinstance(err, errors.ConfigurationError):
+    msg = "Corrupt configuration file: %s" % err
+    logger.Error(msg)
+    obuf.write(msg + "\n")
+    obuf.write("Aborting.")
+    retcode = 2
+  elif isinstance(err, errors.HooksAbort):
+    obuf.write("Failure: hooks execution failed:\n")
+    for node, script, out in err.args[0]:
+      if out:
+        obuf.write("  node: %s, script: %s, output: %s\n" %
+                   (node, script, out))
+      else:
+        obuf.write("  node: %s, script: %s (no output)\n" %
+                   (node, script))
+  elif isinstance(err, errors.HooksFailure):
+    obuf.write("Failure: hooks general failure: %s" % str(err))
+  elif isinstance(err, errors.ResolverError):
+    this_host = utils.HostInfo.SysName()
+    if err.args[0] == this_host:
+      msg = "Failure: can't resolve my own hostname ('%s')"
+    else:
+      msg = "Failure: can't resolve hostname '%s'"
+    obuf.write(msg % err.args[0])
+  elif isinstance(err, errors.OpPrereqError):
+    obuf.write("Failure: prerequisites not met for this"
+               " operation:\n%s" % str(err))
+  elif isinstance(err, errors.OpExecError):
+    obuf.write("Failure: command execution error:\n%s" % str(err))
+  elif isinstance(err, errors.TagError):
+    obuf.write("Failure: invalid tag(s) given:\n%s" % str(err))
+  elif isinstance(err, errors.GenericError):
+    obuf.write("Unhandled Ganeti error: %s" % str(err))
+  else:
+    obuf.write("Unhandled exception: %s" % str(err))
+  return retcode, obuf.getvalue().rstrip('\n')
 
 
 
 
-def GenericMain(commands):
+def GenericMain(commands, override=None):
   """Generic main function for all the gnt-* commands.
 
   """Generic main function for all the gnt-* commands.
 
-  Argument: a dictionary with a special structure, see the design doc
-  for command line handling.
+  Arguments:
+    - commands: a dictionary with a special structure, see the design doc
+                for command line handling.
+    - override: if not None, we expect a dictionary with keys that will
+                override command line options; this can be used to pass
+                options from the scripts to generic functions
 
   """
   # save the program name and the entire command line for later logging
 
   """
   # save the program name and the entire command line for later logging
@@ -263,6 +429,10 @@ def GenericMain(commands):
   if func is None: # parse error
     return 1
 
   if func is None: # parse error
     return 1
 
+  if override is not None:
+    for key, val in override.iteritems():
+      setattr(options, key, val)
+
   logger.SetupLogging(debug=options.debug, program=binary)
 
   try:
   logger.SetupLogging(debug=options.debug, program=binary)
 
   try:
@@ -279,38 +449,9 @@ def GenericMain(commands):
   try:
     try:
       result = func(options, args)
   try:
     try:
       result = func(options, args)
-    except errors.ConfigurationError, err:
-      logger.Error("Corrupt configuration file: %s" % err)
-      logger.ToStderr("Aborting.")
-      result = 2
-    except errors.HooksAbort, err:
-      logger.ToStderr("Failure: hooks execution failed:")
-      for node, script, out in err.args[0]:
-        if out:
-          logger.ToStderr("  node: %s, script: %s, output: %s" %
-                          (node, script, out))
-        else:
-          logger.ToStderr("  node: %s, script: %s (no output)" %
-                          (node, script))
-      result = 1
-    except errors.HooksFailure, err:
-      logger.ToStderr("Failure: hooks general failure: %s" % str(err))
-      result = 1
-    except errors.ResolverError, err:
-      this_host = utils.HostInfo.SysName()
-      if err.args[0] == this_host:
-        msg = "Failure: can't resolve my own hostname ('%s')"
-      else:
-        msg = "Failure: can't resolve hostname '%s'"
-      logger.ToStderr(msg % err.args[0])
-      result = 1
-    except errors.OpPrereqError, err:
-      logger.ToStderr("Failure: prerequisites not met for this"
-                      " operation:\n%s" % str(err))
-      result = 1
-    except errors.OpExecError, err:
-      logger.ToStderr("Failure: command execution error:\n%s" % str(err))
-      result = 1
+    except errors.GenericError, err:
+      result, err_msg = FormatError(err)
+      logger.ToStderr(err_msg)
   finally:
     utils.Unlock('cmd')
     utils.LockCleanup()
   finally:
     utils.Unlock('cmd')
     utils.LockCleanup()
@@ -338,6 +479,9 @@ def GenerateTable(headers, fields, separator, data,
 
   format_fields = []
   for field in fields:
 
   format_fields = []
   for field in fields:
+    if headers and field not in headers:
+      raise errors.ProgrammerError("Missing header description for field '%s'"
+                                   % field)
     if separator is not None:
       format_fields.append("%s")
     elif field in numfields:
     if separator is not None:
       format_fields.append("%s")
     elif field in numfields:
@@ -360,6 +504,7 @@ def GenerateTable(headers, fields, separator, data,
           pass
         else:
           val = row[idx] = utils.FormatUnit(val)
           pass
         else:
           val = row[idx] = utils.FormatUnit(val)
+      val = row[idx] = str(val)
       if separator is None:
         mlens[idx] = max(mlens[idx], len(val))
 
       if separator is None:
         mlens[idx] = max(mlens[idx], len(val))