Statistics
| Branch: | Tag: | Revision:

root / tools / cfgupgrade @ f14863bc

History | View | Annotate | Download (19.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
from bitarray import bitarray
38
from base64 import b64encode, b64decode
39

    
40
from ganeti import constants
41
from ganeti import serializer
42
from ganeti import utils
43
from ganeti import cli
44
from ganeti import bootstrap
45
from ganeti import config
46
from ganeti import netutils
47
from ganeti import pathutils
48

    
49

    
50
options = None
51
args = None
52

    
53

    
54
#: Target major version we will upgrade to
55
TARGET_MAJOR = 2
56
#: Target minor version we will upgrade to
57
TARGET_MINOR = 8
58
#: Target major version for downgrade
59
DOWNGRADE_MAJOR = 2
60
#: Target minor version for downgrade
61
DOWNGRADE_MINOR = 7
62

    
63

    
64
class Error(Exception):
65
  """Generic exception"""
66
  pass
67

    
68

    
69
def SetupLogging():
70
  """Configures the logging module.
71

    
72
  """
73
  formatter = logging.Formatter("%(asctime)s: %(message)s")
74

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

    
84
  root_logger = logging.getLogger("")
85
  root_logger.setLevel(logging.NOTSET)
86
  root_logger.addHandler(stderr_handler)
87

    
88

    
89
def CheckHostname(path):
90
  """Ensures hostname matches ssconf value.
91

    
92
  @param path: Path to ssconf file
93

    
94
  """
95
  ssconf_master_node = utils.ReadOneLineFile(path)
96
  hostname = netutils.GetHostname().name
97

    
98
  if ssconf_master_node == hostname:
99
    return True
100

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

    
106

    
107
def _FillIPolicySpecs(default_ipolicy, ipolicy):
108
  if "minmax" in ipolicy:
109
    for (key, spec) in ipolicy["minmax"][0].items():
110
      for (par, val) in default_ipolicy["minmax"][0][key].items():
111
        if par not in spec:
112
          spec[par] = val
113

    
114

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

    
130

    
131
# pylint: disable=E1101
132
def UpgradeNetworks(config_data):
133
  networks = config_data.get("networks", {})
134
  if not networks:
135
    config_data["networks"] = {}
136
  for nobj in networks.values():
137
    for key in ("reservations", "ext_reservations"):
138
      r = nobj[key]
139
      if options.tob64:
140
        try:
141
          b = bitarray(r)
142
          nobj[key] = b64encode(b.tobytes())
143
        except ValueError:
144
          print("No 01 network found! Probably already in base64.")
145
      if options.to01:
146
        try:
147
          b = bitarray(r)
148
          print("01 network found! Do nothing.")
149
        except ValueError:
150
          b = bitarray()
151
          b.frombytes(b64decode(r))
152
          nobj[key] = b.to01()
153
      print("%s: %s -> %s" % (nobj["name"], r, nobj[key]))
154

    
155

    
156
def UpgradeCluster(config_data):
157
  cluster = config_data.get("cluster", None)
158
  if cluster is None:
159
    raise Error("Cannot find cluster")
160
  ipolicy = cluster.setdefault("ipolicy", None)
161
  if ipolicy:
162
    UpgradeIPolicy(ipolicy, constants.IPOLICY_DEFAULTS, False)
163

    
164

    
165
def UpgradeGroups(config_data):
166
  cl_ipolicy = config_data["cluster"].get("ipolicy")
167
  for group in config_data["nodegroups"].values():
168
    networks = group.get("networks", None)
169
    if not networks:
170
      group["networks"] = {}
171
    ipolicy = group.get("ipolicy", None)
172
    if ipolicy:
173
      if cl_ipolicy is None:
174
        raise Error("A group defines an instance policy but there is no"
175
                    " instance policy at cluster level")
176
      UpgradeIPolicy(ipolicy, cl_ipolicy, True)
177

    
178

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

    
185
  for instance, iobj in config_data["instances"].items():
186
    for nic in iobj["nics"]:
187
      name = nic.get("network", None)
188
      if name:
189
        uuid = network2uuid.get(name, None)
190
        if uuid:
191
          print("NIC with network name %s found."
192
                " Substituting with uuid %s." % (name, uuid))
193
          nic["network"] = uuid
194
      try:
195
        del nic["idx"]
196
        print("Deleting deprecated idx")
197
      except KeyError:
198
        pass
199

    
200
    if "disks" not in iobj:
201
      raise Error("Instance '%s' doesn't have a disks entry?!" % instance)
202
    disks = iobj["disks"]
203
    for idx, dobj in enumerate(disks):
204
      expected = "disk/%s" % idx
205
      current = dobj.get("iv_name", "")
206
      if current != expected:
207
        logging.warning("Updating iv_name for instance %s/disk %s"
208
                        " from '%s' to '%s'",
209
                        instance, idx, current, expected)
210
        dobj["iv_name"] = expected
211
      try:
212
        del dobj["idx"]
213
        print("Deleting deprecated idx")
214
      except KeyError:
215
        pass
216

    
217
    for attr in ("dev_idxs", "hotplug_info", "hotplugs", "pci_reservations"):
218
      try:
219
        del iobj[attr]
220
        print("Deleting deprecated %s" % attr)
221
      except KeyError:
222
        pass
223

    
224

    
225
def UpgradeRapiUsers():
226
  if (os.path.isfile(options.RAPI_USERS_FILE_PRE24) and
227
      not os.path.islink(options.RAPI_USERS_FILE_PRE24)):
228
    if os.path.exists(options.RAPI_USERS_FILE):
229
      raise Error("Found pre-2.4 RAPI users file at %s, but another file"
230
                  " already exists at %s" %
231
                  (options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE))
232
    logging.info("Found pre-2.4 RAPI users file at %s, renaming to %s",
233
                 options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
234
    if not options.dry_run:
235
      utils.RenameFile(options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE,
236
                       mkdir=True, mkdir_mode=0750)
237

    
238
  # Create a symlink for RAPI users file
239
  if (not (os.path.islink(options.RAPI_USERS_FILE_PRE24) or
240
           os.path.isfile(options.RAPI_USERS_FILE_PRE24)) and
241
      os.path.isfile(options.RAPI_USERS_FILE)):
242
    logging.info("Creating symlink from %s to %s",
243
                 options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
244
    if not options.dry_run:
245
      os.symlink(options.RAPI_USERS_FILE, options.RAPI_USERS_FILE_PRE24)
246

    
247

    
248
def UpgradeWatcher():
249
  # Remove old watcher state file if it exists
250
  if os.path.exists(options.WATCHER_STATEFILE):
251
    logging.info("Removing watcher state file %s", options.WATCHER_STATEFILE)
252
    if not options.dry_run:
253
      utils.RemoveFile(options.WATCHER_STATEFILE)
254

    
255

    
256
def UpgradeFileStoragePaths(config_data):
257
  # Write file storage paths
258
  if not os.path.exists(options.FILE_STORAGE_PATHS_FILE):
259
    cluster = config_data["cluster"]
260
    file_storage_dir = cluster.get("file_storage_dir")
261
    shared_file_storage_dir = cluster.get("shared_file_storage_dir")
262
    del cluster
263

    
264
    logging.info("Ganeti 2.7 and later only allow whitelisted directories"
265
                 " for file storage; writing existing configuration values"
266
                 " into '%s'",
267
                 options.FILE_STORAGE_PATHS_FILE)
268

    
269
    if file_storage_dir:
270
      logging.info("File storage directory: %s", file_storage_dir)
271
    if shared_file_storage_dir:
272
      logging.info("Shared file storage directory: %s",
273
                   shared_file_storage_dir)
274

    
275
    buf = StringIO()
276
    buf.write("# List automatically generated from configuration by\n")
277
    buf.write("# cfgupgrade at %s\n" % time.asctime())
278
    if file_storage_dir:
279
      buf.write("%s\n" % file_storage_dir)
280
    if shared_file_storage_dir:
281
      buf.write("%s\n" % shared_file_storage_dir)
282
    utils.WriteFile(file_name=options.FILE_STORAGE_PATHS_FILE,
283
                    data=buf.getvalue(),
284
                    mode=0600,
285
                    dry_run=options.dry_run,
286
                    backup=True)
287

    
288

    
289
def UpgradeAll(config_data):
290
  config_data["version"] = constants.BuildVersion(TARGET_MAJOR,
291
                                                  TARGET_MINOR, 0)
292
  UpgradeRapiUsers()
293
  UpgradeWatcher()
294
  UpgradeFileStoragePaths(config_data)
295
  UpgradeNetworks(config_data)
296
  UpgradeCluster(config_data)
297
  UpgradeGroups(config_data)
298
  UpgradeInstances(config_data)
299

    
300

    
301
def DowngradeIPolicy(ipolicy, owner):
302
  # Downgrade IPolicy to 2.7 (stable)
303
  minmax_keys = ["min", "max"]
304
  specs_is_split = any((k in ipolicy) for k in minmax_keys)
305
  if not specs_is_split:
306
    if "minmax" in ipolicy:
307
      if type(ipolicy["minmax"]) is not list:
308
        raise Error("Invalid minmax type in %s ipolicy: %s" %
309
                    (owner, type(ipolicy["minmax"])))
310
      if len(ipolicy["minmax"]) > 1:
311
        logging.warning("Discarding some limit specs values from %s policy",
312
                        owner)
313
      minmax = ipolicy["minmax"][0]
314
      del ipolicy["minmax"]
315
    else:
316
      minmax = {}
317
    for key in minmax_keys:
318
      spec = minmax.get(key, {})
319
      ipolicy[key] = spec
320
    if "std" not in ipolicy:
321
      ipolicy["std"] = {}
322

    
323

    
324
def DowngradeGroups(config_data):
325
  for group in config_data["nodegroups"].values():
326
    ipolicy = group.get("ipolicy", None)
327
    if ipolicy is not None:
328
      DowngradeIPolicy(ipolicy, "group \"%s\"" % group.get("name"))
329

    
330

    
331
def DowngradeEnabledTemplates(cluster):
332
  # Remove enabled disk templates to downgrade to 2.7
333
  edt_key = "enabled_disk_templates"
334
  if edt_key in cluster:
335
    logging.warning("Removing cluster's enabled disk templates; value = %s",
336
                    utils.CommaJoin(cluster[edt_key]))
337
    del cluster[edt_key]
338

    
339

    
340
def DowngradeCluster(config_data):
341
  cluster = config_data.get("cluster", None)
342
  if cluster is None:
343
    raise Error("Cannot find cluster")
344
  DowngradeEnabledTemplates(cluster)
345
  ipolicy = cluster.get("ipolicy", None)
346
  if ipolicy:
347
    DowngradeIPolicy(ipolicy, "cluster")
348
  if "dsahostkeypub" in cluster:
349
    del cluster["dsahostkeypub"]
350

    
351

    
352
def DowngradeDisk(disk):
353
  if "uuid" in disk:
354
    del disk["uuid"]
355
  if "children" in disk:
356
    for child_disk in disk["children"]:
357
      DowngradeDisk(child_disk)
358

    
359

    
360
def DowngradeInstances(config_data):
361
  if "instances" not in config_data:
362
    raise Error("Can't find the 'instances' key in the configuration!")
363

    
364
  for _, iobj in config_data["instances"].items():
365
    if "disks_active" in iobj:
366
      del iobj["disks_active"]
367

    
368
    # Remove the NICs UUIDs
369
    for nic in iobj["nics"]:
370
      if "uuid" in nic:
371
        del nic["uuid"]
372

    
373
    # Downgrade the disks
374
    for disk in iobj["disks"]:
375
      DowngradeDisk(disk)
376

    
377

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

    
387

    
388
def main():
389
  """Main program.
390

    
391
  """
392
  global options, args # pylint: disable=W0603
393

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

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

    
444
  SetupLogging()
445

    
446
  # Option checking
447
  if args:
448
    raise Error("No arguments expected")
449
  if options.downgrade and not options.no_verify:
450
    options.no_verify = True
451

    
452
  # Check master name
453
  if not (CheckHostname(options.SSCONF_MASTER_NODE) or options.ignore_hostname):
454
    logging.error("Aborting due to hostname mismatch")
455
    sys.exit(constants.EXIT_FAILURE)
456

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

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

    
481
  if not os.path.isdir(options.conf_dir):
482
    raise Error("Not a directory: %s" % options.conf_dir)
483

    
484
  config_data = serializer.LoadJson(utils.ReadFile(options.CONFIG_DATA_PATH))
485

    
486
  try:
487
    config_version = config_data["version"]
488
  except KeyError:
489
    raise Error("Unable to determine configuration version")
490

    
491
  (config_major, config_minor, config_revision) = \
492
    constants.SplitVersion(config_version)
493

    
494
  logging.info("Found configuration version %s (%d.%d.%d)",
495
               config_version, config_major, config_minor, config_revision)
496

    
497
  if "config_version" in config_data["cluster"]:
498
    raise Error("Inconsistent configuration: found config_version in"
499
                " configuration file")
500

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

    
512
  # Upgrade from 2.{0..7} to 2.8
513
  elif config_major == 2 and config_minor in range(0, 9):
514
    if config_revision != 0:
515
      logging.warning("Config revision is %s, not 0", config_revision)
516
    UpgradeAll(config_data)
517

    
518
  elif config_major == TARGET_MAJOR and config_minor == TARGET_MINOR:
519
    logging.info("No changes necessary")
520

    
521
  else:
522
    raise Error("Configuration version %d.%d.%d not supported by this tool" %
523
                (config_major, config_minor, config_revision))
524

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

    
533
    if not options.dry_run:
534
      bootstrap.GenerateClusterCrypto(
535
        False, False, False, False, False,
536
        nodecert_file=options.SERVER_PEM_PATH,
537
        rapicert_file=options.RAPI_CERT_FILE,
538
        spicecert_file=options.SPICE_CERT_FILE,
539
        spicecacert_file=options.SPICE_CACERT_FILE,
540
        hmackey_file=options.CONFD_HMAC_KEY,
541
        cds_file=options.CDS_FILE)
542

    
543
  except Exception:
544
    logging.critical("Writing configuration failed. It is probably in an"
545
                     " inconsistent state and needs manual intervention.")
546
    raise
547

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

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

    
579

    
580
if __name__ == "__main__":
581
  main()