Statistics
| Branch: | Tag: | Revision:

root / tools / cfgupgrade @ ea2bcb82

History | View | Annotate | Download (12 kB)

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 = 6
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/2.1/2.2/2.3 to 2.4
208
  if config_major == 2 and config_minor in (0, 1, 2, 3, 4, 5):
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()