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