Statistics
| Branch: | Tag: | Revision:

root / tools / cfgupgrade @ 4d33e134

History | View | Annotate | Download (20.2 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 = 9
56
#: Target major version for downgrade
57
DOWNGRADE_MAJOR = 2
58
#: Target minor version for downgrade
59
DOWNGRADE_MINOR = 8
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 _FillIPolicySpecs(default_ipolicy, ipolicy):
106
  if "minmax" in ipolicy:
107
    for (key, spec) in ipolicy["minmax"][0].items():
108
      for (par, val) in default_ipolicy["minmax"][0][key].items():
109
        if par not in spec:
110
          spec[par] = val
111

    
112

    
113
def UpgradeIPolicy(ipolicy, default_ipolicy, isgroup):
114
  minmax_keys = ["min", "max"]
115
  if any((k in ipolicy) for k in minmax_keys):
116
    minmax = {}
117
    for key in minmax_keys:
118
      if key in ipolicy:
119
        if ipolicy[key]:
120
          minmax[key] = ipolicy[key]
121
        del ipolicy[key]
122
    if minmax:
123
      ipolicy["minmax"] = [minmax]
124
  if isgroup and "std" in ipolicy:
125
    del ipolicy["std"]
126
  _FillIPolicySpecs(default_ipolicy, ipolicy)
127

    
128

    
129
def UpgradeNetworks(config_data):
130
  networks = config_data.get("networks", None)
131
  if not networks:
132
    config_data["networks"] = {}
133

    
134

    
135
def UpgradeCluster(config_data):
136
  cluster = config_data.get("cluster", None)
137
  if cluster is None:
138
    raise Error("Cannot find cluster")
139
  ipolicy = cluster.setdefault("ipolicy", None)
140
  if ipolicy:
141
    UpgradeIPolicy(ipolicy, constants.IPOLICY_DEFAULTS, False)
142

    
143

    
144
def UpgradeGroups(config_data):
145
  cl_ipolicy = config_data["cluster"].get("ipolicy")
146
  for group in config_data["nodegroups"].values():
147
    networks = group.get("networks", None)
148
    if not networks:
149
      group["networks"] = {}
150
    ipolicy = group.get("ipolicy", None)
151
    if ipolicy:
152
      if cl_ipolicy is None:
153
        raise Error("A group defines an instance policy but there is no"
154
                    " instance policy at cluster level")
155
      UpgradeIPolicy(ipolicy, cl_ipolicy, True)
156

    
157

    
158
def GetExclusiveStorageValue(config_data):
159
  """Return a conservative value of the exclusive_storage flag.
160

    
161
  Return C{True} if the cluster or at least a nodegroup have the flag set.
162

    
163
  """
164
  ret = False
165
  cluster = config_data["cluster"]
166
  ndparams = cluster.get("ndparams")
167
  if ndparams is not None and ndparams.get("exclusive_storage"):
168
    ret = True
169
  for group in config_data["nodegroups"].values():
170
    ndparams = group.get("ndparams")
171
    if ndparams is not None and ndparams.get("exclusive_storage"):
172
      ret = True
173
  return ret
174

    
175

    
176
def UpgradeInstances(config_data):
177
  network2uuid = dict((n["name"], n["uuid"])
178
                      for n in config_data["networks"].values())
179
  if "instances" not in config_data:
180
    raise Error("Can't find the 'instances' key in the configuration!")
181

    
182
  missing_spindles = False
183
  for instance, iobj in config_data["instances"].items():
184
    for nic in iobj["nics"]:
185
      name = nic.get("network", None)
186
      if name:
187
        uuid = network2uuid.get(name, None)
188
        if uuid:
189
          print("NIC with network name %s found."
190
                " Substituting with uuid %s." % (name, uuid))
191
          nic["network"] = uuid
192

    
193
    if "disks" not in iobj:
194
      raise Error("Instance '%s' doesn't have a disks entry?!" % instance)
195
    disks = iobj["disks"]
196
    for idx, dobj in enumerate(disks):
197
      expected = "disk/%s" % idx
198
      current = dobj.get("iv_name", "")
199
      if current != expected:
200
        logging.warning("Updating iv_name for instance %s/disk %s"
201
                        " from '%s' to '%s'",
202
                        instance, idx, current, expected)
203
        dobj["iv_name"] = expected
204
      if not "spindles" in dobj:
205
        missing_spindles = True
206

    
207
  if GetExclusiveStorageValue(config_data) and missing_spindles:
208
    # We cannot be sure that the instances that are missing spindles have
209
    # exclusive storage enabled (the check would be more complicated), so we
210
    # give a noncommittal message
211
    logging.warning("Some instance disks could be needing to update the"
212
                    " spindles parameter; you can check by running"
213
                    " 'gnt-cluster verify', and fix any problem with"
214
                    " 'gnt-cluster repair-disk-sizes'")
215

    
216

    
217
def UpgradeRapiUsers():
218
  if (os.path.isfile(options.RAPI_USERS_FILE_PRE24) and
219
      not os.path.islink(options.RAPI_USERS_FILE_PRE24)):
220
    if os.path.exists(options.RAPI_USERS_FILE):
221
      raise Error("Found pre-2.4 RAPI users file at %s, but another file"
222
                  " already exists at %s" %
223
                  (options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE))
224
    logging.info("Found pre-2.4 RAPI users file at %s, renaming to %s",
225
                 options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
226
    if not options.dry_run:
227
      utils.RenameFile(options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE,
228
                       mkdir=True, mkdir_mode=0750)
229

    
230
  # Create a symlink for RAPI users file
231
  if (not (os.path.islink(options.RAPI_USERS_FILE_PRE24) or
232
           os.path.isfile(options.RAPI_USERS_FILE_PRE24)) and
233
      os.path.isfile(options.RAPI_USERS_FILE)):
234
    logging.info("Creating symlink from %s to %s",
235
                 options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
236
    if not options.dry_run:
237
      os.symlink(options.RAPI_USERS_FILE, options.RAPI_USERS_FILE_PRE24)
238

    
239

    
240
def UpgradeWatcher():
241
  # Remove old watcher state file if it exists
242
  if os.path.exists(options.WATCHER_STATEFILE):
243
    logging.info("Removing watcher state file %s", options.WATCHER_STATEFILE)
244
    if not options.dry_run:
245
      utils.RemoveFile(options.WATCHER_STATEFILE)
246

    
247

    
248
def UpgradeFileStoragePaths(config_data):
249
  # Write file storage paths
250
  if not os.path.exists(options.FILE_STORAGE_PATHS_FILE):
251
    cluster = config_data["cluster"]
252
    file_storage_dir = cluster.get("file_storage_dir")
253
    shared_file_storage_dir = cluster.get("shared_file_storage_dir")
254
    del cluster
255

    
256
    logging.info("Ganeti 2.7 and later only allow whitelisted directories"
257
                 " for file storage; writing existing configuration values"
258
                 " into '%s'",
259
                 options.FILE_STORAGE_PATHS_FILE)
260

    
261
    if file_storage_dir:
262
      logging.info("File storage directory: %s", file_storage_dir)
263
    if shared_file_storage_dir:
264
      logging.info("Shared file storage directory: %s",
265
                   shared_file_storage_dir)
266

    
267
    buf = StringIO()
268
    buf.write("# List automatically generated from configuration by\n")
269
    buf.write("# cfgupgrade at %s\n" % time.asctime())
270
    if file_storage_dir:
271
      buf.write("%s\n" % file_storage_dir)
272
    if shared_file_storage_dir:
273
      buf.write("%s\n" % shared_file_storage_dir)
274
    utils.WriteFile(file_name=options.FILE_STORAGE_PATHS_FILE,
275
                    data=buf.getvalue(),
276
                    mode=0600,
277
                    dry_run=options.dry_run,
278
                    backup=True)
279

    
280

    
281
def GetNewNodeIndex(nodes_by_old_key, old_key, new_key_field):
282
  if old_key not in nodes_by_old_key:
283
    logging.warning("Can't find node '%s' in configuration, assuming that it's"
284
                    " already up-to-date", old_key)
285
    return old_key
286
  return nodes_by_old_key[old_key][new_key_field]
287

    
288

    
289
def ChangeNodeIndices(config_data, old_key_field, new_key_field):
290
  def ChangeDiskNodeIndices(disk):
291
    if disk["dev_type"] in constants.LDS_DRBD:
292
      for i in range(0, 2):
293
        disk["logical_id"][i] = GetNewNodeIndex(nodes_by_old_key,
294
                                                disk["logical_id"][i],
295
                                                new_key_field)
296
    if "children" in disk:
297
      for child in disk["children"]:
298
        ChangeDiskNodeIndices(child)
299

    
300
  nodes_by_old_key = {}
301
  nodes_by_new_key = {}
302
  for (_, node) in config_data["nodes"].items():
303
    nodes_by_old_key[node[old_key_field]] = node
304
    nodes_by_new_key[node[new_key_field]] = node
305

    
306
  config_data["nodes"] = nodes_by_new_key
307

    
308
  cluster = config_data["cluster"]
309
  cluster["master_node"] = GetNewNodeIndex(nodes_by_old_key,
310
                                           cluster["master_node"],
311
                                           new_key_field)
312

    
313
  for inst in config_data["instances"].values():
314
    inst["primary_node"] = GetNewNodeIndex(nodes_by_old_key,
315
                                           inst["primary_node"],
316
                                           new_key_field)
317
    for disk in inst["disks"]:
318
      ChangeDiskNodeIndices(disk)
319

    
320

    
321
def ChangeInstanceIndices(config_data, old_key_field, new_key_field):
322
  insts_by_old_key = {}
323
  insts_by_new_key = {}
324
  for (_, inst) in config_data["instances"].items():
325
    insts_by_old_key[inst[old_key_field]] = inst
326
    insts_by_new_key[inst[new_key_field]] = inst
327

    
328
  config_data["instances"] = insts_by_new_key
329

    
330

    
331
def UpgradeNodeIndices(config_data):
332
  ChangeNodeIndices(config_data, "name", "uuid")
333

    
334

    
335
def UpgradeInstanceIndices(config_data):
336
  ChangeInstanceIndices(config_data, "name", "uuid")
337

    
338

    
339
def UpgradeAll(config_data):
340
  config_data["version"] = constants.BuildVersion(TARGET_MAJOR,
341
                                                  TARGET_MINOR, 0)
342
  UpgradeRapiUsers()
343
  UpgradeWatcher()
344
  UpgradeFileStoragePaths(config_data)
345
  UpgradeNetworks(config_data)
346
  UpgradeCluster(config_data)
347
  UpgradeGroups(config_data)
348
  UpgradeInstances(config_data)
349
  UpgradeNodeIndices(config_data)
350
  UpgradeInstanceIndices(config_data)
351

    
352

    
353
def DowngradeDisks(disks, owner):
354
  for disk in disks:
355
    # Remove spindles to downgrade to 2.8
356
    if "spindles" in disk:
357
      logging.warning("Removing spindles (value=%s) from disk %s (%s) of"
358
                      " instance %s",
359
                      disk["spindles"], disk["iv_name"], disk["uuid"], owner)
360
      del disk["spindles"]
361

    
362

    
363
def DowngradeInstances(config_data):
364
  if "instances" not in config_data:
365
    raise Error("Cannot find the 'instances' key in the configuration!")
366
  for (iname, iobj) in config_data["instances"].items():
367
    if "disks" not in iobj:
368
      raise Error("Cannot find 'disks' key for instance %s" % iname)
369
    DowngradeDisks(iobj["disks"], iname)
370

    
371

    
372
def DowngradeNodeIndices(config_data):
373
  ChangeNodeIndices(config_data, "uuid", "name")
374

    
375

    
376
def DowngradeInstanceIndices(config_data):
377
  ChangeInstanceIndices(config_data, "uuid", "name")
378

    
379

    
380
def DowngradeAll(config_data):
381
  # Any code specific to a particular version should be labeled that way, so
382
  # it can be removed when updating to the next version.
383
  config_data["version"] = constants.BuildVersion(DOWNGRADE_MAJOR,
384
                                                  DOWNGRADE_MINOR, 0)
385
  DowngradeInstances(config_data)
386
  DowngradeNodeIndices(config_data)
387
  DowngradeInstanceIndices(config_data)
388

    
389

    
390
def main():
391
  """Main program.
392

    
393
  """
394
  global options, args # pylint: disable=W0603
395

    
396
  # Option parsing
397
  parser = optparse.OptionParser(usage="%prog [--debug|--verbose] [--force]")
398
  parser.add_option("--dry-run", dest="dry_run",
399
                    action="store_true",
400
                    help="Try to do the conversion, but don't write"
401
                         " output file")
402
  parser.add_option(cli.FORCE_OPT)
403
  parser.add_option(cli.DEBUG_OPT)
404
  parser.add_option(cli.VERBOSE_OPT)
405
  parser.add_option("--ignore-hostname", dest="ignore_hostname",
406
                    action="store_true", default=False,
407
                    help="Don't abort if hostname doesn't match")
408
  parser.add_option("--path", help="Convert configuration in this"
409
                    " directory instead of '%s'" % pathutils.DATA_DIR,
410
                    default=pathutils.DATA_DIR, dest="data_dir")
411
  parser.add_option("--confdir",
412
                    help=("Use this directory instead of '%s'" %
413
                          pathutils.CONF_DIR),
414
                    default=pathutils.CONF_DIR, dest="conf_dir")
415
  parser.add_option("--no-verify",
416
                    help="Do not verify configuration after upgrade",
417
                    action="store_true", dest="no_verify", default=False)
418
  parser.add_option("--downgrade",
419
                    help="Downgrade to the previous stable version",
420
                    action="store_true", dest="downgrade", default=False)
421
  (options, args) = parser.parse_args()
422

    
423
  # We need to keep filenames locally because they might be renamed between
424
  # versions.
425
  options.data_dir = os.path.abspath(options.data_dir)
426
  options.CONFIG_DATA_PATH = options.data_dir + "/config.data"
427
  options.SERVER_PEM_PATH = options.data_dir + "/server.pem"
428
  options.KNOWN_HOSTS_PATH = options.data_dir + "/known_hosts"
429
  options.RAPI_CERT_FILE = options.data_dir + "/rapi.pem"
430
  options.SPICE_CERT_FILE = options.data_dir + "/spice.pem"
431
  options.SPICE_CACERT_FILE = options.data_dir + "/spice-ca.pem"
432
  options.RAPI_USERS_FILE = options.data_dir + "/rapi/users"
433
  options.RAPI_USERS_FILE_PRE24 = options.data_dir + "/rapi_users"
434
  options.CONFD_HMAC_KEY = options.data_dir + "/hmac.key"
435
  options.CDS_FILE = options.data_dir + "/cluster-domain-secret"
436
  options.SSCONF_MASTER_NODE = options.data_dir + "/ssconf_master_node"
437
  options.WATCHER_STATEFILE = options.data_dir + "/watcher.data"
438
  options.FILE_STORAGE_PATHS_FILE = options.conf_dir + "/file-storage-paths"
439

    
440
  SetupLogging()
441

    
442
  # Option checking
443
  if args:
444
    raise Error("No arguments expected")
445
  if options.downgrade and not options.no_verify:
446
    options.no_verify = True
447

    
448
  # Check master name
449
  if not (CheckHostname(options.SSCONF_MASTER_NODE) or options.ignore_hostname):
450
    logging.error("Aborting due to hostname mismatch")
451
    sys.exit(constants.EXIT_FAILURE)
452

    
453
  if not options.force:
454
    if options.downgrade:
455
      usertext = ("The configuration is going to be DOWNGRADED to version %s.%s"
456
                  " Some configuration data might be removed if they don't fit"
457
                  " in the old format. Please make sure you have read the"
458
                  " upgrade notes (available in the UPGRADE file and included"
459
                  " in other documentation formats) to understand what they"
460
                  " are. Continue with *DOWNGRADING* the configuration?" %
461
                  (DOWNGRADE_MAJOR, DOWNGRADE_MINOR))
462
    else:
463
      usertext = ("Please make sure you have read the upgrade notes for"
464
                  " Ganeti %s (available in the UPGRADE file and included"
465
                  " in other documentation formats). Continue with upgrading"
466
                  " configuration?" % constants.RELEASE_VERSION)
467
    if not cli.AskUser(usertext):
468
      sys.exit(constants.EXIT_FAILURE)
469

    
470
  # Check whether it's a Ganeti configuration directory
471
  if not (os.path.isfile(options.CONFIG_DATA_PATH) and
472
          os.path.isfile(options.SERVER_PEM_PATH) and
473
          os.path.isfile(options.KNOWN_HOSTS_PATH)):
474
    raise Error(("%s does not seem to be a Ganeti configuration"
475
                 " directory") % options.data_dir)
476

    
477
  if not os.path.isdir(options.conf_dir):
478
    raise Error("Not a directory: %s" % options.conf_dir)
479

    
480
  config_data = serializer.LoadJson(utils.ReadFile(options.CONFIG_DATA_PATH))
481

    
482
  try:
483
    config_version = config_data["version"]
484
  except KeyError:
485
    raise Error("Unable to determine configuration version")
486

    
487
  (config_major, config_minor, config_revision) = \
488
    constants.SplitVersion(config_version)
489

    
490
  logging.info("Found configuration version %s (%d.%d.%d)",
491
               config_version, config_major, config_minor, config_revision)
492

    
493
  if "config_version" in config_data["cluster"]:
494
    raise Error("Inconsistent configuration: found config_version in"
495
                " configuration file")
496

    
497
  # Downgrade to the previous stable version
498
  if options.downgrade:
499
    if not ((config_major == TARGET_MAJOR and config_minor == TARGET_MINOR) or
500
            (config_major == DOWNGRADE_MAJOR and
501
             config_minor == DOWNGRADE_MINOR)):
502
      raise Error("Downgrade supported only from the latest version (%s.%s),"
503
                  " found %s (%s.%s.%s) instead" %
504
                  (TARGET_MAJOR, TARGET_MINOR, config_version, config_major,
505
                   config_minor, config_revision))
506
    DowngradeAll(config_data)
507

    
508
  # Upgrade from 2.{0..7} to 2.9
509
  elif config_major == 2 and config_minor in range(0, 10):
510
    if config_revision != 0:
511
      logging.warning("Config revision is %s, not 0", config_revision)
512
    UpgradeAll(config_data)
513

    
514
  elif config_major == TARGET_MAJOR and config_minor == TARGET_MINOR:
515
    logging.info("No changes necessary")
516

    
517
  else:
518
    raise Error("Configuration version %d.%d.%d not supported by this tool" %
519
                (config_major, config_minor, config_revision))
520

    
521
  try:
522
    logging.info("Writing configuration file to %s", options.CONFIG_DATA_PATH)
523
    utils.WriteFile(file_name=options.CONFIG_DATA_PATH,
524
                    data=serializer.DumpJson(config_data),
525
                    mode=0600,
526
                    dry_run=options.dry_run,
527
                    backup=True)
528

    
529
    if not options.dry_run:
530
      bootstrap.GenerateClusterCrypto(
531
        False, False, False, False, False,
532
        nodecert_file=options.SERVER_PEM_PATH,
533
        rapicert_file=options.RAPI_CERT_FILE,
534
        spicecert_file=options.SPICE_CERT_FILE,
535
        spicecacert_file=options.SPICE_CACERT_FILE,
536
        hmackey_file=options.CONFD_HMAC_KEY,
537
        cds_file=options.CDS_FILE)
538

    
539
  except Exception:
540
    logging.critical("Writing configuration failed. It is probably in an"
541
                     " inconsistent state and needs manual intervention.")
542
    raise
543

    
544
  # test loading the config file
545
  all_ok = True
546
  if not (options.dry_run or options.no_verify):
547
    logging.info("Testing the new config file...")
548
    cfg = config.ConfigWriter(cfg_file=options.CONFIG_DATA_PATH,
549
                              accept_foreign=options.ignore_hostname,
550
                              offline=True)
551
    # if we reached this, it's all fine
552
    vrfy = cfg.VerifyConfig()
553
    if vrfy:
554
      logging.error("Errors after conversion:")
555
      for item in vrfy:
556
        logging.error(" - %s", item)
557
      all_ok = False
558
    else:
559
      logging.info("File loaded successfully after upgrading")
560
    del cfg
561

    
562
  if options.downgrade:
563
    action = "downgraded"
564
    out_ver = "%s.%s" % (DOWNGRADE_MAJOR, DOWNGRADE_MINOR)
565
  else:
566
    action = "upgraded"
567
    out_ver = constants.RELEASE_VERSION
568
  if all_ok:
569
    cli.ToStderr("Configuration successfully %s to version %s.",
570
                 action, out_ver)
571
  else:
572
    cli.ToStderr("Configuration %s to version %s, but there are errors."
573
                 "\nPlease review the file.", action, out_ver)
574

    
575

    
576
if __name__ == "__main__":
577
  main()