4 # Copyright (C) 2006, 2007, 2008, 2009, 2010 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.OpAddNode(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.OpNodeEvacuationStrategy(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.OpReplaceDisks(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.OpFailoverInstance(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.OpMigrateNode(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.OpRemoveNode(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.OpPowercycleNode(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.OpSetNodeParams(node_name=node, offline=True,
492 auto_promote=opts.auto_promote))
494 opcodelist.append(opcodes.OpOobCommand(node_name=node, command=oob_command))
496 cli.SetGenericOpcodeOpts(opcodelist, opts)
498 job_id = cli.SendJob(opcodelist)
500 # We just want the OOB Opcode status
501 # If it fails PollJob gives us the error message in it
502 result = cli.PollJob(job_id)[-1]
505 if oob_command == constants.OOB_POWER_STATUS:
506 text = "The machine is %spowered"
507 if result[constants.OOB_POWER_STATUS_POWERED]:
510 result = text % "not "
513 return constants.EXIT_SUCCESS
516 def ListVolumes(opts, args):
517 """List logical volumes on node(s).
519 @param opts: the command line options selected by the user
521 @param args: should either be an empty list, in which case
522 we list data for all nodes, or contain a list of nodes
523 to display data only for those
525 @return: the desired exit code
528 selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
530 op = opcodes.OpQueryNodeVolumes(nodes=args, output_fields=selected_fields)
531 output = SubmitOpCode(op, opts=opts)
533 if not opts.no_headers:
534 headers = {"node": "Node", "phys": "PhysDev",
535 "vg": "VG", "name": "Name",
536 "size": "Size", "instance": "Instance"}
540 unitfields = ["size"]
544 data = GenerateTable(separator=opts.separator, headers=headers,
545 fields=selected_fields, unitfields=unitfields,
546 numfields=numfields, data=output, units=opts.units)
554 def ListStorage(opts, args):
555 """List physical volumes on node(s).
557 @param opts: the command line options selected by the user
559 @param args: should either be an empty list, in which case
560 we list data for all nodes, or contain a list of nodes
561 to display data only for those
563 @return: the desired exit code
566 # TODO: Default to ST_FILE if LVM is disabled on the cluster
567 if opts.user_storage_type is None:
568 opts.user_storage_type = constants.ST_LVM_PV
570 storage_type = ConvertStorageType(opts.user_storage_type)
572 selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
574 op = opcodes.OpQueryNodeStorage(nodes=args,
575 storage_type=storage_type,
576 output_fields=selected_fields)
577 output = SubmitOpCode(op, opts=opts)
579 if not opts.no_headers:
581 constants.SF_NODE: "Node",
582 constants.SF_TYPE: "Type",
583 constants.SF_NAME: "Name",
584 constants.SF_SIZE: "Size",
585 constants.SF_USED: "Used",
586 constants.SF_FREE: "Free",
587 constants.SF_ALLOCATABLE: "Allocatable",
592 unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
593 numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
595 # change raw values to nicer strings
597 for idx, field in enumerate(selected_fields):
599 if field == constants.SF_ALLOCATABLE:
606 data = GenerateTable(separator=opts.separator, headers=headers,
607 fields=selected_fields, unitfields=unitfields,
608 numfields=numfields, data=output, units=opts.units)
616 def ModifyStorage(opts, args):
617 """Modify storage volume on a node.
619 @param opts: the command line options selected by the user
621 @param args: should contain 3 items: node name, storage type and volume name
623 @return: the desired exit code
626 (node_name, user_storage_type, volume_name) = args
628 storage_type = ConvertStorageType(user_storage_type)
632 if opts.allocatable is not None:
633 changes[constants.SF_ALLOCATABLE] = opts.allocatable
636 op = opcodes.OpModifyNodeStorage(node_name=node_name,
637 storage_type=storage_type,
640 SubmitOpCode(op, opts=opts)
642 ToStderr("No changes to perform, exiting.")
645 def RepairStorage(opts, args):
646 """Repairs a storage volume on a node.
648 @param opts: the command line options selected by the user
650 @param args: should contain 3 items: node name, storage type and volume name
652 @return: the desired exit code
655 (node_name, user_storage_type, volume_name) = args
657 storage_type = ConvertStorageType(user_storage_type)
659 op = opcodes.OpRepairNodeStorage(node_name=node_name,
660 storage_type=storage_type,
662 ignore_consistency=opts.ignore_consistency)
663 SubmitOpCode(op, opts=opts)
666 def SetNodeParams(opts, args):
669 @param opts: the command line options selected by the user
671 @param args: should contain only one element, the node name
673 @return: the desired exit code
676 all_changes = [opts.master_candidate, opts.drained, opts.offline,
677 opts.master_capable, opts.vm_capable, opts.secondary_ip,
679 if all_changes.count(None) == len(all_changes):
680 ToStderr("Please give at least one of the parameters.")
683 op = opcodes.OpSetNodeParams(node_name=args[0],
684 master_candidate=opts.master_candidate,
685 offline=opts.offline,
686 drained=opts.drained,
687 master_capable=opts.master_capable,
688 vm_capable=opts.vm_capable,
689 secondary_ip=opts.secondary_ip,
691 ndparams=opts.ndparams,
692 auto_promote=opts.auto_promote,
693 powered=opts.node_powered)
695 # even if here we process the result, we allow submit only
696 result = SubmitOrSend(op, opts)
699 ToStdout("Modified node %s", args[0])
700 for param, data in result:
701 ToStdout(" - %-5s -> %s", param, data)
707 AddNode, [ArgHost(min=1, max=1)],
708 [SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT, NONODE_SETUP_OPT,
709 VERBOSE_OPT, NODEGROUP_OPT, PRIORITY_OPT, CAPAB_MASTER_OPT,
710 CAPAB_VM_OPT, NODE_PARAMS_OPT],
711 "[-s ip] [--readd] [--no-ssh-key-check] [--no-node-setup] [--verbose] "
713 "Add a node to the cluster"),
715 EvacuateNode, [ArgNode(min=1)],
716 [FORCE_OPT, IALLOCATOR_OPT, NEW_SECONDARY_OPT, EARLY_RELEASE_OPT,
718 "[-f] {-I <iallocator> | -n <dst>} <node>",
719 "Relocate the secondary instances from a node"
720 " to other nodes (only for instances with drbd disk template)"),
722 FailoverNode, ARGS_ONE_NODE, [FORCE_OPT, IGNORE_CONSIST_OPT, PRIORITY_OPT],
724 "Stops the primary instances on a node and start them on their"
725 " secondary node (only for instances with drbd disk template)"),
727 MigrateNode, ARGS_ONE_NODE,
728 [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT, PRIORITY_OPT],
730 "Migrate all the primary instance on a node away from it"
731 " (only for instances of type drbd)"),
733 ShowNodeConfig, ARGS_MANY_NODES, [],
734 "[<node_name>...]", "Show information about the node(s)"),
736 ListNodes, ARGS_MANY_NODES,
737 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
739 "Lists the nodes in the cluster. The available fields can be shown using"
740 " the \"list-fields\" command (see the man page for details)."
741 " The default field list is (in order): %s." %
742 utils.CommaJoin(_LIST_DEF_FIELDS)),
744 ListNodeFields, [ArgUnknown()],
745 [NOHDR_OPT, SEP_OPT],
747 "Lists all available fields for nodes"),
749 SetNodeParams, ARGS_ONE_NODE,
750 [FORCE_OPT, SUBMIT_OPT, MC_OPT, DRAINED_OPT, OFFLINE_OPT,
751 CAPAB_MASTER_OPT, CAPAB_VM_OPT, SECONDARY_IP_OPT,
752 AUTO_PROMOTE_OPT, DRY_RUN_OPT, PRIORITY_OPT, NODE_PARAMS_OPT,
754 "<node_name>", "Alters the parameters of a node"),
756 PowercycleNode, ARGS_ONE_NODE,
757 [FORCE_OPT, CONFIRM_OPT, DRY_RUN_OPT, PRIORITY_OPT],
758 "<node_name>", "Tries to forcefully powercycle a node"),
761 [ArgChoice(min=1, max=1, choices=_LIST_POWER_COMMANDS),
762 ArgNode(min=1, max=1)],
763 [SUBMIT_OPT, AUTO_PROMOTE_OPT, PRIORITY_OPT],
764 "on|off|cycle|status <node>",
765 "Change power state of node by calling out-of-band helper."),
767 RemoveNode, ARGS_ONE_NODE, [DRY_RUN_OPT, PRIORITY_OPT],
768 "<node_name>", "Removes a node from the cluster"),
770 ListVolumes, [ArgNode()],
771 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, PRIORITY_OPT],
772 "[<node_name>...]", "List logical volumes on node(s)"),
774 ListStorage, ARGS_MANY_NODES,
775 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, _STORAGE_TYPE_OPT,
777 "[<node_name>...]", "List physical volumes on node(s). The available"
778 " fields are (see the man page for details): %s." %
779 (utils.CommaJoin(_LIST_STOR_HEADERS))),
782 [ArgNode(min=1, max=1),
783 ArgChoice(min=1, max=1, choices=_MODIFIABLE_STORAGE_TYPES),
784 ArgFile(min=1, max=1)],
785 [ALLOCATABLE_OPT, DRY_RUN_OPT, PRIORITY_OPT],
786 "<node_name> <storage_type> <name>", "Modify storage volume on a node"),
789 [ArgNode(min=1, max=1),
790 ArgChoice(min=1, max=1, choices=_REPAIRABLE_STORAGE_TYPES),
791 ArgFile(min=1, max=1)],
792 [IGNORE_CONSIST_OPT, DRY_RUN_OPT, PRIORITY_OPT],
793 "<node_name> <storage_type> <name>",
794 "Repairs a storage volume on a node"),
796 ListTags, ARGS_ONE_NODE, [],
797 "<node_name>", "List the tags of the given node"),
799 AddTags, [ArgNode(min=1, max=1), ArgUnknown()], [TAG_SRC_OPT, PRIORITY_OPT],
800 "<node_name> tag...", "Add tags to the given node"),
802 RemoveTags, [ArgNode(min=1, max=1), ArgUnknown()],
803 [TAG_SRC_OPT, PRIORITY_OPT],
804 "<node_name> tag...", "Remove tags from the given node"),
809 return GenericMain(commands, override={"tag_type": constants.TAG_NODE})