use run-parts semantics in conf.d directory
[invirt/packages/invirt-base.git] / python / invirt / config.py
index 69cb899..86d9c8a 100644 (file)
@@ -2,17 +2,71 @@ from __future__ import with_statement
 
 import json
 from invirt.common import *
+import os
 from os import rename
 from os.path import getmtime
 from contextlib import closing
+import yaml
+import re
 
-default_src_path   = '/etc/invirt/master.yaml'
-default_cache_path = '/var/lib/invirt/cache.json'
-lock_path          = '/var/lib/invirt/cache.lock'
+try:    loader = yaml.CSafeLoader
+except: loader = yaml.SafeLoader
 
-def load(src_path = default_src_path,
-         cache_path = default_cache_path,
-         force_refresh = False):
+src_path    = '/etc/invirt/master.yaml'
+src_dirpath = '/etc/invirt/conf.d'
+cache_path  = '/var/lib/invirt/cache.json'
+lock_path   = '/var/lib/invirt/cache.lock'
+
+def augment(d1, d2):
+    """Splice dict-tree d2 into d1.  Return d1.
+
+    Example:
+    >>> d = {'a': {'b': 1}, 'c': 2}
+    >>> augment(d, {'a': {'d': 3}})
+    {'a': {'b', 1, 'd': 3}, 'c': 2}
+    >>> d
+    {'a': {'b', 1, 'd': 3}, 'c': 2}
+    """
+    for k in d2:
+        if k in d1 and isinstance(d1[k], dict):
+            augment(d1[k], d2[k])
+        else:
+            d1[k] = d2[k]
+    return d1
+
+def run_parts_list(dirname):
+    """Reimplements Debian's run-parts --list.
+
+    One difference from run-parts's behavior: run-parts --list /foo/
+    will give output like /foo//bar, because Python code tends to expect this.
+
+    Matches documented behavior of run-parts in debianutils v2.28.2, dated 2007.
+    """
+    # From run-parts(8).
+    lanana_re   = re.compile('^[a-z0-9]+$')
+    lsb_re      = re.compile('^_?([a-z0-9_.]+-)+[a-z0-9]+$')
+    deb_cron_re = re.compile('^[a-z0-9][a-z0-9-]*$')
+    for name in os.listdir(dirname):
+        if lanana_re.match(name) or lsb_re.match(name) or deb_cron_re.match(name):
+            yield os.path.join(dirname, name)
+
+def list_files():
+    yield src_path
+    for name in run_parts_list(src_dirpath):
+        yield name
+
+def load_master():
+    config = dict()
+    for filename in list_files():
+        with closing(file(filename)) as f:
+            augment(config, yaml.load(f, loader))
+    return config
+
+def get_src_mtime():
+    return max(max(getmtime(filename) for filename in list_files()),
+               getmtime(src_dirpath))
+
+def load(force_refresh = False):
     """
     Try loading the configuration from the faster-to-load JSON cache at
     cache_path.  If it doesn't exist or is outdated, load the configuration
@@ -27,7 +81,7 @@ def load(src_path = default_src_path,
     if force_refresh:
         do_refresh = True
     else:
-        src_mtime = getmtime(src_path)
+        src_mtime = get_src_mtime()
         try:            cache_mtime = getmtime(cache_path)
         except OSError: do_refresh  = True
         else:           do_refresh  = src_mtime + 1 >= cache_mtime
@@ -68,21 +122,16 @@ def load(src_path = default_src_path,
         # is interleaved).  The final atomic rename is to keep this
         # transactionally isolated from the above cache read.  If we fail to
         # acquire the lock, just try to load the master configuration.
-        import yaml
-        try:    loader = yaml.CSafeLoader
-        except: loader = yaml.SafeLoader
         try:
             with lock_file(lock_path):
-                with closing(file(src_path)) as f:
-                    ns.cfg = yaml.load(f, loader)
+                ns.cfg = load_master()
                 try: 
                     with closing(file(cache_path + '.tmp', 'w')) as f:
                         f.write(json.write(ns.cfg))
                 except: pass # silent failure
                 else: rename(cache_path + '.tmp', cache_path)
         except IOError:
-            with closing(file(src_path)) as f:
-                ns.cfg = yaml.load(f, loader)
+            ns.cfg = load_master()
     return ns.cfg
 
 dicts = load()