gnt-node: Add instance policy to migrate
[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                              ignore_ipolicy=opts.ignore_ipolicy)
431
432   result = SubmitOpCode(op, cl=cl, opts=opts)
433
434   # Keep track of submitted jobs
435   jex = JobExecutor(cl=cl, opts=opts)
436
437   for (status, job_id) in result[constants.JOB_IDS_KEY]:
438     jex.AddJobId(None, status, job_id)
439
440   results = jex.GetResults()
441   bad_cnt = len([row for row in results if not row[0]])
442   if bad_cnt == 0:
443     ToStdout("All instances migrated successfully.")
444     rcode = constants.EXIT_SUCCESS
445   else:
446     ToStdout("There were %s errors during the node migration.", bad_cnt)
447     rcode = constants.EXIT_FAILURE
448
449   return rcode
450
451
452 def ShowNodeConfig(opts, args):
453   """Show node information.
454
455   @param opts: the command line options selected by the user
456   @type args: list
457   @param args: should either be an empty list, in which case
458       we show information about all nodes, or should contain
459       a list of nodes to be queried for information
460   @rtype: int
461   @return: the desired exit code
462
463   """
464   cl = GetClient()
465   result = cl.QueryNodes(fields=["name", "pip", "sip",
466                                  "pinst_list", "sinst_list",
467                                  "master_candidate", "drained", "offline",
468                                  "master_capable", "vm_capable", "powered",
469                                  "ndparams", "custom_ndparams"],
470                          names=args, use_locking=False)
471
472   for (name, primary_ip, secondary_ip, pinst, sinst, is_mc, drained, offline,
473        master_capable, vm_capable, powered, ndparams,
474        ndparams_custom) in result:
475     ToStdout("Node name: %s", name)
476     ToStdout("  primary ip: %s", primary_ip)
477     ToStdout("  secondary ip: %s", secondary_ip)
478     ToStdout("  master candidate: %s", is_mc)
479     ToStdout("  drained: %s", drained)
480     ToStdout("  offline: %s", offline)
481     if powered is not None:
482       ToStdout("  powered: %s", powered)
483     ToStdout("  master_capable: %s", master_capable)
484     ToStdout("  vm_capable: %s", vm_capable)
485     if vm_capable:
486       if pinst:
487         ToStdout("  primary for instances:")
488         for iname in utils.NiceSort(pinst):
489           ToStdout("    - %s", iname)
490       else:
491         ToStdout("  primary for no instances")
492       if sinst:
493         ToStdout("  secondary for instances:")
494         for iname in utils.NiceSort(sinst):
495           ToStdout("    - %s", iname)
496       else:
497         ToStdout("  secondary for no instances")
498     ToStdout("  node parameters:")
499     buf = StringIO()
500     FormatParameterDict(buf, ndparams_custom, ndparams, level=2)
501     ToStdout(buf.getvalue().rstrip("\n"))
502
503   return 0
504
505
506 def RemoveNode(opts, args):
507   """Remove a node from the cluster.
508
509   @param opts: the command line options selected by the user
510   @type args: list
511   @param args: should contain only one element, the name of
512       the node to be removed
513   @rtype: int
514   @return: the desired exit code
515
516   """
517   op = opcodes.OpNodeRemove(node_name=args[0])
518   SubmitOpCode(op, opts=opts)
519   return 0
520
521
522 def PowercycleNode(opts, args):
523   """Remove a node from the cluster.
524
525   @param opts: the command line options selected by the user
526   @type args: list
527   @param args: should contain only one element, the name of
528       the node to be removed
529   @rtype: int
530   @return: the desired exit code
531
532   """
533   node = args[0]
534   if (not opts.confirm and
535       not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
536     return 2
537
538   op = opcodes.OpNodePowercycle(node_name=node, force=opts.force)
539   result = SubmitOpCode(op, opts=opts)
540   if result:
541     ToStderr(result)
542   return 0
543
544
545 def PowerNode(opts, args):
546   """Change/ask power state of a node.
547
548   @param opts: the command line options selected by the user
549   @type args: list
550   @param args: should contain only one element, the name of
551       the node to be removed
552   @rtype: int
553   @return: the desired exit code
554
555   """
556   command = args.pop(0)
557
558   if opts.no_headers:
559     headers = None
560   else:
561     headers = {"node": "Node", "status": "Status"}
562
563   if command not in _LIST_POWER_COMMANDS:
564     ToStderr("power subcommand %s not supported." % command)
565     return constants.EXIT_FAILURE
566
567   oob_command = "power-%s" % command
568
569   if oob_command in _OOB_COMMAND_ASK:
570     if not args:
571       ToStderr("Please provide at least one node for this command")
572       return constants.EXIT_FAILURE
573     elif not opts.force and not ConfirmOperation(args, "nodes",
574                                                  "power %s" % command):
575       return constants.EXIT_FAILURE
576     assert len(args) > 0
577
578   opcodelist = []
579   if not opts.ignore_status and oob_command == constants.OOB_POWER_OFF:
580     # TODO: This is a little ugly as we can't catch and revert
581     for node in args:
582       opcodelist.append(opcodes.OpNodeSetParams(node_name=node, offline=True,
583                                                 auto_promote=opts.auto_promote))
584
585   opcodelist.append(opcodes.OpOobCommand(node_names=args,
586                                          command=oob_command,
587                                          ignore_status=opts.ignore_status,
588                                          timeout=opts.oob_timeout,
589                                          power_delay=opts.power_delay))
590
591   cli.SetGenericOpcodeOpts(opcodelist, opts)
592
593   job_id = cli.SendJob(opcodelist)
594
595   # We just want the OOB Opcode status
596   # If it fails PollJob gives us the error message in it
597   result = cli.PollJob(job_id)[-1]
598
599   errs = 0
600   data = []
601   for node_result in result:
602     (node_tuple, data_tuple) = node_result
603     (_, node_name) = node_tuple
604     (data_status, data_node) = data_tuple
605     if data_status == constants.RS_NORMAL:
606       if oob_command == constants.OOB_POWER_STATUS:
607         if data_node[constants.OOB_POWER_STATUS_POWERED]:
608           text = "powered"
609         else:
610           text = "unpowered"
611         data.append([node_name, text])
612       else:
613         # We don't expect data here, so we just say, it was successfully invoked
614         data.append([node_name, "invoked"])
615     else:
616       errs += 1
617       data.append([node_name, cli.FormatResultError(data_status, True)])
618
619   data = GenerateTable(separator=opts.separator, headers=headers,
620                        fields=["node", "status"], data=data)
621
622   for line in data:
623     ToStdout(line)
624
625   if errs:
626     return constants.EXIT_FAILURE
627   else:
628     return constants.EXIT_SUCCESS
629
630
631 def Health(opts, args):
632   """Show health of a node using OOB.
633
634   @param opts: the command line options selected by the user
635   @type args: list
636   @param args: should contain only one element, the name of
637       the node to be removed
638   @rtype: int
639   @return: the desired exit code
640
641   """
642   op = opcodes.OpOobCommand(node_names=args, command=constants.OOB_HEALTH,
643                             timeout=opts.oob_timeout)
644   result = SubmitOpCode(op, opts=opts)
645
646   if opts.no_headers:
647     headers = None
648   else:
649     headers = {"node": "Node", "status": "Status"}
650
651   errs = 0
652   data = []
653   for node_result in result:
654     (node_tuple, data_tuple) = node_result
655     (_, node_name) = node_tuple
656     (data_status, data_node) = data_tuple
657     if data_status == constants.RS_NORMAL:
658       data.append([node_name, "%s=%s" % tuple(data_node[0])])
659       for item, status in data_node[1:]:
660         data.append(["", "%s=%s" % (item, status)])
661     else:
662       errs += 1
663       data.append([node_name, cli.FormatResultError(data_status, True)])
664
665   data = GenerateTable(separator=opts.separator, headers=headers,
666                        fields=["node", "status"], data=data)
667
668   for line in data:
669     ToStdout(line)
670
671   if errs:
672     return constants.EXIT_FAILURE
673   else:
674     return constants.EXIT_SUCCESS
675
676
677 def ListVolumes(opts, args):
678   """List logical volumes on node(s).
679
680   @param opts: the command line options selected by the user
681   @type args: list
682   @param args: should either be an empty list, in which case
683       we list data for all nodes, or contain a list of nodes
684       to display data only for those
685   @rtype: int
686   @return: the desired exit code
687
688   """
689   selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
690
691   op = opcodes.OpNodeQueryvols(nodes=args, output_fields=selected_fields)
692   output = SubmitOpCode(op, opts=opts)
693
694   if not opts.no_headers:
695     headers = {"node": "Node", "phys": "PhysDev",
696                "vg": "VG", "name": "Name",
697                "size": "Size", "instance": "Instance"}
698   else:
699     headers = None
700
701   unitfields = ["size"]
702
703   numfields = ["size"]
704
705   data = GenerateTable(separator=opts.separator, headers=headers,
706                        fields=selected_fields, unitfields=unitfields,
707                        numfields=numfields, data=output, units=opts.units)
708
709   for line in data:
710     ToStdout(line)
711
712   return 0
713
714
715 def ListStorage(opts, args):
716   """List physical volumes on node(s).
717
718   @param opts: the command line options selected by the user
719   @type args: list
720   @param args: should either be an empty list, in which case
721       we list data for all nodes, or contain a list of nodes
722       to display data only for those
723   @rtype: int
724   @return: the desired exit code
725
726   """
727   # TODO: Default to ST_FILE if LVM is disabled on the cluster
728   if opts.user_storage_type is None:
729     opts.user_storage_type = constants.ST_LVM_PV
730
731   storage_type = ConvertStorageType(opts.user_storage_type)
732
733   selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
734
735   op = opcodes.OpNodeQueryStorage(nodes=args,
736                                   storage_type=storage_type,
737                                   output_fields=selected_fields)
738   output = SubmitOpCode(op, opts=opts)
739
740   if not opts.no_headers:
741     headers = {
742       constants.SF_NODE: "Node",
743       constants.SF_TYPE: "Type",
744       constants.SF_NAME: "Name",
745       constants.SF_SIZE: "Size",
746       constants.SF_USED: "Used",
747       constants.SF_FREE: "Free",
748       constants.SF_ALLOCATABLE: "Allocatable",
749       }
750   else:
751     headers = None
752
753   unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
754   numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
755
756   # change raw values to nicer strings
757   for row in output:
758     for idx, field in enumerate(selected_fields):
759       val = row[idx]
760       if field == constants.SF_ALLOCATABLE:
761         if val:
762           val = "Y"
763         else:
764           val = "N"
765       row[idx] = str(val)
766
767   data = GenerateTable(separator=opts.separator, headers=headers,
768                        fields=selected_fields, unitfields=unitfields,
769                        numfields=numfields, data=output, units=opts.units)
770
771   for line in data:
772     ToStdout(line)
773
774   return 0
775
776
777 def ModifyStorage(opts, args):
778   """Modify storage volume on a node.
779
780   @param opts: the command line options selected by the user
781   @type args: list
782   @param args: should contain 3 items: node name, storage type and volume name
783   @rtype: int
784   @return: the desired exit code
785
786   """
787   (node_name, user_storage_type, volume_name) = args
788
789   storage_type = ConvertStorageType(user_storage_type)
790
791   changes = {}
792
793   if opts.allocatable is not None:
794     changes[constants.SF_ALLOCATABLE] = opts.allocatable
795
796   if changes:
797     op = opcodes.OpNodeModifyStorage(node_name=node_name,
798                                      storage_type=storage_type,
799                                      name=volume_name,
800                                      changes=changes)
801     SubmitOpCode(op, opts=opts)
802   else:
803     ToStderr("No changes to perform, exiting.")
804
805
806 def RepairStorage(opts, args):
807   """Repairs a storage volume on a node.
808
809   @param opts: the command line options selected by the user
810   @type args: list
811   @param args: should contain 3 items: node name, storage type and volume name
812   @rtype: int
813   @return: the desired exit code
814
815   """
816   (node_name, user_storage_type, volume_name) = args
817
818   storage_type = ConvertStorageType(user_storage_type)
819
820   op = opcodes.OpRepairNodeStorage(node_name=node_name,
821                                    storage_type=storage_type,
822                                    name=volume_name,
823                                    ignore_consistency=opts.ignore_consistency)
824   SubmitOpCode(op, opts=opts)
825
826
827 def SetNodeParams(opts, args):
828   """Modifies a node.
829
830   @param opts: the command line options selected by the user
831   @type args: list
832   @param args: should contain only one element, the node name
833   @rtype: int
834   @return: the desired exit code
835
836   """
837   all_changes = [opts.master_candidate, opts.drained, opts.offline,
838                  opts.master_capable, opts.vm_capable, opts.secondary_ip,
839                  opts.ndparams]
840   if (all_changes.count(None) == len(all_changes) and
841       not (opts.hv_state or opts.disk_state)):
842     ToStderr("Please give at least one of the parameters.")
843     return 1
844
845   if opts.disk_state:
846     disk_state = utils.FlatToDict(opts.disk_state)
847   else:
848     disk_state = {}
849
850   hv_state = dict(opts.hv_state)
851
852   op = opcodes.OpNodeSetParams(node_name=args[0],
853                                master_candidate=opts.master_candidate,
854                                offline=opts.offline,
855                                drained=opts.drained,
856                                master_capable=opts.master_capable,
857                                vm_capable=opts.vm_capable,
858                                secondary_ip=opts.secondary_ip,
859                                force=opts.force,
860                                ndparams=opts.ndparams,
861                                auto_promote=opts.auto_promote,
862                                powered=opts.node_powered,
863                                hv_state=hv_state,
864                                disk_state=disk_state)
865
866   # even if here we process the result, we allow submit only
867   result = SubmitOrSend(op, opts)
868
869   if result:
870     ToStdout("Modified node %s", args[0])
871     for param, data in result:
872       ToStdout(" - %-5s -> %s", param, data)
873   return 0
874
875
876 commands = {
877   "add": (
878     AddNode, [ArgHost(min=1, max=1)],
879     [SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT, NODE_FORCE_JOIN_OPT,
880      NONODE_SETUP_OPT, VERBOSE_OPT, NODEGROUP_OPT, PRIORITY_OPT,
881      CAPAB_MASTER_OPT, CAPAB_VM_OPT, NODE_PARAMS_OPT, HV_STATE_OPT,
882      DISK_STATE_OPT],
883     "[-s ip] [--readd] [--no-ssh-key-check] [--force-join]"
884     " [--no-node-setup] [--verbose]"
885     " <node_name>",
886     "Add a node to the cluster"),
887   "evacuate": (
888     EvacuateNode, ARGS_ONE_NODE,
889     [FORCE_OPT, IALLOCATOR_OPT, NEW_SECONDARY_OPT, EARLY_RELEASE_OPT,
890      PRIORITY_OPT, PRIMARY_ONLY_OPT, SECONDARY_ONLY_OPT],
891     "[-f] {-I <iallocator> | -n <dst>} <node>",
892     "Relocate the secondary instances from a node"
893     " to other nodes"),
894   "failover": (
895     FailoverNode, ARGS_ONE_NODE, [FORCE_OPT, IGNORE_CONSIST_OPT,
896                                   IALLOCATOR_OPT, PRIORITY_OPT],
897     "[-f] <node>",
898     "Stops the primary instances on a node and start them on their"
899     " secondary node (only for instances with drbd disk template)"),
900   "migrate": (
901     MigrateNode, ARGS_ONE_NODE,
902     [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT, DST_NODE_OPT,
903      IALLOCATOR_OPT, PRIORITY_OPT, IGNORE_IPOLICY_OPT],
904     "[-f] <node>",
905     "Migrate all the primary instance on a node away from it"
906     " (only for instances of type drbd)"),
907   "info": (
908     ShowNodeConfig, ARGS_MANY_NODES, [],
909     "[<node_name>...]", "Show information about the node(s)"),
910   "list": (
911     ListNodes, ARGS_MANY_NODES,
912     [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, VERBOSE_OPT,
913      FORCE_FILTER_OPT],
914     "[nodes...]",
915     "Lists the nodes in the cluster. The available fields can be shown using"
916     " the \"list-fields\" command (see the man page for details)."
917     " The default field list is (in order): %s." %
918     utils.CommaJoin(_LIST_DEF_FIELDS)),
919   "list-fields": (
920     ListNodeFields, [ArgUnknown()],
921     [NOHDR_OPT, SEP_OPT],
922     "[fields...]",
923     "Lists all available fields for nodes"),
924   "modify": (
925     SetNodeParams, ARGS_ONE_NODE,
926     [FORCE_OPT, SUBMIT_OPT, MC_OPT, DRAINED_OPT, OFFLINE_OPT,
927      CAPAB_MASTER_OPT, CAPAB_VM_OPT, SECONDARY_IP_OPT,
928      AUTO_PROMOTE_OPT, DRY_RUN_OPT, PRIORITY_OPT, NODE_PARAMS_OPT,
929      NODE_POWERED_OPT, HV_STATE_OPT, DISK_STATE_OPT],
930     "<node_name>", "Alters the parameters of a node"),
931   "powercycle": (
932     PowercycleNode, ARGS_ONE_NODE,
933     [FORCE_OPT, CONFIRM_OPT, DRY_RUN_OPT, PRIORITY_OPT],
934     "<node_name>", "Tries to forcefully powercycle a node"),
935   "power": (
936     PowerNode,
937     [ArgChoice(min=1, max=1, choices=_LIST_POWER_COMMANDS),
938      ArgNode()],
939     [SUBMIT_OPT, AUTO_PROMOTE_OPT, PRIORITY_OPT, IGNORE_STATUS_OPT,
940      FORCE_OPT, NOHDR_OPT, SEP_OPT, OOB_TIMEOUT_OPT, POWER_DELAY_OPT],
941     "on|off|cycle|status [nodes...]",
942     "Change power state of node by calling out-of-band helper."),
943   "remove": (
944     RemoveNode, ARGS_ONE_NODE, [DRY_RUN_OPT, PRIORITY_OPT],
945     "<node_name>", "Removes a node from the cluster"),
946   "volumes": (
947     ListVolumes, [ArgNode()],
948     [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, PRIORITY_OPT],
949     "[<node_name>...]", "List logical volumes on node(s)"),
950   "list-storage": (
951     ListStorage, ARGS_MANY_NODES,
952     [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, _STORAGE_TYPE_OPT,
953      PRIORITY_OPT],
954     "[<node_name>...]", "List physical volumes on node(s). The available"
955     " fields are (see the man page for details): %s." %
956     (utils.CommaJoin(_LIST_STOR_HEADERS))),
957   "modify-storage": (
958     ModifyStorage,
959     [ArgNode(min=1, max=1),
960      ArgChoice(min=1, max=1, choices=_MODIFIABLE_STORAGE_TYPES),
961      ArgFile(min=1, max=1)],
962     [ALLOCATABLE_OPT, DRY_RUN_OPT, PRIORITY_OPT],
963     "<node_name> <storage_type> <name>", "Modify storage volume on a node"),
964   "repair-storage": (
965     RepairStorage,
966     [ArgNode(min=1, max=1),
967      ArgChoice(min=1, max=1, choices=_REPAIRABLE_STORAGE_TYPES),
968      ArgFile(min=1, max=1)],
969     [IGNORE_CONSIST_OPT, DRY_RUN_OPT, PRIORITY_OPT],
970     "<node_name> <storage_type> <name>",
971     "Repairs a storage volume on a node"),
972   "list-tags": (
973     ListTags, ARGS_ONE_NODE, [],
974     "<node_name>", "List the tags of the given node"),
975   "add-tags": (
976     AddTags, [ArgNode(min=1, max=1), ArgUnknown()], [TAG_SRC_OPT, PRIORITY_OPT],
977     "<node_name> tag...", "Add tags to the given node"),
978   "remove-tags": (
979     RemoveTags, [ArgNode(min=1, max=1), ArgUnknown()],
980     [TAG_SRC_OPT, PRIORITY_OPT],
981     "<node_name> tag...", "Remove tags from the given node"),
982   "health": (
983     Health, ARGS_MANY_NODES,
984     [NOHDR_OPT, SEP_OPT, SUBMIT_OPT, PRIORITY_OPT, OOB_TIMEOUT_OPT],
985     "[<node_name>...]", "List health of node(s) using out-of-band"),
986   }
987
988
989 def Main():
990   return GenericMain(commands, override={"tag_type": constants.TAG_NODE},
991                      env_override=_ENV_OVERRIDE)