install.rst: note about the default parameters
[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 = utils.CommaJoin(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",
251            utils.CommaJoin(result["enabled_hypervisors"]))
252
253   ToStdout("Hypervisor parameters:")
254   _PrintGroupedParams(result["hvparams"])
255
256   ToStdout("Cluster parameters:")
257   ToStdout("  - candidate pool size: %s", result["candidate_pool_size"])
258   ToStdout("  - master netdev: %s", result["master_netdev"])
259   ToStdout("  - lvm volume group: %s", result["volume_group_name"])
260   ToStdout("  - file storage path: %s", result["file_storage_dir"])
261
262   ToStdout("Default instance parameters:")
263   _PrintGroupedParams(result["beparams"])
264
265   ToStdout("Default nic parameters:")
266   _PrintGroupedParams(result["nicparams"])
267
268   return 0
269
270
271 def ClusterCopyFile(opts, args):
272   """Copy a file from master to some nodes.
273
274   @param opts: the command line options selected by the user
275   @type args: list
276   @param args: should contain only one element, the path of
277       the file to be copied
278   @rtype: int
279   @return: the desired exit code
280
281   """
282   filename = args[0]
283   if not os.path.exists(filename):
284     raise errors.OpPrereqError("No such filename '%s'" % filename,
285                                errors.ECODE_INVAL)
286
287   cl = GetClient()
288
289   myname = utils.GetHostInfo().name
290
291   cluster_name = cl.QueryConfigValues(["cluster_name"])[0]
292
293   results = GetOnlineNodes(nodes=opts.nodes, cl=cl)
294   results = [name for name in results if name != myname]
295
296   srun = ssh.SshRunner(cluster_name=cluster_name)
297   for node in results:
298     if not srun.CopyFileToNode(node, filename):
299       ToStderr("Copy of file %s to node %s failed", filename, node)
300
301   return 0
302
303
304 def RunClusterCommand(opts, args):
305   """Run a command on some nodes.
306
307   @param opts: the command line options selected by the user
308   @type args: list
309   @param args: should contain the command to be run and its arguments
310   @rtype: int
311   @return: the desired exit code
312
313   """
314   cl = GetClient()
315
316   command = " ".join(args)
317
318   nodes = GetOnlineNodes(nodes=opts.nodes, cl=cl)
319
320   cluster_name, master_node = cl.QueryConfigValues(["cluster_name",
321                                                     "master_node"])
322
323   srun = ssh.SshRunner(cluster_name=cluster_name)
324
325   # Make sure master node is at list end
326   if master_node in nodes:
327     nodes.remove(master_node)
328     nodes.append(master_node)
329
330   for name in nodes:
331     result = srun.Run(name, "root", command)
332     ToStdout("------------------------------------------------")
333     ToStdout("node: %s", name)
334     ToStdout("%s", result.output)
335     ToStdout("return code = %s", result.exit_code)
336
337   return 0
338
339
340 def VerifyCluster(opts, args):
341   """Verify integrity of cluster, performing various test on nodes.
342
343   @param opts: the command line options selected by the user
344   @type args: list
345   @param args: should be an empty list
346   @rtype: int
347   @return: the desired exit code
348
349   """
350   skip_checks = []
351   if opts.skip_nplusone_mem:
352     skip_checks.append(constants.VERIFY_NPLUSONE_MEM)
353   op = opcodes.OpVerifyCluster(skip_checks=skip_checks,
354                                verbose=opts.verbose,
355                                error_codes=opts.error_codes,
356                                debug_simulate_errors=opts.simulate_errors)
357   if SubmitOpCode(op):
358     return 0
359   else:
360     return 1
361
362
363 def VerifyDisks(opts, args):
364   """Verify integrity of cluster disks.
365
366   @param opts: the command line options selected by the user
367   @type args: list
368   @param args: should be an empty list
369   @rtype: int
370   @return: the desired exit code
371
372   """
373   op = opcodes.OpVerifyDisks()
374   result = SubmitOpCode(op)
375   if not isinstance(result, (list, tuple)) or len(result) != 3:
376     raise errors.ProgrammerError("Unknown result type for OpVerifyDisks")
377
378   bad_nodes, instances, missing = result
379
380   retcode = constants.EXIT_SUCCESS
381
382   if bad_nodes:
383     for node, text in bad_nodes.items():
384       ToStdout("Error gathering data on node %s: %s",
385                node, utils.SafeEncode(text[-400:]))
386       retcode |= 1
387       ToStdout("You need to fix these nodes first before fixing instances")
388
389   if instances:
390     for iname in instances:
391       if iname in missing:
392         continue
393       op = opcodes.OpActivateInstanceDisks(instance_name=iname)
394       try:
395         ToStdout("Activating disks for instance '%s'", iname)
396         SubmitOpCode(op)
397       except errors.GenericError, err:
398         nret, msg = FormatError(err)
399         retcode |= nret
400         ToStderr("Error activating disks for instance %s: %s", iname, msg)
401
402   if missing:
403     for iname, ival in missing.iteritems():
404       all_missing = utils.all(ival, lambda x: x[0] in bad_nodes)
405       if all_missing:
406         ToStdout("Instance %s cannot be verified as it lives on"
407                  " broken nodes", iname)
408       else:
409         ToStdout("Instance %s has missing logical volumes:", iname)
410         ival.sort()
411         for node, vol in ival:
412           if node in bad_nodes:
413             ToStdout("\tbroken node %s /dev/xenvg/%s", node, vol)
414           else:
415             ToStdout("\t%s /dev/xenvg/%s", node, vol)
416     ToStdout("You need to run replace_disks for all the above"
417            " instances, if this message persist after fixing nodes.")
418     retcode |= 1
419
420   return retcode
421
422
423 def RepairDiskSizes(opts, args):
424   """Verify sizes of cluster disks.
425
426   @param opts: the command line options selected by the user
427   @type args: list
428   @param args: optional list of instances to restrict check to
429   @rtype: int
430   @return: the desired exit code
431
432   """
433   op = opcodes.OpRepairDiskSizes(instances=args)
434   SubmitOpCode(op)
435
436
437 @UsesRPC
438 def MasterFailover(opts, args):
439   """Failover the master node.
440
441   This command, when run on a non-master node, will cause the current
442   master to cease being master, and the non-master to become new
443   master.
444
445   @param opts: the command line options selected by the user
446   @type args: list
447   @param args: should be an empty list
448   @rtype: int
449   @return: the desired exit code
450
451   """
452   if opts.no_voting:
453     usertext = ("This will perform the failover even if most other nodes"
454                 " are down, or if this node is outdated. This is dangerous"
455                 " as it can lead to a non-consistent cluster. Check the"
456                 " gnt-cluster(8) man page before proceeding. Continue?")
457     if not AskUser(usertext):
458       return 1
459
460   return bootstrap.MasterFailover(no_voting=opts.no_voting)
461
462
463 def SearchTags(opts, args):
464   """Searches the tags on all the cluster.
465
466   @param opts: the command line options selected by the user
467   @type args: list
468   @param args: should contain only one element, the tag pattern
469   @rtype: int
470   @return: the desired exit code
471
472   """
473   op = opcodes.OpSearchTags(pattern=args[0])
474   result = SubmitOpCode(op)
475   if not result:
476     return 1
477   result = list(result)
478   result.sort()
479   for path, tag in result:
480     ToStdout("%s %s", path, tag)
481
482
483 def SetClusterParams(opts, args):
484   """Modify the cluster.
485
486   @param opts: the command line options selected by the user
487   @type args: list
488   @param args: should be an empty list
489   @rtype: int
490   @return: the desired exit code
491
492   """
493   if not (not opts.lvm_storage or opts.vg_name or
494           opts.enabled_hypervisors or opts.hvparams or
495           opts.beparams or opts.nicparams or
496           opts.candidate_pool_size is not None):
497     ToStderr("Please give at least one of the parameters.")
498     return 1
499
500   vg_name = opts.vg_name
501   if not opts.lvm_storage and opts.vg_name:
502     ToStdout("Options --no-lvm-storage and --vg-name conflict.")
503     return 1
504   elif not opts.lvm_storage:
505     vg_name = ''
506
507   hvlist = opts.enabled_hypervisors
508   if hvlist is not None:
509     hvlist = hvlist.split(",")
510
511   # a list of (name, dict) we can pass directly to dict() (or [])
512   hvparams = dict(opts.hvparams)
513   for hv, hv_params in hvparams.iteritems():
514     utils.ForceDictType(hv_params, constants.HVS_PARAMETER_TYPES)
515
516   beparams = opts.beparams
517   utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
518
519   nicparams = opts.nicparams
520   utils.ForceDictType(nicparams, constants.NICS_PARAMETER_TYPES)
521
522   op = opcodes.OpSetClusterParams(vg_name=vg_name,
523                                   enabled_hypervisors=hvlist,
524                                   hvparams=hvparams,
525                                   beparams=beparams,
526                                   nicparams=nicparams,
527                                   candidate_pool_size=opts.candidate_pool_size)
528   SubmitOpCode(op)
529   return 0
530
531
532 def QueueOps(opts, args):
533   """Queue operations.
534
535   @param opts: the command line options selected by the user
536   @type args: list
537   @param args: should contain only one element, the subcommand
538   @rtype: int
539   @return: the desired exit code
540
541   """
542   command = args[0]
543   client = GetClient()
544   if command in ("drain", "undrain"):
545     drain_flag = command == "drain"
546     client.SetQueueDrainFlag(drain_flag)
547   elif command == "info":
548     result = client.QueryConfigValues(["drain_flag"])
549     if result[0]:
550       val = "set"
551     else:
552       val = "unset"
553     ToStdout("The drain flag is %s" % val)
554   else:
555     raise errors.OpPrereqError("Command '%s' is not valid." % command,
556                                errors.ECODE_INVAL)
557
558   return 0
559
560
561 def _ShowWatcherPause(until):
562   if until is None or until < time.time():
563     ToStdout("The watcher is not paused.")
564   else:
565     ToStdout("The watcher is paused until %s.", time.ctime(until))
566
567
568 def WatcherOps(opts, args):
569   """Watcher operations.
570
571   @param opts: the command line options selected by the user
572   @type args: list
573   @param args: should contain only one element, the subcommand
574   @rtype: int
575   @return: the desired exit code
576
577   """
578   command = args[0]
579   client = GetClient()
580
581   if command == "continue":
582     client.SetWatcherPause(None)
583     ToStdout("The watcher is no longer paused.")
584
585   elif command == "pause":
586     if len(args) < 2:
587       raise errors.OpPrereqError("Missing pause duration", errors.ECODE_INVAL)
588
589     result = client.SetWatcherPause(time.time() + ParseTimespec(args[1]))
590     _ShowWatcherPause(result)
591
592   elif command == "info":
593     result = client.QueryConfigValues(["watcher_pause"])
594     _ShowWatcherPause(result)
595
596   else:
597     raise errors.OpPrereqError("Command '%s' is not valid." % command,
598                                errors.ECODE_INVAL)
599
600   return 0
601
602
603 commands = {
604   'init': (
605     InitCluster, [ArgHost(min=1, max=1)],
606     [BACKEND_OPT, CP_SIZE_OPT, ENABLED_HV_OPT, GLOBAL_FILEDIR_OPT,
607      HVLIST_OPT, MAC_PREFIX_OPT, MASTER_NETDEV_OPT, NIC_PARAMS_OPT,
608      NOLVM_STORAGE_OPT, NOMODIFY_ETCHOSTS_OPT, NOMODIFY_SSH_SETUP_OPT,
609      SECONDARY_IP_OPT, VG_NAME_OPT],
610     "[opts...] <cluster_name>", "Initialises a new cluster configuration"),
611   'destroy': (
612     DestroyCluster, ARGS_NONE, [YES_DOIT_OPT],
613     "", "Destroy cluster"),
614   'rename': (
615     RenameCluster, [ArgHost(min=1, max=1)],
616     [FORCE_OPT],
617     "<new_name>",
618     "Renames the cluster"),
619   'redist-conf': (
620     RedistributeConfig, ARGS_NONE, [SUBMIT_OPT],
621     "", "Forces a push of the configuration file and ssconf files"
622     " to the nodes in the cluster"),
623   'verify': (
624     VerifyCluster, ARGS_NONE,
625     [VERBOSE_OPT, DEBUG_SIMERR_OPT, ERROR_CODES_OPT, NONPLUS1_OPT],
626     "", "Does a check on the cluster configuration"),
627   'verify-disks': (
628     VerifyDisks, ARGS_NONE, [],
629     "", "Does a check on the cluster disk status"),
630   'repair-disk-sizes': (
631     RepairDiskSizes, ARGS_MANY_INSTANCES, [],
632     "", "Updates mismatches in recorded disk sizes"),
633   'masterfailover': (
634     MasterFailover, ARGS_NONE, [NOVOTING_OPT],
635     "", "Makes the current node the master"),
636   'version': (
637     ShowClusterVersion, ARGS_NONE, [],
638     "", "Shows the cluster version"),
639   'getmaster': (
640     ShowClusterMaster, ARGS_NONE, [],
641     "", "Shows the cluster master"),
642   'copyfile': (
643     ClusterCopyFile, [ArgFile(min=1, max=1)],
644     [NODE_LIST_OPT],
645     "[-n node...] <filename>", "Copies a file to all (or only some) nodes"),
646   'command': (
647     RunClusterCommand, [ArgCommand(min=1)],
648     [NODE_LIST_OPT],
649     "[-n node...] <command>", "Runs a command on all (or only some) nodes"),
650   'info': (
651     ShowClusterConfig, ARGS_NONE, [],
652     "", "Show cluster configuration"),
653   'list-tags': (
654     ListTags, ARGS_NONE, [], "", "List the tags of the cluster"),
655   'add-tags': (
656     AddTags, [ArgUnknown()], [TAG_SRC_OPT],
657     "tag...", "Add tags to the cluster"),
658   'remove-tags': (
659     RemoveTags, [ArgUnknown()], [TAG_SRC_OPT],
660     "tag...", "Remove tags from the cluster"),
661   'search-tags': (
662     SearchTags, [ArgUnknown(min=1, max=1)],
663     [], "", "Searches the tags on all objects on"
664     " the cluster for a given pattern (regex)"),
665   'queue': (
666     QueueOps,
667     [ArgChoice(min=1, max=1, choices=["drain", "undrain", "info"])],
668     [], "drain|undrain|info", "Change queue properties"),
669   'watcher': (
670     WatcherOps,
671     [ArgChoice(min=1, max=1, choices=["pause", "continue", "info"]),
672      ArgSuggest(min=0, max=1, choices=["30m", "1h", "4h"])],
673     [],
674     "{pause <timespec>|continue|info}", "Change watcher properties"),
675   'modify': (
676     SetClusterParams, ARGS_NONE,
677     [BACKEND_OPT, CP_SIZE_OPT, ENABLED_HV_OPT, HVLIST_OPT,
678      NIC_PARAMS_OPT, NOLVM_STORAGE_OPT, VG_NAME_OPT],
679     "[opts...]",
680     "Alters the parameters of the cluster"),
681   }
682
683 if __name__ == '__main__':
684   sys.exit(GenericMain(commands, override={"tag_type": constants.TAG_CLUSTER}))