Statistics
| Branch: | Tag: | Revision:

root / scripts / gnt-cluster @ b989b9d9

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

    
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 = ", ".join(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", ", ".join(result["enabled_hypervisors"]))
251

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

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

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

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

    
267
  return 0
268

    
269

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

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

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

    
285
  cl = GetClient()
286

    
287
  myname = utils.HostInfo().name
288

    
289
  cluster_name = cl.QueryConfigValues(["cluster_name"])[0]
290

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

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

    
299
  return 0
300

    
301

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

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

    
311
  """
312
  cl = GetClient()
313

    
314
  command = " ".join(args)
315

    
316
  nodes = GetOnlineNodes(nodes=opts.nodes, cl=cl)
317

    
318
  cluster_name, master_node = cl.QueryConfigValues(["cluster_name",
319
                                                    "master_node"])
320

    
321
  srun = ssh.SshRunner(cluster_name=cluster_name)
322

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

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

    
335
  return 0
336

    
337

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

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

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

    
360

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

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

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

    
376
  bad_nodes, instances, missing = result
377

    
378
  retcode = constants.EXIT_SUCCESS
379

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

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

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

    
418
  return retcode
419

    
420

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

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

    
430
  """
431
  op = opcodes.OpRepairDiskSizes(instances=args)
432
  SubmitOpCode(op)
433

    
434

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

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

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

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

    
458
  return bootstrap.MasterFailover(no_voting=opts.no_voting)
459

    
460

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

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

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

    
480

    
481
def SetClusterParams(opts, args):
482
  """Modify the cluster.
483

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

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

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

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

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

    
514
  beparams = opts.beparams
515
  utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
516

    
517
  nicparams = opts.nicparams
518
  utils.ForceDictType(nicparams, constants.NICS_PARAMETER_TYPES)
519

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

    
529

    
530
def QueueOps(opts, args):
531
  """Queue operations.
532

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

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

    
555
  return 0
556

    
557

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

    
564

    
565
def WatcherOps(opts, args):
566
  """Watcher operations.
567

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

    
574
  """
575
  command = args[0]
576
  client = GetClient()
577

    
578
  if command == "continue":
579
    client.SetWatcherPause(None)
580
    ToStdout("The watcher is no longer paused.")
581

    
582
  elif command == "pause":
583
    if len(args) < 2:
584
      raise errors.OpPrereqError("Missing pause duration")
585

    
586
    result = client.SetWatcherPause(time.time() + ParseTimespec(args[1]))
587
    _ShowWatcherPause(result)
588

    
589
  elif command == "info":
590
    result = client.QueryConfigValues(["watcher_pause"])
591
    _ShowWatcherPause(result)
592

    
593
  else:
594
    raise errors.OpPrereqError("Command '%s' is not valid." % command)
595

    
596
  return 0
597

    
598

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

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