Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ bcb66fca

History | View | Annotate | Download (35.1 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
           ]
60

    
61
NO_PREFIX = "no_"
62
UN_PREFIX = "-"
63

    
64

    
65
class _Argument:
66
  def __init__(self, min=0, max=None):
67
    self.min = min
68
    self.max = max
69

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

    
74

    
75
class ArgSuggest(_Argument):
76
  """Suggesting argument.
77

78
  Value can be any of the ones passed to the constructor.
79

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

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

    
89

    
90
class ArgChoice(ArgSuggest):
91
  """Choice argument.
92

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

96
  """
97

    
98

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

102
  """
103

    
104

    
105
class ArgInstance(_Argument):
106
  """Instances argument.
107

108
  """
109

    
110

    
111
class ArgNode(_Argument):
112
  """Node argument.
113

114
  """
115

    
116
class ArgJobId(_Argument):
117
  """Job ID argument.
118

119
  """
120

    
121

    
122
class ArgFile(_Argument):
123
  """File path argument.
124

125
  """
126

    
127

    
128
class ArgCommand(_Argument):
129
  """Command argument.
130

131
  """
132

    
133

    
134
class ArgHost(_Argument):
135
  """Host argument.
136

137
  """
138

    
139

    
140
ARGS_NONE = []
141
ARGS_MANY_INSTANCES = [ArgInstance()]
142
ARGS_MANY_NODES = [ArgNode()]
143
ARGS_ONE_INSTANCE = [ArgInstance(min=1, max=1)]
144
ARGS_ONE_NODE = [ArgNode(min=1, max=1)]
145

    
146

    
147
def _ExtractTagsObject(opts, args):
148
  """Extract the tag type object.
149

150
  Note that this function will modify its args parameter.
151

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

    
167

    
168
def _ExtendTags(opts, args):
169
  """Extend the args if a source file has been given.
170

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

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

    
196

    
197
def ListTags(opts, args):
198
  """List the tags on a given object.
199

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

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

    
214

    
215
def AddTags(opts, args):
216
  """Add tags on a given object.
217

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

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

    
231

    
232
def RemoveTags(opts, args):
233
  """Remove tags from a given object.
234

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

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

    
248

    
249
def check_unit(option, opt, value):
250
  """OptParsers custom converter for units.
251

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

    
258

    
259
def _SplitKeyVal(opt, data):
260
  """Convert a KeyVal string into a dict.
261

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

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

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

    
295

    
296
def check_ident_key_val(option, opt, value):
297
  """Custom parser for ident:key=val,key=val options.
298

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

302
  """
303
  if ":" not in value:
304
    ident, rest = value, ''
305
  else:
306
    ident, rest = value.split(":", 1)
307

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

    
323

    
324
def check_key_val(option, opt, value):
325
  """Custom parser class for key=val,key=val options.
326

327
  This will store the parsed values as a dict {key: val}.
328

329
  """
330
  return _SplitKeyVal(opt, value)
331

    
332

    
333
class CliOption(Option):
334
  """Custom option class for optparse.
335

336
  """
337
  ATTRS = Option.ATTRS + [
338
    "completion_suggest",
339
    ]
340
  TYPES = Option.TYPES + (
341
    "identkeyval",
342
    "keyval",
343
    "unit",
344
    )
345
  TYPE_CHECKER = Option.TYPE_CHECKER.copy()
346
  TYPE_CHECKER["identkeyval"] = check_ident_key_val
347
  TYPE_CHECKER["keyval"] = check_key_val
348
  TYPE_CHECKER["unit"] = check_unit
349

    
350

    
351
# optparse.py sets make_option, so we do it for our own option class, too
352
cli_option = CliOption
353

    
354

    
355
DEBUG_OPT = cli_option("-d", "--debug", default=False,
356
                       action="store_true",
357
                       help="Turn debugging on")
358

    
359
NOHDR_OPT = cli_option("--no-headers", default=False,
360
                       action="store_true", dest="no_headers",
361
                       help="Don't display column headers")
362

    
363
SEP_OPT = cli_option("--separator", default=None,
364
                     action="store", dest="separator",
365
                     help=("Separator between output fields"
366
                           " (defaults to one space)"))
367

    
368
USEUNITS_OPT = cli_option("--units", default=None,
369
                          dest="units", choices=('h', 'm', 'g', 't'),
370
                          help="Specify units for output (one of hmgt)")
371

    
372
FIELDS_OPT = cli_option("-o", "--output", dest="output", action="store",
373
                        type="string", metavar="FIELDS",
374
                        help="Comma separated list of output fields")
375

    
376
FORCE_OPT = cli_option("-f", "--force", dest="force", action="store_true",
377
                       default=False, help="Force the operation")
378

    
379
CONFIRM_OPT = cli_option("--yes", dest="confirm", action="store_true",
380
                         default=False, help="Do not require confirmation")
381

    
382
TAG_SRC_OPT = cli_option("--from", dest="tags_source",
383
                         default=None, help="File with tag names")
384

    
385
SUBMIT_OPT = cli_option("--submit", dest="submit_only",
386
                        default=False, action="store_true",
387
                        help=("Submit the job and return the job ID, but"
388
                              " don't wait for the job to finish"))
389

    
390
SYNC_OPT = cli_option("--sync", dest="do_locking",
391
                      default=False, action="store_true",
392
                      help=("Grab locks while doing the queries"
393
                            " in order to ensure more consistent results"))
394

    
395
_DRY_RUN_OPT = cli_option("--dry-run", default=False,
396
                          action="store_true",
397
                          help=("Do not execute the operation, just run the"
398
                                " check steps and verify it it could be"
399
                                " executed"))
400

    
401

    
402
def _ParseArgs(argv, commands, aliases):
403
  """Parser for the command line arguments.
404

405
  This function parses the arguments and returns the function which
406
  must be executed together with its (modified) arguments.
407

408
  @param argv: the command line
409
  @param commands: dictionary with special contents, see the design
410
      doc for cmdline handling
411
  @param aliases: dictionary with command aliases {'alias': 'target, ...}
412

413
  """
414
  if len(argv) == 0:
415
    binary = "<command>"
416
  else:
417
    binary = argv[0].split("/")[-1]
418

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

    
425
  if len(argv) < 2 or not (argv[1] in commands or
426
                           argv[1] in aliases):
427
    # let's do a nice thing
428
    sortedcmds = commands.keys()
429
    sortedcmds.sort()
430

    
431
    ToStdout("Usage: %s {command} [options...] [argument...]", binary)
432
    ToStdout("%s <command> --help to see details, or man %s", binary, binary)
433
    ToStdout("")
434

    
435
    # compute the max line length for cmd + usage
436
    mlen = max([len(" %s" % cmd) for cmd in commands])
437
    mlen = min(60, mlen) # should not get here...
438

    
439
    # and format a nice command list
440
    ToStdout("Commands:")
441
    for cmd in sortedcmds:
442
      cmdstr = " %s" % (cmd,)
443
      help_text = commands[cmd][4]
444
      help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
445
      ToStdout("%-*s - %s", mlen, cmdstr, help_lines.pop(0))
446
      for line in help_lines:
447
        ToStdout("%-*s   %s", mlen, "", line)
448

    
449
    ToStdout("")
450

    
451
    return None, None, None
452

    
453
  # get command, unalias it, and look it up in commands
454
  cmd = argv.pop(1)
455
  if cmd in aliases:
456
    if cmd in commands:
457
      raise errors.ProgrammerError("Alias '%s' overrides an existing"
458
                                   " command" % cmd)
459

    
460
    if aliases[cmd] not in commands:
461
      raise errors.ProgrammerError("Alias '%s' maps to non-existing"
462
                                   " command '%s'" % (cmd, aliases[cmd]))
463

    
464
    cmd = aliases[cmd]
465

    
466
  func, args_def, parser_opts, usage, description = commands[cmd]
467
  parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT],
468
                        description=description,
469
                        formatter=TitledHelpFormatter(),
470
                        usage="%%prog %s %s" % (cmd, usage))
471
  parser.disable_interspersed_args()
472
  options, args = parser.parse_args()
473

    
474
  if not _CheckArguments(cmd, args_def, args):
475
    return None, None, None
476

    
477
  return func, options, args
478

    
479

    
480
def _CheckArguments(cmd, args_def, args):
481
  """Verifies the arguments using the argument definition.
482

483
  Algorithm:
484

485
    1. Abort with error if values specified by user but none expected.
486

487
    1. For each argument in definition
488

489
      1. Keep running count of minimum number of values (min_count)
490
      1. Keep running count of maximum number of values (max_count)
491
      1. If it has an unlimited number of values
492

493
        1. Abort with error if it's not the last argument in the definition
494

495
    1. If last argument has limited number of values
496

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

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

501
  """
502
  if args and not args_def:
503
    ToStderr("Error: Command %s expects no arguments", cmd)
504
    return False
505

    
506
  min_count = None
507
  max_count = None
508
  check_max = None
509

    
510
  last_idx = len(args_def) - 1
511

    
512
  for idx, arg in enumerate(args_def):
513
    if min_count is None:
514
      min_count = arg.min
515
    elif arg.min is not None:
516
      min_count += arg.min
517

    
518
    if max_count is None:
519
      max_count = arg.max
520
    elif arg.max is not None:
521
      max_count += arg.max
522

    
523
    if idx == last_idx:
524
      check_max = (arg.max is not None)
525

    
526
    elif arg.max is None:
527
      raise errors.ProgrammerError("Only the last argument can have max=None")
528

    
529
  if check_max:
530
    # Command with exact number of arguments
531
    if (min_count is not None and max_count is not None and
532
        min_count == max_count and len(args) != min_count):
533
      ToStderr("Error: Command %s expects %d argument(s)", cmd, min_count)
534
      return False
535

    
536
    # Command with limited number of arguments
537
    if max_count is not None and len(args) > max_count:
538
      ToStderr("Error: Command %s expects only %d argument(s)",
539
               cmd, max_count)
540
      return False
541

    
542
  # Command with some required arguments
543
  if min_count is not None and len(args) < min_count:
544
    ToStderr("Error: Command %s expects at least %d argument(s)",
545
             cmd, min_count)
546
    return False
547

    
548
  return True
549

    
550

    
551
def SplitNodeOption(value):
552
  """Splits the value of a --node option.
553

554
  """
555
  if value and ':' in value:
556
    return value.split(':', 1)
557
  else:
558
    return (value, None)
559

    
560

    
561
def UsesRPC(fn):
562
  def wrapper(*args, **kwargs):
563
    rpc.Init()
564
    try:
565
      return fn(*args, **kwargs)
566
    finally:
567
      rpc.Shutdown()
568
  return wrapper
569

    
570

    
571
def AskUser(text, choices=None):
572
  """Ask the user a question.
573

574
  @param text: the question to ask
575

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

581
  @return: one of the return values from the choices list; if input is
582
      not possible (i.e. not running with a tty, we return the last
583
      entry from the list
584

585
  """
586
  if choices is None:
587
    choices = [('y', True, 'Perform the operation'),
588
               ('n', False, 'Do not perform the operation')]
589
  if not choices or not isinstance(choices, list):
590
    raise errors.ProgrammerError("Invalid choices argument to AskUser")
591
  for entry in choices:
592
    if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
593
      raise errors.ProgrammerError("Invalid choices element to AskUser")
594

    
595
  answer = choices[-1][1]
596
  new_text = []
597
  for line in text.splitlines():
598
    new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
599
  text = "\n".join(new_text)
600
  try:
601
    f = file("/dev/tty", "a+")
602
  except IOError:
603
    return answer
604
  try:
605
    chars = [entry[0] for entry in choices]
606
    chars[-1] = "[%s]" % chars[-1]
607
    chars.append('?')
608
    maps = dict([(entry[0], entry[1]) for entry in choices])
609
    while True:
610
      f.write(text)
611
      f.write('\n')
612
      f.write("/".join(chars))
613
      f.write(": ")
614
      line = f.readline(2).strip().lower()
615
      if line in maps:
616
        answer = maps[line]
617
        break
618
      elif line == '?':
619
        for entry in choices:
620
          f.write(" %s - %s\n" % (entry[0], entry[2]))
621
        f.write("\n")
622
        continue
623
  finally:
624
    f.close()
625
  return answer
626

    
627

    
628
class JobSubmittedException(Exception):
629
  """Job was submitted, client should exit.
630

631
  This exception has one argument, the ID of the job that was
632
  submitted. The handler should print this ID.
633

634
  This is not an error, just a structured way to exit from clients.
635

636
  """
637

    
638

    
639
def SendJob(ops, cl=None):
640
  """Function to submit an opcode without waiting for the results.
641

642
  @type ops: list
643
  @param ops: list of opcodes
644
  @type cl: luxi.Client
645
  @param cl: the luxi client to use for communicating with the master;
646
             if None, a new client will be created
647

648
  """
649
  if cl is None:
650
    cl = GetClient()
651

    
652
  job_id = cl.SubmitJob(ops)
653

    
654
  return job_id
655

    
656

    
657
def PollJob(job_id, cl=None, feedback_fn=None):
658
  """Function to poll for the result of a job.
659

660
  @type job_id: job identified
661
  @param job_id: the job to poll for results
662
  @type cl: luxi.Client
663
  @param cl: the luxi client to use for communicating with the master;
664
             if None, a new client will be created
665

666
  """
667
  if cl is None:
668
    cl = GetClient()
669

    
670
  prev_job_info = None
671
  prev_logmsg_serial = None
672

    
673
  while True:
674
    result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
675
                                 prev_logmsg_serial)
676
    if not result:
677
      # job not found, go away!
678
      raise errors.JobLost("Job with id %s lost" % job_id)
679

    
680
    # Split result, a tuple of (field values, log entries)
681
    (job_info, log_entries) = result
682
    (status, ) = job_info
683

    
684
    if log_entries:
685
      for log_entry in log_entries:
686
        (serial, timestamp, _, message) = log_entry
687
        if callable(feedback_fn):
688
          feedback_fn(log_entry[1:])
689
        else:
690
          encoded = utils.SafeEncode(message)
691
          ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)), encoded)
692
        prev_logmsg_serial = max(prev_logmsg_serial, serial)
693

    
694
    # TODO: Handle canceled and archived jobs
695
    elif status in (constants.JOB_STATUS_SUCCESS,
696
                    constants.JOB_STATUS_ERROR,
697
                    constants.JOB_STATUS_CANCELING,
698
                    constants.JOB_STATUS_CANCELED):
699
      break
700

    
701
    prev_job_info = job_info
702

    
703
  jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
704
  if not jobs:
705
    raise errors.JobLost("Job with id %s lost" % job_id)
706

    
707
  status, opstatus, result = jobs[0]
708
  if status == constants.JOB_STATUS_SUCCESS:
709
    return result
710
  elif status in (constants.JOB_STATUS_CANCELING,
711
                  constants.JOB_STATUS_CANCELED):
712
    raise errors.OpExecError("Job was canceled")
713
  else:
714
    has_ok = False
715
    for idx, (status, msg) in enumerate(zip(opstatus, result)):
716
      if status == constants.OP_STATUS_SUCCESS:
717
        has_ok = True
718
      elif status == constants.OP_STATUS_ERROR:
719
        errors.MaybeRaise(msg)
720
        if has_ok:
721
          raise errors.OpExecError("partial failure (opcode %d): %s" %
722
                                   (idx, msg))
723
        else:
724
          raise errors.OpExecError(str(msg))
725
    # default failure mode
726
    raise errors.OpExecError(result)
727

    
728

    
729
def SubmitOpCode(op, cl=None, feedback_fn=None):
730
  """Legacy function to submit an opcode.
731

732
  This is just a simple wrapper over the construction of the processor
733
  instance. It should be extended to better handle feedback and
734
  interaction functions.
735

736
  """
737
  if cl is None:
738
    cl = GetClient()
739

    
740
  job_id = SendJob([op], cl)
741

    
742
  op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
743

    
744
  return op_results[0]
745

    
746

    
747
def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
748
  """Wrapper around SubmitOpCode or SendJob.
749

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

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

757
  """
758
  if opts and opts.dry_run:
759
    op.dry_run = opts.dry_run
760
  if opts and opts.submit_only:
761
    job_id = SendJob([op], cl=cl)
762
    raise JobSubmittedException(job_id)
763
  else:
764
    return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
765

    
766

    
767
def GetClient():
768
  # TODO: Cache object?
769
  try:
770
    client = luxi.Client()
771
  except luxi.NoMasterError:
772
    master, myself = ssconf.GetMasterAndMyself()
773
    if master != myself:
774
      raise errors.OpPrereqError("This is not the master node, please connect"
775
                                 " to node '%s' and rerun the command" %
776
                                 master)
777
    else:
778
      raise
779
  return client
780

    
781

    
782
def FormatError(err):
783
  """Return a formatted error message for a given error.
784

785
  This function takes an exception instance and returns a tuple
786
  consisting of two values: first, the recommended exit code, and
787
  second, a string describing the error message (not
788
  newline-terminated).
789

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

    
853

    
854
def GenericMain(commands, override=None, aliases=None):
855
  """Generic main function for all the gnt-* commands.
856

857
  Arguments:
858
    - commands: a dictionary with a special structure, see the design doc
859
                for command line handling.
860
    - override: if not None, we expect a dictionary with keys that will
861
                override command line options; this can be used to pass
862
                options from the scripts to generic functions
863
    - aliases: dictionary with command aliases {'alias': 'target, ...}
864

865
  """
866
  # save the program name and the entire command line for later logging
867
  if sys.argv:
868
    binary = os.path.basename(sys.argv[0]) or sys.argv[0]
869
    if len(sys.argv) >= 2:
870
      binary += " " + sys.argv[1]
871
      old_cmdline = " ".join(sys.argv[2:])
872
    else:
873
      old_cmdline = ""
874
  else:
875
    binary = "<unknown program>"
876
    old_cmdline = ""
877

    
878
  if aliases is None:
879
    aliases = {}
880

    
881
  try:
882
    func, options, args = _ParseArgs(sys.argv, commands, aliases)
883
  except errors.ParameterError, err:
884
    result, err_msg = FormatError(err)
885
    ToStderr(err_msg)
886
    return 1
887

    
888
  if func is None: # parse error
889
    return 1
890

    
891
  if override is not None:
892
    for key, val in override.iteritems():
893
      setattr(options, key, val)
894

    
895
  utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
896
                     stderr_logging=True, program=binary)
897

    
898
  if old_cmdline:
899
    logging.info("run with arguments '%s'", old_cmdline)
900
  else:
901
    logging.info("run with no arguments")
902

    
903
  try:
904
    result = func(options, args)
905
  except (errors.GenericError, luxi.ProtocolError,
906
          JobSubmittedException), err:
907
    result, err_msg = FormatError(err)
908
    logging.exception("Error during command processing")
909
    ToStderr(err_msg)
910

    
911
  return result
912

    
913

    
914
def GenerateTable(headers, fields, separator, data,
915
                  numfields=None, unitfields=None,
916
                  units=None):
917
  """Prints a table with headers and different fields.
918

919
  @type headers: dict
920
  @param headers: dictionary mapping field names to headers for
921
      the table
922
  @type fields: list
923
  @param fields: the field names corresponding to each row in
924
      the data field
925
  @param separator: the separator to be used; if this is None,
926
      the default 'smart' algorithm is used which computes optimal
927
      field width, otherwise just the separator is used between
928
      each field
929
  @type data: list
930
  @param data: a list of lists, each sublist being one row to be output
931
  @type numfields: list
932
  @param numfields: a list with the fields that hold numeric
933
      values and thus should be right-aligned
934
  @type unitfields: list
935
  @param unitfields: a list with the fields that hold numeric
936
      values that should be formatted with the units field
937
  @type units: string or None
938
  @param units: the units we should use for formatting, or None for
939
      automatic choice (human-readable for non-separator usage, otherwise
940
      megabytes); this is a one-letter string
941

942
  """
943
  if units is None:
944
    if separator:
945
      units = "m"
946
    else:
947
      units = "h"
948

    
949
  if numfields is None:
950
    numfields = []
951
  if unitfields is None:
952
    unitfields = []
953

    
954
  numfields = utils.FieldSet(*numfields)
955
  unitfields = utils.FieldSet(*unitfields)
956

    
957
  format_fields = []
958
  for field in fields:
959
    if headers and field not in headers:
960
      # TODO: handle better unknown fields (either revert to old
961
      # style of raising exception, or deal more intelligently with
962
      # variable fields)
963
      headers[field] = field
964
    if separator is not None:
965
      format_fields.append("%s")
966
    elif numfields.Matches(field):
967
      format_fields.append("%*s")
968
    else:
969
      format_fields.append("%-*s")
970

    
971
  if separator is None:
972
    mlens = [0 for name in fields]
973
    format = ' '.join(format_fields)
974
  else:
975
    format = separator.replace("%", "%%").join(format_fields)
976

    
977
  for row in data:
978
    if row is None:
979
      continue
980
    for idx, val in enumerate(row):
981
      if unitfields.Matches(fields[idx]):
982
        try:
983
          val = int(val)
984
        except ValueError:
985
          pass
986
        else:
987
          val = row[idx] = utils.FormatUnit(val, units)
988
      val = row[idx] = str(val)
989
      if separator is None:
990
        mlens[idx] = max(mlens[idx], len(val))
991

    
992
  result = []
993
  if headers:
994
    args = []
995
    for idx, name in enumerate(fields):
996
      hdr = headers[name]
997
      if separator is None:
998
        mlens[idx] = max(mlens[idx], len(hdr))
999
        args.append(mlens[idx])
1000
      args.append(hdr)
1001
    result.append(format % tuple(args))
1002

    
1003
  for line in data:
1004
    args = []
1005
    if line is None:
1006
      line = ['-' for _ in fields]
1007
    for idx in xrange(len(fields)):
1008
      if separator is None:
1009
        args.append(mlens[idx])
1010
      args.append(line[idx])
1011
    result.append(format % tuple(args))
1012

    
1013
  return result
1014

    
1015

    
1016
def FormatTimestamp(ts):
1017
  """Formats a given timestamp.
1018

1019
  @type ts: timestamp
1020
  @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
1021

1022
  @rtype: string
1023
  @return: a string with the formatted timestamp
1024

1025
  """
1026
  if not isinstance (ts, (tuple, list)) or len(ts) != 2:
1027
    return '?'
1028
  sec, usec = ts
1029
  return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
1030

    
1031

    
1032
def ParseTimespec(value):
1033
  """Parse a time specification.
1034

1035
  The following suffixed will be recognized:
1036

1037
    - s: seconds
1038
    - m: minutes
1039
    - h: hours
1040
    - d: day
1041
    - w: weeks
1042

1043
  Without any suffix, the value will be taken to be in seconds.
1044

1045
  """
1046
  value = str(value)
1047
  if not value:
1048
    raise errors.OpPrereqError("Empty time specification passed")
1049
  suffix_map = {
1050
    's': 1,
1051
    'm': 60,
1052
    'h': 3600,
1053
    'd': 86400,
1054
    'w': 604800,
1055
    }
1056
  if value[-1] not in suffix_map:
1057
    try:
1058
      value = int(value)
1059
    except ValueError:
1060
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1061
  else:
1062
    multiplier = suffix_map[value[-1]]
1063
    value = value[:-1]
1064
    if not value: # no data left after stripping the suffix
1065
      raise errors.OpPrereqError("Invalid time specification (only"
1066
                                 " suffix passed)")
1067
    try:
1068
      value = int(value) * multiplier
1069
    except ValueError:
1070
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1071
  return value
1072

    
1073

    
1074
def GetOnlineNodes(nodes, cl=None, nowarn=False):
1075
  """Returns the names of online nodes.
1076

1077
  This function will also log a warning on stderr with the names of
1078
  the online nodes.
1079

1080
  @param nodes: if not empty, use only this subset of nodes (minus the
1081
      offline ones)
1082
  @param cl: if not None, luxi client to use
1083
  @type nowarn: boolean
1084
  @param nowarn: by default, this function will output a note with the
1085
      offline nodes that are skipped; if this parameter is True the
1086
      note is not displayed
1087

1088
  """
1089
  if cl is None:
1090
    cl = GetClient()
1091

    
1092
  result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
1093
                         use_locking=False)
1094
  offline = [row[0] for row in result if row[1]]
1095
  if offline and not nowarn:
1096
    ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
1097
  return [row[0] for row in result if not row[1]]
1098

    
1099

    
1100
def _ToStream(stream, txt, *args):
1101
  """Write a message to a stream, bypassing the logging system
1102

1103
  @type stream: file object
1104
  @param stream: the file to which we should write
1105
  @type txt: str
1106
  @param txt: the message
1107

1108
  """
1109
  if args:
1110
    args = tuple(args)
1111
    stream.write(txt % args)
1112
  else:
1113
    stream.write(txt)
1114
  stream.write('\n')
1115
  stream.flush()
1116

    
1117

    
1118
def ToStdout(txt, *args):
1119
  """Write a message to stdout only, bypassing the logging system
1120

1121
  This is just a wrapper over _ToStream.
1122

1123
  @type txt: str
1124
  @param txt: the message
1125

1126
  """
1127
  _ToStream(sys.stdout, txt, *args)
1128

    
1129

    
1130
def ToStderr(txt, *args):
1131
  """Write a message to stderr only, bypassing the logging system
1132

1133
  This is just a wrapper over _ToStream.
1134

1135
  @type txt: str
1136
  @param txt: the message
1137

1138
  """
1139
  _ToStream(sys.stderr, txt, *args)
1140

    
1141

    
1142
class JobExecutor(object):
1143
  """Class which manages the submission and execution of multiple jobs.
1144

1145
  Note that instances of this class should not be reused between
1146
  GetResults() calls.
1147

1148
  """
1149
  def __init__(self, cl=None, verbose=True):
1150
    self.queue = []
1151
    if cl is None:
1152
      cl = GetClient()
1153
    self.cl = cl
1154
    self.verbose = verbose
1155
    self.jobs = []
1156

    
1157
  def QueueJob(self, name, *ops):
1158
    """Record a job for later submit.
1159

1160
    @type name: string
1161
    @param name: a description of the job, will be used in WaitJobSet
1162
    """
1163
    self.queue.append((name, ops))
1164

    
1165
  def SubmitPending(self):
1166
    """Submit all pending jobs.
1167

1168
    """
1169
    results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1170
    for ((status, data), (name, _)) in zip(results, self.queue):
1171
      self.jobs.append((status, data, name))
1172

    
1173
  def GetResults(self):
1174
    """Wait for and return the results of all jobs.
1175

1176
    @rtype: list
1177
    @return: list of tuples (success, job results), in the same order
1178
        as the submitted jobs; if a job has failed, instead of the result
1179
        there will be the error message
1180

1181
    """
1182
    if not self.jobs:
1183
      self.SubmitPending()
1184
    results = []
1185
    if self.verbose:
1186
      ok_jobs = [row[1] for row in self.jobs if row[0]]
1187
      if ok_jobs:
1188
        ToStdout("Submitted jobs %s", ", ".join(ok_jobs))
1189
    for submit_status, jid, name in self.jobs:
1190
      if not submit_status:
1191
        ToStderr("Failed to submit job for %s: %s", name, jid)
1192
        results.append((False, jid))
1193
        continue
1194
      if self.verbose:
1195
        ToStdout("Waiting for job %s for %s...", jid, name)
1196
      try:
1197
        job_result = PollJob(jid, cl=self.cl)
1198
        success = True
1199
      except (errors.GenericError, luxi.ProtocolError), err:
1200
        _, job_result = FormatError(err)
1201
        success = False
1202
        # the error message will always be shown, verbose or not
1203
        ToStderr("Job %s for %s has failed: %s", jid, name, job_result)
1204

    
1205
      results.append((success, job_result))
1206
    return results
1207

    
1208
  def WaitOrShow(self, wait):
1209
    """Wait for job results or only print the job IDs.
1210

1211
    @type wait: boolean
1212
    @param wait: whether to wait or not
1213

1214
    """
1215
    if wait:
1216
      return self.GetResults()
1217
    else:
1218
      if not self.jobs:
1219
        self.SubmitPending()
1220
      for status, result, name in self.jobs:
1221
        if status:
1222
          ToStdout("%s: %s", result, name)
1223
        else:
1224
          ToStderr("Failure for %s: %s", name, result)