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 bootstrap
31 from ganeti import opcodes
32 from ganeti import utils
33 from ganeti import constants
34 from ganeti import compat
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 #: headers (and full field list for L{ListNodes}
65 "name": "Node", "pinst_cnt": "Pinst", "sinst_cnt": "Sinst",
66 "pinst_list": "PriInstances", "sinst_list": "SecInstances",
67 "pip": "PrimaryIP", "sip": "SecondaryIP",
68 "dtotal": "DTotal", "dfree": "DFree",
69 "mtotal": "MTotal", "mnode": "MNode", "mfree": "MFree",
71 "ctotal": "CTotal", "cnodes": "CNodes", "csockets": "CSockets",
73 "serial_no": "SerialNo",
74 "master_candidate": "MasterC",
76 "offline": "Offline", "drained": "Drained",
78 "ctime": "CTime", "mtime": "MTime", "uuid": "UUID",
79 "master_capable": "MasterCapable", "vm_capable": "VMCapable",
83 #: headers (and full field list for L{ListStorage}
84 _LIST_STOR_HEADERS = {
85 constants.SF_NODE: "Node",
86 constants.SF_TYPE: "Type",
87 constants.SF_NAME: "Name",
88 constants.SF_SIZE: "Size",
89 constants.SF_USED: "Used",
90 constants.SF_FREE: "Free",
91 constants.SF_ALLOCATABLE: "Allocatable",
95 #: User-facing storage unit types
96 _USER_STORAGE_TYPE = {
97 constants.ST_FILE: "file",
98 constants.ST_LVM_PV: "lvm-pv",
99 constants.ST_LVM_VG: "lvm-vg",
102 _STORAGE_TYPE_OPT = \
103 cli_option("-t", "--storage-type",
104 dest="user_storage_type",
105 choices=_USER_STORAGE_TYPE.keys(),
107 metavar="STORAGE_TYPE",
108 help=("Storage type (%s)" %
109 utils.CommaJoin(_USER_STORAGE_TYPE.keys())))
111 _REPAIRABLE_STORAGE_TYPES = \
112 [st for st, so in constants.VALID_STORAGE_OPERATIONS.iteritems()
113 if constants.SO_FIX_CONSISTENCY in so]
115 _MODIFIABLE_STORAGE_TYPES = constants.MODIFIABLE_STORAGE_FIELDS.keys()
118 NONODE_SETUP_OPT = cli_option("--no-node-setup", default=True,
119 action="store_false", dest="node_setup",
120 help=("Do not make initial SSH setup on remote"
121 " node (needs to be done manually)"))
124 def ConvertStorageType(user_storage_type):
125 """Converts a user storage type to its internal name.
129 return _USER_STORAGE_TYPE[user_storage_type]
131 raise errors.OpPrereqError("Unknown storage type: %s" % user_storage_type,
135 def _RunSetupSSH(options, nodes):
136 """Wrapper around utils.RunCmd to call setup-ssh
138 @param options: The command line options
139 @param nodes: The nodes to setup
142 cmd = [constants.SETUP_SSH]
144 # Pass --debug|--verbose to the external script if set on our invocation
145 # --debug overrides --verbose
147 cmd.append("--debug")
148 elif options.verbose:
149 cmd.append("--verbose")
150 if not options.ssh_key_check:
151 cmd.append("--no-ssh-key-check")
155 result = utils.RunCmd(cmd, interactive=True)
158 errmsg = ("Command '%s' failed with exit code %s; output %r" %
159 (result.cmd, result.exit_code, result.output))
160 raise errors.OpExecError(errmsg)
164 def AddNode(opts, args):
165 """Add a node to the cluster.
167 @param opts: the command line options selected by the user
169 @param args: should contain only one element, the new node name
171 @return: the desired exit code
175 node = netutils.GetHostname(name=args[0]).name
179 output = cl.QueryNodes(names=[node], fields=['name', 'sip'],
181 node_exists, sip = output[0]
182 except (errors.OpPrereqError, errors.OpExecError):
188 ToStderr("Node %s not in the cluster"
189 " - please retry without '--readd'", node)
193 ToStderr("Node %s already in the cluster (as %s)"
194 " - please retry with '--readd'", node, node_exists)
196 sip = opts.secondary_ip
198 # read the cluster name from the master
199 output = cl.QueryConfigValues(['cluster_name'])
200 cluster_name = output[0]
202 if not readd and opts.node_setup:
203 ToStderr("-- WARNING -- \n"
204 "Performing this operation is going to replace the ssh daemon"
206 "on the target machine (%s) with the ones of the"
208 "and grant full intra-cluster ssh root access to/from it\n", node)
211 _RunSetupSSH(opts, [node])
213 bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
215 op = opcodes.OpAddNode(node_name=args[0], secondary_ip=sip,
216 readd=opts.readd, group=opts.nodegroup)
217 SubmitOpCode(op, opts=opts)
220 def ListNodes(opts, args):
221 """List nodes and their properties.
223 @param opts: the command line options selected by the user
225 @param args: should be an empty list
227 @return: the desired exit code
230 selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
232 output = GetClient().QueryNodes(args, selected_fields, opts.do_locking)
234 if not opts.no_headers:
235 headers = _LIST_HEADERS
239 unitfields = ["dtotal", "dfree", "mtotal", "mnode", "mfree"]
241 numfields = ["dtotal", "dfree",
242 "mtotal", "mnode", "mfree",
243 "pinst_cnt", "sinst_cnt",
244 "ctotal", "serial_no"]
246 list_type_fields = ("pinst_list", "sinst_list", "tags")
247 # change raw values to nicer strings
249 for idx, field in enumerate(selected_fields):
251 if field in list_type_fields:
253 elif field in ('master', 'master_candidate', 'offline', 'drained',
254 'master_capable', 'vm_capable'):
259 elif field == "ctime" or field == "mtime":
260 val = utils.FormatTime(val)
263 elif opts.roman_integers and isinstance(val, int):
264 val = compat.TryToRoman(val)
267 data = GenerateTable(separator=opts.separator, headers=headers,
268 fields=selected_fields, unitfields=unitfields,
269 numfields=numfields, data=output, units=opts.units)
276 def EvacuateNode(opts, args):
277 """Relocate all secondary instance from a node.
279 @param opts: the command line options selected by the user
281 @param args: should be an empty list
283 @return: the desired exit code
289 dst_node = opts.dst_node
290 iallocator = opts.iallocator
292 op = opcodes.OpNodeEvacuationStrategy(nodes=args,
293 iallocator=iallocator,
294 remote_node=dst_node)
296 result = SubmitOpCode(op, cl=cl, opts=opts)
298 # no instances to migrate
299 ToStderr("No secondary instances on node(s) %s, exiting.",
300 utils.CommaJoin(args))
301 return constants.EXIT_SUCCESS
303 if not force and not AskUser("Relocate instance(s) %s from node(s) %s?" %
304 (",".join("'%s'" % name[0] for name in result),
305 utils.CommaJoin(args))):
306 return constants.EXIT_CONFIRMATION
308 jex = JobExecutor(cl=cl, opts=opts)
312 ToStdout("Will relocate instance %s to node %s", iname, node)
313 op = opcodes.OpReplaceDisks(instance_name=iname,
314 remote_node=node, disks=[],
315 mode=constants.REPLACE_DISK_CHG,
316 early_release=opts.early_release)
317 jex.QueueJob(iname, op)
318 results = jex.GetResults()
319 bad_cnt = len([row for row in results if not row[0]])
321 ToStdout("All %d instance(s) failed over successfully.", len(results))
322 rcode = constants.EXIT_SUCCESS
324 ToStdout("There were errors during the failover:\n"
325 "%d error(s) out of %d instance(s).", bad_cnt, len(results))
326 rcode = constants.EXIT_FAILURE
330 def FailoverNode(opts, args):
331 """Failover all primary instance on a node.
333 @param opts: the command line options selected by the user
335 @param args: should be an empty list
337 @return: the desired exit code
342 selected_fields = ["name", "pinst_list"]
344 # these fields are static data anyway, so it doesn't matter, but
345 # locking=True should be safer
346 result = cl.QueryNodes(names=args, fields=selected_fields,
348 node, pinst = result[0]
351 ToStderr("No primary instances on node %s, exiting.", node)
354 pinst = utils.NiceSort(pinst)
358 if not force and not AskUser("Fail over instance(s) %s?" %
359 (",".join("'%s'" % name for name in pinst))):
362 jex = JobExecutor(cl=cl, opts=opts)
364 op = opcodes.OpFailoverInstance(instance_name=iname,
365 ignore_consistency=opts.ignore_consistency)
366 jex.QueueJob(iname, op)
367 results = jex.GetResults()
368 bad_cnt = len([row for row in results if not row[0]])
370 ToStdout("All %d instance(s) failed over successfully.", len(results))
372 ToStdout("There were errors during the failover:\n"
373 "%d error(s) out of %d instance(s).", bad_cnt, len(results))
377 def MigrateNode(opts, args):
378 """Migrate all primary instance on a node.
383 selected_fields = ["name", "pinst_list"]
385 result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
386 node, pinst = result[0]
389 ToStdout("No primary instances on node %s, exiting." % node)
392 pinst = utils.NiceSort(pinst)
394 if not force and not AskUser("Migrate instance(s) %s?" %
395 (",".join("'%s'" % name for name in pinst))):
398 # this should be removed once --non-live is deprecated
399 if not opts.live and opts.migration_mode is not None:
400 raise errors.OpPrereqError("Only one of the --non-live and "
401 "--migration-mode options can be passed",
403 if not opts.live: # --non-live passed
404 mode = constants.HT_MIGRATION_NONLIVE
406 mode = opts.migration_mode
407 op = opcodes.OpMigrateNode(node_name=args[0], mode=mode)
408 SubmitOpCode(op, cl=cl, opts=opts)
411 def ShowNodeConfig(opts, args):
412 """Show node information.
414 @param opts: the command line options selected by the user
416 @param args: should either be an empty list, in which case
417 we show information about all nodes, or should contain
418 a list of nodes to be queried for information
420 @return: the desired exit code
424 result = cl.QueryNodes(fields=["name", "pip", "sip",
425 "pinst_list", "sinst_list",
426 "master_candidate", "drained", "offline"],
427 names=args, use_locking=False)
429 for (name, primary_ip, secondary_ip, pinst, sinst,
430 is_mc, drained, offline) in result:
431 ToStdout("Node name: %s", name)
432 ToStdout(" primary ip: %s", primary_ip)
433 ToStdout(" secondary ip: %s", secondary_ip)
434 ToStdout(" master candidate: %s", is_mc)
435 ToStdout(" drained: %s", drained)
436 ToStdout(" offline: %s", offline)
438 ToStdout(" primary for instances:")
439 for iname in utils.NiceSort(pinst):
440 ToStdout(" - %s", iname)
442 ToStdout(" primary for no instances")
444 ToStdout(" secondary for instances:")
445 for iname in utils.NiceSort(sinst):
446 ToStdout(" - %s", iname)
448 ToStdout(" secondary for no instances")
453 def RemoveNode(opts, args):
454 """Remove a node from the cluster.
456 @param opts: the command line options selected by the user
458 @param args: should contain only one element, the name of
459 the node to be removed
461 @return: the desired exit code
464 op = opcodes.OpRemoveNode(node_name=args[0])
465 SubmitOpCode(op, opts=opts)
469 def PowercycleNode(opts, args):
470 """Remove a node from the cluster.
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
481 if (not opts.confirm and
482 not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
485 op = opcodes.OpPowercycleNode(node_name=node, force=opts.force)
486 result = SubmitOpCode(op, opts=opts)
492 def ListVolumes(opts, args):
493 """List logical volumes on node(s).
495 @param opts: the command line options selected by the user
497 @param args: should either be an empty list, in which case
498 we list data for all nodes, or contain a list of nodes
499 to display data only for those
501 @return: the desired exit code
504 selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
506 op = opcodes.OpQueryNodeVolumes(nodes=args, output_fields=selected_fields)
507 output = SubmitOpCode(op, opts=opts)
509 if not opts.no_headers:
510 headers = {"node": "Node", "phys": "PhysDev",
511 "vg": "VG", "name": "Name",
512 "size": "Size", "instance": "Instance"}
516 unitfields = ["size"]
520 data = GenerateTable(separator=opts.separator, headers=headers,
521 fields=selected_fields, unitfields=unitfields,
522 numfields=numfields, data=output, units=opts.units)
530 def ListStorage(opts, args):
531 """List physical volumes on node(s).
533 @param opts: the command line options selected by the user
535 @param args: should either be an empty list, in which case
536 we list data for all nodes, or contain a list of nodes
537 to display data only for those
539 @return: the desired exit code
542 # TODO: Default to ST_FILE if LVM is disabled on the cluster
543 if opts.user_storage_type is None:
544 opts.user_storage_type = constants.ST_LVM_PV
546 storage_type = ConvertStorageType(opts.user_storage_type)
548 selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
550 op = opcodes.OpQueryNodeStorage(nodes=args,
551 storage_type=storage_type,
552 output_fields=selected_fields)
553 output = SubmitOpCode(op, opts=opts)
555 if not opts.no_headers:
557 constants.SF_NODE: "Node",
558 constants.SF_TYPE: "Type",
559 constants.SF_NAME: "Name",
560 constants.SF_SIZE: "Size",
561 constants.SF_USED: "Used",
562 constants.SF_FREE: "Free",
563 constants.SF_ALLOCATABLE: "Allocatable",
568 unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
569 numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
571 # change raw values to nicer strings
573 for idx, field in enumerate(selected_fields):
575 if field == constants.SF_ALLOCATABLE:
582 data = GenerateTable(separator=opts.separator, headers=headers,
583 fields=selected_fields, unitfields=unitfields,
584 numfields=numfields, data=output, units=opts.units)
592 def ModifyStorage(opts, args):
593 """Modify storage volume on a node.
595 @param opts: the command line options selected by the user
597 @param args: should contain 3 items: node name, storage type and volume name
599 @return: the desired exit code
602 (node_name, user_storage_type, volume_name) = args
604 storage_type = ConvertStorageType(user_storage_type)
608 if opts.allocatable is not None:
609 changes[constants.SF_ALLOCATABLE] = opts.allocatable
612 op = opcodes.OpModifyNodeStorage(node_name=node_name,
613 storage_type=storage_type,
616 SubmitOpCode(op, opts=opts)
618 ToStderr("No changes to perform, exiting.")
621 def RepairStorage(opts, args):
622 """Repairs a storage volume on a node.
624 @param opts: the command line options selected by the user
626 @param args: should contain 3 items: node name, storage type and volume name
628 @return: the desired exit code
631 (node_name, user_storage_type, volume_name) = args
633 storage_type = ConvertStorageType(user_storage_type)
635 op = opcodes.OpRepairNodeStorage(node_name=node_name,
636 storage_type=storage_type,
638 ignore_consistency=opts.ignore_consistency)
639 SubmitOpCode(op, opts=opts)
642 def SetNodeParams(opts, args):
645 @param opts: the command line options selected by the user
647 @param args: should contain only one element, the node name
649 @return: the desired exit code
652 if [opts.master_candidate, opts.drained, opts.offline].count(None) == 3:
653 ToStderr("Please give at least one of the parameters.")
656 op = opcodes.OpSetNodeParams(node_name=args[0],
657 master_candidate=opts.master_candidate,
658 offline=opts.offline,
659 drained=opts.drained,
660 master_capable=opts.master_capable,
662 auto_promote=opts.auto_promote)
664 # even if here we process the result, we allow submit only
665 result = SubmitOrSend(op, opts)
668 ToStdout("Modified node %s", args[0])
669 for param, data in result:
670 ToStdout(" - %-5s -> %s", param, data)
676 AddNode, [ArgHost(min=1, max=1)],
677 [SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT, NONODE_SETUP_OPT,
678 VERBOSE_OPT, NODEGROUP_OPT, PRIORITY_OPT],
679 "[-s ip] [--readd] [--no-ssh-key-check] [--no-node-setup] [--verbose] "
681 "Add a node to the cluster"),
683 EvacuateNode, [ArgNode(min=1)],
684 [FORCE_OPT, IALLOCATOR_OPT, NEW_SECONDARY_OPT, EARLY_RELEASE_OPT,
686 "[-f] {-I <iallocator> | -n <dst>} <node>",
687 "Relocate the secondary instances from a node"
688 " to other nodes (only for instances with drbd disk template)"),
690 FailoverNode, ARGS_ONE_NODE, [FORCE_OPT, IGNORE_CONSIST_OPT, PRIORITY_OPT],
692 "Stops the primary instances on a node and start them on their"
693 " secondary node (only for instances with drbd disk template)"),
695 MigrateNode, ARGS_ONE_NODE,
696 [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT, PRIORITY_OPT],
698 "Migrate all the primary instance on a node away from it"
699 " (only for instances of type drbd)"),
701 ShowNodeConfig, ARGS_MANY_NODES, [],
702 "[<node_name>...]", "Show information about the node(s)"),
704 ListNodes, ARGS_MANY_NODES,
705 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, SYNC_OPT, ROMAN_OPT],
707 "Lists the nodes in the cluster. The available fields are (see the man"
708 " page for details): %s. The default field list is (in order): %s." %
709 (utils.CommaJoin(_LIST_HEADERS), utils.CommaJoin(_LIST_DEF_FIELDS))),
711 SetNodeParams, ARGS_ONE_NODE,
712 [FORCE_OPT, SUBMIT_OPT, MC_OPT, DRAINED_OPT, OFFLINE_OPT, CAPAB_MASTER_OPT,
713 AUTO_PROMOTE_OPT, DRY_RUN_OPT, PRIORITY_OPT],
714 "<node_name>", "Alters the parameters of a node"),
716 PowercycleNode, ARGS_ONE_NODE,
717 [FORCE_OPT, CONFIRM_OPT, DRY_RUN_OPT, PRIORITY_OPT],
718 "<node_name>", "Tries to forcefully powercycle a node"),
720 RemoveNode, ARGS_ONE_NODE, [DRY_RUN_OPT, PRIORITY_OPT],
721 "<node_name>", "Removes a node from the cluster"),
723 ListVolumes, [ArgNode()],
724 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, PRIORITY_OPT],
725 "[<node_name>...]", "List logical volumes on node(s)"),
727 ListStorage, ARGS_MANY_NODES,
728 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, _STORAGE_TYPE_OPT,
730 "[<node_name>...]", "List physical volumes on node(s). The available"
731 " fields are (see the man page for details): %s." %
732 (utils.CommaJoin(_LIST_STOR_HEADERS))),
735 [ArgNode(min=1, max=1),
736 ArgChoice(min=1, max=1, choices=_MODIFIABLE_STORAGE_TYPES),
737 ArgFile(min=1, max=1)],
738 [ALLOCATABLE_OPT, DRY_RUN_OPT, PRIORITY_OPT],
739 "<node_name> <storage_type> <name>", "Modify storage volume on a node"),
742 [ArgNode(min=1, max=1),
743 ArgChoice(min=1, max=1, choices=_REPAIRABLE_STORAGE_TYPES),
744 ArgFile(min=1, max=1)],
745 [IGNORE_CONSIST_OPT, DRY_RUN_OPT, PRIORITY_OPT],
746 "<node_name> <storage_type> <name>",
747 "Repairs a storage volume on a node"),
749 ListTags, ARGS_ONE_NODE, [],
750 "<node_name>", "List the tags of the given node"),
752 AddTags, [ArgNode(min=1, max=1), ArgUnknown()], [TAG_SRC_OPT, PRIORITY_OPT],
753 "<node_name> tag...", "Add tags to the given node"),
755 RemoveTags, [ArgNode(min=1, max=1), ArgUnknown()],
756 [TAG_SRC_OPT, PRIORITY_OPT],
757 "<node_name> tag...", "Remove tags from the given node"),
762 return GenericMain(commands, override={"tag_type": constants.TAG_NODE})