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