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