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
39 #: default list of field for L{ListNodes}
41 "name", "dtotal", "dfree",
42 "mtotal", "mnode", "mfree",
43 "pinst_cnt", "sinst_cnt",
47 #: Default field list for L{ListVolumes}
48 _LIST_VOL_DEF_FIELDS = ["node", "phys", "vg", "name", "size", "instance"]
51 #: default list of field for L{ListStorage}
52 _LIST_STOR_DEF_FIELDS = [
59 constants.SF_ALLOCATABLE,
63 #: default list of power commands
64 _LIST_POWER_COMMANDS = ["on", "off", "cycle", "status"]
67 #: headers (and full field list) for L{ListStorage}
68 _LIST_STOR_HEADERS = {
69 constants.SF_NODE: "Node",
70 constants.SF_TYPE: "Type",
71 constants.SF_NAME: "Name",
72 constants.SF_SIZE: "Size",
73 constants.SF_USED: "Used",
74 constants.SF_FREE: "Free",
75 constants.SF_ALLOCATABLE: "Allocatable",
79 #: User-facing storage unit types
80 _USER_STORAGE_TYPE = {
81 constants.ST_FILE: "file",
82 constants.ST_LVM_PV: "lvm-pv",
83 constants.ST_LVM_VG: "lvm-vg",
87 cli_option("-t", "--storage-type",
88 dest="user_storage_type",
89 choices=_USER_STORAGE_TYPE.keys(),
91 metavar="STORAGE_TYPE",
92 help=("Storage type (%s)" %
93 utils.CommaJoin(_USER_STORAGE_TYPE.keys())))
95 _REPAIRABLE_STORAGE_TYPES = \
96 [st for st, so in constants.VALID_STORAGE_OPERATIONS.iteritems()
97 if constants.SO_FIX_CONSISTENCY in so]
99 _MODIFIABLE_STORAGE_TYPES = constants.MODIFIABLE_STORAGE_FIELDS.keys()
102 NONODE_SETUP_OPT = cli_option("--no-node-setup", default=True,
103 action="store_false", dest="node_setup",
104 help=("Do not make initial SSH setup on remote"
105 " node (needs to be done manually)"))
108 def ConvertStorageType(user_storage_type):
109 """Converts a user storage type to its internal name.
113 return _USER_STORAGE_TYPE[user_storage_type]
115 raise errors.OpPrereqError("Unknown storage type: %s" % user_storage_type,
119 def _RunSetupSSH(options, nodes):
120 """Wrapper around utils.RunCmd to call setup-ssh
122 @param options: The command line options
123 @param nodes: The nodes to setup
126 cmd = [constants.SETUP_SSH]
128 # Pass --debug|--verbose to the external script if set on our invocation
129 # --debug overrides --verbose
131 cmd.append("--debug")
132 elif options.verbose:
133 cmd.append("--verbose")
134 if not options.ssh_key_check:
135 cmd.append("--no-ssh-key-check")
139 result = utils.RunCmd(cmd, interactive=True)
142 errmsg = ("Command '%s' failed with exit code %s; output %r" %
143 (result.cmd, result.exit_code, result.output))
144 raise errors.OpExecError(errmsg)
148 def AddNode(opts, args):
149 """Add a node to the cluster.
151 @param opts: the command line options selected by the user
153 @param args: should contain only one element, the new node name
155 @return: the desired exit code
159 node = netutils.GetHostname(name=args[0]).name
163 output = cl.QueryNodes(names=[node], fields=['name', 'sip'],
165 node_exists, sip = output[0]
166 except (errors.OpPrereqError, errors.OpExecError):
172 ToStderr("Node %s not in the cluster"
173 " - please retry without '--readd'", node)
177 ToStderr("Node %s already in the cluster (as %s)"
178 " - please retry with '--readd'", node, node_exists)
180 sip = opts.secondary_ip
182 # read the cluster name from the master
183 output = cl.QueryConfigValues(['cluster_name'])
184 cluster_name = output[0]
186 if not readd and opts.node_setup:
187 ToStderr("-- WARNING -- \n"
188 "Performing this operation is going to replace the ssh daemon"
190 "on the target machine (%s) with the ones of the"
192 "and grant full intra-cluster ssh root access to/from it\n", node)
195 _RunSetupSSH(opts, [node])
197 bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
199 op = opcodes.OpAddNode(node_name=args[0], secondary_ip=sip,
200 readd=opts.readd, group=opts.nodegroup,
201 vm_capable=opts.vm_capable, ndparams=opts.ndparams,
202 master_capable=opts.master_capable)
203 SubmitOpCode(op, opts=opts)
206 def ListNodes(opts, args):
207 """List nodes and their properties.
209 @param opts: the command line options selected by the user
211 @param args: nodes to list, or empty for all
213 @return: the desired exit code
216 selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
218 fmtoverride = dict.fromkeys(["pinst_list", "sinst_list", "tags"],
221 return GenericList(constants.QR_NODE, selected_fields, args, opts.units,
222 opts.separator, not opts.no_headers,
223 format_override=fmtoverride)
226 def ListNodeFields(opts, args):
229 @param opts: the command line options selected by the user
231 @param args: fields to list, or empty for all
233 @return: the desired exit code
236 return GenericListFields(constants.QR_NODE, args, opts.separator,
240 def EvacuateNode(opts, args):
241 """Relocate all secondary instance from a node.
243 @param opts: the command line options selected by the user
245 @param args: should be an empty list
247 @return: the desired exit code
253 dst_node = opts.dst_node
254 iallocator = opts.iallocator
256 op = opcodes.OpNodeEvacuationStrategy(nodes=args,
257 iallocator=iallocator,
258 remote_node=dst_node)
260 result = SubmitOpCode(op, cl=cl, opts=opts)
262 # no instances to migrate
263 ToStderr("No secondary instances on node(s) %s, exiting.",
264 utils.CommaJoin(args))
265 return constants.EXIT_SUCCESS
267 if not force and not AskUser("Relocate instance(s) %s from node(s) %s?" %
268 (",".join("'%s'" % name[0] for name in result),
269 utils.CommaJoin(args))):
270 return constants.EXIT_CONFIRMATION
272 jex = JobExecutor(cl=cl, opts=opts)
276 ToStdout("Will relocate instance %s to node %s", iname, node)
277 op = opcodes.OpReplaceDisks(instance_name=iname,
278 remote_node=node, disks=[],
279 mode=constants.REPLACE_DISK_CHG,
280 early_release=opts.early_release)
281 jex.QueueJob(iname, op)
282 results = jex.GetResults()
283 bad_cnt = len([row for row in results if not row[0]])
285 ToStdout("All %d instance(s) failed over successfully.", len(results))
286 rcode = constants.EXIT_SUCCESS
288 ToStdout("There were errors during the failover:\n"
289 "%d error(s) out of %d instance(s).", bad_cnt, len(results))
290 rcode = constants.EXIT_FAILURE
294 def FailoverNode(opts, args):
295 """Failover all primary instance on a node.
297 @param opts: the command line options selected by the user
299 @param args: should be an empty list
301 @return: the desired exit code
306 selected_fields = ["name", "pinst_list"]
308 # these fields are static data anyway, so it doesn't matter, but
309 # locking=True should be safer
310 result = cl.QueryNodes(names=args, fields=selected_fields,
312 node, pinst = result[0]
315 ToStderr("No primary instances on node %s, exiting.", node)
318 pinst = utils.NiceSort(pinst)
322 if not force and not AskUser("Fail over instance(s) %s?" %
323 (",".join("'%s'" % name for name in pinst))):
326 jex = JobExecutor(cl=cl, opts=opts)
328 op = opcodes.OpFailoverInstance(instance_name=iname,
329 ignore_consistency=opts.ignore_consistency)
330 jex.QueueJob(iname, op)
331 results = jex.GetResults()
332 bad_cnt = len([row for row in results if not row[0]])
334 ToStdout("All %d instance(s) failed over successfully.", len(results))
336 ToStdout("There were errors during the failover:\n"
337 "%d error(s) out of %d instance(s).", bad_cnt, len(results))
341 def MigrateNode(opts, args):
342 """Migrate all primary instance on a node.
347 selected_fields = ["name", "pinst_list"]
349 result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
350 node, pinst = result[0]
353 ToStdout("No primary instances on node %s, exiting." % node)
356 pinst = utils.NiceSort(pinst)
358 if not force and not AskUser("Migrate instance(s) %s?" %
359 (",".join("'%s'" % name for name in pinst))):
362 # this should be removed once --non-live is deprecated
363 if not opts.live and opts.migration_mode is not None:
364 raise errors.OpPrereqError("Only one of the --non-live and "
365 "--migration-mode options can be passed",
367 if not opts.live: # --non-live passed
368 mode = constants.HT_MIGRATION_NONLIVE
370 mode = opts.migration_mode
371 op = opcodes.OpMigrateNode(node_name=args[0], mode=mode)
372 SubmitOpCode(op, cl=cl, opts=opts)
375 def ShowNodeConfig(opts, args):
376 """Show node information.
378 @param opts: the command line options selected by the user
380 @param args: should either be an empty list, in which case
381 we show information about all nodes, or should contain
382 a list of nodes to be queried for information
384 @return: the desired exit code
388 result = cl.QueryNodes(fields=["name", "pip", "sip",
389 "pinst_list", "sinst_list",
390 "master_candidate", "drained", "offline",
391 "master_capable", "vm_capable", "powered"],
392 names=args, use_locking=False)
394 for (name, primary_ip, secondary_ip, pinst, sinst,
395 is_mc, drained, offline, master_capable, vm_capable, powered) in result:
396 ToStdout("Node name: %s", name)
397 ToStdout(" primary ip: %s", primary_ip)
398 ToStdout(" secondary ip: %s", secondary_ip)
399 ToStdout(" master candidate: %s", is_mc)
400 ToStdout(" drained: %s", drained)
401 ToStdout(" offline: %s", offline)
402 if powered is not None:
403 ToStdout(" powered: %s", powered)
404 ToStdout(" master_capable: %s", master_capable)
405 ToStdout(" vm_capable: %s", vm_capable)
408 ToStdout(" primary for instances:")
409 for iname in utils.NiceSort(pinst):
410 ToStdout(" - %s", iname)
412 ToStdout(" primary for no instances")
414 ToStdout(" secondary for instances:")
415 for iname in utils.NiceSort(sinst):
416 ToStdout(" - %s", iname)
418 ToStdout(" secondary for no instances")
423 def RemoveNode(opts, args):
424 """Remove a node from the cluster.
426 @param opts: the command line options selected by the user
428 @param args: should contain only one element, the name of
429 the node to be removed
431 @return: the desired exit code
434 op = opcodes.OpRemoveNode(node_name=args[0])
435 SubmitOpCode(op, opts=opts)
439 def PowercycleNode(opts, args):
440 """Remove a node from the cluster.
442 @param opts: the command line options selected by the user
444 @param args: should contain only one element, the name of
445 the node to be removed
447 @return: the desired exit code
451 if (not opts.confirm and
452 not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
455 op = opcodes.OpPowercycleNode(node_name=node, force=opts.force)
456 result = SubmitOpCode(op, opts=opts)
462 def PowerNode(opts, args):
463 """Change/ask power state of a node.
465 @param opts: the command line options selected by the user
467 @param args: should contain only one element, the name of
468 the node to be removed
470 @return: the desired exit code
476 if command not in _LIST_POWER_COMMANDS:
477 ToStderr("power subcommand %s not supported." % command)
478 return constants.EXIT_FAILURE
480 oob_command = "power-%s" % command
483 if oob_command == constants.OOB_POWER_OFF:
484 opcodelist.append(opcodes.OpSetNodeParams(node_name=node, offline=True,
485 auto_promote=opts.auto_promote))
487 opcodelist.append(opcodes.OpOobCommand(node_name=node, command=oob_command))
489 cli.SetGenericOpcodeOpts(opcodelist, opts)
491 job_id = cli.SendJob(opcodelist)
493 # We just want the OOB Opcode status
494 # If it fails PollJob gives us the error message in it
495 result = cli.PollJob(job_id)[-1]
498 if oob_command == constants.OOB_POWER_STATUS:
499 text = "The machine is %spowered"
500 if result[constants.OOB_POWER_STATUS_POWERED]:
503 result = text % "not "
506 return constants.EXIT_SUCCESS
509 def ListVolumes(opts, args):
510 """List logical volumes on node(s).
512 @param opts: the command line options selected by the user
514 @param args: should either be an empty list, in which case
515 we list data for all nodes, or contain a list of nodes
516 to display data only for those
518 @return: the desired exit code
521 selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
523 op = opcodes.OpQueryNodeVolumes(nodes=args, output_fields=selected_fields)
524 output = SubmitOpCode(op, opts=opts)
526 if not opts.no_headers:
527 headers = {"node": "Node", "phys": "PhysDev",
528 "vg": "VG", "name": "Name",
529 "size": "Size", "instance": "Instance"}
533 unitfields = ["size"]
537 data = GenerateTable(separator=opts.separator, headers=headers,
538 fields=selected_fields, unitfields=unitfields,
539 numfields=numfields, data=output, units=opts.units)
547 def ListStorage(opts, args):
548 """List physical volumes on node(s).
550 @param opts: the command line options selected by the user
552 @param args: should either be an empty list, in which case
553 we list data for all nodes, or contain a list of nodes
554 to display data only for those
556 @return: the desired exit code
559 # TODO: Default to ST_FILE if LVM is disabled on the cluster
560 if opts.user_storage_type is None:
561 opts.user_storage_type = constants.ST_LVM_PV
563 storage_type = ConvertStorageType(opts.user_storage_type)
565 selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
567 op = opcodes.OpQueryNodeStorage(nodes=args,
568 storage_type=storage_type,
569 output_fields=selected_fields)
570 output = SubmitOpCode(op, opts=opts)
572 if not opts.no_headers:
574 constants.SF_NODE: "Node",
575 constants.SF_TYPE: "Type",
576 constants.SF_NAME: "Name",
577 constants.SF_SIZE: "Size",
578 constants.SF_USED: "Used",
579 constants.SF_FREE: "Free",
580 constants.SF_ALLOCATABLE: "Allocatable",
585 unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
586 numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
588 # change raw values to nicer strings
590 for idx, field in enumerate(selected_fields):
592 if field == constants.SF_ALLOCATABLE:
599 data = GenerateTable(separator=opts.separator, headers=headers,
600 fields=selected_fields, unitfields=unitfields,
601 numfields=numfields, data=output, units=opts.units)
609 def ModifyStorage(opts, args):
610 """Modify storage volume on a node.
612 @param opts: the command line options selected by the user
614 @param args: should contain 3 items: node name, storage type and volume name
616 @return: the desired exit code
619 (node_name, user_storage_type, volume_name) = args
621 storage_type = ConvertStorageType(user_storage_type)
625 if opts.allocatable is not None:
626 changes[constants.SF_ALLOCATABLE] = opts.allocatable
629 op = opcodes.OpModifyNodeStorage(node_name=node_name,
630 storage_type=storage_type,
633 SubmitOpCode(op, opts=opts)
635 ToStderr("No changes to perform, exiting.")
638 def RepairStorage(opts, args):
639 """Repairs a storage volume on a node.
641 @param opts: the command line options selected by the user
643 @param args: should contain 3 items: node name, storage type and volume name
645 @return: the desired exit code
648 (node_name, user_storage_type, volume_name) = args
650 storage_type = ConvertStorageType(user_storage_type)
652 op = opcodes.OpRepairNodeStorage(node_name=node_name,
653 storage_type=storage_type,
655 ignore_consistency=opts.ignore_consistency)
656 SubmitOpCode(op, opts=opts)
659 def SetNodeParams(opts, args):
662 @param opts: the command line options selected by the user
664 @param args: should contain only one element, the node name
666 @return: the desired exit code
669 all_changes = [opts.master_candidate, opts.drained, opts.offline,
670 opts.master_capable, opts.vm_capable, opts.secondary_ip,
672 if all_changes.count(None) == len(all_changes):
673 ToStderr("Please give at least one of the parameters.")
676 op = opcodes.OpSetNodeParams(node_name=args[0],
677 master_candidate=opts.master_candidate,
678 offline=opts.offline,
679 drained=opts.drained,
680 master_capable=opts.master_capable,
681 vm_capable=opts.vm_capable,
682 secondary_ip=opts.secondary_ip,
684 ndparams=opts.ndparams,
685 auto_promote=opts.auto_promote,
686 powered=opts.node_powered)
688 # even if here we process the result, we allow submit only
689 result = SubmitOrSend(op, opts)
692 ToStdout("Modified node %s", args[0])
693 for param, data in result:
694 ToStdout(" - %-5s -> %s", param, data)
700 AddNode, [ArgHost(min=1, max=1)],
701 [SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT, NONODE_SETUP_OPT,
702 VERBOSE_OPT, NODEGROUP_OPT, PRIORITY_OPT, CAPAB_MASTER_OPT,
703 CAPAB_VM_OPT, NODE_PARAMS_OPT],
704 "[-s ip] [--readd] [--no-ssh-key-check] [--no-node-setup] [--verbose] "
706 "Add a node to the cluster"),
708 EvacuateNode, [ArgNode(min=1)],
709 [FORCE_OPT, IALLOCATOR_OPT, NEW_SECONDARY_OPT, EARLY_RELEASE_OPT,
711 "[-f] {-I <iallocator> | -n <dst>} <node>",
712 "Relocate the secondary instances from a node"
713 " to other nodes (only for instances with drbd disk template)"),
715 FailoverNode, ARGS_ONE_NODE, [FORCE_OPT, IGNORE_CONSIST_OPT, PRIORITY_OPT],
717 "Stops the primary instances on a node and start them on their"
718 " secondary node (only for instances with drbd disk template)"),
720 MigrateNode, ARGS_ONE_NODE,
721 [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT, PRIORITY_OPT],
723 "Migrate all the primary instance on a node away from it"
724 " (only for instances of type drbd)"),
726 ShowNodeConfig, ARGS_MANY_NODES, [],
727 "[<node_name>...]", "Show information about the node(s)"),
729 ListNodes, ARGS_MANY_NODES,
730 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
732 "Lists the nodes in the cluster. The available fields can be shown using"
733 " the \"list-fields\" command (see the man page for details)."
734 " The default field list is (in order): %s." %
735 utils.CommaJoin(_LIST_DEF_FIELDS)),
737 ListNodeFields, [ArgUnknown()],
738 [NOHDR_OPT, SEP_OPT],
740 "Lists all available fields for nodes"),
742 SetNodeParams, ARGS_ONE_NODE,
743 [FORCE_OPT, SUBMIT_OPT, MC_OPT, DRAINED_OPT, OFFLINE_OPT,
744 CAPAB_MASTER_OPT, CAPAB_VM_OPT, SECONDARY_IP_OPT,
745 AUTO_PROMOTE_OPT, DRY_RUN_OPT, PRIORITY_OPT, NODE_PARAMS_OPT,
747 "<node_name>", "Alters the parameters of a node"),
749 PowercycleNode, ARGS_ONE_NODE,
750 [FORCE_OPT, CONFIRM_OPT, DRY_RUN_OPT, PRIORITY_OPT],
751 "<node_name>", "Tries to forcefully powercycle a node"),
754 [ArgChoice(min=1, max=1, choices=_LIST_POWER_COMMANDS),
755 ArgNode(min=1, max=1)],
756 [SUBMIT_OPT, AUTO_PROMOTE_OPT, PRIORITY_OPT],
757 "on|off|cycle|status <node>",
758 "Change power state of node by calling out-of-band helper."),
760 RemoveNode, ARGS_ONE_NODE, [DRY_RUN_OPT, PRIORITY_OPT],
761 "<node_name>", "Removes a node from the cluster"),
763 ListVolumes, [ArgNode()],
764 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, PRIORITY_OPT],
765 "[<node_name>...]", "List logical volumes on node(s)"),
767 ListStorage, ARGS_MANY_NODES,
768 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, _STORAGE_TYPE_OPT,
770 "[<node_name>...]", "List physical volumes on node(s). The available"
771 " fields are (see the man page for details): %s." %
772 (utils.CommaJoin(_LIST_STOR_HEADERS))),
775 [ArgNode(min=1, max=1),
776 ArgChoice(min=1, max=1, choices=_MODIFIABLE_STORAGE_TYPES),
777 ArgFile(min=1, max=1)],
778 [ALLOCATABLE_OPT, DRY_RUN_OPT, PRIORITY_OPT],
779 "<node_name> <storage_type> <name>", "Modify storage volume on a node"),
782 [ArgNode(min=1, max=1),
783 ArgChoice(min=1, max=1, choices=_REPAIRABLE_STORAGE_TYPES),
784 ArgFile(min=1, max=1)],
785 [IGNORE_CONSIST_OPT, DRY_RUN_OPT, PRIORITY_OPT],
786 "<node_name> <storage_type> <name>",
787 "Repairs a storage volume on a node"),
789 ListTags, ARGS_ONE_NODE, [],
790 "<node_name>", "List the tags of the given node"),
792 AddTags, [ArgNode(min=1, max=1), ArgUnknown()], [TAG_SRC_OPT, PRIORITY_OPT],
793 "<node_name> tag...", "Add tags to the given node"),
795 RemoveTags, [ArgNode(min=1, max=1), ArgUnknown()],
796 [TAG_SRC_OPT, PRIORITY_OPT],
797 "<node_name> tag...", "Remove tags from the given node"),
802 return GenericMain(commands, override={"tag_type": constants.TAG_NODE})