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