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