Fix error message when masterd is not listening
[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   prev_job_info = None
409   prev_logmsg_serial = None
410
411   while True:
412     result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
413                                  prev_logmsg_serial)
414     if not result:
415       # job not found, go away!
416       raise errors.JobLost("Job with id %s lost" % job_id)
417
418     # Split result, a tuple of (field values, log entries)
419     (job_info, log_entries) = result
420     (status, ) = job_info
421
422     if log_entries:
423       for log_entry in log_entries:
424         (serial, timestamp, _, message) = log_entry
425         if callable(feedback_fn):
426           feedback_fn(log_entry[1:])
427         else:
428           print "%s %s" % (time.ctime(utils.MergeTime(timestamp)), message)
429         prev_logmsg_serial = max(prev_logmsg_serial, serial)
430
431     # TODO: Handle canceled and archived jobs
432     elif status in (constants.JOB_STATUS_SUCCESS, constants.JOB_STATUS_ERROR):
433       break
434
435     prev_job_info = job_info
436
437   jobs = cl.QueryJobs([job_id], ["status", "opresult"])
438   if not jobs:
439     raise errors.JobLost("Job with id %s lost" % job_id)
440
441   status, result = jobs[0]
442   if status == constants.JOB_STATUS_SUCCESS:
443     return result[0]
444   else:
445     raise errors.OpExecError(result)
446
447
448 def SubmitOpCode(op, cl=None, feedback_fn=None):
449   """Legacy function to submit an opcode.
450
451   This is just a simple wrapper over the construction of the processor
452   instance. It should be extended to better handle feedback and
453   interaction functions.
454
455   """
456   if cl is None:
457     cl = GetClient()
458
459   job_id = SendJob([op], cl)
460
461   return PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
462
463
464 def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
465   """Wrapper around SubmitOpCode or SendJob.
466
467   This function will decide, based on the 'opts' parameter, whether to
468   submit and wait for the result of the opcode (and return it), or
469   whether to just send the job and print its identifier. It is used in
470   order to simplify the implementation of the '--submit' option.
471
472   """
473   if opts and opts.submit_only:
474     print SendJob([op], cl=cl)
475     sys.exit(0)
476   else:
477     return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
478
479
480 def GetClient():
481   # TODO: Cache object?
482   try:
483     client = luxi.Client()
484   except luxi.NoMasterError:
485     master, myself = ssconf.GetMasterAndMyself()
486     if master != myself:
487       raise errors.OpPrereqError("This is not the master node, please connect"
488                                  " to node '%s' and rerun the command" %
489                                  master)
490     else:
491       raise
492   return client
493
494
495 def FormatError(err):
496   """Return a formatted error message for a given error.
497
498   This function takes an exception instance and returns a tuple
499   consisting of two values: first, the recommended exit code, and
500   second, a string describing the error message (not
501   newline-terminated).
502
503   """
504   retcode = 1
505   obuf = StringIO()
506   msg = str(err)
507   if isinstance(err, errors.ConfigurationError):
508     txt = "Corrupt configuration file: %s" % msg
509     logger.Error(txt)
510     obuf.write(txt + "\n")
511     obuf.write("Aborting.")
512     retcode = 2
513   elif isinstance(err, errors.HooksAbort):
514     obuf.write("Failure: hooks execution failed:\n")
515     for node, script, out in err.args[0]:
516       if out:
517         obuf.write("  node: %s, script: %s, output: %s\n" %
518                    (node, script, out))
519       else:
520         obuf.write("  node: %s, script: %s (no output)\n" %
521                    (node, script))
522   elif isinstance(err, errors.HooksFailure):
523     obuf.write("Failure: hooks general failure: %s" % msg)
524   elif isinstance(err, errors.ResolverError):
525     this_host = utils.HostInfo.SysName()
526     if err.args[0] == this_host:
527       msg = "Failure: can't resolve my own hostname ('%s')"
528     else:
529       msg = "Failure: can't resolve hostname '%s'"
530     obuf.write(msg % err.args[0])
531   elif isinstance(err, errors.OpPrereqError):
532     obuf.write("Failure: prerequisites not met for this"
533                " operation:\n%s" % msg)
534   elif isinstance(err, errors.OpExecError):
535     obuf.write("Failure: command execution error:\n%s" % msg)
536   elif isinstance(err, errors.TagError):
537     obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
538   elif isinstance(err, errors.GenericError):
539     obuf.write("Unhandled Ganeti error: %s" % msg)
540   elif isinstance(err, luxi.NoMasterError):
541     obuf.write("Cannot communicate with the master daemon.\nIs it running"
542                " and listening for connections?")
543   elif isinstance(err, luxi.TimeoutError):
544     obuf.write("Timeout while talking to the master daemon. Error:\n"
545                "%s" % msg)
546   elif isinstance(err, luxi.ProtocolError):
547     obuf.write("Unhandled protocol error while talking to the master daemon:\n"
548                "%s" % msg)
549   else:
550     obuf.write("Unhandled exception: %s" % msg)
551   return retcode, obuf.getvalue().rstrip('\n')
552
553
554 def GenericMain(commands, override=None, aliases=None):
555   """Generic main function for all the gnt-* commands.
556
557   Arguments:
558     - commands: a dictionary with a special structure, see the design doc
559                 for command line handling.
560     - override: if not None, we expect a dictionary with keys that will
561                 override command line options; this can be used to pass
562                 options from the scripts to generic functions
563     - aliases: dictionary with command aliases {'alias': 'target, ...}
564
565   """
566   # save the program name and the entire command line for later logging
567   if sys.argv:
568     binary = os.path.basename(sys.argv[0]) or sys.argv[0]
569     if len(sys.argv) >= 2:
570       binary += " " + sys.argv[1]
571       old_cmdline = " ".join(sys.argv[2:])
572     else:
573       old_cmdline = ""
574   else:
575     binary = "<unknown program>"
576     old_cmdline = ""
577
578   if aliases is None:
579     aliases = {}
580
581   func, options, args = _ParseArgs(sys.argv, commands, aliases)
582   if func is None: # parse error
583     return 1
584
585   if override is not None:
586     for key, val in override.iteritems():
587       setattr(options, key, val)
588
589   logger.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
590                       stderr_logging=True, program=binary)
591
592   utils.debug = options.debug
593
594   if old_cmdline:
595     logger.Info("run with arguments '%s'" % old_cmdline)
596   else:
597     logger.Info("run with no arguments")
598
599   try:
600     result = func(options, args)
601   except (errors.GenericError, luxi.ProtocolError), err:
602     result, err_msg = FormatError(err)
603     logger.ToStderr(err_msg)
604
605   return result
606
607
608 def GenerateTable(headers, fields, separator, data,
609                   numfields=None, unitfields=None):
610   """Prints a table with headers and different fields.
611
612   Args:
613     headers: Dict of header titles or None if no headers should be shown
614     fields: List of fields to show
615     separator: String used to separate fields or None for spaces
616     data: Data to be printed
617     numfields: List of fields to be aligned to right
618     unitfields: List of fields to be formatted as units
619
620   """
621   if numfields is None:
622     numfields = []
623   if unitfields is None:
624     unitfields = []
625
626   format_fields = []
627   for field in fields:
628     if headers and field not in headers:
629       raise errors.ProgrammerError("Missing header description for field '%s'"
630                                    % field)
631     if separator is not None:
632       format_fields.append("%s")
633     elif field in numfields:
634       format_fields.append("%*s")
635     else:
636       format_fields.append("%-*s")
637
638   if separator is None:
639     mlens = [0 for name in fields]
640     format = ' '.join(format_fields)
641   else:
642     format = separator.replace("%", "%%").join(format_fields)
643
644   for row in data:
645     for idx, val in enumerate(row):
646       if fields[idx] in unitfields:
647         try:
648           val = int(val)
649         except ValueError:
650           pass
651         else:
652           val = row[idx] = utils.FormatUnit(val)
653       val = row[idx] = str(val)
654       if separator is None:
655         mlens[idx] = max(mlens[idx], len(val))
656
657   result = []
658   if headers:
659     args = []
660     for idx, name in enumerate(fields):
661       hdr = headers[name]
662       if separator is None:
663         mlens[idx] = max(mlens[idx], len(hdr))
664         args.append(mlens[idx])
665       args.append(hdr)
666     result.append(format % tuple(args))
667
668   for line in data:
669     args = []
670     for idx in xrange(len(fields)):
671       if separator is None:
672         args.append(mlens[idx])
673       args.append(line[idx])
674     result.append(format % tuple(args))
675
676   return result