| #!/usr/bin/python |
| # |
| |
| # Copyright (C) 2010 Google Inc. |
| # All rights reserved. |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions are |
| # met: |
| # |
| # 1. Redistributions of source code must retain the above copyright notice, |
| # this list of conditions and the following disclaimer. |
| # |
| # 2. Redistributions in binary form must reproduce the above copyright |
| # notice, this list of conditions and the following disclaimer in the |
| # documentation and/or other materials provided with the distribution. |
| # |
| # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS |
| # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
| # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR |
| # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
| # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
| # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
| # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| |
| # pylint: disable=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 pathutils |
| 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'" % pathutils.CLUSTER_CONF_FILE, |
| default=pathutils.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=W0613 |
| """Cleanup configuration secrets. |
| |
| """ |
| cfg["cluster"]["rsahostkeypub"] = "" |
| cfg["cluster"]["dsahostkeypub"] = "" |
| 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=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=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=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.DT_PLAIN and opts.sanitize_lvs: |
| disk["logical_id"][1] = _Get(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.DT_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.DT_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)) |
| |
| # 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() |