DeprecationWarning fixes for pylint
[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=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   options.WATCHER_STATEFILE = options.data_dir + "/watcher.data"
131
132   SetupLogging()
133
134   # Option checking
135   if args:
136     raise Error("No arguments expected")
137
138   # Check master name
139   if not (CheckHostname(options.SSCONF_MASTER_NODE) or options.ignore_hostname):
140     logging.error("Aborting due to hostname mismatch")
141     sys.exit(constants.EXIT_FAILURE)
142
143   if not options.force:
144     usertext = ("Please make sure you have read the upgrade notes for"
145                 " Ganeti %s (available in the UPGRADE file and included"
146                 " in other documentation formats). Continue with upgrading"
147                 " configuration?" % constants.RELEASE_VERSION)
148     if not cli.AskUser(usertext):
149       sys.exit(constants.EXIT_FAILURE)
150
151   # Check whether it's a Ganeti configuration directory
152   if not (os.path.isfile(options.CONFIG_DATA_PATH) and
153           os.path.isfile(options.SERVER_PEM_PATH) and
154           os.path.isfile(options.KNOWN_HOSTS_PATH)):
155     raise Error(("%s does not seem to be a Ganeti configuration"
156                  " directory") % options.data_dir)
157
158   config_data = serializer.LoadJson(utils.ReadFile(options.CONFIG_DATA_PATH))
159
160   try:
161     config_version = config_data["version"]
162   except KeyError:
163     raise Error("Unable to determine configuration version")
164
165   (config_major, config_minor, config_revision) = \
166     constants.SplitVersion(config_version)
167
168   logging.info("Found configuration version %s (%d.%d.%d)",
169                config_version, config_major, config_minor, config_revision)
170
171   if "config_version" in config_data["cluster"]:
172     raise Error("Inconsistent configuration: found config_version in"
173                 " configuration file")
174
175   # Upgrade from 2.0/2.1/2.2/2.3 to 2.4
176   if config_major == 2 and config_minor in (0, 1, 2, 3, 4):
177     if config_revision != 0:
178       logging.warning("Config revision is %s, not 0", config_revision)
179
180     config_data["version"] = constants.BuildVersion(2, 5, 0)
181
182   elif config_major == 2 and config_minor == 5:
183     logging.info("No changes necessary")
184
185   else:
186     raise Error("Configuration version %d.%d.%d not supported by this tool" %
187                 (config_major, config_minor, config_revision))
188
189   if (os.path.isfile(options.RAPI_USERS_FILE_PRE24) and
190       not os.path.islink(options.RAPI_USERS_FILE_PRE24)):
191     if os.path.exists(options.RAPI_USERS_FILE):
192       raise Error("Found pre-2.4 RAPI users file at %s, but another file"
193                   " already exists at %s" %
194                   (options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE))
195     logging.info("Found pre-2.4 RAPI users file at %s, renaming to %s",
196                  options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
197     if not options.dry_run:
198       utils.RenameFile(options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE,
199                        mkdir=True, mkdir_mode=0750)
200
201   # Create a symlink for RAPI users file
202   if (not (os.path.islink(options.RAPI_USERS_FILE_PRE24) or
203            os.path.isfile(options.RAPI_USERS_FILE_PRE24)) and
204       os.path.isfile(options.RAPI_USERS_FILE)):
205     logging.info("Creating symlink from %s to %s",
206                  options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
207     if not options.dry_run:
208       os.symlink(options.RAPI_USERS_FILE, options.RAPI_USERS_FILE_PRE24)
209
210   # Remove old watcher state file if it exists
211   if os.path.exists(options.WATCHER_STATEFILE):
212     logging.info("Removing watcher state file %s", options.WATCHER_STATEFILE)
213     if not options.dry_run:
214       utils.RemoveFile(options.WATCHER_STATEFILE)
215
216   try:
217     logging.info("Writing configuration file to %s", options.CONFIG_DATA_PATH)
218     utils.WriteFile(file_name=options.CONFIG_DATA_PATH,
219                     data=serializer.DumpJson(config_data),
220                     mode=0600,
221                     dry_run=options.dry_run,
222                     backup=True)
223
224     if not options.dry_run:
225       bootstrap.GenerateClusterCrypto(False, False, False, False,
226                                       nodecert_file=options.SERVER_PEM_PATH,
227                                       rapicert_file=options.RAPI_CERT_FILE,
228                                       hmackey_file=options.CONFD_HMAC_KEY,
229                                       cds_file=options.CDS_FILE)
230
231   except Exception:
232     logging.critical("Writing configuration failed. It is probably in an"
233                      " inconsistent state and needs manual intervention.")
234     raise
235
236   # test loading the config file
237   if not (options.dry_run or options.no_verify):
238     logging.info("Testing the new config file...")
239     cfg = config.ConfigWriter(cfg_file=options.CONFIG_DATA_PATH,
240                               accept_foreign=options.ignore_hostname,
241                               offline=True)
242     # if we reached this, it's all fine
243     vrfy = cfg.VerifyConfig()
244     if vrfy:
245       logging.error("Errors after conversion:")
246       for item in vrfy:
247         logging.error(" - %s", item)
248     del cfg
249     logging.info("File loaded successfully")
250
251   cli.ToStderr("Configuration successfully upgraded for version %s.",
252                constants.RELEASE_VERSION)
253
254
255 if __name__ == "__main__":
256   main()