X-Git-Url: https://code.grnet.gr/git/ganeti-local/blobdiff_plain/1c2d87fc826772a5250d35b3094f13d6a3021acb..7617fd6b4e8400eb409594f7a339524fb4c7ca18:/tools/cfgupgrade?ds=sidebyside diff --git a/tools/cfgupgrade b/tools/cfgupgrade index 0c4f5d6..320f229 100755 --- a/tools/cfgupgrade +++ b/tools/cfgupgrade @@ -1,7 +1,7 @@ #!/usr/bin/python # -# Copyright (C) 2007, 2008 Google Inc. +# Copyright (C) 2007, 2008, 2009, 2010 Google Inc. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -21,9 +21,8 @@ """Tool to upgrade the configuration file. -This code handles only the types supported by simplejson. As an example, "set" -is a "list". Old Pickle based configurations files are converted to JSON during -the process. +This code handles only the types supported by simplejson. As an +example, 'set' is a 'list'. """ @@ -31,13 +30,16 @@ the process. import os import os.path import sys -import re import optparse -import tempfile -import simplejson +import logging +from ganeti import constants +from ganeti import serializer from ganeti import utils from ganeti import cli +from ganeti import bootstrap +from ganeti import config +from ganeti import netutils options = None @@ -49,169 +51,187 @@ class Error(Exception): pass -# {{{ Support for old Pickle files -class UpgradeDict(dict): - """Base class for internal config classes. +def SetupLogging(): + """Configures the logging module. """ - def __setstate__(self, state): - self.update(state) + formatter = logging.Formatter("%(asctime)s: %(message)s") + + stderr_handler = logging.StreamHandler() + stderr_handler.setFormatter(formatter) + if options.debug: + stderr_handler.setLevel(logging.NOTSET) + elif options.verbose: + stderr_handler.setLevel(logging.INFO) + else: + stderr_handler.setLevel(logging.WARNING) - def __getstate__(self): - return self.copy() + root_logger = logging.getLogger("") + root_logger.setLevel(logging.NOTSET) + root_logger.addHandler(stderr_handler) -def FindGlobal(module, name): - """Wraps Ganeti config classes to internal ones. +def CheckHostname(path): + """Ensures hostname matches ssconf value. - This function may only return types supported by simplejson. + @param path: Path to ssconf file """ - if module == "ganeti.objects": - return UpgradeDict - elif module == "__builtin__" and name == "set": - return list - - return getattr(sys.modules[module], name) - + ssconf_master_node = utils.ReadOneLineFile(path) + hostname = netutils.GetHostname().name -def ReadPickleFile(f): - """Reads an old Pickle configuration. + if ssconf_master_node == hostname: + return True - """ - import cPickle - - loader = cPickle.Unpickler(f) - loader.find_global = FindGlobal - return loader.load() + logging.warning("Warning: ssconf says master node is '%s', but this" + " machine's name is '%s'; this tool must be run on" + " the master node", ssconf_master_node, hostname) + return False -def IsPickleFile(f): - """Checks whether a file is using the Pickle format. +def main(): + """Main program. """ - magic = f.read(128) - try: - return not re.match('^\s*\{', magic) - finally: - f.seek(-len(magic), 1) -# }}} + global options, args # pylint: disable-msg=W0603 + # Option parsing + parser = optparse.OptionParser(usage="%prog [--debug|--verbose] [--force]") + parser.add_option('--dry-run', dest='dry_run', + action="store_true", + help="Try to do the conversion, but don't write" + " output file") + parser.add_option(cli.FORCE_OPT) + parser.add_option(cli.DEBUG_OPT) + parser.add_option(cli.VERBOSE_OPT) + parser.add_option("--ignore-hostname", dest="ignore_hostname", + action="store_true", default=False, + help="Don't abort if hostname doesn't match") + parser.add_option('--path', help="Convert configuration in this" + " directory instead of '%s'" % constants.DATA_DIR, + default=constants.DATA_DIR, dest="data_dir") + parser.add_option("--no-verify", + help="Do not verify configuration after upgrade", + action="store_true", dest="no_verify", default=False) + (options, args) = parser.parse_args() -def ReadJsonFile(f): - """Reads a JSON file. - - """ - return simplejson.load(f) + # We need to keep filenames locally because they might be renamed between + # versions. + options.data_dir = os.path.abspath(options.data_dir) + options.CONFIG_DATA_PATH = options.data_dir + "/config.data" + options.SERVER_PEM_PATH = options.data_dir + "/server.pem" + options.KNOWN_HOSTS_PATH = options.data_dir + "/known_hosts" + options.RAPI_CERT_FILE = options.data_dir + "/rapi.pem" + options.RAPI_USERS_FILE = options.data_dir + "/rapi/users" + options.RAPI_USERS_FILE_PRE24 = options.data_dir + "/rapi_users" + options.CONFD_HMAC_KEY = options.data_dir + "/hmac.key" + options.CDS_FILE = options.data_dir + "/cluster-domain-secret" + options.SSCONF_MASTER_NODE = options.data_dir + "/ssconf_master_node" + + SetupLogging() + # Option checking + if args: + raise Error("No arguments expected") -def ReadConfig(path): - """Reads configuration file. + # Check master name + if not (CheckHostname(options.SSCONF_MASTER_NODE) or options.ignore_hostname): + logging.error("Aborting due to hostname mismatch") + sys.exit(constants.EXIT_FAILURE) - """ - f = open(path, 'r') - try: - if IsPickleFile(f): - return ReadPickleFile(f) - else: - return ReadJsonFile(f) - finally: - f.close() + if not options.force: + usertext = ("Please make sure you have read the upgrade notes for" + " Ganeti %s (available in the UPGRADE file and included" + " in other documentation formats). Continue with upgrading" + " configuration?" % constants.RELEASE_VERSION) + if not cli.AskUser(usertext): + sys.exit(constants.EXIT_FAILURE) + # Check whether it's a Ganeti configuration directory + if not (os.path.isfile(options.CONFIG_DATA_PATH) and + os.path.isfile(options.SERVER_PEM_PATH) and + os.path.isfile(options.KNOWN_HOSTS_PATH)): + raise Error(("%s does not seem to be a Ganeti configuration" + " directory") % options.data_dir) -def WriteConfig(path, data): - """Writes the configuration file. + config_data = serializer.LoadJson(utils.ReadFile(options.CONFIG_DATA_PATH)) - """ - if not options.dry_run: - utils.CreateBackup(path) - - (fd, name) = tempfile.mkstemp(dir=os.path.dirname(path)) - f = os.fdopen(fd, 'w') try: - try: - simplejson.dump(data, f) - f.flush() - if options.dry_run: - os.unlink(name) - else: - os.rename(name, path) - except: - os.unlink(name) - raise - finally: - f.close() - - -def UpdateFromVersion2To3(cfg): - """Updates the configuration from version 2 to 3. + config_version = config_data["version"] + except KeyError: + raise Error("Unable to determine configuration version") - """ - if cfg['cluster']['config_version'] != 2: - return + (config_major, config_minor, config_revision) = \ + constants.SplitVersion(config_version) - # Add port pool - if 'tcpudp_port_pool' not in cfg['cluster']: - cfg['cluster']['tcpudp_port_pool'] = [] + logging.info("Found configuration version %s (%d.%d.%d)", + config_version, config_major, config_minor, config_revision) - # Add bridge settings - if 'default_bridge' not in cfg['cluster']: - cfg['cluster']['default_bridge'] = 'xen-br0' - for inst in cfg['instances'].values(): - for nic in inst['nics']: - if 'bridge' not in nic: - nic['bridge'] = None + if "config_version" in config_data["cluster"]: + raise Error("Inconsistent configuration: found config_version in" + " configuration file") - cfg['cluster']['config_version'] = 3 + # Upgrade from 2.0/2.1/2.2/2.3 to 2.4 + if config_major == 2 and config_minor in (0, 1, 2, 3): + if config_revision != 0: + logging.warning("Config revision is %s, not 0", config_revision) + config_data["version"] = constants.BuildVersion(2, 4, 0) -# Main program -if __name__ == "__main__": - program = os.path.basename(sys.argv[0]) + elif config_major == 2 and config_minor == 4: + logging.info("No changes necessary") - # Option parsing - parser = optparse.OptionParser(usage="%prog [options] ") - parser.add_option('--dry-run', dest='dry_run', - action="store_true", - help="Try to do the conversion, but don't write" - " output file") - parser.add_option(cli.FORCE_OPT) - parser.add_option('--verbose', dest='verbose', - action="store_true", - help="Verbose output") - (options, args) = parser.parse_args() - - # Option checking - if args: - cfg_file = args[0] else: - raise Error("Configuration file not specified") - - if not options.force: - usertext = ("%s MUST run on the master node. Is this the master" - " node?" % program) - if not cli.AskUser(usertext): - sys.exit(1) - - config = ReadConfig(cfg_file) - - if options.verbose: - import pprint - print "Before upgrade:" - pprint.pprint(config) - print + raise Error("Configuration version %d.%d.%d not supported by this tool" % + (config_major, config_minor, config_revision)) - UpdateFromVersion2To3(config) + if os.path.isfile(options.RAPI_USERS_FILE_PRE24): + logging.info("Found pre-2.4 RAPI users file at %s, renaming to %s", + options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE) + utils.RenameFile(options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE, + mkdir=True, mkdir_mode=0750) - if options.verbose: - print "After upgrade:" - pprint.pprint(config) - print + # Create a symlink for RAPI users file + if not os.path.islink(options.RAPI_USERS_FILE_PRE24): + logging.info("Creating symlink from %s to %s", + options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE) + os.symlink(options.RAPI_USERS_FILE, options.RAPI_USERS_FILE_PRE24) - WriteConfig(cfg_file, config) + try: + logging.info("Writing configuration file to %s", options.CONFIG_DATA_PATH) + utils.WriteFile(file_name=options.CONFIG_DATA_PATH, + data=serializer.DumpJson(config_data), + mode=0600, + dry_run=options.dry_run, + backup=True) + + if not options.dry_run: + bootstrap.GenerateClusterCrypto(False, False, False, False, + nodecert_file=options.SERVER_PEM_PATH, + rapicert_file=options.RAPI_CERT_FILE, + hmackey_file=options.CONFD_HMAC_KEY, + cds_file=options.CDS_FILE) + + except Exception: + logging.critical("Writing configuration failed. It is probably in an" + " inconsistent state and needs manual intervention.") + raise + + # test loading the config file + if not (options.dry_run or options.no_verify): + logging.info("Testing the new config file...") + cfg = config.ConfigWriter(cfg_file=options.CONFIG_DATA_PATH, + accept_foreign=options.ignore_hostname, + offline=True) + # if we reached this, it's all fine + vrfy = cfg.VerifyConfig() + if vrfy: + logging.error("Errors after conversion:") + for item in vrfy: + logging.error(" - %s", item) + del cfg + logging.info("File loaded successfully") - print "The configuration file has been updated successfully. Please run" - print " gnt-cluster copyfile %s" % cfg_file - print "now." -# vim: set foldmethod=marker : +if __name__ == "__main__": + main()