First run at cfgupgrade for 2.0 upgrades
[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
44
45 # We need to keep filenames locally because they might be renamed between
46 # versions.
47 CONFIG_DATA_PATH = constants.DATA_DIR + "/config.data"
48 SERVER_PEM_PATH = constants.DATA_DIR + "/server.pem"
49 KNOWN_HOSTS_PATH = constants.DATA_DIR + "/known_hosts"
50 SSCONF_CLUSTER_NAME_PATH = constants.DATA_DIR + "/ssconf_cluster_name"
51 SSCONF_CONFIG_VERSION_PATH = constants.DATA_DIR + "/ssconf_config_version"
52
53 options = None
54 args = None
55
56 # Unique object to identify calls without default value
57 NoDefault = object()
58
59
60 class Error(Exception):
61   """Generic exception"""
62   pass
63
64
65 def SsconfName(key):
66   """Returns the file name of an (old) ssconf key.
67
68   """
69   return "%s/ssconf_%s" % (constants.DATA_DIR, key)
70
71
72 def ReadFile(file_name, default=NoDefault):
73   """Reads a file.
74
75   """
76   logging.debug("Reading %s", file_name)
77   try:
78     fh = open(file_name, 'r')
79   except IOError, err:
80     if default is not NoDefault and err.errno == errno.ENOENT:
81       return default
82     raise
83
84   try:
85     return fh.read()
86   finally:
87     fh.close()
88
89
90 def WriteFile(file_name, data):
91   """Writes a configuration file.
92
93   """
94   logging.debug("Writing %s", file_name)
95   utils.WriteFile(file_name=file_name, data=data, mode=0600,
96                   dry_run=options.dry_run, backup=True)
97
98
99 def SetupLogging():
100   """Configures the logging module.
101
102   """
103   formatter = logging.Formatter("%(asctime)s: %(message)s")
104
105   stderr_handler = logging.StreamHandler()
106   stderr_handler.setFormatter(formatter)
107   if options.debug:
108     stderr_handler.setLevel(logging.NOTSET)
109   elif options.verbose:
110     stderr_handler.setLevel(logging.INFO)
111   else:
112     stderr_handler.setLevel(logging.CRITICAL)
113
114   root_logger = logging.getLogger("")
115   root_logger.setLevel(logging.NOTSET)
116   root_logger.addHandler(stderr_handler)
117
118
119 def Cluster12To20(cluster):
120   """Upgrades the cluster object from 1.2 to 2.0.
121
122   """
123   logging.info("Upgrading the cluster object")
124   # Upgrade the configuration version
125   if 'config_version' in cluster:
126     del cluster['config_version']
127
128   # Add old ssconf keys back to config
129   logging.info(" - importing ssconf keys")
130   for key in ('master_node', 'master_ip', 'master_netdev', 'cluster_name'):
131     if key not in cluster:
132       cluster[key] = ReadFile(SsconfName(key)).strip()
133
134   if 'default_hypervisor' not in cluster:
135     old_hyp = ReadFile(SsconfName('hypervisor')).strip()
136     if old_hyp == "xen-3.0":
137       hyp = "xen-pvm"
138     elif old_hyp == "xen-hvm-3.1":
139       hyp = "xen-hvm"
140     elif old_hyp == "fake":
141       hyp = "fake"
142     else:
143       raise Error("Unknown old hypervisor name '%s'" % old_hyp)
144
145     logging.info("Setting the default and enabled hypervisor")
146     cluster['default_hypervisor'] = hyp
147     cluster['enabled_hypervisors'] = [hyp]
148
149   # hv/be params
150   if 'hvparams' not in cluster:
151     logging.info(" - adding hvparams")
152     cluster['hvparams'] = constants.HVC_DEFAULTS
153   if 'beparams' not in cluster:
154     logging.info(" - adding beparams")
155     cluster['beparams'] = {constants.BEGR_DEFAULT: constants.BEC_DEFAULTS}
156
157   # file storage
158   if 'file_storage_dir' not in cluster:
159     cluster['file_storage_dir'] = constants.DEFAULT_FILE_STORAGE_DIR
160
161
162 def Node12To20(node):
163   """Upgrades a node from 1.2 to 2.0.
164
165   """
166   logging.info("Upgrading node %s" % node['name'])
167   if 'serial_no' not in node:
168     node['serial_no'] = 1
169   if 'master_candidate' not in node:
170     node['master_candidate'] = True
171   for key in 'offline', 'drained':
172     if key not in node:
173       node[key] = False
174
175
176 def main():
177   """Main program.
178
179   """
180   global options, args
181
182   program = os.path.basename(sys.argv[0])
183
184   # Option parsing
185   parser = optparse.OptionParser(usage="%prog [--debug|--verbose] [--force]")
186   parser.add_option('--dry-run', dest='dry_run',
187                     action="store_true",
188                     help="Try to do the conversion, but don't write"
189                          " output file")
190   parser.add_option(cli.FORCE_OPT)
191   parser.add_option(cli.DEBUG_OPT)
192   parser.add_option('-v', '--verbose', dest='verbose',
193                     action="store_true",
194                     help="Verbose output")
195   (options, args) = parser.parse_args()
196
197   SetupLogging()
198
199   # Option checking
200   if args:
201     raise Error("No arguments expected")
202
203   if not options.force:
204     usertext = ("%s MUST be run on the master node. Is this the master"
205                 " node and are ALL instances down?" % program)
206     if not cli.AskUser(usertext):
207       sys.exit(1)
208
209   # Check whether it's a Ganeti configuration directory
210   if not (os.path.isfile(CONFIG_DATA_PATH) and
211           os.path.isfile(SERVER_PEM_PATH) or
212           os.path.isfile(KNOWN_HOSTS_PATH)):
213     raise Error(("%s does not seem to be a known Ganeti configuration"
214                  " directory") % constants.DATA_DIR)
215
216   config_version = ReadFile(SSCONF_CONFIG_VERSION_PATH, "1.2").strip()
217   logging.info("Found configuration version %s", config_version)
218
219   config_data = serializer.LoadJson(ReadFile(CONFIG_DATA_PATH))
220
221   # Ganeti 1.2?
222   if config_version == "1.2":
223     logging.info("Found a Ganeti 1.2 configuration")
224
225     cluster = config_data["cluster"]
226
227     old_config_version = cluster.get("config_version", None)
228     logging.info("Found old configuration version %s", old_config_version)
229     if old_config_version not in (3, ):
230       raise Error("Unsupported configuration version: %s" %
231                   old_config_version)
232     if 'version' not in config_data:
233       config_data['version'] = constants.BuildVersion(2, 0, 0)
234     if 'serial_no' not in config_data:
235       config_data['serial_no'] = 1
236
237     # Make sure no instance uses remote_raid1 anymore
238     remote_raid1_instances = []
239     for instance in config_data["instances"]:
240       if instance["disk_template"] == "remote_raid1":
241         remote_raid1_instances.append(instance["name"])
242     if remote_raid1_instances:
243       for name in remote_raid1_instances:
244         logging.error("Instance %s still using remote_raid1 disk template")
245       raise Error("Unable to convert configuration as long as there are"
246                   " instances using remote_raid1 disk template")
247
248     # Build content of new known_hosts file
249     cluster_name = ReadFile(SSCONF_CLUSTER_NAME_PATH).rstrip()
250     cluster_key = cluster['rsahostkeypub']
251     known_hosts = "%s ssh-rsa %s\n" % (cluster_name, cluster_key)
252
253     Cluster12To20(cluster)
254
255     # Add node attributes
256     logging.info("Upgrading nodes")
257     # stable-sort the names to have repeatable runs
258     for node_name in utils.NiceSort(config_data['nodes'].keys()):
259       Node12To20(config_data['nodes'][node_name])
260
261     # instance changes
262     # TODO: add instance upgrade
263     for instance in config_data['instances'].values():
264       pass
265
266   else:
267     logging.info("Found a Ganeti 2.0 configuration")
268
269     if "config_version" in config_data["cluster"]:
270       raise Error("Inconsistent configuration: found config_data in"
271                   " configuration file")
272
273     known_hosts = None
274
275   try:
276     logging.info("Writing configuration file")
277     WriteFile(CONFIG_DATA_PATH, serializer.DumpJson(config_data))
278
279     if known_hosts is not None:
280       logging.info("Writing SSH known_hosts file (%s)", known_hosts.strip())
281       WriteFile(KNOWN_HOSTS_PATH, known_hosts)
282
283     if not options.dry_run:
284       if not os.path.exists(constants.RAPI_CERT_FILE):
285         bootstrap._GenerateSelfSignedSslCert(constants.RAPI_CERT_FILE)
286
287   except:
288     logging.critical("Writing configuration failed. It is proably in an"
289                      " inconsistent state and needs manual intervention.")
290     raise
291
292
293 if __name__ == "__main__":
294   main()
295
296 # vim: set foldmethod=marker :