Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ 7d3a9fab

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

    
117
NO_PREFIX = "no_"
118
UN_PREFIX = "-"
119

    
120

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

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

    
130

    
131
class ArgSuggest(_Argument):
132
  """Suggesting argument.
133

134
  Value can be any of the ones passed to the constructor.
135

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

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

    
145

    
146
class ArgChoice(ArgSuggest):
147
  """Choice argument.
148

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

152
  """
153

    
154

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

158
  """
159

    
160

    
161
class ArgInstance(_Argument):
162
  """Instances argument.
163

164
  """
165

    
166

    
167
class ArgNode(_Argument):
168
  """Node argument.
169

170
  """
171

    
172
class ArgJobId(_Argument):
173
  """Job ID argument.
174

175
  """
176

    
177

    
178
class ArgFile(_Argument):
179
  """File path argument.
180

181
  """
182

    
183

    
184
class ArgCommand(_Argument):
185
  """Command argument.
186

187
  """
188

    
189

    
190
class ArgHost(_Argument):
191
  """Host argument.
192

193
  """
194

    
195

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

    
202

    
203

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

207
  Note that this function will modify its args parameter.
208

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

    
224

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

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

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

    
253

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

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

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

    
271

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

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

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

    
288

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

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

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

    
305

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

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

    
315

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

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

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

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

    
352

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

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

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

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

    
380

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

384
  This will store the parsed values as a dict {key: val}.
385

386
  """
387
  return _SplitKeyVal(opt, value)
388

    
389

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

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

    
408

    
409
class CliOption(Option):
410
  """Custom option class for optparse.
411

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

    
426

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

    
430

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
538
NOIPCHECK_OPT = cli_option("--no-ip-check", dest="ip_check", default=True,
539
                           action="store_false",
540
                           help="Don't check that the instance's IP"
541
                           " is alive")
542

    
543
NET_OPT = cli_option("--net",
544
                     help="NIC parameters", default=[],
545
                     dest="nics", action="append", type="identkeyval")
546

    
547

    
548
def _ParseArgs(argv, commands, aliases):
549
  """Parser for the command line arguments.
550

551
  This function parses the arguments and returns the function which
552
  must be executed together with its (modified) arguments.
553

554
  @param argv: the command line
555
  @param commands: dictionary with special contents, see the design
556
      doc for cmdline handling
557
  @param aliases: dictionary with command aliases {'alias': 'target, ...}
558

559
  """
560
  if len(argv) == 0:
561
    binary = "<command>"
562
  else:
563
    binary = argv[0].split("/")[-1]
564

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

    
571
  if len(argv) < 2 or not (argv[1] in commands or
572
                           argv[1] in aliases):
573
    # let's do a nice thing
574
    sortedcmds = commands.keys()
575
    sortedcmds.sort()
576

    
577
    ToStdout("Usage: %s {command} [options...] [argument...]", binary)
578
    ToStdout("%s <command> --help to see details, or man %s", binary, binary)
579
    ToStdout("")
580

    
581
    # compute the max line length for cmd + usage
582
    mlen = max([len(" %s" % cmd) for cmd in commands])
583
    mlen = min(60, mlen) # should not get here...
584

    
585
    # and format a nice command list
586
    ToStdout("Commands:")
587
    for cmd in sortedcmds:
588
      cmdstr = " %s" % (cmd,)
589
      help_text = commands[cmd][4]
590
      help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
591
      ToStdout("%-*s - %s", mlen, cmdstr, help_lines.pop(0))
592
      for line in help_lines:
593
        ToStdout("%-*s   %s", mlen, "", line)
594

    
595
    ToStdout("")
596

    
597
    return None, None, None
598

    
599
  # get command, unalias it, and look it up in commands
600
  cmd = argv.pop(1)
601
  if cmd in aliases:
602
    if cmd in commands:
603
      raise errors.ProgrammerError("Alias '%s' overrides an existing"
604
                                   " command" % cmd)
605

    
606
    if aliases[cmd] not in commands:
607
      raise errors.ProgrammerError("Alias '%s' maps to non-existing"
608
                                   " command '%s'" % (cmd, aliases[cmd]))
609

    
610
    cmd = aliases[cmd]
611

    
612
  func, args_def, parser_opts, usage, description = commands[cmd]
613
  parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT],
614
                        description=description,
615
                        formatter=TitledHelpFormatter(),
616
                        usage="%%prog %s %s" % (cmd, usage))
617
  parser.disable_interspersed_args()
618
  options, args = parser.parse_args()
619

    
620
  if not _CheckArguments(cmd, args_def, args):
621
    return None, None, None
622

    
623
  return func, options, args
624

    
625

    
626
def _CheckArguments(cmd, args_def, args):
627
  """Verifies the arguments using the argument definition.
628

629
  Algorithm:
630

631
    1. Abort with error if values specified by user but none expected.
632

633
    1. For each argument in definition
634

635
      1. Keep running count of minimum number of values (min_count)
636
      1. Keep running count of maximum number of values (max_count)
637
      1. If it has an unlimited number of values
638

639
        1. Abort with error if it's not the last argument in the definition
640

641
    1. If last argument has limited number of values
642

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

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

647
  """
648
  if args and not args_def:
649
    ToStderr("Error: Command %s expects no arguments", cmd)
650
    return False
651

    
652
  min_count = None
653
  max_count = None
654
  check_max = None
655

    
656
  last_idx = len(args_def) - 1
657

    
658
  for idx, arg in enumerate(args_def):
659
    if min_count is None:
660
      min_count = arg.min
661
    elif arg.min is not None:
662
      min_count += arg.min
663

    
664
    if max_count is None:
665
      max_count = arg.max
666
    elif arg.max is not None:
667
      max_count += arg.max
668

    
669
    if idx == last_idx:
670
      check_max = (arg.max is not None)
671

    
672
    elif arg.max is None:
673
      raise errors.ProgrammerError("Only the last argument can have max=None")
674

    
675
  if check_max:
676
    # Command with exact number of arguments
677
    if (min_count is not None and max_count is not None and
678
        min_count == max_count and len(args) != min_count):
679
      ToStderr("Error: Command %s expects %d argument(s)", cmd, min_count)
680
      return False
681

    
682
    # Command with limited number of arguments
683
    if max_count is not None and len(args) > max_count:
684
      ToStderr("Error: Command %s expects only %d argument(s)",
685
               cmd, max_count)
686
      return False
687

    
688
  # Command with some required arguments
689
  if min_count is not None and len(args) < min_count:
690
    ToStderr("Error: Command %s expects at least %d argument(s)",
691
             cmd, min_count)
692
    return False
693

    
694
  return True
695

    
696

    
697
def SplitNodeOption(value):
698
  """Splits the value of a --node option.
699

700
  """
701
  if value and ':' in value:
702
    return value.split(':', 1)
703
  else:
704
    return (value, None)
705

    
706

    
707
def UsesRPC(fn):
708
  def wrapper(*args, **kwargs):
709
    rpc.Init()
710
    try:
711
      return fn(*args, **kwargs)
712
    finally:
713
      rpc.Shutdown()
714
  return wrapper
715

    
716

    
717
def AskUser(text, choices=None):
718
  """Ask the user a question.
719

720
  @param text: the question to ask
721

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

727
  @return: one of the return values from the choices list; if input is
728
      not possible (i.e. not running with a tty, we return the last
729
      entry from the list
730

731
  """
732
  if choices is None:
733
    choices = [('y', True, 'Perform the operation'),
734
               ('n', False, 'Do not perform the operation')]
735
  if not choices or not isinstance(choices, list):
736
    raise errors.ProgrammerError("Invalid choices argument to AskUser")
737
  for entry in choices:
738
    if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
739
      raise errors.ProgrammerError("Invalid choices element to AskUser")
740

    
741
  answer = choices[-1][1]
742
  new_text = []
743
  for line in text.splitlines():
744
    new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
745
  text = "\n".join(new_text)
746
  try:
747
    f = file("/dev/tty", "a+")
748
  except IOError:
749
    return answer
750
  try:
751
    chars = [entry[0] for entry in choices]
752
    chars[-1] = "[%s]" % chars[-1]
753
    chars.append('?')
754
    maps = dict([(entry[0], entry[1]) for entry in choices])
755
    while True:
756
      f.write(text)
757
      f.write('\n')
758
      f.write("/".join(chars))
759
      f.write(": ")
760
      line = f.readline(2).strip().lower()
761
      if line in maps:
762
        answer = maps[line]
763
        break
764
      elif line == '?':
765
        for entry in choices:
766
          f.write(" %s - %s\n" % (entry[0], entry[2]))
767
        f.write("\n")
768
        continue
769
  finally:
770
    f.close()
771
  return answer
772

    
773

    
774
class JobSubmittedException(Exception):
775
  """Job was submitted, client should exit.
776

777
  This exception has one argument, the ID of the job that was
778
  submitted. The handler should print this ID.
779

780
  This is not an error, just a structured way to exit from clients.
781

782
  """
783

    
784

    
785
def SendJob(ops, cl=None):
786
  """Function to submit an opcode without waiting for the results.
787

788
  @type ops: list
789
  @param ops: list of opcodes
790
  @type cl: luxi.Client
791
  @param cl: the luxi client to use for communicating with the master;
792
             if None, a new client will be created
793

794
  """
795
  if cl is None:
796
    cl = GetClient()
797

    
798
  job_id = cl.SubmitJob(ops)
799

    
800
  return job_id
801

    
802

    
803
def PollJob(job_id, cl=None, feedback_fn=None):
804
  """Function to poll for the result of a job.
805

806
  @type job_id: job identified
807
  @param job_id: the job to poll for results
808
  @type cl: luxi.Client
809
  @param cl: the luxi client to use for communicating with the master;
810
             if None, a new client will be created
811

812
  """
813
  if cl is None:
814
    cl = GetClient()
815

    
816
  prev_job_info = None
817
  prev_logmsg_serial = None
818

    
819
  while True:
820
    result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
821
                                 prev_logmsg_serial)
822
    if not result:
823
      # job not found, go away!
824
      raise errors.JobLost("Job with id %s lost" % job_id)
825

    
826
    # Split result, a tuple of (field values, log entries)
827
    (job_info, log_entries) = result
828
    (status, ) = job_info
829

    
830
    if log_entries:
831
      for log_entry in log_entries:
832
        (serial, timestamp, _, message) = log_entry
833
        if callable(feedback_fn):
834
          feedback_fn(log_entry[1:])
835
        else:
836
          encoded = utils.SafeEncode(message)
837
          ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)), encoded)
838
        prev_logmsg_serial = max(prev_logmsg_serial, serial)
839

    
840
    # TODO: Handle canceled and archived jobs
841
    elif status in (constants.JOB_STATUS_SUCCESS,
842
                    constants.JOB_STATUS_ERROR,
843
                    constants.JOB_STATUS_CANCELING,
844
                    constants.JOB_STATUS_CANCELED):
845
      break
846

    
847
    prev_job_info = job_info
848

    
849
  jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
850
  if not jobs:
851
    raise errors.JobLost("Job with id %s lost" % job_id)
852

    
853
  status, opstatus, result = jobs[0]
854
  if status == constants.JOB_STATUS_SUCCESS:
855
    return result
856
  elif status in (constants.JOB_STATUS_CANCELING,
857
                  constants.JOB_STATUS_CANCELED):
858
    raise errors.OpExecError("Job was canceled")
859
  else:
860
    has_ok = False
861
    for idx, (status, msg) in enumerate(zip(opstatus, result)):
862
      if status == constants.OP_STATUS_SUCCESS:
863
        has_ok = True
864
      elif status == constants.OP_STATUS_ERROR:
865
        errors.MaybeRaise(msg)
866
        if has_ok:
867
          raise errors.OpExecError("partial failure (opcode %d): %s" %
868
                                   (idx, msg))
869
        else:
870
          raise errors.OpExecError(str(msg))
871
    # default failure mode
872
    raise errors.OpExecError(result)
873

    
874

    
875
def SubmitOpCode(op, cl=None, feedback_fn=None):
876
  """Legacy function to submit an opcode.
877

878
  This is just a simple wrapper over the construction of the processor
879
  instance. It should be extended to better handle feedback and
880
  interaction functions.
881

882
  """
883
  if cl is None:
884
    cl = GetClient()
885

    
886
  job_id = SendJob([op], cl)
887

    
888
  op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
889

    
890
  return op_results[0]
891

    
892

    
893
def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
894
  """Wrapper around SubmitOpCode or SendJob.
895

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

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

903
  """
904
  if opts and opts.dry_run:
905
    op.dry_run = opts.dry_run
906
  if opts and opts.submit_only:
907
    job_id = SendJob([op], cl=cl)
908
    raise JobSubmittedException(job_id)
909
  else:
910
    return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
911

    
912

    
913
def GetClient():
914
  # TODO: Cache object?
915
  try:
916
    client = luxi.Client()
917
  except luxi.NoMasterError:
918
    master, myself = ssconf.GetMasterAndMyself()
919
    if master != myself:
920
      raise errors.OpPrereqError("This is not the master node, please connect"
921
                                 " to node '%s' and rerun the command" %
922
                                 master)
923
    else:
924
      raise
925
  return client
926

    
927

    
928
def FormatError(err):
929
  """Return a formatted error message for a given error.
930

931
  This function takes an exception instance and returns a tuple
932
  consisting of two values: first, the recommended exit code, and
933
  second, a string describing the error message (not
934
  newline-terminated).
935

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

    
999

    
1000
def GenericMain(commands, override=None, aliases=None):
1001
  """Generic main function for all the gnt-* commands.
1002

1003
  Arguments:
1004
    - commands: a dictionary with a special structure, see the design doc
1005
                for command line handling.
1006
    - override: if not None, we expect a dictionary with keys that will
1007
                override command line options; this can be used to pass
1008
                options from the scripts to generic functions
1009
    - aliases: dictionary with command aliases {'alias': 'target, ...}
1010

1011
  """
1012
  # save the program name and the entire command line for later logging
1013
  if sys.argv:
1014
    binary = os.path.basename(sys.argv[0]) or sys.argv[0]
1015
    if len(sys.argv) >= 2:
1016
      binary += " " + sys.argv[1]
1017
      old_cmdline = " ".join(sys.argv[2:])
1018
    else:
1019
      old_cmdline = ""
1020
  else:
1021
    binary = "<unknown program>"
1022
    old_cmdline = ""
1023

    
1024
  if aliases is None:
1025
    aliases = {}
1026

    
1027
  try:
1028
    func, options, args = _ParseArgs(sys.argv, commands, aliases)
1029
  except errors.ParameterError, err:
1030
    result, err_msg = FormatError(err)
1031
    ToStderr(err_msg)
1032
    return 1
1033

    
1034
  if func is None: # parse error
1035
    return 1
1036

    
1037
  if override is not None:
1038
    for key, val in override.iteritems():
1039
      setattr(options, key, val)
1040

    
1041
  utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
1042
                     stderr_logging=True, program=binary)
1043

    
1044
  if old_cmdline:
1045
    logging.info("run with arguments '%s'", old_cmdline)
1046
  else:
1047
    logging.info("run with no arguments")
1048

    
1049
  try:
1050
    result = func(options, args)
1051
  except (errors.GenericError, luxi.ProtocolError,
1052
          JobSubmittedException), err:
1053
    result, err_msg = FormatError(err)
1054
    logging.exception("Error during command processing")
1055
    ToStderr(err_msg)
1056

    
1057
  return result
1058

    
1059

    
1060
def GenerateTable(headers, fields, separator, data,
1061
                  numfields=None, unitfields=None,
1062
                  units=None):
1063
  """Prints a table with headers and different fields.
1064

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

1088
  """
1089
  if units is None:
1090
    if separator:
1091
      units = "m"
1092
    else:
1093
      units = "h"
1094

    
1095
  if numfields is None:
1096
    numfields = []
1097
  if unitfields is None:
1098
    unitfields = []
1099

    
1100
  numfields = utils.FieldSet(*numfields)
1101
  unitfields = utils.FieldSet(*unitfields)
1102

    
1103
  format_fields = []
1104
  for field in fields:
1105
    if headers and field not in headers:
1106
      # TODO: handle better unknown fields (either revert to old
1107
      # style of raising exception, or deal more intelligently with
1108
      # variable fields)
1109
      headers[field] = field
1110
    if separator is not None:
1111
      format_fields.append("%s")
1112
    elif numfields.Matches(field):
1113
      format_fields.append("%*s")
1114
    else:
1115
      format_fields.append("%-*s")
1116

    
1117
  if separator is None:
1118
    mlens = [0 for name in fields]
1119
    format = ' '.join(format_fields)
1120
  else:
1121
    format = separator.replace("%", "%%").join(format_fields)
1122

    
1123
  for row in data:
1124
    if row is None:
1125
      continue
1126
    for idx, val in enumerate(row):
1127
      if unitfields.Matches(fields[idx]):
1128
        try:
1129
          val = int(val)
1130
        except ValueError:
1131
          pass
1132
        else:
1133
          val = row[idx] = utils.FormatUnit(val, units)
1134
      val = row[idx] = str(val)
1135
      if separator is None:
1136
        mlens[idx] = max(mlens[idx], len(val))
1137

    
1138
  result = []
1139
  if headers:
1140
    args = []
1141
    for idx, name in enumerate(fields):
1142
      hdr = headers[name]
1143
      if separator is None:
1144
        mlens[idx] = max(mlens[idx], len(hdr))
1145
        args.append(mlens[idx])
1146
      args.append(hdr)
1147
    result.append(format % tuple(args))
1148

    
1149
  for line in data:
1150
    args = []
1151
    if line is None:
1152
      line = ['-' for _ in fields]
1153
    for idx in xrange(len(fields)):
1154
      if separator is None:
1155
        args.append(mlens[idx])
1156
      args.append(line[idx])
1157
    result.append(format % tuple(args))
1158

    
1159
  return result
1160

    
1161

    
1162
def FormatTimestamp(ts):
1163
  """Formats a given timestamp.
1164

1165
  @type ts: timestamp
1166
  @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
1167

1168
  @rtype: string
1169
  @return: a string with the formatted timestamp
1170

1171
  """
1172
  if not isinstance (ts, (tuple, list)) or len(ts) != 2:
1173
    return '?'
1174
  sec, usec = ts
1175
  return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
1176

    
1177

    
1178
def ParseTimespec(value):
1179
  """Parse a time specification.
1180

1181
  The following suffixed will be recognized:
1182

1183
    - s: seconds
1184
    - m: minutes
1185
    - h: hours
1186
    - d: day
1187
    - w: weeks
1188

1189
  Without any suffix, the value will be taken to be in seconds.
1190

1191
  """
1192
  value = str(value)
1193
  if not value:
1194
    raise errors.OpPrereqError("Empty time specification passed")
1195
  suffix_map = {
1196
    's': 1,
1197
    'm': 60,
1198
    'h': 3600,
1199
    'd': 86400,
1200
    'w': 604800,
1201
    }
1202
  if value[-1] not in suffix_map:
1203
    try:
1204
      value = int(value)
1205
    except ValueError:
1206
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1207
  else:
1208
    multiplier = suffix_map[value[-1]]
1209
    value = value[:-1]
1210
    if not value: # no data left after stripping the suffix
1211
      raise errors.OpPrereqError("Invalid time specification (only"
1212
                                 " suffix passed)")
1213
    try:
1214
      value = int(value) * multiplier
1215
    except ValueError:
1216
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1217
  return value
1218

    
1219

    
1220
def GetOnlineNodes(nodes, cl=None, nowarn=False):
1221
  """Returns the names of online nodes.
1222

1223
  This function will also log a warning on stderr with the names of
1224
  the online nodes.
1225

1226
  @param nodes: if not empty, use only this subset of nodes (minus the
1227
      offline ones)
1228
  @param cl: if not None, luxi client to use
1229
  @type nowarn: boolean
1230
  @param nowarn: by default, this function will output a note with the
1231
      offline nodes that are skipped; if this parameter is True the
1232
      note is not displayed
1233

1234
  """
1235
  if cl is None:
1236
    cl = GetClient()
1237

    
1238
  result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
1239
                         use_locking=False)
1240
  offline = [row[0] for row in result if row[1]]
1241
  if offline and not nowarn:
1242
    ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
1243
  return [row[0] for row in result if not row[1]]
1244

    
1245

    
1246
def _ToStream(stream, txt, *args):
1247
  """Write a message to a stream, bypassing the logging system
1248

1249
  @type stream: file object
1250
  @param stream: the file to which we should write
1251
  @type txt: str
1252
  @param txt: the message
1253

1254
  """
1255
  if args:
1256
    args = tuple(args)
1257
    stream.write(txt % args)
1258
  else:
1259
    stream.write(txt)
1260
  stream.write('\n')
1261
  stream.flush()
1262

    
1263

    
1264
def ToStdout(txt, *args):
1265
  """Write a message to stdout only, bypassing the logging system
1266

1267
  This is just a wrapper over _ToStream.
1268

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

1272
  """
1273
  _ToStream(sys.stdout, txt, *args)
1274

    
1275

    
1276
def ToStderr(txt, *args):
1277
  """Write a message to stderr only, bypassing the logging system
1278

1279
  This is just a wrapper over _ToStream.
1280

1281
  @type txt: str
1282
  @param txt: the message
1283

1284
  """
1285
  _ToStream(sys.stderr, txt, *args)
1286

    
1287

    
1288
class JobExecutor(object):
1289
  """Class which manages the submission and execution of multiple jobs.
1290

1291
  Note that instances of this class should not be reused between
1292
  GetResults() calls.
1293

1294
  """
1295
  def __init__(self, cl=None, verbose=True):
1296
    self.queue = []
1297
    if cl is None:
1298
      cl = GetClient()
1299
    self.cl = cl
1300
    self.verbose = verbose
1301
    self.jobs = []
1302

    
1303
  def QueueJob(self, name, *ops):
1304
    """Record a job for later submit.
1305

1306
    @type name: string
1307
    @param name: a description of the job, will be used in WaitJobSet
1308
    """
1309
    self.queue.append((name, ops))
1310

    
1311
  def SubmitPending(self):
1312
    """Submit all pending jobs.
1313

1314
    """
1315
    results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1316
    for ((status, data), (name, _)) in zip(results, self.queue):
1317
      self.jobs.append((status, data, name))
1318

    
1319
  def GetResults(self):
1320
    """Wait for and return the results of all jobs.
1321

1322
    @rtype: list
1323
    @return: list of tuples (success, job results), in the same order
1324
        as the submitted jobs; if a job has failed, instead of the result
1325
        there will be the error message
1326

1327
    """
1328
    if not self.jobs:
1329
      self.SubmitPending()
1330
    results = []
1331
    if self.verbose:
1332
      ok_jobs = [row[1] for row in self.jobs if row[0]]
1333
      if ok_jobs:
1334
        ToStdout("Submitted jobs %s", ", ".join(ok_jobs))
1335
    for submit_status, jid, name in self.jobs:
1336
      if not submit_status:
1337
        ToStderr("Failed to submit job for %s: %s", name, jid)
1338
        results.append((False, jid))
1339
        continue
1340
      if self.verbose:
1341
        ToStdout("Waiting for job %s for %s...", jid, name)
1342
      try:
1343
        job_result = PollJob(jid, cl=self.cl)
1344
        success = True
1345
      except (errors.GenericError, luxi.ProtocolError), err:
1346
        _, job_result = FormatError(err)
1347
        success = False
1348
        # the error message will always be shown, verbose or not
1349
        ToStderr("Job %s for %s has failed: %s", jid, name, job_result)
1350

    
1351
      results.append((success, job_result))
1352
    return results
1353

    
1354
  def WaitOrShow(self, wait):
1355
    """Wait for job results or only print the job IDs.
1356

1357
    @type wait: boolean
1358
    @param wait: whether to wait or not
1359

1360
    """
1361
    if wait:
1362
      return self.GetResults()
1363
    else:
1364
      if not self.jobs:
1365
        self.SubmitPending()
1366
      for status, result, name in self.jobs:
1367
        if status:
1368
          ToStdout("%s: %s", result, name)
1369
        else:
1370
          ToStderr("Failure for %s: %s", name, result)