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 _FillIPolicySpecs(default_ipolicy, ipolicy):
106 if "minmax" in ipolicy:
107 for (key, spec) in ipolicy["minmax"][0].items():
108 for (par, val) in default_ipolicy["minmax"][0][key].items():
113 def UpgradeIPolicy(ipolicy, default_ipolicy, isgroup):
114 minmax_keys = ["min", "max"]
115 if any((k in ipolicy) for k in minmax_keys):
117 for key in minmax_keys:
120 minmax[key] = ipolicy[key]
123 ipolicy["minmax"] = [minmax]
124 if isgroup and "std" in ipolicy:
126 _FillIPolicySpecs(default_ipolicy, ipolicy)
129 def UpgradeNetworks(config_data):
130 networks = config_data.get("networks", None)
132 config_data["networks"] = {}
135 def UpgradeCluster(config_data):
136 cluster = config_data.get("cluster", None)
138 raise Error("Cannot find cluster")
139 ipolicy = cluster.setdefault("ipolicy", None)
141 UpgradeIPolicy(ipolicy, constants.IPOLICY_DEFAULTS, False)
144 def UpgradeGroups(config_data):
145 cl_ipolicy = config_data["cluster"].get("ipolicy")
146 for group in config_data["nodegroups"].values():
147 networks = group.get("networks", None)
149 group["networks"] = {}
150 ipolicy = group.get("ipolicy", None)
152 if cl_ipolicy is None:
153 raise Error("A group defines an instance policy but there is no"
154 " instance policy at cluster level")
155 UpgradeIPolicy(ipolicy, cl_ipolicy, True)
158 def GetExclusiveStorageValue(config_data):
159 """Return a conservative value of the exclusive_storage flag.
161 Return C{True} if the cluster or at least a nodegroup have the flag set.
165 cluster = config_data["cluster"]
166 ndparams = cluster.get("ndparams")
167 if ndparams is not None and ndparams.get("exclusive_storage"):
169 for group in config_data["nodegroups"].values():
170 ndparams = group.get("ndparams")
171 if ndparams is not None and ndparams.get("exclusive_storage"):
176 def UpgradeInstances(config_data):
177 network2uuid = dict((n["name"], n["uuid"])
178 for n in config_data["networks"].values())
179 if "instances" not in config_data:
180 raise Error("Can't find the 'instances' key in the configuration!")
182 missing_spindles = False
183 for instance, iobj in config_data["instances"].items():
184 for nic in iobj["nics"]:
185 name = nic.get("network", None)
187 uuid = network2uuid.get(name, None)
189 print("NIC with network name %s found."
190 " Substituting with uuid %s." % (name, uuid))
191 nic["network"] = uuid
193 if "disks" not in iobj:
194 raise Error("Instance '%s' doesn't have a disks entry?!" % instance)
195 disks = iobj["disks"]
196 for idx, dobj in enumerate(disks):
197 expected = "disk/%s" % idx
198 current = dobj.get("iv_name", "")
199 if current != expected:
200 logging.warning("Updating iv_name for instance %s/disk %s"
201 " from '%s' to '%s'",
202 instance, idx, current, expected)
203 dobj["iv_name"] = expected
204 if not "spindles" in dobj:
205 missing_spindles = True
207 if GetExclusiveStorageValue(config_data) and missing_spindles:
208 # We cannot be sure that the instances that are missing spindles have
209 # exclusive storage enabled (the check would be more complicated), so we
210 # give a noncommittal message
211 logging.warning("Some instance disks could be needing to update the"
212 " spindles parameter; you can check by running"
213 " 'gnt-cluster verify', and fix any problem with"
214 " 'gnt-cluster repair-disk-sizes'")
217 def UpgradeRapiUsers():
218 if (os.path.isfile(options.RAPI_USERS_FILE_PRE24) and
219 not os.path.islink(options.RAPI_USERS_FILE_PRE24)):
220 if os.path.exists(options.RAPI_USERS_FILE):
221 raise Error("Found pre-2.4 RAPI users file at %s, but another file"
222 " already exists at %s" %
223 (options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE))
224 logging.info("Found pre-2.4 RAPI users file at %s, renaming to %s",
225 options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
226 if not options.dry_run:
227 utils.RenameFile(options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE,
228 mkdir=True, mkdir_mode=0750)
230 # Create a symlink for RAPI users file
231 if (not (os.path.islink(options.RAPI_USERS_FILE_PRE24) or
232 os.path.isfile(options.RAPI_USERS_FILE_PRE24)) and
233 os.path.isfile(options.RAPI_USERS_FILE)):
234 logging.info("Creating symlink from %s to %s",
235 options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
236 if not options.dry_run:
237 os.symlink(options.RAPI_USERS_FILE, options.RAPI_USERS_FILE_PRE24)
240 def UpgradeWatcher():
241 # Remove old watcher state file if it exists
242 if os.path.exists(options.WATCHER_STATEFILE):
243 logging.info("Removing watcher state file %s", options.WATCHER_STATEFILE)
244 if not options.dry_run:
245 utils.RemoveFile(options.WATCHER_STATEFILE)
248 def UpgradeFileStoragePaths(config_data):
249 # Write file storage paths
250 if not os.path.exists(options.FILE_STORAGE_PATHS_FILE):
251 cluster = config_data["cluster"]
252 file_storage_dir = cluster.get("file_storage_dir")
253 shared_file_storage_dir = cluster.get("shared_file_storage_dir")
256 logging.info("Ganeti 2.7 and later only allow whitelisted directories"
257 " for file storage; writing existing configuration values"
259 options.FILE_STORAGE_PATHS_FILE)
262 logging.info("File storage directory: %s", file_storage_dir)
263 if shared_file_storage_dir:
264 logging.info("Shared file storage directory: %s",
265 shared_file_storage_dir)
268 buf.write("# List automatically generated from configuration by\n")
269 buf.write("# cfgupgrade at %s\n" % time.asctime())
271 buf.write("%s\n" % file_storage_dir)
272 if shared_file_storage_dir:
273 buf.write("%s\n" % shared_file_storage_dir)
274 utils.WriteFile(file_name=options.FILE_STORAGE_PATHS_FILE,
277 dry_run=options.dry_run,
281 def UpgradeAll(config_data):
282 config_data["version"] = constants.BuildVersion(TARGET_MAJOR,
286 UpgradeFileStoragePaths(config_data)
287 UpgradeNetworks(config_data)
288 UpgradeCluster(config_data)
289 UpgradeGroups(config_data)
290 UpgradeInstances(config_data)
293 def DowngradeDisks(disks, owner):
295 # Remove spindles to downgrade to 2.8
296 if "spindles" in disk:
297 logging.warning("Removing spindles (value=%s) from disk %s (%s) of"
299 disk["spindles"], disk["iv_name"], disk["uuid"], owner)
303 def DowngradeInstances(config_data):
304 if "instances" not in config_data:
305 raise Error("Cannot find the 'instances' key in the configuration!")
306 for (iname, iobj) in config_data["instances"].items():
307 if "disks" not in iobj:
308 raise Error("Cannot find 'disks' key for instance %s" % iname)
309 DowngradeDisks(iobj["disks"], iname)
312 def DowngradeAll(config_data):
313 # Any code specific to a particular version should be labeled that way, so
314 # it can be removed when updating to the next version.
315 DowngradeInstances(config_data)
322 global options, args # pylint: disable=W0603
325 parser = optparse.OptionParser(usage="%prog [--debug|--verbose] [--force]")
326 parser.add_option("--dry-run", dest="dry_run",
328 help="Try to do the conversion, but don't write"
330 parser.add_option(cli.FORCE_OPT)
331 parser.add_option(cli.DEBUG_OPT)
332 parser.add_option(cli.VERBOSE_OPT)
333 parser.add_option("--ignore-hostname", dest="ignore_hostname",
334 action="store_true", default=False,
335 help="Don't abort if hostname doesn't match")
336 parser.add_option("--path", help="Convert configuration in this"
337 " directory instead of '%s'" % pathutils.DATA_DIR,
338 default=pathutils.DATA_DIR, dest="data_dir")
339 parser.add_option("--confdir",
340 help=("Use this directory instead of '%s'" %
342 default=pathutils.CONF_DIR, dest="conf_dir")
343 parser.add_option("--no-verify",
344 help="Do not verify configuration after upgrade",
345 action="store_true", dest="no_verify", default=False)
346 parser.add_option("--downgrade",
347 help="Downgrade to the previous stable version",
348 action="store_true", dest="downgrade", default=False)
349 (options, args) = parser.parse_args()
351 # We need to keep filenames locally because they might be renamed between
353 options.data_dir = os.path.abspath(options.data_dir)
354 options.CONFIG_DATA_PATH = options.data_dir + "/config.data"
355 options.SERVER_PEM_PATH = options.data_dir + "/server.pem"
356 options.KNOWN_HOSTS_PATH = options.data_dir + "/known_hosts"
357 options.RAPI_CERT_FILE = options.data_dir + "/rapi.pem"
358 options.SPICE_CERT_FILE = options.data_dir + "/spice.pem"
359 options.SPICE_CACERT_FILE = options.data_dir + "/spice-ca.pem"
360 options.RAPI_USERS_FILE = options.data_dir + "/rapi/users"
361 options.RAPI_USERS_FILE_PRE24 = options.data_dir + "/rapi_users"
362 options.CONFD_HMAC_KEY = options.data_dir + "/hmac.key"
363 options.CDS_FILE = options.data_dir + "/cluster-domain-secret"
364 options.SSCONF_MASTER_NODE = options.data_dir + "/ssconf_master_node"
365 options.WATCHER_STATEFILE = options.data_dir + "/watcher.data"
366 options.FILE_STORAGE_PATHS_FILE = options.conf_dir + "/file-storage-paths"
372 raise Error("No arguments expected")
373 if options.downgrade and not options.no_verify:
374 options.no_verify = True
377 if not (CheckHostname(options.SSCONF_MASTER_NODE) or options.ignore_hostname):
378 logging.error("Aborting due to hostname mismatch")
379 sys.exit(constants.EXIT_FAILURE)
381 if not options.force:
382 if options.downgrade:
383 usertext = ("The configuration is going to be DOWNGRADED to version %s.%s"
384 " Some configuration data might be removed if they don't fit"
385 " in the old format. Please make sure you have read the"
386 " upgrade notes (available in the UPGRADE file and included"
387 " in other documentation formats) to understand what they"
388 " are. Continue with *DOWNGRADING* the configuration?" %
389 (DOWNGRADE_MAJOR, DOWNGRADE_MINOR))
391 usertext = ("Please make sure you have read the upgrade notes for"
392 " Ganeti %s (available in the UPGRADE file and included"
393 " in other documentation formats). Continue with upgrading"
394 " configuration?" % constants.RELEASE_VERSION)
395 if not cli.AskUser(usertext):
396 sys.exit(constants.EXIT_FAILURE)
398 # Check whether it's a Ganeti configuration directory
399 if not (os.path.isfile(options.CONFIG_DATA_PATH) and
400 os.path.isfile(options.SERVER_PEM_PATH) and
401 os.path.isfile(options.KNOWN_HOSTS_PATH)):
402 raise Error(("%s does not seem to be a Ganeti configuration"
403 " directory") % options.data_dir)
405 if not os.path.isdir(options.conf_dir):
406 raise Error("Not a directory: %s" % options.conf_dir)
408 config_data = serializer.LoadJson(utils.ReadFile(options.CONFIG_DATA_PATH))
411 config_version = config_data["version"]
413 raise Error("Unable to determine configuration version")
415 (config_major, config_minor, config_revision) = \
416 constants.SplitVersion(config_version)
418 logging.info("Found configuration version %s (%d.%d.%d)",
419 config_version, config_major, config_minor, config_revision)
421 if "config_version" in config_data["cluster"]:
422 raise Error("Inconsistent configuration: found config_version in"
423 " configuration file")
425 # Downgrade to the previous stable version
426 if options.downgrade:
427 if config_major != TARGET_MAJOR or config_minor != TARGET_MINOR:
428 raise Error("Downgrade supported only from the latest version (%s.%s),"
429 " found %s (%s.%s.%s) instead" %
430 (TARGET_MAJOR, TARGET_MINOR, config_version, config_major,
431 config_minor, config_revision))
432 DowngradeAll(config_data)
434 # Upgrade from 2.{0..7} to 2.7
435 elif config_major == 2 and config_minor in range(0, 8):
436 if config_revision != 0:
437 logging.warning("Config revision is %s, not 0", config_revision)
438 UpgradeAll(config_data)
440 elif config_major == TARGET_MAJOR and config_minor == TARGET_MINOR:
441 logging.info("No changes necessary")
444 raise Error("Configuration version %d.%d.%d not supported by this tool" %
445 (config_major, config_minor, config_revision))
448 logging.info("Writing configuration file to %s", options.CONFIG_DATA_PATH)
449 utils.WriteFile(file_name=options.CONFIG_DATA_PATH,
450 data=serializer.DumpJson(config_data),
452 dry_run=options.dry_run,
455 if not options.dry_run:
456 bootstrap.GenerateClusterCrypto(
457 False, False, False, False, False,
458 nodecert_file=options.SERVER_PEM_PATH,
459 rapicert_file=options.RAPI_CERT_FILE,
460 spicecert_file=options.SPICE_CERT_FILE,
461 spicecacert_file=options.SPICE_CACERT_FILE,
462 hmackey_file=options.CONFD_HMAC_KEY,
463 cds_file=options.CDS_FILE)
466 logging.critical("Writing configuration failed. It is probably in an"
467 " inconsistent state and needs manual intervention.")
470 # test loading the config file
472 if not (options.dry_run or options.no_verify):
473 logging.info("Testing the new config file...")
474 cfg = config.ConfigWriter(cfg_file=options.CONFIG_DATA_PATH,
475 accept_foreign=options.ignore_hostname,
477 # if we reached this, it's all fine
478 vrfy = cfg.VerifyConfig()
480 logging.error("Errors after conversion:")
482 logging.error(" - %s", item)
485 logging.info("File loaded successfully after upgrading")
488 if options.downgrade:
489 action = "downgraded"
490 out_ver = "%s.%s" % (DOWNGRADE_MAJOR, DOWNGRADE_MINOR)
493 out_ver = constants.RELEASE_VERSION
495 cli.ToStderr("Configuration successfully %s to version %s.",
498 cli.ToStderr("Configuration %s to version %s, but there are errors."
499 "\nPlease review the file.", action, out_ver)
502 if __name__ == "__main__":