Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ 4abc4f1e

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

    
102
NO_PREFIX = "no_"
103
UN_PREFIX = "-"
104

    
105

    
106
class _Argument:
107
  def __init__(self, min=0, max=None):
108
    self.min = min
109
    self.max = max
110

    
111
  def __repr__(self):
112
    return ("<%s min=%s max=%s>" %
113
            (self.__class__.__name__, self.min, self.max))
114

    
115

    
116
class ArgSuggest(_Argument):
117
  """Suggesting argument.
118

119
  Value can be any of the ones passed to the constructor.
120

121
  """
122
  def __init__(self, min=0, max=None, choices=None):
123
    _Argument.__init__(self, min=min, max=max)
124
    self.choices = choices
125

    
126
  def __repr__(self):
127
    return ("<%s min=%s max=%s choices=%r>" %
128
            (self.__class__.__name__, self.min, self.max, self.choices))
129

    
130

    
131
class ArgChoice(ArgSuggest):
132
  """Choice argument.
133

134
  Value can be any of the ones passed to the constructor. Like L{ArgSuggest},
135
  but value must be one of the choices.
136

137
  """
138

    
139

    
140
class ArgUnknown(_Argument):
141
  """Unknown argument to program (e.g. determined at runtime).
142

143
  """
144

    
145

    
146
class ArgInstance(_Argument):
147
  """Instances argument.
148

149
  """
150

    
151

    
152
class ArgNode(_Argument):
153
  """Node argument.
154

155
  """
156

    
157
class ArgJobId(_Argument):
158
  """Job ID argument.
159

160
  """
161

    
162

    
163
class ArgFile(_Argument):
164
  """File path argument.
165

166
  """
167

    
168

    
169
class ArgCommand(_Argument):
170
  """Command argument.
171

172
  """
173

    
174

    
175
class ArgHost(_Argument):
176
  """Host argument.
177

178
  """
179

    
180

    
181
ARGS_NONE = []
182
ARGS_MANY_INSTANCES = [ArgInstance()]
183
ARGS_MANY_NODES = [ArgNode()]
184
ARGS_ONE_INSTANCE = [ArgInstance(min=1, max=1)]
185
ARGS_ONE_NODE = [ArgNode(min=1, max=1)]
186

    
187

    
188

    
189
def _ExtractTagsObject(opts, args):
190
  """Extract the tag type object.
191

192
  Note that this function will modify its args parameter.
193

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

    
209

    
210
def _ExtendTags(opts, args):
211
  """Extend the args if a source file has been given.
212

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

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

    
238

    
239
def ListTags(opts, args):
240
  """List the tags on a given object.
241

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

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

    
256

    
257
def AddTags(opts, args):
258
  """Add tags on a given object.
259

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

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

    
273

    
274
def RemoveTags(opts, args):
275
  """Remove tags from a given object.
276

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

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

    
290

    
291
def check_unit(option, opt, value):
292
  """OptParsers custom converter for units.
293

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

    
300

    
301
def _SplitKeyVal(opt, data):
302
  """Convert a KeyVal string into a dict.
303

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

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

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

    
337

    
338
def check_ident_key_val(option, opt, value):
339
  """Custom parser for ident:key=val,key=val options.
340

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

344
  """
345
  if ":" not in value:
346
    ident, rest = value, ''
347
  else:
348
    ident, rest = value.split(":", 1)
349

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

    
365

    
366
def check_key_val(option, opt, value):
367
  """Custom parser class for key=val,key=val options.
368

369
  This will store the parsed values as a dict {key: val}.
370

371
  """
372
  return _SplitKeyVal(opt, value)
373

    
374

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

    
384
OPT_COMPL_ALL = frozenset([
385
  OPT_COMPL_MANY_NODES,
386
  OPT_COMPL_ONE_NODE,
387
  OPT_COMPL_ONE_INSTANCE,
388
  OPT_COMPL_ONE_OS,
389
  OPT_COMPL_ONE_IALLOCATOR,
390
  OPT_COMPL_INST_ADD_NODES,
391
  ])
392

    
393

    
394
class CliOption(Option):
395
  """Custom option class for optparse.
396

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

    
411

    
412
# optparse.py sets make_option, so we do it for our own option class, too
413
cli_option = CliOption
414

    
415

    
416
DEBUG_OPT = cli_option("-d", "--debug", default=False,
417
                       action="store_true",
418
                       help="Turn debugging on")
419

    
420
NOHDR_OPT = cli_option("--no-headers", default=False,
421
                       action="store_true", dest="no_headers",
422
                       help="Don't display column headers")
423

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

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

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

    
437
FORCE_OPT = cli_option("-f", "--force", dest="force", action="store_true",
438
                       default=False, help="Force the operation")
439

    
440
CONFIRM_OPT = cli_option("--yes", dest="confirm", action="store_true",
441
                         default=False, help="Do not require confirmation")
442

    
443
TAG_SRC_OPT = cli_option("--from", dest="tags_source",
444
                         default=None, help="File with tag names")
445

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

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

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

    
462

    
463
def _ParseArgs(argv, commands, aliases):
464
  """Parser for the command line arguments.
465

466
  This function parses the arguments and returns the function which
467
  must be executed together with its (modified) arguments.
468

469
  @param argv: the command line
470
  @param commands: dictionary with special contents, see the design
471
      doc for cmdline handling
472
  @param aliases: dictionary with command aliases {'alias': 'target, ...}
473

474
  """
475
  if len(argv) == 0:
476
    binary = "<command>"
477
  else:
478
    binary = argv[0].split("/")[-1]
479

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

    
486
  if len(argv) < 2 or not (argv[1] in commands or
487
                           argv[1] in aliases):
488
    # let's do a nice thing
489
    sortedcmds = commands.keys()
490
    sortedcmds.sort()
491

    
492
    ToStdout("Usage: %s {command} [options...] [argument...]", binary)
493
    ToStdout("%s <command> --help to see details, or man %s", binary, binary)
494
    ToStdout("")
495

    
496
    # compute the max line length for cmd + usage
497
    mlen = max([len(" %s" % cmd) for cmd in commands])
498
    mlen = min(60, mlen) # should not get here...
499

    
500
    # and format a nice command list
501
    ToStdout("Commands:")
502
    for cmd in sortedcmds:
503
      cmdstr = " %s" % (cmd,)
504
      help_text = commands[cmd][4]
505
      help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
506
      ToStdout("%-*s - %s", mlen, cmdstr, help_lines.pop(0))
507
      for line in help_lines:
508
        ToStdout("%-*s   %s", mlen, "", line)
509

    
510
    ToStdout("")
511

    
512
    return None, None, None
513

    
514
  # get command, unalias it, and look it up in commands
515
  cmd = argv.pop(1)
516
  if cmd in aliases:
517
    if cmd in commands:
518
      raise errors.ProgrammerError("Alias '%s' overrides an existing"
519
                                   " command" % cmd)
520

    
521
    if aliases[cmd] not in commands:
522
      raise errors.ProgrammerError("Alias '%s' maps to non-existing"
523
                                   " command '%s'" % (cmd, aliases[cmd]))
524

    
525
    cmd = aliases[cmd]
526

    
527
  func, args_def, parser_opts, usage, description = commands[cmd]
528
  parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT],
529
                        description=description,
530
                        formatter=TitledHelpFormatter(),
531
                        usage="%%prog %s %s" % (cmd, usage))
532
  parser.disable_interspersed_args()
533
  options, args = parser.parse_args()
534

    
535
  if not _CheckArguments(cmd, args_def, args):
536
    return None, None, None
537

    
538
  return func, options, args
539

    
540

    
541
def _CheckArguments(cmd, args_def, args):
542
  """Verifies the arguments using the argument definition.
543

544
  Algorithm:
545

546
    1. Abort with error if values specified by user but none expected.
547

548
    1. For each argument in definition
549

550
      1. Keep running count of minimum number of values (min_count)
551
      1. Keep running count of maximum number of values (max_count)
552
      1. If it has an unlimited number of values
553

554
        1. Abort with error if it's not the last argument in the definition
555

556
    1. If last argument has limited number of values
557

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

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

562
  """
563
  if args and not args_def:
564
    ToStderr("Error: Command %s expects no arguments", cmd)
565
    return False
566

    
567
  min_count = None
568
  max_count = None
569
  check_max = None
570

    
571
  last_idx = len(args_def) - 1
572

    
573
  for idx, arg in enumerate(args_def):
574
    if min_count is None:
575
      min_count = arg.min
576
    elif arg.min is not None:
577
      min_count += arg.min
578

    
579
    if max_count is None:
580
      max_count = arg.max
581
    elif arg.max is not None:
582
      max_count += arg.max
583

    
584
    if idx == last_idx:
585
      check_max = (arg.max is not None)
586

    
587
    elif arg.max is None:
588
      raise errors.ProgrammerError("Only the last argument can have max=None")
589

    
590
  if check_max:
591
    # Command with exact number of arguments
592
    if (min_count is not None and max_count is not None and
593
        min_count == max_count and len(args) != min_count):
594
      ToStderr("Error: Command %s expects %d argument(s)", cmd, min_count)
595
      return False
596

    
597
    # Command with limited number of arguments
598
    if max_count is not None and len(args) > max_count:
599
      ToStderr("Error: Command %s expects only %d argument(s)",
600
               cmd, max_count)
601
      return False
602

    
603
  # Command with some required arguments
604
  if min_count is not None and len(args) < min_count:
605
    ToStderr("Error: Command %s expects at least %d argument(s)",
606
             cmd, min_count)
607
    return False
608

    
609
  return True
610

    
611

    
612
def SplitNodeOption(value):
613
  """Splits the value of a --node option.
614

615
  """
616
  if value and ':' in value:
617
    return value.split(':', 1)
618
  else:
619
    return (value, None)
620

    
621

    
622
def UsesRPC(fn):
623
  def wrapper(*args, **kwargs):
624
    rpc.Init()
625
    try:
626
      return fn(*args, **kwargs)
627
    finally:
628
      rpc.Shutdown()
629
  return wrapper
630

    
631

    
632
def AskUser(text, choices=None):
633
  """Ask the user a question.
634

635
  @param text: the question to ask
636

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

642
  @return: one of the return values from the choices list; if input is
643
      not possible (i.e. not running with a tty, we return the last
644
      entry from the list
645

646
  """
647
  if choices is None:
648
    choices = [('y', True, 'Perform the operation'),
649
               ('n', False, 'Do not perform the operation')]
650
  if not choices or not isinstance(choices, list):
651
    raise errors.ProgrammerError("Invalid choices argument to AskUser")
652
  for entry in choices:
653
    if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
654
      raise errors.ProgrammerError("Invalid choices element to AskUser")
655

    
656
  answer = choices[-1][1]
657
  new_text = []
658
  for line in text.splitlines():
659
    new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
660
  text = "\n".join(new_text)
661
  try:
662
    f = file("/dev/tty", "a+")
663
  except IOError:
664
    return answer
665
  try:
666
    chars = [entry[0] for entry in choices]
667
    chars[-1] = "[%s]" % chars[-1]
668
    chars.append('?')
669
    maps = dict([(entry[0], entry[1]) for entry in choices])
670
    while True:
671
      f.write(text)
672
      f.write('\n')
673
      f.write("/".join(chars))
674
      f.write(": ")
675
      line = f.readline(2).strip().lower()
676
      if line in maps:
677
        answer = maps[line]
678
        break
679
      elif line == '?':
680
        for entry in choices:
681
          f.write(" %s - %s\n" % (entry[0], entry[2]))
682
        f.write("\n")
683
        continue
684
  finally:
685
    f.close()
686
  return answer
687

    
688

    
689
class JobSubmittedException(Exception):
690
  """Job was submitted, client should exit.
691

692
  This exception has one argument, the ID of the job that was
693
  submitted. The handler should print this ID.
694

695
  This is not an error, just a structured way to exit from clients.
696

697
  """
698

    
699

    
700
def SendJob(ops, cl=None):
701
  """Function to submit an opcode without waiting for the results.
702

703
  @type ops: list
704
  @param ops: list of opcodes
705
  @type cl: luxi.Client
706
  @param cl: the luxi client to use for communicating with the master;
707
             if None, a new client will be created
708

709
  """
710
  if cl is None:
711
    cl = GetClient()
712

    
713
  job_id = cl.SubmitJob(ops)
714

    
715
  return job_id
716

    
717

    
718
def PollJob(job_id, cl=None, feedback_fn=None):
719
  """Function to poll for the result of a job.
720

721
  @type job_id: job identified
722
  @param job_id: the job to poll for results
723
  @type cl: luxi.Client
724
  @param cl: the luxi client to use for communicating with the master;
725
             if None, a new client will be created
726

727
  """
728
  if cl is None:
729
    cl = GetClient()
730

    
731
  prev_job_info = None
732
  prev_logmsg_serial = None
733

    
734
  while True:
735
    result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
736
                                 prev_logmsg_serial)
737
    if not result:
738
      # job not found, go away!
739
      raise errors.JobLost("Job with id %s lost" % job_id)
740

    
741
    # Split result, a tuple of (field values, log entries)
742
    (job_info, log_entries) = result
743
    (status, ) = job_info
744

    
745
    if log_entries:
746
      for log_entry in log_entries:
747
        (serial, timestamp, _, message) = log_entry
748
        if callable(feedback_fn):
749
          feedback_fn(log_entry[1:])
750
        else:
751
          encoded = utils.SafeEncode(message)
752
          ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)), encoded)
753
        prev_logmsg_serial = max(prev_logmsg_serial, serial)
754

    
755
    # TODO: Handle canceled and archived jobs
756
    elif status in (constants.JOB_STATUS_SUCCESS,
757
                    constants.JOB_STATUS_ERROR,
758
                    constants.JOB_STATUS_CANCELING,
759
                    constants.JOB_STATUS_CANCELED):
760
      break
761

    
762
    prev_job_info = job_info
763

    
764
  jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
765
  if not jobs:
766
    raise errors.JobLost("Job with id %s lost" % job_id)
767

    
768
  status, opstatus, result = jobs[0]
769
  if status == constants.JOB_STATUS_SUCCESS:
770
    return result
771
  elif status in (constants.JOB_STATUS_CANCELING,
772
                  constants.JOB_STATUS_CANCELED):
773
    raise errors.OpExecError("Job was canceled")
774
  else:
775
    has_ok = False
776
    for idx, (status, msg) in enumerate(zip(opstatus, result)):
777
      if status == constants.OP_STATUS_SUCCESS:
778
        has_ok = True
779
      elif status == constants.OP_STATUS_ERROR:
780
        errors.MaybeRaise(msg)
781
        if has_ok:
782
          raise errors.OpExecError("partial failure (opcode %d): %s" %
783
                                   (idx, msg))
784
        else:
785
          raise errors.OpExecError(str(msg))
786
    # default failure mode
787
    raise errors.OpExecError(result)
788

    
789

    
790
def SubmitOpCode(op, cl=None, feedback_fn=None):
791
  """Legacy function to submit an opcode.
792

793
  This is just a simple wrapper over the construction of the processor
794
  instance. It should be extended to better handle feedback and
795
  interaction functions.
796

797
  """
798
  if cl is None:
799
    cl = GetClient()
800

    
801
  job_id = SendJob([op], cl)
802

    
803
  op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
804

    
805
  return op_results[0]
806

    
807

    
808
def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
809
  """Wrapper around SubmitOpCode or SendJob.
810

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

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

818
  """
819
  if opts and opts.dry_run:
820
    op.dry_run = opts.dry_run
821
  if opts and opts.submit_only:
822
    job_id = SendJob([op], cl=cl)
823
    raise JobSubmittedException(job_id)
824
  else:
825
    return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
826

    
827

    
828
def GetClient():
829
  # TODO: Cache object?
830
  try:
831
    client = luxi.Client()
832
  except luxi.NoMasterError:
833
    master, myself = ssconf.GetMasterAndMyself()
834
    if master != myself:
835
      raise errors.OpPrereqError("This is not the master node, please connect"
836
                                 " to node '%s' and rerun the command" %
837
                                 master)
838
    else:
839
      raise
840
  return client
841

    
842

    
843
def FormatError(err):
844
  """Return a formatted error message for a given error.
845

846
  This function takes an exception instance and returns a tuple
847
  consisting of two values: first, the recommended exit code, and
848
  second, a string describing the error message (not
849
  newline-terminated).
850

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

    
914

    
915
def GenericMain(commands, override=None, aliases=None):
916
  """Generic main function for all the gnt-* commands.
917

918
  Arguments:
919
    - commands: a dictionary with a special structure, see the design doc
920
                for command line handling.
921
    - override: if not None, we expect a dictionary with keys that will
922
                override command line options; this can be used to pass
923
                options from the scripts to generic functions
924
    - aliases: dictionary with command aliases {'alias': 'target, ...}
925

926
  """
927
  # save the program name and the entire command line for later logging
928
  if sys.argv:
929
    binary = os.path.basename(sys.argv[0]) or sys.argv[0]
930
    if len(sys.argv) >= 2:
931
      binary += " " + sys.argv[1]
932
      old_cmdline = " ".join(sys.argv[2:])
933
    else:
934
      old_cmdline = ""
935
  else:
936
    binary = "<unknown program>"
937
    old_cmdline = ""
938

    
939
  if aliases is None:
940
    aliases = {}
941

    
942
  try:
943
    func, options, args = _ParseArgs(sys.argv, commands, aliases)
944
  except errors.ParameterError, err:
945
    result, err_msg = FormatError(err)
946
    ToStderr(err_msg)
947
    return 1
948

    
949
  if func is None: # parse error
950
    return 1
951

    
952
  if override is not None:
953
    for key, val in override.iteritems():
954
      setattr(options, key, val)
955

    
956
  utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
957
                     stderr_logging=True, program=binary)
958

    
959
  if old_cmdline:
960
    logging.info("run with arguments '%s'", old_cmdline)
961
  else:
962
    logging.info("run with no arguments")
963

    
964
  try:
965
    result = func(options, args)
966
  except (errors.GenericError, luxi.ProtocolError,
967
          JobSubmittedException), err:
968
    result, err_msg = FormatError(err)
969
    logging.exception("Error during command processing")
970
    ToStderr(err_msg)
971

    
972
  return result
973

    
974

    
975
def GenerateTable(headers, fields, separator, data,
976
                  numfields=None, unitfields=None,
977
                  units=None):
978
  """Prints a table with headers and different fields.
979

980
  @type headers: dict
981
  @param headers: dictionary mapping field names to headers for
982
      the table
983
  @type fields: list
984
  @param fields: the field names corresponding to each row in
985
      the data field
986
  @param separator: the separator to be used; if this is None,
987
      the default 'smart' algorithm is used which computes optimal
988
      field width, otherwise just the separator is used between
989
      each field
990
  @type data: list
991
  @param data: a list of lists, each sublist being one row to be output
992
  @type numfields: list
993
  @param numfields: a list with the fields that hold numeric
994
      values and thus should be right-aligned
995
  @type unitfields: list
996
  @param unitfields: a list with the fields that hold numeric
997
      values that should be formatted with the units field
998
  @type units: string or None
999
  @param units: the units we should use for formatting, or None for
1000
      automatic choice (human-readable for non-separator usage, otherwise
1001
      megabytes); this is a one-letter string
1002

1003
  """
1004
  if units is None:
1005
    if separator:
1006
      units = "m"
1007
    else:
1008
      units = "h"
1009

    
1010
  if numfields is None:
1011
    numfields = []
1012
  if unitfields is None:
1013
    unitfields = []
1014

    
1015
  numfields = utils.FieldSet(*numfields)
1016
  unitfields = utils.FieldSet(*unitfields)
1017

    
1018
  format_fields = []
1019
  for field in fields:
1020
    if headers and field not in headers:
1021
      # TODO: handle better unknown fields (either revert to old
1022
      # style of raising exception, or deal more intelligently with
1023
      # variable fields)
1024
      headers[field] = field
1025
    if separator is not None:
1026
      format_fields.append("%s")
1027
    elif numfields.Matches(field):
1028
      format_fields.append("%*s")
1029
    else:
1030
      format_fields.append("%-*s")
1031

    
1032
  if separator is None:
1033
    mlens = [0 for name in fields]
1034
    format = ' '.join(format_fields)
1035
  else:
1036
    format = separator.replace("%", "%%").join(format_fields)
1037

    
1038
  for row in data:
1039
    if row is None:
1040
      continue
1041
    for idx, val in enumerate(row):
1042
      if unitfields.Matches(fields[idx]):
1043
        try:
1044
          val = int(val)
1045
        except ValueError:
1046
          pass
1047
        else:
1048
          val = row[idx] = utils.FormatUnit(val, units)
1049
      val = row[idx] = str(val)
1050
      if separator is None:
1051
        mlens[idx] = max(mlens[idx], len(val))
1052

    
1053
  result = []
1054
  if headers:
1055
    args = []
1056
    for idx, name in enumerate(fields):
1057
      hdr = headers[name]
1058
      if separator is None:
1059
        mlens[idx] = max(mlens[idx], len(hdr))
1060
        args.append(mlens[idx])
1061
      args.append(hdr)
1062
    result.append(format % tuple(args))
1063

    
1064
  for line in data:
1065
    args = []
1066
    if line is None:
1067
      line = ['-' for _ in fields]
1068
    for idx in xrange(len(fields)):
1069
      if separator is None:
1070
        args.append(mlens[idx])
1071
      args.append(line[idx])
1072
    result.append(format % tuple(args))
1073

    
1074
  return result
1075

    
1076

    
1077
def FormatTimestamp(ts):
1078
  """Formats a given timestamp.
1079

1080
  @type ts: timestamp
1081
  @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
1082

1083
  @rtype: string
1084
  @return: a string with the formatted timestamp
1085

1086
  """
1087
  if not isinstance (ts, (tuple, list)) or len(ts) != 2:
1088
    return '?'
1089
  sec, usec = ts
1090
  return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
1091

    
1092

    
1093
def ParseTimespec(value):
1094
  """Parse a time specification.
1095

1096
  The following suffixed will be recognized:
1097

1098
    - s: seconds
1099
    - m: minutes
1100
    - h: hours
1101
    - d: day
1102
    - w: weeks
1103

1104
  Without any suffix, the value will be taken to be in seconds.
1105

1106
  """
1107
  value = str(value)
1108
  if not value:
1109
    raise errors.OpPrereqError("Empty time specification passed")
1110
  suffix_map = {
1111
    's': 1,
1112
    'm': 60,
1113
    'h': 3600,
1114
    'd': 86400,
1115
    'w': 604800,
1116
    }
1117
  if value[-1] not in suffix_map:
1118
    try:
1119
      value = int(value)
1120
    except ValueError:
1121
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1122
  else:
1123
    multiplier = suffix_map[value[-1]]
1124
    value = value[:-1]
1125
    if not value: # no data left after stripping the suffix
1126
      raise errors.OpPrereqError("Invalid time specification (only"
1127
                                 " suffix passed)")
1128
    try:
1129
      value = int(value) * multiplier
1130
    except ValueError:
1131
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1132
  return value
1133

    
1134

    
1135
def GetOnlineNodes(nodes, cl=None, nowarn=False):
1136
  """Returns the names of online nodes.
1137

1138
  This function will also log a warning on stderr with the names of
1139
  the online nodes.
1140

1141
  @param nodes: if not empty, use only this subset of nodes (minus the
1142
      offline ones)
1143
  @param cl: if not None, luxi client to use
1144
  @type nowarn: boolean
1145
  @param nowarn: by default, this function will output a note with the
1146
      offline nodes that are skipped; if this parameter is True the
1147
      note is not displayed
1148

1149
  """
1150
  if cl is None:
1151
    cl = GetClient()
1152

    
1153
  result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
1154
                         use_locking=False)
1155
  offline = [row[0] for row in result if row[1]]
1156
  if offline and not nowarn:
1157
    ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
1158
  return [row[0] for row in result if not row[1]]
1159

    
1160

    
1161
def _ToStream(stream, txt, *args):
1162
  """Write a message to a stream, bypassing the logging system
1163

1164
  @type stream: file object
1165
  @param stream: the file to which we should write
1166
  @type txt: str
1167
  @param txt: the message
1168

1169
  """
1170
  if args:
1171
    args = tuple(args)
1172
    stream.write(txt % args)
1173
  else:
1174
    stream.write(txt)
1175
  stream.write('\n')
1176
  stream.flush()
1177

    
1178

    
1179
def ToStdout(txt, *args):
1180
  """Write a message to stdout only, bypassing the logging system
1181

1182
  This is just a wrapper over _ToStream.
1183

1184
  @type txt: str
1185
  @param txt: the message
1186

1187
  """
1188
  _ToStream(sys.stdout, txt, *args)
1189

    
1190

    
1191
def ToStderr(txt, *args):
1192
  """Write a message to stderr only, bypassing the logging system
1193

1194
  This is just a wrapper over _ToStream.
1195

1196
  @type txt: str
1197
  @param txt: the message
1198

1199
  """
1200
  _ToStream(sys.stderr, txt, *args)
1201

    
1202

    
1203
class JobExecutor(object):
1204
  """Class which manages the submission and execution of multiple jobs.
1205

1206
  Note that instances of this class should not be reused between
1207
  GetResults() calls.
1208

1209
  """
1210
  def __init__(self, cl=None, verbose=True):
1211
    self.queue = []
1212
    if cl is None:
1213
      cl = GetClient()
1214
    self.cl = cl
1215
    self.verbose = verbose
1216
    self.jobs = []
1217

    
1218
  def QueueJob(self, name, *ops):
1219
    """Record a job for later submit.
1220

1221
    @type name: string
1222
    @param name: a description of the job, will be used in WaitJobSet
1223
    """
1224
    self.queue.append((name, ops))
1225

    
1226
  def SubmitPending(self):
1227
    """Submit all pending jobs.
1228

1229
    """
1230
    results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1231
    for ((status, data), (name, _)) in zip(results, self.queue):
1232
      self.jobs.append((status, data, name))
1233

    
1234
  def GetResults(self):
1235
    """Wait for and return the results of all jobs.
1236

1237
    @rtype: list
1238
    @return: list of tuples (success, job results), in the same order
1239
        as the submitted jobs; if a job has failed, instead of the result
1240
        there will be the error message
1241

1242
    """
1243
    if not self.jobs:
1244
      self.SubmitPending()
1245
    results = []
1246
    if self.verbose:
1247
      ok_jobs = [row[1] for row in self.jobs if row[0]]
1248
      if ok_jobs:
1249
        ToStdout("Submitted jobs %s", ", ".join(ok_jobs))
1250
    for submit_status, jid, name in self.jobs:
1251
      if not submit_status:
1252
        ToStderr("Failed to submit job for %s: %s", name, jid)
1253
        results.append((False, jid))
1254
        continue
1255
      if self.verbose:
1256
        ToStdout("Waiting for job %s for %s...", jid, name)
1257
      try:
1258
        job_result = PollJob(jid, cl=self.cl)
1259
        success = True
1260
      except (errors.GenericError, luxi.ProtocolError), err:
1261
        _, job_result = FormatError(err)
1262
        success = False
1263
        # the error message will always be shown, verbose or not
1264
        ToStderr("Job %s for %s has failed: %s", jid, name, job_result)
1265

    
1266
      results.append((success, job_result))
1267
    return results
1268

    
1269
  def WaitOrShow(self, wait):
1270
    """Wait for job results or only print the job IDs.
1271

1272
    @type wait: boolean
1273
    @param wait: whether to wait or not
1274

1275
    """
1276
    if wait:
1277
      return self.GetResults()
1278
    else:
1279
      if not self.jobs:
1280
        self.SubmitPending()
1281
      for status, result, name in self.jobs:
1282
        if status:
1283
          ToStdout("%s: %s", result, name)
1284
        else:
1285
          ToStderr("Failure for %s: %s", name, result)