Statistics
| Branch: | Tag: | Revision:

root / scripts / gnt-cluster @ 6b7d5878

History | View | Annotate | Download (23.9 kB)

1
#!/usr/bin/python
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
"""Cluster related commands"""
22

    
23
# pylint: disable-msg=W0401,W0613,W0614,C0103
24
# W0401: Wildcard import ganeti.cli
25
# W0613: Unused argument, since all functions follow the same API
26
# W0614: Unused import %s from wildcard import (since we need cli)
27
# C0103: Invalid name gnt-cluster
28

    
29
import sys
30
import os.path
31
import time
32
import OpenSSL
33

    
34
from ganeti.cli import *
35
from ganeti import opcodes
36
from ganeti import constants
37
from ganeti import errors
38
from ganeti import utils
39
from ganeti import bootstrap
40
from ganeti import ssh
41
from ganeti import objects
42

    
43

    
44
@UsesRPC
45
def InitCluster(opts, args):
46
  """Initialize the cluster.
47

    
48
  @param opts: the command line options selected by the user
49
  @type args: list
50
  @param args: should contain only one element, the desired
51
      cluster name
52
  @rtype: int
53
  @return: the desired exit code
54

    
55
  """
56
  if not opts.lvm_storage and opts.vg_name:
57
    ToStderr("Options --no-lvm-storage and --vg-name conflict.")
58
    return 1
59

    
60
  vg_name = opts.vg_name
61
  if opts.lvm_storage and not opts.vg_name:
62
    vg_name = constants.DEFAULT_VG
63

    
64
  hvlist = opts.enabled_hypervisors
65
  if hvlist is None:
66
    hvlist = constants.DEFAULT_ENABLED_HYPERVISOR
67
  hvlist = hvlist.split(",")
68

    
69
  hvparams = dict(opts.hvparams)
70
  beparams = opts.beparams
71
  nicparams = opts.nicparams
72

    
73
  # prepare beparams dict
74
  beparams = objects.FillDict(constants.BEC_DEFAULTS, beparams)
75
  utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
76

    
77
  # prepare nicparams dict
78
  nicparams = objects.FillDict(constants.NICC_DEFAULTS, nicparams)
79
  utils.ForceDictType(nicparams, constants.NICS_PARAMETER_TYPES)
80

    
81
  # prepare hvparams dict
82
  for hv in constants.HYPER_TYPES:
83
    if hv not in hvparams:
84
      hvparams[hv] = {}
85
    hvparams[hv] = objects.FillDict(constants.HVC_DEFAULTS[hv], hvparams[hv])
86
    utils.ForceDictType(hvparams[hv], constants.HVS_PARAMETER_TYPES)
87

    
88
  if opts.candidate_pool_size is None:
89
    opts.candidate_pool_size = constants.MASTER_POOL_SIZE_DEFAULT
90

    
91
  if opts.mac_prefix is None:
92
    opts.mac_prefix = constants.DEFAULT_MAC_PREFIX
93

    
94
  bootstrap.InitCluster(cluster_name=args[0],
95
                        secondary_ip=opts.secondary_ip,
96
                        vg_name=vg_name,
97
                        mac_prefix=opts.mac_prefix,
98
                        master_netdev=opts.master_netdev,
99
                        file_storage_dir=opts.file_storage_dir,
100
                        enabled_hypervisors=hvlist,
101
                        hvparams=hvparams,
102
                        beparams=beparams,
103
                        nicparams=nicparams,
104
                        candidate_pool_size=opts.candidate_pool_size,
105
                        modify_etc_hosts=opts.modify_etc_hosts,
106
                        modify_ssh_setup=opts.modify_ssh_setup,
107
                        )
108
  op = opcodes.OpPostInitCluster()
109
  SubmitOpCode(op, opts=opts)
110
  return 0
111

    
112

    
113
@UsesRPC
114
def DestroyCluster(opts, args):
115
  """Destroy the cluster.
116

    
117
  @param opts: the command line options selected by the user
118
  @type args: list
119
  @param args: should be an empty list
120
  @rtype: int
121
  @return: the desired exit code
122

    
123
  """
124
  if not opts.yes_do_it:
125
    ToStderr("Destroying a cluster is irreversible. If you really want"
126
             " destroy this cluster, supply the --yes-do-it option.")
127
    return 1
128

    
129
  op = opcodes.OpDestroyCluster()
130
  master = SubmitOpCode(op, opts=opts)
131
  # if we reached this, the opcode didn't fail; we can proceed to
132
  # shutdown all the daemons
133
  bootstrap.FinalizeClusterDestroy(master)
134
  return 0
135

    
136

    
137
def RenameCluster(opts, args):
138
  """Rename the cluster.
139

    
140
  @param opts: the command line options selected by the user
141
  @type args: list
142
  @param args: should contain only one element, the new cluster name
143
  @rtype: int
144
  @return: the desired exit code
145

    
146
  """
147
  name = args[0]
148
  if not opts.force:
149
    usertext = ("This will rename the cluster to '%s'. If you are connected"
150
                " over the network to the cluster name, the operation is very"
151
                " dangerous as the IP address will be removed from the node"
152
                " and the change may not go through. Continue?") % name
153
    if not AskUser(usertext):
154
      return 1
155

    
156
  op = opcodes.OpRenameCluster(name=name)
157
  SubmitOpCode(op, opts=opts)
158
  return 0
159

    
160

    
161
def RedistributeConfig(opts, args):
162
  """Forces push of the cluster configuration.
163

    
164
  @param opts: the command line options selected by the user
165
  @type args: list
166
  @param args: empty list
167
  @rtype: int
168
  @return: the desired exit code
169

    
170
  """
171
  op = opcodes.OpRedistributeConfig()
172
  SubmitOrSend(op, opts)
173
  return 0
174

    
175

    
176
def ShowClusterVersion(opts, args):
177
  """Write version of ganeti software to the standard output.
178

    
179
  @param opts: the command line options selected by the user
180
  @type args: list
181
  @param args: should be an empty list
182
  @rtype: int
183
  @return: the desired exit code
184

    
185
  """
186
  cl = GetClient()
187
  result = cl.QueryClusterInfo()
188
  ToStdout("Software version: %s", result["software_version"])
189
  ToStdout("Internode protocol: %s", result["protocol_version"])
190
  ToStdout("Configuration format: %s", result["config_version"])
191
  ToStdout("OS api version: %s", result["os_api_version"])
192
  ToStdout("Export interface: %s", result["export_version"])
193
  return 0
194

    
195

    
196
def ShowClusterMaster(opts, args):
197
  """Write name of master node to the standard output.
198

    
199
  @param opts: the command line options selected by the user
200
  @type args: list
201
  @param args: should be an empty list
202
  @rtype: int
203
  @return: the desired exit code
204

    
205
  """
206
  master = bootstrap.GetMaster()
207
  ToStdout(master)
208
  return 0
209

    
210

    
211
def _PrintGroupedParams(paramsdict, level=1):
212
  """Print Grouped parameters (be, nic, disk) by group.
213

    
214
  @type paramsdict: dict of dicts
215
  @param paramsdict: {group: {param: value, ...}, ...}
216
  @type level: int
217
  @param level: Level of indention
218

    
219
  """
220
  indent = "  " * level
221
  for item, val in paramsdict.items():
222
    if isinstance(val, dict):
223
      ToStdout("%s- %s:", indent, item)
224
      _PrintGroupedParams(val, level=level + 1)
225
    else:
226
      ToStdout("%s  %s: %s", indent, item, val)
227

    
228

    
229
def ShowClusterConfig(opts, args):
230
  """Shows cluster information.
231

    
232
  @param opts: the command line options selected by the user
233
  @type args: list
234
  @param args: should be an empty list
235
  @rtype: int
236
  @return: the desired exit code
237

    
238
  """
239
  cl = GetClient()
240
  result = cl.QueryClusterInfo()
241

    
242
  ToStdout("Cluster name: %s", result["name"])
243
  ToStdout("Cluster UUID: %s", result["uuid"])
244

    
245
  ToStdout("Creation time: %s", utils.FormatTime(result["ctime"]))
246
  ToStdout("Modification time: %s", utils.FormatTime(result["mtime"]))
247

    
248
  ToStdout("Master node: %s", result["master"])
249

    
250
  ToStdout("Architecture (this node): %s (%s)",
251
           result["architecture"][0], result["architecture"][1])
252

    
253
  if result["tags"]:
254
    tags = utils.CommaJoin(utils.NiceSort(result["tags"]))
255
  else:
256
    tags = "(none)"
257

    
258
  ToStdout("Tags: %s", tags)
259

    
260
  ToStdout("Default hypervisor: %s", result["default_hypervisor"])
261
  ToStdout("Enabled hypervisors: %s",
262
           utils.CommaJoin(result["enabled_hypervisors"]))
263

    
264
  ToStdout("Hypervisor parameters:")
265
  _PrintGroupedParams(result["hvparams"])
266

    
267
  ToStdout("OS specific hypervisor parameters:")
268
  _PrintGroupedParams(result["os_hvp"])
269

    
270
  ToStdout("Cluster parameters:")
271
  ToStdout("  - candidate pool size: %s", result["candidate_pool_size"])
272
  ToStdout("  - master netdev: %s", result["master_netdev"])
273
  ToStdout("  - lvm volume group: %s", result["volume_group_name"])
274
  ToStdout("  - file storage path: %s", result["file_storage_dir"])
275

    
276
  ToStdout("Default instance parameters:")
277
  _PrintGroupedParams(result["beparams"])
278

    
279
  ToStdout("Default nic parameters:")
280
  _PrintGroupedParams(result["nicparams"])
281

    
282
  return 0
283

    
284

    
285
def ClusterCopyFile(opts, args):
286
  """Copy a file from master to some nodes.
287

    
288
  @param opts: the command line options selected by the user
289
  @type args: list
290
  @param args: should contain only one element, the path of
291
      the file to be copied
292
  @rtype: int
293
  @return: the desired exit code
294

    
295
  """
296
  filename = args[0]
297
  if not os.path.exists(filename):
298
    raise errors.OpPrereqError("No such filename '%s'" % filename,
299
                               errors.ECODE_INVAL)
300

    
301
  cl = GetClient()
302

    
303
  myname = utils.GetHostInfo().name
304

    
305
  cluster_name = cl.QueryConfigValues(["cluster_name"])[0]
306

    
307
  results = GetOnlineNodes(nodes=opts.nodes, cl=cl)
308
  results = [name for name in results if name != myname]
309

    
310
  srun = ssh.SshRunner(cluster_name=cluster_name)
311
  for node in results:
312
    if not srun.CopyFileToNode(node, filename):
313
      ToStderr("Copy of file %s to node %s failed", filename, node)
314

    
315
  return 0
316

    
317

    
318
def RunClusterCommand(opts, args):
319
  """Run a command on some nodes.
320

    
321
  @param opts: the command line options selected by the user
322
  @type args: list
323
  @param args: should contain the command to be run and its arguments
324
  @rtype: int
325
  @return: the desired exit code
326

    
327
  """
328
  cl = GetClient()
329

    
330
  command = " ".join(args)
331

    
332
  nodes = GetOnlineNodes(nodes=opts.nodes, cl=cl)
333

    
334
  cluster_name, master_node = cl.QueryConfigValues(["cluster_name",
335
                                                    "master_node"])
336

    
337
  srun = ssh.SshRunner(cluster_name=cluster_name)
338

    
339
  # Make sure master node is at list end
340
  if master_node in nodes:
341
    nodes.remove(master_node)
342
    nodes.append(master_node)
343

    
344
  for name in nodes:
345
    result = srun.Run(name, "root", command)
346
    ToStdout("------------------------------------------------")
347
    ToStdout("node: %s", name)
348
    ToStdout("%s", result.output)
349
    ToStdout("return code = %s", result.exit_code)
350

    
351
  return 0
352

    
353

    
354
def VerifyCluster(opts, args):
355
  """Verify integrity of cluster, performing various test on nodes.
356

    
357
  @param opts: the command line options selected by the user
358
  @type args: list
359
  @param args: should be an empty list
360
  @rtype: int
361
  @return: the desired exit code
362

    
363
  """
364
  skip_checks = []
365
  if opts.skip_nplusone_mem:
366
    skip_checks.append(constants.VERIFY_NPLUSONE_MEM)
367
  op = opcodes.OpVerifyCluster(skip_checks=skip_checks,
368
                               verbose=opts.verbose,
369
                               error_codes=opts.error_codes,
370
                               debug_simulate_errors=opts.simulate_errors)
371
  if SubmitOpCode(op, opts=opts):
372
    return 0
373
  else:
374
    return 1
375

    
376

    
377
def VerifyDisks(opts, args):
378
  """Verify integrity of cluster disks.
379

    
380
  @param opts: the command line options selected by the user
381
  @type args: list
382
  @param args: should be an empty list
383
  @rtype: int
384
  @return: the desired exit code
385

    
386
  """
387
  op = opcodes.OpVerifyDisks()
388
  result = SubmitOpCode(op, opts=opts)
389
  if not isinstance(result, (list, tuple)) or len(result) != 3:
390
    raise errors.ProgrammerError("Unknown result type for OpVerifyDisks")
391

    
392
  bad_nodes, instances, missing = result
393

    
394
  retcode = constants.EXIT_SUCCESS
395

    
396
  if bad_nodes:
397
    for node, text in bad_nodes.items():
398
      ToStdout("Error gathering data on node %s: %s",
399
               node, utils.SafeEncode(text[-400:]))
400
      retcode |= 1
401
      ToStdout("You need to fix these nodes first before fixing instances")
402

    
403
  if instances:
404
    for iname in instances:
405
      if iname in missing:
406
        continue
407
      op = opcodes.OpActivateInstanceDisks(instance_name=iname)
408
      try:
409
        ToStdout("Activating disks for instance '%s'", iname)
410
        SubmitOpCode(op, opts=opts)
411
      except errors.GenericError, err:
412
        nret, msg = FormatError(err)
413
        retcode |= nret
414
        ToStderr("Error activating disks for instance %s: %s", iname, msg)
415

    
416
  if missing:
417
    for iname, ival in missing.iteritems():
418
      all_missing = utils.all(ival, lambda x: x[0] in bad_nodes)
419
      if all_missing:
420
        ToStdout("Instance %s cannot be verified as it lives on"
421
                 " broken nodes", iname)
422
      else:
423
        ToStdout("Instance %s has missing logical volumes:", iname)
424
        ival.sort()
425
        for node, vol in ival:
426
          if node in bad_nodes:
427
            ToStdout("\tbroken node %s /dev/xenvg/%s", node, vol)
428
          else:
429
            ToStdout("\t%s /dev/xenvg/%s", node, vol)
430
    ToStdout("You need to run replace_disks for all the above"
431
           " instances, if this message persist after fixing nodes.")
432
    retcode |= 1
433

    
434
  return retcode
435

    
436

    
437
def RepairDiskSizes(opts, args):
438
  """Verify sizes of cluster disks.
439

    
440
  @param opts: the command line options selected by the user
441
  @type args: list
442
  @param args: optional list of instances to restrict check to
443
  @rtype: int
444
  @return: the desired exit code
445

    
446
  """
447
  op = opcodes.OpRepairDiskSizes(instances=args)
448
  SubmitOpCode(op, opts=opts)
449

    
450

    
451
@UsesRPC
452
def MasterFailover(opts, args):
453
  """Failover the master node.
454

    
455
  This command, when run on a non-master node, will cause the current
456
  master to cease being master, and the non-master to become new
457
  master.
458

    
459
  @param opts: the command line options selected by the user
460
  @type args: list
461
  @param args: should be an empty list
462
  @rtype: int
463
  @return: the desired exit code
464

    
465
  """
466
  if opts.no_voting:
467
    usertext = ("This will perform the failover even if most other nodes"
468
                " are down, or if this node is outdated. This is dangerous"
469
                " as it can lead to a non-consistent cluster. Check the"
470
                " gnt-cluster(8) man page before proceeding. Continue?")
471
    if not AskUser(usertext):
472
      return 1
473

    
474
  return bootstrap.MasterFailover(no_voting=opts.no_voting)
475

    
476

    
477
def SearchTags(opts, args):
478
  """Searches the tags on all the cluster.
479

    
480
  @param opts: the command line options selected by the user
481
  @type args: list
482
  @param args: should contain only one element, the tag pattern
483
  @rtype: int
484
  @return: the desired exit code
485

    
486
  """
487
  op = opcodes.OpSearchTags(pattern=args[0])
488
  result = SubmitOpCode(op, opts=opts)
489
  if not result:
490
    return 1
491
  result = list(result)
492
  result.sort()
493
  for path, tag in result:
494
    ToStdout("%s %s", path, tag)
495

    
496

    
497
def _RenewCrypto(new_cluster_cert, new_rapi_cert, rapi_cert_filename,
498
                 new_confd_hmac_key, force):
499
  """Renews cluster certificates, keys and secrets.
500

    
501
  @type new_cluster_cert: bool
502
  @param new_cluster_cert: Whether to generate a new cluster certificate
503
  @type new_rapi_cert: bool
504
  @param new_rapi_cert: Whether to generate a new RAPI certificate
505
  @type rapi_cert_filename: string
506
  @param rapi_cert_filename: Path to file containing new RAPI certificate
507
  @type new_confd_hmac_key: bool
508
  @param new_confd_hmac_key: Whether to generate a new HMAC key
509
  @type force: bool
510
  @param force: Whether to ask user for confirmation
511

    
512
  """
513
  assert (new_cluster_cert or new_rapi_cert or rapi_cert_filename or
514
          new_confd_hmac_key)
515

    
516
  if new_rapi_cert and rapi_cert_filename:
517
    ToStderr("Only one of the --new-rapi-certficate and --rapi-certificate"
518
             " options can be specified at the same time.")
519
    return 1
520

    
521
  if rapi_cert_filename:
522
    # Read and verify new certificate
523
    try:
524
      rapi_cert_pem = utils.ReadFile(rapi_cert_filename)
525

    
526
      OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM,
527
                                      rapi_cert_pem)
528
    except Exception, err: # pylint: disable-msg=W0703
529
      ToStderr("Can't load new RAPI certificate from %s: %s" %
530
               (rapi_cert_filename, str(err)))
531
      return 1
532

    
533
    try:
534
      OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, rapi_cert_pem)
535
    except Exception, err: # pylint: disable-msg=W0703
536
      ToStderr("Can't load new RAPI private key from %s: %s" %
537
               (rapi_cert_filename, str(err)))
538
      return 1
539

    
540
  else:
541
    rapi_cert_pem = None
542

    
543
  if not force:
544
    usertext = ("This requires all daemons on all nodes to be restarted and"
545
                " may take some time. Continue?")
546
    if not AskUser(usertext):
547
      return 1
548

    
549
  def _RenewCryptoInner(ctx):
550
    ctx.feedback_fn("Updating certificates and keys")
551
    bootstrap.GenerateClusterCrypto(new_cluster_cert, new_rapi_cert,
552
                                    new_confd_hmac_key,
553
                                    rapi_cert_pem=rapi_cert_pem)
554

    
555
    files_to_copy = []
556

    
557
    if new_cluster_cert:
558
      files_to_copy.append(constants.NODED_CERT_FILE)
559

    
560
    if new_rapi_cert or rapi_cert_pem:
561
      files_to_copy.append(constants.RAPI_CERT_FILE)
562

    
563
    if new_confd_hmac_key:
564
      files_to_copy.append(constants.CONFD_HMAC_KEY)
565

    
566
    if files_to_copy:
567
      for node_name in ctx.nonmaster_nodes:
568
        ctx.feedback_fn("Copying %s to %s" %
569
                        (", ".join(files_to_copy), node_name))
570
        for file_name in files_to_copy:
571
          ctx.ssh.CopyFileToNode(node_name, file_name)
572

    
573
  RunWhileClusterStopped(ToStdout, _RenewCryptoInner)
574

    
575
  ToStdout("All requested certificates and keys have been replaced."
576
           " Running \"gnt-cluster verify\" now is recommended.")
577

    
578
  return 0
579

    
580

    
581
def RenewCrypto(opts, args):
582
  """Renews cluster certificates, keys and secrets.
583

    
584
  """
585
  return _RenewCrypto(opts.new_cluster_cert,
586
                      opts.new_rapi_cert,
587
                      opts.rapi_cert,
588
                      opts.new_confd_hmac_key,
589
                      opts.force)
590

    
591

    
592
def SetClusterParams(opts, args):
593
  """Modify the cluster.
594

    
595
  @param opts: the command line options selected by the user
596
  @type args: list
597
  @param args: should be an empty list
598
  @rtype: int
599
  @return: the desired exit code
600

    
601
  """
602
  if not (not opts.lvm_storage or opts.vg_name or
603
          opts.enabled_hypervisors or opts.hvparams or
604
          opts.beparams or opts.nicparams or
605
          opts.candidate_pool_size is not None):
606
    ToStderr("Please give at least one of the parameters.")
607
    return 1
608

    
609
  vg_name = opts.vg_name
610
  if not opts.lvm_storage and opts.vg_name:
611
    ToStderr("Options --no-lvm-storage and --vg-name conflict.")
612
    return 1
613

    
614
  if not opts.lvm_storage:
615
    vg_name = ""
616

    
617
  hvlist = opts.enabled_hypervisors
618
  if hvlist is not None:
619
    hvlist = hvlist.split(",")
620

    
621
  # a list of (name, dict) we can pass directly to dict() (or [])
622
  hvparams = dict(opts.hvparams)
623
  for hv_params in hvparams.values():
624
    utils.ForceDictType(hv_params, constants.HVS_PARAMETER_TYPES)
625

    
626
  beparams = opts.beparams
627
  utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
628

    
629
  nicparams = opts.nicparams
630
  utils.ForceDictType(nicparams, constants.NICS_PARAMETER_TYPES)
631

    
632
  op = opcodes.OpSetClusterParams(vg_name=vg_name,
633
                                  enabled_hypervisors=hvlist,
634
                                  hvparams=hvparams,
635
                                  os_hvp=None,
636
                                  beparams=beparams,
637
                                  nicparams=nicparams,
638
                                  candidate_pool_size=opts.candidate_pool_size)
639
  SubmitOpCode(op, opts=opts)
640
  return 0
641

    
642

    
643
def QueueOps(opts, args):
644
  """Queue operations.
645

    
646
  @param opts: the command line options selected by the user
647
  @type args: list
648
  @param args: should contain only one element, the subcommand
649
  @rtype: int
650
  @return: the desired exit code
651

    
652
  """
653
  command = args[0]
654
  client = GetClient()
655
  if command in ("drain", "undrain"):
656
    drain_flag = command == "drain"
657
    client.SetQueueDrainFlag(drain_flag)
658
  elif command == "info":
659
    result = client.QueryConfigValues(["drain_flag"])
660
    if result[0]:
661
      val = "set"
662
    else:
663
      val = "unset"
664
    ToStdout("The drain flag is %s" % val)
665
  else:
666
    raise errors.OpPrereqError("Command '%s' is not valid." % command,
667
                               errors.ECODE_INVAL)
668

    
669
  return 0
670

    
671

    
672
def _ShowWatcherPause(until):
673
  if until is None or until < time.time():
674
    ToStdout("The watcher is not paused.")
675
  else:
676
    ToStdout("The watcher is paused until %s.", time.ctime(until))
677

    
678

    
679
def WatcherOps(opts, args):
680
  """Watcher operations.
681

    
682
  @param opts: the command line options selected by the user
683
  @type args: list
684
  @param args: should contain only one element, the subcommand
685
  @rtype: int
686
  @return: the desired exit code
687

    
688
  """
689
  command = args[0]
690
  client = GetClient()
691

    
692
  if command == "continue":
693
    client.SetWatcherPause(None)
694
    ToStdout("The watcher is no longer paused.")
695

    
696
  elif command == "pause":
697
    if len(args) < 2:
698
      raise errors.OpPrereqError("Missing pause duration", errors.ECODE_INVAL)
699

    
700
    result = client.SetWatcherPause(time.time() + ParseTimespec(args[1]))
701
    _ShowWatcherPause(result)
702

    
703
  elif command == "info":
704
    result = client.QueryConfigValues(["watcher_pause"])
705
    _ShowWatcherPause(result[0])
706

    
707
  else:
708
    raise errors.OpPrereqError("Command '%s' is not valid." % command,
709
                               errors.ECODE_INVAL)
710

    
711
  return 0
712

    
713

    
714
commands = {
715
  'init': (
716
    InitCluster, [ArgHost(min=1, max=1)],
717
    [BACKEND_OPT, CP_SIZE_OPT, ENABLED_HV_OPT, GLOBAL_FILEDIR_OPT,
718
     HVLIST_OPT, MAC_PREFIX_OPT, MASTER_NETDEV_OPT, NIC_PARAMS_OPT,
719
     NOLVM_STORAGE_OPT, NOMODIFY_ETCHOSTS_OPT, NOMODIFY_SSH_SETUP_OPT,
720
     SECONDARY_IP_OPT, VG_NAME_OPT],
721
    "[opts...] <cluster_name>", "Initialises a new cluster configuration"),
722
  'destroy': (
723
    DestroyCluster, ARGS_NONE, [YES_DOIT_OPT],
724
    "", "Destroy cluster"),
725
  'rename': (
726
    RenameCluster, [ArgHost(min=1, max=1)],
727
    [FORCE_OPT],
728
    "<new_name>",
729
    "Renames the cluster"),
730
  'redist-conf': (
731
    RedistributeConfig, ARGS_NONE, [SUBMIT_OPT],
732
    "", "Forces a push of the configuration file and ssconf files"
733
    " to the nodes in the cluster"),
734
  'verify': (
735
    VerifyCluster, ARGS_NONE,
736
    [VERBOSE_OPT, DEBUG_SIMERR_OPT, ERROR_CODES_OPT, NONPLUS1_OPT],
737
    "", "Does a check on the cluster configuration"),
738
  'verify-disks': (
739
    VerifyDisks, ARGS_NONE, [],
740
    "", "Does a check on the cluster disk status"),
741
  'repair-disk-sizes': (
742
    RepairDiskSizes, ARGS_MANY_INSTANCES, [],
743
    "", "Updates mismatches in recorded disk sizes"),
744
  'masterfailover': (
745
    MasterFailover, ARGS_NONE, [NOVOTING_OPT],
746
    "", "Makes the current node the master"),
747
  'version': (
748
    ShowClusterVersion, ARGS_NONE, [],
749
    "", "Shows the cluster version"),
750
  'getmaster': (
751
    ShowClusterMaster, ARGS_NONE, [],
752
    "", "Shows the cluster master"),
753
  'copyfile': (
754
    ClusterCopyFile, [ArgFile(min=1, max=1)],
755
    [NODE_LIST_OPT],
756
    "[-n node...] <filename>", "Copies a file to all (or only some) nodes"),
757
  'command': (
758
    RunClusterCommand, [ArgCommand(min=1)],
759
    [NODE_LIST_OPT],
760
    "[-n node...] <command>", "Runs a command on all (or only some) nodes"),
761
  'info': (
762
    ShowClusterConfig, ARGS_NONE, [],
763
    "", "Show cluster configuration"),
764
  'list-tags': (
765
    ListTags, ARGS_NONE, [], "", "List the tags of the cluster"),
766
  'add-tags': (
767
    AddTags, [ArgUnknown()], [TAG_SRC_OPT],
768
    "tag...", "Add tags to the cluster"),
769
  'remove-tags': (
770
    RemoveTags, [ArgUnknown()], [TAG_SRC_OPT],
771
    "tag...", "Remove tags from the cluster"),
772
  'search-tags': (
773
    SearchTags, [ArgUnknown(min=1, max=1)],
774
    [], "", "Searches the tags on all objects on"
775
    " the cluster for a given pattern (regex)"),
776
  'queue': (
777
    QueueOps,
778
    [ArgChoice(min=1, max=1, choices=["drain", "undrain", "info"])],
779
    [], "drain|undrain|info", "Change queue properties"),
780
  'watcher': (
781
    WatcherOps,
782
    [ArgChoice(min=1, max=1, choices=["pause", "continue", "info"]),
783
     ArgSuggest(min=0, max=1, choices=["30m", "1h", "4h"])],
784
    [],
785
    "{pause <timespec>|continue|info}", "Change watcher properties"),
786
  'modify': (
787
    SetClusterParams, ARGS_NONE,
788
    [BACKEND_OPT, CP_SIZE_OPT, ENABLED_HV_OPT, HVLIST_OPT,
789
     NIC_PARAMS_OPT, NOLVM_STORAGE_OPT, VG_NAME_OPT],
790
    "[opts...]",
791
    "Alters the parameters of the cluster"),
792
  "renew-crypto": (
793
    RenewCrypto, ARGS_NONE,
794
    [NEW_CLUSTER_CERT_OPT, NEW_RAPI_CERT_OPT, RAPI_CERT_OPT,
795
     NEW_CONFD_HMAC_KEY_OPT, FORCE_OPT],
796
    "[opts...]",
797
    "Renews cluster certificates, keys and secrets"),
798
  }
799

    
800

    
801
if __name__ == '__main__':
802
  sys.exit(GenericMain(commands, override={"tag_type": constants.TAG_CLUSTER}))