Statistics
| Branch: | Tag: | Revision:

root / tools / cfgupgrade @ 1709435e

History | View | Annotate | Download (15 kB)

1
#!/usr/bin/python
2
#
3

    
4
# Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012, 2013 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
#: Target major version for downgrade
57
DOWNGRADE_MAJOR = 2
58
#: Target minor version for downgrade
59
DOWNGRADE_MINOR = 7
60

    
61

    
62
class Error(Exception):
63
  """Generic exception"""
64
  pass
65

    
66

    
67
def SetupLogging():
68
  """Configures the logging module.
69

    
70
  """
71
  formatter = logging.Formatter("%(asctime)s: %(message)s")
72

    
73
  stderr_handler = logging.StreamHandler()
74
  stderr_handler.setFormatter(formatter)
75
  if options.debug:
76
    stderr_handler.setLevel(logging.NOTSET)
77
  elif options.verbose:
78
    stderr_handler.setLevel(logging.INFO)
79
  else:
80
    stderr_handler.setLevel(logging.WARNING)
81

    
82
  root_logger = logging.getLogger("")
83
  root_logger.setLevel(logging.NOTSET)
84
  root_logger.addHandler(stderr_handler)
85

    
86

    
87
def CheckHostname(path):
88
  """Ensures hostname matches ssconf value.
89

    
90
  @param path: Path to ssconf file
91

    
92
  """
93
  ssconf_master_node = utils.ReadOneLineFile(path)
94
  hostname = netutils.GetHostname().name
95

    
96
  if ssconf_master_node == hostname:
97
    return True
98

    
99
  logging.warning("Warning: ssconf says master node is '%s', but this"
100
                  " machine's name is '%s'; this tool must be run on"
101
                  " the master node", ssconf_master_node, hostname)
102
  return False
103

    
104

    
105
def UpgradeNetworks(config_data):
106
  networks = config_data.get("networks", None)
107
  if not networks:
108
    config_data["networks"] = {}
109

    
110

    
111
def UpgradeGroups(config_data):
112
  for group in config_data["nodegroups"].values():
113
    networks = group.get("networks", None)
114
    if not networks:
115
      group["networks"] = {}
116

    
117

    
118
def UpgradeInstances(config_data):
119
  network2uuid = dict((n["name"], n["uuid"])
120
                      for n in config_data["networks"].values())
121
  if "instances" not in config_data:
122
    raise Error("Can't find the 'instances' key in the configuration!")
123

    
124
  for instance, iobj in config_data["instances"].items():
125
    for nic in iobj["nics"]:
126
      name = nic.get("network", None)
127
      if name:
128
        uuid = network2uuid.get(name, None)
129
        if uuid:
130
          print("NIC with network name %s found."
131
                " Substituting with uuid %s." % (name, uuid))
132
          nic["network"] = uuid
133

    
134
    if "disks" not in iobj:
135
      raise Error("Instance '%s' doesn't have a disks entry?!" % instance)
136
    disks = iobj["disks"]
137
    for idx, dobj in enumerate(disks):
138
      expected = "disk/%s" % idx
139
      current = dobj.get("iv_name", "")
140
      if current != expected:
141
        logging.warning("Updating iv_name for instance %s/disk %s"
142
                        " from '%s' to '%s'",
143
                        instance, idx, current, expected)
144
        dobj["iv_name"] = expected
145

    
146

    
147
def UpgradeRapiUsers():
148
  if (os.path.isfile(options.RAPI_USERS_FILE_PRE24) and
149
      not os.path.islink(options.RAPI_USERS_FILE_PRE24)):
150
    if os.path.exists(options.RAPI_USERS_FILE):
151
      raise Error("Found pre-2.4 RAPI users file at %s, but another file"
152
                  " already exists at %s" %
153
                  (options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE))
154
    logging.info("Found pre-2.4 RAPI users file at %s, renaming to %s",
155
                 options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
156
    if not options.dry_run:
157
      utils.RenameFile(options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE,
158
                       mkdir=True, mkdir_mode=0750)
159

    
160
  # Create a symlink for RAPI users file
161
  if (not (os.path.islink(options.RAPI_USERS_FILE_PRE24) or
162
           os.path.isfile(options.RAPI_USERS_FILE_PRE24)) and
163
      os.path.isfile(options.RAPI_USERS_FILE)):
164
    logging.info("Creating symlink from %s to %s",
165
                 options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
166
    if not options.dry_run:
167
      os.symlink(options.RAPI_USERS_FILE, options.RAPI_USERS_FILE_PRE24)
168

    
169

    
170
def UpgradeWatcher():
171
  # Remove old watcher state file if it exists
172
  if os.path.exists(options.WATCHER_STATEFILE):
173
    logging.info("Removing watcher state file %s", options.WATCHER_STATEFILE)
174
    if not options.dry_run:
175
      utils.RemoveFile(options.WATCHER_STATEFILE)
176

    
177

    
178
def UpgradeFileStoragePaths(config_data):
179
  # Write file storage paths
180
  if not os.path.exists(options.FILE_STORAGE_PATHS_FILE):
181
    cluster = config_data["cluster"]
182
    file_storage_dir = cluster.get("file_storage_dir")
183
    shared_file_storage_dir = cluster.get("shared_file_storage_dir")
184
    del cluster
185

    
186
    logging.info("Ganeti 2.7 and later only allow whitelisted directories"
187
                 " for file storage; writing existing configuration values"
188
                 " into '%s'",
189
                 options.FILE_STORAGE_PATHS_FILE)
190

    
191
    if file_storage_dir:
192
      logging.info("File storage directory: %s", file_storage_dir)
193
    if shared_file_storage_dir:
194
      logging.info("Shared file storage directory: %s",
195
                   shared_file_storage_dir)
196

    
197
    buf = StringIO()
198
    buf.write("# List automatically generated from configuration by\n")
199
    buf.write("# cfgupgrade at %s\n" % time.asctime())
200
    if file_storage_dir:
201
      buf.write("%s\n" % file_storage_dir)
202
    if shared_file_storage_dir:
203
      buf.write("%s\n" % shared_file_storage_dir)
204
    utils.WriteFile(file_name=options.FILE_STORAGE_PATHS_FILE,
205
                    data=buf.getvalue(),
206
                    mode=0600,
207
                    dry_run=options.dry_run,
208
                    backup=True)
209

    
210

    
211
def UpgradeAll(config_data):
212
  config_data["version"] = constants.BuildVersion(TARGET_MAJOR,
213
                                                  TARGET_MINOR, 0)
214
  UpgradeRapiUsers()
215
  UpgradeWatcher()
216
  UpgradeFileStoragePaths(config_data)
217
  UpgradeNetworks(config_data)
218
  UpgradeGroups(config_data)
219
  UpgradeInstances(config_data)
220

    
221

    
222
def DowngradeStorageTypes(cluster):
223
  # Remove storage types to downgrade to 2.7
224
  if "enabled_storage_types" in cluster:
225
    logging.warning("Removing cluster storage types; value = %s",
226
                    utils.CommaJoin(cluster["enabled_storage_types"]))
227
    del cluster["enabled_storage_types"]
228

    
229

    
230
def DowngradeCluster(config_data):
231
  cluster = config_data.get("cluster", None)
232
  if cluster is None:
233
    raise Error("Cannot find cluster")
234
  DowngradeStorageTypes(cluster)
235

    
236

    
237
def DowngradeAll(config_data):
238
  # Any code specific to a particular version should be labeled that way, so
239
  # it can be removed when updating to the next version.
240
  DowngradeCluster(config_data)
241

    
242

    
243
def main():
244
  """Main program.
245

    
246
  """
247
  global options, args # pylint: disable=W0603
248

    
249
  # Option parsing
250
  parser = optparse.OptionParser(usage="%prog [--debug|--verbose] [--force]")
251
  parser.add_option("--dry-run", dest="dry_run",
252
                    action="store_true",
253
                    help="Try to do the conversion, but don't write"
254
                         " output file")
255
  parser.add_option(cli.FORCE_OPT)
256
  parser.add_option(cli.DEBUG_OPT)
257
  parser.add_option(cli.VERBOSE_OPT)
258
  parser.add_option("--ignore-hostname", dest="ignore_hostname",
259
                    action="store_true", default=False,
260
                    help="Don't abort if hostname doesn't match")
261
  parser.add_option("--path", help="Convert configuration in this"
262
                    " directory instead of '%s'" % pathutils.DATA_DIR,
263
                    default=pathutils.DATA_DIR, dest="data_dir")
264
  parser.add_option("--confdir",
265
                    help=("Use this directory instead of '%s'" %
266
                          pathutils.CONF_DIR),
267
                    default=pathutils.CONF_DIR, dest="conf_dir")
268
  parser.add_option("--no-verify",
269
                    help="Do not verify configuration after upgrade",
270
                    action="store_true", dest="no_verify", default=False)
271
  parser.add_option("--downgrade",
272
                    help="Downgrade to the previous stable version",
273
                    action="store_true", dest="downgrade", default=False)
274
  (options, args) = parser.parse_args()
275

    
276
  # We need to keep filenames locally because they might be renamed between
277
  # versions.
278
  options.data_dir = os.path.abspath(options.data_dir)
279
  options.CONFIG_DATA_PATH = options.data_dir + "/config.data"
280
  options.SERVER_PEM_PATH = options.data_dir + "/server.pem"
281
  options.KNOWN_HOSTS_PATH = options.data_dir + "/known_hosts"
282
  options.RAPI_CERT_FILE = options.data_dir + "/rapi.pem"
283
  options.SPICE_CERT_FILE = options.data_dir + "/spice.pem"
284
  options.SPICE_CACERT_FILE = options.data_dir + "/spice-ca.pem"
285
  options.RAPI_USERS_FILE = options.data_dir + "/rapi/users"
286
  options.RAPI_USERS_FILE_PRE24 = options.data_dir + "/rapi_users"
287
  options.CONFD_HMAC_KEY = options.data_dir + "/hmac.key"
288
  options.CDS_FILE = options.data_dir + "/cluster-domain-secret"
289
  options.SSCONF_MASTER_NODE = options.data_dir + "/ssconf_master_node"
290
  options.WATCHER_STATEFILE = options.data_dir + "/watcher.data"
291
  options.FILE_STORAGE_PATHS_FILE = options.conf_dir + "/file-storage-paths"
292

    
293
  SetupLogging()
294

    
295
  # Option checking
296
  if args:
297
    raise Error("No arguments expected")
298
  if options.downgrade and not options.no_verify:
299
    options.no_verify = True
300

    
301
  # Check master name
302
  if not (CheckHostname(options.SSCONF_MASTER_NODE) or options.ignore_hostname):
303
    logging.error("Aborting due to hostname mismatch")
304
    sys.exit(constants.EXIT_FAILURE)
305

    
306
  if not options.force:
307
    if options.downgrade:
308
      usertext = ("The configuration is going to be DOWNGRADED to version %s.%s"
309
                  " Some configuration data might be removed if they don't fit"
310
                  " in the old format. Please make sure you have read the"
311
                  " upgrade notes (available in the UPGRADE file and included"
312
                  " in other documentation formats) to understand what they"
313
                  " are. Continue with *DOWNGRADING* the configuration?" %
314
                  (DOWNGRADE_MAJOR, DOWNGRADE_MINOR))
315
    else:
316
      usertext = ("Please make sure you have read the upgrade notes for"
317
                  " Ganeti %s (available in the UPGRADE file and included"
318
                  " in other documentation formats). Continue with upgrading"
319
                  " configuration?" % constants.RELEASE_VERSION)
320
    if not cli.AskUser(usertext):
321
      sys.exit(constants.EXIT_FAILURE)
322

    
323
  # Check whether it's a Ganeti configuration directory
324
  if not (os.path.isfile(options.CONFIG_DATA_PATH) and
325
          os.path.isfile(options.SERVER_PEM_PATH) and
326
          os.path.isfile(options.KNOWN_HOSTS_PATH)):
327
    raise Error(("%s does not seem to be a Ganeti configuration"
328
                 " directory") % options.data_dir)
329

    
330
  if not os.path.isdir(options.conf_dir):
331
    raise Error("Not a directory: %s" % options.conf_dir)
332

    
333
  config_data = serializer.LoadJson(utils.ReadFile(options.CONFIG_DATA_PATH))
334

    
335
  try:
336
    config_version = config_data["version"]
337
  except KeyError:
338
    raise Error("Unable to determine configuration version")
339

    
340
  (config_major, config_minor, config_revision) = \
341
    constants.SplitVersion(config_version)
342

    
343
  logging.info("Found configuration version %s (%d.%d.%d)",
344
               config_version, config_major, config_minor, config_revision)
345

    
346
  if "config_version" in config_data["cluster"]:
347
    raise Error("Inconsistent configuration: found config_version in"
348
                " configuration file")
349

    
350
  # Downgrade to the previous stable version
351
  if options.downgrade:
352
    if config_major != TARGET_MAJOR or config_minor != TARGET_MINOR:
353
      raise Error("Downgrade supported only from the latest version (%s.%s),"
354
                  " found %s (%s.%s.%s) instead" %
355
                  (TARGET_MAJOR, TARGET_MINOR, config_version, config_major,
356
                   config_minor, config_revision))
357
    DowngradeAll(config_data)
358

    
359
  # Upgrade from 2.{0..6} to 2.7
360
  elif config_major == 2 and config_minor in (0, 1, 2, 3, 4, 5, 6):
361
    if config_revision != 0:
362
      logging.warning("Config revision is %s, not 0", config_revision)
363
    UpgradeAll(config_data)
364

    
365
  elif config_major == TARGET_MAJOR and config_minor == TARGET_MINOR:
366
    logging.info("No changes necessary")
367

    
368
  else:
369
    raise Error("Configuration version %d.%d.%d not supported by this tool" %
370
                (config_major, config_minor, config_revision))
371

    
372
  try:
373
    logging.info("Writing configuration file to %s", options.CONFIG_DATA_PATH)
374
    utils.WriteFile(file_name=options.CONFIG_DATA_PATH,
375
                    data=serializer.DumpJson(config_data),
376
                    mode=0600,
377
                    dry_run=options.dry_run,
378
                    backup=True)
379

    
380
    if not options.dry_run:
381
      bootstrap.GenerateClusterCrypto(
382
        False, False, False, False, False,
383
        nodecert_file=options.SERVER_PEM_PATH,
384
        rapicert_file=options.RAPI_CERT_FILE,
385
        spicecert_file=options.SPICE_CERT_FILE,
386
        spicecacert_file=options.SPICE_CACERT_FILE,
387
        hmackey_file=options.CONFD_HMAC_KEY,
388
        cds_file=options.CDS_FILE)
389

    
390
  except Exception:
391
    logging.critical("Writing configuration failed. It is probably in an"
392
                     " inconsistent state and needs manual intervention.")
393
    raise
394

    
395
  # test loading the config file
396
  all_ok = True
397
  if not (options.dry_run or options.no_verify):
398
    logging.info("Testing the new config file...")
399
    cfg = config.ConfigWriter(cfg_file=options.CONFIG_DATA_PATH,
400
                              accept_foreign=options.ignore_hostname,
401
                              offline=True)
402
    # if we reached this, it's all fine
403
    vrfy = cfg.VerifyConfig()
404
    if vrfy:
405
      logging.error("Errors after conversion:")
406
      for item in vrfy:
407
        logging.error(" - %s", item)
408
      all_ok = False
409
    else:
410
      logging.info("File loaded successfully after upgrading")
411
    del cfg
412

    
413
  if options.downgrade:
414
    action = "downgraded"
415
    out_ver = "%s.%s" % (DOWNGRADE_MAJOR, DOWNGRADE_MINOR)
416
  else:
417
    action = "upgraded"
418
    out_ver = constants.RELEASE_VERSION
419
  if all_ok:
420
    cli.ToStderr("Configuration successfully %s to version %s.",
421
                 action, out_ver)
422
  else:
423
    cli.ToStderr("Configuration %s to version %s, but there are errors."
424
                 "\nPlease review the file.", action, out_ver)
425

    
426

    
427
if __name__ == "__main__":
428
  main()