Statistics
| Branch: | Tag: | Revision:

root / tools / cfgupgrade12 @ effc1b86

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=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 pathutils
51

    
52
from ganeti.utils import version
53

    
54

    
55
options = None
56
args = None
57

    
58
# Unique object to identify calls without default value
59
NoDefault = object()
60

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

    
74
# Instance beparams changes
75
INST_BE_CHG = {
76
  "vcpus": constants.BE_VCPUS,
77
  "memory": constants.BE_MEMORY,
78
  "auto_balance": constants.BE_AUTO_BALANCE,
79
  }
80

    
81
# Field names
82
F_SERIAL = "serial_no"
83

    
84

    
85
class Error(Exception):
86
  """Generic exception"""
87
  pass
88

    
89

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

    
93
  """
94
  return "%s/ssconf_%s" % (options.data_dir, key)
95

    
96

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

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

    
109
  try:
110
    return fh.read()
111
  finally:
112
    fh.close()
113

    
114

    
115
def WriteFile(file_name, data):
116
  """Writes a configuration file.
117

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

    
123

    
124
def GenerateSecret(all_secrets):
125
  """Generate an unique DRBD secret.
126

    
127
  This is a copy from ConfigWriter.
128

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

    
140

    
141
def SetupLogging():
142
  """Configures the logging module.
143

    
144
  """
145
  formatter = logging.Formatter("%(asctime)s: %(message)s")
146

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

    
156
  root_logger = logging.getLogger("")
157
  root_logger.setLevel(logging.NOTSET)
158
  root_logger.addHandler(stderr_handler)
159

    
160

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

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

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

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

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

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

    
199
  # file storage
200
  if "file_storage_dir" not in cluster:
201
    cluster["file_storage_dir"] = pathutils.DEFAULT_FILE_STORAGE_DIR
202

    
203
  # candidate pool size
204
  if "candidate_pool_size" not in cluster:
205
    cluster["candidate_pool_size"] = constants.MASTER_POOL_SIZE_DEFAULT
206

    
207

    
208
def Node12To20(node):
209
  """Upgrades a node from 1.2 to 2.0.
210

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

    
221

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

    
225
  """
226
  if F_SERIAL not in instance:
227
    instance[F_SERIAL] = 1
228

    
229
  if "hypervisor" not in instance:
230
    instance["hypervisor"] = hypervisor
231

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

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

    
252
  # disk changes
253
  for disk in instance["disks"]:
254
    Disk12To20(drbd_minors, secrets, disk)
255

    
256
  # other instance changes
257
  if "status" in instance:
258
    instance["admin_up"] = instance["status"] == "up"
259
    del instance["status"]
260

    
261

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

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

    
282

    
283
def main():
284
  """Main program.
285

    
286
  """
287
  # pylint: disable=W0603
288
  global options, args
289

    
290
  program = os.path.basename(sys.argv[0])
291

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

    
306
  # We need to keep filenames locally because they might be renamed between
307
  # versions.
308
  options.data_dir = os.path.abspath(options.data_dir)
309
  options.CONFIG_DATA_PATH = options.data_dir + "/config.data"
310
  options.SERVER_PEM_PATH = options.data_dir + "/server.pem"
311
  options.KNOWN_HOSTS_PATH = options.data_dir + "/known_hosts"
312
  options.RAPI_CERT_FILE = options.data_dir + "/rapi.pem"
313

    
314
  SetupLogging()
315

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

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

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

    
333
  config_version = ReadFile(SsconfName("config_version"), "1.2").strip()
334
  logging.info("Found configuration version %s", config_version)
335

    
336
  config_data = serializer.LoadJson(ReadFile(options.CONFIG_DATA_PATH))
337

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

    
342
    cluster = config_data["cluster"]
343

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

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

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

    
371
    Cluster12To20(cluster)
372

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

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

    
388
  else:
389
    logging.info("Found a Ganeti 2.0 configuration")
390

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

    
395
    known_hosts = None
396

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

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

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

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

    
415
  logging.info("Configuration file updated.")
416

    
417

    
418
if __name__ == "__main__":
419
  main()