Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ 63d44c55

History | View | Annotate | Download (35.7 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__ = ["DEBUG_OPT", "NOHDR_OPT", "SEP_OPT", "GenericMain",
46
           "SubmitOpCode", "GetClient",
47
           "cli_option",
48
           "GenerateTable", "AskUser",
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
           "ToStderr", "ToStdout", "UsesRPC",
54
           "GetOnlineNodes", "JobExecutor", "SYNC_OPT", "CONFIRM_OPT",
55
           "ArgJobId", "ArgSuggest", "ArgUnknown", "ArgFile", "ArgCommand",
56
           "ArgInstance", "ArgNode", "ArgChoice", "ArgHost",
57
           "ARGS_NONE", "ARGS_ONE_INSTANCE", "ARGS_ONE_NODE",
58
           "ARGS_MANY_INSTANCES", "ARGS_MANY_NODES",
59
           "OPT_COMPL_ONE_NODE", "OPT_COMPL_ONE_INSTANCE",
60
           "OPT_COMPL_MANY_NODES",
61
           "OPT_COMPL_ONE_OS", "OPT_COMPL_ONE_IALLOCATOR",
62
           ]
63

    
64
NO_PREFIX = "no_"
65
UN_PREFIX = "-"
66

    
67

    
68
class _Argument:
69
  def __init__(self, min=0, max=None):
70
    self.min = min
71
    self.max = max
72

    
73
  def __repr__(self):
74
    return ("<%s min=%s max=%s>" %
75
            (self.__class__.__name__, self.min, self.max))
76

    
77

    
78
class ArgSuggest(_Argument):
79
  """Suggesting argument.
80

81
  Value can be any of the ones passed to the constructor.
82

83
  """
84
  def __init__(self, min=0, max=None, choices=None):
85
    _Argument.__init__(self, min=min, max=max)
86
    self.choices = choices
87

    
88
  def __repr__(self):
89
    return ("<%s min=%s max=%s choices=%r>" %
90
            (self.__class__.__name__, self.min, self.max, self.choices))
91

    
92

    
93
class ArgChoice(ArgSuggest):
94
  """Choice argument.
95

96
  Value can be any of the ones passed to the constructor. Like L{ArgSuggest},
97
  but value must be one of the choices.
98

99
  """
100

    
101

    
102
class ArgUnknown(_Argument):
103
  """Unknown argument to program (e.g. determined at runtime).
104

105
  """
106

    
107

    
108
class ArgInstance(_Argument):
109
  """Instances argument.
110

111
  """
112

    
113

    
114
class ArgNode(_Argument):
115
  """Node argument.
116

117
  """
118

    
119
class ArgJobId(_Argument):
120
  """Job ID argument.
121

122
  """
123

    
124

    
125
class ArgFile(_Argument):
126
  """File path argument.
127

128
  """
129

    
130

    
131
class ArgCommand(_Argument):
132
  """Command argument.
133

134
  """
135

    
136

    
137
class ArgHost(_Argument):
138
  """Host argument.
139

140
  """
141

    
142

    
143
ARGS_NONE = []
144
ARGS_MANY_INSTANCES = [ArgInstance()]
145
ARGS_MANY_NODES = [ArgNode()]
146
ARGS_ONE_INSTANCE = [ArgInstance(min=1, max=1)]
147
ARGS_ONE_NODE = [ArgNode(min=1, max=1)]
148

    
149

    
150
def _ExtractTagsObject(opts, args):
151
  """Extract the tag type object.
152

153
  Note that this function will modify its args parameter.
154

155
  """
156
  if not hasattr(opts, "tag_type"):
157
    raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
158
  kind = opts.tag_type
159
  if kind == constants.TAG_CLUSTER:
160
    retval = kind, kind
161
  elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
162
    if not args:
163
      raise errors.OpPrereqError("no arguments passed to the command")
164
    name = args.pop(0)
165
    retval = kind, name
166
  else:
167
    raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
168
  return retval
169

    
170

    
171
def _ExtendTags(opts, args):
172
  """Extend the args if a source file has been given.
173

174
  This function will extend the tags with the contents of the file
175
  passed in the 'tags_source' attribute of the opts parameter. A file
176
  named '-' will be replaced by stdin.
177

178
  """
179
  fname = opts.tags_source
180
  if fname is None:
181
    return
182
  if fname == "-":
183
    new_fh = sys.stdin
184
  else:
185
    new_fh = open(fname, "r")
186
  new_data = []
187
  try:
188
    # we don't use the nice 'new_data = [line.strip() for line in fh]'
189
    # because of python bug 1633941
190
    while True:
191
      line = new_fh.readline()
192
      if not line:
193
        break
194
      new_data.append(line.strip())
195
  finally:
196
    new_fh.close()
197
  args.extend(new_data)
198

    
199

    
200
def ListTags(opts, args):
201
  """List the tags on a given object.
202

203
  This is a generic implementation that knows how to deal with all
204
  three cases of tag objects (cluster, node, instance). The opts
205
  argument is expected to contain a tag_type field denoting what
206
  object type we work on.
207

208
  """
209
  kind, name = _ExtractTagsObject(opts, args)
210
  op = opcodes.OpGetTags(kind=kind, name=name)
211
  result = SubmitOpCode(op)
212
  result = list(result)
213
  result.sort()
214
  for tag in result:
215
    ToStdout(tag)
216

    
217

    
218
def AddTags(opts, args):
219
  """Add tags on a given object.
220

221
  This is a generic implementation that knows how to deal with all
222
  three cases of tag objects (cluster, node, instance). The opts
223
  argument is expected to contain a tag_type field denoting what
224
  object type we work on.
225

226
  """
227
  kind, name = _ExtractTagsObject(opts, args)
228
  _ExtendTags(opts, args)
229
  if not args:
230
    raise errors.OpPrereqError("No tags to be added")
231
  op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
232
  SubmitOpCode(op)
233

    
234

    
235
def RemoveTags(opts, args):
236
  """Remove tags from a given object.
237

238
  This is a generic implementation that knows how to deal with all
239
  three cases of tag objects (cluster, node, instance). The opts
240
  argument is expected to contain a tag_type field denoting what
241
  object type we work on.
242

243
  """
244
  kind, name = _ExtractTagsObject(opts, args)
245
  _ExtendTags(opts, args)
246
  if not args:
247
    raise errors.OpPrereqError("No tags to be removed")
248
  op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
249
  SubmitOpCode(op)
250

    
251

    
252
def check_unit(option, opt, value):
253
  """OptParsers custom converter for units.
254

255
  """
256
  try:
257
    return utils.ParseUnit(value)
258
  except errors.UnitParseError, err:
259
    raise OptionValueError("option %s: %s" % (opt, err))
260

    
261

    
262
def _SplitKeyVal(opt, data):
263
  """Convert a KeyVal string into a dict.
264

265
  This function will convert a key=val[,...] string into a dict. Empty
266
  values will be converted specially: keys which have the prefix 'no_'
267
  will have the value=False and the prefix stripped, the others will
268
  have value=True.
269

270
  @type opt: string
271
  @param opt: a string holding the option name for which we process the
272
      data, used in building error messages
273
  @type data: string
274
  @param data: a string of the format key=val,key=val,...
275
  @rtype: dict
276
  @return: {key=val, key=val}
277
  @raises errors.ParameterError: if there are duplicate keys
278

279
  """
280
  kv_dict = {}
281
  if data:
282
    for elem in data.split(","):
283
      if "=" in elem:
284
        key, val = elem.split("=", 1)
285
      else:
286
        if elem.startswith(NO_PREFIX):
287
          key, val = elem[len(NO_PREFIX):], False
288
        elif elem.startswith(UN_PREFIX):
289
          key, val = elem[len(UN_PREFIX):], None
290
        else:
291
          key, val = elem, True
292
      if key in kv_dict:
293
        raise errors.ParameterError("Duplicate key '%s' in option %s" %
294
                                    (key, opt))
295
      kv_dict[key] = val
296
  return kv_dict
297

    
298

    
299
def check_ident_key_val(option, opt, value):
300
  """Custom parser for ident:key=val,key=val options.
301

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

305
  """
306
  if ":" not in value:
307
    ident, rest = value, ''
308
  else:
309
    ident, rest = value.split(":", 1)
310

    
311
  if ident.startswith(NO_PREFIX):
312
    if rest:
313
      msg = "Cannot pass options when removing parameter groups: %s" % value
314
      raise errors.ParameterError(msg)
315
    retval = (ident[len(NO_PREFIX):], False)
316
  elif ident.startswith(UN_PREFIX):
317
    if rest:
318
      msg = "Cannot pass options when removing parameter groups: %s" % value
319
      raise errors.ParameterError(msg)
320
    retval = (ident[len(UN_PREFIX):], None)
321
  else:
322
    kv_dict = _SplitKeyVal(opt, rest)
323
    retval = (ident, kv_dict)
324
  return retval
325

    
326

    
327
def check_key_val(option, opt, value):
328
  """Custom parser class for key=val,key=val options.
329

330
  This will store the parsed values as a dict {key: val}.
331

332
  """
333
  return _SplitKeyVal(opt, value)
334

    
335

    
336
# completion_suggestion is normally a list. Using numeric values not evaluating
337
# to False for dynamic completion.
338
(OPT_COMPL_MANY_NODES,
339
 OPT_COMPL_ONE_NODE,
340
 OPT_COMPL_ONE_INSTANCE,
341
 OPT_COMPL_ONE_OS,
342
 OPT_COMPL_ONE_IALLOCATOR) = range(100, 105)
343

    
344
OPT_COMPL_ALL = frozenset([
345
  OPT_COMPL_MANY_NODES,
346
  OPT_COMPL_ONE_NODE,
347
  OPT_COMPL_ONE_INSTANCE,
348
  OPT_COMPL_ONE_OS,
349
  OPT_COMPL_ONE_IALLOCATOR,
350
  ])
351

    
352

    
353
class CliOption(Option):
354
  """Custom option class for optparse.
355

356
  """
357
  ATTRS = Option.ATTRS + [
358
    "completion_suggest",
359
    ]
360
  TYPES = Option.TYPES + (
361
    "identkeyval",
362
    "keyval",
363
    "unit",
364
    )
365
  TYPE_CHECKER = Option.TYPE_CHECKER.copy()
366
  TYPE_CHECKER["identkeyval"] = check_ident_key_val
367
  TYPE_CHECKER["keyval"] = check_key_val
368
  TYPE_CHECKER["unit"] = check_unit
369

    
370

    
371
# optparse.py sets make_option, so we do it for our own option class, too
372
cli_option = CliOption
373

    
374

    
375
DEBUG_OPT = cli_option("-d", "--debug", default=False,
376
                       action="store_true",
377
                       help="Turn debugging on")
378

    
379
NOHDR_OPT = cli_option("--no-headers", default=False,
380
                       action="store_true", dest="no_headers",
381
                       help="Don't display column headers")
382

    
383
SEP_OPT = cli_option("--separator", default=None,
384
                     action="store", dest="separator",
385
                     help=("Separator between output fields"
386
                           " (defaults to one space)"))
387

    
388
USEUNITS_OPT = cli_option("--units", default=None,
389
                          dest="units", choices=('h', 'm', 'g', 't'),
390
                          help="Specify units for output (one of hmgt)")
391

    
392
FIELDS_OPT = cli_option("-o", "--output", dest="output", action="store",
393
                        type="string", metavar="FIELDS",
394
                        help="Comma separated list of output fields")
395

    
396
FORCE_OPT = cli_option("-f", "--force", dest="force", action="store_true",
397
                       default=False, help="Force the operation")
398

    
399
CONFIRM_OPT = cli_option("--yes", dest="confirm", action="store_true",
400
                         default=False, help="Do not require confirmation")
401

    
402
TAG_SRC_OPT = cli_option("--from", dest="tags_source",
403
                         default=None, help="File with tag names")
404

    
405
SUBMIT_OPT = cli_option("--submit", dest="submit_only",
406
                        default=False, action="store_true",
407
                        help=("Submit the job and return the job ID, but"
408
                              " don't wait for the job to finish"))
409

    
410
SYNC_OPT = cli_option("--sync", dest="do_locking",
411
                      default=False, action="store_true",
412
                      help=("Grab locks while doing the queries"
413
                            " in order to ensure more consistent results"))
414

    
415
_DRY_RUN_OPT = cli_option("--dry-run", default=False,
416
                          action="store_true",
417
                          help=("Do not execute the operation, just run the"
418
                                " check steps and verify it it could be"
419
                                " executed"))
420

    
421

    
422
def _ParseArgs(argv, commands, aliases):
423
  """Parser for the command line arguments.
424

425
  This function parses the arguments and returns the function which
426
  must be executed together with its (modified) arguments.
427

428
  @param argv: the command line
429
  @param commands: dictionary with special contents, see the design
430
      doc for cmdline handling
431
  @param aliases: dictionary with command aliases {'alias': 'target, ...}
432

433
  """
434
  if len(argv) == 0:
435
    binary = "<command>"
436
  else:
437
    binary = argv[0].split("/")[-1]
438

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

    
445
  if len(argv) < 2 or not (argv[1] in commands or
446
                           argv[1] in aliases):
447
    # let's do a nice thing
448
    sortedcmds = commands.keys()
449
    sortedcmds.sort()
450

    
451
    ToStdout("Usage: %s {command} [options...] [argument...]", binary)
452
    ToStdout("%s <command> --help to see details, or man %s", binary, binary)
453
    ToStdout("")
454

    
455
    # compute the max line length for cmd + usage
456
    mlen = max([len(" %s" % cmd) for cmd in commands])
457
    mlen = min(60, mlen) # should not get here...
458

    
459
    # and format a nice command list
460
    ToStdout("Commands:")
461
    for cmd in sortedcmds:
462
      cmdstr = " %s" % (cmd,)
463
      help_text = commands[cmd][4]
464
      help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
465
      ToStdout("%-*s - %s", mlen, cmdstr, help_lines.pop(0))
466
      for line in help_lines:
467
        ToStdout("%-*s   %s", mlen, "", line)
468

    
469
    ToStdout("")
470

    
471
    return None, None, None
472

    
473
  # get command, unalias it, and look it up in commands
474
  cmd = argv.pop(1)
475
  if cmd in aliases:
476
    if cmd in commands:
477
      raise errors.ProgrammerError("Alias '%s' overrides an existing"
478
                                   " command" % cmd)
479

    
480
    if aliases[cmd] not in commands:
481
      raise errors.ProgrammerError("Alias '%s' maps to non-existing"
482
                                   " command '%s'" % (cmd, aliases[cmd]))
483

    
484
    cmd = aliases[cmd]
485

    
486
  func, args_def, parser_opts, usage, description = commands[cmd]
487
  parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT],
488
                        description=description,
489
                        formatter=TitledHelpFormatter(),
490
                        usage="%%prog %s %s" % (cmd, usage))
491
  parser.disable_interspersed_args()
492
  options, args = parser.parse_args()
493

    
494
  if not _CheckArguments(cmd, args_def, args):
495
    return None, None, None
496

    
497
  return func, options, args
498

    
499

    
500
def _CheckArguments(cmd, args_def, args):
501
  """Verifies the arguments using the argument definition.
502

503
  Algorithm:
504

505
    1. Abort with error if values specified by user but none expected.
506

507
    1. For each argument in definition
508

509
      1. Keep running count of minimum number of values (min_count)
510
      1. Keep running count of maximum number of values (max_count)
511
      1. If it has an unlimited number of values
512

513
        1. Abort with error if it's not the last argument in the definition
514

515
    1. If last argument has limited number of values
516

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

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

521
  """
522
  if args and not args_def:
523
    ToStderr("Error: Command %s expects no arguments", cmd)
524
    return False
525

    
526
  min_count = None
527
  max_count = None
528
  check_max = None
529

    
530
  last_idx = len(args_def) - 1
531

    
532
  for idx, arg in enumerate(args_def):
533
    if min_count is None:
534
      min_count = arg.min
535
    elif arg.min is not None:
536
      min_count += arg.min
537

    
538
    if max_count is None:
539
      max_count = arg.max
540
    elif arg.max is not None:
541
      max_count += arg.max
542

    
543
    if idx == last_idx:
544
      check_max = (arg.max is not None)
545

    
546
    elif arg.max is None:
547
      raise errors.ProgrammerError("Only the last argument can have max=None")
548

    
549
  if check_max:
550
    # Command with exact number of arguments
551
    if (min_count is not None and max_count is not None and
552
        min_count == max_count and len(args) != min_count):
553
      ToStderr("Error: Command %s expects %d argument(s)", cmd, min_count)
554
      return False
555

    
556
    # Command with limited number of arguments
557
    if max_count is not None and len(args) > max_count:
558
      ToStderr("Error: Command %s expects only %d argument(s)",
559
               cmd, max_count)
560
      return False
561

    
562
  # Command with some required arguments
563
  if min_count is not None and len(args) < min_count:
564
    ToStderr("Error: Command %s expects at least %d argument(s)",
565
             cmd, min_count)
566
    return False
567

    
568
  return True
569

    
570

    
571
def SplitNodeOption(value):
572
  """Splits the value of a --node option.
573

574
  """
575
  if value and ':' in value:
576
    return value.split(':', 1)
577
  else:
578
    return (value, None)
579

    
580

    
581
def UsesRPC(fn):
582
  def wrapper(*args, **kwargs):
583
    rpc.Init()
584
    try:
585
      return fn(*args, **kwargs)
586
    finally:
587
      rpc.Shutdown()
588
  return wrapper
589

    
590

    
591
def AskUser(text, choices=None):
592
  """Ask the user a question.
593

594
  @param text: the question to ask
595

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

601
  @return: one of the return values from the choices list; if input is
602
      not possible (i.e. not running with a tty, we return the last
603
      entry from the list
604

605
  """
606
  if choices is None:
607
    choices = [('y', True, 'Perform the operation'),
608
               ('n', False, 'Do not perform the operation')]
609
  if not choices or not isinstance(choices, list):
610
    raise errors.ProgrammerError("Invalid choices argument to AskUser")
611
  for entry in choices:
612
    if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
613
      raise errors.ProgrammerError("Invalid choices element to AskUser")
614

    
615
  answer = choices[-1][1]
616
  new_text = []
617
  for line in text.splitlines():
618
    new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
619
  text = "\n".join(new_text)
620
  try:
621
    f = file("/dev/tty", "a+")
622
  except IOError:
623
    return answer
624
  try:
625
    chars = [entry[0] for entry in choices]
626
    chars[-1] = "[%s]" % chars[-1]
627
    chars.append('?')
628
    maps = dict([(entry[0], entry[1]) for entry in choices])
629
    while True:
630
      f.write(text)
631
      f.write('\n')
632
      f.write("/".join(chars))
633
      f.write(": ")
634
      line = f.readline(2).strip().lower()
635
      if line in maps:
636
        answer = maps[line]
637
        break
638
      elif line == '?':
639
        for entry in choices:
640
          f.write(" %s - %s\n" % (entry[0], entry[2]))
641
        f.write("\n")
642
        continue
643
  finally:
644
    f.close()
645
  return answer
646

    
647

    
648
class JobSubmittedException(Exception):
649
  """Job was submitted, client should exit.
650

651
  This exception has one argument, the ID of the job that was
652
  submitted. The handler should print this ID.
653

654
  This is not an error, just a structured way to exit from clients.
655

656
  """
657

    
658

    
659
def SendJob(ops, cl=None):
660
  """Function to submit an opcode without waiting for the results.
661

662
  @type ops: list
663
  @param ops: list of opcodes
664
  @type cl: luxi.Client
665
  @param cl: the luxi client to use for communicating with the master;
666
             if None, a new client will be created
667

668
  """
669
  if cl is None:
670
    cl = GetClient()
671

    
672
  job_id = cl.SubmitJob(ops)
673

    
674
  return job_id
675

    
676

    
677
def PollJob(job_id, cl=None, feedback_fn=None):
678
  """Function to poll for the result of a job.
679

680
  @type job_id: job identified
681
  @param job_id: the job to poll for results
682
  @type cl: luxi.Client
683
  @param cl: the luxi client to use for communicating with the master;
684
             if None, a new client will be created
685

686
  """
687
  if cl is None:
688
    cl = GetClient()
689

    
690
  prev_job_info = None
691
  prev_logmsg_serial = None
692

    
693
  while True:
694
    result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
695
                                 prev_logmsg_serial)
696
    if not result:
697
      # job not found, go away!
698
      raise errors.JobLost("Job with id %s lost" % job_id)
699

    
700
    # Split result, a tuple of (field values, log entries)
701
    (job_info, log_entries) = result
702
    (status, ) = job_info
703

    
704
    if log_entries:
705
      for log_entry in log_entries:
706
        (serial, timestamp, _, message) = log_entry
707
        if callable(feedback_fn):
708
          feedback_fn(log_entry[1:])
709
        else:
710
          encoded = utils.SafeEncode(message)
711
          ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)), encoded)
712
        prev_logmsg_serial = max(prev_logmsg_serial, serial)
713

    
714
    # TODO: Handle canceled and archived jobs
715
    elif status in (constants.JOB_STATUS_SUCCESS,
716
                    constants.JOB_STATUS_ERROR,
717
                    constants.JOB_STATUS_CANCELING,
718
                    constants.JOB_STATUS_CANCELED):
719
      break
720

    
721
    prev_job_info = job_info
722

    
723
  jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
724
  if not jobs:
725
    raise errors.JobLost("Job with id %s lost" % job_id)
726

    
727
  status, opstatus, result = jobs[0]
728
  if status == constants.JOB_STATUS_SUCCESS:
729
    return result
730
  elif status in (constants.JOB_STATUS_CANCELING,
731
                  constants.JOB_STATUS_CANCELED):
732
    raise errors.OpExecError("Job was canceled")
733
  else:
734
    has_ok = False
735
    for idx, (status, msg) in enumerate(zip(opstatus, result)):
736
      if status == constants.OP_STATUS_SUCCESS:
737
        has_ok = True
738
      elif status == constants.OP_STATUS_ERROR:
739
        errors.MaybeRaise(msg)
740
        if has_ok:
741
          raise errors.OpExecError("partial failure (opcode %d): %s" %
742
                                   (idx, msg))
743
        else:
744
          raise errors.OpExecError(str(msg))
745
    # default failure mode
746
    raise errors.OpExecError(result)
747

    
748

    
749
def SubmitOpCode(op, cl=None, feedback_fn=None):
750
  """Legacy function to submit an opcode.
751

752
  This is just a simple wrapper over the construction of the processor
753
  instance. It should be extended to better handle feedback and
754
  interaction functions.
755

756
  """
757
  if cl is None:
758
    cl = GetClient()
759

    
760
  job_id = SendJob([op], cl)
761

    
762
  op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
763

    
764
  return op_results[0]
765

    
766

    
767
def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
768
  """Wrapper around SubmitOpCode or SendJob.
769

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

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

777
  """
778
  if opts and opts.dry_run:
779
    op.dry_run = opts.dry_run
780
  if opts and opts.submit_only:
781
    job_id = SendJob([op], cl=cl)
782
    raise JobSubmittedException(job_id)
783
  else:
784
    return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
785

    
786

    
787
def GetClient():
788
  # TODO: Cache object?
789
  try:
790
    client = luxi.Client()
791
  except luxi.NoMasterError:
792
    master, myself = ssconf.GetMasterAndMyself()
793
    if master != myself:
794
      raise errors.OpPrereqError("This is not the master node, please connect"
795
                                 " to node '%s' and rerun the command" %
796
                                 master)
797
    else:
798
      raise
799
  return client
800

    
801

    
802
def FormatError(err):
803
  """Return a formatted error message for a given error.
804

805
  This function takes an exception instance and returns a tuple
806
  consisting of two values: first, the recommended exit code, and
807
  second, a string describing the error message (not
808
  newline-terminated).
809

810
  """
811
  retcode = 1
812
  obuf = StringIO()
813
  msg = str(err)
814
  if isinstance(err, errors.ConfigurationError):
815
    txt = "Corrupt configuration file: %s" % msg
816
    logging.error(txt)
817
    obuf.write(txt + "\n")
818
    obuf.write("Aborting.")
819
    retcode = 2
820
  elif isinstance(err, errors.HooksAbort):
821
    obuf.write("Failure: hooks execution failed:\n")
822
    for node, script, out in err.args[0]:
823
      if out:
824
        obuf.write("  node: %s, script: %s, output: %s\n" %
825
                   (node, script, out))
826
      else:
827
        obuf.write("  node: %s, script: %s (no output)\n" %
828
                   (node, script))
829
  elif isinstance(err, errors.HooksFailure):
830
    obuf.write("Failure: hooks general failure: %s" % msg)
831
  elif isinstance(err, errors.ResolverError):
832
    this_host = utils.HostInfo.SysName()
833
    if err.args[0] == this_host:
834
      msg = "Failure: can't resolve my own hostname ('%s')"
835
    else:
836
      msg = "Failure: can't resolve hostname '%s'"
837
    obuf.write(msg % err.args[0])
838
  elif isinstance(err, errors.OpPrereqError):
839
    obuf.write("Failure: prerequisites not met for this"
840
               " operation:\n%s" % msg)
841
  elif isinstance(err, errors.OpExecError):
842
    obuf.write("Failure: command execution error:\n%s" % msg)
843
  elif isinstance(err, errors.TagError):
844
    obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
845
  elif isinstance(err, errors.JobQueueDrainError):
846
    obuf.write("Failure: the job queue is marked for drain and doesn't"
847
               " accept new requests\n")
848
  elif isinstance(err, errors.JobQueueFull):
849
    obuf.write("Failure: the job queue is full and doesn't accept new"
850
               " job submissions until old jobs are archived\n")
851
  elif isinstance(err, errors.TypeEnforcementError):
852
    obuf.write("Parameter Error: %s" % msg)
853
  elif isinstance(err, errors.ParameterError):
854
    obuf.write("Failure: unknown/wrong parameter name '%s'" % msg)
855
  elif isinstance(err, errors.GenericError):
856
    obuf.write("Unhandled Ganeti error: %s" % msg)
857
  elif isinstance(err, luxi.NoMasterError):
858
    obuf.write("Cannot communicate with the master daemon.\nIs it running"
859
               " and listening for connections?")
860
  elif isinstance(err, luxi.TimeoutError):
861
    obuf.write("Timeout while talking to the master daemon. Error:\n"
862
               "%s" % msg)
863
  elif isinstance(err, luxi.ProtocolError):
864
    obuf.write("Unhandled protocol error while talking to the master daemon:\n"
865
               "%s" % msg)
866
  elif isinstance(err, JobSubmittedException):
867
    obuf.write("JobID: %s\n" % err.args[0])
868
    retcode = 0
869
  else:
870
    obuf.write("Unhandled exception: %s" % msg)
871
  return retcode, obuf.getvalue().rstrip('\n')
872

    
873

    
874
def GenericMain(commands, override=None, aliases=None):
875
  """Generic main function for all the gnt-* commands.
876

877
  Arguments:
878
    - commands: a dictionary with a special structure, see the design doc
879
                for command line handling.
880
    - override: if not None, we expect a dictionary with keys that will
881
                override command line options; this can be used to pass
882
                options from the scripts to generic functions
883
    - aliases: dictionary with command aliases {'alias': 'target, ...}
884

885
  """
886
  # save the program name and the entire command line for later logging
887
  if sys.argv:
888
    binary = os.path.basename(sys.argv[0]) or sys.argv[0]
889
    if len(sys.argv) >= 2:
890
      binary += " " + sys.argv[1]
891
      old_cmdline = " ".join(sys.argv[2:])
892
    else:
893
      old_cmdline = ""
894
  else:
895
    binary = "<unknown program>"
896
    old_cmdline = ""
897

    
898
  if aliases is None:
899
    aliases = {}
900

    
901
  try:
902
    func, options, args = _ParseArgs(sys.argv, commands, aliases)
903
  except errors.ParameterError, err:
904
    result, err_msg = FormatError(err)
905
    ToStderr(err_msg)
906
    return 1
907

    
908
  if func is None: # parse error
909
    return 1
910

    
911
  if override is not None:
912
    for key, val in override.iteritems():
913
      setattr(options, key, val)
914

    
915
  utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
916
                     stderr_logging=True, program=binary)
917

    
918
  if old_cmdline:
919
    logging.info("run with arguments '%s'", old_cmdline)
920
  else:
921
    logging.info("run with no arguments")
922

    
923
  try:
924
    result = func(options, args)
925
  except (errors.GenericError, luxi.ProtocolError,
926
          JobSubmittedException), err:
927
    result, err_msg = FormatError(err)
928
    logging.exception("Error during command processing")
929
    ToStderr(err_msg)
930

    
931
  return result
932

    
933

    
934
def GenerateTable(headers, fields, separator, data,
935
                  numfields=None, unitfields=None,
936
                  units=None):
937
  """Prints a table with headers and different fields.
938

939
  @type headers: dict
940
  @param headers: dictionary mapping field names to headers for
941
      the table
942
  @type fields: list
943
  @param fields: the field names corresponding to each row in
944
      the data field
945
  @param separator: the separator to be used; if this is None,
946
      the default 'smart' algorithm is used which computes optimal
947
      field width, otherwise just the separator is used between
948
      each field
949
  @type data: list
950
  @param data: a list of lists, each sublist being one row to be output
951
  @type numfields: list
952
  @param numfields: a list with the fields that hold numeric
953
      values and thus should be right-aligned
954
  @type unitfields: list
955
  @param unitfields: a list with the fields that hold numeric
956
      values that should be formatted with the units field
957
  @type units: string or None
958
  @param units: the units we should use for formatting, or None for
959
      automatic choice (human-readable for non-separator usage, otherwise
960
      megabytes); this is a one-letter string
961

962
  """
963
  if units is None:
964
    if separator:
965
      units = "m"
966
    else:
967
      units = "h"
968

    
969
  if numfields is None:
970
    numfields = []
971
  if unitfields is None:
972
    unitfields = []
973

    
974
  numfields = utils.FieldSet(*numfields)
975
  unitfields = utils.FieldSet(*unitfields)
976

    
977
  format_fields = []
978
  for field in fields:
979
    if headers and field not in headers:
980
      # TODO: handle better unknown fields (either revert to old
981
      # style of raising exception, or deal more intelligently with
982
      # variable fields)
983
      headers[field] = field
984
    if separator is not None:
985
      format_fields.append("%s")
986
    elif numfields.Matches(field):
987
      format_fields.append("%*s")
988
    else:
989
      format_fields.append("%-*s")
990

    
991
  if separator is None:
992
    mlens = [0 for name in fields]
993
    format = ' '.join(format_fields)
994
  else:
995
    format = separator.replace("%", "%%").join(format_fields)
996

    
997
  for row in data:
998
    if row is None:
999
      continue
1000
    for idx, val in enumerate(row):
1001
      if unitfields.Matches(fields[idx]):
1002
        try:
1003
          val = int(val)
1004
        except ValueError:
1005
          pass
1006
        else:
1007
          val = row[idx] = utils.FormatUnit(val, units)
1008
      val = row[idx] = str(val)
1009
      if separator is None:
1010
        mlens[idx] = max(mlens[idx], len(val))
1011

    
1012
  result = []
1013
  if headers:
1014
    args = []
1015
    for idx, name in enumerate(fields):
1016
      hdr = headers[name]
1017
      if separator is None:
1018
        mlens[idx] = max(mlens[idx], len(hdr))
1019
        args.append(mlens[idx])
1020
      args.append(hdr)
1021
    result.append(format % tuple(args))
1022

    
1023
  for line in data:
1024
    args = []
1025
    if line is None:
1026
      line = ['-' for _ in fields]
1027
    for idx in xrange(len(fields)):
1028
      if separator is None:
1029
        args.append(mlens[idx])
1030
      args.append(line[idx])
1031
    result.append(format % tuple(args))
1032

    
1033
  return result
1034

    
1035

    
1036
def FormatTimestamp(ts):
1037
  """Formats a given timestamp.
1038

1039
  @type ts: timestamp
1040
  @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
1041

1042
  @rtype: string
1043
  @return: a string with the formatted timestamp
1044

1045
  """
1046
  if not isinstance (ts, (tuple, list)) or len(ts) != 2:
1047
    return '?'
1048
  sec, usec = ts
1049
  return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
1050

    
1051

    
1052
def ParseTimespec(value):
1053
  """Parse a time specification.
1054

1055
  The following suffixed will be recognized:
1056

1057
    - s: seconds
1058
    - m: minutes
1059
    - h: hours
1060
    - d: day
1061
    - w: weeks
1062

1063
  Without any suffix, the value will be taken to be in seconds.
1064

1065
  """
1066
  value = str(value)
1067
  if not value:
1068
    raise errors.OpPrereqError("Empty time specification passed")
1069
  suffix_map = {
1070
    's': 1,
1071
    'm': 60,
1072
    'h': 3600,
1073
    'd': 86400,
1074
    'w': 604800,
1075
    }
1076
  if value[-1] not in suffix_map:
1077
    try:
1078
      value = int(value)
1079
    except ValueError:
1080
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1081
  else:
1082
    multiplier = suffix_map[value[-1]]
1083
    value = value[:-1]
1084
    if not value: # no data left after stripping the suffix
1085
      raise errors.OpPrereqError("Invalid time specification (only"
1086
                                 " suffix passed)")
1087
    try:
1088
      value = int(value) * multiplier
1089
    except ValueError:
1090
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1091
  return value
1092

    
1093

    
1094
def GetOnlineNodes(nodes, cl=None, nowarn=False):
1095
  """Returns the names of online nodes.
1096

1097
  This function will also log a warning on stderr with the names of
1098
  the online nodes.
1099

1100
  @param nodes: if not empty, use only this subset of nodes (minus the
1101
      offline ones)
1102
  @param cl: if not None, luxi client to use
1103
  @type nowarn: boolean
1104
  @param nowarn: by default, this function will output a note with the
1105
      offline nodes that are skipped; if this parameter is True the
1106
      note is not displayed
1107

1108
  """
1109
  if cl is None:
1110
    cl = GetClient()
1111

    
1112
  result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
1113
                         use_locking=False)
1114
  offline = [row[0] for row in result if row[1]]
1115
  if offline and not nowarn:
1116
    ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
1117
  return [row[0] for row in result if not row[1]]
1118

    
1119

    
1120
def _ToStream(stream, txt, *args):
1121
  """Write a message to a stream, bypassing the logging system
1122

1123
  @type stream: file object
1124
  @param stream: the file to which we should write
1125
  @type txt: str
1126
  @param txt: the message
1127

1128
  """
1129
  if args:
1130
    args = tuple(args)
1131
    stream.write(txt % args)
1132
  else:
1133
    stream.write(txt)
1134
  stream.write('\n')
1135
  stream.flush()
1136

    
1137

    
1138
def ToStdout(txt, *args):
1139
  """Write a message to stdout only, bypassing the logging system
1140

1141
  This is just a wrapper over _ToStream.
1142

1143
  @type txt: str
1144
  @param txt: the message
1145

1146
  """
1147
  _ToStream(sys.stdout, txt, *args)
1148

    
1149

    
1150
def ToStderr(txt, *args):
1151
  """Write a message to stderr only, bypassing the logging system
1152

1153
  This is just a wrapper over _ToStream.
1154

1155
  @type txt: str
1156
  @param txt: the message
1157

1158
  """
1159
  _ToStream(sys.stderr, txt, *args)
1160

    
1161

    
1162
class JobExecutor(object):
1163
  """Class which manages the submission and execution of multiple jobs.
1164

1165
  Note that instances of this class should not be reused between
1166
  GetResults() calls.
1167

1168
  """
1169
  def __init__(self, cl=None, verbose=True):
1170
    self.queue = []
1171
    if cl is None:
1172
      cl = GetClient()
1173
    self.cl = cl
1174
    self.verbose = verbose
1175
    self.jobs = []
1176

    
1177
  def QueueJob(self, name, *ops):
1178
    """Record a job for later submit.
1179

1180
    @type name: string
1181
    @param name: a description of the job, will be used in WaitJobSet
1182
    """
1183
    self.queue.append((name, ops))
1184

    
1185
  def SubmitPending(self):
1186
    """Submit all pending jobs.
1187

1188
    """
1189
    results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1190
    for ((status, data), (name, _)) in zip(results, self.queue):
1191
      self.jobs.append((status, data, name))
1192

    
1193
  def GetResults(self):
1194
    """Wait for and return the results of all jobs.
1195

1196
    @rtype: list
1197
    @return: list of tuples (success, job results), in the same order
1198
        as the submitted jobs; if a job has failed, instead of the result
1199
        there will be the error message
1200

1201
    """
1202
    if not self.jobs:
1203
      self.SubmitPending()
1204
    results = []
1205
    if self.verbose:
1206
      ok_jobs = [row[1] for row in self.jobs if row[0]]
1207
      if ok_jobs:
1208
        ToStdout("Submitted jobs %s", ", ".join(ok_jobs))
1209
    for submit_status, jid, name in self.jobs:
1210
      if not submit_status:
1211
        ToStderr("Failed to submit job for %s: %s", name, jid)
1212
        results.append((False, jid))
1213
        continue
1214
      if self.verbose:
1215
        ToStdout("Waiting for job %s for %s...", jid, name)
1216
      try:
1217
        job_result = PollJob(jid, cl=self.cl)
1218
        success = True
1219
      except (errors.GenericError, luxi.ProtocolError), err:
1220
        _, job_result = FormatError(err)
1221
        success = False
1222
        # the error message will always be shown, verbose or not
1223
        ToStderr("Job %s for %s has failed: %s", jid, name, job_result)
1224

    
1225
      results.append((success, job_result))
1226
    return results
1227

    
1228
  def WaitOrShow(self, wait):
1229
    """Wait for job results or only print the job IDs.
1230

1231
    @type wait: boolean
1232
    @param wait: whether to wait or not
1233

1234
    """
1235
    if wait:
1236
      return self.GetResults()
1237
    else:
1238
      if not self.jobs:
1239
        self.SubmitPending()
1240
      for status, result, name in self.jobs:
1241
        if status:
1242
          ToStdout("%s: %s", result, name)
1243
        else:
1244
          ToStderr("Failure for %s: %s", name, result)