Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ c38c44ad

History | View | Annotate | Download (34.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",
57
           ]
58

    
59
NO_PREFIX = "no_"
60
UN_PREFIX = "-"
61

    
62

    
63
class _Argument:
64
  def __init__(self, min=0, max=None, suggest=None):
65
    self.min = min
66
    self.max = max
67

    
68
  def __repr__(self):
69
    return ("<%s min=%s max=%s>" %
70
            (self.__class__.__name__, self.min, self.max))
71

    
72

    
73
class ArgSuggest(_Argument):
74
  """Suggesting argument.
75

76
  Value can be any of the ones passed to the constructor.
77

78
  """
79
  def __init__(self, min=0, max=None, choices=None):
80
    _Argument.__init__(self, min=min, max=max)
81
    self.choices = choices
82

    
83
  def __repr__(self):
84
    return ("<%s min=%s max=%s choices=%r>" %
85
            (self.__class__.__name__, self.min, self.max, self.choices))
86

    
87

    
88
class ArgChoice(ArgSuggest):
89
  """Choice argument.
90

91
  Value can be any of the ones passed to the constructor. Like L{ArgSuggest},
92
  but value must be one of the choices.
93

94
  """
95

    
96

    
97
class ArgUnknown(_Argument):
98
  """Unknown argument to program (e.g. determined at runtime).
99

100
  """
101

    
102

    
103
class ArgInstance(_Argument):
104
  """Instances argument.
105

106
  """
107

    
108

    
109
class ArgNode(_Argument):
110
  """Node argument.
111

112
  """
113

    
114
class ArgJobId(_Argument):
115
  """Job ID argument.
116

117
  """
118

    
119

    
120
class ArgFile(_Argument):
121
  """File path argument.
122

123
  """
124

    
125

    
126
class ArgCommand(_Argument):
127
  """Command argument.
128

129
  """
130

    
131

    
132
def _ExtractTagsObject(opts, args):
133
  """Extract the tag type object.
134

135
  Note that this function will modify its args parameter.
136

137
  """
138
  if not hasattr(opts, "tag_type"):
139
    raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
140
  kind = opts.tag_type
141
  if kind == constants.TAG_CLUSTER:
142
    retval = kind, kind
143
  elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
144
    if not args:
145
      raise errors.OpPrereqError("no arguments passed to the command")
146
    name = args.pop(0)
147
    retval = kind, name
148
  else:
149
    raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
150
  return retval
151

    
152

    
153
def _ExtendTags(opts, args):
154
  """Extend the args if a source file has been given.
155

156
  This function will extend the tags with the contents of the file
157
  passed in the 'tags_source' attribute of the opts parameter. A file
158
  named '-' will be replaced by stdin.
159

160
  """
161
  fname = opts.tags_source
162
  if fname is None:
163
    return
164
  if fname == "-":
165
    new_fh = sys.stdin
166
  else:
167
    new_fh = open(fname, "r")
168
  new_data = []
169
  try:
170
    # we don't use the nice 'new_data = [line.strip() for line in fh]'
171
    # because of python bug 1633941
172
    while True:
173
      line = new_fh.readline()
174
      if not line:
175
        break
176
      new_data.append(line.strip())
177
  finally:
178
    new_fh.close()
179
  args.extend(new_data)
180

    
181

    
182
def ListTags(opts, args):
183
  """List the tags on a given object.
184

185
  This is a generic implementation that knows how to deal with all
186
  three cases of tag objects (cluster, node, instance). The opts
187
  argument is expected to contain a tag_type field denoting what
188
  object type we work on.
189

190
  """
191
  kind, name = _ExtractTagsObject(opts, args)
192
  op = opcodes.OpGetTags(kind=kind, name=name)
193
  result = SubmitOpCode(op)
194
  result = list(result)
195
  result.sort()
196
  for tag in result:
197
    ToStdout(tag)
198

    
199

    
200
def AddTags(opts, args):
201
  """Add 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
  _ExtendTags(opts, args)
211
  if not args:
212
    raise errors.OpPrereqError("No tags to be added")
213
  op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
214
  SubmitOpCode(op)
215

    
216

    
217
def RemoveTags(opts, args):
218
  """Remove tags from a given object.
219

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

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

    
233

    
234
def check_unit(option, opt, value):
235
  """OptParsers custom converter for units.
236

237
  """
238
  try:
239
    return utils.ParseUnit(value)
240
  except errors.UnitParseError, err:
241
    raise OptionValueError("option %s: %s" % (opt, err))
242

    
243

    
244
def _SplitKeyVal(opt, data):
245
  """Convert a KeyVal string into a dict.
246

247
  This function will convert a key=val[,...] string into a dict. Empty
248
  values will be converted specially: keys which have the prefix 'no_'
249
  will have the value=False and the prefix stripped, the others will
250
  have value=True.
251

252
  @type opt: string
253
  @param opt: a string holding the option name for which we process the
254
      data, used in building error messages
255
  @type data: string
256
  @param data: a string of the format key=val,key=val,...
257
  @rtype: dict
258
  @return: {key=val, key=val}
259
  @raises errors.ParameterError: if there are duplicate keys
260

261
  """
262
  kv_dict = {}
263
  if data:
264
    for elem in data.split(","):
265
      if "=" in elem:
266
        key, val = elem.split("=", 1)
267
      else:
268
        if elem.startswith(NO_PREFIX):
269
          key, val = elem[len(NO_PREFIX):], False
270
        elif elem.startswith(UN_PREFIX):
271
          key, val = elem[len(UN_PREFIX):], None
272
        else:
273
          key, val = elem, True
274
      if key in kv_dict:
275
        raise errors.ParameterError("Duplicate key '%s' in option %s" %
276
                                    (key, opt))
277
      kv_dict[key] = val
278
  return kv_dict
279

    
280

    
281
def check_ident_key_val(option, opt, value):
282
  """Custom parser for ident:key=val,key=val options.
283

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

287
  """
288
  if ":" not in value:
289
    ident, rest = value, ''
290
  else:
291
    ident, rest = value.split(":", 1)
292

    
293
  if ident.startswith(NO_PREFIX):
294
    if rest:
295
      msg = "Cannot pass options when removing parameter groups: %s" % value
296
      raise errors.ParameterError(msg)
297
    retval = (ident[len(NO_PREFIX):], False)
298
  elif ident.startswith(UN_PREFIX):
299
    if rest:
300
      msg = "Cannot pass options when removing parameter groups: %s" % value
301
      raise errors.ParameterError(msg)
302
    retval = (ident[len(UN_PREFIX):], None)
303
  else:
304
    kv_dict = _SplitKeyVal(opt, rest)
305
    retval = (ident, kv_dict)
306
  return retval
307

    
308

    
309
def check_key_val(option, opt, value):
310
  """Custom parser class for key=val,key=val options.
311

312
  This will store the parsed values as a dict {key: val}.
313

314
  """
315
  return _SplitKeyVal(opt, value)
316

    
317

    
318
class CliOption(Option):
319
  """Custom option class for optparse.
320

321
  """
322
  ATTRS = Option.ATTRS + [
323
    "completion_suggest",
324
    ]
325
  TYPES = Option.TYPES + (
326
    "identkeyval",
327
    "keyval",
328
    "unit",
329
    )
330
  TYPE_CHECKER = Option.TYPE_CHECKER.copy()
331
  TYPE_CHECKER["identkeyval"] = check_ident_key_val
332
  TYPE_CHECKER["keyval"] = check_key_val
333
  TYPE_CHECKER["unit"] = check_unit
334

    
335

    
336
# optparse.py sets make_option, so we do it for our own option class, too
337
cli_option = CliOption
338

    
339

    
340
DEBUG_OPT = cli_option("-d", "--debug", default=False,
341
                       action="store_true",
342
                       help="Turn debugging on")
343

    
344
NOHDR_OPT = cli_option("--no-headers", default=False,
345
                       action="store_true", dest="no_headers",
346
                       help="Don't display column headers")
347

    
348
SEP_OPT = cli_option("--separator", default=None,
349
                     action="store", dest="separator",
350
                     help=("Separator between output fields"
351
                           " (defaults to one space)"))
352

    
353
USEUNITS_OPT = cli_option("--units", default=None,
354
                          dest="units", choices=('h', 'm', 'g', 't'),
355
                          help="Specify units for output (one of hmgt)")
356

    
357
FIELDS_OPT = cli_option("-o", "--output", dest="output", action="store",
358
                        type="string", metavar="FIELDS",
359
                        help="Comma separated list of output fields")
360

    
361
FORCE_OPT = cli_option("-f", "--force", dest="force", action="store_true",
362
                       default=False, help="Force the operation")
363

    
364
CONFIRM_OPT = cli_option("--yes", dest="confirm", action="store_true",
365
                         default=False, help="Do not require confirmation")
366

    
367
TAG_SRC_OPT = cli_option("--from", dest="tags_source",
368
                         default=None, help="File with tag names")
369

    
370
SUBMIT_OPT = cli_option("--submit", dest="submit_only",
371
                        default=False, action="store_true",
372
                        help=("Submit the job and return the job ID, but"
373
                              " don't wait for the job to finish"))
374

    
375
SYNC_OPT = cli_option("--sync", dest="do_locking",
376
                      default=False, action="store_true",
377
                      help=("Grab locks while doing the queries"
378
                            " in order to ensure more consistent results"))
379

    
380
_DRY_RUN_OPT = cli_option("--dry-run", default=False,
381
                          action="store_true",
382
                          help=("Do not execute the operation, just run the"
383
                                " check steps and verify it it could be"
384
                                " executed"))
385

    
386

    
387
def _ParseArgs(argv, commands, aliases):
388
  """Parser for the command line arguments.
389

390
  This function parses the arguments and returns the function which
391
  must be executed together with its (modified) arguments.
392

393
  @param argv: the command line
394
  @param commands: dictionary with special contents, see the design
395
      doc for cmdline handling
396
  @param aliases: dictionary with command aliases {'alias': 'target, ...}
397

398
  """
399
  if len(argv) == 0:
400
    binary = "<command>"
401
  else:
402
    binary = argv[0].split("/")[-1]
403

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

    
410
  if len(argv) < 2 or not (argv[1] in commands or
411
                           argv[1] in aliases):
412
    # let's do a nice thing
413
    sortedcmds = commands.keys()
414
    sortedcmds.sort()
415

    
416
    ToStdout("Usage: %s {command} [options...] [argument...]", binary)
417
    ToStdout("%s <command> --help to see details, or man %s", binary, binary)
418
    ToStdout("")
419

    
420
    # compute the max line length for cmd + usage
421
    mlen = max([len(" %s" % cmd) for cmd in commands])
422
    mlen = min(60, mlen) # should not get here...
423

    
424
    # and format a nice command list
425
    ToStdout("Commands:")
426
    for cmd in sortedcmds:
427
      cmdstr = " %s" % (cmd,)
428
      help_text = commands[cmd][4]
429
      help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
430
      ToStdout("%-*s - %s", mlen, cmdstr, help_lines.pop(0))
431
      for line in help_lines:
432
        ToStdout("%-*s   %s", mlen, "", line)
433

    
434
    ToStdout("")
435

    
436
    return None, None, None
437

    
438
  # get command, unalias it, and look it up in commands
439
  cmd = argv.pop(1)
440
  if cmd in aliases:
441
    if cmd in commands:
442
      raise errors.ProgrammerError("Alias '%s' overrides an existing"
443
                                   " command" % cmd)
444

    
445
    if aliases[cmd] not in commands:
446
      raise errors.ProgrammerError("Alias '%s' maps to non-existing"
447
                                   " command '%s'" % (cmd, aliases[cmd]))
448

    
449
    cmd = aliases[cmd]
450

    
451
  func, args_def, parser_opts, usage, description = commands[cmd]
452
  parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT],
453
                        description=description,
454
                        formatter=TitledHelpFormatter(),
455
                        usage="%%prog %s %s" % (cmd, usage))
456
  parser.disable_interspersed_args()
457
  options, args = parser.parse_args()
458

    
459
  if not _CheckArguments(cmd, args_def, args):
460
    return None, None, None
461

    
462
  return func, options, args
463

    
464

    
465
def _CheckArguments(cmd, args_def, args):
466
  """Verifies the arguments using the argument definition.
467

468
  Algorithm:
469

470
    1. Abort with error if values specified by user but none expected.
471

472
    1. For each argument in definition
473

474
      1. Keep running count of minimum number of values (min_count)
475
      1. Keep running count of maximum number of values (max_count)
476
      1. If it has an unlimited number of values
477

478
        1. Abort with error if it's not the last argument in the definition
479

480
    1. If last argument has limited number of values
481

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

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

486
  """
487
  if args and not args_def:
488
    ToStderr("Error: Command %s expects no arguments", cmd)
489
    return False
490

    
491
  min_count = None
492
  max_count = None
493
  check_max = None
494

    
495
  last_idx = len(args_def) - 1
496

    
497
  for idx, arg in enumerate(args_def):
498
    if min_count is None:
499
      min_count = arg.min
500
    elif arg.min is not None:
501
      min_count += arg.min
502

    
503
    if max_count is None:
504
      max_count = arg.max
505
    elif arg.max is not None:
506
      max_count += arg.max
507

    
508
    if idx == last_idx:
509
      check_max = (arg.max is not None)
510

    
511
    elif arg.max is None:
512
      raise errors.ProgrammerError("Only the last argument can have max=None")
513

    
514
  if check_max:
515
    # Command with exact number of arguments
516
    if (min_count is not None and max_count is not None and
517
        min_count == max_count and len(args) != min_count):
518
      ToStderr("Error: Command %s expects %d argument(s)", cmd, min_count)
519
      return False
520

    
521
    # Command with limited number of arguments
522
    if max_count is not None and len(args) > max_count:
523
      ToStderr("Error: Command %s expects only %d argument(s)",
524
               cmd, max_count)
525
      return False
526

    
527
  # Command with some required arguments
528
  if min_count is not None and len(args) < min_count:
529
    ToStderr("Error: Command %s expects at least %d argument(s)",
530
             cmd, min_count)
531
    return False
532

    
533
  return True
534

    
535

    
536
def SplitNodeOption(value):
537
  """Splits the value of a --node option.
538

539
  """
540
  if value and ':' in value:
541
    return value.split(':', 1)
542
  else:
543
    return (value, None)
544

    
545

    
546
def UsesRPC(fn):
547
  def wrapper(*args, **kwargs):
548
    rpc.Init()
549
    try:
550
      return fn(*args, **kwargs)
551
    finally:
552
      rpc.Shutdown()
553
  return wrapper
554

    
555

    
556
def AskUser(text, choices=None):
557
  """Ask the user a question.
558

559
  @param text: the question to ask
560

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

566
  @return: one of the return values from the choices list; if input is
567
      not possible (i.e. not running with a tty, we return the last
568
      entry from the list
569

570
  """
571
  if choices is None:
572
    choices = [('y', True, 'Perform the operation'),
573
               ('n', False, 'Do not perform the operation')]
574
  if not choices or not isinstance(choices, list):
575
    raise errors.ProgrammerError("Invalid choices argument to AskUser")
576
  for entry in choices:
577
    if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
578
      raise errors.ProgrammerError("Invalid choices element to AskUser")
579

    
580
  answer = choices[-1][1]
581
  new_text = []
582
  for line in text.splitlines():
583
    new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
584
  text = "\n".join(new_text)
585
  try:
586
    f = file("/dev/tty", "a+")
587
  except IOError:
588
    return answer
589
  try:
590
    chars = [entry[0] for entry in choices]
591
    chars[-1] = "[%s]" % chars[-1]
592
    chars.append('?')
593
    maps = dict([(entry[0], entry[1]) for entry in choices])
594
    while True:
595
      f.write(text)
596
      f.write('\n')
597
      f.write("/".join(chars))
598
      f.write(": ")
599
      line = f.readline(2).strip().lower()
600
      if line in maps:
601
        answer = maps[line]
602
        break
603
      elif line == '?':
604
        for entry in choices:
605
          f.write(" %s - %s\n" % (entry[0], entry[2]))
606
        f.write("\n")
607
        continue
608
  finally:
609
    f.close()
610
  return answer
611

    
612

    
613
class JobSubmittedException(Exception):
614
  """Job was submitted, client should exit.
615

616
  This exception has one argument, the ID of the job that was
617
  submitted. The handler should print this ID.
618

619
  This is not an error, just a structured way to exit from clients.
620

621
  """
622

    
623

    
624
def SendJob(ops, cl=None):
625
  """Function to submit an opcode without waiting for the results.
626

627
  @type ops: list
628
  @param ops: list of opcodes
629
  @type cl: luxi.Client
630
  @param cl: the luxi client to use for communicating with the master;
631
             if None, a new client will be created
632

633
  """
634
  if cl is None:
635
    cl = GetClient()
636

    
637
  job_id = cl.SubmitJob(ops)
638

    
639
  return job_id
640

    
641

    
642
def PollJob(job_id, cl=None, feedback_fn=None):
643
  """Function to poll for the result of a job.
644

645
  @type job_id: job identified
646
  @param job_id: the job to poll for results
647
  @type cl: luxi.Client
648
  @param cl: the luxi client to use for communicating with the master;
649
             if None, a new client will be created
650

651
  """
652
  if cl is None:
653
    cl = GetClient()
654

    
655
  prev_job_info = None
656
  prev_logmsg_serial = None
657

    
658
  while True:
659
    result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
660
                                 prev_logmsg_serial)
661
    if not result:
662
      # job not found, go away!
663
      raise errors.JobLost("Job with id %s lost" % job_id)
664

    
665
    # Split result, a tuple of (field values, log entries)
666
    (job_info, log_entries) = result
667
    (status, ) = job_info
668

    
669
    if log_entries:
670
      for log_entry in log_entries:
671
        (serial, timestamp, _, message) = log_entry
672
        if callable(feedback_fn):
673
          feedback_fn(log_entry[1:])
674
        else:
675
          encoded = utils.SafeEncode(message)
676
          ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)), encoded)
677
        prev_logmsg_serial = max(prev_logmsg_serial, serial)
678

    
679
    # TODO: Handle canceled and archived jobs
680
    elif status in (constants.JOB_STATUS_SUCCESS,
681
                    constants.JOB_STATUS_ERROR,
682
                    constants.JOB_STATUS_CANCELING,
683
                    constants.JOB_STATUS_CANCELED):
684
      break
685

    
686
    prev_job_info = job_info
687

    
688
  jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
689
  if not jobs:
690
    raise errors.JobLost("Job with id %s lost" % job_id)
691

    
692
  status, opstatus, result = jobs[0]
693
  if status == constants.JOB_STATUS_SUCCESS:
694
    return result
695
  elif status in (constants.JOB_STATUS_CANCELING,
696
                  constants.JOB_STATUS_CANCELED):
697
    raise errors.OpExecError("Job was canceled")
698
  else:
699
    has_ok = False
700
    for idx, (status, msg) in enumerate(zip(opstatus, result)):
701
      if status == constants.OP_STATUS_SUCCESS:
702
        has_ok = True
703
      elif status == constants.OP_STATUS_ERROR:
704
        if has_ok:
705
          raise errors.OpExecError("partial failure (opcode %d): %s" %
706
                                   (idx, msg))
707
        else:
708
          raise errors.OpExecError(str(msg))
709
    # default failure mode
710
    raise errors.OpExecError(result)
711

    
712

    
713
def SubmitOpCode(op, cl=None, feedback_fn=None):
714
  """Legacy function to submit an opcode.
715

716
  This is just a simple wrapper over the construction of the processor
717
  instance. It should be extended to better handle feedback and
718
  interaction functions.
719

720
  """
721
  if cl is None:
722
    cl = GetClient()
723

    
724
  job_id = SendJob([op], cl)
725

    
726
  op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
727

    
728
  return op_results[0]
729

    
730

    
731
def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
732
  """Wrapper around SubmitOpCode or SendJob.
733

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

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

741
  """
742
  if opts and opts.dry_run:
743
    op.dry_run = opts.dry_run
744
  if opts and opts.submit_only:
745
    job_id = SendJob([op], cl=cl)
746
    raise JobSubmittedException(job_id)
747
  else:
748
    return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
749

    
750

    
751
def GetClient():
752
  # TODO: Cache object?
753
  try:
754
    client = luxi.Client()
755
  except luxi.NoMasterError:
756
    master, myself = ssconf.GetMasterAndMyself()
757
    if master != myself:
758
      raise errors.OpPrereqError("This is not the master node, please connect"
759
                                 " to node '%s' and rerun the command" %
760
                                 master)
761
    else:
762
      raise
763
  return client
764

    
765

    
766
def FormatError(err):
767
  """Return a formatted error message for a given error.
768

769
  This function takes an exception instance and returns a tuple
770
  consisting of two values: first, the recommended exit code, and
771
  second, a string describing the error message (not
772
  newline-terminated).
773

774
  """
775
  retcode = 1
776
  obuf = StringIO()
777
  msg = str(err)
778
  if isinstance(err, errors.ConfigurationError):
779
    txt = "Corrupt configuration file: %s" % msg
780
    logging.error(txt)
781
    obuf.write(txt + "\n")
782
    obuf.write("Aborting.")
783
    retcode = 2
784
  elif isinstance(err, errors.HooksAbort):
785
    obuf.write("Failure: hooks execution failed:\n")
786
    for node, script, out in err.args[0]:
787
      if out:
788
        obuf.write("  node: %s, script: %s, output: %s\n" %
789
                   (node, script, out))
790
      else:
791
        obuf.write("  node: %s, script: %s (no output)\n" %
792
                   (node, script))
793
  elif isinstance(err, errors.HooksFailure):
794
    obuf.write("Failure: hooks general failure: %s" % msg)
795
  elif isinstance(err, errors.ResolverError):
796
    this_host = utils.HostInfo.SysName()
797
    if err.args[0] == this_host:
798
      msg = "Failure: can't resolve my own hostname ('%s')"
799
    else:
800
      msg = "Failure: can't resolve hostname '%s'"
801
    obuf.write(msg % err.args[0])
802
  elif isinstance(err, errors.OpPrereqError):
803
    obuf.write("Failure: prerequisites not met for this"
804
               " operation:\n%s" % msg)
805
  elif isinstance(err, errors.OpExecError):
806
    obuf.write("Failure: command execution error:\n%s" % msg)
807
  elif isinstance(err, errors.TagError):
808
    obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
809
  elif isinstance(err, errors.JobQueueDrainError):
810
    obuf.write("Failure: the job queue is marked for drain and doesn't"
811
               " accept new requests\n")
812
  elif isinstance(err, errors.JobQueueFull):
813
    obuf.write("Failure: the job queue is full and doesn't accept new"
814
               " job submissions until old jobs are archived\n")
815
  elif isinstance(err, errors.TypeEnforcementError):
816
    obuf.write("Parameter Error: %s" % msg)
817
  elif isinstance(err, errors.ParameterError):
818
    obuf.write("Failure: unknown/wrong parameter name '%s'" % msg)
819
  elif isinstance(err, errors.GenericError):
820
    obuf.write("Unhandled Ganeti error: %s" % msg)
821
  elif isinstance(err, luxi.NoMasterError):
822
    obuf.write("Cannot communicate with the master daemon.\nIs it running"
823
               " and listening for connections?")
824
  elif isinstance(err, luxi.TimeoutError):
825
    obuf.write("Timeout while talking to the master daemon. Error:\n"
826
               "%s" % msg)
827
  elif isinstance(err, luxi.ProtocolError):
828
    obuf.write("Unhandled protocol error while talking to the master daemon:\n"
829
               "%s" % msg)
830
  elif isinstance(err, JobSubmittedException):
831
    obuf.write("JobID: %s\n" % err.args[0])
832
    retcode = 0
833
  else:
834
    obuf.write("Unhandled exception: %s" % msg)
835
  return retcode, obuf.getvalue().rstrip('\n')
836

    
837

    
838
def GenericMain(commands, override=None, aliases=None):
839
  """Generic main function for all the gnt-* commands.
840

841
  Arguments:
842
    - commands: a dictionary with a special structure, see the design doc
843
                for command line handling.
844
    - override: if not None, we expect a dictionary with keys that will
845
                override command line options; this can be used to pass
846
                options from the scripts to generic functions
847
    - aliases: dictionary with command aliases {'alias': 'target, ...}
848

849
  """
850
  # save the program name and the entire command line for later logging
851
  if sys.argv:
852
    binary = os.path.basename(sys.argv[0]) or sys.argv[0]
853
    if len(sys.argv) >= 2:
854
      binary += " " + sys.argv[1]
855
      old_cmdline = " ".join(sys.argv[2:])
856
    else:
857
      old_cmdline = ""
858
  else:
859
    binary = "<unknown program>"
860
    old_cmdline = ""
861

    
862
  if aliases is None:
863
    aliases = {}
864

    
865
  try:
866
    func, options, args = _ParseArgs(sys.argv, commands, aliases)
867
  except errors.ParameterError, err:
868
    result, err_msg = FormatError(err)
869
    ToStderr(err_msg)
870
    return 1
871

    
872
  if func is None: # parse error
873
    return 1
874

    
875
  if override is not None:
876
    for key, val in override.iteritems():
877
      setattr(options, key, val)
878

    
879
  utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
880
                     stderr_logging=True, program=binary)
881

    
882
  if old_cmdline:
883
    logging.info("run with arguments '%s'", old_cmdline)
884
  else:
885
    logging.info("run with no arguments")
886

    
887
  try:
888
    result = func(options, args)
889
  except (errors.GenericError, luxi.ProtocolError,
890
          JobSubmittedException), err:
891
    result, err_msg = FormatError(err)
892
    logging.exception("Error during command processing")
893
    ToStderr(err_msg)
894

    
895
  return result
896

    
897

    
898
def GenerateTable(headers, fields, separator, data,
899
                  numfields=None, unitfields=None,
900
                  units=None):
901
  """Prints a table with headers and different fields.
902

903
  @type headers: dict
904
  @param headers: dictionary mapping field names to headers for
905
      the table
906
  @type fields: list
907
  @param fields: the field names corresponding to each row in
908
      the data field
909
  @param separator: the separator to be used; if this is None,
910
      the default 'smart' algorithm is used which computes optimal
911
      field width, otherwise just the separator is used between
912
      each field
913
  @type data: list
914
  @param data: a list of lists, each sublist being one row to be output
915
  @type numfields: list
916
  @param numfields: a list with the fields that hold numeric
917
      values and thus should be right-aligned
918
  @type unitfields: list
919
  @param unitfields: a list with the fields that hold numeric
920
      values that should be formatted with the units field
921
  @type units: string or None
922
  @param units: the units we should use for formatting, or None for
923
      automatic choice (human-readable for non-separator usage, otherwise
924
      megabytes); this is a one-letter string
925

926
  """
927
  if units is None:
928
    if separator:
929
      units = "m"
930
    else:
931
      units = "h"
932

    
933
  if numfields is None:
934
    numfields = []
935
  if unitfields is None:
936
    unitfields = []
937

    
938
  numfields = utils.FieldSet(*numfields)
939
  unitfields = utils.FieldSet(*unitfields)
940

    
941
  format_fields = []
942
  for field in fields:
943
    if headers and field not in headers:
944
      # TODO: handle better unknown fields (either revert to old
945
      # style of raising exception, or deal more intelligently with
946
      # variable fields)
947
      headers[field] = field
948
    if separator is not None:
949
      format_fields.append("%s")
950
    elif numfields.Matches(field):
951
      format_fields.append("%*s")
952
    else:
953
      format_fields.append("%-*s")
954

    
955
  if separator is None:
956
    mlens = [0 for name in fields]
957
    format = ' '.join(format_fields)
958
  else:
959
    format = separator.replace("%", "%%").join(format_fields)
960

    
961
  for row in data:
962
    if row is None:
963
      continue
964
    for idx, val in enumerate(row):
965
      if unitfields.Matches(fields[idx]):
966
        try:
967
          val = int(val)
968
        except ValueError:
969
          pass
970
        else:
971
          val = row[idx] = utils.FormatUnit(val, units)
972
      val = row[idx] = str(val)
973
      if separator is None:
974
        mlens[idx] = max(mlens[idx], len(val))
975

    
976
  result = []
977
  if headers:
978
    args = []
979
    for idx, name in enumerate(fields):
980
      hdr = headers[name]
981
      if separator is None:
982
        mlens[idx] = max(mlens[idx], len(hdr))
983
        args.append(mlens[idx])
984
      args.append(hdr)
985
    result.append(format % tuple(args))
986

    
987
  for line in data:
988
    args = []
989
    if line is None:
990
      line = ['-' for _ in fields]
991
    for idx in xrange(len(fields)):
992
      if separator is None:
993
        args.append(mlens[idx])
994
      args.append(line[idx])
995
    result.append(format % tuple(args))
996

    
997
  return result
998

    
999

    
1000
def FormatTimestamp(ts):
1001
  """Formats a given timestamp.
1002

1003
  @type ts: timestamp
1004
  @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
1005

1006
  @rtype: string
1007
  @return: a string with the formatted timestamp
1008

1009
  """
1010
  if not isinstance (ts, (tuple, list)) or len(ts) != 2:
1011
    return '?'
1012
  sec, usec = ts
1013
  return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
1014

    
1015

    
1016
def ParseTimespec(value):
1017
  """Parse a time specification.
1018

1019
  The following suffixed will be recognized:
1020

1021
    - s: seconds
1022
    - m: minutes
1023
    - h: hours
1024
    - d: day
1025
    - w: weeks
1026

1027
  Without any suffix, the value will be taken to be in seconds.
1028

1029
  """
1030
  value = str(value)
1031
  if not value:
1032
    raise errors.OpPrereqError("Empty time specification passed")
1033
  suffix_map = {
1034
    's': 1,
1035
    'm': 60,
1036
    'h': 3600,
1037
    'd': 86400,
1038
    'w': 604800,
1039
    }
1040
  if value[-1] not in suffix_map:
1041
    try:
1042
      value = int(value)
1043
    except ValueError:
1044
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1045
  else:
1046
    multiplier = suffix_map[value[-1]]
1047
    value = value[:-1]
1048
    if not value: # no data left after stripping the suffix
1049
      raise errors.OpPrereqError("Invalid time specification (only"
1050
                                 " suffix passed)")
1051
    try:
1052
      value = int(value) * multiplier
1053
    except ValueError:
1054
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1055
  return value
1056

    
1057

    
1058
def GetOnlineNodes(nodes, cl=None, nowarn=False):
1059
  """Returns the names of online nodes.
1060

1061
  This function will also log a warning on stderr with the names of
1062
  the online nodes.
1063

1064
  @param nodes: if not empty, use only this subset of nodes (minus the
1065
      offline ones)
1066
  @param cl: if not None, luxi client to use
1067
  @type nowarn: boolean
1068
  @param nowarn: by default, this function will output a note with the
1069
      offline nodes that are skipped; if this parameter is True the
1070
      note is not displayed
1071

1072
  """
1073
  if cl is None:
1074
    cl = GetClient()
1075

    
1076
  result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
1077
                         use_locking=False)
1078
  offline = [row[0] for row in result if row[1]]
1079
  if offline and not nowarn:
1080
    ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
1081
  return [row[0] for row in result if not row[1]]
1082

    
1083

    
1084
def _ToStream(stream, txt, *args):
1085
  """Write a message to a stream, bypassing the logging system
1086

1087
  @type stream: file object
1088
  @param stream: the file to which we should write
1089
  @type txt: str
1090
  @param txt: the message
1091

1092
  """
1093
  if args:
1094
    args = tuple(args)
1095
    stream.write(txt % args)
1096
  else:
1097
    stream.write(txt)
1098
  stream.write('\n')
1099
  stream.flush()
1100

    
1101

    
1102
def ToStdout(txt, *args):
1103
  """Write a message to stdout only, bypassing the logging system
1104

1105
  This is just a wrapper over _ToStream.
1106

1107
  @type txt: str
1108
  @param txt: the message
1109

1110
  """
1111
  _ToStream(sys.stdout, txt, *args)
1112

    
1113

    
1114
def ToStderr(txt, *args):
1115
  """Write a message to stderr only, bypassing the logging system
1116

1117
  This is just a wrapper over _ToStream.
1118

1119
  @type txt: str
1120
  @param txt: the message
1121

1122
  """
1123
  _ToStream(sys.stderr, txt, *args)
1124

    
1125

    
1126
class JobExecutor(object):
1127
  """Class which manages the submission and execution of multiple jobs.
1128

1129
  Note that instances of this class should not be reused between
1130
  GetResults() calls.
1131

1132
  """
1133
  def __init__(self, cl=None, verbose=True):
1134
    self.queue = []
1135
    if cl is None:
1136
      cl = GetClient()
1137
    self.cl = cl
1138
    self.verbose = verbose
1139
    self.jobs = []
1140

    
1141
  def QueueJob(self, name, *ops):
1142
    """Record a job for later submit.
1143

1144
    @type name: string
1145
    @param name: a description of the job, will be used in WaitJobSet
1146
    """
1147
    self.queue.append((name, ops))
1148

    
1149
  def SubmitPending(self):
1150
    """Submit all pending jobs.
1151

1152
    """
1153
    results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1154
    for ((status, data), (name, _)) in zip(results, self.queue):
1155
      self.jobs.append((status, data, name))
1156

    
1157
  def GetResults(self):
1158
    """Wait for and return the results of all jobs.
1159

1160
    @rtype: list
1161
    @return: list of tuples (success, job results), in the same order
1162
        as the submitted jobs; if a job has failed, instead of the result
1163
        there will be the error message
1164

1165
    """
1166
    if not self.jobs:
1167
      self.SubmitPending()
1168
    results = []
1169
    if self.verbose:
1170
      ok_jobs = [row[1] for row in self.jobs if row[0]]
1171
      if ok_jobs:
1172
        ToStdout("Submitted jobs %s", ", ".join(ok_jobs))
1173
    for submit_status, jid, name in self.jobs:
1174
      if not submit_status:
1175
        ToStderr("Failed to submit job for %s: %s", name, jid)
1176
        results.append((False, jid))
1177
        continue
1178
      if self.verbose:
1179
        ToStdout("Waiting for job %s for %s...", jid, name)
1180
      try:
1181
        job_result = PollJob(jid, cl=self.cl)
1182
        success = True
1183
      except (errors.GenericError, luxi.ProtocolError), err:
1184
        _, job_result = FormatError(err)
1185
        success = False
1186
        # the error message will always be shown, verbose or not
1187
        ToStderr("Job %s for %s has failed: %s", jid, name, job_result)
1188

    
1189
      results.append((success, job_result))
1190
    return results
1191

    
1192
  def WaitOrShow(self, wait):
1193
    """Wait for job results or only print the job IDs.
1194

1195
    @type wait: boolean
1196
    @param wait: whether to wait or not
1197

1198
    """
1199
    if wait:
1200
      return self.GetResults()
1201
    else:
1202
      if not self.jobs:
1203
        self.SubmitPending()
1204
      for status, result, name in self.jobs:
1205
        if status:
1206
          ToStdout("%s: %s", result, name)
1207
        else:
1208
          ToStderr("Failure for %s: %s", name, result)