Export tags to cluster verify hooks
[ganeti-local] / tools / cfgupgrade
1 #!/usr/bin/python
2 #
3
4 # Copyright (C) 2007, 2008 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 example, "set"
25 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
43
44 # We need to keep filenames locally because they might be renamed between
45 # versions.
46 CONFIG_DATA_PATH = constants.DATA_DIR + "/config.data"
47 SERVER_PEM_PATH = constants.DATA_DIR + "/server.pem"
48 KNOWN_HOSTS_PATH = constants.DATA_DIR + "/known_hosts"
49 SSCONF_CLUSTER_NAME_PATH = constants.DATA_DIR + "/ssconf_cluster_name"
50 SSCONF_CONFIG_VERSION_PATH = constants.DATA_DIR + "/ssconf_config_version"
51
52 options = None
53 args = None
54
55 # Unique object to identify calls without default value
56 NoDefault = object()
57
58
59 class Error(Exception):
60   """Generic exception"""
61   pass
62
63
64 def ReadFile(file_name, default=NoDefault):
65   """Reads a file.
66
67   """
68   logging.debug("Reading %s", file_name)
69   try:
70     fh = open(file_name, 'r')
71   except IOError, err:
72     if default is not NoDefault and err.errno == errno.ENOENT:
73       return default
74     raise
75
76   try:
77     return fh.read()
78   finally:
79     fh.close()
80
81
82 def WriteFile(file_name, data):
83   """Writes a configuration file.
84
85   """
86   logging.debug("Writing %s", file_name)
87   utils.WriteFile(file_name=file_name, data=data, mode=0600,
88                   dry_run=options.dry_run, backup=True)
89
90
91 def SetupLogging():
92   """Configures the logging module.
93
94   """
95   formatter = logging.Formatter("%(asctime)s: %(message)s")
96
97   stderr_handler = logging.StreamHandler()
98   stderr_handler.setFormatter(formatter)
99   if options.debug:
100     stderr_handler.setLevel(logging.NOTSET)
101   elif options.verbose:
102     stderr_handler.setLevel(logging.INFO)
103   else:
104     stderr_handler.setLevel(logging.CRITICAL)
105
106   root_logger = logging.getLogger("")
107   root_logger.setLevel(logging.NOTSET)
108   root_logger.addHandler(stderr_handler)
109
110
111 def main():
112   """Main program.
113
114   """
115   global options, args
116
117   program = os.path.basename(sys.argv[0])
118
119   # Option parsing
120   parser = optparse.OptionParser(usage="%prog [--debug|--verbose] [--force]")
121   parser.add_option('--dry-run', dest='dry_run',
122                     action="store_true",
123                     help="Try to do the conversion, but don't write"
124                          " output file")
125   parser.add_option(cli.FORCE_OPT)
126   parser.add_option(cli.DEBUG_OPT)
127   parser.add_option('--verbose', dest='verbose',
128                     action="store_true",
129                     help="Verbose output")
130   (options, args) = parser.parse_args()
131
132   SetupLogging()
133
134   # Option checking
135   if args:
136     raise Error("No arguments expected")
137
138   if not options.force:
139     usertext = ("%s MUST run on the master node. Is this the master"
140                 " node?" % program)
141     if not cli.AskUser(usertext):
142       sys.exit(1)
143
144   # Check whether it's a Ganeti configuration directory
145   if not (os.path.isfile(CONFIG_DATA_PATH) and
146           os.path.isfile(SERVER_PEM_PATH) or
147           os.path.isfile(KNOWN_HOSTS_PATH)):
148     raise Error(("%s does not seem to be a known Ganeti configuration"
149                  " directory") % constants.DATA_DIR)
150
151   config_version = ReadFile(SSCONF_CONFIG_VERSION_PATH, "1.2").strip()
152   logging.info("Found configuration version %s", config_version)
153
154   config_data = serializer.LoadJson(ReadFile(CONFIG_DATA_PATH))
155
156   # Ganeti 1.2?
157   if config_version == "1.2":
158     logging.info("Found a Ganeti 1.2 configuration")
159
160     old_config_version = config_data["cluster"].get("config_version", None)
161     logging.info("Found old configuration version %s", old_config_version)
162     if old_config_version not in (3, ):
163       raise Error("Unsupported configuration version: %s" %
164                   old_config_version)
165
166     # Make sure no instance uses remote_raid1 anymore
167     remote_raid1_instances = []
168     for instance in config_data["instances"]:
169       if instance["disk_template"] == "remote_raid1":
170         remote_raid1_instances.append(instance["name"])
171     if remote_raid1_instances:
172       for name in remote_raid1_instances:
173         logging.error("Instance %s still using remote_raid1 disk template")
174       raise Error("Unable to convert configuration as long as there are"
175                   " instances using remote_raid1 disk template")
176
177     # The configuration version will be stored in a ssconf file
178     if 'config_version' in config_data['cluster']:
179       del config_data['cluster']['config_version']
180
181     # Build content of new known_hosts file
182     cluster_name = ReadFile(SSCONF_CLUSTER_NAME_PATH).rstrip()
183     cluster_key = config_data['cluster']['rsahostkeypub']
184     known_hosts = "%s ssh-rsa %s\n" % (cluster_name, cluster_key)
185
186   else:
187     logging.info("Found a Ganeti 2.0 configuration")
188
189     if "config_version" in config_data["cluster"]:
190       raise Error("Inconsistent configuration: found config_data in"
191                   " configuration file")
192
193     known_hosts = None
194
195   config_version_str = "%s\n" % constants.BuildVersion(2, 0, 0)
196   try:
197     logging.info("Writing configuration file")
198     WriteFile(CONFIG_DATA_PATH, serializer.DumpJson(config_data))
199
200     logging.info("Writing configuration version %s",
201                  config_version_str.strip())
202     WriteFile(SSCONF_CONFIG_VERSION_PATH, config_version_str)
203
204     if known_hosts is not None:
205       logging.info("Writing SSH known_hosts file (%s)", known_hosts.strip())
206       WriteFile(KNOWN_HOSTS_PATH, known_hosts)
207   except:
208     logging.critical("Writing configuration failed. It is proably in an"
209                      " inconsistent state and needs manual intervention.")
210     raise
211
212
213 if __name__ == "__main__":
214   main()
215
216 # vim: set foldmethod=marker :