4 # Copyright (C) 2006, 2007, 2008 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 dns_data = netutils.GetHostInfo(netutils.HostInfo.NormalizeName(args[0]))
143 output = cl.QueryNodes(names=[node], fields=['name', 'sip'],
145 node_exists, sip = output[0]
146 except (errors.OpPrereqError, errors.OpExecError):
152 ToStderr("Node %s not in the cluster"
153 " - please retry without '--readd'", node)
157 ToStderr("Node %s already in the cluster (as %s)"
158 " - please retry with '--readd'", node, node_exists)
160 sip = opts.secondary_ip
162 # read the cluster name from the master
163 output = cl.QueryConfigValues(['cluster_name'])
164 cluster_name = output[0]
167 ToStderr("-- WARNING -- \n"
168 "Performing this operation is going to replace the ssh daemon"
170 "on the target machine (%s) with the ones of the"
172 "and grant full intra-cluster ssh root access to/from it\n", node)
174 bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
176 op = opcodes.OpAddNode(node_name=args[0], secondary_ip=sip,
178 SubmitOpCode(op, opts=opts)
181 def ListNodes(opts, args):
182 """List nodes and their properties.
184 @param opts: the command line options selected by the user
186 @param args: should be an empty list
188 @return: the desired exit code
191 if opts.output is None:
192 selected_fields = _LIST_DEF_FIELDS
193 elif opts.output.startswith("+"):
194 selected_fields = _LIST_DEF_FIELDS + opts.output[1:].split(",")
196 selected_fields = opts.output.split(",")
198 output = GetClient().QueryNodes(args, selected_fields, opts.do_locking)
200 if not opts.no_headers:
201 headers = _LIST_HEADERS
205 unitfields = ["dtotal", "dfree", "mtotal", "mnode", "mfree"]
207 numfields = ["dtotal", "dfree",
208 "mtotal", "mnode", "mfree",
209 "pinst_cnt", "sinst_cnt",
210 "ctotal", "serial_no"]
212 list_type_fields = ("pinst_list", "sinst_list", "tags")
213 # change raw values to nicer strings
215 for idx, field in enumerate(selected_fields):
217 if field in list_type_fields:
219 elif field in ('master', 'master_candidate', 'offline', 'drained'):
224 elif field == "ctime" or field == "mtime":
225 val = utils.FormatTime(val)
228 elif opts.roman_integers and isinstance(val, int):
229 val = compat.TryToRoman(val)
232 data = GenerateTable(separator=opts.separator, headers=headers,
233 fields=selected_fields, unitfields=unitfields,
234 numfields=numfields, data=output, units=opts.units)
241 def EvacuateNode(opts, args):
242 """Relocate all secondary instance from a node.
244 @param opts: the command line options selected by the user
246 @param args: should be an empty list
248 @return: the desired exit code
254 dst_node = opts.dst_node
255 iallocator = opts.iallocator
257 op = opcodes.OpNodeEvacuationStrategy(nodes=args,
258 iallocator=iallocator,
259 remote_node=dst_node)
261 result = SubmitOpCode(op, cl=cl, opts=opts)
263 # no instances to migrate
264 ToStderr("No secondary instances on node(s) %s, exiting.",
265 utils.CommaJoin(args))
266 return constants.EXIT_SUCCESS
268 if not force and not AskUser("Relocate instance(s) %s from node(s) %s?" %
269 (",".join("'%s'" % name[0] for name in result),
270 utils.CommaJoin(args))):
271 return constants.EXIT_CONFIRMATION
273 jex = JobExecutor(cl=cl, opts=opts)
277 ToStdout("Will relocate instance %s to node %s", iname, node)
278 op = opcodes.OpReplaceDisks(instance_name=iname,
279 remote_node=node, disks=[],
280 mode=constants.REPLACE_DISK_CHG,
281 early_release=opts.early_release)
282 jex.QueueJob(iname, op)
283 results = jex.GetResults()
284 bad_cnt = len([row for row in results if not row[0]])
286 ToStdout("All %d instance(s) failed over successfully.", len(results))
287 rcode = constants.EXIT_SUCCESS
289 ToStdout("There were errors during the failover:\n"
290 "%d error(s) out of %d instance(s).", bad_cnt, len(results))
291 rcode = constants.EXIT_FAILURE
295 def FailoverNode(opts, args):
296 """Failover all primary instance on a node.
298 @param opts: the command line options selected by the user
300 @param args: should be an empty list
302 @return: the desired exit code
307 selected_fields = ["name", "pinst_list"]
309 # these fields are static data anyway, so it doesn't matter, but
310 # locking=True should be safer
311 result = cl.QueryNodes(names=args, fields=selected_fields,
313 node, pinst = result[0]
316 ToStderr("No primary instances on node %s, exiting.", node)
319 pinst = utils.NiceSort(pinst)
323 if not force and not AskUser("Fail over instance(s) %s?" %
324 (",".join("'%s'" % name for name in pinst))):
327 jex = JobExecutor(cl=cl, opts=opts)
329 op = opcodes.OpFailoverInstance(instance_name=iname,
330 ignore_consistency=opts.ignore_consistency)
331 jex.QueueJob(iname, op)
332 results = jex.GetResults()
333 bad_cnt = len([row for row in results if not row[0]])
335 ToStdout("All %d instance(s) failed over successfully.", len(results))
337 ToStdout("There were errors during the failover:\n"
338 "%d error(s) out of %d instance(s).", bad_cnt, len(results))
342 def MigrateNode(opts, args):
343 """Migrate all primary instance on a node.
348 selected_fields = ["name", "pinst_list"]
350 result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
351 node, pinst = result[0]
354 ToStdout("No primary instances on node %s, exiting." % node)
357 pinst = utils.NiceSort(pinst)
359 if not force and not AskUser("Migrate instance(s) %s?" %
360 (",".join("'%s'" % name for name in pinst))):
363 op = opcodes.OpMigrateNode(node_name=args[0], live=opts.live)
364 SubmitOpCode(op, cl=cl, opts=opts)
367 def ShowNodeConfig(opts, args):
368 """Show node information.
370 @param opts: the command line options selected by the user
372 @param args: should either be an empty list, in which case
373 we show information about all nodes, or should contain
374 a list of nodes to be queried for information
376 @return: the desired exit code
380 result = cl.QueryNodes(fields=["name", "pip", "sip",
381 "pinst_list", "sinst_list",
382 "master_candidate", "drained", "offline"],
383 names=args, use_locking=False)
385 for (name, primary_ip, secondary_ip, pinst, sinst,
386 is_mc, drained, offline) in result:
387 ToStdout("Node name: %s", name)
388 ToStdout(" primary ip: %s", primary_ip)
389 ToStdout(" secondary ip: %s", secondary_ip)
390 ToStdout(" master candidate: %s", is_mc)
391 ToStdout(" drained: %s", drained)
392 ToStdout(" offline: %s", offline)
394 ToStdout(" primary for instances:")
395 for iname in utils.NiceSort(pinst):
396 ToStdout(" - %s", iname)
398 ToStdout(" primary for no instances")
400 ToStdout(" secondary for instances:")
401 for iname in utils.NiceSort(sinst):
402 ToStdout(" - %s", iname)
404 ToStdout(" secondary for no instances")
409 def RemoveNode(opts, args):
410 """Remove a node from the cluster.
412 @param opts: the command line options selected by the user
414 @param args: should contain only one element, the name of
415 the node to be removed
417 @return: the desired exit code
420 op = opcodes.OpRemoveNode(node_name=args[0])
421 SubmitOpCode(op, opts=opts)
425 def PowercycleNode(opts, args):
426 """Remove a node from the cluster.
428 @param opts: the command line options selected by the user
430 @param args: should contain only one element, the name of
431 the node to be removed
433 @return: the desired exit code
437 if (not opts.confirm and
438 not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
441 op = opcodes.OpPowercycleNode(node_name=node, force=opts.force)
442 result = SubmitOpCode(op, opts=opts)
447 def ListVolumes(opts, args):
448 """List logical volumes on node(s).
450 @param opts: the command line options selected by the user
452 @param args: should either be an empty list, in which case
453 we list data for all nodes, or contain a list of nodes
454 to display data only for those
456 @return: the desired exit code
459 if opts.output is None:
460 selected_fields = ["node", "phys", "vg",
461 "name", "size", "instance"]
463 selected_fields = opts.output.split(",")
465 op = opcodes.OpQueryNodeVolumes(nodes=args, output_fields=selected_fields)
466 output = SubmitOpCode(op, opts=opts)
468 if not opts.no_headers:
469 headers = {"node": "Node", "phys": "PhysDev",
470 "vg": "VG", "name": "Name",
471 "size": "Size", "instance": "Instance"}
475 unitfields = ["size"]
479 data = GenerateTable(separator=opts.separator, headers=headers,
480 fields=selected_fields, unitfields=unitfields,
481 numfields=numfields, data=output, units=opts.units)
489 def ListStorage(opts, args):
490 """List physical volumes on node(s).
492 @param opts: the command line options selected by the user
494 @param args: should either be an empty list, in which case
495 we list data for all nodes, or contain a list of nodes
496 to display data only for those
498 @return: the desired exit code
501 # TODO: Default to ST_FILE if LVM is disabled on the cluster
502 if opts.user_storage_type is None:
503 opts.user_storage_type = constants.ST_LVM_PV
505 storage_type = ConvertStorageType(opts.user_storage_type)
507 if opts.output is None:
508 selected_fields = _LIST_STOR_DEF_FIELDS
509 elif opts.output.startswith("+"):
510 selected_fields = _LIST_STOR_DEF_FIELDS + opts.output[1:].split(",")
512 selected_fields = opts.output.split(",")
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],
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,
673 "<node_name>", "Alters the parameters of a node"),
675 PowercycleNode, ARGS_ONE_NODE,
676 [FORCE_OPT, CONFIRM_OPT],
677 "<node_name>", "Tries to forcefully powercycle a node"),
679 RemoveNode, ARGS_ONE_NODE, [],
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)],
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],
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}))