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 opcodes
33 from ganeti import utils
34 from ganeti import constants
35 from ganeti import compat
36 from ganeti import errors
37 from ganeti import bootstrap
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 def ConvertStorageType(user_storage_type):
120 """Converts a user storage type to its internal name.
124 return _USER_STORAGE_TYPE[user_storage_type]
126 raise errors.OpPrereqError("Unknown storage type: %s" % user_storage_type,
131 def AddNode(opts, args):
132 """Add a node to the cluster.
134 @param opts: the command line options selected by the user
136 @param args: should contain only one element, the new node name
138 @return: the desired exit code
142 dns_data = netutils.GetHostInfo(netutils.HostInfo.NormalizeName(args[0]))
147 output = cl.QueryNodes(names=[node], fields=['name', 'sip'],
149 node_exists, sip = output[0]
150 except (errors.OpPrereqError, errors.OpExecError):
156 ToStderr("Node %s not in the cluster"
157 " - please retry without '--readd'", node)
161 ToStderr("Node %s already in the cluster (as %s)"
162 " - please retry with '--readd'", node, node_exists)
164 sip = opts.secondary_ip
166 # read the cluster name from the master
167 output = cl.QueryConfigValues(['cluster_name'])
168 cluster_name = output[0]
171 ToStderr("-- WARNING -- \n"
172 "Performing this operation is going to replace the ssh daemon"
174 "on the target machine (%s) with the ones of the"
176 "and grant full intra-cluster ssh root access to/from it\n", node)
178 bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
180 op = opcodes.OpAddNode(node_name=args[0], secondary_ip=sip,
182 SubmitOpCode(op, opts=opts)
185 def ListNodes(opts, args):
186 """List nodes and their properties.
188 @param opts: the command line options selected by the user
190 @param args: should be an empty list
192 @return: the desired exit code
195 selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
197 output = GetClient().QueryNodes(args, selected_fields, opts.do_locking)
199 if not opts.no_headers:
200 headers = _LIST_HEADERS
204 unitfields = ["dtotal", "dfree", "mtotal", "mnode", "mfree"]
206 numfields = ["dtotal", "dfree",
207 "mtotal", "mnode", "mfree",
208 "pinst_cnt", "sinst_cnt",
209 "ctotal", "serial_no"]
211 list_type_fields = ("pinst_list", "sinst_list", "tags")
212 # change raw values to nicer strings
214 for idx, field in enumerate(selected_fields):
216 if field in list_type_fields:
218 elif field in ('master', 'master_candidate', 'offline', 'drained'):
223 elif field == "ctime" or field == "mtime":
224 val = utils.FormatTime(val)
227 elif opts.roman_integers and isinstance(val, int):
228 val = compat.TryToRoman(val)
231 data = GenerateTable(separator=opts.separator, headers=headers,
232 fields=selected_fields, unitfields=unitfields,
233 numfields=numfields, data=output, units=opts.units)
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 names=args, use_locking=False)
393 for (name, primary_ip, secondary_ip, pinst, sinst,
394 is_mc, drained, offline) in result:
395 ToStdout("Node name: %s", name)
396 ToStdout(" primary ip: %s", primary_ip)
397 ToStdout(" secondary ip: %s", secondary_ip)
398 ToStdout(" master candidate: %s", is_mc)
399 ToStdout(" drained: %s", drained)
400 ToStdout(" offline: %s", offline)
402 ToStdout(" primary for instances:")
403 for iname in utils.NiceSort(pinst):
404 ToStdout(" - %s", iname)
406 ToStdout(" primary for no instances")
408 ToStdout(" secondary for instances:")
409 for iname in utils.NiceSort(sinst):
410 ToStdout(" - %s", iname)
412 ToStdout(" secondary for no instances")
417 def RemoveNode(opts, args):
418 """Remove a node from the cluster.
420 @param opts: the command line options selected by the user
422 @param args: should contain only one element, the name of
423 the node to be removed
425 @return: the desired exit code
428 op = opcodes.OpRemoveNode(node_name=args[0])
429 SubmitOpCode(op, opts=opts)
433 def PowercycleNode(opts, args):
434 """Remove a node from the cluster.
436 @param opts: the command line options selected by the user
438 @param args: should contain only one element, the name of
439 the node to be removed
441 @return: the desired exit code
445 if (not opts.confirm and
446 not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
449 op = opcodes.OpPowercycleNode(node_name=node, force=opts.force)
450 result = SubmitOpCode(op, opts=opts)
456 def ListVolumes(opts, args):
457 """List logical volumes on node(s).
459 @param opts: the command line options selected by the user
461 @param args: should either be an empty list, in which case
462 we list data for all nodes, or contain a list of nodes
463 to display data only for those
465 @return: the desired exit code
468 selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
470 op = opcodes.OpQueryNodeVolumes(nodes=args, output_fields=selected_fields)
471 output = SubmitOpCode(op, opts=opts)
473 if not opts.no_headers:
474 headers = {"node": "Node", "phys": "PhysDev",
475 "vg": "VG", "name": "Name",
476 "size": "Size", "instance": "Instance"}
480 unitfields = ["size"]
484 data = GenerateTable(separator=opts.separator, headers=headers,
485 fields=selected_fields, unitfields=unitfields,
486 numfields=numfields, data=output, units=opts.units)
494 def ListStorage(opts, args):
495 """List physical volumes on node(s).
497 @param opts: the command line options selected by the user
499 @param args: should either be an empty list, in which case
500 we list data for all nodes, or contain a list of nodes
501 to display data only for those
503 @return: the desired exit code
506 # TODO: Default to ST_FILE if LVM is disabled on the cluster
507 if opts.user_storage_type is None:
508 opts.user_storage_type = constants.ST_LVM_PV
510 storage_type = ConvertStorageType(opts.user_storage_type)
512 selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
514 op = opcodes.OpQueryNodeStorage(nodes=args,
515 storage_type=storage_type,
516 output_fields=selected_fields)
517 output = SubmitOpCode(op, opts=opts)
519 if not opts.no_headers:
521 constants.SF_NODE: "Node",
522 constants.SF_TYPE: "Type",
523 constants.SF_NAME: "Name",
524 constants.SF_SIZE: "Size",
525 constants.SF_USED: "Used",
526 constants.SF_FREE: "Free",
527 constants.SF_ALLOCATABLE: "Allocatable",
532 unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
533 numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
535 # change raw values to nicer strings
537 for idx, field in enumerate(selected_fields):
539 if field == constants.SF_ALLOCATABLE:
546 data = GenerateTable(separator=opts.separator, headers=headers,
547 fields=selected_fields, unitfields=unitfields,
548 numfields=numfields, data=output, units=opts.units)
556 def ModifyStorage(opts, args):
557 """Modify storage volume on a node.
559 @param opts: the command line options selected by the user
561 @param args: should contain 3 items: node name, storage type and volume name
563 @return: the desired exit code
566 (node_name, user_storage_type, volume_name) = args
568 storage_type = ConvertStorageType(user_storage_type)
572 if opts.allocatable is not None:
573 changes[constants.SF_ALLOCATABLE] = opts.allocatable
576 op = opcodes.OpModifyNodeStorage(node_name=node_name,
577 storage_type=storage_type,
580 SubmitOpCode(op, opts=opts)
582 ToStderr("No changes to perform, exiting.")
585 def RepairStorage(opts, args):
586 """Repairs a storage volume on a node.
588 @param opts: the command line options selected by the user
590 @param args: should contain 3 items: node name, storage type and volume name
592 @return: the desired exit code
595 (node_name, user_storage_type, volume_name) = args
597 storage_type = ConvertStorageType(user_storage_type)
599 op = opcodes.OpRepairNodeStorage(node_name=node_name,
600 storage_type=storage_type,
602 ignore_consistency=opts.ignore_consistency)
603 SubmitOpCode(op, opts=opts)
606 def SetNodeParams(opts, args):
609 @param opts: the command line options selected by the user
611 @param args: should contain only one element, the node name
613 @return: the desired exit code
616 if [opts.master_candidate, opts.drained, opts.offline].count(None) == 3:
617 ToStderr("Please give at least one of the parameters.")
620 op = opcodes.OpSetNodeParams(node_name=args[0],
621 master_candidate=opts.master_candidate,
622 offline=opts.offline,
623 drained=opts.drained,
625 auto_promote=opts.auto_promote)
627 # even if here we process the result, we allow submit only
628 result = SubmitOrSend(op, opts)
631 ToStdout("Modified node %s", args[0])
632 for param, data in result:
633 ToStdout(" - %-5s -> %s", param, data)
639 AddNode, [ArgHost(min=1, max=1)],
640 [SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT],
641 "[-s ip] [--readd] [--no-ssh-key-check] <node_name>",
642 "Add a node to the cluster"),
644 EvacuateNode, [ArgNode(min=1)],
645 [FORCE_OPT, IALLOCATOR_OPT, NEW_SECONDARY_OPT, EARLY_RELEASE_OPT],
646 "[-f] {-I <iallocator> | -n <dst>} <node>",
647 "Relocate the secondary instances from a node"
648 " to other nodes (only for instances with drbd disk template)"),
650 FailoverNode, ARGS_ONE_NODE, [FORCE_OPT, IGNORE_CONSIST_OPT],
652 "Stops the primary instances on a node and start them on their"
653 " secondary node (only for instances with drbd disk template)"),
655 MigrateNode, ARGS_ONE_NODE, [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT],
657 "Migrate all the primary instance on a node away from it"
658 " (only for instances of type drbd)"),
660 ShowNodeConfig, ARGS_MANY_NODES, [],
661 "[<node_name>...]", "Show information about the node(s)"),
663 ListNodes, ARGS_MANY_NODES,
664 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, SYNC_OPT, ROMAN_OPT],
666 "Lists the nodes in the cluster. The available fields are (see the man"
667 " page for details): %s. The default field list is (in order): %s." %
668 (utils.CommaJoin(_LIST_HEADERS), utils.CommaJoin(_LIST_DEF_FIELDS))),
670 SetNodeParams, ARGS_ONE_NODE,
671 [FORCE_OPT, SUBMIT_OPT, MC_OPT, DRAINED_OPT, OFFLINE_OPT,
672 AUTO_PROMOTE_OPT, DRY_RUN_OPT],
673 "<node_name>", "Alters the parameters of a node"),
675 PowercycleNode, ARGS_ONE_NODE,
676 [FORCE_OPT, CONFIRM_OPT, DRY_RUN_OPT],
677 "<node_name>", "Tries to forcefully powercycle a node"),
679 RemoveNode, ARGS_ONE_NODE, [DRY_RUN_OPT],
680 "<node_name>", "Removes a node from the cluster"),
682 ListVolumes, [ArgNode()],
683 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
684 "[<node_name>...]", "List logical volumes on node(s)"),
686 ListStorage, ARGS_MANY_NODES,
687 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, _STORAGE_TYPE_OPT],
688 "[<node_name>...]", "List physical volumes on node(s). The available"
689 " fields are (see the man page for details): %s." %
690 (utils.CommaJoin(_LIST_STOR_HEADERS))),
693 [ArgNode(min=1, max=1),
694 ArgChoice(min=1, max=1, choices=_MODIFIABLE_STORAGE_TYPES),
695 ArgFile(min=1, max=1)],
696 [ALLOCATABLE_OPT, DRY_RUN_OPT],
697 "<node_name> <storage_type> <name>", "Modify storage volume on a node"),
700 [ArgNode(min=1, max=1),
701 ArgChoice(min=1, max=1, choices=_REPAIRABLE_STORAGE_TYPES),
702 ArgFile(min=1, max=1)],
703 [IGNORE_CONSIST_OPT, DRY_RUN_OPT],
704 "<node_name> <storage_type> <name>",
705 "Repairs a storage volume on a node"),
707 ListTags, ARGS_ONE_NODE, [],
708 "<node_name>", "List the tags of the given node"),
710 AddTags, [ArgNode(min=1, max=1), ArgUnknown()], [TAG_SRC_OPT],
711 "<node_name> tag...", "Add tags to the given node"),
713 RemoveTags, [ArgNode(min=1, max=1), ArgUnknown()], [TAG_SRC_OPT],
714 "<node_name> tag...", "Remove tags from the given node"),
718 if __name__ == '__main__':
719 sys.exit(GenericMain(commands, override={"tag_type": constants.TAG_NODE}))