Move gnt-node to ganeti.client.gnt_node
[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   SubmitOpCode(op, opts=opts)
218
219
220 def ListNodes(opts, args):
221   """List nodes and their properties.
222
223   @param opts: the command line options selected by the user
224   @type args: list
225   @param args: should be an empty list
226   @rtype: int
227   @return: the desired exit code
228
229   """
230   selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
231
232   output = GetClient().QueryNodes(args, selected_fields, opts.do_locking)
233
234   if not opts.no_headers:
235     headers = _LIST_HEADERS
236   else:
237     headers = None
238
239   unitfields = ["dtotal", "dfree", "mtotal", "mnode", "mfree"]
240
241   numfields = ["dtotal", "dfree",
242                "mtotal", "mnode", "mfree",
243                "pinst_cnt", "sinst_cnt",
244                "ctotal", "serial_no"]
245
246   list_type_fields = ("pinst_list", "sinst_list", "tags")
247   # change raw values to nicer strings
248   for row in output:
249     for idx, field in enumerate(selected_fields):
250       val = row[idx]
251       if field in list_type_fields:
252         val = ",".join(val)
253       elif field in ('master', 'master_candidate', 'offline', 'drained',
254                      'master_capable', 'vm_capable'):
255         if val:
256           val = 'Y'
257         else:
258           val = 'N'
259       elif field == "ctime" or field == "mtime":
260         val = utils.FormatTime(val)
261       elif val is None:
262         val = "?"
263       elif opts.roman_integers and isinstance(val, int):
264         val = compat.TryToRoman(val)
265       row[idx] = str(val)
266
267   data = GenerateTable(separator=opts.separator, headers=headers,
268                        fields=selected_fields, unitfields=unitfields,
269                        numfields=numfields, data=output, units=opts.units)
270   for line in data:
271     ToStdout(line)
272
273   return 0
274
275
276 def EvacuateNode(opts, args):
277   """Relocate all secondary instance from a node.
278
279   @param opts: the command line options selected by the user
280   @type args: list
281   @param args: should be an empty list
282   @rtype: int
283   @return: the desired exit code
284
285   """
286   cl = GetClient()
287   force = opts.force
288
289   dst_node = opts.dst_node
290   iallocator = opts.iallocator
291
292   op = opcodes.OpNodeEvacuationStrategy(nodes=args,
293                                         iallocator=iallocator,
294                                         remote_node=dst_node)
295
296   result = SubmitOpCode(op, cl=cl, opts=opts)
297   if not result:
298     # no instances to migrate
299     ToStderr("No secondary instances on node(s) %s, exiting.",
300              utils.CommaJoin(args))
301     return constants.EXIT_SUCCESS
302
303   if not force and not AskUser("Relocate instance(s) %s from node(s) %s?" %
304                                (",".join("'%s'" % name[0] for name in result),
305                                utils.CommaJoin(args))):
306     return constants.EXIT_CONFIRMATION
307
308   jex = JobExecutor(cl=cl, opts=opts)
309   for row in result:
310     iname = row[0]
311     node = row[1]
312     ToStdout("Will relocate instance %s to node %s", iname, node)
313     op = opcodes.OpReplaceDisks(instance_name=iname,
314                                 remote_node=node, disks=[],
315                                 mode=constants.REPLACE_DISK_CHG,
316                                 early_release=opts.early_release)
317     jex.QueueJob(iname, op)
318   results = jex.GetResults()
319   bad_cnt = len([row for row in results if not row[0]])
320   if bad_cnt == 0:
321     ToStdout("All %d instance(s) failed over successfully.", len(results))
322     rcode = constants.EXIT_SUCCESS
323   else:
324     ToStdout("There were errors during the failover:\n"
325              "%d error(s) out of %d instance(s).", bad_cnt, len(results))
326     rcode = constants.EXIT_FAILURE
327   return rcode
328
329
330 def FailoverNode(opts, args):
331   """Failover all primary instance on a node.
332
333   @param opts: the command line options selected by the user
334   @type args: list
335   @param args: should be an empty list
336   @rtype: int
337   @return: the desired exit code
338
339   """
340   cl = GetClient()
341   force = opts.force
342   selected_fields = ["name", "pinst_list"]
343
344   # these fields are static data anyway, so it doesn't matter, but
345   # locking=True should be safer
346   result = cl.QueryNodes(names=args, fields=selected_fields,
347                          use_locking=False)
348   node, pinst = result[0]
349
350   if not pinst:
351     ToStderr("No primary instances on node %s, exiting.", node)
352     return 0
353
354   pinst = utils.NiceSort(pinst)
355
356   retcode = 0
357
358   if not force and not AskUser("Fail over instance(s) %s?" %
359                                (",".join("'%s'" % name for name in pinst))):
360     return 2
361
362   jex = JobExecutor(cl=cl, opts=opts)
363   for iname in pinst:
364     op = opcodes.OpFailoverInstance(instance_name=iname,
365                                     ignore_consistency=opts.ignore_consistency)
366     jex.QueueJob(iname, op)
367   results = jex.GetResults()
368   bad_cnt = len([row for row in results if not row[0]])
369   if bad_cnt == 0:
370     ToStdout("All %d instance(s) failed over successfully.", len(results))
371   else:
372     ToStdout("There were errors during the failover:\n"
373              "%d error(s) out of %d instance(s).", bad_cnt, len(results))
374   return retcode
375
376
377 def MigrateNode(opts, args):
378   """Migrate all primary instance on a node.
379
380   """
381   cl = GetClient()
382   force = opts.force
383   selected_fields = ["name", "pinst_list"]
384
385   result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
386   node, pinst = result[0]
387
388   if not pinst:
389     ToStdout("No primary instances on node %s, exiting." % node)
390     return 0
391
392   pinst = utils.NiceSort(pinst)
393
394   if not force and not AskUser("Migrate instance(s) %s?" %
395                                (",".join("'%s'" % name for name in pinst))):
396     return 2
397
398   # this should be removed once --non-live is deprecated
399   if not opts.live and opts.migration_mode is not None:
400     raise errors.OpPrereqError("Only one of the --non-live and "
401                                "--migration-mode options can be passed",
402                                errors.ECODE_INVAL)
403   if not opts.live: # --non-live passed
404     mode = constants.HT_MIGRATION_NONLIVE
405   else:
406     mode = opts.migration_mode
407   op = opcodes.OpMigrateNode(node_name=args[0], mode=mode)
408   SubmitOpCode(op, cl=cl, opts=opts)
409
410
411 def ShowNodeConfig(opts, args):
412   """Show node information.
413
414   @param opts: the command line options selected by the user
415   @type args: list
416   @param args: should either be an empty list, in which case
417       we show information about all nodes, or should contain
418       a list of nodes to be queried for information
419   @rtype: int
420   @return: the desired exit code
421
422   """
423   cl = GetClient()
424   result = cl.QueryNodes(fields=["name", "pip", "sip",
425                                  "pinst_list", "sinst_list",
426                                  "master_candidate", "drained", "offline"],
427                          names=args, use_locking=False)
428
429   for (name, primary_ip, secondary_ip, pinst, sinst,
430        is_mc, drained, offline) in result:
431     ToStdout("Node name: %s", name)
432     ToStdout("  primary ip: %s", primary_ip)
433     ToStdout("  secondary ip: %s", secondary_ip)
434     ToStdout("  master candidate: %s", is_mc)
435     ToStdout("  drained: %s", drained)
436     ToStdout("  offline: %s", offline)
437     if pinst:
438       ToStdout("  primary for instances:")
439       for iname in utils.NiceSort(pinst):
440         ToStdout("    - %s", iname)
441     else:
442       ToStdout("  primary for no instances")
443     if sinst:
444       ToStdout("  secondary for instances:")
445       for iname in utils.NiceSort(sinst):
446         ToStdout("    - %s", iname)
447     else:
448       ToStdout("  secondary for no instances")
449
450   return 0
451
452
453 def RemoveNode(opts, args):
454   """Remove a node from the cluster.
455
456   @param opts: the command line options selected by the user
457   @type args: list
458   @param args: should contain only one element, the name of
459       the node to be removed
460   @rtype: int
461   @return: the desired exit code
462
463   """
464   op = opcodes.OpRemoveNode(node_name=args[0])
465   SubmitOpCode(op, opts=opts)
466   return 0
467
468
469 def PowercycleNode(opts, args):
470   """Remove a node from the cluster.
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   node = args[0]
481   if (not opts.confirm and
482       not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
483     return 2
484
485   op = opcodes.OpPowercycleNode(node_name=node, force=opts.force)
486   result = SubmitOpCode(op, opts=opts)
487   if result:
488     ToStderr(result)
489   return 0
490
491
492 def ListVolumes(opts, args):
493   """List logical volumes on node(s).
494
495   @param opts: the command line options selected by the user
496   @type args: list
497   @param args: should either be an empty list, in which case
498       we list data for all nodes, or contain a list of nodes
499       to display data only for those
500   @rtype: int
501   @return: the desired exit code
502
503   """
504   selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
505
506   op = opcodes.OpQueryNodeVolumes(nodes=args, output_fields=selected_fields)
507   output = SubmitOpCode(op, opts=opts)
508
509   if not opts.no_headers:
510     headers = {"node": "Node", "phys": "PhysDev",
511                "vg": "VG", "name": "Name",
512                "size": "Size", "instance": "Instance"}
513   else:
514     headers = None
515
516   unitfields = ["size"]
517
518   numfields = ["size"]
519
520   data = GenerateTable(separator=opts.separator, headers=headers,
521                        fields=selected_fields, unitfields=unitfields,
522                        numfields=numfields, data=output, units=opts.units)
523
524   for line in data:
525     ToStdout(line)
526
527   return 0
528
529
530 def ListStorage(opts, args):
531   """List physical volumes on node(s).
532
533   @param opts: the command line options selected by the user
534   @type args: list
535   @param args: should either be an empty list, in which case
536       we list data for all nodes, or contain a list of nodes
537       to display data only for those
538   @rtype: int
539   @return: the desired exit code
540
541   """
542   # TODO: Default to ST_FILE if LVM is disabled on the cluster
543   if opts.user_storage_type is None:
544     opts.user_storage_type = constants.ST_LVM_PV
545
546   storage_type = ConvertStorageType(opts.user_storage_type)
547
548   selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
549
550   op = opcodes.OpQueryNodeStorage(nodes=args,
551                                   storage_type=storage_type,
552                                   output_fields=selected_fields)
553   output = SubmitOpCode(op, opts=opts)
554
555   if not opts.no_headers:
556     headers = {
557       constants.SF_NODE: "Node",
558       constants.SF_TYPE: "Type",
559       constants.SF_NAME: "Name",
560       constants.SF_SIZE: "Size",
561       constants.SF_USED: "Used",
562       constants.SF_FREE: "Free",
563       constants.SF_ALLOCATABLE: "Allocatable",
564       }
565   else:
566     headers = None
567
568   unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
569   numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
570
571   # change raw values to nicer strings
572   for row in output:
573     for idx, field in enumerate(selected_fields):
574       val = row[idx]
575       if field == constants.SF_ALLOCATABLE:
576         if val:
577           val = "Y"
578         else:
579           val = "N"
580       row[idx] = str(val)
581
582   data = GenerateTable(separator=opts.separator, headers=headers,
583                        fields=selected_fields, unitfields=unitfields,
584                        numfields=numfields, data=output, units=opts.units)
585
586   for line in data:
587     ToStdout(line)
588
589   return 0
590
591
592 def ModifyStorage(opts, args):
593   """Modify storage volume on a node.
594
595   @param opts: the command line options selected by the user
596   @type args: list
597   @param args: should contain 3 items: node name, storage type and volume name
598   @rtype: int
599   @return: the desired exit code
600
601   """
602   (node_name, user_storage_type, volume_name) = args
603
604   storage_type = ConvertStorageType(user_storage_type)
605
606   changes = {}
607
608   if opts.allocatable is not None:
609     changes[constants.SF_ALLOCATABLE] = opts.allocatable
610
611   if changes:
612     op = opcodes.OpModifyNodeStorage(node_name=node_name,
613                                      storage_type=storage_type,
614                                      name=volume_name,
615                                      changes=changes)
616     SubmitOpCode(op, opts=opts)
617   else:
618     ToStderr("No changes to perform, exiting.")
619
620
621 def RepairStorage(opts, args):
622   """Repairs a storage volume on a node.
623
624   @param opts: the command line options selected by the user
625   @type args: list
626   @param args: should contain 3 items: node name, storage type and volume name
627   @rtype: int
628   @return: the desired exit code
629
630   """
631   (node_name, user_storage_type, volume_name) = args
632
633   storage_type = ConvertStorageType(user_storage_type)
634
635   op = opcodes.OpRepairNodeStorage(node_name=node_name,
636                                    storage_type=storage_type,
637                                    name=volume_name,
638                                    ignore_consistency=opts.ignore_consistency)
639   SubmitOpCode(op, opts=opts)
640
641
642 def SetNodeParams(opts, args):
643   """Modifies a node.
644
645   @param opts: the command line options selected by the user
646   @type args: list
647   @param args: should contain only one element, the node name
648   @rtype: int
649   @return: the desired exit code
650
651   """
652   if [opts.master_candidate, opts.drained, opts.offline].count(None) == 3:
653     ToStderr("Please give at least one of the parameters.")
654     return 1
655
656   op = opcodes.OpSetNodeParams(node_name=args[0],
657                                master_candidate=opts.master_candidate,
658                                offline=opts.offline,
659                                drained=opts.drained,
660                                master_capable=opts.master_capable,
661                                force=opts.force,
662                                auto_promote=opts.auto_promote)
663
664   # even if here we process the result, we allow submit only
665   result = SubmitOrSend(op, opts)
666
667   if result:
668     ToStdout("Modified node %s", args[0])
669     for param, data in result:
670       ToStdout(" - %-5s -> %s", param, data)
671   return 0
672
673
674 commands = {
675   'add': (
676     AddNode, [ArgHost(min=1, max=1)],
677     [SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT, NONODE_SETUP_OPT,
678      VERBOSE_OPT, NODEGROUP_OPT, PRIORITY_OPT],
679     "[-s ip] [--readd] [--no-ssh-key-check] [--no-node-setup]  [--verbose] "
680     " <node_name>",
681     "Add a node to the cluster"),
682   'evacuate': (
683     EvacuateNode, [ArgNode(min=1)],
684     [FORCE_OPT, IALLOCATOR_OPT, NEW_SECONDARY_OPT, EARLY_RELEASE_OPT,
685      PRIORITY_OPT],
686     "[-f] {-I <iallocator> | -n <dst>} <node>",
687     "Relocate the secondary instances from a node"
688     " to other nodes (only for instances with drbd disk template)"),
689   'failover': (
690     FailoverNode, ARGS_ONE_NODE, [FORCE_OPT, IGNORE_CONSIST_OPT, PRIORITY_OPT],
691     "[-f] <node>",
692     "Stops the primary instances on a node and start them on their"
693     " secondary node (only for instances with drbd disk template)"),
694   'migrate': (
695     MigrateNode, ARGS_ONE_NODE,
696     [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT, PRIORITY_OPT],
697     "[-f] <node>",
698     "Migrate all the primary instance on a node away from it"
699     " (only for instances of type drbd)"),
700   'info': (
701     ShowNodeConfig, ARGS_MANY_NODES, [],
702     "[<node_name>...]", "Show information about the node(s)"),
703   'list': (
704     ListNodes, ARGS_MANY_NODES,
705     [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, SYNC_OPT, ROMAN_OPT],
706     "[nodes...]",
707     "Lists the nodes in the cluster. The available fields are (see the man"
708     " page for details): %s. The default field list is (in order): %s." %
709     (utils.CommaJoin(_LIST_HEADERS), utils.CommaJoin(_LIST_DEF_FIELDS))),
710   'modify': (
711     SetNodeParams, ARGS_ONE_NODE,
712     [FORCE_OPT, SUBMIT_OPT, MC_OPT, DRAINED_OPT, OFFLINE_OPT, CAPAB_MASTER_OPT,
713      AUTO_PROMOTE_OPT, DRY_RUN_OPT, PRIORITY_OPT],
714     "<node_name>", "Alters the parameters of a node"),
715   'powercycle': (
716     PowercycleNode, ARGS_ONE_NODE,
717     [FORCE_OPT, CONFIRM_OPT, DRY_RUN_OPT, PRIORITY_OPT],
718     "<node_name>", "Tries to forcefully powercycle a node"),
719   'remove': (
720     RemoveNode, ARGS_ONE_NODE, [DRY_RUN_OPT, PRIORITY_OPT],
721     "<node_name>", "Removes a node from the cluster"),
722   'volumes': (
723     ListVolumes, [ArgNode()],
724     [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, PRIORITY_OPT],
725     "[<node_name>...]", "List logical volumes on node(s)"),
726   'list-storage': (
727     ListStorage, ARGS_MANY_NODES,
728     [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, _STORAGE_TYPE_OPT,
729      PRIORITY_OPT],
730     "[<node_name>...]", "List physical volumes on node(s). The available"
731     " fields are (see the man page for details): %s." %
732     (utils.CommaJoin(_LIST_STOR_HEADERS))),
733   'modify-storage': (
734     ModifyStorage,
735     [ArgNode(min=1, max=1),
736      ArgChoice(min=1, max=1, choices=_MODIFIABLE_STORAGE_TYPES),
737      ArgFile(min=1, max=1)],
738     [ALLOCATABLE_OPT, DRY_RUN_OPT, PRIORITY_OPT],
739     "<node_name> <storage_type> <name>", "Modify storage volume on a node"),
740   'repair-storage': (
741     RepairStorage,
742     [ArgNode(min=1, max=1),
743      ArgChoice(min=1, max=1, choices=_REPAIRABLE_STORAGE_TYPES),
744      ArgFile(min=1, max=1)],
745     [IGNORE_CONSIST_OPT, DRY_RUN_OPT, PRIORITY_OPT],
746     "<node_name> <storage_type> <name>",
747     "Repairs a storage volume on a node"),
748   'list-tags': (
749     ListTags, ARGS_ONE_NODE, [],
750     "<node_name>", "List the tags of the given node"),
751   'add-tags': (
752     AddTags, [ArgNode(min=1, max=1), ArgUnknown()], [TAG_SRC_OPT, PRIORITY_OPT],
753     "<node_name> tag...", "Add tags to the given node"),
754   'remove-tags': (
755     RemoveTags, [ArgNode(min=1, max=1), ArgUnknown()],
756     [TAG_SRC_OPT, PRIORITY_OPT],
757     "<node_name> tag...", "Remove tags from the given node"),
758   }
759
760
761 def Main():
762   return GenericMain(commands, override={"tag_type": constants.TAG_NODE})