Statistics
| Branch: | Tag: | Revision:

root / tools / cfgupgrade @ 4d98c565

History | View | Annotate | Download (12.2 kB)

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

    
4
# Copyright (C) 2007, 2008, 2009 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 tempfile
35
import logging
36
import errno
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

    
45

    
46
options = None
47
args = None
48

    
49
# Unique object to identify calls without default value
50
NoDefault = object()
51

    
52
# Dictionary with instance old keys, and new hypervisor keys
53
INST_HV_CHG = {
54
  'hvm_pae': constants.HV_PAE,
55
  'vnc_bind_address': constants.HV_VNC_BIND_ADDRESS,
56
  'initrd_path': constants.HV_INITRD_PATH,
57
  'hvm_nic_type': constants.HV_NIC_TYPE,
58
  'kernel_path': constants.HV_KERNEL_PATH,
59
  'hvm_acpi': constants.HV_ACPI,
60
  'hvm_cdrom_image_path': constants.HV_CDROM_IMAGE_PATH,
61
  'hvm_boot_order': constants.HV_BOOT_ORDER,
62
  'hvm_disk_type': constants.HV_DISK_TYPE,
63
  }
64

    
65
# Instance beparams changes
66
INST_BE_CHG = {
67
  'vcpus': constants.BE_VCPUS,
68
  'memory': constants.BE_MEMORY,
69
  'auto_balance': constants.BE_AUTO_BALANCE,
70
  }
71

    
72
# Field names
73
F_SERIAL = 'serial_no'
74

    
75

    
76
class Error(Exception):
77
  """Generic exception"""
78
  pass
79

    
80

    
81
def SsconfName(key):
82
  """Returns the file name of an (old) ssconf key.
83

    
84
  """
85
  return "%s/ssconf_%s" % (options.data_dir, key)
86

    
87

    
88
def ReadFile(file_name, default=NoDefault):
89
  """Reads a file.
90

    
91
  """
92
  logging.debug("Reading %s", file_name)
93
  try:
94
    fh = open(file_name, 'r')
95
  except IOError, err:
96
    if default is not NoDefault and err.errno == errno.ENOENT:
97
      return default
98
    raise
99

    
100
  try:
101
    return fh.read()
102
  finally:
103
    fh.close()
104

    
105

    
106
def WriteFile(file_name, data):
107
  """Writes a configuration file.
108

    
109
  """
110
  logging.debug("Writing %s", file_name)
111
  utils.WriteFile(file_name=file_name, data=data, mode=0600,
112
                  dry_run=options.dry_run, backup=True)
113

    
114

    
115
def GenerateSecret(all_secrets):
116
  """Generate an unique DRBD secret.
117

    
118
  This is a copy from ConfigWriter.
119

    
120
  """
121
  retries = 64
122
  while retries > 0:
123
    secret = utils.GenerateSecret()
124
    if secret not in all_secrets:
125
      break
126
    retries -= 1
127
  else:
128
    raise Error("Can't generate unique DRBD secret")
129
  return secret
130

    
131

    
132
def SetupLogging():
133
  """Configures the logging module.
134

    
135
  """
136
  formatter = logging.Formatter("%(asctime)s: %(message)s")
137

    
138
  stderr_handler = logging.StreamHandler()
139
  stderr_handler.setFormatter(formatter)
140
  if options.debug:
141
    stderr_handler.setLevel(logging.NOTSET)
142
  elif options.verbose:
143
    stderr_handler.setLevel(logging.INFO)
144
  else:
145
    stderr_handler.setLevel(logging.CRITICAL)
146

    
147
  root_logger = logging.getLogger("")
148
  root_logger.setLevel(logging.NOTSET)
149
  root_logger.addHandler(stderr_handler)
150

    
151

    
152
def Cluster12To20(cluster):
153
  """Upgrades the cluster object from 1.2 to 2.0.
154

    
155
  """
156
  logging.info("Upgrading the cluster object")
157
  # Upgrade the configuration version
158
  if 'config_version' in cluster:
159
    del cluster['config_version']
160

    
161
  # Add old ssconf keys back to config
162
  logging.info(" - importing ssconf keys")
163
  for key in ('master_node', 'master_ip', 'master_netdev', 'cluster_name'):
164
    if key not in cluster:
165
      cluster[key] = ReadFile(SsconfName(key)).strip()
166

    
167
  if 'default_hypervisor' not in cluster:
168
    old_hyp = ReadFile(SsconfName('hypervisor')).strip()
169
    if old_hyp == "xen-3.0":
170
      hyp = "xen-pvm"
171
    elif old_hyp == "xen-hvm-3.1":
172
      hyp = "xen-hvm"
173
    elif old_hyp == "fake":
174
      hyp = "fake"
175
    else:
176
      raise Error("Unknown old hypervisor name '%s'" % old_hyp)
177

    
178
    logging.info("Setting the default and enabled hypervisor")
179
    cluster['default_hypervisor'] = hyp
180
    cluster['enabled_hypervisors'] = [hyp]
181

    
182
  # hv/be params
183
  if 'hvparams' not in cluster:
184
    logging.info(" - adding hvparams")
185
    cluster['hvparams'] = constants.HVC_DEFAULTS
186
  if 'beparams' not in cluster:
187
    logging.info(" - adding beparams")
188
    cluster['beparams'] = {constants.PP_DEFAULT: constants.BEC_DEFAULTS}
189

    
190
  # file storage
191
  if 'file_storage_dir' not in cluster:
192
    cluster['file_storage_dir'] = constants.DEFAULT_FILE_STORAGE_DIR
193

    
194
  # candidate pool size
195
  if 'candidate_pool_size' not in cluster:
196
    cluster['candidate_pool_size'] = constants.MASTER_POOL_SIZE_DEFAULT
197

    
198

    
199
def Node12To20(node):
200
  """Upgrades a node from 1.2 to 2.0.
201

    
202
  """
203
  logging.info("Upgrading node %s" % node['name'])
204
  if F_SERIAL not in node:
205
    node[F_SERIAL] = 1
206
  if 'master_candidate' not in node:
207
    node['master_candidate'] = True
208
  for key in 'offline', 'drained':
209
    if key not in node:
210
      node[key] = False
211

    
212

    
213
def Instance12To20(drbd_minors, secrets, hypervisor, instance):
214
  """Upgrades an instance from 1.2 to 2.0.
215

    
216
  """
217
  if F_SERIAL not in instance:
218
    instance[F_SERIAL] = 1
219

    
220
  if 'hypervisor' not in instance:
221
    instance['hypervisor'] = hypervisor
222

    
223
  # hvparams changes
224
  if 'hvparams' not in instance:
225
    instance['hvparams'] = hvp = {}
226
  for old, new in INST_HV_CHG.items():
227
    if old in instance:
228
      if (instance[old] is not None and
229
          instance[old] != constants.VALUE_DEFAULT and # no longer valid in 2.0
230
          new in constants.HVC_DEFAULTS[hypervisor]):
231
        hvp[new] = instance[old]
232
      del instance[old]
233

    
234
  # beparams changes
235
  if 'beparams' not in instance:
236
    instance['beparams'] = bep = {}
237
  for old, new in INST_BE_CHG.items():
238
    if old in instance:
239
      if instance[old] is not None:
240
        bep[new] = instance[old]
241
      del instance[old]
242

    
243
  # disk changes
244
  for disk in instance['disks']:
245
    Disk12To20(drbd_minors, secrets, disk)
246

    
247
  # other instance changes
248
  if 'status' in instance:
249
    instance['admin_up'] = instance['status'] == 'up'
250
    del instance['status']
251

    
252

    
253
def Disk12To20(drbd_minors, secrets, disk):
254
  """Upgrades a disk from 1.2 to 2.0.
255

    
256
  """
257
  if 'mode' not in disk:
258
    disk['mode'] = constants.DISK_RDWR
259
  if disk['dev_type'] == constants.LD_DRBD8:
260
    old_lid = disk['logical_id']
261
    for node in old_lid[:2]:
262
      if node not in drbd_minors:
263
        raise Error("Can't find node '%s' while upgrading disk" % node)
264
      drbd_minors[node] += 1
265
      minor = drbd_minors[node]
266
      old_lid.append(minor)
267
    old_lid.append(GenerateSecret(secrets))
268
    del disk['physical_id']
269
  if disk['children']:
270
    for child in disk['children']:
271
      Disk12To20(drbd_minors, secrets, child)
272

    
273

    
274
def main():
275
  """Main program.
276

    
277
  """
278
  global options, args
279

    
280
  program = os.path.basename(sys.argv[0])
281

    
282
  # Option parsing
283
  parser = optparse.OptionParser(usage="%prog [--debug|--verbose] [--force]")
284
  parser.add_option('--dry-run', dest='dry_run',
285
                    action="store_true",
286
                    help="Try to do the conversion, but don't write"
287
                         " output file")
288
  parser.add_option(cli.FORCE_OPT)
289
  parser.add_option(cli.DEBUG_OPT)
290
  parser.add_option(cli.VERBOSE_OPT)
291
  parser.add_option('--path', help="Convert configuration in this"
292
                    " directory instead of '%s'" % constants.DATA_DIR,
293
                    default=constants.DATA_DIR, dest="data_dir")
294
  (options, args) = parser.parse_args()
295

    
296
  # We need to keep filenames locally because they might be renamed between
297
  # versions.
298
  options.CONFIG_DATA_PATH = options.data_dir + "/config.data"
299
  options.SERVER_PEM_PATH = options.data_dir + "/server.pem"
300
  options.KNOWN_HOSTS_PATH = options.data_dir + "/known_hosts"
301
  options.RAPI_CERT_FILE = options.data_dir + "/rapi.pem"
302

    
303
  SetupLogging()
304

    
305
  # Option checking
306
  if args:
307
    raise Error("No arguments expected")
308

    
309
  if not options.force:
310
    usertext = ("%s MUST be run on the master node. Is this the master"
311
                " node and are ALL instances down?" % program)
312
    if not cli.AskUser(usertext):
313
      sys.exit(1)
314

    
315
  # Check whether it's a Ganeti configuration directory
316
  if not (os.path.isfile(options.CONFIG_DATA_PATH) and
317
          os.path.isfile(options.SERVER_PEM_PATH) or
318
          os.path.isfile(options.KNOWN_HOSTS_PATH)):
319
    raise Error(("%s does not seem to be a known Ganeti configuration"
320
                 " directory") % options.data_dir)
321

    
322
  config_version = ReadFile(SsconfName('config_version'), "1.2").strip()
323
  logging.info("Found configuration version %s", config_version)
324

    
325
  config_data = serializer.LoadJson(ReadFile(options.CONFIG_DATA_PATH))
326

    
327
  # Ganeti 1.2?
328
  if config_version == "1.2":
329
    logging.info("Found a Ganeti 1.2 configuration")
330

    
331
    cluster = config_data["cluster"]
332

    
333
    old_config_version = cluster.get("config_version", None)
334
    logging.info("Found old configuration version %s", old_config_version)
335
    if old_config_version not in (3, ):
336
      raise Error("Unsupported configuration version: %s" %
337
                  old_config_version)
338
    if 'version' not in config_data:
339
      config_data['version'] = constants.BuildVersion(2, 0, 0)
340
    if F_SERIAL not in config_data:
341
      config_data[F_SERIAL] = 1
342

    
343
    # Make sure no instance uses remote_raid1 anymore
344
    remote_raid1_instances = []
345
    for instance in config_data["instances"].values():
346
      if instance["disk_template"] == "remote_raid1":
347
        remote_raid1_instances.append(instance["name"])
348
    if remote_raid1_instances:
349
      for name in remote_raid1_instances:
350
        logging.error("Instance %s still using remote_raid1 disk template")
351
      raise Error("Unable to convert configuration as long as there are"
352
                  " instances using remote_raid1 disk template")
353

    
354
    # Build content of new known_hosts file
355
    cluster_name = ReadFile(SsconfName('cluster_name')).rstrip()
356
    cluster_key = cluster['rsahostkeypub']
357
    known_hosts = "%s ssh-rsa %s\n" % (cluster_name, cluster_key)
358

    
359
    Cluster12To20(cluster)
360

    
361
    # Add node attributes
362
    logging.info("Upgrading nodes")
363
    # stable-sort the names to have repeatable runs
364
    for node_name in utils.NiceSort(config_data['nodes'].keys()):
365
      Node12To20(config_data['nodes'][node_name])
366

    
367
    # Instance changes
368
    logging.info("Upgrading instances")
369
    drbd_minors = dict.fromkeys(config_data['nodes'], 0)
370
    secrets = set()
371
    # stable-sort the names to have repeatable runs
372
    for instance_name in utils.NiceSort(config_data['instances'].keys()):
373
      Instance12To20(drbd_minors, secrets, cluster['default_hypervisor'],
374
                     config_data['instances'][instance_name])
375

    
376
  else:
377
    logging.info("Found a Ganeti 2.0 configuration")
378

    
379
    if "config_version" in config_data["cluster"]:
380
      raise Error("Inconsistent configuration: found config_data in"
381
                  " configuration file")
382

    
383
    known_hosts = None
384

    
385
  try:
386
    logging.info("Writing configuration file")
387
    WriteFile(options.CONFIG_DATA_PATH, serializer.DumpJson(config_data))
388

    
389
    if known_hosts is not None:
390
      logging.info("Writing SSH known_hosts file (%s)", known_hosts.strip())
391
      WriteFile(options.KNOWN_HOSTS_PATH, known_hosts)
392

    
393
    if not options.dry_run:
394
      if not os.path.exists(options.RAPI_CERT_FILE):
395
        bootstrap._GenerateSelfSignedSslCert(options.RAPI_CERT_FILE)
396

    
397
  except:
398
    logging.critical("Writing configuration failed. It is proably in an"
399
                     " inconsistent state and needs manual intervention.")
400
    raise
401

    
402
  # test loading the config file
403
  if not options.dry_run:
404
    logging.info("Testing the new config file...")
405
    cfg = config.ConfigWriter(cfg_file=options.CONFIG_DATA_PATH,
406
                              offline=True)
407
    # if we reached this, it's all fine
408
    vrfy = cfg.VerifyConfig()
409
    if vrfy:
410
      logging.error("Errors after conversion:")
411
      for item in vrfy:
412
        logging.error(" - %s" % item)
413
    del cfg
414
    logging.info("File loaded successfully")
415

    
416

    
417
if __name__ == "__main__":
418
  main()
419

    
420
# vim: set foldmethod=marker :