Statistics
| Branch: | Tag: | Revision:

root / scripts / gnt-cluster @ debac808

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 = ", ".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
                               errors.ECODE_INVAL)
285

    
286
  cl = GetClient()
287

    
288
  myname = utils.HostInfo().name
289

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

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

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

    
300
  return 0
301

    
302

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

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

    
312
  """
313
  cl = GetClient()
314

    
315
  command = " ".join(args)
316

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

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

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

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

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

    
336
  return 0
337

    
338

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

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

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

    
361

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

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

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

    
377
  bad_nodes, instances, missing = result
378

    
379
  retcode = constants.EXIT_SUCCESS
380

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

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

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

    
419
  return retcode
420

    
421

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

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

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

    
435

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

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

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

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

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

    
461

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

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

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

    
481

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

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

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

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

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

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

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

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

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

    
530

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

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

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

    
557
  return 0
558

    
559

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

    
566

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

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

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

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

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

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

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

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

    
599
  return 0
600

    
601

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

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