4 # Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012, 2013 Google Inc.
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.
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.
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
22 """Tool to upgrade the configuration file.
24 This code handles only the types supported by simplejson. As an
25 example, 'set' is a 'list'.
36 from cStringIO import StringIO
38 from ganeti import constants
39 from ganeti import serializer
40 from ganeti import utils
41 from ganeti import cli
42 from ganeti import bootstrap
43 from ganeti import config
44 from ganeti import netutils
45 from ganeti import pathutils
52 #: Target major version we will upgrade to
54 #: Target minor version we will upgrade to
56 #: Target major version for downgrade
58 #: Target minor version for downgrade
62 class Error(Exception):
63 """Generic exception"""
68 """Configures the logging module.
71 formatter = logging.Formatter("%(asctime)s: %(message)s")
73 stderr_handler = logging.StreamHandler()
74 stderr_handler.setFormatter(formatter)
76 stderr_handler.setLevel(logging.NOTSET)
78 stderr_handler.setLevel(logging.INFO)
80 stderr_handler.setLevel(logging.WARNING)
82 root_logger = logging.getLogger("")
83 root_logger.setLevel(logging.NOTSET)
84 root_logger.addHandler(stderr_handler)
87 def CheckHostname(path):
88 """Ensures hostname matches ssconf value.
90 @param path: Path to ssconf file
93 ssconf_master_node = utils.ReadOneLineFile(path)
94 hostname = netutils.GetHostname().name
96 if ssconf_master_node == hostname:
99 logging.warning("Warning: ssconf says master node is '%s', but this"
100 " machine's name is '%s'; this tool must be run on"
101 " the master node", ssconf_master_node, hostname)
105 def UpgradeNetworks(config_data):
106 networks = config_data.get("networks", None)
108 config_data["networks"] = {}
111 def UpgradeGroups(config_data):
112 for group in config_data["nodegroups"].values():
113 networks = group.get("networks", None)
115 group["networks"] = {}
118 def UpgradeInstances(config_data):
119 network2uuid = dict((n["name"], n["uuid"])
120 for n in config_data["networks"].values())
121 if "instances" not in config_data:
122 raise Error("Can't find the 'instances' key in the configuration!")
124 for instance, iobj in config_data["instances"].items():
125 for nic in iobj["nics"]:
126 name = nic.get("network", None)
128 uuid = network2uuid.get(name, None)
130 print("NIC with network name %s found."
131 " Substituting with uuid %s." % (name, uuid))
132 nic["network"] = uuid
134 if "disks" not in iobj:
135 raise Error("Instance '%s' doesn't have a disks entry?!" % instance)
136 disks = iobj["disks"]
137 for idx, dobj in enumerate(disks):
138 expected = "disk/%s" % idx
139 current = dobj.get("iv_name", "")
140 if current != expected:
141 logging.warning("Updating iv_name for instance %s/disk %s"
142 " from '%s' to '%s'",
143 instance, idx, current, expected)
144 dobj["iv_name"] = expected
147 def UpgradeRapiUsers():
148 if (os.path.isfile(options.RAPI_USERS_FILE_PRE24) and
149 not os.path.islink(options.RAPI_USERS_FILE_PRE24)):
150 if os.path.exists(options.RAPI_USERS_FILE):
151 raise Error("Found pre-2.4 RAPI users file at %s, but another file"
152 " already exists at %s" %
153 (options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE))
154 logging.info("Found pre-2.4 RAPI users file at %s, renaming to %s",
155 options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
156 if not options.dry_run:
157 utils.RenameFile(options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE,
158 mkdir=True, mkdir_mode=0750)
160 # Create a symlink for RAPI users file
161 if (not (os.path.islink(options.RAPI_USERS_FILE_PRE24) or
162 os.path.isfile(options.RAPI_USERS_FILE_PRE24)) and
163 os.path.isfile(options.RAPI_USERS_FILE)):
164 logging.info("Creating symlink from %s to %s",
165 options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
166 if not options.dry_run:
167 os.symlink(options.RAPI_USERS_FILE, options.RAPI_USERS_FILE_PRE24)
170 def UpgradeWatcher():
171 # Remove old watcher state file if it exists
172 if os.path.exists(options.WATCHER_STATEFILE):
173 logging.info("Removing watcher state file %s", options.WATCHER_STATEFILE)
174 if not options.dry_run:
175 utils.RemoveFile(options.WATCHER_STATEFILE)
178 def UpgradeFileStoragePaths(config_data):
179 # Write file storage paths
180 if not os.path.exists(options.FILE_STORAGE_PATHS_FILE):
181 cluster = config_data["cluster"]
182 file_storage_dir = cluster.get("file_storage_dir")
183 shared_file_storage_dir = cluster.get("shared_file_storage_dir")
186 logging.info("Ganeti 2.7 and later only allow whitelisted directories"
187 " for file storage; writing existing configuration values"
189 options.FILE_STORAGE_PATHS_FILE)
192 logging.info("File storage directory: %s", file_storage_dir)
193 if shared_file_storage_dir:
194 logging.info("Shared file storage directory: %s",
195 shared_file_storage_dir)
198 buf.write("# List automatically generated from configuration by\n")
199 buf.write("# cfgupgrade at %s\n" % time.asctime())
201 buf.write("%s\n" % file_storage_dir)
202 if shared_file_storage_dir:
203 buf.write("%s\n" % shared_file_storage_dir)
204 utils.WriteFile(file_name=options.FILE_STORAGE_PATHS_FILE,
207 dry_run=options.dry_run,
211 def UpgradeAll(config_data):
212 config_data["version"] = constants.BuildVersion(TARGET_MAJOR,
216 UpgradeFileStoragePaths(config_data)
217 UpgradeNetworks(config_data)
218 UpgradeGroups(config_data)
219 UpgradeInstances(config_data)
222 def DowngradeStorageTypes(cluster):
223 # Remove storage types to downgrade to 2.7
224 if "enabled_storage_types" in cluster:
225 logging.warning("Removing cluster storage types; value = %s",
226 utils.CommaJoin(cluster["enabled_storage_types"]))
227 del cluster["enabled_storage_types"]
230 def DowngradeCluster(config_data):
231 cluster = config_data.get("cluster", None)
233 raise Error("Cannot find cluster")
234 DowngradeStorageTypes(cluster)
237 def DowngradeAll(config_data):
238 # Any code specific to a particular version should be labeled that way, so
239 # it can be removed when updating to the next version.
240 DowngradeCluster(config_data)
247 global options, args # pylint: disable=W0603
250 parser = optparse.OptionParser(usage="%prog [--debug|--verbose] [--force]")
251 parser.add_option("--dry-run", dest="dry_run",
253 help="Try to do the conversion, but don't write"
255 parser.add_option(cli.FORCE_OPT)
256 parser.add_option(cli.DEBUG_OPT)
257 parser.add_option(cli.VERBOSE_OPT)
258 parser.add_option("--ignore-hostname", dest="ignore_hostname",
259 action="store_true", default=False,
260 help="Don't abort if hostname doesn't match")
261 parser.add_option("--path", help="Convert configuration in this"
262 " directory instead of '%s'" % pathutils.DATA_DIR,
263 default=pathutils.DATA_DIR, dest="data_dir")
264 parser.add_option("--confdir",
265 help=("Use this directory instead of '%s'" %
267 default=pathutils.CONF_DIR, dest="conf_dir")
268 parser.add_option("--no-verify",
269 help="Do not verify configuration after upgrade",
270 action="store_true", dest="no_verify", default=False)
271 parser.add_option("--downgrade",
272 help="Downgrade to the previous stable version",
273 action="store_true", dest="downgrade", default=False)
274 (options, args) = parser.parse_args()
276 # We need to keep filenames locally because they might be renamed between
278 options.data_dir = os.path.abspath(options.data_dir)
279 options.CONFIG_DATA_PATH = options.data_dir + "/config.data"
280 options.SERVER_PEM_PATH = options.data_dir + "/server.pem"
281 options.KNOWN_HOSTS_PATH = options.data_dir + "/known_hosts"
282 options.RAPI_CERT_FILE = options.data_dir + "/rapi.pem"
283 options.SPICE_CERT_FILE = options.data_dir + "/spice.pem"
284 options.SPICE_CACERT_FILE = options.data_dir + "/spice-ca.pem"
285 options.RAPI_USERS_FILE = options.data_dir + "/rapi/users"
286 options.RAPI_USERS_FILE_PRE24 = options.data_dir + "/rapi_users"
287 options.CONFD_HMAC_KEY = options.data_dir + "/hmac.key"
288 options.CDS_FILE = options.data_dir + "/cluster-domain-secret"
289 options.SSCONF_MASTER_NODE = options.data_dir + "/ssconf_master_node"
290 options.WATCHER_STATEFILE = options.data_dir + "/watcher.data"
291 options.FILE_STORAGE_PATHS_FILE = options.conf_dir + "/file-storage-paths"
297 raise Error("No arguments expected")
298 if options.downgrade and not options.no_verify:
299 options.no_verify = True
302 if not (CheckHostname(options.SSCONF_MASTER_NODE) or options.ignore_hostname):
303 logging.error("Aborting due to hostname mismatch")
304 sys.exit(constants.EXIT_FAILURE)
306 if not options.force:
307 if options.downgrade:
308 usertext = ("The configuration is going to be DOWNGRADED to version %s.%s"
309 " Some configuration data might be removed if they don't fit"
310 " in the old format. Please make sure you have read the"
311 " upgrade notes (available in the UPGRADE file and included"
312 " in other documentation formats) to understand what they"
313 " are. Continue with *DOWNGRADING* the configuration?" %
314 (DOWNGRADE_MAJOR, DOWNGRADE_MINOR))
316 usertext = ("Please make sure you have read the upgrade notes for"
317 " Ganeti %s (available in the UPGRADE file and included"
318 " in other documentation formats). Continue with upgrading"
319 " configuration?" % constants.RELEASE_VERSION)
320 if not cli.AskUser(usertext):
321 sys.exit(constants.EXIT_FAILURE)
323 # Check whether it's a Ganeti configuration directory
324 if not (os.path.isfile(options.CONFIG_DATA_PATH) and
325 os.path.isfile(options.SERVER_PEM_PATH) and
326 os.path.isfile(options.KNOWN_HOSTS_PATH)):
327 raise Error(("%s does not seem to be a Ganeti configuration"
328 " directory") % options.data_dir)
330 if not os.path.isdir(options.conf_dir):
331 raise Error("Not a directory: %s" % options.conf_dir)
333 config_data = serializer.LoadJson(utils.ReadFile(options.CONFIG_DATA_PATH))
336 config_version = config_data["version"]
338 raise Error("Unable to determine configuration version")
340 (config_major, config_minor, config_revision) = \
341 constants.SplitVersion(config_version)
343 logging.info("Found configuration version %s (%d.%d.%d)",
344 config_version, config_major, config_minor, config_revision)
346 if "config_version" in config_data["cluster"]:
347 raise Error("Inconsistent configuration: found config_version in"
348 " configuration file")
350 # Downgrade to the previous stable version
351 if options.downgrade:
352 if config_major != TARGET_MAJOR or config_minor != TARGET_MINOR:
353 raise Error("Downgrade supported only from the latest version (%s.%s),"
354 " found %s (%s.%s.%s) instead" %
355 (TARGET_MAJOR, TARGET_MINOR, config_version, config_major,
356 config_minor, config_revision))
357 DowngradeAll(config_data)
359 # Upgrade from 2.{0..6} to 2.7
360 elif config_major == 2 and config_minor in (0, 1, 2, 3, 4, 5, 6):
361 if config_revision != 0:
362 logging.warning("Config revision is %s, not 0", config_revision)
363 UpgradeAll(config_data)
365 elif config_major == TARGET_MAJOR and config_minor == TARGET_MINOR:
366 logging.info("No changes necessary")
369 raise Error("Configuration version %d.%d.%d not supported by this tool" %
370 (config_major, config_minor, config_revision))
373 logging.info("Writing configuration file to %s", options.CONFIG_DATA_PATH)
374 utils.WriteFile(file_name=options.CONFIG_DATA_PATH,
375 data=serializer.DumpJson(config_data),
377 dry_run=options.dry_run,
380 if not options.dry_run:
381 bootstrap.GenerateClusterCrypto(
382 False, False, False, False, False,
383 nodecert_file=options.SERVER_PEM_PATH,
384 rapicert_file=options.RAPI_CERT_FILE,
385 spicecert_file=options.SPICE_CERT_FILE,
386 spicecacert_file=options.SPICE_CACERT_FILE,
387 hmackey_file=options.CONFD_HMAC_KEY,
388 cds_file=options.CDS_FILE)
391 logging.critical("Writing configuration failed. It is probably in an"
392 " inconsistent state and needs manual intervention.")
395 # test loading the config file
397 if not (options.dry_run or options.no_verify):
398 logging.info("Testing the new config file...")
399 cfg = config.ConfigWriter(cfg_file=options.CONFIG_DATA_PATH,
400 accept_foreign=options.ignore_hostname,
402 # if we reached this, it's all fine
403 vrfy = cfg.VerifyConfig()
405 logging.error("Errors after conversion:")
407 logging.error(" - %s", item)
410 logging.info("File loaded successfully after upgrading")
413 if options.downgrade:
414 action = "downgraded"
415 out_ver = "%s.%s" % (DOWNGRADE_MAJOR, DOWNGRADE_MINOR)
418 out_ver = constants.RELEASE_VERSION
420 cli.ToStderr("Configuration successfully %s to version %s.",
423 cli.ToStderr("Configuration %s to version %s, but there are errors."
424 "\nPlease review the file.", action, out_ver)
427 if __name__ == "__main__":