Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ 073271f6

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

    
115
NO_PREFIX = "no_"
116
UN_PREFIX = "-"
117

    
118

    
119
class _Argument:
120
  def __init__(self, min=0, max=None):
121
    self.min = min
122
    self.max = max
123

    
124
  def __repr__(self):
125
    return ("<%s min=%s max=%s>" %
126
            (self.__class__.__name__, self.min, self.max))
127

    
128

    
129
class ArgSuggest(_Argument):
130
  """Suggesting argument.
131

132
  Value can be any of the ones passed to the constructor.
133

134
  """
135
  def __init__(self, min=0, max=None, choices=None):
136
    _Argument.__init__(self, min=min, max=max)
137
    self.choices = choices
138

    
139
  def __repr__(self):
140
    return ("<%s min=%s max=%s choices=%r>" %
141
            (self.__class__.__name__, self.min, self.max, self.choices))
142

    
143

    
144
class ArgChoice(ArgSuggest):
145
  """Choice argument.
146

147
  Value can be any of the ones passed to the constructor. Like L{ArgSuggest},
148
  but value must be one of the choices.
149

150
  """
151

    
152

    
153
class ArgUnknown(_Argument):
154
  """Unknown argument to program (e.g. determined at runtime).
155

156
  """
157

    
158

    
159
class ArgInstance(_Argument):
160
  """Instances argument.
161

162
  """
163

    
164

    
165
class ArgNode(_Argument):
166
  """Node argument.
167

168
  """
169

    
170
class ArgJobId(_Argument):
171
  """Job ID argument.
172

173
  """
174

    
175

    
176
class ArgFile(_Argument):
177
  """File path argument.
178

179
  """
180

    
181

    
182
class ArgCommand(_Argument):
183
  """Command argument.
184

185
  """
186

    
187

    
188
class ArgHost(_Argument):
189
  """Host argument.
190

191
  """
192

    
193

    
194
ARGS_NONE = []
195
ARGS_MANY_INSTANCES = [ArgInstance()]
196
ARGS_MANY_NODES = [ArgNode()]
197
ARGS_ONE_INSTANCE = [ArgInstance(min=1, max=1)]
198
ARGS_ONE_NODE = [ArgNode(min=1, max=1)]
199

    
200

    
201

    
202
def _ExtractTagsObject(opts, args):
203
  """Extract the tag type object.
204

205
  Note that this function will modify its args parameter.
206

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

    
222

    
223
def _ExtendTags(opts, args):
224
  """Extend the args if a source file has been given.
225

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

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

    
251

    
252
def ListTags(opts, args):
253
  """List the tags on a given object.
254

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

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

    
269

    
270
def AddTags(opts, args):
271
  """Add tags on a given object.
272

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

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

    
286

    
287
def RemoveTags(opts, args):
288
  """Remove tags from a given object.
289

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

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

    
303

    
304
def check_unit(option, opt, value):
305
  """OptParsers custom converter for units.
306

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

    
313

    
314
def _SplitKeyVal(opt, data):
315
  """Convert a KeyVal string into a dict.
316

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

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

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

    
350

    
351
def check_ident_key_val(option, opt, value):
352
  """Custom parser for ident:key=val,key=val options.
353

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

357
  """
358
  if ":" not in value:
359
    ident, rest = value, ''
360
  else:
361
    ident, rest = value.split(":", 1)
362

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

    
378

    
379
def check_key_val(option, opt, value):
380
  """Custom parser class for key=val,key=val options.
381

382
  This will store the parsed values as a dict {key: val}.
383

384
  """
385
  return _SplitKeyVal(opt, value)
386

    
387

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

    
397
OPT_COMPL_ALL = frozenset([
398
  OPT_COMPL_MANY_NODES,
399
  OPT_COMPL_ONE_NODE,
400
  OPT_COMPL_ONE_INSTANCE,
401
  OPT_COMPL_ONE_OS,
402
  OPT_COMPL_ONE_IALLOCATOR,
403
  OPT_COMPL_INST_ADD_NODES,
404
  ])
405

    
406

    
407
class CliOption(Option):
408
  """Custom option class for optparse.
409

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

    
424

    
425
# optparse.py sets make_option, so we do it for our own option class, too
426
cli_option = CliOption
427

    
428

    
429
DEBUG_OPT = cli_option("-d", "--debug", default=False,
430
                       action="store_true",
431
                       help="Turn debugging on")
432

    
433
NOHDR_OPT = cli_option("--no-headers", default=False,
434
                       action="store_true", dest="no_headers",
435
                       help="Don't display column headers")
436

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

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

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

    
450
FORCE_OPT = cli_option("-f", "--force", dest="force", action="store_true",
451
                       default=False, help="Force the operation")
452

    
453
CONFIRM_OPT = cli_option("--yes", dest="confirm", action="store_true",
454
                         default=False, help="Do not require confirmation")
455

    
456
TAG_SRC_OPT = cli_option("--from", dest="tags_source",
457
                         default=None, help="File with tag names")
458

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

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

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

    
475
VERBOSE_OPT = cli_option("-v", "--verbose", default=False,
476
                         action="store_true",
477
                         help="Increase the verbosity of the operation")
478

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

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

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

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

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

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

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

    
514
OS_OPT = cli_option("-o", "--os-type", dest="os", help="What OS to run",
515
                    metavar="<os>",
516
                    completion_suggest=OPT_COMPL_ONE_OS)
517

    
518
BACKEND_OPT = cli_option("-B", "--backend-parameters", dest="beparams",
519
                         type="keyval", default={},
520
                         help="Backend parameters")
521

    
522
HVOPTS_OPT =  cli_option("-H", "--hypervisor-parameters", type="keyval",
523
                         default={}, dest="hvparams",
524
                         help="Hypervisor parameters")
525

    
526
HYPERVISOR_OPT = cli_option("-H", "--hypervisor-parameters", dest="hypervisor",
527
                            help="Hypervisor and hypervisor options, in the"
528
                            " format hypervisor:option=value,option=value,...",
529
                            default=None, type="identkeyval")
530

    
531
HVLIST_OPT = cli_option("-H", "--hypervisor-parameters", dest="hvparams",
532
                        help="Hypervisor and hypervisor options, in the"
533
                        " format hypervisor:option=value,option=value,...",
534
                        default=[], action="append", type="identkeyval")
535

    
536

    
537
def _ParseArgs(argv, commands, aliases):
538
  """Parser for the command line arguments.
539

540
  This function parses the arguments and returns the function which
541
  must be executed together with its (modified) arguments.
542

543
  @param argv: the command line
544
  @param commands: dictionary with special contents, see the design
545
      doc for cmdline handling
546
  @param aliases: dictionary with command aliases {'alias': 'target, ...}
547

548
  """
549
  if len(argv) == 0:
550
    binary = "<command>"
551
  else:
552
    binary = argv[0].split("/")[-1]
553

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

    
560
  if len(argv) < 2 or not (argv[1] in commands or
561
                           argv[1] in aliases):
562
    # let's do a nice thing
563
    sortedcmds = commands.keys()
564
    sortedcmds.sort()
565

    
566
    ToStdout("Usage: %s {command} [options...] [argument...]", binary)
567
    ToStdout("%s <command> --help to see details, or man %s", binary, binary)
568
    ToStdout("")
569

    
570
    # compute the max line length for cmd + usage
571
    mlen = max([len(" %s" % cmd) for cmd in commands])
572
    mlen = min(60, mlen) # should not get here...
573

    
574
    # and format a nice command list
575
    ToStdout("Commands:")
576
    for cmd in sortedcmds:
577
      cmdstr = " %s" % (cmd,)
578
      help_text = commands[cmd][4]
579
      help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
580
      ToStdout("%-*s - %s", mlen, cmdstr, help_lines.pop(0))
581
      for line in help_lines:
582
        ToStdout("%-*s   %s", mlen, "", line)
583

    
584
    ToStdout("")
585

    
586
    return None, None, None
587

    
588
  # get command, unalias it, and look it up in commands
589
  cmd = argv.pop(1)
590
  if cmd in aliases:
591
    if cmd in commands:
592
      raise errors.ProgrammerError("Alias '%s' overrides an existing"
593
                                   " command" % cmd)
594

    
595
    if aliases[cmd] not in commands:
596
      raise errors.ProgrammerError("Alias '%s' maps to non-existing"
597
                                   " command '%s'" % (cmd, aliases[cmd]))
598

    
599
    cmd = aliases[cmd]
600

    
601
  func, args_def, parser_opts, usage, description = commands[cmd]
602
  parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT],
603
                        description=description,
604
                        formatter=TitledHelpFormatter(),
605
                        usage="%%prog %s %s" % (cmd, usage))
606
  parser.disable_interspersed_args()
607
  options, args = parser.parse_args()
608

    
609
  if not _CheckArguments(cmd, args_def, args):
610
    return None, None, None
611

    
612
  return func, options, args
613

    
614

    
615
def _CheckArguments(cmd, args_def, args):
616
  """Verifies the arguments using the argument definition.
617

618
  Algorithm:
619

620
    1. Abort with error if values specified by user but none expected.
621

622
    1. For each argument in definition
623

624
      1. Keep running count of minimum number of values (min_count)
625
      1. Keep running count of maximum number of values (max_count)
626
      1. If it has an unlimited number of values
627

628
        1. Abort with error if it's not the last argument in the definition
629

630
    1. If last argument has limited number of values
631

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

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

636
  """
637
  if args and not args_def:
638
    ToStderr("Error: Command %s expects no arguments", cmd)
639
    return False
640

    
641
  min_count = None
642
  max_count = None
643
  check_max = None
644

    
645
  last_idx = len(args_def) - 1
646

    
647
  for idx, arg in enumerate(args_def):
648
    if min_count is None:
649
      min_count = arg.min
650
    elif arg.min is not None:
651
      min_count += arg.min
652

    
653
    if max_count is None:
654
      max_count = arg.max
655
    elif arg.max is not None:
656
      max_count += arg.max
657

    
658
    if idx == last_idx:
659
      check_max = (arg.max is not None)
660

    
661
    elif arg.max is None:
662
      raise errors.ProgrammerError("Only the last argument can have max=None")
663

    
664
  if check_max:
665
    # Command with exact number of arguments
666
    if (min_count is not None and max_count is not None and
667
        min_count == max_count and len(args) != min_count):
668
      ToStderr("Error: Command %s expects %d argument(s)", cmd, min_count)
669
      return False
670

    
671
    # Command with limited number of arguments
672
    if max_count is not None and len(args) > max_count:
673
      ToStderr("Error: Command %s expects only %d argument(s)",
674
               cmd, max_count)
675
      return False
676

    
677
  # Command with some required arguments
678
  if min_count is not None and len(args) < min_count:
679
    ToStderr("Error: Command %s expects at least %d argument(s)",
680
             cmd, min_count)
681
    return False
682

    
683
  return True
684

    
685

    
686
def SplitNodeOption(value):
687
  """Splits the value of a --node option.
688

689
  """
690
  if value and ':' in value:
691
    return value.split(':', 1)
692
  else:
693
    return (value, None)
694

    
695

    
696
def UsesRPC(fn):
697
  def wrapper(*args, **kwargs):
698
    rpc.Init()
699
    try:
700
      return fn(*args, **kwargs)
701
    finally:
702
      rpc.Shutdown()
703
  return wrapper
704

    
705

    
706
def AskUser(text, choices=None):
707
  """Ask the user a question.
708

709
  @param text: the question to ask
710

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

716
  @return: one of the return values from the choices list; if input is
717
      not possible (i.e. not running with a tty, we return the last
718
      entry from the list
719

720
  """
721
  if choices is None:
722
    choices = [('y', True, 'Perform the operation'),
723
               ('n', False, 'Do not perform the operation')]
724
  if not choices or not isinstance(choices, list):
725
    raise errors.ProgrammerError("Invalid choices argument to AskUser")
726
  for entry in choices:
727
    if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
728
      raise errors.ProgrammerError("Invalid choices element to AskUser")
729

    
730
  answer = choices[-1][1]
731
  new_text = []
732
  for line in text.splitlines():
733
    new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
734
  text = "\n".join(new_text)
735
  try:
736
    f = file("/dev/tty", "a+")
737
  except IOError:
738
    return answer
739
  try:
740
    chars = [entry[0] for entry in choices]
741
    chars[-1] = "[%s]" % chars[-1]
742
    chars.append('?')
743
    maps = dict([(entry[0], entry[1]) for entry in choices])
744
    while True:
745
      f.write(text)
746
      f.write('\n')
747
      f.write("/".join(chars))
748
      f.write(": ")
749
      line = f.readline(2).strip().lower()
750
      if line in maps:
751
        answer = maps[line]
752
        break
753
      elif line == '?':
754
        for entry in choices:
755
          f.write(" %s - %s\n" % (entry[0], entry[2]))
756
        f.write("\n")
757
        continue
758
  finally:
759
    f.close()
760
  return answer
761

    
762

    
763
class JobSubmittedException(Exception):
764
  """Job was submitted, client should exit.
765

766
  This exception has one argument, the ID of the job that was
767
  submitted. The handler should print this ID.
768

769
  This is not an error, just a structured way to exit from clients.
770

771
  """
772

    
773

    
774
def SendJob(ops, cl=None):
775
  """Function to submit an opcode without waiting for the results.
776

777
  @type ops: list
778
  @param ops: list of opcodes
779
  @type cl: luxi.Client
780
  @param cl: the luxi client to use for communicating with the master;
781
             if None, a new client will be created
782

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

    
787
  job_id = cl.SubmitJob(ops)
788

    
789
  return job_id
790

    
791

    
792
def PollJob(job_id, cl=None, feedback_fn=None):
793
  """Function to poll for the result of a job.
794

795
  @type job_id: job identified
796
  @param job_id: the job to poll for results
797
  @type cl: luxi.Client
798
  @param cl: the luxi client to use for communicating with the master;
799
             if None, a new client will be created
800

801
  """
802
  if cl is None:
803
    cl = GetClient()
804

    
805
  prev_job_info = None
806
  prev_logmsg_serial = None
807

    
808
  while True:
809
    result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
810
                                 prev_logmsg_serial)
811
    if not result:
812
      # job not found, go away!
813
      raise errors.JobLost("Job with id %s lost" % job_id)
814

    
815
    # Split result, a tuple of (field values, log entries)
816
    (job_info, log_entries) = result
817
    (status, ) = job_info
818

    
819
    if log_entries:
820
      for log_entry in log_entries:
821
        (serial, timestamp, _, message) = log_entry
822
        if callable(feedback_fn):
823
          feedback_fn(log_entry[1:])
824
        else:
825
          encoded = utils.SafeEncode(message)
826
          ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)), encoded)
827
        prev_logmsg_serial = max(prev_logmsg_serial, serial)
828

    
829
    # TODO: Handle canceled and archived jobs
830
    elif status in (constants.JOB_STATUS_SUCCESS,
831
                    constants.JOB_STATUS_ERROR,
832
                    constants.JOB_STATUS_CANCELING,
833
                    constants.JOB_STATUS_CANCELED):
834
      break
835

    
836
    prev_job_info = job_info
837

    
838
  jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
839
  if not jobs:
840
    raise errors.JobLost("Job with id %s lost" % job_id)
841

    
842
  status, opstatus, result = jobs[0]
843
  if status == constants.JOB_STATUS_SUCCESS:
844
    return result
845
  elif status in (constants.JOB_STATUS_CANCELING,
846
                  constants.JOB_STATUS_CANCELED):
847
    raise errors.OpExecError("Job was canceled")
848
  else:
849
    has_ok = False
850
    for idx, (status, msg) in enumerate(zip(opstatus, result)):
851
      if status == constants.OP_STATUS_SUCCESS:
852
        has_ok = True
853
      elif status == constants.OP_STATUS_ERROR:
854
        errors.MaybeRaise(msg)
855
        if has_ok:
856
          raise errors.OpExecError("partial failure (opcode %d): %s" %
857
                                   (idx, msg))
858
        else:
859
          raise errors.OpExecError(str(msg))
860
    # default failure mode
861
    raise errors.OpExecError(result)
862

    
863

    
864
def SubmitOpCode(op, cl=None, feedback_fn=None):
865
  """Legacy function to submit an opcode.
866

867
  This is just a simple wrapper over the construction of the processor
868
  instance. It should be extended to better handle feedback and
869
  interaction functions.
870

871
  """
872
  if cl is None:
873
    cl = GetClient()
874

    
875
  job_id = SendJob([op], cl)
876

    
877
  op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
878

    
879
  return op_results[0]
880

    
881

    
882
def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
883
  """Wrapper around SubmitOpCode or SendJob.
884

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

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

892
  """
893
  if opts and opts.dry_run:
894
    op.dry_run = opts.dry_run
895
  if opts and opts.submit_only:
896
    job_id = SendJob([op], cl=cl)
897
    raise JobSubmittedException(job_id)
898
  else:
899
    return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
900

    
901

    
902
def GetClient():
903
  # TODO: Cache object?
904
  try:
905
    client = luxi.Client()
906
  except luxi.NoMasterError:
907
    master, myself = ssconf.GetMasterAndMyself()
908
    if master != myself:
909
      raise errors.OpPrereqError("This is not the master node, please connect"
910
                                 " to node '%s' and rerun the command" %
911
                                 master)
912
    else:
913
      raise
914
  return client
915

    
916

    
917
def FormatError(err):
918
  """Return a formatted error message for a given error.
919

920
  This function takes an exception instance and returns a tuple
921
  consisting of two values: first, the recommended exit code, and
922
  second, a string describing the error message (not
923
  newline-terminated).
924

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

    
988

    
989
def GenericMain(commands, override=None, aliases=None):
990
  """Generic main function for all the gnt-* commands.
991

992
  Arguments:
993
    - commands: a dictionary with a special structure, see the design doc
994
                for command line handling.
995
    - override: if not None, we expect a dictionary with keys that will
996
                override command line options; this can be used to pass
997
                options from the scripts to generic functions
998
    - aliases: dictionary with command aliases {'alias': 'target, ...}
999

1000
  """
1001
  # save the program name and the entire command line for later logging
1002
  if sys.argv:
1003
    binary = os.path.basename(sys.argv[0]) or sys.argv[0]
1004
    if len(sys.argv) >= 2:
1005
      binary += " " + sys.argv[1]
1006
      old_cmdline = " ".join(sys.argv[2:])
1007
    else:
1008
      old_cmdline = ""
1009
  else:
1010
    binary = "<unknown program>"
1011
    old_cmdline = ""
1012

    
1013
  if aliases is None:
1014
    aliases = {}
1015

    
1016
  try:
1017
    func, options, args = _ParseArgs(sys.argv, commands, aliases)
1018
  except errors.ParameterError, err:
1019
    result, err_msg = FormatError(err)
1020
    ToStderr(err_msg)
1021
    return 1
1022

    
1023
  if func is None: # parse error
1024
    return 1
1025

    
1026
  if override is not None:
1027
    for key, val in override.iteritems():
1028
      setattr(options, key, val)
1029

    
1030
  utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
1031
                     stderr_logging=True, program=binary)
1032

    
1033
  if old_cmdline:
1034
    logging.info("run with arguments '%s'", old_cmdline)
1035
  else:
1036
    logging.info("run with no arguments")
1037

    
1038
  try:
1039
    result = func(options, args)
1040
  except (errors.GenericError, luxi.ProtocolError,
1041
          JobSubmittedException), err:
1042
    result, err_msg = FormatError(err)
1043
    logging.exception("Error during command processing")
1044
    ToStderr(err_msg)
1045

    
1046
  return result
1047

    
1048

    
1049
def GenerateTable(headers, fields, separator, data,
1050
                  numfields=None, unitfields=None,
1051
                  units=None):
1052
  """Prints a table with headers and different fields.
1053

1054
  @type headers: dict
1055
  @param headers: dictionary mapping field names to headers for
1056
      the table
1057
  @type fields: list
1058
  @param fields: the field names corresponding to each row in
1059
      the data field
1060
  @param separator: the separator to be used; if this is None,
1061
      the default 'smart' algorithm is used which computes optimal
1062
      field width, otherwise just the separator is used between
1063
      each field
1064
  @type data: list
1065
  @param data: a list of lists, each sublist being one row to be output
1066
  @type numfields: list
1067
  @param numfields: a list with the fields that hold numeric
1068
      values and thus should be right-aligned
1069
  @type unitfields: list
1070
  @param unitfields: a list with the fields that hold numeric
1071
      values that should be formatted with the units field
1072
  @type units: string or None
1073
  @param units: the units we should use for formatting, or None for
1074
      automatic choice (human-readable for non-separator usage, otherwise
1075
      megabytes); this is a one-letter string
1076

1077
  """
1078
  if units is None:
1079
    if separator:
1080
      units = "m"
1081
    else:
1082
      units = "h"
1083

    
1084
  if numfields is None:
1085
    numfields = []
1086
  if unitfields is None:
1087
    unitfields = []
1088

    
1089
  numfields = utils.FieldSet(*numfields)
1090
  unitfields = utils.FieldSet(*unitfields)
1091

    
1092
  format_fields = []
1093
  for field in fields:
1094
    if headers and field not in headers:
1095
      # TODO: handle better unknown fields (either revert to old
1096
      # style of raising exception, or deal more intelligently with
1097
      # variable fields)
1098
      headers[field] = field
1099
    if separator is not None:
1100
      format_fields.append("%s")
1101
    elif numfields.Matches(field):
1102
      format_fields.append("%*s")
1103
    else:
1104
      format_fields.append("%-*s")
1105

    
1106
  if separator is None:
1107
    mlens = [0 for name in fields]
1108
    format = ' '.join(format_fields)
1109
  else:
1110
    format = separator.replace("%", "%%").join(format_fields)
1111

    
1112
  for row in data:
1113
    if row is None:
1114
      continue
1115
    for idx, val in enumerate(row):
1116
      if unitfields.Matches(fields[idx]):
1117
        try:
1118
          val = int(val)
1119
        except ValueError:
1120
          pass
1121
        else:
1122
          val = row[idx] = utils.FormatUnit(val, units)
1123
      val = row[idx] = str(val)
1124
      if separator is None:
1125
        mlens[idx] = max(mlens[idx], len(val))
1126

    
1127
  result = []
1128
  if headers:
1129
    args = []
1130
    for idx, name in enumerate(fields):
1131
      hdr = headers[name]
1132
      if separator is None:
1133
        mlens[idx] = max(mlens[idx], len(hdr))
1134
        args.append(mlens[idx])
1135
      args.append(hdr)
1136
    result.append(format % tuple(args))
1137

    
1138
  for line in data:
1139
    args = []
1140
    if line is None:
1141
      line = ['-' for _ in fields]
1142
    for idx in xrange(len(fields)):
1143
      if separator is None:
1144
        args.append(mlens[idx])
1145
      args.append(line[idx])
1146
    result.append(format % tuple(args))
1147

    
1148
  return result
1149

    
1150

    
1151
def FormatTimestamp(ts):
1152
  """Formats a given timestamp.
1153

1154
  @type ts: timestamp
1155
  @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
1156

1157
  @rtype: string
1158
  @return: a string with the formatted timestamp
1159

1160
  """
1161
  if not isinstance (ts, (tuple, list)) or len(ts) != 2:
1162
    return '?'
1163
  sec, usec = ts
1164
  return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
1165

    
1166

    
1167
def ParseTimespec(value):
1168
  """Parse a time specification.
1169

1170
  The following suffixed will be recognized:
1171

1172
    - s: seconds
1173
    - m: minutes
1174
    - h: hours
1175
    - d: day
1176
    - w: weeks
1177

1178
  Without any suffix, the value will be taken to be in seconds.
1179

1180
  """
1181
  value = str(value)
1182
  if not value:
1183
    raise errors.OpPrereqError("Empty time specification passed")
1184
  suffix_map = {
1185
    's': 1,
1186
    'm': 60,
1187
    'h': 3600,
1188
    'd': 86400,
1189
    'w': 604800,
1190
    }
1191
  if value[-1] not in suffix_map:
1192
    try:
1193
      value = int(value)
1194
    except ValueError:
1195
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1196
  else:
1197
    multiplier = suffix_map[value[-1]]
1198
    value = value[:-1]
1199
    if not value: # no data left after stripping the suffix
1200
      raise errors.OpPrereqError("Invalid time specification (only"
1201
                                 " suffix passed)")
1202
    try:
1203
      value = int(value) * multiplier
1204
    except ValueError:
1205
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1206
  return value
1207

    
1208

    
1209
def GetOnlineNodes(nodes, cl=None, nowarn=False):
1210
  """Returns the names of online nodes.
1211

1212
  This function will also log a warning on stderr with the names of
1213
  the online nodes.
1214

1215
  @param nodes: if not empty, use only this subset of nodes (minus the
1216
      offline ones)
1217
  @param cl: if not None, luxi client to use
1218
  @type nowarn: boolean
1219
  @param nowarn: by default, this function will output a note with the
1220
      offline nodes that are skipped; if this parameter is True the
1221
      note is not displayed
1222

1223
  """
1224
  if cl is None:
1225
    cl = GetClient()
1226

    
1227
  result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
1228
                         use_locking=False)
1229
  offline = [row[0] for row in result if row[1]]
1230
  if offline and not nowarn:
1231
    ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
1232
  return [row[0] for row in result if not row[1]]
1233

    
1234

    
1235
def _ToStream(stream, txt, *args):
1236
  """Write a message to a stream, bypassing the logging system
1237

1238
  @type stream: file object
1239
  @param stream: the file to which we should write
1240
  @type txt: str
1241
  @param txt: the message
1242

1243
  """
1244
  if args:
1245
    args = tuple(args)
1246
    stream.write(txt % args)
1247
  else:
1248
    stream.write(txt)
1249
  stream.write('\n')
1250
  stream.flush()
1251

    
1252

    
1253
def ToStdout(txt, *args):
1254
  """Write a message to stdout only, bypassing the logging system
1255

1256
  This is just a wrapper over _ToStream.
1257

1258
  @type txt: str
1259
  @param txt: the message
1260

1261
  """
1262
  _ToStream(sys.stdout, txt, *args)
1263

    
1264

    
1265
def ToStderr(txt, *args):
1266
  """Write a message to stderr only, bypassing the logging system
1267

1268
  This is just a wrapper over _ToStream.
1269

1270
  @type txt: str
1271
  @param txt: the message
1272

1273
  """
1274
  _ToStream(sys.stderr, txt, *args)
1275

    
1276

    
1277
class JobExecutor(object):
1278
  """Class which manages the submission and execution of multiple jobs.
1279

1280
  Note that instances of this class should not be reused between
1281
  GetResults() calls.
1282

1283
  """
1284
  def __init__(self, cl=None, verbose=True):
1285
    self.queue = []
1286
    if cl is None:
1287
      cl = GetClient()
1288
    self.cl = cl
1289
    self.verbose = verbose
1290
    self.jobs = []
1291

    
1292
  def QueueJob(self, name, *ops):
1293
    """Record a job for later submit.
1294

1295
    @type name: string
1296
    @param name: a description of the job, will be used in WaitJobSet
1297
    """
1298
    self.queue.append((name, ops))
1299

    
1300
  def SubmitPending(self):
1301
    """Submit all pending jobs.
1302

1303
    """
1304
    results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1305
    for ((status, data), (name, _)) in zip(results, self.queue):
1306
      self.jobs.append((status, data, name))
1307

    
1308
  def GetResults(self):
1309
    """Wait for and return the results of all jobs.
1310

1311
    @rtype: list
1312
    @return: list of tuples (success, job results), in the same order
1313
        as the submitted jobs; if a job has failed, instead of the result
1314
        there will be the error message
1315

1316
    """
1317
    if not self.jobs:
1318
      self.SubmitPending()
1319
    results = []
1320
    if self.verbose:
1321
      ok_jobs = [row[1] for row in self.jobs if row[0]]
1322
      if ok_jobs:
1323
        ToStdout("Submitted jobs %s", ", ".join(ok_jobs))
1324
    for submit_status, jid, name in self.jobs:
1325
      if not submit_status:
1326
        ToStderr("Failed to submit job for %s: %s", name, jid)
1327
        results.append((False, jid))
1328
        continue
1329
      if self.verbose:
1330
        ToStdout("Waiting for job %s for %s...", jid, name)
1331
      try:
1332
        job_result = PollJob(jid, cl=self.cl)
1333
        success = True
1334
      except (errors.GenericError, luxi.ProtocolError), err:
1335
        _, job_result = FormatError(err)
1336
        success = False
1337
        # the error message will always be shown, verbose or not
1338
        ToStderr("Job %s for %s has failed: %s", jid, name, job_result)
1339

    
1340
      results.append((success, job_result))
1341
    return results
1342

    
1343
  def WaitOrShow(self, wait):
1344
    """Wait for job results or only print the job IDs.
1345

1346
    @type wait: boolean
1347
    @param wait: whether to wait or not
1348

1349
    """
1350
    if wait:
1351
      return self.GetResults()
1352
    else:
1353
      if not self.jobs:
1354
        self.SubmitPending()
1355
      for status, result, name in self.jobs:
1356
        if status:
1357
          ToStdout("%s: %s", result, name)
1358
        else:
1359
          ToStderr("Failure for %s: %s", name, result)