phosphor-settings: Add task to handle settings overrides

Add a BitBake task that merges YAML fragments that overwrite the default
settings YAML file. These fragments have the suffix ".override.yml" and
allow e.g. machine layers to override the values in settings.yaml
without replacing its entire contents.

Fixes openbmc/openbmc#1269.

Signed-off-by: Xo Wang <xow@google.com>
Change-Id: I8d463630deee74ed6d5c18aa98e837a6ca7f7633
diff --git a/common/recipes-phosphor/settings/phosphor-settings.bb b/common/recipes-phosphor/settings/phosphor-settings.bb
index 686f3c0..d03eca8 100644
--- a/common/recipes-phosphor/settings/phosphor-settings.bb
+++ b/common/recipes-phosphor/settings/phosphor-settings.bb
@@ -7,6 +7,7 @@
 inherit obmc-phosphor-license
 inherit setuptools
 inherit obmc-phosphor-dbus-service
+inherit pythonnative
 
 DBUS_SERVICE_${PN} = "org.openbmc.settings.Host.service"
 
@@ -16,7 +17,35 @@
 RPROVIDES_${PN} += "virtual-obmc-settings-mgmt"
 
 SRC_URI += "git://github.com/openbmc/phosphor-settingsd"
+SRC_URI += "file://merge_settings.py"
 
 SRCREV = "3b8d055ca761a68c74dad01a306f238674d71878"
 
 S = "${WORKDIR}/git"
+
+# Collect files in SRC_URI that end in ".override.yml" and call a script that
+# writes their contents over that of settings.yaml, which is then updated to
+# the merged data values.
+# This doesn't correctly handle globs in ".override.yml" entries in SRC_URI.
+python do_merge_settings () {
+    import subprocess
+
+    # TODO: Perform the merge in a temporary directory?
+    workdir = d.getVar('WORKDIR', True)
+    cmd = []
+    cmd.append(os.path.join(workdir, 'merge_settings.py'))
+    cmd.append(os.path.join(d.getVar('S', True), 'settings.yaml'))
+
+    fetch = bb.fetch2.Fetch([], d)
+    override_urls = filter(lambda f: f.endswith('.override.yml'), fetch.urls)
+    for url in override_urls:
+        bb.debug(2, 'Overriding with source: ' + url)
+        local_base = os.path.basename(fetch.localpath(url))
+        filename = os.path.join(workdir, local_base)
+        cmd.append(filename)
+
+    # Invoke the script and don't catch any resulting exception.
+    subprocess.check_call(cmd)
+}
+# python-pyyaml-native is installed by do_configure, so put this task after
+addtask merge_settings after do_configure before do_compile
diff --git a/common/recipes-phosphor/settings/phosphor-settings/merge_settings.py b/common/recipes-phosphor/settings/phosphor-settings/merge_settings.py
new file mode 100755
index 0000000..01f5e35
--- /dev/null
+++ b/common/recipes-phosphor/settings/phosphor-settings/merge_settings.py
@@ -0,0 +1,58 @@
+#!/usr/bin/env python
+"""Loads a "target" YAML file and overwrites its values with values from
+"override" YAML files.
+
+Override files are processed in the order given.
+
+Usage:
+    merge_settings.py <target yaml> [override yamls]
+"""
+import sys
+import yaml
+import copy
+
+def dict_merge(target, source):
+    """Deep merge for dicts.
+
+    Works like dict.update() that recursively updates any dict values present in
+    both parameters.
+
+    Args:
+        target (dict): Values to be overwritten by corresponding values from
+            `source`.
+        source (dict): Overriding values. Not changed by call.
+
+    Returns:
+        `target` with values overwritten from those in `source` at any and all
+        levels of nested dicts.
+    """
+    if not isinstance(source, dict):
+        return source
+    for k, v in source.iteritems():
+        if k in target and isinstance(target[k], dict):
+            dict_merge(target[k], v)
+        else:
+            target[k] = copy.deepcopy(v)
+    return target
+
+if len(sys.argv) < 2:
+    sys.exit('Argument required: target yaml')
+
+if len(sys.argv) == 2:
+    # No overrides to handle
+    sys.exit()
+
+target_filename = sys.argv[1]
+with open(target_filename) as target_file:
+    data = yaml.safe_load(target_file)
+    print('Loaded target YAML file ' + target_filename)
+
+for override_filename in sys.argv[2:]:
+    with open(override_filename) as override_file:
+        override = yaml.safe_load(override_file)
+        dict_merge(data, override)
+        print('Merged override YAML file ' + override_filename)
+
+with open(target_filename, 'w') as target_file:
+    yaml.dump(data, target_file)
+    print('Wrote merged target YAML file ' + target_filename)