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