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 UpgradeIPolicy(ipolicy):
106 minmax_keys = ["min", "max"]
107 if any((k in ipolicy) for k in minmax_keys):
109 ipolicy["minmax"] = minmax
110 for key in minmax_keys:
112 minmax[key] = ipolicy[key]
118 def UpgradeNetworks(config_data):
119 networks = config_data.get("networks", None)
121 config_data["networks"] = {}
124 def UpgradeCluster(config_data):
125 cluster = config_data.get("cluster", None)
127 raise Error("Cannot find cluster")
128 ipolicy = cluster.get("ipolicy", None)
130 UpgradeIPolicy(ipolicy)
133 def UpgradeGroups(config_data):
134 for group in config_data["nodegroups"].values():
135 networks = group.get("networks", None)
137 group["networks"] = {}
138 ipolicy = group.get("ipolicy", None)
140 UpgradeIPolicy(ipolicy)
143 def UpgradeInstances(config_data):
144 network2uuid = dict((n["name"], n["uuid"])
145 for n in config_data["networks"].values())
146 if "instances" not in config_data:
147 raise Error("Can't find the 'instances' key in the configuration!")
149 for instance, iobj in config_data["instances"].items():
150 for nic in iobj["nics"]:
151 name = nic.get("network", None)
153 uuid = network2uuid.get(name, None)
155 print("NIC with network name %s found."
156 " Substituting with uuid %s." % (name, uuid))
157 nic["network"] = uuid
159 if "disks" not in iobj:
160 raise Error("Instance '%s' doesn't have a disks entry?!" % instance)
161 disks = iobj["disks"]
162 for idx, dobj in enumerate(disks):
163 expected = "disk/%s" % idx
164 current = dobj.get("iv_name", "")
165 if current != expected:
166 logging.warning("Updating iv_name for instance %s/disk %s"
167 " from '%s' to '%s'",
168 instance, idx, current, expected)
169 dobj["iv_name"] = expected
172 def UpgradeRapiUsers():
173 if (os.path.isfile(options.RAPI_USERS_FILE_PRE24) and
174 not os.path.islink(options.RAPI_USERS_FILE_PRE24)):
175 if os.path.exists(options.RAPI_USERS_FILE):
176 raise Error("Found pre-2.4 RAPI users file at %s, but another file"
177 " already exists at %s" %
178 (options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE))
179 logging.info("Found pre-2.4 RAPI users file at %s, renaming to %s",
180 options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
181 if not options.dry_run:
182 utils.RenameFile(options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE,
183 mkdir=True, mkdir_mode=0750)
185 # Create a symlink for RAPI users file
186 if (not (os.path.islink(options.RAPI_USERS_FILE_PRE24) or
187 os.path.isfile(options.RAPI_USERS_FILE_PRE24)) and
188 os.path.isfile(options.RAPI_USERS_FILE)):
189 logging.info("Creating symlink from %s to %s",
190 options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
191 if not options.dry_run:
192 os.symlink(options.RAPI_USERS_FILE, options.RAPI_USERS_FILE_PRE24)
195 def UpgradeWatcher():
196 # Remove old watcher state file if it exists
197 if os.path.exists(options.WATCHER_STATEFILE):
198 logging.info("Removing watcher state file %s", options.WATCHER_STATEFILE)
199 if not options.dry_run:
200 utils.RemoveFile(options.WATCHER_STATEFILE)
203 def UpgradeFileStoragePaths(config_data):
204 # Write file storage paths
205 if not os.path.exists(options.FILE_STORAGE_PATHS_FILE):
206 cluster = config_data["cluster"]
207 file_storage_dir = cluster.get("file_storage_dir")
208 shared_file_storage_dir = cluster.get("shared_file_storage_dir")
211 logging.info("Ganeti 2.7 and later only allow whitelisted directories"
212 " for file storage; writing existing configuration values"
214 options.FILE_STORAGE_PATHS_FILE)
217 logging.info("File storage directory: %s", file_storage_dir)
218 if shared_file_storage_dir:
219 logging.info("Shared file storage directory: %s",
220 shared_file_storage_dir)
223 buf.write("# List automatically generated from configuration by\n")
224 buf.write("# cfgupgrade at %s\n" % time.asctime())
226 buf.write("%s\n" % file_storage_dir)
227 if shared_file_storage_dir:
228 buf.write("%s\n" % shared_file_storage_dir)
229 utils.WriteFile(file_name=options.FILE_STORAGE_PATHS_FILE,
232 dry_run=options.dry_run,
236 def UpgradeAll(config_data):
237 config_data["version"] = constants.BuildVersion(TARGET_MAJOR,
241 UpgradeFileStoragePaths(config_data)
242 UpgradeNetworks(config_data)
243 UpgradeCluster(config_data)
244 UpgradeGroups(config_data)
245 UpgradeInstances(config_data)
248 def DowngradeIPolicy(ipolicy):
249 # Downgrade IPolicy to 2.7 (stable)
250 minmax_keys = ["min", "max"]
251 specs_is_split = any((k in ipolicy) for k in minmax_keys)
252 if not specs_is_split:
253 if "minmax" in ipolicy:
254 minmax = ipolicy["minmax"]
255 del ipolicy["minmax"]
258 for key in minmax_keys:
259 spec = minmax.get(key, {})
263 def DowngradeGroups(config_data):
264 for group in config_data["nodegroups"].values():
265 ipolicy = group.get("ipolicy", None)
267 DowngradeIPolicy(ipolicy)
270 def DowngradeStorageTypes(cluster):
271 # Remove storage types to downgrade to 2.7
272 if "enabled_storage_types" in cluster:
273 logging.warning("Removing cluster storage types; value = %s",
274 utils.CommaJoin(cluster["enabled_storage_types"]))
275 del cluster["enabled_storage_types"]
278 def DowngradeCluster(config_data):
279 cluster = config_data.get("cluster", None)
281 raise Error("Cannot find cluster")
282 DowngradeStorageTypes(cluster)
283 ipolicy = cluster.get("ipolicy", None)
285 DowngradeIPolicy(ipolicy)
288 def DowngradeAll(config_data):
289 # Any code specific to a particular version should be labeled that way, so
290 # it can be removed when updating to the next version.
291 DowngradeCluster(config_data)
292 DowngradeGroups(config_data)
299 global options, args # pylint: disable=W0603
302 parser = optparse.OptionParser(usage="%prog [--debug|--verbose] [--force]")
303 parser.add_option("--dry-run", dest="dry_run",
305 help="Try to do the conversion, but don't write"
307 parser.add_option(cli.FORCE_OPT)
308 parser.add_option(cli.DEBUG_OPT)
309 parser.add_option(cli.VERBOSE_OPT)
310 parser.add_option("--ignore-hostname", dest="ignore_hostname",
311 action="store_true", default=False,
312 help="Don't abort if hostname doesn't match")
313 parser.add_option("--path", help="Convert configuration in this"
314 " directory instead of '%s'" % pathutils.DATA_DIR,
315 default=pathutils.DATA_DIR, dest="data_dir")
316 parser.add_option("--confdir",
317 help=("Use this directory instead of '%s'" %
319 default=pathutils.CONF_DIR, dest="conf_dir")
320 parser.add_option("--no-verify",
321 help="Do not verify configuration after upgrade",
322 action="store_true", dest="no_verify", default=False)
323 parser.add_option("--downgrade",
324 help="Downgrade to the previous stable version",
325 action="store_true", dest="downgrade", default=False)
326 (options, args) = parser.parse_args()
328 # We need to keep filenames locally because they might be renamed between
330 options.data_dir = os.path.abspath(options.data_dir)
331 options.CONFIG_DATA_PATH = options.data_dir + "/config.data"
332 options.SERVER_PEM_PATH = options.data_dir + "/server.pem"
333 options.KNOWN_HOSTS_PATH = options.data_dir + "/known_hosts"
334 options.RAPI_CERT_FILE = options.data_dir + "/rapi.pem"
335 options.SPICE_CERT_FILE = options.data_dir + "/spice.pem"
336 options.SPICE_CACERT_FILE = options.data_dir + "/spice-ca.pem"
337 options.RAPI_USERS_FILE = options.data_dir + "/rapi/users"
338 options.RAPI_USERS_FILE_PRE24 = options.data_dir + "/rapi_users"
339 options.CONFD_HMAC_KEY = options.data_dir + "/hmac.key"
340 options.CDS_FILE = options.data_dir + "/cluster-domain-secret"
341 options.SSCONF_MASTER_NODE = options.data_dir + "/ssconf_master_node"
342 options.WATCHER_STATEFILE = options.data_dir + "/watcher.data"
343 options.FILE_STORAGE_PATHS_FILE = options.conf_dir + "/file-storage-paths"
349 raise Error("No arguments expected")
350 if options.downgrade and not options.no_verify:
351 options.no_verify = True
354 if not (CheckHostname(options.SSCONF_MASTER_NODE) or options.ignore_hostname):
355 logging.error("Aborting due to hostname mismatch")
356 sys.exit(constants.EXIT_FAILURE)
358 if not options.force:
359 if options.downgrade:
360 usertext = ("The configuration is going to be DOWNGRADED to version %s.%s"
361 " Some configuration data might be removed if they don't fit"
362 " in the old format. Please make sure you have read the"
363 " upgrade notes (available in the UPGRADE file and included"
364 " in other documentation formats) to understand what they"
365 " are. Continue with *DOWNGRADING* the configuration?" %
366 (DOWNGRADE_MAJOR, DOWNGRADE_MINOR))
368 usertext = ("Please make sure you have read the upgrade notes for"
369 " Ganeti %s (available in the UPGRADE file and included"
370 " in other documentation formats). Continue with upgrading"
371 " configuration?" % constants.RELEASE_VERSION)
372 if not cli.AskUser(usertext):
373 sys.exit(constants.EXIT_FAILURE)
375 # Check whether it's a Ganeti configuration directory
376 if not (os.path.isfile(options.CONFIG_DATA_PATH) and
377 os.path.isfile(options.SERVER_PEM_PATH) and
378 os.path.isfile(options.KNOWN_HOSTS_PATH)):
379 raise Error(("%s does not seem to be a Ganeti configuration"
380 " directory") % options.data_dir)
382 if not os.path.isdir(options.conf_dir):
383 raise Error("Not a directory: %s" % options.conf_dir)
385 config_data = serializer.LoadJson(utils.ReadFile(options.CONFIG_DATA_PATH))
388 config_version = config_data["version"]
390 raise Error("Unable to determine configuration version")
392 (config_major, config_minor, config_revision) = \
393 constants.SplitVersion(config_version)
395 logging.info("Found configuration version %s (%d.%d.%d)",
396 config_version, config_major, config_minor, config_revision)
398 if "config_version" in config_data["cluster"]:
399 raise Error("Inconsistent configuration: found config_version in"
400 " configuration file")
402 # Downgrade to the previous stable version
403 if options.downgrade:
404 if config_major != TARGET_MAJOR or config_minor != TARGET_MINOR:
405 raise Error("Downgrade supported only from the latest version (%s.%s),"
406 " found %s (%s.%s.%s) instead" %
407 (TARGET_MAJOR, TARGET_MINOR, config_version, config_major,
408 config_minor, config_revision))
409 DowngradeAll(config_data)
411 # Upgrade from 2.{0..7} to 2.7
412 elif config_major == 2 and config_minor in range(0, 8):
413 if config_revision != 0:
414 logging.warning("Config revision is %s, not 0", config_revision)
415 UpgradeAll(config_data)
417 elif config_major == TARGET_MAJOR and config_minor == TARGET_MINOR:
418 logging.info("No changes necessary")
421 raise Error("Configuration version %d.%d.%d not supported by this tool" %
422 (config_major, config_minor, config_revision))
425 logging.info("Writing configuration file to %s", options.CONFIG_DATA_PATH)
426 utils.WriteFile(file_name=options.CONFIG_DATA_PATH,
427 data=serializer.DumpJson(config_data),
429 dry_run=options.dry_run,
432 if not options.dry_run:
433 bootstrap.GenerateClusterCrypto(
434 False, False, False, False, False,
435 nodecert_file=options.SERVER_PEM_PATH,
436 rapicert_file=options.RAPI_CERT_FILE,
437 spicecert_file=options.SPICE_CERT_FILE,
438 spicecacert_file=options.SPICE_CACERT_FILE,
439 hmackey_file=options.CONFD_HMAC_KEY,
440 cds_file=options.CDS_FILE)
443 logging.critical("Writing configuration failed. It is probably in an"
444 " inconsistent state and needs manual intervention.")
447 # test loading the config file
449 if not (options.dry_run or options.no_verify):
450 logging.info("Testing the new config file...")
451 cfg = config.ConfigWriter(cfg_file=options.CONFIG_DATA_PATH,
452 accept_foreign=options.ignore_hostname,
454 # if we reached this, it's all fine
455 vrfy = cfg.VerifyConfig()
457 logging.error("Errors after conversion:")
459 logging.error(" - %s", item)
462 logging.info("File loaded successfully after upgrading")
465 if options.downgrade:
466 action = "downgraded"
467 out_ver = "%s.%s" % (DOWNGRADE_MAJOR, DOWNGRADE_MINOR)
470 out_ver = constants.RELEASE_VERSION
472 cli.ToStderr("Configuration successfully %s to version %s.",
475 cli.ToStderr("Configuration %s to version %s, but there are errors."
476 "\nPlease review the file.", action, out_ver)
479 if __name__ == "__main__":