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