Add --uid-pool option to gnt-cluster init
[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 import OpenSSL
33
34 from ganeti.cli import *
35 from ganeti import opcodes
36 from ganeti import constants
37 from ganeti import errors
38 from ganeti import utils
39 from ganeti import bootstrap
40 from ganeti import ssh
41 from ganeti import objects
42 from ganeti import uidpool
43
44
45 @UsesRPC
46 def InitCluster(opts, args):
47   """Initialize the cluster.
48
49   @param opts: the command line options selected by the user
50   @type args: list
51   @param args: should contain only one element, the desired
52       cluster name
53   @rtype: int
54   @return: the desired exit code
55
56   """
57   if not opts.lvm_storage and opts.vg_name:
58     ToStderr("Options --no-lvm-storage and --vg-name conflict.")
59     return 1
60
61   vg_name = opts.vg_name
62   if opts.lvm_storage and not opts.vg_name:
63     vg_name = constants.DEFAULT_VG
64
65   hvlist = opts.enabled_hypervisors
66   if hvlist is None:
67     hvlist = constants.DEFAULT_ENABLED_HYPERVISOR
68   hvlist = hvlist.split(",")
69
70   hvparams = dict(opts.hvparams)
71   beparams = opts.beparams
72   nicparams = opts.nicparams
73
74   # prepare beparams dict
75   beparams = objects.FillDict(constants.BEC_DEFAULTS, beparams)
76   utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
77
78   # prepare nicparams dict
79   nicparams = objects.FillDict(constants.NICC_DEFAULTS, nicparams)
80   utils.ForceDictType(nicparams, constants.NICS_PARAMETER_TYPES)
81
82   # prepare hvparams dict
83   for hv in constants.HYPER_TYPES:
84     if hv not in hvparams:
85       hvparams[hv] = {}
86     hvparams[hv] = objects.FillDict(constants.HVC_DEFAULTS[hv], hvparams[hv])
87     utils.ForceDictType(hvparams[hv], constants.HVS_PARAMETER_TYPES)
88
89   if opts.candidate_pool_size is None:
90     opts.candidate_pool_size = constants.MASTER_POOL_SIZE_DEFAULT
91
92   if opts.mac_prefix is None:
93     opts.mac_prefix = constants.DEFAULT_MAC_PREFIX
94
95   uid_pool = opts.uid_pool
96   if uid_pool is not None:
97     uid_pool = uidpool.ParseUidPool(uid_pool)
98
99   bootstrap.InitCluster(cluster_name=args[0],
100                         secondary_ip=opts.secondary_ip,
101                         vg_name=vg_name,
102                         mac_prefix=opts.mac_prefix,
103                         master_netdev=opts.master_netdev,
104                         file_storage_dir=opts.file_storage_dir,
105                         enabled_hypervisors=hvlist,
106                         hvparams=hvparams,
107                         beparams=beparams,
108                         nicparams=nicparams,
109                         candidate_pool_size=opts.candidate_pool_size,
110                         modify_etc_hosts=opts.modify_etc_hosts,
111                         modify_ssh_setup=opts.modify_ssh_setup,
112                         maintain_node_health=opts.maintain_node_health,
113                         uid_pool=uid_pool,
114                         )
115   op = opcodes.OpPostInitCluster()
116   SubmitOpCode(op, opts=opts)
117   return 0
118
119
120 @UsesRPC
121 def DestroyCluster(opts, args):
122   """Destroy the cluster.
123
124   @param opts: the command line options selected by the user
125   @type args: list
126   @param args: should be an empty list
127   @rtype: int
128   @return: the desired exit code
129
130   """
131   if not opts.yes_do_it:
132     ToStderr("Destroying a cluster is irreversible. If you really want"
133              " destroy this cluster, supply the --yes-do-it option.")
134     return 1
135
136   op = opcodes.OpDestroyCluster()
137   master = SubmitOpCode(op, opts=opts)
138   # if we reached this, the opcode didn't fail; we can proceed to
139   # shutdown all the daemons
140   bootstrap.FinalizeClusterDestroy(master)
141   return 0
142
143
144 def RenameCluster(opts, args):
145   """Rename the cluster.
146
147   @param opts: the command line options selected by the user
148   @type args: list
149   @param args: should contain only one element, the new cluster name
150   @rtype: int
151   @return: the desired exit code
152
153   """
154   name = args[0]
155   if not opts.force:
156     usertext = ("This will rename the cluster to '%s'. If you are connected"
157                 " over the network to the cluster name, the operation is very"
158                 " dangerous as the IP address will be removed from the node"
159                 " and the change may not go through. Continue?") % name
160     if not AskUser(usertext):
161       return 1
162
163   op = opcodes.OpRenameCluster(name=name)
164   SubmitOpCode(op, opts=opts)
165   return 0
166
167
168 def RedistributeConfig(opts, args):
169   """Forces push of the cluster configuration.
170
171   @param opts: the command line options selected by the user
172   @type args: list
173   @param args: empty list
174   @rtype: int
175   @return: the desired exit code
176
177   """
178   op = opcodes.OpRedistributeConfig()
179   SubmitOrSend(op, opts)
180   return 0
181
182
183 def ShowClusterVersion(opts, args):
184   """Write version of ganeti software to the standard output.
185
186   @param opts: the command line options selected by the user
187   @type args: list
188   @param args: should be an empty list
189   @rtype: int
190   @return: the desired exit code
191
192   """
193   cl = GetClient()
194   result = cl.QueryClusterInfo()
195   ToStdout("Software version: %s", result["software_version"])
196   ToStdout("Internode protocol: %s", result["protocol_version"])
197   ToStdout("Configuration format: %s", result["config_version"])
198   ToStdout("OS api version: %s", result["os_api_version"])
199   ToStdout("Export interface: %s", result["export_version"])
200   return 0
201
202
203 def ShowClusterMaster(opts, args):
204   """Write name of master node to the standard output.
205
206   @param opts: the command line options selected by the user
207   @type args: list
208   @param args: should be an empty list
209   @rtype: int
210   @return: the desired exit code
211
212   """
213   master = bootstrap.GetMaster()
214   ToStdout(master)
215   return 0
216
217
218 def _PrintGroupedParams(paramsdict, level=1):
219   """Print Grouped parameters (be, nic, disk) by group.
220
221   @type paramsdict: dict of dicts
222   @param paramsdict: {group: {param: value, ...}, ...}
223   @type level: int
224   @param level: Level of indention
225
226   """
227   indent = "  " * level
228   for item, val in paramsdict.items():
229     if isinstance(val, dict):
230       ToStdout("%s- %s:", indent, item)
231       _PrintGroupedParams(val, level=level + 1)
232     else:
233       ToStdout("%s  %s: %s", indent, item, val)
234
235
236 def ShowClusterConfig(opts, args):
237   """Shows cluster information.
238
239   @param opts: the command line options selected by the user
240   @type args: list
241   @param args: should be an empty list
242   @rtype: int
243   @return: the desired exit code
244
245   """
246   cl = GetClient()
247   result = cl.QueryClusterInfo()
248
249   ToStdout("Cluster name: %s", result["name"])
250   ToStdout("Cluster UUID: %s", result["uuid"])
251
252   ToStdout("Creation time: %s", utils.FormatTime(result["ctime"]))
253   ToStdout("Modification time: %s", utils.FormatTime(result["mtime"]))
254
255   ToStdout("Master node: %s", result["master"])
256
257   ToStdout("Architecture (this node): %s (%s)",
258            result["architecture"][0], result["architecture"][1])
259
260   if result["tags"]:
261     tags = utils.CommaJoin(utils.NiceSort(result["tags"]))
262   else:
263     tags = "(none)"
264
265   ToStdout("Tags: %s", tags)
266
267   ToStdout("Default hypervisor: %s", result["default_hypervisor"])
268   ToStdout("Enabled hypervisors: %s",
269            utils.CommaJoin(result["enabled_hypervisors"]))
270
271   ToStdout("Hypervisor parameters:")
272   _PrintGroupedParams(result["hvparams"])
273
274   ToStdout("OS specific hypervisor parameters:")
275   _PrintGroupedParams(result["os_hvp"])
276
277   ToStdout("Cluster parameters:")
278   ToStdout("  - candidate pool size: %s", result["candidate_pool_size"])
279   ToStdout("  - master netdev: %s", result["master_netdev"])
280   ToStdout("  - lvm volume group: %s", result["volume_group_name"])
281   ToStdout("  - file storage path: %s", result["file_storage_dir"])
282   ToStdout("  - maintenance of node health: %s",
283            result["maintain_node_health"])
284   ToStdout("  - uid pool: %s", uidpool.FormatUidPool(result["uid_pool"]))
285
286   ToStdout("Default instance parameters:")
287   _PrintGroupedParams(result["beparams"])
288
289   ToStdout("Default nic parameters:")
290   _PrintGroupedParams(result["nicparams"])
291
292   return 0
293
294
295 def ClusterCopyFile(opts, args):
296   """Copy a file from master to some nodes.
297
298   @param opts: the command line options selected by the user
299   @type args: list
300   @param args: should contain only one element, the path of
301       the file to be copied
302   @rtype: int
303   @return: the desired exit code
304
305   """
306   filename = args[0]
307   if not os.path.exists(filename):
308     raise errors.OpPrereqError("No such filename '%s'" % filename,
309                                errors.ECODE_INVAL)
310
311   cl = GetClient()
312
313   cluster_name = cl.QueryConfigValues(["cluster_name"])[0]
314
315   results = GetOnlineNodes(nodes=opts.nodes, cl=cl, filter_master=True,
316                            secondary_ips=opts.use_replication_network)
317
318   srun = ssh.SshRunner(cluster_name=cluster_name)
319   for node in results:
320     if not srun.CopyFileToNode(node, filename):
321       ToStderr("Copy of file %s to node %s failed", filename, node)
322
323   return 0
324
325
326 def RunClusterCommand(opts, args):
327   """Run a command on some nodes.
328
329   @param opts: the command line options selected by the user
330   @type args: list
331   @param args: should contain the command to be run and its arguments
332   @rtype: int
333   @return: the desired exit code
334
335   """
336   cl = GetClient()
337
338   command = " ".join(args)
339
340   nodes = GetOnlineNodes(nodes=opts.nodes, cl=cl)
341
342   cluster_name, master_node = cl.QueryConfigValues(["cluster_name",
343                                                     "master_node"])
344
345   srun = ssh.SshRunner(cluster_name=cluster_name)
346
347   # Make sure master node is at list end
348   if master_node in nodes:
349     nodes.remove(master_node)
350     nodes.append(master_node)
351
352   for name in nodes:
353     result = srun.Run(name, "root", command)
354     ToStdout("------------------------------------------------")
355     ToStdout("node: %s", name)
356     ToStdout("%s", result.output)
357     ToStdout("return code = %s", result.exit_code)
358
359   return 0
360
361
362 def VerifyCluster(opts, args):
363   """Verify integrity of cluster, performing various test on nodes.
364
365   @param opts: the command line options selected by the user
366   @type args: list
367   @param args: should be an empty list
368   @rtype: int
369   @return: the desired exit code
370
371   """
372   skip_checks = []
373   if opts.skip_nplusone_mem:
374     skip_checks.append(constants.VERIFY_NPLUSONE_MEM)
375   op = opcodes.OpVerifyCluster(skip_checks=skip_checks,
376                                verbose=opts.verbose,
377                                error_codes=opts.error_codes,
378                                debug_simulate_errors=opts.simulate_errors)
379   if SubmitOpCode(op, opts=opts):
380     return 0
381   else:
382     return 1
383
384
385 def VerifyDisks(opts, args):
386   """Verify integrity of cluster disks.
387
388   @param opts: the command line options selected by the user
389   @type args: list
390   @param args: should be an empty list
391   @rtype: int
392   @return: the desired exit code
393
394   """
395   op = opcodes.OpVerifyDisks()
396   result = SubmitOpCode(op, opts=opts)
397   if not isinstance(result, (list, tuple)) or len(result) != 3:
398     raise errors.ProgrammerError("Unknown result type for OpVerifyDisks")
399
400   bad_nodes, instances, missing = result
401
402   retcode = constants.EXIT_SUCCESS
403
404   if bad_nodes:
405     for node, text in bad_nodes.items():
406       ToStdout("Error gathering data on node %s: %s",
407                node, utils.SafeEncode(text[-400:]))
408       retcode |= 1
409       ToStdout("You need to fix these nodes first before fixing instances")
410
411   if instances:
412     for iname in instances:
413       if iname in missing:
414         continue
415       op = opcodes.OpActivateInstanceDisks(instance_name=iname)
416       try:
417         ToStdout("Activating disks for instance '%s'", iname)
418         SubmitOpCode(op, opts=opts)
419       except errors.GenericError, err:
420         nret, msg = FormatError(err)
421         retcode |= nret
422         ToStderr("Error activating disks for instance %s: %s", iname, msg)
423
424   if missing:
425     for iname, ival in missing.iteritems():
426       all_missing = utils.all(ival, lambda x: x[0] in bad_nodes)
427       if all_missing:
428         ToStdout("Instance %s cannot be verified as it lives on"
429                  " broken nodes", iname)
430       else:
431         ToStdout("Instance %s has missing logical volumes:", iname)
432         ival.sort()
433         for node, vol in ival:
434           if node in bad_nodes:
435             ToStdout("\tbroken node %s /dev/xenvg/%s", node, vol)
436           else:
437             ToStdout("\t%s /dev/xenvg/%s", node, vol)
438     ToStdout("You need to run replace_disks for all the above"
439            " instances, if this message persist after fixing nodes.")
440     retcode |= 1
441
442   return retcode
443
444
445 def RepairDiskSizes(opts, args):
446   """Verify sizes of cluster disks.
447
448   @param opts: the command line options selected by the user
449   @type args: list
450   @param args: optional list of instances to restrict check to
451   @rtype: int
452   @return: the desired exit code
453
454   """
455   op = opcodes.OpRepairDiskSizes(instances=args)
456   SubmitOpCode(op, opts=opts)
457
458
459 @UsesRPC
460 def MasterFailover(opts, args):
461   """Failover the master node.
462
463   This command, when run on a non-master node, will cause the current
464   master to cease being master, and the non-master to become new
465   master.
466
467   @param opts: the command line options selected by the user
468   @type args: list
469   @param args: should be an empty list
470   @rtype: int
471   @return: the desired exit code
472
473   """
474   if opts.no_voting:
475     usertext = ("This will perform the failover even if most other nodes"
476                 " are down, or if this node is outdated. This is dangerous"
477                 " as it can lead to a non-consistent cluster. Check the"
478                 " gnt-cluster(8) man page before proceeding. Continue?")
479     if not AskUser(usertext):
480       return 1
481
482   return bootstrap.MasterFailover(no_voting=opts.no_voting)
483
484
485 def SearchTags(opts, args):
486   """Searches the tags on all the cluster.
487
488   @param opts: the command line options selected by the user
489   @type args: list
490   @param args: should contain only one element, the tag pattern
491   @rtype: int
492   @return: the desired exit code
493
494   """
495   op = opcodes.OpSearchTags(pattern=args[0])
496   result = SubmitOpCode(op, opts=opts)
497   if not result:
498     return 1
499   result = list(result)
500   result.sort()
501   for path, tag in result:
502     ToStdout("%s %s", path, tag)
503
504
505 def _RenewCrypto(new_cluster_cert, new_rapi_cert, rapi_cert_filename,
506                  new_confd_hmac_key, force):
507   """Renews cluster certificates, keys and secrets.
508
509   @type new_cluster_cert: bool
510   @param new_cluster_cert: Whether to generate a new cluster certificate
511   @type new_rapi_cert: bool
512   @param new_rapi_cert: Whether to generate a new RAPI certificate
513   @type rapi_cert_filename: string
514   @param rapi_cert_filename: Path to file containing new RAPI certificate
515   @type new_confd_hmac_key: bool
516   @param new_confd_hmac_key: Whether to generate a new HMAC key
517   @type force: bool
518   @param force: Whether to ask user for confirmation
519
520   """
521   if new_rapi_cert and rapi_cert_filename:
522     ToStderr("Only one of the --new-rapi-certficate and --rapi-certificate"
523              " options can be specified at the same time.")
524     return 1
525
526   if rapi_cert_filename:
527     # Read and verify new certificate
528     try:
529       rapi_cert_pem = utils.ReadFile(rapi_cert_filename)
530
531       OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM,
532                                       rapi_cert_pem)
533     except Exception, err: # pylint: disable-msg=W0703
534       ToStderr("Can't load new RAPI certificate from %s: %s" %
535                (rapi_cert_filename, str(err)))
536       return 1
537
538     try:
539       OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, rapi_cert_pem)
540     except Exception, err: # pylint: disable-msg=W0703
541       ToStderr("Can't load new RAPI private key from %s: %s" %
542                (rapi_cert_filename, str(err)))
543       return 1
544
545   else:
546     rapi_cert_pem = None
547
548   if not force:
549     usertext = ("This requires all daemons on all nodes to be restarted and"
550                 " may take some time. Continue?")
551     if not AskUser(usertext):
552       return 1
553
554   def _RenewCryptoInner(ctx):
555     ctx.feedback_fn("Updating certificates and keys")
556     bootstrap.GenerateClusterCrypto(new_cluster_cert, new_rapi_cert,
557                                     new_confd_hmac_key,
558                                     rapi_cert_pem=rapi_cert_pem)
559
560     files_to_copy = []
561
562     if new_cluster_cert:
563       files_to_copy.append(constants.NODED_CERT_FILE)
564
565     if new_rapi_cert or rapi_cert_pem:
566       files_to_copy.append(constants.RAPI_CERT_FILE)
567
568     if new_confd_hmac_key:
569       files_to_copy.append(constants.CONFD_HMAC_KEY)
570
571     if files_to_copy:
572       for node_name in ctx.nonmaster_nodes:
573         ctx.feedback_fn("Copying %s to %s" %
574                         (", ".join(files_to_copy), node_name))
575         for file_name in files_to_copy:
576           ctx.ssh.CopyFileToNode(node_name, file_name)
577
578   RunWhileClusterStopped(ToStdout, _RenewCryptoInner)
579
580   ToStdout("All requested certificates and keys have been replaced."
581            " Running \"gnt-cluster verify\" now is recommended.")
582
583   return 0
584
585
586 def RenewCrypto(opts, args):
587   """Renews cluster certificates, keys and secrets.
588
589   """
590   return _RenewCrypto(opts.new_cluster_cert,
591                       opts.new_rapi_cert,
592                       opts.rapi_cert,
593                       opts.new_confd_hmac_key,
594                       opts.force)
595
596
597 def SetClusterParams(opts, args):
598   """Modify the cluster.
599
600   @param opts: the command line options selected by the user
601   @type args: list
602   @param args: should be an empty list
603   @rtype: int
604   @return: the desired exit code
605
606   """
607   if not (not opts.lvm_storage or opts.vg_name or
608           opts.enabled_hypervisors or opts.hvparams or
609           opts.beparams or opts.nicparams or
610           opts.candidate_pool_size is not None or
611           opts.uid_pool is not None or
612           opts.maintain_node_health is not None or
613           opts.add_uids is not None or
614           opts.remove_uids is not None):
615     ToStderr("Please give at least one of the parameters.")
616     return 1
617
618   vg_name = opts.vg_name
619   if not opts.lvm_storage and opts.vg_name:
620     ToStderr("Options --no-lvm-storage and --vg-name conflict.")
621     return 1
622
623   if not opts.lvm_storage:
624     vg_name = ""
625
626   hvlist = opts.enabled_hypervisors
627   if hvlist is not None:
628     hvlist = hvlist.split(",")
629
630   # a list of (name, dict) we can pass directly to dict() (or [])
631   hvparams = dict(opts.hvparams)
632   for hv_params in hvparams.values():
633     utils.ForceDictType(hv_params, constants.HVS_PARAMETER_TYPES)
634
635   beparams = opts.beparams
636   utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
637
638   nicparams = opts.nicparams
639   utils.ForceDictType(nicparams, constants.NICS_PARAMETER_TYPES)
640
641
642   mnh = opts.maintain_node_health
643
644   uid_pool = opts.uid_pool
645   if uid_pool is not None:
646     uid_pool = uidpool.ParseUidPool(uid_pool)
647
648   add_uids = opts.add_uids
649   if add_uids is not None:
650     add_uids = uidpool.ParseUidPool(add_uids)
651
652   remove_uids = opts.remove_uids
653   if remove_uids is not None:
654     remove_uids = uidpool.ParseUidPool(remove_uids)
655
656   op = opcodes.OpSetClusterParams(vg_name=vg_name,
657                                   enabled_hypervisors=hvlist,
658                                   hvparams=hvparams,
659                                   os_hvp=None,
660                                   beparams=beparams,
661                                   nicparams=nicparams,
662                                   candidate_pool_size=opts.candidate_pool_size,
663                                   maintain_node_health=mnh,
664                                   uid_pool=uid_pool,
665                                   add_uids=add_uids,
666                                   remove_uids=remove_uids)
667   SubmitOpCode(op, opts=opts)
668   return 0
669
670
671 def QueueOps(opts, args):
672   """Queue operations.
673
674   @param opts: the command line options selected by the user
675   @type args: list
676   @param args: should contain only one element, the subcommand
677   @rtype: int
678   @return: the desired exit code
679
680   """
681   command = args[0]
682   client = GetClient()
683   if command in ("drain", "undrain"):
684     drain_flag = command == "drain"
685     client.SetQueueDrainFlag(drain_flag)
686   elif command == "info":
687     result = client.QueryConfigValues(["drain_flag"])
688     if result[0]:
689       val = "set"
690     else:
691       val = "unset"
692     ToStdout("The drain flag is %s" % val)
693   else:
694     raise errors.OpPrereqError("Command '%s' is not valid." % command,
695                                errors.ECODE_INVAL)
696
697   return 0
698
699
700 def _ShowWatcherPause(until):
701   if until is None or until < time.time():
702     ToStdout("The watcher is not paused.")
703   else:
704     ToStdout("The watcher is paused until %s.", time.ctime(until))
705
706
707 def WatcherOps(opts, args):
708   """Watcher operations.
709
710   @param opts: the command line options selected by the user
711   @type args: list
712   @param args: should contain only one element, the subcommand
713   @rtype: int
714   @return: the desired exit code
715
716   """
717   command = args[0]
718   client = GetClient()
719
720   if command == "continue":
721     client.SetWatcherPause(None)
722     ToStdout("The watcher is no longer paused.")
723
724   elif command == "pause":
725     if len(args) < 2:
726       raise errors.OpPrereqError("Missing pause duration", errors.ECODE_INVAL)
727
728     result = client.SetWatcherPause(time.time() + ParseTimespec(args[1]))
729     _ShowWatcherPause(result)
730
731   elif command == "info":
732     result = client.QueryConfigValues(["watcher_pause"])
733     _ShowWatcherPause(result[0])
734
735   else:
736     raise errors.OpPrereqError("Command '%s' is not valid." % command,
737                                errors.ECODE_INVAL)
738
739   return 0
740
741
742 commands = {
743   'init': (
744     InitCluster, [ArgHost(min=1, max=1)],
745     [BACKEND_OPT, CP_SIZE_OPT, ENABLED_HV_OPT, GLOBAL_FILEDIR_OPT,
746      HVLIST_OPT, MAC_PREFIX_OPT, MASTER_NETDEV_OPT, NIC_PARAMS_OPT,
747      NOLVM_STORAGE_OPT, NOMODIFY_ETCHOSTS_OPT, NOMODIFY_SSH_SETUP_OPT,
748      SECONDARY_IP_OPT, VG_NAME_OPT, MAINTAIN_NODE_HEALTH_OPT,
749      UIDPOOL_OPT],
750     "[opts...] <cluster_name>", "Initialises a new cluster configuration"),
751   'destroy': (
752     DestroyCluster, ARGS_NONE, [YES_DOIT_OPT],
753     "", "Destroy cluster"),
754   'rename': (
755     RenameCluster, [ArgHost(min=1, max=1)],
756     [FORCE_OPT],
757     "<new_name>",
758     "Renames the cluster"),
759   'redist-conf': (
760     RedistributeConfig, ARGS_NONE, [SUBMIT_OPT],
761     "", "Forces a push of the configuration file and ssconf files"
762     " to the nodes in the cluster"),
763   'verify': (
764     VerifyCluster, ARGS_NONE,
765     [VERBOSE_OPT, DEBUG_SIMERR_OPT, ERROR_CODES_OPT, NONPLUS1_OPT],
766     "", "Does a check on the cluster configuration"),
767   'verify-disks': (
768     VerifyDisks, ARGS_NONE, [],
769     "", "Does a check on the cluster disk status"),
770   'repair-disk-sizes': (
771     RepairDiskSizes, ARGS_MANY_INSTANCES, [],
772     "", "Updates mismatches in recorded disk sizes"),
773   'masterfailover': (
774     MasterFailover, ARGS_NONE, [NOVOTING_OPT],
775     "", "Makes the current node the master"),
776   'version': (
777     ShowClusterVersion, ARGS_NONE, [],
778     "", "Shows the cluster version"),
779   'getmaster': (
780     ShowClusterMaster, ARGS_NONE, [],
781     "", "Shows the cluster master"),
782   'copyfile': (
783     ClusterCopyFile, [ArgFile(min=1, max=1)],
784     [NODE_LIST_OPT, USE_REPL_NET_OPT],
785     "[-n node...] <filename>", "Copies a file to all (or only some) nodes"),
786   'command': (
787     RunClusterCommand, [ArgCommand(min=1)],
788     [NODE_LIST_OPT],
789     "[-n node...] <command>", "Runs a command on all (or only some) nodes"),
790   'info': (
791     ShowClusterConfig, ARGS_NONE, [],
792     "", "Show cluster configuration"),
793   'list-tags': (
794     ListTags, ARGS_NONE, [], "", "List the tags of the cluster"),
795   'add-tags': (
796     AddTags, [ArgUnknown()], [TAG_SRC_OPT],
797     "tag...", "Add tags to the cluster"),
798   'remove-tags': (
799     RemoveTags, [ArgUnknown()], [TAG_SRC_OPT],
800     "tag...", "Remove tags from the cluster"),
801   'search-tags': (
802     SearchTags, [ArgUnknown(min=1, max=1)],
803     [], "", "Searches the tags on all objects on"
804     " the cluster for a given pattern (regex)"),
805   'queue': (
806     QueueOps,
807     [ArgChoice(min=1, max=1, choices=["drain", "undrain", "info"])],
808     [], "drain|undrain|info", "Change queue properties"),
809   'watcher': (
810     WatcherOps,
811     [ArgChoice(min=1, max=1, choices=["pause", "continue", "info"]),
812      ArgSuggest(min=0, max=1, choices=["30m", "1h", "4h"])],
813     [],
814     "{pause <timespec>|continue|info}", "Change watcher properties"),
815   'modify': (
816     SetClusterParams, ARGS_NONE,
817     [BACKEND_OPT, CP_SIZE_OPT, ENABLED_HV_OPT, HVLIST_OPT,
818      NIC_PARAMS_OPT, NOLVM_STORAGE_OPT, VG_NAME_OPT, MAINTAIN_NODE_HEALTH_OPT,
819      UIDPOOL_OPT, ADD_UIDS_OPT, REMOVE_UIDS_OPT],
820     "[opts...]",
821     "Alters the parameters of the cluster"),
822   "renew-crypto": (
823     RenewCrypto, ARGS_NONE,
824     [NEW_CLUSTER_CERT_OPT, NEW_RAPI_CERT_OPT, RAPI_CERT_OPT,
825      NEW_CONFD_HMAC_KEY_OPT, FORCE_OPT],
826     "[opts...]",
827     "Renews cluster certificates, keys and secrets"),
828   }
829
830
831 if __name__ == '__main__':
832   sys.exit(GenericMain(commands, override={"tag_type": constants.TAG_CLUSTER}))