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