Statistics
| Branch: | Tag: | Revision:

root / tools / cfgupgrade12 @ 88b02ee9

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

    
51

    
52
options = None
53
args = None
54

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

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

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

    
78
# Field names
79
F_SERIAL = 'serial_no'
80

    
81

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

    
86

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

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

    
93

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

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

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

    
111

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

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

    
120

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

    
124
  This is a copy from ConfigWriter.
125

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

    
137

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

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

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

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

    
157

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

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

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

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

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

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

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

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

    
204

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

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

    
218

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

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

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

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

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

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

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

    
258

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

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

    
279

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

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

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

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

    
303
  # We need to keep filenames locally because they might be renamed between
304
  # versions.
305
  options.data_dir = os.path.abspath(options.data_dir)
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
        utils.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()