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