4 # Copyright (C) 2006, 2007 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
22 # pylint: disable-msg=W0401,W0614
23 # W0401: Wildcard import ganeti.cli
24 # W0614: Unused import %s from wildcard import (since we need cli)
30 from ganeti.cli import *
31 from ganeti import opcodes
32 from ganeti import constants
33 from ganeti import errors
34 from ganeti import utils
35 from ganeti import bootstrap
36 from ganeti import ssh
37 from ganeti import objects
41 def InitCluster(opts, args):
42 """Initialize the cluster.
44 @param opts: the command line options selected by the user
46 @param args: should contain only one element, the desired
49 @return: the desired exit code
52 if not opts.lvm_storage and opts.vg_name:
53 ToStderr("Options --no-lvm-storage and --vg-name conflict.")
56 vg_name = opts.vg_name
57 if opts.lvm_storage and not opts.vg_name:
58 vg_name = constants.DEFAULT_VG
60 hvlist = opts.enabled_hypervisors
62 hvlist = constants.DEFAULT_ENABLED_HYPERVISOR
63 hvlist = hvlist.split(",")
65 hvparams = dict(opts.hvparams)
66 beparams = opts.beparams
67 nicparams = opts.nicparams
69 # prepare beparams dict
70 beparams = objects.FillDict(constants.BEC_DEFAULTS, beparams)
71 utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
73 # prepare nicparams dict
74 nicparams = objects.FillDict(constants.NICC_DEFAULTS, nicparams)
75 utils.ForceDictType(nicparams, constants.NICS_PARAMETER_TYPES)
77 # prepare hvparams dict
78 for hv in constants.HYPER_TYPES:
79 if hv not in hvparams:
81 hvparams[hv] = objects.FillDict(constants.HVC_DEFAULTS[hv], hvparams[hv])
82 utils.ForceDictType(hvparams[hv], constants.HVS_PARAMETER_TYPES)
84 if opts.candidate_pool_size is None:
85 opts.candidate_pool_size = constants.MASTER_POOL_SIZE_DEFAULT
87 if opts.mac_prefix is None:
88 opts.mac_prefix = constants.DEFAULT_MAC_PREFIX
90 bootstrap.InitCluster(cluster_name=args[0],
91 secondary_ip=opts.secondary_ip,
93 mac_prefix=opts.mac_prefix,
94 master_netdev=opts.master_netdev,
95 file_storage_dir=opts.file_storage_dir,
96 enabled_hypervisors=hvlist,
100 candidate_pool_size=opts.candidate_pool_size,
101 modify_etc_hosts=opts.modify_etc_hosts,
102 modify_ssh_setup=opts.modify_ssh_setup,
104 op = opcodes.OpPostInitCluster()
110 def DestroyCluster(opts, args):
111 """Destroy the cluster.
113 @param opts: the command line options selected by the user
115 @param args: should be an empty list
117 @return: the desired exit code
120 if not opts.yes_do_it:
121 ToStderr("Destroying a cluster is irreversible. If you really want"
122 " destroy this cluster, supply the --yes-do-it option.")
125 op = opcodes.OpDestroyCluster()
126 master = SubmitOpCode(op)
127 # if we reached this, the opcode didn't fail; we can proceed to
128 # shutdown all the daemons
129 bootstrap.FinalizeClusterDestroy(master)
133 def RenameCluster(opts, args):
134 """Rename the cluster.
136 @param opts: the command line options selected by the user
138 @param args: should contain only one element, the new cluster name
140 @return: the desired exit code
145 usertext = ("This will rename the cluster to '%s'. If you are connected"
146 " over the network to the cluster name, the operation is very"
147 " dangerous as the IP address will be removed from the node"
148 " and the change may not go through. Continue?") % name
149 if not AskUser(usertext):
152 op = opcodes.OpRenameCluster(name=name)
157 def RedistributeConfig(opts, args):
158 """Forces push of the cluster configuration.
160 @param opts: the command line options selected by the user
162 @param args: empty list
164 @return: the desired exit code
167 op = opcodes.OpRedistributeConfig()
168 SubmitOrSend(op, opts)
172 def ShowClusterVersion(opts, args):
173 """Write version of ganeti software to the standard output.
175 @param opts: the command line options selected by the user
177 @param args: should be an empty list
179 @return: the desired exit code
183 result = cl.QueryClusterInfo()
184 ToStdout("Software version: %s", result["software_version"])
185 ToStdout("Internode protocol: %s", result["protocol_version"])
186 ToStdout("Configuration format: %s", result["config_version"])
187 ToStdout("OS api version: %s", result["os_api_version"])
188 ToStdout("Export interface: %s", result["export_version"])
192 def ShowClusterMaster(opts, args):
193 """Write name of master node to the standard output.
195 @param opts: the command line options selected by the user
197 @param args: should be an empty list
199 @return: the desired exit code
202 master = bootstrap.GetMaster()
206 def _PrintGroupedParams(paramsdict):
207 """Print Grouped parameters (be, nic, disk) by group.
209 @type paramsdict: dict of dicts
210 @param paramsdict: {group: {param: value, ...}, ...}
213 for gr_name, gr_dict in paramsdict.items():
214 ToStdout(" - %s:", gr_name)
215 for item, val in gr_dict.iteritems():
216 ToStdout(" %s: %s", item, val)
218 def ShowClusterConfig(opts, args):
219 """Shows cluster information.
221 @param opts: the command line options selected by the user
223 @param args: should be an empty list
225 @return: the desired exit code
229 result = cl.QueryClusterInfo()
231 ToStdout("Cluster name: %s", result["name"])
232 ToStdout("Cluster UUID: %s", result["uuid"])
234 ToStdout("Creation time: %s", utils.FormatTime(result["ctime"]))
235 ToStdout("Modification time: %s", utils.FormatTime(result["mtime"]))
237 ToStdout("Master node: %s", result["master"])
239 ToStdout("Architecture (this node): %s (%s)",
240 result["architecture"][0], result["architecture"][1])
243 tags = ", ".join(utils.NiceSort(result["tags"]))
247 ToStdout("Tags: %s", tags)
249 ToStdout("Default hypervisor: %s", result["default_hypervisor"])
250 ToStdout("Enabled hypervisors: %s", ", ".join(result["enabled_hypervisors"]))
252 ToStdout("Hypervisor parameters:")
253 _PrintGroupedParams(result["hvparams"])
255 ToStdout("Cluster parameters:")
256 ToStdout(" - candidate pool size: %s", result["candidate_pool_size"])
257 ToStdout(" - master netdev: %s", result["master_netdev"])
258 ToStdout(" - lvm volume group: %s", result["volume_group_name"])
259 ToStdout(" - file storage path: %s", result["file_storage_dir"])
261 ToStdout("Default instance parameters:")
262 _PrintGroupedParams(result["beparams"])
264 ToStdout("Default nic parameters:")
265 _PrintGroupedParams(result["nicparams"])
270 def ClusterCopyFile(opts, args):
271 """Copy a file from master to some nodes.
273 @param opts: the command line options selected by the user
275 @param args: should contain only one element, the path of
276 the file to be copied
278 @return: the desired exit code
282 if not os.path.exists(filename):
283 raise errors.OpPrereqError("No such filename '%s'" % filename,
288 myname = utils.HostInfo().name
290 cluster_name = cl.QueryConfigValues(["cluster_name"])[0]
292 results = GetOnlineNodes(nodes=opts.nodes, cl=cl)
293 results = [name for name in results if name != myname]
295 srun = ssh.SshRunner(cluster_name=cluster_name)
297 if not srun.CopyFileToNode(node, filename):
298 ToStderr("Copy of file %s to node %s failed", filename, node)
303 def RunClusterCommand(opts, args):
304 """Run a command on some nodes.
306 @param opts: the command line options selected by the user
308 @param args: should contain the command to be run and its arguments
310 @return: the desired exit code
315 command = " ".join(args)
317 nodes = GetOnlineNodes(nodes=opts.nodes, cl=cl)
319 cluster_name, master_node = cl.QueryConfigValues(["cluster_name",
322 srun = ssh.SshRunner(cluster_name=cluster_name)
324 # Make sure master node is at list end
325 if master_node in nodes:
326 nodes.remove(master_node)
327 nodes.append(master_node)
330 result = srun.Run(name, "root", command)
331 ToStdout("------------------------------------------------")
332 ToStdout("node: %s", name)
333 ToStdout("%s", result.output)
334 ToStdout("return code = %s", result.exit_code)
339 def VerifyCluster(opts, args):
340 """Verify integrity of cluster, performing various test on nodes.
342 @param opts: the command line options selected by the user
344 @param args: should be an empty list
346 @return: the desired exit code
350 if opts.skip_nplusone_mem:
351 skip_checks.append(constants.VERIFY_NPLUSONE_MEM)
352 op = opcodes.OpVerifyCluster(skip_checks=skip_checks,
353 verbose=opts.verbose,
354 error_codes=opts.error_codes,
355 debug_simulate_errors=opts.simulate_errors)
362 def VerifyDisks(opts, args):
363 """Verify integrity of cluster disks.
365 @param opts: the command line options selected by the user
367 @param args: should be an empty list
369 @return: the desired exit code
372 op = opcodes.OpVerifyDisks()
373 result = SubmitOpCode(op)
374 if not isinstance(result, (list, tuple)) or len(result) != 3:
375 raise errors.ProgrammerError("Unknown result type for OpVerifyDisks")
377 bad_nodes, instances, missing = result
379 retcode = constants.EXIT_SUCCESS
382 for node, text in bad_nodes.items():
383 ToStdout("Error gathering data on node %s: %s",
384 node, utils.SafeEncode(text[-400:]))
386 ToStdout("You need to fix these nodes first before fixing instances")
389 for iname in instances:
392 op = opcodes.OpActivateInstanceDisks(instance_name=iname)
394 ToStdout("Activating disks for instance '%s'", iname)
396 except errors.GenericError, err:
397 nret, msg = FormatError(err)
399 ToStderr("Error activating disks for instance %s: %s", iname, msg)
402 for iname, ival in missing.iteritems():
403 all_missing = utils.all(ival, lambda x: x[0] in bad_nodes)
405 ToStdout("Instance %s cannot be verified as it lives on"
406 " broken nodes", iname)
408 ToStdout("Instance %s has missing logical volumes:", iname)
410 for node, vol in ival:
411 if node in bad_nodes:
412 ToStdout("\tbroken node %s /dev/xenvg/%s", node, vol)
414 ToStdout("\t%s /dev/xenvg/%s", node, vol)
415 ToStdout("You need to run replace_disks for all the above"
416 " instances, if this message persist after fixing nodes.")
422 def RepairDiskSizes(opts, args):
423 """Verify sizes of cluster disks.
425 @param opts: the command line options selected by the user
427 @param args: optional list of instances to restrict check to
429 @return: the desired exit code
432 op = opcodes.OpRepairDiskSizes(instances=args)
437 def MasterFailover(opts, args):
438 """Failover the master node.
440 This command, when run on a non-master node, will cause the current
441 master to cease being master, and the non-master to become new
444 @param opts: the command line options selected by the user
446 @param args: should be an empty list
448 @return: the desired exit code
452 usertext = ("This will perform the failover even if most other nodes"
453 " are down, or if this node is outdated. This is dangerous"
454 " as it can lead to a non-consistent cluster. Check the"
455 " gnt-cluster(8) man page before proceeding. Continue?")
456 if not AskUser(usertext):
459 return bootstrap.MasterFailover(no_voting=opts.no_voting)
462 def SearchTags(opts, args):
463 """Searches the tags on all the cluster.
465 @param opts: the command line options selected by the user
467 @param args: should contain only one element, the tag pattern
469 @return: the desired exit code
472 op = opcodes.OpSearchTags(pattern=args[0])
473 result = SubmitOpCode(op)
476 result = list(result)
478 for path, tag in result:
479 ToStdout("%s %s", path, tag)
482 def SetClusterParams(opts, args):
483 """Modify the cluster.
485 @param opts: the command line options selected by the user
487 @param args: should be an empty list
489 @return: the desired exit code
492 if not (not opts.lvm_storage or opts.vg_name or
493 opts.enabled_hypervisors or opts.hvparams or
494 opts.beparams or opts.nicparams or
495 opts.candidate_pool_size is not None):
496 ToStderr("Please give at least one of the parameters.")
499 vg_name = opts.vg_name
500 if not opts.lvm_storage and opts.vg_name:
501 ToStdout("Options --no-lvm-storage and --vg-name conflict.")
503 elif not opts.lvm_storage:
506 hvlist = opts.enabled_hypervisors
507 if hvlist is not None:
508 hvlist = hvlist.split(",")
510 # a list of (name, dict) we can pass directly to dict() (or [])
511 hvparams = dict(opts.hvparams)
512 for hv, hv_params in hvparams.iteritems():
513 utils.ForceDictType(hv_params, constants.HVS_PARAMETER_TYPES)
515 beparams = opts.beparams
516 utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
518 nicparams = opts.nicparams
519 utils.ForceDictType(nicparams, constants.NICS_PARAMETER_TYPES)
521 op = opcodes.OpSetClusterParams(vg_name=vg_name,
522 enabled_hypervisors=hvlist,
526 candidate_pool_size=opts.candidate_pool_size)
531 def QueueOps(opts, args):
534 @param opts: the command line options selected by the user
536 @param args: should contain only one element, the subcommand
538 @return: the desired exit code
543 if command in ("drain", "undrain"):
544 drain_flag = command == "drain"
545 client.SetQueueDrainFlag(drain_flag)
546 elif command == "info":
547 result = client.QueryConfigValues(["drain_flag"])
552 ToStdout("The drain flag is %s" % val)
554 raise errors.OpPrereqError("Command '%s' is not valid." % command,
560 def _ShowWatcherPause(until):
561 if until is None or until < time.time():
562 ToStdout("The watcher is not paused.")
564 ToStdout("The watcher is paused until %s.", time.ctime(until))
567 def WatcherOps(opts, args):
568 """Watcher operations.
570 @param opts: the command line options selected by the user
572 @param args: should contain only one element, the subcommand
574 @return: the desired exit code
580 if command == "continue":
581 client.SetWatcherPause(None)
582 ToStdout("The watcher is no longer paused.")
584 elif command == "pause":
586 raise errors.OpPrereqError("Missing pause duration", errors.ECODE_INVAL)
588 result = client.SetWatcherPause(time.time() + ParseTimespec(args[1]))
589 _ShowWatcherPause(result)
591 elif command == "info":
592 result = client.QueryConfigValues(["watcher_pause"])
593 _ShowWatcherPause(result)
596 raise errors.OpPrereqError("Command '%s' is not valid." % command,
604 InitCluster, [ArgHost(min=1, max=1)],
605 [BACKEND_OPT, CP_SIZE_OPT, ENABLED_HV_OPT, GLOBAL_FILEDIR_OPT,
606 HVLIST_OPT, MAC_PREFIX_OPT, MASTER_NETDEV_OPT, NIC_PARAMS_OPT,
607 NOLVM_STORAGE_OPT, NOMODIFY_ETCHOSTS_OPT, NOMODIFY_SSH_SETUP_OPT,
608 SECONDARY_IP_OPT, VG_NAME_OPT],
609 "[opts...] <cluster_name>", "Initialises a new cluster configuration"),
611 DestroyCluster, ARGS_NONE, [YES_DOIT_OPT],
612 "", "Destroy cluster"),
614 RenameCluster, [ArgHost(min=1, max=1)],
617 "Renames the cluster"),
619 RedistributeConfig, ARGS_NONE, [SUBMIT_OPT],
620 "", "Forces a push of the configuration file and ssconf files"
621 " to the nodes in the cluster"),
623 VerifyCluster, ARGS_NONE,
624 [VERBOSE_OPT, DEBUG_SIMERR_OPT, ERROR_CODES_OPT, NONPLUS1_OPT],
625 "", "Does a check on the cluster configuration"),
627 VerifyDisks, ARGS_NONE, [],
628 "", "Does a check on the cluster disk status"),
629 'repair-disk-sizes': (
630 RepairDiskSizes, ARGS_MANY_INSTANCES, [],
631 "", "Updates mismatches in recorded disk sizes"),
633 MasterFailover, ARGS_NONE, [NOVOTING_OPT],
634 "", "Makes the current node the master"),
636 ShowClusterVersion, ARGS_NONE, [],
637 "", "Shows the cluster version"),
639 ShowClusterMaster, ARGS_NONE, [],
640 "", "Shows the cluster master"),
642 ClusterCopyFile, [ArgFile(min=1, max=1)],
644 "[-n node...] <filename>", "Copies a file to all (or only some) nodes"),
646 RunClusterCommand, [ArgCommand(min=1)],
648 "[-n node...] <command>", "Runs a command on all (or only some) nodes"),
650 ShowClusterConfig, ARGS_NONE, [],
651 "", "Show cluster configuration"),
653 ListTags, ARGS_NONE, [], "", "List the tags of the cluster"),
655 AddTags, [ArgUnknown()], [TAG_SRC_OPT],
656 "tag...", "Add tags to the cluster"),
658 RemoveTags, [ArgUnknown()], [TAG_SRC_OPT],
659 "tag...", "Remove tags from the cluster"),
661 SearchTags, [ArgUnknown(min=1, max=1)],
662 [], "", "Searches the tags on all objects on"
663 " the cluster for a given pattern (regex)"),
666 [ArgChoice(min=1, max=1, choices=["drain", "undrain", "info"])],
667 [], "drain|undrain|info", "Change queue properties"),
670 [ArgChoice(min=1, max=1, choices=["pause", "continue", "info"]),
671 ArgSuggest(min=0, max=1, choices=["30m", "1h", "4h"])],
673 "{pause <timespec>|continue|info}", "Change watcher properties"),
675 SetClusterParams, ARGS_NONE,
676 [BACKEND_OPT, CP_SIZE_OPT, ENABLED_HV_OPT, HVLIST_OPT,
677 NIC_PARAMS_OPT, NOLVM_STORAGE_OPT, VG_NAME_OPT],
679 "Alters the parameters of the cluster"),
682 if __name__ == '__main__':
683 sys.exit(GenericMain(commands, override={"tag_type": constants.TAG_CLUSTER}))