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