Enforce that new node groups have unique names
[ganeti-local] / lib / client / gnt_node.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 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.OpNodeAdd(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.OpNodeEvacStrategy(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.OpInstanceReplaceDisks(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.OpInstanceFailover(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.OpNodeMigrate(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.OpNodeRemove(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.OpNodePowercycle(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.OpNodeSetParams(node_name=node, offline=True,
492                                               auto_promote=opts.auto_promote))
493
494   opcodelist.append(opcodes.OpOobCommand(node_names=[node],
495                                          command=oob_command))
496
497   cli.SetGenericOpcodeOpts(opcodelist, opts)
498
499   job_id = cli.SendJob(opcodelist)
500
501   # We just want the OOB Opcode status
502   # If it fails PollJob gives us the error message in it
503   result = cli.PollJob(job_id)[-1]
504
505   if result:
506     (_, data_tuple) = result[0]
507     if data_tuple[0] != constants.RS_NORMAL:
508       if data_tuple[0] == constants.RS_UNAVAIL:
509         result = "OOB is not supported"
510       else:
511         result = "RPC failed, look out for warning in the output"
512       ToStderr(result)
513       return constants.EXIT_FAILURE
514     else:
515       if oob_command == constants.OOB_POWER_STATUS:
516         text = "The machine is %spowered"
517         if data_tuple[1][constants.OOB_POWER_STATUS_POWERED]:
518           result = text % ""
519         else:
520           result = text % "not "
521         ToStdout(result)
522
523   return constants.EXIT_SUCCESS
524
525
526 def ListVolumes(opts, args):
527   """List logical volumes on node(s).
528
529   @param opts: the command line options selected by the user
530   @type args: list
531   @param args: should either be an empty list, in which case
532       we list data for all nodes, or contain a list of nodes
533       to display data only for those
534   @rtype: int
535   @return: the desired exit code
536
537   """
538   selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
539
540   op = opcodes.OpNodeQueryvols(nodes=args, output_fields=selected_fields)
541   output = SubmitOpCode(op, opts=opts)
542
543   if not opts.no_headers:
544     headers = {"node": "Node", "phys": "PhysDev",
545                "vg": "VG", "name": "Name",
546                "size": "Size", "instance": "Instance"}
547   else:
548     headers = None
549
550   unitfields = ["size"]
551
552   numfields = ["size"]
553
554   data = GenerateTable(separator=opts.separator, headers=headers,
555                        fields=selected_fields, unitfields=unitfields,
556                        numfields=numfields, data=output, units=opts.units)
557
558   for line in data:
559     ToStdout(line)
560
561   return 0
562
563
564 def ListStorage(opts, args):
565   """List physical volumes on node(s).
566
567   @param opts: the command line options selected by the user
568   @type args: list
569   @param args: should either be an empty list, in which case
570       we list data for all nodes, or contain a list of nodes
571       to display data only for those
572   @rtype: int
573   @return: the desired exit code
574
575   """
576   # TODO: Default to ST_FILE if LVM is disabled on the cluster
577   if opts.user_storage_type is None:
578     opts.user_storage_type = constants.ST_LVM_PV
579
580   storage_type = ConvertStorageType(opts.user_storage_type)
581
582   selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
583
584   op = opcodes.OpNodeQueryStorage(nodes=args,
585                                   storage_type=storage_type,
586                                   output_fields=selected_fields)
587   output = SubmitOpCode(op, opts=opts)
588
589   if not opts.no_headers:
590     headers = {
591       constants.SF_NODE: "Node",
592       constants.SF_TYPE: "Type",
593       constants.SF_NAME: "Name",
594       constants.SF_SIZE: "Size",
595       constants.SF_USED: "Used",
596       constants.SF_FREE: "Free",
597       constants.SF_ALLOCATABLE: "Allocatable",
598       }
599   else:
600     headers = None
601
602   unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
603   numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
604
605   # change raw values to nicer strings
606   for row in output:
607     for idx, field in enumerate(selected_fields):
608       val = row[idx]
609       if field == constants.SF_ALLOCATABLE:
610         if val:
611           val = "Y"
612         else:
613           val = "N"
614       row[idx] = str(val)
615
616   data = GenerateTable(separator=opts.separator, headers=headers,
617                        fields=selected_fields, unitfields=unitfields,
618                        numfields=numfields, data=output, units=opts.units)
619
620   for line in data:
621     ToStdout(line)
622
623   return 0
624
625
626 def ModifyStorage(opts, args):
627   """Modify storage volume on a node.
628
629   @param opts: the command line options selected by the user
630   @type args: list
631   @param args: should contain 3 items: node name, storage type and volume name
632   @rtype: int
633   @return: the desired exit code
634
635   """
636   (node_name, user_storage_type, volume_name) = args
637
638   storage_type = ConvertStorageType(user_storage_type)
639
640   changes = {}
641
642   if opts.allocatable is not None:
643     changes[constants.SF_ALLOCATABLE] = opts.allocatable
644
645   if changes:
646     op = opcodes.OpNodeModifyStorage(node_name=node_name,
647                                      storage_type=storage_type,
648                                      name=volume_name,
649                                      changes=changes)
650     SubmitOpCode(op, opts=opts)
651   else:
652     ToStderr("No changes to perform, exiting.")
653
654
655 def RepairStorage(opts, args):
656   """Repairs a storage volume on a node.
657
658   @param opts: the command line options selected by the user
659   @type args: list
660   @param args: should contain 3 items: node name, storage type and volume name
661   @rtype: int
662   @return: the desired exit code
663
664   """
665   (node_name, user_storage_type, volume_name) = args
666
667   storage_type = ConvertStorageType(user_storage_type)
668
669   op = opcodes.OpRepairNodeStorage(node_name=node_name,
670                                    storage_type=storage_type,
671                                    name=volume_name,
672                                    ignore_consistency=opts.ignore_consistency)
673   SubmitOpCode(op, opts=opts)
674
675
676 def SetNodeParams(opts, args):
677   """Modifies a node.
678
679   @param opts: the command line options selected by the user
680   @type args: list
681   @param args: should contain only one element, the node name
682   @rtype: int
683   @return: the desired exit code
684
685   """
686   all_changes = [opts.master_candidate, opts.drained, opts.offline,
687                  opts.master_capable, opts.vm_capable, opts.secondary_ip,
688                  opts.ndparams]
689   if all_changes.count(None) == len(all_changes):
690     ToStderr("Please give at least one of the parameters.")
691     return 1
692
693   op = opcodes.OpNodeSetParams(node_name=args[0],
694                                master_candidate=opts.master_candidate,
695                                offline=opts.offline,
696                                drained=opts.drained,
697                                master_capable=opts.master_capable,
698                                vm_capable=opts.vm_capable,
699                                secondary_ip=opts.secondary_ip,
700                                force=opts.force,
701                                ndparams=opts.ndparams,
702                                auto_promote=opts.auto_promote,
703                                powered=opts.node_powered)
704
705   # even if here we process the result, we allow submit only
706   result = SubmitOrSend(op, opts)
707
708   if result:
709     ToStdout("Modified node %s", args[0])
710     for param, data in result:
711       ToStdout(" - %-5s -> %s", param, data)
712   return 0
713
714
715 commands = {
716   'add': (
717     AddNode, [ArgHost(min=1, max=1)],
718     [SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT, NONODE_SETUP_OPT,
719      VERBOSE_OPT, NODEGROUP_OPT, PRIORITY_OPT, CAPAB_MASTER_OPT,
720      CAPAB_VM_OPT, NODE_PARAMS_OPT],
721     "[-s ip] [--readd] [--no-ssh-key-check] [--no-node-setup]  [--verbose] "
722     " <node_name>",
723     "Add a node to the cluster"),
724   'evacuate': (
725     EvacuateNode, [ArgNode(min=1)],
726     [FORCE_OPT, IALLOCATOR_OPT, NEW_SECONDARY_OPT, EARLY_RELEASE_OPT,
727      PRIORITY_OPT],
728     "[-f] {-I <iallocator> | -n <dst>} <node>",
729     "Relocate the secondary instances from a node"
730     " to other nodes (only for instances with drbd disk template)"),
731   'failover': (
732     FailoverNode, ARGS_ONE_NODE, [FORCE_OPT, IGNORE_CONSIST_OPT, PRIORITY_OPT],
733     "[-f] <node>",
734     "Stops the primary instances on a node and start them on their"
735     " secondary node (only for instances with drbd disk template)"),
736   'migrate': (
737     MigrateNode, ARGS_ONE_NODE,
738     [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT, PRIORITY_OPT],
739     "[-f] <node>",
740     "Migrate all the primary instance on a node away from it"
741     " (only for instances of type drbd)"),
742   'info': (
743     ShowNodeConfig, ARGS_MANY_NODES, [],
744     "[<node_name>...]", "Show information about the node(s)"),
745   'list': (
746     ListNodes, ARGS_MANY_NODES,
747     [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
748     "[nodes...]",
749     "Lists the nodes in the cluster. The available fields can be shown using"
750     " the \"list-fields\" command (see the man page for details)."
751     " The default field list is (in order): %s." %
752     utils.CommaJoin(_LIST_DEF_FIELDS)),
753   "list-fields": (
754     ListNodeFields, [ArgUnknown()],
755     [NOHDR_OPT, SEP_OPT],
756     "[fields...]",
757     "Lists all available fields for nodes"),
758   'modify': (
759     SetNodeParams, ARGS_ONE_NODE,
760     [FORCE_OPT, SUBMIT_OPT, MC_OPT, DRAINED_OPT, OFFLINE_OPT,
761      CAPAB_MASTER_OPT, CAPAB_VM_OPT, SECONDARY_IP_OPT,
762      AUTO_PROMOTE_OPT, DRY_RUN_OPT, PRIORITY_OPT, NODE_PARAMS_OPT,
763      NODE_POWERED_OPT],
764     "<node_name>", "Alters the parameters of a node"),
765   'powercycle': (
766     PowercycleNode, ARGS_ONE_NODE,
767     [FORCE_OPT, CONFIRM_OPT, DRY_RUN_OPT, PRIORITY_OPT],
768     "<node_name>", "Tries to forcefully powercycle a node"),
769   'power': (
770     PowerNode,
771     [ArgChoice(min=1, max=1, choices=_LIST_POWER_COMMANDS),
772      ArgNode(min=1, max=1)],
773     [SUBMIT_OPT, AUTO_PROMOTE_OPT, PRIORITY_OPT],
774     "on|off|cycle|status <node>",
775     "Change power state of node by calling out-of-band helper."),
776   'remove': (
777     RemoveNode, ARGS_ONE_NODE, [DRY_RUN_OPT, PRIORITY_OPT],
778     "<node_name>", "Removes a node from the cluster"),
779   'volumes': (
780     ListVolumes, [ArgNode()],
781     [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, PRIORITY_OPT],
782     "[<node_name>...]", "List logical volumes on node(s)"),
783   'list-storage': (
784     ListStorage, ARGS_MANY_NODES,
785     [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, _STORAGE_TYPE_OPT,
786      PRIORITY_OPT],
787     "[<node_name>...]", "List physical volumes on node(s). The available"
788     " fields are (see the man page for details): %s." %
789     (utils.CommaJoin(_LIST_STOR_HEADERS))),
790   'modify-storage': (
791     ModifyStorage,
792     [ArgNode(min=1, max=1),
793      ArgChoice(min=1, max=1, choices=_MODIFIABLE_STORAGE_TYPES),
794      ArgFile(min=1, max=1)],
795     [ALLOCATABLE_OPT, DRY_RUN_OPT, PRIORITY_OPT],
796     "<node_name> <storage_type> <name>", "Modify storage volume on a node"),
797   'repair-storage': (
798     RepairStorage,
799     [ArgNode(min=1, max=1),
800      ArgChoice(min=1, max=1, choices=_REPAIRABLE_STORAGE_TYPES),
801      ArgFile(min=1, max=1)],
802     [IGNORE_CONSIST_OPT, DRY_RUN_OPT, PRIORITY_OPT],
803     "<node_name> <storage_type> <name>",
804     "Repairs a storage volume on a node"),
805   'list-tags': (
806     ListTags, ARGS_ONE_NODE, [],
807     "<node_name>", "List the tags of the given node"),
808   'add-tags': (
809     AddTags, [ArgNode(min=1, max=1), ArgUnknown()], [TAG_SRC_OPT, PRIORITY_OPT],
810     "<node_name> tag...", "Add tags to the given node"),
811   'remove-tags': (
812     RemoveTags, [ArgNode(min=1, max=1), ArgUnknown()],
813     [TAG_SRC_OPT, PRIORITY_OPT],
814     "<node_name> tag...", "Remove tags from the given node"),
815   }
816
817
818 def Main():
819   return GenericMain(commands, override={"tag_type": constants.TAG_NODE})