Be paranoid about existing keys in cfgupgrade
[ganeti-local] / tools / cfgupgrade12
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()