Unify the multiple 'verbose' options
[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.PP_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(cli.VERBOSE_OPT)
291   parser.add_option('--path', help="Convert configuration in this"
292                     " directory instead of '%s'" % constants.DATA_DIR,
293                     default=constants.DATA_DIR, dest="data_dir")
294   (options, args) = parser.parse_args()
295
296   # We need to keep filenames locally because they might be renamed between
297   # versions.
298   options.CONFIG_DATA_PATH = options.data_dir + "/config.data"
299   options.SERVER_PEM_PATH = options.data_dir + "/server.pem"
300   options.KNOWN_HOSTS_PATH = options.data_dir + "/known_hosts"
301   options.RAPI_CERT_FILE = options.data_dir + "/rapi.pem"
302
303   SetupLogging()
304
305   # Option checking
306   if args:
307     raise Error("No arguments expected")
308
309   if not options.force:
310     usertext = ("%s MUST be run on the master node. Is this the master"
311                 " node and are ALL instances down?" % program)
312     if not cli.AskUser(usertext):
313       sys.exit(1)
314
315   # Check whether it's a Ganeti configuration directory
316   if not (os.path.isfile(options.CONFIG_DATA_PATH) and
317           os.path.isfile(options.SERVER_PEM_PATH) or
318           os.path.isfile(options.KNOWN_HOSTS_PATH)):
319     raise Error(("%s does not seem to be a known Ganeti configuration"
320                  " directory") % options.data_dir)
321
322   config_version = ReadFile(SsconfName('config_version'), "1.2").strip()
323   logging.info("Found configuration version %s", config_version)
324
325   config_data = serializer.LoadJson(ReadFile(options.CONFIG_DATA_PATH))
326
327   # Ganeti 1.2?
328   if config_version == "1.2":
329     logging.info("Found a Ganeti 1.2 configuration")
330
331     cluster = config_data["cluster"]
332
333     old_config_version = cluster.get("config_version", None)
334     logging.info("Found old configuration version %s", old_config_version)
335     if old_config_version not in (3, ):
336       raise Error("Unsupported configuration version: %s" %
337                   old_config_version)
338     if 'version' not in config_data:
339       config_data['version'] = constants.BuildVersion(2, 0, 0)
340     if F_SERIAL not in config_data:
341       config_data[F_SERIAL] = 1
342
343     # Make sure no instance uses remote_raid1 anymore
344     remote_raid1_instances = []
345     for instance in config_data["instances"].values():
346       if instance["disk_template"] == "remote_raid1":
347         remote_raid1_instances.append(instance["name"])
348     if remote_raid1_instances:
349       for name in remote_raid1_instances:
350         logging.error("Instance %s still using remote_raid1 disk template")
351       raise Error("Unable to convert configuration as long as there are"
352                   " instances using remote_raid1 disk template")
353
354     # Build content of new known_hosts file
355     cluster_name = ReadFile(SsconfName('cluster_name')).rstrip()
356     cluster_key = cluster['rsahostkeypub']
357     known_hosts = "%s ssh-rsa %s\n" % (cluster_name, cluster_key)
358
359     Cluster12To20(cluster)
360
361     # Add node attributes
362     logging.info("Upgrading nodes")
363     # stable-sort the names to have repeatable runs
364     for node_name in utils.NiceSort(config_data['nodes'].keys()):
365       Node12To20(config_data['nodes'][node_name])
366
367     # Instance changes
368     logging.info("Upgrading instances")
369     drbd_minors = dict.fromkeys(config_data['nodes'], 0)
370     secrets = set()
371     # stable-sort the names to have repeatable runs
372     for instance_name in utils.NiceSort(config_data['instances'].keys()):
373       Instance12To20(drbd_minors, secrets, cluster['default_hypervisor'],
374                      config_data['instances'][instance_name])
375
376   else:
377     logging.info("Found a Ganeti 2.0 configuration")
378
379     if "config_version" in config_data["cluster"]:
380       raise Error("Inconsistent configuration: found config_data in"
381                   " configuration file")
382
383     known_hosts = None
384
385   try:
386     logging.info("Writing configuration file")
387     WriteFile(options.CONFIG_DATA_PATH, serializer.DumpJson(config_data))
388
389     if known_hosts is not None:
390       logging.info("Writing SSH known_hosts file (%s)", known_hosts.strip())
391       WriteFile(options.KNOWN_HOSTS_PATH, known_hosts)
392
393     if not options.dry_run:
394       if not os.path.exists(options.RAPI_CERT_FILE):
395         bootstrap._GenerateSelfSignedSslCert(options.RAPI_CERT_FILE)
396
397   except:
398     logging.critical("Writing configuration failed. It is proably in an"
399                      " inconsistent state and needs manual intervention.")
400     raise
401
402   # test loading the config file
403   if not options.dry_run:
404     logging.info("Testing the new config file...")
405     cfg = config.ConfigWriter(cfg_file=options.CONFIG_DATA_PATH,
406                               offline=True)
407     # if we reached this, it's all fine
408     vrfy = cfg.VerifyConfig()
409     if vrfy:
410       logging.error("Errors after conversion:")
411       for item in vrfy:
412         logging.error(" - %s" % item)
413     del cfg
414     logging.info("File loaded successfully")
415
416
417 if __name__ == "__main__":
418   main()
419
420 # vim: set foldmethod=marker :