Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ b58726e8

History | View | Annotate | Download (48.6 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
  "ALLOCATABLE_OPT",
48
  "ALL_OPT",
49
  "AUTO_REPLACE_OPT",
50
  "BACKEND_OPT",
51
  "CLEANUP_OPT",
52
  "CONFIRM_OPT",
53
  "CP_SIZE_OPT",
54
  "DEBUG_OPT",
55
  "DEBUG_SIMERR_OPT",
56
  "DISKIDX_OPT",
57
  "DISK_OPT",
58
  "DISK_TEMPLATE_OPT",
59
  "DRAINED_OPT",
60
  "ENABLED_HV_OPT",
61
  "FIELDS_OPT",
62
  "FILESTORE_DIR_OPT",
63
  "FILESTORE_DRIVER_OPT",
64
  "HVLIST_OPT",
65
  "HVOPTS_OPT",
66
  "HYPERVISOR_OPT",
67
  "IALLOCATOR_OPT",
68
  "IGNORE_CONSIST_OPT",
69
  "IGNORE_FAILURES_OPT",
70
  "IGNORE_SIZE_OPT",
71
  "FORCE_OPT",
72
  "MC_OPT",
73
  "NET_OPT",
74
  "NEW_SECONDARY_OPT",
75
  "NIC_PARAMS_OPT",
76
  "NODE_LIST_OPT",
77
  "NODE_PLACEMENT_OPT",
78
  "NOHDR_OPT",
79
  "NOIPCHECK_OPT",
80
  "NOLVM_STORAGE_OPT",
81
  "NONICS_OPT",
82
  "NONLIVE_OPT",
83
  "NOSTART_OPT",
84
  "NOSSH_KEYCHECK_OPT",
85
  "NWSYNC_OPT",
86
  "ON_PRIMARY_OPT",
87
  "ON_SECONDARY_OPT",
88
  "OFFLINE_OPT",
89
  "OS_OPT",
90
  "OS_SIZE_OPT",
91
  "READD_OPT",
92
  "SECONDARY_IP_OPT",
93
  "SELECT_OS_OPT",
94
  "SEP_OPT",
95
  "SHOWCMD_OPT",
96
  "SINGLE_NODE_OPT",
97
  "SRC_DIR_OPT",
98
  "SRC_NODE_OPT",
99
  "SUBMIT_OPT",
100
  "STATIC_OPT",
101
  "SYNC_OPT",
102
  "TAG_SRC_OPT",
103
  "USEUNITS_OPT",
104
  "VERBOSE_OPT",
105
  "VG_NAME_OPT",
106
  # Generic functions for CLI programs
107
  "GenericMain",
108
  "GetClient",
109
  "GetOnlineNodes",
110
  "JobExecutor",
111
  "JobSubmittedException",
112
  "ParseTimespec",
113
  "SubmitOpCode",
114
  "SubmitOrSend",
115
  "UsesRPC",
116
  # Formatting functions
117
  "ToStderr", "ToStdout",
118
  "FormatError",
119
  "GenerateTable",
120
  "AskUser",
121
  "FormatTimestamp",
122
  # Tags functions
123
  "ListTags",
124
  "AddTags",
125
  "RemoveTags",
126
  # command line options support infrastructure
127
  "ARGS_MANY_INSTANCES",
128
  "ARGS_MANY_NODES",
129
  "ARGS_NONE",
130
  "ARGS_ONE_INSTANCE",
131
  "ARGS_ONE_NODE",
132
  "ArgChoice",
133
  "ArgCommand",
134
  "ArgFile",
135
  "ArgHost",
136
  "ArgInstance",
137
  "ArgJobId",
138
  "ArgNode",
139
  "ArgSuggest",
140
  "ArgUnknown",
141
  "OPT_COMPL_INST_ADD_NODES",
142
  "OPT_COMPL_MANY_NODES",
143
  "OPT_COMPL_ONE_IALLOCATOR",
144
  "OPT_COMPL_ONE_INSTANCE",
145
  "OPT_COMPL_ONE_NODE",
146
  "OPT_COMPL_ONE_OS",
147
  "cli_option",
148
  "SplitNodeOption",
149
  ]
150

    
151
NO_PREFIX = "no_"
152
UN_PREFIX = "-"
153

    
154

    
155
class _Argument:
156
  def __init__(self, min=0, max=None):
157
    self.min = min
158
    self.max = max
159

    
160
  def __repr__(self):
161
    return ("<%s min=%s max=%s>" %
162
            (self.__class__.__name__, self.min, self.max))
163

    
164

    
165
class ArgSuggest(_Argument):
166
  """Suggesting argument.
167

168
  Value can be any of the ones passed to the constructor.
169

170
  """
171
  def __init__(self, min=0, max=None, choices=None):
172
    _Argument.__init__(self, min=min, max=max)
173
    self.choices = choices
174

    
175
  def __repr__(self):
176
    return ("<%s min=%s max=%s choices=%r>" %
177
            (self.__class__.__name__, self.min, self.max, self.choices))
178

    
179

    
180
class ArgChoice(ArgSuggest):
181
  """Choice argument.
182

183
  Value can be any of the ones passed to the constructor. Like L{ArgSuggest},
184
  but value must be one of the choices.
185

186
  """
187

    
188

    
189
class ArgUnknown(_Argument):
190
  """Unknown argument to program (e.g. determined at runtime).
191

192
  """
193

    
194

    
195
class ArgInstance(_Argument):
196
  """Instances argument.
197

198
  """
199

    
200

    
201
class ArgNode(_Argument):
202
  """Node argument.
203

204
  """
205

    
206
class ArgJobId(_Argument):
207
  """Job ID argument.
208

209
  """
210

    
211

    
212
class ArgFile(_Argument):
213
  """File path argument.
214

215
  """
216

    
217

    
218
class ArgCommand(_Argument):
219
  """Command argument.
220

221
  """
222

    
223

    
224
class ArgHost(_Argument):
225
  """Host argument.
226

227
  """
228

    
229

    
230
ARGS_NONE = []
231
ARGS_MANY_INSTANCES = [ArgInstance()]
232
ARGS_MANY_NODES = [ArgNode()]
233
ARGS_ONE_INSTANCE = [ArgInstance(min=1, max=1)]
234
ARGS_ONE_NODE = [ArgNode(min=1, max=1)]
235

    
236

    
237

    
238
def _ExtractTagsObject(opts, args):
239
  """Extract the tag type object.
240

241
  Note that this function will modify its args parameter.
242

243
  """
244
  if not hasattr(opts, "tag_type"):
245
    raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
246
  kind = opts.tag_type
247
  if kind == constants.TAG_CLUSTER:
248
    retval = kind, kind
249
  elif kind == constants.TAG_NODE or kind == constants.TAG_INSTANCE:
250
    if not args:
251
      raise errors.OpPrereqError("no arguments passed to the command")
252
    name = args.pop(0)
253
    retval = kind, name
254
  else:
255
    raise errors.ProgrammerError("Unhandled tag type '%s'" % kind)
256
  return retval
257

    
258

    
259
def _ExtendTags(opts, args):
260
  """Extend the args if a source file has been given.
261

262
  This function will extend the tags with the contents of the file
263
  passed in the 'tags_source' attribute of the opts parameter. A file
264
  named '-' will be replaced by stdin.
265

266
  """
267
  fname = opts.tags_source
268
  if fname is None:
269
    return
270
  if fname == "-":
271
    new_fh = sys.stdin
272
  else:
273
    new_fh = open(fname, "r")
274
  new_data = []
275
  try:
276
    # we don't use the nice 'new_data = [line.strip() for line in fh]'
277
    # because of python bug 1633941
278
    while True:
279
      line = new_fh.readline()
280
      if not line:
281
        break
282
      new_data.append(line.strip())
283
  finally:
284
    new_fh.close()
285
  args.extend(new_data)
286

    
287

    
288
def ListTags(opts, args):
289
  """List the tags on a given object.
290

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

296
  """
297
  kind, name = _ExtractTagsObject(opts, args)
298
  op = opcodes.OpGetTags(kind=kind, name=name)
299
  result = SubmitOpCode(op)
300
  result = list(result)
301
  result.sort()
302
  for tag in result:
303
    ToStdout(tag)
304

    
305

    
306
def AddTags(opts, args):
307
  """Add tags on a given object.
308

309
  This is a generic implementation that knows how to deal with all
310
  three cases of tag objects (cluster, node, instance). The opts
311
  argument is expected to contain a tag_type field denoting what
312
  object type we work on.
313

314
  """
315
  kind, name = _ExtractTagsObject(opts, args)
316
  _ExtendTags(opts, args)
317
  if not args:
318
    raise errors.OpPrereqError("No tags to be added")
319
  op = opcodes.OpAddTags(kind=kind, name=name, tags=args)
320
  SubmitOpCode(op)
321

    
322

    
323
def RemoveTags(opts, args):
324
  """Remove tags from a given object.
325

326
  This is a generic implementation that knows how to deal with all
327
  three cases of tag objects (cluster, node, instance). The opts
328
  argument is expected to contain a tag_type field denoting what
329
  object type we work on.
330

331
  """
332
  kind, name = _ExtractTagsObject(opts, args)
333
  _ExtendTags(opts, args)
334
  if not args:
335
    raise errors.OpPrereqError("No tags to be removed")
336
  op = opcodes.OpDelTags(kind=kind, name=name, tags=args)
337
  SubmitOpCode(op)
338

    
339

    
340
def check_unit(option, opt, value):
341
  """OptParsers custom converter for units.
342

343
  """
344
  try:
345
    return utils.ParseUnit(value)
346
  except errors.UnitParseError, err:
347
    raise OptionValueError("option %s: %s" % (opt, err))
348

    
349

    
350
def _SplitKeyVal(opt, data):
351
  """Convert a KeyVal string into a dict.
352

353
  This function will convert a key=val[,...] string into a dict. Empty
354
  values will be converted specially: keys which have the prefix 'no_'
355
  will have the value=False and the prefix stripped, the others will
356
  have value=True.
357

358
  @type opt: string
359
  @param opt: a string holding the option name for which we process the
360
      data, used in building error messages
361
  @type data: string
362
  @param data: a string of the format key=val,key=val,...
363
  @rtype: dict
364
  @return: {key=val, key=val}
365
  @raises errors.ParameterError: if there are duplicate keys
366

367
  """
368
  kv_dict = {}
369
  if data:
370
    for elem in data.split(","):
371
      if "=" in elem:
372
        key, val = elem.split("=", 1)
373
      else:
374
        if elem.startswith(NO_PREFIX):
375
          key, val = elem[len(NO_PREFIX):], False
376
        elif elem.startswith(UN_PREFIX):
377
          key, val = elem[len(UN_PREFIX):], None
378
        else:
379
          key, val = elem, True
380
      if key in kv_dict:
381
        raise errors.ParameterError("Duplicate key '%s' in option %s" %
382
                                    (key, opt))
383
      kv_dict[key] = val
384
  return kv_dict
385

    
386

    
387
def check_ident_key_val(option, opt, value):
388
  """Custom parser for ident:key=val,key=val options.
389

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

393
  """
394
  if ":" not in value:
395
    ident, rest = value, ''
396
  else:
397
    ident, rest = value.split(":", 1)
398

    
399
  if ident.startswith(NO_PREFIX):
400
    if rest:
401
      msg = "Cannot pass options when removing parameter groups: %s" % value
402
      raise errors.ParameterError(msg)
403
    retval = (ident[len(NO_PREFIX):], False)
404
  elif ident.startswith(UN_PREFIX):
405
    if rest:
406
      msg = "Cannot pass options when removing parameter groups: %s" % value
407
      raise errors.ParameterError(msg)
408
    retval = (ident[len(UN_PREFIX):], None)
409
  else:
410
    kv_dict = _SplitKeyVal(opt, rest)
411
    retval = (ident, kv_dict)
412
  return retval
413

    
414

    
415
def check_key_val(option, opt, value):
416
  """Custom parser class for key=val,key=val options.
417

418
  This will store the parsed values as a dict {key: val}.
419

420
  """
421
  return _SplitKeyVal(opt, value)
422

    
423

    
424
# completion_suggestion is normally a list. Using numeric values not evaluating
425
# to False for dynamic completion.
426
(OPT_COMPL_MANY_NODES,
427
 OPT_COMPL_ONE_NODE,
428
 OPT_COMPL_ONE_INSTANCE,
429
 OPT_COMPL_ONE_OS,
430
 OPT_COMPL_ONE_IALLOCATOR,
431
 OPT_COMPL_INST_ADD_NODES) = range(100, 106)
432

    
433
OPT_COMPL_ALL = frozenset([
434
  OPT_COMPL_MANY_NODES,
435
  OPT_COMPL_ONE_NODE,
436
  OPT_COMPL_ONE_INSTANCE,
437
  OPT_COMPL_ONE_OS,
438
  OPT_COMPL_ONE_IALLOCATOR,
439
  OPT_COMPL_INST_ADD_NODES,
440
  ])
441

    
442

    
443
class CliOption(Option):
444
  """Custom option class for optparse.
445

446
  """
447
  ATTRS = Option.ATTRS + [
448
    "completion_suggest",
449
    ]
450
  TYPES = Option.TYPES + (
451
    "identkeyval",
452
    "keyval",
453
    "unit",
454
    )
455
  TYPE_CHECKER = Option.TYPE_CHECKER.copy()
456
  TYPE_CHECKER["identkeyval"] = check_ident_key_val
457
  TYPE_CHECKER["keyval"] = check_key_val
458
  TYPE_CHECKER["unit"] = check_unit
459

    
460

    
461
# optparse.py sets make_option, so we do it for our own option class, too
462
cli_option = CliOption
463

    
464

    
465
_YESNO = ("yes", "no")
466
_YORNO = "yes|no"
467

    
468
DEBUG_OPT = cli_option("-d", "--debug", default=False,
469
                       action="store_true",
470
                       help="Turn debugging on")
471

    
472
NOHDR_OPT = cli_option("--no-headers", default=False,
473
                       action="store_true", dest="no_headers",
474
                       help="Don't display column headers")
475

    
476
SEP_OPT = cli_option("--separator", default=None,
477
                     action="store", dest="separator",
478
                     help=("Separator between output fields"
479
                           " (defaults to one space)"))
480

    
481
USEUNITS_OPT = cli_option("--units", default=None,
482
                          dest="units", choices=('h', 'm', 'g', 't'),
483
                          help="Specify units for output (one of hmgt)")
484

    
485
FIELDS_OPT = cli_option("-o", "--output", dest="output", action="store",
486
                        type="string", metavar="FIELDS",
487
                        help="Comma separated list of output fields")
488

    
489
FORCE_OPT = cli_option("-f", "--force", dest="force", action="store_true",
490
                       default=False, help="Force the operation")
491

    
492
CONFIRM_OPT = cli_option("--yes", dest="confirm", action="store_true",
493
                         default=False, help="Do not require confirmation")
494

    
495
TAG_SRC_OPT = cli_option("--from", dest="tags_source",
496
                         default=None, help="File with tag names")
497

    
498
SUBMIT_OPT = cli_option("--submit", dest="submit_only",
499
                        default=False, action="store_true",
500
                        help=("Submit the job and return the job ID, but"
501
                              " don't wait for the job to finish"))
502

    
503
SYNC_OPT = cli_option("--sync", dest="do_locking",
504
                      default=False, action="store_true",
505
                      help=("Grab locks while doing the queries"
506
                            " in order to ensure more consistent results"))
507

    
508
_DRY_RUN_OPT = cli_option("--dry-run", default=False,
509
                          action="store_true",
510
                          help=("Do not execute the operation, just run the"
511
                                " check steps and verify it it could be"
512
                                " executed"))
513

    
514
VERBOSE_OPT = cli_option("-v", "--verbose", default=False,
515
                         action="store_true",
516
                         help="Increase the verbosity of the operation")
517

    
518
DEBUG_SIMERR_OPT = cli_option("--debug-simulate-errors", default=False,
519
                              action="store_true", dest="simulate_errors",
520
                              help="Debugging option that makes the operation"
521
                              " treat most runtime checks as failed")
522

    
523
NWSYNC_OPT = cli_option("--no-wait-for-sync", dest="wait_for_sync",
524
                        default=True, action="store_false",
525
                        help="Don't wait for sync (DANGEROUS!)")
526

    
527
DISK_TEMPLATE_OPT = cli_option("-t", "--disk-template", dest="disk_template",
528
                               help="Custom disk setup (diskless, file,"
529
                               " plain or drbd)",
530
                               default=None, metavar="TEMPL",
531
                               choices=list(constants.DISK_TEMPLATES))
532

    
533
NONICS_OPT = cli_option("--no-nics", default=False, action="store_true",
534
                        help="Do not create any network cards for"
535
                        " the instance")
536

    
537
FILESTORE_DIR_OPT = cli_option("--file-storage-dir", dest="file_storage_dir",
538
                               help="Relative path under default cluster-wide"
539
                               " file storage dir to store file-based disks",
540
                               default=None, metavar="<DIR>")
541

    
542
FILESTORE_DRIVER_OPT = cli_option("--file-driver", dest="file_driver",
543
                                  help="Driver to use for image files",
544
                                  default="loop", metavar="<DRIVER>",
545
                                  choices=list(constants.FILE_DRIVER))
546

    
547
IALLOCATOR_OPT = cli_option("-I", "--iallocator", metavar="<NAME>",
548
                            help="Select nodes for the instance automatically"
549
                            " using the <NAME> iallocator plugin",
550
                            default=None, type="string",
551
                            completion_suggest=OPT_COMPL_ONE_IALLOCATOR)
552

    
553
OS_OPT = cli_option("-o", "--os-type", dest="os", help="What OS to run",
554
                    metavar="<os>",
555
                    completion_suggest=OPT_COMPL_ONE_OS)
556

    
557
BACKEND_OPT = cli_option("-B", "--backend-parameters", dest="beparams",
558
                         type="keyval", default={},
559
                         help="Backend parameters")
560

    
561
HVOPTS_OPT =  cli_option("-H", "--hypervisor-parameters", type="keyval",
562
                         default={}, dest="hvparams",
563
                         help="Hypervisor parameters")
564

    
565
HYPERVISOR_OPT = cli_option("-H", "--hypervisor-parameters", dest="hypervisor",
566
                            help="Hypervisor and hypervisor options, in the"
567
                            " format hypervisor:option=value,option=value,...",
568
                            default=None, type="identkeyval")
569

    
570
HVLIST_OPT = cli_option("-H", "--hypervisor-parameters", dest="hvparams",
571
                        help="Hypervisor and hypervisor options, in the"
572
                        " format hypervisor:option=value,option=value,...",
573
                        default=[], action="append", type="identkeyval")
574

    
575
NOIPCHECK_OPT = cli_option("--no-ip-check", dest="ip_check", default=True,
576
                           action="store_false",
577
                           help="Don't check that the instance's IP"
578
                           " is alive")
579

    
580
NET_OPT = cli_option("--net",
581
                     help="NIC parameters", default=[],
582
                     dest="nics", action="append", type="identkeyval")
583

    
584
DISK_OPT = cli_option("--disk", help="Disk parameters", default=[],
585
                      dest="disks", action="append", type="identkeyval")
586

    
587
DISKIDX_OPT = cli_option("--disks", dest="disks", default=None,
588
                         help="Comma-separated list of disks"
589
                         " indices to act on (e.g. 0,2) (optional,"
590
                         " defaults to all disks)")
591

    
592
OS_SIZE_OPT = cli_option("-s", "--os-size", dest="sd_size",
593
                         help="Enforces a single-disk configuration using the"
594
                         " given disk size, in MiB unless a suffix is used",
595
                         default=None, type="unit", metavar="<size>")
596

    
597
IGNORE_CONSIST_OPT = cli_option("--ignore-consistency",
598
                                dest="ignore_consistency",
599
                                action="store_true", default=False,
600
                                help="Ignore the consistency of the disks on"
601
                                " the secondary")
602

    
603
NONLIVE_OPT = cli_option("--non-live", dest="live",
604
                         default=True, action="store_false",
605
                         help="Do a non-live migration (this usually means"
606
                         " freeze the instance, save the state, transfer and"
607
                         " only then resume running on the secondary node)")
608

    
609
NODE_PLACEMENT_OPT = cli_option("-n", "--node", dest="node",
610
                                help="Target node and optional secondary node",
611
                                metavar="<pnode>[:<snode>]",
612
                                completion_suggest=OPT_COMPL_INST_ADD_NODES)
613

    
614
NODE_LIST_OPT = cli_option("-n", "--node", dest="nodes", default=[],
615
                           action="append", metavar="<node>",
616
                           help="Use only this node (can be used multiple"
617
                           " times, if not given defaults to all nodes)",
618
                           completion_suggest=OPT_COMPL_ONE_NODE)
619

    
620
SINGLE_NODE_OPT = cli_option("-n", "--node", dest="node", help="Target node",
621
                             metavar="<node>",
622
                             completion_suggest=OPT_COMPL_ONE_NODE)
623

    
624
NOSTART_OPT = cli_option("--no-start", dest="start", default=True,
625
                         action="store_false",
626
                         help="Don't start the instance after creation")
627

    
628
SHOWCMD_OPT = cli_option("--show-cmd", dest="show_command",
629
                         action="store_true", default=False,
630
                         help="Show command instead of executing it")
631

    
632
CLEANUP_OPT = cli_option("--cleanup", dest="cleanup",
633
                         default=False, action="store_true",
634
                         help="Instead of performing the migration, try to"
635
                         " recover from a failed cleanup. This is safe"
636
                         " to run even if the instance is healthy, but it"
637
                         " will create extra replication traffic and "
638
                         " disrupt briefly the replication (like during the"
639
                         " migration")
640

    
641
STATIC_OPT = cli_option("-s", "--static", dest="static",
642
                        action="store_true", default=False,
643
                        help="Only show configuration data, not runtime data")
644

    
645
ALL_OPT = cli_option("--all", dest="show_all",
646
                     default=False, action="store_true",
647
                     help="Show info on all instances on the cluster."
648
                     " This can take a long time to run, use wisely")
649

    
650
SELECT_OS_OPT = cli_option("--select-os", dest="select_os",
651
                           action="store_true", default=False,
652
                           help="Interactive OS reinstall, lists available"
653
                           " OS templates for selection")
654

    
655
IGNORE_FAILURES_OPT = cli_option("--ignore-failures", dest="ignore_failures",
656
                                 action="store_true", default=False,
657
                                 help="Remove the instance from the cluster"
658
                                 " configuration even if there are failures"
659
                                 " during the removal process")
660

    
661
NEW_SECONDARY_OPT = cli_option("-n", "--new-secondary", dest="dst_node",
662
                               help="Specifies the new secondary node",
663
                               metavar="NODE", default=None,
664
                               completion_suggest=OPT_COMPL_ONE_NODE)
665

    
666
ON_PRIMARY_OPT = cli_option("-p", "--on-primary", dest="on_primary",
667
                            default=False, action="store_true",
668
                            help="Replace the disk(s) on the primary"
669
                            " node (only for the drbd template)")
670

    
671
ON_SECONDARY_OPT = cli_option("-s", "--on-secondary", dest="on_secondary",
672
                              default=False, action="store_true",
673
                              help="Replace the disk(s) on the secondary"
674
                              " node (only for the drbd template)")
675

    
676
AUTO_REPLACE_OPT = cli_option("-a", "--auto", dest="auto",
677
                              default=False, action="store_true",
678
                              help="Automatically replace faulty disks"
679
                              " (only for the drbd template)")
680

    
681
IGNORE_SIZE_OPT = cli_option("--ignore-size", dest="ignore_size",
682
                             default=False, action="store_true",
683
                             help="Ignore current recorded size"
684
                             " (useful for forcing activation when"
685
                             " the recorded size is wrong)")
686

    
687
SRC_NODE_OPT = cli_option("--src-node", dest="src_node", help="Source node",
688
                          metavar="<node>",
689
                          completion_suggest=OPT_COMPL_ONE_NODE)
690

    
691
SRC_DIR_OPT = cli_option("--src-dir", dest="src_dir", help="Source directory",
692
                         metavar="<dir>")
693

    
694
SECONDARY_IP_OPT = cli_option("-s", "--secondary-ip", dest="secondary_ip",
695
                              help="Specify the secondary ip for the node",
696
                              metavar="ADDRESS", default=None)
697

    
698
READD_OPT = cli_option("--readd", dest="readd",
699
                       default=False, action="store_true",
700
                       help="Readd old node after replacing it")
701

    
702
NOSSH_KEYCHECK_OPT = cli_option("--no-ssh-key-check", dest="ssh_key_check",
703
                                default=True, action="store_false",
704
                                help="Disable SSH key fingerprint checking")
705

    
706

    
707
MC_OPT = cli_option("-C", "--master-candidate", dest="master_candidate",
708
                    choices=_YESNO, default=None, metavar=_YORNO,
709
                    help="Set the master_candidate flag on the node")
710

    
711
OFFLINE_OPT = cli_option("-O", "--offline", dest="offline", metavar=_YORNO,
712
                         choices=_YESNO, default=None,
713
                         help="Set the offline flag on the node")
714

    
715
DRAINED_OPT = cli_option("-D", "--drained", dest="drained", metavar=_YORNO,
716
                         choices=_YESNO, default=None,
717
                         help="Set the drained flag on the node")
718

    
719
ALLOCATABLE_OPT = cli_option("--allocatable", dest="allocatable",
720
                             choices=_YESNO, default=None, metavar=_YORNO,
721
                             help="Set the allocatable flag on a volume")
722

    
723
NOLVM_STORAGE_OPT = cli_option("--no-lvm-storage", dest="lvm_storage",
724
                               help="Disable support for lvm based instances"
725
                               " (cluster-wide)",
726
                               action="store_false", default=True)
727

    
728
ENABLED_HV_OPT = cli_option("--enabled-hypervisors",
729
                            dest="enabled_hypervisors",
730
                            help="Comma-separated list of hypervisors",
731
                            type="string", default=None)
732

    
733
NIC_PARAMS_OPT = cli_option("-N", "--nic-parameters", dest="nicparams",
734
                            type="keyval", default={},
735
                            help="NIC parameters")
736

    
737
CP_SIZE_OPT = cli_option("-C", "--candidate-pool-size", default=None,
738
                         dest="candidate_pool_size", type="int",
739
                         help="Set the candidate pool size")
740

    
741
VG_NAME_OPT = cli_option("-g", "--vg-name", dest="vg_name",
742
                         help="Enables LVM and specifies the volume group"
743
                         " name (cluster-wide) for disk allocation [xenvg]",
744
                         metavar="VG", default=None)
745

    
746

    
747

    
748
def _ParseArgs(argv, commands, aliases):
749
  """Parser for the command line arguments.
750

751
  This function parses the arguments and returns the function which
752
  must be executed together with its (modified) arguments.
753

754
  @param argv: the command line
755
  @param commands: dictionary with special contents, see the design
756
      doc for cmdline handling
757
  @param aliases: dictionary with command aliases {'alias': 'target, ...}
758

759
  """
760
  if len(argv) == 0:
761
    binary = "<command>"
762
  else:
763
    binary = argv[0].split("/")[-1]
764

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

    
771
  if len(argv) < 2 or not (argv[1] in commands or
772
                           argv[1] in aliases):
773
    # let's do a nice thing
774
    sortedcmds = commands.keys()
775
    sortedcmds.sort()
776

    
777
    ToStdout("Usage: %s {command} [options...] [argument...]", binary)
778
    ToStdout("%s <command> --help to see details, or man %s", binary, binary)
779
    ToStdout("")
780

    
781
    # compute the max line length for cmd + usage
782
    mlen = max([len(" %s" % cmd) for cmd in commands])
783
    mlen = min(60, mlen) # should not get here...
784

    
785
    # and format a nice command list
786
    ToStdout("Commands:")
787
    for cmd in sortedcmds:
788
      cmdstr = " %s" % (cmd,)
789
      help_text = commands[cmd][4]
790
      help_lines = textwrap.wrap(help_text, 79 - 3 - mlen)
791
      ToStdout("%-*s - %s", mlen, cmdstr, help_lines.pop(0))
792
      for line in help_lines:
793
        ToStdout("%-*s   %s", mlen, "", line)
794

    
795
    ToStdout("")
796

    
797
    return None, None, None
798

    
799
  # get command, unalias it, and look it up in commands
800
  cmd = argv.pop(1)
801
  if cmd in aliases:
802
    if cmd in commands:
803
      raise errors.ProgrammerError("Alias '%s' overrides an existing"
804
                                   " command" % cmd)
805

    
806
    if aliases[cmd] not in commands:
807
      raise errors.ProgrammerError("Alias '%s' maps to non-existing"
808
                                   " command '%s'" % (cmd, aliases[cmd]))
809

    
810
    cmd = aliases[cmd]
811

    
812
  func, args_def, parser_opts, usage, description = commands[cmd]
813
  parser = OptionParser(option_list=parser_opts + [_DRY_RUN_OPT],
814
                        description=description,
815
                        formatter=TitledHelpFormatter(),
816
                        usage="%%prog %s %s" % (cmd, usage))
817
  parser.disable_interspersed_args()
818
  options, args = parser.parse_args()
819

    
820
  if not _CheckArguments(cmd, args_def, args):
821
    return None, None, None
822

    
823
  return func, options, args
824

    
825

    
826
def _CheckArguments(cmd, args_def, args):
827
  """Verifies the arguments using the argument definition.
828

829
  Algorithm:
830

831
    1. Abort with error if values specified by user but none expected.
832

833
    1. For each argument in definition
834

835
      1. Keep running count of minimum number of values (min_count)
836
      1. Keep running count of maximum number of values (max_count)
837
      1. If it has an unlimited number of values
838

839
        1. Abort with error if it's not the last argument in the definition
840

841
    1. If last argument has limited number of values
842

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

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

847
  """
848
  if args and not args_def:
849
    ToStderr("Error: Command %s expects no arguments", cmd)
850
    return False
851

    
852
  min_count = None
853
  max_count = None
854
  check_max = None
855

    
856
  last_idx = len(args_def) - 1
857

    
858
  for idx, arg in enumerate(args_def):
859
    if min_count is None:
860
      min_count = arg.min
861
    elif arg.min is not None:
862
      min_count += arg.min
863

    
864
    if max_count is None:
865
      max_count = arg.max
866
    elif arg.max is not None:
867
      max_count += arg.max
868

    
869
    if idx == last_idx:
870
      check_max = (arg.max is not None)
871

    
872
    elif arg.max is None:
873
      raise errors.ProgrammerError("Only the last argument can have max=None")
874

    
875
  if check_max:
876
    # Command with exact number of arguments
877
    if (min_count is not None and max_count is not None and
878
        min_count == max_count and len(args) != min_count):
879
      ToStderr("Error: Command %s expects %d argument(s)", cmd, min_count)
880
      return False
881

    
882
    # Command with limited number of arguments
883
    if max_count is not None and len(args) > max_count:
884
      ToStderr("Error: Command %s expects only %d argument(s)",
885
               cmd, max_count)
886
      return False
887

    
888
  # Command with some required arguments
889
  if min_count is not None and len(args) < min_count:
890
    ToStderr("Error: Command %s expects at least %d argument(s)",
891
             cmd, min_count)
892
    return False
893

    
894
  return True
895

    
896

    
897
def SplitNodeOption(value):
898
  """Splits the value of a --node option.
899

900
  """
901
  if value and ':' in value:
902
    return value.split(':', 1)
903
  else:
904
    return (value, None)
905

    
906

    
907
def UsesRPC(fn):
908
  def wrapper(*args, **kwargs):
909
    rpc.Init()
910
    try:
911
      return fn(*args, **kwargs)
912
    finally:
913
      rpc.Shutdown()
914
  return wrapper
915

    
916

    
917
def AskUser(text, choices=None):
918
  """Ask the user a question.
919

920
  @param text: the question to ask
921

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

927
  @return: one of the return values from the choices list; if input is
928
      not possible (i.e. not running with a tty, we return the last
929
      entry from the list
930

931
  """
932
  if choices is None:
933
    choices = [('y', True, 'Perform the operation'),
934
               ('n', False, 'Do not perform the operation')]
935
  if not choices or not isinstance(choices, list):
936
    raise errors.ProgrammerError("Invalid choices argument to AskUser")
937
  for entry in choices:
938
    if not isinstance(entry, tuple) or len(entry) < 3 or entry[0] == '?':
939
      raise errors.ProgrammerError("Invalid choices element to AskUser")
940

    
941
  answer = choices[-1][1]
942
  new_text = []
943
  for line in text.splitlines():
944
    new_text.append(textwrap.fill(line, 70, replace_whitespace=False))
945
  text = "\n".join(new_text)
946
  try:
947
    f = file("/dev/tty", "a+")
948
  except IOError:
949
    return answer
950
  try:
951
    chars = [entry[0] for entry in choices]
952
    chars[-1] = "[%s]" % chars[-1]
953
    chars.append('?')
954
    maps = dict([(entry[0], entry[1]) for entry in choices])
955
    while True:
956
      f.write(text)
957
      f.write('\n')
958
      f.write("/".join(chars))
959
      f.write(": ")
960
      line = f.readline(2).strip().lower()
961
      if line in maps:
962
        answer = maps[line]
963
        break
964
      elif line == '?':
965
        for entry in choices:
966
          f.write(" %s - %s\n" % (entry[0], entry[2]))
967
        f.write("\n")
968
        continue
969
  finally:
970
    f.close()
971
  return answer
972

    
973

    
974
class JobSubmittedException(Exception):
975
  """Job was submitted, client should exit.
976

977
  This exception has one argument, the ID of the job that was
978
  submitted. The handler should print this ID.
979

980
  This is not an error, just a structured way to exit from clients.
981

982
  """
983

    
984

    
985
def SendJob(ops, cl=None):
986
  """Function to submit an opcode without waiting for the results.
987

988
  @type ops: list
989
  @param ops: list of opcodes
990
  @type cl: luxi.Client
991
  @param cl: the luxi client to use for communicating with the master;
992
             if None, a new client will be created
993

994
  """
995
  if cl is None:
996
    cl = GetClient()
997

    
998
  job_id = cl.SubmitJob(ops)
999

    
1000
  return job_id
1001

    
1002

    
1003
def PollJob(job_id, cl=None, feedback_fn=None):
1004
  """Function to poll for the result of a job.
1005

1006
  @type job_id: job identified
1007
  @param job_id: the job to poll for results
1008
  @type cl: luxi.Client
1009
  @param cl: the luxi client to use for communicating with the master;
1010
             if None, a new client will be created
1011

1012
  """
1013
  if cl is None:
1014
    cl = GetClient()
1015

    
1016
  prev_job_info = None
1017
  prev_logmsg_serial = None
1018

    
1019
  while True:
1020
    result = cl.WaitForJobChange(job_id, ["status"], prev_job_info,
1021
                                 prev_logmsg_serial)
1022
    if not result:
1023
      # job not found, go away!
1024
      raise errors.JobLost("Job with id %s lost" % job_id)
1025

    
1026
    # Split result, a tuple of (field values, log entries)
1027
    (job_info, log_entries) = result
1028
    (status, ) = job_info
1029

    
1030
    if log_entries:
1031
      for log_entry in log_entries:
1032
        (serial, timestamp, _, message) = log_entry
1033
        if callable(feedback_fn):
1034
          feedback_fn(log_entry[1:])
1035
        else:
1036
          encoded = utils.SafeEncode(message)
1037
          ToStdout("%s %s", time.ctime(utils.MergeTime(timestamp)), encoded)
1038
        prev_logmsg_serial = max(prev_logmsg_serial, serial)
1039

    
1040
    # TODO: Handle canceled and archived jobs
1041
    elif status in (constants.JOB_STATUS_SUCCESS,
1042
                    constants.JOB_STATUS_ERROR,
1043
                    constants.JOB_STATUS_CANCELING,
1044
                    constants.JOB_STATUS_CANCELED):
1045
      break
1046

    
1047
    prev_job_info = job_info
1048

    
1049
  jobs = cl.QueryJobs([job_id], ["status", "opstatus", "opresult"])
1050
  if not jobs:
1051
    raise errors.JobLost("Job with id %s lost" % job_id)
1052

    
1053
  status, opstatus, result = jobs[0]
1054
  if status == constants.JOB_STATUS_SUCCESS:
1055
    return result
1056
  elif status in (constants.JOB_STATUS_CANCELING,
1057
                  constants.JOB_STATUS_CANCELED):
1058
    raise errors.OpExecError("Job was canceled")
1059
  else:
1060
    has_ok = False
1061
    for idx, (status, msg) in enumerate(zip(opstatus, result)):
1062
      if status == constants.OP_STATUS_SUCCESS:
1063
        has_ok = True
1064
      elif status == constants.OP_STATUS_ERROR:
1065
        errors.MaybeRaise(msg)
1066
        if has_ok:
1067
          raise errors.OpExecError("partial failure (opcode %d): %s" %
1068
                                   (idx, msg))
1069
        else:
1070
          raise errors.OpExecError(str(msg))
1071
    # default failure mode
1072
    raise errors.OpExecError(result)
1073

    
1074

    
1075
def SubmitOpCode(op, cl=None, feedback_fn=None):
1076
  """Legacy function to submit an opcode.
1077

1078
  This is just a simple wrapper over the construction of the processor
1079
  instance. It should be extended to better handle feedback and
1080
  interaction functions.
1081

1082
  """
1083
  if cl is None:
1084
    cl = GetClient()
1085

    
1086
  job_id = SendJob([op], cl)
1087

    
1088
  op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
1089

    
1090
  return op_results[0]
1091

    
1092

    
1093
def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
1094
  """Wrapper around SubmitOpCode or SendJob.
1095

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

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

1103
  """
1104
  if opts and opts.dry_run:
1105
    op.dry_run = opts.dry_run
1106
  if opts and opts.submit_only:
1107
    job_id = SendJob([op], cl=cl)
1108
    raise JobSubmittedException(job_id)
1109
  else:
1110
    return SubmitOpCode(op, cl=cl, feedback_fn=feedback_fn)
1111

    
1112

    
1113
def GetClient():
1114
  # TODO: Cache object?
1115
  try:
1116
    client = luxi.Client()
1117
  except luxi.NoMasterError:
1118
    master, myself = ssconf.GetMasterAndMyself()
1119
    if master != myself:
1120
      raise errors.OpPrereqError("This is not the master node, please connect"
1121
                                 " to node '%s' and rerun the command" %
1122
                                 master)
1123
    else:
1124
      raise
1125
  return client
1126

    
1127

    
1128
def FormatError(err):
1129
  """Return a formatted error message for a given error.
1130

1131
  This function takes an exception instance and returns a tuple
1132
  consisting of two values: first, the recommended exit code, and
1133
  second, a string describing the error message (not
1134
  newline-terminated).
1135

1136
  """
1137
  retcode = 1
1138
  obuf = StringIO()
1139
  msg = str(err)
1140
  if isinstance(err, errors.ConfigurationError):
1141
    txt = "Corrupt configuration file: %s" % msg
1142
    logging.error(txt)
1143
    obuf.write(txt + "\n")
1144
    obuf.write("Aborting.")
1145
    retcode = 2
1146
  elif isinstance(err, errors.HooksAbort):
1147
    obuf.write("Failure: hooks execution failed:\n")
1148
    for node, script, out in err.args[0]:
1149
      if out:
1150
        obuf.write("  node: %s, script: %s, output: %s\n" %
1151
                   (node, script, out))
1152
      else:
1153
        obuf.write("  node: %s, script: %s (no output)\n" %
1154
                   (node, script))
1155
  elif isinstance(err, errors.HooksFailure):
1156
    obuf.write("Failure: hooks general failure: %s" % msg)
1157
  elif isinstance(err, errors.ResolverError):
1158
    this_host = utils.HostInfo.SysName()
1159
    if err.args[0] == this_host:
1160
      msg = "Failure: can't resolve my own hostname ('%s')"
1161
    else:
1162
      msg = "Failure: can't resolve hostname '%s'"
1163
    obuf.write(msg % err.args[0])
1164
  elif isinstance(err, errors.OpPrereqError):
1165
    obuf.write("Failure: prerequisites not met for this"
1166
               " operation:\n%s" % msg)
1167
  elif isinstance(err, errors.OpExecError):
1168
    obuf.write("Failure: command execution error:\n%s" % msg)
1169
  elif isinstance(err, errors.TagError):
1170
    obuf.write("Failure: invalid tag(s) given:\n%s" % msg)
1171
  elif isinstance(err, errors.JobQueueDrainError):
1172
    obuf.write("Failure: the job queue is marked for drain and doesn't"
1173
               " accept new requests\n")
1174
  elif isinstance(err, errors.JobQueueFull):
1175
    obuf.write("Failure: the job queue is full and doesn't accept new"
1176
               " job submissions until old jobs are archived\n")
1177
  elif isinstance(err, errors.TypeEnforcementError):
1178
    obuf.write("Parameter Error: %s" % msg)
1179
  elif isinstance(err, errors.ParameterError):
1180
    obuf.write("Failure: unknown/wrong parameter name '%s'" % msg)
1181
  elif isinstance(err, errors.GenericError):
1182
    obuf.write("Unhandled Ganeti error: %s" % msg)
1183
  elif isinstance(err, luxi.NoMasterError):
1184
    obuf.write("Cannot communicate with the master daemon.\nIs it running"
1185
               " and listening for connections?")
1186
  elif isinstance(err, luxi.TimeoutError):
1187
    obuf.write("Timeout while talking to the master daemon. Error:\n"
1188
               "%s" % msg)
1189
  elif isinstance(err, luxi.ProtocolError):
1190
    obuf.write("Unhandled protocol error while talking to the master daemon:\n"
1191
               "%s" % msg)
1192
  elif isinstance(err, JobSubmittedException):
1193
    obuf.write("JobID: %s\n" % err.args[0])
1194
    retcode = 0
1195
  else:
1196
    obuf.write("Unhandled exception: %s" % msg)
1197
  return retcode, obuf.getvalue().rstrip('\n')
1198

    
1199

    
1200
def GenericMain(commands, override=None, aliases=None):
1201
  """Generic main function for all the gnt-* commands.
1202

1203
  Arguments:
1204
    - commands: a dictionary with a special structure, see the design doc
1205
                for command line handling.
1206
    - override: if not None, we expect a dictionary with keys that will
1207
                override command line options; this can be used to pass
1208
                options from the scripts to generic functions
1209
    - aliases: dictionary with command aliases {'alias': 'target, ...}
1210

1211
  """
1212
  # save the program name and the entire command line for later logging
1213
  if sys.argv:
1214
    binary = os.path.basename(sys.argv[0]) or sys.argv[0]
1215
    if len(sys.argv) >= 2:
1216
      binary += " " + sys.argv[1]
1217
      old_cmdline = " ".join(sys.argv[2:])
1218
    else:
1219
      old_cmdline = ""
1220
  else:
1221
    binary = "<unknown program>"
1222
    old_cmdline = ""
1223

    
1224
  if aliases is None:
1225
    aliases = {}
1226

    
1227
  try:
1228
    func, options, args = _ParseArgs(sys.argv, commands, aliases)
1229
  except errors.ParameterError, err:
1230
    result, err_msg = FormatError(err)
1231
    ToStderr(err_msg)
1232
    return 1
1233

    
1234
  if func is None: # parse error
1235
    return 1
1236

    
1237
  if override is not None:
1238
    for key, val in override.iteritems():
1239
      setattr(options, key, val)
1240

    
1241
  utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
1242
                     stderr_logging=True, program=binary)
1243

    
1244
  if old_cmdline:
1245
    logging.info("run with arguments '%s'", old_cmdline)
1246
  else:
1247
    logging.info("run with no arguments")
1248

    
1249
  try:
1250
    result = func(options, args)
1251
  except (errors.GenericError, luxi.ProtocolError,
1252
          JobSubmittedException), err:
1253
    result, err_msg = FormatError(err)
1254
    logging.exception("Error during command processing")
1255
    ToStderr(err_msg)
1256

    
1257
  return result
1258

    
1259

    
1260
def GenerateTable(headers, fields, separator, data,
1261
                  numfields=None, unitfields=None,
1262
                  units=None):
1263
  """Prints a table with headers and different fields.
1264

1265
  @type headers: dict
1266
  @param headers: dictionary mapping field names to headers for
1267
      the table
1268
  @type fields: list
1269
  @param fields: the field names corresponding to each row in
1270
      the data field
1271
  @param separator: the separator to be used; if this is None,
1272
      the default 'smart' algorithm is used which computes optimal
1273
      field width, otherwise just the separator is used between
1274
      each field
1275
  @type data: list
1276
  @param data: a list of lists, each sublist being one row to be output
1277
  @type numfields: list
1278
  @param numfields: a list with the fields that hold numeric
1279
      values and thus should be right-aligned
1280
  @type unitfields: list
1281
  @param unitfields: a list with the fields that hold numeric
1282
      values that should be formatted with the units field
1283
  @type units: string or None
1284
  @param units: the units we should use for formatting, or None for
1285
      automatic choice (human-readable for non-separator usage, otherwise
1286
      megabytes); this is a one-letter string
1287

1288
  """
1289
  if units is None:
1290
    if separator:
1291
      units = "m"
1292
    else:
1293
      units = "h"
1294

    
1295
  if numfields is None:
1296
    numfields = []
1297
  if unitfields is None:
1298
    unitfields = []
1299

    
1300
  numfields = utils.FieldSet(*numfields)
1301
  unitfields = utils.FieldSet(*unitfields)
1302

    
1303
  format_fields = []
1304
  for field in fields:
1305
    if headers and field not in headers:
1306
      # TODO: handle better unknown fields (either revert to old
1307
      # style of raising exception, or deal more intelligently with
1308
      # variable fields)
1309
      headers[field] = field
1310
    if separator is not None:
1311
      format_fields.append("%s")
1312
    elif numfields.Matches(field):
1313
      format_fields.append("%*s")
1314
    else:
1315
      format_fields.append("%-*s")
1316

    
1317
  if separator is None:
1318
    mlens = [0 for name in fields]
1319
    format = ' '.join(format_fields)
1320
  else:
1321
    format = separator.replace("%", "%%").join(format_fields)
1322

    
1323
  for row in data:
1324
    if row is None:
1325
      continue
1326
    for idx, val in enumerate(row):
1327
      if unitfields.Matches(fields[idx]):
1328
        try:
1329
          val = int(val)
1330
        except ValueError:
1331
          pass
1332
        else:
1333
          val = row[idx] = utils.FormatUnit(val, units)
1334
      val = row[idx] = str(val)
1335
      if separator is None:
1336
        mlens[idx] = max(mlens[idx], len(val))
1337

    
1338
  result = []
1339
  if headers:
1340
    args = []
1341
    for idx, name in enumerate(fields):
1342
      hdr = headers[name]
1343
      if separator is None:
1344
        mlens[idx] = max(mlens[idx], len(hdr))
1345
        args.append(mlens[idx])
1346
      args.append(hdr)
1347
    result.append(format % tuple(args))
1348

    
1349
  for line in data:
1350
    args = []
1351
    if line is None:
1352
      line = ['-' for _ in fields]
1353
    for idx in xrange(len(fields)):
1354
      if separator is None:
1355
        args.append(mlens[idx])
1356
      args.append(line[idx])
1357
    result.append(format % tuple(args))
1358

    
1359
  return result
1360

    
1361

    
1362
def FormatTimestamp(ts):
1363
  """Formats a given timestamp.
1364

1365
  @type ts: timestamp
1366
  @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
1367

1368
  @rtype: string
1369
  @return: a string with the formatted timestamp
1370

1371
  """
1372
  if not isinstance (ts, (tuple, list)) or len(ts) != 2:
1373
    return '?'
1374
  sec, usec = ts
1375
  return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
1376

    
1377

    
1378
def ParseTimespec(value):
1379
  """Parse a time specification.
1380

1381
  The following suffixed will be recognized:
1382

1383
    - s: seconds
1384
    - m: minutes
1385
    - h: hours
1386
    - d: day
1387
    - w: weeks
1388

1389
  Without any suffix, the value will be taken to be in seconds.
1390

1391
  """
1392
  value = str(value)
1393
  if not value:
1394
    raise errors.OpPrereqError("Empty time specification passed")
1395
  suffix_map = {
1396
    's': 1,
1397
    'm': 60,
1398
    'h': 3600,
1399
    'd': 86400,
1400
    'w': 604800,
1401
    }
1402
  if value[-1] not in suffix_map:
1403
    try:
1404
      value = int(value)
1405
    except ValueError:
1406
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1407
  else:
1408
    multiplier = suffix_map[value[-1]]
1409
    value = value[:-1]
1410
    if not value: # no data left after stripping the suffix
1411
      raise errors.OpPrereqError("Invalid time specification (only"
1412
                                 " suffix passed)")
1413
    try:
1414
      value = int(value) * multiplier
1415
    except ValueError:
1416
      raise errors.OpPrereqError("Invalid time specification '%s'" % value)
1417
  return value
1418

    
1419

    
1420
def GetOnlineNodes(nodes, cl=None, nowarn=False):
1421
  """Returns the names of online nodes.
1422

1423
  This function will also log a warning on stderr with the names of
1424
  the online nodes.
1425

1426
  @param nodes: if not empty, use only this subset of nodes (minus the
1427
      offline ones)
1428
  @param cl: if not None, luxi client to use
1429
  @type nowarn: boolean
1430
  @param nowarn: by default, this function will output a note with the
1431
      offline nodes that are skipped; if this parameter is True the
1432
      note is not displayed
1433

1434
  """
1435
  if cl is None:
1436
    cl = GetClient()
1437

    
1438
  result = cl.QueryNodes(names=nodes, fields=["name", "offline"],
1439
                         use_locking=False)
1440
  offline = [row[0] for row in result if row[1]]
1441
  if offline and not nowarn:
1442
    ToStderr("Note: skipping offline node(s): %s" % ", ".join(offline))
1443
  return [row[0] for row in result if not row[1]]
1444

    
1445

    
1446
def _ToStream(stream, txt, *args):
1447
  """Write a message to a stream, bypassing the logging system
1448

1449
  @type stream: file object
1450
  @param stream: the file to which we should write
1451
  @type txt: str
1452
  @param txt: the message
1453

1454
  """
1455
  if args:
1456
    args = tuple(args)
1457
    stream.write(txt % args)
1458
  else:
1459
    stream.write(txt)
1460
  stream.write('\n')
1461
  stream.flush()
1462

    
1463

    
1464
def ToStdout(txt, *args):
1465
  """Write a message to stdout only, bypassing the logging system
1466

1467
  This is just a wrapper over _ToStream.
1468

1469
  @type txt: str
1470
  @param txt: the message
1471

1472
  """
1473
  _ToStream(sys.stdout, txt, *args)
1474

    
1475

    
1476
def ToStderr(txt, *args):
1477
  """Write a message to stderr only, bypassing the logging system
1478

1479
  This is just a wrapper over _ToStream.
1480

1481
  @type txt: str
1482
  @param txt: the message
1483

1484
  """
1485
  _ToStream(sys.stderr, txt, *args)
1486

    
1487

    
1488
class JobExecutor(object):
1489
  """Class which manages the submission and execution of multiple jobs.
1490

1491
  Note that instances of this class should not be reused between
1492
  GetResults() calls.
1493

1494
  """
1495
  def __init__(self, cl=None, verbose=True):
1496
    self.queue = []
1497
    if cl is None:
1498
      cl = GetClient()
1499
    self.cl = cl
1500
    self.verbose = verbose
1501
    self.jobs = []
1502

    
1503
  def QueueJob(self, name, *ops):
1504
    """Record a job for later submit.
1505

1506
    @type name: string
1507
    @param name: a description of the job, will be used in WaitJobSet
1508
    """
1509
    self.queue.append((name, ops))
1510

    
1511
  def SubmitPending(self):
1512
    """Submit all pending jobs.
1513

1514
    """
1515
    results = self.cl.SubmitManyJobs([row[1] for row in self.queue])
1516
    for ((status, data), (name, _)) in zip(results, self.queue):
1517
      self.jobs.append((status, data, name))
1518

    
1519
  def GetResults(self):
1520
    """Wait for and return the results of all jobs.
1521

1522
    @rtype: list
1523
    @return: list of tuples (success, job results), in the same order
1524
        as the submitted jobs; if a job has failed, instead of the result
1525
        there will be the error message
1526

1527
    """
1528
    if not self.jobs:
1529
      self.SubmitPending()
1530
    results = []
1531
    if self.verbose:
1532
      ok_jobs = [row[1] for row in self.jobs if row[0]]
1533
      if ok_jobs:
1534
        ToStdout("Submitted jobs %s", ", ".join(ok_jobs))
1535
    for submit_status, jid, name in self.jobs:
1536
      if not submit_status:
1537
        ToStderr("Failed to submit job for %s: %s", name, jid)
1538
        results.append((False, jid))
1539
        continue
1540
      if self.verbose:
1541
        ToStdout("Waiting for job %s for %s...", jid, name)
1542
      try:
1543
        job_result = PollJob(jid, cl=self.cl)
1544
        success = True
1545
      except (errors.GenericError, luxi.ProtocolError), err:
1546
        _, job_result = FormatError(err)
1547
        success = False
1548
        # the error message will always be shown, verbose or not
1549
        ToStderr("Job %s for %s has failed: %s", jid, name, job_result)
1550

    
1551
      results.append((success, job_result))
1552
    return results
1553

    
1554
  def WaitOrShow(self, wait):
1555
    """Wait for job results or only print the job IDs.
1556

1557
    @type wait: boolean
1558
    @param wait: whether to wait or not
1559

1560
    """
1561
    if wait:
1562
      return self.GetResults()
1563
    else:
1564
      if not self.jobs:
1565
        self.SubmitPending()
1566
      for status, result, name in self.jobs:
1567
        if status:
1568
          ToStdout("%s: %s", result, name)
1569
        else:
1570
          ToStderr("Failure for %s: %s", name, result)