Move function cleaning directory to module level
[ganeti-local] / lib / cli.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007 Google Inc.
5 #
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.
10 #
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.
15 #
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
19 # 02110-1301, USA.
20
21
22 """Module dealing with command line parsing"""
23
24
25 import sys
26 import textwrap
27 import os.path
28 import copy
29 import time
30 from cStringIO import StringIO
31
32 from ganeti import utils
33 from ganeti import logger
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
40 from optparse import (OptionParser, make_option, TitledHelpFormatter,
41                       Option, OptionValueError)
42
43 __all__ = ["DEBUG_OPT", "NOHDR_OPT", "SEP_OPT", "GenericMain",
44            "SubmitOpCode", "GetClient",
45            "cli_option", "GenerateTable", "AskUser",
46            "ARGS_NONE", "ARGS_FIXED", "ARGS_ATLEAST", "ARGS_ANY", "ARGS_ONE",
47            "USEUNITS_OPT", "FIELDS_OPT", "FORCE_OPT", "SUBMIT_OPT",
48            "ListTags", "AddTags", "RemoveTags", "TAG_SRC_OPT",
49            "FormatError", "SplitNodeOption", "SubmitOrSend",
50            ]
51
52
53 def _ExtractTagsObject(opts, args):
54   """Extract the tag type object.
55
56   Note that this function will modify its args parameter.
57
58   """
59   if not hasattr(opts, "tag_type"):
60     raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
61   kind = opts.tag_type
62   if kind == constants.TAG_CLUSTER:
63     retval = kind, kind
64   elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
65     if not args:
66       raise errors.OpPrereqError("no arguments passed to the command")
67     name = args.pop(0)
68     retval = kind, name
69   else:
70     raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
71   return retval
72
73
74 def _ExtendTags(opts, args):
75   """Extend the args if a source file has been given.
76
77   This function will extend the tags with the contents of the file
78   passed in the 'tags_source' attribute of the opts parameter. A file
79   named '-' will be replaced by stdin.
80
81   """
82   fname = opts.tags_source
83   if fname is None:
84     return
85   if fname == "-":
86     new_fh = sys.stdin
87   else:
88     new_fh = open(fname, "r")
89   new_data = []
90   try:
91     # we don't use the nice 'new_data = [line.strip() for line in fh]'
92     # because of python bug 1633941
93     while True:
94       line = new_fh.readline()
95       if not line:
96         break
97       new_data.append(line.strip())
98   finally:
99     new_fh.close()
100   args.extend(new_data)
101
102
103 def ListTags(opts, args):
104   """List the tags on a given object.
105
106   This is a generic implementation that knows how to deal with all
107   three cases of tag objects (cluster, node, instance). The opts
108   argument is expected to contain a tag_type field denoting what
109   object type we work on.
110
111   """
112   kind, name = _ExtractTagsObject(opts, args)
113   op = opcodes.OpGetTags(kind=kind, name=name)
114   result = SubmitOpCode(op)
115   result = list(result)
116   result.sort()
117   for tag in result:
118     print tag
119
120
121 def AddTags(opts, args):
122   """Add tags on a given object.
123
124   This is a generic implementation that knows how to deal with all
125   three cases of tag objects (cluster, node, instance). The opts
126   argument is expected to contain a tag_type field denoting what
127   object type we work on.
128
129   """
130   kind, name = _ExtractTagsObject(opts, args)
131   _ExtendTags(opts, args)
132   if not args:
133     raise errors.OpPrereqError("No tags to be added")
134   op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
135   SubmitOpCode(op)
136
137
138 def RemoveTags(opts, args):
139   """Remove tags from a given object.
140
141   This is a generic implementation that knows how to deal with all
142   three cases of tag objects (cluster, node, instance). The opts
143   argument is expected to contain a tag_type field denoting what
144   object type we work on.
145
146   """
147   kind, name = _ExtractTagsObject(opts, args)
148   _ExtendTags(opts, args)
149   if not args:
150     raise errors.OpPrereqError("No tags to be removed")
151   op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
152   SubmitOpCode(op)
153
154
155 DEBUG_OPT = make_option("-d", "--debug", default=False,
156                         action="store_true",
157                         help="Turn debugging on")
158
159 NOHDR_OPT = make_option("--no-headers", default=False,
160                         action="store_true", dest="no_headers",
161                         help="Don't display column headers")
162
163 SEP_OPT = make_option("--separator", default=None,
164                       action="store", dest="separator",
165                       help="Separator between output fields"
166                       " (defaults to one space)")
167
168 USEUNITS_OPT = make_option("--human-readable", default=False,
169                            action="store_true", dest="human_readable",
170                            help="Print sizes in human readable format")
171
172 FIELDS_OPT = make_option("-o", "--output", dest="output", action="store",
173                          type="string", help="Comma separated list of"
174                          " output fields",
175                          metavar="FIELDS")
176
177 FORCE_OPT = make_option("-f", "--force", dest="force", action="store_true",
178                         default=False, help="Force the operation")
179
180 TAG_SRC_OPT = make_option("--from", dest="tags_source",
181                           default=None, help="File with tag names")
182
183 SUBMIT_OPT = make_option("--submit", dest="submit_only",
184                          default=False, action="store_true",
185                          help="Submit the job and return the job ID, but"
186                          " don't wait for the job to finish")
187
188
189 def ARGS_FIXED(val):
190   """Macro-like function denoting a fixed number of arguments"""
191   return -val
192
193
194 def ARGS_ATLEAST(val):
195   """Macro-like function denoting a minimum number of arguments"""
196   return val
197
198
199 ARGS_NONE = None
200 ARGS_ONE = ARGS_FIXED(1)
201 ARGS_ANY = ARGS_ATLEAST(0)
202
203
204 def check_unit(option, opt, value):
205   """OptParsers custom converter for units.
206
207   """
208   try:
209     return utils.ParseUnit(value)
210   except errors.UnitParseError, err:
211     raise OptionValueError("option %s: %s" % (opt, err))
212
213
214 class CliOption(Option):
215   """Custom option class for optparse.
216
217   """
218   TYPES = Option.TYPES + ("unit",)
219   TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
220   TYPE_CHECKER["unit"] = check_unit
221
222
223 # optparse.py sets make_option, so we do it for our own option class, too
224 cli_option = CliOption
225
226
227 def _ParseArgs(argv, commands, aliases):
228   """Parses the command line and return the function which must be
229   executed together with its arguments
230
231   Arguments:
232     argv: the command line
233
234     commands: dictionary with special contents, see the design doc for
235     cmdline handling
236     aliases: dictionary with command aliases {'alias': 'target, ...}
237
238   """
239   if len(argv) == 0:
240     binary = "<command>"
241   else:
242     binary = argv[0].split("/")[-1]
243
244   if len(argv) > 1 and argv[1] == "--version":
245     print "%s (ganeti) %s" % (binary, constants.RELEASE_VERSION)
246     # Quit right away. That way we don't have to care about this special
247     # argument. optparse.py does it the same.
248     sys.exit(0)
249
250   if len(argv) < 2 or not (argv[1] in commands or
251                            argv[1] in aliases):
252     # let's do a nice thing
253     sortedcmds = commands.keys()
254     sortedcmds.sort()
255     print ("Usage: %(bin)s {command} [options...] [argument...]"
256            "\n%(bin)s <command> --help to see details, or"
257            " man %(bin)s\n" % {"bin": binary})
258     # compute the max line length for cmd + usage
259     mlen = max([len(" %s" % cmd) for cmd in commands])
260     mlen = min(60, mlen) # should not get here...
261     # and format a nice command list
262     print "Commands:"
263     for cmd in sortedcmds:
264       cmdstr = " %s" % (cmd,)
265       help_text = commands[cmd][4]
266       help_lines = textwrap.wrap(help_text, 79-3-mlen)
267       print "%-*s - %s" % (mlen, cmdstr, help_lines.pop(0))
268       for line in help_lines:
269         print "%-*s   %s" % (mlen, "", line)
270     print
271     return None, None, None
272
273   # get command, unalias it, and look it up in commands
274   cmd = argv.pop(1)
275   if cmd in aliases:
276     if cmd in commands:
277       raise errors.ProgrammerError("Alias '%s' overrides an existing"
278                                    " command" % cmd)
279
280     if aliases[cmd] not in commands:
281       raise errors.ProgrammerError("Alias '%s' maps to non-existing"
282                                    " command '%s'" % (cmd, aliases[cmd]))
283
284     cmd = aliases[cmd]
285
286   func, nargs, parser_opts, usage, description = commands[cmd]
287   parser = OptionParser(option_list=parser_opts,
288                         description=description,
289                         formatter=TitledHelpFormatter(),
290                         usage="%%prog %s %s" % (cmd, usage))
291   parser.disable_interspersed_args()
292   options, args = parser.parse_args()
293   if nargs is None:
294     if len(args) != 0:
295       print >> sys.stderr, ("Error: Command %s expects no arguments" % cmd)
296       return None, None, None
297   elif nargs < 0 and len(args) != -nargs:
298     print >> sys.stderr, ("Error: Command %s expects %d argument(s)" %
299                          (cmd, -nargs))
300     return None, None, None
301   elif nargs >= 0 and len(args) < nargs:
302     print >> sys.stderr, ("Error: Command %s expects at least %d argument(s)" %
303                          (cmd, nargs))
304     return None, None, None
305
306   return func, options, args
307
308
309 def SplitNodeOption(value):
310   """Splits the value of a --node option.
311
312   """
313   if value and ':' in value:
314     return value.split(':', 1)
315   else:
316     return (value, None)
317
318
319 def AskUser(text, choices=None):
320   """Ask the user a question.
321
322   Args:
323     text - the question to ask.
324
325     choices - list with elements tuples (input_char, return_value,
326     description); if not given, it will default to: [('y', True,
327     'Perform the operation'), ('n', False, 'Do no do the operation')];
328     note that the '?' char is reserved for help
329
330   Returns: one of the return values from the choices list; if input is
331   not possible (i.e. not running with a tty, we return the last entry
332   from the list
333
334   """
335   if choices is None:
336     choices = [('y', True, 'Perform the operation'),
337                ('n', False, 'Do not perform the operation')]
338   if not choices or not isinstance(choices, list):
339     raise errors.ProgrammerError("Invalid choiches argument to AskUser")
340   for entry in choices:
341     if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
342       raise errors.ProgrammerError("Invalid choiches element to AskUser")
343
344   answer = choices[-1][1]
345   new_text = []
346   for line in text.splitlines():
347     new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
348   text = "\n".join(new_text)
349   try:
350     f = file("/dev/tty", "a+")
351   except IOError:
352     return answer
353   try:
354     chars = [entry[0] for entry in choices]
355     chars[-1] = "[%s]" % chars[-1]
356     chars.append('?')
357     maps = dict([(entry[0], entry[1]) for entry in choices])
358     while True:
359       f.write(text)
360       f.write('\n')
361       f.write("/".join(chars))
362       f.write(": ")
363       line = f.readline(2).strip().lower()
364       if line in maps:
365         answer = maps[line]
366         break
367       elif line == '?':
368         for entry in choices:
369           f.write(" %s - %s\n" % (entry[0], entry[2]))
370         f.write("\n")
371         continue
372   finally:
373     f.close()
374   return answer
375
376
377 def SendJob(ops, cl=None):
378   """Function to submit an opcode without waiting for the results.
379
380   @type ops: list
381   @param ops: list of opcodes
382   @type cl: luxi.Client
383   @param cl: the luxi client to use for communicating with the master;
384              if None, a new client will be created
385
386   """
387   if cl is None:
388     cl = GetClient()
389
390   job_id = cl.SubmitJob(ops)
391
392   return job_id
393
394
395 def PollJob(job_id, cl=None, feedback_fn=None):
396   """Function to poll for the result of a job.
397
398   @type job_id: job identified
399   @param job_id: the job to poll for results
400   @type cl: luxi.Client
401   @param cl: the luxi client to use for communicating with the master;
402              if None, a new client will be created
403
404   """
405   if cl is None:
406     cl = GetClient()
407
408   lastmsg = None
409   while True:
410     jobs = cl.QueryJobs([job_id], ["status", "ticker"])
411     if not jobs:
412       # job not found, go away!
413       raise errors.JobLost("Job with id %s lost" % job_id)
414
415     # TODO: Handle canceled and archived jobs
416     status = jobs[0][0]
417     if status in (constants.JOB_STATUS_SUCCESS, constants.JOB_STATUS_ERROR):
418       break
419     msg = jobs[0][1]
420     if msg is not None and msg != lastmsg:
421       if callable(feedback_fn):
422         feedback_fn(msg)
423       else:
424         print "%s %s" % (time.ctime(msg[0]), msg[2])
425     lastmsg = msg
426     time.sleep(1)
427
428   jobs = cl.QueryJobs([job_id], ["status", "opresult"])
429   if not jobs:
430     raise errors.JobLost("Job with id %s lost" % job_id)
431
432   status, result = jobs[0]
433   if status == constants.JOB_STATUS_SUCCESS:
434     return result[0]
435   else:
436     raise errors.OpExecError(result)
437
438
439 def SubmitOpCode(op, cl=None, feedback_fn=None):
440   """Legacy function to submit an opcode.
441
442   This is just a simple wrapper over the construction of the processor
443   instance. It should be extended to better handle feedback and
444   interaction functions.
445
446   """
447   if cl is None:
448     cl = GetClient()
449
450   job_id = SendJob([op], cl)
451
452   return PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
453
454
455 def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
456   """Wrapper around SubmitOpCode or SendJob.
457
458   This function will decide, based on the 'opts' parameter, whether to
459   submit and wait for the result of the opcode (and return it), or
460   whether to just send the job and print its identifier. It is used in
461   order to simplify the implementation of the '--submit' option.
462
463   """
464   if opts and opts.submit_only:
465     print SendJob([op], cl=cl)
466     sys.exit(0)
467   else:
468     return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
469
470
471 def GetClient():
472   # TODO: Cache object?
473   try:
474     client = luxi.Client()
475   except luxi.NoMasterError:
476     master, myself = ssconf.GetMasterAndMyself()
477     if master != myself:
478       raise errors.OpPrereqError("This is not the master node, please connect"
479                                  " to node '%s' and rerun the command" %
480                                  master)
481     else:
482       raise
483   return client
484
485
486 def FormatError(err):
487   """Return a formatted error message for a given error.
488
489   This function takes an exception instance and returns a tuple
490   consisting of two values: first, the recommended exit code, and
491   second, a string describing the error message (not
492   newline-terminated).
493
494   """
495   retcode = 1
496   obuf = StringIO()
497   msg = str(err)
498   if isinstance(err, errors.ConfigurationError):
499     txt = "Corrupt configuration file: %s" % msg
500     logger.Error(txt)
501     obuf.write(txt + "\n")
502     obuf.write("Aborting.")
503     retcode = 2
504   elif isinstance(err, errors.HooksAbort):
505     obuf.write("Failure: hooks execution failed:\n")
506     for node, script, out in err.args[0]:
507       if out:
508         obuf.write("  node: %s, script: %s, output: %s\n" %
509                    (node, script, out))
510       else:
511         obuf.write("  node: %s, script: %s (no output)\n" %
512                    (node, script))
513   elif isinstance(err, errors.HooksFailure):
514     obuf.write("Failure: hooks general failure: %s" % msg)
515   elif isinstance(err, errors.ResolverError):
516     this_host = utils.HostInfo.SysName()
517     if err.args[0] == this_host:
518       msg = "Failure: can't resolve my own hostname ('%s')"
519     else:
520       msg = "Failure: can't resolve hostname '%s'"
521     obuf.write(msg % err.args[0])
522   elif isinstance(err, errors.OpPrereqError):
523     obuf.write("Failure: prerequisites not met for this"
524                " operation:\n%s" % msg)
525   elif isinstance(err, errors.OpExecError):
526     obuf.write("Failure: command execution error:\n%s" % msg)
527   elif isinstance(err, errors.TagError):
528     obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
529   elif isinstance(err, errors.GenericError):
530     obuf.write("Unhandled Ganeti error: %s" % msg)
531   elif isinstance(err, luxi.NoMasterError):
532     obuf.write("Cannot communicate with the master daemon.\nIs it running"
533                " and listening on '%s'?" % err.args[0])
534   elif isinstance(err, luxi.TimeoutError):
535     obuf.write("Timeout while talking to the master daemon. Error:\n"
536                "%s" % msg)
537   elif isinstance(err, luxi.ProtocolError):
538     obuf.write("Unhandled protocol error while talking to the master daemon:\n"
539                "%s" % msg)
540   else:
541     obuf.write("Unhandled exception: %s" % msg)
542   return retcode, obuf.getvalue().rstrip('\n')
543
544
545 def GenericMain(commands, override=None, aliases=None):
546   """Generic main function for all the gnt-* commands.
547
548   Arguments:
549     - commands: a dictionary with a special structure, see the design doc
550                 for command line handling.
551     - override: if not None, we expect a dictionary with keys that will
552                 override command line options; this can be used to pass
553                 options from the scripts to generic functions
554     - aliases: dictionary with command aliases {'alias': 'target, ...}
555
556   """
557   # save the program name and the entire command line for later logging
558   if sys.argv:
559     binary = os.path.basename(sys.argv[0]) or sys.argv[0]
560     if len(sys.argv) >= 2:
561       binary += " " + sys.argv[1]
562       old_cmdline = " ".join(sys.argv[2:])
563     else:
564       old_cmdline = ""
565   else:
566     binary = "<unknown program>"
567     old_cmdline = ""
568
569   if aliases is None:
570     aliases = {}
571
572   func, options, args = _ParseArgs(sys.argv, commands, aliases)
573   if func is None: # parse error
574     return 1
575
576   if override is not None:
577     for key, val in override.iteritems():
578       setattr(options, key, val)
579
580   logger.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
581                       stderr_logging=True, program=binary)
582
583   utils.debug = options.debug
584
585   if old_cmdline:
586     logger.Info("run with arguments '%s'" % old_cmdline)
587   else:
588     logger.Info("run with no arguments")
589
590   try:
591     result = func(options, args)
592   except (errors.GenericError, luxi.ProtocolError), err:
593     result, err_msg = FormatError(err)
594     logger.ToStderr(err_msg)
595
596   return result
597
598
599 def GenerateTable(headers, fields, separator, data,
600                   numfields=None, unitfields=None):
601   """Prints a table with headers and different fields.
602
603   Args:
604     headers: Dict of header titles or None if no headers should be shown
605     fields: List of fields to show
606     separator: String used to separate fields or None for spaces
607     data: Data to be printed
608     numfields: List of fields to be aligned to right
609     unitfields: List of fields to be formatted as units
610
611   """
612   if numfields is None:
613     numfields = []
614   if unitfields is None:
615     unitfields = []
616
617   format_fields = []
618   for field in fields:
619     if headers and field not in headers:
620       raise errors.ProgrammerError("Missing header description for field '%s'"
621                                    % field)
622     if separator is not None:
623       format_fields.append("%s")
624     elif field in numfields:
625       format_fields.append("%*s")
626     else:
627       format_fields.append("%-*s")
628
629   if separator is None:
630     mlens = [0 for name in fields]
631     format = ' '.join(format_fields)
632   else:
633     format = separator.replace("%", "%%").join(format_fields)
634
635   for row in data:
636     for idx, val in enumerate(row):
637       if fields[idx] in unitfields:
638         try:
639           val = int(val)
640         except ValueError:
641           pass
642         else:
643           val = row[idx] = utils.FormatUnit(val)
644       val = row[idx] = str(val)
645       if separator is None:
646         mlens[idx] = max(mlens[idx], len(val))
647
648   result = []
649   if headers:
650     args = []
651     for idx, name in enumerate(fields):
652       hdr = headers[name]
653       if separator is None:
654         mlens[idx] = max(mlens[idx], len(hdr))
655         args.append(mlens[idx])
656       args.append(hdr)
657     result.append(format % tuple(args))
658
659   for line in data:
660     args = []
661     for idx in xrange(len(fields)):
662       if separator is None:
663         args.append(mlens[idx])
664       args.append(line[idx])
665     result.append(format % tuple(args))
666
667   return result