Statistics
| Branch: | Tag: | Revision:

root / scripts / gnt-cluster @ 2d54e29c

History | View | Annotate | Download (20.2 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

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

    
42

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

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

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

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

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

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

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

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

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

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

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

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

    
111

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

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

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

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

    
135

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

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

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

    
155
  op = opcodes.OpRenameCluster(name=name)
156
  SubmitOpCode(op)
157
  return 0
158

    
159

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

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

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

    
174

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

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

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

    
194

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

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

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

    
209
def _PrintGroupedParams(paramsdict):
210
  """Print Grouped parameters (be, nic, disk) by group.
211

    
212
  @type paramsdict: dict of dicts
213
  @param paramsdict: {group: {param: value, ...}, ...}
214

    
215
  """
216
  for gr_name, gr_dict in paramsdict.items():
217
    ToStdout("  - %s:", gr_name)
218
    for item, val in gr_dict.iteritems():
219
      ToStdout("      %s: %s", item, val)
220

    
221
def ShowClusterConfig(opts, args):
222
  """Shows cluster information.
223

    
224
  @param opts: the command line options selected by the user
225
  @type args: list
226
  @param args: should be an empty list
227
  @rtype: int
228
  @return: the desired exit code
229

    
230
  """
231
  cl = GetClient()
232
  result = cl.QueryClusterInfo()
233

    
234
  ToStdout("Cluster name: %s", result["name"])
235
  ToStdout("Cluster UUID: %s", result["uuid"])
236

    
237
  ToStdout("Creation time: %s", utils.FormatTime(result["ctime"]))
238
  ToStdout("Modification time: %s", utils.FormatTime(result["mtime"]))
239

    
240
  ToStdout("Master node: %s", result["master"])
241

    
242
  ToStdout("Architecture (this node): %s (%s)",
243
           result["architecture"][0], result["architecture"][1])
244

    
245
  if result["tags"]:
246
    tags = utils.CommaJoin(utils.NiceSort(result["tags"]))
247
  else:
248
    tags = "(none)"
249

    
250
  ToStdout("Tags: %s", tags)
251

    
252
  ToStdout("Default hypervisor: %s", result["default_hypervisor"])
253
  ToStdout("Enabled hypervisors: %s",
254
           utils.CommaJoin(result["enabled_hypervisors"]))
255

    
256
  ToStdout("Hypervisor parameters:")
257
  _PrintGroupedParams(result["hvparams"])
258

    
259
  ToStdout("Cluster parameters:")
260
  ToStdout("  - candidate pool size: %s", result["candidate_pool_size"])
261
  ToStdout("  - master netdev: %s", result["master_netdev"])
262
  ToStdout("  - lvm volume group: %s", result["volume_group_name"])
263
  ToStdout("  - file storage path: %s", result["file_storage_dir"])
264

    
265
  ToStdout("Default instance parameters:")
266
  _PrintGroupedParams(result["beparams"])
267

    
268
  ToStdout("Default nic parameters:")
269
  _PrintGroupedParams(result["nicparams"])
270

    
271
  return 0
272

    
273

    
274
def ClusterCopyFile(opts, args):
275
  """Copy a file from master to some nodes.
276

    
277
  @param opts: the command line options selected by the user
278
  @type args: list
279
  @param args: should contain only one element, the path of
280
      the file to be copied
281
  @rtype: int
282
  @return: the desired exit code
283

    
284
  """
285
  filename = args[0]
286
  if not os.path.exists(filename):
287
    raise errors.OpPrereqError("No such filename '%s'" % filename,
288
                               errors.ECODE_INVAL)
289

    
290
  cl = GetClient()
291

    
292
  myname = utils.GetHostInfo().name
293

    
294
  cluster_name = cl.QueryConfigValues(["cluster_name"])[0]
295

    
296
  results = GetOnlineNodes(nodes=opts.nodes, cl=cl)
297
  results = [name for name in results if name != myname]
298

    
299
  srun = ssh.SshRunner(cluster_name=cluster_name)
300
  for node in results:
301
    if not srun.CopyFileToNode(node, filename):
302
      ToStderr("Copy of file %s to node %s failed", filename, node)
303

    
304
  return 0
305

    
306

    
307
def RunClusterCommand(opts, args):
308
  """Run a command on some nodes.
309

    
310
  @param opts: the command line options selected by the user
311
  @type args: list
312
  @param args: should contain the command to be run and its arguments
313
  @rtype: int
314
  @return: the desired exit code
315

    
316
  """
317
  cl = GetClient()
318

    
319
  command = " ".join(args)
320

    
321
  nodes = GetOnlineNodes(nodes=opts.nodes, cl=cl)
322

    
323
  cluster_name, master_node = cl.QueryConfigValues(["cluster_name",
324
                                                    "master_node"])
325

    
326
  srun = ssh.SshRunner(cluster_name=cluster_name)
327

    
328
  # Make sure master node is at list end
329
  if master_node in nodes:
330
    nodes.remove(master_node)
331
    nodes.append(master_node)
332

    
333
  for name in nodes:
334
    result = srun.Run(name, "root", command)
335
    ToStdout("------------------------------------------------")
336
    ToStdout("node: %s", name)
337
    ToStdout("%s", result.output)
338
    ToStdout("return code = %s", result.exit_code)
339

    
340
  return 0
341

    
342

    
343
def VerifyCluster(opts, args):
344
  """Verify integrity of cluster, performing various test on nodes.
345

    
346
  @param opts: the command line options selected by the user
347
  @type args: list
348
  @param args: should be an empty list
349
  @rtype: int
350
  @return: the desired exit code
351

    
352
  """
353
  skip_checks = []
354
  if opts.skip_nplusone_mem:
355
    skip_checks.append(constants.VERIFY_NPLUSONE_MEM)
356
  op = opcodes.OpVerifyCluster(skip_checks=skip_checks,
357
                               verbose=opts.verbose,
358
                               error_codes=opts.error_codes,
359
                               debug_simulate_errors=opts.simulate_errors)
360
  if SubmitOpCode(op):
361
    return 0
362
  else:
363
    return 1
364

    
365

    
366
def VerifyDisks(opts, args):
367
  """Verify integrity of cluster disks.
368

    
369
  @param opts: the command line options selected by the user
370
  @type args: list
371
  @param args: should be an empty list
372
  @rtype: int
373
  @return: the desired exit code
374

    
375
  """
376
  op = opcodes.OpVerifyDisks()
377
  result = SubmitOpCode(op)
378
  if not isinstance(result, (list, tuple)) or len(result) != 3:
379
    raise errors.ProgrammerError("Unknown result type for OpVerifyDisks")
380

    
381
  bad_nodes, instances, missing = result
382

    
383
  retcode = constants.EXIT_SUCCESS
384

    
385
  if bad_nodes:
386
    for node, text in bad_nodes.items():
387
      ToStdout("Error gathering data on node %s: %s",
388
               node, utils.SafeEncode(text[-400:]))
389
      retcode |= 1
390
      ToStdout("You need to fix these nodes first before fixing instances")
391

    
392
  if instances:
393
    for iname in instances:
394
      if iname in missing:
395
        continue
396
      op = opcodes.OpActivateInstanceDisks(instance_name=iname)
397
      try:
398
        ToStdout("Activating disks for instance '%s'", iname)
399
        SubmitOpCode(op)
400
      except errors.GenericError, err:
401
        nret, msg = FormatError(err)
402
        retcode |= nret
403
        ToStderr("Error activating disks for instance %s: %s", iname, msg)
404

    
405
  if missing:
406
    for iname, ival in missing.iteritems():
407
      all_missing = utils.all(ival, lambda x: x[0] in bad_nodes)
408
      if all_missing:
409
        ToStdout("Instance %s cannot be verified as it lives on"
410
                 " broken nodes", iname)
411
      else:
412
        ToStdout("Instance %s has missing logical volumes:", iname)
413
        ival.sort()
414
        for node, vol in ival:
415
          if node in bad_nodes:
416
            ToStdout("\tbroken node %s /dev/xenvg/%s", node, vol)
417
          else:
418
            ToStdout("\t%s /dev/xenvg/%s", node, vol)
419
    ToStdout("You need to run replace_disks for all the above"
420
           " instances, if this message persist after fixing nodes.")
421
    retcode |= 1
422

    
423
  return retcode
424

    
425

    
426
def RepairDiskSizes(opts, args):
427
  """Verify sizes of cluster disks.
428

    
429
  @param opts: the command line options selected by the user
430
  @type args: list
431
  @param args: optional list of instances to restrict check to
432
  @rtype: int
433
  @return: the desired exit code
434

    
435
  """
436
  op = opcodes.OpRepairDiskSizes(instances=args)
437
  SubmitOpCode(op)
438

    
439

    
440
@UsesRPC
441
def MasterFailover(opts, args):
442
  """Failover the master node.
443

    
444
  This command, when run on a non-master node, will cause the current
445
  master to cease being master, and the non-master to become new
446
  master.
447

    
448
  @param opts: the command line options selected by the user
449
  @type args: list
450
  @param args: should be an empty list
451
  @rtype: int
452
  @return: the desired exit code
453

    
454
  """
455
  if opts.no_voting:
456
    usertext = ("This will perform the failover even if most other nodes"
457
                " are down, or if this node is outdated. This is dangerous"
458
                " as it can lead to a non-consistent cluster. Check the"
459
                " gnt-cluster(8) man page before proceeding. Continue?")
460
    if not AskUser(usertext):
461
      return 1
462

    
463
  return bootstrap.MasterFailover(no_voting=opts.no_voting)
464

    
465

    
466
def SearchTags(opts, args):
467
  """Searches the tags on all the cluster.
468

    
469
  @param opts: the command line options selected by the user
470
  @type args: list
471
  @param args: should contain only one element, the tag pattern
472
  @rtype: int
473
  @return: the desired exit code
474

    
475
  """
476
  op = opcodes.OpSearchTags(pattern=args[0])
477
  result = SubmitOpCode(op)
478
  if not result:
479
    return 1
480
  result = list(result)
481
  result.sort()
482
  for path, tag in result:
483
    ToStdout("%s %s", path, tag)
484

    
485

    
486
def SetClusterParams(opts, args):
487
  """Modify the cluster.
488

    
489
  @param opts: the command line options selected by the user
490
  @type args: list
491
  @param args: should be an empty list
492
  @rtype: int
493
  @return: the desired exit code
494

    
495
  """
496
  if not (not opts.lvm_storage or opts.vg_name or
497
          opts.enabled_hypervisors or opts.hvparams or
498
          opts.beparams or opts.nicparams or
499
          opts.candidate_pool_size is not None):
500
    ToStderr("Please give at least one of the parameters.")
501
    return 1
502

    
503
  vg_name = opts.vg_name
504
  if not opts.lvm_storage and opts.vg_name:
505
    ToStdout("Options --no-lvm-storage and --vg-name conflict.")
506
    return 1
507
  elif not opts.lvm_storage:
508
    vg_name = ''
509

    
510
  hvlist = opts.enabled_hypervisors
511
  if hvlist is not None:
512
    hvlist = hvlist.split(",")
513

    
514
  # a list of (name, dict) we can pass directly to dict() (or [])
515
  hvparams = dict(opts.hvparams)
516
  for hv_params in hvparams.values():
517
    utils.ForceDictType(hv_params, constants.HVS_PARAMETER_TYPES)
518

    
519
  beparams = opts.beparams
520
  utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
521

    
522
  nicparams = opts.nicparams
523
  utils.ForceDictType(nicparams, constants.NICS_PARAMETER_TYPES)
524

    
525
  op = opcodes.OpSetClusterParams(vg_name=vg_name,
526
                                  enabled_hypervisors=hvlist,
527
                                  hvparams=hvparams,
528
                                  beparams=beparams,
529
                                  nicparams=nicparams,
530
                                  candidate_pool_size=opts.candidate_pool_size)
531
  SubmitOpCode(op)
532
  return 0
533

    
534

    
535
def QueueOps(opts, args):
536
  """Queue operations.
537

    
538
  @param opts: the command line options selected by the user
539
  @type args: list
540
  @param args: should contain only one element, the subcommand
541
  @rtype: int
542
  @return: the desired exit code
543

    
544
  """
545
  command = args[0]
546
  client = GetClient()
547
  if command in ("drain", "undrain"):
548
    drain_flag = command == "drain"
549
    client.SetQueueDrainFlag(drain_flag)
550
  elif command == "info":
551
    result = client.QueryConfigValues(["drain_flag"])
552
    if result[0]:
553
      val = "set"
554
    else:
555
      val = "unset"
556
    ToStdout("The drain flag is %s" % val)
557
  else:
558
    raise errors.OpPrereqError("Command '%s' is not valid." % command,
559
                               errors.ECODE_INVAL)
560

    
561
  return 0
562

    
563

    
564
def _ShowWatcherPause(until):
565
  if until is None or until < time.time():
566
    ToStdout("The watcher is not paused.")
567
  else:
568
    ToStdout("The watcher is paused until %s.", time.ctime(until))
569

    
570

    
571
def WatcherOps(opts, args):
572
  """Watcher operations.
573

    
574
  @param opts: the command line options selected by the user
575
  @type args: list
576
  @param args: should contain only one element, the subcommand
577
  @rtype: int
578
  @return: the desired exit code
579

    
580
  """
581
  command = args[0]
582
  client = GetClient()
583

    
584
  if command == "continue":
585
    client.SetWatcherPause(None)
586
    ToStdout("The watcher is no longer paused.")
587

    
588
  elif command == "pause":
589
    if len(args) < 2:
590
      raise errors.OpPrereqError("Missing pause duration", errors.ECODE_INVAL)
591

    
592
    result = client.SetWatcherPause(time.time() + ParseTimespec(args[1]))
593
    _ShowWatcherPause(result)
594

    
595
  elif command == "info":
596
    result = client.QueryConfigValues(["watcher_pause"])
597
    _ShowWatcherPause(result)
598

    
599
  else:
600
    raise errors.OpPrereqError("Command '%s' is not valid." % command,
601
                               errors.ECODE_INVAL)
602

    
603
  return 0
604

    
605

    
606
commands = {
607
  'init': (
608
    InitCluster, [ArgHost(min=1, max=1)],
609
    [BACKEND_OPT, CP_SIZE_OPT, ENABLED_HV_OPT, GLOBAL_FILEDIR_OPT,
610
     HVLIST_OPT, MAC_PREFIX_OPT, MASTER_NETDEV_OPT, NIC_PARAMS_OPT,
611
     NOLVM_STORAGE_OPT, NOMODIFY_ETCHOSTS_OPT, NOMODIFY_SSH_SETUP_OPT,
612
     SECONDARY_IP_OPT, VG_NAME_OPT],
613
    "[opts...] <cluster_name>", "Initialises a new cluster configuration"),
614
  'destroy': (
615
    DestroyCluster, ARGS_NONE, [YES_DOIT_OPT],
616
    "", "Destroy cluster"),
617
  'rename': (
618
    RenameCluster, [ArgHost(min=1, max=1)],
619
    [FORCE_OPT],
620
    "<new_name>",
621
    "Renames the cluster"),
622
  'redist-conf': (
623
    RedistributeConfig, ARGS_NONE, [SUBMIT_OPT],
624
    "", "Forces a push of the configuration file and ssconf files"
625
    " to the nodes in the cluster"),
626
  'verify': (
627
    VerifyCluster, ARGS_NONE,
628
    [VERBOSE_OPT, DEBUG_SIMERR_OPT, ERROR_CODES_OPT, NONPLUS1_OPT],
629
    "", "Does a check on the cluster configuration"),
630
  'verify-disks': (
631
    VerifyDisks, ARGS_NONE, [],
632
    "", "Does a check on the cluster disk status"),
633
  'repair-disk-sizes': (
634
    RepairDiskSizes, ARGS_MANY_INSTANCES, [],
635
    "", "Updates mismatches in recorded disk sizes"),
636
  'masterfailover': (
637
    MasterFailover, ARGS_NONE, [NOVOTING_OPT],
638
    "", "Makes the current node the master"),
639
  'version': (
640
    ShowClusterVersion, ARGS_NONE, [],
641
    "", "Shows the cluster version"),
642
  'getmaster': (
643
    ShowClusterMaster, ARGS_NONE, [],
644
    "", "Shows the cluster master"),
645
  'copyfile': (
646
    ClusterCopyFile, [ArgFile(min=1, max=1)],
647
    [NODE_LIST_OPT],
648
    "[-n node...] <filename>", "Copies a file to all (or only some) nodes"),
649
  'command': (
650
    RunClusterCommand, [ArgCommand(min=1)],
651
    [NODE_LIST_OPT],
652
    "[-n node...] <command>", "Runs a command on all (or only some) nodes"),
653
  'info': (
654
    ShowClusterConfig, ARGS_NONE, [],
655
    "", "Show cluster configuration"),
656
  'list-tags': (
657
    ListTags, ARGS_NONE, [], "", "List the tags of the cluster"),
658
  'add-tags': (
659
    AddTags, [ArgUnknown()], [TAG_SRC_OPT],
660
    "tag...", "Add tags to the cluster"),
661
  'remove-tags': (
662
    RemoveTags, [ArgUnknown()], [TAG_SRC_OPT],
663
    "tag...", "Remove tags from the cluster"),
664
  'search-tags': (
665
    SearchTags, [ArgUnknown(min=1, max=1)],
666
    [], "", "Searches the tags on all objects on"
667
    " the cluster for a given pattern (regex)"),
668
  'queue': (
669
    QueueOps,
670
    [ArgChoice(min=1, max=1, choices=["drain", "undrain", "info"])],
671
    [], "drain|undrain|info", "Change queue properties"),
672
  'watcher': (
673
    WatcherOps,
674
    [ArgChoice(min=1, max=1, choices=["pause", "continue", "info"]),
675
     ArgSuggest(min=0, max=1, choices=["30m", "1h", "4h"])],
676
    [],
677
    "{pause <timespec>|continue|info}", "Change watcher properties"),
678
  'modify': (
679
    SetClusterParams, ARGS_NONE,
680
    [BACKEND_OPT, CP_SIZE_OPT, ENABLED_HV_OPT, HVLIST_OPT,
681
     NIC_PARAMS_OPT, NOLVM_STORAGE_OPT, VG_NAME_OPT],
682
    "[opts...]",
683
    "Alters the parameters of the cluster"),
684
  }
685

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