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 list of field for L{ListStorage}
50 _LIST_STOR_DEF_FIELDS = [
57 constants.SF_ALLOCATABLE,
61 #: headers (and full field list for L{ListNodes}
63 "name": "Node", "pinst_cnt": "Pinst", "sinst_cnt": "Sinst",
64 "pinst_list": "PriInstances", "sinst_list": "SecInstances",
65 "pip": "PrimaryIP", "sip": "SecondaryIP",
66 "dtotal": "DTotal", "dfree": "DFree",
67 "mtotal": "MTotal", "mnode": "MNode", "mfree": "MFree",
69 "ctotal": "CTotal", "cnodes": "CNodes", "csockets": "CSockets",
71 "serial_no": "SerialNo",
72 "master_candidate": "MasterC",
74 "offline": "Offline", "drained": "Drained",
76 "ctime": "CTime", "mtime": "MTime", "uuid": "UUID"
80 #: headers (and full field list for L{ListStorage}
81 _LIST_STOR_HEADERS = {
82 constants.SF_NODE: "Node",
83 constants.SF_TYPE: "Type",
84 constants.SF_NAME: "Name",
85 constants.SF_SIZE: "Size",
86 constants.SF_USED: "Used",
87 constants.SF_FREE: "Free",
88 constants.SF_ALLOCATABLE: "Allocatable",
92 #: User-facing storage unit types
93 _USER_STORAGE_TYPE = {
94 constants.ST_FILE: "file",
95 constants.ST_LVM_PV: "lvm-pv",
96 constants.ST_LVM_VG: "lvm-vg",
100 cli_option("-t", "--storage-type",
101 dest="user_storage_type",
102 choices=_USER_STORAGE_TYPE.keys(),
104 metavar="STORAGE_TYPE",
105 help=("Storage type (%s)" %
106 utils.CommaJoin(_USER_STORAGE_TYPE.keys())))
108 _REPAIRABLE_STORAGE_TYPES = \
109 [st for st, so in constants.VALID_STORAGE_OPERATIONS.iteritems()
110 if constants.SO_FIX_CONSISTENCY in so]
112 _MODIFIABLE_STORAGE_TYPES = constants.MODIFIABLE_STORAGE_FIELDS.keys()
115 def ConvertStorageType(user_storage_type):
116 """Converts a user storage type to its internal name.
120 return _USER_STORAGE_TYPE[user_storage_type]
122 raise errors.OpPrereqError("Unknown storage type: %s" % user_storage_type,
127 def AddNode(opts, args):
128 """Add a node to the cluster.
130 @param opts: the command line options selected by the user
132 @param args: should contain only one element, the new node name
134 @return: the desired exit code
138 node = netutils.GetHostname(name=args[0]).name
142 output = cl.QueryNodes(names=[node], fields=['name', 'sip'],
144 node_exists, sip = output[0]
145 except (errors.OpPrereqError, errors.OpExecError):
151 ToStderr("Node %s not in the cluster"
152 " - please retry without '--readd'", node)
156 ToStderr("Node %s already in the cluster (as %s)"
157 " - please retry with '--readd'", node, node_exists)
159 sip = opts.secondary_ip
161 # read the cluster name from the master
162 output = cl.QueryConfigValues(['cluster_name'])
163 cluster_name = output[0]
166 ToStderr("-- WARNING -- \n"
167 "Performing this operation is going to replace the ssh daemon"
169 "on the target machine (%s) with the ones of the"
171 "and grant full intra-cluster ssh root access to/from it\n", node)
173 bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
175 op = opcodes.OpAddNode(node_name=args[0], secondary_ip=sip,
177 SubmitOpCode(op, opts=opts)
180 def ListNodes(opts, args):
181 """List nodes and their properties.
183 @param opts: the command line options selected by the user
185 @param args: should be an empty list
187 @return: the desired exit code
190 if opts.output is None:
191 selected_fields = _LIST_DEF_FIELDS
192 elif opts.output.startswith("+"):
193 selected_fields = _LIST_DEF_FIELDS + opts.output[1:].split(",")
195 selected_fields = opts.output.split(",")
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)
455 def ListVolumes(opts, args):
456 """List logical volumes on node(s).
458 @param opts: the command line options selected by the user
460 @param args: should either be an empty list, in which case
461 we list data for all nodes, or contain a list of nodes
462 to display data only for those
464 @return: the desired exit code
467 if opts.output is None:
468 selected_fields = ["node", "phys", "vg",
469 "name", "size", "instance"]
471 selected_fields = opts.output.split(",")
473 op = opcodes.OpQueryNodeVolumes(nodes=args, output_fields=selected_fields)
474 output = SubmitOpCode(op, opts=opts)
476 if not opts.no_headers:
477 headers = {"node": "Node", "phys": "PhysDev",
478 "vg": "VG", "name": "Name",
479 "size": "Size", "instance": "Instance"}
483 unitfields = ["size"]
487 data = GenerateTable(separator=opts.separator, headers=headers,
488 fields=selected_fields, unitfields=unitfields,
489 numfields=numfields, data=output, units=opts.units)
497 def ListStorage(opts, args):
498 """List physical volumes on node(s).
500 @param opts: the command line options selected by the user
502 @param args: should either be an empty list, in which case
503 we list data for all nodes, or contain a list of nodes
504 to display data only for those
506 @return: the desired exit code
509 # TODO: Default to ST_FILE if LVM is disabled on the cluster
510 if opts.user_storage_type is None:
511 opts.user_storage_type = constants.ST_LVM_PV
513 storage_type = ConvertStorageType(opts.user_storage_type)
515 if opts.output is None:
516 selected_fields = _LIST_STOR_DEF_FIELDS
517 elif opts.output.startswith("+"):
518 selected_fields = _LIST_STOR_DEF_FIELDS + opts.output[1:].split(",")
520 selected_fields = opts.output.split(",")
522 op = opcodes.OpQueryNodeStorage(nodes=args,
523 storage_type=storage_type,
524 output_fields=selected_fields)
525 output = SubmitOpCode(op, opts=opts)
527 if not opts.no_headers:
529 constants.SF_NODE: "Node",
530 constants.SF_TYPE: "Type",
531 constants.SF_NAME: "Name",
532 constants.SF_SIZE: "Size",
533 constants.SF_USED: "Used",
534 constants.SF_FREE: "Free",
535 constants.SF_ALLOCATABLE: "Allocatable",
540 unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
541 numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
543 # change raw values to nicer strings
545 for idx, field in enumerate(selected_fields):
547 if field == constants.SF_ALLOCATABLE:
554 data = GenerateTable(separator=opts.separator, headers=headers,
555 fields=selected_fields, unitfields=unitfields,
556 numfields=numfields, data=output, units=opts.units)
564 def ModifyStorage(opts, args):
565 """Modify storage volume on a node.
567 @param opts: the command line options selected by the user
569 @param args: should contain 3 items: node name, storage type and volume name
571 @return: the desired exit code
574 (node_name, user_storage_type, volume_name) = args
576 storage_type = ConvertStorageType(user_storage_type)
580 if opts.allocatable is not None:
581 changes[constants.SF_ALLOCATABLE] = opts.allocatable
584 op = opcodes.OpModifyNodeStorage(node_name=node_name,
585 storage_type=storage_type,
588 SubmitOpCode(op, opts=opts)
590 ToStderr("No changes to perform, exiting.")
593 def RepairStorage(opts, args):
594 """Repairs a storage volume on a node.
596 @param opts: the command line options selected by the user
598 @param args: should contain 3 items: node name, storage type and volume name
600 @return: the desired exit code
603 (node_name, user_storage_type, volume_name) = args
605 storage_type = ConvertStorageType(user_storage_type)
607 op = opcodes.OpRepairNodeStorage(node_name=node_name,
608 storage_type=storage_type,
610 ignore_consistency=opts.ignore_consistency)
611 SubmitOpCode(op, opts=opts)
614 def SetNodeParams(opts, args):
617 @param opts: the command line options selected by the user
619 @param args: should contain only one element, the node name
621 @return: the desired exit code
624 if [opts.master_candidate, opts.drained, opts.offline].count(None) == 3:
625 ToStderr("Please give at least one of the parameters.")
628 op = opcodes.OpSetNodeParams(node_name=args[0],
629 master_candidate=opts.master_candidate,
630 offline=opts.offline,
631 drained=opts.drained,
633 auto_promote=opts.auto_promote)
635 # even if here we process the result, we allow submit only
636 result = SubmitOrSend(op, opts)
639 ToStdout("Modified node %s", args[0])
640 for param, data in result:
641 ToStdout(" - %-5s -> %s", param, data)
647 AddNode, [ArgHost(min=1, max=1)],
648 [SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT],
649 "[-s ip] [--readd] [--no-ssh-key-check] <node_name>",
650 "Add a node to the cluster"),
652 EvacuateNode, [ArgNode(min=1)],
653 [FORCE_OPT, IALLOCATOR_OPT, NEW_SECONDARY_OPT, EARLY_RELEASE_OPT],
654 "[-f] {-I <iallocator> | -n <dst>} <node>",
655 "Relocate the secondary instances from a node"
656 " to other nodes (only for instances with drbd disk template)"),
658 FailoverNode, ARGS_ONE_NODE, [FORCE_OPT, IGNORE_CONSIST_OPT],
660 "Stops the primary instances on a node and start them on their"
661 " secondary node (only for instances with drbd disk template)"),
663 MigrateNode, ARGS_ONE_NODE, [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT],
665 "Migrate all the primary instance on a node away from it"
666 " (only for instances of type drbd)"),
668 ShowNodeConfig, ARGS_MANY_NODES, [],
669 "[<node_name>...]", "Show information about the node(s)"),
671 ListNodes, ARGS_MANY_NODES,
672 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, SYNC_OPT, ROMAN_OPT],
674 "Lists the nodes in the cluster. The available fields are (see the man"
675 " page for details): %s. The default field list is (in order): %s." %
676 (utils.CommaJoin(_LIST_HEADERS), utils.CommaJoin(_LIST_DEF_FIELDS))),
678 SetNodeParams, ARGS_ONE_NODE,
679 [FORCE_OPT, SUBMIT_OPT, MC_OPT, DRAINED_OPT, OFFLINE_OPT,
681 "<node_name>", "Alters the parameters of a node"),
683 PowercycleNode, ARGS_ONE_NODE,
684 [FORCE_OPT, CONFIRM_OPT],
685 "<node_name>", "Tries to forcefully powercycle a node"),
687 RemoveNode, ARGS_ONE_NODE, [],
688 "<node_name>", "Removes a node from the cluster"),
690 ListVolumes, [ArgNode()],
691 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
692 "[<node_name>...]", "List logical volumes on node(s)"),
694 ListStorage, ARGS_MANY_NODES,
695 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, _STORAGE_TYPE_OPT],
696 "[<node_name>...]", "List physical volumes on node(s). The available"
697 " fields are (see the man page for details): %s." %
698 (utils.CommaJoin(_LIST_STOR_HEADERS))),
701 [ArgNode(min=1, max=1),
702 ArgChoice(min=1, max=1, choices=_MODIFIABLE_STORAGE_TYPES),
703 ArgFile(min=1, max=1)],
705 "<node_name> <storage_type> <name>", "Modify storage volume on a node"),
708 [ArgNode(min=1, max=1),
709 ArgChoice(min=1, max=1, choices=_REPAIRABLE_STORAGE_TYPES),
710 ArgFile(min=1, max=1)],
711 [IGNORE_CONSIST_OPT],
712 "<node_name> <storage_type> <name>",
713 "Repairs a storage volume on a node"),
715 ListTags, ARGS_ONE_NODE, [],
716 "<node_name>", "List the tags of the given node"),
718 AddTags, [ArgNode(min=1, max=1), ArgUnknown()], [TAG_SRC_OPT],
719 "<node_name> tag...", "Add tags to the given node"),
721 RemoveTags, [ArgNode(min=1, max=1), ArgUnknown()], [TAG_SRC_OPT],
722 "<node_name> tag...", "Remove tags from the given node"),
726 if __name__ == '__main__':
727 sys.exit(GenericMain(commands, override={"tag_type": constants.TAG_NODE}))