Statistics
| Branch: | Tag: | Revision:

root / tools / cfgupgrade @ 4daa1d57

History | View | Annotate | Download (18.8 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
      try:
174
        del nic["idx"]
175
        print("Deleting deprecated idx")
176
      except KeyError:
177
        pass
178

    
179
    if "disks" not in iobj:
180
      raise Error("Instance '%s' doesn't have a disks entry?!" % instance)
181
    disks = iobj["disks"]
182
    for idx, dobj in enumerate(disks):
183
      expected = "disk/%s" % idx
184
      current = dobj.get("iv_name", "")
185
      if current != expected:
186
        logging.warning("Updating iv_name for instance %s/disk %s"
187
                        " from '%s' to '%s'",
188
                        instance, idx, current, expected)
189
        dobj["iv_name"] = expected
190
      try:
191
        del dobj["idx"]
192
        print("Deleting deprecated idx")
193
      except KeyError:
194
        pass
195

    
196
    for attr in ("dev_idxs", "hotplug_info", "hotplugs", "pci_reservations"):
197
      try:
198
        del iobj[attr]
199
        print("Deleting deprecated %s" % attr)
200
      except KeyError:
201
        pass
202

    
203

    
204
def UpgradeRapiUsers():
205
  if (os.path.isfile(options.RAPI_USERS_FILE_PRE24) and
206
      not os.path.islink(options.RAPI_USERS_FILE_PRE24)):
207
    if os.path.exists(options.RAPI_USERS_FILE):
208
      raise Error("Found pre-2.4 RAPI users file at %s, but another file"
209
                  " already exists at %s" %
210
                  (options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE))
211
    logging.info("Found pre-2.4 RAPI users file at %s, renaming to %s",
212
                 options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
213
    if not options.dry_run:
214
      utils.RenameFile(options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE,
215
                       mkdir=True, mkdir_mode=0750)
216

    
217
  # Create a symlink for RAPI users file
218
  if (not (os.path.islink(options.RAPI_USERS_FILE_PRE24) or
219
           os.path.isfile(options.RAPI_USERS_FILE_PRE24)) and
220
      os.path.isfile(options.RAPI_USERS_FILE)):
221
    logging.info("Creating symlink from %s to %s",
222
                 options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
223
    if not options.dry_run:
224
      os.symlink(options.RAPI_USERS_FILE, options.RAPI_USERS_FILE_PRE24)
225

    
226

    
227
def UpgradeWatcher():
228
  # Remove old watcher state file if it exists
229
  if os.path.exists(options.WATCHER_STATEFILE):
230
    logging.info("Removing watcher state file %s", options.WATCHER_STATEFILE)
231
    if not options.dry_run:
232
      utils.RemoveFile(options.WATCHER_STATEFILE)
233

    
234

    
235
def UpgradeFileStoragePaths(config_data):
236
  # Write file storage paths
237
  if not os.path.exists(options.FILE_STORAGE_PATHS_FILE):
238
    cluster = config_data["cluster"]
239
    file_storage_dir = cluster.get("file_storage_dir")
240
    shared_file_storage_dir = cluster.get("shared_file_storage_dir")
241
    del cluster
242

    
243
    logging.info("Ganeti 2.7 and later only allow whitelisted directories"
244
                 " for file storage; writing existing configuration values"
245
                 " into '%s'",
246
                 options.FILE_STORAGE_PATHS_FILE)
247

    
248
    if file_storage_dir:
249
      logging.info("File storage directory: %s", file_storage_dir)
250
    if shared_file_storage_dir:
251
      logging.info("Shared file storage directory: %s",
252
                   shared_file_storage_dir)
253

    
254
    buf = StringIO()
255
    buf.write("# List automatically generated from configuration by\n")
256
    buf.write("# cfgupgrade at %s\n" % time.asctime())
257
    if file_storage_dir:
258
      buf.write("%s\n" % file_storage_dir)
259
    if shared_file_storage_dir:
260
      buf.write("%s\n" % shared_file_storage_dir)
261
    utils.WriteFile(file_name=options.FILE_STORAGE_PATHS_FILE,
262
                    data=buf.getvalue(),
263
                    mode=0600,
264
                    dry_run=options.dry_run,
265
                    backup=True)
266

    
267

    
268
def UpgradeAll(config_data):
269
  config_data["version"] = constants.BuildVersion(TARGET_MAJOR,
270
                                                  TARGET_MINOR, 0)
271
  UpgradeRapiUsers()
272
  UpgradeWatcher()
273
  UpgradeFileStoragePaths(config_data)
274
  UpgradeNetworks(config_data)
275
  UpgradeCluster(config_data)
276
  UpgradeGroups(config_data)
277
  UpgradeInstances(config_data)
278

    
279

    
280
def DowngradeIPolicy(ipolicy, owner):
281
  # Downgrade IPolicy to 2.7 (stable)
282
  minmax_keys = ["min", "max"]
283
  specs_is_split = any((k in ipolicy) for k in minmax_keys)
284
  if not specs_is_split:
285
    if "minmax" in ipolicy:
286
      if type(ipolicy["minmax"]) is not list:
287
        raise Error("Invalid minmax type in %s ipolicy: %s" %
288
                    (owner, type(ipolicy["minmax"])))
289
      if len(ipolicy["minmax"]) > 1:
290
        logging.warning("Discarding some limit specs values from %s policy",
291
                        owner)
292
      minmax = ipolicy["minmax"][0]
293
      del ipolicy["minmax"]
294
    else:
295
      minmax = {}
296
    for key in minmax_keys:
297
      spec = minmax.get(key, {})
298
      ipolicy[key] = spec
299
    if "std" not in ipolicy:
300
      ipolicy["std"] = {}
301

    
302

    
303
def DowngradeGroups(config_data):
304
  for group in config_data["nodegroups"].values():
305
    ipolicy = group.get("ipolicy", None)
306
    if ipolicy is not None:
307
      DowngradeIPolicy(ipolicy, "group \"%s\"" % group.get("name"))
308

    
309

    
310
def DowngradeEnabledTemplates(cluster):
311
  # Remove enabled disk templates to downgrade to 2.7
312
  edt_key = "enabled_disk_templates"
313
  if edt_key in cluster:
314
    logging.warning("Removing cluster's enabled disk templates; value = %s",
315
                    utils.CommaJoin(cluster[edt_key]))
316
    del cluster[edt_key]
317

    
318

    
319
def DowngradeCluster(config_data):
320
  cluster = config_data.get("cluster", None)
321
  if cluster is None:
322
    raise Error("Cannot find cluster")
323
  DowngradeEnabledTemplates(cluster)
324
  ipolicy = cluster.get("ipolicy", None)
325
  if ipolicy:
326
    DowngradeIPolicy(ipolicy, "cluster")
327
  if "dsahostkeypub" in cluster:
328
    del cluster["dsahostkeypub"]
329

    
330

    
331
def DowngradeDisk(disk):
332
  if "uuid" in disk:
333
    del disk["uuid"]
334
  if "children" in disk:
335
    for child_disk in disk["children"]:
336
      DowngradeDisk(child_disk)
337

    
338

    
339
def DowngradeInstances(config_data):
340
  if "instances" not in config_data:
341
    raise Error("Can't find the 'instances' key in the configuration!")
342

    
343
  for _, iobj in config_data["instances"].items():
344
    if "disks_active" in iobj:
345
      del iobj["disks_active"]
346

    
347
    # Remove the NICs UUIDs
348
    for nic in iobj["nics"]:
349
      if "uuid" in nic:
350
        del nic["uuid"]
351

    
352
    # Downgrade the disks
353
    for disk in iobj["disks"]:
354
      DowngradeDisk(disk)
355

    
356

    
357
def DowngradeAll(config_data):
358
  # Any code specific to a particular version should be labeled that way, so
359
  # it can be removed when updating to the next version.
360
  config_data["version"] = constants.BuildVersion(DOWNGRADE_MAJOR,
361
                                                  DOWNGRADE_MINOR, 0)
362
  DowngradeCluster(config_data)
363
  DowngradeGroups(config_data)
364
  DowngradeInstances(config_data)
365

    
366

    
367
def main():
368
  """Main program.
369

    
370
  """
371
  global options, args # pylint: disable=W0603
372

    
373
  # Option parsing
374
  parser = optparse.OptionParser(usage="%prog [--debug|--verbose] [--force]")
375
  parser.add_option("--dry-run", dest="dry_run",
376
                    action="store_true",
377
                    help="Try to do the conversion, but don't write"
378
                         " output file")
379
  parser.add_option(cli.FORCE_OPT)
380
  parser.add_option(cli.DEBUG_OPT)
381
  parser.add_option(cli.VERBOSE_OPT)
382
  parser.add_option("--ignore-hostname", dest="ignore_hostname",
383
                    action="store_true", default=False,
384
                    help="Don't abort if hostname doesn't match")
385
  parser.add_option("--path", help="Convert configuration in this"
386
                    " directory instead of '%s'" % pathutils.DATA_DIR,
387
                    default=pathutils.DATA_DIR, dest="data_dir")
388
  parser.add_option("--confdir",
389
                    help=("Use this directory instead of '%s'" %
390
                          pathutils.CONF_DIR),
391
                    default=pathutils.CONF_DIR, dest="conf_dir")
392
  parser.add_option("--no-verify",
393
                    help="Do not verify configuration after upgrade",
394
                    action="store_true", dest="no_verify", default=False)
395
  parser.add_option("--downgrade",
396
                    help="Downgrade to the previous stable version",
397
                    action="store_true", dest="downgrade", default=False)
398
  (options, args) = parser.parse_args()
399

    
400
  # We need to keep filenames locally because they might be renamed between
401
  # versions.
402
  options.data_dir = os.path.abspath(options.data_dir)
403
  options.CONFIG_DATA_PATH = options.data_dir + "/config.data"
404
  options.SERVER_PEM_PATH = options.data_dir + "/server.pem"
405
  options.KNOWN_HOSTS_PATH = options.data_dir + "/known_hosts"
406
  options.RAPI_CERT_FILE = options.data_dir + "/rapi.pem"
407
  options.SPICE_CERT_FILE = options.data_dir + "/spice.pem"
408
  options.SPICE_CACERT_FILE = options.data_dir + "/spice-ca.pem"
409
  options.RAPI_USERS_FILE = options.data_dir + "/rapi/users"
410
  options.RAPI_USERS_FILE_PRE24 = options.data_dir + "/rapi_users"
411
  options.CONFD_HMAC_KEY = options.data_dir + "/hmac.key"
412
  options.CDS_FILE = options.data_dir + "/cluster-domain-secret"
413
  options.SSCONF_MASTER_NODE = options.data_dir + "/ssconf_master_node"
414
  options.WATCHER_STATEFILE = options.data_dir + "/watcher.data"
415
  options.FILE_STORAGE_PATHS_FILE = options.conf_dir + "/file-storage-paths"
416

    
417
  SetupLogging()
418

    
419
  # Option checking
420
  if args:
421
    raise Error("No arguments expected")
422
  if options.downgrade and not options.no_verify:
423
    options.no_verify = True
424

    
425
  # Check master name
426
  if not (CheckHostname(options.SSCONF_MASTER_NODE) or options.ignore_hostname):
427
    logging.error("Aborting due to hostname mismatch")
428
    sys.exit(constants.EXIT_FAILURE)
429

    
430
  if not options.force:
431
    if options.downgrade:
432
      usertext = ("The configuration is going to be DOWNGRADED to version %s.%s"
433
                  " Some configuration data might be removed if they don't fit"
434
                  " in the old format. Please make sure you have read the"
435
                  " upgrade notes (available in the UPGRADE file and included"
436
                  " in other documentation formats) to understand what they"
437
                  " are. Continue with *DOWNGRADING* the configuration?" %
438
                  (DOWNGRADE_MAJOR, DOWNGRADE_MINOR))
439
    else:
440
      usertext = ("Please make sure you have read the upgrade notes for"
441
                  " Ganeti %s (available in the UPGRADE file and included"
442
                  " in other documentation formats). Continue with upgrading"
443
                  " configuration?" % constants.RELEASE_VERSION)
444
    if not cli.AskUser(usertext):
445
      sys.exit(constants.EXIT_FAILURE)
446

    
447
  # Check whether it's a Ganeti configuration directory
448
  if not (os.path.isfile(options.CONFIG_DATA_PATH) and
449
          os.path.isfile(options.SERVER_PEM_PATH) and
450
          os.path.isfile(options.KNOWN_HOSTS_PATH)):
451
    raise Error(("%s does not seem to be a Ganeti configuration"
452
                 " directory") % options.data_dir)
453

    
454
  if not os.path.isdir(options.conf_dir):
455
    raise Error("Not a directory: %s" % options.conf_dir)
456

    
457
  config_data = serializer.LoadJson(utils.ReadFile(options.CONFIG_DATA_PATH))
458

    
459
  try:
460
    config_version = config_data["version"]
461
  except KeyError:
462
    raise Error("Unable to determine configuration version")
463

    
464
  (config_major, config_minor, config_revision) = \
465
    constants.SplitVersion(config_version)
466

    
467
  logging.info("Found configuration version %s (%d.%d.%d)",
468
               config_version, config_major, config_minor, config_revision)
469

    
470
  if "config_version" in config_data["cluster"]:
471
    raise Error("Inconsistent configuration: found config_version in"
472
                " configuration file")
473

    
474
  # Downgrade to the previous stable version
475
  if options.downgrade:
476
    if not ((config_major == TARGET_MAJOR and config_minor == TARGET_MINOR) or
477
            (config_major == DOWNGRADE_MAJOR and
478
             config_minor == DOWNGRADE_MINOR)):
479
      raise Error("Downgrade supported only from the latest version (%s.%s),"
480
                  " found %s (%s.%s.%s) instead" %
481
                  (TARGET_MAJOR, TARGET_MINOR, config_version, config_major,
482
                   config_minor, config_revision))
483
    DowngradeAll(config_data)
484

    
485
  # Upgrade from 2.{0..7} to 2.8
486
  elif config_major == 2 and config_minor in range(0, 9):
487
    if config_revision != 0:
488
      logging.warning("Config revision is %s, not 0", config_revision)
489
    UpgradeAll(config_data)
490

    
491
  elif config_major == TARGET_MAJOR and config_minor == TARGET_MINOR:
492
    logging.info("No changes necessary")
493

    
494
  else:
495
    raise Error("Configuration version %d.%d.%d not supported by this tool" %
496
                (config_major, config_minor, config_revision))
497

    
498
  try:
499
    logging.info("Writing configuration file to %s", options.CONFIG_DATA_PATH)
500
    utils.WriteFile(file_name=options.CONFIG_DATA_PATH,
501
                    data=serializer.DumpJson(config_data),
502
                    mode=0600,
503
                    dry_run=options.dry_run,
504
                    backup=True)
505

    
506
    if not options.dry_run:
507
      bootstrap.GenerateClusterCrypto(
508
        False, False, False, False, False,
509
        nodecert_file=options.SERVER_PEM_PATH,
510
        rapicert_file=options.RAPI_CERT_FILE,
511
        spicecert_file=options.SPICE_CERT_FILE,
512
        spicecacert_file=options.SPICE_CACERT_FILE,
513
        hmackey_file=options.CONFD_HMAC_KEY,
514
        cds_file=options.CDS_FILE)
515

    
516
  except Exception:
517
    logging.critical("Writing configuration failed. It is probably in an"
518
                     " inconsistent state and needs manual intervention.")
519
    raise
520

    
521
  # test loading the config file
522
  all_ok = True
523
  if not (options.dry_run or options.no_verify):
524
    logging.info("Testing the new config file...")
525
    cfg = config.ConfigWriter(cfg_file=options.CONFIG_DATA_PATH,
526
                              accept_foreign=options.ignore_hostname,
527
                              offline=True)
528
    # if we reached this, it's all fine
529
    vrfy = cfg.VerifyConfig()
530
    if vrfy:
531
      logging.error("Errors after conversion:")
532
      for item in vrfy:
533
        logging.error(" - %s", item)
534
      all_ok = False
535
    else:
536
      logging.info("File loaded successfully after upgrading")
537
    del cfg
538

    
539
  if options.downgrade:
540
    action = "downgraded"
541
    out_ver = "%s.%s" % (DOWNGRADE_MAJOR, DOWNGRADE_MINOR)
542
  else:
543
    action = "upgraded"
544
    out_ver = constants.RELEASE_VERSION
545
  if all_ok:
546
    cli.ToStderr("Configuration successfully %s to version %s.",
547
                 action, out_ver)
548
  else:
549
    cli.ToStderr("Configuration %s to version %s, but there are errors."
550
                 "\nPlease review the file.", action, out_ver)
551

    
552

    
553
if __name__ == "__main__":
554
  main()