Adding '--no-ssh-init' option to 'gnt-cluster init'.
[ganeti-local] / scripts / gnt-cluster
1 #!/usr/bin/python
2 #
3
4 # Copyright (C) 2006, 2007 Google Inc.
5 #
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.
10 #
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.
15 #
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
19 # 02110-1301, USA.
20
21
22 # pylint: disable-msg=W0401,W0614
23 # W0401: Wildcard import ganeti.cli
24 # W0614: Unused import %s from wildcard import (since we need cli)
25
26 import sys
27 import os.path
28 import time
29
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
38
39
40 @UsesRPC
41 def InitCluster(opts, args):
42   """Initialize the cluster.
43
44   @param opts: the command line options selected by the user
45   @type args: list
46   @param args: should contain only one element, the desired
47       cluster name
48   @rtype: int
49   @return: the desired exit code
50
51   """
52   if not opts.lvm_storage and opts.vg_name:
53     ToStderr("Options --no-lvm-storage and --vg-name conflict.")
54     return 1
55
56   vg_name = opts.vg_name
57   if opts.lvm_storage and not opts.vg_name:
58     vg_name = constants.DEFAULT_VG
59
60   hvlist = opts.enabled_hypervisors
61   if hvlist is None:
62     hvlist = constants.DEFAULT_ENABLED_HYPERVISOR
63   hvlist = hvlist.split(",")
64
65   hvparams = dict(opts.hvparams)
66   beparams = opts.beparams
67   nicparams = opts.nicparams
68
69   # prepare beparams dict
70   beparams = objects.FillDict(constants.BEC_DEFAULTS, beparams)
71   utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
72
73   # prepare nicparams dict
74   nicparams = objects.FillDict(constants.NICC_DEFAULTS, nicparams)
75   utils.ForceDictType(nicparams, constants.NICS_PARAMETER_TYPES)
76
77   # prepare hvparams dict
78   for hv in constants.HYPER_TYPES:
79     if hv not in hvparams:
80       hvparams[hv] = {}
81     hvparams[hv] = objects.FillDict(constants.HVC_DEFAULTS[hv], hvparams[hv])
82     utils.ForceDictType(hvparams[hv], constants.HVS_PARAMETER_TYPES)
83
84   if opts.candidate_pool_size is None:
85     opts.candidate_pool_size = constants.MASTER_POOL_SIZE_DEFAULT
86
87   if opts.mac_prefix is None:
88     opts.mac_prefix = constants.DEFAULT_MAC_PREFIX
89
90   bootstrap.InitCluster(cluster_name=args[0],
91                         secondary_ip=opts.secondary_ip,
92                         vg_name=vg_name,
93                         mac_prefix=opts.mac_prefix,
94                         master_netdev=opts.master_netdev,
95                         file_storage_dir=opts.file_storage_dir,
96                         enabled_hypervisors=hvlist,
97                         hvparams=hvparams,
98                         beparams=beparams,
99                         nicparams=nicparams,
100                         candidate_pool_size=opts.candidate_pool_size,
101                         modify_etc_hosts=opts.modify_etc_hosts,
102                         modify_ssh_setup=opts.modify_ssh_setup,
103                         )
104   op = opcodes.OpPostInitCluster()
105   SubmitOpCode(op)
106   return 0
107
108
109 @UsesRPC
110 def DestroyCluster(opts, args):
111   """Destroy the cluster.
112
113   @param opts: the command line options selected by the user
114   @type args: list
115   @param args: should be an empty list
116   @rtype: int
117   @return: the desired exit code
118
119   """
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.")
123     return 1
124
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)
130   return 0
131
132
133 def RenameCluster(opts, args):
134   """Rename the cluster.
135
136   @param opts: the command line options selected by the user
137   @type args: list
138   @param args: should contain only one element, the new cluster name
139   @rtype: int
140   @return: the desired exit code
141
142   """
143   name = args[0]
144   if not opts.force:
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):
150       return 1
151
152   op = opcodes.OpRenameCluster(name=name)
153   SubmitOpCode(op)
154   return 0
155
156
157 def RedistributeConfig(opts, args):
158   """Forces push of the cluster configuration.
159
160   @param opts: the command line options selected by the user
161   @type args: list
162   @param args: empty list
163   @rtype: int
164   @return: the desired exit code
165
166   """
167   op = opcodes.OpRedistributeConfig()
168   SubmitOrSend(op, opts)
169   return 0
170
171
172 def ShowClusterVersion(opts, args):
173   """Write version of ganeti software to the standard output.
174
175   @param opts: the command line options selected by the user
176   @type args: list
177   @param args: should be an empty list
178   @rtype: int
179   @return: the desired exit code
180
181   """
182   cl = GetClient()
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"])
189   return 0
190
191
192 def ShowClusterMaster(opts, args):
193   """Write name of master node to the standard output.
194
195   @param opts: the command line options selected by the user
196   @type args: list
197   @param args: should be an empty list
198   @rtype: int
199   @return: the desired exit code
200
201   """
202   master = bootstrap.GetMaster()
203   ToStdout(master)
204   return 0
205
206 def _PrintGroupedParams(paramsdict):
207   """Print Grouped parameters (be, nic, disk) by group.
208
209   @type paramsdict: dict of dicts
210   @param paramsdict: {group: {param: value, ...}, ...}
211
212   """
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)
217
218 def ShowClusterConfig(opts, args):
219   """Shows cluster information.
220
221   @param opts: the command line options selected by the user
222   @type args: list
223   @param args: should be an empty list
224   @rtype: int
225   @return: the desired exit code
226
227   """
228   cl = GetClient()
229   result = cl.QueryClusterInfo()
230
231   ToStdout("Cluster name: %s", result["name"])
232   ToStdout("Cluster UUID: %s", result["uuid"])
233
234   ToStdout("Creation time: %s", utils.FormatTime(result["ctime"]))
235   ToStdout("Modification time: %s", utils.FormatTime(result["mtime"]))
236
237   ToStdout("Master node: %s", result["master"])
238
239   ToStdout("Architecture (this node): %s (%s)",
240            result["architecture"][0], result["architecture"][1])
241
242   if result["tags"]:
243     tags = ", ".join(utils.NiceSort(result["tags"]))
244   else:
245     tags = "(none)"
246
247   ToStdout("Tags: %s", tags)
248
249   ToStdout("Default hypervisor: %s", result["default_hypervisor"])
250   ToStdout("Enabled hypervisors: %s", ", ".join(result["enabled_hypervisors"]))
251
252   ToStdout("Hypervisor parameters:")
253   _PrintGroupedParams(result["hvparams"])
254
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"])
260
261   ToStdout("Default instance parameters:")
262   _PrintGroupedParams(result["beparams"])
263
264   ToStdout("Default nic parameters:")
265   _PrintGroupedParams(result["nicparams"])
266
267   return 0
268
269
270 def ClusterCopyFile(opts, args):
271   """Copy a file from master to some nodes.
272
273   @param opts: the command line options selected by the user
274   @type args: list
275   @param args: should contain only one element, the path of
276       the file to be copied
277   @rtype: int
278   @return: the desired exit code
279
280   """
281   filename = args[0]
282   if not os.path.exists(filename):
283     raise errors.OpPrereqError("No such filename '%s'" % filename)
284
285   cl = GetClient()
286
287   myname = utils.HostInfo().name
288
289   cluster_name = cl.QueryConfigValues(["cluster_name"])[0]
290
291   results = GetOnlineNodes(nodes=opts.nodes, cl=cl)
292   results = [name for name in results if name != myname]
293
294   srun = ssh.SshRunner(cluster_name=cluster_name)
295   for node in results:
296     if not srun.CopyFileToNode(node, filename):
297       ToStderr("Copy of file %s to node %s failed", filename, node)
298
299   return 0
300
301
302 def RunClusterCommand(opts, args):
303   """Run a command on some nodes.
304
305   @param opts: the command line options selected by the user
306   @type args: list
307   @param args: should contain the command to be run and its arguments
308   @rtype: int
309   @return: the desired exit code
310
311   """
312   cl = GetClient()
313
314   command = " ".join(args)
315
316   nodes = GetOnlineNodes(nodes=opts.nodes, cl=cl)
317
318   cluster_name, master_node = cl.QueryConfigValues(["cluster_name",
319                                                     "master_node"])
320
321   srun = ssh.SshRunner(cluster_name=cluster_name)
322
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)
327
328   for name in nodes:
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)
334
335   return 0
336
337
338 def VerifyCluster(opts, args):
339   """Verify integrity of cluster, performing various test on nodes.
340
341   @param opts: the command line options selected by the user
342   @type args: list
343   @param args: should be an empty list
344   @rtype: int
345   @return: the desired exit code
346
347   """
348   skip_checks = []
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)
355   if SubmitOpCode(op):
356     return 0
357   else:
358     return 1
359
360
361 def VerifyDisks(opts, args):
362   """Verify integrity of cluster disks.
363
364   @param opts: the command line options selected by the user
365   @type args: list
366   @param args: should be an empty list
367   @rtype: int
368   @return: the desired exit code
369
370   """
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")
375
376   bad_nodes, instances, missing = result
377
378   retcode = constants.EXIT_SUCCESS
379
380   if bad_nodes:
381     for node, text in bad_nodes.items():
382       ToStdout("Error gathering data on node %s: %s",
383                node, utils.SafeEncode(text[-400:]))
384       retcode |= 1
385       ToStdout("You need to fix these nodes first before fixing instances")
386
387   if instances:
388     for iname in instances:
389       if iname in missing:
390         continue
391       op = opcodes.OpActivateInstanceDisks(instance_name=iname)
392       try:
393         ToStdout("Activating disks for instance '%s'", iname)
394         SubmitOpCode(op)
395       except errors.GenericError, err:
396         nret, msg = FormatError(err)
397         retcode |= nret
398         ToStderr("Error activating disks for instance %s: %s", iname, msg)
399
400   if missing:
401     for iname, ival in missing.iteritems():
402       all_missing = utils.all(ival, lambda x: x[0] in bad_nodes)
403       if all_missing:
404         ToStdout("Instance %s cannot be verified as it lives on"
405                  " broken nodes", iname)
406       else:
407         ToStdout("Instance %s has missing logical volumes:", iname)
408         ival.sort()
409         for node, vol in ival:
410           if node in bad_nodes:
411             ToStdout("\tbroken node %s /dev/xenvg/%s", node, vol)
412           else:
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.")
416     retcode |= 1
417
418   return retcode
419
420
421 def RepairDiskSizes(opts, args):
422   """Verify sizes of cluster disks.
423
424   @param opts: the command line options selected by the user
425   @type args: list
426   @param args: optional list of instances to restrict check to
427   @rtype: int
428   @return: the desired exit code
429
430   """
431   op = opcodes.OpRepairDiskSizes(instances=args)
432   SubmitOpCode(op)
433
434
435 @UsesRPC
436 def MasterFailover(opts, args):
437   """Failover the master node.
438
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
441   master.
442
443   @param opts: the command line options selected by the user
444   @type args: list
445   @param args: should be an empty list
446   @rtype: int
447   @return: the desired exit code
448
449   """
450   if opts.no_voting:
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):
456       return 1
457
458   return bootstrap.MasterFailover(no_voting=opts.no_voting)
459
460
461 def SearchTags(opts, args):
462   """Searches the tags on all the cluster.
463
464   @param opts: the command line options selected by the user
465   @type args: list
466   @param args: should contain only one element, the tag pattern
467   @rtype: int
468   @return: the desired exit code
469
470   """
471   op = opcodes.OpSearchTags(pattern=args[0])
472   result = SubmitOpCode(op)
473   if not result:
474     return 1
475   result = list(result)
476   result.sort()
477   for path, tag in result:
478     ToStdout("%s %s", path, tag)
479
480
481 def SetClusterParams(opts, args):
482   """Modify the cluster.
483
484   @param opts: the command line options selected by the user
485   @type args: list
486   @param args: should be an empty list
487   @rtype: int
488   @return: the desired exit code
489
490   """
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.")
496     return 1
497
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.")
501     return 1
502   elif not opts.lvm_storage:
503     vg_name = ''
504
505   hvlist = opts.enabled_hypervisors
506   if hvlist is not None:
507     hvlist = hvlist.split(",")
508
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)
513
514   beparams = opts.beparams
515   utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
516
517   nicparams = opts.nicparams
518   utils.ForceDictType(nicparams, constants.NICS_PARAMETER_TYPES)
519
520   op = opcodes.OpSetClusterParams(vg_name=vg_name,
521                                   enabled_hypervisors=hvlist,
522                                   hvparams=hvparams,
523                                   beparams=beparams,
524                                   nicparams=nicparams,
525                                   candidate_pool_size=opts.candidate_pool_size)
526   SubmitOpCode(op)
527   return 0
528
529
530 def QueueOps(opts, args):
531   """Queue operations.
532
533   @param opts: the command line options selected by the user
534   @type args: list
535   @param args: should contain only one element, the subcommand
536   @rtype: int
537   @return: the desired exit code
538
539   """
540   command = args[0]
541   client = GetClient()
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"])
547     if result[0]:
548       val = "set"
549     else:
550       val = "unset"
551     ToStdout("The drain flag is %s" % val)
552   else:
553     raise errors.OpPrereqError("Command '%s' is not valid." % command)
554
555   return 0
556
557
558 def _ShowWatcherPause(until):
559   if until is None or until < time.time():
560     ToStdout("The watcher is not paused.")
561   else:
562     ToStdout("The watcher is paused until %s.", time.ctime(until))
563
564
565 def WatcherOps(opts, args):
566   """Watcher operations.
567
568   @param opts: the command line options selected by the user
569   @type args: list
570   @param args: should contain only one element, the subcommand
571   @rtype: int
572   @return: the desired exit code
573
574   """
575   command = args[0]
576   client = GetClient()
577
578   if command == "continue":
579     client.SetWatcherPause(None)
580     ToStdout("The watcher is no longer paused.")
581
582   elif command == "pause":
583     if len(args) < 2:
584       raise errors.OpPrereqError("Missing pause duration")
585
586     result = client.SetWatcherPause(time.time() + ParseTimespec(args[1]))
587     _ShowWatcherPause(result)
588
589   elif command == "info":
590     result = client.QueryConfigValues(["watcher_pause"])
591     _ShowWatcherPause(result)
592
593   else:
594     raise errors.OpPrereqError("Command '%s' is not valid." % command)
595
596   return 0
597
598
599 commands = {
600   'init': (
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"),
607   'destroy': (
608     DestroyCluster, ARGS_NONE, [YES_DOIT_OPT],
609     "", "Destroy cluster"),
610   'rename': (
611     RenameCluster, [ArgHost(min=1, max=1)],
612     [FORCE_OPT],
613     "<new_name>",
614     "Renames the cluster"),
615   'redist-conf': (
616     RedistributeConfig, ARGS_NONE, [SUBMIT_OPT],
617     "", "Forces a push of the configuration file and ssconf files"
618     " to the nodes in the cluster"),
619   'verify': (
620     VerifyCluster, ARGS_NONE,
621     [VERBOSE_OPT, DEBUG_SIMERR_OPT, ERROR_CODES_OPT, NONPLUS1_OPT],
622     "", "Does a check on the cluster configuration"),
623   'verify-disks': (
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"),
629   'masterfailover': (
630     MasterFailover, ARGS_NONE, [NOVOTING_OPT],
631     "", "Makes the current node the master"),
632   'version': (
633     ShowClusterVersion, ARGS_NONE, [],
634     "", "Shows the cluster version"),
635   'getmaster': (
636     ShowClusterMaster, ARGS_NONE, [],
637     "", "Shows the cluster master"),
638   'copyfile': (
639     ClusterCopyFile, [ArgFile(min=1, max=1)],
640     [NODE_LIST_OPT],
641     "[-n node...] <filename>", "Copies a file to all (or only some) nodes"),
642   'command': (
643     RunClusterCommand, [ArgCommand(min=1)],
644     [NODE_LIST_OPT],
645     "[-n node...] <command>", "Runs a command on all (or only some) nodes"),
646   'info': (
647     ShowClusterConfig, ARGS_NONE, [],
648     "", "Show cluster configuration"),
649   'list-tags': (
650     ListTags, ARGS_NONE, [], "", "List the tags of the cluster"),
651   'add-tags': (
652     AddTags, [ArgUnknown()], [TAG_SRC_OPT],
653     "tag...", "Add tags to the cluster"),
654   'remove-tags': (
655     RemoveTags, [ArgUnknown()], [TAG_SRC_OPT],
656     "tag...", "Remove tags from the cluster"),
657   'search-tags': (
658     SearchTags, [ArgUnknown(min=1, max=1)],
659     [], "", "Searches the tags on all objects on"
660     " the cluster for a given pattern (regex)"),
661   'queue': (
662     QueueOps,
663     [ArgChoice(min=1, max=1, choices=["drain", "undrain", "info"])],
664     [], "drain|undrain|info", "Change queue properties"),
665   'watcher': (
666     WatcherOps,
667     [ArgChoice(min=1, max=1, choices=["pause", "continue", "info"]),
668      ArgSuggest(min=0, max=1, choices=["30m", "1h", "4h"])],
669     [],
670     "{pause <timespec>|continue|info}", "Change watcher properties"),
671   'modify': (
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],
675     "[opts...]",
676     "Alters the parameters of the cluster"),
677   }
678
679 if __name__ == '__main__':
680   sys.exit(GenericMain(commands, override={"tag_type": constants.TAG_CLUSTER}))