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