Remove editor-specific settings from cfgupgrade*
[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
43
44 options = None
45 args = None
46
47
48 class Error(Exception):
49   """Generic exception"""
50   pass
51
52
53 def SetupLogging():
54   """Configures the logging module.
55
56   """
57   formatter = logging.Formatter("%(asctime)s: %(message)s")
58
59   stderr_handler = logging.StreamHandler()
60   stderr_handler.setFormatter(formatter)
61   if options.debug:
62     stderr_handler.setLevel(logging.NOTSET)
63   elif options.verbose:
64     stderr_handler.setLevel(logging.INFO)
65   else:
66     stderr_handler.setLevel(logging.CRITICAL)
67
68   root_logger = logging.getLogger("")
69   root_logger.setLevel(logging.NOTSET)
70   root_logger.addHandler(stderr_handler)
71
72
73 def main():
74   """Main program.
75
76   """
77   global options, args # pylint: disable-msg=W0603
78
79   program = os.path.basename(sys.argv[0])
80
81   # Option parsing
82   parser = optparse.OptionParser(usage="%prog [--debug|--verbose] [--force]")
83   parser.add_option('--dry-run', dest='dry_run',
84                     action="store_true",
85                     help="Try to do the conversion, but don't write"
86                          " output file")
87   parser.add_option(cli.FORCE_OPT)
88   parser.add_option(cli.DEBUG_OPT)
89   parser.add_option(cli.VERBOSE_OPT)
90   parser.add_option('--path', help="Convert configuration in this"
91                     " directory instead of '%s'" % constants.DATA_DIR,
92                     default=constants.DATA_DIR, dest="data_dir")
93   parser.add_option("--no-verify",
94                     help="Do not verify configuration after upgrade",
95                     action="store_true", dest="no_verify", default=False)
96   (options, args) = parser.parse_args()
97
98   # We need to keep filenames locally because they might be renamed between
99   # versions.
100   options.data_dir = os.path.abspath(options.data_dir)
101   options.CONFIG_DATA_PATH = options.data_dir + "/config.data"
102   options.SERVER_PEM_PATH = options.data_dir + "/server.pem"
103   options.KNOWN_HOSTS_PATH = options.data_dir + "/known_hosts"
104   options.RAPI_CERT_FILE = options.data_dir + "/rapi.pem"
105   options.RAPI_USERS_FILE = options.data_dir + "/rapi/users"
106   options.RAPI_USERS_FILE_PRE24 = options.data_dir + "/rapi_users"
107   options.CONFD_HMAC_KEY = options.data_dir + "/hmac.key"
108   options.CDS_FILE = options.data_dir + "/cluster-domain-secret"
109
110   SetupLogging()
111
112   # Option checking
113   if args:
114     raise Error("No arguments expected")
115
116   if not options.force:
117     usertext = ("%s MUST be run on the master node. Is this the master"
118                 " node and are ALL instances down?" % program)
119     if not cli.AskUser(usertext):
120       sys.exit(constants.EXIT_FAILURE)
121
122   # Check whether it's a Ganeti configuration directory
123   if not (os.path.isfile(options.CONFIG_DATA_PATH) and
124           os.path.isfile(options.SERVER_PEM_PATH) and
125           os.path.isfile(options.KNOWN_HOSTS_PATH)):
126     raise Error(("%s does not seem to be a Ganeti configuration"
127                  " directory") % options.data_dir)
128
129   config_data = serializer.LoadJson(utils.ReadFile(options.CONFIG_DATA_PATH))
130
131   try:
132     config_version = config_data["version"]
133   except KeyError:
134     raise Error("Unable to determine configuration version")
135
136   (config_major, config_minor, config_revision) = \
137     constants.SplitVersion(config_version)
138
139   logging.info("Found configuration version %s (%d.%d.%d)",
140                config_version, config_major, config_minor, config_revision)
141
142   if "config_version" in config_data["cluster"]:
143     raise Error("Inconsistent configuration: found config_version in"
144                 " configuration file")
145
146   # Upgrade from 2.0/2.1/2.2 to 2.3
147   if config_major == 2 and config_minor in (0, 1, 2):
148     if config_revision != 0:
149       logging.warning("Config revision is %s, not 0", config_revision)
150
151     config_data["version"] = constants.BuildVersion(2, 3, 0)
152
153   elif config_major == 2 and config_minor == 3:
154     logging.info("No changes necessary")
155
156   else:
157     raise Error("Configuration version %d.%d.%d not supported by this tool" %
158                 (config_major, config_minor, config_revision))
159
160   if os.path.isfile(options.RAPI_USERS_FILE_PRE24):
161     logging.info("Found pre-2.4 RAPI users file at %s, renaming to %s",
162                  options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
163     utils.RenameFile(options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE,
164                      mkdir=True, mkdir_mode=0750)
165
166   # Create a symlink for RAPI users file
167   if not os.path.islink(options.RAPI_USERS_FILE_PRE24):
168     logging.info("Creating symlink from %s to %s",
169                  options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
170     os.symlink(options.RAPI_USERS_FILE, options.RAPI_USERS_FILE_PRE24)
171
172   try:
173     logging.info("Writing configuration file to %s", options.CONFIG_DATA_PATH)
174     utils.WriteFile(file_name=options.CONFIG_DATA_PATH,
175                     data=serializer.DumpJson(config_data),
176                     mode=0600,
177                     dry_run=options.dry_run,
178                     backup=True)
179
180     if not options.dry_run:
181       bootstrap.GenerateClusterCrypto(False, False, False, False,
182                                       nodecert_file=options.SERVER_PEM_PATH,
183                                       rapicert_file=options.RAPI_CERT_FILE,
184                                       hmackey_file=options.CONFD_HMAC_KEY,
185                                       cds_file=options.CDS_FILE)
186
187   except Exception:
188     logging.critical("Writing configuration failed. It is probably in an"
189                      " inconsistent state and needs manual intervention.")
190     raise
191
192   # test loading the config file
193   if not (options.dry_run or options.no_verify):
194     logging.info("Testing the new config file...")
195     cfg = config.ConfigWriter(cfg_file=options.CONFIG_DATA_PATH,
196                               offline=True)
197     # if we reached this, it's all fine
198     vrfy = cfg.VerifyConfig()
199     if vrfy:
200       logging.error("Errors after conversion:")
201       for item in vrfy:
202         logging.error(" - %s", item)
203     del cfg
204     logging.info("File loaded successfully")
205
206
207 if __name__ == "__main__":
208   main()