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