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