9c2775d408ad61f19a0686d47068835454578abc
[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
528 def DowngradeGroups(config_data):
529   for group in config_data["nodegroups"].values():
530     DowngradeNodeParams(group)
531
532
533 def DowngradeNodes(config_data):
534   for group in config_data["nodes"].values():
535     DowngradeNodeParams(group)
536
537
538 def DowngradeInstances(config_data):
539   instances = config_data.get("instances", None)
540   if instances is None:
541     raise Error("Cannot find the 'instances' key in the configuration")
542
543   for (_, iobj) in instances.items():
544     if "osparams_private" in iobj:
545       del iobj["osparams_private"]
546
547     DowngradeInstanceHVParams(iobj)
548
549
550 def DowngradeTopLevelDisks(config_data):
551   """Downgrade the disks from config top level citizens."""
552   def RemoveSerialNumber(disk):
553     if "serial_no" in disk:
554       del disk["serial_no"]
555     if "ctime" in disk:
556       del disk["ctime"]
557     if "mtime" in disk:
558       del disk["mtime"]
559     if "children" in disk:
560       for subdisk in disk["children"]:
561         RemoveSerialNumber(subdisk)
562
563   if "instances" not in config_data:
564     raise Error("Can't find the 'instances' key in the configuration!")
565
566   if "disks" not in config_data:
567     # Disks are not top level citizens
568     return
569
570   for iobj in config_data["instances"].values():
571     disks = list()
572     for disk_uuid in iobj["disks"]:
573       disk_obj = config_data["disks"][disk_uuid]
574       RemoveSerialNumber(disk_obj)
575       disks.append(disk_obj)
576       del config_data["disks"][disk_uuid]
577     iobj["disks"] = disks
578
579   for disk_uuid in config_data["disks"].keys():
580     print("Disk %s is not attached to any Instance and will be removed."
581           % disk_uuid)
582   del config_data["disks"]
583
584
585 def DowngradeAll(config_data):
586   # Any code specific to a particular version should be labeled that way, so
587   # it can be removed when updating to the next version.
588   config_data["version"] = version.BuildVersion(DOWNGRADE_MAJOR,
589                                                 DOWNGRADE_MINOR, 0)
590   DowngradeCluster(config_data)
591   DowngradeGroups(config_data)
592   DowngradeNodes(config_data)
593   DowngradeTopLevelDisks(config_data)
594   DowngradeInstances(config_data)
595
596
597 def _ParseOptions():
598   parser = optparse.OptionParser(usage="%prog [--debug|--verbose] [--force]")
599   parser.add_option("--dry-run", dest="dry_run",
600                     action="store_true",
601                     help="Try to do the conversion, but don't write"
602                          " output file")
603   parser.add_option(cli.FORCE_OPT)
604   parser.add_option(cli.DEBUG_OPT)
605   parser.add_option(cli.VERBOSE_OPT)
606   parser.add_option("--ignore-hostname", dest="ignore_hostname",
607                     action="store_true", default=False,
608                     help="Don't abort if hostname doesn't match")
609   parser.add_option("--path", help="Convert configuration in this"
610                     " directory instead of '%s'" % pathutils.DATA_DIR,
611                     default=pathutils.DATA_DIR, dest="data_dir")
612   parser.add_option("--confdir",
613                     help=("Use this directory instead of '%s'" %
614                           pathutils.CONF_DIR),
615                     default=pathutils.CONF_DIR, dest="conf_dir")
616   parser.add_option("--no-verify",
617                     help="Do not verify configuration after upgrade",
618                     action="store_true", dest="no_verify", default=False)
619   parser.add_option("--downgrade",
620                     help="Downgrade to the previous stable version",
621                     action="store_true", dest="downgrade", default=False)
622   return parser.parse_args()
623
624
625 def _ComposePaths():
626   # We need to keep filenames locally because they might be renamed between
627   # versions.
628   options.data_dir = os.path.abspath(options.data_dir)
629   options.CONFIG_DATA_PATH = options.data_dir + "/config.data"
630   options.SERVER_PEM_PATH = options.data_dir + "/server.pem"
631   options.CLIENT_PEM_PATH = options.data_dir + "/client.pem"
632   options.KNOWN_HOSTS_PATH = options.data_dir + "/known_hosts"
633   options.RAPI_CERT_FILE = options.data_dir + "/rapi.pem"
634   options.SPICE_CERT_FILE = options.data_dir + "/spice.pem"
635   options.SPICE_CACERT_FILE = options.data_dir + "/spice-ca.pem"
636   options.RAPI_USERS_FILE = options.data_dir + "/rapi/users"
637   options.RAPI_USERS_FILE_PRE24 = options.data_dir + "/rapi_users"
638   options.CONFD_HMAC_KEY = options.data_dir + "/hmac.key"
639   options.CDS_FILE = options.data_dir + "/cluster-domain-secret"
640   options.SSCONF_MASTER_NODE = options.data_dir + "/ssconf_master_node"
641   options.WATCHER_STATEFILE = options.data_dir + "/watcher.data"
642   options.FILE_STORAGE_PATHS_FILE = options.conf_dir + "/file-storage-paths"
643
644
645 def _AskUser():
646   if not options.force:
647     if options.downgrade:
648       usertext = ("The configuration is going to be DOWNGRADED to version %s.%s"
649                   " Some configuration data might be removed if they don't fit"
650                   " in the old format. Please make sure you have read the"
651                   " upgrade notes (available in the UPGRADE file and included"
652                   " in other documentation formats) to understand what they"
653                   " are. Continue with *DOWNGRADING* the configuration?" %
654                   (DOWNGRADE_MAJOR, DOWNGRADE_MINOR))
655     else:
656       usertext = ("Please make sure you have read the upgrade notes for"
657                   " Ganeti %s (available in the UPGRADE file and included"
658                   " in other documentation formats). Continue with upgrading"
659                   " configuration?" % constants.RELEASE_VERSION)
660     if not cli.AskUser(usertext):
661       sys.exit(constants.EXIT_FAILURE)
662
663
664 def _Downgrade(config_major, config_minor, config_version, config_data,
665                config_revision):
666   if not ((config_major == TARGET_MAJOR and config_minor == TARGET_MINOR) or
667           (config_major == DOWNGRADE_MAJOR and
668            config_minor == DOWNGRADE_MINOR)):
669     raise Error("Downgrade supported only from the latest version (%s.%s),"
670                 " found %s (%s.%s.%s) instead" %
671                 (TARGET_MAJOR, TARGET_MINOR, config_version, config_major,
672                  config_minor, config_revision))
673   DowngradeAll(config_data)
674
675
676 def _TestLoadingConfigFile():
677   # test loading the config file
678   all_ok = True
679   if not (options.dry_run or options.no_verify):
680     logging.info("Testing the new config file...")
681     cfg = config.ConfigWriter(cfg_file=options.CONFIG_DATA_PATH,
682                               accept_foreign=options.ignore_hostname,
683                               offline=True)
684     # if we reached this, it's all fine
685     vrfy = cfg.VerifyConfig()
686     if vrfy:
687       logging.error("Errors after conversion:")
688       for item in vrfy:
689         logging.error(" - %s", item)
690       all_ok = False
691     else:
692       logging.info("File loaded successfully after upgrading")
693     del cfg
694
695   if options.downgrade:
696     action = "downgraded"
697     out_ver = "%s.%s" % (DOWNGRADE_MAJOR, DOWNGRADE_MINOR)
698   else:
699     action = "upgraded"
700     out_ver = constants.RELEASE_VERSION
701   if all_ok:
702     cli.ToStderr("Configuration successfully %s to version %s.",
703                  action, out_ver)
704   else:
705     cli.ToStderr("Configuration %s to version %s, but there are errors."
706                  "\nPlease review the file.", action, out_ver)
707
708
709 def main():
710   """Main program.
711
712   """
713   global options, args # pylint: disable=W0603
714
715   (options, args) = _ParseOptions()
716   _ComposePaths()
717
718   SetupLogging()
719
720   # Option checking
721   if args:
722     raise Error("No arguments expected")
723   if options.downgrade and not options.no_verify:
724     options.no_verify = True
725
726   # Check master name
727   if not (CheckHostname(options.SSCONF_MASTER_NODE) or options.ignore_hostname):
728     logging.error("Aborting due to hostname mismatch")
729     sys.exit(constants.EXIT_FAILURE)
730
731   _AskUser()
732
733   # Check whether it's a Ganeti configuration directory
734   if not (os.path.isfile(options.CONFIG_DATA_PATH) and
735           os.path.isfile(options.SERVER_PEM_PATH) and
736           os.path.isfile(options.KNOWN_HOSTS_PATH)):
737     raise Error(("%s does not seem to be a Ganeti configuration"
738                  " directory") % options.data_dir)
739
740   if not os.path.isdir(options.conf_dir):
741     raise Error("Not a directory: %s" % options.conf_dir)
742
743   config_data = serializer.LoadJson(utils.ReadFile(options.CONFIG_DATA_PATH))
744
745   try:
746     config_version = config_data["version"]
747   except KeyError:
748     raise Error("Unable to determine configuration version")
749
750   (config_major, config_minor, config_revision) = \
751     version.SplitVersion(config_version)
752
753   logging.info("Found configuration version %s (%d.%d.%d)",
754                config_version, config_major, config_minor, config_revision)
755
756   if "config_version" in config_data["cluster"]:
757     raise Error("Inconsistent configuration: found config_version in"
758                 " configuration file")
759
760   # Downgrade to the previous stable version
761   if options.downgrade:
762     _Downgrade(config_major, config_minor, config_version, config_data,
763                config_revision)
764
765   # Upgrade from 2.{0..10} to 2.12
766   elif config_major == 2 and config_minor in range(0, 12):
767     if config_revision != 0:
768       logging.warning("Config revision is %s, not 0", config_revision)
769     UpgradeAll(config_data)
770
771   elif config_major == TARGET_MAJOR and config_minor == TARGET_MINOR:
772     logging.info("No changes necessary")
773
774   else:
775     raise Error("Configuration version %d.%d.%d not supported by this tool" %
776                 (config_major, config_minor, config_revision))
777
778   try:
779     logging.info("Writing configuration file to %s", options.CONFIG_DATA_PATH)
780     utils.WriteFile(file_name=options.CONFIG_DATA_PATH,
781                     data=serializer.DumpJson(config_data),
782                     mode=0600,
783                     dry_run=options.dry_run,
784                     backup=True)
785
786     if not options.dry_run:
787       # This creates the cluster certificate if it does not exist yet.
788       # In this case, we do not automatically create a client certificate
789       # as well, because if the cluster certificate did not exist before,
790       # no client certificate will exist on any node yet. In this case
791       # all client certificate should be renewed by 'gnt-cluster
792       # renew-crypto --new-node-certificates'. This will be enforced
793       # by a nagging warning in 'gnt-cluster verify'.
794       bootstrap.GenerateClusterCrypto(
795         False, False, False, False, False, False, None,
796         nodecert_file=options.SERVER_PEM_PATH,
797         rapicert_file=options.RAPI_CERT_FILE,
798         spicecert_file=options.SPICE_CERT_FILE,
799         spicecacert_file=options.SPICE_CACERT_FILE,
800         hmackey_file=options.CONFD_HMAC_KEY,
801         cds_file=options.CDS_FILE)
802
803   except Exception:
804     logging.critical("Writing configuration failed. It is probably in an"
805                      " inconsistent state and needs manual intervention.")
806     raise
807
808   _TestLoadingConfigFile()
809
810
811 if __name__ == "__main__":
812   main()