Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ e3876ccb

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

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

    
121

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

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

    
131

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

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

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

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

    
146

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

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

153
  """
154

    
155

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

159
  """
160

    
161

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

165
  """
166

    
167

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

171
  """
172

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

176
  """
177

    
178

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

182
  """
183

    
184

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

188
  """
189

    
190

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

194
  """
195

    
196

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

    
203

    
204

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

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

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

    
225

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

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

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

    
254

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

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

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

    
272

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

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

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

    
289

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

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

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

    
306

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

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

    
316

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

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

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

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

    
353

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

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

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

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

    
381

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

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

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

    
390

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

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

    
409

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

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

    
427

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

    
431

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
548
DISK_OPT = cli_option("--disk", help="Disk parameters", default=[],
549
                      dest="disks", action="append", type="identkeyval")
550

    
551

    
552
def _ParseArgs(argv, commands, aliases):
553
  """Parser for the command line arguments.
554

555
  This function parses the arguments and returns the function which
556
  must be executed together with its (modified) arguments.
557

558
  @param argv: the command line
559
  @param commands: dictionary with special contents, see the design
560
      doc for cmdline handling
561
  @param aliases: dictionary with command aliases {'alias': 'target, ...}
562

563
  """
564
  if len(argv) == 0:
565
    binary = "<command>"
566
  else:
567
    binary = argv[0].split("/")[-1]
568

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

    
575
  if len(argv) < 2 or not (argv[1] in commands or
576
                           argv[1] in aliases):
577
    # let's do a nice thing
578
    sortedcmds = commands.keys()
579
    sortedcmds.sort()
580

    
581
    ToStdout("Usage: %s {command} [options...] [argument...]", binary)
582
    ToStdout("%s <command> --help to see details, or man %s", binary, binary)
583
    ToStdout("")
584

    
585
    # compute the max line length for cmd + usage
586
    mlen = max([len(" %s" % cmd) for cmd in commands])
587
    mlen = min(60, mlen) # should not get here...
588

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

    
599
    ToStdout("")
600

    
601
    return None, None, None
602

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

    
610
    if aliases[cmd] not in commands:
611
      raise errors.ProgrammerError("Alias '%s' maps to non-existing"
612
                                   " command '%s'" % (cmd, aliases[cmd]))
613

    
614
    cmd = aliases[cmd]
615

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

    
624
  if not _CheckArguments(cmd, args_def, args):
625
    return None, None, None
626

    
627
  return func, options, args
628

    
629

    
630
def _CheckArguments(cmd, args_def, args):
631
  """Verifies the arguments using the argument definition.
632

633
  Algorithm:
634

635
    1. Abort with error if values specified by user but none expected.
636

637
    1. For each argument in definition
638

639
      1. Keep running count of minimum number of values (min_count)
640
      1. Keep running count of maximum number of values (max_count)
641
      1. If it has an unlimited number of values
642

643
        1. Abort with error if it's not the last argument in the definition
644

645
    1. If last argument has limited number of values
646

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

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

651
  """
652
  if args and not args_def:
653
    ToStderr("Error: Command %s expects no arguments", cmd)
654
    return False
655

    
656
  min_count = None
657
  max_count = None
658
  check_max = None
659

    
660
  last_idx = len(args_def) - 1
661

    
662
  for idx, arg in enumerate(args_def):
663
    if min_count is None:
664
      min_count = arg.min
665
    elif arg.min is not None:
666
      min_count += arg.min
667

    
668
    if max_count is None:
669
      max_count = arg.max
670
    elif arg.max is not None:
671
      max_count += arg.max
672

    
673
    if idx == last_idx:
674
      check_max = (arg.max is not None)
675

    
676
    elif arg.max is None:
677
      raise errors.ProgrammerError("Only the last argument can have max=None")
678

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

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

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

    
698
  return True
699

    
700

    
701
def SplitNodeOption(value):
702
  """Splits the value of a --node option.
703

704
  """
705
  if value and ':' in value:
706
    return value.split(':', 1)
707
  else:
708
    return (value, None)
709

    
710

    
711
def UsesRPC(fn):
712
  def wrapper(*args, **kwargs):
713
    rpc.Init()
714
    try:
715
      return fn(*args, **kwargs)
716
    finally:
717
      rpc.Shutdown()
718
  return wrapper
719

    
720

    
721
def AskUser(text, choices=None):
722
  """Ask the user a question.
723

724
  @param text: the question to ask
725

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

731
  @return: one of the return values from the choices list; if input is
732
      not possible (i.e. not running with a tty, we return the last
733
      entry from the list
734

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

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

    
777

    
778
class JobSubmittedException(Exception):
779
  """Job was submitted, client should exit.
780

781
  This exception has one argument, the ID of the job that was
782
  submitted. The handler should print this ID.
783

784
  This is not an error, just a structured way to exit from clients.
785

786
  """
787

    
788

    
789
def SendJob(ops, cl=None):
790
  """Function to submit an opcode without waiting for the results.
791

792
  @type ops: list
793
  @param ops: list of opcodes
794
  @type cl: luxi.Client
795
  @param cl: the luxi client to use for communicating with the master;
796
             if None, a new client will be created
797

798
  """
799
  if cl is None:
800
    cl = GetClient()
801

    
802
  job_id = cl.SubmitJob(ops)
803

    
804
  return job_id
805

    
806

    
807
def PollJob(job_id, cl=None, feedback_fn=None):
808
  """Function to poll for the result of a job.
809

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

816
  """
817
  if cl is None:
818
    cl = GetClient()
819

    
820
  prev_job_info = None
821
  prev_logmsg_serial = None
822

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

    
830
    # Split result, a tuple of (field values, log entries)
831
    (job_info, log_entries) = result
832
    (status, ) = job_info
833

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

    
844
    # TODO: Handle canceled and archived jobs
845
    elif status in (constants.JOB_STATUS_SUCCESS,
846
                    constants.JOB_STATUS_ERROR,
847
                    constants.JOB_STATUS_CANCELING,
848
                    constants.JOB_STATUS_CANCELED):
849
      break
850

    
851
    prev_job_info = job_info
852

    
853
  jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
854
  if not jobs:
855
    raise errors.JobLost("Job with id %s lost" % job_id)
856

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

    
878

    
879
def SubmitOpCode(op, cl=None, feedback_fn=None):
880
  """Legacy function to submit an opcode.
881

882
  This is just a simple wrapper over the construction of the processor
883
  instance. It should be extended to better handle feedback and
884
  interaction functions.
885

886
  """
887
  if cl is None:
888
    cl = GetClient()
889

    
890
  job_id = SendJob([op], cl)
891

    
892
  op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
893

    
894
  return op_results[0]
895

    
896

    
897
def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
898
  """Wrapper around SubmitOpCode or SendJob.
899

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

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

907
  """
908
  if opts and opts.dry_run:
909
    op.dry_run = opts.dry_run
910
  if opts and opts.submit_only:
911
    job_id = SendJob([op], cl=cl)
912
    raise JobSubmittedException(job_id)
913
  else:
914
    return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
915

    
916

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

    
931

    
932
def FormatError(err):
933
  """Return a formatted error message for a given error.
934

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

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

    
1003

    
1004
def GenericMain(commands, override=None, aliases=None):
1005
  """Generic main function for all the gnt-* commands.
1006

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

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

    
1028
  if aliases is None:
1029
    aliases = {}
1030

    
1031
  try:
1032
    func, options, args = _ParseArgs(sys.argv, commands, aliases)
1033
  except errors.ParameterError, err:
1034
    result, err_msg = FormatError(err)
1035
    ToStderr(err_msg)
1036
    return 1
1037

    
1038
  if func is None: # parse error
1039
    return 1
1040

    
1041
  if override is not None:
1042
    for key, val in override.iteritems():
1043
      setattr(options, key, val)
1044

    
1045
  utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
1046
                     stderr_logging=True, program=binary)
1047

    
1048
  if old_cmdline:
1049
    logging.info("run with arguments '%s'", old_cmdline)
1050
  else:
1051
    logging.info("run with no arguments")
1052

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

    
1061
  return result
1062

    
1063

    
1064
def GenerateTable(headers, fields, separator, data,
1065
                  numfields=None, unitfields=None,
1066
                  units=None):
1067
  """Prints a table with headers and different fields.
1068

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

1092
  """
1093
  if units is None:
1094
    if separator:
1095
      units = "m"
1096
    else:
1097
      units = "h"
1098

    
1099
  if numfields is None:
1100
    numfields = []
1101
  if unitfields is None:
1102
    unitfields = []
1103

    
1104
  numfields = utils.FieldSet(*numfields)
1105
  unitfields = utils.FieldSet(*unitfields)
1106

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

    
1121
  if separator is None:
1122
    mlens = [0 for name in fields]
1123
    format = ' '.join(format_fields)
1124
  else:
1125
    format = separator.replace("%", "%%").join(format_fields)
1126

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

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

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

    
1163
  return result
1164

    
1165

    
1166
def FormatTimestamp(ts):
1167
  """Formats a given timestamp.
1168

1169
  @type ts: timestamp
1170
  @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
1171

1172
  @rtype: string
1173
  @return: a string with the formatted timestamp
1174

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

    
1181

    
1182
def ParseTimespec(value):
1183
  """Parse a time specification.
1184

1185
  The following suffixed will be recognized:
1186

1187
    - s: seconds
1188
    - m: minutes
1189
    - h: hours
1190
    - d: day
1191
    - w: weeks
1192

1193
  Without any suffix, the value will be taken to be in seconds.
1194

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

    
1223

    
1224
def GetOnlineNodes(nodes, cl=None, nowarn=False):
1225
  """Returns the names of online nodes.
1226

1227
  This function will also log a warning on stderr with the names of
1228
  the online nodes.
1229

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

1238
  """
1239
  if cl is None:
1240
    cl = GetClient()
1241

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

    
1249

    
1250
def _ToStream(stream, txt, *args):
1251
  """Write a message to a stream, bypassing the logging system
1252

1253
  @type stream: file object
1254
  @param stream: the file to which we should write
1255
  @type txt: str
1256
  @param txt: the message
1257

1258
  """
1259
  if args:
1260
    args = tuple(args)
1261
    stream.write(txt % args)
1262
  else:
1263
    stream.write(txt)
1264
  stream.write('\n')
1265
  stream.flush()
1266

    
1267

    
1268
def ToStdout(txt, *args):
1269
  """Write a message to stdout only, bypassing the logging system
1270

1271
  This is just a wrapper over _ToStream.
1272

1273
  @type txt: str
1274
  @param txt: the message
1275

1276
  """
1277
  _ToStream(sys.stdout, txt, *args)
1278

    
1279

    
1280
def ToStderr(txt, *args):
1281
  """Write a message to stderr only, bypassing the logging system
1282

1283
  This is just a wrapper over _ToStream.
1284

1285
  @type txt: str
1286
  @param txt: the message
1287

1288
  """
1289
  _ToStream(sys.stderr, txt, *args)
1290

    
1291

    
1292
class JobExecutor(object):
1293
  """Class which manages the submission and execution of multiple jobs.
1294

1295
  Note that instances of this class should not be reused between
1296
  GetResults() calls.
1297

1298
  """
1299
  def __init__(self, cl=None, verbose=True):
1300
    self.queue = []
1301
    if cl is None:
1302
      cl = GetClient()
1303
    self.cl = cl
1304
    self.verbose = verbose
1305
    self.jobs = []
1306

    
1307
  def QueueJob(self, name, *ops):
1308
    """Record a job for later submit.
1309

1310
    @type name: string
1311
    @param name: a description of the job, will be used in WaitJobSet
1312
    """
1313
    self.queue.append((name, ops))
1314

    
1315
  def SubmitPending(self):
1316
    """Submit all pending jobs.
1317

1318
    """
1319
    results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1320
    for ((status, data), (name, _)) in zip(results, self.queue):
1321
      self.jobs.append((status, data, name))
1322

    
1323
  def GetResults(self):
1324
    """Wait for and return the results of all jobs.
1325

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

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

    
1355
      results.append((success, job_result))
1356
    return results
1357

    
1358
  def WaitOrShow(self, wait):
1359
    """Wait for job results or only print the job IDs.
1360

1361
    @type wait: boolean
1362
    @param wait: whether to wait or not
1363

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