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