Statistics
| Branch: | Tag: | Revision:

root / lib / cli.py @ 1f587d3d

History | View | Annotate | Download (48.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
  "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
  "YES_DOIT_OPT",
107
  # Generic functions for CLI programs
108
  "GenericMain",
109
  "GetClient",
110
  "GetOnlineNodes",
111
  "JobExecutor",
112
  "JobSubmittedException",
113
  "ParseTimespec",
114
  "SubmitOpCode",
115
  "SubmitOrSend",
116
  "UsesRPC",
117
  # Formatting functions
118
  "ToStderr", "ToStdout",
119
  "FormatError",
120
  "GenerateTable",
121
  "AskUser",
122
  "FormatTimestamp",
123
  # Tags functions
124
  "ListTags",
125
  "AddTags",
126
  "RemoveTags",
127
  # command line options support infrastructure
128
  "ARGS_MANY_INSTANCES",
129
  "ARGS_MANY_NODES",
130
  "ARGS_NONE",
131
  "ARGS_ONE_INSTANCE",
132
  "ARGS_ONE_NODE",
133
  "ArgChoice",
134
  "ArgCommand",
135
  "ArgFile",
136
  "ArgHost",
137
  "ArgInstance",
138
  "ArgJobId",
139
  "ArgNode",
140
  "ArgSuggest",
141
  "ArgUnknown",
142
  "OPT_COMPL_INST_ADD_NODES",
143
  "OPT_COMPL_MANY_NODES",
144
  "OPT_COMPL_ONE_IALLOCATOR",
145
  "OPT_COMPL_ONE_INSTANCE",
146
  "OPT_COMPL_ONE_NODE",
147
  "OPT_COMPL_ONE_OS",
148
  "cli_option",
149
  "SplitNodeOption",
150
  ]
151

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

    
155

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

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

    
165

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

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

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

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

    
180

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

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

187
  """
188

    
189

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

193
  """
194

    
195

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

199
  """
200

    
201

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

205
  """
206

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

210
  """
211

    
212

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

216
  """
217

    
218

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

222
  """
223

    
224

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

228
  """
229

    
230

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

    
237

    
238

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

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

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

    
259

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

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

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

    
288

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

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

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

    
306

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

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

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

    
323

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

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

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

    
340

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

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

    
350

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

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

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

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

    
387

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

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

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

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

    
415

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

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

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

    
424

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

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

    
443

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

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

    
461

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

    
465

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
707

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

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

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

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

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

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

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

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

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

    
747
YES_DOIT_OPT = cli_option("--yes-do-it", dest="yes_do_it",
748
                          help="Destroy cluster", action="store_true")
749

    
750

    
751
def _ParseArgs(argv, commands, aliases):
752
  """Parser for the command line arguments.
753

754
  This function parses the arguments and returns the function which
755
  must be executed together with its (modified) arguments.
756

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

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

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

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

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

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

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

    
798
    ToStdout("")
799

    
800
    return None, None, None
801

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

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

    
813
    cmd = aliases[cmd]
814

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

    
823
  if not _CheckArguments(cmd, args_def, args):
824
    return None, None, None
825

    
826
  return func, options, args
827

    
828

    
829
def _CheckArguments(cmd, args_def, args):
830
  """Verifies the arguments using the argument definition.
831

832
  Algorithm:
833

834
    1. Abort with error if values specified by user but none expected.
835

836
    1. For each argument in definition
837

838
      1. Keep running count of minimum number of values (min_count)
839
      1. Keep running count of maximum number of values (max_count)
840
      1. If it has an unlimited number of values
841

842
        1. Abort with error if it's not the last argument in the definition
843

844
    1. If last argument has limited number of values
845

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

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

850
  """
851
  if args and not args_def:
852
    ToStderr("Error: Command %s expects no arguments", cmd)
853
    return False
854

    
855
  min_count = None
856
  max_count = None
857
  check_max = None
858

    
859
  last_idx = len(args_def) - 1
860

    
861
  for idx, arg in enumerate(args_def):
862
    if min_count is None:
863
      min_count = arg.min
864
    elif arg.min is not None:
865
      min_count += arg.min
866

    
867
    if max_count is None:
868
      max_count = arg.max
869
    elif arg.max is not None:
870
      max_count += arg.max
871

    
872
    if idx == last_idx:
873
      check_max = (arg.max is not None)
874

    
875
    elif arg.max is None:
876
      raise errors.ProgrammerError("Only the last argument can have max=None")
877

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

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

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

    
897
  return True
898

    
899

    
900
def SplitNodeOption(value):
901
  """Splits the value of a --node option.
902

903
  """
904
  if value and ':' in value:
905
    return value.split(':', 1)
906
  else:
907
    return (value, None)
908

    
909

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

    
919

    
920
def AskUser(text, choices=None):
921
  """Ask the user a question.
922

923
  @param text: the question to ask
924

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

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

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

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

    
976

    
977
class JobSubmittedException(Exception):
978
  """Job was submitted, client should exit.
979

980
  This exception has one argument, the ID of the job that was
981
  submitted. The handler should print this ID.
982

983
  This is not an error, just a structured way to exit from clients.
984

985
  """
986

    
987

    
988
def SendJob(ops, cl=None):
989
  """Function to submit an opcode without waiting for the results.
990

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

997
  """
998
  if cl is None:
999
    cl = GetClient()
1000

    
1001
  job_id = cl.SubmitJob(ops)
1002

    
1003
  return job_id
1004

    
1005

    
1006
def PollJob(job_id, cl=None, feedback_fn=None):
1007
  """Function to poll for the result of a job.
1008

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

1015
  """
1016
  if cl is None:
1017
    cl = GetClient()
1018

    
1019
  prev_job_info = None
1020
  prev_logmsg_serial = None
1021

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

    
1029
    # Split result, a tuple of (field values, log entries)
1030
    (job_info, log_entries) = result
1031
    (status, ) = job_info
1032

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

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

    
1050
    prev_job_info = job_info
1051

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

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

    
1077

    
1078
def SubmitOpCode(op, cl=None, feedback_fn=None):
1079
  """Legacy function to submit an opcode.
1080

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

1085
  """
1086
  if cl is None:
1087
    cl = GetClient()
1088

    
1089
  job_id = SendJob([op], cl)
1090

    
1091
  op_results = PollJob(job_id, cl=cl, feedback_fn=feedback_fn)
1092

    
1093
  return op_results[0]
1094

    
1095

    
1096
def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
1097
  """Wrapper around SubmitOpCode or SendJob.
1098

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

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

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

    
1115

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

    
1130

    
1131
def FormatError(err):
1132
  """Return a formatted error message for a given error.
1133

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

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

    
1202

    
1203
def GenericMain(commands, override=None, aliases=None):
1204
  """Generic main function for all the gnt-* commands.
1205

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

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

    
1227
  if aliases is None:
1228
    aliases = {}
1229

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

    
1237
  if func is None: # parse error
1238
    return 1
1239

    
1240
  if override is not None:
1241
    for key, val in override.iteritems():
1242
      setattr(options, key, val)
1243

    
1244
  utils.SetupLogging(constants.LOG_COMMANDS, debug=options.debug,
1245
                     stderr_logging=True, program=binary)
1246

    
1247
  if old_cmdline:
1248
    logging.info("run with arguments '%s'", old_cmdline)
1249
  else:
1250
    logging.info("run with no arguments")
1251

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

    
1260
  return result
1261

    
1262

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

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

1291
  """
1292
  if units is None:
1293
    if separator:
1294
      units = "m"
1295
    else:
1296
      units = "h"
1297

    
1298
  if numfields is None:
1299
    numfields = []
1300
  if unitfields is None:
1301
    unitfields = []
1302

    
1303
  numfields = utils.FieldSet(*numfields)
1304
  unitfields = utils.FieldSet(*unitfields)
1305

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

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

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

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

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

    
1362
  return result
1363

    
1364

    
1365
def FormatTimestamp(ts):
1366
  """Formats a given timestamp.
1367

1368
  @type ts: timestamp
1369
  @param ts: a timeval-type timestamp, a tuple of seconds and microseconds
1370

1371
  @rtype: string
1372
  @return: a string with the formatted timestamp
1373

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

    
1380

    
1381
def ParseTimespec(value):
1382
  """Parse a time specification.
1383

1384
  The following suffixed will be recognized:
1385

1386
    - s: seconds
1387
    - m: minutes
1388
    - h: hours
1389
    - d: day
1390
    - w: weeks
1391

1392
  Without any suffix, the value will be taken to be in seconds.
1393

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

    
1422

    
1423
def GetOnlineNodes(nodes, cl=None, nowarn=False):
1424
  """Returns the names of online nodes.
1425

1426
  This function will also log a warning on stderr with the names of
1427
  the online nodes.
1428

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

1437
  """
1438
  if cl is None:
1439
    cl = GetClient()
1440

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

    
1448

    
1449
def _ToStream(stream, txt, *args):
1450
  """Write a message to a stream, bypassing the logging system
1451

1452
  @type stream: file object
1453
  @param stream: the file to which we should write
1454
  @type txt: str
1455
  @param txt: the message
1456

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

    
1466

    
1467
def ToStdout(txt, *args):
1468
  """Write a message to stdout only, bypassing the logging system
1469

1470
  This is just a wrapper over _ToStream.
1471

1472
  @type txt: str
1473
  @param txt: the message
1474

1475
  """
1476
  _ToStream(sys.stdout, txt, *args)
1477

    
1478

    
1479
def ToStderr(txt, *args):
1480
  """Write a message to stderr only, bypassing the logging system
1481

1482
  This is just a wrapper over _ToStream.
1483

1484
  @type txt: str
1485
  @param txt: the message
1486

1487
  """
1488
  _ToStream(sys.stderr, txt, *args)
1489

    
1490

    
1491
class JobExecutor(object):
1492
  """Class which manages the submission and execution of multiple jobs.
1493

1494
  Note that instances of this class should not be reused between
1495
  GetResults() calls.
1496

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

    
1506
  def QueueJob(self, name, *ops):
1507
    """Record a job for later submit.
1508

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

    
1514
  def SubmitPending(self):
1515
    """Submit all pending jobs.
1516

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

    
1522
  def GetResults(self):
1523
    """Wait for and return the results of all jobs.
1524

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

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

    
1554
      results.append((success, job_result))
1555
    return results
1556

    
1557
  def WaitOrShow(self, wait):
1558
    """Wait for job results or only print the job IDs.
1559

1560
    @type wait: boolean
1561
    @param wait: whether to wait or not
1562

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