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