4 # Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 Google Inc.
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.
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.
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
21 """Node related commands"""
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
29 from ganeti.cli import *
30 from ganeti import cli
31 from ganeti import bootstrap
32 from ganeti import opcodes
33 from ganeti import utils
34 from ganeti import constants
35 from ganeti import errors
36 from ganeti import netutils
37 from cStringIO import StringIO
40 #: default list of field for L{ListNodes}
42 "name", "dtotal", "dfree",
43 "mtotal", "mnode", "mfree",
44 "pinst_cnt", "sinst_cnt",
48 #: Default field list for L{ListVolumes}
49 _LIST_VOL_DEF_FIELDS = ["node", "phys", "vg", "name", "size", "instance"]
52 #: default list of field for L{ListStorage}
53 _LIST_STOR_DEF_FIELDS = [
60 constants.SF_ALLOCATABLE,
64 #: default list of power commands
65 _LIST_POWER_COMMANDS = ["on", "off", "cycle", "status"]
68 #: headers (and full field list) for L{ListStorage}
69 _LIST_STOR_HEADERS = {
70 constants.SF_NODE: "Node",
71 constants.SF_TYPE: "Type",
72 constants.SF_NAME: "Name",
73 constants.SF_SIZE: "Size",
74 constants.SF_USED: "Used",
75 constants.SF_FREE: "Free",
76 constants.SF_ALLOCATABLE: "Allocatable",
80 #: User-facing storage unit types
81 _USER_STORAGE_TYPE = {
82 constants.ST_FILE: "file",
83 constants.ST_LVM_PV: "lvm-pv",
84 constants.ST_LVM_VG: "lvm-vg",
88 cli_option("-t", "--storage-type",
89 dest="user_storage_type",
90 choices=_USER_STORAGE_TYPE.keys(),
92 metavar="STORAGE_TYPE",
93 help=("Storage type (%s)" %
94 utils.CommaJoin(_USER_STORAGE_TYPE.keys())))
96 _REPAIRABLE_STORAGE_TYPES = \
97 [st for st, so in constants.VALID_STORAGE_OPERATIONS.iteritems()
98 if constants.SO_FIX_CONSISTENCY in so]
100 _MODIFIABLE_STORAGE_TYPES = constants.MODIFIABLE_STORAGE_FIELDS.keys()
103 NONODE_SETUP_OPT = cli_option("--no-node-setup", default=True,
104 action="store_false", dest="node_setup",
105 help=("Do not make initial SSH setup on remote"
106 " node (needs to be done manually)"))
109 def ConvertStorageType(user_storage_type):
110 """Converts a user storage type to its internal name.
114 return _USER_STORAGE_TYPE[user_storage_type]
116 raise errors.OpPrereqError("Unknown storage type: %s" % user_storage_type,
120 def _RunSetupSSH(options, nodes):
121 """Wrapper around utils.RunCmd to call setup-ssh
123 @param options: The command line options
124 @param nodes: The nodes to setup
127 cmd = [constants.SETUP_SSH]
129 # Pass --debug|--verbose to the external script if set on our invocation
130 # --debug overrides --verbose
132 cmd.append("--debug")
133 elif options.verbose:
134 cmd.append("--verbose")
135 if not options.ssh_key_check:
136 cmd.append("--no-ssh-key-check")
140 result = utils.RunCmd(cmd, interactive=True)
143 errmsg = ("Command '%s' failed with exit code %s; output %r" %
144 (result.cmd, result.exit_code, result.output))
145 raise errors.OpExecError(errmsg)
149 def AddNode(opts, args):
150 """Add a node to the cluster.
152 @param opts: the command line options selected by the user
154 @param args: should contain only one element, the new node name
156 @return: the desired exit code
160 node = netutils.GetHostname(name=args[0]).name
164 output = cl.QueryNodes(names=[node], fields=['name', 'sip'],
166 node_exists, sip = output[0]
167 except (errors.OpPrereqError, errors.OpExecError):
173 ToStderr("Node %s not in the cluster"
174 " - please retry without '--readd'", node)
178 ToStderr("Node %s already in the cluster (as %s)"
179 " - please retry with '--readd'", node, node_exists)
181 sip = opts.secondary_ip
183 # read the cluster name from the master
184 output = cl.QueryConfigValues(['cluster_name'])
185 cluster_name = output[0]
187 if not readd and opts.node_setup:
188 ToStderr("-- WARNING -- \n"
189 "Performing this operation is going to replace the ssh daemon"
191 "on the target machine (%s) with the ones of the"
193 "and grant full intra-cluster ssh root access to/from it\n", node)
196 _RunSetupSSH(opts, [node])
198 bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
200 op = opcodes.OpNodeAdd(node_name=args[0], secondary_ip=sip,
201 readd=opts.readd, group=opts.nodegroup,
202 vm_capable=opts.vm_capable, ndparams=opts.ndparams,
203 master_capable=opts.master_capable)
204 SubmitOpCode(op, opts=opts)
207 def ListNodes(opts, args):
208 """List nodes and their properties.
210 @param opts: the command line options selected by the user
212 @param args: nodes to list, or empty for all
214 @return: the desired exit code
217 selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
219 fmtoverride = dict.fromkeys(["pinst_list", "sinst_list", "tags"],
222 return GenericList(constants.QR_NODE, selected_fields, args, opts.units,
223 opts.separator, not opts.no_headers,
224 format_override=fmtoverride)
227 def ListNodeFields(opts, args):
230 @param opts: the command line options selected by the user
232 @param args: fields to list, or empty for all
234 @return: the desired exit code
237 return GenericListFields(constants.QR_NODE, args, opts.separator,
241 def EvacuateNode(opts, args):
242 """Relocate all secondary instance from a node.
244 @param opts: the command line options selected by the user
246 @param args: should be an empty list
248 @return: the desired exit code
254 dst_node = opts.dst_node
255 iallocator = opts.iallocator
257 op = opcodes.OpNodeEvacStrategy(nodes=args,
258 iallocator=iallocator,
259 remote_node=dst_node)
261 result = SubmitOpCode(op, cl=cl, opts=opts)
263 # no instances to migrate
264 ToStderr("No secondary instances on node(s) %s, exiting.",
265 utils.CommaJoin(args))
266 return constants.EXIT_SUCCESS
268 if not force and not AskUser("Relocate instance(s) %s from node(s) %s?" %
269 (",".join("'%s'" % name[0] for name in result),
270 utils.CommaJoin(args))):
271 return constants.EXIT_CONFIRMATION
273 jex = JobExecutor(cl=cl, opts=opts)
277 ToStdout("Will relocate instance %s to node %s", iname, node)
278 op = opcodes.OpInstanceReplaceDisks(instance_name=iname,
279 remote_node=node, disks=[],
280 mode=constants.REPLACE_DISK_CHG,
281 early_release=opts.early_release)
282 jex.QueueJob(iname, op)
283 results = jex.GetResults()
284 bad_cnt = len([row for row in results if not row[0]])
286 ToStdout("All %d instance(s) failed over successfully.", len(results))
287 rcode = constants.EXIT_SUCCESS
289 ToStdout("There were errors during the failover:\n"
290 "%d error(s) out of %d instance(s).", bad_cnt, len(results))
291 rcode = constants.EXIT_FAILURE
295 def FailoverNode(opts, args):
296 """Failover all primary instance on a node.
298 @param opts: the command line options selected by the user
300 @param args: should be an empty list
302 @return: the desired exit code
307 selected_fields = ["name", "pinst_list"]
309 # these fields are static data anyway, so it doesn't matter, but
310 # locking=True should be safer
311 result = cl.QueryNodes(names=args, fields=selected_fields,
313 node, pinst = result[0]
316 ToStderr("No primary instances on node %s, exiting.", node)
319 pinst = utils.NiceSort(pinst)
323 if not force and not AskUser("Fail over instance(s) %s?" %
324 (",".join("'%s'" % name for name in pinst))):
327 jex = JobExecutor(cl=cl, opts=opts)
329 op = opcodes.OpInstanceFailover(instance_name=iname,
330 ignore_consistency=opts.ignore_consistency)
331 jex.QueueJob(iname, op)
332 results = jex.GetResults()
333 bad_cnt = len([row for row in results if not row[0]])
335 ToStdout("All %d instance(s) failed over successfully.", len(results))
337 ToStdout("There were errors during the failover:\n"
338 "%d error(s) out of %d instance(s).", bad_cnt, len(results))
342 def MigrateNode(opts, args):
343 """Migrate all primary instance on a node.
348 selected_fields = ["name", "pinst_list"]
350 result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
351 node, pinst = result[0]
354 ToStdout("No primary instances on node %s, exiting." % node)
357 pinst = utils.NiceSort(pinst)
359 if not force and not AskUser("Migrate instance(s) %s?" %
360 (",".join("'%s'" % name for name in pinst))):
363 # this should be removed once --non-live is deprecated
364 if not opts.live and opts.migration_mode is not None:
365 raise errors.OpPrereqError("Only one of the --non-live and "
366 "--migration-mode options can be passed",
368 if not opts.live: # --non-live passed
369 mode = constants.HT_MIGRATION_NONLIVE
371 mode = opts.migration_mode
372 op = opcodes.OpNodeMigrate(node_name=args[0], mode=mode)
373 SubmitOpCode(op, cl=cl, opts=opts)
376 def ShowNodeConfig(opts, args):
377 """Show node information.
379 @param opts: the command line options selected by the user
381 @param args: should either be an empty list, in which case
382 we show information about all nodes, or should contain
383 a list of nodes to be queried for information
385 @return: the desired exit code
389 result = cl.QueryNodes(fields=["name", "pip", "sip",
390 "pinst_list", "sinst_list",
391 "master_candidate", "drained", "offline",
392 "master_capable", "vm_capable", "powered",
393 "ndparams", "custom_ndparams"],
394 names=args, use_locking=False)
396 for (name, primary_ip, secondary_ip, pinst, sinst, is_mc, drained, offline,
397 master_capable, vm_capable, powered, ndparams,
398 ndparams_custom) in result:
399 ToStdout("Node name: %s", name)
400 ToStdout(" primary ip: %s", primary_ip)
401 ToStdout(" secondary ip: %s", secondary_ip)
402 ToStdout(" master candidate: %s", is_mc)
403 ToStdout(" drained: %s", drained)
404 ToStdout(" offline: %s", offline)
405 if powered is not None:
406 ToStdout(" powered: %s", powered)
407 ToStdout(" master_capable: %s", master_capable)
408 ToStdout(" vm_capable: %s", vm_capable)
411 ToStdout(" primary for instances:")
412 for iname in utils.NiceSort(pinst):
413 ToStdout(" - %s", iname)
415 ToStdout(" primary for no instances")
417 ToStdout(" secondary for instances:")
418 for iname in utils.NiceSort(sinst):
419 ToStdout(" - %s", iname)
421 ToStdout(" secondary for no instances")
422 ToStdout(" node parameters:")
424 FormatParameterDict(buf, ndparams_custom, ndparams, level=2)
425 ToStdout(buf.getvalue().rstrip("\n"))
430 def RemoveNode(opts, args):
431 """Remove a node from the cluster.
433 @param opts: the command line options selected by the user
435 @param args: should contain only one element, the name of
436 the node to be removed
438 @return: the desired exit code
441 op = opcodes.OpNodeRemove(node_name=args[0])
442 SubmitOpCode(op, opts=opts)
446 def PowercycleNode(opts, args):
447 """Remove a node from the cluster.
449 @param opts: the command line options selected by the user
451 @param args: should contain only one element, the name of
452 the node to be removed
454 @return: the desired exit code
458 if (not opts.confirm and
459 not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
462 op = opcodes.OpNodePowercycle(node_name=node, force=opts.force)
463 result = SubmitOpCode(op, opts=opts)
469 def PowerNode(opts, args):
470 """Change/ask power state of a node.
472 @param opts: the command line options selected by the user
474 @param args: should contain only one element, the name of
475 the node to be removed
477 @return: the desired exit code
483 if command not in _LIST_POWER_COMMANDS:
484 ToStderr("power subcommand %s not supported." % command)
485 return constants.EXIT_FAILURE
487 oob_command = "power-%s" % command
490 if oob_command == constants.OOB_POWER_OFF:
491 opcodelist.append(opcodes.OpNodeSetParams(node_name=node, offline=True,
492 auto_promote=opts.auto_promote))
494 opcodelist.append(opcodes.OpOobCommand(node_names=[node],
495 command=oob_command))
497 cli.SetGenericOpcodeOpts(opcodelist, opts)
499 job_id = cli.SendJob(opcodelist)
501 # We just want the OOB Opcode status
502 # If it fails PollJob gives us the error message in it
503 result = cli.PollJob(job_id)[-1]
506 (_, data_tuple) = result[0]
507 if data_tuple[0] != constants.RS_NORMAL:
508 if data_tuple[0] == constants.RS_UNAVAIL:
509 result = "OOB is not supported"
511 result = "RPC failed, look out for warning in the output"
513 return constants.EXIT_FAILURE
515 if oob_command == constants.OOB_POWER_STATUS:
516 text = "The machine is %spowered"
517 if data_tuple[1][constants.OOB_POWER_STATUS_POWERED]:
520 result = text % "not "
523 return constants.EXIT_SUCCESS
526 def ListVolumes(opts, args):
527 """List logical volumes on node(s).
529 @param opts: the command line options selected by the user
531 @param args: should either be an empty list, in which case
532 we list data for all nodes, or contain a list of nodes
533 to display data only for those
535 @return: the desired exit code
538 selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
540 op = opcodes.OpNodeQueryvols(nodes=args, output_fields=selected_fields)
541 output = SubmitOpCode(op, opts=opts)
543 if not opts.no_headers:
544 headers = {"node": "Node", "phys": "PhysDev",
545 "vg": "VG", "name": "Name",
546 "size": "Size", "instance": "Instance"}
550 unitfields = ["size"]
554 data = GenerateTable(separator=opts.separator, headers=headers,
555 fields=selected_fields, unitfields=unitfields,
556 numfields=numfields, data=output, units=opts.units)
564 def ListStorage(opts, args):
565 """List physical volumes on node(s).
567 @param opts: the command line options selected by the user
569 @param args: should either be an empty list, in which case
570 we list data for all nodes, or contain a list of nodes
571 to display data only for those
573 @return: the desired exit code
576 # TODO: Default to ST_FILE if LVM is disabled on the cluster
577 if opts.user_storage_type is None:
578 opts.user_storage_type = constants.ST_LVM_PV
580 storage_type = ConvertStorageType(opts.user_storage_type)
582 selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
584 op = opcodes.OpNodeQueryStorage(nodes=args,
585 storage_type=storage_type,
586 output_fields=selected_fields)
587 output = SubmitOpCode(op, opts=opts)
589 if not opts.no_headers:
591 constants.SF_NODE: "Node",
592 constants.SF_TYPE: "Type",
593 constants.SF_NAME: "Name",
594 constants.SF_SIZE: "Size",
595 constants.SF_USED: "Used",
596 constants.SF_FREE: "Free",
597 constants.SF_ALLOCATABLE: "Allocatable",
602 unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
603 numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
605 # change raw values to nicer strings
607 for idx, field in enumerate(selected_fields):
609 if field == constants.SF_ALLOCATABLE:
616 data = GenerateTable(separator=opts.separator, headers=headers,
617 fields=selected_fields, unitfields=unitfields,
618 numfields=numfields, data=output, units=opts.units)
626 def ModifyStorage(opts, args):
627 """Modify storage volume on a node.
629 @param opts: the command line options selected by the user
631 @param args: should contain 3 items: node name, storage type and volume name
633 @return: the desired exit code
636 (node_name, user_storage_type, volume_name) = args
638 storage_type = ConvertStorageType(user_storage_type)
642 if opts.allocatable is not None:
643 changes[constants.SF_ALLOCATABLE] = opts.allocatable
646 op = opcodes.OpNodeModifyStorage(node_name=node_name,
647 storage_type=storage_type,
650 SubmitOpCode(op, opts=opts)
652 ToStderr("No changes to perform, exiting.")
655 def RepairStorage(opts, args):
656 """Repairs a storage volume on a node.
658 @param opts: the command line options selected by the user
660 @param args: should contain 3 items: node name, storage type and volume name
662 @return: the desired exit code
665 (node_name, user_storage_type, volume_name) = args
667 storage_type = ConvertStorageType(user_storage_type)
669 op = opcodes.OpRepairNodeStorage(node_name=node_name,
670 storage_type=storage_type,
672 ignore_consistency=opts.ignore_consistency)
673 SubmitOpCode(op, opts=opts)
676 def SetNodeParams(opts, args):
679 @param opts: the command line options selected by the user
681 @param args: should contain only one element, the node name
683 @return: the desired exit code
686 all_changes = [opts.master_candidate, opts.drained, opts.offline,
687 opts.master_capable, opts.vm_capable, opts.secondary_ip,
689 if all_changes.count(None) == len(all_changes):
690 ToStderr("Please give at least one of the parameters.")
693 op = opcodes.OpNodeSetParams(node_name=args[0],
694 master_candidate=opts.master_candidate,
695 offline=opts.offline,
696 drained=opts.drained,
697 master_capable=opts.master_capable,
698 vm_capable=opts.vm_capable,
699 secondary_ip=opts.secondary_ip,
701 ndparams=opts.ndparams,
702 auto_promote=opts.auto_promote,
703 powered=opts.node_powered)
705 # even if here we process the result, we allow submit only
706 result = SubmitOrSend(op, opts)
709 ToStdout("Modified node %s", args[0])
710 for param, data in result:
711 ToStdout(" - %-5s -> %s", param, data)
717 AddNode, [ArgHost(min=1, max=1)],
718 [SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT, NONODE_SETUP_OPT,
719 VERBOSE_OPT, NODEGROUP_OPT, PRIORITY_OPT, CAPAB_MASTER_OPT,
720 CAPAB_VM_OPT, NODE_PARAMS_OPT],
721 "[-s ip] [--readd] [--no-ssh-key-check] [--no-node-setup] [--verbose] "
723 "Add a node to the cluster"),
725 EvacuateNode, [ArgNode(min=1)],
726 [FORCE_OPT, IALLOCATOR_OPT, NEW_SECONDARY_OPT, EARLY_RELEASE_OPT,
728 "[-f] {-I <iallocator> | -n <dst>} <node>",
729 "Relocate the secondary instances from a node"
730 " to other nodes (only for instances with drbd disk template)"),
732 FailoverNode, ARGS_ONE_NODE, [FORCE_OPT, IGNORE_CONSIST_OPT, PRIORITY_OPT],
734 "Stops the primary instances on a node and start them on their"
735 " secondary node (only for instances with drbd disk template)"),
737 MigrateNode, ARGS_ONE_NODE,
738 [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT, PRIORITY_OPT],
740 "Migrate all the primary instance on a node away from it"
741 " (only for instances of type drbd)"),
743 ShowNodeConfig, ARGS_MANY_NODES, [],
744 "[<node_name>...]", "Show information about the node(s)"),
746 ListNodes, ARGS_MANY_NODES,
747 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
749 "Lists the nodes in the cluster. The available fields can be shown using"
750 " the \"list-fields\" command (see the man page for details)."
751 " The default field list is (in order): %s." %
752 utils.CommaJoin(_LIST_DEF_FIELDS)),
754 ListNodeFields, [ArgUnknown()],
755 [NOHDR_OPT, SEP_OPT],
757 "Lists all available fields for nodes"),
759 SetNodeParams, ARGS_ONE_NODE,
760 [FORCE_OPT, SUBMIT_OPT, MC_OPT, DRAINED_OPT, OFFLINE_OPT,
761 CAPAB_MASTER_OPT, CAPAB_VM_OPT, SECONDARY_IP_OPT,
762 AUTO_PROMOTE_OPT, DRY_RUN_OPT, PRIORITY_OPT, NODE_PARAMS_OPT,
764 "<node_name>", "Alters the parameters of a node"),
766 PowercycleNode, ARGS_ONE_NODE,
767 [FORCE_OPT, CONFIRM_OPT, DRY_RUN_OPT, PRIORITY_OPT],
768 "<node_name>", "Tries to forcefully powercycle a node"),
771 [ArgChoice(min=1, max=1, choices=_LIST_POWER_COMMANDS),
772 ArgNode(min=1, max=1)],
773 [SUBMIT_OPT, AUTO_PROMOTE_OPT, PRIORITY_OPT],
774 "on|off|cycle|status <node>",
775 "Change power state of node by calling out-of-band helper."),
777 RemoveNode, ARGS_ONE_NODE, [DRY_RUN_OPT, PRIORITY_OPT],
778 "<node_name>", "Removes a node from the cluster"),
780 ListVolumes, [ArgNode()],
781 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, PRIORITY_OPT],
782 "[<node_name>...]", "List logical volumes on node(s)"),
784 ListStorage, ARGS_MANY_NODES,
785 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, _STORAGE_TYPE_OPT,
787 "[<node_name>...]", "List physical volumes on node(s). The available"
788 " fields are (see the man page for details): %s." %
789 (utils.CommaJoin(_LIST_STOR_HEADERS))),
792 [ArgNode(min=1, max=1),
793 ArgChoice(min=1, max=1, choices=_MODIFIABLE_STORAGE_TYPES),
794 ArgFile(min=1, max=1)],
795 [ALLOCATABLE_OPT, DRY_RUN_OPT, PRIORITY_OPT],
796 "<node_name> <storage_type> <name>", "Modify storage volume on a node"),
799 [ArgNode(min=1, max=1),
800 ArgChoice(min=1, max=1, choices=_REPAIRABLE_STORAGE_TYPES),
801 ArgFile(min=1, max=1)],
802 [IGNORE_CONSIST_OPT, DRY_RUN_OPT, PRIORITY_OPT],
803 "<node_name> <storage_type> <name>",
804 "Repairs a storage volume on a node"),
806 ListTags, ARGS_ONE_NODE, [],
807 "<node_name>", "List the tags of the given node"),
809 AddTags, [ArgNode(min=1, max=1), ArgUnknown()], [TAG_SRC_OPT, PRIORITY_OPT],
810 "<node_name> tag...", "Add tags to the given node"),
812 RemoveTags, [ArgNode(min=1, max=1), ArgUnknown()],
813 [TAG_SRC_OPT, PRIORITY_OPT],
814 "<node_name> tag...", "Remove tags from the given node"),
819 return GenericMain(commands, override={"tag_type": constants.TAG_NODE})