Statistics
| Branch: | Tag: | Revision:

root / scripts / gnt-cluster @ 1f587d3d

History | View | Annotate | Download (22.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
  bootstrap.InitCluster(cluster_name=args[0],
88
                        secondary_ip=opts.secondary_ip,
89
                        vg_name=vg_name,
90
                        mac_prefix=opts.mac_prefix,
91
                        master_netdev=opts.master_netdev,
92
                        file_storage_dir=opts.file_storage_dir,
93
                        enabled_hypervisors=hvlist,
94
                        hvparams=hvparams,
95
                        beparams=beparams,
96
                        nicparams=nicparams,
97
                        candidate_pool_size=opts.candidate_pool_size,
98
                        modify_etc_hosts=opts.modify_etc_hosts,
99
                        )
100
  op = opcodes.OpPostInitCluster()
101
  SubmitOpCode(op)
102
  return 0
103

    
104

    
105
@UsesRPC
106
def DestroyCluster(opts, args):
107
  """Destroy the cluster.
108

    
109
  @param opts: the command line options selected by the user
110
  @type args: list
111
  @param args: should be an empty list
112
  @rtype: int
113
  @return: the desired exit code
114

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

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

    
128

    
129
def RenameCluster(opts, args):
130
  """Rename the cluster.
131

    
132
  @param opts: the command line options selected by the user
133
  @type args: list
134
  @param args: should contain only one element, the new cluster name
135
  @rtype: int
136
  @return: the desired exit code
137

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

    
148
  op = opcodes.OpRenameCluster(name=name)
149
  SubmitOpCode(op)
150
  return 0
151

    
152

    
153
def RedistributeConfig(opts, args):
154
  """Forces push of the cluster configuration.
155

    
156
  @param opts: the command line options selected by the user
157
  @type args: list
158
  @param args: empty list
159
  @rtype: int
160
  @return: the desired exit code
161

    
162
  """
163
  op = opcodes.OpRedistributeConfig()
164
  SubmitOrSend(op, opts)
165
  return 0
166

    
167

    
168
def ShowClusterVersion(opts, args):
169
  """Write version of ganeti software to the standard output.
170

    
171
  @param opts: the command line options selected by the user
172
  @type args: list
173
  @param args: should be an empty list
174
  @rtype: int
175
  @return: the desired exit code
176

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

    
187

    
188
def ShowClusterMaster(opts, args):
189
  """Write name of master node to the standard output.
190

    
191
  @param opts: the command line options selected by the user
192
  @type args: list
193
  @param args: should be an empty list
194
  @rtype: int
195
  @return: the desired exit code
196

    
197
  """
198
  master = bootstrap.GetMaster()
199
  ToStdout(master)
200
  return 0
201

    
202
def _PrintGroupedParams(paramsdict):
203
  """Print Grouped parameters (be, nic, disk) by group.
204

    
205
  @type paramsdict: dict of dicts
206
  @param paramsdict: {group: {param: value, ...}, ...}
207

    
208
  """
209
  for gr_name, gr_dict in paramsdict.items():
210
    ToStdout("  - %s:", gr_name)
211
    for item, val in gr_dict.iteritems():
212
      ToStdout("      %s: %s", item, val)
213

    
214
def ShowClusterConfig(opts, args):
215
  """Shows cluster information.
216

    
217
  @param opts: the command line options selected by the user
218
  @type args: list
219
  @param args: should be an empty list
220
  @rtype: int
221
  @return: the desired exit code
222

    
223
  """
224
  cl = GetClient()
225
  result = cl.QueryClusterInfo()
226

    
227
  ToStdout("Cluster name: %s", result["name"])
228

    
229
  ToStdout("Creation time: %s", utils.FormatTime(result["ctime"]))
230
  ToStdout("Modification time: %s", utils.FormatTime(result["mtime"]))
231

    
232
  ToStdout("Master node: %s", result["master"])
233

    
234
  ToStdout("Architecture (this node): %s (%s)",
235
           result["architecture"][0], result["architecture"][1])
236

    
237
  if result["tags"]:
238
    tags = ", ".join(utils.NiceSort(result["tags"]))
239
  else:
240
    tags = "(none)"
241

    
242
  ToStdout("Tags: %s", tags)
243

    
244
  ToStdout("Default hypervisor: %s", result["default_hypervisor"])
245
  ToStdout("Enabled hypervisors: %s", ", ".join(result["enabled_hypervisors"]))
246

    
247
  ToStdout("Hypervisor parameters:")
248
  _PrintGroupedParams(result["hvparams"])
249

    
250
  ToStdout("Cluster parameters:")
251
  ToStdout("  - candidate pool size: %s", result["candidate_pool_size"])
252
  ToStdout("  - master netdev: %s", result["master_netdev"])
253
  ToStdout("  - lvm volume group: %s", result["volume_group_name"])
254
  ToStdout("  - file storage path: %s", result["file_storage_dir"])
255

    
256
  ToStdout("Default instance parameters:")
257
  _PrintGroupedParams(result["beparams"])
258

    
259
  ToStdout("Default nic parameters:")
260
  _PrintGroupedParams(result["nicparams"])
261

    
262
  return 0
263

    
264

    
265
def ClusterCopyFile(opts, args):
266
  """Copy a file from master to some nodes.
267

    
268
  @param opts: the command line options selected by the user
269
  @type args: list
270
  @param args: should contain only one element, the path of
271
      the file to be copied
272
  @rtype: int
273
  @return: the desired exit code
274

    
275
  """
276
  filename = args[0]
277
  if not os.path.exists(filename):
278
    raise errors.OpPrereqError("No such filename '%s'" % filename)
279

    
280
  cl = GetClient()
281

    
282
  myname = utils.HostInfo().name
283

    
284
  cluster_name = cl.QueryConfigValues(["cluster_name"])[0]
285

    
286
  results = GetOnlineNodes(nodes=opts.nodes, cl=cl)
287
  results = [name for name in results if name != myname]
288

    
289
  srun = ssh.SshRunner(cluster_name=cluster_name)
290
  for node in results:
291
    if not srun.CopyFileToNode(node, filename):
292
      ToStderr("Copy of file %s to node %s failed", filename, node)
293

    
294
  return 0
295

    
296

    
297
def RunClusterCommand(opts, args):
298
  """Run a command on some nodes.
299

    
300
  @param opts: the command line options selected by the user
301
  @type args: list
302
  @param args: should contain the command to be run and its arguments
303
  @rtype: int
304
  @return: the desired exit code
305

    
306
  """
307
  cl = GetClient()
308

    
309
  command = " ".join(args)
310

    
311
  nodes = GetOnlineNodes(nodes=opts.nodes, cl=cl)
312

    
313
  cluster_name, master_node = cl.QueryConfigValues(["cluster_name",
314
                                                    "master_node"])
315

    
316
  srun = ssh.SshRunner(cluster_name=cluster_name)
317

    
318
  # Make sure master node is at list end
319
  if master_node in nodes:
320
    nodes.remove(master_node)
321
    nodes.append(master_node)
322

    
323
  for name in nodes:
324
    result = srun.Run(name, "root", command)
325
    ToStdout("------------------------------------------------")
326
    ToStdout("node: %s", name)
327
    ToStdout("%s", result.output)
328
    ToStdout("return code = %s", result.exit_code)
329

    
330
  return 0
331

    
332

    
333
def VerifyCluster(opts, args):
334
  """Verify integrity of cluster, performing various test on nodes.
335

    
336
  @param opts: the command line options selected by the user
337
  @type args: list
338
  @param args: should be an empty list
339
  @rtype: int
340
  @return: the desired exit code
341

    
342
  """
343
  skip_checks = []
344
  if opts.skip_nplusone_mem:
345
    skip_checks.append(constants.VERIFY_NPLUSONE_MEM)
346
  op = opcodes.OpVerifyCluster(skip_checks=skip_checks,
347
                               verbose=opts.verbose,
348
                               error_codes=opts.error_codes,
349
                               debug_simulate_errors=opts.simulate_errors)
350
  if SubmitOpCode(op):
351
    return 0
352
  else:
353
    return 1
354

    
355

    
356
def VerifyDisks(opts, args):
357
  """Verify integrity of cluster disks.
358

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

    
365
  """
366
  op = opcodes.OpVerifyDisks()
367
  result = SubmitOpCode(op)
368
  if not isinstance(result, (list, tuple)) or len(result) != 3:
369
    raise errors.ProgrammerError("Unknown result type for OpVerifyDisks")
370

    
371
  bad_nodes, instances, missing = result
372

    
373
  retcode = constants.EXIT_SUCCESS
374

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

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

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

    
413
  return retcode
414

    
415

    
416
def RepairDiskSizes(opts, args):
417
  """Verify sizes of cluster disks.
418

    
419
  @param opts: the command line options selected by the user
420
  @type args: list
421
  @param args: optional list of instances to restrict check to
422
  @rtype: int
423
  @return: the desired exit code
424

    
425
  """
426
  op = opcodes.OpRepairDiskSizes(instances=args)
427
  SubmitOpCode(op)
428

    
429

    
430
@UsesRPC
431
def MasterFailover(opts, args):
432
  """Failover the master node.
433

    
434
  This command, when run on a non-master node, will cause the current
435
  master to cease being master, and the non-master to become new
436
  master.
437

    
438
  @param opts: the command line options selected by the user
439
  @type args: list
440
  @param args: should be an empty list
441
  @rtype: int
442
  @return: the desired exit code
443

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

    
453
  return bootstrap.MasterFailover(no_voting=opts.no_voting)
454

    
455

    
456
def SearchTags(opts, args):
457
  """Searches the tags on all the cluster.
458

    
459
  @param opts: the command line options selected by the user
460
  @type args: list
461
  @param args: should contain only one element, the tag pattern
462
  @rtype: int
463
  @return: the desired exit code
464

    
465
  """
466
  op = opcodes.OpSearchTags(pattern=args[0])
467
  result = SubmitOpCode(op)
468
  if not result:
469
    return 1
470
  result = list(result)
471
  result.sort()
472
  for path, tag in result:
473
    ToStdout("%s %s", path, tag)
474

    
475

    
476
def SetClusterParams(opts, args):
477
  """Modify the cluster.
478

    
479
  @param opts: the command line options selected by the user
480
  @type args: list
481
  @param args: should be an empty list
482
  @rtype: int
483
  @return: the desired exit code
484

    
485
  """
486
  if not (not opts.lvm_storage or opts.vg_name or
487
          opts.enabled_hypervisors or opts.hvparams or
488
          opts.beparams or opts.nicparams or
489
          opts.candidate_pool_size is not None):
490
    ToStderr("Please give at least one of the parameters.")
491
    return 1
492

    
493
  vg_name = opts.vg_name
494
  if not opts.lvm_storage and opts.vg_name:
495
    ToStdout("Options --no-lvm-storage and --vg-name conflict.")
496
    return 1
497
  elif not opts.lvm_storage:
498
    vg_name = ''
499

    
500
  hvlist = opts.enabled_hypervisors
501
  if hvlist is not None:
502
    hvlist = hvlist.split(",")
503

    
504
  # a list of (name, dict) we can pass directly to dict() (or [])
505
  hvparams = dict(opts.hvparams)
506
  for hv, hv_params in hvparams.iteritems():
507
    utils.ForceDictType(hv_params, constants.HVS_PARAMETER_TYPES)
508

    
509
  beparams = opts.beparams
510
  utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
511

    
512
  nicparams = opts.nicparams
513
  utils.ForceDictType(nicparams, constants.NICS_PARAMETER_TYPES)
514

    
515
  op = opcodes.OpSetClusterParams(vg_name=vg_name,
516
                                  enabled_hypervisors=hvlist,
517
                                  hvparams=hvparams,
518
                                  beparams=beparams,
519
                                  nicparams=nicparams,
520
                                  candidate_pool_size=opts.candidate_pool_size)
521
  SubmitOpCode(op)
522
  return 0
523

    
524

    
525
def QueueOps(opts, args):
526
  """Queue operations.
527

    
528
  @param opts: the command line options selected by the user
529
  @type args: list
530
  @param args: should contain only one element, the subcommand
531
  @rtype: int
532
  @return: the desired exit code
533

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

    
550
  return 0
551

    
552

    
553
def _ShowWatcherPause(until):
554
  if until is None or until < time.time():
555
    ToStdout("The watcher is not paused.")
556
  else:
557
    ToStdout("The watcher is paused until %s.", time.ctime(until))
558

    
559

    
560
def WatcherOps(opts, args):
561
  """Watcher operations.
562

    
563
  @param opts: the command line options selected by the user
564
  @type args: list
565
  @param args: should contain only one element, the subcommand
566
  @rtype: int
567
  @return: the desired exit code
568

    
569
  """
570
  command = args[0]
571
  client = GetClient()
572

    
573
  if command == "continue":
574
    client.SetWatcherPause(None)
575
    ToStdout("The watcher is no longer paused.")
576

    
577
  elif command == "pause":
578
    if len(args) < 2:
579
      raise errors.OpPrereqError("Missing pause duration")
580

    
581
    result = client.SetWatcherPause(time.time() + ParseTimespec(args[1]))
582
    _ShowWatcherPause(result)
583

    
584
  elif command == "info":
585
    result = client.QueryConfigValues(["watcher_pause"])
586
    _ShowWatcherPause(result)
587

    
588
  else:
589
    raise errors.OpPrereqError("Command '%s' is not valid." % command)
590

    
591
  return 0
592

    
593

    
594
commands = {
595
  'init': (InitCluster, [ArgHost(min=1, max=1)],
596
           [DEBUG_OPT,
597
            SECONDARY_IP_OPT,
598
            cli_option("-m", "--mac-prefix", dest="mac_prefix",
599
                       help="Specify the mac prefix for the instance IP"
600
                       " addresses, in the format XX:XX:XX",
601
                       metavar="PREFIX",
602
                       default=constants.DEFAULT_MAC_PREFIX,),
603
            VG_NAME_OPT,
604
            cli_option("--master-netdev", dest="master_netdev",
605
                       help="Specify the node interface (cluster-wide)"
606
                         " on which the master IP address will be added "
607
                         " [%s]" % constants.DEFAULT_BRIDGE,
608
                       metavar="NETDEV",
609
                       default=constants.DEFAULT_BRIDGE,),
610
            cli_option("--file-storage-dir", dest="file_storage_dir",
611
                       help="Specify the default directory (cluster-wide)"
612
                            " for storing the file-based disks [%s]" %
613
                            constants.DEFAULT_FILE_STORAGE_DIR,
614
                       metavar="DIR",
615
                       default=constants.DEFAULT_FILE_STORAGE_DIR,),
616
            NOLVM_STORAGE_OPT,
617
            cli_option("--no-etc-hosts", dest="modify_etc_hosts",
618
                       help="Don't modify /etc/hosts"
619
                            " (cluster-wide)",
620
                       action="store_false", default=True,),
621
            ENABLED_HV_OPT,
622
            HVLIST_OPT,
623
            BACKEND_OPT,
624
            NIC_PARAMS_OPT,
625
            CP_SIZE_OPT,
626
            ],
627
           "[opts...] <cluster_name>",
628
           "Initialises a new cluster configuration"),
629
  'destroy': (DestroyCluster, ARGS_NONE,
630
              [DEBUG_OPT, YES_DOIT_OPT],
631
              "", "Destroy cluster"),
632
  'rename': (RenameCluster, [ArgHost(min=1, max=1)],
633
             [DEBUG_OPT, FORCE_OPT],
634
             "<new_name>",
635
             "Renames the cluster"),
636
  'redist-conf': (RedistributeConfig, ARGS_NONE, [DEBUG_OPT, SUBMIT_OPT],
637
                  "",
638
                  "Forces a push of the configuration file and ssconf files"
639
                  " to the nodes in the cluster"),
640
  'verify': (VerifyCluster, ARGS_NONE,
641
             [DEBUG_OPT, VERBOSE_OPT, DEBUG_SIMERR_OPT,
642
              cli_option("--error-codes", dest="error_codes",
643
                         help="Enable parseable error messages",
644
                         action="store_true", default=False),
645
              cli_option("--no-nplus1-mem", dest="skip_nplusone_mem",
646
                         help="Skip N+1 memory redundancy tests",
647
                         action="store_true", default=False),
648
              ],
649
             "", "Does a check on the cluster configuration"),
650
  'verify-disks': (VerifyDisks, ARGS_NONE, [DEBUG_OPT],
651
                   "", "Does a check on the cluster disk status"),
652
  'repair-disk-sizes': (RepairDiskSizes, ARGS_MANY_INSTANCES, [DEBUG_OPT],
653
                   "", "Updates mismatches in recorded disk sizes"),
654
  'masterfailover': (MasterFailover, ARGS_NONE, [DEBUG_OPT,
655
                     cli_option("--no-voting", dest="no_voting",
656
                                help="Skip node agreement check (dangerous)",
657
                                action="store_true",
658
                                default=False,),
659
                     ],
660
                     "", "Makes the current node the master"),
661
  'version': (ShowClusterVersion, ARGS_NONE, [DEBUG_OPT],
662
              "", "Shows the cluster version"),
663
  'getmaster': (ShowClusterMaster, ARGS_NONE, [DEBUG_OPT],
664
                "", "Shows the cluster master"),
665
  'copyfile': (ClusterCopyFile, [ArgFile(min=1, max=1)],
666
               [DEBUG_OPT, NODE_LIST_OPT],
667
               "[-n node...] <filename>",
668
               "Copies a file to all (or only some) nodes"),
669
  'command': (RunClusterCommand, [ArgCommand(min=1)],
670
              [DEBUG_OPT, NODE_LIST_OPT],
671
              "[-n node...] <command>",
672
              "Runs a command on all (or only some) nodes"),
673
  'info': (ShowClusterConfig, ARGS_NONE, [DEBUG_OPT],
674
           "", "Show cluster configuration"),
675
  'list-tags': (ListTags, ARGS_NONE,
676
                [DEBUG_OPT], "", "List the tags of the cluster"),
677
  'add-tags': (AddTags, [ArgUnknown()], [DEBUG_OPT, TAG_SRC_OPT],
678
               "tag...", "Add tags to the cluster"),
679
  'remove-tags': (RemoveTags, [ArgUnknown()], [DEBUG_OPT, TAG_SRC_OPT],
680
                  "tag...", "Remove tags from the cluster"),
681
  'search-tags': (SearchTags, [ArgUnknown(min=1, max=1)],
682
                  [DEBUG_OPT], "", "Searches the tags on all objects on"
683
                  " the cluster for a given pattern (regex)"),
684
  'queue': (QueueOps,
685
            [ArgChoice(min=1, max=1, choices=["drain", "undrain", "info"])],
686
            [DEBUG_OPT],
687
            "drain|undrain|info", "Change queue properties"),
688
  'watcher': (WatcherOps,
689
              [ArgChoice(min=1, max=1,
690
                         choices=["pause", "continue", "info"]),
691
               ArgSuggest(min=0, max=1, choices=["30m", "1h", "4h"])],
692
              [DEBUG_OPT],
693
              "{pause <timespec>|continue|info}", "Change watcher properties"),
694
  'modify': (SetClusterParams, ARGS_NONE,
695
             [DEBUG_OPT, BACKEND_OPT, CP_SIZE_OPT, ENABLED_HV_OPT, HVLIST_OPT,
696
              NIC_PARAMS_OPT, NOLVM_STORAGE_OPT, VG_NAME_OPT],
697
             "[opts...]",
698
             "Alters the parameters of the cluster"),
699
  }
700

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