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
175 print("Deleting deprecated idx")
179 if "disks" not in iobj:
180 raise Error("Instance '%s' doesn't have a disks entry?!" % instance)
181 disks = iobj["disks"]
182 for idx, dobj in enumerate(disks):
183 expected = "disk/%s" % idx
184 current = dobj.get("iv_name", "")
185 if current != expected:
186 logging.warning("Updating iv_name for instance %s/disk %s"
187 " from '%s' to '%s'",
188 instance, idx, current, expected)
189 dobj["iv_name"] = expected
192 print("Deleting deprecated idx")
196 for attr in ("dev_idxs", "hotplug_info", "hotplugs", "pci_reservations"):
199 print("Deleting deprecated %s" % attr)
204 def UpgradeRapiUsers():
205 if (os.path.isfile(options.RAPI_USERS_FILE_PRE24) and
206 not os.path.islink(options.RAPI_USERS_FILE_PRE24)):
207 if os.path.exists(options.RAPI_USERS_FILE):
208 raise Error("Found pre-2.4 RAPI users file at %s, but another file"
209 " already exists at %s" %
210 (options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE))
211 logging.info("Found pre-2.4 RAPI users file at %s, renaming to %s",
212 options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
213 if not options.dry_run:
214 utils.RenameFile(options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE,
215 mkdir=True, mkdir_mode=0750)
217 # Create a symlink for RAPI users file
218 if (not (os.path.islink(options.RAPI_USERS_FILE_PRE24) or
219 os.path.isfile(options.RAPI_USERS_FILE_PRE24)) and
220 os.path.isfile(options.RAPI_USERS_FILE)):
221 logging.info("Creating symlink from %s to %s",
222 options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
223 if not options.dry_run:
224 os.symlink(options.RAPI_USERS_FILE, options.RAPI_USERS_FILE_PRE24)
227 def UpgradeWatcher():
228 # Remove old watcher state file if it exists
229 if os.path.exists(options.WATCHER_STATEFILE):
230 logging.info("Removing watcher state file %s", options.WATCHER_STATEFILE)
231 if not options.dry_run:
232 utils.RemoveFile(options.WATCHER_STATEFILE)
235 def UpgradeFileStoragePaths(config_data):
236 # Write file storage paths
237 if not os.path.exists(options.FILE_STORAGE_PATHS_FILE):
238 cluster = config_data["cluster"]
239 file_storage_dir = cluster.get("file_storage_dir")
240 shared_file_storage_dir = cluster.get("shared_file_storage_dir")
243 logging.info("Ganeti 2.7 and later only allow whitelisted directories"
244 " for file storage; writing existing configuration values"
246 options.FILE_STORAGE_PATHS_FILE)
249 logging.info("File storage directory: %s", file_storage_dir)
250 if shared_file_storage_dir:
251 logging.info("Shared file storage directory: %s",
252 shared_file_storage_dir)
255 buf.write("# List automatically generated from configuration by\n")
256 buf.write("# cfgupgrade at %s\n" % time.asctime())
258 buf.write("%s\n" % file_storage_dir)
259 if shared_file_storage_dir:
260 buf.write("%s\n" % shared_file_storage_dir)
261 utils.WriteFile(file_name=options.FILE_STORAGE_PATHS_FILE,
264 dry_run=options.dry_run,
268 def UpgradeAll(config_data):
269 config_data["version"] = constants.BuildVersion(TARGET_MAJOR,
273 UpgradeFileStoragePaths(config_data)
274 UpgradeNetworks(config_data)
275 UpgradeCluster(config_data)
276 UpgradeGroups(config_data)
277 UpgradeInstances(config_data)
280 def DowngradeIPolicy(ipolicy, owner):
281 # Downgrade IPolicy to 2.7 (stable)
282 minmax_keys = ["min", "max"]
283 specs_is_split = any((k in ipolicy) for k in minmax_keys)
284 if not specs_is_split:
285 if "minmax" in ipolicy:
286 if type(ipolicy["minmax"]) is not list:
287 raise Error("Invalid minmax type in %s ipolicy: %s" %
288 (owner, type(ipolicy["minmax"])))
289 if len(ipolicy["minmax"]) > 1:
290 logging.warning("Discarding some limit specs values from %s policy",
292 minmax = ipolicy["minmax"][0]
293 del ipolicy["minmax"]
296 for key in minmax_keys:
297 spec = minmax.get(key, {})
299 if "std" not in ipolicy:
303 def DowngradeGroups(config_data):
304 for group in config_data["nodegroups"].values():
305 ipolicy = group.get("ipolicy", None)
306 if ipolicy is not None:
307 DowngradeIPolicy(ipolicy, "group \"%s\"" % group.get("name"))
310 def DowngradeEnabledTemplates(cluster):
311 # Remove enabled disk templates to downgrade to 2.7
312 edt_key = "enabled_disk_templates"
313 if edt_key in cluster:
314 logging.warning("Removing cluster's enabled disk templates; value = %s",
315 utils.CommaJoin(cluster[edt_key]))
319 def DowngradeCluster(config_data):
320 cluster = config_data.get("cluster", None)
322 raise Error("Cannot find cluster")
323 DowngradeEnabledTemplates(cluster)
324 ipolicy = cluster.get("ipolicy", None)
326 DowngradeIPolicy(ipolicy, "cluster")
327 if "dsahostkeypub" in cluster:
328 del cluster["dsahostkeypub"]
331 def DowngradeDisk(disk):
334 if "children" in disk:
335 for child_disk in disk["children"]:
336 DowngradeDisk(child_disk)
339 def DowngradeInstances(config_data):
340 if "instances" not in config_data:
341 raise Error("Can't find the 'instances' key in the configuration!")
343 for _, iobj in config_data["instances"].items():
344 if "disks_active" in iobj:
345 del iobj["disks_active"]
347 # Remove the NICs UUIDs
348 for nic in iobj["nics"]:
352 # Downgrade the disks
353 for disk in iobj["disks"]:
357 def DowngradeAll(config_data):
358 # Any code specific to a particular version should be labeled that way, so
359 # it can be removed when updating to the next version.
360 config_data["version"] = constants.BuildVersion(DOWNGRADE_MAJOR,
362 DowngradeCluster(config_data)
363 DowngradeGroups(config_data)
364 DowngradeInstances(config_data)
371 global options, args # pylint: disable=W0603
374 parser = optparse.OptionParser(usage="%prog [--debug|--verbose] [--force]")
375 parser.add_option("--dry-run", dest="dry_run",
377 help="Try to do the conversion, but don't write"
379 parser.add_option(cli.FORCE_OPT)
380 parser.add_option(cli.DEBUG_OPT)
381 parser.add_option(cli.VERBOSE_OPT)
382 parser.add_option("--ignore-hostname", dest="ignore_hostname",
383 action="store_true", default=False,
384 help="Don't abort if hostname doesn't match")
385 parser.add_option("--path", help="Convert configuration in this"
386 " directory instead of '%s'" % pathutils.DATA_DIR,
387 default=pathutils.DATA_DIR, dest="data_dir")
388 parser.add_option("--confdir",
389 help=("Use this directory instead of '%s'" %
391 default=pathutils.CONF_DIR, dest="conf_dir")
392 parser.add_option("--no-verify",
393 help="Do not verify configuration after upgrade",
394 action="store_true", dest="no_verify", default=False)
395 parser.add_option("--downgrade",
396 help="Downgrade to the previous stable version",
397 action="store_true", dest="downgrade", default=False)
398 (options, args) = parser.parse_args()
400 # We need to keep filenames locally because they might be renamed between
402 options.data_dir = os.path.abspath(options.data_dir)
403 options.CONFIG_DATA_PATH = options.data_dir + "/config.data"
404 options.SERVER_PEM_PATH = options.data_dir + "/server.pem"
405 options.KNOWN_HOSTS_PATH = options.data_dir + "/known_hosts"
406 options.RAPI_CERT_FILE = options.data_dir + "/rapi.pem"
407 options.SPICE_CERT_FILE = options.data_dir + "/spice.pem"
408 options.SPICE_CACERT_FILE = options.data_dir + "/spice-ca.pem"
409 options.RAPI_USERS_FILE = options.data_dir + "/rapi/users"
410 options.RAPI_USERS_FILE_PRE24 = options.data_dir + "/rapi_users"
411 options.CONFD_HMAC_KEY = options.data_dir + "/hmac.key"
412 options.CDS_FILE = options.data_dir + "/cluster-domain-secret"
413 options.SSCONF_MASTER_NODE = options.data_dir + "/ssconf_master_node"
414 options.WATCHER_STATEFILE = options.data_dir + "/watcher.data"
415 options.FILE_STORAGE_PATHS_FILE = options.conf_dir + "/file-storage-paths"
421 raise Error("No arguments expected")
422 if options.downgrade and not options.no_verify:
423 options.no_verify = True
426 if not (CheckHostname(options.SSCONF_MASTER_NODE) or options.ignore_hostname):
427 logging.error("Aborting due to hostname mismatch")
428 sys.exit(constants.EXIT_FAILURE)
430 if not options.force:
431 if options.downgrade:
432 usertext = ("The configuration is going to be DOWNGRADED to version %s.%s"
433 " Some configuration data might be removed if they don't fit"
434 " in the old format. Please make sure you have read the"
435 " upgrade notes (available in the UPGRADE file and included"
436 " in other documentation formats) to understand what they"
437 " are. Continue with *DOWNGRADING* the configuration?" %
438 (DOWNGRADE_MAJOR, DOWNGRADE_MINOR))
440 usertext = ("Please make sure you have read the upgrade notes for"
441 " Ganeti %s (available in the UPGRADE file and included"
442 " in other documentation formats). Continue with upgrading"
443 " configuration?" % constants.RELEASE_VERSION)
444 if not cli.AskUser(usertext):
445 sys.exit(constants.EXIT_FAILURE)
447 # Check whether it's a Ganeti configuration directory
448 if not (os.path.isfile(options.CONFIG_DATA_PATH) and
449 os.path.isfile(options.SERVER_PEM_PATH) and
450 os.path.isfile(options.KNOWN_HOSTS_PATH)):
451 raise Error(("%s does not seem to be a Ganeti configuration"
452 " directory") % options.data_dir)
454 if not os.path.isdir(options.conf_dir):
455 raise Error("Not a directory: %s" % options.conf_dir)
457 config_data = serializer.LoadJson(utils.ReadFile(options.CONFIG_DATA_PATH))
460 config_version = config_data["version"]
462 raise Error("Unable to determine configuration version")
464 (config_major, config_minor, config_revision) = \
465 constants.SplitVersion(config_version)
467 logging.info("Found configuration version %s (%d.%d.%d)",
468 config_version, config_major, config_minor, config_revision)
470 if "config_version" in config_data["cluster"]:
471 raise Error("Inconsistent configuration: found config_version in"
472 " configuration file")
474 # Downgrade to the previous stable version
475 if options.downgrade:
476 if not ((config_major == TARGET_MAJOR and config_minor == TARGET_MINOR) or
477 (config_major == DOWNGRADE_MAJOR and
478 config_minor == DOWNGRADE_MINOR)):
479 raise Error("Downgrade supported only from the latest version (%s.%s),"
480 " found %s (%s.%s.%s) instead" %
481 (TARGET_MAJOR, TARGET_MINOR, config_version, config_major,
482 config_minor, config_revision))
483 DowngradeAll(config_data)
485 # Upgrade from 2.{0..7} to 2.8
486 elif config_major == 2 and config_minor in range(0, 9):
487 if config_revision != 0:
488 logging.warning("Config revision is %s, not 0", config_revision)
489 UpgradeAll(config_data)
491 elif config_major == TARGET_MAJOR and config_minor == TARGET_MINOR:
492 logging.info("No changes necessary")
495 raise Error("Configuration version %d.%d.%d not supported by this tool" %
496 (config_major, config_minor, config_revision))
499 logging.info("Writing configuration file to %s", options.CONFIG_DATA_PATH)
500 utils.WriteFile(file_name=options.CONFIG_DATA_PATH,
501 data=serializer.DumpJson(config_data),
503 dry_run=options.dry_run,
506 if not options.dry_run:
507 bootstrap.GenerateClusterCrypto(
508 False, False, False, False, False,
509 nodecert_file=options.SERVER_PEM_PATH,
510 rapicert_file=options.RAPI_CERT_FILE,
511 spicecert_file=options.SPICE_CERT_FILE,
512 spicecacert_file=options.SPICE_CACERT_FILE,
513 hmackey_file=options.CONFD_HMAC_KEY,
514 cds_file=options.CDS_FILE)
517 logging.critical("Writing configuration failed. It is probably in an"
518 " inconsistent state and needs manual intervention.")
521 # test loading the config file
523 if not (options.dry_run or options.no_verify):
524 logging.info("Testing the new config file...")
525 cfg = config.ConfigWriter(cfg_file=options.CONFIG_DATA_PATH,
526 accept_foreign=options.ignore_hostname,
528 # if we reached this, it's all fine
529 vrfy = cfg.VerifyConfig()
531 logging.error("Errors after conversion:")
533 logging.error(" - %s", item)
536 logging.info("File loaded successfully after upgrading")
539 if options.downgrade:
540 action = "downgraded"
541 out_ver = "%s.%s" % (DOWNGRADE_MAJOR, DOWNGRADE_MINOR)
544 out_ver = constants.RELEASE_VERSION
546 cli.ToStderr("Configuration successfully %s to version %s.",
549 cli.ToStderr("Configuration %s to version %s, but there are errors."
550 "\nPlease review the file.", action, out_ver)
553 if __name__ == "__main__":