Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ 0f87c43e

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

    
109
NO_PREFIX = "no_"
110
UN_PREFIX = "-"
111

    
112

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

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

    
122

    
123
class ArgSuggest(_Argument):
124
  """Suggesting argument.
125

126
  Value can be any of the ones passed to the constructor.
127

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

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

    
137

    
138
class ArgChoice(ArgSuggest):
139
  """Choice argument.
140

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

144
  """
145

    
146

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

150
  """
151

    
152

    
153
class ArgInstance(_Argument):
154
  """Instances argument.
155

156
  """
157

    
158

    
159
class ArgNode(_Argument):
160
  """Node argument.
161

162
  """
163

    
164
class ArgJobId(_Argument):
165
  """Job ID argument.
166

167
  """
168

    
169

    
170
class ArgFile(_Argument):
171
  """File path argument.
172

173
  """
174

    
175

    
176
class ArgCommand(_Argument):
177
  """Command argument.
178

179
  """
180

    
181

    
182
class ArgHost(_Argument):
183
  """Host argument.
184

185
  """
186

    
187

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

    
194

    
195

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

199
  Note that this function will modify its args parameter.
200

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

    
216

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

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

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

    
245

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

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

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

    
263

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

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

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

    
280

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

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

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

    
297

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

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

    
307

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

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

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

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

    
344

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

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

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

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

    
372

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

376
  This will store the parsed values as a dict {key: val}.
377

378
  """
379
  return _SplitKeyVal(opt, value)
380

    
381

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

    
391
OPT_COMPL_ALL = frozenset([
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,
398
  ])
399

    
400

    
401
class CliOption(Option):
402
  """Custom option class for optparse.
403

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

    
418

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

    
422

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
502

    
503
def _ParseArgs(argv, commands, aliases):
504
  """Parser for the command line arguments.
505

506
  This function parses the arguments and returns the function which
507
  must be executed together with its (modified) arguments.
508

509
  @param argv: the command line
510
  @param commands: dictionary with special contents, see the design
511
      doc for cmdline handling
512
  @param aliases: dictionary with command aliases {'alias': 'target, ...}
513

514
  """
515
  if len(argv) == 0:
516
    binary = "<command>"
517
  else:
518
    binary = argv[0].split("/")[-1]
519

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

    
526
  if len(argv) < 2 or not (argv[1] in commands or
527
                           argv[1] in aliases):
528
    # let's do a nice thing
529
    sortedcmds = commands.keys()
530
    sortedcmds.sort()
531

    
532
    ToStdout("Usage: %s {command} [options...] [argument...]", binary)
533
    ToStdout("%s <command> --help to see details, or man %s", binary, binary)
534
    ToStdout("")
535

    
536
    # compute the max line length for cmd + usage
537
    mlen = max([len(" %s" % cmd) for cmd in commands])
538
    mlen = min(60, mlen) # should not get here...
539

    
540
    # and format a nice command list
541
    ToStdout("Commands:")
542
    for cmd in sortedcmds:
543
      cmdstr = " %s" % (cmd,)
544
      help_text = commands[cmd][4]
545
      help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
546
      ToStdout("%-*s - %s", mlen, cmdstr, help_lines.pop(0))
547
      for line in help_lines:
548
        ToStdout("%-*s   %s", mlen, "", line)
549

    
550
    ToStdout("")
551

    
552
    return None, None, None
553

    
554
  # get command, unalias it, and look it up in commands
555
  cmd = argv.pop(1)
556
  if cmd in aliases:
557
    if cmd in commands:
558
      raise errors.ProgrammerError("Alias '%s' overrides an existing"
559
                                   " command" % cmd)
560

    
561
    if aliases[cmd] not in commands:
562
      raise errors.ProgrammerError("Alias '%s' maps to non-existing"
563
                                   " command '%s'" % (cmd, aliases[cmd]))
564

    
565
    cmd = aliases[cmd]
566

    
567
  func, args_def, parser_opts, usage, description = commands[cmd]
568
  parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT],
569
                        description=description,
570
                        formatter=TitledHelpFormatter(),
571
                        usage="%%prog %s %s" % (cmd, usage))
572
  parser.disable_interspersed_args()
573
  options, args = parser.parse_args()
574

    
575
  if not _CheckArguments(cmd, args_def, args):
576
    return None, None, None
577

    
578
  return func, options, args
579

    
580

    
581
def _CheckArguments(cmd, args_def, args):
582
  """Verifies the arguments using the argument definition.
583

584
  Algorithm:
585

586
    1. Abort with error if values specified by user but none expected.
587

588
    1. For each argument in definition
589

590
      1. Keep running count of minimum number of values (min_count)
591
      1. Keep running count of maximum number of values (max_count)
592
      1. If it has an unlimited number of values
593

594
        1. Abort with error if it's not the last argument in the definition
595

596
    1. If last argument has limited number of values
597

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

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

602
  """
603
  if args and not args_def:
604
    ToStderr("Error: Command %s expects no arguments", cmd)
605
    return False
606

    
607
  min_count = None
608
  max_count = None
609
  check_max = None
610

    
611
  last_idx = len(args_def) - 1
612

    
613
  for idx, arg in enumerate(args_def):
614
    if min_count is None:
615
      min_count = arg.min
616
    elif arg.min is not None:
617
      min_count += arg.min
618

    
619
    if max_count is None:
620
      max_count = arg.max
621
    elif arg.max is not None:
622
      max_count += arg.max
623

    
624
    if idx == last_idx:
625
      check_max = (arg.max is not None)
626

    
627
    elif arg.max is None:
628
      raise errors.ProgrammerError("Only the last argument can have max=None")
629

    
630
  if check_max:
631
    # Command with exact number of arguments
632
    if (min_count is not None and max_count is not None and
633
        min_count == max_count and len(args) != min_count):
634
      ToStderr("Error: Command %s expects %d argument(s)", cmd, min_count)
635
      return False
636

    
637
    # Command with limited number of arguments
638
    if max_count is not None and len(args) > max_count:
639
      ToStderr("Error: Command %s expects only %d argument(s)",
640
               cmd, max_count)
641
      return False
642

    
643
  # Command with some required arguments
644
  if min_count is not None and len(args) < min_count:
645
    ToStderr("Error: Command %s expects at least %d argument(s)",
646
             cmd, min_count)
647
    return False
648

    
649
  return True
650

    
651

    
652
def SplitNodeOption(value):
653
  """Splits the value of a --node option.
654

655
  """
656
  if value and ':' in value:
657
    return value.split(':', 1)
658
  else:
659
    return (value, None)
660

    
661

    
662
def UsesRPC(fn):
663
  def wrapper(*args, **kwargs):
664
    rpc.Init()
665
    try:
666
      return fn(*args, **kwargs)
667
    finally:
668
      rpc.Shutdown()
669
  return wrapper
670

    
671

    
672
def AskUser(text, choices=None):
673
  """Ask the user a question.
674

675
  @param text: the question to ask
676

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

682
  @return: one of the return values from the choices list; if input is
683
      not possible (i.e. not running with a tty, we return the last
684
      entry from the list
685

686
  """
687
  if choices is None:
688
    choices = [('y', True, 'Perform the operation'),
689
               ('n', False, 'Do not perform the operation')]
690
  if not choices or not isinstance(choices, list):
691
    raise errors.ProgrammerError("Invalid choices argument to AskUser")
692
  for entry in choices:
693
    if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
694
      raise errors.ProgrammerError("Invalid choices element to AskUser")
695

    
696
  answer = choices[-1][1]
697
  new_text = []
698
  for line in text.splitlines():
699
    new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
700
  text = "\n".join(new_text)
701
  try:
702
    f = file("/dev/tty", "a+")
703
  except IOError:
704
    return answer
705
  try:
706
    chars = [entry[0] for entry in choices]
707
    chars[-1] = "[%s]" % chars[-1]
708
    chars.append('?')
709
    maps = dict([(entry[0], entry[1]) for entry in choices])
710
    while True:
711
      f.write(text)
712
      f.write('\n')
713
      f.write("/".join(chars))
714
      f.write(": ")
715
      line = f.readline(2).strip().lower()
716
      if line in maps:
717
        answer = maps[line]
718
        break
719
      elif line == '?':
720
        for entry in choices:
721
          f.write(" %s - %s\n" % (entry[0], entry[2]))
722
        f.write("\n")
723
        continue
724
  finally:
725
    f.close()
726
  return answer
727

    
728

    
729
class JobSubmittedException(Exception):
730
  """Job was submitted, client should exit.
731

732
  This exception has one argument, the ID of the job that was
733
  submitted. The handler should print this ID.
734

735
  This is not an error, just a structured way to exit from clients.
736

737
  """
738

    
739

    
740
def SendJob(ops, cl=None):
741
  """Function to submit an opcode without waiting for the results.
742

743
  @type ops: list
744
  @param ops: list of opcodes
745
  @type cl: luxi.Client
746
  @param cl: the luxi client to use for communicating with the master;
747
             if None, a new client will be created
748

749
  """
750
  if cl is None:
751
    cl = GetClient()
752

    
753
  job_id = cl.SubmitJob(ops)
754

    
755
  return job_id
756

    
757

    
758
def PollJob(job_id, cl=None, feedback_fn=None):
759
  """Function to poll for the result of a job.
760

761
  @type job_id: job identified
762
  @param job_id: the job to poll for results
763
  @type cl: luxi.Client
764
  @param cl: the luxi client to use for communicating with the master;
765
             if None, a new client will be created
766

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

    
771
  prev_job_info = None
772
  prev_logmsg_serial = None
773

    
774
  while True:
775
    result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
776
                                 prev_logmsg_serial)
777
    if not result:
778
      # job not found, go away!
779
      raise errors.JobLost("Job with id %s lost" % job_id)
780

    
781
    # Split result, a tuple of (field values, log entries)
782
    (job_info, log_entries) = result
783
    (status, ) = job_info
784

    
785
    if log_entries:
786
      for log_entry in log_entries:
787
        (serial, timestamp, _, message) = log_entry
788
        if callable(feedback_fn):
789
          feedback_fn(log_entry[1:])
790
        else:
791
          encoded = utils.SafeEncode(message)
792
          ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)), encoded)
793
        prev_logmsg_serial = max(prev_logmsg_serial, serial)
794

    
795
    # TODO: Handle canceled and archived jobs
796
    elif status in (constants.JOB_STATUS_SUCCESS,
797
                    constants.JOB_STATUS_ERROR,
798
                    constants.JOB_STATUS_CANCELING,
799
                    constants.JOB_STATUS_CANCELED):
800
      break
801

    
802
    prev_job_info = job_info
803

    
804
  jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
805
  if not jobs:
806
    raise errors.JobLost("Job with id %s lost" % job_id)
807

    
808
  status, opstatus, result = jobs[0]
809
  if status == constants.JOB_STATUS_SUCCESS:
810
    return result
811
  elif status in (constants.JOB_STATUS_CANCELING,
812
                  constants.JOB_STATUS_CANCELED):
813
    raise errors.OpExecError("Job was canceled")
814
  else:
815
    has_ok = False
816
    for idx, (status, msg) in enumerate(zip(opstatus, result)):
817
      if status == constants.OP_STATUS_SUCCESS:
818
        has_ok = True
819
      elif status == constants.OP_STATUS_ERROR:
820
        errors.MaybeRaise(msg)
821
        if has_ok:
822
          raise errors.OpExecError("partial failure (opcode %d): %s" %
823
                                   (idx, msg))
824
        else:
825
          raise errors.OpExecError(str(msg))
826
    # default failure mode
827
    raise errors.OpExecError(result)
828

    
829

    
830
def SubmitOpCode(op, cl=None, feedback_fn=None):
831
  """Legacy function to submit an opcode.
832

833
  This is just a simple wrapper over the construction of the processor
834
  instance. It should be extended to better handle feedback and
835
  interaction functions.
836

837
  """
838
  if cl is None:
839
    cl = GetClient()
840

    
841
  job_id = SendJob([op], cl)
842

    
843
  op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
844

    
845
  return op_results[0]
846

    
847

    
848
def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
849
  """Wrapper around SubmitOpCode or SendJob.
850

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

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

858
  """
859
  if opts and opts.dry_run:
860
    op.dry_run = opts.dry_run
861
  if opts and opts.submit_only:
862
    job_id = SendJob([op], cl=cl)
863
    raise JobSubmittedException(job_id)
864
  else:
865
    return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
866

    
867

    
868
def GetClient():
869
  # TODO: Cache object?
870
  try:
871
    client = luxi.Client()
872
  except luxi.NoMasterError:
873
    master, myself = ssconf.GetMasterAndMyself()
874
    if master != myself:
875
      raise errors.OpPrereqError("This is not the master node, please connect"
876
                                 " to node '%s' and rerun the command" %
877
                                 master)
878
    else:
879
      raise
880
  return client
881

    
882

    
883
def FormatError(err):
884
  """Return a formatted error message for a given error.
885

886
  This function takes an exception instance and returns a tuple
887
  consisting of two values: first, the recommended exit code, and
888
  second, a string describing the error message (not
889
  newline-terminated).
890

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

    
954

    
955
def GenericMain(commands, override=None, aliases=None):
956
  """Generic main function for all the gnt-* commands.
957

958
  Arguments:
959
    - commands: a dictionary with a special structure, see the design doc
960
                for command line handling.
961
    - override: if not None, we expect a dictionary with keys that will
962
                override command line options; this can be used to pass
963
                options from the scripts to generic functions
964
    - aliases: dictionary with command aliases {'alias': 'target, ...}
965

966
  """
967
  # save the program name and the entire command line for later logging
968
  if sys.argv:
969
    binary = os.path.basename(sys.argv[0]) or sys.argv[0]
970
    if len(sys.argv) >= 2:
971
      binary += " " + sys.argv[1]
972
      old_cmdline = " ".join(sys.argv[2:])
973
    else:
974
      old_cmdline = ""
975
  else:
976
    binary = "<unknown program>"
977
    old_cmdline = ""
978

    
979
  if aliases is None:
980
    aliases = {}
981

    
982
  try:
983
    func, options, args = _ParseArgs(sys.argv, commands, aliases)
984
  except errors.ParameterError, err:
985
    result, err_msg = FormatError(err)
986
    ToStderr(err_msg)
987
    return 1
988

    
989
  if func is None: # parse error
990
    return 1
991

    
992
  if override is not None:
993
    for key, val in override.iteritems():
994
      setattr(options, key, val)
995

    
996
  utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
997
                     stderr_logging=True, program=binary)
998

    
999
  if old_cmdline:
1000
    logging.info("run with arguments '%s'", old_cmdline)
1001
  else:
1002
    logging.info("run with no arguments")
1003

    
1004
  try:
1005
    result = func(options, args)
1006
  except (errors.GenericError, luxi.ProtocolError,
1007
          JobSubmittedException), err:
1008
    result, err_msg = FormatError(err)
1009
    logging.exception("Error during command processing")
1010
    ToStderr(err_msg)
1011

    
1012
  return result
1013

    
1014

    
1015
def GenerateTable(headers, fields, separator, data,
1016
                  numfields=None, unitfields=None,
1017
                  units=None):
1018
  """Prints a table with headers and different fields.
1019

1020
  @type headers: dict
1021
  @param headers: dictionary mapping field names to headers for
1022
      the table
1023
  @type fields: list
1024
  @param fields: the field names corresponding to each row in
1025
      the data field
1026
  @param separator: the separator to be used; if this is None,
1027
      the default 'smart' algorithm is used which computes optimal
1028
      field width, otherwise just the separator is used between
1029
      each field
1030
  @type data: list
1031
  @param data: a list of lists, each sublist being one row to be output
1032
  @type numfields: list
1033
  @param numfields: a list with the fields that hold numeric
1034
      values and thus should be right-aligned
1035
  @type unitfields: list
1036
  @param unitfields: a list with the fields that hold numeric
1037
      values that should be formatted with the units field
1038
  @type units: string or None
1039
  @param units: the units we should use for formatting, or None for
1040
      automatic choice (human-readable for non-separator usage, otherwise
1041
      megabytes); this is a one-letter string
1042

1043
  """
1044
  if units is None:
1045
    if separator:
1046
      units = "m"
1047
    else:
1048
      units = "h"
1049

    
1050
  if numfields is None:
1051
    numfields = []
1052
  if unitfields is None:
1053
    unitfields = []
1054

    
1055
  numfields = utils.FieldSet(*numfields)
1056
  unitfields = utils.FieldSet(*unitfields)
1057

    
1058
  format_fields = []
1059
  for field in fields:
1060
    if headers and field not in headers:
1061
      # TODO: handle better unknown fields (either revert to old
1062
      # style of raising exception, or deal more intelligently with
1063
      # variable fields)
1064
      headers[field] = field
1065
    if separator is not None:
1066
      format_fields.append("%s")
1067
    elif numfields.Matches(field):
1068
      format_fields.append("%*s")
1069
    else:
1070
      format_fields.append("%-*s")
1071

    
1072
  if separator is None:
1073
    mlens = [0 for name in fields]
1074
    format = ' '.join(format_fields)
1075
  else:
1076
    format = separator.replace("%", "%%").join(format_fields)
1077

    
1078
  for row in data:
1079
    if row is None:
1080
      continue
1081
    for idx, val in enumerate(row):
1082
      if unitfields.Matches(fields[idx]):
1083
        try:
1084
          val = int(val)
1085
        except ValueError:
1086
          pass
1087
        else:
1088
          val = row[idx] = utils.FormatUnit(val, units)
1089
      val = row[idx] = str(val)
1090
      if separator is None:
1091
        mlens[idx] = max(mlens[idx], len(val))
1092

    
1093
  result = []
1094
  if headers:
1095
    args = []
1096
    for idx, name in enumerate(fields):
1097
      hdr = headers[name]
1098
      if separator is None:
1099
        mlens[idx] = max(mlens[idx], len(hdr))
1100
        args.append(mlens[idx])
1101
      args.append(hdr)
1102
    result.append(format % tuple(args))
1103

    
1104
  for line in data:
1105
    args = []
1106
    if line is None:
1107
      line = ['-' for _ in fields]
1108
    for idx in xrange(len(fields)):
1109
      if separator is None:
1110
        args.append(mlens[idx])
1111
      args.append(line[idx])
1112
    result.append(format % tuple(args))
1113

    
1114
  return result
1115

    
1116

    
1117
def FormatTimestamp(ts):
1118
  """Formats a given timestamp.
1119

1120
  @type ts: timestamp
1121
  @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
1122

1123
  @rtype: string
1124
  @return: a string with the formatted timestamp
1125

1126
  """
1127
  if not isinstance (ts, (tuple, list)) or len(ts) != 2:
1128
    return '?'
1129
  sec, usec = ts
1130
  return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
1131

    
1132

    
1133
def ParseTimespec(value):
1134
  """Parse a time specification.
1135

1136
  The following suffixed will be recognized:
1137

1138
    - s: seconds
1139
    - m: minutes
1140
    - h: hours
1141
    - d: day
1142
    - w: weeks
1143

1144
  Without any suffix, the value will be taken to be in seconds.
1145

1146
  """
1147
  value = str(value)
1148
  if not value:
1149
    raise errors.OpPrereqError("Empty time specification passed")
1150
  suffix_map = {
1151
    's': 1,
1152
    'm': 60,
1153
    'h': 3600,
1154
    'd': 86400,
1155
    'w': 604800,
1156
    }
1157
  if value[-1] not in suffix_map:
1158
    try:
1159
      value = int(value)
1160
    except ValueError:
1161
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1162
  else:
1163
    multiplier = suffix_map[value[-1]]
1164
    value = value[:-1]
1165
    if not value: # no data left after stripping the suffix
1166
      raise errors.OpPrereqError("Invalid time specification (only"
1167
                                 " suffix passed)")
1168
    try:
1169
      value = int(value) * multiplier
1170
    except ValueError:
1171
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1172
  return value
1173

    
1174

    
1175
def GetOnlineNodes(nodes, cl=None, nowarn=False):
1176
  """Returns the names of online nodes.
1177

1178
  This function will also log a warning on stderr with the names of
1179
  the online nodes.
1180

1181
  @param nodes: if not empty, use only this subset of nodes (minus the
1182
      offline ones)
1183
  @param cl: if not None, luxi client to use
1184
  @type nowarn: boolean
1185
  @param nowarn: by default, this function will output a note with the
1186
      offline nodes that are skipped; if this parameter is True the
1187
      note is not displayed
1188

1189
  """
1190
  if cl is None:
1191
    cl = GetClient()
1192

    
1193
  result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
1194
                         use_locking=False)
1195
  offline = [row[0] for row in result if row[1]]
1196
  if offline and not nowarn:
1197
    ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
1198
  return [row[0] for row in result if not row[1]]
1199

    
1200

    
1201
def _ToStream(stream, txt, *args):
1202
  """Write a message to a stream, bypassing the logging system
1203

1204
  @type stream: file object
1205
  @param stream: the file to which we should write
1206
  @type txt: str
1207
  @param txt: the message
1208

1209
  """
1210
  if args:
1211
    args = tuple(args)
1212
    stream.write(txt % args)
1213
  else:
1214
    stream.write(txt)
1215
  stream.write('\n')
1216
  stream.flush()
1217

    
1218

    
1219
def ToStdout(txt, *args):
1220
  """Write a message to stdout only, bypassing the logging system
1221

1222
  This is just a wrapper over _ToStream.
1223

1224
  @type txt: str
1225
  @param txt: the message
1226

1227
  """
1228
  _ToStream(sys.stdout, txt, *args)
1229

    
1230

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

1234
  This is just a wrapper over _ToStream.
1235

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

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

    
1242

    
1243
class JobExecutor(object):
1244
  """Class which manages the submission and execution of multiple jobs.
1245

1246
  Note that instances of this class should not be reused between
1247
  GetResults() calls.
1248

1249
  """
1250
  def __init__(self, cl=None, verbose=True):
1251
    self.queue = []
1252
    if cl is None:
1253
      cl = GetClient()
1254
    self.cl = cl
1255
    self.verbose = verbose
1256
    self.jobs = []
1257

    
1258
  def QueueJob(self, name, *ops):
1259
    """Record a job for later submit.
1260

1261
    @type name: string
1262
    @param name: a description of the job, will be used in WaitJobSet
1263
    """
1264
    self.queue.append((name, ops))
1265

    
1266
  def SubmitPending(self):
1267
    """Submit all pending jobs.
1268

1269
    """
1270
    results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1271
    for ((status, data), (name, _)) in zip(results, self.queue):
1272
      self.jobs.append((status, data, name))
1273

    
1274
  def GetResults(self):
1275
    """Wait for and return the results of all jobs.
1276

1277
    @rtype: list
1278
    @return: list of tuples (success, job results), in the same order
1279
        as the submitted jobs; if a job has failed, instead of the result
1280
        there will be the error message
1281

1282
    """
1283
    if not self.jobs:
1284
      self.SubmitPending()
1285
    results = []
1286
    if self.verbose:
1287
      ok_jobs = [row[1] for row in self.jobs if row[0]]
1288
      if ok_jobs:
1289
        ToStdout("Submitted jobs %s", ", ".join(ok_jobs))
1290
    for submit_status, jid, name in self.jobs:
1291
      if not submit_status:
1292
        ToStderr("Failed to submit job for %s: %s", name, jid)
1293
        results.append((False, jid))
1294
        continue
1295
      if self.verbose:
1296
        ToStdout("Waiting for job %s for %s...", jid, name)
1297
      try:
1298
        job_result = PollJob(jid, cl=self.cl)
1299
        success = True
1300
      except (errors.GenericError, luxi.ProtocolError), err:
1301
        _, job_result = FormatError(err)
1302
        success = False
1303
        # the error message will always be shown, verbose or not
1304
        ToStderr("Job %s for %s has failed: %s", jid, name, job_result)
1305

    
1306
      results.append((success, job_result))
1307
    return results
1308

    
1309
  def WaitOrShow(self, wait):
1310
    """Wait for job results or only print the job IDs.
1311

1312
    @type wait: boolean
1313
    @param wait: whether to wait or not
1314

1315
    """
1316
    if wait:
1317
      return self.GetResults()
1318
    else:
1319
      if not self.jobs:
1320
        self.SubmitPending()
1321
      for status, result, name in self.jobs:
1322
        if status:
1323
          ToStdout("%s: %s", result, name)
1324
        else:
1325
          ToStderr("Failure for %s: %s", name, result)