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