Statistics
| Branch: | Tag: | Revision:

root / tools / cfgupgrade12 @ 693843f9

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.CONFIG_DATA_PATH = options.data_dir + "/config.data"
306
  options.SERVER_PEM_PATH = options.data_dir + "/server.pem"
307
  options.KNOWN_HOSTS_PATH = options.data_dir + "/known_hosts"
308
  options.RAPI_CERT_FILE = options.data_dir + "/rapi.pem"
309

    
310
  SetupLogging()
311

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

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

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

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

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

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

    
338
    cluster = config_data["cluster"]
339

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

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

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

    
367
    Cluster12To20(cluster)
368

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

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

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

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

    
391
    known_hosts = None
392

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

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

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

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

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

    
413

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

    
417
# vim: set foldmethod=marker :