Statistics
| Branch: | Tag: | Revision:

root / tools / cfgupgrade @ ac4d25b6

History | View | Annotate | Download (12.2 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
class Error(Exception):
73
  """Generic exception"""
74
  pass
75

    
76

    
77
def SsconfName(key):
78
  """Returns the file name of an (old) ssconf key.
79

    
80
  """
81
  return "%s/ssconf_%s" % (options.data_dir, key)
82

    
83

    
84
def ReadFile(file_name, default=NoDefault):
85
  """Reads a file.
86

    
87
  """
88
  logging.debug("Reading %s", file_name)
89
  try:
90
    fh = open(file_name, 'r')
91
  except IOError, err:
92
    if default is not NoDefault and err.errno == errno.ENOENT:
93
      return default
94
    raise
95

    
96
  try:
97
    return fh.read()
98
  finally:
99
    fh.close()
100

    
101

    
102
def WriteFile(file_name, data):
103
  """Writes a configuration file.
104

    
105
  """
106
  logging.debug("Writing %s", file_name)
107
  utils.WriteFile(file_name=file_name, data=data, mode=0600,
108
                  dry_run=options.dry_run, backup=True)
109

    
110

    
111
def GenerateSecret(all_secrets):
112
  """Generate an unique DRBD secret.
113

    
114
  This is a copy from ConfigWriter.
115

    
116
  """
117
  retries = 64
118
  while retries > 0:
119
    secret = utils.GenerateSecret()
120
    if secret not in all_secrets:
121
      break
122
    retries -= 1
123
  else:
124
    raise Error("Can't generate unique DRBD secret")
125
  return secret
126

    
127

    
128
def SetupLogging():
129
  """Configures the logging module.
130

    
131
  """
132
  formatter = logging.Formatter("%(asctime)s: %(message)s")
133

    
134
  stderr_handler = logging.StreamHandler()
135
  stderr_handler.setFormatter(formatter)
136
  if options.debug:
137
    stderr_handler.setLevel(logging.NOTSET)
138
  elif options.verbose:
139
    stderr_handler.setLevel(logging.INFO)
140
  else:
141
    stderr_handler.setLevel(logging.CRITICAL)
142

    
143
  root_logger = logging.getLogger("")
144
  root_logger.setLevel(logging.NOTSET)
145
  root_logger.addHandler(stderr_handler)
146

    
147

    
148
def Cluster12To20(cluster):
149
  """Upgrades the cluster object from 1.2 to 2.0.
150

    
151
  """
152
  logging.info("Upgrading the cluster object")
153
  # Upgrade the configuration version
154
  if 'config_version' in cluster:
155
    del cluster['config_version']
156

    
157
  # Add old ssconf keys back to config
158
  logging.info(" - importing ssconf keys")
159
  for key in ('master_node', 'master_ip', 'master_netdev', 'cluster_name'):
160
    if key not in cluster:
161
      cluster[key] = ReadFile(SsconfName(key)).strip()
162

    
163
  if 'default_hypervisor' not in cluster:
164
    old_hyp = ReadFile(SsconfName('hypervisor')).strip()
165
    if old_hyp == "xen-3.0":
166
      hyp = "xen-pvm"
167
    elif old_hyp == "xen-hvm-3.1":
168
      hyp = "xen-hvm"
169
    elif old_hyp == "fake":
170
      hyp = "fake"
171
    else:
172
      raise Error("Unknown old hypervisor name '%s'" % old_hyp)
173

    
174
    logging.info("Setting the default and enabled hypervisor")
175
    cluster['default_hypervisor'] = hyp
176
    cluster['enabled_hypervisors'] = [hyp]
177

    
178
  # hv/be params
179
  if 'hvparams' not in cluster:
180
    logging.info(" - adding hvparams")
181
    cluster['hvparams'] = constants.HVC_DEFAULTS
182
  if 'beparams' not in cluster:
183
    logging.info(" - adding beparams")
184
    cluster['beparams'] = {constants.BEGR_DEFAULT: constants.BEC_DEFAULTS}
185

    
186
  # file storage
187
  if 'file_storage_dir' not in cluster:
188
    cluster['file_storage_dir'] = constants.DEFAULT_FILE_STORAGE_DIR
189

    
190
  # candidate pool size
191
  if 'candidate_pool_size' not in cluster:
192
    cluster['candidate_pool_size'] = constants.MASTER_POOL_SIZE_DEFAULT
193

    
194

    
195
def Node12To20(node):
196
  """Upgrades a node from 1.2 to 2.0.
197

    
198
  """
199
  logging.info("Upgrading node %s" % node['name'])
200
  if 'serial_no' not in node:
201
    node['serial_no'] = 1
202
  if 'master_candidate' not in node:
203
    node['master_candidate'] = True
204
  for key in 'offline', 'drained':
205
    if key not in node:
206
      node[key] = False
207

    
208

    
209
def Instance12To20(drbd_minors, secrets, hypervisor, instance):
210
  """Upgrades an instance from 1.2 to 2.0.
211

    
212
  """
213
  if 'hypervisor' not in instance:
214
    instance['hypervisor'] = hypervisor
215

    
216
  # hvparams changes
217
  if 'hvparams' not in instance:
218
    instance['hvparams'] = hvp = {}
219
  for old, new in INST_HV_CHG.items():
220
    if old in instance:
221
      if (instance[old] is not None and
222
          instance[old] != constants.VALUE_DEFAULT and # no longer valid in 2.0
223
          new in constants.HVC_DEFAULTS[hypervisor]):
224
        hvp[new] = instance[old]
225
      del instance[old]
226

    
227
  # beparams changes
228
  if 'beparams' not in instance:
229
    instance['beparams'] = bep = {}
230
  for old, new in INST_BE_CHG.items():
231
    if old in instance:
232
      if instance[old] is not None:
233
        bep[new] = instance[old]
234
      del instance[old]
235

    
236
  # disk changes
237
  for disk in instance['disks']:
238
    Disk12To20(drbd_minors, secrets, disk)
239

    
240
  # other instance changes
241
  if 'status' in instance:
242
    instance['admin_up'] = instance['status'] == 'up'
243
    del instance['status']
244

    
245

    
246
def Disk12To20(drbd_minors, secrets, disk):
247
  """Upgrades a disk from 1.2 to 2.0.
248

    
249
  """
250
  if 'mode' not in disk:
251
    disk['mode'] = constants.DISK_RDWR
252
  if disk['dev_type'] == constants.LD_DRBD8:
253
    old_lid = disk['logical_id']
254
    for node in old_lid[:2]:
255
      if node not in drbd_minors:
256
        raise Error("Can't find node '%s' while upgrading disk" % node)
257
      drbd_minors[node] += 1
258
      minor = drbd_minors[node]
259
      old_lid.append(minor)
260
    old_lid.append(GenerateSecret(secrets))
261
    del disk['physical_id']
262
  if disk['children']:
263
    for child in disk['children']:
264
      Disk12To20(drbd_minors, secrets, child)
265

    
266

    
267
def main():
268
  """Main program.
269

    
270
  """
271
  global options, args
272

    
273
  program = os.path.basename(sys.argv[0])
274

    
275
  # Option parsing
276
  parser = optparse.OptionParser(usage="%prog [--debug|--verbose] [--force]")
277
  parser.add_option('--dry-run', dest='dry_run',
278
                    action="store_true",
279
                    help="Try to do the conversion, but don't write"
280
                         " output file")
281
  parser.add_option(cli.FORCE_OPT)
282
  parser.add_option(cli.DEBUG_OPT)
283
  parser.add_option('-v', '--verbose', dest='verbose',
284
                    action="store_true",
285
                    help="Verbose output")
286
  parser.add_option('--path', help="Convert configuration in this"
287
                    " directory instead of '%s'" % constants.DATA_DIR,
288
                    default=constants.DATA_DIR, dest="data_dir")
289
  (options, args) = parser.parse_args()
290

    
291
  # We need to keep filenames locally because they might be renamed between
292
  # versions.
293
  options.CONFIG_DATA_PATH = options.data_dir + "/config.data"
294
  options.SERVER_PEM_PATH = options.data_dir + "/server.pem"
295
  options.KNOWN_HOSTS_PATH = options.data_dir + "/known_hosts"
296
  options.RAPI_CERT_FILE = options.data_dir + "/rapi.pem"
297

    
298
  SetupLogging()
299

    
300
  # Option checking
301
  if args:
302
    raise Error("No arguments expected")
303

    
304
  if not options.force:
305
    usertext = ("%s MUST be run on the master node. Is this the master"
306
                " node and are ALL instances down?" % program)
307
    if not cli.AskUser(usertext):
308
      sys.exit(1)
309

    
310
  # Check whether it's a Ganeti configuration directory
311
  if not (os.path.isfile(options.CONFIG_DATA_PATH) and
312
          os.path.isfile(options.SERVER_PEM_PATH) or
313
          os.path.isfile(options.KNOWN_HOSTS_PATH)):
314
    raise Error(("%s does not seem to be a known Ganeti configuration"
315
                 " directory") % options.data_dir)
316

    
317
  config_version = ReadFile(SsconfName('config_version'), "1.2").strip()
318
  logging.info("Found configuration version %s", config_version)
319

    
320
  config_data = serializer.LoadJson(ReadFile(options.CONFIG_DATA_PATH))
321

    
322
  # Ganeti 1.2?
323
  if config_version == "1.2":
324
    logging.info("Found a Ganeti 1.2 configuration")
325

    
326
    cluster = config_data["cluster"]
327

    
328
    old_config_version = cluster.get("config_version", None)
329
    logging.info("Found old configuration version %s", old_config_version)
330
    if old_config_version not in (3, ):
331
      raise Error("Unsupported configuration version: %s" %
332
                  old_config_version)
333
    if 'version' not in config_data:
334
      config_data['version'] = constants.BuildVersion(2, 0, 0)
335
    if 'serial_no' not in config_data:
336
      config_data['serial_no'] = 1
337

    
338
    # Make sure no instance uses remote_raid1 anymore
339
    remote_raid1_instances = []
340
    for instance in config_data["instances"].values():
341
      if instance["disk_template"] == "remote_raid1":
342
        remote_raid1_instances.append(instance["name"])
343
    if remote_raid1_instances:
344
      for name in remote_raid1_instances:
345
        logging.error("Instance %s still using remote_raid1 disk template")
346
      raise Error("Unable to convert configuration as long as there are"
347
                  " instances using remote_raid1 disk template")
348

    
349
    # Build content of new known_hosts file
350
    cluster_name = ReadFile(SsconfName('cluster_name')).rstrip()
351
    cluster_key = cluster['rsahostkeypub']
352
    known_hosts = "%s ssh-rsa %s\n" % (cluster_name, cluster_key)
353

    
354
    Cluster12To20(cluster)
355

    
356
    # Add node attributes
357
    logging.info("Upgrading nodes")
358
    # stable-sort the names to have repeatable runs
359
    for node_name in utils.NiceSort(config_data['nodes'].keys()):
360
      Node12To20(config_data['nodes'][node_name])
361

    
362
    # Instance changes
363
    logging.info("Upgrading instances")
364
    drbd_minors = dict.fromkeys(config_data['nodes'], 0)
365
    secrets = set()
366
    # stable-sort the names to have repeatable runs
367
    for instance_name in utils.NiceSort(config_data['instances'].keys()):
368
      Instance12To20(drbd_minors, secrets, cluster['default_hypervisor'],
369
                     config_data['instances'][instance_name])
370

    
371
  else:
372
    logging.info("Found a Ganeti 2.0 configuration")
373

    
374
    if "config_version" in config_data["cluster"]:
375
      raise Error("Inconsistent configuration: found config_data in"
376
                  " configuration file")
377

    
378
    known_hosts = None
379

    
380
  try:
381
    logging.info("Writing configuration file")
382
    WriteFile(options.CONFIG_DATA_PATH, serializer.DumpJson(config_data))
383

    
384
    if known_hosts is not None:
385
      logging.info("Writing SSH known_hosts file (%s)", known_hosts.strip())
386
      WriteFile(options.KNOWN_HOSTS_PATH, known_hosts)
387

    
388
    if not options.dry_run:
389
      if not os.path.exists(options.RAPI_CERT_FILE):
390
        bootstrap._GenerateSelfSignedSslCert(options.RAPI_CERT_FILE)
391

    
392
  except:
393
    logging.critical("Writing configuration failed. It is proably in an"
394
                     " inconsistent state and needs manual intervention.")
395
    raise
396

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

    
411

    
412
if __name__ == "__main__":
413
  main()
414

    
415
# vim: set foldmethod=marker :