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