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)
287 myname = utils.HostInfo().name
289 cluster_name = cl.QueryConfigValues(["cluster_name"])[0]
291 results = GetOnlineNodes(nodes=opts.nodes, cl=cl)
292 results = [name for name in results if name != myname]
294 srun = ssh.SshRunner(cluster_name=cluster_name)
296 if not srun.CopyFileToNode(node, filename):
297 ToStderr("Copy of file %s to node %s failed", filename, node)
302 def RunClusterCommand(opts, args):
303 """Run a command on some nodes.
305 @param opts: the command line options selected by the user
307 @param args: should contain the command to be run and its arguments
309 @return: the desired exit code
314 command = " ".join(args)
316 nodes = GetOnlineNodes(nodes=opts.nodes, cl=cl)
318 cluster_name, master_node = cl.QueryConfigValues(["cluster_name",
321 srun = ssh.SshRunner(cluster_name=cluster_name)
323 # Make sure master node is at list end
324 if master_node in nodes:
325 nodes.remove(master_node)
326 nodes.append(master_node)
329 result = srun.Run(name, "root", command)
330 ToStdout("------------------------------------------------")
331 ToStdout("node: %s", name)
332 ToStdout("%s", result.output)
333 ToStdout("return code = %s", result.exit_code)
338 def VerifyCluster(opts, args):
339 """Verify integrity of cluster, performing various test on nodes.
341 @param opts: the command line options selected by the user
343 @param args: should be an empty list
345 @return: the desired exit code
349 if opts.skip_nplusone_mem:
350 skip_checks.append(constants.VERIFY_NPLUSONE_MEM)
351 op = opcodes.OpVerifyCluster(skip_checks=skip_checks,
352 verbose=opts.verbose,
353 error_codes=opts.error_codes,
354 debug_simulate_errors=opts.simulate_errors)
361 def VerifyDisks(opts, args):
362 """Verify integrity of cluster disks.
364 @param opts: the command line options selected by the user
366 @param args: should be an empty list
368 @return: the desired exit code
371 op = opcodes.OpVerifyDisks()
372 result = SubmitOpCode(op)
373 if not isinstance(result, (list, tuple)) or len(result) != 3:
374 raise errors.ProgrammerError("Unknown result type for OpVerifyDisks")
376 bad_nodes, instances, missing = result
378 retcode = constants.EXIT_SUCCESS
381 for node, text in bad_nodes.items():
382 ToStdout("Error gathering data on node %s: %s",
383 node, utils.SafeEncode(text[-400:]))
385 ToStdout("You need to fix these nodes first before fixing instances")
388 for iname in instances:
391 op = opcodes.OpActivateInstanceDisks(instance_name=iname)
393 ToStdout("Activating disks for instance '%s'", iname)
395 except errors.GenericError, err:
396 nret, msg = FormatError(err)
398 ToStderr("Error activating disks for instance %s: %s", iname, msg)
401 for iname, ival in missing.iteritems():
402 all_missing = utils.all(ival, lambda x: x[0] in bad_nodes)
404 ToStdout("Instance %s cannot be verified as it lives on"
405 " broken nodes", iname)
407 ToStdout("Instance %s has missing logical volumes:", iname)
409 for node, vol in ival:
410 if node in bad_nodes:
411 ToStdout("\tbroken node %s /dev/xenvg/%s", node, vol)
413 ToStdout("\t%s /dev/xenvg/%s", node, vol)
414 ToStdout("You need to run replace_disks for all the above"
415 " instances, if this message persist after fixing nodes.")
421 def RepairDiskSizes(opts, args):
422 """Verify sizes of cluster disks.
424 @param opts: the command line options selected by the user
426 @param args: optional list of instances to restrict check to
428 @return: the desired exit code
431 op = opcodes.OpRepairDiskSizes(instances=args)
436 def MasterFailover(opts, args):
437 """Failover the master node.
439 This command, when run on a non-master node, will cause the current
440 master to cease being master, and the non-master to become new
443 @param opts: the command line options selected by the user
445 @param args: should be an empty list
447 @return: the desired exit code
451 usertext = ("This will perform the failover even if most other nodes"
452 " are down, or if this node is outdated. This is dangerous"
453 " as it can lead to a non-consistent cluster. Check the"
454 " gnt-cluster(8) man page before proceeding. Continue?")
455 if not AskUser(usertext):
458 return bootstrap.MasterFailover(no_voting=opts.no_voting)
461 def SearchTags(opts, args):
462 """Searches the tags on all the cluster.
464 @param opts: the command line options selected by the user
466 @param args: should contain only one element, the tag pattern
468 @return: the desired exit code
471 op = opcodes.OpSearchTags(pattern=args[0])
472 result = SubmitOpCode(op)
475 result = list(result)
477 for path, tag in result:
478 ToStdout("%s %s", path, tag)
481 def SetClusterParams(opts, args):
482 """Modify the cluster.
484 @param opts: the command line options selected by the user
486 @param args: should be an empty list
488 @return: the desired exit code
491 if not (not opts.lvm_storage or opts.vg_name or
492 opts.enabled_hypervisors or opts.hvparams or
493 opts.beparams or opts.nicparams or
494 opts.candidate_pool_size is not None):
495 ToStderr("Please give at least one of the parameters.")
498 vg_name = opts.vg_name
499 if not opts.lvm_storage and opts.vg_name:
500 ToStdout("Options --no-lvm-storage and --vg-name conflict.")
502 elif not opts.lvm_storage:
505 hvlist = opts.enabled_hypervisors
506 if hvlist is not None:
507 hvlist = hvlist.split(",")
509 # a list of (name, dict) we can pass directly to dict() (or [])
510 hvparams = dict(opts.hvparams)
511 for hv, hv_params in hvparams.iteritems():
512 utils.ForceDictType(hv_params, constants.HVS_PARAMETER_TYPES)
514 beparams = opts.beparams
515 utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
517 nicparams = opts.nicparams
518 utils.ForceDictType(nicparams, constants.NICS_PARAMETER_TYPES)
520 op = opcodes.OpSetClusterParams(vg_name=vg_name,
521 enabled_hypervisors=hvlist,
525 candidate_pool_size=opts.candidate_pool_size)
530 def QueueOps(opts, args):
533 @param opts: the command line options selected by the user
535 @param args: should contain only one element, the subcommand
537 @return: the desired exit code
542 if command in ("drain", "undrain"):
543 drain_flag = command == "drain"
544 client.SetQueueDrainFlag(drain_flag)
545 elif command == "info":
546 result = client.QueryConfigValues(["drain_flag"])
551 ToStdout("The drain flag is %s" % val)
553 raise errors.OpPrereqError("Command '%s' is not valid." % command)
558 def _ShowWatcherPause(until):
559 if until is None or until < time.time():
560 ToStdout("The watcher is not paused.")
562 ToStdout("The watcher is paused until %s.", time.ctime(until))
565 def WatcherOps(opts, args):
566 """Watcher operations.
568 @param opts: the command line options selected by the user
570 @param args: should contain only one element, the subcommand
572 @return: the desired exit code
578 if command == "continue":
579 client.SetWatcherPause(None)
580 ToStdout("The watcher is no longer paused.")
582 elif command == "pause":
584 raise errors.OpPrereqError("Missing pause duration")
586 result = client.SetWatcherPause(time.time() + ParseTimespec(args[1]))
587 _ShowWatcherPause(result)
589 elif command == "info":
590 result = client.QueryConfigValues(["watcher_pause"])
591 _ShowWatcherPause(result)
594 raise errors.OpPrereqError("Command '%s' is not valid." % command)
601 InitCluster, [ArgHost(min=1, max=1)],
602 [BACKEND_OPT, CP_SIZE_OPT, ENABLED_HV_OPT, GLOBAL_FILEDIR_OPT,
603 HVLIST_OPT, MAC_PREFIX_OPT, MASTER_NETDEV_OPT, NIC_PARAMS_OPT,
604 NOLVM_STORAGE_OPT, NOMODIFY_ETCHOSTS_OPT, NOMODIFY_SSH_SETUP_OPT,
605 SECONDARY_IP_OPT, VG_NAME_OPT],
606 "[opts...] <cluster_name>", "Initialises a new cluster configuration"),
608 DestroyCluster, ARGS_NONE, [YES_DOIT_OPT],
609 "", "Destroy cluster"),
611 RenameCluster, [ArgHost(min=1, max=1)],
614 "Renames the cluster"),
616 RedistributeConfig, ARGS_NONE, [SUBMIT_OPT],
617 "", "Forces a push of the configuration file and ssconf files"
618 " to the nodes in the cluster"),
620 VerifyCluster, ARGS_NONE,
621 [VERBOSE_OPT, DEBUG_SIMERR_OPT, ERROR_CODES_OPT, NONPLUS1_OPT],
622 "", "Does a check on the cluster configuration"),
624 VerifyDisks, ARGS_NONE, [],
625 "", "Does a check on the cluster disk status"),
626 'repair-disk-sizes': (
627 RepairDiskSizes, ARGS_MANY_INSTANCES, [],
628 "", "Updates mismatches in recorded disk sizes"),
630 MasterFailover, ARGS_NONE, [NOVOTING_OPT],
631 "", "Makes the current node the master"),
633 ShowClusterVersion, ARGS_NONE, [],
634 "", "Shows the cluster version"),
636 ShowClusterMaster, ARGS_NONE, [],
637 "", "Shows the cluster master"),
639 ClusterCopyFile, [ArgFile(min=1, max=1)],
641 "[-n node...] <filename>", "Copies a file to all (or only some) nodes"),
643 RunClusterCommand, [ArgCommand(min=1)],
645 "[-n node...] <command>", "Runs a command on all (or only some) nodes"),
647 ShowClusterConfig, ARGS_NONE, [],
648 "", "Show cluster configuration"),
650 ListTags, ARGS_NONE, [], "", "List the tags of the cluster"),
652 AddTags, [ArgUnknown()], [TAG_SRC_OPT],
653 "tag...", "Add tags to the cluster"),
655 RemoveTags, [ArgUnknown()], [TAG_SRC_OPT],
656 "tag...", "Remove tags from the cluster"),
658 SearchTags, [ArgUnknown(min=1, max=1)],
659 [], "", "Searches the tags on all objects on"
660 " the cluster for a given pattern (regex)"),
663 [ArgChoice(min=1, max=1, choices=["drain", "undrain", "info"])],
664 [], "drain|undrain|info", "Change queue properties"),
667 [ArgChoice(min=1, max=1, choices=["pause", "continue", "info"]),
668 ArgSuggest(min=0, max=1, choices=["30m", "1h", "4h"])],
670 "{pause <timespec>|continue|info}", "Change watcher properties"),
672 SetClusterParams, ARGS_NONE,
673 [BACKEND_OPT, CP_SIZE_OPT, ENABLED_HV_OPT, HVLIST_OPT,
674 NIC_PARAMS_OPT, NOLVM_STORAGE_OPT, VG_NAME_OPT],
676 "Alters the parameters of the cluster"),
679 if __name__ == '__main__':
680 sys.exit(GenericMain(commands, override={"tag_type": constants.TAG_CLUSTER}))