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 RemovePhysicalId(disk):
177 if "children" in disk:
178 for d in disk["children"]:
180 if "physical_id" in disk:
181 del disk["physical_id"]
184 def UpgradeInstances(config_data):
185 network2uuid = dict((n["name"], n["uuid"])
186 for n in config_data["networks"].values())
187 if "instances" not in config_data:
188 raise Error("Can't find the 'instances' key in the configuration!")
190 missing_spindles = False
191 for instance, iobj in config_data["instances"].items():
192 for nic in iobj["nics"]:
193 name = nic.get("network", None)
195 uuid = network2uuid.get(name, None)
197 print("NIC with network name %s found."
198 " Substituting with uuid %s." % (name, uuid))
199 nic["network"] = uuid
201 if "disks" not in iobj:
202 raise Error("Instance '%s' doesn't have a disks entry?!" % instance)
203 disks = iobj["disks"]
204 for idx, dobj in enumerate(disks):
205 RemovePhysicalId(dobj)
207 expected = "disk/%s" % idx
208 current = dobj.get("iv_name", "")
209 if current != expected:
210 logging.warning("Updating iv_name for instance %s/disk %s"
211 " from '%s' to '%s'",
212 instance, idx, current, expected)
213 dobj["iv_name"] = expected
214 if not "spindles" in dobj:
215 missing_spindles = True
217 if GetExclusiveStorageValue(config_data) and missing_spindles:
218 # We cannot be sure that the instances that are missing spindles have
219 # exclusive storage enabled (the check would be more complicated), so we
220 # give a noncommittal message
221 logging.warning("Some instance disks could be needing to update the"
222 " spindles parameter; you can check by running"
223 " 'gnt-cluster verify', and fix any problem with"
224 " 'gnt-cluster repair-disk-sizes'")
227 def UpgradeRapiUsers():
228 if (os.path.isfile(options.RAPI_USERS_FILE_PRE24) and
229 not os.path.islink(options.RAPI_USERS_FILE_PRE24)):
230 if os.path.exists(options.RAPI_USERS_FILE):
231 raise Error("Found pre-2.4 RAPI users file at %s, but another file"
232 " already exists at %s" %
233 (options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE))
234 logging.info("Found pre-2.4 RAPI users file at %s, renaming to %s",
235 options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
236 if not options.dry_run:
237 utils.RenameFile(options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE,
238 mkdir=True, mkdir_mode=0750)
240 # Create a symlink for RAPI users file
241 if (not (os.path.islink(options.RAPI_USERS_FILE_PRE24) or
242 os.path.isfile(options.RAPI_USERS_FILE_PRE24)) and
243 os.path.isfile(options.RAPI_USERS_FILE)):
244 logging.info("Creating symlink from %s to %s",
245 options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
246 if not options.dry_run:
247 os.symlink(options.RAPI_USERS_FILE, options.RAPI_USERS_FILE_PRE24)
250 def UpgradeWatcher():
251 # Remove old watcher state file if it exists
252 if os.path.exists(options.WATCHER_STATEFILE):
253 logging.info("Removing watcher state file %s", options.WATCHER_STATEFILE)
254 if not options.dry_run:
255 utils.RemoveFile(options.WATCHER_STATEFILE)
258 def UpgradeFileStoragePaths(config_data):
259 # Write file storage paths
260 if not os.path.exists(options.FILE_STORAGE_PATHS_FILE):
261 cluster = config_data["cluster"]
262 file_storage_dir = cluster.get("file_storage_dir")
263 shared_file_storage_dir = cluster.get("shared_file_storage_dir")
266 logging.info("Ganeti 2.7 and later only allow whitelisted directories"
267 " for file storage; writing existing configuration values"
269 options.FILE_STORAGE_PATHS_FILE)
272 logging.info("File storage directory: %s", file_storage_dir)
273 if shared_file_storage_dir:
274 logging.info("Shared file storage directory: %s",
275 shared_file_storage_dir)
278 buf.write("# List automatically generated from configuration by\n")
279 buf.write("# cfgupgrade at %s\n" % time.asctime())
281 buf.write("%s\n" % file_storage_dir)
282 if shared_file_storage_dir:
283 buf.write("%s\n" % shared_file_storage_dir)
284 utils.WriteFile(file_name=options.FILE_STORAGE_PATHS_FILE,
287 dry_run=options.dry_run,
291 def GetNewNodeIndex(nodes_by_old_key, old_key, new_key_field):
292 if old_key not in nodes_by_old_key:
293 logging.warning("Can't find node '%s' in configuration, assuming that it's"
294 " already up-to-date", old_key)
296 return nodes_by_old_key[old_key][new_key_field]
299 def ChangeNodeIndices(config_data, old_key_field, new_key_field):
300 def ChangeDiskNodeIndices(disk):
301 if disk["dev_type"] in constants.LDS_DRBD:
302 for i in range(0, 2):
303 disk["logical_id"][i] = GetNewNodeIndex(nodes_by_old_key,
304 disk["logical_id"][i],
306 if "children" in disk:
307 for child in disk["children"]:
308 ChangeDiskNodeIndices(child)
310 nodes_by_old_key = {}
311 nodes_by_new_key = {}
312 for (_, node) in config_data["nodes"].items():
313 nodes_by_old_key[node[old_key_field]] = node
314 nodes_by_new_key[node[new_key_field]] = node
316 config_data["nodes"] = nodes_by_new_key
318 cluster = config_data["cluster"]
319 cluster["master_node"] = GetNewNodeIndex(nodes_by_old_key,
320 cluster["master_node"],
323 for inst in config_data["instances"].values():
324 inst["primary_node"] = GetNewNodeIndex(nodes_by_old_key,
325 inst["primary_node"],
327 for disk in inst["disks"]:
328 ChangeDiskNodeIndices(disk)
331 def ChangeInstanceIndices(config_data, old_key_field, new_key_field):
332 insts_by_old_key = {}
333 insts_by_new_key = {}
334 for (_, inst) in config_data["instances"].items():
335 insts_by_old_key[inst[old_key_field]] = inst
336 insts_by_new_key[inst[new_key_field]] = inst
338 config_data["instances"] = insts_by_new_key
341 def UpgradeNodeIndices(config_data):
342 ChangeNodeIndices(config_data, "name", "uuid")
345 def UpgradeInstanceIndices(config_data):
346 ChangeInstanceIndices(config_data, "name", "uuid")
349 def UpgradeAll(config_data):
350 config_data["version"] = constants.BuildVersion(TARGET_MAJOR,
354 UpgradeFileStoragePaths(config_data)
355 UpgradeNetworks(config_data)
356 UpgradeCluster(config_data)
357 UpgradeGroups(config_data)
358 UpgradeInstances(config_data)
359 UpgradeNodeIndices(config_data)
360 UpgradeInstanceIndices(config_data)
363 def DowngradeInstances(config_data):
364 if "instances" not in config_data:
365 raise Error("Cannot find the 'instances' key in the configuration!")
366 for (iname, iobj) in config_data["instances"].items():
367 DowngradeNicParamsVLAN(iobj["nics"], iname)
370 def DowngradeNicParamsVLAN(nics, owner):
372 vlan = nic["nicparams"].get("vlan", None)
374 logging.warning("Instance with name %s found. Removing VLAN information"
376 del nic["nicparams"]["vlan"]
379 def DowngradeAll(config_data):
380 # Any code specific to a particular version should be labeled that way, so
381 # it can be removed when updating to the next version.
382 config_data["version"] = constants.BuildVersion(DOWNGRADE_MAJOR,
384 DowngradeInstances(config_data)
391 global options, args # pylint: disable=W0603
394 parser = optparse.OptionParser(usage="%prog [--debug|--verbose] [--force]")
395 parser.add_option("--dry-run", dest="dry_run",
397 help="Try to do the conversion, but don't write"
399 parser.add_option(cli.FORCE_OPT)
400 parser.add_option(cli.DEBUG_OPT)
401 parser.add_option(cli.VERBOSE_OPT)
402 parser.add_option("--ignore-hostname", dest="ignore_hostname",
403 action="store_true", default=False,
404 help="Don't abort if hostname doesn't match")
405 parser.add_option("--path", help="Convert configuration in this"
406 " directory instead of '%s'" % pathutils.DATA_DIR,
407 default=pathutils.DATA_DIR, dest="data_dir")
408 parser.add_option("--confdir",
409 help=("Use this directory instead of '%s'" %
411 default=pathutils.CONF_DIR, dest="conf_dir")
412 parser.add_option("--no-verify",
413 help="Do not verify configuration after upgrade",
414 action="store_true", dest="no_verify", default=False)
415 parser.add_option("--downgrade",
416 help="Downgrade to the previous stable version",
417 action="store_true", dest="downgrade", default=False)
418 (options, args) = parser.parse_args()
420 # We need to keep filenames locally because they might be renamed between
422 options.data_dir = os.path.abspath(options.data_dir)
423 options.CONFIG_DATA_PATH = options.data_dir + "/config.data"
424 options.SERVER_PEM_PATH = options.data_dir + "/server.pem"
425 options.KNOWN_HOSTS_PATH = options.data_dir + "/known_hosts"
426 options.RAPI_CERT_FILE = options.data_dir + "/rapi.pem"
427 options.SPICE_CERT_FILE = options.data_dir + "/spice.pem"
428 options.SPICE_CACERT_FILE = options.data_dir + "/spice-ca.pem"
429 options.RAPI_USERS_FILE = options.data_dir + "/rapi/users"
430 options.RAPI_USERS_FILE_PRE24 = options.data_dir + "/rapi_users"
431 options.CONFD_HMAC_KEY = options.data_dir + "/hmac.key"
432 options.CDS_FILE = options.data_dir + "/cluster-domain-secret"
433 options.SSCONF_MASTER_NODE = options.data_dir + "/ssconf_master_node"
434 options.WATCHER_STATEFILE = options.data_dir + "/watcher.data"
435 options.FILE_STORAGE_PATHS_FILE = options.conf_dir + "/file-storage-paths"
441 raise Error("No arguments expected")
442 if options.downgrade and not options.no_verify:
443 options.no_verify = True
446 if not (CheckHostname(options.SSCONF_MASTER_NODE) or options.ignore_hostname):
447 logging.error("Aborting due to hostname mismatch")
448 sys.exit(constants.EXIT_FAILURE)
450 if not options.force:
451 if options.downgrade:
452 usertext = ("The configuration is going to be DOWNGRADED to version %s.%s"
453 " Some configuration data might be removed if they don't fit"
454 " in the old format. Please make sure you have read the"
455 " upgrade notes (available in the UPGRADE file and included"
456 " in other documentation formats) to understand what they"
457 " are. Continue with *DOWNGRADING* the configuration?" %
458 (DOWNGRADE_MAJOR, DOWNGRADE_MINOR))
460 usertext = ("Please make sure you have read the upgrade notes for"
461 " Ganeti %s (available in the UPGRADE file and included"
462 " in other documentation formats). Continue with upgrading"
463 " configuration?" % constants.RELEASE_VERSION)
464 if not cli.AskUser(usertext):
465 sys.exit(constants.EXIT_FAILURE)
467 # Check whether it's a Ganeti configuration directory
468 if not (os.path.isfile(options.CONFIG_DATA_PATH) and
469 os.path.isfile(options.SERVER_PEM_PATH) and
470 os.path.isfile(options.KNOWN_HOSTS_PATH)):
471 raise Error(("%s does not seem to be a Ganeti configuration"
472 " directory") % options.data_dir)
474 if not os.path.isdir(options.conf_dir):
475 raise Error("Not a directory: %s" % options.conf_dir)
477 config_data = serializer.LoadJson(utils.ReadFile(options.CONFIG_DATA_PATH))
480 config_version = config_data["version"]
482 raise Error("Unable to determine configuration version")
484 (config_major, config_minor, config_revision) = \
485 constants.SplitVersion(config_version)
487 logging.info("Found configuration version %s (%d.%d.%d)",
488 config_version, config_major, config_minor, config_revision)
490 if "config_version" in config_data["cluster"]:
491 raise Error("Inconsistent configuration: found config_version in"
492 " configuration file")
494 # Downgrade to the previous stable version
495 if options.downgrade:
496 if not ((config_major == TARGET_MAJOR and config_minor == TARGET_MINOR) or
497 (config_major == DOWNGRADE_MAJOR and
498 config_minor == DOWNGRADE_MINOR)):
499 raise Error("Downgrade supported only from the latest version (%s.%s),"
500 " found %s (%s.%s.%s) instead" %
501 (TARGET_MAJOR, TARGET_MINOR, config_version, config_major,
502 config_minor, config_revision))
503 DowngradeAll(config_data)
505 # Upgrade from 2.{0..7} to 2.9
506 elif config_major == 2 and config_minor in range(0, 10):
507 if config_revision != 0:
508 logging.warning("Config revision is %s, not 0", config_revision)
509 UpgradeAll(config_data)
511 elif config_major == TARGET_MAJOR and config_minor == TARGET_MINOR:
512 logging.info("No changes necessary")
515 raise Error("Configuration version %d.%d.%d not supported by this tool" %
516 (config_major, config_minor, config_revision))
519 logging.info("Writing configuration file to %s", options.CONFIG_DATA_PATH)
520 utils.WriteFile(file_name=options.CONFIG_DATA_PATH,
521 data=serializer.DumpJson(config_data),
523 dry_run=options.dry_run,
526 if not options.dry_run:
527 bootstrap.GenerateClusterCrypto(
528 False, False, False, False, False,
529 nodecert_file=options.SERVER_PEM_PATH,
530 rapicert_file=options.RAPI_CERT_FILE,
531 spicecert_file=options.SPICE_CERT_FILE,
532 spicecacert_file=options.SPICE_CACERT_FILE,
533 hmackey_file=options.CONFD_HMAC_KEY,
534 cds_file=options.CDS_FILE)
537 logging.critical("Writing configuration failed. It is probably in an"
538 " inconsistent state and needs manual intervention.")
541 # test loading the config file
543 if not (options.dry_run or options.no_verify):
544 logging.info("Testing the new config file...")
545 cfg = config.ConfigWriter(cfg_file=options.CONFIG_DATA_PATH,
546 accept_foreign=options.ignore_hostname,
548 # if we reached this, it's all fine
549 vrfy = cfg.VerifyConfig()
551 logging.error("Errors after conversion:")
553 logging.error(" - %s", item)
556 logging.info("File loaded successfully after upgrading")
559 if options.downgrade:
560 action = "downgraded"
561 out_ver = "%s.%s" % (DOWNGRADE_MAJOR, DOWNGRADE_MINOR)
564 out_ver = constants.RELEASE_VERSION
566 cli.ToStderr("Configuration successfully %s to version %s.",
569 cli.ToStderr("Configuration %s to version %s, but there are errors."
570 "\nPlease review the file.", action, out_ver)
573 if __name__ == "__main__":