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