Statistics
| Branch: | Tag: | Revision:

root / tools / cfgupgrade @ 78237f71

History | View | Annotate | Download (18.3 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 = 8
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 _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 UpgradeInstances(config_data):
159
  network2uuid = dict((n["name"], n["uuid"])
160
                      for n in config_data["networks"].values())
161
  if "instances" not in config_data:
162
    raise Error("Can't find the 'instances' key in the configuration!")
163

    
164
  for instance, iobj in config_data["instances"].items():
165
    for nic in iobj["nics"]:
166
      name = nic.get("network", None)
167
      if name:
168
        uuid = network2uuid.get(name, None)
169
        if uuid:
170
          print("NIC with network name %s found."
171
                " Substituting with uuid %s." % (name, uuid))
172
          nic["network"] = uuid
173

    
174
    if "disks" not in iobj:
175
      raise Error("Instance '%s' doesn't have a disks entry?!" % instance)
176
    disks = iobj["disks"]
177
    for idx, dobj in enumerate(disks):
178
      expected = "disk/%s" % idx
179
      current = dobj.get("iv_name", "")
180
      if current != expected:
181
        logging.warning("Updating iv_name for instance %s/disk %s"
182
                        " from '%s' to '%s'",
183
                        instance, idx, current, expected)
184
        dobj["iv_name"] = expected
185

    
186

    
187
def UpgradeRapiUsers():
188
  if (os.path.isfile(options.RAPI_USERS_FILE_PRE24) and
189
      not os.path.islink(options.RAPI_USERS_FILE_PRE24)):
190
    if os.path.exists(options.RAPI_USERS_FILE):
191
      raise Error("Found pre-2.4 RAPI users file at %s, but another file"
192
                  " already exists at %s" %
193
                  (options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE))
194
    logging.info("Found pre-2.4 RAPI users file at %s, renaming to %s",
195
                 options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
196
    if not options.dry_run:
197
      utils.RenameFile(options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE,
198
                       mkdir=True, mkdir_mode=0750)
199

    
200
  # Create a symlink for RAPI users file
201
  if (not (os.path.islink(options.RAPI_USERS_FILE_PRE24) or
202
           os.path.isfile(options.RAPI_USERS_FILE_PRE24)) and
203
      os.path.isfile(options.RAPI_USERS_FILE)):
204
    logging.info("Creating symlink from %s to %s",
205
                 options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
206
    if not options.dry_run:
207
      os.symlink(options.RAPI_USERS_FILE, options.RAPI_USERS_FILE_PRE24)
208

    
209

    
210
def UpgradeWatcher():
211
  # Remove old watcher state file if it exists
212
  if os.path.exists(options.WATCHER_STATEFILE):
213
    logging.info("Removing watcher state file %s", options.WATCHER_STATEFILE)
214
    if not options.dry_run:
215
      utils.RemoveFile(options.WATCHER_STATEFILE)
216

    
217

    
218
def UpgradeFileStoragePaths(config_data):
219
  # Write file storage paths
220
  if not os.path.exists(options.FILE_STORAGE_PATHS_FILE):
221
    cluster = config_data["cluster"]
222
    file_storage_dir = cluster.get("file_storage_dir")
223
    shared_file_storage_dir = cluster.get("shared_file_storage_dir")
224
    del cluster
225

    
226
    logging.info("Ganeti 2.7 and later only allow whitelisted directories"
227
                 " for file storage; writing existing configuration values"
228
                 " into '%s'",
229
                 options.FILE_STORAGE_PATHS_FILE)
230

    
231
    if file_storage_dir:
232
      logging.info("File storage directory: %s", file_storage_dir)
233
    if shared_file_storage_dir:
234
      logging.info("Shared file storage directory: %s",
235
                   shared_file_storage_dir)
236

    
237
    buf = StringIO()
238
    buf.write("# List automatically generated from configuration by\n")
239
    buf.write("# cfgupgrade at %s\n" % time.asctime())
240
    if file_storage_dir:
241
      buf.write("%s\n" % file_storage_dir)
242
    if shared_file_storage_dir:
243
      buf.write("%s\n" % shared_file_storage_dir)
244
    utils.WriteFile(file_name=options.FILE_STORAGE_PATHS_FILE,
245
                    data=buf.getvalue(),
246
                    mode=0600,
247
                    dry_run=options.dry_run,
248
                    backup=True)
249

    
250

    
251
def UpgradeAll(config_data):
252
  config_data["version"] = constants.BuildVersion(TARGET_MAJOR,
253
                                                  TARGET_MINOR, 0)
254
  UpgradeRapiUsers()
255
  UpgradeWatcher()
256
  UpgradeFileStoragePaths(config_data)
257
  UpgradeNetworks(config_data)
258
  UpgradeCluster(config_data)
259
  UpgradeGroups(config_data)
260
  UpgradeInstances(config_data)
261

    
262

    
263
def DowngradeIPolicy(ipolicy, owner):
264
  # Downgrade IPolicy to 2.7 (stable)
265
  minmax_keys = ["min", "max"]
266
  specs_is_split = any((k in ipolicy) for k in minmax_keys)
267
  if not specs_is_split:
268
    if "minmax" in ipolicy:
269
      if type(ipolicy["minmax"]) is not list:
270
        raise Error("Invalid minmax type in %s ipolicy: %s" %
271
                    (owner, type(ipolicy["minmax"])))
272
      if len(ipolicy["minmax"]) > 1:
273
        logging.warning("Discarding some limit specs values from %s policy",
274
                        owner)
275
      minmax = ipolicy["minmax"][0]
276
      del ipolicy["minmax"]
277
    else:
278
      minmax = {}
279
    for key in minmax_keys:
280
      spec = minmax.get(key, {})
281
      ipolicy[key] = spec
282
    if "std" not in ipolicy:
283
      ipolicy["std"] = {}
284

    
285

    
286
def DowngradeGroups(config_data):
287
  for group in config_data["nodegroups"].values():
288
    ipolicy = group.get("ipolicy", None)
289
    if ipolicy is not None:
290
      DowngradeIPolicy(ipolicy, "group \"%s\"" % group.get("name"))
291

    
292

    
293
def DowngradeEnabledTemplates(cluster):
294
  # Remove enabled disk templates to downgrade to 2.7
295
  edt_key = "enabled_disk_templates"
296
  if edt_key in cluster:
297
    logging.warning("Removing cluster's enabled disk templates; value = %s",
298
                    utils.CommaJoin(cluster[edt_key]))
299
    del cluster[edt_key]
300

    
301

    
302
def DowngradeCluster(config_data):
303
  cluster = config_data.get("cluster", None)
304
  if cluster is None:
305
    raise Error("Cannot find cluster")
306
  DowngradeEnabledTemplates(cluster)
307
  ipolicy = cluster.get("ipolicy", None)
308
  if ipolicy:
309
    DowngradeIPolicy(ipolicy, "cluster")
310
  if "dsahostkeypub" in cluster:
311
    del cluster["dsahostkeypub"]
312

    
313

    
314
def DowngradeDisk(disk):
315
  if "uuid" in disk:
316
    del disk["uuid"]
317
  if "children" in disk:
318
    for child_disk in disk["children"]:
319
      DowngradeDisk(child_disk)
320

    
321

    
322
def DowngradeInstances(config_data):
323
  if "instances" not in config_data:
324
    raise Error("Can't find the 'instances' key in the configuration!")
325

    
326
  for _, iobj in config_data["instances"].items():
327
    if "disks_active" in iobj:
328
      del iobj["disks_active"]
329

    
330
    # Remove the NICs UUIDs
331
    for nic in iobj["nics"]:
332
      if "uuid" in nic:
333
        del nic["uuid"]
334

    
335
    # Downgrade the disks
336
    for disk in iobj["disks"]:
337
      DowngradeDisk(disk)
338

    
339

    
340
def DowngradeAll(config_data):
341
  # Any code specific to a particular version should be labeled that way, so
342
  # it can be removed when updating to the next version.
343
  config_data["version"] = constants.BuildVersion(DOWNGRADE_MAJOR,
344
                                                  DOWNGRADE_MINOR, 0)
345
  DowngradeCluster(config_data)
346
  DowngradeGroups(config_data)
347
  DowngradeInstances(config_data)
348

    
349

    
350
def main():
351
  """Main program.
352

    
353
  """
354
  global options, args # pylint: disable=W0603
355

    
356
  # Option parsing
357
  parser = optparse.OptionParser(usage="%prog [--debug|--verbose] [--force]")
358
  parser.add_option("--dry-run", dest="dry_run",
359
                    action="store_true",
360
                    help="Try to do the conversion, but don't write"
361
                         " output file")
362
  parser.add_option(cli.FORCE_OPT)
363
  parser.add_option(cli.DEBUG_OPT)
364
  parser.add_option(cli.VERBOSE_OPT)
365
  parser.add_option("--ignore-hostname", dest="ignore_hostname",
366
                    action="store_true", default=False,
367
                    help="Don't abort if hostname doesn't match")
368
  parser.add_option("--path", help="Convert configuration in this"
369
                    " directory instead of '%s'" % pathutils.DATA_DIR,
370
                    default=pathutils.DATA_DIR, dest="data_dir")
371
  parser.add_option("--confdir",
372
                    help=("Use this directory instead of '%s'" %
373
                          pathutils.CONF_DIR),
374
                    default=pathutils.CONF_DIR, dest="conf_dir")
375
  parser.add_option("--no-verify",
376
                    help="Do not verify configuration after upgrade",
377
                    action="store_true", dest="no_verify", default=False)
378
  parser.add_option("--downgrade",
379
                    help="Downgrade to the previous stable version",
380
                    action="store_true", dest="downgrade", default=False)
381
  (options, args) = parser.parse_args()
382

    
383
  # We need to keep filenames locally because they might be renamed between
384
  # versions.
385
  options.data_dir = os.path.abspath(options.data_dir)
386
  options.CONFIG_DATA_PATH = options.data_dir + "/config.data"
387
  options.SERVER_PEM_PATH = options.data_dir + "/server.pem"
388
  options.KNOWN_HOSTS_PATH = options.data_dir + "/known_hosts"
389
  options.RAPI_CERT_FILE = options.data_dir + "/rapi.pem"
390
  options.SPICE_CERT_FILE = options.data_dir + "/spice.pem"
391
  options.SPICE_CACERT_FILE = options.data_dir + "/spice-ca.pem"
392
  options.RAPI_USERS_FILE = options.data_dir + "/rapi/users"
393
  options.RAPI_USERS_FILE_PRE24 = options.data_dir + "/rapi_users"
394
  options.CONFD_HMAC_KEY = options.data_dir + "/hmac.key"
395
  options.CDS_FILE = options.data_dir + "/cluster-domain-secret"
396
  options.SSCONF_MASTER_NODE = options.data_dir + "/ssconf_master_node"
397
  options.WATCHER_STATEFILE = options.data_dir + "/watcher.data"
398
  options.FILE_STORAGE_PATHS_FILE = options.conf_dir + "/file-storage-paths"
399

    
400
  SetupLogging()
401

    
402
  # Option checking
403
  if args:
404
    raise Error("No arguments expected")
405
  if options.downgrade and not options.no_verify:
406
    options.no_verify = True
407

    
408
  # Check master name
409
  if not (CheckHostname(options.SSCONF_MASTER_NODE) or options.ignore_hostname):
410
    logging.error("Aborting due to hostname mismatch")
411
    sys.exit(constants.EXIT_FAILURE)
412

    
413
  if not options.force:
414
    if options.downgrade:
415
      usertext = ("The configuration is going to be DOWNGRADED to version %s.%s"
416
                  " Some configuration data might be removed if they don't fit"
417
                  " in the old format. Please make sure you have read the"
418
                  " upgrade notes (available in the UPGRADE file and included"
419
                  " in other documentation formats) to understand what they"
420
                  " are. Continue with *DOWNGRADING* the configuration?" %
421
                  (DOWNGRADE_MAJOR, DOWNGRADE_MINOR))
422
    else:
423
      usertext = ("Please make sure you have read the upgrade notes for"
424
                  " Ganeti %s (available in the UPGRADE file and included"
425
                  " in other documentation formats). Continue with upgrading"
426
                  " configuration?" % constants.RELEASE_VERSION)
427
    if not cli.AskUser(usertext):
428
      sys.exit(constants.EXIT_FAILURE)
429

    
430
  # Check whether it's a Ganeti configuration directory
431
  if not (os.path.isfile(options.CONFIG_DATA_PATH) and
432
          os.path.isfile(options.SERVER_PEM_PATH) and
433
          os.path.isfile(options.KNOWN_HOSTS_PATH)):
434
    raise Error(("%s does not seem to be a Ganeti configuration"
435
                 " directory") % options.data_dir)
436

    
437
  if not os.path.isdir(options.conf_dir):
438
    raise Error("Not a directory: %s" % options.conf_dir)
439

    
440
  config_data = serializer.LoadJson(utils.ReadFile(options.CONFIG_DATA_PATH))
441

    
442
  try:
443
    config_version = config_data["version"]
444
  except KeyError:
445
    raise Error("Unable to determine configuration version")
446

    
447
  (config_major, config_minor, config_revision) = \
448
    constants.SplitVersion(config_version)
449

    
450
  logging.info("Found configuration version %s (%d.%d.%d)",
451
               config_version, config_major, config_minor, config_revision)
452

    
453
  if "config_version" in config_data["cluster"]:
454
    raise Error("Inconsistent configuration: found config_version in"
455
                " configuration file")
456

    
457
  # Downgrade to the previous stable version
458
  if options.downgrade:
459
    if not ((config_major == TARGET_MAJOR and config_minor == TARGET_MINOR) or
460
            (config_major == DOWNGRADE_MAJOR and
461
             config_minor == DOWNGRADE_MINOR)):
462
      raise Error("Downgrade supported only from the latest version (%s.%s),"
463
                  " found %s (%s.%s.%s) instead" %
464
                  (TARGET_MAJOR, TARGET_MINOR, config_version, config_major,
465
                   config_minor, config_revision))
466
    DowngradeAll(config_data)
467

    
468
  # Upgrade from 2.{0..7} to 2.8
469
  elif config_major == 2 and config_minor in range(0, 9):
470
    if config_revision != 0:
471
      logging.warning("Config revision is %s, not 0", config_revision)
472
    UpgradeAll(config_data)
473

    
474
  elif config_major == TARGET_MAJOR and config_minor == TARGET_MINOR:
475
    logging.info("No changes necessary")
476

    
477
  else:
478
    raise Error("Configuration version %d.%d.%d not supported by this tool" %
479
                (config_major, config_minor, config_revision))
480

    
481
  try:
482
    logging.info("Writing configuration file to %s", options.CONFIG_DATA_PATH)
483
    utils.WriteFile(file_name=options.CONFIG_DATA_PATH,
484
                    data=serializer.DumpJson(config_data),
485
                    mode=0600,
486
                    dry_run=options.dry_run,
487
                    backup=True)
488

    
489
    if not options.dry_run:
490
      bootstrap.GenerateClusterCrypto(
491
        False, False, False, False, False,
492
        nodecert_file=options.SERVER_PEM_PATH,
493
        rapicert_file=options.RAPI_CERT_FILE,
494
        spicecert_file=options.SPICE_CERT_FILE,
495
        spicecacert_file=options.SPICE_CACERT_FILE,
496
        hmackey_file=options.CONFD_HMAC_KEY,
497
        cds_file=options.CDS_FILE)
498

    
499
  except Exception:
500
    logging.critical("Writing configuration failed. It is probably in an"
501
                     " inconsistent state and needs manual intervention.")
502
    raise
503

    
504
  # test loading the config file
505
  all_ok = True
506
  if not (options.dry_run or options.no_verify):
507
    logging.info("Testing the new config file...")
508
    cfg = config.ConfigWriter(cfg_file=options.CONFIG_DATA_PATH,
509
                              accept_foreign=options.ignore_hostname,
510
                              offline=True)
511
    # if we reached this, it's all fine
512
    vrfy = cfg.VerifyConfig()
513
    if vrfy:
514
      logging.error("Errors after conversion:")
515
      for item in vrfy:
516
        logging.error(" - %s", item)
517
      all_ok = False
518
    else:
519
      logging.info("File loaded successfully after upgrading")
520
    del cfg
521

    
522
  if options.downgrade:
523
    action = "downgraded"
524
    out_ver = "%s.%s" % (DOWNGRADE_MAJOR, DOWNGRADE_MINOR)
525
  else:
526
    action = "upgraded"
527
    out_ver = constants.RELEASE_VERSION
528
  if all_ok:
529
    cli.ToStderr("Configuration successfully %s to version %s.",
530
                 action, out_ver)
531
  else:
532
    cli.ToStderr("Configuration %s to version %s, but there are errors."
533
                 "\nPlease review the file.", action, out_ver)
534

    
535

    
536
if __name__ == "__main__":
537
  main()