Statistics
| Branch: | Tag: | Revision:

root / tools / cfgupgrade @ d3a5e2de

History | View | Annotate | Download (11 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
def UpgradeNetworks(config_data):
98
  networks = config_data.get("networks", None)
99
  if not networks:
100
    config_data["networks"] = {}
101

    
102

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

    
110
    for network_uuid in group["networks"].keys():
111
      netparams = group["networks"][network_uuid]
112
      if (not isinstance(netparams, dict) or
113
          not netparams.get("link", None) or not netparams.get("mode", None)):
114
        nicparams = constants.NICC_DEFAULTS
115
        group["networks"][network_uuid] = dict(nicparams)
116

    
117

    
118
def UpgradeInstances(config_data):
119
  for instance in config_data["instances"].values():
120
    instance["hotplugs"] = 0
121
    for nic in instance["nics"]:
122
      nic["idx"] = -1
123

    
124

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

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

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

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

    
167
  SetupLogging()
168

    
169
  # Option checking
170
  if args:
171
    raise Error("No arguments expected")
172

    
173
  # Check master name
174
  if not (CheckHostname(options.SSCONF_MASTER_NODE) or options.ignore_hostname):
175
    logging.error("Aborting due to hostname mismatch")
176
    sys.exit(constants.EXIT_FAILURE)
177

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

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

    
193
  config_data = serializer.LoadJson(utils.ReadFile(options.CONFIG_DATA_PATH))
194

    
195
  try:
196
    config_version = config_data["version"]
197
  except KeyError:
198
    raise Error("Unable to determine configuration version")
199

    
200
  (config_major, config_minor, config_revision) = \
201
    constants.SplitVersion(config_version)
202

    
203
  logging.info("Found configuration version %s (%d.%d.%d)",
204
               config_version, config_major, config_minor, config_revision)
205

    
206
  if "config_version" in config_data["cluster"]:
207
    raise Error("Inconsistent configuration: found config_version in"
208
                " configuration file")
209

    
210
  # Upgrade from 2.0/2.1/2.2/2.3 to 2.4
211
  if config_major == 2 and config_minor in (0, 1, 2, 3, 4, 5):
212
    if config_revision != 0:
213
      logging.warning("Config revision is %s, not 0", config_revision)
214

    
215
    config_data["version"] = constants.BuildVersion(TARGET_MAJOR,
216
                                                    TARGET_MINOR, 0)
217

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

    
233
  elif config_major == TARGET_MAJOR and config_minor == TARGET_MINOR:
234
    logging.info("No changes necessary")
235

    
236
  else:
237
    raise Error("Configuration version %d.%d.%d not supported by this tool" %
238
                (config_major, config_minor, config_revision))
239

    
240
  if (os.path.isfile(options.RAPI_USERS_FILE_PRE24) and
241
      not os.path.islink(options.RAPI_USERS_FILE_PRE24)):
242
    if os.path.exists(options.RAPI_USERS_FILE):
243
      raise Error("Found pre-2.4 RAPI users file at %s, but another file"
244
                  " already exists at %s" %
245
                  (options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE))
246
    logging.info("Found pre-2.4 RAPI users file at %s, renaming to %s",
247
                 options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
248
    if not options.dry_run:
249
      utils.RenameFile(options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE,
250
                       mkdir=True, mkdir_mode=0750)
251

    
252
  # Create a symlink for RAPI users file
253
  if (not (os.path.islink(options.RAPI_USERS_FILE_PRE24) or
254
           os.path.isfile(options.RAPI_USERS_FILE_PRE24)) and
255
      os.path.isfile(options.RAPI_USERS_FILE)):
256
    logging.info("Creating symlink from %s to %s",
257
                 options.RAPI_USERS_FILE_PRE24, options.RAPI_USERS_FILE)
258
    if not options.dry_run:
259
      os.symlink(options.RAPI_USERS_FILE, options.RAPI_USERS_FILE_PRE24)
260

    
261
  # Remove old watcher state file if it exists
262
  if os.path.exists(options.WATCHER_STATEFILE):
263
    logging.info("Removing watcher state file %s", options.WATCHER_STATEFILE)
264
    if not options.dry_run:
265
      utils.RemoveFile(options.WATCHER_STATEFILE)
266

    
267
  UpgradeNetworks(config_data)
268
  UpgradeGroups(config_data)
269
  UpgradeInstances(config_data)
270

    
271
  try:
272
    logging.info("Writing configuration file to %s", options.CONFIG_DATA_PATH)
273
    utils.WriteFile(file_name=options.CONFIG_DATA_PATH,
274
                    data=serializer.DumpJson(config_data),
275
                    mode=0600,
276
                    dry_run=options.dry_run,
277
                    backup=True)
278

    
279
    if not options.dry_run:
280
      bootstrap.GenerateClusterCrypto(False, False, False, False, False,
281
                                     nodecert_file=options.SERVER_PEM_PATH,
282
                                     rapicert_file=options.RAPI_CERT_FILE,
283
                                     spicecert_file=options.SPICE_CERT_FILE,
284
                                     spicecacert_file=options.SPICE_CACERT_FILE,
285
                                     hmackey_file=options.CONFD_HMAC_KEY,
286
                                     cds_file=options.CDS_FILE)
287

    
288
  except Exception:
289
    logging.critical("Writing configuration failed. It is probably in an"
290
                     " inconsistent state and needs manual intervention.")
291
    raise
292

    
293
  # test loading the config file
294
  if not (options.dry_run or options.no_verify):
295
    logging.info("Testing the new config file...")
296
    cfg = config.ConfigWriter(cfg_file=options.CONFIG_DATA_PATH,
297
                              accept_foreign=options.ignore_hostname,
298
                              offline=True)
299
    # if we reached this, it's all fine
300
    vrfy = cfg.VerifyConfig()
301
    if vrfy:
302
      logging.error("Errors after conversion:")
303
      for item in vrfy:
304
        logging.error(" - %s", item)
305
    del cfg
306
    logging.info("File loaded successfully")
307

    
308
  cli.ToStderr("Configuration successfully upgraded for version %s.",
309
               constants.RELEASE_VERSION)
310

    
311

    
312
if __name__ == "__main__":
313
  main()