Statistics
| Branch: | Tag: | Revision:

root / tools / cfgupgrade @ 5ae4945a

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

    
44

    
45
options = None
46
args = None
47

    
48

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

    
54

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

    
59

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

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

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

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

    
79

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

    
83
  @param path: Path to ssconf file
84

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

    
89
  if ssconf_master_node == hostname:
90
    return True
91

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

    
97

    
98
def main():
99
  """Main program.
100

    
101
  """
102
  global options, args # pylint: disable=W0603
103

    
104
  # Option parsing
105
  parser = optparse.OptionParser(usage="%prog [--debug|--verbose] [--force]")
106
  parser.add_option("--dry-run", dest="dry_run",
107
                    action="store_true",
108
                    help="Try to do the conversion, but don't write"
109
                         " output file")
110
  parser.add_option(cli.FORCE_OPT)
111
  parser.add_option(cli.DEBUG_OPT)
112
  parser.add_option(cli.VERBOSE_OPT)
113
  parser.add_option("--ignore-hostname", dest="ignore_hostname",
114
                    action="store_true", default=False,
115
                    help="Don't abort if hostname doesn't match")
116
  parser.add_option("--path", help="Convert configuration in this"
117
                    " directory instead of '%s'" % constants.DATA_DIR,
118
                    default=constants.DATA_DIR, dest="data_dir")
119
  parser.add_option("--no-verify",
120
                    help="Do not verify configuration after upgrade",
121
                    action="store_true", dest="no_verify", default=False)
122
  (options, args) = parser.parse_args()
123

    
124
  # We need to keep filenames locally because they might be renamed between
125
  # versions.
126
  options.data_dir = os.path.abspath(options.data_dir)
127
  options.CONFIG_DATA_PATH = options.data_dir + "/config.data"
128
  options.SERVER_PEM_PATH = options.data_dir + "/server.pem"
129
  options.KNOWN_HOSTS_PATH = options.data_dir + "/known_hosts"
130
  options.RAPI_CERT_FILE = options.data_dir + "/rapi.pem"
131
  options.SPICE_CERT_FILE = options.data_dir + "/spice.pem"
132
  options.SPICE_CACERT_FILE = options.data_dir + "/spice-ca.pem"
133
  options.RAPI_USERS_FILE = options.data_dir + "/rapi/users"
134
  options.RAPI_USERS_FILE_PRE24 = options.data_dir + "/rapi_users"
135
  options.CONFD_HMAC_KEY = options.data_dir + "/hmac.key"
136
  options.CDS_FILE = options.data_dir + "/cluster-domain-secret"
137
  options.SSCONF_MASTER_NODE = options.data_dir + "/ssconf_master_node"
138
  options.WATCHER_STATEFILE = options.data_dir + "/watcher.data"
139

    
140
  SetupLogging()
141

    
142
  # Option checking
143
  if args:
144
    raise Error("No arguments expected")
145

    
146
  # Check master name
147
  if not (CheckHostname(options.SSCONF_MASTER_NODE) or options.ignore_hostname):
148
    logging.error("Aborting due to hostname mismatch")
149
    sys.exit(constants.EXIT_FAILURE)
150

    
151
  if not options.force:
152
    usertext = ("Please make sure you have read the upgrade notes for"
153
                " Ganeti %s (available in the UPGRADE file and included"
154
                " in other documentation formats). Continue with upgrading"
155
                " configuration?" % constants.RELEASE_VERSION)
156
    if not cli.AskUser(usertext):
157
      sys.exit(constants.EXIT_FAILURE)
158

    
159
  # Check whether it's a Ganeti configuration directory
160
  if not (os.path.isfile(options.CONFIG_DATA_PATH) and
161
          os.path.isfile(options.SERVER_PEM_PATH) and
162
          os.path.isfile(options.KNOWN_HOSTS_PATH)):
163
    raise Error(("%s does not seem to be a Ganeti configuration"
164
                 " directory") % options.data_dir)
165

    
166
  config_data = serializer.LoadJson(utils.ReadFile(options.CONFIG_DATA_PATH))
167

    
168
  try:
169
    config_version = config_data["version"]
170
  except KeyError:
171
    raise Error("Unable to determine configuration version")
172

    
173
  (config_major, config_minor, config_revision) = \
174
    constants.SplitVersion(config_version)
175

    
176
  logging.info("Found configuration version %s (%d.%d.%d)",
177
               config_version, config_major, config_minor, config_revision)
178

    
179
  if "config_version" in config_data["cluster"]:
180
    raise Error("Inconsistent configuration: found config_version in"
181
                " configuration file")
182

    
183
  # Upgrade from 2.0/2.1/2.2/2.3 to 2.4
184
  if config_major == 2 and config_minor in (0, 1, 2, 3, 4, 5):
185
    if config_revision != 0:
186
      logging.warning("Config revision is %s, not 0", config_revision)
187

    
188
    config_data["version"] = constants.BuildVersion(TARGET_MAJOR,
189
                                                    TARGET_MINOR, 0)
190

    
191
    if "instances" not in config_data:
192
      raise Error("Can't find the 'instances' key in the configuration!")
193
    for instance, iobj in config_data["instances"].items():
194
      if "disks" not in iobj:
195
        raise Error("Instance '%s' doesn't have a disks entry?!" % instance)
196
      disks = iobj["disks"]
197
      for idx, dobj in enumerate(disks):
198
        expected = "disk/%s" % idx
199
        current = dobj.get("iv_name", "")
200
        if current != expected:
201
          logging.warning("Updating iv_name for instance %s/disk %s"
202
                          " from '%s' to '%s'",
203
                          instance, idx, current, expected)
204
          dobj["iv_name"] = expected
205

    
206
  elif config_major == TARGET_MAJOR and config_minor == TARGET_MINOR:
207
    logging.info("No changes necessary")
208

    
209
  else:
210
    raise Error("Configuration version %d.%d.%d not supported by this tool" %
211
                (config_major, config_minor, config_revision))
212

    
213
  if (os.path.isfile(options.RAPI_USERS_FILE_PRE24) and
214
      not os.path.islink(options.RAPI_USERS_FILE_PRE24)):
215
    if os.path.exists(options.RAPI_USERS_FILE):
216
      raise Error("Found pre-2.4 RAPI users file at %s, but another file"
217
                  " already exists at %s" %
218
                  (options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE))
219
    logging.info("Found pre-2.4 RAPI users file at %s, renaming to %s",
220
                 options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
221
    if not options.dry_run:
222
      utils.RenameFile(options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE,
223
                       mkdir=True, mkdir_mode=0750)
224

    
225
  # Create a symlink for RAPI users file
226
  if (not (os.path.islink(options.RAPI_USERS_FILE_PRE24) or
227
           os.path.isfile(options.RAPI_USERS_FILE_PRE24)) and
228
      os.path.isfile(options.RAPI_USERS_FILE)):
229
    logging.info("Creating symlink from %s to %s",
230
                 options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
231
    if not options.dry_run:
232
      os.symlink(options.RAPI_USERS_FILE, options.RAPI_USERS_FILE_PRE24)
233

    
234
  # Remove old watcher state file if it exists
235
  if os.path.exists(options.WATCHER_STATEFILE):
236
    logging.info("Removing watcher state file %s", options.WATCHER_STATEFILE)
237
    if not options.dry_run:
238
      utils.RemoveFile(options.WATCHER_STATEFILE)
239

    
240
  try:
241
    logging.info("Writing configuration file to %s", options.CONFIG_DATA_PATH)
242
    utils.WriteFile(file_name=options.CONFIG_DATA_PATH,
243
                    data=serializer.DumpJson(config_data),
244
                    mode=0600,
245
                    dry_run=options.dry_run,
246
                    backup=True)
247

    
248
    if not options.dry_run:
249
      bootstrap.GenerateClusterCrypto(
250
        False, False, False, False, False,
251
        nodecert_file=options.SERVER_PEM_PATH,
252
        rapicert_file=options.RAPI_CERT_FILE,
253
        spicecert_file=options.SPICE_CERT_FILE,
254
        spicecacert_file=options.SPICE_CACERT_FILE,
255
        hmackey_file=options.CONFD_HMAC_KEY,
256
        cds_file=options.CDS_FILE)
257

    
258
  except Exception:
259
    logging.critical("Writing configuration failed. It is probably in an"
260
                     " inconsistent state and needs manual intervention.")
261
    raise
262

    
263
  # test loading the config file
264
  if not (options.dry_run or options.no_verify):
265
    logging.info("Testing the new config file...")
266
    cfg = config.ConfigWriter(cfg_file=options.CONFIG_DATA_PATH,
267
                              accept_foreign=options.ignore_hostname,
268
                              offline=True)
269
    # if we reached this, it's all fine
270
    vrfy = cfg.VerifyConfig()
271
    if vrfy:
272
      logging.error("Errors after conversion:")
273
      for item in vrfy:
274
        logging.error(" - %s", item)
275
    del cfg
276
    logging.info("File loaded successfully")
277

    
278
  cli.ToStderr("Configuration successfully upgraded for version %s.",
279
               constants.RELEASE_VERSION)
280

    
281

    
282
if __name__ == "__main__":
283
  main()