LUSetInstanceParams: nic parameters
[ganeti-local] / scripts / gnt-cluster
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 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 not None:
62     hvlist = hvlist.split(",")
63   else:
64     hvlist = [opts.default_hypervisor]
65
66   # avoid an impossible situation
67   if opts.default_hypervisor not in hvlist:
68     ToStderr("The default hypervisor requested (%s) is not"
69              " within the enabled hypervisor list (%s)" %
70              (opts.default_hypervisor, hvlist))
71     return 1
72
73   hvparams = dict(opts.hvparams)
74   beparams = opts.beparams
75   nicparams = opts.nicparams
76
77   # prepare beparams dict
78   beparams = objects.FillDict(constants.BEC_DEFAULTS, beparams)
79   utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
80
81   # prepare nicparams dict
82   nicparams = objects.FillDict(constants.NICC_DEFAULTS, nicparams)
83   utils.ForceDictType(nicparams, constants.NICS_PARAMETER_TYPES)
84
85   # prepare hvparams dict
86   for hv in constants.HYPER_TYPES:
87     if hv not in hvparams:
88       hvparams[hv] = {}
89     hvparams[hv] = objects.FillDict(constants.HVC_DEFAULTS[hv], hvparams[hv])
90     utils.ForceDictType(hvparams[hv], constants.HVS_PARAMETER_TYPES)
91
92   for hv in hvlist:
93     if hv not in constants.HYPER_TYPES:
94       ToStderr("invalid hypervisor: %s", hv)
95       return 1
96
97   bootstrap.InitCluster(cluster_name=args[0],
98                         secondary_ip=opts.secondary_ip,
99                         vg_name=vg_name,
100                         mac_prefix=opts.mac_prefix,
101                         def_bridge=opts.def_bridge,
102                         master_netdev=opts.master_netdev,
103                         file_storage_dir=opts.file_storage_dir,
104                         enabled_hypervisors=hvlist,
105                         default_hypervisor=opts.default_hypervisor,
106                         hvparams=hvparams,
107                         beparams=beparams,
108                         nicparams=nicparams,
109                         candidate_pool_size=opts.candidate_pool_size,
110                         modify_etc_hosts=opts.modify_etc_hosts,
111                         )
112   return 0
113
114
115 @UsesRPC
116 def DestroyCluster(opts, args):
117   """Destroy the cluster.
118
119   @param opts: the command line options selected by the user
120   @type args: list
121   @param args: should be an empty list
122   @rtype: int
123   @return: the desired exit code
124
125   """
126   if not opts.yes_do_it:
127     ToStderr("Destroying a cluster is irreversible. If you really want"
128              " destroy this cluster, supply the --yes-do-it option.")
129     return 1
130
131   op = opcodes.OpDestroyCluster()
132   master = SubmitOpCode(op)
133   # if we reached this, the opcode didn't fail; we can proceed to
134   # shutdown all the daemons
135   bootstrap.FinalizeClusterDestroy(master)
136   return 0
137
138
139 def RenameCluster(opts, args):
140   """Rename the cluster.
141
142   @param opts: the command line options selected by the user
143   @type args: list
144   @param args: should contain only one element, the new cluster name
145   @rtype: int
146   @return: the desired exit code
147
148   """
149   name = args[0]
150   if not opts.force:
151     usertext = ("This will rename the cluster to '%s'. If you are connected"
152                 " over the network to the cluster name, the operation is very"
153                 " dangerous as the IP address will be removed from the node"
154                 " and the change may not go through. Continue?") % name
155     if not AskUser(usertext):
156       return 1
157
158   op = opcodes.OpRenameCluster(name=name)
159   SubmitOpCode(op)
160   return 0
161
162
163 def RedistributeConfig(opts, args):
164   """Forces push of the cluster configuration.
165
166   @param opts: the command line options selected by the user
167   @type args: list
168   @param args: empty list
169   @rtype: int
170   @return: the desired exit code
171
172   """
173   op = opcodes.OpRedistributeConfig()
174   SubmitOrSend(op, opts)
175   return 0
176
177
178 def ShowClusterVersion(opts, args):
179   """Write version of ganeti software to the standard output.
180
181   @param opts: the command line options selected by the user
182   @type args: list
183   @param args: should be an empty list
184   @rtype: int
185   @return: the desired exit code
186
187   """
188   cl = GetClient()
189   result = cl.QueryClusterInfo()
190   ToStdout("Software version: %s", result["software_version"])
191   ToStdout("Internode protocol: %s", result["protocol_version"])
192   ToStdout("Configuration format: %s", result["config_version"])
193   ToStdout("OS api version: %s", result["os_api_version"])
194   ToStdout("Export interface: %s", result["export_version"])
195   return 0
196
197
198 def ShowClusterMaster(opts, args):
199   """Write name of master node to the standard output.
200
201   @param opts: the command line options selected by the user
202   @type args: list
203   @param args: should be an empty list
204   @rtype: int
205   @return: the desired exit code
206
207   """
208   master = bootstrap.GetMaster()
209   ToStdout(master)
210   return 0
211
212 def _PrintGroupedParams(paramsdict):
213   """Print Grouped parameters (be, nic, disk) by group.
214
215   @type paramsdict: dict of dicts
216   @param paramsdict: {group: {param: value, ...}, ...}
217
218   """
219   for gr_name, gr_dict in paramsdict.items():
220     ToStdout("  - %s:", gr_name)
221     for item, val in gr_dict.iteritems():
222       ToStdout("      %s: %s", item, val)
223
224 def ShowClusterConfig(opts, args):
225   """Shows cluster information.
226
227   @param opts: the command line options selected by the user
228   @type args: list
229   @param args: should be an empty list
230   @rtype: int
231   @return: the desired exit code
232
233   """
234   cl = GetClient()
235   result = cl.QueryClusterInfo()
236
237   ToStdout("Cluster name: %s", result["name"])
238
239   ToStdout("Master node: %s", result["master"])
240
241   ToStdout("Architecture (this node): %s (%s)",
242            result["architecture"][0], result["architecture"][1])
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("  - default bridge: %s", result["default_bridge"])
254   ToStdout("  - lvm volume group: %s", result["volume_group_name"])
255   ToStdout("  - file storage path: %s", result["file_storage_dir"])
256
257   ToStdout("Default instance parameters:")
258   _PrintGroupedParams(result["beparams"])
259
260   ToStdout("Default nic parameters:")
261   _PrintGroupedParams(result["nicparams"])
262
263   return 0
264
265
266 def ClusterCopyFile(opts, args):
267   """Copy a file from master to some nodes.
268
269   @param opts: the command line options selected by the user
270   @type args: list
271   @param args: should contain only one element, the path of
272       the file to be copied
273   @rtype: int
274   @return: the desired exit code
275
276   """
277   filename = args[0]
278   if not os.path.exists(filename):
279     raise errors.OpPrereqError("No such filename '%s'" % filename)
280
281   cl = GetClient()
282
283   myname = utils.HostInfo().name
284
285   cluster_name = cl.QueryConfigValues(["cluster_name"])[0]
286
287   results = GetOnlineNodes(nodes=opts.nodes, cl=cl)
288   results = [name for name in results if name != myname]
289
290   srun = ssh.SshRunner(cluster_name=cluster_name)
291   for node in results:
292     if not srun.CopyFileToNode(node, filename):
293       ToStderr("Copy of file %s to node %s failed", filename, node)
294
295   return 0
296
297
298 def RunClusterCommand(opts, args):
299   """Run a command on some nodes.
300
301   @param opts: the command line options selected by the user
302   @type args: list
303   @param args: should contain the command to be run and its arguments
304   @rtype: int
305   @return: the desired exit code
306
307   """
308   cl = GetClient()
309
310   command = " ".join(args)
311
312   nodes = GetOnlineNodes(nodes=opts.nodes, cl=cl)
313
314   cluster_name, master_node = cl.QueryConfigValues(["cluster_name",
315                                                     "master_node"])
316
317   srun = ssh.SshRunner(cluster_name=cluster_name)
318
319   # Make sure master node is at list end
320   if master_node in nodes:
321     nodes.remove(master_node)
322     nodes.append(master_node)
323
324   for name in nodes:
325     result = srun.Run(name, "root", command)
326     ToStdout("------------------------------------------------")
327     ToStdout("node: %s", name)
328     ToStdout("%s", result.output)
329     ToStdout("return code = %s", result.exit_code)
330
331   return 0
332
333
334 def VerifyCluster(opts, args):
335   """Verify integrity of cluster, performing various test on nodes.
336
337   @param opts: the command line options selected by the user
338   @type args: list
339   @param args: should be an empty list
340   @rtype: int
341   @return: the desired exit code
342
343   """
344   skip_checks = []
345   if opts.skip_nplusone_mem:
346     skip_checks.append(constants.VERIFY_NPLUSONE_MEM)
347   op = opcodes.OpVerifyCluster(skip_checks=skip_checks)
348   if SubmitOpCode(op):
349     return 0
350   else:
351     return 1
352
353
354 def VerifyDisks(opts, args):
355   """Verify integrity of cluster disks.
356
357   @param opts: the command line options selected by the user
358   @type args: list
359   @param args: should be an empty list
360   @rtype: int
361   @return: the desired exit code
362
363   """
364   op = opcodes.OpVerifyDisks()
365   result = SubmitOpCode(op)
366   if not isinstance(result, (list, tuple)) or len(result) != 4:
367     raise errors.ProgrammerError("Unknown result type for OpVerifyDisks")
368
369   nodes, nlvm, instances, missing = result
370
371   if nodes:
372     ToStdout("Nodes unreachable or with bad data:")
373     for name in nodes:
374       ToStdout("\t%s", name)
375   retcode = constants.EXIT_SUCCESS
376
377   if nlvm:
378     for node, text in nlvm.iteritems():
379       ToStdout("Error on node %s: LVM error: %s",
380                node, utils.SafeEncode(text[-400:]))
381       retcode |= 1
382       ToStdout("You need to fix these nodes first before fixing instances")
383
384   if instances:
385     for iname in instances:
386       if iname in missing:
387         continue
388       op = opcodes.OpActivateInstanceDisks(instance_name=iname)
389       try:
390         ToStdout("Activating disks for instance '%s'", iname)
391         SubmitOpCode(op)
392       except errors.GenericError, err:
393         nret, msg = FormatError(err)
394         retcode |= nret
395         ToStderr("Error activating disks for instance %s: %s", iname, msg)
396
397   if missing:
398     for iname, ival in missing.iteritems():
399       all_missing = utils.all(ival, lambda x: x[0] in nlvm)
400       if all_missing:
401         ToStdout("Instance %s cannot be verified as it lives on"
402                  " broken nodes", iname)
403       else:
404         ToStdout("Instance %s has missing logical volumes:", iname)
405         ival.sort()
406         for node, vol in ival:
407           if node in nlvm:
408             ToStdout("\tbroken node %s /dev/xenvg/%s", node, vol)
409           else:
410             ToStdout("\t%s /dev/xenvg/%s", node, vol)
411     ToStdout("You need to run replace_disks for all the above"
412            " instances, if this message persist after fixing nodes.")
413     retcode |= 1
414
415   return retcode
416
417
418 @UsesRPC
419 def MasterFailover(opts, args):
420   """Failover the master node.
421
422   This command, when run on a non-master node, will cause the current
423   master to cease being master, and the non-master to become new
424   master.
425
426   @param opts: the command line options selected by the user
427   @type args: list
428   @param args: should be an empty list
429   @rtype: int
430   @return: the desired exit code
431
432   """
433   return bootstrap.MasterFailover()
434
435
436 def SearchTags(opts, args):
437   """Searches the tags on all the cluster.
438
439   @param opts: the command line options selected by the user
440   @type args: list
441   @param args: should contain only one element, the tag pattern
442   @rtype: int
443   @return: the desired exit code
444
445   """
446   op = opcodes.OpSearchTags(pattern=args[0])
447   result = SubmitOpCode(op)
448   if not result:
449     return 1
450   result = list(result)
451   result.sort()
452   for path, tag in result:
453     ToStdout("%s %s", path, tag)
454
455
456 def SetClusterParams(opts, args):
457   """Modify the cluster.
458
459   @param opts: the command line options selected by the user
460   @type args: list
461   @param args: should be an empty list
462   @rtype: int
463   @return: the desired exit code
464
465   """
466   if not (not opts.lvm_storage or opts.vg_name or
467           opts.enabled_hypervisors or opts.hvparams or
468           opts.beparams or opts.nicparams or
469           opts.candidate_pool_size is not None):
470     ToStderr("Please give at least one of the parameters.")
471     return 1
472
473   vg_name = opts.vg_name
474   if not opts.lvm_storage and opts.vg_name:
475     ToStdout("Options --no-lvm-storage and --vg-name conflict.")
476     return 1
477   elif not opts.lvm_storage:
478     vg_name = ''
479
480   hvlist = opts.enabled_hypervisors
481   if hvlist is not None:
482     hvlist = hvlist.split(",")
483
484   # a list of (name, dict) we can pass directly to dict() (or [])
485   hvparams = dict(opts.hvparams)
486   for hv, hv_params in hvparams.iteritems():
487     utils.ForceDictType(hv_params, constants.HVS_PARAMETER_TYPES)
488
489   beparams = opts.beparams
490   utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
491
492   nicparams = opts.nicparams
493   utils.ForceDictType(nicparams, constants.NICS_PARAMETER_TYPES)
494
495   op = opcodes.OpSetClusterParams(vg_name=vg_name,
496                                   enabled_hypervisors=hvlist,
497                                   hvparams=hvparams,
498                                   beparams=beparams,
499                                   nicparams=nicparams,
500                                   candidate_pool_size=opts.candidate_pool_size)
501   SubmitOpCode(op)
502   return 0
503
504
505 def QueueOps(opts, args):
506   """Queue operations.
507
508   @param opts: the command line options selected by the user
509   @type args: list
510   @param args: should contain only one element, the subcommand
511   @rtype: int
512   @return: the desired exit code
513
514   """
515   command = args[0]
516   client = GetClient()
517   if command in ("drain", "undrain"):
518     drain_flag = command == "drain"
519     client.SetQueueDrainFlag(drain_flag)
520   elif command == "info":
521     result = client.QueryConfigValues(["drain_flag"])
522     if result[0]:
523       val = "set"
524     else:
525       val = "unset"
526     ToStdout("The drain flag is %s" % val)
527   else:
528     raise errors.OpPrereqError("Command '%s' is not valid." % command)
529
530   return 0
531
532 # this is an option common to more than one command, so we declare
533 # it here and reuse it
534 node_option = make_option("-n", "--node", action="append", dest="nodes",
535                           help="Node to copy to (if not given, all nodes),"
536                                " can be given multiple times",
537                           metavar="<node>", default=[])
538
539 commands = {
540   'init': (InitCluster, ARGS_ONE,
541            [DEBUG_OPT,
542             make_option("-s", "--secondary-ip", dest="secondary_ip",
543                         help="Specify the secondary ip for this node;"
544                         " if given, the entire cluster must have secondary"
545                         " addresses",
546                         metavar="ADDRESS", default=None),
547             make_option("-m", "--mac-prefix", dest="mac_prefix",
548                         help="Specify the mac prefix for the instance IP"
549                         " addresses, in the format XX:XX:XX",
550                         metavar="PREFIX",
551                         default=constants.DEFAULT_MAC_PREFIX,),
552             make_option("-g", "--vg-name", dest="vg_name",
553                         help="Specify the volume group name "
554                         " (cluster-wide) for disk allocation [xenvg]",
555                         metavar="VG",
556                         default=None,),
557             make_option("-b", "--bridge", dest="def_bridge",
558                         help="Specify the default bridge name (cluster-wide)"
559                           " to connect the instances to [%s]" %
560                           constants.DEFAULT_BRIDGE,
561                         metavar="BRIDGE",
562                         default=constants.DEFAULT_BRIDGE,),
563             make_option("--master-netdev", dest="master_netdev",
564                         help="Specify the node interface (cluster-wide)"
565                           " on which the master IP address will be added "
566                           " [%s]" % constants.DEFAULT_BRIDGE,
567                         metavar="NETDEV",
568                         default=constants.DEFAULT_BRIDGE,),
569             make_option("--file-storage-dir", dest="file_storage_dir",
570                         help="Specify the default directory (cluster-wide)"
571                              " for storing the file-based disks [%s]" %
572                              constants.DEFAULT_FILE_STORAGE_DIR,
573                         metavar="DIR",
574                         default=constants.DEFAULT_FILE_STORAGE_DIR,),
575             make_option("--no-lvm-storage", dest="lvm_storage",
576                         help="No support for lvm based instances"
577                              " (cluster-wide)",
578                         action="store_false", default=True,),
579             make_option("--no-etc-hosts", dest="modify_etc_hosts",
580                         help="Don't modify /etc/hosts"
581                              " (cluster-wide)",
582                         action="store_false", default=True,),
583             make_option("--enabled-hypervisors", dest="enabled_hypervisors",
584                         help="Comma-separated list of hypervisors",
585                         type="string", default=None),
586             make_option("-t", "--default-hypervisor",
587                         dest="default_hypervisor",
588                         help="Default hypervisor to use for instance creation",
589                         choices=list(constants.HYPER_TYPES),
590                         default=constants.DEFAULT_ENABLED_HYPERVISOR),
591             ikv_option("-H", "--hypervisor-parameters", dest="hvparams",
592                        help="Hypervisor and hypervisor options, in the"
593                          " format"
594                        " hypervisor:option=value,option=value,...",
595                        default=[],
596                        action="append",
597                        type="identkeyval"),
598             keyval_option("-B", "--backend-parameters", dest="beparams",
599                           type="keyval", default={},
600                           help="Backend parameters"),
601             keyval_option("-N", "--nic-parameters", dest="nicparams",
602                           type="keyval", default={},
603                           help="NIC parameters"),
604             make_option("-C", "--candidate-pool-size",
605                         default=constants.MASTER_POOL_SIZE_DEFAULT,
606                         help="Set the candidate pool size",
607                         dest="candidate_pool_size", type="int"),
608             ],
609            "[opts...] <cluster_name>",
610            "Initialises a new cluster configuration"),
611   'destroy': (DestroyCluster, ARGS_NONE,
612               [DEBUG_OPT,
613                make_option("--yes-do-it", dest="yes_do_it",
614                            help="Destroy cluster",
615                            action="store_true"),
616               ],
617               "", "Destroy cluster"),
618   'rename': (RenameCluster, ARGS_ONE, [DEBUG_OPT, FORCE_OPT],
619                "<new_name>",
620                "Renames the cluster"),
621   'redist-conf': (RedistributeConfig, ARGS_NONE, [DEBUG_OPT, SUBMIT_OPT],
622                   "",
623                   "Forces a push of the configuration file and ssconf files"
624                   " to the nodes in the cluster"),
625   'verify': (VerifyCluster, ARGS_NONE, [DEBUG_OPT,
626              make_option("--no-nplus1-mem", dest="skip_nplusone_mem",
627                          help="Skip N+1 memory redundancy tests",
628                          action="store_true",
629                          default=False,),
630              ],
631              "", "Does a check on the cluster configuration"),
632   'verify-disks': (VerifyDisks, ARGS_NONE, [DEBUG_OPT],
633                    "", "Does a check on the cluster disk status"),
634   'masterfailover': (MasterFailover, ARGS_NONE, [DEBUG_OPT],
635                      "", "Makes the current node the master"),
636   'version': (ShowClusterVersion, ARGS_NONE, [DEBUG_OPT],
637               "", "Shows the cluster version"),
638   'getmaster': (ShowClusterMaster, ARGS_NONE, [DEBUG_OPT],
639                 "", "Shows the cluster master"),
640   'copyfile': (ClusterCopyFile, ARGS_ONE, [DEBUG_OPT, node_option],
641                "[-n node...] <filename>",
642                "Copies a file to all (or only some) nodes"),
643   'command': (RunClusterCommand, ARGS_ATLEAST(1), [DEBUG_OPT, node_option],
644               "[-n node...] <command>",
645               "Runs a command on all (or only some) nodes"),
646   'info': (ShowClusterConfig, ARGS_NONE, [DEBUG_OPT],
647                  "", "Show cluster configuration"),
648   'list-tags': (ListTags, ARGS_NONE,
649                 [DEBUG_OPT], "", "List the tags of the cluster"),
650   'add-tags': (AddTags, ARGS_ANY, [DEBUG_OPT, TAG_SRC_OPT],
651                "tag...", "Add tags to the cluster"),
652   'remove-tags': (RemoveTags, ARGS_ANY, [DEBUG_OPT, TAG_SRC_OPT],
653                   "tag...", "Remove tags from the cluster"),
654   'search-tags': (SearchTags, ARGS_ONE,
655                   [DEBUG_OPT], "", "Searches the tags on all objects on"
656                   " the cluster for a given pattern (regex)"),
657   'queue': (QueueOps, ARGS_ONE, [DEBUG_OPT],
658             "drain|undrain|info", "Change queue properties"),
659   'modify': (SetClusterParams, ARGS_NONE,
660              [DEBUG_OPT,
661               make_option("-g", "--vg-name", dest="vg_name",
662                           help="Specify the volume group name "
663                           " (cluster-wide) for disk allocation "
664                           "and enable lvm based storage",
665                           metavar="VG",),
666               make_option("--no-lvm-storage", dest="lvm_storage",
667                           help="Disable support for lvm based instances"
668                                " (cluster-wide)",
669                           action="store_false", default=True,),
670               make_option("--enabled-hypervisors", dest="enabled_hypervisors",
671                           help="Comma-separated list of hypervisors",
672                           type="string", default=None),
673               ikv_option("-H", "--hypervisor-parameters", dest="hvparams",
674                          help="Hypervisor and hypervisor options, in the"
675                          " format"
676                          " hypervisor:option=value,option=value,...",
677                          default=[],
678                          action="append",
679                          type="identkeyval"),
680               keyval_option("-B", "--backend-parameters", dest="beparams",
681                             type="keyval", default={},
682                             help="Backend parameters"),
683               keyval_option("-N", "--nic-parameters", dest="nicparams",
684                             type="keyval", default={},
685                             help="NIC parameters"),
686               make_option("-C", "--candidate-pool-size", default=None,
687                           help="Set the candidate pool size",
688                           dest="candidate_pool_size", type="int"),
689               ],
690              "[opts...]",
691              "Alters the parameters of the cluster"),
692   }
693
694 if __name__ == '__main__':
695   sys.exit(GenericMain(commands, override={"tag_type": constants.TAG_CLUSTER}))