Statistics
| Branch: | Tag: | Revision:

root / tools / cfgupgrade @ c118d1f4

History | View | Annotate | Download (12.3 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.BEGR_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('-v', '--verbose', dest='verbose',
291
                    action="store_true",
292
                    help="Verbose output")
293
  parser.add_option('--path', help="Convert configuration in this"
294
                    " directory instead of '%s'" % constants.DATA_DIR,
295
                    default=constants.DATA_DIR, dest="data_dir")
296
  (options, args) = parser.parse_args()
297

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

    
305
  SetupLogging()
306

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

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

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

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

    
327
  config_data = serializer.LoadJson(ReadFile(options.CONFIG_DATA_PATH))
328

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

    
333
    cluster = config_data["cluster"]
334

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

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

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

    
361
    Cluster12To20(cluster)
362

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

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

    
378
  else:
379
    logging.info("Found a Ganeti 2.0 configuration")
380

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

    
385
    known_hosts = None
386

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

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

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

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

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

    
418

    
419
if __name__ == "__main__":
420
  main()
421

    
422
# vim: set foldmethod=marker :