Merge branch 'stable-2.6-esi' into stable-2.6-ippool-hotplug-esi
[ganeti-local] / tools / sanitize-config
1 #!/usr/bin/python
2 #
3
4 # Copyright (C) 2010 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 # pylint: disable=C0103
23
24 """Tool to sanitize/randomize the configuration file.
25
26 """
27
28 import sys
29 import os
30 import os.path
31 import optparse
32
33 from ganeti import constants
34 from ganeti import serializer
35 from ganeti import utils
36 from ganeti import cli
37 from ganeti.cli import cli_option
38
39
40 OPTS = [
41   cli.VERBOSE_OPT,
42   cli_option("--path", help="Convert this configuration file"
43              " instead of '%s'" % constants.CLUSTER_CONF_FILE,
44              default=constants.CLUSTER_CONF_FILE, dest="CONFIG_DATA_PATH"),
45   cli_option("--sanitize-names", default="yes", type="bool",
46              help="Randomize the cluster, node and instance names [yes]"),
47   cli_option("--sanitize-ips", default="yes", type="bool",
48              help="Randomize the cluster, node and instance IPs [yes]"),
49   cli_option("--sanitize-lvs", default="no", type="bool",
50              help="Randomize the LV names (for old clusters) [no]"),
51   cli_option("--sanitize-os-names", default="yes", type="bool",
52              help="Randomize the OS names [yes]"),
53   cli_option("--no-randomization", default=False, action="store_true",
54              help="Disable all name randomization (only randomize secrets)"),
55   cli_option("--base-domain", default="example.com",
56              help="The base domain used for new names [example.com]"),
57   ]
58
59
60 def Error(txt, *args):
61   """Writes a message to standard error and exits.
62
63   """
64   cli.ToStderr(txt, *args)
65   sys.exit(1)
66
67
68 def GenerateNameMap(opts, names, base):
69   """For a given set of names, generate a list of sane new names.
70
71   """
72   names = utils.NiceSort(names)
73   name_map = {}
74   for idx, old_name in enumerate(names):
75     new_name = "%s%d.%s" % (base, idx + 1, opts.base_domain)
76     if new_name in names:
77       Error("Name conflict for %s: %s already exists", base, new_name)
78     name_map[old_name] = new_name
79   return name_map
80
81
82 def SanitizeSecrets(opts, cfg): # pylint: disable=W0613
83   """Cleanup configuration secrets.
84
85   """
86   cfg["cluster"]["rsahostkeypub"] = ""
87   for instance in cfg["instances"].values():
88     for disk in instance["disks"]:
89       RandomizeDiskSecrets(disk)
90
91
92 def SanitizeCluster(opts, cfg):
93   """Sanitize the cluster names.
94
95   """
96   cfg["cluster"]["cluster_name"] = "cluster." + opts.base_domain
97
98
99 def SanitizeNodes(opts, cfg):
100   """Sanitize node names.
101
102   """
103   old_names = cfg["nodes"].keys()
104   old_map = GenerateNameMap(opts, old_names, "node")
105
106   # rename nodes
107   RenameDictKeys(cfg["nodes"], old_map, True)
108
109   # update master node
110   cfg["cluster"]["master_node"] = old_map[cfg["cluster"]["master_node"]]
111
112   # update instance configuration
113   for instance in cfg["instances"].values():
114     instance["primary_node"] = old_map[instance["primary_node"]]
115     for disk in instance["disks"]:
116       RenameDiskNodes(disk, old_map)
117
118
119 def SanitizeInstances(opts, cfg):
120   """Sanitize instance names.
121
122   """
123   old_names = cfg["instances"].keys()
124   old_map = GenerateNameMap(opts, old_names, "instance")
125
126   RenameDictKeys(cfg["instances"], old_map, True)
127
128
129 def SanitizeIps(opts, cfg): # pylint: disable=W0613
130   """Sanitize the IP names.
131
132   @note: we're interested in obscuring the old IPs, not in generating
133       actually valid new IPs, so we chose to simply put IPv4
134       addresses, irrelevant of whether IPv6 or IPv4 addresses existed
135       before.
136
137   """
138   def _Get(old):
139     if old in ip_map:
140       return ip_map[old]
141     idx = len(ip_map) + 1
142     rest, d_octet = divmod(idx, 256)
143     rest, c_octet = divmod(rest, 256)
144     rest, b_octet = divmod(rest, 256)
145     if rest > 0:
146       Error("Too many IPs!")
147     new_ip = "%d.%d.%d.%d" % (10, b_octet, c_octet, d_octet)
148     ip_map[old] = new_ip
149     return new_ip
150
151   ip_map = {}
152
153   cfg["cluster"]["master_ip"] = _Get(cfg["cluster"]["master_ip"])
154   for node in cfg["nodes"].values():
155     node["primary_ip"] = _Get(node["primary_ip"])
156     node["secondary_ip"] = _Get(node["secondary_ip"])
157
158   for instance in cfg["instances"].values():
159     for nic in instance["nics"]:
160       if "ip" in nic and nic["ip"]:
161         nic["ip"] = _Get(nic["ip"])
162
163
164 def SanitizeOsNames(opts, cfg): # pylint: disable=W0613
165   """Sanitize the OS names.
166
167   """
168   def _Get(old):
169     if old in os_map:
170       return os_map[old]
171     os_map[old] = "ganeti-os%d" % (len(os_map) + 1)
172     return os_map[old]
173
174   os_map = {}
175   for instance in cfg["instances"].values():
176     instance["os"] = _Get(instance["os"])
177
178   if "os_hvp" in cfg["cluster"]:
179     for os_name in cfg["cluster"]["os_hvp"]:
180       # force population of the entire os map
181       _Get(os_name)
182     RenameDictKeys(cfg["cluster"]["os_hvp"], os_map, False)
183
184
185 def SanitizeDisks(opts, cfg): # pylint: disable=W0613
186   """Cleanup disks disks.
187
188   """
189   def _Get(old):
190     if old in lv_map:
191       return old
192     lv_map[old] = utils.NewUUID()
193     return lv_map[old]
194
195   def helper(disk):
196     if "children" in disk and disk["children"]:
197       for child in disk["children"]:
198         helper(child)
199
200     if disk["dev_type"] == constants.LD_DRBD8:
201       if "physical_id" in disk:
202         del disk["physical_id"]
203
204     if disk["dev_type"] == constants.LD_LV and opts.sanitize_lvs:
205       disk["logical_id"][1] = _Get(disk["logical_id"][1])
206       disk["physical_id"][1] = disk["logical_id"][1]
207
208   lv_map = {}
209
210   for instance in cfg["instances"].values():
211     for disk in instance["disks"]:
212       helper(disk)
213
214
215 def RandomizeDiskSecrets(disk):
216   """Randomize a disks' secrets (if any).
217
218   """
219   if "children" in disk and disk["children"]:
220     for child in disk["children"]:
221       RandomizeDiskSecrets(child)
222
223   # only disk type to contain secrets is the drbd one
224   if disk["dev_type"] == constants.LD_DRBD8:
225     disk["logical_id"][5] = utils.GenerateSecret()
226
227
228 def RenameDiskNodes(disk, node_map):
229   """Rename nodes in the disk config.
230
231   """
232   if "children" in disk and disk["children"]:
233     for child in disk["children"]:
234       RenameDiskNodes(child, node_map)
235
236   # only disk type to contain nodes is the drbd one
237   if disk["dev_type"] == constants.LD_DRBD8:
238     lid = disk["logical_id"]
239     lid[0] = node_map[lid[0]]
240     lid[1] = node_map[lid[1]]
241
242
243 def RenameDictKeys(a_dict, name_map, update_name):
244   """Rename the dictionary keys based on a name map.
245
246   """
247   for old_name in a_dict.keys():
248     new_name = name_map[old_name]
249     a_dict[new_name] = a_dict[old_name]
250     del a_dict[old_name]
251     if update_name:
252       a_dict[new_name]["name"] = new_name
253
254
255 def main():
256   """Main program.
257
258   """
259   # Option parsing
260   parser = optparse.OptionParser(usage="%prog [--verbose] output_file")
261
262   for o in OPTS:
263     parser.add_option(o)
264
265   (opts, args) = parser.parse_args()
266   if opts.no_randomization:
267     opts.sanitize_names = opts.sanitize_ips = opts.sanitize_os_names = \
268         opts.sanitize_lvs = False
269
270   # Option checking
271   if len(args) != 1:
272     Error("Usage: sanitize-config [options] {<output_file> | -}")
273
274   # Check whether it's a Ganeti configuration directory
275   if not os.path.isfile(opts.CONFIG_DATA_PATH):
276     Error("Cannot find Ganeti configuration file %s", opts.CONFIG_DATA_PATH)
277
278   config_data = serializer.LoadJson(utils.ReadFile(opts.CONFIG_DATA_PATH))
279
280   # first, do some disk cleanup: remove DRBD physical_ids, since it
281   # contains both IPs (which we want changed) and the DRBD secret, and
282   # it's not needed for normal functioning, and randomize LVM names
283   SanitizeDisks(opts, config_data)
284
285   SanitizeSecrets(opts, config_data)
286
287   if opts.sanitize_names:
288     SanitizeCluster(opts, config_data)
289     SanitizeNodes(opts, config_data)
290     SanitizeInstances(opts, config_data)
291
292   if opts.sanitize_ips:
293     SanitizeIps(opts, config_data)
294
295   if opts.sanitize_os_names:
296     SanitizeOsNames(opts, config_data)
297
298   data = serializer.DumpJson(config_data)
299   if args[0] == "-":
300     sys.stdout.write(data)
301   else:
302     utils.WriteFile(file_name=args[0],
303                     data=data,
304                     mode=0600,
305                     backup=True)
306
307 if __name__ == "__main__":
308   main()