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 UpgradeInstances(config_data):
159 network2uuid = dict((n["name"], n["uuid"])
160 for n in config_data["networks"].values())
161 if "instances" not in config_data:
162 raise Error("Can't find the 'instances' key in the configuration!")
164 for instance, iobj in config_data["instances"].items():
165 for nic in iobj["nics"]:
166 name = nic.get("network", None)
168 uuid = network2uuid.get(name, None)
170 print("NIC with network name %s found."
171 " Substituting with uuid %s." % (name, uuid))
172 nic["network"] = uuid
174 if "disks" not in iobj:
175 raise Error("Instance '%s' doesn't have a disks entry?!" % instance)
176 disks = iobj["disks"]
177 for idx, dobj in enumerate(disks):
178 expected = "disk/%s" % idx
179 current = dobj.get("iv_name", "")
180 if current != expected:
181 logging.warning("Updating iv_name for instance %s/disk %s"
182 " from '%s' to '%s'",
183 instance, idx, current, expected)
184 dobj["iv_name"] = expected
187 def UpgradeRapiUsers():
188 if (os.path.isfile(options.RAPI_USERS_FILE_PRE24) and
189 not os.path.islink(options.RAPI_USERS_FILE_PRE24)):
190 if os.path.exists(options.RAPI_USERS_FILE):
191 raise Error("Found pre-2.4 RAPI users file at %s, but another file"
192 " already exists at %s" %
193 (options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE))
194 logging.info("Found pre-2.4 RAPI users file at %s, renaming to %s",
195 options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
196 if not options.dry_run:
197 utils.RenameFile(options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE,
198 mkdir=True, mkdir_mode=0750)
200 # Create a symlink for RAPI users file
201 if (not (os.path.islink(options.RAPI_USERS_FILE_PRE24) or
202 os.path.isfile(options.RAPI_USERS_FILE_PRE24)) and
203 os.path.isfile(options.RAPI_USERS_FILE)):
204 logging.info("Creating symlink from %s to %s",
205 options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
206 if not options.dry_run:
207 os.symlink(options.RAPI_USERS_FILE, options.RAPI_USERS_FILE_PRE24)
210 def UpgradeWatcher():
211 # Remove old watcher state file if it exists
212 if os.path.exists(options.WATCHER_STATEFILE):
213 logging.info("Removing watcher state file %s", options.WATCHER_STATEFILE)
214 if not options.dry_run:
215 utils.RemoveFile(options.WATCHER_STATEFILE)
218 def UpgradeFileStoragePaths(config_data):
219 # Write file storage paths
220 if not os.path.exists(options.FILE_STORAGE_PATHS_FILE):
221 cluster = config_data["cluster"]
222 file_storage_dir = cluster.get("file_storage_dir")
223 shared_file_storage_dir = cluster.get("shared_file_storage_dir")
226 logging.info("Ganeti 2.7 and later only allow whitelisted directories"
227 " for file storage; writing existing configuration values"
229 options.FILE_STORAGE_PATHS_FILE)
232 logging.info("File storage directory: %s", file_storage_dir)
233 if shared_file_storage_dir:
234 logging.info("Shared file storage directory: %s",
235 shared_file_storage_dir)
238 buf.write("# List automatically generated from configuration by\n")
239 buf.write("# cfgupgrade at %s\n" % time.asctime())
241 buf.write("%s\n" % file_storage_dir)
242 if shared_file_storage_dir:
243 buf.write("%s\n" % shared_file_storage_dir)
244 utils.WriteFile(file_name=options.FILE_STORAGE_PATHS_FILE,
247 dry_run=options.dry_run,
251 def UpgradeAll(config_data):
252 config_data["version"] = constants.BuildVersion(TARGET_MAJOR,
256 UpgradeFileStoragePaths(config_data)
257 UpgradeNetworks(config_data)
258 UpgradeCluster(config_data)
259 UpgradeGroups(config_data)
260 UpgradeInstances(config_data)
263 def DowngradeIPolicy(ipolicy, owner):
264 # Downgrade IPolicy to 2.7 (stable)
265 minmax_keys = ["min", "max"]
266 specs_is_split = any((k in ipolicy) for k in minmax_keys)
267 if not specs_is_split:
268 if "minmax" in ipolicy:
269 if type(ipolicy["minmax"]) is not list:
270 raise Error("Invalid minmax type in %s ipolicy: %s" %
271 (owner, type(ipolicy["minmax"])))
272 if len(ipolicy["minmax"]) > 1:
273 logging.warning("Discarding some limit specs values from %s policy",
275 minmax = ipolicy["minmax"][0]
276 del ipolicy["minmax"]
279 for key in minmax_keys:
280 spec = minmax.get(key, {})
282 if "std" not in ipolicy:
286 def DowngradeGroups(config_data):
287 for group in config_data["nodegroups"].values():
288 ipolicy = group.get("ipolicy", None)
289 if ipolicy is not None:
290 DowngradeIPolicy(ipolicy, "group \"%s\"" % group.get("name"))
293 def DowngradeEnabledTemplates(cluster):
294 # Remove enabled disk templates to downgrade to 2.7
295 edt_key = "enabled_disk_templates"
296 if edt_key in cluster:
297 logging.warning("Removing cluster's enabled disk templates; value = %s",
298 utils.CommaJoin(cluster[edt_key]))
302 def DowngradeCluster(config_data):
303 cluster = config_data.get("cluster", None)
305 raise Error("Cannot find cluster")
306 DowngradeEnabledTemplates(cluster)
307 ipolicy = cluster.get("ipolicy", None)
309 DowngradeIPolicy(ipolicy, "cluster")
310 if "dsahostkeypub" in cluster:
311 del cluster["dsahostkeypub"]
314 def DowngradeDisk(disk):
317 if "children" in disk:
318 for child_disk in disk["children"]:
319 DowngradeDisk(child_disk)
322 def DowngradeInstances(config_data):
323 if "instances" not in config_data:
324 raise Error("Can't find the 'instances' key in the configuration!")
326 for _, iobj in config_data["instances"].items():
327 if "disks_active" in iobj:
328 del iobj["disks_active"]
330 # Remove the NICs UUIDs
331 for nic in iobj["nics"]:
335 # Downgrade the disks
336 for disk in iobj["disks"]:
340 def DowngradeAll(config_data):
341 # Any code specific to a particular version should be labeled that way, so
342 # it can be removed when updating to the next version.
343 config_data["version"] = constants.BuildVersion(DOWNGRADE_MAJOR,
345 DowngradeCluster(config_data)
346 DowngradeGroups(config_data)
347 DowngradeInstances(config_data)
354 global options, args # pylint: disable=W0603
357 parser = optparse.OptionParser(usage="%prog [--debug|--verbose] [--force]")
358 parser.add_option("--dry-run", dest="dry_run",
360 help="Try to do the conversion, but don't write"
362 parser.add_option(cli.FORCE_OPT)
363 parser.add_option(cli.DEBUG_OPT)
364 parser.add_option(cli.VERBOSE_OPT)
365 parser.add_option("--ignore-hostname", dest="ignore_hostname",
366 action="store_true", default=False,
367 help="Don't abort if hostname doesn't match")
368 parser.add_option("--path", help="Convert configuration in this"
369 " directory instead of '%s'" % pathutils.DATA_DIR,
370 default=pathutils.DATA_DIR, dest="data_dir")
371 parser.add_option("--confdir",
372 help=("Use this directory instead of '%s'" %
374 default=pathutils.CONF_DIR, dest="conf_dir")
375 parser.add_option("--no-verify",
376 help="Do not verify configuration after upgrade",
377 action="store_true", dest="no_verify", default=False)
378 parser.add_option("--downgrade",
379 help="Downgrade to the previous stable version",
380 action="store_true", dest="downgrade", default=False)
381 (options, args) = parser.parse_args()
383 # We need to keep filenames locally because they might be renamed between
385 options.data_dir = os.path.abspath(options.data_dir)
386 options.CONFIG_DATA_PATH = options.data_dir + "/config.data"
387 options.SERVER_PEM_PATH = options.data_dir + "/server.pem"
388 options.KNOWN_HOSTS_PATH = options.data_dir + "/known_hosts"
389 options.RAPI_CERT_FILE = options.data_dir + "/rapi.pem"
390 options.SPICE_CERT_FILE = options.data_dir + "/spice.pem"
391 options.SPICE_CACERT_FILE = options.data_dir + "/spice-ca.pem"
392 options.RAPI_USERS_FILE = options.data_dir + "/rapi/users"
393 options.RAPI_USERS_FILE_PRE24 = options.data_dir + "/rapi_users"
394 options.CONFD_HMAC_KEY = options.data_dir + "/hmac.key"
395 options.CDS_FILE = options.data_dir + "/cluster-domain-secret"
396 options.SSCONF_MASTER_NODE = options.data_dir + "/ssconf_master_node"
397 options.WATCHER_STATEFILE = options.data_dir + "/watcher.data"
398 options.FILE_STORAGE_PATHS_FILE = options.conf_dir + "/file-storage-paths"
404 raise Error("No arguments expected")
405 if options.downgrade and not options.no_verify:
406 options.no_verify = True
409 if not (CheckHostname(options.SSCONF_MASTER_NODE) or options.ignore_hostname):
410 logging.error("Aborting due to hostname mismatch")
411 sys.exit(constants.EXIT_FAILURE)
413 if not options.force:
414 if options.downgrade:
415 usertext = ("The configuration is going to be DOWNGRADED to version %s.%s"
416 " Some configuration data might be removed if they don't fit"
417 " in the old format. Please make sure you have read the"
418 " upgrade notes (available in the UPGRADE file and included"
419 " in other documentation formats) to understand what they"
420 " are. Continue with *DOWNGRADING* the configuration?" %
421 (DOWNGRADE_MAJOR, DOWNGRADE_MINOR))
423 usertext = ("Please make sure you have read the upgrade notes for"
424 " Ganeti %s (available in the UPGRADE file and included"
425 " in other documentation formats). Continue with upgrading"
426 " configuration?" % constants.RELEASE_VERSION)
427 if not cli.AskUser(usertext):
428 sys.exit(constants.EXIT_FAILURE)
430 # Check whether it's a Ganeti configuration directory
431 if not (os.path.isfile(options.CONFIG_DATA_PATH) and
432 os.path.isfile(options.SERVER_PEM_PATH) and
433 os.path.isfile(options.KNOWN_HOSTS_PATH)):
434 raise Error(("%s does not seem to be a Ganeti configuration"
435 " directory") % options.data_dir)
437 if not os.path.isdir(options.conf_dir):
438 raise Error("Not a directory: %s" % options.conf_dir)
440 config_data = serializer.LoadJson(utils.ReadFile(options.CONFIG_DATA_PATH))
443 config_version = config_data["version"]
445 raise Error("Unable to determine configuration version")
447 (config_major, config_minor, config_revision) = \
448 constants.SplitVersion(config_version)
450 logging.info("Found configuration version %s (%d.%d.%d)",
451 config_version, config_major, config_minor, config_revision)
453 if "config_version" in config_data["cluster"]:
454 raise Error("Inconsistent configuration: found config_version in"
455 " configuration file")
457 # Downgrade to the previous stable version
458 if options.downgrade:
459 if not ((config_major == TARGET_MAJOR and config_minor == TARGET_MINOR) or
460 (config_major == DOWNGRADE_MAJOR and
461 config_minor == DOWNGRADE_MINOR)):
462 raise Error("Downgrade supported only from the latest version (%s.%s),"
463 " found %s (%s.%s.%s) instead" %
464 (TARGET_MAJOR, TARGET_MINOR, config_version, config_major,
465 config_minor, config_revision))
466 DowngradeAll(config_data)
468 # Upgrade from 2.{0..7} to 2.8
469 elif config_major == 2 and config_minor in range(0, 9):
470 if config_revision != 0:
471 logging.warning("Config revision is %s, not 0", config_revision)
472 UpgradeAll(config_data)
474 elif config_major == TARGET_MAJOR and config_minor == TARGET_MINOR:
475 logging.info("No changes necessary")
478 raise Error("Configuration version %d.%d.%d not supported by this tool" %
479 (config_major, config_minor, config_revision))
482 logging.info("Writing configuration file to %s", options.CONFIG_DATA_PATH)
483 utils.WriteFile(file_name=options.CONFIG_DATA_PATH,
484 data=serializer.DumpJson(config_data),
486 dry_run=options.dry_run,
489 if not options.dry_run:
490 bootstrap.GenerateClusterCrypto(
491 False, False, False, False, False,
492 nodecert_file=options.SERVER_PEM_PATH,
493 rapicert_file=options.RAPI_CERT_FILE,
494 spicecert_file=options.SPICE_CERT_FILE,
495 spicecacert_file=options.SPICE_CACERT_FILE,
496 hmackey_file=options.CONFD_HMAC_KEY,
497 cds_file=options.CDS_FILE)
500 logging.critical("Writing configuration failed. It is probably in an"
501 " inconsistent state and needs manual intervention.")
504 # test loading the config file
506 if not (options.dry_run or options.no_verify):
507 logging.info("Testing the new config file...")
508 cfg = config.ConfigWriter(cfg_file=options.CONFIG_DATA_PATH,
509 accept_foreign=options.ignore_hostname,
511 # if we reached this, it's all fine
512 vrfy = cfg.VerifyConfig()
514 logging.error("Errors after conversion:")
516 logging.error(" - %s", item)
519 logging.info("File loaded successfully after upgrading")
522 if options.downgrade:
523 action = "downgraded"
524 out_ver = "%s.%s" % (DOWNGRADE_MAJOR, DOWNGRADE_MINOR)
527 out_ver = constants.RELEASE_VERSION
529 cli.ToStderr("Configuration successfully %s to version %s.",
532 cli.ToStderr("Configuration %s to version %s, but there are errors."
533 "\nPlease review the file.", action, out_ver)
536 if __name__ == "__main__":