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