Statistics
| Branch: | Tag: | Revision:

root / scripts / gnt-cluster @ fdad8c4d

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
from ganeti import uidpool
43

    
44

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

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

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

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

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

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

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

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

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

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

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

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

    
114

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

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

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

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

    
138

    
139
def RenameCluster(opts, args):
140
  """Rename the cluster.
141

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

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

    
158
  op = opcodes.OpRenameCluster(name=name)
159
  SubmitOpCode(op, opts=opts)
160
  return 0
161

    
162

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

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

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

    
177

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

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

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

    
197

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

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

    
207
  """
208
  master = bootstrap.GetMaster()
209
  ToStdout(master)
210
  return 0
211

    
212

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

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

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

    
230

    
231
def ShowClusterConfig(opts, args):
232
  """Shows cluster information.
233

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

    
240
  """
241
  cl = GetClient()
242
  result = cl.QueryClusterInfo()
243

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

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

    
250
  ToStdout("Master node: %s", result["master"])
251

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

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

    
260
  ToStdout("Tags: %s", tags)
261

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

    
266
  ToStdout("Hypervisor parameters:")
267
  _PrintGroupedParams(result["hvparams"])
268

    
269
  ToStdout("OS specific hypervisor parameters:")
270
  _PrintGroupedParams(result["os_hvp"])
271

    
272
  ToStdout("Cluster parameters:")
273
  ToStdout("  - candidate pool size: %s", result["candidate_pool_size"])
274
  ToStdout("  - master netdev: %s", result["master_netdev"])
275
  ToStdout("  - lvm volume group: %s", result["volume_group_name"])
276
  ToStdout("  - file storage path: %s", result["file_storage_dir"])
277
  ToStdout("  - maintenance of node health: %s",
278
           result["maintain_node_health"])
279

    
280
  ToStdout("Default instance parameters:")
281
  _PrintGroupedParams(result["beparams"])
282

    
283
  ToStdout("Default nic parameters:")
284
  _PrintGroupedParams(result["nicparams"])
285

    
286
  return 0
287

    
288

    
289
def ClusterCopyFile(opts, args):
290
  """Copy a file from master to some nodes.
291

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

    
299
  """
300
  filename = args[0]
301
  if not os.path.exists(filename):
302
    raise errors.OpPrereqError("No such filename '%s'" % filename,
303
                               errors.ECODE_INVAL)
304

    
305
  cl = GetClient()
306

    
307
  cluster_name = cl.QueryConfigValues(["cluster_name"])[0]
308

    
309
  results = GetOnlineNodes(nodes=opts.nodes, cl=cl, filter_master=True,
310
                           secondary_ips=opts.use_replication_network)
311

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

    
317
  return 0
318

    
319

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

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

    
329
  """
330
  cl = GetClient()
331

    
332
  command = " ".join(args)
333

    
334
  nodes = GetOnlineNodes(nodes=opts.nodes, cl=cl)
335

    
336
  cluster_name, master_node = cl.QueryConfigValues(["cluster_name",
337
                                                    "master_node"])
338

    
339
  srun = ssh.SshRunner(cluster_name=cluster_name)
340

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

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

    
353
  return 0
354

    
355

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

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

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

    
378

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

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

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

    
394
  bad_nodes, instances, missing = result
395

    
396
  retcode = constants.EXIT_SUCCESS
397

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

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

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

    
436
  return retcode
437

    
438

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

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

    
448
  """
449
  op = opcodes.OpRepairDiskSizes(instances=args)
450
  SubmitOpCode(op, opts=opts)
451

    
452

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

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

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

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

    
476
  return bootstrap.MasterFailover(no_voting=opts.no_voting)
477

    
478

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

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

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

    
498

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

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

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

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

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

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

    
539
  else:
540
    rapi_cert_pem = None
541

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

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

    
554
    files_to_copy = []
555

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

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

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

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

    
572
  RunWhileClusterStopped(ToStdout, _RenewCryptoInner)
573

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

    
577
  return 0
578

    
579

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

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

    
590

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

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

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

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

    
617
  if not opts.lvm_storage:
618
    vg_name = ""
619

    
620
  hvlist = opts.enabled_hypervisors
621
  if hvlist is not None:
622
    hvlist = hvlist.split(",")
623

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

    
629
  beparams = opts.beparams
630
  utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
631

    
632
  nicparams = opts.nicparams
633
  utils.ForceDictType(nicparams, constants.NICS_PARAMETER_TYPES)
634

    
635

    
636
  mnh = opts.maintain_node_health
637

    
638
  uid_pool = opts.uid_pool
639
  if uid_pool is not None:
640
    uid_pool = uidpool.ParseUidPool(uid_pool)
641

    
642
  add_uids = opts.add_uids
643
  if add_uids is not None:
644
    add_uids = uidpool.ParseUidPool(add_uids)
645

    
646
  remove_uids = opts.remove_uids
647
  if remove_uids is not None:
648
    remove_uids = uidpool.ParseUidPool(remove_uids)
649

    
650
  op = opcodes.OpSetClusterParams(vg_name=vg_name,
651
                                  enabled_hypervisors=hvlist,
652
                                  hvparams=hvparams,
653
                                  os_hvp=None,
654
                                  beparams=beparams,
655
                                  nicparams=nicparams,
656
                                  candidate_pool_size=opts.candidate_pool_size,
657
                                  maintain_node_health=mnh,
658
                                  uid_pool=uid_pool,
659
                                  add_uids=add_uids,
660
                                  remove_uids=remove_uids)
661
  SubmitOpCode(op, opts=opts)
662
  return 0
663

    
664

    
665
def QueueOps(opts, args):
666
  """Queue operations.
667

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

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

    
691
  return 0
692

    
693

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

    
700

    
701
def WatcherOps(opts, args):
702
  """Watcher operations.
703

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

    
710
  """
711
  command = args[0]
712
  client = GetClient()
713

    
714
  if command == "continue":
715
    client.SetWatcherPause(None)
716
    ToStdout("The watcher is no longer paused.")
717

    
718
  elif command == "pause":
719
    if len(args) < 2:
720
      raise errors.OpPrereqError("Missing pause duration", errors.ECODE_INVAL)
721

    
722
    result = client.SetWatcherPause(time.time() + ParseTimespec(args[1]))
723
    _ShowWatcherPause(result)
724

    
725
  elif command == "info":
726
    result = client.QueryConfigValues(["watcher_pause"])
727
    _ShowWatcherPause(result[0])
728

    
729
  else:
730
    raise errors.OpPrereqError("Command '%s' is not valid." % command,
731
                               errors.ECODE_INVAL)
732

    
733
  return 0
734

    
735

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

    
823

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