Revision b5672ea0

b/Makefile.am
234 234
	tools/burnin \
235 235
	tools/cfgshell \
236 236
	tools/cfgupgrade \
237
	tools/cfgupgrade12 \
237 238
	tools/cluster-merge \
238 239
	tools/lvmstrap \
239 240
	tools/sanitize-config
b/doc/admin.rst
1304 1304
More information about the upgrade procedure is listed on the wiki at
1305 1305
http://code.google.com/p/ganeti/wiki/UpgradeNotes.
1306 1306

  
1307
There is also a script designed to upgrade from Ganeti 1.2 to 2.0,
1308
called ``cfgupgrade12``.
1309

  
1307 1310
cfgshell
1308 1311
++++++++
1309 1312

  
b/tools/cfgupgrade12
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
# pylint: disable-msg=C0103,E1103
22

  
23
# C0103: invalid name NoDefault
24
# E1103: Instance of 'foor' has no 'bar' member (but some types could
25
# not be inferred)
26

  
27

  
28
"""Tool to upgrade the configuration file.
29

  
30
This code handles only the types supported by simplejson. As an
31
example, 'set' is a 'list'.
32

  
33
@note: this has lots of duplicate content with C{cfgupgrade}. Ideally, it
34
should be merged.
35

  
36
"""
37

  
38

  
39
import os
40
import os.path
41
import sys
42
import optparse
43
import logging
44
import errno
45

  
46
from ganeti import constants
47
from ganeti import serializer
48
from ganeti import utils
49
from ganeti import cli
50
from ganeti import bootstrap
51

  
52

  
53
options = None
54
args = None
55

  
56
# Unique object to identify calls without default value
57
NoDefault = object()
58

  
59
# Dictionary with instance old keys, and new hypervisor keys
60
INST_HV_CHG = {
61
  'hvm_pae': constants.HV_PAE,
62
  'vnc_bind_address': constants.HV_VNC_BIND_ADDRESS,
63
  'initrd_path': constants.HV_INITRD_PATH,
64
  'hvm_nic_type': constants.HV_NIC_TYPE,
65
  'kernel_path': constants.HV_KERNEL_PATH,
66
  'hvm_acpi': constants.HV_ACPI,
67
  'hvm_cdrom_image_path': constants.HV_CDROM_IMAGE_PATH,
68
  'hvm_boot_order': constants.HV_BOOT_ORDER,
69
  'hvm_disk_type': constants.HV_DISK_TYPE,
70
  }
71

  
72
# Instance beparams changes
73
INST_BE_CHG = {
74
  'vcpus': constants.BE_VCPUS,
75
  'memory': constants.BE_MEMORY,
76
  'auto_balance': constants.BE_AUTO_BALANCE,
77
  }
78

  
79
# Field names
80
F_SERIAL = 'serial_no'
81

  
82

  
83
class Error(Exception):
84
  """Generic exception"""
85
  pass
86

  
87

  
88
def SsconfName(key):
89
  """Returns the file name of an (old) ssconf key.
90

  
91
  """
92
  return "%s/ssconf_%s" % (options.data_dir, key)
93

  
94

  
95
def ReadFile(file_name, default=NoDefault):
96
  """Reads a file.
97

  
98
  """
99
  logging.debug("Reading %s", file_name)
100
  try:
101
    fh = open(file_name, 'r')
102
  except IOError, err:
103
    if default is not NoDefault and err.errno == errno.ENOENT:
104
      return default
105
    raise
106

  
107
  try:
108
    return fh.read()
109
  finally:
110
    fh.close()
111

  
112

  
113
def WriteFile(file_name, data):
114
  """Writes a configuration file.
115

  
116
  """
117
  logging.debug("Writing %s", file_name)
118
  utils.WriteFile(file_name=file_name, data=data, mode=0600,
119
                  dry_run=options.dry_run, backup=True)
120

  
121

  
122
def GenerateSecret(all_secrets):
123
  """Generate an unique DRBD secret.
124

  
125
  This is a copy from ConfigWriter.
126

  
127
  """
128
  retries = 64
129
  while retries > 0:
130
    secret = utils.GenerateSecret()
131
    if secret not in all_secrets:
132
      break
133
    retries -= 1
134
  else:
135
    raise Error("Can't generate unique DRBD secret")
136
  return secret
137

  
138

  
139
def SetupLogging():
140
  """Configures the logging module.
141

  
142
  """
143
  formatter = logging.Formatter("%(asctime)s: %(message)s")
144

  
145
  stderr_handler = logging.StreamHandler()
146
  stderr_handler.setFormatter(formatter)
147
  if options.debug:
148
    stderr_handler.setLevel(logging.NOTSET)
149
  elif options.verbose:
150
    stderr_handler.setLevel(logging.INFO)
151
  else:
152
    stderr_handler.setLevel(logging.CRITICAL)
153

  
154
  root_logger = logging.getLogger("")
155
  root_logger.setLevel(logging.NOTSET)
156
  root_logger.addHandler(stderr_handler)
157

  
158

  
159
def Cluster12To20(cluster):
160
  """Upgrades the cluster object from 1.2 to 2.0.
161

  
162
  """
163
  logging.info("Upgrading the cluster object")
164
  # Upgrade the configuration version
165
  if 'config_version' in cluster:
166
    del cluster['config_version']
167

  
168
  # Add old ssconf keys back to config
169
  logging.info(" - importing ssconf keys")
170
  for key in ('master_node', 'master_ip', 'master_netdev', 'cluster_name'):
171
    if key not in cluster:
172
      cluster[key] = ReadFile(SsconfName(key)).strip()
173

  
174
  if 'default_hypervisor' not in cluster:
175
    old_hyp = ReadFile(SsconfName('hypervisor')).strip()
176
    if old_hyp == "xen-3.0":
177
      hyp = "xen-pvm"
178
    elif old_hyp == "xen-hvm-3.1":
179
      hyp = "xen-hvm"
180
    elif old_hyp == "fake":
181
      hyp = "fake"
182
    else:
183
      raise Error("Unknown old hypervisor name '%s'" % old_hyp)
184

  
185
    logging.info("Setting the default and enabled hypervisor")
186
    cluster['default_hypervisor'] = hyp
187
    cluster['enabled_hypervisors'] = [hyp]
188

  
189
  # hv/be params
190
  if 'hvparams' not in cluster:
191
    logging.info(" - adding hvparams")
192
    cluster['hvparams'] = constants.HVC_DEFAULTS
193
  if 'beparams' not in cluster:
194
    logging.info(" - adding beparams")
195
    cluster['beparams'] = {constants.PP_DEFAULT: constants.BEC_DEFAULTS}
196

  
197
  # file storage
198
  if 'file_storage_dir' not in cluster:
199
    cluster['file_storage_dir'] = constants.DEFAULT_FILE_STORAGE_DIR
200

  
201
  # candidate pool size
202
  if 'candidate_pool_size' not in cluster:
203
    cluster['candidate_pool_size'] = constants.MASTER_POOL_SIZE_DEFAULT
204

  
205

  
206
def Node12To20(node):
207
  """Upgrades a node from 1.2 to 2.0.
208

  
209
  """
210
  logging.info("Upgrading node %s", node['name'])
211
  if F_SERIAL not in node:
212
    node[F_SERIAL] = 1
213
  if 'master_candidate' not in node:
214
    node['master_candidate'] = True
215
  for key in 'offline', 'drained':
216
    if key not in node:
217
      node[key] = False
218

  
219

  
220
def Instance12To20(drbd_minors, secrets, hypervisor, instance):
221
  """Upgrades an instance from 1.2 to 2.0.
222

  
223
  """
224
  if F_SERIAL not in instance:
225
    instance[F_SERIAL] = 1
226

  
227
  if 'hypervisor' not in instance:
228
    instance['hypervisor'] = hypervisor
229

  
230
  # hvparams changes
231
  if 'hvparams' not in instance:
232
    instance['hvparams'] = hvp = {}
233
  for old, new in INST_HV_CHG.items():
234
    if old in instance:
235
      if (instance[old] is not None and
236
          instance[old] != constants.VALUE_DEFAULT and # no longer valid in 2.0
237
          new in constants.HVC_DEFAULTS[hypervisor]):
238
        hvp[new] = instance[old]
239
      del instance[old]
240

  
241
  # beparams changes
242
  if 'beparams' not in instance:
243
    instance['beparams'] = bep = {}
244
  for old, new in INST_BE_CHG.items():
245
    if old in instance:
246
      if instance[old] is not None:
247
        bep[new] = instance[old]
248
      del instance[old]
249

  
250
  # disk changes
251
  for disk in instance['disks']:
252
    Disk12To20(drbd_minors, secrets, disk)
253

  
254
  # other instance changes
255
  if 'status' in instance:
256
    instance['admin_up'] = instance['status'] == 'up'
257
    del instance['status']
258

  
259

  
260
def Disk12To20(drbd_minors, secrets, disk):
261
  """Upgrades a disk from 1.2 to 2.0.
262

  
263
  """
264
  if 'mode' not in disk:
265
    disk['mode'] = constants.DISK_RDWR
266
  if disk['dev_type'] == constants.LD_DRBD8:
267
    old_lid = disk['logical_id']
268
    for node in old_lid[:2]:
269
      if node not in drbd_minors:
270
        raise Error("Can't find node '%s' while upgrading disk" % node)
271
      drbd_minors[node] += 1
272
      minor = drbd_minors[node]
273
      old_lid.append(minor)
274
    old_lid.append(GenerateSecret(secrets))
275
    del disk['physical_id']
276
  if disk['children']:
277
    for child in disk['children']:
278
      Disk12To20(drbd_minors, secrets, child)
279

  
280

  
281
def main():
282
  """Main program.
283

  
284
  """
285
  # pylint: disable-msg=W0603
286
  global options, args
287

  
288
  program = os.path.basename(sys.argv[0])
289

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

  
304
  # We need to keep filenames locally because they might be renamed between
305
  # versions.
306
  options.CONFIG_DATA_PATH = options.data_dir + "/config.data"
307
  options.SERVER_PEM_PATH = options.data_dir + "/server.pem"
308
  options.KNOWN_HOSTS_PATH = options.data_dir + "/known_hosts"
309
  options.RAPI_CERT_FILE = options.data_dir + "/rapi.pem"
310

  
311
  SetupLogging()
312

  
313
  # Option checking
314
  if args:
315
    raise Error("No arguments expected")
316

  
317
  if not options.force:
318
    usertext = ("%s MUST be run on the master node. Is this the master"
319
                " node and are ALL instances down?" % program)
320
    if not cli.AskUser(usertext):
321
      sys.exit(1)
322

  
323
  # Check whether it's a Ganeti configuration directory
324
  if not (os.path.isfile(options.CONFIG_DATA_PATH) and
325
          os.path.isfile(options.SERVER_PEM_PATH) or
326
          os.path.isfile(options.KNOWN_HOSTS_PATH)):
327
    raise Error(("%s does not seem to be a known Ganeti configuration"
328
                 " directory") % options.data_dir)
329

  
330
  config_version = ReadFile(SsconfName('config_version'), "1.2").strip()
331
  logging.info("Found configuration version %s", config_version)
332

  
333
  config_data = serializer.LoadJson(ReadFile(options.CONFIG_DATA_PATH))
334

  
335
  # Ganeti 1.2?
336
  if config_version == "1.2":
337
    logging.info("Found a Ganeti 1.2 configuration")
338

  
339
    cluster = config_data["cluster"]
340

  
341
    old_config_version = cluster.get("config_version", None)
342
    logging.info("Found old configuration version %s", old_config_version)
343
    if old_config_version not in (3, ):
344
      raise Error("Unsupported configuration version: %s" %
345
                  old_config_version)
346
    if 'version' not in config_data:
347
      config_data['version'] = constants.BuildVersion(2, 0, 0)
348
    if F_SERIAL not in config_data:
349
      config_data[F_SERIAL] = 1
350

  
351
    # Make sure no instance uses remote_raid1 anymore
352
    remote_raid1_instances = []
353
    for instance in config_data["instances"].values():
354
      if instance["disk_template"] == "remote_raid1":
355
        remote_raid1_instances.append(instance["name"])
356
    if remote_raid1_instances:
357
      for name in remote_raid1_instances:
358
        logging.error("Instance %s still using remote_raid1 disk template",
359
                      name)
360
      raise Error("Unable to convert configuration as long as there are"
361
                  " instances using remote_raid1 disk template")
362

  
363
    # Build content of new known_hosts file
364
    cluster_name = ReadFile(SsconfName('cluster_name')).rstrip()
365
    cluster_key = cluster['rsahostkeypub']
366
    known_hosts = "%s ssh-rsa %s\n" % (cluster_name, cluster_key)
367

  
368
    Cluster12To20(cluster)
369

  
370
    # Add node attributes
371
    logging.info("Upgrading nodes")
372
    # stable-sort the names to have repeatable runs
373
    for node_name in utils.NiceSort(config_data['nodes'].keys()):
374
      Node12To20(config_data['nodes'][node_name])
375

  
376
    # Instance changes
377
    logging.info("Upgrading instances")
378
    drbd_minors = dict.fromkeys(config_data['nodes'], 0)
379
    secrets = set()
380
    # stable-sort the names to have repeatable runs
381
    for instance_name in utils.NiceSort(config_data['instances'].keys()):
382
      Instance12To20(drbd_minors, secrets, cluster['default_hypervisor'],
383
                     config_data['instances'][instance_name])
384

  
385
  else:
386
    logging.info("Found a Ganeti 2.0 configuration")
387

  
388
    if "config_version" in config_data["cluster"]:
389
      raise Error("Inconsistent configuration: found config_data in"
390
                  " configuration file")
391

  
392
    known_hosts = None
393

  
394
  try:
395
    logging.info("Writing configuration file")
396
    WriteFile(options.CONFIG_DATA_PATH, serializer.DumpJson(config_data))
397

  
398
    if known_hosts is not None:
399
      logging.info("Writing SSH known_hosts file (%s)", known_hosts.strip())
400
      WriteFile(options.KNOWN_HOSTS_PATH, known_hosts)
401

  
402
    if not options.dry_run:
403
      if not os.path.exists(options.RAPI_CERT_FILE):
404
        logging.debug("Writing RAPI certificate to %s", options.RAPI_CERT_FILE)
405
        bootstrap.GenerateSelfSignedSslCert(options.RAPI_CERT_FILE)
406

  
407
  except:
408
    logging.critical("Writing configuration failed. It is probably in an"
409
                     " inconsistent state and needs manual intervention.")
410
    raise
411

  
412
  logging.info("Configuration file updated.")
413

  
414

  
415
if __name__ == "__main__":
416
  main()
417

  
418
# vim: set foldmethod=marker :

Also available in: Unified diff