Statistics
| Branch: | Tag: | Revision:

root / scripts / gnt-cluster @ 3db3eb2a

History | View | Annotate | Download (24.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, new_cds, cds_filename,
499
                 force):
500
  """Renews cluster certificates, keys and secrets.
501

    
502
  @type new_cluster_cert: bool
503
  @param new_cluster_cert: Whether to generate a new cluster certificate
504
  @type new_rapi_cert: bool
505
  @param new_rapi_cert: Whether to generate a new RAPI certificate
506
  @type rapi_cert_filename: string
507
  @param rapi_cert_filename: Path to file containing new RAPI certificate
508
  @type new_confd_hmac_key: bool
509
  @param new_confd_hmac_key: Whether to generate a new HMAC key
510
  @type new_cds: bool
511
  @param new_cds: Whether to generate a new cluster domain secret
512
  @type cds_filename: string
513
  @param cds_filename: Path to file containing new cluster domain secret
514
  @type force: bool
515
  @param force: Whether to ask user for confirmation
516

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

    
523
  if new_cds and cds_filename:
524
    ToStderr("Only one of the --new-cluster-domain-secret and"
525
             " --cluster-domain-secret options can be specified at"
526
             " the same time.")
527
    return 1
528

    
529
  if rapi_cert_filename:
530
    # Read and verify new certificate
531
    try:
532
      rapi_cert_pem = utils.ReadFile(rapi_cert_filename)
533

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

    
541
    try:
542
      OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, rapi_cert_pem)
543
    except Exception, err: # pylint: disable-msg=W0703
544
      ToStderr("Can't load new RAPI private key from %s: %s" %
545
               (rapi_cert_filename, str(err)))
546
      return 1
547

    
548
  else:
549
    rapi_cert_pem = None
550

    
551
  if cds_filename:
552
    try:
553
      cds = utils.ReadFile(cds_filename)
554
    except Exception, err: # pylint: disable-msg=W0703
555
      ToStderr("Can't load new cluster domain secret from %s: %s" %
556
               (cds_filename, str(err)))
557
      return 1
558
  else:
559
    cds = None
560

    
561
  if not force:
562
    usertext = ("This requires all daemons on all nodes to be restarted and"
563
                " may take some time. Continue?")
564
    if not AskUser(usertext):
565
      return 1
566

    
567
  def _RenewCryptoInner(ctx):
568
    ctx.feedback_fn("Updating certificates and keys")
569
    bootstrap.GenerateClusterCrypto(new_cluster_cert, new_rapi_cert,
570
                                    new_confd_hmac_key,
571
                                    new_cds,
572
                                    rapi_cert_pem=rapi_cert_pem,
573
                                    cds=cds)
574

    
575
    files_to_copy = []
576

    
577
    if new_cluster_cert:
578
      files_to_copy.append(constants.NODED_CERT_FILE)
579

    
580
    if new_rapi_cert or rapi_cert_pem:
581
      files_to_copy.append(constants.RAPI_CERT_FILE)
582

    
583
    if new_confd_hmac_key:
584
      files_to_copy.append(constants.CONFD_HMAC_KEY)
585

    
586
    if new_cds or cds:
587
      files_to_copy.append(constants.CLUSTER_DOMAIN_SECRET_FILE)
588

    
589
    if files_to_copy:
590
      for node_name in ctx.nonmaster_nodes:
591
        ctx.feedback_fn("Copying %s to %s" %
592
                        (", ".join(files_to_copy), node_name))
593
        for file_name in files_to_copy:
594
          ctx.ssh.CopyFileToNode(node_name, file_name)
595

    
596
  RunWhileClusterStopped(ToStdout, _RenewCryptoInner)
597

    
598
  ToStdout("All requested certificates and keys have been replaced."
599
           " Running \"gnt-cluster verify\" now is recommended.")
600

    
601
  return 0
602

    
603

    
604
def RenewCrypto(opts, args):
605
  """Renews cluster certificates, keys and secrets.
606

    
607
  """
608
  return _RenewCrypto(opts.new_cluster_cert,
609
                      opts.new_rapi_cert,
610
                      opts.rapi_cert,
611
                      opts.new_confd_hmac_key,
612
                      opts.new_cluster_domain_secret,
613
                      opts.cluster_domain_secret,
614
                      opts.force)
615

    
616

    
617
def SetClusterParams(opts, args):
618
  """Modify the cluster.
619

    
620
  @param opts: the command line options selected by the user
621
  @type args: list
622
  @param args: should be an empty list
623
  @rtype: int
624
  @return: the desired exit code
625

    
626
  """
627
  if not (not opts.lvm_storage or opts.vg_name or
628
          opts.enabled_hypervisors or opts.hvparams or
629
          opts.beparams or opts.nicparams or
630
          opts.candidate_pool_size is not None):
631
    ToStderr("Please give at least one of the parameters.")
632
    return 1
633

    
634
  vg_name = opts.vg_name
635
  if not opts.lvm_storage and opts.vg_name:
636
    ToStderr("Options --no-lvm-storage and --vg-name conflict.")
637
    return 1
638

    
639
  if not opts.lvm_storage:
640
    vg_name = ""
641

    
642
  hvlist = opts.enabled_hypervisors
643
  if hvlist is not None:
644
    hvlist = hvlist.split(",")
645

    
646
  # a list of (name, dict) we can pass directly to dict() (or [])
647
  hvparams = dict(opts.hvparams)
648
  for hv_params in hvparams.values():
649
    utils.ForceDictType(hv_params, constants.HVS_PARAMETER_TYPES)
650

    
651
  beparams = opts.beparams
652
  utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
653

    
654
  nicparams = opts.nicparams
655
  utils.ForceDictType(nicparams, constants.NICS_PARAMETER_TYPES)
656

    
657
  op = opcodes.OpSetClusterParams(vg_name=vg_name,
658
                                  enabled_hypervisors=hvlist,
659
                                  hvparams=hvparams,
660
                                  os_hvp=None,
661
                                  beparams=beparams,
662
                                  nicparams=nicparams,
663
                                  candidate_pool_size=opts.candidate_pool_size)
664
  SubmitOpCode(op, opts=opts)
665
  return 0
666

    
667

    
668
def QueueOps(opts, args):
669
  """Queue operations.
670

    
671
  @param opts: the command line options selected by the user
672
  @type args: list
673
  @param args: should contain only one element, the subcommand
674
  @rtype: int
675
  @return: the desired exit code
676

    
677
  """
678
  command = args[0]
679
  client = GetClient()
680
  if command in ("drain", "undrain"):
681
    drain_flag = command == "drain"
682
    client.SetQueueDrainFlag(drain_flag)
683
  elif command == "info":
684
    result = client.QueryConfigValues(["drain_flag"])
685
    if result[0]:
686
      val = "set"
687
    else:
688
      val = "unset"
689
    ToStdout("The drain flag is %s" % val)
690
  else:
691
    raise errors.OpPrereqError("Command '%s' is not valid." % command,
692
                               errors.ECODE_INVAL)
693

    
694
  return 0
695

    
696

    
697
def _ShowWatcherPause(until):
698
  if until is None or until < time.time():
699
    ToStdout("The watcher is not paused.")
700
  else:
701
    ToStdout("The watcher is paused until %s.", time.ctime(until))
702

    
703

    
704
def WatcherOps(opts, args):
705
  """Watcher operations.
706

    
707
  @param opts: the command line options selected by the user
708
  @type args: list
709
  @param args: should contain only one element, the subcommand
710
  @rtype: int
711
  @return: the desired exit code
712

    
713
  """
714
  command = args[0]
715
  client = GetClient()
716

    
717
  if command == "continue":
718
    client.SetWatcherPause(None)
719
    ToStdout("The watcher is no longer paused.")
720

    
721
  elif command == "pause":
722
    if len(args) < 2:
723
      raise errors.OpPrereqError("Missing pause duration", errors.ECODE_INVAL)
724

    
725
    result = client.SetWatcherPause(time.time() + ParseTimespec(args[1]))
726
    _ShowWatcherPause(result)
727

    
728
  elif command == "info":
729
    result = client.QueryConfigValues(["watcher_pause"])
730
    _ShowWatcherPause(result[0])
731

    
732
  else:
733
    raise errors.OpPrereqError("Command '%s' is not valid." % command,
734
                               errors.ECODE_INVAL)
735

    
736
  return 0
737

    
738

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

    
826

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