Adjust cfgupgrade for new minor version
[ganeti-local] / tools / cfgupgrade
1 #!/usr/bin/python
2 #
3
4 # Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012 Google Inc.
5 #
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.
10 #
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.
15 #
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
19 # 02110-1301, USA.
20
21
22 """Tool to upgrade the configuration file.
23
24 This code handles only the types supported by simplejson. As an
25 example, 'set' is a 'list'.
26
27 """
28
29
30 import os
31 import os.path
32 import sys
33 import optparse
34 import logging
35
36 from ganeti import constants
37 from ganeti import serializer
38 from ganeti import utils
39 from ganeti import cli
40 from ganeti import bootstrap
41 from ganeti import config
42 from ganeti import netutils
43
44
45 options = None
46 args = None
47
48
49 #: Target major version we will upgrade to
50 TARGET_MAJOR = 2
51 #: Target minor version we will upgrade to
52 TARGET_MINOR = 6
53
54
55 class Error(Exception):
56   """Generic exception"""
57   pass
58
59
60 def SetupLogging():
61   """Configures the logging module.
62
63   """
64   formatter = logging.Formatter("%(asctime)s: %(message)s")
65
66   stderr_handler = logging.StreamHandler()
67   stderr_handler.setFormatter(formatter)
68   if options.debug:
69     stderr_handler.setLevel(logging.NOTSET)
70   elif options.verbose:
71     stderr_handler.setLevel(logging.INFO)
72   else:
73     stderr_handler.setLevel(logging.WARNING)
74
75   root_logger = logging.getLogger("")
76   root_logger.setLevel(logging.NOTSET)
77   root_logger.addHandler(stderr_handler)
78
79
80 def CheckHostname(path):
81   """Ensures hostname matches ssconf value.
82
83   @param path: Path to ssconf file
84
85   """
86   ssconf_master_node = utils.ReadOneLineFile(path)
87   hostname = netutils.GetHostname().name
88
89   if ssconf_master_node == hostname:
90     return True
91
92   logging.warning("Warning: ssconf says master node is '%s', but this"
93                   " machine's name is '%s'; this tool must be run on"
94                   " the master node", ssconf_master_node, hostname)
95   return False
96
97
98 def main():
99   """Main program.
100
101   """
102   global options, args # pylint: disable=W0603
103
104   # Option parsing
105   parser = optparse.OptionParser(usage="%prog [--debug|--verbose] [--force]")
106   parser.add_option("--dry-run", dest="dry_run",
107                     action="store_true",
108                     help="Try to do the conversion, but don't write"
109                          " output file")
110   parser.add_option(cli.FORCE_OPT)
111   parser.add_option(cli.DEBUG_OPT)
112   parser.add_option(cli.VERBOSE_OPT)
113   parser.add_option("--ignore-hostname", dest="ignore_hostname",
114                     action="store_true", default=False,
115                     help="Don't abort if hostname doesn't match")
116   parser.add_option("--path", help="Convert configuration in this"
117                     " directory instead of '%s'" % constants.DATA_DIR,
118                     default=constants.DATA_DIR, dest="data_dir")
119   parser.add_option("--no-verify",
120                     help="Do not verify configuration after upgrade",
121                     action="store_true", dest="no_verify", default=False)
122   (options, args) = parser.parse_args()
123
124   # We need to keep filenames locally because they might be renamed between
125   # versions.
126   options.data_dir = os.path.abspath(options.data_dir)
127   options.CONFIG_DATA_PATH = options.data_dir + "/config.data"
128   options.SERVER_PEM_PATH = options.data_dir + "/server.pem"
129   options.KNOWN_HOSTS_PATH = options.data_dir + "/known_hosts"
130   options.RAPI_CERT_FILE = options.data_dir + "/rapi.pem"
131   options.SPICE_CERT_FILE = options.data_dir + "/spice.pem"
132   options.SPICE_CACERT_FILE = options.data_dir + "/spice-ca.pem"
133   options.RAPI_USERS_FILE = options.data_dir + "/rapi/users"
134   options.RAPI_USERS_FILE_PRE24 = options.data_dir + "/rapi_users"
135   options.CONFD_HMAC_KEY = options.data_dir + "/hmac.key"
136   options.CDS_FILE = options.data_dir + "/cluster-domain-secret"
137   options.SSCONF_MASTER_NODE = options.data_dir + "/ssconf_master_node"
138   options.WATCHER_STATEFILE = options.data_dir + "/watcher.data"
139
140   SetupLogging()
141
142   # Option checking
143   if args:
144     raise Error("No arguments expected")
145
146   # Check master name
147   if not (CheckHostname(options.SSCONF_MASTER_NODE) or options.ignore_hostname):
148     logging.error("Aborting due to hostname mismatch")
149     sys.exit(constants.EXIT_FAILURE)
150
151   if not options.force:
152     usertext = ("Please make sure you have read the upgrade notes for"
153                 " Ganeti %s (available in the UPGRADE file and included"
154                 " in other documentation formats). Continue with upgrading"
155                 " configuration?" % constants.RELEASE_VERSION)
156     if not cli.AskUser(usertext):
157       sys.exit(constants.EXIT_FAILURE)
158
159   # Check whether it's a Ganeti configuration directory
160   if not (os.path.isfile(options.CONFIG_DATA_PATH) and
161           os.path.isfile(options.SERVER_PEM_PATH) and
162           os.path.isfile(options.KNOWN_HOSTS_PATH)):
163     raise Error(("%s does not seem to be a Ganeti configuration"
164                  " directory") % options.data_dir)
165
166   config_data = serializer.LoadJson(utils.ReadFile(options.CONFIG_DATA_PATH))
167
168   try:
169     config_version = config_data["version"]
170   except KeyError:
171     raise Error("Unable to determine configuration version")
172
173   (config_major, config_minor, config_revision) = \
174     constants.SplitVersion(config_version)
175
176   logging.info("Found configuration version %s (%d.%d.%d)",
177                config_version, config_major, config_minor, config_revision)
178
179   if "config_version" in config_data["cluster"]:
180     raise Error("Inconsistent configuration: found config_version in"
181                 " configuration file")
182
183   # Upgrade from 2.0/2.1/2.2/2.3 to 2.4
184   if config_major == 2 and config_minor in (0, 1, 2, 3, 4, 5):
185     if config_revision != 0:
186       logging.warning("Config revision is %s, not 0", config_revision)
187
188     config_data["version"] = constants.BuildVersion(TARGET_MAJOR,
189                                                     TARGET_MINOR, 0)
190
191   elif config_major == TARGET_MAJOR and config_minor == TARGET_MINOR:
192     logging.info("No changes necessary")
193
194   else:
195     raise Error("Configuration version %d.%d.%d not supported by this tool" %
196                 (config_major, config_minor, config_revision))
197
198   if (os.path.isfile(options.RAPI_USERS_FILE_PRE24) and
199       not os.path.islink(options.RAPI_USERS_FILE_PRE24)):
200     if os.path.exists(options.RAPI_USERS_FILE):
201       raise Error("Found pre-2.4 RAPI users file at %s, but another file"
202                   " already exists at %s" %
203                   (options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE))
204     logging.info("Found pre-2.4 RAPI users file at %s, renaming to %s",
205                  options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
206     if not options.dry_run:
207       utils.RenameFile(options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE,
208                        mkdir=True, mkdir_mode=0750)
209
210   # Create a symlink for RAPI users file
211   if (not (os.path.islink(options.RAPI_USERS_FILE_PRE24) or
212            os.path.isfile(options.RAPI_USERS_FILE_PRE24)) and
213       os.path.isfile(options.RAPI_USERS_FILE)):
214     logging.info("Creating symlink from %s to %s",
215                  options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
216     if not options.dry_run:
217       os.symlink(options.RAPI_USERS_FILE, options.RAPI_USERS_FILE_PRE24)
218
219   # Remove old watcher state file if it exists
220   if os.path.exists(options.WATCHER_STATEFILE):
221     logging.info("Removing watcher state file %s", options.WATCHER_STATEFILE)
222     if not options.dry_run:
223       utils.RemoveFile(options.WATCHER_STATEFILE)
224
225   try:
226     logging.info("Writing configuration file to %s", options.CONFIG_DATA_PATH)
227     utils.WriteFile(file_name=options.CONFIG_DATA_PATH,
228                     data=serializer.DumpJson(config_data),
229                     mode=0600,
230                     dry_run=options.dry_run,
231                     backup=True)
232
233     if not options.dry_run:
234       bootstrap.GenerateClusterCrypto(False, False, False, False, False,
235                                      nodecert_file=options.SERVER_PEM_PATH,
236                                      rapicert_file=options.RAPI_CERT_FILE,
237                                      spicecert_file=options.SPICE_CERT_FILE,
238                                      spicecacert_file=options.SPICE_CACERT_FILE,
239                                      hmackey_file=options.CONFD_HMAC_KEY,
240                                      cds_file=options.CDS_FILE)
241
242   except Exception:
243     logging.critical("Writing configuration failed. It is probably in an"
244                      " inconsistent state and needs manual intervention.")
245     raise
246
247   # test loading the config file
248   if not (options.dry_run or options.no_verify):
249     logging.info("Testing the new config file...")
250     cfg = config.ConfigWriter(cfg_file=options.CONFIG_DATA_PATH,
251                               accept_foreign=options.ignore_hostname,
252                               offline=True)
253     # if we reached this, it's all fine
254     vrfy = cfg.VerifyConfig()
255     if vrfy:
256       logging.error("Errors after conversion:")
257       for item in vrfy:
258         logging.error(" - %s", item)
259     del cfg
260     logging.info("File loaded successfully")
261
262   cli.ToStderr("Configuration successfully upgraded for version %s.",
263                constants.RELEASE_VERSION)
264
265
266 if __name__ == "__main__":
267   main()