If _UnlockedLookupNetwork() fails raise error
[ganeti-local] / tools / cfgupgrade
1 #!/usr/bin/python
2 #
3
4 # Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012 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 time
36 from cStringIO import StringIO
37
38 from ganeti import constants
39 from ganeti import serializer
40 from ganeti import utils
41 from ganeti import cli
42 from ganeti import bootstrap
43 from ganeti import config
44 from ganeti import netutils
45 from ganeti import pathutils
46
47
48 options = None
49 args = None
50
51
52 #: Target major version we will upgrade to
53 TARGET_MAJOR = 2
54 #: Target minor version we will upgrade to
55 TARGET_MINOR = 7
56
57
58 class Error(Exception):
59   """Generic exception"""
60   pass
61
62
63 def SetupLogging():
64   """Configures the logging module.
65
66   """
67   formatter = logging.Formatter("%(asctime)s: %(message)s")
68
69   stderr_handler = logging.StreamHandler()
70   stderr_handler.setFormatter(formatter)
71   if options.debug:
72     stderr_handler.setLevel(logging.NOTSET)
73   elif options.verbose:
74     stderr_handler.setLevel(logging.INFO)
75   else:
76     stderr_handler.setLevel(logging.WARNING)
77
78   root_logger = logging.getLogger("")
79   root_logger.setLevel(logging.NOTSET)
80   root_logger.addHandler(stderr_handler)
81
82
83 def CheckHostname(path):
84   """Ensures hostname matches ssconf value.
85
86   @param path: Path to ssconf file
87
88   """
89   ssconf_master_node = utils.ReadOneLineFile(path)
90   hostname = netutils.GetHostname().name
91
92   if ssconf_master_node == hostname:
93     return True
94
95   logging.warning("Warning: ssconf says master node is '%s', but this"
96                   " machine's name is '%s'; this tool must be run on"
97                   " the master node", ssconf_master_node, hostname)
98   return False
99
100
101 def UpgradeNetworks(config_data):
102   networks = config_data.get("networks", None)
103   if not networks:
104     config_data["networks"] = {}
105
106
107 def UpgradeGroups(config_data):
108   for group in config_data["nodegroups"].values():
109     networks = group.get("networks", None)
110     if not networks:
111       group["networks"] = {}
112
113
114 def main():
115   """Main program.
116
117   """
118   global options, args # pylint: disable=W0603
119
120   # Option parsing
121   parser = optparse.OptionParser(usage="%prog [--debug|--verbose] [--force]")
122   parser.add_option("--dry-run", dest="dry_run",
123                     action="store_true",
124                     help="Try to do the conversion, but don't write"
125                          " output file")
126   parser.add_option(cli.FORCE_OPT)
127   parser.add_option(cli.DEBUG_OPT)
128   parser.add_option(cli.VERBOSE_OPT)
129   parser.add_option("--ignore-hostname", dest="ignore_hostname",
130                     action="store_true", default=False,
131                     help="Don't abort if hostname doesn't match")
132   parser.add_option("--path", help="Convert configuration in this"
133                     " directory instead of '%s'" % pathutils.DATA_DIR,
134                     default=pathutils.DATA_DIR, dest="data_dir")
135   parser.add_option("--confdir",
136                     help=("Use this directory instead of '%s'" %
137                           pathutils.CONF_DIR),
138                     default=pathutils.CONF_DIR, dest="conf_dir")
139   parser.add_option("--no-verify",
140                     help="Do not verify configuration after upgrade",
141                     action="store_true", dest="no_verify", default=False)
142   (options, args) = parser.parse_args()
143
144   # We need to keep filenames locally because they might be renamed between
145   # versions.
146   options.data_dir = os.path.abspath(options.data_dir)
147   options.CONFIG_DATA_PATH = options.data_dir + "/config.data"
148   options.SERVER_PEM_PATH = options.data_dir + "/server.pem"
149   options.KNOWN_HOSTS_PATH = options.data_dir + "/known_hosts"
150   options.RAPI_CERT_FILE = options.data_dir + "/rapi.pem"
151   options.SPICE_CERT_FILE = options.data_dir + "/spice.pem"
152   options.SPICE_CACERT_FILE = options.data_dir + "/spice-ca.pem"
153   options.RAPI_USERS_FILE = options.data_dir + "/rapi/users"
154   options.RAPI_USERS_FILE_PRE24 = options.data_dir + "/rapi_users"
155   options.CONFD_HMAC_KEY = options.data_dir + "/hmac.key"
156   options.CDS_FILE = options.data_dir + "/cluster-domain-secret"
157   options.SSCONF_MASTER_NODE = options.data_dir + "/ssconf_master_node"
158   options.WATCHER_STATEFILE = options.data_dir + "/watcher.data"
159   options.FILE_STORAGE_PATHS_FILE = options.conf_dir + "/file-storage-paths"
160
161   SetupLogging()
162
163   # Option checking
164   if args:
165     raise Error("No arguments expected")
166
167   # Check master name
168   if not (CheckHostname(options.SSCONF_MASTER_NODE) or options.ignore_hostname):
169     logging.error("Aborting due to hostname mismatch")
170     sys.exit(constants.EXIT_FAILURE)
171
172   if not options.force:
173     usertext = ("Please make sure you have read the upgrade notes for"
174                 " Ganeti %s (available in the UPGRADE file and included"
175                 " in other documentation formats). Continue with upgrading"
176                 " configuration?" % constants.RELEASE_VERSION)
177     if not cli.AskUser(usertext):
178       sys.exit(constants.EXIT_FAILURE)
179
180   # Check whether it's a Ganeti configuration directory
181   if not (os.path.isfile(options.CONFIG_DATA_PATH) and
182           os.path.isfile(options.SERVER_PEM_PATH) and
183           os.path.isfile(options.KNOWN_HOSTS_PATH)):
184     raise Error(("%s does not seem to be a Ganeti configuration"
185                  " directory") % options.data_dir)
186
187   if not os.path.isdir(options.conf_dir):
188     raise Error("Not a directory: %s" % options.conf_dir)
189
190   config_data = serializer.LoadJson(utils.ReadFile(options.CONFIG_DATA_PATH))
191
192   try:
193     config_version = config_data["version"]
194   except KeyError:
195     raise Error("Unable to determine configuration version")
196
197   (config_major, config_minor, config_revision) = \
198     constants.SplitVersion(config_version)
199
200   logging.info("Found configuration version %s (%d.%d.%d)",
201                config_version, config_major, config_minor, config_revision)
202
203   if "config_version" in config_data["cluster"]:
204     raise Error("Inconsistent configuration: found config_version in"
205                 " configuration file")
206
207   # Upgrade from 2.{0..6} to 2.7
208   if config_major == 2 and config_minor in (0, 1, 2, 3, 4, 5, 6):
209     if config_revision != 0:
210       logging.warning("Config revision is %s, not 0", config_revision)
211
212     config_data["version"] = constants.BuildVersion(TARGET_MAJOR,
213                                                     TARGET_MINOR, 0)
214
215     if "instances" not in config_data:
216       raise Error("Can't find the 'instances' key in the configuration!")
217     for instance, iobj in config_data["instances"].items():
218       if "disks" not in iobj:
219         raise Error("Instance '%s' doesn't have a disks entry?!" % instance)
220       disks = iobj["disks"]
221       for idx, dobj in enumerate(disks):
222         expected = "disk/%s" % idx
223         current = dobj.get("iv_name", "")
224         if current != expected:
225           logging.warning("Updating iv_name for instance %s/disk %s"
226                           " from '%s' to '%s'",
227                           instance, idx, current, expected)
228           dobj["iv_name"] = expected
229
230   elif config_major == TARGET_MAJOR and config_minor == TARGET_MINOR:
231     logging.info("No changes necessary")
232
233   else:
234     raise Error("Configuration version %d.%d.%d not supported by this tool" %
235                 (config_major, config_minor, config_revision))
236
237   if (os.path.isfile(options.RAPI_USERS_FILE_PRE24) and
238       not os.path.islink(options.RAPI_USERS_FILE_PRE24)):
239     if os.path.exists(options.RAPI_USERS_FILE):
240       raise Error("Found pre-2.4 RAPI users file at %s, but another file"
241                   " already exists at %s" %
242                   (options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE))
243     logging.info("Found pre-2.4 RAPI users file at %s, renaming to %s",
244                  options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
245     if not options.dry_run:
246       utils.RenameFile(options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE,
247                        mkdir=True, mkdir_mode=0750)
248
249   # Create a symlink for RAPI users file
250   if (not (os.path.islink(options.RAPI_USERS_FILE_PRE24) or
251            os.path.isfile(options.RAPI_USERS_FILE_PRE24)) and
252       os.path.isfile(options.RAPI_USERS_FILE)):
253     logging.info("Creating symlink from %s to %s",
254                  options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
255     if not options.dry_run:
256       os.symlink(options.RAPI_USERS_FILE, options.RAPI_USERS_FILE_PRE24)
257
258   # Remove old watcher state file if it exists
259   if os.path.exists(options.WATCHER_STATEFILE):
260     logging.info("Removing watcher state file %s", options.WATCHER_STATEFILE)
261     if not options.dry_run:
262       utils.RemoveFile(options.WATCHER_STATEFILE)
263
264   # Write file storage paths
265   if not os.path.exists(options.FILE_STORAGE_PATHS_FILE):
266     cluster = config_data["cluster"]
267     file_storage_dir = cluster.get("file_storage_dir")
268     shared_file_storage_dir = cluster.get("shared_file_storage_dir")
269     del cluster
270
271     logging.info("Ganeti 2.7 and later only allow whitelisted directories"
272                  " for file storage; writing existing configuration values"
273                  " into '%s'",
274                  options.FILE_STORAGE_PATHS_FILE)
275
276     if file_storage_dir:
277       logging.info("File storage directory: %s", file_storage_dir)
278     if shared_file_storage_dir:
279       logging.info("Shared file storage directory: %s",
280                    shared_file_storage_dir)
281
282     buf = StringIO()
283     buf.write("# List automatically generated from configuration by\n")
284     buf.write("# cfgupgrade at %s\n" % time.asctime())
285     if file_storage_dir:
286       buf.write("%s\n" % file_storage_dir)
287     if shared_file_storage_dir:
288       buf.write("%s\n" % shared_file_storage_dir)
289     utils.WriteFile(file_name=options.FILE_STORAGE_PATHS_FILE,
290                     data=buf.getvalue(),
291                     mode=0600,
292                     dry_run=options.dry_run,
293                     backup=True)
294
295   UpgradeNetworks(config_data)
296   UpgradeGroups(config_data)
297
298   try:
299     logging.info("Writing configuration file to %s", options.CONFIG_DATA_PATH)
300     utils.WriteFile(file_name=options.CONFIG_DATA_PATH,
301                     data=serializer.DumpJson(config_data),
302                     mode=0600,
303                     dry_run=options.dry_run,
304                     backup=True)
305
306     if not options.dry_run:
307       bootstrap.GenerateClusterCrypto(
308         False, False, False, False, False,
309         nodecert_file=options.SERVER_PEM_PATH,
310         rapicert_file=options.RAPI_CERT_FILE,
311         spicecert_file=options.SPICE_CERT_FILE,
312         spicecacert_file=options.SPICE_CACERT_FILE,
313         hmackey_file=options.CONFD_HMAC_KEY,
314         cds_file=options.CDS_FILE)
315
316   except Exception:
317     logging.critical("Writing configuration failed. It is probably in an"
318                      " inconsistent state and needs manual intervention.")
319     raise
320
321   # test loading the config file
322   if not (options.dry_run or options.no_verify):
323     logging.info("Testing the new config file...")
324     cfg = config.ConfigWriter(cfg_file=options.CONFIG_DATA_PATH,
325                               accept_foreign=options.ignore_hostname,
326                               offline=True)
327     # if we reached this, it's all fine
328     vrfy = cfg.VerifyConfig()
329     if vrfy:
330       logging.error("Errors after conversion:")
331       for item in vrfy:
332         logging.error(" - %s", item)
333     del cfg
334     logging.info("File loaded successfully")
335
336   cli.ToStderr("Configuration successfully upgraded for version %s.",
337                constants.RELEASE_VERSION)
338
339
340 if __name__ == "__main__":
341   main()