Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ 4040a784

History | View | Annotate | Download (29.2 kB)

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
import logging
31
from cStringIO import StringIO
32

    
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
40

    
41
from optparse import (OptionParser, make_option, TitledHelpFormatter,
42
                      Option, OptionValueError)
43

    
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",
53
           "ValidateBeParams",
54
           "ToStderr", "ToStdout",
55
           "UsesRPC",
56
           "GetOnlineNodes",
57
           ]
58

    
59

    
60
def _ExtractTagsObject(opts, args):
61
  """Extract the tag type object.
62

63
  Note that this function will modify its args parameter.
64

65
  """
66
  if not hasattr(opts, "tag_type"):
67
    raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
68
  kind = opts.tag_type
69
  if kind == constants.TAG_CLUSTER:
70
    retval = kind, kind
71
  elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
72
    if not args:
73
      raise errors.OpPrereqError("no arguments passed to the command")
74
    name = args.pop(0)
75
    retval = kind, name
76
  else:
77
    raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
78
  return retval
79

    
80

    
81
def _ExtendTags(opts, args):
82
  """Extend the args if a source file has been given.
83

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.
87

88
  """
89
  fname = opts.tags_source
90
  if fname is None:
91
    return
92
  if fname == "-":
93
    new_fh = sys.stdin
94
  else:
95
    new_fh = open(fname, "r")
96
  new_data = []
97
  try:
98
    # we don't use the nice 'new_data = [line.strip() for line in fh]'
99
    # because of python bug 1633941
100
    while True:
101
      line = new_fh.readline()
102
      if not line:
103
        break
104
      new_data.append(line.strip())
105
  finally:
106
    new_fh.close()
107
  args.extend(new_data)
108

    
109

    
110
def ListTags(opts, args):
111
  """List the tags on a given object.
112

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.
117

118
  """
119
  kind, name = _ExtractTagsObject(opts, args)
120
  op = opcodes.OpGetTags(kind=kind, name=name)
121
  result = SubmitOpCode(op)
122
  result = list(result)
123
  result.sort()
124
  for tag in result:
125
    print tag
126

    
127

    
128
def AddTags(opts, args):
129
  """Add tags on a given object.
130

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.
135

136
  """
137
  kind, name = _ExtractTagsObject(opts, args)
138
  _ExtendTags(opts, args)
139
  if not args:
140
    raise errors.OpPrereqError("No tags to be added")
141
  op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
142
  SubmitOpCode(op)
143

    
144

    
145
def RemoveTags(opts, args):
146
  """Remove tags from a given object.
147

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.
152

153
  """
154
  kind, name = _ExtractTagsObject(opts, args)
155
  _ExtendTags(opts, args)
156
  if not args:
157
    raise errors.OpPrereqError("No tags to be removed")
158
  op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
159
  SubmitOpCode(op)
160

    
161

    
162
DEBUG_OPT = make_option("-d", "--debug", default=False,
163
                        action="store_true",
164
                        help="Turn debugging on")
165

    
166
NOHDR_OPT = make_option("--no-headers", default=False,
167
                        action="store_true", dest="no_headers",
168
                        help="Don't display column headers")
169

    
170
SEP_OPT = make_option("--separator", default=None,
171
                      action="store", dest="separator",
172
                      help="Separator between output fields"
173
                      " (defaults to one space)")
174

    
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)")
178

    
179
FIELDS_OPT = make_option("-o", "--output", dest="output", action="store",
180
                         type="string", help="Comma separated list of"
181
                         " output fields",
182
                         metavar="FIELDS")
183

    
184
FORCE_OPT = make_option("-f", "--force", dest="force", action="store_true",
185
                        default=False, help="Force the operation")
186

    
187
TAG_SRC_OPT = make_option("--from", dest="tags_source",
188
                          default=None, help="File with tag names")
189

    
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")
194

    
195

    
196
def ARGS_FIXED(val):
197
  """Macro-like function denoting a fixed number of arguments"""
198
  return -val
199

    
200

    
201
def ARGS_ATLEAST(val):
202
  """Macro-like function denoting a minimum number of arguments"""
203
  return val
204

    
205

    
206
ARGS_NONE = None
207
ARGS_ONE = ARGS_FIXED(1)
208
ARGS_ANY = ARGS_ATLEAST(0)
209

    
210

    
211
def check_unit(option, opt, value):
212
  """OptParsers custom converter for units.
213

214
  """
215
  try:
216
    return utils.ParseUnit(value)
217
  except errors.UnitParseError, err:
218
    raise OptionValueError("option %s: %s" % (opt, err))
219

    
220

    
221
class CliOption(Option):
222
  """Custom option class for optparse.
223

224
  """
225
  TYPES = Option.TYPES + ("unit",)
226
  TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
227
  TYPE_CHECKER["unit"] = check_unit
228

    
229

    
230
def _SplitKeyVal(opt, data):
231
  """Convert a KeyVal string into a dict.
232

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
236
  have value=True.
237

238
  @type opt: string
239
  @param opt: a string holding the option name for which we process the
240
      data, used in building error messages
241
  @type data: string
242
  @param data: a string of the format key=val,key=val,...
243
  @rtype: dict
244
  @return: {key=val, key=val}
245
  @raises errors.ParameterError: if there are duplicate keys
246

247
  """
248
  NO_PREFIX = "no_"
249
  UN_PREFIX = "-"
250
  kv_dict = {}
251
  for elem in data.split(","):
252
    if "=" in elem:
253
      key, val = elem.split("=", 1)
254
    else:
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
259
      else:
260
        key, val = elem, True
261
    if key in kv_dict:
262
      raise errors.ParameterError("Duplicate key '%s' in option %s" %
263
                                  (key, opt))
264
    kv_dict[key] = val
265
  return kv_dict
266

    
267

    
268
def check_ident_key_val(option, opt, value):
269
  """Custom parser for the IdentKeyVal option type.
270

271
  """
272
  if ":" not in value:
273
    retval =  (value, {})
274
  else:
275
    ident, rest = value.split(":", 1)
276
    kv_dict = _SplitKeyVal(opt, rest)
277
    retval = (ident, kv_dict)
278
  return retval
279

    
280

    
281
class IdentKeyValOption(Option):
282
  """Custom option class for ident:key=val,key=val options.
283

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.
286

287
  """
288
  TYPES = Option.TYPES + ("identkeyval",)
289
  TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
290
  TYPE_CHECKER["identkeyval"] = check_ident_key_val
291

    
292

    
293
def check_key_val(option, opt, value):
294
  """Custom parser for the KeyVal option type.
295

296
  """
297
  return _SplitKeyVal(opt, value)
298

    
299

    
300
class KeyValOption(Option):
301
  """Custom option class for key=val,key=val options.
302

303
  This will store the parsed values as a dict {key: val}.
304

305
  """
306
  TYPES = Option.TYPES + ("keyval",)
307
  TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
308
  TYPE_CHECKER["keyval"] = check_key_val
309

    
310

    
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
315

    
316

    
317
def _ParseArgs(argv, commands, aliases):
318
  """Parser for the command line arguments.
319

320
  This function parses the arguements and returns the function which
321
  must be executed together with its (modified) arguments.
322

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, ...}
327

328
  """
329
  if len(argv) == 0:
330
    binary = "<command>"
331
  else:
332
    binary = argv[0].split("/")[-1]
333

    
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.
338
    sys.exit(0)
339

    
340
  if len(argv) < 2 or not (argv[1] in commands or
341
                           argv[1] in aliases):
342
    # let's do a nice thing
343
    sortedcmds = commands.keys()
344
    sortedcmds.sort()
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
352
    print "Commands:"
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)
360
    print
361
    return None, None, None
362

    
363
  # get command, unalias it, and look it up in commands
364
  cmd = argv.pop(1)
365
  if cmd in aliases:
366
    if cmd in commands:
367
      raise errors.ProgrammerError("Alias '%s' overrides an existing"
368
                                   " command" % cmd)
369

    
370
    if aliases[cmd] not in commands:
371
      raise errors.ProgrammerError("Alias '%s' maps to non-existing"
372
                                   " command '%s'" % (cmd, aliases[cmd]))
373

    
374
    cmd = aliases[cmd]
375

    
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()
383
  if nargs is None:
384
    if len(args) != 0:
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)" %
389
                         (cmd, -nargs))
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)" %
393
                         (cmd, nargs))
394
    return None, None, None
395

    
396
  return func, options, args
397

    
398

    
399
def SplitNodeOption(value):
400
  """Splits the value of a --node option.
401

402
  """
403
  if value and ':' in value:
404
    return value.split(':', 1)
405
  else:
406
    return (value, None)
407

    
408

    
409
def ValidateBeParams(bep):
410
  """Parse and check the given beparams.
411

412
  The function will update in-place the given dictionary.
413

414
  @type bep: dict
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
418

419
  """
420
  if constants.BE_MEMORY in bep:
421
    bep[constants.BE_MEMORY] = utils.ParseUnit(bep[constants.BE_MEMORY])
422

    
423
  if constants.BE_VCPUS in bep:
424
    try:
425
      bep[constants.BE_VCPUS] = int(bep[constants.BE_VCPUS])
426
    except ValueError:
427
      raise errors.ParameterError("Invalid number of VCPUs")
428

    
429

    
430
def UsesRPC(fn):
431
  def wrapper(*args, **kwargs):
432
    rpc.Init()
433
    try:
434
      return fn(*args, **kwargs)
435
    finally:
436
      rpc.Shutdown()
437
  return wrapper
438

    
439

    
440
def AskUser(text, choices=None):
441
  """Ask the user a question.
442

443
  @param text: the question to ask
444

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
449

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
452
      entry from the list
453

454
  """
455
  if choices is None:
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")
463

    
464
  answer = choices[-1][1]
465
  new_text = []
466
  for line in text.splitlines():
467
    new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
468
  text = "\n".join(new_text)
469
  try:
470
    f = file("/dev/tty", "a+")
471
  except IOError:
472
    return answer
473
  try:
474
    chars = [entry[0] for entry in choices]
475
    chars[-1] = "[%s]" % chars[-1]
476
    chars.append('?')
477
    maps = dict([(entry[0], entry[1]) for entry in choices])
478
    while True:
479
      f.write(text)
480
      f.write('\n')
481
      f.write("/".join(chars))
482
      f.write(": ")
483
      line = f.readline(2).strip().lower()
484
      if line in maps:
485
        answer = maps[line]
486
        break
487
      elif line == '?':
488
        for entry in choices:
489
          f.write(" %s - %s\n" % (entry[0], entry[2]))
490
        f.write("\n")
491
        continue
492
  finally:
493
    f.close()
494
  return answer
495

    
496

    
497
class JobSubmittedException(Exception):
498
  """Job was submitted, client should exit.
499

500
  This exception has one argument, the ID of the job that was
501
  submitted. The handler should print this ID.
502

503
  This is not an error, just a structured way to exit from clients.
504

505
  """
506

    
507

    
508
def SendJob(ops, cl=None):
509
  """Function to submit an opcode without waiting for the results.
510

511
  @type ops: list
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
516

517
  """
518
  if cl is None:
519
    cl = GetClient()
520

    
521
  job_id = cl.SubmitJob(ops)
522

    
523
  return job_id
524

    
525

    
526
def PollJob(job_id, cl=None, feedback_fn=None):
527
  """Function to poll for the result of a job.
528

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
534

535
  """
536
  if cl is None:
537
    cl = GetClient()
538

    
539
  prev_job_info = None
540
  prev_logmsg_serial = None
541

    
542
  while True:
543
    result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
544
                                 prev_logmsg_serial)
545
    if not result:
546
      # job not found, go away!
547
      raise errors.JobLost("Job with id %s lost" % job_id)
548

    
549
    # Split result, a tuple of (field values, log entries)
550
    (job_info, log_entries) = result
551
    (status, ) = job_info
552

    
553
    if log_entries:
554
      for log_entry in log_entries:
555
        (serial, timestamp, _, message) = log_entry
556
        if callable(feedback_fn):
557
          feedback_fn(log_entry[1:])
558
        else:
559
          print "%s %s" % (time.ctime(utils.MergeTime(timestamp)), message)
560
        prev_logmsg_serial = max(prev_logmsg_serial, serial)
561

    
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):
567
      break
568

    
569
    prev_job_info = job_info
570

    
571
  jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
572
  if not jobs:
573
    raise errors.JobLost("Job with id %s lost" % job_id)
574

    
575
  status, opstatus, result = jobs[0]
576
  if status == constants.JOB_STATUS_SUCCESS:
577
    return result
578
  elif status in (constants.JOB_STATUS_CANCELING,
579
                  constants.JOB_STATUS_CANCELED):
580
    raise errors.OpExecError("Job was canceled")
581
  else:
582
    has_ok = False
583
    for idx, (status, msg) in enumerate(zip(opstatus, result)):
584
      if status == constants.OP_STATUS_SUCCESS:
585
        has_ok = True
586
      elif status == constants.OP_STATUS_ERROR:
587
        if has_ok:
588
          raise errors.OpExecError("partial failure (opcode %d): %s" %
589
                                   (idx, msg))
590
        else:
591
          raise errors.OpExecError(str(msg))
592
    # default failure mode
593
    raise errors.OpExecError(result)
594

    
595

    
596
def SubmitOpCode(op, cl=None, feedback_fn=None):
597
  """Legacy function to submit an opcode.
598

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.
602

603
  """
604
  if cl is None:
605
    cl = GetClient()
606

    
607
  job_id = SendJob([op], cl)
608

    
609
  op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
610

    
611
  return op_results[0]
612

    
613

    
614
def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
615
  """Wrapper around SubmitOpCode or SendJob.
616

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.
621

622
  """
623
  if opts and opts.submit_only:
624
    job_id = SendJob([op], cl=cl)
625
    raise JobSubmittedException(job_id)
626
  else:
627
    return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
628

    
629

    
630
def GetClient():
631
  # TODO: Cache object?
632
  try:
633
    client = luxi.Client()
634
  except luxi.NoMasterError:
635
    master, myself = ssconf.GetMasterAndMyself()
636
    if master != myself:
637
      raise errors.OpPrereqError("This is not the master node, please connect"
638
                                 " to node '%s' and rerun the command" %
639
                                 master)
640
    else:
641
      raise
642
  return client
643

    
644

    
645
def FormatError(err):
646
  """Return a formatted error message for a given error.
647

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
651
  newline-terminated).
652

653
  """
654
  retcode = 1
655
  obuf = StringIO()
656
  msg = str(err)
657
  if isinstance(err, errors.ConfigurationError):
658
    txt = "Corrupt configuration file: %s" % msg
659
    logging.error(txt)
660
    obuf.write(txt + "\n")
661
    obuf.write("Aborting.")
662
    retcode = 2
663
  elif isinstance(err, errors.HooksAbort):
664
    obuf.write("Failure: hooks execution failed:\n")
665
    for node, script, out in err.args[0]:
666
      if out:
667
        obuf.write("  node: %s, script: %s, output: %s\n" %
668
                   (node, script, out))
669
      else:
670
        obuf.write("  node: %s, script: %s (no output)\n" %
671
                   (node, script))
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')"
678
    else:
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"
701
               "%s" % msg)
702
  elif isinstance(err, luxi.ProtocolError):
703
    obuf.write("Unhandled protocol error while talking to the master daemon:\n"
704
               "%s" % msg)
705
  elif isinstance(err, JobSubmittedException):
706
    obuf.write("JobID: %s\n" % err.args[0])
707
    retcode = 0
708
  else:
709
    obuf.write("Unhandled exception: %s" % msg)
710
  return retcode, obuf.getvalue().rstrip('\n')
711

    
712

    
713
def GenericMain(commands, override=None, aliases=None):
714
  """Generic main function for all the gnt-* commands.
715

716
  Arguments:
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, ...}
723

724
  """
725
  # save the program name and the entire command line for later logging
726
  if sys.argv:
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:])
731
    else:
732
      old_cmdline = ""
733
  else:
734
    binary = "<unknown program>"
735
    old_cmdline = ""
736

    
737
  if aliases is None:
738
    aliases = {}
739

    
740
  func, options, args = _ParseArgs(sys.argv, commands, aliases)
741
  if func is None: # parse error
742
    return 1
743

    
744
  if override is not None:
745
    for key, val in override.iteritems():
746
      setattr(options, key, val)
747

    
748
  utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
749
                     stderr_logging=True, program=binary)
750

    
751
  utils.debug = options.debug
752

    
753
  if old_cmdline:
754
    logging.info("run with arguments '%s'", old_cmdline)
755
  else:
756
    logging.info("run with no arguments")
757

    
758
  try:
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")
764
    ToStderr(err_msg)
765

    
766
  return result
767

    
768

    
769
def GenerateTable(headers, fields, separator, data,
770
                  numfields=None, unitfields=None,
771
                  units=None):
772
  """Prints a table with headers and different fields.
773

774
  @type headers: dict
775
  @param headers: dictionary mapping field names to headers for
776
      the table
777
  @type fields: list
778
  @param fields: the field names corresponding to each row in
779
      the data field
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
783
      each field
784
  @type data: list
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
796

797
  """
798
  if units is None:
799
    if separator:
800
      units = "m"
801
    else:
802
      units = "h"
803

    
804
  if numfields is None:
805
    numfields = []
806
  if unitfields is None:
807
    unitfields = []
808

    
809
  numfields = utils.FieldSet(*numfields)
810
  unitfields = utils.FieldSet(*unitfields)
811

    
812
  format_fields = []
813
  for field in fields:
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
817
      # variable fields)
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")
823
    else:
824
      format_fields.append("%-*s")
825

    
826
  if separator is None:
827
    mlens = [0 for name in fields]
828
    format = ' '.join(format_fields)
829
  else:
830
    format = separator.replace("%", "%%").join(format_fields)
831

    
832
  for row in data:
833
    for idx, val in enumerate(row):
834
      if unitfields.Matches(fields[idx]):
835
        try:
836
          val = int(val)
837
        except ValueError:
838
          pass
839
        else:
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))
844

    
845
  result = []
846
  if headers:
847
    args = []
848
    for idx, name in enumerate(fields):
849
      hdr = headers[name]
850
      if separator is None:
851
        mlens[idx] = max(mlens[idx], len(hdr))
852
        args.append(mlens[idx])
853
      args.append(hdr)
854
    result.append(format % tuple(args))
855

    
856
  for line in data:
857
    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))
863

    
864
  return result
865

    
866

    
867
def FormatTimestamp(ts):
868
  """Formats a given timestamp.
869

870
  @type ts: timestamp
871
  @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
872

873
  @rtype: string
874
  @returns: a string with the formatted timestamp
875

876
  """
877
  if not isinstance (ts, (tuple, list)) or len(ts) != 2:
878
    return '?'
879
  sec, usec = ts
880
  return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
881

    
882

    
883
def ParseTimespec(value):
884
  """Parse a time specification.
885

886
  The following suffixed will be recognized:
887

888
    - s: seconds
889
    - m: minutes
890
    - h: hours
891
    - d: day
892
    - w: weeks
893

894
  Without any suffix, the value will be taken to be in seconds.
895

896
  """
897
  value = str(value)
898
  if not value:
899
    raise errors.OpPrereqError("Empty time specification passed")
900
  suffix_map = {
901
    's': 1,
902
    'm': 60,
903
    'h': 3600,
904
    'd': 86400,
905
    'w': 604800,
906
    }
907
  if value[-1] not in suffix_map:
908
    try:
909
      value = int(value)
910
    except ValueError:
911
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
912
  else:
913
    multiplier = suffix_map[value[-1]]
914
    value = value[:-1]
915
    if not value: # no data left after stripping the suffix
916
      raise errors.OpPrereqError("Invalid time specification (only"
917
                                 " suffix passed)")
918
    try:
919
      value = int(value) * multiplier
920
    except ValueError:
921
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
922
  return value
923

    
924

    
925
def GetOnlineNodes(nodes, cl=None, nowarn=False):
926
  """Returns the names of online nodes.
927

928
  This function will also log a warning on stderr with the names of
929
  the online nodes.
930

931
  @param nodes: if not empty, use only this subset of nodes (minus the
932
      offline ones)
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
938

939
  """
940
  if cl is None:
941
    cl = GetClient()
942

    
943
  op = opcodes.OpQueryNodes(output_fields=["name", "offline"],
944
                            names=nodes)
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]]
950

    
951

    
952
def _ToStream(stream, txt, *args):
953
  """Write a message to a stream, bypassing the logging system
954

955
  @type stream: file object
956
  @param stream: the file to which we should write
957
  @type txt: str
958
  @param txt: the message
959

960
  """
961
  if args:
962
    args = tuple(args)
963
    stream.write(txt % args)
964
  else:
965
    stream.write(txt)
966
  stream.write('\n')
967
  stream.flush()
968

    
969

    
970
def ToStdout(txt, *args):
971
  """Write a message to stdout only, bypassing the logging system
972

973
  This is just a wrapper over _ToStream.
974

975
  @type txt: str
976
  @param txt: the message
977

978
  """
979
  _ToStream(sys.stdout, txt, *args)
980

    
981

    
982
def ToStderr(txt, *args):
983
  """Write a message to stderr only, bypassing the logging system
984

985
  This is just a wrapper over _ToStream.
986

987
  @type txt: str
988
  @param txt: the message
989

990
  """
991
  _ToStream(sys.stderr, txt, *args)