Statistics
| Branch: | Tag: | Revision:

root / tools / cfgupgrade @ 6e8091f9

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
def UpgradeNetworks(config_data):
101
  networks = config_data.get("networks", None)
102
  if not networks:
103
    config_data["networks"] = {}
104

    
105

    
106
def UpgradeGroups(config_data):
107
  for group in config_data["nodegroups"].values():
108
    networks = group.get("networks", None)
109
    if not networks:
110
      group["networks"] = {}
111

    
112

    
113
def main():
114
  """Main program.
115

    
116
  """
117
  global options, args # pylint: disable=W0603
118

    
119
  # Option parsing
120
  parser = optparse.OptionParser(usage="%prog [--debug|--verbose] [--force]")
121
  parser.add_option("--dry-run", dest="dry_run",
122
                    action="store_true",
123
                    help="Try to do the conversion, but don't write"
124
                         " output file")
125
  parser.add_option(cli.FORCE_OPT)
126
  parser.add_option(cli.DEBUG_OPT)
127
  parser.add_option(cli.VERBOSE_OPT)
128
  parser.add_option("--ignore-hostname", dest="ignore_hostname",
129
                    action="store_true", default=False,
130
                    help="Don't abort if hostname doesn't match")
131
  parser.add_option("--path", help="Convert configuration in this"
132
                    " directory instead of '%s'" % pathutils.DATA_DIR,
133
                    default=pathutils.DATA_DIR, dest="data_dir")
134
  parser.add_option("--confdir",
135
                    help=("Use this directory instead of '%s'" %
136
                          pathutils.CONF_DIR),
137
                    default=pathutils.CONF_DIR, dest="conf_dir")
138
  parser.add_option("--no-verify",
139
                    help="Do not verify configuration after upgrade",
140
                    action="store_true", dest="no_verify", default=False)
141
  (options, args) = parser.parse_args()
142

    
143
  # We need to keep filenames locally because they might be renamed between
144
  # versions.
145
  options.data_dir = os.path.abspath(options.data_dir)
146
  options.CONFIG_DATA_PATH = options.data_dir + "/config.data"
147
  options.SERVER_PEM_PATH = options.data_dir + "/server.pem"
148
  options.KNOWN_HOSTS_PATH = options.data_dir + "/known_hosts"
149
  options.RAPI_CERT_FILE = options.data_dir + "/rapi.pem"
150
  options.SPICE_CERT_FILE = options.data_dir + "/spice.pem"
151
  options.SPICE_CACERT_FILE = options.data_dir + "/spice-ca.pem"
152
  options.RAPI_USERS_FILE = options.data_dir + "/rapi/users"
153
  options.RAPI_USERS_FILE_PRE24 = options.data_dir + "/rapi_users"
154
  options.CONFD_HMAC_KEY = options.data_dir + "/hmac.key"
155
  options.CDS_FILE = options.data_dir + "/cluster-domain-secret"
156
  options.SSCONF_MASTER_NODE = options.data_dir + "/ssconf_master_node"
157
  options.WATCHER_STATEFILE = options.data_dir + "/watcher.data"
158
  options.FILE_STORAGE_PATHS_FILE = options.conf_dir + "/file-storage-paths"
159

    
160
  SetupLogging()
161

    
162
  # Option checking
163
  if args:
164
    raise Error("No arguments expected")
165

    
166
  # Check master name
167
  if not (CheckHostname(options.SSCONF_MASTER_NODE) or options.ignore_hostname):
168
    logging.error("Aborting due to hostname mismatch")
169
    sys.exit(constants.EXIT_FAILURE)
170

    
171
  if not options.force:
172
    usertext = ("Please make sure you have read the upgrade notes for"
173
                " Ganeti %s (available in the UPGRADE file and included"
174
                " in other documentation formats). Continue with upgrading"
175
                " configuration?" % constants.RELEASE_VERSION)
176
    if not cli.AskUser(usertext):
177
      sys.exit(constants.EXIT_FAILURE)
178

    
179
  # Check whether it's a Ganeti configuration directory
180
  if not (os.path.isfile(options.CONFIG_DATA_PATH) and
181
          os.path.isfile(options.SERVER_PEM_PATH) and
182
          os.path.isfile(options.KNOWN_HOSTS_PATH)):
183
    raise Error(("%s does not seem to be a Ganeti configuration"
184
                 " directory") % options.data_dir)
185

    
186
  if not os.path.isdir(options.conf_dir):
187
    raise Error("Not a directory: %s" % options.conf_dir)
188

    
189
  config_data = serializer.LoadJson(utils.ReadFile(options.CONFIG_DATA_PATH))
190

    
191
  try:
192
    config_version = config_data["version"]
193
  except KeyError:
194
    raise Error("Unable to determine configuration version")
195

    
196
  (config_major, config_minor, config_revision) = \
197
    constants.SplitVersion(config_version)
198

    
199
  logging.info("Found configuration version %s (%d.%d.%d)",
200
               config_version, config_major, config_minor, config_revision)
201

    
202
  if "config_version" in config_data["cluster"]:
203
    raise Error("Inconsistent configuration: found config_version in"
204
                " configuration file")
205

    
206
  # Upgrade from 2.0/2.1/2.2/2.3 to 2.4
207
  if config_major == 2 and config_minor in (0, 1, 2, 3, 4, 5):
208
    if config_revision != 0:
209
      logging.warning("Config revision is %s, not 0", config_revision)
210

    
211
    config_data["version"] = constants.BuildVersion(TARGET_MAJOR,
212
                                                    TARGET_MINOR, 0)
213

    
214
    if "instances" not in config_data:
215
      raise Error("Can't find the 'instances' key in the configuration!")
216
    for instance, iobj in config_data["instances"].items():
217
      if "disks" not in iobj:
218
        raise Error("Instance '%s' doesn't have a disks entry?!" % instance)
219
      disks = iobj["disks"]
220
      for idx, dobj in enumerate(disks):
221
        expected = "disk/%s" % idx
222
        current = dobj.get("iv_name", "")
223
        if current != expected:
224
          logging.warning("Updating iv_name for instance %s/disk %s"
225
                          " from '%s' to '%s'",
226
                          instance, idx, current, expected)
227
          dobj["iv_name"] = expected
228

    
229
  elif config_major == TARGET_MAJOR and config_minor == TARGET_MINOR:
230
    logging.info("No changes necessary")
231

    
232
  else:
233
    raise Error("Configuration version %d.%d.%d not supported by this tool" %
234
                (config_major, config_minor, config_revision))
235

    
236
  if (os.path.isfile(options.RAPI_USERS_FILE_PRE24) and
237
      not os.path.islink(options.RAPI_USERS_FILE_PRE24)):
238
    if os.path.exists(options.RAPI_USERS_FILE):
239
      raise Error("Found pre-2.4 RAPI users file at %s, but another file"
240
                  " already exists at %s" %
241
                  (options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE))
242
    logging.info("Found pre-2.4 RAPI users file at %s, renaming to %s",
243
                 options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
244
    if not options.dry_run:
245
      utils.RenameFile(options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE,
246
                       mkdir=True, mkdir_mode=0750)
247

    
248
  # Create a symlink for RAPI users file
249
  if (not (os.path.islink(options.RAPI_USERS_FILE_PRE24) or
250
           os.path.isfile(options.RAPI_USERS_FILE_PRE24)) and
251
      os.path.isfile(options.RAPI_USERS_FILE)):
252
    logging.info("Creating symlink from %s to %s",
253
                 options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
254
    if not options.dry_run:
255
      os.symlink(options.RAPI_USERS_FILE, options.RAPI_USERS_FILE_PRE24)
256

    
257
  # Remove old watcher state file if it exists
258
  if os.path.exists(options.WATCHER_STATEFILE):
259
    logging.info("Removing watcher state file %s", options.WATCHER_STATEFILE)
260
    if not options.dry_run:
261
      utils.RemoveFile(options.WATCHER_STATEFILE)
262

    
263
  # Write file storage paths
264
  if not os.path.exists(options.FILE_STORAGE_PATHS_FILE):
265
    cluster = config_data["cluster"]
266
    file_storage_dir = cluster.get("file_storage_dir")
267
    shared_file_storage_dir = cluster.get("shared_file_storage_dir")
268
    del cluster
269

    
270
    logging.info("Ganeti 2.7 and later only allow whitelisted directories"
271
                 " for file storage; writing existing configuration values"
272
                 " into '%s'",
273
                 options.FILE_STORAGE_PATHS_FILE)
274

    
275
    if file_storage_dir:
276
      logging.info("File storage directory: %s", file_storage_dir)
277
    if shared_file_storage_dir:
278
      logging.info("Shared file storage directory: %s",
279
                   shared_file_storage_dir)
280

    
281
    buf = StringIO()
282
    buf.write("# List automatically generated from configuration by\n")
283
    buf.write("# cfgupgrade at %s\n" % time.asctime())
284
    if file_storage_dir:
285
      buf.write("%s\n" % file_storage_dir)
286
    if shared_file_storage_dir:
287
      buf.write("%s\n" % shared_file_storage_dir)
288
    utils.WriteFile(file_name=options.FILE_STORAGE_PATHS_FILE,
289
                    data=buf.getvalue(),
290
                    mode=0600,
291
                    dry_run=options.dry_run,
292
                    backup=True)
293

    
294
  UpgradeNetworks(config_data)
295
  UpgradeGroups(config_data)
296

    
297
  try:
298
    logging.info("Writing configuration file to %s", options.CONFIG_DATA_PATH)
299
    utils.WriteFile(file_name=options.CONFIG_DATA_PATH,
300
                    data=serializer.DumpJson(config_data),
301
                    mode=0600,
302
                    dry_run=options.dry_run,
303
                    backup=True)
304

    
305
    if not options.dry_run:
306
      bootstrap.GenerateClusterCrypto(
307
        False, False, False, False, False,
308
        nodecert_file=options.SERVER_PEM_PATH,
309
        rapicert_file=options.RAPI_CERT_FILE,
310
        spicecert_file=options.SPICE_CERT_FILE,
311
        spicecacert_file=options.SPICE_CACERT_FILE,
312
        hmackey_file=options.CONFD_HMAC_KEY,
313
        cds_file=options.CDS_FILE)
314

    
315
  except Exception:
316
    logging.critical("Writing configuration failed. It is probably in an"
317
                     " inconsistent state and needs manual intervention.")
318
    raise
319

    
320
  # test loading the config file
321
  if not (options.dry_run or options.no_verify):
322
    logging.info("Testing the new config file...")
323
    cfg = config.ConfigWriter(cfg_file=options.CONFIG_DATA_PATH,
324
                              accept_foreign=options.ignore_hostname,
325
                              offline=True)
326
    # if we reached this, it's all fine
327
    vrfy = cfg.VerifyConfig()
328
    if vrfy:
329
      logging.error("Errors after conversion:")
330
      for item in vrfy:
331
        logging.error(" - %s", item)
332
    del cfg
333
    logging.info("File loaded successfully")
334

    
335
  cli.ToStderr("Configuration successfully upgraded for version %s.",
336
               constants.RELEASE_VERSION)
337

    
338

    
339
if __name__ == "__main__":
340
  main()