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