4 # Copyright (C) 2007, 2008, 2009 Google Inc.
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.
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.
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
22 """Tool to upgrade the configuration file.
24 This code handles only the types supported by simplejson. As an
25 example, 'set' is a 'list'.
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
45 # We need to keep filenames locally because they might be renamed between
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"
56 # Unique object to identify calls without default value
60 class Error(Exception):
61 """Generic exception"""
66 """Returns the file name of an (old) ssconf key.
69 return "%s/ssconf_%s" % (constants.DATA_DIR, key)
72 def ReadFile(file_name, default=NoDefault):
76 logging.debug("Reading %s", file_name)
78 fh = open(file_name, 'r')
80 if default is not NoDefault and err.errno == errno.ENOENT:
90 def WriteFile(file_name, data):
91 """Writes a configuration file.
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)
100 """Configures the logging module.
103 formatter = logging.Formatter("%(asctime)s: %(message)s")
105 stderr_handler = logging.StreamHandler()
106 stderr_handler.setFormatter(formatter)
108 stderr_handler.setLevel(logging.NOTSET)
109 elif options.verbose:
110 stderr_handler.setLevel(logging.INFO)
112 stderr_handler.setLevel(logging.CRITICAL)
114 root_logger = logging.getLogger("")
115 root_logger.setLevel(logging.NOTSET)
116 root_logger.addHandler(stderr_handler)
119 def Cluster12To20(cluster):
120 """Upgrades the cluster object from 1.2 to 2.0.
123 logging.info("Upgrading the cluster object")
124 # Upgrade the configuration version
125 if 'config_version' in cluster:
126 del cluster['config_version']
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()
134 if 'default_hypervisor' not in cluster:
135 old_hyp = ReadFile(SsconfName('hypervisor')).strip()
136 if old_hyp == "xen-3.0":
138 elif old_hyp == "xen-hvm-3.1":
140 elif old_hyp == "fake":
143 raise Error("Unknown old hypervisor name '%s'" % old_hyp)
145 logging.info("Setting the default and enabled hypervisor")
146 cluster['default_hypervisor'] = hyp
147 cluster['enabled_hypervisors'] = [hyp]
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}
158 if 'file_storage_dir' not in cluster:
159 cluster['file_storage_dir'] = constants.DEFAULT_FILE_STORAGE_DIR
162 def Node12To20(node):
163 """Upgrades a node from 1.2 to 2.0.
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':
182 program = os.path.basename(sys.argv[0])
185 parser = optparse.OptionParser(usage="%prog [--debug|--verbose] [--force]")
186 parser.add_option('--dry-run', dest='dry_run',
188 help="Try to do the conversion, but don't write"
190 parser.add_option(cli.FORCE_OPT)
191 parser.add_option(cli.DEBUG_OPT)
192 parser.add_option('-v', '--verbose', dest='verbose',
194 help="Verbose output")
195 (options, args) = parser.parse_args()
201 raise Error("No arguments expected")
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):
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)
216 config_version = ReadFile(SSCONF_CONFIG_VERSION_PATH, "1.2").strip()
217 logging.info("Found configuration version %s", config_version)
219 config_data = serializer.LoadJson(ReadFile(CONFIG_DATA_PATH))
222 if config_version == "1.2":
223 logging.info("Found a Ganeti 1.2 configuration")
225 cluster = config_data["cluster"]
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" %
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
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")
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)
253 Cluster12To20(cluster)
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])
262 # TODO: add instance upgrade
263 for instance in config_data['instances'].values():
267 logging.info("Found a Ganeti 2.0 configuration")
269 if "config_version" in config_data["cluster"]:
270 raise Error("Inconsistent configuration: found config_data in"
271 " configuration file")
276 logging.info("Writing configuration file")
277 WriteFile(CONFIG_DATA_PATH, serializer.DumpJson(config_data))
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)
283 if not options.dry_run:
284 if not os.path.exists(constants.RAPI_CERT_FILE):
285 bootstrap._GenerateSelfSignedSslCert(constants.RAPI_CERT_FILE)
288 logging.critical("Writing configuration failed. It is proably in an"
289 " inconsistent state and needs manual intervention.")
293 if __name__ == "__main__":
296 # vim: set foldmethod=marker :