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