4 # Copyright (C) 2006, 2007 Google Inc.
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 # General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
22 """Module dealing with command line parsing"""
31 from cStringIO import StringIO
33 from ganeti import utils
34 from ganeti import errors
35 from ganeti import constants
36 from ganeti import opcodes
37 from ganeti import luxi
38 from ganeti import ssconf
39 from ganeti import rpc
41 from optparse import (OptionParser, make_option, TitledHelpFormatter,
42 Option, OptionValueError)
44 __all__ = ["DEBUG_OPT", "NOHDR_OPT", "SEP_OPT", "GenericMain",
45 "SubmitOpCode", "GetClient",
46 "cli_option", "ikv_option", "keyval_option",
47 "GenerateTable", "AskUser",
48 "ARGS_NONE", "ARGS_FIXED", "ARGS_ATLEAST", "ARGS_ANY", "ARGS_ONE",
49 "USEUNITS_OPT", "FIELDS_OPT", "FORCE_OPT", "SUBMIT_OPT",
50 "ListTags", "AddTags", "RemoveTags", "TAG_SRC_OPT",
51 "FormatError", "SplitNodeOption", "SubmitOrSend",
52 "JobSubmittedException", "FormatTimestamp", "ParseTimespec",
54 "ToStderr", "ToStdout",
60 def _ExtractTagsObject(opts, args):
61 """Extract the tag type object.
63 Note that this function will modify its args parameter.
66 if not hasattr(opts, "tag_type"):
67 raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
69 if kind == constants.TAG_CLUSTER:
71 elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
73 raise errors.OpPrereqError("no arguments passed to the command")
77 raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
81 def _ExtendTags(opts, args):
82 """Extend the args if a source file has been given.
84 This function will extend the tags with the contents of the file
85 passed in the 'tags_source' attribute of the opts parameter. A file
86 named '-' will be replaced by stdin.
89 fname = opts.tags_source
95 new_fh = open(fname, "r")
98 # we don't use the nice 'new_data = [line.strip() for line in fh]'
99 # because of python bug 1633941
101 line = new_fh.readline()
104 new_data.append(line.strip())
107 args.extend(new_data)
110 def ListTags(opts, args):
111 """List the tags on a given object.
113 This is a generic implementation that knows how to deal with all
114 three cases of tag objects (cluster, node, instance). The opts
115 argument is expected to contain a tag_type field denoting what
116 object type we work on.
119 kind, name = _ExtractTagsObject(opts, args)
120 op = opcodes.OpGetTags(kind=kind, name=name)
121 result = SubmitOpCode(op)
122 result = list(result)
128 def AddTags(opts, args):
129 """Add tags on a given object.
131 This is a generic implementation that knows how to deal with all
132 three cases of tag objects (cluster, node, instance). The opts
133 argument is expected to contain a tag_type field denoting what
134 object type we work on.
137 kind, name = _ExtractTagsObject(opts, args)
138 _ExtendTags(opts, args)
140 raise errors.OpPrereqError("No tags to be added")
141 op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
145 def RemoveTags(opts, args):
146 """Remove tags from a given object.
148 This is a generic implementation that knows how to deal with all
149 three cases of tag objects (cluster, node, instance). The opts
150 argument is expected to contain a tag_type field denoting what
151 object type we work on.
154 kind, name = _ExtractTagsObject(opts, args)
155 _ExtendTags(opts, args)
157 raise errors.OpPrereqError("No tags to be removed")
158 op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
162 DEBUG_OPT = make_option("-d", "--debug", default=False,
164 help="Turn debugging on")
166 NOHDR_OPT = make_option("--no-headers", default=False,
167 action="store_true", dest="no_headers",
168 help="Don't display column headers")
170 SEP_OPT = make_option("--separator", default=None,
171 action="store", dest="separator",
172 help="Separator between output fields"
173 " (defaults to one space)")
175 USEUNITS_OPT = make_option("--units", default=None,
176 dest="units", choices=('h', 'm', 'g', 't'),
177 help="Specify units for output (one of hmgt)")
179 FIELDS_OPT = make_option("-o", "--output", dest="output", action="store",
180 type="string", help="Comma separated list of"
184 FORCE_OPT = make_option("-f", "--force", dest="force", action="store_true",
185 default=False, help="Force the operation")
187 TAG_SRC_OPT = make_option("--from", dest="tags_source",
188 default=None, help="File with tag names")
190 SUBMIT_OPT = make_option("--submit", dest="submit_only",
191 default=False, action="store_true",
192 help="Submit the job and return the job ID, but"
193 " don't wait for the job to finish")
197 """Macro-like function denoting a fixed number of arguments"""
201 def ARGS_ATLEAST(val):
202 """Macro-like function denoting a minimum number of arguments"""
207 ARGS_ONE = ARGS_FIXED(1)
208 ARGS_ANY = ARGS_ATLEAST(0)
211 def check_unit(option, opt, value):
212 """OptParsers custom converter for units.
216 return utils.ParseUnit(value)
217 except errors.UnitParseError, err:
218 raise OptionValueError("option %s: %s" % (opt, err))
221 class CliOption(Option):
222 """Custom option class for optparse.
225 TYPES = Option.TYPES + ("unit",)
226 TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
227 TYPE_CHECKER["unit"] = check_unit
230 def _SplitKeyVal(opt, data):
231 """Convert a KeyVal string into a dict.
233 This function will convert a key=val[,...] string into a dict. Empty
234 values will be converted specially: keys which have the prefix 'no_'
235 will have the value=False and the prefix stripped, the others will
239 @param opt: a string holding the option name for which we process the
240 data, used in building error messages
242 @param data: a string of the format key=val,key=val,...
244 @return: {key=val, key=val}
245 @raises errors.ParameterError: if there are duplicate keys
251 for elem in data.split(","):
253 key, val = elem.split("=", 1)
255 if elem.startswith(NO_PREFIX):
256 key, val = elem[len(NO_PREFIX):], False
257 elif elem.startswith(UN_PREFIX):
258 key, val = elem[len(UN_PREFIX):], None
260 key, val = elem, True
262 raise errors.ParameterError("Duplicate key '%s' in option %s" %
268 def check_ident_key_val(option, opt, value):
269 """Custom parser for the IdentKeyVal option type.
275 ident, rest = value.split(":", 1)
276 kv_dict = _SplitKeyVal(opt, rest)
277 retval = (ident, kv_dict)
281 class IdentKeyValOption(Option):
282 """Custom option class for ident:key=val,key=val options.
284 This will store the parsed values as a tuple (ident, {key: val}). As
285 such, multiple uses of this option via action=append is possible.
288 TYPES = Option.TYPES + ("identkeyval",)
289 TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
290 TYPE_CHECKER["identkeyval"] = check_ident_key_val
293 def check_key_val(option, opt, value):
294 """Custom parser for the KeyVal option type.
297 return _SplitKeyVal(opt, value)
300 class KeyValOption(Option):
301 """Custom option class for key=val,key=val options.
303 This will store the parsed values as a dict {key: val}.
306 TYPES = Option.TYPES + ("keyval",)
307 TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
308 TYPE_CHECKER["keyval"] = check_key_val
311 # optparse.py sets make_option, so we do it for our own option class, too
312 cli_option = CliOption
313 ikv_option = IdentKeyValOption
314 keyval_option = KeyValOption
317 def _ParseArgs(argv, commands, aliases):
318 """Parser for the command line arguments.
320 This function parses the arguements and returns the function which
321 must be executed together with its (modified) arguments.
323 @param argv: the command line
324 @param commands: dictionary with special contents, see the design
325 doc for cmdline handling
326 @param aliases: dictionary with command aliases {'alias': 'target, ...}
332 binary = argv[0].split("/")[-1]
334 if len(argv) > 1 and argv[1] == "--version":
335 print "%s (ganeti) %s" % (binary, constants.RELEASE_VERSION)
336 # Quit right away. That way we don't have to care about this special
337 # argument. optparse.py does it the same.
340 if len(argv) < 2 or not (argv[1] in commands or
342 # let's do a nice thing
343 sortedcmds = commands.keys()
345 print ("Usage: %(bin)s {command} [options...] [argument...]"
346 "\n%(bin)s <command> --help to see details, or"
347 " man %(bin)s\n" % {"bin": binary})
348 # compute the max line length for cmd + usage
349 mlen = max([len(" %s" % cmd) for cmd in commands])
350 mlen = min(60, mlen) # should not get here...
351 # and format a nice command list
353 for cmd in sortedcmds:
354 cmdstr = " %s" % (cmd,)
355 help_text = commands[cmd][4]
356 help_lines = textwrap.wrap(help_text, 79-3-mlen)
357 print "%-*s - %s" % (mlen, cmdstr, help_lines.pop(0))
358 for line in help_lines:
359 print "%-*s %s" % (mlen, "", line)
361 return None, None, None
363 # get command, unalias it, and look it up in commands
367 raise errors.ProgrammerError("Alias '%s' overrides an existing"
370 if aliases[cmd] not in commands:
371 raise errors.ProgrammerError("Alias '%s' maps to non-existing"
372 " command '%s'" % (cmd, aliases[cmd]))
376 func, nargs, parser_opts, usage, description = commands[cmd]
377 parser = OptionParser(option_list=parser_opts,
378 description=description,
379 formatter=TitledHelpFormatter(),
380 usage="%%prog %s %s" % (cmd, usage))
381 parser.disable_interspersed_args()
382 options, args = parser.parse_args()
385 print >> sys.stderr, ("Error: Command %s expects no arguments" % cmd)
386 return None, None, None
387 elif nargs < 0 and len(args) != -nargs:
388 print >> sys.stderr, ("Error: Command %s expects %d argument(s)" %
390 return None, None, None
391 elif nargs >= 0 and len(args) < nargs:
392 print >> sys.stderr, ("Error: Command %s expects at least %d argument(s)" %
394 return None, None, None
396 return func, options, args
399 def SplitNodeOption(value):
400 """Splits the value of a --node option.
403 if value and ':' in value:
404 return value.split(':', 1)
409 def ValidateBeParams(bep):
410 """Parse and check the given beparams.
412 The function will update in-place the given dictionary.
415 @param bep: input beparams
416 @raise errors.ParameterError: if the input values are not OK
417 @raise errors.UnitParseError: if the input values are not OK
420 if constants.BE_MEMORY in bep:
421 bep[constants.BE_MEMORY] = utils.ParseUnit(bep[constants.BE_MEMORY])
423 if constants.BE_VCPUS in bep:
425 bep[constants.BE_VCPUS] = int(bep[constants.BE_VCPUS])
427 raise errors.ParameterError("Invalid number of VCPUs")
431 def wrapper(*args, **kwargs):
434 return fn(*args, **kwargs)
440 def AskUser(text, choices=None):
441 """Ask the user a question.
443 @param text: the question to ask
445 @param choices: list with elements tuples (input_char, return_value,
446 description); if not given, it will default to: [('y', True,
447 'Perform the operation'), ('n', False, 'Do no do the operation')];
448 note that the '?' char is reserved for help
450 @return: one of the return values from the choices list; if input is
451 not possible (i.e. not running with a tty, we return the last
456 choices = [('y', True, 'Perform the operation'),
457 ('n', False, 'Do not perform the operation')]
458 if not choices or not isinstance(choices, list):
459 raise errors.ProgrammerError("Invalid choiches argument to AskUser")
460 for entry in choices:
461 if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
462 raise errors.ProgrammerError("Invalid choiches element to AskUser")
464 answer = choices[-1][1]
466 for line in text.splitlines():
467 new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
468 text = "\n".join(new_text)
470 f = file("/dev/tty", "a+")
474 chars = [entry[0] for entry in choices]
475 chars[-1] = "[%s]" % chars[-1]
477 maps = dict([(entry[0], entry[1]) for entry in choices])
481 f.write("/".join(chars))
483 line = f.readline(2).strip().lower()
488 for entry in choices:
489 f.write(" %s - %s\n" % (entry[0], entry[2]))
497 class JobSubmittedException(Exception):
498 """Job was submitted, client should exit.
500 This exception has one argument, the ID of the job that was
501 submitted. The handler should print this ID.
503 This is not an error, just a structured way to exit from clients.
508 def SendJob(ops, cl=None):
509 """Function to submit an opcode without waiting for the results.
512 @param ops: list of opcodes
513 @type cl: luxi.Client
514 @param cl: the luxi client to use for communicating with the master;
515 if None, a new client will be created
521 job_id = cl.SubmitJob(ops)
526 def PollJob(job_id, cl=None, feedback_fn=None):
527 """Function to poll for the result of a job.
529 @type job_id: job identified
530 @param job_id: the job to poll for results
531 @type cl: luxi.Client
532 @param cl: the luxi client to use for communicating with the master;
533 if None, a new client will be created
540 prev_logmsg_serial = None
543 result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
546 # job not found, go away!
547 raise errors.JobLost("Job with id %s lost" % job_id)
549 # Split result, a tuple of (field values, log entries)
550 (job_info, log_entries) = result
551 (status, ) = job_info
554 for log_entry in log_entries:
555 (serial, timestamp, _, message) = log_entry
556 if callable(feedback_fn):
557 feedback_fn(log_entry[1:])
559 print "%s %s" % (time.ctime(utils.MergeTime(timestamp)), message)
560 prev_logmsg_serial = max(prev_logmsg_serial, serial)
562 # TODO: Handle canceled and archived jobs
563 elif status in (constants.JOB_STATUS_SUCCESS,
564 constants.JOB_STATUS_ERROR,
565 constants.JOB_STATUS_CANCELING,
566 constants.JOB_STATUS_CANCELED):
569 prev_job_info = job_info
571 jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
573 raise errors.JobLost("Job with id %s lost" % job_id)
575 status, opstatus, result = jobs[0]
576 if status == constants.JOB_STATUS_SUCCESS:
578 elif status in (constants.JOB_STATUS_CANCELING,
579 constants.JOB_STATUS_CANCELED):
580 raise errors.OpExecError("Job was canceled")
583 for idx, (status, msg) in enumerate(zip(opstatus, result)):
584 if status == constants.OP_STATUS_SUCCESS:
586 elif status == constants.OP_STATUS_ERROR:
588 raise errors.OpExecError("partial failure (opcode %d): %s" %
591 raise errors.OpExecError(str(msg))
592 # default failure mode
593 raise errors.OpExecError(result)
596 def SubmitOpCode(op, cl=None, feedback_fn=None):
597 """Legacy function to submit an opcode.
599 This is just a simple wrapper over the construction of the processor
600 instance. It should be extended to better handle feedback and
601 interaction functions.
607 job_id = SendJob([op], cl)
609 op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
614 def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
615 """Wrapper around SubmitOpCode or SendJob.
617 This function will decide, based on the 'opts' parameter, whether to
618 submit and wait for the result of the opcode (and return it), or
619 whether to just send the job and print its identifier. It is used in
620 order to simplify the implementation of the '--submit' option.
623 if opts and opts.submit_only:
624 job_id = SendJob([op], cl=cl)
625 raise JobSubmittedException(job_id)
627 return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
631 # TODO: Cache object?
633 client = luxi.Client()
634 except luxi.NoMasterError:
635 master, myself = ssconf.GetMasterAndMyself()
637 raise errors.OpPrereqError("This is not the master node, please connect"
638 " to node '%s' and rerun the command" %
645 def FormatError(err):
646 """Return a formatted error message for a given error.
648 This function takes an exception instance and returns a tuple
649 consisting of two values: first, the recommended exit code, and
650 second, a string describing the error message (not
657 if isinstance(err, errors.ConfigurationError):
658 txt = "Corrupt configuration file: %s" % msg
660 obuf.write(txt + "\n")
661 obuf.write("Aborting.")
663 elif isinstance(err, errors.HooksAbort):
664 obuf.write("Failure: hooks execution failed:\n")
665 for node, script, out in err.args[0]:
667 obuf.write(" node: %s, script: %s, output: %s\n" %
670 obuf.write(" node: %s, script: %s (no output)\n" %
672 elif isinstance(err, errors.HooksFailure):
673 obuf.write("Failure: hooks general failure: %s" % msg)
674 elif isinstance(err, errors.ResolverError):
675 this_host = utils.HostInfo.SysName()
676 if err.args[0] == this_host:
677 msg = "Failure: can't resolve my own hostname ('%s')"
679 msg = "Failure: can't resolve hostname '%s'"
680 obuf.write(msg % err.args[0])
681 elif isinstance(err, errors.OpPrereqError):
682 obuf.write("Failure: prerequisites not met for this"
683 " operation:\n%s" % msg)
684 elif isinstance(err, errors.OpExecError):
685 obuf.write("Failure: command execution error:\n%s" % msg)
686 elif isinstance(err, errors.TagError):
687 obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
688 elif isinstance(err, errors.JobQueueDrainError):
689 obuf.write("Failure: the job queue is marked for drain and doesn't"
690 " accept new requests\n")
691 elif isinstance(err, errors.JobQueueFull):
692 obuf.write("Failure: the job queue is full and doesn't accept new"
693 " job submissions until old jobs are archived\n")
694 elif isinstance(err, errors.GenericError):
695 obuf.write("Unhandled Ganeti error: %s" % msg)
696 elif isinstance(err, luxi.NoMasterError):
697 obuf.write("Cannot communicate with the master daemon.\nIs it running"
698 " and listening for connections?")
699 elif isinstance(err, luxi.TimeoutError):
700 obuf.write("Timeout while talking to the master daemon. Error:\n"
702 elif isinstance(err, luxi.ProtocolError):
703 obuf.write("Unhandled protocol error while talking to the master daemon:\n"
705 elif isinstance(err, JobSubmittedException):
706 obuf.write("JobID: %s\n" % err.args[0])
709 obuf.write("Unhandled exception: %s" % msg)
710 return retcode, obuf.getvalue().rstrip('\n')
713 def GenericMain(commands, override=None, aliases=None):
714 """Generic main function for all the gnt-* commands.
717 - commands: a dictionary with a special structure, see the design doc
718 for command line handling.
719 - override: if not None, we expect a dictionary with keys that will
720 override command line options; this can be used to pass
721 options from the scripts to generic functions
722 - aliases: dictionary with command aliases {'alias': 'target, ...}
725 # save the program name and the entire command line for later logging
727 binary = os.path.basename(sys.argv[0]) or sys.argv[0]
728 if len(sys.argv) >= 2:
729 binary += " " + sys.argv[1]
730 old_cmdline = " ".join(sys.argv[2:])
734 binary = "<unknown program>"
740 func, options, args = _ParseArgs(sys.argv, commands, aliases)
741 if func is None: # parse error
744 if override is not None:
745 for key, val in override.iteritems():
746 setattr(options, key, val)
748 utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
749 stderr_logging=True, program=binary)
751 utils.debug = options.debug
754 logging.info("run with arguments '%s'", old_cmdline)
756 logging.info("run with no arguments")
759 result = func(options, args)
760 except (errors.GenericError, luxi.ProtocolError,
761 JobSubmittedException), err:
762 result, err_msg = FormatError(err)
763 logging.exception("Error durring command processing")
769 def GenerateTable(headers, fields, separator, data,
770 numfields=None, unitfields=None,
772 """Prints a table with headers and different fields.
775 @param headers: dictionary mapping field names to headers for
778 @param fields: the field names corresponding to each row in
780 @param separator: the separator to be used; if this is None,
781 the default 'smart' algorithm is used which computes optimal
782 field width, otherwise just the separator is used between
785 @param data: a list of lists, each sublist being one row to be output
786 @type numfields: list
787 @param numfields: a list with the fields that hold numeric
788 values and thus should be right-aligned
789 @type unitfields: list
790 @param unitfields: a list with the fields that hold numeric
791 values that should be formatted with the units field
792 @type units: string or None
793 @param units: the units we should use for formatting, or None for
794 automatic choice (human-readable for non-separator usage, otherwise
795 megabytes); this is a one-letter string
804 if numfields is None:
806 if unitfields is None:
809 numfields = utils.FieldSet(*numfields)
810 unitfields = utils.FieldSet(*unitfields)
814 if headers and field not in headers:
815 # FIXME: handle better unknown fields (either revert to old
816 # style of raising exception, or deal more intelligently with
818 headers[field] = field
819 if separator is not None:
820 format_fields.append("%s")
821 elif numfields.Matches(field):
822 format_fields.append("%*s")
824 format_fields.append("%-*s")
826 if separator is None:
827 mlens = [0 for name in fields]
828 format = ' '.join(format_fields)
830 format = separator.replace("%", "%%").join(format_fields)
833 for idx, val in enumerate(row):
834 if unitfields.Matches(fields[idx]):
840 val = row[idx] = utils.FormatUnit(val, units)
841 val = row[idx] = str(val)
842 if separator is None:
843 mlens[idx] = max(mlens[idx], len(val))
848 for idx, name in enumerate(fields):
850 if separator is None:
851 mlens[idx] = max(mlens[idx], len(hdr))
852 args.append(mlens[idx])
854 result.append(format % tuple(args))
858 for idx in xrange(len(fields)):
859 if separator is None:
860 args.append(mlens[idx])
861 args.append(line[idx])
862 result.append(format % tuple(args))
867 def FormatTimestamp(ts):
868 """Formats a given timestamp.
871 @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
874 @returns: a string with the formatted timestamp
877 if not isinstance (ts, (tuple, list)) or len(ts) != 2:
880 return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
883 def ParseTimespec(value):
884 """Parse a time specification.
886 The following suffixed will be recognized:
894 Without any suffix, the value will be taken to be in seconds.
899 raise errors.OpPrereqError("Empty time specification passed")
907 if value[-1] not in suffix_map:
911 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
913 multiplier = suffix_map[value[-1]]
915 if not value: # no data left after stripping the suffix
916 raise errors.OpPrereqError("Invalid time specification (only"
919 value = int(value) * multiplier
921 raise errors.OpPrereqError("Invalid time specification '%s'" % value)
925 def GetOnlineNodes(nodes, cl=None, nowarn=False):
926 """Returns the names of online nodes.
928 This function will also log a warning on stderr with the names of
931 @param nodes: if not empty, use only this subset of nodes (minus the
933 @param cl: if not None, luxi client to use
934 @type nowarn: boolean
935 @param nowarn: by default, this function will output a note with the
936 offline nodes that are skipped; if this parameter is True the
937 note is not displayed
943 op = opcodes.OpQueryNodes(output_fields=["name", "offline"],
945 result = SubmitOpCode(op, cl=cl)
946 offline = [row[0] for row in result if row[1]]
947 if offline and not nowarn:
948 ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
949 return [row[0] for row in result if not row[1]]
952 def _ToStream(stream, txt, *args):
953 """Write a message to a stream, bypassing the logging system
955 @type stream: file object
956 @param stream: the file to which we should write
958 @param txt: the message
963 stream.write(txt % args)
970 def ToStdout(txt, *args):
971 """Write a message to stdout only, bypassing the logging system
973 This is just a wrapper over _ToStream.
976 @param txt: the message
979 _ToStream(sys.stdout, txt, *args)
982 def ToStderr(txt, *args):
983 """Write a message to stderr only, bypassing the logging system
985 This is just a wrapper over _ToStream.
988 @param txt: the message
991 _ToStream(sys.stderr, txt, *args)