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