Statistics
| Branch: | Tag: | Revision:

root / scripts / gnt-cluster @ 1f864b60

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

    
22
# pylint: disable-msg=W0401,W0614
23
# W0401: Wildcard import ganeti.cli
24
# W0614: Unused import %s from wildcard import (since we need cli)
25

    
26
import sys
27
import os.path
28
import time
29

    
30
from ganeti.cli import *
31
from ganeti import opcodes
32
from ganeti import constants
33
from ganeti import errors
34
from ganeti import utils
35
from ganeti import bootstrap
36
from ganeti import ssh
37
from ganeti import objects
38

    
39

    
40
@UsesRPC
41
def InitCluster(opts, args):
42
  """Initialize the cluster.
43

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

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

    
56
  vg_name = opts.vg_name
57
  if opts.lvm_storage and not opts.vg_name:
58
    vg_name = constants.DEFAULT_VG
59

    
60
  hvlist = opts.enabled_hypervisors
61
  if hvlist is None:
62
    hvlist = constants.DEFAULT_ENABLED_HYPERVISOR
63
  hvlist = hvlist.split(",")
64

    
65
  hvparams = dict(opts.hvparams)
66
  beparams = opts.beparams
67
  nicparams = opts.nicparams
68

    
69
  # prepare beparams dict
70
  beparams = objects.FillDict(constants.BEC_DEFAULTS, beparams)
71
  utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
72

    
73
  # prepare nicparams dict
74
  nicparams = objects.FillDict(constants.NICC_DEFAULTS, nicparams)
75
  utils.ForceDictType(nicparams, constants.NICS_PARAMETER_TYPES)
76

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

    
84
  if opts.candidate_pool_size is None:
85
    opts.candidate_pool_size = constants.MASTER_POOL_SIZE_DEFAULT
86

    
87
  if opts.mac_prefix is None:
88
    opts.mac_prefix = constants.DEFAULT_MAC_PREFIX
89

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

    
108

    
109
@UsesRPC
110
def DestroyCluster(opts, args):
111
  """Destroy the cluster.
112

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

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

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

    
132

    
133
def RenameCluster(opts, args):
134
  """Rename the cluster.
135

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

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

    
152
  op = opcodes.OpRenameCluster(name=name)
153
  SubmitOpCode(op)
154
  return 0
155

    
156

    
157
def RedistributeConfig(opts, args):
158
  """Forces push of the cluster configuration.
159

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

    
166
  """
167
  op = opcodes.OpRedistributeConfig()
168
  SubmitOrSend(op, opts)
169
  return 0
170

    
171

    
172
def ShowClusterVersion(opts, args):
173
  """Write version of ganeti software to the standard output.
174

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

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

    
191

    
192
def ShowClusterMaster(opts, args):
193
  """Write name of master node to the standard output.
194

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

    
201
  """
202
  master = bootstrap.GetMaster()
203
  ToStdout(master)
204
  return 0
205

    
206
def _PrintGroupedParams(paramsdict):
207
  """Print Grouped parameters (be, nic, disk) by group.
208

    
209
  @type paramsdict: dict of dicts
210
  @param paramsdict: {group: {param: value, ...}, ...}
211

    
212
  """
213
  for gr_name, gr_dict in paramsdict.items():
214
    ToStdout("  - %s:", gr_name)
215
    for item, val in gr_dict.iteritems():
216
      ToStdout("      %s: %s", item, val)
217

    
218
def ShowClusterConfig(opts, args):
219
  """Shows cluster information.
220

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

    
227
  """
228
  cl = GetClient()
229
  result = cl.QueryClusterInfo()
230

    
231
  ToStdout("Cluster name: %s", result["name"])
232
  ToStdout("Cluster UUID: %s", result["uuid"])
233

    
234
  ToStdout("Creation time: %s", utils.FormatTime(result["ctime"]))
235
  ToStdout("Modification time: %s", utils.FormatTime(result["mtime"]))
236

    
237
  ToStdout("Master node: %s", result["master"])
238

    
239
  ToStdout("Architecture (this node): %s (%s)",
240
           result["architecture"][0], result["architecture"][1])
241

    
242
  if result["tags"]:
243
    tags = utils.CommaJoin(utils.NiceSort(result["tags"]))
244
  else:
245
    tags = "(none)"
246

    
247
  ToStdout("Tags: %s", tags)
248

    
249
  ToStdout("Default hypervisor: %s", result["default_hypervisor"])
250
  ToStdout("Enabled hypervisors: %s",
251
           utils.CommaJoin(result["enabled_hypervisors"]))
252

    
253
  ToStdout("Hypervisor parameters:")
254
  _PrintGroupedParams(result["hvparams"])
255

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

    
262
  ToStdout("Default instance parameters:")
263
  _PrintGroupedParams(result["beparams"])
264

    
265
  ToStdout("Default nic parameters:")
266
  _PrintGroupedParams(result["nicparams"])
267

    
268
  return 0
269

    
270

    
271
def ClusterCopyFile(opts, args):
272
  """Copy a file from master to some nodes.
273

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

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

    
287
  cl = GetClient()
288

    
289
  myname = utils.GetHostInfo().name
290

    
291
  cluster_name = cl.QueryConfigValues(["cluster_name"])[0]
292

    
293
  results = GetOnlineNodes(nodes=opts.nodes, cl=cl)
294
  results = [name for name in results if name != myname]
295

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

    
301
  return 0
302

    
303

    
304
def RunClusterCommand(opts, args):
305
  """Run a command on some nodes.
306

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

    
313
  """
314
  cl = GetClient()
315

    
316
  command = " ".join(args)
317

    
318
  nodes = GetOnlineNodes(nodes=opts.nodes, cl=cl)
319

    
320
  cluster_name, master_node = cl.QueryConfigValues(["cluster_name",
321
                                                    "master_node"])
322

    
323
  srun = ssh.SshRunner(cluster_name=cluster_name)
324

    
325
  # Make sure master node is at list end
326
  if master_node in nodes:
327
    nodes.remove(master_node)
328
    nodes.append(master_node)
329

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

    
337
  return 0
338

    
339

    
340
def VerifyCluster(opts, args):
341
  """Verify integrity of cluster, performing various test on nodes.
342

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

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

    
362

    
363
def VerifyDisks(opts, args):
364
  """Verify integrity of cluster disks.
365

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

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

    
378
  bad_nodes, instances, missing = result
379

    
380
  retcode = constants.EXIT_SUCCESS
381

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

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

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

    
420
  return retcode
421

    
422

    
423
def RepairDiskSizes(opts, args):
424
  """Verify sizes of cluster disks.
425

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

    
432
  """
433
  op = opcodes.OpRepairDiskSizes(instances=args)
434
  SubmitOpCode(op)
435

    
436

    
437
@UsesRPC
438
def MasterFailover(opts, args):
439
  """Failover the master node.
440

    
441
  This command, when run on a non-master node, will cause the current
442
  master to cease being master, and the non-master to become new
443
  master.
444

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

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

    
460
  return bootstrap.MasterFailover(no_voting=opts.no_voting)
461

    
462

    
463
def SearchTags(opts, args):
464
  """Searches the tags on all the cluster.
465

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

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

    
482

    
483
def SetClusterParams(opts, args):
484
  """Modify the cluster.
485

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

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

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

    
507
  hvlist = opts.enabled_hypervisors
508
  if hvlist is not None:
509
    hvlist = hvlist.split(",")
510

    
511
  # a list of (name, dict) we can pass directly to dict() (or [])
512
  hvparams = dict(opts.hvparams)
513
  for hv, hv_params in hvparams.iteritems():
514
    utils.ForceDictType(hv_params, constants.HVS_PARAMETER_TYPES)
515

    
516
  beparams = opts.beparams
517
  utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
518

    
519
  nicparams = opts.nicparams
520
  utils.ForceDictType(nicparams, constants.NICS_PARAMETER_TYPES)
521

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

    
531

    
532
def QueueOps(opts, args):
533
  """Queue operations.
534

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

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

    
558
  return 0
559

    
560

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

    
567

    
568
def WatcherOps(opts, args):
569
  """Watcher operations.
570

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

    
577
  """
578
  command = args[0]
579
  client = GetClient()
580

    
581
  if command == "continue":
582
    client.SetWatcherPause(None)
583
    ToStdout("The watcher is no longer paused.")
584

    
585
  elif command == "pause":
586
    if len(args) < 2:
587
      raise errors.OpPrereqError("Missing pause duration", errors.ECODE_INVAL)
588

    
589
    result = client.SetWatcherPause(time.time() + ParseTimespec(args[1]))
590
    _ShowWatcherPause(result)
591

    
592
  elif command == "info":
593
    result = client.QueryConfigValues(["watcher_pause"])
594
    _ShowWatcherPause(result)
595

    
596
  else:
597
    raise errors.OpPrereqError("Command '%s' is not valid." % command,
598
                               errors.ECODE_INVAL)
599

    
600
  return 0
601

    
602

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

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