From ea5fd476ad05e0f5b01051fe38d171eabe69c5dd Mon Sep 17 00:00:00 2001 From: Iustin Pop <iustin@google.com> Date: Tue, 13 Apr 2010 18:19:39 +0200 Subject: [PATCH] Add a new tool: sanitize-config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This can be used for two purposes: - safety copy of the config file, with just the secrets changed - cleanup of the config file (full randomization), so that (e.g.) users could send a broken config file to the devel-list Signed-off-by: Iustin Pop <iustin@google.com> Reviewed-by: RenΓ© Nussbaumer <rn@google.com> --- Makefile.am | 3 +- doc/admin.rst | 22 +++ tools/sanitize-config | 308 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 332 insertions(+), 1 deletion(-) create mode 100755 tools/sanitize-config diff --git a/Makefile.am b/Makefile.am index 4e9d3c423..fbb8f8745 100644 --- a/Makefile.am +++ b/Makefile.am @@ -235,7 +235,8 @@ dist_tools_SCRIPTS = \ tools/cfgshell \ tools/cfgupgrade \ tools/cluster-merge \ - tools/lvmstrap + tools/lvmstrap \ + tools/sanitize-config pkglib_SCRIPTS = \ daemons/daemon-util diff --git a/doc/admin.rst b/doc/admin.rst index cefa8e2e9..8ac2b59c5 100644 --- a/doc/admin.rst +++ b/doc/admin.rst @@ -1350,6 +1350,28 @@ systems. Depending on the passed options, it can also test that the instance OS definitions are executing properly the rename, import and export operations. +sanitize-config ++++++++++++++++ + +This tool takes the Ganeti configuration and outputs a "sanitized" +version, by randomizing or clearing: + +- DRBD secrets and cluster public key (always) +- host names (optional) +- IPs (optional) +- OS names (optional) +- LV names (optional, only useful for very old clusters which still have + instances whose LVs are based on the instance name) + +By default, all optional items are activated except the LV name +randomization. When passing ``--no-randomization``, which disables the +optional items (i.e. just the DRBD secrets and cluster public keys are +randomized), the resulting file can be used as a safety copy of the +cluster config - while not trivial, the layout of the cluster can be +recreated from it and if the instance disks have not been lost it +permits recovery from the loss of all master candidates. + + Other Ganeti projects --------------------- diff --git a/tools/sanitize-config b/tools/sanitize-config new file mode 100755 index 000000000..ca9c95539 --- /dev/null +++ b/tools/sanitize-config @@ -0,0 +1,308 @@ +#!/usr/bin/python +# + +# Copyright (C) 2010 Google Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. + + +# pylint: disable-msg=C0103 + +"""Tool to sanitize/randomize the configuration file. + +""" + +import sys +import os +import os.path +import optparse + +from ganeti import constants +from ganeti import serializer +from ganeti import utils +from ganeti import cli +from ganeti.cli import cli_option + + +OPTS = [ + cli.VERBOSE_OPT, + cli_option("--path", help="Convert this configuration file" + " instead of '%s'" % constants.CLUSTER_CONF_FILE, + default=constants.CLUSTER_CONF_FILE, dest="CONFIG_DATA_PATH"), + cli_option("--sanitize-names", default="yes", type="bool", + help="Randomize the cluster, node and instance names [yes]"), + cli_option("--sanitize-ips", default="yes", type="bool", + help="Randomize the cluster, node and instance IPs [yes]"), + cli_option("--sanitize-lvs", default="no", type="bool", + help="Randomize the LV names (for old clusters) [no]"), + cli_option("--sanitize-os-names", default="yes", type="bool", + help="Randomize the OS names [yes]"), + cli_option("--no-randomization", default=False, action="store_true", + help="Disable all name randomization (only randomize secrets)"), + cli_option("--base-domain", default="example.com", + help="The base domain used for new names [example.com]"), + ] + + +def Error(txt, *args): + """Writes a message to standard error and exits. + + """ + cli.ToStderr(txt, *args) + sys.exit(1) + + +def GenerateNameMap(opts, names, base): + """For a given set of names, generate a list of sane new names. + + """ + names = utils.NiceSort(names) + name_map = {} + for idx, old_name in enumerate(names): + new_name = "%s%d.%s" % (base, idx + 1, opts.base_domain) + if new_name in names: + Error("Name conflict for %s: %s already exists", base, new_name) + name_map[old_name] = new_name + return name_map + + +def SanitizeSecrets(opts, cfg): # pylint: disable-msg=W0613 + """Cleanup configuration secrets. + + """ + cfg["cluster"]["rsahostkeypub"] = "" + for instance in cfg["instances"].values(): + for disk in instance["disks"]: + RandomizeDiskSecrets(disk) + + +def SanitizeCluster(opts, cfg): + """Sanitize the cluster names. + + """ + cfg["cluster"]["cluster_name"] = "cluster." + opts.base_domain + + +def SanitizeNodes(opts, cfg): + """Sanitize node names. + + """ + old_names = cfg["nodes"].keys() + old_map = GenerateNameMap(opts, old_names, "node") + + # rename nodes + RenameDictKeys(cfg["nodes"], old_map, True) + + # update master node + cfg["cluster"]["master_node"] = old_map[cfg["cluster"]["master_node"]] + + # update instance configuration + for instance in cfg["instances"].values(): + instance["primary_node"] = old_map[instance["primary_node"]] + for disk in instance["disks"]: + RenameDiskNodes(disk, old_map) + + +def SanitizeInstances(opts, cfg): + """Sanitize instance names. + + """ + old_names = cfg["instances"].keys() + old_map = GenerateNameMap(opts, old_names, "instance") + + RenameDictKeys(cfg["instances"], old_map, True) + + +def SanitizeIps(opts, cfg): # pylint: disable-msg=W0613 + """Sanitize the IP names. + + @note: we're interested in obscuring the old IPs, not in generating + actually valid new IPs, so we chose to simply put IPv4 + addresses, irrelevant of whether IPv6 or IPv4 addresses existed + before. + + """ + def _Get(old): + if old in ip_map: + return ip_map[old] + idx = len(ip_map) + 1 + rest, d_octet = divmod(idx, 256) + rest, c_octet = divmod(rest, 256) + rest, b_octet = divmod(rest, 256) + if rest > 0: + Error("Too many IPs!") + new_ip = "%d.%d.%d.%d" % (10, b_octet, c_octet, d_octet) + ip_map[old] = new_ip + return new_ip + + ip_map = {} + + cfg["cluster"]["master_ip"] = _Get(cfg["cluster"]["master_ip"]) + for node in cfg["nodes"].values(): + node["primary_ip"] = _Get(node["primary_ip"]) + node["secondary_ip"] = _Get(node["secondary_ip"]) + + for instance in cfg["instances"].values(): + for nic in instance["nics"]: + if "ip" in nic and nic["ip"]: + nic["ip"] = _Get(nic["ip"]) + + +def SanitizeOsNames(opts, cfg): # pylint: disable-msg=W0613 + """Sanitize the OS names. + + """ + def _Get(old): + if old in os_map: + return os_map[old] + os_map[old] = "ganeti-os%d" % (len(os_map) + 1) + return os_map[old] + + os_map = {} + for instance in cfg["instances"].values(): + instance["os"] = _Get(instance["os"]) + + if "os_hvp" in cfg["cluster"]: + for os_name in cfg["cluster"]["os_hvp"]: + # force population of the entire os map + _Get(os_name) + RenameDictKeys(cfg["cluster"]["os_hvp"], os_map, False) + + +def SanitizeDisks(opts, cfg): # pylint: disable-msg=W0613 + """Cleanup disks disks. + + """ + def _Get(old): + if old in lv_map: + return old + lv_map[old] = utils.NewUUID() + return lv_map[old] + + def helper(disk): + if "children" in disk and disk["children"]: + for child in disk["children"]: + helper(child) + + if disk["dev_type"] == constants.LD_DRBD8: + if "physical_id" in disk: + del disk["physical_id"] + + if disk["dev_type"] == constants.LD_LV and opts.sanitize_lvs: + disk["logical_id"][1] = _Get(disk["logical_id"][1]) + disk["physical_id"][1] = disk["logical_id"][1] + + lv_map = {} + + for instance in cfg["instances"].values(): + for disk in instance["disks"]: + helper(disk) + + +def RandomizeDiskSecrets(disk): + """Randomize a disks' secrets (if any). + + """ + if "children" in disk and disk["children"]: + for child in disk["children"]: + RandomizeDiskSecrets(child) + + # only disk type to contain secrets is the drbd one + if disk["dev_type"] == constants.LD_DRBD8: + disk["logical_id"][5] = utils.GenerateSecret() + + +def RenameDiskNodes(disk, node_map): + """Rename nodes in the disk config. + + """ + if "children" in disk and disk["children"]: + for child in disk["children"]: + RenameDiskNodes(child, node_map) + + # only disk type to contain nodes is the drbd one + if disk["dev_type"] == constants.LD_DRBD8: + lid = disk["logical_id"] + lid[0] = node_map[lid[0]] + lid[1] = node_map[lid[1]] + + +def RenameDictKeys(a_dict, name_map, update_name): + """Rename the dictionary keys based on a name map. + + """ + for old_name in a_dict.keys(): + new_name = name_map[old_name] + a_dict[new_name] = a_dict[old_name] + del a_dict[old_name] + if update_name: + a_dict[new_name]["name"] = new_name + + +def main(): + """Main program. + + """ + # Option parsing + parser = optparse.OptionParser(usage="%prog [--verbose] output_file") + + for o in OPTS: + parser.add_option(o) + + (opts, args) = parser.parse_args() + if opts.no_randomization: + opts.sanitize_names = opts.sanitize_ips = opts.sanitize_os_names = \ + opts.sanitize_lvs = False + + # Option checking + if len(args) != 1: + Error("Usage: sanitize-config [options] {<output_file> | -}") + + # Check whether it's a Ganeti configuration directory + if not os.path.isfile(opts.CONFIG_DATA_PATH): + Error("Cannot find Ganeti configuration file %s", opts.CONFIG_DATA_PATH) + + config_data = serializer.LoadJson(utils.ReadFile(opts.CONFIG_DATA_PATH)) + + # first, do some disk cleanup: remove DRBD physical_ids, since it + # contains both IPs (which we want changed) and the DRBD secret, and + # it's not needed for normal functioning, and randomize LVM names + SanitizeDisks(opts, config_data) + + SanitizeSecrets(opts, config_data) + + if opts.sanitize_names: + SanitizeCluster(opts, config_data) + SanitizeNodes(opts, config_data) + SanitizeInstances(opts, config_data) + + if opts.sanitize_ips: + SanitizeIps(opts, config_data) + + if opts.sanitize_os_names: + SanitizeOsNames(opts, config_data) + + data = serializer.DumpJson(config_data) + if args[0] == "-": + sys.stdout.write(data) + else: + utils.WriteFile(file_name=args[0], + data=data, + mode=0600, + backup=True) + +if __name__ == "__main__": + main() -- GitLab