Handle SSL setup when downgrading
[ganeti-github.git] / tools / cfgupgrade
1 #!/usr/bin/python
2 #
3
4 # Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012, 2013 Google Inc.
5 # All rights reserved.
6 #
7 # Redistribution and use in source and binary forms, with or without
8 # modification, are permitted provided that the following conditions are
9 # met:
10 #
11 # 1. Redistributions of source code must retain the above copyright notice,
12 # this list of conditions and the following disclaimer.
13 #
14 # 2. Redistributions in binary form must reproduce the above copyright
15 # notice, this list of conditions and the following disclaimer in the
16 # documentation and/or other materials provided with the distribution.
17 #
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
19 # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
20 # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
22 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
23 # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
24 # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
25 # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
26 # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
27 # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
30
31 """Tool to upgrade the configuration file.
32
33 This code handles only the types supported by simplejson. As an
34 example, 'set' is a 'list'.
35
36 """
37
38
39 import os
40 import os.path
41 import sys
42 import optparse
43 import logging
44 import time
45 from cStringIO import StringIO
46
47 from ganeti import constants
48 from ganeti import serializer
49 from ganeti import utils
50 from ganeti import cli
51 from ganeti import bootstrap
52 from ganeti import config
53 from ganeti import netutils
54 from ganeti import pathutils
55
56 from ganeti.utils import version
57
58
59 options = None
60 args = None
61
62
63 #: Target major version we will upgrade to
64 TARGET_MAJOR = 2
65 #: Target minor version we will upgrade to
66 TARGET_MINOR = 12
67 #: Target major version for downgrade
68 DOWNGRADE_MAJOR = 2
69 #: Target minor version for downgrade
70 DOWNGRADE_MINOR = 11
71
72 # map of legacy device types
73 # (mapping differing old LD_* constants to new DT_* constants)
74 DEV_TYPE_OLD_NEW = {"lvm": constants.DT_PLAIN, "drbd8": constants.DT_DRBD8}
75 # (mapping differing new DT_* constants to old LD_* constants)
76 DEV_TYPE_NEW_OLD = dict((v, k) for k, v in DEV_TYPE_OLD_NEW.items())
77
78
79 class Error(Exception):
80   """Generic exception"""
81   pass
82
83
84 def SetupLogging():
85   """Configures the logging module.
86
87   """
88   formatter = logging.Formatter("%(asctime)s: %(message)s")
89
90   stderr_handler = logging.StreamHandler()
91   stderr_handler.setFormatter(formatter)
92   if options.debug:
93     stderr_handler.setLevel(logging.NOTSET)
94   elif options.verbose:
95     stderr_handler.setLevel(logging.INFO)
96   else:
97     stderr_handler.setLevel(logging.WARNING)
98
99   root_logger = logging.getLogger("")
100   root_logger.setLevel(logging.NOTSET)
101   root_logger.addHandler(stderr_handler)
102
103
104 def CheckHostname(path):
105   """Ensures hostname matches ssconf value.
106
107   @param path: Path to ssconf file
108
109   """
110   ssconf_master_node = utils.ReadOneLineFile(path)
111   hostname = netutils.GetHostname().name
112
113   if ssconf_master_node == hostname:
114     return True
115
116   logging.warning("Warning: ssconf says master node is '%s', but this"
117                   " machine's name is '%s'; this tool must be run on"
118                   " the master node", ssconf_master_node, hostname)
119   return False
120
121
122 def _FillIPolicySpecs(default_ipolicy, ipolicy):
123   if "minmax" in ipolicy:
124     for (key, spec) in ipolicy["minmax"][0].items():
125       for (par, val) in default_ipolicy["minmax"][0][key].items():
126         if par not in spec:
127           spec[par] = val
128
129
130 def UpgradeIPolicy(ipolicy, default_ipolicy, isgroup):
131   minmax_keys = ["min", "max"]
132   if any((k in ipolicy) for k in minmax_keys):
133     minmax = {}
134     for key in minmax_keys:
135       if key in ipolicy:
136         if ipolicy[key]:
137           minmax[key] = ipolicy[key]
138         del ipolicy[key]
139     if minmax:
140       ipolicy["minmax"] = [minmax]
141   if isgroup and "std" in ipolicy:
142     del ipolicy["std"]
143   _FillIPolicySpecs(default_ipolicy, ipolicy)
144
145
146 def UpgradeNetworks(config_data):
147   networks = config_data.get("networks", None)
148   if not networks:
149     config_data["networks"] = {}
150
151
152 def UpgradeCluster(config_data):
153   cluster = config_data.get("cluster", None)
154   if cluster is None:
155     raise Error("Cannot find cluster")
156   ipolicy = cluster.setdefault("ipolicy", None)
157   if ipolicy:
158     UpgradeIPolicy(ipolicy, constants.IPOLICY_DEFAULTS, False)
159   ial_params = cluster.get("default_iallocator_params", None)
160   if not ial_params:
161     cluster["default_iallocator_params"] = {}
162   if not "candidate_certs" in cluster:
163     cluster["candidate_certs"] = {}
164   cluster["instance_communication_network"] = \
165     cluster.get("instance_communication_network", "")
166   cluster["install_image"] = \
167     cluster.get("install_image", "")
168   cluster["zeroing_image"] = \
169     cluster.get("zeroing_image", "")
170   cluster["compression_tools"] = \
171     cluster.get("compression_tools", constants.IEC_DEFAULT_TOOLS)
172   if "enabled_user_shutdown" not in cluster:
173     cluster["enabled_user_shutdown"] = False
174
175
176 def UpgradeGroups(config_data):
177   cl_ipolicy = config_data["cluster"].get("ipolicy")
178   for group in config_data["nodegroups"].values():
179     networks = group.get("networks", None)
180     if not networks:
181       group["networks"] = {}
182     ipolicy = group.get("ipolicy", None)
183     if ipolicy:
184       if cl_ipolicy is None:
185         raise Error("A group defines an instance policy but there is no"
186                     " instance policy at cluster level")
187       UpgradeIPolicy(ipolicy, cl_ipolicy, True)
188
189
190 def GetExclusiveStorageValue(config_data):
191   """Return a conservative value of the exclusive_storage flag.
192
193   Return C{True} if the cluster or at least a nodegroup have the flag set.
194
195   """
196   ret = False
197   cluster = config_data["cluster"]
198   ndparams = cluster.get("ndparams")
199   if ndparams is not None and ndparams.get("exclusive_storage"):
200     ret = True
201   for group in config_data["nodegroups"].values():
202     ndparams = group.get("ndparams")
203     if ndparams is not None and ndparams.get("exclusive_storage"):
204       ret = True
205   return ret
206
207
208 def RemovePhysicalId(disk):
209   if "children" in disk:
210     for d in disk["children"]:
211       RemovePhysicalId(d)
212   if "physical_id" in disk:
213     del disk["physical_id"]
214
215
216 def ChangeDiskDevType(disk, dev_type_map):
217   """Replaces disk's dev_type attributes according to the given map.
218
219   This can be used for both, up or downgrading the disks.
220   """
221   if disk["dev_type"] in dev_type_map:
222     disk["dev_type"] = dev_type_map[disk["dev_type"]]
223   if "children" in disk:
224     for child in disk["children"]:
225       ChangeDiskDevType(child, dev_type_map)
226
227
228 def UpgradeDiskDevType(disk):
229   """Upgrades the disks' device type."""
230   ChangeDiskDevType(disk, DEV_TYPE_OLD_NEW)
231
232
233 def _ConvertNicNameToUuid(iobj, network2uuid):
234   for nic in iobj["nics"]:
235     name = nic.get("network", None)
236     if name:
237       uuid = network2uuid.get(name, None)
238       if uuid:
239         print("NIC with network name %s found."
240               " Substituting with uuid %s." % (name, uuid))
241         nic["network"] = uuid
242
243
244 def AssignUuid(disk):
245   if not "uuid" in disk:
246     disk["uuid"] = utils.io.NewUUID()
247   if "children" in disk:
248     for d in disk["children"]:
249       AssignUuid(d)
250
251
252 def _ConvertDiskAndCheckMissingSpindles(iobj, instance):
253   missing_spindles = False
254   if "disks" not in iobj:
255     raise Error("Instance '%s' doesn't have a disks entry?!" % instance)
256   disks = iobj["disks"]
257   if not all(isinstance(d, str) for d in disks):
258     #  Disks are not top level citizens
259     for idx, dobj in enumerate(disks):
260       RemovePhysicalId(dobj)
261
262       expected = "disk/%s" % idx
263       current = dobj.get("iv_name", "")
264       if current != expected:
265         logging.warning("Updating iv_name for instance %s/disk %s"
266                         " from '%s' to '%s'",
267                         instance, idx, current, expected)
268         dobj["iv_name"] = expected
269
270       if "dev_type" in dobj:
271         UpgradeDiskDevType(dobj)
272
273       if not "spindles" in dobj:
274         missing_spindles = True
275
276       AssignUuid(dobj)
277   return missing_spindles
278
279
280 def UpgradeInstances(config_data):
281   """Upgrades the instances' configuration."""
282
283   network2uuid = dict((n["name"], n["uuid"])
284                       for n in config_data["networks"].values())
285   if "instances" not in config_data:
286     raise Error("Can't find the 'instances' key in the configuration!")
287
288   missing_spindles = False
289   for instance, iobj in config_data["instances"].items():
290     _ConvertNicNameToUuid(iobj, network2uuid)
291     if _ConvertDiskAndCheckMissingSpindles(iobj, instance):
292       missing_spindles = True
293     if "admin_state_source" not in iobj:
294       iobj["admin_state_source"] = constants.ADMIN_SOURCE
295
296   if GetExclusiveStorageValue(config_data) and missing_spindles:
297     # We cannot be sure that the instances that are missing spindles have
298     # exclusive storage enabled (the check would be more complicated), so we
299     # give a noncommittal message
300     logging.warning("Some instance disks could be needing to update the"
301                     " spindles parameter; you can check by running"
302                     " 'gnt-cluster verify', and fix any problem with"
303                     " 'gnt-cluster repair-disk-sizes'")
304
305
306 def UpgradeRapiUsers():
307   if (os.path.isfile(options.RAPI_USERS_FILE_PRE24) and
308       not os.path.islink(options.RAPI_USERS_FILE_PRE24)):
309     if os.path.exists(options.RAPI_USERS_FILE):
310       raise Error("Found pre-2.4 RAPI users file at %s, but another file"
311                   " already exists at %s" %
312                   (options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE))
313     logging.info("Found pre-2.4 RAPI users file at %s, renaming to %s",
314                  options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
315     if not options.dry_run:
316       utils.RenameFile(options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE,
317                        mkdir=True, mkdir_mode=0750)
318
319   # Create a symlink for RAPI users file
320   if (not (os.path.islink(options.RAPI_USERS_FILE_PRE24) or
321            os.path.isfile(options.RAPI_USERS_FILE_PRE24)) and
322       os.path.isfile(options.RAPI_USERS_FILE)):
323     logging.info("Creating symlink from %s to %s",
324                  options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
325     if not options.dry_run:
326       os.symlink(options.RAPI_USERS_FILE, options.RAPI_USERS_FILE_PRE24)
327
328
329 def UpgradeWatcher():
330   # Remove old watcher state file if it exists
331   if os.path.exists(options.WATCHER_STATEFILE):
332     logging.info("Removing watcher state file %s", options.WATCHER_STATEFILE)
333     if not options.dry_run:
334       utils.RemoveFile(options.WATCHER_STATEFILE)
335
336
337 def UpgradeFileStoragePaths(config_data):
338   # Write file storage paths
339   if not os.path.exists(options.FILE_STORAGE_PATHS_FILE):
340     cluster = config_data["cluster"]
341     file_storage_dir = cluster.get("file_storage_dir")
342     shared_file_storage_dir = cluster.get("shared_file_storage_dir")
343     del cluster
344
345     logging.info("Ganeti 2.7 and later only allow whitelisted directories"
346                  " for file storage; writing existing configuration values"
347                  " into '%s'",
348                  options.FILE_STORAGE_PATHS_FILE)
349
350     if file_storage_dir:
351       logging.info("File storage directory: %s", file_storage_dir)
352     if shared_file_storage_dir:
353       logging.info("Shared file storage directory: %s",
354                    shared_file_storage_dir)
355
356     buf = StringIO()
357     buf.write("# List automatically generated from configuration by\n")
358     buf.write("# cfgupgrade at %s\n" % time.asctime())
359     if file_storage_dir:
360       buf.write("%s\n" % file_storage_dir)
361     if shared_file_storage_dir:
362       buf.write("%s\n" % shared_file_storage_dir)
363     utils.WriteFile(file_name=options.FILE_STORAGE_PATHS_FILE,
364                     data=buf.getvalue(),
365                     mode=0600,
366                     dry_run=options.dry_run,
367                     backup=True)
368
369
370 def GetNewNodeIndex(nodes_by_old_key, old_key, new_key_field):
371   if old_key not in nodes_by_old_key:
372     logging.warning("Can't find node '%s' in configuration, assuming that it's"
373                     " already up-to-date", old_key)
374     return old_key
375   return nodes_by_old_key[old_key][new_key_field]
376
377
378 def ChangeNodeIndices(config_data, old_key_field, new_key_field):
379   def ChangeDiskNodeIndices(disk):
380     # Note: 'drbd8' is a legacy device type from pre 2.9 and needs to be
381     # considered when up/downgrading from/to any versions touching 2.9 on the
382     # way.
383     drbd_disk_types = set(["drbd8"]) | constants.DTS_DRBD
384     if disk["dev_type"] in drbd_disk_types:
385       for i in range(0, 2):
386         disk["logical_id"][i] = GetNewNodeIndex(nodes_by_old_key,
387                                                 disk["logical_id"][i],
388                                                 new_key_field)
389     if "children" in disk:
390       for child in disk["children"]:
391         ChangeDiskNodeIndices(child)
392
393   nodes_by_old_key = {}
394   nodes_by_new_key = {}
395   for (_, node) in config_data["nodes"].items():
396     nodes_by_old_key[node[old_key_field]] = node
397     nodes_by_new_key[node[new_key_field]] = node
398
399   config_data["nodes"] = nodes_by_new_key
400
401   cluster = config_data["cluster"]
402   cluster["master_node"] = GetNewNodeIndex(nodes_by_old_key,
403                                            cluster["master_node"],
404                                            new_key_field)
405
406   for inst in config_data["instances"].values():
407     inst["primary_node"] = GetNewNodeIndex(nodes_by_old_key,
408                                            inst["primary_node"],
409                                            new_key_field)
410
411   for disk in config_data["disks"].values():
412     ChangeDiskNodeIndices(disk)
413
414
415 def ChangeInstanceIndices(config_data, old_key_field, new_key_field):
416   insts_by_old_key = {}
417   insts_by_new_key = {}
418   for (_, inst) in config_data["instances"].items():
419     insts_by_old_key[inst[old_key_field]] = inst
420     insts_by_new_key[inst[new_key_field]] = inst
421
422   config_data["instances"] = insts_by_new_key
423
424
425 def UpgradeNodeIndices(config_data):
426   ChangeNodeIndices(config_data, "name", "uuid")
427
428
429 def UpgradeInstanceIndices(config_data):
430   ChangeInstanceIndices(config_data, "name", "uuid")
431
432
433 def UpgradeTopLevelDisks(config_data):
434   """Upgrades the disks as config top level citizens."""
435   if "instances" not in config_data:
436     raise Error("Can't find the 'instances' key in the configuration!")
437
438   if "disks" in config_data:
439     # Disks are already top level citizens
440     return
441
442   config_data["disks"] = dict()
443   for iobj in config_data["instances"].values():
444     disk_uuids = []
445     for disk in iobj["disks"]:
446       duuid = disk["uuid"]
447       disk["serial_no"] = 1
448       disk["ctime"] = disk["mtime"] = iobj["ctime"]
449       config_data["disks"][duuid] = disk
450       disk_uuids.append(duuid)
451     iobj["disks"] = disk_uuids
452
453
454 def UpgradeAll(config_data):
455   config_data["version"] = version.BuildVersion(TARGET_MAJOR, TARGET_MINOR, 0)
456   UpgradeRapiUsers()
457   UpgradeWatcher()
458   UpgradeFileStoragePaths(config_data)
459   UpgradeNetworks(config_data)
460   UpgradeCluster(config_data)
461   UpgradeGroups(config_data)
462   UpgradeInstances(config_data)
463   UpgradeTopLevelDisks(config_data)
464   UpgradeNodeIndices(config_data)
465   UpgradeInstanceIndices(config_data)
466
467
468 # DOWNGRADE ------------------------------------------------------------
469
470 def DowngradeNodeParams(config_object):
471   if "ndparams" in config_object:
472     if "cpu_speed" in config_object["ndparams"]:
473       del config_object["ndparams"]["cpu_speed"]
474
475
476 def DowngradeKvmHvParams(params):
477   """Remove hypervisor parameters added in 2.12"""
478   if "migration_caps" in params:
479     del params["migration_caps"]
480
481   if "disk_aio" in params:
482     del params["disk_aio"]
483
484   if "virtio_net_queues" in params:
485     del params["virtio_net_queues"]
486
487
488 def DowngradeClusterHVParams(cluster):
489   """Downgrade newly introduced HV parameters"""
490   if "hvparams" in cluster:
491     if "kvm" in cluster["hvparams"]:
492       DowngradeKvmHvParams(cluster["hvparams"]["kvm"])
493
494
495 def DowngradeInstanceHVParams(instance):
496   """Downgrade newly introduced HV parameters"""
497   if "hvparams" in instance:
498     DowngradeKvmHvParams(instance["hvparams"])
499
500
501 def DowngradeCluster(config_data):
502   cluster = config_data.get("cluster", None)
503   if not cluster:
504     raise Error("Cannot find the 'cluster' key in the configuration")
505
506   DowngradeNodeParams(cluster)
507   DowngradeClusterHVParams(cluster)
508
509   if "osparams_private_cluster" in cluster:
510     del cluster["osparams_private_cluster"]
511
512   if "instance_communication_network" in cluster:
513     del cluster["instance_communication_network"]
514
515   if "install_image" in cluster:
516     del cluster["install_image"]
517
518   if "zeroing_image" in cluster:
519     del cluster["zeroing_image"]
520
521   if "compression_tools" in cluster:
522     del cluster["compression_tools"]
523
524   if "max_tracked_jobs" in cluster:
525     del cluster["max_tracked_jobs"]
526
527   if "candidate_certs" in cluster:
528     # Clear the candidate certs to make people run 'gnt-cluster renew-crypto'
529     # after a downgrade from 2.12 to 2.11.
530     cluster["candidate_certs"] = {}
531
532
533 def DowngradeGroups(config_data):
534   for group in config_data["nodegroups"].values():
535     DowngradeNodeParams(group)
536
537
538 def DowngradeNodes(config_data):
539   for group in config_data["nodes"].values():
540     DowngradeNodeParams(group)
541
542
543 def DowngradeInstances(config_data):
544   instances = config_data.get("instances", None)
545   if instances is None:
546     raise Error("Cannot find the 'instances' key in the configuration")
547
548   for (_, iobj) in instances.items():
549     if "osparams_private" in iobj:
550       del iobj["osparams_private"]
551
552     DowngradeInstanceHVParams(iobj)
553
554
555 def DowngradeTopLevelDisks(config_data):
556   """Downgrade the disks from config top level citizens."""
557   def RemoveSerialNumber(disk):
558     if "serial_no" in disk:
559       del disk["serial_no"]
560     if "ctime" in disk:
561       del disk["ctime"]
562     if "mtime" in disk:
563       del disk["mtime"]
564     if "children" in disk:
565       for subdisk in disk["children"]:
566         RemoveSerialNumber(subdisk)
567
568   if "instances" not in config_data:
569     raise Error("Can't find the 'instances' key in the configuration!")
570
571   if "disks" not in config_data:
572     # Disks are not top level citizens
573     return
574
575   for iobj in config_data["instances"].values():
576     disks = list()
577     for disk_uuid in iobj["disks"]:
578       disk_obj = config_data["disks"][disk_uuid]
579       RemoveSerialNumber(disk_obj)
580       disks.append(disk_obj)
581       del config_data["disks"][disk_uuid]
582     iobj["disks"] = disks
583
584   for disk_uuid in config_data["disks"].keys():
585     print("Disk %s is not attached to any Instance and will be removed."
586           % disk_uuid)
587   del config_data["disks"]
588
589
590 def DowngradeAll(config_data):
591   # Any code specific to a particular version should be labeled that way, so
592   # it can be removed when updating to the next version.
593   config_data["version"] = version.BuildVersion(DOWNGRADE_MAJOR,
594                                                 DOWNGRADE_MINOR, 0)
595   DowngradeCluster(config_data)
596   DowngradeGroups(config_data)
597   DowngradeNodes(config_data)
598   DowngradeTopLevelDisks(config_data)
599   DowngradeInstances(config_data)
600
601
602 def _ParseOptions():
603   parser = optparse.OptionParser(usage="%prog [--debug|--verbose] [--force]")
604   parser.add_option("--dry-run", dest="dry_run",
605                     action="store_true",
606                     help="Try to do the conversion, but don't write"
607                          " output file")
608   parser.add_option(cli.FORCE_OPT)
609   parser.add_option(cli.DEBUG_OPT)
610   parser.add_option(cli.VERBOSE_OPT)
611   parser.add_option("--ignore-hostname", dest="ignore_hostname",
612                     action="store_true", default=False,
613                     help="Don't abort if hostname doesn't match")
614   parser.add_option("--path", help="Convert configuration in this"
615                     " directory instead of '%s'" % pathutils.DATA_DIR,
616                     default=pathutils.DATA_DIR, dest="data_dir")
617   parser.add_option("--confdir",
618                     help=("Use this directory instead of '%s'" %
619                           pathutils.CONF_DIR),
620                     default=pathutils.CONF_DIR, dest="conf_dir")
621   parser.add_option("--no-verify",
622                     help="Do not verify configuration after upgrade",
623                     action="store_true", dest="no_verify", default=False)
624   parser.add_option("--downgrade",
625                     help="Downgrade to the previous stable version",
626                     action="store_true", dest="downgrade", default=False)
627   return parser.parse_args()
628
629
630 def _ComposePaths():
631   # We need to keep filenames locally because they might be renamed between
632   # versions.
633   options.data_dir = os.path.abspath(options.data_dir)
634   options.CONFIG_DATA_PATH = options.data_dir + "/config.data"
635   options.SERVER_PEM_PATH = options.data_dir + "/server.pem"
636   options.CLIENT_PEM_PATH = options.data_dir + "/client.pem"
637   options.KNOWN_HOSTS_PATH = options.data_dir + "/known_hosts"
638   options.RAPI_CERT_FILE = options.data_dir + "/rapi.pem"
639   options.SPICE_CERT_FILE = options.data_dir + "/spice.pem"
640   options.SPICE_CACERT_FILE = options.data_dir + "/spice-ca.pem"
641   options.RAPI_USERS_FILE = options.data_dir + "/rapi/users"
642   options.RAPI_USERS_FILE_PRE24 = options.data_dir + "/rapi_users"
643   options.CONFD_HMAC_KEY = options.data_dir + "/hmac.key"
644   options.CDS_FILE = options.data_dir + "/cluster-domain-secret"
645   options.SSCONF_MASTER_NODE = options.data_dir + "/ssconf_master_node"
646   options.WATCHER_STATEFILE = options.data_dir + "/watcher.data"
647   options.FILE_STORAGE_PATHS_FILE = options.conf_dir + "/file-storage-paths"
648
649
650 def _AskUser():
651   if not options.force:
652     if options.downgrade:
653       usertext = ("The configuration is going to be DOWNGRADED to version %s.%s"
654                   " Some configuration data might be removed if they don't fit"
655                   " in the old format. Please make sure you have read the"
656                   " upgrade notes (available in the UPGRADE file and included"
657                   " in other documentation formats) to understand what they"
658                   " are. Continue with *DOWNGRADING* the configuration?" %
659                   (DOWNGRADE_MAJOR, DOWNGRADE_MINOR))
660     else:
661       usertext = ("Please make sure you have read the upgrade notes for"
662                   " Ganeti %s (available in the UPGRADE file and included"
663                   " in other documentation formats). Continue with upgrading"
664                   " configuration?" % constants.RELEASE_VERSION)
665     if not cli.AskUser(usertext):
666       sys.exit(constants.EXIT_FAILURE)
667
668
669 def _Downgrade(config_major, config_minor, config_version, config_data,
670                config_revision):
671   if not ((config_major == TARGET_MAJOR and config_minor == TARGET_MINOR) or
672           (config_major == DOWNGRADE_MAJOR and
673            config_minor == DOWNGRADE_MINOR)):
674     raise Error("Downgrade supported only from the latest version (%s.%s),"
675                 " found %s (%s.%s.%s) instead" %
676                 (TARGET_MAJOR, TARGET_MINOR, config_version, config_major,
677                  config_minor, config_revision))
678   DowngradeAll(config_data)
679
680
681 def _TestLoadingConfigFile():
682   # test loading the config file
683   all_ok = True
684   if not (options.dry_run or options.no_verify):
685     logging.info("Testing the new config file...")
686     cfg = config.ConfigWriter(cfg_file=options.CONFIG_DATA_PATH,
687                               accept_foreign=options.ignore_hostname,
688                               offline=True)
689     # if we reached this, it's all fine
690     vrfy = cfg.VerifyConfig()
691     if vrfy:
692       logging.error("Errors after conversion:")
693       for item in vrfy:
694         logging.error(" - %s", item)
695       all_ok = False
696     else:
697       logging.info("File loaded successfully after upgrading")
698     del cfg
699
700   if options.downgrade:
701     action = "downgraded"
702     out_ver = "%s.%s" % (DOWNGRADE_MAJOR, DOWNGRADE_MINOR)
703   else:
704     action = "upgraded"
705     out_ver = constants.RELEASE_VERSION
706   if all_ok:
707     cli.ToStderr("Configuration successfully %s to version %s.",
708                  action, out_ver)
709   else:
710     cli.ToStderr("Configuration %s to version %s, but there are errors."
711                  "\nPlease review the file.", action, out_ver)
712
713
714 def main():
715   """Main program.
716
717   """
718   global options, args # pylint: disable=W0603
719
720   (options, args) = _ParseOptions()
721   _ComposePaths()
722
723   SetupLogging()
724
725   # Option checking
726   if args:
727     raise Error("No arguments expected")
728   if options.downgrade and not options.no_verify:
729     options.no_verify = True
730
731   # Check master name
732   if not (CheckHostname(options.SSCONF_MASTER_NODE) or options.ignore_hostname):
733     logging.error("Aborting due to hostname mismatch")
734     sys.exit(constants.EXIT_FAILURE)
735
736   _AskUser()
737
738   # Check whether it's a Ganeti configuration directory
739   if not (os.path.isfile(options.CONFIG_DATA_PATH) and
740           os.path.isfile(options.SERVER_PEM_PATH) and
741           os.path.isfile(options.KNOWN_HOSTS_PATH)):
742     raise Error(("%s does not seem to be a Ganeti configuration"
743                  " directory") % options.data_dir)
744
745   if not os.path.isdir(options.conf_dir):
746     raise Error("Not a directory: %s" % options.conf_dir)
747
748   config_data = serializer.LoadJson(utils.ReadFile(options.CONFIG_DATA_PATH))
749
750   try:
751     config_version = config_data["version"]
752   except KeyError:
753     raise Error("Unable to determine configuration version")
754
755   (config_major, config_minor, config_revision) = \
756     version.SplitVersion(config_version)
757
758   logging.info("Found configuration version %s (%d.%d.%d)",
759                config_version, config_major, config_minor, config_revision)
760
761   if "config_version" in config_data["cluster"]:
762     raise Error("Inconsistent configuration: found config_version in"
763                 " configuration file")
764
765   # Downgrade to the previous stable version
766   if options.downgrade:
767     _Downgrade(config_major, config_minor, config_version, config_data,
768                config_revision)
769
770   # Upgrade from 2.{0..10} to 2.12
771   elif config_major == 2 and config_minor in range(0, 12):
772     if config_revision != 0:
773       logging.warning("Config revision is %s, not 0", config_revision)
774     UpgradeAll(config_data)
775
776   elif config_major == TARGET_MAJOR and config_minor == TARGET_MINOR:
777     logging.info("No changes necessary")
778
779   else:
780     raise Error("Configuration version %d.%d.%d not supported by this tool" %
781                 (config_major, config_minor, config_revision))
782
783   try:
784     logging.info("Writing configuration file to %s", options.CONFIG_DATA_PATH)
785     utils.WriteFile(file_name=options.CONFIG_DATA_PATH,
786                     data=serializer.DumpJson(config_data),
787                     mode=0600,
788                     dry_run=options.dry_run,
789                     backup=True)
790
791     if not options.dry_run:
792       # This creates the cluster certificate if it does not exist yet.
793       # In this case, we do not automatically create a client certificate
794       # as well, because if the cluster certificate did not exist before,
795       # no client certificate will exist on any node yet. In this case
796       # all client certificate should be renewed by 'gnt-cluster
797       # renew-crypto --new-node-certificates'. This will be enforced
798       # by a nagging warning in 'gnt-cluster verify'.
799       bootstrap.GenerateClusterCrypto(
800         False, False, False, False, False, False, None,
801         nodecert_file=options.SERVER_PEM_PATH,
802         rapicert_file=options.RAPI_CERT_FILE,
803         spicecert_file=options.SPICE_CERT_FILE,
804         spicecacert_file=options.SPICE_CACERT_FILE,
805         hmackey_file=options.CONFD_HMAC_KEY,
806         cds_file=options.CDS_FILE)
807
808   except Exception:
809     logging.critical("Writing configuration failed. It is probably in an"
810                      " inconsistent state and needs manual intervention.")
811     raise
812
813   _TestLoadingConfigFile()
814
815
816 if __name__ == "__main__":
817   main()