Statistics
| Branch: | Tag: | Revision:

root / tools / cfgupgrade @ fdaacd25

History | View | Annotate | Download (10.4 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

    
36
from ganeti import constants
37
from ganeti import serializer
38
from ganeti import utils
39
from ganeti import cli
40
from ganeti import bootstrap
41
from ganeti import config
42
from ganeti import netutils
43
from ganeti import pathutils
44

    
45

    
46
options = None
47
args = None
48

    
49

    
50
#: Target major version we will upgrade to
51
TARGET_MAJOR = 2
52
#: Target minor version we will upgrade to
53
TARGET_MINOR = 6
54

    
55

    
56
class Error(Exception):
57
  """Generic exception"""
58
  pass
59

    
60

    
61
def SetupLogging():
62
  """Configures the logging module.
63

    
64
  """
65
  formatter = logging.Formatter("%(asctime)s: %(message)s")
66

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

    
76
  root_logger = logging.getLogger("")
77
  root_logger.setLevel(logging.NOTSET)
78
  root_logger.addHandler(stderr_handler)
79

    
80

    
81
def CheckHostname(path):
82
  """Ensures hostname matches ssconf value.
83

    
84
  @param path: Path to ssconf file
85

    
86
  """
87
  ssconf_master_node = utils.ReadOneLineFile(path)
88
  hostname = netutils.GetHostname().name
89

    
90
  if ssconf_master_node == hostname:
91
    return True
92

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

    
98
def UpgradeNetworks(config_data):
99
  networks = config_data.get("networks", None)
100
  if not networks:
101
    config_data["networks"] = {}
102

    
103

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

    
110

    
111
def main():
112
  """Main program.
113

    
114
  """
115
  global options, args # pylint: disable=W0603
116

    
117
  # Option parsing
118
  parser = optparse.OptionParser(usage="%prog [--debug|--verbose] [--force]")
119
  parser.add_option("--dry-run", dest="dry_run",
120
                    action="store_true",
121
                    help="Try to do the conversion, but don't write"
122
                         " output file")
123
  parser.add_option(cli.FORCE_OPT)
124
  parser.add_option(cli.DEBUG_OPT)
125
  parser.add_option(cli.VERBOSE_OPT)
126
  parser.add_option("--ignore-hostname", dest="ignore_hostname",
127
                    action="store_true", default=False,
128
                    help="Don't abort if hostname doesn't match")
129
  parser.add_option("--path", help="Convert configuration in this"
130
                    " directory instead of '%s'" % pathutils.DATA_DIR,
131
                    default=pathutils.DATA_DIR, dest="data_dir")
132
  parser.add_option("--no-verify",
133
                    help="Do not verify configuration after upgrade",
134
                    action="store_true", dest="no_verify", default=False)
135
  (options, args) = parser.parse_args()
136

    
137
  # We need to keep filenames locally because they might be renamed between
138
  # versions.
139
  options.data_dir = os.path.abspath(options.data_dir)
140
  options.CONFIG_DATA_PATH = options.data_dir + "/config.data"
141
  options.SERVER_PEM_PATH = options.data_dir + "/server.pem"
142
  options.KNOWN_HOSTS_PATH = options.data_dir + "/known_hosts"
143
  options.RAPI_CERT_FILE = options.data_dir + "/rapi.pem"
144
  options.SPICE_CERT_FILE = options.data_dir + "/spice.pem"
145
  options.SPICE_CACERT_FILE = options.data_dir + "/spice-ca.pem"
146
  options.RAPI_USERS_FILE = options.data_dir + "/rapi/users"
147
  options.RAPI_USERS_FILE_PRE24 = options.data_dir + "/rapi_users"
148
  options.CONFD_HMAC_KEY = options.data_dir + "/hmac.key"
149
  options.CDS_FILE = options.data_dir + "/cluster-domain-secret"
150
  options.SSCONF_MASTER_NODE = options.data_dir + "/ssconf_master_node"
151
  options.WATCHER_STATEFILE = options.data_dir + "/watcher.data"
152

    
153
  SetupLogging()
154

    
155
  # Option checking
156
  if args:
157
    raise Error("No arguments expected")
158

    
159
  # Check master name
160
  if not (CheckHostname(options.SSCONF_MASTER_NODE) or options.ignore_hostname):
161
    logging.error("Aborting due to hostname mismatch")
162
    sys.exit(constants.EXIT_FAILURE)
163

    
164
  if not options.force:
165
    usertext = ("Please make sure you have read the upgrade notes for"
166
                " Ganeti %s (available in the UPGRADE file and included"
167
                " in other documentation formats). Continue with upgrading"
168
                " configuration?" % constants.RELEASE_VERSION)
169
    if not cli.AskUser(usertext):
170
      sys.exit(constants.EXIT_FAILURE)
171

    
172
  # Check whether it's a Ganeti configuration directory
173
  if not (os.path.isfile(options.CONFIG_DATA_PATH) and
174
          os.path.isfile(options.SERVER_PEM_PATH) and
175
          os.path.isfile(options.KNOWN_HOSTS_PATH)):
176
    raise Error(("%s does not seem to be a Ganeti configuration"
177
                 " directory") % options.data_dir)
178

    
179
  config_data = serializer.LoadJson(utils.ReadFile(options.CONFIG_DATA_PATH))
180

    
181
  try:
182
    config_version = config_data["version"]
183
  except KeyError:
184
    raise Error("Unable to determine configuration version")
185

    
186
  (config_major, config_minor, config_revision) = \
187
    constants.SplitVersion(config_version)
188

    
189
  logging.info("Found configuration version %s (%d.%d.%d)",
190
               config_version, config_major, config_minor, config_revision)
191

    
192
  if "config_version" in config_data["cluster"]:
193
    raise Error("Inconsistent configuration: found config_version in"
194
                " configuration file")
195

    
196
  # Upgrade from 2.0/2.1/2.2/2.3 to 2.4
197
  if config_major == 2 and config_minor in (0, 1, 2, 3, 4, 5):
198
    if config_revision != 0:
199
      logging.warning("Config revision is %s, not 0", config_revision)
200

    
201
    config_data["version"] = constants.BuildVersion(TARGET_MAJOR,
202
                                                    TARGET_MINOR, 0)
203

    
204
    if "instances" not in config_data:
205
      raise Error("Can't find the 'instances' key in the configuration!")
206
    for instance, iobj in config_data["instances"].items():
207
      if "disks" not in iobj:
208
        raise Error("Instance '%s' doesn't have a disks entry?!" % instance)
209
      disks = iobj["disks"]
210
      for idx, dobj in enumerate(disks):
211
        expected = "disk/%s" % idx
212
        current = dobj.get("iv_name", "")
213
        if current != expected:
214
          logging.warning("Updating iv_name for instance %s/disk %s"
215
                          " from '%s' to '%s'",
216
                          instance, idx, current, expected)
217
          dobj["iv_name"] = expected
218

    
219
  elif config_major == TARGET_MAJOR and config_minor == TARGET_MINOR:
220
    logging.info("No changes necessary")
221

    
222
  else:
223
    raise Error("Configuration version %d.%d.%d not supported by this tool" %
224
                (config_major, config_minor, config_revision))
225

    
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
  # Remove old watcher state file if it exists
248
  if os.path.exists(options.WATCHER_STATEFILE):
249
    logging.info("Removing watcher state file %s", options.WATCHER_STATEFILE)
250
    if not options.dry_run:
251
      utils.RemoveFile(options.WATCHER_STATEFILE)
252

    
253
  UpgradeNetworks(config_data)
254
  UpgradeGroups(config_data)
255

    
256
  try:
257
    logging.info("Writing configuration file to %s", options.CONFIG_DATA_PATH)
258
    utils.WriteFile(file_name=options.CONFIG_DATA_PATH,
259
                    data=serializer.DumpJson(config_data),
260
                    mode=0600,
261
                    dry_run=options.dry_run,
262
                    backup=True)
263

    
264
    if not options.dry_run:
265
      bootstrap.GenerateClusterCrypto(
266
        False, False, False, False, False,
267
        nodecert_file=options.SERVER_PEM_PATH,
268
        rapicert_file=options.RAPI_CERT_FILE,
269
        spicecert_file=options.SPICE_CERT_FILE,
270
        spicecacert_file=options.SPICE_CACERT_FILE,
271
        hmackey_file=options.CONFD_HMAC_KEY,
272
        cds_file=options.CDS_FILE)
273

    
274
  except Exception:
275
    logging.critical("Writing configuration failed. It is probably in an"
276
                     " inconsistent state and needs manual intervention.")
277
    raise
278

    
279
  # test loading the config file
280
  if not (options.dry_run or options.no_verify):
281
    logging.info("Testing the new config file...")
282
    cfg = config.ConfigWriter(cfg_file=options.CONFIG_DATA_PATH,
283
                              accept_foreign=options.ignore_hostname,
284
                              offline=True)
285
    # if we reached this, it's all fine
286
    vrfy = cfg.VerifyConfig()
287
    if vrfy:
288
      logging.error("Errors after conversion:")
289
      for item in vrfy:
290
        logging.error(" - %s", item)
291
    del cfg
292
    logging.info("File loaded successfully")
293

    
294
  cli.ToStderr("Configuration successfully upgraded for version %s.",
295
               constants.RELEASE_VERSION)
296

    
297

    
298
if __name__ == "__main__":
299
  main()