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