Implement “gnt-node repair-volume”
[ganeti-local] / scripts / gnt-node
1 #!/usr/bin/python
2 #
3
4 # Copyright (C) 2006, 2007, 2008 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
29 from ganeti.cli import *
30 from ganeti import cli
31 from ganeti import opcodes
32 from ganeti import utils
33 from ganeti import constants
34 from ganeti import errors
35 from ganeti import bootstrap
36
37
38 #: default list of field for L{ListNodes}
39 _LIST_DEF_FIELDS = [
40   "name", "dtotal", "dfree",
41   "mtotal", "mnode", "mfree",
42   "pinst_cnt", "sinst_cnt",
43   ]
44
45 #: headers (and full field list for L{ListNodes}
46 _LIST_HEADERS = {
47   "name": "Node", "pinst_cnt": "Pinst", "sinst_cnt": "Sinst",
48   "pinst_list": "PriInstances", "sinst_list": "SecInstances",
49   "pip": "PrimaryIP", "sip": "SecondaryIP",
50   "dtotal": "DTotal", "dfree": "DFree",
51   "mtotal": "MTotal", "mnode": "MNode", "mfree": "MFree",
52   "bootid": "BootID",
53   "ctotal": "CTotal", "cnodes": "CNodes", "csockets": "CSockets",
54   "tags": "Tags",
55   "serial_no": "SerialNo",
56   "master_candidate": "MasterC",
57   "master": "IsMaster",
58   "offline": "Offline", "drained": "Drained",
59   "role": "Role",
60   }
61
62 #: User-facing storage unit types
63 _USER_STORAGE_TYPE = {
64   constants.ST_FILE: "file",
65   constants.ST_LVM_PV: "lvm-pv",
66   constants.ST_LVM_VG: "lvm-vg",
67   }
68
69
70 def ConvertStorageType(user_storage_type):
71   """Converts a user storage type to its internal name.
72
73   """
74   try:
75     return _USER_STORAGE_TYPE[user_storage_type]
76   except KeyError:
77     raise errors.OpPrereqError("Unknown storage type: %s" % user_storage_type)
78
79
80 @UsesRPC
81 def AddNode(opts, args):
82   """Add a node to the cluster.
83
84   @param opts: the command line options selected by the user
85   @type args: list
86   @param args: should contain only one element, the new node name
87   @rtype: int
88   @return: the desired exit code
89
90   """
91   cl = GetClient()
92   dns_data = utils.HostInfo(args[0])
93   node = dns_data.name
94   readd = opts.readd
95
96   try:
97     output = cl.QueryNodes(names=[node], fields=['name', 'sip'],
98                            use_locking=False)
99     node_exists, sip = output[0]
100   except (errors.OpPrereqError, errors.OpExecError):
101     node_exists = ""
102     sip = None
103
104   if readd:
105     if not node_exists:
106       ToStderr("Node %s not in the cluster"
107                " - please retry without '--readd'", node)
108       return 1
109   else:
110     if node_exists:
111       ToStderr("Node %s already in the cluster (as %s)"
112                " - please retry with '--readd'", node, node_exists)
113       return 1
114     sip = opts.secondary_ip
115
116   # read the cluster name from the master
117   output = cl.QueryConfigValues(['cluster_name'])
118   cluster_name = output[0]
119
120   if not readd:
121     ToStderr("-- WARNING -- \n"
122              "Performing this operation is going to replace the ssh daemon"
123              " keypair\n"
124              "on the target machine (%s) with the ones of the"
125              " current one\n"
126              "and grant full intra-cluster ssh root access to/from it\n", node)
127
128   bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
129
130   op = opcodes.OpAddNode(node_name=args[0], secondary_ip=sip,
131                          readd=opts.readd)
132   SubmitOpCode(op)
133
134
135 def ListNodes(opts, args):
136   """List nodes and their properties.
137
138   @param opts: the command line options selected by the user
139   @type args: list
140   @param args: should be an empty list
141   @rtype: int
142   @return: the desired exit code
143
144   """
145   if opts.output is None:
146     selected_fields = _LIST_DEF_FIELDS
147   elif opts.output.startswith("+"):
148     selected_fields = _LIST_DEF_FIELDS + opts.output[1:].split(",")
149   else:
150     selected_fields = opts.output.split(",")
151
152   output = GetClient().QueryNodes(args, selected_fields, opts.do_locking)
153
154   if not opts.no_headers:
155     headers = _LIST_HEADERS
156   else:
157     headers = None
158
159   unitfields = ["dtotal", "dfree", "mtotal", "mnode", "mfree"]
160
161   numfields = ["dtotal", "dfree",
162                "mtotal", "mnode", "mfree",
163                "pinst_cnt", "sinst_cnt",
164                "ctotal", "serial_no"]
165
166   list_type_fields = ("pinst_list", "sinst_list", "tags")
167   # change raw values to nicer strings
168   for row in output:
169     for idx, field in enumerate(selected_fields):
170       val = row[idx]
171       if field in list_type_fields:
172         val = ",".join(val)
173       elif field in ('master', 'master_candidate', 'offline', 'drained'):
174         if val:
175           val = 'Y'
176         else:
177           val = 'N'
178       elif val is None:
179         val = "?"
180       row[idx] = str(val)
181
182   data = GenerateTable(separator=opts.separator, headers=headers,
183                        fields=selected_fields, unitfields=unitfields,
184                        numfields=numfields, data=output, units=opts.units)
185   for line in data:
186     ToStdout(line)
187
188   return 0
189
190
191 def EvacuateNode(opts, args):
192   """Relocate all secondary instance from a node.
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   cl = GetClient()
202   force = opts.force
203
204   dst_node = opts.dst_node
205   iallocator = opts.iallocator
206
207   cnt = [dst_node, iallocator].count(None)
208   if cnt != 1:
209     raise errors.OpPrereqError("One and only one of the -n and -i"
210                                " options must be passed")
211
212   selected_fields = ["name", "sinst_list"]
213   src_node = args[0]
214
215   result = cl.QueryNodes(names=[src_node], fields=selected_fields,
216                          use_locking=False)
217   src_node, sinst = result[0]
218
219   if not sinst:
220     ToStderr("No secondary instances on node %s, exiting.", src_node)
221     return constants.EXIT_SUCCESS
222
223   if dst_node is not None:
224     result = cl.QueryNodes(names=[dst_node], fields=["name"],
225                            use_locking=False)
226     dst_node = result[0][0]
227
228     if src_node == dst_node:
229       raise errors.OpPrereqError("Evacuate node needs different source and"
230                                  " target nodes (node %s given twice)" %
231                                  src_node)
232     txt_msg = "to node %s" % dst_node
233   else:
234     txt_msg = "using iallocator %s" % iallocator
235
236   sinst = utils.NiceSort(sinst)
237
238   if not force and not AskUser("Relocate instance(s) %s from node\n"
239                                " %s %s?" %
240                                (",".join("'%s'" % name for name in sinst),
241                                src_node, txt_msg)):
242     return constants.EXIT_CONFIRMATION
243
244   op = opcodes.OpEvacuateNode(node_name=args[0], remote_node=dst_node,
245                               iallocator=iallocator)
246   SubmitOpCode(op, cl=cl)
247
248
249 def FailoverNode(opts, args):
250   """Failover all primary instance on a node.
251
252   @param opts: the command line options selected by the user
253   @type args: list
254   @param args: should be an empty list
255   @rtype: int
256   @return: the desired exit code
257
258   """
259   cl = GetClient()
260   force = opts.force
261   selected_fields = ["name", "pinst_list"]
262
263   # these fields are static data anyway, so it doesn't matter, but
264   # locking=True should be safer
265   result = cl.QueryNodes(names=args, fields=selected_fields,
266                          use_locking=False)
267   node, pinst = result[0]
268
269   if not pinst:
270     ToStderr("No primary instances on node %s, exiting.", node)
271     return 0
272
273   pinst = utils.NiceSort(pinst)
274
275   retcode = 0
276
277   if not force and not AskUser("Fail over instance(s) %s?" %
278                                (",".join("'%s'" % name for name in pinst))):
279     return 2
280
281   jex = JobExecutor(cl=cl)
282   for iname in pinst:
283     op = opcodes.OpFailoverInstance(instance_name=iname,
284                                     ignore_consistency=opts.ignore_consistency)
285     jex.QueueJob(iname, op)
286   results = jex.GetResults()
287   bad_cnt = len([row for row in results if not row[0]])
288   if bad_cnt == 0:
289     ToStdout("All %d instance(s) failed over successfully.", len(results))
290   else:
291     ToStdout("There were errors during the failover:\n"
292              "%d error(s) out of %d instance(s).", bad_cnt, len(results))
293   return retcode
294
295
296 def MigrateNode(opts, args):
297   """Migrate all primary instance on a node.
298
299   """
300   cl = GetClient()
301   force = opts.force
302   selected_fields = ["name", "pinst_list"]
303
304   result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
305   node, pinst = result[0]
306
307   if not pinst:
308     ToStdout("No primary instances on node %s, exiting." % node)
309     return 0
310
311   pinst = utils.NiceSort(pinst)
312
313   retcode = 0
314
315   if not force and not AskUser("Migrate instance(s) %s?" %
316                                (",".join("'%s'" % name for name in pinst))):
317     return 2
318
319   op = opcodes.OpMigrateNode(node_name=args[0], live=opts.live)
320   SubmitOpCode(op, cl=cl)
321
322
323 def ShowNodeConfig(opts, args):
324   """Show node information.
325
326   @param opts: the command line options selected by the user
327   @type args: list
328   @param args: should either be an empty list, in which case
329       we show information about all nodes, or should contain
330       a list of nodes to be queried for information
331   @rtype: int
332   @return: the desired exit code
333
334   """
335   cl = GetClient()
336   result = cl.QueryNodes(fields=["name", "pip", "sip",
337                                  "pinst_list", "sinst_list",
338                                  "master_candidate", "drained", "offline"],
339                          names=args, use_locking=False)
340
341   for (name, primary_ip, secondary_ip, pinst, sinst,
342        is_mc, drained, offline) in result:
343     ToStdout("Node name: %s", name)
344     ToStdout("  primary ip: %s", primary_ip)
345     ToStdout("  secondary ip: %s", secondary_ip)
346     ToStdout("  master candidate: %s", is_mc)
347     ToStdout("  drained: %s", drained)
348     ToStdout("  offline: %s", offline)
349     if pinst:
350       ToStdout("  primary for instances:")
351       for iname in utils.NiceSort(pinst):
352         ToStdout("    - %s", iname)
353     else:
354       ToStdout("  primary for no instances")
355     if sinst:
356       ToStdout("  secondary for instances:")
357       for iname in utils.NiceSort(sinst):
358         ToStdout("    - %s", iname)
359     else:
360       ToStdout("  secondary for no instances")
361
362   return 0
363
364
365 def RemoveNode(opts, args):
366   """Remove a node from the cluster.
367
368   @param opts: the command line options selected by the user
369   @type args: list
370   @param args: should contain only one element, the name of
371       the node to be removed
372   @rtype: int
373   @return: the desired exit code
374
375   """
376   op = opcodes.OpRemoveNode(node_name=args[0])
377   SubmitOpCode(op)
378   return 0
379
380
381 def PowercycleNode(opts, args):
382   """Remove a node from the cluster.
383
384   @param opts: the command line options selected by the user
385   @type args: list
386   @param args: should contain only one element, the name of
387       the node to be removed
388   @rtype: int
389   @return: the desired exit code
390
391   """
392   node = args[0]
393   if (not opts.confirm and
394       not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
395     return 2
396
397   op = opcodes.OpPowercycleNode(node_name=node, force=opts.force)
398   result = SubmitOpCode(op)
399   ToStderr(result)
400   return 0
401
402
403 def ListVolumes(opts, args):
404   """List logical volumes on node(s).
405
406   @param opts: the command line options selected by the user
407   @type args: list
408   @param args: should either be an empty list, in which case
409       we list data for all nodes, or contain a list of nodes
410       to display data only for those
411   @rtype: int
412   @return: the desired exit code
413
414   """
415   if opts.output is None:
416     selected_fields = ["node", "phys", "vg",
417                        "name", "size", "instance"]
418   else:
419     selected_fields = opts.output.split(",")
420
421   op = opcodes.OpQueryNodeVolumes(nodes=args, output_fields=selected_fields)
422   output = SubmitOpCode(op)
423
424   if not opts.no_headers:
425     headers = {"node": "Node", "phys": "PhysDev",
426                "vg": "VG", "name": "Name",
427                "size": "Size", "instance": "Instance"}
428   else:
429     headers = None
430
431   unitfields = ["size"]
432
433   numfields = ["size"]
434
435   data = GenerateTable(separator=opts.separator, headers=headers,
436                        fields=selected_fields, unitfields=unitfields,
437                        numfields=numfields, data=output, units=opts.units)
438
439   for line in data:
440     ToStdout(line)
441
442   return 0
443
444
445 def ListPhysicalVolumes(opts, args):
446   """List physical volumes on node(s).
447
448   @param opts: the command line options selected by the user
449   @type args: list
450   @param args: should either be an empty list, in which case
451       we list data for all nodes, or contain a list of nodes
452       to display data only for those
453   @rtype: int
454   @return: the desired exit code
455
456   """
457   # TODO: Default to ST_FILE if LVM is disabled on the cluster
458   if opts.user_storage_type is None:
459     opts.user_storage_type = constants.ST_LVM_PV
460
461   storage_type = ConvertStorageType(opts.user_storage_type)
462
463   default_fields = {
464     constants.ST_FILE: [
465       constants.SF_NAME,
466       constants.SF_USED,
467       constants.SF_FREE,
468       ],
469     constants.ST_LVM_PV: [
470       constants.SF_NAME,
471       constants.SF_SIZE,
472       constants.SF_USED,
473       constants.SF_FREE,
474       ],
475     constants.ST_LVM_VG: [
476       constants.SF_NAME,
477       constants.SF_SIZE,
478       ],
479   }
480
481   if opts.output is None:
482     selected_fields = ["node"]
483     selected_fields.extend(default_fields[storage_type])
484   else:
485     selected_fields = opts.output.split(",")
486
487   op = opcodes.OpQueryNodeStorage(nodes=args,
488                                   storage_type=storage_type,
489                                   output_fields=selected_fields)
490   output = SubmitOpCode(op)
491
492   if not opts.no_headers:
493     headers = {
494       "node": "Node",
495       constants.SF_NAME: "Name",
496       constants.SF_SIZE: "Size",
497       constants.SF_USED: "Used",
498       constants.SF_FREE: "Free",
499       constants.SF_ALLOCATABLE: "Allocatable",
500       }
501   else:
502     headers = None
503
504   unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
505   numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
506
507   data = GenerateTable(separator=opts.separator, headers=headers,
508                        fields=selected_fields, unitfields=unitfields,
509                        numfields=numfields, data=output, units=opts.units)
510
511   for line in data:
512     ToStdout(line)
513
514   return 0
515
516
517 def ModifyVolume(opts, args):
518   """Modify storage volume on a node.
519
520   @param opts: the command line options selected by the user
521   @type args: list
522   @param args: should contain 3 items: node name, storage type and volume name
523   @rtype: int
524   @return: the desired exit code
525
526   """
527   (node_name, user_storage_type, volume_name) = args
528
529   storage_type = ConvertStorageType(user_storage_type)
530
531   changes = {}
532
533   if opts.allocatable is not None:
534     changes[constants.SF_ALLOCATABLE] = (opts.allocatable == "yes")
535
536   if changes:
537     op = opcodes.OpModifyNodeStorage(node_name=node_name,
538                                      storage_type=storage_type,
539                                      name=volume_name,
540                                      changes=changes)
541     SubmitOpCode(op)
542
543
544 def RepairVolume(opts, args):
545   """Repairs a storage volume on a node.
546
547   @param opts: the command line options selected by the user
548   @type args: list
549   @param args: should contain 3 items: node name, storage type and volume name
550   @rtype: int
551   @return: the desired exit code
552
553   """
554   (node_name, user_storage_type, volume_name) = args
555
556   storage_type = ConvertStorageType(user_storage_type)
557
558   op = opcodes.OpRepairNodeStorage(node_name=node_name,
559                                    storage_type=storage_type,
560                                    name=volume_name)
561   SubmitOpCode(op)
562
563
564 def SetNodeParams(opts, args):
565   """Modifies a node.
566
567   @param opts: the command line options selected by the user
568   @type args: list
569   @param args: should contain only one element, the node name
570   @rtype: int
571   @return: the desired exit code
572
573   """
574   if [opts.master_candidate, opts.drained, opts.offline].count(None) == 3:
575     ToStderr("Please give at least one of the parameters.")
576     return 1
577
578   if opts.master_candidate is not None:
579     candidate = opts.master_candidate == 'yes'
580   else:
581     candidate = None
582   if opts.offline is not None:
583     offline = opts.offline == 'yes'
584   else:
585     offline = None
586
587   if opts.drained is not None:
588     drained = opts.drained == 'yes'
589   else:
590     drained = None
591   op = opcodes.OpSetNodeParams(node_name=args[0],
592                                master_candidate=candidate,
593                                offline=offline,
594                                drained=drained,
595                                force=opts.force)
596
597   # even if here we process the result, we allow submit only
598   result = SubmitOrSend(op, opts)
599
600   if result:
601     ToStdout("Modified node %s", args[0])
602     for param, data in result:
603       ToStdout(" - %-5s -> %s", param, data)
604   return 0
605
606
607 commands = {
608   'add': (AddNode, ARGS_ONE,
609           [DEBUG_OPT,
610            make_option("-s", "--secondary-ip", dest="secondary_ip",
611                        help="Specify the secondary ip for the node",
612                        metavar="ADDRESS", default=None),
613            make_option("--readd", dest="readd",
614                        default=False, action="store_true",
615                        help="Readd old node after replacing it"),
616            make_option("--no-ssh-key-check", dest="ssh_key_check",
617                        default=True, action="store_false",
618                        help="Disable SSH key fingerprint checking"),
619            ],
620           "[-s ip] [--readd] [--no-ssh-key-check] <node_name>",
621           "Add a node to the cluster"),
622   'evacuate': (EvacuateNode, ARGS_ONE,
623                [DEBUG_OPT, FORCE_OPT,
624                 make_option("-n", "--new-secondary", dest="dst_node",
625                             help="New secondary node", metavar="NODE",
626                             default=None),
627                 make_option("-I", "--iallocator", metavar="<NAME>",
628                             help="Select new secondary for the instance"
629                             " automatically using the"
630                             " <NAME> iallocator plugin",
631                             default=None, type="string"),
632                 ],
633                "[-f] {-I <iallocator> | -n <dst>} <node>",
634                "Relocate the secondary instances from a node"
635                " to other nodes (only for instances with drbd disk template)"),
636   'failover': (FailoverNode, ARGS_ONE,
637                [DEBUG_OPT, FORCE_OPT,
638                 make_option("--ignore-consistency", dest="ignore_consistency",
639                             action="store_true", default=False,
640                             help="Ignore the consistency of the disks on"
641                             " the secondary"),
642                 ],
643                "[-f] <node>",
644                "Stops the primary instances on a node and start them on their"
645                " secondary node (only for instances with drbd disk template)"),
646   'migrate': (MigrateNode, ARGS_ONE,
647                [DEBUG_OPT, FORCE_OPT,
648                 make_option("--non-live", dest="live",
649                             default=True, action="store_false",
650                             help="Do a non-live migration (this usually means"
651                             " freeze the instance, save the state,"
652                             " transfer and only then resume running on the"
653                             " secondary node)"),
654                 ],
655                "[-f] <node>",
656                "Migrate all the primary instance on a node away from it"
657                " (only for instances of type drbd)"),
658   'info': (ShowNodeConfig, ARGS_ANY, [DEBUG_OPT],
659            "[<node_name>...]", "Show information about the node(s)"),
660   'list': (ListNodes, ARGS_ANY,
661            [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, SYNC_OPT],
662            "[nodes...]",
663            "Lists the nodes in the cluster. The available fields"
664            " are (see the man page for details): %s"
665            " The default field list is (in order): %s." %
666            (", ".join(_LIST_HEADERS), ", ".join(_LIST_DEF_FIELDS))),
667   'modify': (SetNodeParams, ARGS_ONE,
668              [DEBUG_OPT, FORCE_OPT,
669               SUBMIT_OPT,
670               make_option("-C", "--master-candidate", dest="master_candidate",
671                           choices=('yes', 'no'), default=None,
672                           metavar="yes|no",
673                           help="Set the master_candidate flag on the node"),
674               make_option("-O", "--offline", dest="offline", metavar="yes|no",
675                           choices=('yes', 'no'), default=None,
676                           help="Set the offline flag on the node"),
677               make_option("-D", "--drained", dest="drained", metavar="yes|no",
678                           choices=('yes', 'no'), default=None,
679                           help="Set the drained flag on the node"),
680               ],
681              "<instance>", "Alters the parameters of an instance"),
682   'powercycle': (PowercycleNode, ARGS_ONE, [DEBUG_OPT, FORCE_OPT, CONFIRM_OPT],
683                  "<node_name>", "Tries to forcefully powercycle a node"),
684   'remove': (RemoveNode, ARGS_ONE, [DEBUG_OPT],
685              "<node_name>", "Removes a node from the cluster"),
686   'volumes': (ListVolumes, ARGS_ANY,
687               [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
688               "[<node_name>...]", "List logical volumes on node(s)"),
689   'physical-volumes': (ListPhysicalVolumes, ARGS_ANY,
690                        [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT,
691                         FIELDS_OPT,
692                         make_option("--storage-type",
693                                     dest="user_storage_type",
694                                     choices=_USER_STORAGE_TYPE.keys(),
695                                     default=None,
696                                     metavar="STORAGE_TYPE",
697                                     help=("Storage type (%s)" %
698                                           utils.CommaJoin(_USER_STORAGE_TYPE.keys()))),
699                        ],
700                        "[<node_name>...]",
701                        "List physical volumes on node(s)"),
702   'modify-volume': (ModifyVolume, ARGS_FIXED(3),
703                     [DEBUG_OPT,
704                      make_option("--allocatable", dest="allocatable",
705                                  choices=["yes", "no"], default=None,
706                                  metavar="yes|no",
707                                  help="Set the allocatable flag on a volume"),
708                      ],
709                     "<node_name> <storage_type> <name>",
710                     "Modify storage volume on a node"),
711   'repair-volume': (RepairVolume, ARGS_FIXED(3),
712                     [DEBUG_OPT],
713                     "<node_name> <storage_type> <name>",
714                     "Repairs a storage volume on a node"),
715   'list-tags': (ListTags, ARGS_ONE, [DEBUG_OPT],
716                 "<node_name>", "List the tags of the given node"),
717   'add-tags': (AddTags, ARGS_ATLEAST(1), [DEBUG_OPT, TAG_SRC_OPT],
718                "<node_name> tag...", "Add tags to the given node"),
719   'remove-tags': (RemoveTags, ARGS_ATLEAST(1), [DEBUG_OPT, TAG_SRC_OPT],
720                   "<node_name> tag...", "Remove tags from the given node"),
721   }
722
723
724 if __name__ == '__main__':
725   sys.exit(GenericMain(commands, override={"tag_type": constants.TAG_NODE}))