cfgupgrade: really ignore hostname when told so
[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):
189     logging.info("Found pre-2.4 RAPI users file at %s, renaming to %s",
190                  options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
191     utils.RenameFile(options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE,
192                      mkdir=True, mkdir_mode=0750)
193
194   # Create a symlink for RAPI users file
195   if not os.path.islink(options.RAPI_USERS_FILE_PRE24):
196     logging.info("Creating symlink from %s to %s",
197                  options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
198     os.symlink(options.RAPI_USERS_FILE, options.RAPI_USERS_FILE_PRE24)
199
200   try:
201     logging.info("Writing configuration file to %s", options.CONFIG_DATA_PATH)
202     utils.WriteFile(file_name=options.CONFIG_DATA_PATH,
203                     data=serializer.DumpJson(config_data),
204                     mode=0600,
205                     dry_run=options.dry_run,
206                     backup=True)
207
208     if not options.dry_run:
209       bootstrap.GenerateClusterCrypto(False, False, False, False,
210                                       nodecert_file=options.SERVER_PEM_PATH,
211                                       rapicert_file=options.RAPI_CERT_FILE,
212                                       hmackey_file=options.CONFD_HMAC_KEY,
213                                       cds_file=options.CDS_FILE)
214
215   except Exception:
216     logging.critical("Writing configuration failed. It is probably in an"
217                      " inconsistent state and needs manual intervention.")
218     raise
219
220   # test loading the config file
221   if not (options.dry_run or options.no_verify):
222     logging.info("Testing the new config file...")
223     cfg = config.ConfigWriter(cfg_file=options.CONFIG_DATA_PATH,
224                               accept_foreign=options.ignore_hostname,
225                               offline=True)
226     # if we reached this, it's all fine
227     vrfy = cfg.VerifyConfig()
228     if vrfy:
229       logging.error("Errors after conversion:")
230       for item in vrfy:
231         logging.error(" - %s", item)
232     del cfg
233     logging.info("File loaded successfully")
234
235
236 if __name__ == "__main__":
237   main()