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 vm_capable=opts.vm_capable,
218 master_capable=opts.master_capable)
219 SubmitOpCode(op, opts=opts)
222 def ListNodes(opts, args):
223 """List nodes and their properties.
225 @param opts: the command line options selected by the user
227 @param args: should be an empty list
229 @return: the desired exit code
232 selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
234 output = GetClient().QueryNodes(args, selected_fields, opts.do_locking)
236 if not opts.no_headers:
237 headers = _LIST_HEADERS
241 unitfields = ["dtotal", "dfree", "mtotal", "mnode", "mfree"]
243 numfields = ["dtotal", "dfree",
244 "mtotal", "mnode", "mfree",
245 "pinst_cnt", "sinst_cnt",
246 "ctotal", "serial_no"]
248 list_type_fields = ("pinst_list", "sinst_list", "tags")
249 # change raw values to nicer strings
251 for idx, field in enumerate(selected_fields):
253 if field in list_type_fields:
255 elif field in ('master', 'master_candidate', 'offline', 'drained',
256 'master_capable', 'vm_capable'):
261 elif field == "ctime" or field == "mtime":
262 val = utils.FormatTime(val)
265 elif opts.roman_integers and isinstance(val, int):
266 val = compat.TryToRoman(val)
269 data = GenerateTable(separator=opts.separator, headers=headers,
270 fields=selected_fields, unitfields=unitfields,
271 numfields=numfields, data=output, units=opts.units)
278 def EvacuateNode(opts, args):
279 """Relocate all secondary instance from a node.
281 @param opts: the command line options selected by the user
283 @param args: should be an empty list
285 @return: the desired exit code
291 dst_node = opts.dst_node
292 iallocator = opts.iallocator
294 op = opcodes.OpNodeEvacuationStrategy(nodes=args,
295 iallocator=iallocator,
296 remote_node=dst_node)
298 result = SubmitOpCode(op, cl=cl, opts=opts)
300 # no instances to migrate
301 ToStderr("No secondary instances on node(s) %s, exiting.",
302 utils.CommaJoin(args))
303 return constants.EXIT_SUCCESS
305 if not force and not AskUser("Relocate instance(s) %s from node(s) %s?" %
306 (",".join("'%s'" % name[0] for name in result),
307 utils.CommaJoin(args))):
308 return constants.EXIT_CONFIRMATION
310 jex = JobExecutor(cl=cl, opts=opts)
314 ToStdout("Will relocate instance %s to node %s", iname, node)
315 op = opcodes.OpReplaceDisks(instance_name=iname,
316 remote_node=node, disks=[],
317 mode=constants.REPLACE_DISK_CHG,
318 early_release=opts.early_release)
319 jex.QueueJob(iname, op)
320 results = jex.GetResults()
321 bad_cnt = len([row for row in results if not row[0]])
323 ToStdout("All %d instance(s) failed over successfully.", len(results))
324 rcode = constants.EXIT_SUCCESS
326 ToStdout("There were errors during the failover:\n"
327 "%d error(s) out of %d instance(s).", bad_cnt, len(results))
328 rcode = constants.EXIT_FAILURE
332 def FailoverNode(opts, args):
333 """Failover all primary instance on a node.
335 @param opts: the command line options selected by the user
337 @param args: should be an empty list
339 @return: the desired exit code
344 selected_fields = ["name", "pinst_list"]
346 # these fields are static data anyway, so it doesn't matter, but
347 # locking=True should be safer
348 result = cl.QueryNodes(names=args, fields=selected_fields,
350 node, pinst = result[0]
353 ToStderr("No primary instances on node %s, exiting.", node)
356 pinst = utils.NiceSort(pinst)
360 if not force and not AskUser("Fail over instance(s) %s?" %
361 (",".join("'%s'" % name for name in pinst))):
364 jex = JobExecutor(cl=cl, opts=opts)
366 op = opcodes.OpFailoverInstance(instance_name=iname,
367 ignore_consistency=opts.ignore_consistency)
368 jex.QueueJob(iname, op)
369 results = jex.GetResults()
370 bad_cnt = len([row for row in results if not row[0]])
372 ToStdout("All %d instance(s) failed over successfully.", len(results))
374 ToStdout("There were errors during the failover:\n"
375 "%d error(s) out of %d instance(s).", bad_cnt, len(results))
379 def MigrateNode(opts, args):
380 """Migrate all primary instance on a node.
385 selected_fields = ["name", "pinst_list"]
387 result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
388 node, pinst = result[0]
391 ToStdout("No primary instances on node %s, exiting." % node)
394 pinst = utils.NiceSort(pinst)
396 if not force and not AskUser("Migrate instance(s) %s?" %
397 (",".join("'%s'" % name for name in pinst))):
400 # this should be removed once --non-live is deprecated
401 if not opts.live and opts.migration_mode is not None:
402 raise errors.OpPrereqError("Only one of the --non-live and "
403 "--migration-mode options can be passed",
405 if not opts.live: # --non-live passed
406 mode = constants.HT_MIGRATION_NONLIVE
408 mode = opts.migration_mode
409 op = opcodes.OpMigrateNode(node_name=args[0], mode=mode)
410 SubmitOpCode(op, cl=cl, opts=opts)
413 def ShowNodeConfig(opts, args):
414 """Show node information.
416 @param opts: the command line options selected by the user
418 @param args: should either be an empty list, in which case
419 we show information about all nodes, or should contain
420 a list of nodes to be queried for information
422 @return: the desired exit code
426 result = cl.QueryNodes(fields=["name", "pip", "sip",
427 "pinst_list", "sinst_list",
428 "master_candidate", "drained", "offline",
429 "master_capable", "vm_capable"],
430 names=args, use_locking=False)
432 for (name, primary_ip, secondary_ip, pinst, sinst,
433 is_mc, drained, offline, master_capable, vm_capable) in result:
434 ToStdout("Node name: %s", name)
435 ToStdout(" primary ip: %s", primary_ip)
436 ToStdout(" secondary ip: %s", secondary_ip)
437 ToStdout(" master candidate: %s", is_mc)
438 ToStdout(" drained: %s", drained)
439 ToStdout(" offline: %s", offline)
440 ToStdout(" master_capable: %s", master_capable)
441 ToStdout(" vm_capable: %s", vm_capable)
444 ToStdout(" primary for instances:")
445 for iname in utils.NiceSort(pinst):
446 ToStdout(" - %s", iname)
448 ToStdout(" primary for no instances")
450 ToStdout(" secondary for instances:")
451 for iname in utils.NiceSort(sinst):
452 ToStdout(" - %s", iname)
454 ToStdout(" secondary for no instances")
459 def RemoveNode(opts, args):
460 """Remove a node from the cluster.
462 @param opts: the command line options selected by the user
464 @param args: should contain only one element, the name of
465 the node to be removed
467 @return: the desired exit code
470 op = opcodes.OpRemoveNode(node_name=args[0])
471 SubmitOpCode(op, opts=opts)
475 def PowercycleNode(opts, args):
476 """Remove a node from the cluster.
478 @param opts: the command line options selected by the user
480 @param args: should contain only one element, the name of
481 the node to be removed
483 @return: the desired exit code
487 if (not opts.confirm and
488 not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
491 op = opcodes.OpPowercycleNode(node_name=node, force=opts.force)
492 result = SubmitOpCode(op, opts=opts)
498 def ListVolumes(opts, args):
499 """List logical volumes on node(s).
501 @param opts: the command line options selected by the user
503 @param args: should either be an empty list, in which case
504 we list data for all nodes, or contain a list of nodes
505 to display data only for those
507 @return: the desired exit code
510 selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
512 op = opcodes.OpQueryNodeVolumes(nodes=args, output_fields=selected_fields)
513 output = SubmitOpCode(op, opts=opts)
515 if not opts.no_headers:
516 headers = {"node": "Node", "phys": "PhysDev",
517 "vg": "VG", "name": "Name",
518 "size": "Size", "instance": "Instance"}
522 unitfields = ["size"]
526 data = GenerateTable(separator=opts.separator, headers=headers,
527 fields=selected_fields, unitfields=unitfields,
528 numfields=numfields, data=output, units=opts.units)
536 def ListStorage(opts, args):
537 """List physical volumes on node(s).
539 @param opts: the command line options selected by the user
541 @param args: should either be an empty list, in which case
542 we list data for all nodes, or contain a list of nodes
543 to display data only for those
545 @return: the desired exit code
548 # TODO: Default to ST_FILE if LVM is disabled on the cluster
549 if opts.user_storage_type is None:
550 opts.user_storage_type = constants.ST_LVM_PV
552 storage_type = ConvertStorageType(opts.user_storage_type)
554 selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
556 op = opcodes.OpQueryNodeStorage(nodes=args,
557 storage_type=storage_type,
558 output_fields=selected_fields)
559 output = SubmitOpCode(op, opts=opts)
561 if not opts.no_headers:
563 constants.SF_NODE: "Node",
564 constants.SF_TYPE: "Type",
565 constants.SF_NAME: "Name",
566 constants.SF_SIZE: "Size",
567 constants.SF_USED: "Used",
568 constants.SF_FREE: "Free",
569 constants.SF_ALLOCATABLE: "Allocatable",
574 unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
575 numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
577 # change raw values to nicer strings
579 for idx, field in enumerate(selected_fields):
581 if field == constants.SF_ALLOCATABLE:
588 data = GenerateTable(separator=opts.separator, headers=headers,
589 fields=selected_fields, unitfields=unitfields,
590 numfields=numfields, data=output, units=opts.units)
598 def ModifyStorage(opts, args):
599 """Modify storage volume on a node.
601 @param opts: the command line options selected by the user
603 @param args: should contain 3 items: node name, storage type and volume name
605 @return: the desired exit code
608 (node_name, user_storage_type, volume_name) = args
610 storage_type = ConvertStorageType(user_storage_type)
614 if opts.allocatable is not None:
615 changes[constants.SF_ALLOCATABLE] = opts.allocatable
618 op = opcodes.OpModifyNodeStorage(node_name=node_name,
619 storage_type=storage_type,
622 SubmitOpCode(op, opts=opts)
624 ToStderr("No changes to perform, exiting.")
627 def RepairStorage(opts, args):
628 """Repairs a storage volume on a node.
630 @param opts: the command line options selected by the user
632 @param args: should contain 3 items: node name, storage type and volume name
634 @return: the desired exit code
637 (node_name, user_storage_type, volume_name) = args
639 storage_type = ConvertStorageType(user_storage_type)
641 op = opcodes.OpRepairNodeStorage(node_name=node_name,
642 storage_type=storage_type,
644 ignore_consistency=opts.ignore_consistency)
645 SubmitOpCode(op, opts=opts)
648 def SetNodeParams(opts, args):
651 @param opts: the command line options selected by the user
653 @param args: should contain only one element, the node name
655 @return: the desired exit code
658 all_changes = [opts.master_candidate, opts.drained, opts.offline,
659 opts.master_capable, opts.vm_capable]
660 if all_changes.count(None) == len(all_changes):
661 ToStderr("Please give at least one of the parameters.")
664 op = opcodes.OpSetNodeParams(node_name=args[0],
665 master_candidate=opts.master_candidate,
666 offline=opts.offline,
667 drained=opts.drained,
668 master_capable=opts.master_capable,
669 vm_capable=opts.vm_capable,
671 auto_promote=opts.auto_promote)
673 # even if here we process the result, we allow submit only
674 result = SubmitOrSend(op, opts)
677 ToStdout("Modified node %s", args[0])
678 for param, data in result:
679 ToStdout(" - %-5s -> %s", param, data)
685 AddNode, [ArgHost(min=1, max=1)],
686 [SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT, NONODE_SETUP_OPT,
687 VERBOSE_OPT, NODEGROUP_OPT, PRIORITY_OPT, CAPAB_MASTER_OPT,
689 "[-s ip] [--readd] [--no-ssh-key-check] [--no-node-setup] [--verbose] "
691 "Add a node to the cluster"),
693 EvacuateNode, [ArgNode(min=1)],
694 [FORCE_OPT, IALLOCATOR_OPT, NEW_SECONDARY_OPT, EARLY_RELEASE_OPT,
696 "[-f] {-I <iallocator> | -n <dst>} <node>",
697 "Relocate the secondary instances from a node"
698 " to other nodes (only for instances with drbd disk template)"),
700 FailoverNode, ARGS_ONE_NODE, [FORCE_OPT, IGNORE_CONSIST_OPT, PRIORITY_OPT],
702 "Stops the primary instances on a node and start them on their"
703 " secondary node (only for instances with drbd disk template)"),
705 MigrateNode, ARGS_ONE_NODE,
706 [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT, PRIORITY_OPT],
708 "Migrate all the primary instance on a node away from it"
709 " (only for instances of type drbd)"),
711 ShowNodeConfig, ARGS_MANY_NODES, [],
712 "[<node_name>...]", "Show information about the node(s)"),
714 ListNodes, ARGS_MANY_NODES,
715 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, SYNC_OPT, ROMAN_OPT],
717 "Lists the nodes in the cluster. The available fields are (see the man"
718 " page for details): %s. The default field list is (in order): %s." %
719 (utils.CommaJoin(_LIST_HEADERS), utils.CommaJoin(_LIST_DEF_FIELDS))),
721 SetNodeParams, ARGS_ONE_NODE,
722 [FORCE_OPT, SUBMIT_OPT, MC_OPT, DRAINED_OPT, OFFLINE_OPT,
723 CAPAB_MASTER_OPT, CAPAB_VM_OPT,
724 AUTO_PROMOTE_OPT, DRY_RUN_OPT, PRIORITY_OPT],
725 "<node_name>", "Alters the parameters of a node"),
727 PowercycleNode, ARGS_ONE_NODE,
728 [FORCE_OPT, CONFIRM_OPT, DRY_RUN_OPT, PRIORITY_OPT],
729 "<node_name>", "Tries to forcefully powercycle a node"),
731 RemoveNode, ARGS_ONE_NODE, [DRY_RUN_OPT, PRIORITY_OPT],
732 "<node_name>", "Removes a node from the cluster"),
734 ListVolumes, [ArgNode()],
735 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, PRIORITY_OPT],
736 "[<node_name>...]", "List logical volumes on node(s)"),
738 ListStorage, ARGS_MANY_NODES,
739 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, _STORAGE_TYPE_OPT,
741 "[<node_name>...]", "List physical volumes on node(s). The available"
742 " fields are (see the man page for details): %s." %
743 (utils.CommaJoin(_LIST_STOR_HEADERS))),
746 [ArgNode(min=1, max=1),
747 ArgChoice(min=1, max=1, choices=_MODIFIABLE_STORAGE_TYPES),
748 ArgFile(min=1, max=1)],
749 [ALLOCATABLE_OPT, DRY_RUN_OPT, PRIORITY_OPT],
750 "<node_name> <storage_type> <name>", "Modify storage volume on a node"),
753 [ArgNode(min=1, max=1),
754 ArgChoice(min=1, max=1, choices=_REPAIRABLE_STORAGE_TYPES),
755 ArgFile(min=1, max=1)],
756 [IGNORE_CONSIST_OPT, DRY_RUN_OPT, PRIORITY_OPT],
757 "<node_name> <storage_type> <name>",
758 "Repairs a storage volume on a node"),
760 ListTags, ARGS_ONE_NODE, [],
761 "<node_name>", "List the tags of the given node"),
763 AddTags, [ArgNode(min=1, max=1), ArgUnknown()], [TAG_SRC_OPT, PRIORITY_OPT],
764 "<node_name> tag...", "Add tags to the given node"),
766 RemoveTags, [ArgNode(min=1, max=1), ArgUnknown()],
767 [TAG_SRC_OPT, PRIORITY_OPT],
768 "<node_name> tag...", "Remove tags from the given node"),
773 return GenericMain(commands, override={"tag_type": constants.TAG_NODE})