Move FormatParameterDict from gnt_instance.py to cli.py
[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.OpPostInitCluster()
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.OpDestroyCluster()
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.OpRenameCluster(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.OpRedistributeConfig()
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 instance parameters:")
347   _PrintGroupedParams(result["beparams"], roman=opts.roman_integers)
348
349   ToStdout("Default nic parameters:")
350   _PrintGroupedParams(result["nicparams"], roman=opts.roman_integers)
351
352   return 0
353
354
355 def ClusterCopyFile(opts, args):
356   """Copy a file from master to some nodes.
357
358   @param opts: the command line options selected by the user
359   @type args: list
360   @param args: should contain only one element, the path of
361       the file to be copied
362   @rtype: int
363   @return: the desired exit code
364
365   """
366   filename = args[0]
367   if not os.path.exists(filename):
368     raise errors.OpPrereqError("No such filename '%s'" % filename,
369                                errors.ECODE_INVAL)
370
371   cl = GetClient()
372
373   cluster_name = cl.QueryConfigValues(["cluster_name"])[0]
374
375   results = GetOnlineNodes(nodes=opts.nodes, cl=cl, filter_master=True,
376                            secondary_ips=opts.use_replication_network)
377
378   srun = ssh.SshRunner(cluster_name=cluster_name)
379   for node in results:
380     if not srun.CopyFileToNode(node, filename):
381       ToStderr("Copy of file %s to node %s failed", filename, node)
382
383   return 0
384
385
386 def RunClusterCommand(opts, args):
387   """Run a command on some nodes.
388
389   @param opts: the command line options selected by the user
390   @type args: list
391   @param args: should contain the command to be run and its arguments
392   @rtype: int
393   @return: the desired exit code
394
395   """
396   cl = GetClient()
397
398   command = " ".join(args)
399
400   nodes = GetOnlineNodes(nodes=opts.nodes, cl=cl)
401
402   cluster_name, master_node = cl.QueryConfigValues(["cluster_name",
403                                                     "master_node"])
404
405   srun = ssh.SshRunner(cluster_name=cluster_name)
406
407   # Make sure master node is at list end
408   if master_node in nodes:
409     nodes.remove(master_node)
410     nodes.append(master_node)
411
412   for name in nodes:
413     result = srun.Run(name, "root", command)
414     ToStdout("------------------------------------------------")
415     ToStdout("node: %s", name)
416     ToStdout("%s", result.output)
417     ToStdout("return code = %s", result.exit_code)
418
419   return 0
420
421
422 def VerifyCluster(opts, args):
423   """Verify integrity of cluster, performing various test on nodes.
424
425   @param opts: the command line options selected by the user
426   @type args: list
427   @param args: should be an empty list
428   @rtype: int
429   @return: the desired exit code
430
431   """
432   skip_checks = []
433   if opts.skip_nplusone_mem:
434     skip_checks.append(constants.VERIFY_NPLUSONE_MEM)
435   op = opcodes.OpVerifyCluster(skip_checks=skip_checks,
436                                verbose=opts.verbose,
437                                error_codes=opts.error_codes,
438                                debug_simulate_errors=opts.simulate_errors)
439   if SubmitOpCode(op, opts=opts):
440     return 0
441   else:
442     return 1
443
444
445 def VerifyDisks(opts, args):
446   """Verify integrity of cluster disks.
447
448   @param opts: the command line options selected by the user
449   @type args: list
450   @param args: should be an empty list
451   @rtype: int
452   @return: the desired exit code
453
454   """
455   cl = GetClient()
456
457   op = opcodes.OpVerifyDisks()
458   result = SubmitOpCode(op, opts=opts, cl=cl)
459   if not isinstance(result, (list, tuple)) or len(result) != 3:
460     raise errors.ProgrammerError("Unknown result type for OpVerifyDisks")
461
462   bad_nodes, instances, missing = result
463
464   retcode = constants.EXIT_SUCCESS
465
466   if bad_nodes:
467     for node, text in bad_nodes.items():
468       ToStdout("Error gathering data on node %s: %s",
469                node, utils.SafeEncode(text[-400:]))
470       retcode |= 1
471       ToStdout("You need to fix these nodes first before fixing instances")
472
473   if instances:
474     for iname in instances:
475       if iname in missing:
476         continue
477       op = opcodes.OpActivateInstanceDisks(instance_name=iname)
478       try:
479         ToStdout("Activating disks for instance '%s'", iname)
480         SubmitOpCode(op, opts=opts, cl=cl)
481       except errors.GenericError, err:
482         nret, msg = FormatError(err)
483         retcode |= nret
484         ToStderr("Error activating disks for instance %s: %s", iname, msg)
485
486   if missing:
487     (vg_name, ) = cl.QueryConfigValues(["volume_group_name"])
488
489     for iname, ival in missing.iteritems():
490       all_missing = compat.all(x[0] in bad_nodes for x in ival)
491       if all_missing:
492         ToStdout("Instance %s cannot be verified as it lives on"
493                  " broken nodes", iname)
494       else:
495         ToStdout("Instance %s has missing logical volumes:", iname)
496         ival.sort()
497         for node, vol in ival:
498           if node in bad_nodes:
499             ToStdout("\tbroken node %s /dev/%s/%s", node, vg_name, vol)
500           else:
501             ToStdout("\t%s /dev/%s/%s", node, vg_name, vol)
502
503     ToStdout("You need to run replace_disks for all the above"
504              " instances, if this message persist after fixing nodes.")
505     retcode |= 1
506
507   return retcode
508
509
510 def RepairDiskSizes(opts, args):
511   """Verify sizes of cluster disks.
512
513   @param opts: the command line options selected by the user
514   @type args: list
515   @param args: optional list of instances to restrict check to
516   @rtype: int
517   @return: the desired exit code
518
519   """
520   op = opcodes.OpRepairDiskSizes(instances=args)
521   SubmitOpCode(op, opts=opts)
522
523
524 @UsesRPC
525 def MasterFailover(opts, args):
526   """Failover the master node.
527
528   This command, when run on a non-master node, will cause the current
529   master to cease being master, and the non-master to become new
530   master.
531
532   @param opts: the command line options selected by the user
533   @type args: list
534   @param args: should be an empty list
535   @rtype: int
536   @return: the desired exit code
537
538   """
539   if opts.no_voting:
540     usertext = ("This will perform the failover even if most other nodes"
541                 " are down, or if this node is outdated. This is dangerous"
542                 " as it can lead to a non-consistent cluster. Check the"
543                 " gnt-cluster(8) man page before proceeding. Continue?")
544     if not AskUser(usertext):
545       return 1
546
547   return bootstrap.MasterFailover(no_voting=opts.no_voting)
548
549
550 def MasterPing(opts, args):
551   """Checks if the master is alive.
552
553   @param opts: the command line options selected by the user
554   @type args: list
555   @param args: should be an empty list
556   @rtype: int
557   @return: the desired exit code
558
559   """
560   try:
561     cl = GetClient()
562     cl.QueryClusterInfo()
563     return 0
564   except Exception: # pylint: disable-msg=W0703
565     return 1
566
567
568 def SearchTags(opts, args):
569   """Searches the tags on all the cluster.
570
571   @param opts: the command line options selected by the user
572   @type args: list
573   @param args: should contain only one element, the tag pattern
574   @rtype: int
575   @return: the desired exit code
576
577   """
578   op = opcodes.OpSearchTags(pattern=args[0])
579   result = SubmitOpCode(op, opts=opts)
580   if not result:
581     return 1
582   result = list(result)
583   result.sort()
584   for path, tag in result:
585     ToStdout("%s %s", path, tag)
586
587
588 def _RenewCrypto(new_cluster_cert, new_rapi_cert, rapi_cert_filename,
589                  new_confd_hmac_key, new_cds, cds_filename,
590                  force):
591   """Renews cluster certificates, keys and secrets.
592
593   @type new_cluster_cert: bool
594   @param new_cluster_cert: Whether to generate a new cluster certificate
595   @type new_rapi_cert: bool
596   @param new_rapi_cert: Whether to generate a new RAPI certificate
597   @type rapi_cert_filename: string
598   @param rapi_cert_filename: Path to file containing new RAPI certificate
599   @type new_confd_hmac_key: bool
600   @param new_confd_hmac_key: Whether to generate a new HMAC key
601   @type new_cds: bool
602   @param new_cds: Whether to generate a new cluster domain secret
603   @type cds_filename: string
604   @param cds_filename: Path to file containing new cluster domain secret
605   @type force: bool
606   @param force: Whether to ask user for confirmation
607
608   """
609   if new_rapi_cert and rapi_cert_filename:
610     ToStderr("Only one of the --new-rapi-certficate and --rapi-certificate"
611              " options can be specified at the same time.")
612     return 1
613
614   if new_cds and cds_filename:
615     ToStderr("Only one of the --new-cluster-domain-secret and"
616              " --cluster-domain-secret options can be specified at"
617              " the same time.")
618     return 1
619
620   if rapi_cert_filename:
621     # Read and verify new certificate
622     try:
623       rapi_cert_pem = utils.ReadFile(rapi_cert_filename)
624
625       OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM,
626                                       rapi_cert_pem)
627     except Exception, err: # pylint: disable-msg=W0703
628       ToStderr("Can't load new RAPI certificate from %s: %s" %
629                (rapi_cert_filename, str(err)))
630       return 1
631
632     try:
633       OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, rapi_cert_pem)
634     except Exception, err: # pylint: disable-msg=W0703
635       ToStderr("Can't load new RAPI private key from %s: %s" %
636                (rapi_cert_filename, str(err)))
637       return 1
638
639   else:
640     rapi_cert_pem = None
641
642   if cds_filename:
643     try:
644       cds = utils.ReadFile(cds_filename)
645     except Exception, err: # pylint: disable-msg=W0703
646       ToStderr("Can't load new cluster domain secret from %s: %s" %
647                (cds_filename, str(err)))
648       return 1
649   else:
650     cds = None
651
652   if not force:
653     usertext = ("This requires all daemons on all nodes to be restarted and"
654                 " may take some time. Continue?")
655     if not AskUser(usertext):
656       return 1
657
658   def _RenewCryptoInner(ctx):
659     ctx.feedback_fn("Updating certificates and keys")
660     bootstrap.GenerateClusterCrypto(new_cluster_cert, new_rapi_cert,
661                                     new_confd_hmac_key,
662                                     new_cds,
663                                     rapi_cert_pem=rapi_cert_pem,
664                                     cds=cds)
665
666     files_to_copy = []
667
668     if new_cluster_cert:
669       files_to_copy.append(constants.NODED_CERT_FILE)
670
671     if new_rapi_cert or rapi_cert_pem:
672       files_to_copy.append(constants.RAPI_CERT_FILE)
673
674     if new_confd_hmac_key:
675       files_to_copy.append(constants.CONFD_HMAC_KEY)
676
677     if new_cds or cds:
678       files_to_copy.append(constants.CLUSTER_DOMAIN_SECRET_FILE)
679
680     if files_to_copy:
681       for node_name in ctx.nonmaster_nodes:
682         ctx.feedback_fn("Copying %s to %s" %
683                         (", ".join(files_to_copy), node_name))
684         for file_name in files_to_copy:
685           ctx.ssh.CopyFileToNode(node_name, file_name)
686
687   RunWhileClusterStopped(ToStdout, _RenewCryptoInner)
688
689   ToStdout("All requested certificates and keys have been replaced."
690            " Running \"gnt-cluster verify\" now is recommended.")
691
692   return 0
693
694
695 def RenewCrypto(opts, args):
696   """Renews cluster certificates, keys and secrets.
697
698   """
699   return _RenewCrypto(opts.new_cluster_cert,
700                       opts.new_rapi_cert,
701                       opts.rapi_cert,
702                       opts.new_confd_hmac_key,
703                       opts.new_cluster_domain_secret,
704                       opts.cluster_domain_secret,
705                       opts.force)
706
707
708 def SetClusterParams(opts, args):
709   """Modify the cluster.
710
711   @param opts: the command line options selected by the user
712   @type args: list
713   @param args: should be an empty list
714   @rtype: int
715   @return: the desired exit code
716
717   """
718   if not (not opts.lvm_storage or opts.vg_name or
719           not opts.drbd_storage or opts.drbd_helper or
720           opts.enabled_hypervisors or opts.hvparams or
721           opts.beparams or opts.nicparams or opts.ndparams or
722           opts.candidate_pool_size is not None or
723           opts.uid_pool is not None or
724           opts.maintain_node_health is not None or
725           opts.add_uids is not None or
726           opts.remove_uids is not None or
727           opts.default_iallocator is not None or
728           opts.reserved_lvs is not None or
729           opts.master_netdev is not None or
730           opts.prealloc_wipe_disks is not None):
731     ToStderr("Please give at least one of the parameters.")
732     return 1
733
734   vg_name = opts.vg_name
735   if not opts.lvm_storage and opts.vg_name:
736     ToStderr("Options --no-lvm-storage and --vg-name conflict.")
737     return 1
738
739   if not opts.lvm_storage:
740     vg_name = ""
741
742   drbd_helper = opts.drbd_helper
743   if not opts.drbd_storage and opts.drbd_helper:
744     ToStderr("Options --no-drbd-storage and --drbd-usermode-helper conflict.")
745     return 1
746
747   if not opts.drbd_storage:
748     drbd_helper = ""
749
750   hvlist = opts.enabled_hypervisors
751   if hvlist is not None:
752     hvlist = hvlist.split(",")
753
754   # a list of (name, dict) we can pass directly to dict() (or [])
755   hvparams = dict(opts.hvparams)
756   for hv_params in hvparams.values():
757     utils.ForceDictType(hv_params, constants.HVS_PARAMETER_TYPES)
758
759   beparams = opts.beparams
760   utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
761
762   nicparams = opts.nicparams
763   utils.ForceDictType(nicparams, constants.NICS_PARAMETER_TYPES)
764
765   ndparams = opts.ndparams
766   if ndparams is not None:
767     utils.ForceDictType(ndparams, constants.NDS_PARAMETER_TYPES)
768
769   mnh = opts.maintain_node_health
770
771   uid_pool = opts.uid_pool
772   if uid_pool is not None:
773     uid_pool = uidpool.ParseUidPool(uid_pool)
774
775   add_uids = opts.add_uids
776   if add_uids is not None:
777     add_uids = uidpool.ParseUidPool(add_uids)
778
779   remove_uids = opts.remove_uids
780   if remove_uids is not None:
781     remove_uids = uidpool.ParseUidPool(remove_uids)
782
783   if opts.reserved_lvs is not None:
784     if opts.reserved_lvs == "":
785       opts.reserved_lvs = []
786     else:
787       opts.reserved_lvs = utils.UnescapeAndSplit(opts.reserved_lvs, sep=",")
788
789   op = opcodes.OpSetClusterParams(vg_name=vg_name,
790                                   drbd_helper=drbd_helper,
791                                   enabled_hypervisors=hvlist,
792                                   hvparams=hvparams,
793                                   os_hvp=None,
794                                   beparams=beparams,
795                                   nicparams=nicparams,
796                                   ndparams=ndparams,
797                                   candidate_pool_size=opts.candidate_pool_size,
798                                   maintain_node_health=mnh,
799                                   uid_pool=uid_pool,
800                                   add_uids=add_uids,
801                                   remove_uids=remove_uids,
802                                   default_iallocator=opts.default_iallocator,
803                                   prealloc_wipe_disks=opts.prealloc_wipe_disks,
804                                   master_netdev=opts.master_netdev,
805                                   reserved_lvs=opts.reserved_lvs)
806   SubmitOpCode(op, opts=opts)
807   return 0
808
809
810 def QueueOps(opts, args):
811   """Queue operations.
812
813   @param opts: the command line options selected by the user
814   @type args: list
815   @param args: should contain only one element, the subcommand
816   @rtype: int
817   @return: the desired exit code
818
819   """
820   command = args[0]
821   client = GetClient()
822   if command in ("drain", "undrain"):
823     drain_flag = command == "drain"
824     client.SetQueueDrainFlag(drain_flag)
825   elif command == "info":
826     result = client.QueryConfigValues(["drain_flag"])
827     if result[0]:
828       val = "set"
829     else:
830       val = "unset"
831     ToStdout("The drain flag is %s" % val)
832   else:
833     raise errors.OpPrereqError("Command '%s' is not valid." % command,
834                                errors.ECODE_INVAL)
835
836   return 0
837
838
839 def _ShowWatcherPause(until):
840   if until is None or until < time.time():
841     ToStdout("The watcher is not paused.")
842   else:
843     ToStdout("The watcher is paused until %s.", time.ctime(until))
844
845
846 def WatcherOps(opts, args):
847   """Watcher operations.
848
849   @param opts: the command line options selected by the user
850   @type args: list
851   @param args: should contain only one element, the subcommand
852   @rtype: int
853   @return: the desired exit code
854
855   """
856   command = args[0]
857   client = GetClient()
858
859   if command == "continue":
860     client.SetWatcherPause(None)
861     ToStdout("The watcher is no longer paused.")
862
863   elif command == "pause":
864     if len(args) < 2:
865       raise errors.OpPrereqError("Missing pause duration", errors.ECODE_INVAL)
866
867     result = client.SetWatcherPause(time.time() + ParseTimespec(args[1]))
868     _ShowWatcherPause(result)
869
870   elif command == "info":
871     result = client.QueryConfigValues(["watcher_pause"])
872     _ShowWatcherPause(result[0])
873
874   else:
875     raise errors.OpPrereqError("Command '%s' is not valid." % command,
876                                errors.ECODE_INVAL)
877
878   return 0
879
880
881 commands = {
882   'init': (
883     InitCluster, [ArgHost(min=1, max=1)],
884     [BACKEND_OPT, CP_SIZE_OPT, ENABLED_HV_OPT, GLOBAL_FILEDIR_OPT,
885      HVLIST_OPT, MAC_PREFIX_OPT, MASTER_NETDEV_OPT, NIC_PARAMS_OPT,
886      NOLVM_STORAGE_OPT, NOMODIFY_ETCHOSTS_OPT, NOMODIFY_SSH_SETUP_OPT,
887      SECONDARY_IP_OPT, VG_NAME_OPT, MAINTAIN_NODE_HEALTH_OPT,
888      UIDPOOL_OPT, DRBD_HELPER_OPT, NODRBD_STORAGE_OPT,
889      DEFAULT_IALLOCATOR_OPT, PRIMARY_IP_VERSION_OPT, PREALLOC_WIPE_DISKS_OPT,
890      NODE_PARAMS_OPT],
891     "[opts...] <cluster_name>", "Initialises a new cluster configuration"),
892   'destroy': (
893     DestroyCluster, ARGS_NONE, [YES_DOIT_OPT],
894     "", "Destroy cluster"),
895   'rename': (
896     RenameCluster, [ArgHost(min=1, max=1)],
897     [FORCE_OPT, DRY_RUN_OPT],
898     "<new_name>",
899     "Renames the cluster"),
900   'redist-conf': (
901     RedistributeConfig, ARGS_NONE, [SUBMIT_OPT, DRY_RUN_OPT, PRIORITY_OPT],
902     "", "Forces a push of the configuration file and ssconf files"
903     " to the nodes in the cluster"),
904   'verify': (
905     VerifyCluster, ARGS_NONE,
906     [VERBOSE_OPT, DEBUG_SIMERR_OPT, ERROR_CODES_OPT, NONPLUS1_OPT,
907      DRY_RUN_OPT, PRIORITY_OPT],
908     "", "Does a check on the cluster configuration"),
909   'verify-disks': (
910     VerifyDisks, ARGS_NONE, [PRIORITY_OPT],
911     "", "Does a check on the cluster disk status"),
912   'repair-disk-sizes': (
913     RepairDiskSizes, ARGS_MANY_INSTANCES, [DRY_RUN_OPT, PRIORITY_OPT],
914     "", "Updates mismatches in recorded disk sizes"),
915   'master-failover': (
916     MasterFailover, ARGS_NONE, [NOVOTING_OPT],
917     "", "Makes the current node the master"),
918   'master-ping': (
919     MasterPing, ARGS_NONE, [],
920     "", "Checks if the master is alive"),
921   'version': (
922     ShowClusterVersion, ARGS_NONE, [],
923     "", "Shows the cluster version"),
924   'getmaster': (
925     ShowClusterMaster, ARGS_NONE, [],
926     "", "Shows the cluster master"),
927   'copyfile': (
928     ClusterCopyFile, [ArgFile(min=1, max=1)],
929     [NODE_LIST_OPT, USE_REPL_NET_OPT],
930     "[-n node...] <filename>", "Copies a file to all (or only some) nodes"),
931   'command': (
932     RunClusterCommand, [ArgCommand(min=1)],
933     [NODE_LIST_OPT],
934     "[-n node...] <command>", "Runs a command on all (or only some) nodes"),
935   'info': (
936     ShowClusterConfig, ARGS_NONE, [ROMAN_OPT],
937     "[--roman]", "Show cluster configuration"),
938   'list-tags': (
939     ListTags, ARGS_NONE, [], "", "List the tags of the cluster"),
940   'add-tags': (
941     AddTags, [ArgUnknown()], [TAG_SRC_OPT, PRIORITY_OPT],
942     "tag...", "Add tags to the cluster"),
943   'remove-tags': (
944     RemoveTags, [ArgUnknown()], [TAG_SRC_OPT, PRIORITY_OPT],
945     "tag...", "Remove tags from the cluster"),
946   'search-tags': (
947     SearchTags, [ArgUnknown(min=1, max=1)], [PRIORITY_OPT], "",
948     "Searches the tags on all objects on"
949     " the cluster for a given pattern (regex)"),
950   'queue': (
951     QueueOps,
952     [ArgChoice(min=1, max=1, choices=["drain", "undrain", "info"])],
953     [], "drain|undrain|info", "Change queue properties"),
954   'watcher': (
955     WatcherOps,
956     [ArgChoice(min=1, max=1, choices=["pause", "continue", "info"]),
957      ArgSuggest(min=0, max=1, choices=["30m", "1h", "4h"])],
958     [],
959     "{pause <timespec>|continue|info}", "Change watcher properties"),
960   'modify': (
961     SetClusterParams, ARGS_NONE,
962     [BACKEND_OPT, CP_SIZE_OPT, ENABLED_HV_OPT, HVLIST_OPT, MASTER_NETDEV_OPT,
963      NIC_PARAMS_OPT, NOLVM_STORAGE_OPT, VG_NAME_OPT, MAINTAIN_NODE_HEALTH_OPT,
964      UIDPOOL_OPT, ADD_UIDS_OPT, REMOVE_UIDS_OPT, DRBD_HELPER_OPT,
965      NODRBD_STORAGE_OPT, DEFAULT_IALLOCATOR_OPT, RESERVED_LVS_OPT,
966      DRY_RUN_OPT, PRIORITY_OPT, PREALLOC_WIPE_DISKS_OPT, NODE_PARAMS_OPT],
967     "[opts...]",
968     "Alters the parameters of the cluster"),
969   "renew-crypto": (
970     RenewCrypto, ARGS_NONE,
971     [NEW_CLUSTER_CERT_OPT, NEW_RAPI_CERT_OPT, RAPI_CERT_OPT,
972      NEW_CONFD_HMAC_KEY_OPT, FORCE_OPT,
973      NEW_CLUSTER_DOMAIN_SECRET_OPT, CLUSTER_DOMAIN_SECRET_OPT],
974     "[opts...]",
975     "Renews cluster certificates, keys and secrets"),
976   }
977
978
979 #: dictionary with aliases for commands
980 aliases = {
981   'masterfailover': 'master-failover',
982 }
983
984
985 def Main():
986   return GenericMain(commands, override={"tag_type": constants.TAG_CLUSTER},
987                      aliases=aliases)