Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ d3ed23ff

History | View | Annotate | Download (38.2 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007 Google Inc.
5
#
6
# This program is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2 of the License, or
9
# (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful, but
12
# WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14
# General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19
# 02110-1301, USA.
20

    
21

    
22
"""Module dealing with command line parsing"""
23

    
24

    
25
import sys
26
import textwrap
27
import os.path
28
import copy
29
import time
30
import logging
31
from cStringIO import StringIO
32

    
33
from ganeti import utils
34
from ganeti import errors
35
from ganeti import constants
36
from ganeti import opcodes
37
from ganeti import luxi
38
from ganeti import ssconf
39
from ganeti import rpc
40

    
41
from optparse import (OptionParser, TitledHelpFormatter,
42
                      Option, OptionValueError)
43

    
44

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

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

    
114

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

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

    
124

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

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

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

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

    
139

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

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

146
  """
147

    
148

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

152
  """
153

    
154

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

158
  """
159

    
160

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

164
  """
165

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

169
  """
170

    
171

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

175
  """
176

    
177

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

181
  """
182

    
183

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

187
  """
188

    
189

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

    
196

    
197

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

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

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

    
218

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

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

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

    
247

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

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

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

    
265

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

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

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

    
282

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

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

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

    
299

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

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

    
309

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

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

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

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

    
346

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

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

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

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

    
374

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

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

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

    
383

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

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

    
402

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

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

    
420

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

    
424

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
514

    
515
def _ParseArgs(argv, commands, aliases):
516
  """Parser for the command line arguments.
517

518
  This function parses the arguments and returns the function which
519
  must be executed together with its (modified) arguments.
520

521
  @param argv: the command line
522
  @param commands: dictionary with special contents, see the design
523
      doc for cmdline handling
524
  @param aliases: dictionary with command aliases {'alias': 'target, ...}
525

526
  """
527
  if len(argv) == 0:
528
    binary = "<command>"
529
  else:
530
    binary = argv[0].split("/")[-1]
531

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

    
538
  if len(argv) < 2 or not (argv[1] in commands or
539
                           argv[1] in aliases):
540
    # let's do a nice thing
541
    sortedcmds = commands.keys()
542
    sortedcmds.sort()
543

    
544
    ToStdout("Usage: %s {command} [options...] [argument...]", binary)
545
    ToStdout("%s <command> --help to see details, or man %s", binary, binary)
546
    ToStdout("")
547

    
548
    # compute the max line length for cmd + usage
549
    mlen = max([len(" %s" % cmd) for cmd in commands])
550
    mlen = min(60, mlen) # should not get here...
551

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

    
562
    ToStdout("")
563

    
564
    return None, None, None
565

    
566
  # get command, unalias it, and look it up in commands
567
  cmd = argv.pop(1)
568
  if cmd in aliases:
569
    if cmd in commands:
570
      raise errors.ProgrammerError("Alias '%s' overrides an existing"
571
                                   " command" % cmd)
572

    
573
    if aliases[cmd] not in commands:
574
      raise errors.ProgrammerError("Alias '%s' maps to non-existing"
575
                                   " command '%s'" % (cmd, aliases[cmd]))
576

    
577
    cmd = aliases[cmd]
578

    
579
  func, args_def, parser_opts, usage, description = commands[cmd]
580
  parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT],
581
                        description=description,
582
                        formatter=TitledHelpFormatter(),
583
                        usage="%%prog %s %s" % (cmd, usage))
584
  parser.disable_interspersed_args()
585
  options, args = parser.parse_args()
586

    
587
  if not _CheckArguments(cmd, args_def, args):
588
    return None, None, None
589

    
590
  return func, options, args
591

    
592

    
593
def _CheckArguments(cmd, args_def, args):
594
  """Verifies the arguments using the argument definition.
595

596
  Algorithm:
597

598
    1. Abort with error if values specified by user but none expected.
599

600
    1. For each argument in definition
601

602
      1. Keep running count of minimum number of values (min_count)
603
      1. Keep running count of maximum number of values (max_count)
604
      1. If it has an unlimited number of values
605

606
        1. Abort with error if it's not the last argument in the definition
607

608
    1. If last argument has limited number of values
609

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

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

614
  """
615
  if args and not args_def:
616
    ToStderr("Error: Command %s expects no arguments", cmd)
617
    return False
618

    
619
  min_count = None
620
  max_count = None
621
  check_max = None
622

    
623
  last_idx = len(args_def) - 1
624

    
625
  for idx, arg in enumerate(args_def):
626
    if min_count is None:
627
      min_count = arg.min
628
    elif arg.min is not None:
629
      min_count += arg.min
630

    
631
    if max_count is None:
632
      max_count = arg.max
633
    elif arg.max is not None:
634
      max_count += arg.max
635

    
636
    if idx == last_idx:
637
      check_max = (arg.max is not None)
638

    
639
    elif arg.max is None:
640
      raise errors.ProgrammerError("Only the last argument can have max=None")
641

    
642
  if check_max:
643
    # Command with exact number of arguments
644
    if (min_count is not None and max_count is not None and
645
        min_count == max_count and len(args) != min_count):
646
      ToStderr("Error: Command %s expects %d argument(s)", cmd, min_count)
647
      return False
648

    
649
    # Command with limited number of arguments
650
    if max_count is not None and len(args) > max_count:
651
      ToStderr("Error: Command %s expects only %d argument(s)",
652
               cmd, max_count)
653
      return False
654

    
655
  # Command with some required arguments
656
  if min_count is not None and len(args) < min_count:
657
    ToStderr("Error: Command %s expects at least %d argument(s)",
658
             cmd, min_count)
659
    return False
660

    
661
  return True
662

    
663

    
664
def SplitNodeOption(value):
665
  """Splits the value of a --node option.
666

667
  """
668
  if value and ':' in value:
669
    return value.split(':', 1)
670
  else:
671
    return (value, None)
672

    
673

    
674
def UsesRPC(fn):
675
  def wrapper(*args, **kwargs):
676
    rpc.Init()
677
    try:
678
      return fn(*args, **kwargs)
679
    finally:
680
      rpc.Shutdown()
681
  return wrapper
682

    
683

    
684
def AskUser(text, choices=None):
685
  """Ask the user a question.
686

687
  @param text: the question to ask
688

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

694
  @return: one of the return values from the choices list; if input is
695
      not possible (i.e. not running with a tty, we return the last
696
      entry from the list
697

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

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

    
740

    
741
class JobSubmittedException(Exception):
742
  """Job was submitted, client should exit.
743

744
  This exception has one argument, the ID of the job that was
745
  submitted. The handler should print this ID.
746

747
  This is not an error, just a structured way to exit from clients.
748

749
  """
750

    
751

    
752
def SendJob(ops, cl=None):
753
  """Function to submit an opcode without waiting for the results.
754

755
  @type ops: list
756
  @param ops: list of opcodes
757
  @type cl: luxi.Client
758
  @param cl: the luxi client to use for communicating with the master;
759
             if None, a new client will be created
760

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

    
765
  job_id = cl.SubmitJob(ops)
766

    
767
  return job_id
768

    
769

    
770
def PollJob(job_id, cl=None, feedback_fn=None):
771
  """Function to poll for the result of a job.
772

773
  @type job_id: job identified
774
  @param job_id: the job to poll for results
775
  @type cl: luxi.Client
776
  @param cl: the luxi client to use for communicating with the master;
777
             if None, a new client will be created
778

779
  """
780
  if cl is None:
781
    cl = GetClient()
782

    
783
  prev_job_info = None
784
  prev_logmsg_serial = None
785

    
786
  while True:
787
    result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
788
                                 prev_logmsg_serial)
789
    if not result:
790
      # job not found, go away!
791
      raise errors.JobLost("Job with id %s lost" % job_id)
792

    
793
    # Split result, a tuple of (field values, log entries)
794
    (job_info, log_entries) = result
795
    (status, ) = job_info
796

    
797
    if log_entries:
798
      for log_entry in log_entries:
799
        (serial, timestamp, _, message) = log_entry
800
        if callable(feedback_fn):
801
          feedback_fn(log_entry[1:])
802
        else:
803
          encoded = utils.SafeEncode(message)
804
          ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)), encoded)
805
        prev_logmsg_serial = max(prev_logmsg_serial, serial)
806

    
807
    # TODO: Handle canceled and archived jobs
808
    elif status in (constants.JOB_STATUS_SUCCESS,
809
                    constants.JOB_STATUS_ERROR,
810
                    constants.JOB_STATUS_CANCELING,
811
                    constants.JOB_STATUS_CANCELED):
812
      break
813

    
814
    prev_job_info = job_info
815

    
816
  jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
817
  if not jobs:
818
    raise errors.JobLost("Job with id %s lost" % job_id)
819

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

    
841

    
842
def SubmitOpCode(op, cl=None, feedback_fn=None):
843
  """Legacy function to submit an opcode.
844

845
  This is just a simple wrapper over the construction of the processor
846
  instance. It should be extended to better handle feedback and
847
  interaction functions.
848

849
  """
850
  if cl is None:
851
    cl = GetClient()
852

    
853
  job_id = SendJob([op], cl)
854

    
855
  op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
856

    
857
  return op_results[0]
858

    
859

    
860
def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
861
  """Wrapper around SubmitOpCode or SendJob.
862

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

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

870
  """
871
  if opts and opts.dry_run:
872
    op.dry_run = opts.dry_run
873
  if opts and opts.submit_only:
874
    job_id = SendJob([op], cl=cl)
875
    raise JobSubmittedException(job_id)
876
  else:
877
    return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
878

    
879

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

    
894

    
895
def FormatError(err):
896
  """Return a formatted error message for a given error.
897

898
  This function takes an exception instance and returns a tuple
899
  consisting of two values: first, the recommended exit code, and
900
  second, a string describing the error message (not
901
  newline-terminated).
902

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

    
966

    
967
def GenericMain(commands, override=None, aliases=None):
968
  """Generic main function for all the gnt-* commands.
969

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

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

    
991
  if aliases is None:
992
    aliases = {}
993

    
994
  try:
995
    func, options, args = _ParseArgs(sys.argv, commands, aliases)
996
  except errors.ParameterError, err:
997
    result, err_msg = FormatError(err)
998
    ToStderr(err_msg)
999
    return 1
1000

    
1001
  if func is None: # parse error
1002
    return 1
1003

    
1004
  if override is not None:
1005
    for key, val in override.iteritems():
1006
      setattr(options, key, val)
1007

    
1008
  utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
1009
                     stderr_logging=True, program=binary)
1010

    
1011
  if old_cmdline:
1012
    logging.info("run with arguments '%s'", old_cmdline)
1013
  else:
1014
    logging.info("run with no arguments")
1015

    
1016
  try:
1017
    result = func(options, args)
1018
  except (errors.GenericError, luxi.ProtocolError,
1019
          JobSubmittedException), err:
1020
    result, err_msg = FormatError(err)
1021
    logging.exception("Error during command processing")
1022
    ToStderr(err_msg)
1023

    
1024
  return result
1025

    
1026

    
1027
def GenerateTable(headers, fields, separator, data,
1028
                  numfields=None, unitfields=None,
1029
                  units=None):
1030
  """Prints a table with headers and different fields.
1031

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

1055
  """
1056
  if units is None:
1057
    if separator:
1058
      units = "m"
1059
    else:
1060
      units = "h"
1061

    
1062
  if numfields is None:
1063
    numfields = []
1064
  if unitfields is None:
1065
    unitfields = []
1066

    
1067
  numfields = utils.FieldSet(*numfields)
1068
  unitfields = utils.FieldSet(*unitfields)
1069

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

    
1084
  if separator is None:
1085
    mlens = [0 for name in fields]
1086
    format = ' '.join(format_fields)
1087
  else:
1088
    format = separator.replace("%", "%%").join(format_fields)
1089

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

    
1105
  result = []
1106
  if headers:
1107
    args = []
1108
    for idx, name in enumerate(fields):
1109
      hdr = headers[name]
1110
      if separator is None:
1111
        mlens[idx] = max(mlens[idx], len(hdr))
1112
        args.append(mlens[idx])
1113
      args.append(hdr)
1114
    result.append(format % tuple(args))
1115

    
1116
  for line in data:
1117
    args = []
1118
    if line is None:
1119
      line = ['-' for _ in fields]
1120
    for idx in xrange(len(fields)):
1121
      if separator is None:
1122
        args.append(mlens[idx])
1123
      args.append(line[idx])
1124
    result.append(format % tuple(args))
1125

    
1126
  return result
1127

    
1128

    
1129
def FormatTimestamp(ts):
1130
  """Formats a given timestamp.
1131

1132
  @type ts: timestamp
1133
  @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
1134

1135
  @rtype: string
1136
  @return: a string with the formatted timestamp
1137

1138
  """
1139
  if not isinstance (ts, (tuple, list)) or len(ts) != 2:
1140
    return '?'
1141
  sec, usec = ts
1142
  return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
1143

    
1144

    
1145
def ParseTimespec(value):
1146
  """Parse a time specification.
1147

1148
  The following suffixed will be recognized:
1149

1150
    - s: seconds
1151
    - m: minutes
1152
    - h: hours
1153
    - d: day
1154
    - w: weeks
1155

1156
  Without any suffix, the value will be taken to be in seconds.
1157

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

    
1186

    
1187
def GetOnlineNodes(nodes, cl=None, nowarn=False):
1188
  """Returns the names of online nodes.
1189

1190
  This function will also log a warning on stderr with the names of
1191
  the online nodes.
1192

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

1201
  """
1202
  if cl is None:
1203
    cl = GetClient()
1204

    
1205
  result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
1206
                         use_locking=False)
1207
  offline = [row[0] for row in result if row[1]]
1208
  if offline and not nowarn:
1209
    ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
1210
  return [row[0] for row in result if not row[1]]
1211

    
1212

    
1213
def _ToStream(stream, txt, *args):
1214
  """Write a message to a stream, bypassing the logging system
1215

1216
  @type stream: file object
1217
  @param stream: the file to which we should write
1218
  @type txt: str
1219
  @param txt: the message
1220

1221
  """
1222
  if args:
1223
    args = tuple(args)
1224
    stream.write(txt % args)
1225
  else:
1226
    stream.write(txt)
1227
  stream.write('\n')
1228
  stream.flush()
1229

    
1230

    
1231
def ToStdout(txt, *args):
1232
  """Write a message to stdout only, bypassing the logging system
1233

1234
  This is just a wrapper over _ToStream.
1235

1236
  @type txt: str
1237
  @param txt: the message
1238

1239
  """
1240
  _ToStream(sys.stdout, txt, *args)
1241

    
1242

    
1243
def ToStderr(txt, *args):
1244
  """Write a message to stderr only, bypassing the logging system
1245

1246
  This is just a wrapper over _ToStream.
1247

1248
  @type txt: str
1249
  @param txt: the message
1250

1251
  """
1252
  _ToStream(sys.stderr, txt, *args)
1253

    
1254

    
1255
class JobExecutor(object):
1256
  """Class which manages the submission and execution of multiple jobs.
1257

1258
  Note that instances of this class should not be reused between
1259
  GetResults() calls.
1260

1261
  """
1262
  def __init__(self, cl=None, verbose=True):
1263
    self.queue = []
1264
    if cl is None:
1265
      cl = GetClient()
1266
    self.cl = cl
1267
    self.verbose = verbose
1268
    self.jobs = []
1269

    
1270
  def QueueJob(self, name, *ops):
1271
    """Record a job for later submit.
1272

1273
    @type name: string
1274
    @param name: a description of the job, will be used in WaitJobSet
1275
    """
1276
    self.queue.append((name, ops))
1277

    
1278
  def SubmitPending(self):
1279
    """Submit all pending jobs.
1280

1281
    """
1282
    results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1283
    for ((status, data), (name, _)) in zip(results, self.queue):
1284
      self.jobs.append((status, data, name))
1285

    
1286
  def GetResults(self):
1287
    """Wait for and return the results of all jobs.
1288

1289
    @rtype: list
1290
    @return: list of tuples (success, job results), in the same order
1291
        as the submitted jobs; if a job has failed, instead of the result
1292
        there will be the error message
1293

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

    
1318
      results.append((success, job_result))
1319
    return results
1320

    
1321
  def WaitOrShow(self, wait):
1322
    """Wait for job results or only print the job IDs.
1323

1324
    @type wait: boolean
1325
    @param wait: whether to wait or not
1326

1327
    """
1328
    if wait:
1329
      return self.GetResults()
1330
    else:
1331
      if not self.jobs:
1332
        self.SubmitPending()
1333
      for status, result, name in self.jobs:
1334
        if status:
1335
          ToStdout("%s: %s", result, name)
1336
        else:
1337
          ToStderr("Failure for %s: %s", name, result)