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