Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ 3f75b4f3

History | View | Annotate | Download (36.6 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, TitledHelpFormatter,
42
                      Option, OptionValueError)
43

    
44

    
45
__all__ = [
46
  # Command line options
47
  "CONFIRM_OPT",
48
  "DEBUG_OPT",
49
  "DEBUG_SIMERR_OPT",
50
  "FIELDS_OPT",
51
  "FORCE_OPT",
52
  "NOHDR_OPT",
53
  "NWSYNC_OPT",
54
  "SEP_OPT",
55
  "SUBMIT_OPT",
56
  "SYNC_OPT",
57
  "TAG_SRC_OPT",
58
  "USEUNITS_OPT",
59
  "VERBOSE_OPT",
60
  # Generic functions for CLI programs
61
  "GenericMain",
62
  "GetClient",
63
  "GetOnlineNodes",
64
  "JobExecutor",
65
  "JobSubmittedException",
66
  "ParseTimespec",
67
  "SubmitOpCode",
68
  "SubmitOrSend",
69
  "UsesRPC",
70
  # Formatting functions
71
  "ToStderr", "ToStdout",
72
  "FormatError",
73
  "GenerateTable",
74
  "AskUser",
75
  "FormatTimestamp",
76
  # Tags functions
77
  "ListTags",
78
  "AddTags",
79
  "RemoveTags",
80
  # command line options support infrastructure
81
  "ARGS_MANY_INSTANCES",
82
  "ARGS_MANY_NODES",
83
  "ARGS_NONE",
84
  "ARGS_ONE_INSTANCE",
85
  "ARGS_ONE_NODE",
86
  "ArgChoice",
87
  "ArgCommand",
88
  "ArgFile",
89
  "ArgHost",
90
  "ArgInstance",
91
  "ArgJobId",
92
  "ArgNode",
93
  "ArgSuggest",
94
  "ArgUnknown",
95
  "OPT_COMPL_INST_ADD_NODES",
96
  "OPT_COMPL_MANY_NODES",
97
  "OPT_COMPL_ONE_IALLOCATOR",
98
  "OPT_COMPL_ONE_INSTANCE",
99
  "OPT_COMPL_ONE_NODE",
100
  "OPT_COMPL_ONE_OS",
101
  "cli_option",
102
  "SplitNodeOption",
103
  ]
104

    
105
NO_PREFIX = "no_"
106
UN_PREFIX = "-"
107

    
108

    
109
class _Argument:
110
  def __init__(self, min=0, max=None):
111
    self.min = min
112
    self.max = max
113

    
114
  def __repr__(self):
115
    return ("<%s min=%s max=%s>" %
116
            (self.__class__.__name__, self.min, self.max))
117

    
118

    
119
class ArgSuggest(_Argument):
120
  """Suggesting argument.
121

122
  Value can be any of the ones passed to the constructor.
123

124
  """
125
  def __init__(self, min=0, max=None, choices=None):
126
    _Argument.__init__(self, min=min, max=max)
127
    self.choices = choices
128

    
129
  def __repr__(self):
130
    return ("<%s min=%s max=%s choices=%r>" %
131
            (self.__class__.__name__, self.min, self.max, self.choices))
132

    
133

    
134
class ArgChoice(ArgSuggest):
135
  """Choice argument.
136

137
  Value can be any of the ones passed to the constructor. Like L{ArgSuggest},
138
  but value must be one of the choices.
139

140
  """
141

    
142

    
143
class ArgUnknown(_Argument):
144
  """Unknown argument to program (e.g. determined at runtime).
145

146
  """
147

    
148

    
149
class ArgInstance(_Argument):
150
  """Instances argument.
151

152
  """
153

    
154

    
155
class ArgNode(_Argument):
156
  """Node argument.
157

158
  """
159

    
160
class ArgJobId(_Argument):
161
  """Job ID argument.
162

163
  """
164

    
165

    
166
class ArgFile(_Argument):
167
  """File path argument.
168

169
  """
170

    
171

    
172
class ArgCommand(_Argument):
173
  """Command argument.
174

175
  """
176

    
177

    
178
class ArgHost(_Argument):
179
  """Host argument.
180

181
  """
182

    
183

    
184
ARGS_NONE = []
185
ARGS_MANY_INSTANCES = [ArgInstance()]
186
ARGS_MANY_NODES = [ArgNode()]
187
ARGS_ONE_INSTANCE = [ArgInstance(min=1, max=1)]
188
ARGS_ONE_NODE = [ArgNode(min=1, max=1)]
189

    
190

    
191

    
192
def _ExtractTagsObject(opts, args):
193
  """Extract the tag type object.
194

195
  Note that this function will modify its args parameter.
196

197
  """
198
  if not hasattr(opts, "tag_type"):
199
    raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
200
  kind = opts.tag_type
201
  if kind == constants.TAG_CLUSTER:
202
    retval = kind, kind
203
  elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
204
    if not args:
205
      raise errors.OpPrereqError("no arguments passed to the command")
206
    name = args.pop(0)
207
    retval = kind, name
208
  else:
209
    raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
210
  return retval
211

    
212

    
213
def _ExtendTags(opts, args):
214
  """Extend the args if a source file has been given.
215

216
  This function will extend the tags with the contents of the file
217
  passed in the 'tags_source' attribute of the opts parameter. A file
218
  named '-' will be replaced by stdin.
219

220
  """
221
  fname = opts.tags_source
222
  if fname is None:
223
    return
224
  if fname == "-":
225
    new_fh = sys.stdin
226
  else:
227
    new_fh = open(fname, "r")
228
  new_data = []
229
  try:
230
    # we don't use the nice 'new_data = [line.strip() for line in fh]'
231
    # because of python bug 1633941
232
    while True:
233
      line = new_fh.readline()
234
      if not line:
235
        break
236
      new_data.append(line.strip())
237
  finally:
238
    new_fh.close()
239
  args.extend(new_data)
240

    
241

    
242
def ListTags(opts, args):
243
  """List the tags on a given object.
244

245
  This is a generic implementation that knows how to deal with all
246
  three cases of tag objects (cluster, node, instance). The opts
247
  argument is expected to contain a tag_type field denoting what
248
  object type we work on.
249

250
  """
251
  kind, name = _ExtractTagsObject(opts, args)
252
  op = opcodes.OpGetTags(kind=kind, name=name)
253
  result = SubmitOpCode(op)
254
  result = list(result)
255
  result.sort()
256
  for tag in result:
257
    ToStdout(tag)
258

    
259

    
260
def AddTags(opts, args):
261
  """Add tags on a given object.
262

263
  This is a generic implementation that knows how to deal with all
264
  three cases of tag objects (cluster, node, instance). The opts
265
  argument is expected to contain a tag_type field denoting what
266
  object type we work on.
267

268
  """
269
  kind, name = _ExtractTagsObject(opts, args)
270
  _ExtendTags(opts, args)
271
  if not args:
272
    raise errors.OpPrereqError("No tags to be added")
273
  op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
274
  SubmitOpCode(op)
275

    
276

    
277
def RemoveTags(opts, args):
278
  """Remove tags from a given object.
279

280
  This is a generic implementation that knows how to deal with all
281
  three cases of tag objects (cluster, node, instance). The opts
282
  argument is expected to contain a tag_type field denoting what
283
  object type we work on.
284

285
  """
286
  kind, name = _ExtractTagsObject(opts, args)
287
  _ExtendTags(opts, args)
288
  if not args:
289
    raise errors.OpPrereqError("No tags to be removed")
290
  op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
291
  SubmitOpCode(op)
292

    
293

    
294
def check_unit(option, opt, value):
295
  """OptParsers custom converter for units.
296

297
  """
298
  try:
299
    return utils.ParseUnit(value)
300
  except errors.UnitParseError, err:
301
    raise OptionValueError("option %s: %s" % (opt, err))
302

    
303

    
304
def _SplitKeyVal(opt, data):
305
  """Convert a KeyVal string into a dict.
306

307
  This function will convert a key=val[,...] string into a dict. Empty
308
  values will be converted specially: keys which have the prefix 'no_'
309
  will have the value=False and the prefix stripped, the others will
310
  have value=True.
311

312
  @type opt: string
313
  @param opt: a string holding the option name for which we process the
314
      data, used in building error messages
315
  @type data: string
316
  @param data: a string of the format key=val,key=val,...
317
  @rtype: dict
318
  @return: {key=val, key=val}
319
  @raises errors.ParameterError: if there are duplicate keys
320

321
  """
322
  kv_dict = {}
323
  if data:
324
    for elem in data.split(","):
325
      if "=" in elem:
326
        key, val = elem.split("=", 1)
327
      else:
328
        if elem.startswith(NO_PREFIX):
329
          key, val = elem[len(NO_PREFIX):], False
330
        elif elem.startswith(UN_PREFIX):
331
          key, val = elem[len(UN_PREFIX):], None
332
        else:
333
          key, val = elem, True
334
      if key in kv_dict:
335
        raise errors.ParameterError("Duplicate key '%s' in option %s" %
336
                                    (key, opt))
337
      kv_dict[key] = val
338
  return kv_dict
339

    
340

    
341
def check_ident_key_val(option, opt, value):
342
  """Custom parser for ident:key=val,key=val options.
343

344
  This will store the parsed values as a tuple (ident, {key: val}). As such,
345
  multiple uses of this option via action=append is possible.
346

347
  """
348
  if ":" not in value:
349
    ident, rest = value, ''
350
  else:
351
    ident, rest = value.split(":", 1)
352

    
353
  if ident.startswith(NO_PREFIX):
354
    if rest:
355
      msg = "Cannot pass options when removing parameter groups: %s" % value
356
      raise errors.ParameterError(msg)
357
    retval = (ident[len(NO_PREFIX):], False)
358
  elif ident.startswith(UN_PREFIX):
359
    if rest:
360
      msg = "Cannot pass options when removing parameter groups: %s" % value
361
      raise errors.ParameterError(msg)
362
    retval = (ident[len(UN_PREFIX):], None)
363
  else:
364
    kv_dict = _SplitKeyVal(opt, rest)
365
    retval = (ident, kv_dict)
366
  return retval
367

    
368

    
369
def check_key_val(option, opt, value):
370
  """Custom parser class for key=val,key=val options.
371

372
  This will store the parsed values as a dict {key: val}.
373

374
  """
375
  return _SplitKeyVal(opt, value)
376

    
377

    
378
# completion_suggestion is normally a list. Using numeric values not evaluating
379
# to False for dynamic completion.
380
(OPT_COMPL_MANY_NODES,
381
 OPT_COMPL_ONE_NODE,
382
 OPT_COMPL_ONE_INSTANCE,
383
 OPT_COMPL_ONE_OS,
384
 OPT_COMPL_ONE_IALLOCATOR,
385
 OPT_COMPL_INST_ADD_NODES) = range(100, 106)
386

    
387
OPT_COMPL_ALL = frozenset([
388
  OPT_COMPL_MANY_NODES,
389
  OPT_COMPL_ONE_NODE,
390
  OPT_COMPL_ONE_INSTANCE,
391
  OPT_COMPL_ONE_OS,
392
  OPT_COMPL_ONE_IALLOCATOR,
393
  OPT_COMPL_INST_ADD_NODES,
394
  ])
395

    
396

    
397
class CliOption(Option):
398
  """Custom option class for optparse.
399

400
  """
401
  ATTRS = Option.ATTRS + [
402
    "completion_suggest",
403
    ]
404
  TYPES = Option.TYPES + (
405
    "identkeyval",
406
    "keyval",
407
    "unit",
408
    )
409
  TYPE_CHECKER = Option.TYPE_CHECKER.copy()
410
  TYPE_CHECKER["identkeyval"] = check_ident_key_val
411
  TYPE_CHECKER["keyval"] = check_key_val
412
  TYPE_CHECKER["unit"] = check_unit
413

    
414

    
415
# optparse.py sets make_option, so we do it for our own option class, too
416
cli_option = CliOption
417

    
418

    
419
DEBUG_OPT = cli_option("-d", "--debug", default=False,
420
                       action="store_true",
421
                       help="Turn debugging on")
422

    
423
NOHDR_OPT = cli_option("--no-headers", default=False,
424
                       action="store_true", dest="no_headers",
425
                       help="Don't display column headers")
426

    
427
SEP_OPT = cli_option("--separator", default=None,
428
                     action="store", dest="separator",
429
                     help=("Separator between output fields"
430
                           " (defaults to one space)"))
431

    
432
USEUNITS_OPT = cli_option("--units", default=None,
433
                          dest="units", choices=('h', 'm', 'g', 't'),
434
                          help="Specify units for output (one of hmgt)")
435

    
436
FIELDS_OPT = cli_option("-o", "--output", dest="output", action="store",
437
                        type="string", metavar="FIELDS",
438
                        help="Comma separated list of output fields")
439

    
440
FORCE_OPT = cli_option("-f", "--force", dest="force", action="store_true",
441
                       default=False, help="Force the operation")
442

    
443
CONFIRM_OPT = cli_option("--yes", dest="confirm", action="store_true",
444
                         default=False, help="Do not require confirmation")
445

    
446
TAG_SRC_OPT = cli_option("--from", dest="tags_source",
447
                         default=None, help="File with tag names")
448

    
449
SUBMIT_OPT = cli_option("--submit", dest="submit_only",
450
                        default=False, action="store_true",
451
                        help=("Submit the job and return the job ID, but"
452
                              " don't wait for the job to finish"))
453

    
454
SYNC_OPT = cli_option("--sync", dest="do_locking",
455
                      default=False, action="store_true",
456
                      help=("Grab locks while doing the queries"
457
                            " in order to ensure more consistent results"))
458

    
459
_DRY_RUN_OPT = cli_option("--dry-run", default=False,
460
                          action="store_true",
461
                          help=("Do not execute the operation, just run the"
462
                                " check steps and verify it it could be"
463
                                " executed"))
464

    
465
VERBOSE_OPT = cli_option("-v", "--verbose", default=False,
466
                         action="store_true",
467
                         help="Increase the verbosity of the operation")
468

    
469
DEBUG_SIMERR_OPT = cli_option("--debug-simulate-errors", default=False,
470
                              action="store_true", dest="simulate_errors",
471
                              help="Debugging option that makes the operation"
472
                              " treat most runtime checks as failed")
473

    
474
NWSYNC_OPT = cli_option("--no-wait-for-sync", dest="wait_for_sync",
475
                        default=True, action="store_false",
476
                        help="Don't wait for sync (DANGEROUS!)")
477

    
478

    
479
def _ParseArgs(argv, commands, aliases):
480
  """Parser for the command line arguments.
481

482
  This function parses the arguments and returns the function which
483
  must be executed together with its (modified) arguments.
484

485
  @param argv: the command line
486
  @param commands: dictionary with special contents, see the design
487
      doc for cmdline handling
488
  @param aliases: dictionary with command aliases {'alias': 'target, ...}
489

490
  """
491
  if len(argv) == 0:
492
    binary = "<command>"
493
  else:
494
    binary = argv[0].split("/")[-1]
495

    
496
  if len(argv) > 1 and argv[1] == "--version":
497
    ToStdout("%s (ganeti) %s", binary, constants.RELEASE_VERSION)
498
    # Quit right away. That way we don't have to care about this special
499
    # argument. optparse.py does it the same.
500
    sys.exit(0)
501

    
502
  if len(argv) < 2 or not (argv[1] in commands or
503
                           argv[1] in aliases):
504
    # let's do a nice thing
505
    sortedcmds = commands.keys()
506
    sortedcmds.sort()
507

    
508
    ToStdout("Usage: %s {command} [options...] [argument...]", binary)
509
    ToStdout("%s <command> --help to see details, or man %s", binary, binary)
510
    ToStdout("")
511

    
512
    # compute the max line length for cmd + usage
513
    mlen = max([len(" %s" % cmd) for cmd in commands])
514
    mlen = min(60, mlen) # should not get here...
515

    
516
    # and format a nice command list
517
    ToStdout("Commands:")
518
    for cmd in sortedcmds:
519
      cmdstr = " %s" % (cmd,)
520
      help_text = commands[cmd][4]
521
      help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
522
      ToStdout("%-*s - %s", mlen, cmdstr, help_lines.pop(0))
523
      for line in help_lines:
524
        ToStdout("%-*s   %s", mlen, "", line)
525

    
526
    ToStdout("")
527

    
528
    return None, None, None
529

    
530
  # get command, unalias it, and look it up in commands
531
  cmd = argv.pop(1)
532
  if cmd in aliases:
533
    if cmd in commands:
534
      raise errors.ProgrammerError("Alias '%s' overrides an existing"
535
                                   " command" % cmd)
536

    
537
    if aliases[cmd] not in commands:
538
      raise errors.ProgrammerError("Alias '%s' maps to non-existing"
539
                                   " command '%s'" % (cmd, aliases[cmd]))
540

    
541
    cmd = aliases[cmd]
542

    
543
  func, args_def, parser_opts, usage, description = commands[cmd]
544
  parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT],
545
                        description=description,
546
                        formatter=TitledHelpFormatter(),
547
                        usage="%%prog %s %s" % (cmd, usage))
548
  parser.disable_interspersed_args()
549
  options, args = parser.parse_args()
550

    
551
  if not _CheckArguments(cmd, args_def, args):
552
    return None, None, None
553

    
554
  return func, options, args
555

    
556

    
557
def _CheckArguments(cmd, args_def, args):
558
  """Verifies the arguments using the argument definition.
559

560
  Algorithm:
561

562
    1. Abort with error if values specified by user but none expected.
563

564
    1. For each argument in definition
565

566
      1. Keep running count of minimum number of values (min_count)
567
      1. Keep running count of maximum number of values (max_count)
568
      1. If it has an unlimited number of values
569

570
        1. Abort with error if it's not the last argument in the definition
571

572
    1. If last argument has limited number of values
573

574
      1. Abort with error if number of values doesn't match or is too large
575

576
    1. Abort with error if user didn't pass enough values (min_count)
577

578
  """
579
  if args and not args_def:
580
    ToStderr("Error: Command %s expects no arguments", cmd)
581
    return False
582

    
583
  min_count = None
584
  max_count = None
585
  check_max = None
586

    
587
  last_idx = len(args_def) - 1
588

    
589
  for idx, arg in enumerate(args_def):
590
    if min_count is None:
591
      min_count = arg.min
592
    elif arg.min is not None:
593
      min_count += arg.min
594

    
595
    if max_count is None:
596
      max_count = arg.max
597
    elif arg.max is not None:
598
      max_count += arg.max
599

    
600
    if idx == last_idx:
601
      check_max = (arg.max is not None)
602

    
603
    elif arg.max is None:
604
      raise errors.ProgrammerError("Only the last argument can have max=None")
605

    
606
  if check_max:
607
    # Command with exact number of arguments
608
    if (min_count is not None and max_count is not None and
609
        min_count == max_count and len(args) != min_count):
610
      ToStderr("Error: Command %s expects %d argument(s)", cmd, min_count)
611
      return False
612

    
613
    # Command with limited number of arguments
614
    if max_count is not None and len(args) > max_count:
615
      ToStderr("Error: Command %s expects only %d argument(s)",
616
               cmd, max_count)
617
      return False
618

    
619
  # Command with some required arguments
620
  if min_count is not None and len(args) < min_count:
621
    ToStderr("Error: Command %s expects at least %d argument(s)",
622
             cmd, min_count)
623
    return False
624

    
625
  return True
626

    
627

    
628
def SplitNodeOption(value):
629
  """Splits the value of a --node option.
630

631
  """
632
  if value and ':' in value:
633
    return value.split(':', 1)
634
  else:
635
    return (value, None)
636

    
637

    
638
def UsesRPC(fn):
639
  def wrapper(*args, **kwargs):
640
    rpc.Init()
641
    try:
642
      return fn(*args, **kwargs)
643
    finally:
644
      rpc.Shutdown()
645
  return wrapper
646

    
647

    
648
def AskUser(text, choices=None):
649
  """Ask the user a question.
650

651
  @param text: the question to ask
652

653
  @param choices: list with elements tuples (input_char, return_value,
654
      description); if not given, it will default to: [('y', True,
655
      'Perform the operation'), ('n', False, 'Do no do the operation')];
656
      note that the '?' char is reserved for help
657

658
  @return: one of the return values from the choices list; if input is
659
      not possible (i.e. not running with a tty, we return the last
660
      entry from the list
661

662
  """
663
  if choices is None:
664
    choices = [('y', True, 'Perform the operation'),
665
               ('n', False, 'Do not perform the operation')]
666
  if not choices or not isinstance(choices, list):
667
    raise errors.ProgrammerError("Invalid choices argument to AskUser")
668
  for entry in choices:
669
    if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
670
      raise errors.ProgrammerError("Invalid choices element to AskUser")
671

    
672
  answer = choices[-1][1]
673
  new_text = []
674
  for line in text.splitlines():
675
    new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
676
  text = "\n".join(new_text)
677
  try:
678
    f = file("/dev/tty", "a+")
679
  except IOError:
680
    return answer
681
  try:
682
    chars = [entry[0] for entry in choices]
683
    chars[-1] = "[%s]" % chars[-1]
684
    chars.append('?')
685
    maps = dict([(entry[0], entry[1]) for entry in choices])
686
    while True:
687
      f.write(text)
688
      f.write('\n')
689
      f.write("/".join(chars))
690
      f.write(": ")
691
      line = f.readline(2).strip().lower()
692
      if line in maps:
693
        answer = maps[line]
694
        break
695
      elif line == '?':
696
        for entry in choices:
697
          f.write(" %s - %s\n" % (entry[0], entry[2]))
698
        f.write("\n")
699
        continue
700
  finally:
701
    f.close()
702
  return answer
703

    
704

    
705
class JobSubmittedException(Exception):
706
  """Job was submitted, client should exit.
707

708
  This exception has one argument, the ID of the job that was
709
  submitted. The handler should print this ID.
710

711
  This is not an error, just a structured way to exit from clients.
712

713
  """
714

    
715

    
716
def SendJob(ops, cl=None):
717
  """Function to submit an opcode without waiting for the results.
718

719
  @type ops: list
720
  @param ops: list of opcodes
721
  @type cl: luxi.Client
722
  @param cl: the luxi client to use for communicating with the master;
723
             if None, a new client will be created
724

725
  """
726
  if cl is None:
727
    cl = GetClient()
728

    
729
  job_id = cl.SubmitJob(ops)
730

    
731
  return job_id
732

    
733

    
734
def PollJob(job_id, cl=None, feedback_fn=None):
735
  """Function to poll for the result of a job.
736

737
  @type job_id: job identified
738
  @param job_id: the job to poll for results
739
  @type cl: luxi.Client
740
  @param cl: the luxi client to use for communicating with the master;
741
             if None, a new client will be created
742

743
  """
744
  if cl is None:
745
    cl = GetClient()
746

    
747
  prev_job_info = None
748
  prev_logmsg_serial = None
749

    
750
  while True:
751
    result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
752
                                 prev_logmsg_serial)
753
    if not result:
754
      # job not found, go away!
755
      raise errors.JobLost("Job with id %s lost" % job_id)
756

    
757
    # Split result, a tuple of (field values, log entries)
758
    (job_info, log_entries) = result
759
    (status, ) = job_info
760

    
761
    if log_entries:
762
      for log_entry in log_entries:
763
        (serial, timestamp, _, message) = log_entry
764
        if callable(feedback_fn):
765
          feedback_fn(log_entry[1:])
766
        else:
767
          encoded = utils.SafeEncode(message)
768
          ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)), encoded)
769
        prev_logmsg_serial = max(prev_logmsg_serial, serial)
770

    
771
    # TODO: Handle canceled and archived jobs
772
    elif status in (constants.JOB_STATUS_SUCCESS,
773
                    constants.JOB_STATUS_ERROR,
774
                    constants.JOB_STATUS_CANCELING,
775
                    constants.JOB_STATUS_CANCELED):
776
      break
777

    
778
    prev_job_info = job_info
779

    
780
  jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
781
  if not jobs:
782
    raise errors.JobLost("Job with id %s lost" % job_id)
783

    
784
  status, opstatus, result = jobs[0]
785
  if status == constants.JOB_STATUS_SUCCESS:
786
    return result
787
  elif status in (constants.JOB_STATUS_CANCELING,
788
                  constants.JOB_STATUS_CANCELED):
789
    raise errors.OpExecError("Job was canceled")
790
  else:
791
    has_ok = False
792
    for idx, (status, msg) in enumerate(zip(opstatus, result)):
793
      if status == constants.OP_STATUS_SUCCESS:
794
        has_ok = True
795
      elif status == constants.OP_STATUS_ERROR:
796
        errors.MaybeRaise(msg)
797
        if has_ok:
798
          raise errors.OpExecError("partial failure (opcode %d): %s" %
799
                                   (idx, msg))
800
        else:
801
          raise errors.OpExecError(str(msg))
802
    # default failure mode
803
    raise errors.OpExecError(result)
804

    
805

    
806
def SubmitOpCode(op, cl=None, feedback_fn=None):
807
  """Legacy function to submit an opcode.
808

809
  This is just a simple wrapper over the construction of the processor
810
  instance. It should be extended to better handle feedback and
811
  interaction functions.
812

813
  """
814
  if cl is None:
815
    cl = GetClient()
816

    
817
  job_id = SendJob([op], cl)
818

    
819
  op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
820

    
821
  return op_results[0]
822

    
823

    
824
def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
825
  """Wrapper around SubmitOpCode or SendJob.
826

827
  This function will decide, based on the 'opts' parameter, whether to
828
  submit and wait for the result of the opcode (and return it), or
829
  whether to just send the job and print its identifier. It is used in
830
  order to simplify the implementation of the '--submit' option.
831

832
  It will also add the dry-run parameter from the options passed, if true.
833

834
  """
835
  if opts and opts.dry_run:
836
    op.dry_run = opts.dry_run
837
  if opts and opts.submit_only:
838
    job_id = SendJob([op], cl=cl)
839
    raise JobSubmittedException(job_id)
840
  else:
841
    return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
842

    
843

    
844
def GetClient():
845
  # TODO: Cache object?
846
  try:
847
    client = luxi.Client()
848
  except luxi.NoMasterError:
849
    master, myself = ssconf.GetMasterAndMyself()
850
    if master != myself:
851
      raise errors.OpPrereqError("This is not the master node, please connect"
852
                                 " to node '%s' and rerun the command" %
853
                                 master)
854
    else:
855
      raise
856
  return client
857

    
858

    
859
def FormatError(err):
860
  """Return a formatted error message for a given error.
861

862
  This function takes an exception instance and returns a tuple
863
  consisting of two values: first, the recommended exit code, and
864
  second, a string describing the error message (not
865
  newline-terminated).
866

867
  """
868
  retcode = 1
869
  obuf = StringIO()
870
  msg = str(err)
871
  if isinstance(err, errors.ConfigurationError):
872
    txt = "Corrupt configuration file: %s" % msg
873
    logging.error(txt)
874
    obuf.write(txt + "\n")
875
    obuf.write("Aborting.")
876
    retcode = 2
877
  elif isinstance(err, errors.HooksAbort):
878
    obuf.write("Failure: hooks execution failed:\n")
879
    for node, script, out in err.args[0]:
880
      if out:
881
        obuf.write("  node: %s, script: %s, output: %s\n" %
882
                   (node, script, out))
883
      else:
884
        obuf.write("  node: %s, script: %s (no output)\n" %
885
                   (node, script))
886
  elif isinstance(err, errors.HooksFailure):
887
    obuf.write("Failure: hooks general failure: %s" % msg)
888
  elif isinstance(err, errors.ResolverError):
889
    this_host = utils.HostInfo.SysName()
890
    if err.args[0] == this_host:
891
      msg = "Failure: can't resolve my own hostname ('%s')"
892
    else:
893
      msg = "Failure: can't resolve hostname '%s'"
894
    obuf.write(msg % err.args[0])
895
  elif isinstance(err, errors.OpPrereqError):
896
    obuf.write("Failure: prerequisites not met for this"
897
               " operation:\n%s" % msg)
898
  elif isinstance(err, errors.OpExecError):
899
    obuf.write("Failure: command execution error:\n%s" % msg)
900
  elif isinstance(err, errors.TagError):
901
    obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
902
  elif isinstance(err, errors.JobQueueDrainError):
903
    obuf.write("Failure: the job queue is marked for drain and doesn't"
904
               " accept new requests\n")
905
  elif isinstance(err, errors.JobQueueFull):
906
    obuf.write("Failure: the job queue is full and doesn't accept new"
907
               " job submissions until old jobs are archived\n")
908
  elif isinstance(err, errors.TypeEnforcementError):
909
    obuf.write("Parameter Error: %s" % msg)
910
  elif isinstance(err, errors.ParameterError):
911
    obuf.write("Failure: unknown/wrong parameter name '%s'" % msg)
912
  elif isinstance(err, errors.GenericError):
913
    obuf.write("Unhandled Ganeti error: %s" % msg)
914
  elif isinstance(err, luxi.NoMasterError):
915
    obuf.write("Cannot communicate with the master daemon.\nIs it running"
916
               " and listening for connections?")
917
  elif isinstance(err, luxi.TimeoutError):
918
    obuf.write("Timeout while talking to the master daemon. Error:\n"
919
               "%s" % msg)
920
  elif isinstance(err, luxi.ProtocolError):
921
    obuf.write("Unhandled protocol error while talking to the master daemon:\n"
922
               "%s" % msg)
923
  elif isinstance(err, JobSubmittedException):
924
    obuf.write("JobID: %s\n" % err.args[0])
925
    retcode = 0
926
  else:
927
    obuf.write("Unhandled exception: %s" % msg)
928
  return retcode, obuf.getvalue().rstrip('\n')
929

    
930

    
931
def GenericMain(commands, override=None, aliases=None):
932
  """Generic main function for all the gnt-* commands.
933

934
  Arguments:
935
    - commands: a dictionary with a special structure, see the design doc
936
                for command line handling.
937
    - override: if not None, we expect a dictionary with keys that will
938
                override command line options; this can be used to pass
939
                options from the scripts to generic functions
940
    - aliases: dictionary with command aliases {'alias': 'target, ...}
941

942
  """
943
  # save the program name and the entire command line for later logging
944
  if sys.argv:
945
    binary = os.path.basename(sys.argv[0]) or sys.argv[0]
946
    if len(sys.argv) >= 2:
947
      binary += " " + sys.argv[1]
948
      old_cmdline = " ".join(sys.argv[2:])
949
    else:
950
      old_cmdline = ""
951
  else:
952
    binary = "<unknown program>"
953
    old_cmdline = ""
954

    
955
  if aliases is None:
956
    aliases = {}
957

    
958
  try:
959
    func, options, args = _ParseArgs(sys.argv, commands, aliases)
960
  except errors.ParameterError, err:
961
    result, err_msg = FormatError(err)
962
    ToStderr(err_msg)
963
    return 1
964

    
965
  if func is None: # parse error
966
    return 1
967

    
968
  if override is not None:
969
    for key, val in override.iteritems():
970
      setattr(options, key, val)
971

    
972
  utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
973
                     stderr_logging=True, program=binary)
974

    
975
  if old_cmdline:
976
    logging.info("run with arguments '%s'", old_cmdline)
977
  else:
978
    logging.info("run with no arguments")
979

    
980
  try:
981
    result = func(options, args)
982
  except (errors.GenericError, luxi.ProtocolError,
983
          JobSubmittedException), err:
984
    result, err_msg = FormatError(err)
985
    logging.exception("Error during command processing")
986
    ToStderr(err_msg)
987

    
988
  return result
989

    
990

    
991
def GenerateTable(headers, fields, separator, data,
992
                  numfields=None, unitfields=None,
993
                  units=None):
994
  """Prints a table with headers and different fields.
995

996
  @type headers: dict
997
  @param headers: dictionary mapping field names to headers for
998
      the table
999
  @type fields: list
1000
  @param fields: the field names corresponding to each row in
1001
      the data field
1002
  @param separator: the separator to be used; if this is None,
1003
      the default 'smart' algorithm is used which computes optimal
1004
      field width, otherwise just the separator is used between
1005
      each field
1006
  @type data: list
1007
  @param data: a list of lists, each sublist being one row to be output
1008
  @type numfields: list
1009
  @param numfields: a list with the fields that hold numeric
1010
      values and thus should be right-aligned
1011
  @type unitfields: list
1012
  @param unitfields: a list with the fields that hold numeric
1013
      values that should be formatted with the units field
1014
  @type units: string or None
1015
  @param units: the units we should use for formatting, or None for
1016
      automatic choice (human-readable for non-separator usage, otherwise
1017
      megabytes); this is a one-letter string
1018

1019
  """
1020
  if units is None:
1021
    if separator:
1022
      units = "m"
1023
    else:
1024
      units = "h"
1025

    
1026
  if numfields is None:
1027
    numfields = []
1028
  if unitfields is None:
1029
    unitfields = []
1030

    
1031
  numfields = utils.FieldSet(*numfields)
1032
  unitfields = utils.FieldSet(*unitfields)
1033

    
1034
  format_fields = []
1035
  for field in fields:
1036
    if headers and field not in headers:
1037
      # TODO: handle better unknown fields (either revert to old
1038
      # style of raising exception, or deal more intelligently with
1039
      # variable fields)
1040
      headers[field] = field
1041
    if separator is not None:
1042
      format_fields.append("%s")
1043
    elif numfields.Matches(field):
1044
      format_fields.append("%*s")
1045
    else:
1046
      format_fields.append("%-*s")
1047

    
1048
  if separator is None:
1049
    mlens = [0 for name in fields]
1050
    format = ' '.join(format_fields)
1051
  else:
1052
    format = separator.replace("%", "%%").join(format_fields)
1053

    
1054
  for row in data:
1055
    if row is None:
1056
      continue
1057
    for idx, val in enumerate(row):
1058
      if unitfields.Matches(fields[idx]):
1059
        try:
1060
          val = int(val)
1061
        except ValueError:
1062
          pass
1063
        else:
1064
          val = row[idx] = utils.FormatUnit(val, units)
1065
      val = row[idx] = str(val)
1066
      if separator is None:
1067
        mlens[idx] = max(mlens[idx], len(val))
1068

    
1069
  result = []
1070
  if headers:
1071
    args = []
1072
    for idx, name in enumerate(fields):
1073
      hdr = headers[name]
1074
      if separator is None:
1075
        mlens[idx] = max(mlens[idx], len(hdr))
1076
        args.append(mlens[idx])
1077
      args.append(hdr)
1078
    result.append(format % tuple(args))
1079

    
1080
  for line in data:
1081
    args = []
1082
    if line is None:
1083
      line = ['-' for _ in fields]
1084
    for idx in xrange(len(fields)):
1085
      if separator is None:
1086
        args.append(mlens[idx])
1087
      args.append(line[idx])
1088
    result.append(format % tuple(args))
1089

    
1090
  return result
1091

    
1092

    
1093
def FormatTimestamp(ts):
1094
  """Formats a given timestamp.
1095

1096
  @type ts: timestamp
1097
  @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
1098

1099
  @rtype: string
1100
  @return: a string with the formatted timestamp
1101

1102
  """
1103
  if not isinstance (ts, (tuple, list)) or len(ts) != 2:
1104
    return '?'
1105
  sec, usec = ts
1106
  return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
1107

    
1108

    
1109
def ParseTimespec(value):
1110
  """Parse a time specification.
1111

1112
  The following suffixed will be recognized:
1113

1114
    - s: seconds
1115
    - m: minutes
1116
    - h: hours
1117
    - d: day
1118
    - w: weeks
1119

1120
  Without any suffix, the value will be taken to be in seconds.
1121

1122
  """
1123
  value = str(value)
1124
  if not value:
1125
    raise errors.OpPrereqError("Empty time specification passed")
1126
  suffix_map = {
1127
    's': 1,
1128
    'm': 60,
1129
    'h': 3600,
1130
    'd': 86400,
1131
    'w': 604800,
1132
    }
1133
  if value[-1] not in suffix_map:
1134
    try:
1135
      value = int(value)
1136
    except ValueError:
1137
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1138
  else:
1139
    multiplier = suffix_map[value[-1]]
1140
    value = value[:-1]
1141
    if not value: # no data left after stripping the suffix
1142
      raise errors.OpPrereqError("Invalid time specification (only"
1143
                                 " suffix passed)")
1144
    try:
1145
      value = int(value) * multiplier
1146
    except ValueError:
1147
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1148
  return value
1149

    
1150

    
1151
def GetOnlineNodes(nodes, cl=None, nowarn=False):
1152
  """Returns the names of online nodes.
1153

1154
  This function will also log a warning on stderr with the names of
1155
  the online nodes.
1156

1157
  @param nodes: if not empty, use only this subset of nodes (minus the
1158
      offline ones)
1159
  @param cl: if not None, luxi client to use
1160
  @type nowarn: boolean
1161
  @param nowarn: by default, this function will output a note with the
1162
      offline nodes that are skipped; if this parameter is True the
1163
      note is not displayed
1164

1165
  """
1166
  if cl is None:
1167
    cl = GetClient()
1168

    
1169
  result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
1170
                         use_locking=False)
1171
  offline = [row[0] for row in result if row[1]]
1172
  if offline and not nowarn:
1173
    ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
1174
  return [row[0] for row in result if not row[1]]
1175

    
1176

    
1177
def _ToStream(stream, txt, *args):
1178
  """Write a message to a stream, bypassing the logging system
1179

1180
  @type stream: file object
1181
  @param stream: the file to which we should write
1182
  @type txt: str
1183
  @param txt: the message
1184

1185
  """
1186
  if args:
1187
    args = tuple(args)
1188
    stream.write(txt % args)
1189
  else:
1190
    stream.write(txt)
1191
  stream.write('\n')
1192
  stream.flush()
1193

    
1194

    
1195
def ToStdout(txt, *args):
1196
  """Write a message to stdout only, bypassing the logging system
1197

1198
  This is just a wrapper over _ToStream.
1199

1200
  @type txt: str
1201
  @param txt: the message
1202

1203
  """
1204
  _ToStream(sys.stdout, txt, *args)
1205

    
1206

    
1207
def ToStderr(txt, *args):
1208
  """Write a message to stderr only, bypassing the logging system
1209

1210
  This is just a wrapper over _ToStream.
1211

1212
  @type txt: str
1213
  @param txt: the message
1214

1215
  """
1216
  _ToStream(sys.stderr, txt, *args)
1217

    
1218

    
1219
class JobExecutor(object):
1220
  """Class which manages the submission and execution of multiple jobs.
1221

1222
  Note that instances of this class should not be reused between
1223
  GetResults() calls.
1224

1225
  """
1226
  def __init__(self, cl=None, verbose=True):
1227
    self.queue = []
1228
    if cl is None:
1229
      cl = GetClient()
1230
    self.cl = cl
1231
    self.verbose = verbose
1232
    self.jobs = []
1233

    
1234
  def QueueJob(self, name, *ops):
1235
    """Record a job for later submit.
1236

1237
    @type name: string
1238
    @param name: a description of the job, will be used in WaitJobSet
1239
    """
1240
    self.queue.append((name, ops))
1241

    
1242
  def SubmitPending(self):
1243
    """Submit all pending jobs.
1244

1245
    """
1246
    results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1247
    for ((status, data), (name, _)) in zip(results, self.queue):
1248
      self.jobs.append((status, data, name))
1249

    
1250
  def GetResults(self):
1251
    """Wait for and return the results of all jobs.
1252

1253
    @rtype: list
1254
    @return: list of tuples (success, job results), in the same order
1255
        as the submitted jobs; if a job has failed, instead of the result
1256
        there will be the error message
1257

1258
    """
1259
    if not self.jobs:
1260
      self.SubmitPending()
1261
    results = []
1262
    if self.verbose:
1263
      ok_jobs = [row[1] for row in self.jobs if row[0]]
1264
      if ok_jobs:
1265
        ToStdout("Submitted jobs %s", ", ".join(ok_jobs))
1266
    for submit_status, jid, name in self.jobs:
1267
      if not submit_status:
1268
        ToStderr("Failed to submit job for %s: %s", name, jid)
1269
        results.append((False, jid))
1270
        continue
1271
      if self.verbose:
1272
        ToStdout("Waiting for job %s for %s...", jid, name)
1273
      try:
1274
        job_result = PollJob(jid, cl=self.cl)
1275
        success = True
1276
      except (errors.GenericError, luxi.ProtocolError), err:
1277
        _, job_result = FormatError(err)
1278
        success = False
1279
        # the error message will always be shown, verbose or not
1280
        ToStderr("Job %s for %s has failed: %s", jid, name, job_result)
1281

    
1282
      results.append((success, job_result))
1283
    return results
1284

    
1285
  def WaitOrShow(self, wait):
1286
    """Wait for job results or only print the job IDs.
1287

1288
    @type wait: boolean
1289
    @param wait: whether to wait or not
1290

1291
    """
1292
    if wait:
1293
      return self.GetResults()
1294
    else:
1295
      if not self.jobs:
1296
        self.SubmitPending()
1297
      for status, result, name in self.jobs:
1298
        if status:
1299
          ToStdout("%s: %s", result, name)
1300
        else:
1301
          ToStderr("Failure for %s: %s", name, result)