4 # Copyright (C) 2007, 2008, 2009 Google Inc.
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.
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.
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
21 # pylint: disable-msg=C0103,E1103
23 # C0103: invalid name NoDefault
24 # E1103: Instance of 'foor' has no 'bar' member (but some types could
28 """Tool to upgrade the configuration file.
30 This code handles only the types supported by simplejson. As an
31 example, 'set' is a 'list'.
33 @note: this has lots of duplicate content with C{cfgupgrade}. Ideally, it
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
56 # Unique object to identify calls without default value
59 # Dictionary with instance old keys, and new hypervisor keys
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,
72 # Instance beparams changes
74 'vcpus': constants.BE_VCPUS,
75 'memory': constants.BE_MEMORY,
76 'auto_balance': constants.BE_AUTO_BALANCE,
80 F_SERIAL = 'serial_no'
83 class Error(Exception):
84 """Generic exception"""
89 """Returns the file name of an (old) ssconf key.
92 return "%s/ssconf_%s" % (options.data_dir, key)
95 def ReadFile(file_name, default=NoDefault):
99 logging.debug("Reading %s", file_name)
101 fh = open(file_name, 'r')
103 if default is not NoDefault and err.errno == errno.ENOENT:
113 def WriteFile(file_name, data):
114 """Writes a configuration file.
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)
122 def GenerateSecret(all_secrets):
123 """Generate an unique DRBD secret.
125 This is a copy from ConfigWriter.
130 secret = utils.GenerateSecret()
131 if secret not in all_secrets:
135 raise Error("Can't generate unique DRBD secret")
140 """Configures the logging module.
143 formatter = logging.Formatter("%(asctime)s: %(message)s")
145 stderr_handler = logging.StreamHandler()
146 stderr_handler.setFormatter(formatter)
148 stderr_handler.setLevel(logging.NOTSET)
149 elif options.verbose:
150 stderr_handler.setLevel(logging.INFO)
152 stderr_handler.setLevel(logging.CRITICAL)
154 root_logger = logging.getLogger("")
155 root_logger.setLevel(logging.NOTSET)
156 root_logger.addHandler(stderr_handler)
159 def Cluster12To20(cluster):
160 """Upgrades the cluster object from 1.2 to 2.0.
163 logging.info("Upgrading the cluster object")
164 # Upgrade the configuration version
165 if 'config_version' in cluster:
166 del cluster['config_version']
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()
174 if 'default_hypervisor' not in cluster:
175 old_hyp = ReadFile(SsconfName('hypervisor')).strip()
176 if old_hyp == "xen-3.0":
178 elif old_hyp == "xen-hvm-3.1":
180 elif old_hyp == "fake":
183 raise Error("Unknown old hypervisor name '%s'" % old_hyp)
185 logging.info("Setting the default and enabled hypervisor")
186 cluster['default_hypervisor'] = hyp
187 cluster['enabled_hypervisors'] = [hyp]
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}
198 if 'file_storage_dir' not in cluster:
199 cluster['file_storage_dir'] = constants.DEFAULT_FILE_STORAGE_DIR
201 # candidate pool size
202 if 'candidate_pool_size' not in cluster:
203 cluster['candidate_pool_size'] = constants.MASTER_POOL_SIZE_DEFAULT
206 def Node12To20(node):
207 """Upgrades a node from 1.2 to 2.0.
210 logging.info("Upgrading node %s", node['name'])
211 if F_SERIAL not in node:
213 if 'master_candidate' not in node:
214 node['master_candidate'] = True
215 for key in 'offline', 'drained':
220 def Instance12To20(drbd_minors, secrets, hypervisor, instance):
221 """Upgrades an instance from 1.2 to 2.0.
224 if F_SERIAL not in instance:
225 instance[F_SERIAL] = 1
227 if 'hypervisor' not in instance:
228 instance['hypervisor'] = hypervisor
231 if 'hvparams' not in instance:
232 instance['hvparams'] = hvp = {}
233 for old, new in INST_HV_CHG.items():
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]
242 if 'beparams' not in instance:
243 instance['beparams'] = bep = {}
244 for old, new in INST_BE_CHG.items():
246 if instance[old] is not None:
247 bep[new] = instance[old]
251 for disk in instance['disks']:
252 Disk12To20(drbd_minors, secrets, disk)
254 # other instance changes
255 if 'status' in instance:
256 instance['admin_up'] = instance['status'] == 'up'
257 del instance['status']
260 def Disk12To20(drbd_minors, secrets, disk):
261 """Upgrades a disk from 1.2 to 2.0.
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']
277 for child in disk['children']:
278 Disk12To20(drbd_minors, secrets, child)
285 # pylint: disable-msg=W0603
288 program = os.path.basename(sys.argv[0])
291 parser = optparse.OptionParser(usage="%prog [--debug|--verbose] [--force]")
292 parser.add_option('--dry-run', dest='dry_run',
294 help="Try to do the conversion, but don't write"
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()
304 # We need to keep filenames locally because they might be renamed between
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"
315 raise Error("No arguments expected")
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):
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)
330 config_version = ReadFile(SsconfName('config_version'), "1.2").strip()
331 logging.info("Found configuration version %s", config_version)
333 config_data = serializer.LoadJson(ReadFile(options.CONFIG_DATA_PATH))
336 if config_version == "1.2":
337 logging.info("Found a Ganeti 1.2 configuration")
339 cluster = config_data["cluster"]
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" %
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
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",
360 raise Error("Unable to convert configuration as long as there are"
361 " instances using remote_raid1 disk template")
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)
368 Cluster12To20(cluster)
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])
377 logging.info("Upgrading instances")
378 drbd_minors = dict.fromkeys(config_data['nodes'], 0)
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])
386 logging.info("Found a Ganeti 2.0 configuration")
388 if "config_version" in config_data["cluster"]:
389 raise Error("Inconsistent configuration: found config_data in"
390 " configuration file")
395 logging.info("Writing configuration file")
396 WriteFile(options.CONFIG_DATA_PATH, serializer.DumpJson(config_data))
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)
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 bootstrap.GenerateSelfSignedSslCert(options.RAPI_CERT_FILE)
408 logging.critical("Writing configuration failed. It is probably in an"
409 " inconsistent state and needs manual intervention.")
412 logging.info("Configuration file updated.")
415 if __name__ == "__main__":
418 # vim: set foldmethod=marker :