Statistics
| Branch: | Tag: | Revision:

root / tools / cfgupgrade12 @ 0cddd44d

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
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.data_dir = os.path.abspath(options.data_dir)
307
  options.CONFIG_DATA_PATH = options.data_dir + "/config.data"
308
  options.SERVER_PEM_PATH = options.data_dir + "/server.pem"
309
  options.KNOWN_HOSTS_PATH = options.data_dir + "/known_hosts"
310
  options.RAPI_CERT_FILE = options.data_dir + "/rapi.pem"
311

    
312
  SetupLogging()
313

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

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

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

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

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

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

    
340
    cluster = config_data["cluster"]
341

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

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

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

    
369
    Cluster12To20(cluster)
370

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

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

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

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

    
393
    known_hosts = None
394

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

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

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

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

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

    
415

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

    
419
# vim: set foldmethod=marker :