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 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 # this should be removed once --non-live is deprecated
364 if not opts.live and opts.migration_mode is not None:
365 raise errors.OpPrereqError("Only one of the --non-live and "
366 "--migration-mode options can be passed",
368 if not opts.live: # --non-live passed
369 mode = constants.HT_MIGRATION_NONLIVE
371 mode = opts.migration_mode
372 op = opcodes.OpMigrateNode(node_name=args[0], mode=mode)
373 SubmitOpCode(op, cl=cl, opts=opts)
376 def ShowNodeConfig(opts, args):
377 """Show node information.
379 @param opts: the command line options selected by the user
381 @param args: should either be an empty list, in which case
382 we show information about all nodes, or should contain
383 a list of nodes to be queried for information
385 @return: the desired exit code
389 result = cl.QueryNodes(fields=["name", "pip", "sip",
390 "pinst_list", "sinst_list",
391 "master_candidate", "drained", "offline"],
392 names=args, use_locking=False)
394 for (name, primary_ip, secondary_ip, pinst, sinst,
395 is_mc, drained, offline) in result:
396 ToStdout("Node name: %s", name)
397 ToStdout(" primary ip: %s", primary_ip)
398 ToStdout(" secondary ip: %s", secondary_ip)
399 ToStdout(" master candidate: %s", is_mc)
400 ToStdout(" drained: %s", drained)
401 ToStdout(" offline: %s", offline)
403 ToStdout(" primary for instances:")
404 for iname in utils.NiceSort(pinst):
405 ToStdout(" - %s", iname)
407 ToStdout(" primary for no instances")
409 ToStdout(" secondary for instances:")
410 for iname in utils.NiceSort(sinst):
411 ToStdout(" - %s", iname)
413 ToStdout(" secondary for no instances")
418 def RemoveNode(opts, args):
419 """Remove a node from the cluster.
421 @param opts: the command line options selected by the user
423 @param args: should contain only one element, the name of
424 the node to be removed
426 @return: the desired exit code
429 op = opcodes.OpRemoveNode(node_name=args[0])
430 SubmitOpCode(op, opts=opts)
434 def PowercycleNode(opts, args):
435 """Remove a node from the cluster.
437 @param opts: the command line options selected by the user
439 @param args: should contain only one element, the name of
440 the node to be removed
442 @return: the desired exit code
446 if (not opts.confirm and
447 not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
450 op = opcodes.OpPowercycleNode(node_name=node, force=opts.force)
451 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 if opts.output is None:
469 selected_fields = ["node", "phys", "vg",
470 "name", "size", "instance"]
472 selected_fields = opts.output.split(",")
474 op = opcodes.OpQueryNodeVolumes(nodes=args, output_fields=selected_fields)
475 output = SubmitOpCode(op, opts=opts)
477 if not opts.no_headers:
478 headers = {"node": "Node", "phys": "PhysDev",
479 "vg": "VG", "name": "Name",
480 "size": "Size", "instance": "Instance"}
484 unitfields = ["size"]
488 data = GenerateTable(separator=opts.separator, headers=headers,
489 fields=selected_fields, unitfields=unitfields,
490 numfields=numfields, data=output, units=opts.units)
498 def ListStorage(opts, args):
499 """List physical 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 # TODO: Default to ST_FILE if LVM is disabled on the cluster
511 if opts.user_storage_type is None:
512 opts.user_storage_type = constants.ST_LVM_PV
514 storage_type = ConvertStorageType(opts.user_storage_type)
516 if opts.output is None:
517 selected_fields = _LIST_STOR_DEF_FIELDS
518 elif opts.output.startswith("+"):
519 selected_fields = _LIST_STOR_DEF_FIELDS + opts.output[1:].split(",")
521 selected_fields = opts.output.split(",")
523 op = opcodes.OpQueryNodeStorage(nodes=args,
524 storage_type=storage_type,
525 output_fields=selected_fields)
526 output = SubmitOpCode(op, opts=opts)
528 if not opts.no_headers:
530 constants.SF_NODE: "Node",
531 constants.SF_TYPE: "Type",
532 constants.SF_NAME: "Name",
533 constants.SF_SIZE: "Size",
534 constants.SF_USED: "Used",
535 constants.SF_FREE: "Free",
536 constants.SF_ALLOCATABLE: "Allocatable",
541 unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
542 numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
544 # change raw values to nicer strings
546 for idx, field in enumerate(selected_fields):
548 if field == constants.SF_ALLOCATABLE:
555 data = GenerateTable(separator=opts.separator, headers=headers,
556 fields=selected_fields, unitfields=unitfields,
557 numfields=numfields, data=output, units=opts.units)
565 def ModifyStorage(opts, args):
566 """Modify storage volume on a node.
568 @param opts: the command line options selected by the user
570 @param args: should contain 3 items: node name, storage type and volume name
572 @return: the desired exit code
575 (node_name, user_storage_type, volume_name) = args
577 storage_type = ConvertStorageType(user_storage_type)
581 if opts.allocatable is not None:
582 changes[constants.SF_ALLOCATABLE] = opts.allocatable
585 op = opcodes.OpModifyNodeStorage(node_name=node_name,
586 storage_type=storage_type,
589 SubmitOpCode(op, opts=opts)
591 ToStderr("No changes to perform, exiting.")
594 def RepairStorage(opts, args):
595 """Repairs a storage volume on a node.
597 @param opts: the command line options selected by the user
599 @param args: should contain 3 items: node name, storage type and volume name
601 @return: the desired exit code
604 (node_name, user_storage_type, volume_name) = args
606 storage_type = ConvertStorageType(user_storage_type)
608 op = opcodes.OpRepairNodeStorage(node_name=node_name,
609 storage_type=storage_type,
611 ignore_consistency=opts.ignore_consistency)
612 SubmitOpCode(op, opts=opts)
615 def SetNodeParams(opts, args):
618 @param opts: the command line options selected by the user
620 @param args: should contain only one element, the node name
622 @return: the desired exit code
625 if [opts.master_candidate, opts.drained, opts.offline].count(None) == 3:
626 ToStderr("Please give at least one of the parameters.")
629 op = opcodes.OpSetNodeParams(node_name=args[0],
630 master_candidate=opts.master_candidate,
631 offline=opts.offline,
632 drained=opts.drained,
634 auto_promote=opts.auto_promote)
636 # even if here we process the result, we allow submit only
637 result = SubmitOrSend(op, opts)
640 ToStdout("Modified node %s", args[0])
641 for param, data in result:
642 ToStdout(" - %-5s -> %s", param, data)
648 AddNode, [ArgHost(min=1, max=1)],
649 [SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT],
650 "[-s ip] [--readd] [--no-ssh-key-check] <node_name>",
651 "Add a node to the cluster"),
653 EvacuateNode, [ArgNode(min=1)],
654 [FORCE_OPT, IALLOCATOR_OPT, NEW_SECONDARY_OPT, EARLY_RELEASE_OPT],
655 "[-f] {-I <iallocator> | -n <dst>} <node>",
656 "Relocate the secondary instances from a node"
657 " to other nodes (only for instances with drbd disk template)"),
659 FailoverNode, ARGS_ONE_NODE, [FORCE_OPT, IGNORE_CONSIST_OPT],
661 "Stops the primary instances on a node and start them on their"
662 " secondary node (only for instances with drbd disk template)"),
664 MigrateNode, ARGS_ONE_NODE, [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT],
666 "Migrate all the primary instance on a node away from it"
667 " (only for instances of type drbd)"),
669 ShowNodeConfig, ARGS_MANY_NODES, [],
670 "[<node_name>...]", "Show information about the node(s)"),
672 ListNodes, ARGS_MANY_NODES,
673 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, SYNC_OPT, ROMAN_OPT],
675 "Lists the nodes in the cluster. The available fields are (see the man"
676 " page for details): %s. The default field list is (in order): %s." %
677 (utils.CommaJoin(_LIST_HEADERS), utils.CommaJoin(_LIST_DEF_FIELDS))),
679 SetNodeParams, ARGS_ONE_NODE,
680 [FORCE_OPT, SUBMIT_OPT, MC_OPT, DRAINED_OPT, OFFLINE_OPT,
682 "<node_name>", "Alters the parameters of a node"),
684 PowercycleNode, ARGS_ONE_NODE,
685 [FORCE_OPT, CONFIRM_OPT],
686 "<node_name>", "Tries to forcefully powercycle a node"),
688 RemoveNode, ARGS_ONE_NODE, [],
689 "<node_name>", "Removes a node from the cluster"),
691 ListVolumes, [ArgNode()],
692 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
693 "[<node_name>...]", "List logical volumes on node(s)"),
695 ListStorage, ARGS_MANY_NODES,
696 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, _STORAGE_TYPE_OPT],
697 "[<node_name>...]", "List physical volumes on node(s). The available"
698 " fields are (see the man page for details): %s." %
699 (utils.CommaJoin(_LIST_STOR_HEADERS))),
702 [ArgNode(min=1, max=1),
703 ArgChoice(min=1, max=1, choices=_MODIFIABLE_STORAGE_TYPES),
704 ArgFile(min=1, max=1)],
706 "<node_name> <storage_type> <name>", "Modify storage volume on a node"),
709 [ArgNode(min=1, max=1),
710 ArgChoice(min=1, max=1, choices=_REPAIRABLE_STORAGE_TYPES),
711 ArgFile(min=1, max=1)],
712 [IGNORE_CONSIST_OPT],
713 "<node_name> <storage_type> <name>",
714 "Repairs a storage volume on a node"),
716 ListTags, ARGS_ONE_NODE, [],
717 "<node_name>", "List the tags of the given node"),
719 AddTags, [ArgNode(min=1, max=1), ArgUnknown()], [TAG_SRC_OPT],
720 "<node_name> tag...", "Add tags to the given node"),
722 RemoveTags, [ArgNode(min=1, max=1), ArgUnknown()], [TAG_SRC_OPT],
723 "<node_name> tag...", "Remove tags from the given node"),
727 if __name__ == '__main__':
728 sys.exit(GenericMain(commands, override={"tag_type": constants.TAG_NODE}))