Change indentation style in commands definitions
[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                         )
103   op = opcodes.OpPostInitCluster()
104   SubmitOpCode(op)
105   return 0
106
107
108 @UsesRPC
109 def DestroyCluster(opts, args):
110   """Destroy the cluster.
111
112   @param opts: the command line options selected by the user
113   @type args: list
114   @param args: should be an empty list
115   @rtype: int
116   @return: the desired exit code
117
118   """
119   if not opts.yes_do_it:
120     ToStderr("Destroying a cluster is irreversible. If you really want"
121              " destroy this cluster, supply the --yes-do-it option.")
122     return 1
123
124   op = opcodes.OpDestroyCluster()
125   master = SubmitOpCode(op)
126   # if we reached this, the opcode didn't fail; we can proceed to
127   # shutdown all the daemons
128   bootstrap.FinalizeClusterDestroy(master)
129   return 0
130
131
132 def RenameCluster(opts, args):
133   """Rename the cluster.
134
135   @param opts: the command line options selected by the user
136   @type args: list
137   @param args: should contain only one element, the new cluster name
138   @rtype: int
139   @return: the desired exit code
140
141   """
142   name = args[0]
143   if not opts.force:
144     usertext = ("This will rename the cluster to '%s'. If you are connected"
145                 " over the network to the cluster name, the operation is very"
146                 " dangerous as the IP address will be removed from the node"
147                 " and the change may not go through. Continue?") % name
148     if not AskUser(usertext):
149       return 1
150
151   op = opcodes.OpRenameCluster(name=name)
152   SubmitOpCode(op)
153   return 0
154
155
156 def RedistributeConfig(opts, args):
157   """Forces push of the cluster configuration.
158
159   @param opts: the command line options selected by the user
160   @type args: list
161   @param args: empty list
162   @rtype: int
163   @return: the desired exit code
164
165   """
166   op = opcodes.OpRedistributeConfig()
167   SubmitOrSend(op, opts)
168   return 0
169
170
171 def ShowClusterVersion(opts, args):
172   """Write version of ganeti software to the standard output.
173
174   @param opts: the command line options selected by the user
175   @type args: list
176   @param args: should be an empty list
177   @rtype: int
178   @return: the desired exit code
179
180   """
181   cl = GetClient()
182   result = cl.QueryClusterInfo()
183   ToStdout("Software version: %s", result["software_version"])
184   ToStdout("Internode protocol: %s", result["protocol_version"])
185   ToStdout("Configuration format: %s", result["config_version"])
186   ToStdout("OS api version: %s", result["os_api_version"])
187   ToStdout("Export interface: %s", result["export_version"])
188   return 0
189
190
191 def ShowClusterMaster(opts, args):
192   """Write name of master node to the standard output.
193
194   @param opts: the command line options selected by the user
195   @type args: list
196   @param args: should be an empty list
197   @rtype: int
198   @return: the desired exit code
199
200   """
201   master = bootstrap.GetMaster()
202   ToStdout(master)
203   return 0
204
205 def _PrintGroupedParams(paramsdict):
206   """Print Grouped parameters (be, nic, disk) by group.
207
208   @type paramsdict: dict of dicts
209   @param paramsdict: {group: {param: value, ...}, ...}
210
211   """
212   for gr_name, gr_dict in paramsdict.items():
213     ToStdout("  - %s:", gr_name)
214     for item, val in gr_dict.iteritems():
215       ToStdout("      %s: %s", item, val)
216
217 def ShowClusterConfig(opts, args):
218   """Shows cluster information.
219
220   @param opts: the command line options selected by the user
221   @type args: list
222   @param args: should be an empty list
223   @rtype: int
224   @return: the desired exit code
225
226   """
227   cl = GetClient()
228   result = cl.QueryClusterInfo()
229
230   ToStdout("Cluster name: %s", result["name"])
231
232   ToStdout("Creation time: %s", utils.FormatTime(result["ctime"]))
233   ToStdout("Modification time: %s", utils.FormatTime(result["mtime"]))
234
235   ToStdout("Master node: %s", result["master"])
236
237   ToStdout("Architecture (this node): %s (%s)",
238            result["architecture"][0], result["architecture"][1])
239
240   if result["tags"]:
241     tags = ", ".join(utils.NiceSort(result["tags"]))
242   else:
243     tags = "(none)"
244
245   ToStdout("Tags: %s", tags)
246
247   ToStdout("Default hypervisor: %s", result["default_hypervisor"])
248   ToStdout("Enabled hypervisors: %s", ", ".join(result["enabled_hypervisors"]))
249
250   ToStdout("Hypervisor parameters:")
251   _PrintGroupedParams(result["hvparams"])
252
253   ToStdout("Cluster parameters:")
254   ToStdout("  - candidate pool size: %s", result["candidate_pool_size"])
255   ToStdout("  - master netdev: %s", result["master_netdev"])
256   ToStdout("  - lvm volume group: %s", result["volume_group_name"])
257   ToStdout("  - file storage path: %s", result["file_storage_dir"])
258
259   ToStdout("Default instance parameters:")
260   _PrintGroupedParams(result["beparams"])
261
262   ToStdout("Default nic parameters:")
263   _PrintGroupedParams(result["nicparams"])
264
265   return 0
266
267
268 def ClusterCopyFile(opts, args):
269   """Copy a file from master to some nodes.
270
271   @param opts: the command line options selected by the user
272   @type args: list
273   @param args: should contain only one element, the path of
274       the file to be copied
275   @rtype: int
276   @return: the desired exit code
277
278   """
279   filename = args[0]
280   if not os.path.exists(filename):
281     raise errors.OpPrereqError("No such filename '%s'" % filename)
282
283   cl = GetClient()
284
285   myname = utils.HostInfo().name
286
287   cluster_name = cl.QueryConfigValues(["cluster_name"])[0]
288
289   results = GetOnlineNodes(nodes=opts.nodes, cl=cl)
290   results = [name for name in results if name != myname]
291
292   srun = ssh.SshRunner(cluster_name=cluster_name)
293   for node in results:
294     if not srun.CopyFileToNode(node, filename):
295       ToStderr("Copy of file %s to node %s failed", filename, node)
296
297   return 0
298
299
300 def RunClusterCommand(opts, args):
301   """Run a command on some nodes.
302
303   @param opts: the command line options selected by the user
304   @type args: list
305   @param args: should contain the command to be run and its arguments
306   @rtype: int
307   @return: the desired exit code
308
309   """
310   cl = GetClient()
311
312   command = " ".join(args)
313
314   nodes = GetOnlineNodes(nodes=opts.nodes, cl=cl)
315
316   cluster_name, master_node = cl.QueryConfigValues(["cluster_name",
317                                                     "master_node"])
318
319   srun = ssh.SshRunner(cluster_name=cluster_name)
320
321   # Make sure master node is at list end
322   if master_node in nodes:
323     nodes.remove(master_node)
324     nodes.append(master_node)
325
326   for name in nodes:
327     result = srun.Run(name, "root", command)
328     ToStdout("------------------------------------------------")
329     ToStdout("node: %s", name)
330     ToStdout("%s", result.output)
331     ToStdout("return code = %s", result.exit_code)
332
333   return 0
334
335
336 def VerifyCluster(opts, args):
337   """Verify integrity of cluster, performing various test on nodes.
338
339   @param opts: the command line options selected by the user
340   @type args: list
341   @param args: should be an empty list
342   @rtype: int
343   @return: the desired exit code
344
345   """
346   skip_checks = []
347   if opts.skip_nplusone_mem:
348     skip_checks.append(constants.VERIFY_NPLUSONE_MEM)
349   op = opcodes.OpVerifyCluster(skip_checks=skip_checks,
350                                verbose=opts.verbose,
351                                error_codes=opts.error_codes,
352                                debug_simulate_errors=opts.simulate_errors)
353   if SubmitOpCode(op):
354     return 0
355   else:
356     return 1
357
358
359 def VerifyDisks(opts, args):
360   """Verify integrity of cluster disks.
361
362   @param opts: the command line options selected by the user
363   @type args: list
364   @param args: should be an empty list
365   @rtype: int
366   @return: the desired exit code
367
368   """
369   op = opcodes.OpVerifyDisks()
370   result = SubmitOpCode(op)
371   if not isinstance(result, (list, tuple)) or len(result) != 3:
372     raise errors.ProgrammerError("Unknown result type for OpVerifyDisks")
373
374   bad_nodes, instances, missing = result
375
376   retcode = constants.EXIT_SUCCESS
377
378   if bad_nodes:
379     for node, text in bad_nodes.items():
380       ToStdout("Error gathering data on node %s: %s",
381                node, utils.SafeEncode(text[-400:]))
382       retcode |= 1
383       ToStdout("You need to fix these nodes first before fixing instances")
384
385   if instances:
386     for iname in instances:
387       if iname in missing:
388         continue
389       op = opcodes.OpActivateInstanceDisks(instance_name=iname)
390       try:
391         ToStdout("Activating disks for instance '%s'", iname)
392         SubmitOpCode(op)
393       except errors.GenericError, err:
394         nret, msg = FormatError(err)
395         retcode |= nret
396         ToStderr("Error activating disks for instance %s: %s", iname, msg)
397
398   if missing:
399     for iname, ival in missing.iteritems():
400       all_missing = utils.all(ival, lambda x: x[0] in bad_nodes)
401       if all_missing:
402         ToStdout("Instance %s cannot be verified as it lives on"
403                  " broken nodes", iname)
404       else:
405         ToStdout("Instance %s has missing logical volumes:", iname)
406         ival.sort()
407         for node, vol in ival:
408           if node in bad_nodes:
409             ToStdout("\tbroken node %s /dev/xenvg/%s", node, vol)
410           else:
411             ToStdout("\t%s /dev/xenvg/%s", node, vol)
412     ToStdout("You need to run replace_disks for all the above"
413            " instances, if this message persist after fixing nodes.")
414     retcode |= 1
415
416   return retcode
417
418
419 def RepairDiskSizes(opts, args):
420   """Verify sizes of cluster disks.
421
422   @param opts: the command line options selected by the user
423   @type args: list
424   @param args: optional list of instances to restrict check to
425   @rtype: int
426   @return: the desired exit code
427
428   """
429   op = opcodes.OpRepairDiskSizes(instances=args)
430   SubmitOpCode(op)
431
432
433 @UsesRPC
434 def MasterFailover(opts, args):
435   """Failover the master node.
436
437   This command, when run on a non-master node, will cause the current
438   master to cease being master, and the non-master to become new
439   master.
440
441   @param opts: the command line options selected by the user
442   @type args: list
443   @param args: should be an empty list
444   @rtype: int
445   @return: the desired exit code
446
447   """
448   if opts.no_voting:
449     usertext = ("This will perform the failover even if most other nodes"
450                 " are down, or if this node is outdated. This is dangerous"
451                 " as it can lead to a non-consistent cluster. Check the"
452                 " gnt-cluster(8) man page before proceeding. Continue?")
453     if not AskUser(usertext):
454       return 1
455
456   return bootstrap.MasterFailover(no_voting=opts.no_voting)
457
458
459 def SearchTags(opts, args):
460   """Searches the tags on all the cluster.
461
462   @param opts: the command line options selected by the user
463   @type args: list
464   @param args: should contain only one element, the tag pattern
465   @rtype: int
466   @return: the desired exit code
467
468   """
469   op = opcodes.OpSearchTags(pattern=args[0])
470   result = SubmitOpCode(op)
471   if not result:
472     return 1
473   result = list(result)
474   result.sort()
475   for path, tag in result:
476     ToStdout("%s %s", path, tag)
477
478
479 def SetClusterParams(opts, args):
480   """Modify the cluster.
481
482   @param opts: the command line options selected by the user
483   @type args: list
484   @param args: should be an empty list
485   @rtype: int
486   @return: the desired exit code
487
488   """
489   if not (not opts.lvm_storage or opts.vg_name or
490           opts.enabled_hypervisors or opts.hvparams or
491           opts.beparams or opts.nicparams or
492           opts.candidate_pool_size is not None):
493     ToStderr("Please give at least one of the parameters.")
494     return 1
495
496   vg_name = opts.vg_name
497   if not opts.lvm_storage and opts.vg_name:
498     ToStdout("Options --no-lvm-storage and --vg-name conflict.")
499     return 1
500   elif not opts.lvm_storage:
501     vg_name = ''
502
503   hvlist = opts.enabled_hypervisors
504   if hvlist is not None:
505     hvlist = hvlist.split(",")
506
507   # a list of (name, dict) we can pass directly to dict() (or [])
508   hvparams = dict(opts.hvparams)
509   for hv, hv_params in hvparams.iteritems():
510     utils.ForceDictType(hv_params, constants.HVS_PARAMETER_TYPES)
511
512   beparams = opts.beparams
513   utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
514
515   nicparams = opts.nicparams
516   utils.ForceDictType(nicparams, constants.NICS_PARAMETER_TYPES)
517
518   op = opcodes.OpSetClusterParams(vg_name=vg_name,
519                                   enabled_hypervisors=hvlist,
520                                   hvparams=hvparams,
521                                   beparams=beparams,
522                                   nicparams=nicparams,
523                                   candidate_pool_size=opts.candidate_pool_size)
524   SubmitOpCode(op)
525   return 0
526
527
528 def QueueOps(opts, args):
529   """Queue operations.
530
531   @param opts: the command line options selected by the user
532   @type args: list
533   @param args: should contain only one element, the subcommand
534   @rtype: int
535   @return: the desired exit code
536
537   """
538   command = args[0]
539   client = GetClient()
540   if command in ("drain", "undrain"):
541     drain_flag = command == "drain"
542     client.SetQueueDrainFlag(drain_flag)
543   elif command == "info":
544     result = client.QueryConfigValues(["drain_flag"])
545     if result[0]:
546       val = "set"
547     else:
548       val = "unset"
549     ToStdout("The drain flag is %s" % val)
550   else:
551     raise errors.OpPrereqError("Command '%s' is not valid." % command)
552
553   return 0
554
555
556 def _ShowWatcherPause(until):
557   if until is None or until < time.time():
558     ToStdout("The watcher is not paused.")
559   else:
560     ToStdout("The watcher is paused until %s.", time.ctime(until))
561
562
563 def WatcherOps(opts, args):
564   """Watcher operations.
565
566   @param opts: the command line options selected by the user
567   @type args: list
568   @param args: should contain only one element, the subcommand
569   @rtype: int
570   @return: the desired exit code
571
572   """
573   command = args[0]
574   client = GetClient()
575
576   if command == "continue":
577     client.SetWatcherPause(None)
578     ToStdout("The watcher is no longer paused.")
579
580   elif command == "pause":
581     if len(args) < 2:
582       raise errors.OpPrereqError("Missing pause duration")
583
584     result = client.SetWatcherPause(time.time() + ParseTimespec(args[1]))
585     _ShowWatcherPause(result)
586
587   elif command == "info":
588     result = client.QueryConfigValues(["watcher_pause"])
589     _ShowWatcherPause(result)
590
591   else:
592     raise errors.OpPrereqError("Command '%s' is not valid." % command)
593
594   return 0
595
596
597 commands = {
598   'init': (
599     InitCluster, [ArgHost(min=1, max=1)],
600     [DEBUG_OPT, BACKEND_OPT, CP_SIZE_OPT, ENABLED_HV_OPT, GLOBAL_FILEDIR_OPT,
601      HVLIST_OPT, MAC_PREFIX_OPT, MASTER_NETDEV_OPT, NIC_PARAMS_OPT,
602      NOLVM_STORAGE_OPT, NOMODIFY_ETCHOSTS_OPT, SECONDARY_IP_OPT, VG_NAME_OPT],
603     "[opts...] <cluster_name>", "Initialises a new cluster configuration"),
604   'destroy': (
605     DestroyCluster, ARGS_NONE,
606     [DEBUG_OPT, YES_DOIT_OPT],
607     "", "Destroy cluster"),
608   'rename': (
609     RenameCluster, [ArgHost(min=1, max=1)],
610     [DEBUG_OPT, FORCE_OPT],
611     "<new_name>",
612     "Renames the cluster"),
613   'redist-conf': (
614     RedistributeConfig, ARGS_NONE, [DEBUG_OPT, SUBMIT_OPT],
615     "", "Forces a push of the configuration file and ssconf files"
616     " to the nodes in the cluster"),
617   'verify': (
618     VerifyCluster, ARGS_NONE,
619     [DEBUG_OPT, VERBOSE_OPT, DEBUG_SIMERR_OPT, ERROR_CODES_OPT, NONPLUS1_OPT],
620     "", "Does a check on the cluster configuration"),
621   'verify-disks': (
622     VerifyDisks, ARGS_NONE, [DEBUG_OPT],
623     "", "Does a check on the cluster disk status"),
624   'repair-disk-sizes': (
625     RepairDiskSizes, ARGS_MANY_INSTANCES, [DEBUG_OPT],
626     "", "Updates mismatches in recorded disk sizes"),
627   'masterfailover': (
628     MasterFailover, ARGS_NONE, [DEBUG_OPT, NOVOTING_OPT],
629     "", "Makes the current node the master"),
630   'version': (
631     ShowClusterVersion, ARGS_NONE, [DEBUG_OPT],
632     "", "Shows the cluster version"),
633   'getmaster': (
634     ShowClusterMaster, ARGS_NONE, [DEBUG_OPT],
635     "", "Shows the cluster master"),
636   'copyfile': (
637     ClusterCopyFile, [ArgFile(min=1, max=1)],
638     [DEBUG_OPT, NODE_LIST_OPT],
639     "[-n node...] <filename>", "Copies a file to all (or only some) nodes"),
640   'command': (
641     RunClusterCommand, [ArgCommand(min=1)],
642     [DEBUG_OPT, NODE_LIST_OPT],
643     "[-n node...] <command>", "Runs a command on all (or only some) nodes"),
644   'info': (
645     ShowClusterConfig, ARGS_NONE, [DEBUG_OPT],
646     "", "Show cluster configuration"),
647   'list-tags': (
648     ListTags, ARGS_NONE,
649     [DEBUG_OPT], "", "List the tags of the cluster"),
650   'add-tags': (
651     AddTags, [ArgUnknown()], [DEBUG_OPT, TAG_SRC_OPT],
652     "tag...", "Add tags to the cluster"),
653   'remove-tags': (
654     RemoveTags, [ArgUnknown()], [DEBUG_OPT, TAG_SRC_OPT],
655     "tag...", "Remove tags from the cluster"),
656   'search-tags': (
657     SearchTags, [ArgUnknown(min=1, max=1)],
658     [DEBUG_OPT], "", "Searches the tags on all objects on"
659     " the cluster for a given pattern (regex)"),
660   'queue': (
661     QueueOps,
662     [ArgChoice(min=1, max=1, choices=["drain", "undrain", "info"])],
663     [DEBUG_OPT], "drain|undrain|info", "Change queue properties"),
664   'watcher': (
665     WatcherOps,
666     [ArgChoice(min=1, max=1, choices=["pause", "continue", "info"]),
667      ArgSuggest(min=0, max=1, choices=["30m", "1h", "4h"])],
668     [DEBUG_OPT],
669     "{pause <timespec>|continue|info}", "Change watcher properties"),
670   'modify': (
671     SetClusterParams, ARGS_NONE,
672     [DEBUG_OPT, BACKEND_OPT, CP_SIZE_OPT, ENABLED_HV_OPT, HVLIST_OPT,
673      NIC_PARAMS_OPT, NOLVM_STORAGE_OPT, VG_NAME_OPT],
674     "[opts...]",
675     "Alters the parameters of the cluster"),
676   }
677
678 if __name__ == '__main__':
679   sys.exit(GenericMain(commands, override={"tag_type": constants.TAG_CLUSTER}))