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