Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ e948770c

History | View | Annotate | Download (35.8 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
           "OPT_COMPL_INST_ADD_NODES",
63
           ]
64

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

    
68

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

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

    
78

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

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

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

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

    
93

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

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

100
  """
101

    
102

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

106
  """
107

    
108

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

112
  """
113

    
114

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

118
  """
119

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

123
  """
124

    
125

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

129
  """
130

    
131

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

135
  """
136

    
137

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

141
  """
142

    
143

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

    
150

    
151

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

155
  Note that this function will modify its args parameter.
156

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

    
172

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

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

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

    
201

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

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

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

    
219

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

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

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

    
236

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

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

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

    
253

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

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

    
263

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

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

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

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

    
300

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

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

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

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

    
328

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

332
  This will store the parsed values as a dict {key: val}.
333

334
  """
335
  return _SplitKeyVal(opt, value)
336

    
337

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

    
347
OPT_COMPL_ALL = frozenset([
348
  OPT_COMPL_MANY_NODES,
349
  OPT_COMPL_ONE_NODE,
350
  OPT_COMPL_ONE_INSTANCE,
351
  OPT_COMPL_ONE_OS,
352
  OPT_COMPL_ONE_IALLOCATOR,
353
  OPT_COMPL_INST_ADD_NODES,
354
  ])
355

    
356

    
357
class CliOption(Option):
358
  """Custom option class for optparse.
359

360
  """
361
  ATTRS = Option.ATTRS + [
362
    "completion_suggest",
363
    ]
364
  TYPES = Option.TYPES + (
365
    "identkeyval",
366
    "keyval",
367
    "unit",
368
    )
369
  TYPE_CHECKER = Option.TYPE_CHECKER.copy()
370
  TYPE_CHECKER["identkeyval"] = check_ident_key_val
371
  TYPE_CHECKER["keyval"] = check_key_val
372
  TYPE_CHECKER["unit"] = check_unit
373

    
374

    
375
# optparse.py sets make_option, so we do it for our own option class, too
376
cli_option = CliOption
377

    
378

    
379
DEBUG_OPT = cli_option("-d", "--debug", default=False,
380
                       action="store_true",
381
                       help="Turn debugging on")
382

    
383
NOHDR_OPT = cli_option("--no-headers", default=False,
384
                       action="store_true", dest="no_headers",
385
                       help="Don't display column headers")
386

    
387
SEP_OPT = cli_option("--separator", default=None,
388
                     action="store", dest="separator",
389
                     help=("Separator between output fields"
390
                           " (defaults to one space)"))
391

    
392
USEUNITS_OPT = cli_option("--units", default=None,
393
                          dest="units", choices=('h', 'm', 'g', 't'),
394
                          help="Specify units for output (one of hmgt)")
395

    
396
FIELDS_OPT = cli_option("-o", "--output", dest="output", action="store",
397
                        type="string", metavar="FIELDS",
398
                        help="Comma separated list of output fields")
399

    
400
FORCE_OPT = cli_option("-f", "--force", dest="force", action="store_true",
401
                       default=False, help="Force the operation")
402

    
403
CONFIRM_OPT = cli_option("--yes", dest="confirm", action="store_true",
404
                         default=False, help="Do not require confirmation")
405

    
406
TAG_SRC_OPT = cli_option("--from", dest="tags_source",
407
                         default=None, help="File with tag names")
408

    
409
SUBMIT_OPT = cli_option("--submit", dest="submit_only",
410
                        default=False, action="store_true",
411
                        help=("Submit the job and return the job ID, but"
412
                              " don't wait for the job to finish"))
413

    
414
SYNC_OPT = cli_option("--sync", dest="do_locking",
415
                      default=False, action="store_true",
416
                      help=("Grab locks while doing the queries"
417
                            " in order to ensure more consistent results"))
418

    
419
_DRY_RUN_OPT = cli_option("--dry-run", default=False,
420
                          action="store_true",
421
                          help=("Do not execute the operation, just run the"
422
                                " check steps and verify it it could be"
423
                                " executed"))
424

    
425

    
426
def _ParseArgs(argv, commands, aliases):
427
  """Parser for the command line arguments.
428

429
  This function parses the arguments and returns the function which
430
  must be executed together with its (modified) arguments.
431

432
  @param argv: the command line
433
  @param commands: dictionary with special contents, see the design
434
      doc for cmdline handling
435
  @param aliases: dictionary with command aliases {'alias': 'target, ...}
436

437
  """
438
  if len(argv) == 0:
439
    binary = "<command>"
440
  else:
441
    binary = argv[0].split("/")[-1]
442

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

    
449
  if len(argv) < 2 or not (argv[1] in commands or
450
                           argv[1] in aliases):
451
    # let's do a nice thing
452
    sortedcmds = commands.keys()
453
    sortedcmds.sort()
454

    
455
    ToStdout("Usage: %s {command} [options...] [argument...]", binary)
456
    ToStdout("%s <command> --help to see details, or man %s", binary, binary)
457
    ToStdout("")
458

    
459
    # compute the max line length for cmd + usage
460
    mlen = max([len(" %s" % cmd) for cmd in commands])
461
    mlen = min(60, mlen) # should not get here...
462

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

    
473
    ToStdout("")
474

    
475
    return None, None, None
476

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

    
484
    if aliases[cmd] not in commands:
485
      raise errors.ProgrammerError("Alias '%s' maps to non-existing"
486
                                   " command '%s'" % (cmd, aliases[cmd]))
487

    
488
    cmd = aliases[cmd]
489

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

    
498
  if not _CheckArguments(cmd, args_def, args):
499
    return None, None, None
500

    
501
  return func, options, args
502

    
503

    
504
def _CheckArguments(cmd, args_def, args):
505
  """Verifies the arguments using the argument definition.
506

507
  Algorithm:
508

509
    1. Abort with error if values specified by user but none expected.
510

511
    1. For each argument in definition
512

513
      1. Keep running count of minimum number of values (min_count)
514
      1. Keep running count of maximum number of values (max_count)
515
      1. If it has an unlimited number of values
516

517
        1. Abort with error if it's not the last argument in the definition
518

519
    1. If last argument has limited number of values
520

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

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

525
  """
526
  if args and not args_def:
527
    ToStderr("Error: Command %s expects no arguments", cmd)
528
    return False
529

    
530
  min_count = None
531
  max_count = None
532
  check_max = None
533

    
534
  last_idx = len(args_def) - 1
535

    
536
  for idx, arg in enumerate(args_def):
537
    if min_count is None:
538
      min_count = arg.min
539
    elif arg.min is not None:
540
      min_count += arg.min
541

    
542
    if max_count is None:
543
      max_count = arg.max
544
    elif arg.max is not None:
545
      max_count += arg.max
546

    
547
    if idx == last_idx:
548
      check_max = (arg.max is not None)
549

    
550
    elif arg.max is None:
551
      raise errors.ProgrammerError("Only the last argument can have max=None")
552

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

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

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

    
572
  return True
573

    
574

    
575
def SplitNodeOption(value):
576
  """Splits the value of a --node option.
577

578
  """
579
  if value and ':' in value:
580
    return value.split(':', 1)
581
  else:
582
    return (value, None)
583

    
584

    
585
def UsesRPC(fn):
586
  def wrapper(*args, **kwargs):
587
    rpc.Init()
588
    try:
589
      return fn(*args, **kwargs)
590
    finally:
591
      rpc.Shutdown()
592
  return wrapper
593

    
594

    
595
def AskUser(text, choices=None):
596
  """Ask the user a question.
597

598
  @param text: the question to ask
599

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

605
  @return: one of the return values from the choices list; if input is
606
      not possible (i.e. not running with a tty, we return the last
607
      entry from the list
608

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

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

    
651

    
652
class JobSubmittedException(Exception):
653
  """Job was submitted, client should exit.
654

655
  This exception has one argument, the ID of the job that was
656
  submitted. The handler should print this ID.
657

658
  This is not an error, just a structured way to exit from clients.
659

660
  """
661

    
662

    
663
def SendJob(ops, cl=None):
664
  """Function to submit an opcode without waiting for the results.
665

666
  @type ops: list
667
  @param ops: list of opcodes
668
  @type cl: luxi.Client
669
  @param cl: the luxi client to use for communicating with the master;
670
             if None, a new client will be created
671

672
  """
673
  if cl is None:
674
    cl = GetClient()
675

    
676
  job_id = cl.SubmitJob(ops)
677

    
678
  return job_id
679

    
680

    
681
def PollJob(job_id, cl=None, feedback_fn=None):
682
  """Function to poll for the result of a job.
683

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

690
  """
691
  if cl is None:
692
    cl = GetClient()
693

    
694
  prev_job_info = None
695
  prev_logmsg_serial = None
696

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

    
704
    # Split result, a tuple of (field values, log entries)
705
    (job_info, log_entries) = result
706
    (status, ) = job_info
707

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

    
718
    # TODO: Handle canceled and archived jobs
719
    elif status in (constants.JOB_STATUS_SUCCESS,
720
                    constants.JOB_STATUS_ERROR,
721
                    constants.JOB_STATUS_CANCELING,
722
                    constants.JOB_STATUS_CANCELED):
723
      break
724

    
725
    prev_job_info = job_info
726

    
727
  jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
728
  if not jobs:
729
    raise errors.JobLost("Job with id %s lost" % job_id)
730

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

    
752

    
753
def SubmitOpCode(op, cl=None, feedback_fn=None):
754
  """Legacy function to submit an opcode.
755

756
  This is just a simple wrapper over the construction of the processor
757
  instance. It should be extended to better handle feedback and
758
  interaction functions.
759

760
  """
761
  if cl is None:
762
    cl = GetClient()
763

    
764
  job_id = SendJob([op], cl)
765

    
766
  op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
767

    
768
  return op_results[0]
769

    
770

    
771
def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
772
  """Wrapper around SubmitOpCode or SendJob.
773

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

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

781
  """
782
  if opts and opts.dry_run:
783
    op.dry_run = opts.dry_run
784
  if opts and opts.submit_only:
785
    job_id = SendJob([op], cl=cl)
786
    raise JobSubmittedException(job_id)
787
  else:
788
    return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
789

    
790

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

    
805

    
806
def FormatError(err):
807
  """Return a formatted error message for a given error.
808

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

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

    
877

    
878
def GenericMain(commands, override=None, aliases=None):
879
  """Generic main function for all the gnt-* commands.
880

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

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

    
902
  if aliases is None:
903
    aliases = {}
904

    
905
  try:
906
    func, options, args = _ParseArgs(sys.argv, commands, aliases)
907
  except errors.ParameterError, err:
908
    result, err_msg = FormatError(err)
909
    ToStderr(err_msg)
910
    return 1
911

    
912
  if func is None: # parse error
913
    return 1
914

    
915
  if override is not None:
916
    for key, val in override.iteritems():
917
      setattr(options, key, val)
918

    
919
  utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
920
                     stderr_logging=True, program=binary)
921

    
922
  if old_cmdline:
923
    logging.info("run with arguments '%s'", old_cmdline)
924
  else:
925
    logging.info("run with no arguments")
926

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

    
935
  return result
936

    
937

    
938
def GenerateTable(headers, fields, separator, data,
939
                  numfields=None, unitfields=None,
940
                  units=None):
941
  """Prints a table with headers and different fields.
942

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

966
  """
967
  if units is None:
968
    if separator:
969
      units = "m"
970
    else:
971
      units = "h"
972

    
973
  if numfields is None:
974
    numfields = []
975
  if unitfields is None:
976
    unitfields = []
977

    
978
  numfields = utils.FieldSet(*numfields)
979
  unitfields = utils.FieldSet(*unitfields)
980

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

    
995
  if separator is None:
996
    mlens = [0 for name in fields]
997
    format = ' '.join(format_fields)
998
  else:
999
    format = separator.replace("%", "%%").join(format_fields)
1000

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

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

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

    
1037
  return result
1038

    
1039

    
1040
def FormatTimestamp(ts):
1041
  """Formats a given timestamp.
1042

1043
  @type ts: timestamp
1044
  @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
1045

1046
  @rtype: string
1047
  @return: a string with the formatted timestamp
1048

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

    
1055

    
1056
def ParseTimespec(value):
1057
  """Parse a time specification.
1058

1059
  The following suffixed will be recognized:
1060

1061
    - s: seconds
1062
    - m: minutes
1063
    - h: hours
1064
    - d: day
1065
    - w: weeks
1066

1067
  Without any suffix, the value will be taken to be in seconds.
1068

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

    
1097

    
1098
def GetOnlineNodes(nodes, cl=None, nowarn=False):
1099
  """Returns the names of online nodes.
1100

1101
  This function will also log a warning on stderr with the names of
1102
  the online nodes.
1103

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

1112
  """
1113
  if cl is None:
1114
    cl = GetClient()
1115

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

    
1123

    
1124
def _ToStream(stream, txt, *args):
1125
  """Write a message to a stream, bypassing the logging system
1126

1127
  @type stream: file object
1128
  @param stream: the file to which we should write
1129
  @type txt: str
1130
  @param txt: the message
1131

1132
  """
1133
  if args:
1134
    args = tuple(args)
1135
    stream.write(txt % args)
1136
  else:
1137
    stream.write(txt)
1138
  stream.write('\n')
1139
  stream.flush()
1140

    
1141

    
1142
def ToStdout(txt, *args):
1143
  """Write a message to stdout only, bypassing the logging system
1144

1145
  This is just a wrapper over _ToStream.
1146

1147
  @type txt: str
1148
  @param txt: the message
1149

1150
  """
1151
  _ToStream(sys.stdout, txt, *args)
1152

    
1153

    
1154
def ToStderr(txt, *args):
1155
  """Write a message to stderr only, bypassing the logging system
1156

1157
  This is just a wrapper over _ToStream.
1158

1159
  @type txt: str
1160
  @param txt: the message
1161

1162
  """
1163
  _ToStream(sys.stderr, txt, *args)
1164

    
1165

    
1166
class JobExecutor(object):
1167
  """Class which manages the submission and execution of multiple jobs.
1168

1169
  Note that instances of this class should not be reused between
1170
  GetResults() calls.
1171

1172
  """
1173
  def __init__(self, cl=None, verbose=True):
1174
    self.queue = []
1175
    if cl is None:
1176
      cl = GetClient()
1177
    self.cl = cl
1178
    self.verbose = verbose
1179
    self.jobs = []
1180

    
1181
  def QueueJob(self, name, *ops):
1182
    """Record a job for later submit.
1183

1184
    @type name: string
1185
    @param name: a description of the job, will be used in WaitJobSet
1186
    """
1187
    self.queue.append((name, ops))
1188

    
1189
  def SubmitPending(self):
1190
    """Submit all pending jobs.
1191

1192
    """
1193
    results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1194
    for ((status, data), (name, _)) in zip(results, self.queue):
1195
      self.jobs.append((status, data, name))
1196

    
1197
  def GetResults(self):
1198
    """Wait for and return the results of all jobs.
1199

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

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

    
1229
      results.append((success, job_result))
1230
    return results
1231

    
1232
  def WaitOrShow(self, wait):
1233
    """Wait for job results or only print the job IDs.
1234

1235
    @type wait: boolean
1236
    @param wait: whether to wait or not
1237

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