Statistics
| Branch: | Tag: | Revision:

root / scripts / gnt-cluster @ 064c21f8

History | View | Annotate | Download (19.8 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
                        )
103
  op = opcodes.OpPostInitCluster()
104
  SubmitOpCode(op)
105
  return 0
106

    
107

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

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

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

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

    
131

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

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

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

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

    
155

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

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

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

    
170

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

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

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

    
190

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

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

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

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

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

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

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

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

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

    
230
  ToStdout("Cluster name: %s", result["name"])
231

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

    
235
  ToStdout("Master node: %s", result["master"])
236

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

    
240
  if result["tags"]:
241
    tags = ", ".join(utils.NiceSort(result["tags"]))
242
  else:
243
    tags = "(none)"
244

    
245
  ToStdout("Tags: %s", tags)
246

    
247
  ToStdout("Default hypervisor: %s", result["default_hypervisor"])
248
  ToStdout("Enabled hypervisors: %s", ", ".join(result["enabled_hypervisors"]))
249

    
250
  ToStdout("Hypervisor parameters:")
251
  _PrintGroupedParams(result["hvparams"])
252

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

    
259
  ToStdout("Default instance parameters:")
260
  _PrintGroupedParams(result["beparams"])
261

    
262
  ToStdout("Default nic parameters:")
263
  _PrintGroupedParams(result["nicparams"])
264

    
265
  return 0
266

    
267

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

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

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

    
283
  cl = GetClient()
284

    
285
  myname = utils.HostInfo().name
286

    
287
  cluster_name = cl.QueryConfigValues(["cluster_name"])[0]
288

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

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

    
297
  return 0
298

    
299

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

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

    
309
  """
310
  cl = GetClient()
311

    
312
  command = " ".join(args)
313

    
314
  nodes = GetOnlineNodes(nodes=opts.nodes, cl=cl)
315

    
316
  cluster_name, master_node = cl.QueryConfigValues(["cluster_name",
317
                                                    "master_node"])
318

    
319
  srun = ssh.SshRunner(cluster_name=cluster_name)
320

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

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

    
333
  return 0
334

    
335

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

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

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

    
358

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

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

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

    
374
  bad_nodes, instances, missing = result
375

    
376
  retcode = constants.EXIT_SUCCESS
377

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

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

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

    
416
  return retcode
417

    
418

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

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

    
428
  """
429
  op = opcodes.OpRepairDiskSizes(instances=args)
430
  SubmitOpCode(op)
431

    
432

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

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

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

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

    
456
  return bootstrap.MasterFailover(no_voting=opts.no_voting)
457

    
458

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

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

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

    
478

    
479
def SetClusterParams(opts, args):
480
  """Modify the cluster.
481

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

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

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

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

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

    
512
  beparams = opts.beparams
513
  utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
514

    
515
  nicparams = opts.nicparams
516
  utils.ForceDictType(nicparams, constants.NICS_PARAMETER_TYPES)
517

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

    
527

    
528
def QueueOps(opts, args):
529
  """Queue operations.
530

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

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

    
553
  return 0
554

    
555

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

    
562

    
563
def WatcherOps(opts, args):
564
  """Watcher operations.
565

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

    
572
  """
573
  command = args[0]
574
  client = GetClient()
575

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

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

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

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

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

    
594
  return 0
595

    
596

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

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