burnin: Implement retryable operations
[ganeti-local] / tools / cfgupgrade
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
22 """Tool to upgrade the configuration file.
23
24 This code handles only the types supported by simplejson. As an
25 example, 'set' is a 'list'.
26
27 """
28
29
30 import os
31 import os.path
32 import sys
33 import optparse
34 import tempfile
35 import logging
36 import errno
37
38 from ganeti import constants
39 from ganeti import serializer
40 from ganeti import utils
41 from ganeti import cli
42 from ganeti import bootstrap
43 from ganeti import config
44
45
46 options = None
47 args = None
48
49 # Unique object to identify calls without default value
50 NoDefault = object()
51
52 # Dictionary with instance old keys, and new hypervisor keys
53 INST_HV_CHG = {
54   'hvm_pae': constants.HV_PAE,
55   'vnc_bind_address': constants.HV_VNC_BIND_ADDRESS,
56   'initrd_path': constants.HV_INITRD_PATH,
57   'hvm_nic_type': constants.HV_NIC_TYPE,
58   'kernel_path': constants.HV_KERNEL_PATH,
59   'hvm_acpi': constants.HV_ACPI,
60   'hvm_cdrom_image_path': constants.HV_CDROM_IMAGE_PATH,
61   'hvm_boot_order': constants.HV_BOOT_ORDER,
62   'hvm_disk_type': constants.HV_DISK_TYPE,
63   }
64
65 # Instance beparams changes
66 INST_BE_CHG = {
67   'vcpus': constants.BE_VCPUS,
68   'memory': constants.BE_MEMORY,
69   'auto_balance': constants.BE_AUTO_BALANCE,
70   }
71
72 # Field names
73 F_SERIAL = 'serial_no'
74
75
76 class Error(Exception):
77   """Generic exception"""
78   pass
79
80
81 def SsconfName(key):
82   """Returns the file name of an (old) ssconf key.
83
84   """
85   return "%s/ssconf_%s" % (options.data_dir, key)
86
87
88 def ReadFile(file_name, default=NoDefault):
89   """Reads a file.
90
91   """
92   logging.debug("Reading %s", file_name)
93   try:
94     fh = open(file_name, 'r')
95   except IOError, err:
96     if default is not NoDefault and err.errno == errno.ENOENT:
97       return default
98     raise
99
100   try:
101     return fh.read()
102   finally:
103     fh.close()
104
105
106 def WriteFile(file_name, data):
107   """Writes a configuration file.
108
109   """
110   logging.debug("Writing %s", file_name)
111   utils.WriteFile(file_name=file_name, data=data, mode=0600,
112                   dry_run=options.dry_run, backup=True)
113
114
115 def GenerateSecret(all_secrets):
116   """Generate an unique DRBD secret.
117
118   This is a copy from ConfigWriter.
119
120   """
121   retries = 64
122   while retries > 0:
123     secret = utils.GenerateSecret()
124     if secret not in all_secrets:
125       break
126     retries -= 1
127   else:
128     raise Error("Can't generate unique DRBD secret")
129   return secret
130
131
132 def SetupLogging():
133   """Configures the logging module.
134
135   """
136   formatter = logging.Formatter("%(asctime)s: %(message)s")
137
138   stderr_handler = logging.StreamHandler()
139   stderr_handler.setFormatter(formatter)
140   if options.debug:
141     stderr_handler.setLevel(logging.NOTSET)
142   elif options.verbose:
143     stderr_handler.setLevel(logging.INFO)
144   else:
145     stderr_handler.setLevel(logging.CRITICAL)
146
147   root_logger = logging.getLogger("")
148   root_logger.setLevel(logging.NOTSET)
149   root_logger.addHandler(stderr_handler)
150
151
152 def Cluster12To20(cluster):
153   """Upgrades the cluster object from 1.2 to 2.0.
154
155   """
156   logging.info("Upgrading the cluster object")
157   # Upgrade the configuration version
158   if 'config_version' in cluster:
159     del cluster['config_version']
160
161   # Add old ssconf keys back to config
162   logging.info(" - importing ssconf keys")
163   for key in ('master_node', 'master_ip', 'master_netdev', 'cluster_name'):
164     if key not in cluster:
165       cluster[key] = ReadFile(SsconfName(key)).strip()
166
167   if 'default_hypervisor' not in cluster:
168     old_hyp = ReadFile(SsconfName('hypervisor')).strip()
169     if old_hyp == "xen-3.0":
170       hyp = "xen-pvm"
171     elif old_hyp == "xen-hvm-3.1":
172       hyp = "xen-hvm"
173     elif old_hyp == "fake":
174       hyp = "fake"
175     else:
176       raise Error("Unknown old hypervisor name '%s'" % old_hyp)
177
178     logging.info("Setting the default and enabled hypervisor")
179     cluster['default_hypervisor'] = hyp
180     cluster['enabled_hypervisors'] = [hyp]
181
182   # hv/be params
183   if 'hvparams' not in cluster:
184     logging.info(" - adding hvparams")
185     cluster['hvparams'] = constants.HVC_DEFAULTS
186   if 'beparams' not in cluster:
187     logging.info(" - adding beparams")
188     cluster['beparams'] = {constants.BEGR_DEFAULT: constants.BEC_DEFAULTS}
189
190   # file storage
191   if 'file_storage_dir' not in cluster:
192     cluster['file_storage_dir'] = constants.DEFAULT_FILE_STORAGE_DIR
193
194   # candidate pool size
195   if 'candidate_pool_size' not in cluster:
196     cluster['candidate_pool_size'] = constants.MASTER_POOL_SIZE_DEFAULT
197
198
199 def Node12To20(node):
200   """Upgrades a node from 1.2 to 2.0.
201
202   """
203   logging.info("Upgrading node %s" % node['name'])
204   if F_SERIAL not in node:
205     node[F_SERIAL] = 1
206   if 'master_candidate' not in node:
207     node['master_candidate'] = True
208   for key in 'offline', 'drained':
209     if key not in node:
210       node[key] = False
211
212
213 def Instance12To20(drbd_minors, secrets, hypervisor, instance):
214   """Upgrades an instance from 1.2 to 2.0.
215
216   """
217   if F_SERIAL not in instance:
218     instance[F_SERIAL] = 1
219
220   if 'hypervisor' not in instance:
221     instance['hypervisor'] = hypervisor
222
223   # hvparams changes
224   if 'hvparams' not in instance:
225     instance['hvparams'] = hvp = {}
226   for old, new in INST_HV_CHG.items():
227     if old in instance:
228       if (instance[old] is not None and
229           instance[old] != constants.VALUE_DEFAULT and # no longer valid in 2.0
230           new in constants.HVC_DEFAULTS[hypervisor]):
231         hvp[new] = instance[old]
232       del instance[old]
233
234   # beparams changes
235   if 'beparams' not in instance:
236     instance['beparams'] = bep = {}
237   for old, new in INST_BE_CHG.items():
238     if old in instance:
239       if instance[old] is not None:
240         bep[new] = instance[old]
241       del instance[old]
242
243   # disk changes
244   for disk in instance['disks']:
245     Disk12To20(drbd_minors, secrets, disk)
246
247   # other instance changes
248   if 'status' in instance:
249     instance['admin_up'] = instance['status'] == 'up'
250     del instance['status']
251
252
253 def Disk12To20(drbd_minors, secrets, disk):
254   """Upgrades a disk from 1.2 to 2.0.
255
256   """
257   if 'mode' not in disk:
258     disk['mode'] = constants.DISK_RDWR
259   if disk['dev_type'] == constants.LD_DRBD8:
260     old_lid = disk['logical_id']
261     for node in old_lid[:2]:
262       if node not in drbd_minors:
263         raise Error("Can't find node '%s' while upgrading disk" % node)
264       drbd_minors[node] += 1
265       minor = drbd_minors[node]
266       old_lid.append(minor)
267     old_lid.append(GenerateSecret(secrets))
268     del disk['physical_id']
269   if disk['children']:
270     for child in disk['children']:
271       Disk12To20(drbd_minors, secrets, child)
272
273
274 def main():
275   """Main program.
276
277   """
278   global options, args
279
280   program = os.path.basename(sys.argv[0])
281
282   # Option parsing
283   parser = optparse.OptionParser(usage="%prog [--debug|--verbose] [--force]")
284   parser.add_option('--dry-run', dest='dry_run',
285                     action="store_true",
286                     help="Try to do the conversion, but don't write"
287                          " output file")
288   parser.add_option(cli.FORCE_OPT)
289   parser.add_option(cli.DEBUG_OPT)
290   parser.add_option('-v', '--verbose', dest='verbose',
291                     action="store_true",
292                     help="Verbose output")
293   parser.add_option('--path', help="Convert configuration in this"
294                     " directory instead of '%s'" % constants.DATA_DIR,
295                     default=constants.DATA_DIR, dest="data_dir")
296   (options, args) = parser.parse_args()
297
298   # We need to keep filenames locally because they might be renamed between
299   # versions.
300   options.CONFIG_DATA_PATH = options.data_dir + "/config.data"
301   options.SERVER_PEM_PATH = options.data_dir + "/server.pem"
302   options.KNOWN_HOSTS_PATH = options.data_dir + "/known_hosts"
303   options.RAPI_CERT_FILE = options.data_dir + "/rapi.pem"
304
305   SetupLogging()
306
307   # Option checking
308   if args:
309     raise Error("No arguments expected")
310
311   if not options.force:
312     usertext = ("%s MUST be run on the master node. Is this the master"
313                 " node and are ALL instances down?" % program)
314     if not cli.AskUser(usertext):
315       sys.exit(1)
316
317   # Check whether it's a Ganeti configuration directory
318   if not (os.path.isfile(options.CONFIG_DATA_PATH) and
319           os.path.isfile(options.SERVER_PEM_PATH) or
320           os.path.isfile(options.KNOWN_HOSTS_PATH)):
321     raise Error(("%s does not seem to be a known Ganeti configuration"
322                  " directory") % options.data_dir)
323
324   config_version = ReadFile(SsconfName('config_version'), "1.2").strip()
325   logging.info("Found configuration version %s", config_version)
326
327   config_data = serializer.LoadJson(ReadFile(options.CONFIG_DATA_PATH))
328
329   # Ganeti 1.2?
330   if config_version == "1.2":
331     logging.info("Found a Ganeti 1.2 configuration")
332
333     cluster = config_data["cluster"]
334
335     old_config_version = cluster.get("config_version", None)
336     logging.info("Found old configuration version %s", old_config_version)
337     if old_config_version not in (3, ):
338       raise Error("Unsupported configuration version: %s" %
339                   old_config_version)
340     if 'version' not in config_data:
341       config_data['version'] = constants.BuildVersion(2, 0, 0)
342     if F_SERIAL not in config_data:
343       config_data[F_SERIAL] = 1
344
345     # Make sure no instance uses remote_raid1 anymore
346     remote_raid1_instances = []
347     for instance in config_data["instances"].values():
348       if instance["disk_template"] == "remote_raid1":
349         remote_raid1_instances.append(instance["name"])
350     if remote_raid1_instances:
351       for name in remote_raid1_instances:
352         logging.error("Instance %s still using remote_raid1 disk template")
353       raise Error("Unable to convert configuration as long as there are"
354                   " instances using remote_raid1 disk template")
355
356     # Build content of new known_hosts file
357     cluster_name = ReadFile(SsconfName('cluster_name')).rstrip()
358     cluster_key = cluster['rsahostkeypub']
359     known_hosts = "%s ssh-rsa %s\n" % (cluster_name, cluster_key)
360
361     Cluster12To20(cluster)
362
363     # Add node attributes
364     logging.info("Upgrading nodes")
365     # stable-sort the names to have repeatable runs
366     for node_name in utils.NiceSort(config_data['nodes'].keys()):
367       Node12To20(config_data['nodes'][node_name])
368
369     # Instance changes
370     logging.info("Upgrading instances")
371     drbd_minors = dict.fromkeys(config_data['nodes'], 0)
372     secrets = set()
373     # stable-sort the names to have repeatable runs
374     for instance_name in utils.NiceSort(config_data['instances'].keys()):
375       Instance12To20(drbd_minors, secrets, cluster['default_hypervisor'],
376                      config_data['instances'][instance_name])
377
378   else:
379     logging.info("Found a Ganeti 2.0 configuration")
380
381     if "config_version" in config_data["cluster"]:
382       raise Error("Inconsistent configuration: found config_data in"
383                   " configuration file")
384
385     known_hosts = None
386
387   try:
388     logging.info("Writing configuration file")
389     WriteFile(options.CONFIG_DATA_PATH, serializer.DumpJson(config_data))
390
391     if known_hosts is not None:
392       logging.info("Writing SSH known_hosts file (%s)", known_hosts.strip())
393       WriteFile(options.KNOWN_HOSTS_PATH, known_hosts)
394
395     if not options.dry_run:
396       if not os.path.exists(options.RAPI_CERT_FILE):
397         bootstrap._GenerateSelfSignedSslCert(options.RAPI_CERT_FILE)
398
399   except:
400     logging.critical("Writing configuration failed. It is proably in an"
401                      " inconsistent state and needs manual intervention.")
402     raise
403
404   # test loading the config file
405   if not options.dry_run:
406     logging.info("Testing the new config file...")
407     cfg = config.ConfigWriter(cfg_file=options.CONFIG_DATA_PATH,
408                               offline=True)
409     # if we reached this, it's all fine
410     vrfy = cfg.VerifyConfig()
411     if vrfy:
412       logging.error("Errors after conversion:")
413       for item in vrfy:
414         logging.error(" - %s" % item)
415     del cfg
416     logging.info("File loaded successfully")
417
418
419 if __name__ == "__main__":
420   main()
421
422 # vim: set foldmethod=marker :