Statistics
| Branch: | Tag: | Revision:

root / scripts / gnt-cluster @ 4b7735f9

History | View | Annotate | Download (22 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
from optparse import make_option
28
import os.path
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

    
38

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

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

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

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

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

    
65
  # avoid an impossible situation
66
  if opts.default_hypervisor in hvlist:
67
    default_hypervisor = opts.default_hypervisor
68
  else:
69
    default_hypervisor = hvlist[0]
70

    
71
  hvparams = opts.hvparams
72
  if hvparams:
73
    # a list of (name, dict) we can pass directly to dict()
74
    hvparams = dict(opts.hvparams)
75
  else:
76
    # otherwise init as empty dict
77
    hvparams = {}
78

    
79
  beparams = opts.beparams
80
  # check for invalid parameters
81
  for parameter in beparams:
82
    if parameter not in constants.BES_PARAMETERS:
83
      ToStderr("Invalid backend parameter: %s", parameter)
84
      return 1
85

    
86
  # prepare beparams dict
87
  for parameter in constants.BES_PARAMETERS:
88
    if parameter not in beparams:
89
      beparams[parameter] = constants.BEC_DEFAULTS[parameter]
90

    
91
  # type wrangling
92
  try:
93
    beparams[constants.BE_VCPUS] = int(beparams[constants.BE_VCPUS])
94
  except ValueError:
95
    ToStderr("%s must be an integer", constants.BE_VCPUS)
96
    return 1
97

    
98
  if not isinstance(beparams[constants.BE_MEMORY], int):
99
    beparams[constants.BE_MEMORY] = utils.ParseUnit(
100
        beparams[constants.BE_MEMORY])
101

    
102
  # prepare hvparams dict
103
  for hv in constants.HYPER_TYPES:
104
    if hv not in hvparams:
105
      hvparams[hv] = {}
106
    for parameter in constants.HVC_DEFAULTS[hv]:
107
      if parameter not in hvparams[hv]:
108
        hvparams[hv][parameter] = constants.HVC_DEFAULTS[hv][parameter]
109

    
110
  for hv in hvlist:
111
    if hv not in constants.HYPER_TYPES:
112
      ToStderr("invalid hypervisor: %s", hv)
113
      return 1
114

    
115
  bootstrap.InitCluster(cluster_name=args[0],
116
                        secondary_ip=opts.secondary_ip,
117
                        vg_name=vg_name,
118
                        mac_prefix=opts.mac_prefix,
119
                        def_bridge=opts.def_bridge,
120
                        master_netdev=opts.master_netdev,
121
                        file_storage_dir=opts.file_storage_dir,
122
                        enabled_hypervisors=hvlist,
123
                        default_hypervisor=default_hypervisor,
124
                        hvparams=hvparams,
125
                        beparams=beparams)
126
  return 0
127

    
128

    
129
@UsesRPC
130
def DestroyCluster(opts, args):
131
  """Destroy the cluster.
132

    
133
  @param opts: the command line options selected by the user
134
  @type args: list
135
  @param args: should be an empty list
136
  @rtype: int
137
  @return: the desired exit code
138

    
139
  """
140
  if not opts.yes_do_it:
141
    ToStderr("Destroying a cluster is irreversible. If you really want"
142
             " destroy this cluster, supply the --yes-do-it option.")
143
    return 1
144

    
145
  op = opcodes.OpDestroyCluster()
146
  master = SubmitOpCode(op)
147
  # if we reached this, the opcode didn't fail; we can proceed to
148
  # shutdown all the daemons
149
  bootstrap.FinalizeClusterDestroy(master)
150
  return 0
151

    
152

    
153
def RenameCluster(opts, args):
154
  """Rename the cluster.
155

    
156
  @param opts: the command line options selected by the user
157
  @type args: list
158
  @param args: should contain only one element, the new cluster name
159
  @rtype: int
160
  @return: the desired exit code
161

    
162
  """
163
  name = args[0]
164
  if not opts.force:
165
    usertext = ("This will rename the cluster to '%s'. If you are connected"
166
                " over the network to the cluster name, the operation is very"
167
                " dangerous as the IP address will be removed from the node"
168
                " and the change may not go through. Continue?") % name
169
    if not AskUser(usertext):
170
      return 1
171

    
172
  op = opcodes.OpRenameCluster(name=name)
173
  SubmitOpCode(op)
174
  return 0
175

    
176

    
177
def ShowClusterVersion(opts, args):
178
  """Write version of ganeti software to the standard output.
179

    
180
  @param opts: the command line options selected by the user
181
  @type args: list
182
  @param args: should be an empty list
183
  @rtype: int
184
  @return: the desired exit code
185

    
186
  """
187
  op = opcodes.OpQueryClusterInfo()
188
  result = SubmitOpCode(op)
189
  ToStdout("Software version: %s", result["software_version"])
190
  ToStdout("Internode protocol: %s", result["protocol_version"])
191
  ToStdout("Configuration format: %s", result["config_version"])
192
  ToStdout("OS api version: %s", result["os_api_version"])
193
  ToStdout("Export interface: %s", result["export_version"])
194
  return 0
195

    
196

    
197
def ShowClusterMaster(opts, args):
198
  """Write name of master node to the standard output.
199

    
200
  @param opts: the command line options selected by the user
201
  @type args: list
202
  @param args: should be an empty list
203
  @rtype: int
204
  @return: the desired exit code
205

    
206
  """
207
  ToStdout("%s", GetClient().QueryConfigValues(["master_node"])[0])
208
  return 0
209

    
210

    
211
def ShowClusterConfig(opts, args):
212
  """Shows cluster information.
213

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

    
220
  """
221
  op = opcodes.OpQueryClusterInfo()
222
  result = SubmitOpCode(op)
223

    
224
  ToStdout("Cluster name: %s", result["name"])
225

    
226
  ToStdout("Master node: %s", result["master"])
227

    
228
  ToStdout("Architecture (this node): %s (%s)",
229
           result["architecture"][0], result["architecture"][1])
230

    
231
  ToStdout("Default hypervisor: %s", result["default_hypervisor"])
232
  ToStdout("Enabled hypervisors: %s", ", ".join(result["enabled_hypervisors"]))
233

    
234
  ToStdout("Hypervisor parameters:")
235
  for hv_name, hv_dict in result["hvparams"].items():
236
    ToStdout("  - %s:", hv_name)
237
    for item, val in hv_dict.iteritems():
238
      ToStdout("      %s: %s", item, val)
239

    
240
  ToStdout("Cluster parameters:")
241
  ToStdout("  - candidate pool size: %s", result["candidate_pool_size"])
242

    
243
  ToStdout("Default instance parameters:")
244
  for gr_name, gr_dict in result["beparams"].items():
245
    ToStdout("  - %s:", gr_name)
246
    for item, val in gr_dict.iteritems():
247
      ToStdout("      %s: %s", item, val)
248

    
249
  return 0
250

    
251

    
252
def ClusterCopyFile(opts, args):
253
  """Copy a file from master to some nodes.
254

    
255
  @param opts: the command line options selected by the user
256
  @type args: list
257
  @param args: should contain only one element, the path of
258
      the file to be copied
259
  @rtype: int
260
  @return: the desired exit code
261

    
262
  """
263
  filename = args[0]
264
  if not os.path.exists(filename):
265
    raise errors.OpPrereqError("No such filename '%s'" % filename)
266

    
267
  cl = GetClient()
268

    
269
  myname = utils.HostInfo().name
270

    
271
  cluster_name = cl.QueryConfigValues(["cluster_name"])[0]
272

    
273
  op = opcodes.OpQueryNodes(output_fields=["name"], names=opts.nodes)
274
  results = [row[0] for row in SubmitOpCode(op, cl=cl) if row[0] != myname]
275

    
276
  srun = ssh.SshRunner(cluster_name=cluster_name)
277
  for node in results:
278
    if not srun.CopyFileToNode(node, filename):
279
      ToStderr("Copy of file %s to node %s failed", filename, node)
280

    
281
  return 0
282

    
283

    
284
def RunClusterCommand(opts, args):
285
  """Run a command on some nodes.
286

    
287
  @param opts: the command line options selected by the user
288
  @type args: list
289
  @param args: should contain the command to be run and its arguments
290
  @rtype: int
291
  @return: the desired exit code
292

    
293
  """
294
  cl = GetClient()
295

    
296
  command = " ".join(args)
297
  op = opcodes.OpQueryNodes(output_fields=["name"], names=opts.nodes)
298
  nodes = [row[0] for row in SubmitOpCode(op, cl=cl)]
299

    
300
  cluster_name, master_node = cl.QueryConfigValues(["cluster_name",
301
                                                    "master_node"])
302

    
303
  srun = ssh.SshRunner(cluster_name=cluster_name)
304

    
305
  # Make sure master node is at list end
306
  if master_node in nodes:
307
    nodes.remove(master_node)
308
    nodes.append(master_node)
309

    
310
  for name in nodes:
311
    result = srun.Run(name, "root", command)
312
    ToStdout("------------------------------------------------")
313
    ToStdout("node: %s", name)
314
    ToStdout("%s", result.output)
315
    ToStdout("return code = %s", result.exit_code)
316

    
317
  return 0
318

    
319

    
320
def VerifyCluster(opts, args):
321
  """Verify integrity of cluster, performing various test on nodes.
322

    
323
  @param opts: the command line options selected by the user
324
  @type args: list
325
  @param args: should be an empty list
326
  @rtype: int
327
  @return: the desired exit code
328

    
329
  """
330
  skip_checks = []
331
  if opts.skip_nplusone_mem:
332
    skip_checks.append(constants.VERIFY_NPLUSONE_MEM)
333
  op = opcodes.OpVerifyCluster(skip_checks=skip_checks)
334
  if SubmitOpCode(op):
335
    return 0
336
  else:
337
    return 1
338

    
339

    
340
def VerifyDisks(opts, args):
341
  """Verify integrity of cluster disks.
342

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

    
349
  """
350
  op = opcodes.OpVerifyDisks()
351
  result = SubmitOpCode(op)
352
  if not isinstance(result, (list, tuple)) or len(result) != 4:
353
    raise errors.ProgrammerError("Unknown result type for OpVerifyDisks")
354

    
355
  nodes, nlvm, instances, missing = result
356

    
357
  if nodes:
358
    ToStdout("Nodes unreachable or with bad data:")
359
    for name in nodes:
360
      ToStdout("\t%s", name)
361
  retcode = constants.EXIT_SUCCESS
362

    
363
  if nlvm:
364
    for node, text in nlvm.iteritems():
365
      ToStdout("Error on node %s: LVM error: %s",
366
               node, text[-400:].encode('string_escape'))
367
      retcode |= 1
368
      ToStdout("You need to fix these nodes first before fixing instances")
369

    
370
  if instances:
371
    for iname in instances:
372
      if iname in missing:
373
        continue
374
      op = opcodes.OpActivateInstanceDisks(instance_name=iname)
375
      try:
376
        ToStdout("Activating disks for instance '%s'", iname)
377
        SubmitOpCode(op)
378
      except errors.GenericError, err:
379
        nret, msg = FormatError(err)
380
        retcode |= nret
381
        ToStderr("Error activating disks for instance %s: %s", iname, msg)
382

    
383
  if missing:
384
    for iname, ival in missing.iteritems():
385
      all_missing = utils.all(ival, lambda x: x[0] in nlvm)
386
      if all_missing:
387
        ToStdout("Instance %s cannot be verified as it lives on"
388
                 " broken nodes", iname)
389
      else:
390
        ToStdout("Instance %s has missing logical volumes:", iname)
391
        ival.sort()
392
        for node, vol in ival:
393
          if node in nlvm:
394
            ToStdout("\tbroken node %s /dev/xenvg/%s", node, vol)
395
          else:
396
            ToStdout("\t%s /dev/xenvg/%s", node, vol)
397
    ToStdout("You need to run replace_disks for all the above"
398
           " instances, if this message persist after fixing nodes.")
399
    retcode |= 1
400

    
401
  return retcode
402

    
403

    
404
@UsesRPC
405
def MasterFailover(opts, args):
406
  """Failover the master node.
407

    
408
  This command, when run on a non-master node, will cause the current
409
  master to cease being master, and the non-master to become new
410
  master.
411

    
412
  @param opts: the command line options selected by the user
413
  @type args: list
414
  @param args: should be an empty list
415
  @rtype: int
416
  @return: the desired exit code
417

    
418
  """
419
  return bootstrap.MasterFailover()
420

    
421

    
422
def SearchTags(opts, args):
423
  """Searches the tags on all the cluster.
424

    
425
  @param opts: the command line options selected by the user
426
  @type args: list
427
  @param args: should contain only one element, the tag pattern
428
  @rtype: int
429
  @return: the desired exit code
430

    
431
  """
432
  op = opcodes.OpSearchTags(pattern=args[0])
433
  result = SubmitOpCode(op)
434
  if not result:
435
    return 1
436
  result = list(result)
437
  result.sort()
438
  for path, tag in result:
439
    ToStdout("%s %s", path, tag)
440

    
441

    
442
def SetClusterParams(opts, args):
443
  """Modify the cluster.
444

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

    
451
  """
452
  if not (not opts.lvm_storage or opts.vg_name or
453
          opts.enabled_hypervisors or opts.hvparams or
454
          opts.beparams or opts.candidate_pool_size is not None):
455
    ToStderr("Please give at least one of the parameters.")
456
    return 1
457

    
458
  vg_name = opts.vg_name
459
  if not opts.lvm_storage and opts.vg_name:
460
    ToStdout("Options --no-lvm-storage and --vg-name conflict.")
461
    return 1
462

    
463
  hvlist = opts.enabled_hypervisors
464
  if hvlist is not None:
465
    hvlist = hvlist.split(",")
466

    
467
  hvparams = opts.hvparams
468
  if hvparams:
469
    # a list of (name, dict) we can pass directly to dict()
470
    hvparams = dict(opts.hvparams)
471

    
472
  beparams = opts.beparams
473

    
474
  op = opcodes.OpSetClusterParams(vg_name=opts.vg_name,
475
                                  enabled_hypervisors=hvlist,
476
                                  hvparams=hvparams,
477
                                  beparams=beparams,
478
                                  candidate_pool_size=opts.candidate_pool_size)
479
  SubmitOpCode(op)
480
  return 0
481

    
482

    
483
def QueueOps(opts, args):
484
  """Queue operations.
485

    
486
  @param opts: the command line options selected by the user
487
  @type args: list
488
  @param args: should contain only one element, the subcommand
489
  @rtype: int
490
  @return: the desired exit code
491

    
492
  """
493
  command = args[0]
494
  client = GetClient()
495
  if command in ("drain", "undrain"):
496
    drain_flag = command == "drain"
497
    client.SetQueueDrainFlag(drain_flag)
498
  elif command == "info":
499
    result = client.QueryConfigValues(["drain_flag"])
500
    if result[0]:
501
      val = "set"
502
    else:
503
      val = "unset"
504
    ToStdout("The drain flag is %s" % val)
505
  return 0
506

    
507
# this is an option common to more than one command, so we declare
508
# it here and reuse it
509
node_option = make_option("-n", "--node", action="append", dest="nodes",
510
                          help="Node to copy to (if not given, all nodes),"
511
                               " can be given multiple times",
512
                          metavar="<node>", default=[])
513

    
514
commands = {
515
  'init': (InitCluster, ARGS_ONE,
516
           [DEBUG_OPT,
517
            make_option("-s", "--secondary-ip", dest="secondary_ip",
518
                        help="Specify the secondary ip for this node;"
519
                        " if given, the entire cluster must have secondary"
520
                        " addresses",
521
                        metavar="ADDRESS", default=None),
522
            make_option("-m", "--mac-prefix", dest="mac_prefix",
523
                        help="Specify the mac prefix for the instance IP"
524
                        " addresses, in the format XX:XX:XX",
525
                        metavar="PREFIX",
526
                        default="aa:00:00",),
527
            make_option("-g", "--vg-name", dest="vg_name",
528
                        help="Specify the volume group name "
529
                        " (cluster-wide) for disk allocation [xenvg]",
530
                        metavar="VG",
531
                        default=None,),
532
            make_option("-b", "--bridge", dest="def_bridge",
533
                        help="Specify the default bridge name (cluster-wide)"
534
                          " to connect the instances to [%s]" %
535
                          constants.DEFAULT_BRIDGE,
536
                        metavar="BRIDGE",
537
                        default=constants.DEFAULT_BRIDGE,),
538
            make_option("--master-netdev", dest="master_netdev",
539
                        help="Specify the node interface (cluster-wide)"
540
                          " on which the master IP address will be added "
541
                          " [%s]" % constants.DEFAULT_BRIDGE,
542
                        metavar="NETDEV",
543
                        default=constants.DEFAULT_BRIDGE,),
544
            make_option("--file-storage-dir", dest="file_storage_dir",
545
                        help="Specify the default directory (cluster-wide)"
546
                             " for storing the file-based disks [%s]" %
547
                             constants.DEFAULT_FILE_STORAGE_DIR,
548
                        metavar="DIR",
549
                        default=constants.DEFAULT_FILE_STORAGE_DIR,),
550
            make_option("--no-lvm-storage", dest="lvm_storage",
551
                        help="No support for lvm based instances"
552
                             " (cluster-wide)",
553
                        action="store_false", default=True,),
554
            make_option("--enabled-hypervisors", dest="enabled_hypervisors",
555
                        help="Comma-separated list of hypervisors",
556
                        type="string", default=None),
557
            make_option("-t", "--default-hypervisor",
558
                        dest="default_hypervisor",
559
                        help="Default hypervisor to use for instance creation",
560
                        choices=list(constants.HYPER_TYPES),
561
                        default=constants.DEFAULT_ENABLED_HYPERVISOR),
562
            ikv_option("-H", "--hypervisor-parameters", dest="hvparams",
563
                       help="Hypervisor and hypervisor options, in the"
564
                         " format"
565
                       " hypervisor:option=value,option=value,...",
566
                       default=[],
567
                       action="append",
568
                       type="identkeyval"),
569
            keyval_option("-B", "--backend-parameters", dest="beparams",
570
                          type="keyval", default={},
571
                          help="Backend parameters"),
572
            ],
573
           "[opts...] <cluster_name>",
574
           "Initialises a new cluster configuration"),
575
  'destroy': (DestroyCluster, ARGS_NONE,
576
              [DEBUG_OPT,
577
               make_option("--yes-do-it", dest="yes_do_it",
578
                           help="Destroy cluster",
579
                           action="store_true"),
580
              ],
581
              "", "Destroy cluster"),
582
  'rename': (RenameCluster, ARGS_ONE, [DEBUG_OPT, FORCE_OPT],
583
               "<new_name>",
584
               "Renames the cluster"),
585
  'verify': (VerifyCluster, ARGS_NONE, [DEBUG_OPT,
586
             make_option("--no-nplus1-mem", dest="skip_nplusone_mem",
587
                         help="Skip N+1 memory redundancy tests",
588
                         action="store_true",
589
                         default=False,),
590
             ],
591
             "", "Does a check on the cluster configuration"),
592
  'verify-disks': (VerifyDisks, ARGS_NONE, [DEBUG_OPT],
593
                   "", "Does a check on the cluster disk status"),
594
  'masterfailover': (MasterFailover, ARGS_NONE, [DEBUG_OPT],
595
                     "", "Makes the current node the master"),
596
  'version': (ShowClusterVersion, ARGS_NONE, [DEBUG_OPT],
597
              "", "Shows the cluster version"),
598
  'getmaster': (ShowClusterMaster, ARGS_NONE, [DEBUG_OPT],
599
                "", "Shows the cluster master"),
600
  'copyfile': (ClusterCopyFile, ARGS_ONE, [DEBUG_OPT, node_option],
601
               "[-n node...] <filename>",
602
               "Copies a file to all (or only some) nodes"),
603
  'command': (RunClusterCommand, ARGS_ATLEAST(1), [DEBUG_OPT, node_option],
604
              "[-n node...] <command>",
605
              "Runs a command on all (or only some) nodes"),
606
  'info': (ShowClusterConfig, ARGS_NONE, [DEBUG_OPT],
607
                 "", "Show cluster configuration"),
608
  'list-tags': (ListTags, ARGS_NONE,
609
                [DEBUG_OPT], "", "List the tags of the cluster"),
610
  'add-tags': (AddTags, ARGS_ANY, [DEBUG_OPT, TAG_SRC_OPT],
611
               "tag...", "Add tags to the cluster"),
612
  'remove-tags': (RemoveTags, ARGS_ANY, [DEBUG_OPT, TAG_SRC_OPT],
613
                  "tag...", "Remove tags from the cluster"),
614
  'search-tags': (SearchTags, ARGS_ONE,
615
                  [DEBUG_OPT], "", "Searches the tags on all objects on"
616
                  " the cluster for a given pattern (regex)"),
617
  'queue': (QueueOps, ARGS_ONE, [DEBUG_OPT],
618
            "drain|undrain|info", "Change queue properties"),
619
  'modify': (SetClusterParams, ARGS_NONE,
620
             [DEBUG_OPT,
621
              make_option("-g", "--vg-name", dest="vg_name",
622
                          help="Specify the volume group name "
623
                          " (cluster-wide) for disk allocation "
624
                          "and enable lvm based storage",
625
                          metavar="VG",),
626
              make_option("--no-lvm-storage", dest="lvm_storage",
627
                          help="Disable support for lvm based instances"
628
                               " (cluster-wide)",
629
                          action="store_false", default=True,),
630
              make_option("--enabled-hypervisors", dest="enabled_hypervisors",
631
                          help="Comma-separated list of hypervisors",
632
                          type="string", default=None),
633
              ikv_option("-H", "--hypervisor-parameters", dest="hvparams",
634
                         help="Hypervisor and hypervisor options, in the"
635
                         " format"
636
                         " hypervisor:option=value,option=value,...",
637
                         default=[],
638
                         action="append",
639
                         type="identkeyval"),
640
              keyval_option("-B", "--backend-parameters", dest="beparams",
641
                            type="keyval", default={},
642
                            help="Backend parameters"),
643
              make_option("-C", "--candidate-pool-size", default=None,
644
                          help="Set the candidate pool size",
645
                          dest="candidate_pool_size", type="int"),
646
              ],
647
             "[opts...]",
648
             "Alters the parameters of the cluster"),
649
  }
650

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