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