cd0f719f5d505d5e98ca7553bed80895fd224bf2
[ganeti-local] / lib / client / gnt_cluster.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2010, 2011 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=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 os.path
30 import time
31 import OpenSSL
32 import itertools
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 from ganeti import compat
44 from ganeti import netutils
45
46
47 ON_OPT = cli_option("--on", default=False,
48                     action="store_true", dest="on",
49                     help="Recover from an EPO")
50
51 GROUPS_OPT = cli_option("--groups", default=False,
52                     action="store_true", dest="groups",
53                     help="Arguments are node groups instead of nodes")
54
55 _EPO_PING_INTERVAL = 30 # 30 seconds between pings
56 _EPO_PING_TIMEOUT = 1 # 1 second
57 _EPO_REACHABLE_TIMEOUT = 15 * 60 # 15 minutes
58
59
60 @UsesRPC
61 def InitCluster(opts, args):
62   """Initialize the cluster.
63
64   @param opts: the command line options selected by the user
65   @type args: list
66   @param args: should contain only one element, the desired
67       cluster name
68   @rtype: int
69   @return: the desired exit code
70
71   """
72   if not opts.lvm_storage and opts.vg_name:
73     ToStderr("Options --no-lvm-storage and --vg-name conflict.")
74     return 1
75
76   vg_name = opts.vg_name
77   if opts.lvm_storage and not opts.vg_name:
78     vg_name = constants.DEFAULT_VG
79
80   if not opts.drbd_storage and opts.drbd_helper:
81     ToStderr("Options --no-drbd-storage and --drbd-usermode-helper conflict.")
82     return 1
83
84   drbd_helper = opts.drbd_helper
85   if opts.drbd_storage and not opts.drbd_helper:
86     drbd_helper = constants.DEFAULT_DRBD_HELPER
87
88   master_netdev = opts.master_netdev
89   if master_netdev is None:
90     master_netdev = constants.DEFAULT_BRIDGE
91
92   hvlist = opts.enabled_hypervisors
93   if hvlist is None:
94     hvlist = constants.DEFAULT_ENABLED_HYPERVISOR
95   hvlist = hvlist.split(",")
96
97   hvparams = dict(opts.hvparams)
98   beparams = opts.beparams
99   nicparams = opts.nicparams
100
101   # prepare beparams dict
102   beparams = objects.FillDict(constants.BEC_DEFAULTS, beparams)
103   utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
104
105   # prepare nicparams dict
106   nicparams = objects.FillDict(constants.NICC_DEFAULTS, nicparams)
107   utils.ForceDictType(nicparams, constants.NICS_PARAMETER_TYPES)
108
109   # prepare ndparams dict
110   if opts.ndparams is None:
111     ndparams = dict(constants.NDC_DEFAULTS)
112   else:
113     ndparams = objects.FillDict(constants.NDC_DEFAULTS, opts.ndparams)
114     utils.ForceDictType(ndparams, constants.NDS_PARAMETER_TYPES)
115
116   # prepare hvparams dict
117   for hv in constants.HYPER_TYPES:
118     if hv not in hvparams:
119       hvparams[hv] = {}
120     hvparams[hv] = objects.FillDict(constants.HVC_DEFAULTS[hv], hvparams[hv])
121     utils.ForceDictType(hvparams[hv], constants.HVS_PARAMETER_TYPES)
122
123   if opts.candidate_pool_size is None:
124     opts.candidate_pool_size = constants.MASTER_POOL_SIZE_DEFAULT
125
126   if opts.mac_prefix is None:
127     opts.mac_prefix = constants.DEFAULT_MAC_PREFIX
128
129   uid_pool = opts.uid_pool
130   if uid_pool is not None:
131     uid_pool = uidpool.ParseUidPool(uid_pool)
132
133   if opts.prealloc_wipe_disks is None:
134     opts.prealloc_wipe_disks = False
135
136   try:
137     primary_ip_version = int(opts.primary_ip_version)
138   except (ValueError, TypeError), err:
139     ToStderr("Invalid primary ip version value: %s" % str(err))
140     return 1
141
142   master_netmask = opts.master_netmask
143   try:
144     if master_netmask is not None:
145       master_netmask = int(master_netmask)
146   except (ValueError, TypeError), err:
147     ToStderr("Invalid master netmask value: %s" % str(err))
148     return 1
149
150   bootstrap.InitCluster(cluster_name=args[0],
151                         secondary_ip=opts.secondary_ip,
152                         vg_name=vg_name,
153                         mac_prefix=opts.mac_prefix,
154                         master_netmask=master_netmask,
155                         master_netdev=master_netdev,
156                         file_storage_dir=opts.file_storage_dir,
157                         shared_file_storage_dir=opts.shared_file_storage_dir,
158                         enabled_hypervisors=hvlist,
159                         hvparams=hvparams,
160                         beparams=beparams,
161                         nicparams=nicparams,
162                         ndparams=ndparams,
163                         candidate_pool_size=opts.candidate_pool_size,
164                         modify_etc_hosts=opts.modify_etc_hosts,
165                         modify_ssh_setup=opts.modify_ssh_setup,
166                         maintain_node_health=opts.maintain_node_health,
167                         drbd_helper=drbd_helper,
168                         uid_pool=uid_pool,
169                         default_iallocator=opts.default_iallocator,
170                         primary_ip_version=primary_ip_version,
171                         prealloc_wipe_disks=opts.prealloc_wipe_disks,
172                         )
173   op = opcodes.OpClusterPostInit()
174   SubmitOpCode(op, opts=opts)
175   return 0
176
177
178 @UsesRPC
179 def DestroyCluster(opts, args):
180   """Destroy the cluster.
181
182   @param opts: the command line options selected by the user
183   @type args: list
184   @param args: should be an empty list
185   @rtype: int
186   @return: the desired exit code
187
188   """
189   if not opts.yes_do_it:
190     ToStderr("Destroying a cluster is irreversible. If you really want"
191              " destroy this cluster, supply the --yes-do-it option.")
192     return 1
193
194   op = opcodes.OpClusterDestroy()
195   master = SubmitOpCode(op, opts=opts)
196   # if we reached this, the opcode didn't fail; we can proceed to
197   # shutdown all the daemons
198   bootstrap.FinalizeClusterDestroy(master)
199   return 0
200
201
202 def RenameCluster(opts, args):
203   """Rename the cluster.
204
205   @param opts: the command line options selected by the user
206   @type args: list
207   @param args: should contain only one element, the new cluster name
208   @rtype: int
209   @return: the desired exit code
210
211   """
212   cl = GetClient()
213
214   (cluster_name, ) = cl.QueryConfigValues(["cluster_name"])
215
216   new_name = args[0]
217   if not opts.force:
218     usertext = ("This will rename the cluster from '%s' to '%s'. If you are"
219                 " connected over the network to the cluster name, the"
220                 " operation is very dangerous as the IP address will be"
221                 " removed from the node and the change may not go through."
222                 " Continue?") % (cluster_name, new_name)
223     if not AskUser(usertext):
224       return 1
225
226   op = opcodes.OpClusterRename(name=new_name)
227   result = SubmitOpCode(op, opts=opts, cl=cl)
228
229   if result:
230     ToStdout("Cluster renamed from '%s' to '%s'", cluster_name, result)
231
232   return 0
233
234
235 def ActivateMasterIp(opts, args):
236   """Activates the master IP.
237
238   """
239   op = opcodes.OpClusterActivateMasterIp()
240   SubmitOpCode(op)
241   return 0
242
243
244 def DeactivateMasterIp(opts, args):
245   """Deactivates the master IP.
246
247   """
248   if not opts.confirm:
249     usertext = ("This will disable the master IP. All the open connections to"
250                 " the master IP will be closed. To reach the master you will"
251                 " need to use its node IP."
252                 " Continue?")
253     if not AskUser(usertext):
254       return 1
255
256   op = opcodes.OpClusterDeactivateMasterIp()
257   SubmitOpCode(op)
258   return 0
259
260
261 def RedistributeConfig(opts, args):
262   """Forces push of the cluster configuration.
263
264   @param opts: the command line options selected by the user
265   @type args: list
266   @param args: empty list
267   @rtype: int
268   @return: the desired exit code
269
270   """
271   op = opcodes.OpClusterRedistConf()
272   SubmitOrSend(op, opts)
273   return 0
274
275
276 def ShowClusterVersion(opts, args):
277   """Write version of ganeti software to the standard output.
278
279   @param opts: the command line options selected by the user
280   @type args: list
281   @param args: should be an empty list
282   @rtype: int
283   @return: the desired exit code
284
285   """
286   cl = GetClient()
287   result = cl.QueryClusterInfo()
288   ToStdout("Software version: %s", result["software_version"])
289   ToStdout("Internode protocol: %s", result["protocol_version"])
290   ToStdout("Configuration format: %s", result["config_version"])
291   ToStdout("OS api version: %s", result["os_api_version"])
292   ToStdout("Export interface: %s", result["export_version"])
293   return 0
294
295
296 def ShowClusterMaster(opts, args):
297   """Write name of master node to the standard output.
298
299   @param opts: the command line options selected by the user
300   @type args: list
301   @param args: should be an empty list
302   @rtype: int
303   @return: the desired exit code
304
305   """
306   master = bootstrap.GetMaster()
307   ToStdout(master)
308   return 0
309
310
311 def _PrintGroupedParams(paramsdict, level=1, roman=False):
312   """Print Grouped parameters (be, nic, disk) by group.
313
314   @type paramsdict: dict of dicts
315   @param paramsdict: {group: {param: value, ...}, ...}
316   @type level: int
317   @param level: Level of indention
318
319   """
320   indent = "  " * level
321   for item, val in sorted(paramsdict.items()):
322     if isinstance(val, dict):
323       ToStdout("%s- %s:", indent, item)
324       _PrintGroupedParams(val, level=level + 1, roman=roman)
325     elif roman and isinstance(val, int):
326       ToStdout("%s  %s: %s", indent, item, compat.TryToRoman(val))
327     else:
328       ToStdout("%s  %s: %s", indent, item, val)
329
330
331 def ShowClusterConfig(opts, args):
332   """Shows cluster information.
333
334   @param opts: the command line options selected by the user
335   @type args: list
336   @param args: should be an empty list
337   @rtype: int
338   @return: the desired exit code
339
340   """
341   cl = GetClient()
342   result = cl.QueryClusterInfo()
343
344   ToStdout("Cluster name: %s", result["name"])
345   ToStdout("Cluster UUID: %s", result["uuid"])
346
347   ToStdout("Creation time: %s", utils.FormatTime(result["ctime"]))
348   ToStdout("Modification time: %s", utils.FormatTime(result["mtime"]))
349
350   ToStdout("Master node: %s", result["master"])
351
352   ToStdout("Architecture (this node): %s (%s)",
353            result["architecture"][0], result["architecture"][1])
354
355   if result["tags"]:
356     tags = utils.CommaJoin(utils.NiceSort(result["tags"]))
357   else:
358     tags = "(none)"
359
360   ToStdout("Tags: %s", tags)
361
362   ToStdout("Default hypervisor: %s", result["default_hypervisor"])
363   ToStdout("Enabled hypervisors: %s",
364            utils.CommaJoin(result["enabled_hypervisors"]))
365
366   ToStdout("Hypervisor parameters:")
367   _PrintGroupedParams(result["hvparams"])
368
369   ToStdout("OS-specific hypervisor parameters:")
370   _PrintGroupedParams(result["os_hvp"])
371
372   ToStdout("OS parameters:")
373   _PrintGroupedParams(result["osparams"])
374
375   ToStdout("Hidden OSes: %s", utils.CommaJoin(result["hidden_os"]))
376   ToStdout("Blacklisted OSes: %s", utils.CommaJoin(result["blacklisted_os"]))
377
378   ToStdout("Cluster parameters:")
379   ToStdout("  - candidate pool size: %s",
380             compat.TryToRoman(result["candidate_pool_size"],
381                               convert=opts.roman_integers))
382   ToStdout("  - master netdev: %s", result["master_netdev"])
383   ToStdout("  - master netmask: %s", result["master_netmask"])
384   ToStdout("  - lvm volume group: %s", result["volume_group_name"])
385   if result["reserved_lvs"]:
386     reserved_lvs = utils.CommaJoin(result["reserved_lvs"])
387   else:
388     reserved_lvs = "(none)"
389   ToStdout("  - lvm reserved volumes: %s", reserved_lvs)
390   ToStdout("  - drbd usermode helper: %s", result["drbd_usermode_helper"])
391   ToStdout("  - file storage path: %s", result["file_storage_dir"])
392   ToStdout("  - shared file storage path: %s",
393            result["shared_file_storage_dir"])
394   ToStdout("  - maintenance of node health: %s",
395            result["maintain_node_health"])
396   ToStdout("  - uid pool: %s",
397             uidpool.FormatUidPool(result["uid_pool"],
398                                   roman=opts.roman_integers))
399   ToStdout("  - default instance allocator: %s", result["default_iallocator"])
400   ToStdout("  - primary ip version: %d", result["primary_ip_version"])
401   ToStdout("  - preallocation wipe disks: %s", result["prealloc_wipe_disks"])
402   ToStdout("  - OS search path: %s", utils.CommaJoin(constants.OS_SEARCH_PATH))
403
404   ToStdout("Default node parameters:")
405   _PrintGroupedParams(result["ndparams"], roman=opts.roman_integers)
406
407   ToStdout("Default instance parameters:")
408   _PrintGroupedParams(result["beparams"], roman=opts.roman_integers)
409
410   ToStdout("Default nic parameters:")
411   _PrintGroupedParams(result["nicparams"], roman=opts.roman_integers)
412
413   return 0
414
415
416 def ClusterCopyFile(opts, args):
417   """Copy a file from master to some nodes.
418
419   @param opts: the command line options selected by the user
420   @type args: list
421   @param args: should contain only one element, the path of
422       the file to be copied
423   @rtype: int
424   @return: the desired exit code
425
426   """
427   filename = args[0]
428   if not os.path.exists(filename):
429     raise errors.OpPrereqError("No such filename '%s'" % filename,
430                                errors.ECODE_INVAL)
431
432   cl = GetClient()
433
434   cluster_name = cl.QueryConfigValues(["cluster_name"])[0]
435
436   results = GetOnlineNodes(nodes=opts.nodes, cl=cl, filter_master=True,
437                            secondary_ips=opts.use_replication_network,
438                            nodegroup=opts.nodegroup)
439
440   srun = ssh.SshRunner(cluster_name=cluster_name)
441   for node in results:
442     if not srun.CopyFileToNode(node, filename):
443       ToStderr("Copy of file %s to node %s failed", filename, node)
444
445   return 0
446
447
448 def RunClusterCommand(opts, args):
449   """Run a command on some nodes.
450
451   @param opts: the command line options selected by the user
452   @type args: list
453   @param args: should contain the command to be run and its arguments
454   @rtype: int
455   @return: the desired exit code
456
457   """
458   cl = GetClient()
459
460   command = " ".join(args)
461
462   nodes = GetOnlineNodes(nodes=opts.nodes, cl=cl, nodegroup=opts.nodegroup)
463
464   cluster_name, master_node = cl.QueryConfigValues(["cluster_name",
465                                                     "master_node"])
466
467   srun = ssh.SshRunner(cluster_name=cluster_name)
468
469   # Make sure master node is at list end
470   if master_node in nodes:
471     nodes.remove(master_node)
472     nodes.append(master_node)
473
474   for name in nodes:
475     result = srun.Run(name, "root", command)
476     ToStdout("------------------------------------------------")
477     ToStdout("node: %s", name)
478     ToStdout("%s", result.output)
479     ToStdout("return code = %s", result.exit_code)
480
481   return 0
482
483
484 def VerifyCluster(opts, args):
485   """Verify integrity of cluster, performing various test on nodes.
486
487   @param opts: the command line options selected by the user
488   @type args: list
489   @param args: should be an empty list
490   @rtype: int
491   @return: the desired exit code
492
493   """
494   skip_checks = []
495
496   if opts.skip_nplusone_mem:
497     skip_checks.append(constants.VERIFY_NPLUSONE_MEM)
498
499   cl = GetClient()
500
501   op = opcodes.OpClusterVerify(verbose=opts.verbose,
502                                error_codes=opts.error_codes,
503                                debug_simulate_errors=opts.simulate_errors,
504                                skip_checks=skip_checks,
505                                group_name=opts.nodegroup)
506   result = SubmitOpCode(op, cl=cl, opts=opts)
507
508   # Keep track of submitted jobs
509   jex = JobExecutor(cl=cl, opts=opts)
510
511   for (status, job_id) in result[constants.JOB_IDS_KEY]:
512     jex.AddJobId(None, status, job_id)
513
514   results = jex.GetResults()
515
516   (bad_jobs, bad_results) = \
517     map(len,
518         # Convert iterators to lists
519         map(list,
520             # Count errors
521             map(compat.partial(itertools.ifilterfalse, bool),
522                 # Convert result to booleans in a tuple
523                 zip(*((job_success, len(op_results) == 1 and op_results[0])
524                       for (job_success, op_results) in results)))))
525
526   if bad_jobs == 0 and bad_results == 0:
527     rcode = constants.EXIT_SUCCESS
528   else:
529     rcode = constants.EXIT_FAILURE
530     if bad_jobs > 0:
531       ToStdout("%s job(s) failed while verifying the cluster.", bad_jobs)
532
533   return rcode
534
535
536 def VerifyDisks(opts, args):
537   """Verify integrity of cluster disks.
538
539   @param opts: the command line options selected by the user
540   @type args: list
541   @param args: should be an empty list
542   @rtype: int
543   @return: the desired exit code
544
545   """
546   cl = GetClient()
547
548   op = opcodes.OpClusterVerifyDisks()
549
550   result = SubmitOpCode(op, cl=cl, opts=opts)
551
552   # Keep track of submitted jobs
553   jex = JobExecutor(cl=cl, opts=opts)
554
555   for (status, job_id) in result[constants.JOB_IDS_KEY]:
556     jex.AddJobId(None, status, job_id)
557
558   retcode = constants.EXIT_SUCCESS
559
560   for (status, result) in jex.GetResults():
561     if not status:
562       ToStdout("Job failed: %s", result)
563       continue
564
565     ((bad_nodes, instances, missing), ) = result
566
567     for node, text in bad_nodes.items():
568       ToStdout("Error gathering data on node %s: %s",
569                node, utils.SafeEncode(text[-400:]))
570       retcode = constants.EXIT_FAILURE
571       ToStdout("You need to fix these nodes first before fixing instances")
572
573     for iname in instances:
574       if iname in missing:
575         continue
576       op = opcodes.OpInstanceActivateDisks(instance_name=iname)
577       try:
578         ToStdout("Activating disks for instance '%s'", iname)
579         SubmitOpCode(op, opts=opts, cl=cl)
580       except errors.GenericError, err:
581         nret, msg = FormatError(err)
582         retcode |= nret
583         ToStderr("Error activating disks for instance %s: %s", iname, msg)
584
585     if missing:
586       for iname, ival in missing.iteritems():
587         all_missing = compat.all(x[0] in bad_nodes for x in ival)
588         if all_missing:
589           ToStdout("Instance %s cannot be verified as it lives on"
590                    " broken nodes", iname)
591         else:
592           ToStdout("Instance %s has missing logical volumes:", iname)
593           ival.sort()
594           for node, vol in ival:
595             if node in bad_nodes:
596               ToStdout("\tbroken node %s /dev/%s", node, vol)
597             else:
598               ToStdout("\t%s /dev/%s", node, vol)
599
600       ToStdout("You need to replace or recreate disks for all the above"
601                " instances if this message persists after fixing broken nodes.")
602       retcode = constants.EXIT_FAILURE
603
604   return retcode
605
606
607 def RepairDiskSizes(opts, args):
608   """Verify sizes of cluster disks.
609
610   @param opts: the command line options selected by the user
611   @type args: list
612   @param args: optional list of instances to restrict check to
613   @rtype: int
614   @return: the desired exit code
615
616   """
617   op = opcodes.OpClusterRepairDiskSizes(instances=args)
618   SubmitOpCode(op, opts=opts)
619
620
621 @UsesRPC
622 def MasterFailover(opts, args):
623   """Failover the master node.
624
625   This command, when run on a non-master node, will cause the current
626   master to cease being master, and the non-master to become new
627   master.
628
629   @param opts: the command line options selected by the user
630   @type args: list
631   @param args: should be an empty list
632   @rtype: int
633   @return: the desired exit code
634
635   """
636   if opts.no_voting:
637     usertext = ("This will perform the failover even if most other nodes"
638                 " are down, or if this node is outdated. This is dangerous"
639                 " as it can lead to a non-consistent cluster. Check the"
640                 " gnt-cluster(8) man page before proceeding. Continue?")
641     if not AskUser(usertext):
642       return 1
643
644   return bootstrap.MasterFailover(no_voting=opts.no_voting)
645
646
647 def MasterPing(opts, args):
648   """Checks if the master is alive.
649
650   @param opts: the command line options selected by the user
651   @type args: list
652   @param args: should be an empty list
653   @rtype: int
654   @return: the desired exit code
655
656   """
657   try:
658     cl = GetClient()
659     cl.QueryClusterInfo()
660     return 0
661   except Exception: # pylint: disable=W0703
662     return 1
663
664
665 def SearchTags(opts, args):
666   """Searches the tags on all the cluster.
667
668   @param opts: the command line options selected by the user
669   @type args: list
670   @param args: should contain only one element, the tag pattern
671   @rtype: int
672   @return: the desired exit code
673
674   """
675   op = opcodes.OpTagsSearch(pattern=args[0])
676   result = SubmitOpCode(op, opts=opts)
677   if not result:
678     return 1
679   result = list(result)
680   result.sort()
681   for path, tag in result:
682     ToStdout("%s %s", path, tag)
683
684
685 def _ReadAndVerifyCert(cert_filename, verify_private_key=False):
686   """Reads and verifies an X509 certificate.
687
688   @type cert_filename: string
689   @param cert_filename: the path of the file containing the certificate to
690                         verify encoded in PEM format
691   @type verify_private_key: bool
692   @param verify_private_key: whether to verify the private key in addition to
693                              the public certificate
694   @rtype: string
695   @return: a string containing the PEM-encoded certificate.
696
697   """
698   try:
699     pem = utils.ReadFile(cert_filename)
700   except IOError, err:
701     raise errors.X509CertError(cert_filename,
702                                "Unable to read certificate: %s" % str(err))
703
704   try:
705     OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, pem)
706   except Exception, err:
707     raise errors.X509CertError(cert_filename,
708                                "Unable to load certificate: %s" % str(err))
709
710   if verify_private_key:
711     try:
712       OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, pem)
713     except Exception, err:
714       raise errors.X509CertError(cert_filename,
715                                  "Unable to load private key: %s" % str(err))
716
717   return pem
718
719
720 def _RenewCrypto(new_cluster_cert, new_rapi_cert, #pylint: disable=R0911
721                  rapi_cert_filename, new_spice_cert, spice_cert_filename,
722                  spice_cacert_filename, new_confd_hmac_key, new_cds,
723                  cds_filename, force):
724   """Renews cluster certificates, keys and secrets.
725
726   @type new_cluster_cert: bool
727   @param new_cluster_cert: Whether to generate a new cluster certificate
728   @type new_rapi_cert: bool
729   @param new_rapi_cert: Whether to generate a new RAPI certificate
730   @type rapi_cert_filename: string
731   @param rapi_cert_filename: Path to file containing new RAPI certificate
732   @type new_spice_cert: bool
733   @param new_spice_cert: Whether to generate a new SPICE certificate
734   @type spice_cert_filename: string
735   @param spice_cert_filename: Path to file containing new SPICE certificate
736   @type spice_cacert_filename: string
737   @param spice_cacert_filename: Path to file containing the certificate of the
738                                 CA that signed the SPICE certificate
739   @type new_confd_hmac_key: bool
740   @param new_confd_hmac_key: Whether to generate a new HMAC key
741   @type new_cds: bool
742   @param new_cds: Whether to generate a new cluster domain secret
743   @type cds_filename: string
744   @param cds_filename: Path to file containing new cluster domain secret
745   @type force: bool
746   @param force: Whether to ask user for confirmation
747
748   """
749   if new_rapi_cert and rapi_cert_filename:
750     ToStderr("Only one of the --new-rapi-certificate and --rapi-certificate"
751              " options can be specified at the same time.")
752     return 1
753
754   if new_cds and cds_filename:
755     ToStderr("Only one of the --new-cluster-domain-secret and"
756              " --cluster-domain-secret options can be specified at"
757              " the same time.")
758     return 1
759
760   if new_spice_cert and (spice_cert_filename or spice_cacert_filename):
761     ToStderr("When using --new-spice-certificate, the --spice-certificate"
762              " and --spice-ca-certificate must not be used.")
763     return 1
764
765   if bool(spice_cacert_filename) ^ bool(spice_cert_filename):
766     ToStderr("Both --spice-certificate and --spice-ca-certificate must be"
767              " specified.")
768     return 1
769
770   rapi_cert_pem, spice_cert_pem, spice_cacert_pem = (None, None, None)
771   try:
772     if rapi_cert_filename:
773       rapi_cert_pem = _ReadAndVerifyCert(rapi_cert_filename, True)
774     if spice_cert_filename:
775       spice_cert_pem = _ReadAndVerifyCert(spice_cert_filename, True)
776       spice_cacert_pem = _ReadAndVerifyCert(spice_cacert_filename)
777   except errors.X509CertError, err:
778     ToStderr("Unable to load X509 certificate from %s: %s", err[0], err[1])
779     return 1
780
781   if cds_filename:
782     try:
783       cds = utils.ReadFile(cds_filename)
784     except Exception, err: # pylint: disable=W0703
785       ToStderr("Can't load new cluster domain secret from %s: %s" %
786                (cds_filename, str(err)))
787       return 1
788   else:
789     cds = None
790
791   if not force:
792     usertext = ("This requires all daemons on all nodes to be restarted and"
793                 " may take some time. Continue?")
794     if not AskUser(usertext):
795       return 1
796
797   def _RenewCryptoInner(ctx):
798     ctx.feedback_fn("Updating certificates and keys")
799     bootstrap.GenerateClusterCrypto(new_cluster_cert,
800                                     new_rapi_cert,
801                                     new_spice_cert,
802                                     new_confd_hmac_key,
803                                     new_cds,
804                                     rapi_cert_pem=rapi_cert_pem,
805                                     spice_cert_pem=spice_cert_pem,
806                                     spice_cacert_pem=spice_cacert_pem,
807                                     cds=cds)
808
809     files_to_copy = []
810
811     if new_cluster_cert:
812       files_to_copy.append(constants.NODED_CERT_FILE)
813
814     if new_rapi_cert or rapi_cert_pem:
815       files_to_copy.append(constants.RAPI_CERT_FILE)
816
817     if new_spice_cert or spice_cert_pem:
818       files_to_copy.append(constants.SPICE_CERT_FILE)
819       files_to_copy.append(constants.SPICE_CACERT_FILE)
820
821     if new_confd_hmac_key:
822       files_to_copy.append(constants.CONFD_HMAC_KEY)
823
824     if new_cds or cds:
825       files_to_copy.append(constants.CLUSTER_DOMAIN_SECRET_FILE)
826
827     if files_to_copy:
828       for node_name in ctx.nonmaster_nodes:
829         ctx.feedback_fn("Copying %s to %s" %
830                         (", ".join(files_to_copy), node_name))
831         for file_name in files_to_copy:
832           ctx.ssh.CopyFileToNode(node_name, file_name)
833
834   RunWhileClusterStopped(ToStdout, _RenewCryptoInner)
835
836   ToStdout("All requested certificates and keys have been replaced."
837            " Running \"gnt-cluster verify\" now is recommended.")
838
839   return 0
840
841
842 def RenewCrypto(opts, args):
843   """Renews cluster certificates, keys and secrets.
844
845   """
846   return _RenewCrypto(opts.new_cluster_cert,
847                       opts.new_rapi_cert,
848                       opts.rapi_cert,
849                       opts.new_spice_cert,
850                       opts.spice_cert,
851                       opts.spice_cacert,
852                       opts.new_confd_hmac_key,
853                       opts.new_cluster_domain_secret,
854                       opts.cluster_domain_secret,
855                       opts.force)
856
857
858 def SetClusterParams(opts, args):
859   """Modify the cluster.
860
861   @param opts: the command line options selected by the user
862   @type args: list
863   @param args: should be an empty list
864   @rtype: int
865   @return: the desired exit code
866
867   """
868   if not (not opts.lvm_storage or opts.vg_name or
869           not opts.drbd_storage or opts.drbd_helper or
870           opts.enabled_hypervisors or opts.hvparams or
871           opts.beparams or opts.nicparams or opts.ndparams or
872           opts.candidate_pool_size is not None or
873           opts.uid_pool is not None or
874           opts.maintain_node_health is not None or
875           opts.add_uids is not None or
876           opts.remove_uids is not None or
877           opts.default_iallocator is not None or
878           opts.reserved_lvs is not None or
879           opts.master_netdev is not None or
880           opts.master_netmask is not None or
881           opts.prealloc_wipe_disks is not None):
882     ToStderr("Please give at least one of the parameters.")
883     return 1
884
885   vg_name = opts.vg_name
886   if not opts.lvm_storage and opts.vg_name:
887     ToStderr("Options --no-lvm-storage and --vg-name conflict.")
888     return 1
889
890   if not opts.lvm_storage:
891     vg_name = ""
892
893   drbd_helper = opts.drbd_helper
894   if not opts.drbd_storage and opts.drbd_helper:
895     ToStderr("Options --no-drbd-storage and --drbd-usermode-helper conflict.")
896     return 1
897
898   if not opts.drbd_storage:
899     drbd_helper = ""
900
901   hvlist = opts.enabled_hypervisors
902   if hvlist is not None:
903     hvlist = hvlist.split(",")
904
905   # a list of (name, dict) we can pass directly to dict() (or [])
906   hvparams = dict(opts.hvparams)
907   for hv_params in hvparams.values():
908     utils.ForceDictType(hv_params, constants.HVS_PARAMETER_TYPES)
909
910   beparams = opts.beparams
911   utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
912
913   nicparams = opts.nicparams
914   utils.ForceDictType(nicparams, constants.NICS_PARAMETER_TYPES)
915
916   ndparams = opts.ndparams
917   if ndparams is not None:
918     utils.ForceDictType(ndparams, constants.NDS_PARAMETER_TYPES)
919
920   mnh = opts.maintain_node_health
921
922   uid_pool = opts.uid_pool
923   if uid_pool is not None:
924     uid_pool = uidpool.ParseUidPool(uid_pool)
925
926   add_uids = opts.add_uids
927   if add_uids is not None:
928     add_uids = uidpool.ParseUidPool(add_uids)
929
930   remove_uids = opts.remove_uids
931   if remove_uids is not None:
932     remove_uids = uidpool.ParseUidPool(remove_uids)
933
934   if opts.reserved_lvs is not None:
935     if opts.reserved_lvs == "":
936       opts.reserved_lvs = []
937     else:
938       opts.reserved_lvs = utils.UnescapeAndSplit(opts.reserved_lvs, sep=",")
939
940   if opts.master_netmask is not None:
941     try:
942       opts.master_netmask = int(opts.master_netmask)
943     except ValueError:
944       ToStderr("The --master-netmask option expects an int parameter.")
945       return 1
946
947   op = opcodes.OpClusterSetParams(vg_name=vg_name,
948                                   drbd_helper=drbd_helper,
949                                   enabled_hypervisors=hvlist,
950                                   hvparams=hvparams,
951                                   os_hvp=None,
952                                   beparams=beparams,
953                                   nicparams=nicparams,
954                                   ndparams=ndparams,
955                                   candidate_pool_size=opts.candidate_pool_size,
956                                   maintain_node_health=mnh,
957                                   uid_pool=uid_pool,
958                                   add_uids=add_uids,
959                                   remove_uids=remove_uids,
960                                   default_iallocator=opts.default_iallocator,
961                                   prealloc_wipe_disks=opts.prealloc_wipe_disks,
962                                   master_netdev=opts.master_netdev,
963                                   master_netmask=opts.master_netmask,
964                                   reserved_lvs=opts.reserved_lvs)
965   SubmitOpCode(op, opts=opts)
966   return 0
967
968
969 def QueueOps(opts, args):
970   """Queue operations.
971
972   @param opts: the command line options selected by the user
973   @type args: list
974   @param args: should contain only one element, the subcommand
975   @rtype: int
976   @return: the desired exit code
977
978   """
979   command = args[0]
980   client = GetClient()
981   if command in ("drain", "undrain"):
982     drain_flag = command == "drain"
983     client.SetQueueDrainFlag(drain_flag)
984   elif command == "info":
985     result = client.QueryConfigValues(["drain_flag"])
986     if result[0]:
987       val = "set"
988     else:
989       val = "unset"
990     ToStdout("The drain flag is %s" % val)
991   else:
992     raise errors.OpPrereqError("Command '%s' is not valid." % command,
993                                errors.ECODE_INVAL)
994
995   return 0
996
997
998 def _ShowWatcherPause(until):
999   if until is None or until < time.time():
1000     ToStdout("The watcher is not paused.")
1001   else:
1002     ToStdout("The watcher is paused until %s.", time.ctime(until))
1003
1004
1005 def WatcherOps(opts, args):
1006   """Watcher operations.
1007
1008   @param opts: the command line options selected by the user
1009   @type args: list
1010   @param args: should contain only one element, the subcommand
1011   @rtype: int
1012   @return: the desired exit code
1013
1014   """
1015   command = args[0]
1016   client = GetClient()
1017
1018   if command == "continue":
1019     client.SetWatcherPause(None)
1020     ToStdout("The watcher is no longer paused.")
1021
1022   elif command == "pause":
1023     if len(args) < 2:
1024       raise errors.OpPrereqError("Missing pause duration", errors.ECODE_INVAL)
1025
1026     result = client.SetWatcherPause(time.time() + ParseTimespec(args[1]))
1027     _ShowWatcherPause(result)
1028
1029   elif command == "info":
1030     result = client.QueryConfigValues(["watcher_pause"])
1031     _ShowWatcherPause(result[0])
1032
1033   else:
1034     raise errors.OpPrereqError("Command '%s' is not valid." % command,
1035                                errors.ECODE_INVAL)
1036
1037   return 0
1038
1039
1040 def _OobPower(opts, node_list, power):
1041   """Puts the node in the list to desired power state.
1042
1043   @param opts: The command line options selected by the user
1044   @param node_list: The list of nodes to operate on
1045   @param power: True if they should be powered on, False otherwise
1046   @return: The success of the operation (none failed)
1047
1048   """
1049   if power:
1050     command = constants.OOB_POWER_ON
1051   else:
1052     command = constants.OOB_POWER_OFF
1053
1054   op = opcodes.OpOobCommand(node_names=node_list,
1055                             command=command,
1056                             ignore_status=True,
1057                             timeout=opts.oob_timeout,
1058                             power_delay=opts.power_delay)
1059   result = SubmitOpCode(op, opts=opts)
1060   errs = 0
1061   for node_result in result:
1062     (node_tuple, data_tuple) = node_result
1063     (_, node_name) = node_tuple
1064     (data_status, _) = data_tuple
1065     if data_status != constants.RS_NORMAL:
1066       assert data_status != constants.RS_UNAVAIL
1067       errs += 1
1068       ToStderr("There was a problem changing power for %s, please investigate",
1069                node_name)
1070
1071   if errs > 0:
1072     return False
1073
1074   return True
1075
1076
1077 def _InstanceStart(opts, inst_list, start):
1078   """Puts the instances in the list to desired state.
1079
1080   @param opts: The command line options selected by the user
1081   @param inst_list: The list of instances to operate on
1082   @param start: True if they should be started, False for shutdown
1083   @return: The success of the operation (none failed)
1084
1085   """
1086   if start:
1087     opcls = opcodes.OpInstanceStartup
1088     text_submit, text_success, text_failed = ("startup", "started", "starting")
1089   else:
1090     opcls = compat.partial(opcodes.OpInstanceShutdown,
1091                            timeout=opts.shutdown_timeout)
1092     text_submit, text_success, text_failed = ("shutdown", "stopped", "stopping")
1093
1094   jex = JobExecutor(opts=opts)
1095
1096   for inst in inst_list:
1097     ToStdout("Submit %s of instance %s", text_submit, inst)
1098     op = opcls(instance_name=inst)
1099     jex.QueueJob(inst, op)
1100
1101   results = jex.GetResults()
1102   bad_cnt = len([1 for (success, _) in results if not success])
1103
1104   if bad_cnt == 0:
1105     ToStdout("All instances have been %s successfully", text_success)
1106   else:
1107     ToStderr("There were errors while %s instances:\n"
1108              "%d error(s) out of %d instance(s)", text_failed, bad_cnt,
1109              len(results))
1110     return False
1111
1112   return True
1113
1114
1115 class _RunWhenNodesReachableHelper:
1116   """Helper class to make shared internal state sharing easier.
1117
1118   @ivar success: Indicates if all action_cb calls were successful
1119
1120   """
1121   def __init__(self, node_list, action_cb, node2ip, port, feedback_fn,
1122                _ping_fn=netutils.TcpPing, _sleep_fn=time.sleep):
1123     """Init the object.
1124
1125     @param node_list: The list of nodes to be reachable
1126     @param action_cb: Callback called when a new host is reachable
1127     @type node2ip: dict
1128     @param node2ip: Node to ip mapping
1129     @param port: The port to use for the TCP ping
1130     @param feedback_fn: The function used for feedback
1131     @param _ping_fn: Function to check reachabilty (for unittest use only)
1132     @param _sleep_fn: Function to sleep (for unittest use only)
1133
1134     """
1135     self.down = set(node_list)
1136     self.up = set()
1137     self.node2ip = node2ip
1138     self.success = True
1139     self.action_cb = action_cb
1140     self.port = port
1141     self.feedback_fn = feedback_fn
1142     self._ping_fn = _ping_fn
1143     self._sleep_fn = _sleep_fn
1144
1145   def __call__(self):
1146     """When called we run action_cb.
1147
1148     @raises utils.RetryAgain: When there are still down nodes
1149
1150     """
1151     if not self.action_cb(self.up):
1152       self.success = False
1153
1154     if self.down:
1155       raise utils.RetryAgain()
1156     else:
1157       return self.success
1158
1159   def Wait(self, secs):
1160     """Checks if a host is up or waits remaining seconds.
1161
1162     @param secs: The secs remaining
1163
1164     """
1165     start = time.time()
1166     for node in self.down:
1167       if self._ping_fn(self.node2ip[node], self.port, timeout=_EPO_PING_TIMEOUT,
1168                        live_port_needed=True):
1169         self.feedback_fn("Node %s became available" % node)
1170         self.up.add(node)
1171         self.down -= self.up
1172         # If we have a node available there is the possibility to run the
1173         # action callback successfully, therefore we don't wait and return
1174         return
1175
1176     self._sleep_fn(max(0.0, start + secs - time.time()))
1177
1178
1179 def _RunWhenNodesReachable(node_list, action_cb, interval):
1180   """Run action_cb when nodes become reachable.
1181
1182   @param node_list: The list of nodes to be reachable
1183   @param action_cb: Callback called when a new host is reachable
1184   @param interval: The earliest time to retry
1185
1186   """
1187   client = GetClient()
1188   cluster_info = client.QueryClusterInfo()
1189   if cluster_info["primary_ip_version"] == constants.IP4_VERSION:
1190     family = netutils.IPAddress.family
1191   else:
1192     family = netutils.IP6Address.family
1193
1194   node2ip = dict((node, netutils.GetHostname(node, family=family).ip)
1195                  for node in node_list)
1196
1197   port = netutils.GetDaemonPort(constants.NODED)
1198   helper = _RunWhenNodesReachableHelper(node_list, action_cb, node2ip, port,
1199                                         ToStdout)
1200
1201   try:
1202     return utils.Retry(helper, interval, _EPO_REACHABLE_TIMEOUT,
1203                        wait_fn=helper.Wait)
1204   except utils.RetryTimeout:
1205     ToStderr("Time exceeded while waiting for nodes to become reachable"
1206              " again:\n  - %s", "  - ".join(helper.down))
1207     return False
1208
1209
1210 def _MaybeInstanceStartup(opts, inst_map, nodes_online,
1211                           _instance_start_fn=_InstanceStart):
1212   """Start the instances conditional based on node_states.
1213
1214   @param opts: The command line options selected by the user
1215   @param inst_map: A dict of inst -> nodes mapping
1216   @param nodes_online: A list of nodes online
1217   @param _instance_start_fn: Callback to start instances (unittest use only)
1218   @return: Success of the operation on all instances
1219
1220   """
1221   start_inst_list = []
1222   for (inst, nodes) in inst_map.items():
1223     if not (nodes - nodes_online):
1224       # All nodes the instance lives on are back online
1225       start_inst_list.append(inst)
1226
1227   for inst in start_inst_list:
1228     del inst_map[inst]
1229
1230   if start_inst_list:
1231     return _instance_start_fn(opts, start_inst_list, True)
1232
1233   return True
1234
1235
1236 def _EpoOn(opts, full_node_list, node_list, inst_map):
1237   """Does the actual power on.
1238
1239   @param opts: The command line options selected by the user
1240   @param full_node_list: All nodes to operate on (includes nodes not supporting
1241                          OOB)
1242   @param node_list: The list of nodes to operate on (all need to support OOB)
1243   @param inst_map: A dict of inst -> nodes mapping
1244   @return: The desired exit status
1245
1246   """
1247   if node_list and not _OobPower(opts, node_list, False):
1248     ToStderr("Not all nodes seem to get back up, investigate and start"
1249              " manually if needed")
1250
1251   # Wait for the nodes to be back up
1252   action_cb = compat.partial(_MaybeInstanceStartup, opts, dict(inst_map))
1253
1254   ToStdout("Waiting until all nodes are available again")
1255   if not _RunWhenNodesReachable(full_node_list, action_cb, _EPO_PING_INTERVAL):
1256     ToStderr("Please investigate and start stopped instances manually")
1257     return constants.EXIT_FAILURE
1258
1259   return constants.EXIT_SUCCESS
1260
1261
1262 def _EpoOff(opts, node_list, inst_map):
1263   """Does the actual power off.
1264
1265   @param opts: The command line options selected by the user
1266   @param node_list: The list of nodes to operate on (all need to support OOB)
1267   @param inst_map: A dict of inst -> nodes mapping
1268   @return: The desired exit status
1269
1270   """
1271   if not _InstanceStart(opts, inst_map.keys(), False):
1272     ToStderr("Please investigate and stop instances manually before continuing")
1273     return constants.EXIT_FAILURE
1274
1275   if not node_list:
1276     return constants.EXIT_SUCCESS
1277
1278   if _OobPower(opts, node_list, False):
1279     return constants.EXIT_SUCCESS
1280   else:
1281     return constants.EXIT_FAILURE
1282
1283
1284 def Epo(opts, args):
1285   """EPO operations.
1286
1287   @param opts: the command line options selected by the user
1288   @type args: list
1289   @param args: should contain only one element, the subcommand
1290   @rtype: int
1291   @return: the desired exit code
1292
1293   """
1294   if opts.groups and opts.show_all:
1295     ToStderr("Only one of --groups or --all are allowed")
1296     return constants.EXIT_FAILURE
1297   elif args and opts.show_all:
1298     ToStderr("Arguments in combination with --all are not allowed")
1299     return constants.EXIT_FAILURE
1300
1301   client = GetClient()
1302
1303   if opts.groups:
1304     node_query_list = itertools.chain(*client.QueryGroups(names=args,
1305                                                           fields=["node_list"],
1306                                                           use_locking=False))
1307   else:
1308     node_query_list = args
1309
1310   result = client.QueryNodes(names=node_query_list,
1311                              fields=["name", "master", "pinst_list",
1312                                      "sinst_list", "powered", "offline"],
1313                              use_locking=False)
1314   node_list = []
1315   inst_map = {}
1316   for (idx, (node, master, pinsts, sinsts, powered,
1317              offline)) in enumerate(result):
1318     # Normalize the node_query_list as well
1319     if not opts.show_all:
1320       node_query_list[idx] = node
1321     if not offline:
1322       for inst in (pinsts + sinsts):
1323         if inst in inst_map:
1324           if not master:
1325             inst_map[inst].add(node)
1326         elif master:
1327           inst_map[inst] = set()
1328         else:
1329           inst_map[inst] = set([node])
1330
1331     if master and opts.on:
1332       # We ignore the master for turning on the machines, in fact we are
1333       # already operating on the master at this point :)
1334       continue
1335     elif master and not opts.show_all:
1336       ToStderr("%s is the master node, please do a master-failover to another"
1337                " node not affected by the EPO or use --all if you intend to"
1338                " shutdown the whole cluster", node)
1339       return constants.EXIT_FAILURE
1340     elif powered is None:
1341       ToStdout("Node %s does not support out-of-band handling, it can not be"
1342                " handled in a fully automated manner", node)
1343     elif powered == opts.on:
1344       ToStdout("Node %s is already in desired power state, skipping", node)
1345     elif not offline or (offline and powered):
1346       node_list.append(node)
1347
1348   if not opts.force and not ConfirmOperation(node_query_list, "nodes", "epo"):
1349     return constants.EXIT_FAILURE
1350
1351   if opts.on:
1352     return _EpoOn(opts, node_query_list, node_list, inst_map)
1353   else:
1354     return _EpoOff(opts, node_list, inst_map)
1355
1356
1357 commands = {
1358   "init": (
1359     InitCluster, [ArgHost(min=1, max=1)],
1360     [BACKEND_OPT, CP_SIZE_OPT, ENABLED_HV_OPT, GLOBAL_FILEDIR_OPT,
1361      HVLIST_OPT, MAC_PREFIX_OPT, MASTER_NETDEV_OPT, MASTER_NETMASK_OPT,
1362      NIC_PARAMS_OPT, NOLVM_STORAGE_OPT, NOMODIFY_ETCHOSTS_OPT,
1363      NOMODIFY_SSH_SETUP_OPT, SECONDARY_IP_OPT, VG_NAME_OPT,
1364      MAINTAIN_NODE_HEALTH_OPT, UIDPOOL_OPT, DRBD_HELPER_OPT, NODRBD_STORAGE_OPT,
1365      DEFAULT_IALLOCATOR_OPT, PRIMARY_IP_VERSION_OPT, PREALLOC_WIPE_DISKS_OPT,
1366      NODE_PARAMS_OPT, GLOBAL_SHARED_FILEDIR_OPT],
1367     "[opts...] <cluster_name>", "Initialises a new cluster configuration"),
1368   "destroy": (
1369     DestroyCluster, ARGS_NONE, [YES_DOIT_OPT],
1370     "", "Destroy cluster"),
1371   "rename": (
1372     RenameCluster, [ArgHost(min=1, max=1)],
1373     [FORCE_OPT, DRY_RUN_OPT],
1374     "<new_name>",
1375     "Renames the cluster"),
1376   "redist-conf": (
1377     RedistributeConfig, ARGS_NONE, [SUBMIT_OPT, DRY_RUN_OPT, PRIORITY_OPT],
1378     "", "Forces a push of the configuration file and ssconf files"
1379     " to the nodes in the cluster"),
1380   "verify": (
1381     VerifyCluster, ARGS_NONE,
1382     [VERBOSE_OPT, DEBUG_SIMERR_OPT, ERROR_CODES_OPT, NONPLUS1_OPT,
1383      DRY_RUN_OPT, PRIORITY_OPT, NODEGROUP_OPT],
1384     "", "Does a check on the cluster configuration"),
1385   "verify-disks": (
1386     VerifyDisks, ARGS_NONE, [PRIORITY_OPT],
1387     "", "Does a check on the cluster disk status"),
1388   "repair-disk-sizes": (
1389     RepairDiskSizes, ARGS_MANY_INSTANCES, [DRY_RUN_OPT, PRIORITY_OPT],
1390     "", "Updates mismatches in recorded disk sizes"),
1391   "master-failover": (
1392     MasterFailover, ARGS_NONE, [NOVOTING_OPT],
1393     "", "Makes the current node the master"),
1394   "master-ping": (
1395     MasterPing, ARGS_NONE, [],
1396     "", "Checks if the master is alive"),
1397   "version": (
1398     ShowClusterVersion, ARGS_NONE, [],
1399     "", "Shows the cluster version"),
1400   "getmaster": (
1401     ShowClusterMaster, ARGS_NONE, [],
1402     "", "Shows the cluster master"),
1403   "copyfile": (
1404     ClusterCopyFile, [ArgFile(min=1, max=1)],
1405     [NODE_LIST_OPT, USE_REPL_NET_OPT, NODEGROUP_OPT],
1406     "[-n node...] <filename>", "Copies a file to all (or only some) nodes"),
1407   "command": (
1408     RunClusterCommand, [ArgCommand(min=1)],
1409     [NODE_LIST_OPT, NODEGROUP_OPT],
1410     "[-n node...] <command>", "Runs a command on all (or only some) nodes"),
1411   "info": (
1412     ShowClusterConfig, ARGS_NONE, [ROMAN_OPT],
1413     "[--roman]", "Show cluster configuration"),
1414   "list-tags": (
1415     ListTags, ARGS_NONE, [], "", "List the tags of the cluster"),
1416   "add-tags": (
1417     AddTags, [ArgUnknown()], [TAG_SRC_OPT, PRIORITY_OPT],
1418     "tag...", "Add tags to the cluster"),
1419   "remove-tags": (
1420     RemoveTags, [ArgUnknown()], [TAG_SRC_OPT, PRIORITY_OPT],
1421     "tag...", "Remove tags from the cluster"),
1422   "search-tags": (
1423     SearchTags, [ArgUnknown(min=1, max=1)], [PRIORITY_OPT], "",
1424     "Searches the tags on all objects on"
1425     " the cluster for a given pattern (regex)"),
1426   "queue": (
1427     QueueOps,
1428     [ArgChoice(min=1, max=1, choices=["drain", "undrain", "info"])],
1429     [], "drain|undrain|info", "Change queue properties"),
1430   "watcher": (
1431     WatcherOps,
1432     [ArgChoice(min=1, max=1, choices=["pause", "continue", "info"]),
1433      ArgSuggest(min=0, max=1, choices=["30m", "1h", "4h"])],
1434     [],
1435     "{pause <timespec>|continue|info}", "Change watcher properties"),
1436   "modify": (
1437     SetClusterParams, ARGS_NONE,
1438     [BACKEND_OPT, CP_SIZE_OPT, ENABLED_HV_OPT, HVLIST_OPT, MASTER_NETDEV_OPT,
1439      MASTER_NETMASK_OPT, NIC_PARAMS_OPT, NOLVM_STORAGE_OPT, VG_NAME_OPT,
1440      MAINTAIN_NODE_HEALTH_OPT, UIDPOOL_OPT, ADD_UIDS_OPT, REMOVE_UIDS_OPT,
1441      DRBD_HELPER_OPT, NODRBD_STORAGE_OPT, DEFAULT_IALLOCATOR_OPT,
1442      RESERVED_LVS_OPT, DRY_RUN_OPT, PRIORITY_OPT, PREALLOC_WIPE_DISKS_OPT,
1443      NODE_PARAMS_OPT],
1444     "[opts...]",
1445     "Alters the parameters of the cluster"),
1446   "renew-crypto": (
1447     RenewCrypto, ARGS_NONE,
1448     [NEW_CLUSTER_CERT_OPT, NEW_RAPI_CERT_OPT, RAPI_CERT_OPT,
1449      NEW_CONFD_HMAC_KEY_OPT, FORCE_OPT,
1450      NEW_CLUSTER_DOMAIN_SECRET_OPT, CLUSTER_DOMAIN_SECRET_OPT,
1451      NEW_SPICE_CERT_OPT, SPICE_CERT_OPT, SPICE_CACERT_OPT],
1452     "[opts...]",
1453     "Renews cluster certificates, keys and secrets"),
1454   "epo": (
1455     Epo, [ArgUnknown()],
1456     [FORCE_OPT, ON_OPT, GROUPS_OPT, ALL_OPT, OOB_TIMEOUT_OPT,
1457      SHUTDOWN_TIMEOUT_OPT, POWER_DELAY_OPT],
1458     "[opts...] [args]",
1459     "Performs an emergency power-off on given args"),
1460   "activate-master-ip": (
1461     ActivateMasterIp, ARGS_NONE, [], "", "Activates the master IP"),
1462   "deactivate-master-ip": (
1463     DeactivateMasterIp, ARGS_NONE, [CONFIRM_OPT], "",
1464     "Deactivates the master IP"),
1465   }
1466
1467
1468 #: dictionary with aliases for commands
1469 aliases = {
1470   "masterfailover": "master-failover",
1471 }
1472
1473
1474 def Main():
1475   return GenericMain(commands, override={"tag_type": constants.TAG_CLUSTER},
1476                      aliases=aliases)