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
31 from ganeti.cli import *
32 from ganeti import bootstrap
33 from ganeti import opcodes
34 from ganeti import utils
35 from ganeti import constants
36 from ganeti import compat
37 from ganeti import errors
38 from ganeti import netutils
41 #: default list of field for L{ListNodes}
43 "name", "dtotal", "dfree",
44 "mtotal", "mnode", "mfree",
45 "pinst_cnt", "sinst_cnt",
49 #: Default field list for L{ListVolumes}
50 _LIST_VOL_DEF_FIELDS = ["node", "phys", "vg", "name", "size", "instance"]
53 #: default list of field for L{ListStorage}
54 _LIST_STOR_DEF_FIELDS = [
61 constants.SF_ALLOCATABLE,
65 #: headers (and full field list for L{ListNodes}
67 "name": "Node", "pinst_cnt": "Pinst", "sinst_cnt": "Sinst",
68 "pinst_list": "PriInstances", "sinst_list": "SecInstances",
69 "pip": "PrimaryIP", "sip": "SecondaryIP",
70 "dtotal": "DTotal", "dfree": "DFree",
71 "mtotal": "MTotal", "mnode": "MNode", "mfree": "MFree",
73 "ctotal": "CTotal", "cnodes": "CNodes", "csockets": "CSockets",
75 "serial_no": "SerialNo",
76 "master_candidate": "MasterC",
78 "offline": "Offline", "drained": "Drained",
80 "ctime": "CTime", "mtime": "MTime", "uuid": "UUID"
84 #: headers (and full field list for L{ListStorage}
85 _LIST_STOR_HEADERS = {
86 constants.SF_NODE: "Node",
87 constants.SF_TYPE: "Type",
88 constants.SF_NAME: "Name",
89 constants.SF_SIZE: "Size",
90 constants.SF_USED: "Used",
91 constants.SF_FREE: "Free",
92 constants.SF_ALLOCATABLE: "Allocatable",
96 #: User-facing storage unit types
97 _USER_STORAGE_TYPE = {
98 constants.ST_FILE: "file",
99 constants.ST_LVM_PV: "lvm-pv",
100 constants.ST_LVM_VG: "lvm-vg",
103 _STORAGE_TYPE_OPT = \
104 cli_option("-t", "--storage-type",
105 dest="user_storage_type",
106 choices=_USER_STORAGE_TYPE.keys(),
108 metavar="STORAGE_TYPE",
109 help=("Storage type (%s)" %
110 utils.CommaJoin(_USER_STORAGE_TYPE.keys())))
112 _REPAIRABLE_STORAGE_TYPES = \
113 [st for st, so in constants.VALID_STORAGE_OPERATIONS.iteritems()
114 if constants.SO_FIX_CONSISTENCY in so]
116 _MODIFIABLE_STORAGE_TYPES = constants.MODIFIABLE_STORAGE_FIELDS.keys()
119 NONODE_SETUP_OPT = cli_option("--no-node-setup", default=True,
120 action="store_false", dest="node_setup",
121 help=("Do not make initial SSH setup on remote"
122 " node (needs to be done manually)"))
125 def ConvertStorageType(user_storage_type):
126 """Converts a user storage type to its internal name.
130 return _USER_STORAGE_TYPE[user_storage_type]
132 raise errors.OpPrereqError("Unknown storage type: %s" % user_storage_type,
136 def _RunSetupSSH(options, nodes):
137 """Wrapper around utils.RunCmd to call setup-ssh
139 @param options: The command line options
140 @param nodes: The nodes to setup
143 cmd = [constants.SETUP_SSH]
145 # Pass --debug|--verbose to the external script if set on our invocation
146 # --debug overrides --verbose
148 cmd.append("--debug")
149 elif options.verbose:
150 cmd.append("--verbose")
151 if not options.ssh_key_check:
152 cmd.append("--no-ssh-key-check")
156 result = utils.RunCmd(cmd, interactive=True)
159 errmsg = ("Command '%s' failed with exit code %s; output %r" %
160 (result.cmd, result.exit_code, result.output))
161 raise errors.OpExecError(errmsg)
165 def AddNode(opts, args):
166 """Add a node to the cluster.
168 @param opts: the command line options selected by the user
170 @param args: should contain only one element, the new node name
172 @return: the desired exit code
176 node = netutils.GetHostname(name=args[0]).name
180 output = cl.QueryNodes(names=[node], fields=['name', 'sip'],
182 node_exists, sip = output[0]
183 except (errors.OpPrereqError, errors.OpExecError):
189 ToStderr("Node %s not in the cluster"
190 " - please retry without '--readd'", node)
194 ToStderr("Node %s already in the cluster (as %s)"
195 " - please retry with '--readd'", node, node_exists)
197 sip = opts.secondary_ip
199 # read the cluster name from the master
200 output = cl.QueryConfigValues(['cluster_name'])
201 cluster_name = output[0]
203 if not readd and opts.node_setup:
204 ToStderr("-- WARNING -- \n"
205 "Performing this operation is going to replace the ssh daemon"
207 "on the target machine (%s) with the ones of the"
209 "and grant full intra-cluster ssh root access to/from it\n", node)
212 _RunSetupSSH(opts, [node])
214 bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
216 op = opcodes.OpAddNode(node_name=args[0], secondary_ip=sip,
217 readd=opts.readd, nodegroup=opts.nodegroup)
218 SubmitOpCode(op, opts=opts)
221 def ListNodes(opts, args):
222 """List nodes and their properties.
224 @param opts: the command line options selected by the user
226 @param args: should be an empty list
228 @return: the desired exit code
231 selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
233 output = GetClient().QueryNodes(args, selected_fields, opts.do_locking)
235 if not opts.no_headers:
236 headers = _LIST_HEADERS
240 unitfields = ["dtotal", "dfree", "mtotal", "mnode", "mfree"]
242 numfields = ["dtotal", "dfree",
243 "mtotal", "mnode", "mfree",
244 "pinst_cnt", "sinst_cnt",
245 "ctotal", "serial_no"]
247 list_type_fields = ("pinst_list", "sinst_list", "tags")
248 # change raw values to nicer strings
250 for idx, field in enumerate(selected_fields):
252 if field in list_type_fields:
254 elif field in ('master', 'master_candidate', 'offline', 'drained'):
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,
661 auto_promote=opts.auto_promote)
663 # even if here we process the result, we allow submit only
664 result = SubmitOrSend(op, opts)
667 ToStdout("Modified node %s", args[0])
668 for param, data in result:
669 ToStdout(" - %-5s -> %s", param, data)
675 AddNode, [ArgHost(min=1, max=1)],
676 [SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT, NONODE_SETUP_OPT,
677 VERBOSE_OPT, NODEGROUP_OPT, PRIORITY_OPT],
678 "[-s ip] [--readd] [--no-ssh-key-check] [--no-node-setup] [--verbose] "
680 "Add a node to the cluster"),
682 EvacuateNode, [ArgNode(min=1)],
683 [FORCE_OPT, IALLOCATOR_OPT, NEW_SECONDARY_OPT, EARLY_RELEASE_OPT,
685 "[-f] {-I <iallocator> | -n <dst>} <node>",
686 "Relocate the secondary instances from a node"
687 " to other nodes (only for instances with drbd disk template)"),
689 FailoverNode, ARGS_ONE_NODE, [FORCE_OPT, IGNORE_CONSIST_OPT, PRIORITY_OPT],
691 "Stops the primary instances on a node and start them on their"
692 " secondary node (only for instances with drbd disk template)"),
694 MigrateNode, ARGS_ONE_NODE,
695 [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT, PRIORITY_OPT],
697 "Migrate all the primary instance on a node away from it"
698 " (only for instances of type drbd)"),
700 ShowNodeConfig, ARGS_MANY_NODES, [],
701 "[<node_name>...]", "Show information about the node(s)"),
703 ListNodes, ARGS_MANY_NODES,
704 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, SYNC_OPT, ROMAN_OPT],
706 "Lists the nodes in the cluster. The available fields are (see the man"
707 " page for details): %s. The default field list is (in order): %s." %
708 (utils.CommaJoin(_LIST_HEADERS), utils.CommaJoin(_LIST_DEF_FIELDS))),
710 SetNodeParams, ARGS_ONE_NODE,
711 [FORCE_OPT, SUBMIT_OPT, MC_OPT, DRAINED_OPT, OFFLINE_OPT,
712 AUTO_PROMOTE_OPT, DRY_RUN_OPT, PRIORITY_OPT],
713 "<node_name>", "Alters the parameters of a node"),
715 PowercycleNode, ARGS_ONE_NODE,
716 [FORCE_OPT, CONFIRM_OPT, DRY_RUN_OPT, PRIORITY_OPT],
717 "<node_name>", "Tries to forcefully powercycle a node"),
719 RemoveNode, ARGS_ONE_NODE, [DRY_RUN_OPT, PRIORITY_OPT],
720 "<node_name>", "Removes a node from the cluster"),
722 ListVolumes, [ArgNode()],
723 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, PRIORITY_OPT],
724 "[<node_name>...]", "List logical volumes on node(s)"),
726 ListStorage, ARGS_MANY_NODES,
727 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, _STORAGE_TYPE_OPT,
729 "[<node_name>...]", "List physical volumes on node(s). The available"
730 " fields are (see the man page for details): %s." %
731 (utils.CommaJoin(_LIST_STOR_HEADERS))),
734 [ArgNode(min=1, max=1),
735 ArgChoice(min=1, max=1, choices=_MODIFIABLE_STORAGE_TYPES),
736 ArgFile(min=1, max=1)],
737 [ALLOCATABLE_OPT, DRY_RUN_OPT, PRIORITY_OPT],
738 "<node_name> <storage_type> <name>", "Modify storage volume on a node"),
741 [ArgNode(min=1, max=1),
742 ArgChoice(min=1, max=1, choices=_REPAIRABLE_STORAGE_TYPES),
743 ArgFile(min=1, max=1)],
744 [IGNORE_CONSIST_OPT, DRY_RUN_OPT, PRIORITY_OPT],
745 "<node_name> <storage_type> <name>",
746 "Repairs a storage volume on a node"),
748 ListTags, ARGS_ONE_NODE, [],
749 "<node_name>", "List the tags of the given node"),
751 AddTags, [ArgNode(min=1, max=1), ArgUnknown()], [TAG_SRC_OPT, PRIORITY_OPT],
752 "<node_name> tag...", "Add tags to the given node"),
754 RemoveTags, [ArgNode(min=1, max=1), ArgUnknown()],
755 [TAG_SRC_OPT, PRIORITY_OPT],
756 "<node_name> tag...", "Remove tags from the given node"),
760 if __name__ == '__main__':
761 sys.exit(GenericMain(commands, override={"tag_type": constants.TAG_NODE}))