Implement --dry-run for cfgupgrade.
[ganeti-local] / tools / cfgupgrade
1 #!/usr/bin/python
2 #
3
4 # Copyright (C) 2007 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 The upgrade is done by unpickling the configuration file into custom classes
25 derivating from dict. We then update the configuration by modifying these
26 dicts. To save the configuration, it's pickled into a buffer and unpickled
27 again using the Ganeti objects before being finally pickled into a file.
28
29 Not using the custom classes wouldn't allow us to rename or remove attributes
30 between versions without loosing their values.
31
32 """
33
34
35 import os
36 import os.path
37 import sys
38 import optparse
39 import cPickle
40 import tempfile
41 from cStringIO import StringIO
42
43 from ganeti import objects
44
45 class Error(Exception):
46   """Generic exception"""
47   pass
48
49
50 def _BaseFindGlobal(module, name):
51   """Helper function for the other FindGlobal functions.
52
53   """
54   return getattr(sys.modules[module], name)
55
56
57 # Internal config representation
58 class UpgradeDict(dict):
59   """Base class for internal config classes.
60
61   """
62   def __setstate__(self, state):
63     self.update(state)
64
65   def __getstate__(self):
66     return self.copy()
67
68
69 class UpgradeConfigData(UpgradeDict): pass
70 class UpgradeCluster(UpgradeDict): pass
71 class UpgradeNode(UpgradeDict): pass
72 class UpgradeInstance(UpgradeDict): pass
73 class UpgradeDisk(UpgradeDict): pass
74 class UpgradeNIC(UpgradeDict): pass
75 class UpgradeOS(UpgradeDict): pass
76
77
78 _ClassMap = {
79   objects.ConfigData: UpgradeConfigData,
80   objects.Cluster: UpgradeCluster,
81   objects.Node: UpgradeNode,
82   objects.Instance: UpgradeInstance,
83   objects.Disk: UpgradeDisk,
84   objects.NIC: UpgradeNIC,
85   objects.OS: UpgradeOS,
86 }
87
88 # Build mapping dicts
89 WriteMapping = dict()
90 ReadMapping = dict()
91 for key, value in _ClassMap.iteritems():
92   WriteMapping[value.__name__] = key
93   ReadMapping[key.__name__] = value
94
95
96 # Read config
97 def _ReadFindGlobal(module, name):
98   """Wraps Ganeti config classes to internal ones.
99
100   """
101   if module == "ganeti.objects" and name in ReadMapping:
102     return ReadMapping[name]
103
104   return _BaseFindGlobal(module, name)
105
106
107 def ReadConfig(path):
108   """Reads configuration file.
109
110   """
111   f = open(path, 'r')
112   try:
113     loader = cPickle.Unpickler(f)
114     loader.find_global = _ReadFindGlobal
115     data = loader.load()
116   finally:
117     f.close()
118
119   return data
120
121
122 # Write config
123 def _WriteFindGlobal(module, name):
124   """Maps our internal config classes to Ganeti's.
125
126   """
127   if module == "__main__" and name in WriteMapping:
128     return WriteMapping[name]
129
130   return _BaseFindGlobal(module, name)
131
132
133 def WriteConfig(path, data, dry_run):
134   """Writes the configuration file.
135
136   """
137   buf = StringIO()
138
139   # Write intermediate representation
140   dumper = cPickle.Pickler(buf, cPickle.HIGHEST_PROTOCOL)
141   dumper.dump(data)
142   del dumper
143
144   # Convert back to Ganeti objects
145   buf.seek(0)
146   loader = cPickle.Unpickler(buf)
147   loader.find_global = _WriteFindGlobal
148   data = loader.load()
149
150   # Write target file
151   (fd, name) = tempfile.mkstemp(dir=os.path.dirname(path))
152   f = os.fdopen(fd, 'w')
153   try:
154     try:
155       dumper = cPickle.Pickler(f, cPickle.HIGHEST_PROTOCOL)
156       dumper.dump(data)
157       f.flush()
158       if dry_run:
159         os.unlink(name)
160       else:
161         os.rename(name, path)
162     except:
163       os.unlink(name)
164       raise
165   finally:
166     f.close()
167
168
169 def UpdateFromVersion2To3(cfg):
170   """Updates the configuration from version 2 to 3.
171
172   """
173   if cfg['cluster']['config_version'] != 2:
174     return
175
176   # Add port pool
177   if 'tcpudp_port_pool' not in cfg['cluster']:
178     cfg['cluster']['tcpudp_port_pool'] = set()
179
180   # Add bridge settings
181   if 'default_bridge' not in cfg['cluster']:
182     cfg['cluster']['default_bridge'] = 'xen-br0'
183   for inst in cfg['instances'].values():
184     for nic in inst['nics']:
185       if 'bridge' not in nic:
186         nic['bridge'] = None
187
188   cfg['cluster']['config_version'] = 3
189
190
191 # Main program
192 if __name__ == "__main__":
193   # Option parsing
194   parser = optparse.OptionParser()
195   parser.add_option('--dry-run', dest='dry_run',
196                     action="store_true",
197                     help="Try to do the conversion, but don't write "
198                       "output file")
199   parser.add_option('--verbose', dest='verbose',
200                     action="store_true",
201                     help="Verbose output")
202   (options, args) = parser.parse_args()
203
204   # Option checking
205   if args:
206     cfg_file = args[0]
207   else:
208     raise Error, ("Configuration file not specified")
209
210   config = ReadConfig(cfg_file)
211
212   UpdateFromVersion2To3(config)
213
214   if options.verbose:
215     import pprint
216     pprint.pprint(config)
217
218   WriteConfig(cfg_file, config, options.dry_run)