4 # Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012 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
58 class Error(Exception):
59 """Generic exception"""
64 """Configures the logging module.
67 formatter = logging.Formatter("%(asctime)s: %(message)s")
69 stderr_handler = logging.StreamHandler()
70 stderr_handler.setFormatter(formatter)
72 stderr_handler.setLevel(logging.NOTSET)
74 stderr_handler.setLevel(logging.INFO)
76 stderr_handler.setLevel(logging.WARNING)
78 root_logger = logging.getLogger("")
79 root_logger.setLevel(logging.NOTSET)
80 root_logger.addHandler(stderr_handler)
83 def CheckHostname(path):
84 """Ensures hostname matches ssconf value.
86 @param path: Path to ssconf file
89 ssconf_master_node = utils.ReadOneLineFile(path)
90 hostname = netutils.GetHostname().name
92 if ssconf_master_node == hostname:
95 logging.warning("Warning: ssconf says master node is '%s', but this"
96 " machine's name is '%s'; this tool must be run on"
97 " the master node", ssconf_master_node, hostname)
101 def UpgradeNetworks(config_data):
102 networks = config_data.get("networks", None)
104 config_data["networks"] = {}
107 def UpgradeGroups(config_data):
108 for group in config_data["nodegroups"].values():
109 networks = group.get("networks", None)
111 group["networks"] = {}
118 global options, args # pylint: disable=W0603
121 parser = optparse.OptionParser(usage="%prog [--debug|--verbose] [--force]")
122 parser.add_option("--dry-run", dest="dry_run",
124 help="Try to do the conversion, but don't write"
126 parser.add_option(cli.FORCE_OPT)
127 parser.add_option(cli.DEBUG_OPT)
128 parser.add_option(cli.VERBOSE_OPT)
129 parser.add_option("--ignore-hostname", dest="ignore_hostname",
130 action="store_true", default=False,
131 help="Don't abort if hostname doesn't match")
132 parser.add_option("--path", help="Convert configuration in this"
133 " directory instead of '%s'" % pathutils.DATA_DIR,
134 default=pathutils.DATA_DIR, dest="data_dir")
135 parser.add_option("--confdir",
136 help=("Use this directory instead of '%s'" %
138 default=pathutils.CONF_DIR, dest="conf_dir")
139 parser.add_option("--no-verify",
140 help="Do not verify configuration after upgrade",
141 action="store_true", dest="no_verify", default=False)
142 (options, args) = parser.parse_args()
144 # We need to keep filenames locally because they might be renamed between
146 options.data_dir = os.path.abspath(options.data_dir)
147 options.CONFIG_DATA_PATH = options.data_dir + "/config.data"
148 options.SERVER_PEM_PATH = options.data_dir + "/server.pem"
149 options.KNOWN_HOSTS_PATH = options.data_dir + "/known_hosts"
150 options.RAPI_CERT_FILE = options.data_dir + "/rapi.pem"
151 options.SPICE_CERT_FILE = options.data_dir + "/spice.pem"
152 options.SPICE_CACERT_FILE = options.data_dir + "/spice-ca.pem"
153 options.RAPI_USERS_FILE = options.data_dir + "/rapi/users"
154 options.RAPI_USERS_FILE_PRE24 = options.data_dir + "/rapi_users"
155 options.CONFD_HMAC_KEY = options.data_dir + "/hmac.key"
156 options.CDS_FILE = options.data_dir + "/cluster-domain-secret"
157 options.SSCONF_MASTER_NODE = options.data_dir + "/ssconf_master_node"
158 options.WATCHER_STATEFILE = options.data_dir + "/watcher.data"
159 options.FILE_STORAGE_PATHS_FILE = options.conf_dir + "/file-storage-paths"
165 raise Error("No arguments expected")
168 if not (CheckHostname(options.SSCONF_MASTER_NODE) or options.ignore_hostname):
169 logging.error("Aborting due to hostname mismatch")
170 sys.exit(constants.EXIT_FAILURE)
172 if not options.force:
173 usertext = ("Please make sure you have read the upgrade notes for"
174 " Ganeti %s (available in the UPGRADE file and included"
175 " in other documentation formats). Continue with upgrading"
176 " configuration?" % constants.RELEASE_VERSION)
177 if not cli.AskUser(usertext):
178 sys.exit(constants.EXIT_FAILURE)
180 # Check whether it's a Ganeti configuration directory
181 if not (os.path.isfile(options.CONFIG_DATA_PATH) and
182 os.path.isfile(options.SERVER_PEM_PATH) and
183 os.path.isfile(options.KNOWN_HOSTS_PATH)):
184 raise Error(("%s does not seem to be a Ganeti configuration"
185 " directory") % options.data_dir)
187 if not os.path.isdir(options.conf_dir):
188 raise Error("Not a directory: %s" % options.conf_dir)
190 config_data = serializer.LoadJson(utils.ReadFile(options.CONFIG_DATA_PATH))
193 config_version = config_data["version"]
195 raise Error("Unable to determine configuration version")
197 (config_major, config_minor, config_revision) = \
198 constants.SplitVersion(config_version)
200 logging.info("Found configuration version %s (%d.%d.%d)",
201 config_version, config_major, config_minor, config_revision)
203 if "config_version" in config_data["cluster"]:
204 raise Error("Inconsistent configuration: found config_version in"
205 " configuration file")
207 # Upgrade from 2.0/2.1/2.2/2.3 to 2.4
208 if config_major == 2 and config_minor in (0, 1, 2, 3, 4, 5):
209 if config_revision != 0:
210 logging.warning("Config revision is %s, not 0", config_revision)
212 config_data["version"] = constants.BuildVersion(TARGET_MAJOR,
215 if "instances" not in config_data:
216 raise Error("Can't find the 'instances' key in the configuration!")
217 for instance, iobj in config_data["instances"].items():
218 if "disks" not in iobj:
219 raise Error("Instance '%s' doesn't have a disks entry?!" % instance)
220 disks = iobj["disks"]
221 for idx, dobj in enumerate(disks):
222 expected = "disk/%s" % idx
223 current = dobj.get("iv_name", "")
224 if current != expected:
225 logging.warning("Updating iv_name for instance %s/disk %s"
226 " from '%s' to '%s'",
227 instance, idx, current, expected)
228 dobj["iv_name"] = expected
230 elif config_major == TARGET_MAJOR and config_minor == TARGET_MINOR:
231 logging.info("No changes necessary")
234 raise Error("Configuration version %d.%d.%d not supported by this tool" %
235 (config_major, config_minor, config_revision))
237 if (os.path.isfile(options.RAPI_USERS_FILE_PRE24) and
238 not os.path.islink(options.RAPI_USERS_FILE_PRE24)):
239 if os.path.exists(options.RAPI_USERS_FILE):
240 raise Error("Found pre-2.4 RAPI users file at %s, but another file"
241 " already exists at %s" %
242 (options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE))
243 logging.info("Found pre-2.4 RAPI users file at %s, renaming to %s",
244 options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
245 if not options.dry_run:
246 utils.RenameFile(options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE,
247 mkdir=True, mkdir_mode=0750)
249 # Create a symlink for RAPI users file
250 if (not (os.path.islink(options.RAPI_USERS_FILE_PRE24) or
251 os.path.isfile(options.RAPI_USERS_FILE_PRE24)) and
252 os.path.isfile(options.RAPI_USERS_FILE)):
253 logging.info("Creating symlink from %s to %s",
254 options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
255 if not options.dry_run:
256 os.symlink(options.RAPI_USERS_FILE, options.RAPI_USERS_FILE_PRE24)
258 # Remove old watcher state file if it exists
259 if os.path.exists(options.WATCHER_STATEFILE):
260 logging.info("Removing watcher state file %s", options.WATCHER_STATEFILE)
261 if not options.dry_run:
262 utils.RemoveFile(options.WATCHER_STATEFILE)
264 # Write file storage paths
265 if not os.path.exists(options.FILE_STORAGE_PATHS_FILE):
266 cluster = config_data["cluster"]
267 file_storage_dir = cluster.get("file_storage_dir")
268 shared_file_storage_dir = cluster.get("shared_file_storage_dir")
271 logging.info("Ganeti 2.7 and later only allow whitelisted directories"
272 " for file storage; writing existing configuration values"
274 options.FILE_STORAGE_PATHS_FILE)
277 logging.info("File storage directory: %s", file_storage_dir)
278 if shared_file_storage_dir:
279 logging.info("Shared file storage directory: %s",
280 shared_file_storage_dir)
283 buf.write("# List automatically generated from configuration by\n")
284 buf.write("# cfgupgrade at %s\n" % time.asctime())
286 buf.write("%s\n" % file_storage_dir)
287 if shared_file_storage_dir:
288 buf.write("%s\n" % shared_file_storage_dir)
289 utils.WriteFile(file_name=options.FILE_STORAGE_PATHS_FILE,
292 dry_run=options.dry_run,
295 UpgradeNetworks(config_data)
296 UpgradeGroups(config_data)
299 logging.info("Writing configuration file to %s", options.CONFIG_DATA_PATH)
300 utils.WriteFile(file_name=options.CONFIG_DATA_PATH,
301 data=serializer.DumpJson(config_data),
303 dry_run=options.dry_run,
306 if not options.dry_run:
307 bootstrap.GenerateClusterCrypto(
308 False, False, False, False, False,
309 nodecert_file=options.SERVER_PEM_PATH,
310 rapicert_file=options.RAPI_CERT_FILE,
311 spicecert_file=options.SPICE_CERT_FILE,
312 spicecacert_file=options.SPICE_CACERT_FILE,
313 hmackey_file=options.CONFD_HMAC_KEY,
314 cds_file=options.CDS_FILE)
317 logging.critical("Writing configuration failed. It is probably in an"
318 " inconsistent state and needs manual intervention.")
321 # test loading the config file
322 if not (options.dry_run or options.no_verify):
323 logging.info("Testing the new config file...")
324 cfg = config.ConfigWriter(cfg_file=options.CONFIG_DATA_PATH,
325 accept_foreign=options.ignore_hostname,
327 # if we reached this, it's all fine
328 vrfy = cfg.VerifyConfig()
330 logging.error("Errors after conversion:")
332 logging.error(" - %s", item)
334 logging.info("File loaded successfully")
336 cli.ToStderr("Configuration successfully upgraded for version %s.",
337 constants.RELEASE_VERSION)
340 if __name__ == "__main__":