Typos in ganeti-os-interface.rst
[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
53 options = None
54 args = None
55
56 # Unique object to identify calls without default value
57 NoDefault = object()
58
59 # Dictionary with instance old keys, and new hypervisor keys
60 INST_HV_CHG = {
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,
70   }
71
72 # Instance beparams changes
73 INST_BE_CHG = {
74   "vcpus": constants.BE_VCPUS,
75   "memory": constants.BE_MEMORY,
76   "auto_balance": constants.BE_AUTO_BALANCE,
77   }
78
79 # Field names
80 F_SERIAL = "serial_no"
81
82
83 class Error(Exception):
84   """Generic exception"""
85   pass
86
87
88 def SsconfName(key):
89   """Returns the file name of an (old) ssconf key.
90
91   """
92   return "%s/ssconf_%s" % (options.data_dir, key)
93
94
95 def ReadFile(file_name, default=NoDefault):
96   """Reads a file.
97
98   """
99   logging.debug("Reading %s", file_name)
100   try:
101     fh = open(file_name, "r")
102   except IOError, err:
103     if default is not NoDefault and err.errno == errno.ENOENT:
104       return default
105     raise
106
107   try:
108     return fh.read()
109   finally:
110     fh.close()
111
112
113 def WriteFile(file_name, data):
114   """Writes a configuration file.
115
116   """
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)
120
121
122 def GenerateSecret(all_secrets):
123   """Generate an unique DRBD secret.
124
125   This is a copy from ConfigWriter.
126
127   """
128   retries = 64
129   while retries > 0:
130     secret = utils.GenerateSecret()
131     if secret not in all_secrets:
132       break
133     retries -= 1
134   else:
135     raise Error("Can't generate unique DRBD secret")
136   return secret
137
138
139 def SetupLogging():
140   """Configures the logging module.
141
142   """
143   formatter = logging.Formatter("%(asctime)s: %(message)s")
144
145   stderr_handler = logging.StreamHandler()
146   stderr_handler.setFormatter(formatter)
147   if options.debug:
148     stderr_handler.setLevel(logging.NOTSET)
149   elif options.verbose:
150     stderr_handler.setLevel(logging.INFO)
151   else:
152     stderr_handler.setLevel(logging.CRITICAL)
153
154   root_logger = logging.getLogger("")
155   root_logger.setLevel(logging.NOTSET)
156   root_logger.addHandler(stderr_handler)
157
158
159 def Cluster12To20(cluster):
160   """Upgrades the cluster object from 1.2 to 2.0.
161
162   """
163   logging.info("Upgrading the cluster object")
164   # Upgrade the configuration version
165   if "config_version" in cluster:
166     del cluster["config_version"]
167
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()
173
174   if "default_hypervisor" not in cluster:
175     old_hyp = ReadFile(SsconfName("hypervisor")).strip()
176     if old_hyp == "xen-3.0":
177       hyp = "xen-pvm"
178     elif old_hyp == "xen-hvm-3.1":
179       hyp = "xen-hvm"
180     elif old_hyp == "fake":
181       hyp = "fake"
182     else:
183       raise Error("Unknown old hypervisor name '%s'" % old_hyp)
184
185     logging.info("Setting the default and enabled hypervisor")
186     cluster["default_hypervisor"] = hyp
187     cluster["enabled_hypervisors"] = [hyp]
188
189   # hv/be params
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}
196
197   # file storage
198   if "file_storage_dir" not in cluster:
199     cluster["file_storage_dir"] = pathutils.DEFAULT_FILE_STORAGE_DIR
200
201   # candidate pool size
202   if "candidate_pool_size" not in cluster:
203     cluster["candidate_pool_size"] = constants.MASTER_POOL_SIZE_DEFAULT
204
205
206 def Node12To20(node):
207   """Upgrades a node from 1.2 to 2.0.
208
209   """
210   logging.info("Upgrading node %s", node['name'])
211   if F_SERIAL not in node:
212     node[F_SERIAL] = 1
213   if "master_candidate" not in node:
214     node["master_candidate"] = True
215   for key in "offline", "drained":
216     if key not in node:
217       node[key] = False
218
219
220 def Instance12To20(drbd_minors, secrets, hypervisor, instance):
221   """Upgrades an instance from 1.2 to 2.0.
222
223   """
224   if F_SERIAL not in instance:
225     instance[F_SERIAL] = 1
226
227   if "hypervisor" not in instance:
228     instance["hypervisor"] = hypervisor
229
230   # hvparams changes
231   if "hvparams" not in instance:
232     instance["hvparams"] = hvp = {}
233   for old, new in INST_HV_CHG.items():
234     if old in instance:
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]
239       del instance[old]
240
241   # beparams changes
242   if "beparams" not in instance:
243     instance["beparams"] = bep = {}
244   for old, new in INST_BE_CHG.items():
245     if old in instance:
246       if instance[old] is not None:
247         bep[new] = instance[old]
248       del instance[old]
249
250   # disk changes
251   for disk in instance["disks"]:
252     Disk12To20(drbd_minors, secrets, disk)
253
254   # other instance changes
255   if "status" in instance:
256     instance["admin_up"] = instance["status"] == "up"
257     del instance["status"]
258
259
260 def Disk12To20(drbd_minors, secrets, disk):
261   """Upgrades a disk from 1.2 to 2.0.
262
263   """
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"]
276   if disk["children"]:
277     for child in disk["children"]:
278       Disk12To20(drbd_minors, secrets, child)
279
280
281 def main():
282   """Main program.
283
284   """
285   # pylint: disable=W0603
286   global options, args
287
288   program = os.path.basename(sys.argv[0])
289
290   # Option parsing
291   parser = optparse.OptionParser(usage="%prog [--debug|--verbose] [--force]")
292   parser.add_option("--dry-run", dest="dry_run",
293                     action="store_true",
294                     help="Try to do the conversion, but don't write"
295                          " output file")
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'" % pathutils.DATA_DIR,
301                     default=pathutils.DATA_DIR, dest="data_dir")
302   (options, args) = parser.parse_args()
303
304   # We need to keep filenames locally because they might be renamed between
305   # versions.
306   options.data_dir = os.path.abspath(options.data_dir)
307   options.CONFIG_DATA_PATH = options.data_dir + "/config.data"
308   options.SERVER_PEM_PATH = options.data_dir + "/server.pem"
309   options.KNOWN_HOSTS_PATH = options.data_dir + "/known_hosts"
310   options.RAPI_CERT_FILE = options.data_dir + "/rapi.pem"
311
312   SetupLogging()
313
314   # Option checking
315   if args:
316     raise Error("No arguments expected")
317
318   if not options.force:
319     usertext = ("%s MUST be run on the master node. Is this the master"
320                 " node and are ALL instances down?" % program)
321     if not cli.AskUser(usertext):
322       sys.exit(1)
323
324   # Check whether it's a Ganeti configuration directory
325   if not (os.path.isfile(options.CONFIG_DATA_PATH) and
326           os.path.isfile(options.SERVER_PEM_PATH) or
327           os.path.isfile(options.KNOWN_HOSTS_PATH)):
328     raise Error(("%s does not seem to be a known Ganeti configuration"
329                  " directory") % options.data_dir)
330
331   config_version = ReadFile(SsconfName("config_version"), "1.2").strip()
332   logging.info("Found configuration version %s", config_version)
333
334   config_data = serializer.LoadJson(ReadFile(options.CONFIG_DATA_PATH))
335
336   # Ganeti 1.2?
337   if config_version == "1.2":
338     logging.info("Found a Ganeti 1.2 configuration")
339
340     cluster = config_data["cluster"]
341
342     old_config_version = cluster.get("config_version", None)
343     logging.info("Found old configuration version %s", old_config_version)
344     if old_config_version not in (3, ):
345       raise Error("Unsupported configuration version: %s" %
346                   old_config_version)
347     if "version" not in config_data:
348       config_data["version"] = constants.BuildVersion(2, 0, 0)
349     if F_SERIAL not in config_data:
350       config_data[F_SERIAL] = 1
351
352     # Make sure no instance uses remote_raid1 anymore
353     remote_raid1_instances = []
354     for instance in config_data["instances"].values():
355       if instance["disk_template"] == "remote_raid1":
356         remote_raid1_instances.append(instance["name"])
357     if remote_raid1_instances:
358       for name in remote_raid1_instances:
359         logging.error("Instance %s still using remote_raid1 disk template",
360                       name)
361       raise Error("Unable to convert configuration as long as there are"
362                   " instances using remote_raid1 disk template")
363
364     # Build content of new known_hosts file
365     cluster_name = ReadFile(SsconfName("cluster_name")).rstrip()
366     cluster_key = cluster["rsahostkeypub"]
367     known_hosts = "%s ssh-rsa %s\n" % (cluster_name, cluster_key)
368
369     Cluster12To20(cluster)
370
371     # Add node attributes
372     logging.info("Upgrading nodes")
373     # stable-sort the names to have repeatable runs
374     for node_name in utils.NiceSort(config_data["nodes"].keys()):
375       Node12To20(config_data["nodes"][node_name])
376
377     # Instance changes
378     logging.info("Upgrading instances")
379     drbd_minors = dict.fromkeys(config_data["nodes"], 0)
380     secrets = set()
381     # stable-sort the names to have repeatable runs
382     for instance_name in utils.NiceSort(config_data["instances"].keys()):
383       Instance12To20(drbd_minors, secrets, cluster["default_hypervisor"],
384                      config_data["instances"][instance_name])
385
386   else:
387     logging.info("Found a Ganeti 2.0 configuration")
388
389     if "config_version" in config_data["cluster"]:
390       raise Error("Inconsistent configuration: found config_data in"
391                   " configuration file")
392
393     known_hosts = None
394
395   try:
396     logging.info("Writing configuration file")
397     WriteFile(options.CONFIG_DATA_PATH, serializer.DumpJson(config_data))
398
399     if known_hosts is not None:
400       logging.info("Writing SSH known_hosts file (%s)", known_hosts.strip())
401       WriteFile(options.KNOWN_HOSTS_PATH, known_hosts)
402
403     if not options.dry_run:
404       if not os.path.exists(options.RAPI_CERT_FILE):
405         logging.debug("Writing RAPI certificate to %s", options.RAPI_CERT_FILE)
406         utils.GenerateSelfSignedSslCert(options.RAPI_CERT_FILE)
407
408   except:
409     logging.critical("Writing configuration failed. It is probably in an"
410                      " inconsistent state and needs manual intervention.")
411     raise
412
413   logging.info("Configuration file updated.")
414
415
416 if __name__ == "__main__":
417   main()