Statistics
| Branch: | Tag: | Revision:

root / tools / cfgupgrade @ 6a1e009c

History | View | Annotate | Download (12.3 kB)

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

    
4
# Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012 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 = 6
56

    
57

    
58
class Error(Exception):
59
  """Generic exception"""
60
  pass
61

    
62

    
63
def SetupLogging():
64
  """Configures the logging module.
65

    
66
  """
67
  formatter = logging.Formatter("%(asctime)s: %(message)s")
68

    
69
  stderr_handler = logging.StreamHandler()
70
  stderr_handler.setFormatter(formatter)
71
  if options.debug:
72
    stderr_handler.setLevel(logging.NOTSET)
73
  elif options.verbose:
74
    stderr_handler.setLevel(logging.INFO)
75
  else:
76
    stderr_handler.setLevel(logging.WARNING)
77

    
78
  root_logger = logging.getLogger("")
79
  root_logger.setLevel(logging.NOTSET)
80
  root_logger.addHandler(stderr_handler)
81

    
82

    
83
def CheckHostname(path):
84
  """Ensures hostname matches ssconf value.
85

    
86
  @param path: Path to ssconf file
87

    
88
  """
89
  ssconf_master_node = utils.ReadOneLineFile(path)
90
  hostname = netutils.GetHostname().name
91

    
92
  if ssconf_master_node == hostname:
93
    return True
94

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

    
100

    
101
def UpgradeNetworks(config_data):
102
  networks = config_data.get("networks", None)
103
  if not networks:
104
    config_data["networks"] = {}
105

    
106

    
107
def UpgradeGroups(config_data):
108
  for group in config_data["nodegroups"].values():
109
    networks = group.get("networks", None)
110
    if not networks:
111
      group["networks"] = {}
112

    
113

    
114
def UpgradeInstances(config_data):
115
  for instance in config_data["instances"].values():
116
    hotplug_info = instance.get("hotplug_info", None)
117
    if hotplug_info:
118
      try:
119
        del hotplug_info["pci_pool"]
120
      except KeyError:
121
        pass
122

    
123

    
124
def main():
125
  """Main program.
126

    
127
  """
128
  global options, args # pylint: disable=W0603
129

    
130
  # Option parsing
131
  parser = optparse.OptionParser(usage="%prog [--debug|--verbose] [--force]")
132
  parser.add_option("--dry-run", dest="dry_run",
133
                    action="store_true",
134
                    help="Try to do the conversion, but don't write"
135
                         " output file")
136
  parser.add_option(cli.FORCE_OPT)
137
  parser.add_option(cli.DEBUG_OPT)
138
  parser.add_option(cli.VERBOSE_OPT)
139
  parser.add_option("--ignore-hostname", dest="ignore_hostname",
140
                    action="store_true", default=False,
141
                    help="Don't abort if hostname doesn't match")
142
  parser.add_option("--path", help="Convert configuration in this"
143
                    " directory instead of '%s'" % pathutils.DATA_DIR,
144
                    default=pathutils.DATA_DIR, dest="data_dir")
145
  parser.add_option("--confdir",
146
                    help=("Use this directory instead of '%s'" %
147
                          pathutils.CONF_DIR),
148
                    default=pathutils.CONF_DIR, dest="conf_dir")
149
  parser.add_option("--no-verify",
150
                    help="Do not verify configuration after upgrade",
151
                    action="store_true", dest="no_verify", default=False)
152
  (options, args) = parser.parse_args()
153

    
154
  # We need to keep filenames locally because they might be renamed between
155
  # versions.
156
  options.data_dir = os.path.abspath(options.data_dir)
157
  options.CONFIG_DATA_PATH = options.data_dir + "/config.data"
158
  options.SERVER_PEM_PATH = options.data_dir + "/server.pem"
159
  options.KNOWN_HOSTS_PATH = options.data_dir + "/known_hosts"
160
  options.RAPI_CERT_FILE = options.data_dir + "/rapi.pem"
161
  options.SPICE_CERT_FILE = options.data_dir + "/spice.pem"
162
  options.SPICE_CACERT_FILE = options.data_dir + "/spice-ca.pem"
163
  options.RAPI_USERS_FILE = options.data_dir + "/rapi/users"
164
  options.RAPI_USERS_FILE_PRE24 = options.data_dir + "/rapi_users"
165
  options.CONFD_HMAC_KEY = options.data_dir + "/hmac.key"
166
  options.CDS_FILE = options.data_dir + "/cluster-domain-secret"
167
  options.SSCONF_MASTER_NODE = options.data_dir + "/ssconf_master_node"
168
  options.WATCHER_STATEFILE = options.data_dir + "/watcher.data"
169
  options.FILE_STORAGE_PATHS_FILE = options.conf_dir + "/file-storage-paths"
170

    
171
  SetupLogging()
172

    
173
  # Option checking
174
  if args:
175
    raise Error("No arguments expected")
176

    
177
  # Check master name
178
  if not (CheckHostname(options.SSCONF_MASTER_NODE) or options.ignore_hostname):
179
    logging.error("Aborting due to hostname mismatch")
180
    sys.exit(constants.EXIT_FAILURE)
181

    
182
  if not options.force:
183
    usertext = ("Please make sure you have read the upgrade notes for"
184
                " Ganeti %s (available in the UPGRADE file and included"
185
                " in other documentation formats). Continue with upgrading"
186
                " configuration?" % constants.RELEASE_VERSION)
187
    if not cli.AskUser(usertext):
188
      sys.exit(constants.EXIT_FAILURE)
189

    
190
  # Check whether it's a Ganeti configuration directory
191
  if not (os.path.isfile(options.CONFIG_DATA_PATH) and
192
          os.path.isfile(options.SERVER_PEM_PATH) and
193
          os.path.isfile(options.KNOWN_HOSTS_PATH)):
194
    raise Error(("%s does not seem to be a Ganeti configuration"
195
                 " directory") % options.data_dir)
196

    
197
  if not os.path.isdir(options.conf_dir):
198
    raise Error("Not a directory: %s" % options.conf_dir)
199

    
200
  config_data = serializer.LoadJson(utils.ReadFile(options.CONFIG_DATA_PATH))
201

    
202
  try:
203
    config_version = config_data["version"]
204
  except KeyError:
205
    raise Error("Unable to determine configuration version")
206

    
207
  (config_major, config_minor, config_revision) = \
208
    constants.SplitVersion(config_version)
209

    
210
  logging.info("Found configuration version %s (%d.%d.%d)",
211
               config_version, config_major, config_minor, config_revision)
212

    
213
  if "config_version" in config_data["cluster"]:
214
    raise Error("Inconsistent configuration: found config_version in"
215
                " configuration file")
216

    
217
  # Upgrade from 2.0/2.1/2.2/2.3 to 2.4
218
  if config_major == 2 and config_minor in (0, 1, 2, 3, 4, 5):
219
    if config_revision != 0:
220
      logging.warning("Config revision is %s, not 0", config_revision)
221

    
222
    config_data["version"] = constants.BuildVersion(TARGET_MAJOR,
223
                                                    TARGET_MINOR, 0)
224

    
225
    if "instances" not in config_data:
226
      raise Error("Can't find the 'instances' key in the configuration!")
227
    for instance, iobj in config_data["instances"].items():
228
      if "disks" not in iobj:
229
        raise Error("Instance '%s' doesn't have a disks entry?!" % instance)
230
      disks = iobj["disks"]
231
      for idx, dobj in enumerate(disks):
232
        expected = "disk/%s" % idx
233
        current = dobj.get("iv_name", "")
234
        if current != expected:
235
          logging.warning("Updating iv_name for instance %s/disk %s"
236
                          " from '%s' to '%s'",
237
                          instance, idx, current, expected)
238
          dobj["iv_name"] = expected
239

    
240
  elif config_major == TARGET_MAJOR and config_minor == TARGET_MINOR:
241
    logging.info("No changes necessary")
242

    
243
  else:
244
    raise Error("Configuration version %d.%d.%d not supported by this tool" %
245
                (config_major, config_minor, config_revision))
246

    
247
  if (os.path.isfile(options.RAPI_USERS_FILE_PRE24) and
248
      not os.path.islink(options.RAPI_USERS_FILE_PRE24)):
249
    if os.path.exists(options.RAPI_USERS_FILE):
250
      raise Error("Found pre-2.4 RAPI users file at %s, but another file"
251
                  " already exists at %s" %
252
                  (options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE))
253
    logging.info("Found pre-2.4 RAPI users file at %s, renaming to %s",
254
                 options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
255
    if not options.dry_run:
256
      utils.RenameFile(options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE,
257
                       mkdir=True, mkdir_mode=0750)
258

    
259
  # Create a symlink for RAPI users file
260
  if (not (os.path.islink(options.RAPI_USERS_FILE_PRE24) or
261
           os.path.isfile(options.RAPI_USERS_FILE_PRE24)) and
262
      os.path.isfile(options.RAPI_USERS_FILE)):
263
    logging.info("Creating symlink from %s to %s",
264
                 options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
265
    if not options.dry_run:
266
      os.symlink(options.RAPI_USERS_FILE, options.RAPI_USERS_FILE_PRE24)
267

    
268
  # Remove old watcher state file if it exists
269
  if os.path.exists(options.WATCHER_STATEFILE):
270
    logging.info("Removing watcher state file %s", options.WATCHER_STATEFILE)
271
    if not options.dry_run:
272
      utils.RemoveFile(options.WATCHER_STATEFILE)
273

    
274
  # Write file storage paths
275
  if not os.path.exists(options.FILE_STORAGE_PATHS_FILE):
276
    cluster = config_data["cluster"]
277
    file_storage_dir = cluster.get("file_storage_dir")
278
    shared_file_storage_dir = cluster.get("shared_file_storage_dir")
279
    del cluster
280

    
281
    logging.info("Ganeti 2.7 and later only allow whitelisted directories"
282
                 " for file storage; writing existing configuration values"
283
                 " into '%s'",
284
                 options.FILE_STORAGE_PATHS_FILE)
285

    
286
    if file_storage_dir:
287
      logging.info("File storage directory: %s", file_storage_dir)
288
    if shared_file_storage_dir:
289
      logging.info("Shared file storage directory: %s",
290
                   shared_file_storage_dir)
291

    
292
    buf = StringIO()
293
    buf.write("# List automatically generated from configuration by\n")
294
    buf.write("# cfgupgrade at %s\n" % time.asctime())
295
    if file_storage_dir:
296
      buf.write("%s\n" % file_storage_dir)
297
    if shared_file_storage_dir:
298
      buf.write("%s\n" % shared_file_storage_dir)
299
    utils.WriteFile(file_name=options.FILE_STORAGE_PATHS_FILE,
300
                    data=buf.getvalue(),
301
                    mode=0600,
302
                    dry_run=options.dry_run,
303
                    backup=True)
304

    
305
  UpgradeNetworks(config_data)
306
  UpgradeGroups(config_data)
307
  UpgradeInstances(config_data)
308

    
309
  try:
310
    logging.info("Writing configuration file to %s", options.CONFIG_DATA_PATH)
311
    utils.WriteFile(file_name=options.CONFIG_DATA_PATH,
312
                    data=serializer.DumpJson(config_data),
313
                    mode=0600,
314
                    dry_run=options.dry_run,
315
                    backup=True)
316

    
317
    if not options.dry_run:
318
      bootstrap.GenerateClusterCrypto(
319
        False, False, False, False, False,
320
        nodecert_file=options.SERVER_PEM_PATH,
321
        rapicert_file=options.RAPI_CERT_FILE,
322
        spicecert_file=options.SPICE_CERT_FILE,
323
        spicecacert_file=options.SPICE_CACERT_FILE,
324
        hmackey_file=options.CONFD_HMAC_KEY,
325
        cds_file=options.CDS_FILE)
326

    
327
  except Exception:
328
    logging.critical("Writing configuration failed. It is probably in an"
329
                     " inconsistent state and needs manual intervention.")
330
    raise
331

    
332
  # test loading the config file
333
  if not (options.dry_run or options.no_verify):
334
    logging.info("Testing the new config file...")
335
    cfg = config.ConfigWriter(cfg_file=options.CONFIG_DATA_PATH,
336
                              accept_foreign=options.ignore_hostname,
337
                              offline=True)
338
    # if we reached this, it's all fine
339
    vrfy = cfg.VerifyConfig()
340
    if vrfy:
341
      logging.error("Errors after conversion:")
342
      for item in vrfy:
343
        logging.error(" - %s", item)
344
    del cfg
345
    logging.info("File loaded successfully")
346

    
347
  cli.ToStderr("Configuration successfully upgraded for version %s.",
348
               constants.RELEASE_VERSION)
349

    
350

    
351
if __name__ == "__main__":
352
  main()