Re-indent kvm-ifup.in
[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
51
52 options = None
53 args = None
54
55 # Unique object to identify calls without default value
56 NoDefault = object()
57
58 # Dictionary with instance old keys, and new hypervisor keys
59 INST_HV_CHG = {
60   "hvm_pae": constants.HV_PAE,
61   "vnc_bind_address": constants.HV_VNC_BIND_ADDRESS,
62   "initrd_path": constants.HV_INITRD_PATH,
63   "hvm_nic_type": constants.HV_NIC_TYPE,
64   "kernel_path": constants.HV_KERNEL_PATH,
65   "hvm_acpi": constants.HV_ACPI,
66   "hvm_cdrom_image_path": constants.HV_CDROM_IMAGE_PATH,
67   "hvm_boot_order": constants.HV_BOOT_ORDER,
68   "hvm_disk_type": constants.HV_DISK_TYPE,
69   }
70
71 # Instance beparams changes
72 INST_BE_CHG = {
73   "vcpus": constants.BE_VCPUS,
74   "memory": constants.BE_MEMORY,
75   "auto_balance": constants.BE_AUTO_BALANCE,
76   }
77
78 # Field names
79 F_SERIAL = "serial_no"
80
81
82 class Error(Exception):
83   """Generic exception"""
84   pass
85
86
87 def SsconfName(key):
88   """Returns the file name of an (old) ssconf key.
89
90   """
91   return "%s/ssconf_%s" % (options.data_dir, key)
92
93
94 def ReadFile(file_name, default=NoDefault):
95   """Reads a file.
96
97   """
98   logging.debug("Reading %s", file_name)
99   try:
100     fh = open(file_name, "r")
101   except IOError, err:
102     if default is not NoDefault and err.errno == errno.ENOENT:
103       return default
104     raise
105
106   try:
107     return fh.read()
108   finally:
109     fh.close()
110
111
112 def WriteFile(file_name, data):
113   """Writes a configuration file.
114
115   """
116   logging.debug("Writing %s", file_name)
117   utils.WriteFile(file_name=file_name, data=data, mode=0600,
118                   dry_run=options.dry_run, backup=True)
119
120
121 def GenerateSecret(all_secrets):
122   """Generate an unique DRBD secret.
123
124   This is a copy from ConfigWriter.
125
126   """
127   retries = 64
128   while retries > 0:
129     secret = utils.GenerateSecret()
130     if secret not in all_secrets:
131       break
132     retries -= 1
133   else:
134     raise Error("Can't generate unique DRBD secret")
135   return secret
136
137
138 def SetupLogging():
139   """Configures the logging module.
140
141   """
142   formatter = logging.Formatter("%(asctime)s: %(message)s")
143
144   stderr_handler = logging.StreamHandler()
145   stderr_handler.setFormatter(formatter)
146   if options.debug:
147     stderr_handler.setLevel(logging.NOTSET)
148   elif options.verbose:
149     stderr_handler.setLevel(logging.INFO)
150   else:
151     stderr_handler.setLevel(logging.CRITICAL)
152
153   root_logger = logging.getLogger("")
154   root_logger.setLevel(logging.NOTSET)
155   root_logger.addHandler(stderr_handler)
156
157
158 def Cluster12To20(cluster):
159   """Upgrades the cluster object from 1.2 to 2.0.
160
161   """
162   logging.info("Upgrading the cluster object")
163   # Upgrade the configuration version
164   if "config_version" in cluster:
165     del cluster["config_version"]
166
167   # Add old ssconf keys back to config
168   logging.info(" - importing ssconf keys")
169   for key in ("master_node", "master_ip", "master_netdev", "cluster_name"):
170     if key not in cluster:
171       cluster[key] = ReadFile(SsconfName(key)).strip()
172
173   if "default_hypervisor" not in cluster:
174     old_hyp = ReadFile(SsconfName("hypervisor")).strip()
175     if old_hyp == "xen-3.0":
176       hyp = "xen-pvm"
177     elif old_hyp == "xen-hvm-3.1":
178       hyp = "xen-hvm"
179     elif old_hyp == "fake":
180       hyp = "fake"
181     else:
182       raise Error("Unknown old hypervisor name '%s'" % old_hyp)
183
184     logging.info("Setting the default and enabled hypervisor")
185     cluster["default_hypervisor"] = hyp
186     cluster["enabled_hypervisors"] = [hyp]
187
188   # hv/be params
189   if "hvparams" not in cluster:
190     logging.info(" - adding hvparams")
191     cluster["hvparams"] = constants.HVC_DEFAULTS
192   if "beparams" not in cluster:
193     logging.info(" - adding beparams")
194     cluster["beparams"] = {constants.PP_DEFAULT: constants.BEC_DEFAULTS}
195
196   # file storage
197   if "file_storage_dir" not in cluster:
198     cluster["file_storage_dir"] = constants.DEFAULT_FILE_STORAGE_DIR
199
200   # candidate pool size
201   if "candidate_pool_size" not in cluster:
202     cluster["candidate_pool_size"] = constants.MASTER_POOL_SIZE_DEFAULT
203
204
205 def Node12To20(node):
206   """Upgrades a node from 1.2 to 2.0.
207
208   """
209   logging.info("Upgrading node %s", node['name'])
210   if F_SERIAL not in node:
211     node[F_SERIAL] = 1
212   if "master_candidate" not in node:
213     node["master_candidate"] = True
214   for key in "offline", "drained":
215     if key not in node:
216       node[key] = False
217
218
219 def Instance12To20(drbd_minors, secrets, hypervisor, instance):
220   """Upgrades an instance from 1.2 to 2.0.
221
222   """
223   if F_SERIAL not in instance:
224     instance[F_SERIAL] = 1
225
226   if "hypervisor" not in instance:
227     instance["hypervisor"] = hypervisor
228
229   # hvparams changes
230   if "hvparams" not in instance:
231     instance["hvparams"] = hvp = {}
232   for old, new in INST_HV_CHG.items():
233     if old in instance:
234       if (instance[old] is not None and
235           instance[old] != constants.VALUE_DEFAULT and # no longer valid in 2.0
236           new in constants.HVC_DEFAULTS[hypervisor]):
237         hvp[new] = instance[old]
238       del instance[old]
239
240   # beparams changes
241   if "beparams" not in instance:
242     instance["beparams"] = bep = {}
243   for old, new in INST_BE_CHG.items():
244     if old in instance:
245       if instance[old] is not None:
246         bep[new] = instance[old]
247       del instance[old]
248
249   # disk changes
250   for disk in instance["disks"]:
251     Disk12To20(drbd_minors, secrets, disk)
252
253   # other instance changes
254   if "status" in instance:
255     instance["admin_up"] = instance["status"] == "up"
256     del instance["status"]
257
258
259 def Disk12To20(drbd_minors, secrets, disk):
260   """Upgrades a disk from 1.2 to 2.0.
261
262   """
263   if "mode" not in disk:
264     disk["mode"] = constants.DISK_RDWR
265   if disk["dev_type"] == constants.LD_DRBD8:
266     old_lid = disk["logical_id"]
267     for node in old_lid[:2]:
268       if node not in drbd_minors:
269         raise Error("Can't find node '%s' while upgrading disk" % node)
270       drbd_minors[node] += 1
271       minor = drbd_minors[node]
272       old_lid.append(minor)
273     old_lid.append(GenerateSecret(secrets))
274     del disk["physical_id"]
275   if disk["children"]:
276     for child in disk["children"]:
277       Disk12To20(drbd_minors, secrets, child)
278
279
280 def main():
281   """Main program.
282
283   """
284   # pylint: disable=W0603
285   global options, args
286
287   program = os.path.basename(sys.argv[0])
288
289   # Option parsing
290   parser = optparse.OptionParser(usage="%prog [--debug|--verbose] [--force]")
291   parser.add_option("--dry-run", dest="dry_run",
292                     action="store_true",
293                     help="Try to do the conversion, but don't write"
294                          " output file")
295   parser.add_option(cli.FORCE_OPT)
296   parser.add_option(cli.DEBUG_OPT)
297   parser.add_option(cli.VERBOSE_OPT)
298   parser.add_option("--path", help="Convert configuration in this"
299                     " directory instead of '%s'" % constants.DATA_DIR,
300                     default=constants.DATA_DIR, dest="data_dir")
301   (options, args) = parser.parse_args()
302
303   # We need to keep filenames locally because they might be renamed between
304   # versions.
305   options.data_dir = os.path.abspath(options.data_dir)
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"
310
311   SetupLogging()
312
313   # Option checking
314   if args:
315     raise Error("No arguments expected")
316
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):
321       sys.exit(1)
322
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)
329
330   config_version = ReadFile(SsconfName("config_version"), "1.2").strip()
331   logging.info("Found configuration version %s", config_version)
332
333   config_data = serializer.LoadJson(ReadFile(options.CONFIG_DATA_PATH))
334
335   # Ganeti 1.2?
336   if config_version == "1.2":
337     logging.info("Found a Ganeti 1.2 configuration")
338
339     cluster = config_data["cluster"]
340
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" %
345                   old_config_version)
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
350
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",
359                       name)
360       raise Error("Unable to convert configuration as long as there are"
361                   " instances using remote_raid1 disk template")
362
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)
367
368     Cluster12To20(cluster)
369
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])
375
376     # Instance changes
377     logging.info("Upgrading instances")
378     drbd_minors = dict.fromkeys(config_data["nodes"], 0)
379     secrets = set()
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])
384
385   else:
386     logging.info("Found a Ganeti 2.0 configuration")
387
388     if "config_version" in config_data["cluster"]:
389       raise Error("Inconsistent configuration: found config_data in"
390                   " configuration file")
391
392     known_hosts = None
393
394   try:
395     logging.info("Writing configuration file")
396     WriteFile(options.CONFIG_DATA_PATH, serializer.DumpJson(config_data))
397
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)
401
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         utils.GenerateSelfSignedSslCert(options.RAPI_CERT_FILE)
406
407   except:
408     logging.critical("Writing configuration failed. It is probably in an"
409                      " inconsistent state and needs manual intervention.")
410     raise
411
412   logging.info("Configuration file updated.")
413
414
415 if __name__ == "__main__":
416   main()