← Back to team overview

cloud-init-dev team mailing list archive

[Merge] ~harlowja/cloud-init:tpl-cloud-cfg into cloud-init:master

 

Joshua Harlow has proposed merging ~harlowja/cloud-init:tpl-cloud-cfg into cloud-init:master.

Requested reviews:
  cloud init development team (cloud-init-dev)

For more details, see:
https://code.launchpad.net/~harlowja/cloud-init/+git/cloud-init/+merge/307485
-- 
Your team cloud init development team is requested to review the proposed merge of ~harlowja/cloud-init:tpl-cloud-cfg into cloud-init:master.
diff --git a/cloudinit/helpers.py b/cloudinit/helpers.py
index fb95bab..12c8ef3 100644
--- a/cloudinit/helpers.py
+++ b/cloudinit/helpers.py
@@ -33,6 +33,7 @@ from cloudinit.settings import (PER_INSTANCE, PER_ALWAYS, PER_ONCE,
                                 CFG_ENV_NAME)
 
 from cloudinit import log as logging
+from cloudinit import templater
 from cloudinit import type_utils
 from cloudinit import util
 
@@ -200,18 +201,47 @@ class Runners(object):
                 return (True, results)
 
 
+class ConfigError(Exception):
+    pass
+
+
 class ConfigMerger(object):
     def __init__(self, paths=None, datasource=None,
                  additional_fns=None, base_cfg=None,
                  include_vendor=True):
         self._paths = paths
         self._ds = datasource
-        self._fns = additional_fns
+        self._fns = additional_fns or []
         self._base_cfg = base_cfg
         self._include_vendor = include_vendor
         # Created on first use
         self._cfg = None
 
+    @staticmethod
+    def _read_conf(fn, kind="config", tpl_params=None):
+        try:
+            c = util.load_file(fn, quiet=False)
+        except (IOError, OSError) as e:
+            raise ConfigError("Failed loading of %s from"
+                              " %s due to: %s" % (kind, fn, e))
+        else:
+            if fn.endswith(".tpl"):
+                if tpl_params is None:
+                    tpl_params = {}
+                tpl_params.setdefault('system', util.system_info())
+                try:
+                    c = templater.render_string(c, tpl_params)
+                except Exception as e:
+                    raise ConfigError(
+                        "Failed rendering %s from %s"
+                        " using params %s due to: %s" % (kind, fn,
+                                                         tpl_params, e))
+            try:
+                return util.load_yaml(c, default={})
+            except TypeError as e:
+                raise ConfigError("Failed parsing (into yaml) %s"
+                                  " from %s due to: %s" % (kind, fn, e))
+
     def _get_datasource_configs(self):
         d_cfgs = []
         if self._ds:
@@ -229,10 +259,9 @@ class ConfigMerger(object):
         if CFG_ENV_NAME in os.environ:
             e_fn = os.environ[CFG_ENV_NAME]
             try:
-                e_cfgs.append(util.read_conf(e_fn))
-            except Exception:
-                util.logexc(LOG, 'Failed loading of env. config from %s',
-                            e_fn)
+                e_cfgs.append(self._read_conf(e_fn, "environment config"))
+            except ConfigError as e:
+                util.logexc(LOG, str(e))
         return e_cfgs
 
     def _get_instance_configs(self):
@@ -241,19 +270,16 @@ class ConfigMerger(object):
         # a configuration file to use when running...
         if not self._paths:
             return i_cfgs
-
-        cc_paths = ['cloud_config']
+        cc_paths = [("cloud config", 'cloud_config')]
         if self._include_vendor:
-            cc_paths.append('vendor_cloud_config')
-
-        for cc_p in cc_paths:
+            cc_paths.append(("vendor cloud config", 'vendor_cloud_config'))
+        for cc_kind, cc_p in cc_paths:
             cc_fn = self._paths.get_ipath_cur(cc_p)
             if cc_fn and os.path.isfile(cc_fn):
                 try:
-                    i_cfgs.append(util.read_conf(cc_fn))
-                except Exception:
-                    util.logexc(LOG, 'Failed loading of cloud-config from %s',
-                                cc_fn)
+                    i_cfgs.append(self._read_conf(cc_fn, cc_kind))
+                except ConfigError as e:
+                    util.logexc(LOG, str(e))
         return i_cfgs
 
     def _read_cfg(self):
@@ -264,14 +290,11 @@ class ConfigMerger(object):
         # configs which override
         # base configuration
         cfgs = []
-        if self._fns:
-            for c_fn in self._fns:
-                try:
-                    cfgs.append(util.read_conf(c_fn))
-                except Exception:
-                    util.logexc(LOG, "Failed loading of configuration from %s",
-                                c_fn)
-
+        for c_fn in self._fns:
+            try:
+                cfgs.append(self._read_conf(c_fn, "config"))
+            except ConfigError as e:
+                util.logexc(LOG, str(e))
         cfgs.extend(self._get_env_configs())
         cfgs.extend(self._get_instance_configs())
         cfgs.extend(self._get_datasource_configs())
diff --git a/cloudinit/settings.py b/cloudinit/settings.py
index 8c258ea..3796262 100644
--- a/cloudinit/settings.py
+++ b/cloudinit/settings.py
@@ -23,8 +23,12 @@
 # Set and read for determining the cloud config file location
 CFG_ENV_NAME = "CLOUD_CFG"
 
-# This is expected to be a yaml formatted file
-CLOUD_CONFIG = '/etc/cloud/cloud.cfg'
+# This is expected to be a yaml (or yaml formatted template) file.
+CLOUD_CONFIG = '/etc/cloud/cloud.cfg.tpl'
+
+# This is expected to be a directory of yaml formatted file(s)
+# or templates that can/will turn into yaml formatted file(s)
+CLOUD_CONFIG_D = '/etc/cloud/cloud.cfg.d/'
 
 # What u get if no config is provided
 CFG_BUILTIN = {
diff --git a/cloudinit/util.py b/cloudinit/util.py
index eb3e589..421a68b 100644
--- a/cloudinit/util.py
+++ b/cloudinit/util.py
@@ -579,13 +579,40 @@ def get_cfg_option_int(yobj, key, default=0):
 
 
 def system_info():
-    return {
+    info = {
         'platform': platform.platform(),
         'release': platform.release(),
         'python': platform.python_version(),
         'uname': platform.uname(),
         'dist': platform.linux_distribution(),
     }
+    plat = info['platform'].lower()
+    # Try to get more info about what it actually is, in a format
+    # that we can easily use across linux and variants...
+    if plat.startswith('darwin'):
+        info['variant'] = 'darwin'
+    elif plat.endswith("bsd"):
+        info['variant'] = 'bsd'
+    elif plat.startswith('win'):
+        info['variant'] = 'windows'
+    elif 'linux' in plat:
+        # Try to get a single string out of these...
+        linux_dist, _version, _id = info['dist']
+        linux_dist = linux_dist.lower()
+        if linux_dist in ('ubuntu', 'linuxmint', 'mint'):
+            info['variant'] = 'ubuntu'
+        else:
+            for prefix, variant in [('redhat', 'rhel'),
+                                    ('centos', 'rhel'),
+                                    ('fedora', 'fedora'),
+                                    ('debian', 'debian')]:
+                if linux_dist.startswith(prefix):
+                    info['variant'] = variant
+        if 'variant' not in info:
+            info['variant'] = 'linux'
+    if 'variant' not in info:
+        info['variant'] = 'unknown'
+    return info
 
 
 def get_cfg_option_list(yobj, key, default=None):
diff --git a/config/cloud.cfg b/config/cloud.cfg
deleted file mode 100644
index d608dc8..0000000
--- a/config/cloud.cfg
+++ /dev/null
@@ -1,116 +0,0 @@
-# The top level settings are used as module
-# and system configuration.
-
-# A set of users which may be applied and/or used by various modules
-# when a 'default' entry is found it will reference the 'default_user'
-# from the distro configuration specified below
-users:
-   - default
-
-# If this is set, 'root' will not be able to ssh in and they 
-# will get a message to login instead as the above $user (ubuntu)
-disable_root: true
-
-# This will cause the set+update hostname module to not operate (if true)
-preserve_hostname: false
-
-# Example datasource config
-# datasource: 
-#    Ec2: 
-#      metadata_urls: [ 'blah.com' ]
-#      timeout: 5 # (defaults to 50 seconds)
-#      max_wait: 10 # (defaults to 120 seconds)
-
-# The modules that run in the 'init' stage
-cloud_init_modules:
- - migrator
- - ubuntu-init-switch
- - seed_random
- - bootcmd
- - write-files
- - growpart
- - resizefs
- - disk_setup
- - mounts
- - set_hostname
- - update_hostname
- - update_etc_hosts
- - ca-certs
- - rsyslog
- - users-groups
- - ssh
-
-# The modules that run in the 'config' stage
-cloud_config_modules:
-# Emit the cloud config ready event
-# this can be used by upstart jobs for 'start on cloud-config'.
- - emit_upstart
- - ssh-import-id
- - locale
- - set-passwords
- - grub-dpkg
- - apt-pipelining
- - apt-configure
- - ntp
- - timezone
- - disable-ec2-metadata
- - runcmd
- - byobu
-
-# The modules that run in the 'final' stage
-cloud_final_modules:
- - snappy
- - package-update-upgrade-install
- - fan
- - landscape
- - lxd
- - puppet
- - chef
- - salt-minion
- - mcollective
- - rightscale_userdata
- - scripts-vendor
- - scripts-per-once
- - scripts-per-boot
- - scripts-per-instance
- - scripts-user
- - ssh-authkey-fingerprints
- - keys-to-console
- - phone-home
- - final-message
- - power-state-change
-
-# System and/or distro specific settings
-# (not accessible to handlers/transforms)
-system_info:
-   # This will affect which distro class gets used
-   distro: ubuntu
-   # Default user name + that default users groups (if added/used)
-   default_user:
-     name: ubuntu
-     lock_passwd: True
-     gecos: Ubuntu
-     groups: [adm, audio, cdrom, dialout, dip, floppy, lxd, netdev, plugdev, sudo, video]
-     sudo: ["ALL=(ALL) NOPASSWD:ALL"]
-     shell: /bin/bash
-   # Other config here will be given to the distro class and/or path classes
-   paths:
-      cloud_dir: /var/lib/cloud/
-      templates_dir: /etc/cloud/templates/
-      upstart_dir: /etc/init/
-   package_mirrors:
-     - arches: [i386, amd64]
-       failsafe:
-         primary: http://archive.ubuntu.com/ubuntu
-         security: http://security.ubuntu.com/ubuntu
-       search:
-         primary:
-           - http://%(ec2_region)s.ec2.archive.ubuntu.com/ubuntu/
-           - http://%(availability_zone)s.clouds.archive.ubuntu.com/ubuntu/
-           - http://%(region)s.clouds.archive.ubuntu.com/ubuntu/
-         security: []
-     - arches: [armhf, armel, default]
-       failsafe:
-         primary: http://ports.ubuntu.com/ubuntu-ports
-         security: http://ports.ubuntu.com/ubuntu-ports
-   ssh_svcname: ssh
diff --git a/config/cloud.cfg-freebsd b/config/cloud.cfg-freebsd
deleted file mode 100644
index be664f5..0000000
--- a/config/cloud.cfg-freebsd
+++ /dev/null
@@ -1,88 +0,0 @@
-# The top level settings are used as module
-# and system configuration.
-
-syslog_fix_perms: root:wheel
-
-# This should not be required, but leave it in place until the real cause of
-# not beeing able to find -any- datasources is resolved.
-datasource_list: ['ConfigDrive', 'OpenStack', 'Ec2']
-
-# A set of users which may be applied and/or used by various modules
-# when a 'default' entry is found it will reference the 'default_user'
-# from the distro configuration specified below
-users:
-   - default
-
-# If this is set, 'root' will not be able to ssh in and they 
-# will get a message to login instead as the above $user (ubuntu)
-disable_root: false
-
-# This will cause the set+update hostname module to not operate (if true)
-preserve_hostname: false
-
-# Example datasource config
-# datasource: 
-#    Ec2: 
-#      metadata_urls: [ 'blah.com' ]
-#      timeout: 5 # (defaults to 50 seconds)
-#      max_wait: 10 # (defaults to 120 seconds)
-
-# The modules that run in the 'init' stage
-cloud_init_modules:
-# - migrator
- - seed_random
- - bootcmd
-# - write-files
- - growpart
- - resizefs
- - set_hostname
- - update_hostname
-# - update_etc_hosts
-# - ca-certs
-# - rsyslog
- - users-groups
- - ssh
-
-# The modules that run in the 'config' stage
-cloud_config_modules:
-# - disk_setup
-# - mounts
- - ssh-import-id
- - locale
- - set-passwords
- - package-update-upgrade-install
-# - landscape
- - timezone
-# - puppet
-# - chef
-# - salt-minion
-# - mcollective
- - disable-ec2-metadata
- - runcmd
-# - byobu
-
-# The modules that run in the 'final' stage
-cloud_final_modules:
- - rightscale_userdata
- - scripts-vendor
- - scripts-per-once
- - scripts-per-boot
- - scripts-per-instance
- - scripts-user
- - ssh-authkey-fingerprints
- - keys-to-console
- - phone-home
- - final-message
- - power-state-change
-
-# System and/or distro specific settings
-# (not accessible to handlers/transforms)
-system_info:
-   distro: freebsd
-   default_user:
-     name: freebsd
-     lock_passwd: True
-     gecos: FreeBSD
-     groups: [wheel]
-     sudo: ["ALL=(ALL) NOPASSWD:ALL"]
-     shell: /bin/tcsh
diff --git a/config/cloud.cfg.tpl b/config/cloud.cfg.tpl
new file mode 100644
index 0000000..224c564
--- /dev/null
+++ b/config/cloud.cfg.tpl
@@ -0,0 +1,198 @@
+## template:jinja
+
+# The top level settings are used as module
+# and system configuration.
+
+{% if platform.variant in ["bsd"] %}
+syslog_fix_perms: root:wheel
+{% endif %}
+
+# A set of users which may be applied and/or used by various modules
+# when a 'default' entry is found it will reference the 'default_user'
+# from the distro configuration specified below
+users:
+   - default
+
+# If this is set, 'root' will not be able to ssh in and they 
+# will get a message to login instead as the default $user
+{% if platform.variant in ["bsd"] %}
+disable_root: false
+{% else %}
+disable_root: true
+{% endif %}
+
+# This will cause the set+update hostname module to not operate (if true)
+preserve_hostname: false
+
+# Example datasource config
+# datasource: 
+#    Ec2: 
+#      metadata_urls: [ 'blah.com' ]
+#      timeout: 5 # (defaults to 50 seconds)
+#      max_wait: 10 # (defaults to 120 seconds)
+
+{% if platform.variant in ["bsd"] %}
+# This should not be required, but leave it in place until the real cause of
+# not beeing able to find -any- datasources is resolved.
+datasource_list: ['ConfigDrive', 'OpenStack', 'Ec2']
+{% endif %}
+
+# The modules that run in the 'init' stage
+cloud_init_modules:
+ - migrator
+{% if platform.variant in ["ubuntu", "unknown", "debian"] %}
+ - ubuntu-init-switch
+{% endif %}
+ - seed_random
+ - bootcmd
+# Setup disks and filesystems ... before we do much else.
+ - growpart
+ - resizefs
+ - disk_setup
+ - mounts
+# (end block of disk/fs modules)
+{% if platform.variant not in ["bsd"] %}
+ - write-files
+{% endif %}
+ - set_hostname
+ - update_hostname
+{% if platform.variant not in ["bsd"] %}
+ - update_etc_hosts
+ - ca-certs
+ - rsyslog
+{% endif %}
+ - users-groups
+ - ssh
+
+# The modules that run in the 'config' stage
+cloud_config_modules:
+{% if platform.variant in ["ubuntu", "unknown", "debian"] %}
+# Emit the cloud config ready event
+# this can be used by upstart jobs for 'start on cloud-config'.
+ - emit_upstart
+{% endif %}
+ - ssh-import-id
+ - locale
+ - set-passwords
+{% if platform.variant in ["rhel", "fedora"] %}
+ - spacewalk
+ - yum-add-repo
+{% endif %}
+{% if platform.variant in ["ubuntu", "unknown", "debian"] %}
+ - grub-dpkg
+ - apt-pipelining
+ - apt-configure
+{% endif %}
+ - ntp
+ - timezone
+ - disable-ec2-metadata
+ - runcmd
+{% if platform.variant in ["ubuntu", "unknown", "debian"] %}
+ - byobu
+{% endif %}
+
+# The modules that run in the 'final' stage
+cloud_final_modules:
+{% if platform.variant in ["ubuntu", "unknown", "debian"] %}
+ - snappy
+{% endif %}
+ - package-update-upgrade-install
+{% if platform.variant in ["ubuntu", "unknown", "debian"] %}
+ - fan
+ - landscape
+ - lxd
+{% endif %}
+{% if platform.variant not in ["bsd"] %}
+ # These are all grouped together (but typically only one of them
+ # actually is doing anything, since most people don't run many of these
+ # package/configuration management systems at the same time).
+ - puppet
+ - chef
+ - salt-minion
+ - mcollective
+ # (end block of configuration management 'like' modules)
+{% endif %}
+ - rightscale_userdata
+ - scripts-vendor
+ - scripts-per-once
+ - scripts-per-boot
+ - scripts-per-instance
+ - scripts-user
+ - ssh-authkey-fingerprints
+ - keys-to-console
+ - phone-home
+ - final-message
+ - power-state-change
+
+# System and/or distro specific settings
+# (not accessible to handlers/transforms)
+system_info:
+   # This will affect which distro class gets used...
+{% if platform.variant in ["ubuntu"] %}
+   distro: ubuntu
+{% elif platform.variant in ["fedora"] %}
+   distro: fedora
+{% elif platform.variant in ["debian"] %}
+   distro: debian
+{% elif platform.variant in ["rhel"] %}
+   distro: rhel
+{% elif platform.variant in ["bsd"] %}
+   distro: freebsd
+{% else %}
+   # Unknown/fallback distro.
+   distro: ubuntu
+{% endif %}
+{% if platform.variant in ["ubuntu", "unknown", "debian"] %}
+   # Default user name + that default users groups (if added/used)
+   default_user:
+     name: ubuntu
+     lock_passwd: True
+     gecos: Ubuntu
+     groups: [adm, audio, cdrom, dialout, dip, floppy, lxd, netdev, plugdev, sudo, video]
+     sudo: ["ALL=(ALL) NOPASSWD:ALL"]
+     shell: /bin/bash
+   # Other config here will be given to the distro class and/or path classes
+   paths:
+      cloud_dir: /var/lib/cloud/
+      templates_dir: /etc/cloud/templates/
+      upstart_dir: /etc/init/
+   package_mirrors:
+     - arches: [i386, amd64]
+       failsafe:
+         primary: http://archive.ubuntu.com/ubuntu
+         security: http://security.ubuntu.com/ubuntu
+       search:
+         primary:
+           - http://%(ec2_region)s.ec2.archive.ubuntu.com/ubuntu/
+           - http://%(availability_zone)s.clouds.archive.ubuntu.com/ubuntu/
+           - http://%(region)s.clouds.archive.ubuntu.com/ubuntu/
+         security: []
+     - arches: [armhf, armel, default]
+       failsafe:
+         primary: http://ports.ubuntu.com/ubuntu-ports
+         security: http://ports.ubuntu.com/ubuntu-ports
+   ssh_svcname: ssh
+{% elif platform.variant in ["rhel", "fedora"] %}
+   # Default user name + that default users groups (if added/used)
+   default_user:
+     name: fedora
+     lock_passwd: True
+     gecos: Fedora Cloud User
+     groups: [wheel, adm, systemd-journal]
+     sudo: ["ALL=(ALL) NOPASSWD:ALL"]
+     shell: /bin/bash
+   # Other config here will be given to the distro class and/or path classes
+   paths:
+      cloud_dir: /var/lib/cloud/
+      templates_dir: /etc/cloud/templates/
+   ssh_svcname: sshd
+{% elif platform.variant in ["bsd"] %}
+    # Default user name + that default users groups (if added/used)
+    default_user:
+      name: freebsd
+      lock_passwd: True
+      gecos: FreeBSD
+      groups: [wheel]
+      sudo: ["ALL=(ALL) NOPASSWD:ALL"]
+      shell: /bin/tcsh
+{% endif %}
diff --git a/packages/debian/rules.in b/packages/debian/rules.in
index 9b00435..1d4f829 100755
--- a/packages/debian/rules.in
+++ b/packages/debian/rules.in
@@ -11,6 +11,7 @@ override_dh_install:
 	dh_install
 	install -d debian/cloud-init/etc/rsyslog.d
 	cp tools/21-cloudinit.conf debian/cloud-init/etc/rsyslog.d/21-cloudinit.conf
+	mv debian/cloud-init/etc/cloud.cfg-ubuntu debian/cloud-init/etc/cloud.cfg
 
 override_dh_auto_test:
 ifeq (,$(findstring nocheck,$(DEB_BUILD_OPTIONS)))
diff --git a/packages/redhat/cloud-init.spec.in b/packages/redhat/cloud-init.spec.in
index d0ae048..9b8fd33 100644
--- a/packages/redhat/cloud-init.spec.in
+++ b/packages/redhat/cloud-init.spec.in
@@ -95,11 +95,23 @@ rm -rf \$RPM_BUILD_ROOT%{python_sitelib}/tests
 mkdir -p \$RPM_BUILD_ROOT/%{_sharedstatedir}/cloud
 mkdir -p \$RPM_BUILD_ROOT/%{_libexecdir}/%{name}
 
+# Remove these for now (not sure if they work)...
+rm \$RPM_BUILD_ROOT/%{_sysconfdir}/NetworkManager/dispatcher.d/hook-network-manager
+rm \$RPM_BUILD_ROOT/%{_sysconfdir}/dhcp/dhclient-exit-hooks.d/hook-dhclient
+
 #if $systemd
 mkdir -p         \$RPM_BUILD_ROOT/%{_unitdir}
 cp -p systemd/*  \$RPM_BUILD_ROOT/%{_unitdir}
 #end if
 
+# The fedora (and/or rhel) file is what should be working on rhel based
+# systems.
+mv \$RPM_BUILD_ROOT/%{_sysconfdir}/cloud/cloud.cfg-fedora \$RPM_BUILD_ROOT/%{_sysconfdir}/cloud/cloud.cfg
+
+# Remove the other configs (we don't need them anymore)
+rm %{buildroot}/%{_sysconfdir}/cloud/cloud.cfg-ubuntu
+rm %{buildroot}/%{_sysconfdir}/cloud/cloud.cfg-freebsd
+
 %clean
 rm -rf \$RPM_BUILD_ROOT
 
diff --git a/packages/suse/cloud-init.spec.in b/packages/suse/cloud-init.spec.in
index f994a0c..5906828 100644
--- a/packages/suse/cloud-init.spec.in
+++ b/packages/suse/cloud-init.spec.in
@@ -77,6 +77,14 @@ ssh keys and to let the user run various scripts.
             --record-rpm=INSTALLED_FILES --install-lib=%{python_sitelib} \
             --init-system=%{initsys}
 
+# Unsure what file should work here, so we will copy over the prior
+# default which is the ubuntu one and use that.
+mv \$RPM_BUILD_ROOT/%{_sysconfdir}/cloud/cloud.cfg-ubuntu \$RPM_BUILD_ROOT/%{_sysconfdir}/cloud/cloud.cfg
+
+# Remove the other configs (we don't need them anymore)
+rm %{buildroot}/%{_sysconfdir}/cloud/cloud.cfg-fedora
+rm %{buildroot}/%{_sysconfdir}/cloud/cloud.cfg-freebsd
+
 # Remove non-SUSE templates
 rm %{buildroot}/%{_sysconfdir}/cloud/templates/*.debian.*
 rm %{buildroot}/%{_sysconfdir}/cloud/templates/*.redhat.*
diff --git a/setup.py b/setup.py
index 8ff667d..eab9a16 100755
--- a/setup.py
+++ b/setup.py
@@ -98,12 +98,15 @@ USR = "/usr"
 ETC = "/etc"
 USR_LIB_EXEC = "/usr/lib"
 LIB = "/lib"
+CLOUD_CFG = "config/cloud.cfg-ubuntu"
 if os.uname()[0] == 'FreeBSD':
     USR = "/usr/local"
     USR_LIB_EXEC = "/usr/local/lib"
     ETC = "/usr/local/etc"
+    CLOUD_CFG = "config/cloud.cfg-freebsd"
 elif os.path.isfile('/etc/redhat-release'):
     USR_LIB_EXEC = "/usr/libexec"
+    CLOUD_CFG = "config/cloud.cfg-fedora"
 
 
 # Avoid having datafiles installed in a virtualenv...
@@ -175,7 +178,7 @@ if in_virtualenv():
     cmdclass = {}
 else:
     data_files = [
-        (ETC + '/cloud', glob('config/*.cfg')),
+        (ETC + '/cloud', [CLOUD_CFG]),
         (ETC + '/cloud/cloud.cfg.d', glob('config/cloud.cfg.d/*')),
         (ETC + '/cloud/templates', glob('templates/*')),
         (ETC + '/NetworkManager/dispatcher.d/', ['tools/hook-network-manager']),
diff --git a/tools/render_ud.py b/tools/render_ud.py
new file mode 100644
index 0000000..624403f
--- /dev/null
+++ b/tools/render_ud.py
@@ -0,0 +1,20 @@
+import sys
+
+from cloudinit import templater
+from cloudinit import util
+
+
+def main():
+    fn = sys.argv[1]
+    tpl_params = {
+        'platform': util.system_info(),
+    }
+    with open(fn, 'rb') as fh:
+        contents = fh.read()
+    contents = (templater.render_string(contents, tpl_params))
+    print(contents)
+    util.load_yaml(contents)
+
+
+if __name__ == '__main__':
+    main()

Follow ups