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