Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ 087ed2ed

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

    
112
NO_PREFIX = "no_"
113
UN_PREFIX = "-"
114

    
115

    
116
class _Argument:
117
  def __init__(self, min=0, max=None):
118
    self.min = min
119
    self.max = max
120

    
121
  def __repr__(self):
122
    return ("<%s min=%s max=%s>" %
123
            (self.__class__.__name__, self.min, self.max))
124

    
125

    
126
class ArgSuggest(_Argument):
127
  """Suggesting argument.
128

129
  Value can be any of the ones passed to the constructor.
130

131
  """
132
  def __init__(self, min=0, max=None, choices=None):
133
    _Argument.__init__(self, min=min, max=max)
134
    self.choices = choices
135

    
136
  def __repr__(self):
137
    return ("<%s min=%s max=%s choices=%r>" %
138
            (self.__class__.__name__, self.min, self.max, self.choices))
139

    
140

    
141
class ArgChoice(ArgSuggest):
142
  """Choice argument.
143

144
  Value can be any of the ones passed to the constructor. Like L{ArgSuggest},
145
  but value must be one of the choices.
146

147
  """
148

    
149

    
150
class ArgUnknown(_Argument):
151
  """Unknown argument to program (e.g. determined at runtime).
152

153
  """
154

    
155

    
156
class ArgInstance(_Argument):
157
  """Instances argument.
158

159
  """
160

    
161

    
162
class ArgNode(_Argument):
163
  """Node argument.
164

165
  """
166

    
167
class ArgJobId(_Argument):
168
  """Job ID argument.
169

170
  """
171

    
172

    
173
class ArgFile(_Argument):
174
  """File path argument.
175

176
  """
177

    
178

    
179
class ArgCommand(_Argument):
180
  """Command argument.
181

182
  """
183

    
184

    
185
class ArgHost(_Argument):
186
  """Host argument.
187

188
  """
189

    
190

    
191
ARGS_NONE = []
192
ARGS_MANY_INSTANCES = [ArgInstance()]
193
ARGS_MANY_NODES = [ArgNode()]
194
ARGS_ONE_INSTANCE = [ArgInstance(min=1, max=1)]
195
ARGS_ONE_NODE = [ArgNode(min=1, max=1)]
196

    
197

    
198

    
199
def _ExtractTagsObject(opts, args):
200
  """Extract the tag type object.
201

202
  Note that this function will modify its args parameter.
203

204
  """
205
  if not hasattr(opts, "tag_type"):
206
    raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
207
  kind = opts.tag_type
208
  if kind == constants.TAG_CLUSTER:
209
    retval = kind, kind
210
  elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
211
    if not args:
212
      raise errors.OpPrereqError("no arguments passed to the command")
213
    name = args.pop(0)
214
    retval = kind, name
215
  else:
216
    raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
217
  return retval
218

    
219

    
220
def _ExtendTags(opts, args):
221
  """Extend the args if a source file has been given.
222

223
  This function will extend the tags with the contents of the file
224
  passed in the 'tags_source' attribute of the opts parameter. A file
225
  named '-' will be replaced by stdin.
226

227
  """
228
  fname = opts.tags_source
229
  if fname is None:
230
    return
231
  if fname == "-":
232
    new_fh = sys.stdin
233
  else:
234
    new_fh = open(fname, "r")
235
  new_data = []
236
  try:
237
    # we don't use the nice 'new_data = [line.strip() for line in fh]'
238
    # because of python bug 1633941
239
    while True:
240
      line = new_fh.readline()
241
      if not line:
242
        break
243
      new_data.append(line.strip())
244
  finally:
245
    new_fh.close()
246
  args.extend(new_data)
247

    
248

    
249
def ListTags(opts, args):
250
  """List the tags on a given object.
251

252
  This is a generic implementation that knows how to deal with all
253
  three cases of tag objects (cluster, node, instance). The opts
254
  argument is expected to contain a tag_type field denoting what
255
  object type we work on.
256

257
  """
258
  kind, name = _ExtractTagsObject(opts, args)
259
  op = opcodes.OpGetTags(kind=kind, name=name)
260
  result = SubmitOpCode(op)
261
  result = list(result)
262
  result.sort()
263
  for tag in result:
264
    ToStdout(tag)
265

    
266

    
267
def AddTags(opts, args):
268
  """Add tags on a given object.
269

270
  This is a generic implementation that knows how to deal with all
271
  three cases of tag objects (cluster, node, instance). The opts
272
  argument is expected to contain a tag_type field denoting what
273
  object type we work on.
274

275
  """
276
  kind, name = _ExtractTagsObject(opts, args)
277
  _ExtendTags(opts, args)
278
  if not args:
279
    raise errors.OpPrereqError("No tags to be added")
280
  op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
281
  SubmitOpCode(op)
282

    
283

    
284
def RemoveTags(opts, args):
285
  """Remove tags from a given object.
286

287
  This is a generic implementation that knows how to deal with all
288
  three cases of tag objects (cluster, node, instance). The opts
289
  argument is expected to contain a tag_type field denoting what
290
  object type we work on.
291

292
  """
293
  kind, name = _ExtractTagsObject(opts, args)
294
  _ExtendTags(opts, args)
295
  if not args:
296
    raise errors.OpPrereqError("No tags to be removed")
297
  op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
298
  SubmitOpCode(op)
299

    
300

    
301
def check_unit(option, opt, value):
302
  """OptParsers custom converter for units.
303

304
  """
305
  try:
306
    return utils.ParseUnit(value)
307
  except errors.UnitParseError, err:
308
    raise OptionValueError("option %s: %s" % (opt, err))
309

    
310

    
311
def _SplitKeyVal(opt, data):
312
  """Convert a KeyVal string into a dict.
313

314
  This function will convert a key=val[,...] string into a dict. Empty
315
  values will be converted specially: keys which have the prefix 'no_'
316
  will have the value=False and the prefix stripped, the others will
317
  have value=True.
318

319
  @type opt: string
320
  @param opt: a string holding the option name for which we process the
321
      data, used in building error messages
322
  @type data: string
323
  @param data: a string of the format key=val,key=val,...
324
  @rtype: dict
325
  @return: {key=val, key=val}
326
  @raises errors.ParameterError: if there are duplicate keys
327

328
  """
329
  kv_dict = {}
330
  if data:
331
    for elem in data.split(","):
332
      if "=" in elem:
333
        key, val = elem.split("=", 1)
334
      else:
335
        if elem.startswith(NO_PREFIX):
336
          key, val = elem[len(NO_PREFIX):], False
337
        elif elem.startswith(UN_PREFIX):
338
          key, val = elem[len(UN_PREFIX):], None
339
        else:
340
          key, val = elem, True
341
      if key in kv_dict:
342
        raise errors.ParameterError("Duplicate key '%s' in option %s" %
343
                                    (key, opt))
344
      kv_dict[key] = val
345
  return kv_dict
346

    
347

    
348
def check_ident_key_val(option, opt, value):
349
  """Custom parser for ident:key=val,key=val options.
350

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

354
  """
355
  if ":" not in value:
356
    ident, rest = value, ''
357
  else:
358
    ident, rest = value.split(":", 1)
359

    
360
  if ident.startswith(NO_PREFIX):
361
    if rest:
362
      msg = "Cannot pass options when removing parameter groups: %s" % value
363
      raise errors.ParameterError(msg)
364
    retval = (ident[len(NO_PREFIX):], False)
365
  elif ident.startswith(UN_PREFIX):
366
    if rest:
367
      msg = "Cannot pass options when removing parameter groups: %s" % value
368
      raise errors.ParameterError(msg)
369
    retval = (ident[len(UN_PREFIX):], None)
370
  else:
371
    kv_dict = _SplitKeyVal(opt, rest)
372
    retval = (ident, kv_dict)
373
  return retval
374

    
375

    
376
def check_key_val(option, opt, value):
377
  """Custom parser class for key=val,key=val options.
378

379
  This will store the parsed values as a dict {key: val}.
380

381
  """
382
  return _SplitKeyVal(opt, value)
383

    
384

    
385
# completion_suggestion is normally a list. Using numeric values not evaluating
386
# to False for dynamic completion.
387
(OPT_COMPL_MANY_NODES,
388
 OPT_COMPL_ONE_NODE,
389
 OPT_COMPL_ONE_INSTANCE,
390
 OPT_COMPL_ONE_OS,
391
 OPT_COMPL_ONE_IALLOCATOR,
392
 OPT_COMPL_INST_ADD_NODES) = range(100, 106)
393

    
394
OPT_COMPL_ALL = frozenset([
395
  OPT_COMPL_MANY_NODES,
396
  OPT_COMPL_ONE_NODE,
397
  OPT_COMPL_ONE_INSTANCE,
398
  OPT_COMPL_ONE_OS,
399
  OPT_COMPL_ONE_IALLOCATOR,
400
  OPT_COMPL_INST_ADD_NODES,
401
  ])
402

    
403

    
404
class CliOption(Option):
405
  """Custom option class for optparse.
406

407
  """
408
  ATTRS = Option.ATTRS + [
409
    "completion_suggest",
410
    ]
411
  TYPES = Option.TYPES + (
412
    "identkeyval",
413
    "keyval",
414
    "unit",
415
    )
416
  TYPE_CHECKER = Option.TYPE_CHECKER.copy()
417
  TYPE_CHECKER["identkeyval"] = check_ident_key_val
418
  TYPE_CHECKER["keyval"] = check_key_val
419
  TYPE_CHECKER["unit"] = check_unit
420

    
421

    
422
# optparse.py sets make_option, so we do it for our own option class, too
423
cli_option = CliOption
424

    
425

    
426
DEBUG_OPT = cli_option("-d", "--debug", default=False,
427
                       action="store_true",
428
                       help="Turn debugging on")
429

    
430
NOHDR_OPT = cli_option("--no-headers", default=False,
431
                       action="store_true", dest="no_headers",
432
                       help="Don't display column headers")
433

    
434
SEP_OPT = cli_option("--separator", default=None,
435
                     action="store", dest="separator",
436
                     help=("Separator between output fields"
437
                           " (defaults to one space)"))
438

    
439
USEUNITS_OPT = cli_option("--units", default=None,
440
                          dest="units", choices=('h', 'm', 'g', 't'),
441
                          help="Specify units for output (one of hmgt)")
442

    
443
FIELDS_OPT = cli_option("-o", "--output", dest="output", action="store",
444
                        type="string", metavar="FIELDS",
445
                        help="Comma separated list of output fields")
446

    
447
FORCE_OPT = cli_option("-f", "--force", dest="force", action="store_true",
448
                       default=False, help="Force the operation")
449

    
450
CONFIRM_OPT = cli_option("--yes", dest="confirm", action="store_true",
451
                         default=False, help="Do not require confirmation")
452

    
453
TAG_SRC_OPT = cli_option("--from", dest="tags_source",
454
                         default=None, help="File with tag names")
455

    
456
SUBMIT_OPT = cli_option("--submit", dest="submit_only",
457
                        default=False, action="store_true",
458
                        help=("Submit the job and return the job ID, but"
459
                              " don't wait for the job to finish"))
460

    
461
SYNC_OPT = cli_option("--sync", dest="do_locking",
462
                      default=False, action="store_true",
463
                      help=("Grab locks while doing the queries"
464
                            " in order to ensure more consistent results"))
465

    
466
_DRY_RUN_OPT = cli_option("--dry-run", default=False,
467
                          action="store_true",
468
                          help=("Do not execute the operation, just run the"
469
                                " check steps and verify it it could be"
470
                                " executed"))
471

    
472
VERBOSE_OPT = cli_option("-v", "--verbose", default=False,
473
                         action="store_true",
474
                         help="Increase the verbosity of the operation")
475

    
476
DEBUG_SIMERR_OPT = cli_option("--debug-simulate-errors", default=False,
477
                              action="store_true", dest="simulate_errors",
478
                              help="Debugging option that makes the operation"
479
                              " treat most runtime checks as failed")
480

    
481
NWSYNC_OPT = cli_option("--no-wait-for-sync", dest="wait_for_sync",
482
                        default=True, action="store_false",
483
                        help="Don't wait for sync (DANGEROUS!)")
484

    
485
DISK_TEMPLATE_OPT = cli_option("-t", "--disk-template", dest="disk_template",
486
                               help="Custom disk setup (diskless, file,"
487
                               " plain or drbd)",
488
                               default=None, metavar="TEMPL",
489
                               choices=list(constants.DISK_TEMPLATES))
490

    
491
NONICS_OPT = cli_option("--no-nics", default=False, action="store_true",
492
                        help="Do not create any network cards for"
493
                        " the instance")
494

    
495
FILESTORE_DIR_OPT = cli_option("--file-storage-dir", dest="file_storage_dir",
496
                               help="Relative path under default cluster-wide"
497
                               " file storage dir to store file-based disks",
498
                               default=None, metavar="<DIR>")
499

    
500
FILESTORE_DRIVER_OPT = cli_option("--file-driver", dest="file_driver",
501
                                  help="Driver to use for image files",
502
                                  default="loop", metavar="<DRIVER>",
503
                                  choices=list(constants.FILE_DRIVER))
504

    
505
IALLOCATOR_OPT = cli_option("-I", "--iallocator", metavar="<NAME>",
506
                            help="Select nodes for the instance automatically"
507
                            " using the <NAME> iallocator plugin",
508
                            default=None, type="string",
509
                            completion_suggest=OPT_COMPL_ONE_IALLOCATOR)
510

    
511
OS_OPT = cli_option("-o", "--os-type", dest="os", help="What OS to run",
512
                    metavar="<os>",
513
                    completion_suggest=OPT_COMPL_ONE_OS)
514

    
515
BACKEND_OPT = cli_option("-B", "--backend-parameters", dest="beparams",
516
                         type="keyval", default={},
517
                         help="Backend parameters")
518

    
519

    
520
def _ParseArgs(argv, commands, aliases):
521
  """Parser for the command line arguments.
522

523
  This function parses the arguments and returns the function which
524
  must be executed together with its (modified) arguments.
525

526
  @param argv: the command line
527
  @param commands: dictionary with special contents, see the design
528
      doc for cmdline handling
529
  @param aliases: dictionary with command aliases {'alias': 'target, ...}
530

531
  """
532
  if len(argv) == 0:
533
    binary = "<command>"
534
  else:
535
    binary = argv[0].split("/")[-1]
536

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

    
543
  if len(argv) < 2 or not (argv[1] in commands or
544
                           argv[1] in aliases):
545
    # let's do a nice thing
546
    sortedcmds = commands.keys()
547
    sortedcmds.sort()
548

    
549
    ToStdout("Usage: %s {command} [options...] [argument...]", binary)
550
    ToStdout("%s <command> --help to see details, or man %s", binary, binary)
551
    ToStdout("")
552

    
553
    # compute the max line length for cmd + usage
554
    mlen = max([len(" %s" % cmd) for cmd in commands])
555
    mlen = min(60, mlen) # should not get here...
556

    
557
    # and format a nice command list
558
    ToStdout("Commands:")
559
    for cmd in sortedcmds:
560
      cmdstr = " %s" % (cmd,)
561
      help_text = commands[cmd][4]
562
      help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
563
      ToStdout("%-*s - %s", mlen, cmdstr, help_lines.pop(0))
564
      for line in help_lines:
565
        ToStdout("%-*s   %s", mlen, "", line)
566

    
567
    ToStdout("")
568

    
569
    return None, None, None
570

    
571
  # get command, unalias it, and look it up in commands
572
  cmd = argv.pop(1)
573
  if cmd in aliases:
574
    if cmd in commands:
575
      raise errors.ProgrammerError("Alias '%s' overrides an existing"
576
                                   " command" % cmd)
577

    
578
    if aliases[cmd] not in commands:
579
      raise errors.ProgrammerError("Alias '%s' maps to non-existing"
580
                                   " command '%s'" % (cmd, aliases[cmd]))
581

    
582
    cmd = aliases[cmd]
583

    
584
  func, args_def, parser_opts, usage, description = commands[cmd]
585
  parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT],
586
                        description=description,
587
                        formatter=TitledHelpFormatter(),
588
                        usage="%%prog %s %s" % (cmd, usage))
589
  parser.disable_interspersed_args()
590
  options, args = parser.parse_args()
591

    
592
  if not _CheckArguments(cmd, args_def, args):
593
    return None, None, None
594

    
595
  return func, options, args
596

    
597

    
598
def _CheckArguments(cmd, args_def, args):
599
  """Verifies the arguments using the argument definition.
600

601
  Algorithm:
602

603
    1. Abort with error if values specified by user but none expected.
604

605
    1. For each argument in definition
606

607
      1. Keep running count of minimum number of values (min_count)
608
      1. Keep running count of maximum number of values (max_count)
609
      1. If it has an unlimited number of values
610

611
        1. Abort with error if it's not the last argument in the definition
612

613
    1. If last argument has limited number of values
614

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

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

619
  """
620
  if args and not args_def:
621
    ToStderr("Error: Command %s expects no arguments", cmd)
622
    return False
623

    
624
  min_count = None
625
  max_count = None
626
  check_max = None
627

    
628
  last_idx = len(args_def) - 1
629

    
630
  for idx, arg in enumerate(args_def):
631
    if min_count is None:
632
      min_count = arg.min
633
    elif arg.min is not None:
634
      min_count += arg.min
635

    
636
    if max_count is None:
637
      max_count = arg.max
638
    elif arg.max is not None:
639
      max_count += arg.max
640

    
641
    if idx == last_idx:
642
      check_max = (arg.max is not None)
643

    
644
    elif arg.max is None:
645
      raise errors.ProgrammerError("Only the last argument can have max=None")
646

    
647
  if check_max:
648
    # Command with exact number of arguments
649
    if (min_count is not None and max_count is not None and
650
        min_count == max_count and len(args) != min_count):
651
      ToStderr("Error: Command %s expects %d argument(s)", cmd, min_count)
652
      return False
653

    
654
    # Command with limited number of arguments
655
    if max_count is not None and len(args) > max_count:
656
      ToStderr("Error: Command %s expects only %d argument(s)",
657
               cmd, max_count)
658
      return False
659

    
660
  # Command with some required arguments
661
  if min_count is not None and len(args) < min_count:
662
    ToStderr("Error: Command %s expects at least %d argument(s)",
663
             cmd, min_count)
664
    return False
665

    
666
  return True
667

    
668

    
669
def SplitNodeOption(value):
670
  """Splits the value of a --node option.
671

672
  """
673
  if value and ':' in value:
674
    return value.split(':', 1)
675
  else:
676
    return (value, None)
677

    
678

    
679
def UsesRPC(fn):
680
  def wrapper(*args, **kwargs):
681
    rpc.Init()
682
    try:
683
      return fn(*args, **kwargs)
684
    finally:
685
      rpc.Shutdown()
686
  return wrapper
687

    
688

    
689
def AskUser(text, choices=None):
690
  """Ask the user a question.
691

692
  @param text: the question to ask
693

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

699
  @return: one of the return values from the choices list; if input is
700
      not possible (i.e. not running with a tty, we return the last
701
      entry from the list
702

703
  """
704
  if choices is None:
705
    choices = [('y', True, 'Perform the operation'),
706
               ('n', False, 'Do not perform the operation')]
707
  if not choices or not isinstance(choices, list):
708
    raise errors.ProgrammerError("Invalid choices argument to AskUser")
709
  for entry in choices:
710
    if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
711
      raise errors.ProgrammerError("Invalid choices element to AskUser")
712

    
713
  answer = choices[-1][1]
714
  new_text = []
715
  for line in text.splitlines():
716
    new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
717
  text = "\n".join(new_text)
718
  try:
719
    f = file("/dev/tty", "a+")
720
  except IOError:
721
    return answer
722
  try:
723
    chars = [entry[0] for entry in choices]
724
    chars[-1] = "[%s]" % chars[-1]
725
    chars.append('?')
726
    maps = dict([(entry[0], entry[1]) for entry in choices])
727
    while True:
728
      f.write(text)
729
      f.write('\n')
730
      f.write("/".join(chars))
731
      f.write(": ")
732
      line = f.readline(2).strip().lower()
733
      if line in maps:
734
        answer = maps[line]
735
        break
736
      elif line == '?':
737
        for entry in choices:
738
          f.write(" %s - %s\n" % (entry[0], entry[2]))
739
        f.write("\n")
740
        continue
741
  finally:
742
    f.close()
743
  return answer
744

    
745

    
746
class JobSubmittedException(Exception):
747
  """Job was submitted, client should exit.
748

749
  This exception has one argument, the ID of the job that was
750
  submitted. The handler should print this ID.
751

752
  This is not an error, just a structured way to exit from clients.
753

754
  """
755

    
756

    
757
def SendJob(ops, cl=None):
758
  """Function to submit an opcode without waiting for the results.
759

760
  @type ops: list
761
  @param ops: list of opcodes
762
  @type cl: luxi.Client
763
  @param cl: the luxi client to use for communicating with the master;
764
             if None, a new client will be created
765

766
  """
767
  if cl is None:
768
    cl = GetClient()
769

    
770
  job_id = cl.SubmitJob(ops)
771

    
772
  return job_id
773

    
774

    
775
def PollJob(job_id, cl=None, feedback_fn=None):
776
  """Function to poll for the result of a job.
777

778
  @type job_id: job identified
779
  @param job_id: the job to poll for results
780
  @type cl: luxi.Client
781
  @param cl: the luxi client to use for communicating with the master;
782
             if None, a new client will be created
783

784
  """
785
  if cl is None:
786
    cl = GetClient()
787

    
788
  prev_job_info = None
789
  prev_logmsg_serial = None
790

    
791
  while True:
792
    result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
793
                                 prev_logmsg_serial)
794
    if not result:
795
      # job not found, go away!
796
      raise errors.JobLost("Job with id %s lost" % job_id)
797

    
798
    # Split result, a tuple of (field values, log entries)
799
    (job_info, log_entries) = result
800
    (status, ) = job_info
801

    
802
    if log_entries:
803
      for log_entry in log_entries:
804
        (serial, timestamp, _, message) = log_entry
805
        if callable(feedback_fn):
806
          feedback_fn(log_entry[1:])
807
        else:
808
          encoded = utils.SafeEncode(message)
809
          ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)), encoded)
810
        prev_logmsg_serial = max(prev_logmsg_serial, serial)
811

    
812
    # TODO: Handle canceled and archived jobs
813
    elif status in (constants.JOB_STATUS_SUCCESS,
814
                    constants.JOB_STATUS_ERROR,
815
                    constants.JOB_STATUS_CANCELING,
816
                    constants.JOB_STATUS_CANCELED):
817
      break
818

    
819
    prev_job_info = job_info
820

    
821
  jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
822
  if not jobs:
823
    raise errors.JobLost("Job with id %s lost" % job_id)
824

    
825
  status, opstatus, result = jobs[0]
826
  if status == constants.JOB_STATUS_SUCCESS:
827
    return result
828
  elif status in (constants.JOB_STATUS_CANCELING,
829
                  constants.JOB_STATUS_CANCELED):
830
    raise errors.OpExecError("Job was canceled")
831
  else:
832
    has_ok = False
833
    for idx, (status, msg) in enumerate(zip(opstatus, result)):
834
      if status == constants.OP_STATUS_SUCCESS:
835
        has_ok = True
836
      elif status == constants.OP_STATUS_ERROR:
837
        errors.MaybeRaise(msg)
838
        if has_ok:
839
          raise errors.OpExecError("partial failure (opcode %d): %s" %
840
                                   (idx, msg))
841
        else:
842
          raise errors.OpExecError(str(msg))
843
    # default failure mode
844
    raise errors.OpExecError(result)
845

    
846

    
847
def SubmitOpCode(op, cl=None, feedback_fn=None):
848
  """Legacy function to submit an opcode.
849

850
  This is just a simple wrapper over the construction of the processor
851
  instance. It should be extended to better handle feedback and
852
  interaction functions.
853

854
  """
855
  if cl is None:
856
    cl = GetClient()
857

    
858
  job_id = SendJob([op], cl)
859

    
860
  op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
861

    
862
  return op_results[0]
863

    
864

    
865
def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
866
  """Wrapper around SubmitOpCode or SendJob.
867

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

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

875
  """
876
  if opts and opts.dry_run:
877
    op.dry_run = opts.dry_run
878
  if opts and opts.submit_only:
879
    job_id = SendJob([op], cl=cl)
880
    raise JobSubmittedException(job_id)
881
  else:
882
    return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
883

    
884

    
885
def GetClient():
886
  # TODO: Cache object?
887
  try:
888
    client = luxi.Client()
889
  except luxi.NoMasterError:
890
    master, myself = ssconf.GetMasterAndMyself()
891
    if master != myself:
892
      raise errors.OpPrereqError("This is not the master node, please connect"
893
                                 " to node '%s' and rerun the command" %
894
                                 master)
895
    else:
896
      raise
897
  return client
898

    
899

    
900
def FormatError(err):
901
  """Return a formatted error message for a given error.
902

903
  This function takes an exception instance and returns a tuple
904
  consisting of two values: first, the recommended exit code, and
905
  second, a string describing the error message (not
906
  newline-terminated).
907

908
  """
909
  retcode = 1
910
  obuf = StringIO()
911
  msg = str(err)
912
  if isinstance(err, errors.ConfigurationError):
913
    txt = "Corrupt configuration file: %s" % msg
914
    logging.error(txt)
915
    obuf.write(txt + "\n")
916
    obuf.write("Aborting.")
917
    retcode = 2
918
  elif isinstance(err, errors.HooksAbort):
919
    obuf.write("Failure: hooks execution failed:\n")
920
    for node, script, out in err.args[0]:
921
      if out:
922
        obuf.write("  node: %s, script: %s, output: %s\n" %
923
                   (node, script, out))
924
      else:
925
        obuf.write("  node: %s, script: %s (no output)\n" %
926
                   (node, script))
927
  elif isinstance(err, errors.HooksFailure):
928
    obuf.write("Failure: hooks general failure: %s" % msg)
929
  elif isinstance(err, errors.ResolverError):
930
    this_host = utils.HostInfo.SysName()
931
    if err.args[0] == this_host:
932
      msg = "Failure: can't resolve my own hostname ('%s')"
933
    else:
934
      msg = "Failure: can't resolve hostname '%s'"
935
    obuf.write(msg % err.args[0])
936
  elif isinstance(err, errors.OpPrereqError):
937
    obuf.write("Failure: prerequisites not met for this"
938
               " operation:\n%s" % msg)
939
  elif isinstance(err, errors.OpExecError):
940
    obuf.write("Failure: command execution error:\n%s" % msg)
941
  elif isinstance(err, errors.TagError):
942
    obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
943
  elif isinstance(err, errors.JobQueueDrainError):
944
    obuf.write("Failure: the job queue is marked for drain and doesn't"
945
               " accept new requests\n")
946
  elif isinstance(err, errors.JobQueueFull):
947
    obuf.write("Failure: the job queue is full and doesn't accept new"
948
               " job submissions until old jobs are archived\n")
949
  elif isinstance(err, errors.TypeEnforcementError):
950
    obuf.write("Parameter Error: %s" % msg)
951
  elif isinstance(err, errors.ParameterError):
952
    obuf.write("Failure: unknown/wrong parameter name '%s'" % msg)
953
  elif isinstance(err, errors.GenericError):
954
    obuf.write("Unhandled Ganeti error: %s" % msg)
955
  elif isinstance(err, luxi.NoMasterError):
956
    obuf.write("Cannot communicate with the master daemon.\nIs it running"
957
               " and listening for connections?")
958
  elif isinstance(err, luxi.TimeoutError):
959
    obuf.write("Timeout while talking to the master daemon. Error:\n"
960
               "%s" % msg)
961
  elif isinstance(err, luxi.ProtocolError):
962
    obuf.write("Unhandled protocol error while talking to the master daemon:\n"
963
               "%s" % msg)
964
  elif isinstance(err, JobSubmittedException):
965
    obuf.write("JobID: %s\n" % err.args[0])
966
    retcode = 0
967
  else:
968
    obuf.write("Unhandled exception: %s" % msg)
969
  return retcode, obuf.getvalue().rstrip('\n')
970

    
971

    
972
def GenericMain(commands, override=None, aliases=None):
973
  """Generic main function for all the gnt-* commands.
974

975
  Arguments:
976
    - commands: a dictionary with a special structure, see the design doc
977
                for command line handling.
978
    - override: if not None, we expect a dictionary with keys that will
979
                override command line options; this can be used to pass
980
                options from the scripts to generic functions
981
    - aliases: dictionary with command aliases {'alias': 'target, ...}
982

983
  """
984
  # save the program name and the entire command line for later logging
985
  if sys.argv:
986
    binary = os.path.basename(sys.argv[0]) or sys.argv[0]
987
    if len(sys.argv) >= 2:
988
      binary += " " + sys.argv[1]
989
      old_cmdline = " ".join(sys.argv[2:])
990
    else:
991
      old_cmdline = ""
992
  else:
993
    binary = "<unknown program>"
994
    old_cmdline = ""
995

    
996
  if aliases is None:
997
    aliases = {}
998

    
999
  try:
1000
    func, options, args = _ParseArgs(sys.argv, commands, aliases)
1001
  except errors.ParameterError, err:
1002
    result, err_msg = FormatError(err)
1003
    ToStderr(err_msg)
1004
    return 1
1005

    
1006
  if func is None: # parse error
1007
    return 1
1008

    
1009
  if override is not None:
1010
    for key, val in override.iteritems():
1011
      setattr(options, key, val)
1012

    
1013
  utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
1014
                     stderr_logging=True, program=binary)
1015

    
1016
  if old_cmdline:
1017
    logging.info("run with arguments '%s'", old_cmdline)
1018
  else:
1019
    logging.info("run with no arguments")
1020

    
1021
  try:
1022
    result = func(options, args)
1023
  except (errors.GenericError, luxi.ProtocolError,
1024
          JobSubmittedException), err:
1025
    result, err_msg = FormatError(err)
1026
    logging.exception("Error during command processing")
1027
    ToStderr(err_msg)
1028

    
1029
  return result
1030

    
1031

    
1032
def GenerateTable(headers, fields, separator, data,
1033
                  numfields=None, unitfields=None,
1034
                  units=None):
1035
  """Prints a table with headers and different fields.
1036

1037
  @type headers: dict
1038
  @param headers: dictionary mapping field names to headers for
1039
      the table
1040
  @type fields: list
1041
  @param fields: the field names corresponding to each row in
1042
      the data field
1043
  @param separator: the separator to be used; if this is None,
1044
      the default 'smart' algorithm is used which computes optimal
1045
      field width, otherwise just the separator is used between
1046
      each field
1047
  @type data: list
1048
  @param data: a list of lists, each sublist being one row to be output
1049
  @type numfields: list
1050
  @param numfields: a list with the fields that hold numeric
1051
      values and thus should be right-aligned
1052
  @type unitfields: list
1053
  @param unitfields: a list with the fields that hold numeric
1054
      values that should be formatted with the units field
1055
  @type units: string or None
1056
  @param units: the units we should use for formatting, or None for
1057
      automatic choice (human-readable for non-separator usage, otherwise
1058
      megabytes); this is a one-letter string
1059

1060
  """
1061
  if units is None:
1062
    if separator:
1063
      units = "m"
1064
    else:
1065
      units = "h"
1066

    
1067
  if numfields is None:
1068
    numfields = []
1069
  if unitfields is None:
1070
    unitfields = []
1071

    
1072
  numfields = utils.FieldSet(*numfields)
1073
  unitfields = utils.FieldSet(*unitfields)
1074

    
1075
  format_fields = []
1076
  for field in fields:
1077
    if headers and field not in headers:
1078
      # TODO: handle better unknown fields (either revert to old
1079
      # style of raising exception, or deal more intelligently with
1080
      # variable fields)
1081
      headers[field] = field
1082
    if separator is not None:
1083
      format_fields.append("%s")
1084
    elif numfields.Matches(field):
1085
      format_fields.append("%*s")
1086
    else:
1087
      format_fields.append("%-*s")
1088

    
1089
  if separator is None:
1090
    mlens = [0 for name in fields]
1091
    format = ' '.join(format_fields)
1092
  else:
1093
    format = separator.replace("%", "%%").join(format_fields)
1094

    
1095
  for row in data:
1096
    if row is None:
1097
      continue
1098
    for idx, val in enumerate(row):
1099
      if unitfields.Matches(fields[idx]):
1100
        try:
1101
          val = int(val)
1102
        except ValueError:
1103
          pass
1104
        else:
1105
          val = row[idx] = utils.FormatUnit(val, units)
1106
      val = row[idx] = str(val)
1107
      if separator is None:
1108
        mlens[idx] = max(mlens[idx], len(val))
1109

    
1110
  result = []
1111
  if headers:
1112
    args = []
1113
    for idx, name in enumerate(fields):
1114
      hdr = headers[name]
1115
      if separator is None:
1116
        mlens[idx] = max(mlens[idx], len(hdr))
1117
        args.append(mlens[idx])
1118
      args.append(hdr)
1119
    result.append(format % tuple(args))
1120

    
1121
  for line in data:
1122
    args = []
1123
    if line is None:
1124
      line = ['-' for _ in fields]
1125
    for idx in xrange(len(fields)):
1126
      if separator is None:
1127
        args.append(mlens[idx])
1128
      args.append(line[idx])
1129
    result.append(format % tuple(args))
1130

    
1131
  return result
1132

    
1133

    
1134
def FormatTimestamp(ts):
1135
  """Formats a given timestamp.
1136

1137
  @type ts: timestamp
1138
  @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
1139

1140
  @rtype: string
1141
  @return: a string with the formatted timestamp
1142

1143
  """
1144
  if not isinstance (ts, (tuple, list)) or len(ts) != 2:
1145
    return '?'
1146
  sec, usec = ts
1147
  return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
1148

    
1149

    
1150
def ParseTimespec(value):
1151
  """Parse a time specification.
1152

1153
  The following suffixed will be recognized:
1154

1155
    - s: seconds
1156
    - m: minutes
1157
    - h: hours
1158
    - d: day
1159
    - w: weeks
1160

1161
  Without any suffix, the value will be taken to be in seconds.
1162

1163
  """
1164
  value = str(value)
1165
  if not value:
1166
    raise errors.OpPrereqError("Empty time specification passed")
1167
  suffix_map = {
1168
    's': 1,
1169
    'm': 60,
1170
    'h': 3600,
1171
    'd': 86400,
1172
    'w': 604800,
1173
    }
1174
  if value[-1] not in suffix_map:
1175
    try:
1176
      value = int(value)
1177
    except ValueError:
1178
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1179
  else:
1180
    multiplier = suffix_map[value[-1]]
1181
    value = value[:-1]
1182
    if not value: # no data left after stripping the suffix
1183
      raise errors.OpPrereqError("Invalid time specification (only"
1184
                                 " suffix passed)")
1185
    try:
1186
      value = int(value) * multiplier
1187
    except ValueError:
1188
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1189
  return value
1190

    
1191

    
1192
def GetOnlineNodes(nodes, cl=None, nowarn=False):
1193
  """Returns the names of online nodes.
1194

1195
  This function will also log a warning on stderr with the names of
1196
  the online nodes.
1197

1198
  @param nodes: if not empty, use only this subset of nodes (minus the
1199
      offline ones)
1200
  @param cl: if not None, luxi client to use
1201
  @type nowarn: boolean
1202
  @param nowarn: by default, this function will output a note with the
1203
      offline nodes that are skipped; if this parameter is True the
1204
      note is not displayed
1205

1206
  """
1207
  if cl is None:
1208
    cl = GetClient()
1209

    
1210
  result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
1211
                         use_locking=False)
1212
  offline = [row[0] for row in result if row[1]]
1213
  if offline and not nowarn:
1214
    ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
1215
  return [row[0] for row in result if not row[1]]
1216

    
1217

    
1218
def _ToStream(stream, txt, *args):
1219
  """Write a message to a stream, bypassing the logging system
1220

1221
  @type stream: file object
1222
  @param stream: the file to which we should write
1223
  @type txt: str
1224
  @param txt: the message
1225

1226
  """
1227
  if args:
1228
    args = tuple(args)
1229
    stream.write(txt % args)
1230
  else:
1231
    stream.write(txt)
1232
  stream.write('\n')
1233
  stream.flush()
1234

    
1235

    
1236
def ToStdout(txt, *args):
1237
  """Write a message to stdout only, bypassing the logging system
1238

1239
  This is just a wrapper over _ToStream.
1240

1241
  @type txt: str
1242
  @param txt: the message
1243

1244
  """
1245
  _ToStream(sys.stdout, txt, *args)
1246

    
1247

    
1248
def ToStderr(txt, *args):
1249
  """Write a message to stderr only, bypassing the logging system
1250

1251
  This is just a wrapper over _ToStream.
1252

1253
  @type txt: str
1254
  @param txt: the message
1255

1256
  """
1257
  _ToStream(sys.stderr, txt, *args)
1258

    
1259

    
1260
class JobExecutor(object):
1261
  """Class which manages the submission and execution of multiple jobs.
1262

1263
  Note that instances of this class should not be reused between
1264
  GetResults() calls.
1265

1266
  """
1267
  def __init__(self, cl=None, verbose=True):
1268
    self.queue = []
1269
    if cl is None:
1270
      cl = GetClient()
1271
    self.cl = cl
1272
    self.verbose = verbose
1273
    self.jobs = []
1274

    
1275
  def QueueJob(self, name, *ops):
1276
    """Record a job for later submit.
1277

1278
    @type name: string
1279
    @param name: a description of the job, will be used in WaitJobSet
1280
    """
1281
    self.queue.append((name, ops))
1282

    
1283
  def SubmitPending(self):
1284
    """Submit all pending jobs.
1285

1286
    """
1287
    results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1288
    for ((status, data), (name, _)) in zip(results, self.queue):
1289
      self.jobs.append((status, data, name))
1290

    
1291
  def GetResults(self):
1292
    """Wait for and return the results of all jobs.
1293

1294
    @rtype: list
1295
    @return: list of tuples (success, job results), in the same order
1296
        as the submitted jobs; if a job has failed, instead of the result
1297
        there will be the error message
1298

1299
    """
1300
    if not self.jobs:
1301
      self.SubmitPending()
1302
    results = []
1303
    if self.verbose:
1304
      ok_jobs = [row[1] for row in self.jobs if row[0]]
1305
      if ok_jobs:
1306
        ToStdout("Submitted jobs %s", ", ".join(ok_jobs))
1307
    for submit_status, jid, name in self.jobs:
1308
      if not submit_status:
1309
        ToStderr("Failed to submit job for %s: %s", name, jid)
1310
        results.append((False, jid))
1311
        continue
1312
      if self.verbose:
1313
        ToStdout("Waiting for job %s for %s...", jid, name)
1314
      try:
1315
        job_result = PollJob(jid, cl=self.cl)
1316
        success = True
1317
      except (errors.GenericError, luxi.ProtocolError), err:
1318
        _, job_result = FormatError(err)
1319
        success = False
1320
        # the error message will always be shown, verbose or not
1321
        ToStderr("Job %s for %s has failed: %s", jid, name, job_result)
1322

    
1323
      results.append((success, job_result))
1324
    return results
1325

    
1326
  def WaitOrShow(self, wait):
1327
    """Wait for job results or only print the job IDs.
1328

1329
    @type wait: boolean
1330
    @param wait: whether to wait or not
1331

1332
    """
1333
    if wait:
1334
      return self.GetResults()
1335
    else:
1336
      if not self.jobs:
1337
        self.SubmitPending()
1338
      for status, result, name in self.jobs:
1339
        if status:
1340
          ToStdout("%s: %s", result, name)
1341
        else:
1342
          ToStderr("Failure for %s: %s", name, result)