Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

File templates in blueprints #100

Merged
merged 25 commits into from

1 participant

@rcrowley
Owner

This patch allows filesystems to contain files like /etc/foobar.blueprint-template.mustache and optionally /etc/foobar.blueprint-template.sh that define a mustache(5)-like template and optionally some data to feed it that, when rendered, result in the desired content for the file /etc/foobar.

Standard data sources for templates (think Puppet's Facter or Chef's Ohai) come from /etc/blueprint-template.d/*.sh.

The templates are rendered on-demaned by blueprint-template(1). The necessary code to render the templates is included with generated shell code.

The template rendering engine is mustache.sh: https://github.com/rcrowley/mustache.sh.

Puppet and Chef do not support these templates.

The following manual pages need to be updated or written:

  • blueprint(5)
  • blueprint-template(1)

The standard template data in /etc/blueprint-template.d needs to be expanded and made part of the distribution. This may mean distribution via PyPI is no longer feasible.

rcrowley added some commits
@rcrowley rcrowley mustache.sh submodule for experimental templating. 6c750cd
@rcrowley rcrowley Track mustache.sh changes. a71a3d8
@rcrowley rcrowley Tracking mustache.sh. a43fd9b
@rcrowley rcrowley First pass at the blueprint-template(1) command. 566034f
@rcrowley rcrowley Updated submodule mustache.sh. 9000ab0
@rcrowley rcrowley Tracking mustache.sh. e97ae47
@rcrowley rcrowley Grab templates and data when available.
This gives the blueprint the opportunity to render a template.
5d80663
@rcrowley rcrowley Warnings about templates and Puppet/Chef. 0735a02
@rcrowley rcrowley Warnings about templates and CloudFormation. 66ff051
@rcrowley rcrowley Fully-qualified reference in error message. 1192df3
@rcrowley rcrowley Added a new mode to git.cat_file.
Now it can write directly to the local filesystem without a second subprocess.
406c052
@rcrowley rcrowley Refactored commands in shell backend.
Now there's a more regular API to add commands and lists of commands.  Adding tarballs has also been made much more memory efficient.
5f88b35
@rcrowley rcrowley Included mustache.sh when necessary. 04c30f0
@rcrowley rcrowley Use new add_list method. a20eb2f
@rcrowley rcrowley Distribute template data with blueprints.
This keeps the zero-dep application of a blueprint intact even when there are templates.
f899fde
@rcrowley rcrowley Including mustache.sh for blueprint-template(1).
The larger effect of this commit is the removal of m4 as a build dependency in favor of the bundled mustache.sh.
2ce0948
@rcrowley rcrowley Manual updates for file templates. c594cef
@rcrowley rcrowley Rebuilt manual. a96611b
@rcrowley rcrowley Rebuilt manual. ff0407e
@rcrowley rcrowley Install blueprint-template as an executable. d27e82c
@rcrowley rcrowley Allow reinstallation. 4d606d1
@rcrowley rcrowley Don't clobber mustache.sh's debug file descriptor. e6fbdc5
@rcrowley rcrowley Track mustache.sh submodule. 5588bbc
@rcrowley rcrowley Bring basic template data into /usr.
This drastically simplifies distribution and makes it possible to keep using PyPI.
2a52564
@rcrowley rcrowley Adding more basic system properties for templates. 86dbc75
@rcrowley rcrowley merged commit 86dbc75 into from
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Oct 25, 2011
  1. @rcrowley
  2. @rcrowley

    Track mustache.sh changes.

    rcrowley authored
  3. @rcrowley

    Tracking mustache.sh.

    rcrowley authored
  4. @rcrowley
  5. @rcrowley
  6. @rcrowley

    Tracking mustache.sh.

    rcrowley authored
  7. @rcrowley

    Grab templates and data when available.

    rcrowley authored
    This gives the blueprint the opportunity to render a template.
  8. @rcrowley
  9. @rcrowley
Commits on Oct 26, 2011
  1. @rcrowley
Commits on Nov 2, 2011
  1. @rcrowley

    Added a new mode to git.cat_file.

    rcrowley authored
    Now it can write directly to the local filesystem without a second subprocess.
  2. @rcrowley

    Refactored commands in shell backend.

    rcrowley authored
    Now there's a more regular API to add commands and lists of commands.  Adding tarballs has also been made much more memory efficient.
  3. @rcrowley
  4. @rcrowley

    Use new add_list method.

    rcrowley authored
  5. @rcrowley

    Distribute template data with blueprints.

    rcrowley authored
    This keeps the zero-dep application of a blueprint intact even when there are templates.
Commits on Nov 4, 2011
  1. @rcrowley

    Including mustache.sh for blueprint-template(1).

    rcrowley authored
    The larger effect of this commit is the removal of m4 as a build dependency in favor of the bundled mustache.sh.
  2. @rcrowley
  3. @rcrowley

    Rebuilt manual.

    rcrowley authored
Commits on Nov 8, 2011
  1. @rcrowley

    Rebuilt manual.

    rcrowley authored
  2. @rcrowley
  3. @rcrowley

    Allow reinstallation.

    rcrowley authored
  4. @rcrowley
  5. @rcrowley

    Track mustache.sh submodule.

    rcrowley authored
  6. @rcrowley

    Bring basic template data into /usr.

    rcrowley authored
    This drastically simplifies distribution and makes it possible to keep using PyPI.
  7. @rcrowley
This page is out of date. Refresh to see the latest.
Showing with 585 additions and 148 deletions.
  1. +2 −0  .gitignore
  2. +3 −0  .gitmodules
  3. +15 −3 Makefile
  4. +1 −1  bin/blueprint-show
  5. +37 −0 bin/blueprint-template.mustache
  6. +81 −48 blueprint/backend/files.py
  7. +27 −0 blueprint/frontend/blueprint-template.d/lsb.sh
  8. +40 −0 blueprint/frontend/blueprint-template.d/net.sh
  9. +8 −0 blueprint/frontend/blueprint-template.d/proc.sh
  10. +10 −0 blueprint/frontend/blueprint-template.d/uname.sh
  11. +11 −5 blueprint/frontend/cfn.py
  12. +5 −0 blueprint/frontend/chef.py
  13. +6 −1 blueprint/frontend/puppet.py
  14. +188 −67 blueprint/frontend/sh.py
  15. +17 −9 blueprint/git.py
  16. +1 −0  blueprint/ignore.py
  17. +1 −4 blueprint/interactive.py
  18. +1 −1  bootstrap.sh
  19. +2 −2 man/man1/blueprint-create.1
  20. +1 −1  man/man1/blueprint-create.1.ronn
  21. +4 −1 man/man1/blueprint-show.1
  22. +2 −0  man/man1/blueprint-show.1.ronn
  23. +38 −0 man/man1/blueprint-template.1
  24. +34 −0 man/man1/blueprint-template.1.ronn
  25. +23 −2 man/man5/blueprint.5
  26. +20 −1 man/man5/blueprint.5.ronn
  27. +4 −1 man/man5/blueprintignore.5
  28. +1 −0  man/man5/blueprintignore.5.ronn
  29. +1 −0  mustache.sh
  30. +1 −1  setup.py.m4 → setup.py.mustache
View
2  .gitignore
@@ -8,6 +8,8 @@
*.tar
*.tar.gz
.coverage
+bin/blueprint-template
+blueprint/frontend/mustache.sh
build
control
dist
View
3  .gitmodules
@@ -0,0 +1,3 @@
+[submodule "mustache.sh"]
+ path = mustache.sh
+ url = git@github.com:rcrowley/mustache.sh.git
View
18 Makefile
@@ -11,9 +11,17 @@ pydir=$(shell ${PYTHON} pydir.py ${libdir})
mandir=${prefix}/share/man
sysconfdir=${prefix}/etc
-all:
+all: bin/blueprint-template blueprint/frontend/mustache.sh
+
+bin/blueprint-template: bin/blueprint-template.mustache
+ pydir=$(pydir) mustache.sh/bin/mustache.sh <$< >$@
+ chmod 755 $@
+
+blueprint/frontend/mustache.sh: mustache.sh/lib/mustache.sh
+ install -m644 $< $@
clean:
+ rm -f bin/blueprint-template blueprint/frontend/mustache.sh
rm -rf \
*.deb \
setup.py build dist *.egg *.egg-info \
@@ -36,12 +44,14 @@ install-bin:
install-lib:
find blueprint -type d -printf %P\\0 | xargs -0r -I__ install -d $(DESTDIR)$(pydir)/blueprint/__
find blueprint -type f -name \*.py -printf %P\\0 | xargs -0r -I__ install -m644 blueprint/__ $(DESTDIR)$(pydir)/blueprint/__
+ install -m644 blueprint/frontend/mustache.sh $(DESTDIR)$(pydir)/blueprint/frontend/
+ find blueprint/frontend/blueprint-template.d -type f -name \*.sh -printf %P\\0 | xargs -0r -I__ install -m644 blueprint/frontend/blueprint-template.d/__ $(DESTDIR)$(pydir)/blueprint/frontend/blueprint-template.d/__
PYTHONPATH=$(DESTDIR)$(pydir) $(PYTHON) -mcompileall $(DESTDIR)$(pydir)/blueprint
install-man:
find man -type d -printf %P\\0 | xargs -0r -I__ install -d $(DESTDIR)$(mandir)/__
find man -type f -name \*.[12345678] -printf %P\\0 | xargs -0r -I__ install -m644 man/__ $(DESTDIR)$(mandir)/__
- find man -type f -name \*.[12345678] -printf %P\\0 | xargs -0r -I__ gzip $(DESTDIR)$(mandir)/__
+ find man -type f -name \*.[12345678] -printf %P\\0 | xargs -0r -I__ gzip -f $(DESTDIR)$(mandir)/__
install-sysconf:
find etc -type d -printf %P\\0 | xargs -0r -I__ install -d $(DESTDIR)$(sysconfdir)/__
@@ -55,6 +65,8 @@ uninstall-bin:
uninstall-lib:
find blueprint -type f -name \*.py -printf %P\\0 | xargs -0r -I__ rm -f $(DESTDIR)$(pydir)/blueprint/__ $(DESTDIR)$(pydir)/blueprint/__c
+ rm -f $(DESTDIR)$(pydir)/blueprint/frontend/mustache.sh
+ find blueprint/frontend/blueprint-template.d -type f -name \*.sh -printf %P\\0 | xargs -0r -I__ rm -f $(DESTDIR)$(pydir)/blueprint/frontend/blueprint-template.d/__
find blueprint -depth -mindepth 1 -type d -printf %P\\0 | xargs -0r -I__ rmdir $(DESTDIR)$(pydir)/blueprint/__ || true
rmdir -p --ignore-fail-on-non-empty $(DESTDIR)$(pydir)/blueprint || true
@@ -85,7 +97,7 @@ build-deb:
make uninstall prefix=/usr sysconfdir=/etc DESTDIR=debian
build-pypi:
- m4 -D__VERSION__=$(VERSION) setup.py.m4 >setup.py
+ VERSION=$(VERSION) mustache.sh/bin/mustache.sh <setup.py.mustache >setup.py
$(PYTHON) setup.py bdist_egg
deploy: deploy-deb deploy-pypi
View
2  bin/blueprint-show
@@ -66,7 +66,7 @@ try:
filename = getattr(b, options.generate)(options.relaxed).dumpf()
except OSError as e:
if errno.EEXIST == e.errno:
- logging.error('{0} already exists'.format(name))
+ logging.error('{0} already exists'.format(b.name))
sys.exit(1)
if not options.quiet:
print(filename)
View
37 bin/blueprint-template.mustache
@@ -0,0 +1,37 @@
+#!/bin/sh
+
+set -e
+
+exec 6>&2
+
+#/ Usage: blueprint template [-q] <pathname>
+usage() {
+ grep "^#/" "$0" | cut -c4- 1>&2
+ exit 1
+}
+while [ "$#" -gt 0 ]
+do
+ case "$1" in
+ -h|--help) usage;;
+ -q|--quiet) exec 6>/dev/null;;
+ *) break;;
+ esac
+done
+
+# BEGIN mustache.sh
+{{`cat mustache.sh/lib/mustache.sh`}}
+# END mustache.sh
+
+for PATHNAME in $(find "/etc/blueprint-template.d" "{{pydir}}/blueprint/frontend/blueprint-template.d" "-name" "*.sh" 2>/dev/null)
+do
+ echo "# [blueprint-template] sourcing $PATHNAME" >&6
+ . "$PATHNAME"
+done
+
+if [ -f "$1.blueprint-template.sh" ]
+then
+ echo "# [blueprint-template] sourcing $1.blueprint-template.sh" >&6
+ . "$1.blueprint-template.sh"
+fi
+
+mustache <"$1.blueprint-template.mustache" >"$1"
View
129 blueprint/backend/files.py
@@ -695,6 +695,19 @@ def files(b):
True):
continue
+ # Check for a Mustache template and an optional shell script
+ # that templatize this file.
+ try:
+ template = open(
+ '{0}.blueprint-template.mustache'.format(pathname)).read()
+ except IOError:
+ template = None
+ try:
+ data = open(
+ '{0}.blueprint-template.sh'.format(pathname)).read()
+ except IOError:
+ data = None
+
# The content is used even for symbolic links to determine whether
# it has changed from the packaged version.
try:
@@ -703,25 +716,23 @@ def files(b):
#logging.warning('{0} not readable'.format(pathname))
continue
- # Ignore files that are from the `base-files` package (which
- # doesn't include MD5 sums for every file for some reason).
- apt_packages = _dpkg_query_S(pathname)
- if 'base-files' in apt_packages:
+ # Ignore files that are unchanged from their packaged version.
+ if _unchanged(pathname, content):
continue
- # Ignore files that are unchanged from their packaged version,
- # or match in MD5SUMS.
- md5sums = MD5SUMS.get(pathname, [])
- md5sums.extend([_dpkg_md5sum(package, pathname)
- for package in apt_packages])
- md5sum = _rpm_md5sum(pathname)
- if md5sum is not None:
- md5sums.append(md5sum)
- if (hashlib.md5(content).hexdigest() in md5sums \
- or 64 in [len(md5sum or '') for md5sum in md5sums] \
- and hashlib.sha256(content).hexdigest() in md5sums) \
- and ignore.file(pathname, True):
- continue
+ # Resolve the rest of the file's metadata from the
+ # `/etc/passwd` and `/etc/group` databases.
+ try:
+ pw = pwd.getpwuid(s.st_uid)
+ owner = pw.pw_name
+ except KeyError:
+ owner = s.st_uid
+ try:
+ gr = grp.getgrgid(s.st_gid)
+ group = gr.gr_name
+ except KeyError:
+ group = s.st_gid
+ mode = '{0:o}'.format(s.st_mode)
# A symbolic link's content is the link target.
if stat.S_ISLNK(s.st_mode):
@@ -737,41 +748,36 @@ def files(b):
if content.startswith('/etc/alternatives/'):
continue
- encoding = 'plain'
+ b.add_file(pathname,
+ content=content,
+ encoding='plain',
+ group=group,
+ mode=mode,
+ owner=owner)
# A regular file is stored as plain text only if it is valid
# UTF-8, which is required for JSON serialization.
- elif stat.S_ISREG(s.st_mode):
+ else:
+ kwargs = dict(group=group,
+ mode=mode,
+ owner=owner)
try:
- content = content.decode('utf_8')
- encoding = 'plain'
+ if template:
+ if data:
+ kwargs['data'] = data.decode('utf_8')
+ kwargs['template'] = template.decode('utf_8')
+ else:
+ kwargs['content'] = content.decode('utf_8')
+ kwargs['encoding'] = 'plain'
except UnicodeDecodeError:
- content = base64.b64encode(content)
- encoding = 'base64'
-
- # Other types, like FIFOs and sockets are not supported within
- # a blueprint and really shouldn't appear in `/etc` at all.
- else:
- logging.warning('{0} is not a regular file or symbolic link'.
- format(pathname))
- continue
-
- try:
- pw = pwd.getpwuid(s.st_uid)
- owner = pw.pw_name
- except KeyError:
- owner = s.st_uid
- try:
- gr = grp.getgrgid(s.st_gid)
- group = gr.gr_name
- except KeyError:
- group = s.st_gid
- b.add_file(pathname,
- content=content,
- encoding=encoding,
- group=group,
- mode='{0:o}'.format(s.st_mode),
- owner=owner)
+ if template:
+ if data:
+ kwargs['data'] = base64.b64encode(data)
+ kwargs['template'] = base64.b64encode(template)
+ else:
+ kwargs['content'] = base64.b64encode(content)
+ kwargs['encoding'] = 'base64'
+ b.add_file(pathname, **kwargs)
# If this file is a service init script or config , create a
# service resource.
@@ -782,7 +788,7 @@ def files(b):
b.add_service_package(manager,
service,
'apt',
- *apt_packages)
+ *_dpkg_query_S(pathname))
b.add_service_package(manager,
service,
'yum',
@@ -937,3 +943,30 @@ def _rpm_md5sum(pathname):
pass
return _rpm_md5sum._cache.get(pathname, None)
+
+def _unchanged(pathname, content):
+ """
+ Return `True` if a file is unchanged from its packaged version.
+ """
+
+ # Ignore files that are from the `base-files` package (which
+ # doesn't include MD5 sums for every file for some reason).
+ apt_packages = _dpkg_query_S(pathname)
+ if 'base-files' in apt_packages:
+ return True
+
+ # Ignore files that are unchanged from their packaged version,
+ # or match in MD5SUMS.
+ md5sums = MD5SUMS.get(pathname, [])
+ md5sums.extend([_dpkg_md5sum(package, pathname)
+ for package in apt_packages])
+ md5sum = _rpm_md5sum(pathname)
+ if md5sum is not None:
+ md5sums.append(md5sum)
+ if (hashlib.md5(content).hexdigest() in md5sums \
+ or 64 in [len(md5sum or '') for md5sum in md5sums] \
+ and hashlib.sha256(content).hexdigest() in md5sums) \
+ and ignore.file(pathname, True):
+ return True
+
+ return False
View
27 blueprint/frontend/blueprint-template.d/lsb.sh
@@ -0,0 +1,27 @@
+# LSB properties for use in Blueprint file templates.
+
+# The distro name.
+DISTRO="$(lsb_release -si 2>/dev/null || {
+ if [ -f "/etc/debian_version" ]
+ then echo "Debian"
+ elif [ -f "/etc/fedora-release" ]
+ then echo "Fedora"
+ elif [ -f "/etc/redhat-release" ]
+ then
+ if grep -i "CentOS" "/etc/redhat-release"
+ then echo "CentOS"
+ elif grep -i "Scientific" "/etc/redhat-release"
+ then echo "Scientific"
+ else echo "RedHat"
+ fi
+ fi
+})"
+
+# The operating system release codename.
+RELEASE="$(lsb_release -sc 2>/dev/null || {
+ if [ -f "/etc/debian_version" ]
+ then cat "/etc/debian_version"
+ elif [ -f "/etc/redhat-release" ]
+ then egrep -o "release [0-9.]+" /etc/redhat-release | cut -d" " -f2
+ fi
+})"
View
40 blueprint/frontend/blueprint-template.d/net.sh
@@ -0,0 +1,40 @@
+# Network properties for use in Blueprint file templates.
+
+# Each IPv4 address assigned to an interface, one per line.
+EACH_IP="$(ifconfig | egrep -o "inet addr:[0-9.]+" | cut -d":" -f2)"
+
+# Each IPv6 address assigned to an interface, one per line.
+EACH_IPv6="$(ifconfig | egrep -o "inet6 addr: [0-9a-f:/]+" | cut -d" " -f3)"
+
+# The first IPv6 address assigned to any interface.
+IPv6="$(echo "$EACH_IPv6" | head -n1)"
+
+# The first private IPv4 address assigned to any interface.
+PRIVATE_IP="$(echo "$EACH_IP" | while read IP
+do
+ case "$IP" in
+ "10."*) echo "$IP";;
+ "127."*) ;;
+ "172.16."*|"172.32."*|"172.48."*|"172.64."*|"172.80."*|"172.96."*|"172.112."*|"172.128."*|"172.144."*|"172.160."*|"172.176."*|"172.192."*|"172.208."*|"172.224."*|"172.240."*) echo "$IP";;
+ "192.168."*) echo "$IP";;
+ *) ;;
+ esac
+done | head -n1)"
+
+# The first public IPv4 address assigned to any interface.
+PUBLIC_IP="$(echo "$EACH_IP" | while read IP
+do
+ case "$IP" in
+ "10."*) ;;
+ "127."*) ;;
+ "172.16."*|"172.32."*|"172.48."*|"172.64."*|"172.80."*|"172.96."*|"172.112."*|"172.128."*|"172.144."*|"172.160."*|"172.176."*|"172.192."*|"172.208."*|"172.224."*|"172.240."*) ;;
+ "192.168."*) ;;
+ *) echo "$IP";;
+ esac
+done | head -n1)"
+
+HOSTNAME="$(hostname)"
+
+DNSDOMAINNAME="$(dnsdomainname)"
+
+FQDN="$(hostname --fqdn)"
View
8 blueprint/frontend/blueprint-template.d/proc.sh
@@ -0,0 +1,8 @@
+# System properties from `/proc` for use in Blueprint file templates.
+
+# The number of cores in the system. The semantics of this value when read
+# in a virtualized environment are undefined.
+CORES="$(grep "^processor" "/proc/cpuinfo" | wc -l)"
+
+# The total amount of memory in the system in bytes.
+MEM="$(grep "^MemTotal" "/proc/meminfo" | awk '{print $2 * 1024}')"
View
10 blueprint/frontend/blueprint-template.d/uname.sh
@@ -0,0 +1,10 @@
+# uname(1) properties for use in Blueprint file templates.
+
+# The kernel name. This is always "Linux" on systems Blueprint supports.
+KERNEL="$(uname -s)"
+
+# The kernel release.
+KERNEL_RELEASE="$(uname -r)"
+
+# The system's hardware architecture. Probably "i386" or "x86_64".
+ARCH="$(uname -i)"
View
16 blueprint/frontend/cfn.py
@@ -14,13 +14,19 @@
def cfn(b, relaxed=False):
+ b2 = copy.deepcopy(b)
+ def file(pathname, f):
+ if 'template' in f:
+ logging.warning('file template {0} won\'t appear in generated '
+ 'CloudFormation templates'.format(pathname))
+ del b2.files[pathname]
if relaxed:
- b_relaxed = copy.deepcopy(b)
def package(manager, package, version):
- b_relaxed.packages[manager][package] = []
- b.walk(package=package)
- return Template(b_relaxed)
- return Template(b)
+ b2.packages[manager][package] = []
+ b.walk(file=file, package=package)
+ else:
+ b.walk(file=file)
+ return Template(b2)
class Template(dict):
View
5 blueprint/frontend/chef.py
@@ -5,6 +5,7 @@
import base64
import codecs
import errno
+import logging
import os
import os.path
import re
@@ -47,6 +48,10 @@ def file(pathname, f):
"""
Create a cookbook_file resource.
"""
+ if 'template' in f:
+ logging.warning('file template {0} won\'t appear in generated '
+ 'Chef cookbooks'.format(pathname))
+ return
c.directory(os.path.dirname(pathname),
group='root',
mode='0755',
View
7 blueprint/frontend/puppet.py
@@ -4,8 +4,9 @@
import base64
import codecs
-import errno
from collections import defaultdict
+import errno
+import logging
import os
import os.path
import re
@@ -58,6 +59,10 @@ def file(pathname, f):
"""
Create a file resource.
"""
+ if 'template' in f:
+ logging.warning('file template {0} won\'t appear in generated '
+ 'Puppet modules'.format(pathname))
+ return
# Create resources for parent directories and let the
# autorequire mechanism work out dependencies.
View
255 blueprint/frontend/sh.py
@@ -8,8 +8,10 @@
import os
import os.path
import re
+from shutil import copyfile
import tarfile
+from blueprint import git
from blueprint import util
@@ -34,73 +36,90 @@ def service_source(manager, service, dirname):
service_package=service_package,
service_source=service_source)
+ commit = git.rev_parse(b.name)
+ tree = git.tree(commit)
def source(dirname, filename, gen_content, url):
"""
Extract a source tarball.
"""
if dirname in lut['sources']:
- s.add('MD5SUM="$(find "{0}" -printf %T@\\\\n | md5sum)"', dirname)
+ s.add('MD5SUM="$(find "{0}" -printf %T@\\\\n | md5sum)"',
+ args=(dirname,))
if url is not None:
- s.add('curl -o "{0}" "{1}" || wget -O "{0}" "{1}"', filename, url)
+ s.add_list(('curl -o "{0}" "{1}"',),
+ ('wget -O "{0}" "{1}"',),
+ args=(filename, url),
+ operator='||')
if '.zip' == pathname[-4:]:
- s.add('unzip "{0}" -d "{1}"', filename, dirname)
+ s.add('unzip "{0}" -d "{1}"', args=(filename, dirname))
else:
- s.add('tar xf "{0}" -C "{1}"', filename, dirname)
+ s.add('tar xf "{0}" -C "{1}"', args=(filename, dirname))
elif secret is not None:
- s.add('curl -O "{0}/{1}/{2}/{3}" || wget "{0}/{1}/{2}/{3}"',
- server,
- secret,
- b.name,
- filename)
- s.add('tar xf "{0}" -C "{1}"', filename, dirname)
+ s.add_list(('curl -O "{0}/{1}/{2}/{3}"',)
+ ('wget "{0}/{1}/{2}/{3}"',),
+ args=(server, secret, b.name, filename),
+ operator='||')
+ s.add('tar xf "{0}" -C "{1}"', args=(filename, dirname))
elif gen_content is not None:
- s.add('tar xf "{0}" -C "{1}"',
- filename,
- dirname,
- sources={filename: gen_content()})
+ s.add('tar xf "{0}" -C "{1}"', args=(filename, dirname))
+ s.add_source(filename, git.blob(tree, filename))
for manager, service in lut['sources'][dirname]:
- s.add('[ "$MD5SUM" != "$(find "{0}" -printf %T@\\\\n ' # No ,
- '| md5sum)" ] && {1}=1',
- dirname,
- manager.env_var(service))
+ s.add_list(('[ "$MD5SUM" != "$(find "{0}" -printf %T@\\\\n '
+ '| md5sum)" ]',),
+ ('{1}=1',),
+ args=(dirname, manager.env_var(service)),
+ operator='&&')
def file(pathname, f):
"""
Place a file.
"""
if pathname in lut['files']:
- s.add('MD5SUM="$(md5sum "{0}" 2>/dev/null)"', pathname)
- s.add('mkdir -p "{0}"', os.path.dirname(pathname))
+ s.add('MD5SUM="$(md5sum "{0}" 2>/dev/null)"', args=(pathname,))
+ s.add('mkdir -p "{0}"', args=(os.path.dirname(pathname),))
if '120000' == f['mode'] or '120777' == f['mode']:
- s.add('ln -s "{0}" "{1}"', f['content'], pathname)
+ s.add('ln -s "{0}" "{1}"', args=(f['content'], pathname))
else:
if 'source' in f:
- s.add('curl -o "{0}" "{1}" || wget -O "{0}" "{1}"',
- pathname,
- f['source'])
+ s.add_list(('curl -o "{0}" "{1}"',),
+ ('wget -O "{0}" "{1}"',),
+ args=(pathname, f['source']),
+ operator='||')
else:
- eof = 'EOF'
- while re.search(r'{0}'.format(eof), f['content']):
- eof += 'EOF'
- s.add(
- '{0} >"{1}" <<{2}',
- 'base64 --decode' if 'base64' == f['encoding'] else 'cat',
- pathname,
- eof)
- s.add(raw=f['content'])
- if 0 < len(f['content']) and '\n' != f['content'][-1]:
- eof = '\n{0}'.format(eof)
- s.add(eof)
+ if 'template' in f:
+ s.templates = True
+ if 'base64' == f['encoding']:
+ commands = ('base64 --decode', 'mustache')
+ else:
+ commands = ('mustache',)
+ s.add_list(('set +x',),
+ ('. "lib/mustache.sh"',),
+ ('for F in */blueprint-template.d/*.sh',),
+ ('do',),
+ ('\t. "$F"',),
+ ('done',),
+ (f['data'].rstrip(),),
+ (command(*commands,
+ escape_stdin=True,
+ stdin=f['template'],
+ stdout=pathname),),
+ operator='\n',
+ wrapper='()')
+ else:
+ if 'base64' == f['encoding']:
+ commands = ('base64 --decode',)
+ else:
+ commands = ('cat',)
+ s.add(*commands, stdin=f['content'], stdout=pathname)
if 'root' != f['owner']:
- s.add('chown {0} "{1}"', f['owner'], pathname)
+ s.add('chown {0} "{1}"', args=(f['owner'], pathname))
if 'root' != f['group']:
- s.add('chgrp {0} "{1}"', f['group'], pathname)
+ s.add('chgrp {0} "{1}"', args=(f['group'], pathname))
if '100644' != f['mode']:
- s.add('chmod {0} "{1}"', f['mode'][-4:], pathname)
+ s.add('chmod {0} "{1}"', args=(f['mode'][-4:], pathname))
for manager, service in lut['files'][pathname]:
s.add('[ "$MD5SUM" != "$(md5sum "{0}")" ] && {1}=1',
- pathname,
- manager.env_var(service))
+ args=(pathname, manager.env_var(service)))
def before_packages(manager):
"""
@@ -124,11 +143,15 @@ def package(manager, package, version):
return
if manager in lut['packages'] and package in lut['packages'][manager]:
- env_vars = ['{0}=1'.format(m.env_var(service))
- for m, service in lut['packages'][manager][package]]
- s.add(manager.gate(package, version, relaxed) + ' || {{ ' \
- + manager.install(package, version, relaxed) + '; ' \
- + '; '.join(env_vars) + '; }}')
+ s.add_list((manager.gate(package, version, relaxed),),
+ (command_list((manager.install(package,
+ version,
+ relaxed),),
+ *[('{0}=1'.format(m.env_var(service)),)
+ for m, service in
+ lut['packages'][manager][package]],
+ wrapper='{{}}'),),
+ operator='||')
else:
s.add(manager(package, version, relaxed))
@@ -138,16 +161,20 @@ def package(manager, package, version):
# See comments on this section in `blueprint.frontend.puppet`.
match = re.match(r'^rubygems(\d+\.\d+(?:\.\d+)?)$', package)
if match is not None and util.rubygems_update():
- s.add('/usr/bin/gem{0} install --no-rdoc --no-ri ' # No ,
- 'rubygems-update', match.group(1))
- s.add('/usr/bin/ruby{0} $(PATH=$PATH:/var/lib/gems/{0}/bin ' # No ,
- 'which update_rubygems)', match.group(1))
+ s.add('/usr/bin/gem{0} install --no-rdoc --no-ri rubygems-update',
+ args=(match.group(1),))
+ s.add('/usr/bin/ruby{0} $(PATH=$PATH:/var/lib/gems/{0}/bin '
+ 'which update_rubygems)',
+ args=(match.group(1),))
if 'nodejs' == package:
- s.add('which npm || {{ ' # No ,
- 'curl http://npmjs.org/install.sh || ' # No ,
- 'wget -O- http://npmjs.org/install.sh ' # No ,
- '}} | sh')
+ s.add_list(('which npm',),
+ (command_list(('curl http://npmjs.org/install.sh',),
+ ('wget -O- http://npmjs.org/install.sh',),
+ operator='||',
+ wrapper='{{}}'),
+ 'sh'),
+ operator='||')
def service(manager, service):
s.add(manager(service))
@@ -161,6 +188,42 @@ def service(manager, service):
return s
+def command(*commands, **kwargs):
+ commands = list(commands)
+ if 'stdout' in kwargs:
+ commands[-1] += ' >"{0}"'.format(kwargs['stdout'])
+ if 'stdin' in kwargs:
+ stdin = (kwargs['stdin'].replace(u'\\', u'\\\\').
+ replace(u'$', u'\\$').
+ replace(u'`', u'\\`'))
+ if kwargs.get('escape_stdin', False):
+ stdin = stdin.replace(u'{', u'{{').replace(u'}', u'}}')
+ eof = 'EOF'
+ while eof in stdin:
+ eof += 'EOF'
+ commands[0] += ' <<{0}'.format(eof)
+ return ''.join([' | '.join(commands).format(*kwargs.get('args', ())),
+ '\n',
+ stdin,
+ '' if '\n' == stdin[-1] else '\n',
+ eof])
+ return ' | '.join(commands).format(*kwargs.get('args', ()))
+
+
+def command_list(*commands, **kwargs):
+ operator = {'&&': u' && ',
+ '||': u' || ',
+ '\n': u'\n',
+ ';': u'; '}[kwargs.get('operator', ';')]
+ wrapper = {'()': (u'(\n', u'\n)') if u'\n' == operator else (u'(', u')'),
+ '{}': (u'{ ', u'; }'),
+ '{{}}': (u'{{ ', u'; }}'), # Prevent double-escaping.
+ '': (u'', u'')}[kwargs.get('wrapper', '')]
+ return wrapper[0] \
+ + operator.join([command(*c, **kwargs) for c in commands]) \
+ + wrapper[-1]
+
+
class Script(object):
"""
A script is a list of shell commands. The pomp and circumstance is
@@ -177,18 +240,44 @@ def __init__(self, name, comment=None):
'set -x\n',
'cd "$(dirname "$0")"\n']
self.sources = {}
+ self.templates = False
def add(self, s='', *args, **kwargs):
- if 'raw' in kwargs:
- self.out.append(kwargs['raw'].
- replace(u'\\', u'\\\\').
- replace(u'$', u'\\$').
- replace(u'`', u'\\`'))
- else:
- self.out.append((unicode(s) + u'\n').format(*args))
+ self.out.append((unicode(s) + u'\n').format(*args))
for filename, content in kwargs.get('sources', {}).iteritems():
self.sources[filename] = content
+ def add(self, *args, **kwargs):
+ """
+ Add a command or pipeline to the `Script`. Each positional `str`
+ is an element in the pipeline. The keyword argument `args`, if
+ present, should contain an iterable of arguments to be substituted
+ into the final pipeline by the new-style string formatting library.
+ """
+ self.out.append(command(*args, **kwargs))
+
+ def add_list(self, *args, **kwargs):
+ """
+ Add a command or pipeline, or list of commands or pipelines, to
+ the `Script`. Each positional `str` or `tuple` argument is a
+ pipeline. The keyword argument `operator`, if present, must be
+ `';'`, `'&&'`, or `'||'` to control how the pipelines are joined.
+ The keyword argument `stdin`, if present, should contain a string
+ that will be given heredoc-style. The keyword argument `stdout`,
+ if present, should contain a string pathname that will receive
+ standard output. The keyword argument `args`, if present, should
+ contain an iterable of arguments to be substituted into the final
+ pipeline by the new-style string formatting library.
+ """
+ self.out.append(command_list(*args, **kwargs))
+
+ def add_source(self, filename, blob):
+ """
+ Add a reference to a source tarball to the `Script`. It will be
+ placed in the output directory/tarball later via `git-cat-file`(1).
+ """
+ self.sources[filename] = blob
+
def dumps(self):
"""
Generate a string containing shell code and all file contents.
@@ -199,7 +288,9 @@ def dumpf(self, gzip=False):
"""
Generate a file containing shell code and all file contents.
"""
- if 0 != len(self.sources):
+
+ # Open a file by the correct name, possibly with inline gzipping.
+ if 0 < len(self.sources) or self.templates:
os.mkdir(self.name)
filename = os.path.join(self.name, 'bootstrap.sh')
f = codecs.open(filename, 'w', encoding='utf-8')
@@ -209,17 +300,47 @@ def dumpf(self, gzip=False):
else:
filename = '{0}.sh'.format(self.name)
f = codecs.open(filename, 'w', encoding='utf-8')
+
+ # Bring along `mustache.sh`, the default template data files, and
+ # any user-provided template data files.
+ if self.templates:
+ os.mkdir(os.path.join(self.name, 'etc'))
+ os.mkdir(os.path.join(self.name, 'etc', 'blueprint-template.d'))
+ os.mkdir(os.path.join(self.name, 'lib'))
+ os.mkdir(os.path.join(self.name, 'lib', 'blueprint-template.d'))
+ copyfile(os.path.join(os.path.dirname(__file__), 'mustache.sh'),
+ os.path.join(self.name, 'lib', 'mustache.sh'))
+ for src, dest in [('/etc/blueprint-template.d', 'etc'),
+ (os.path.join(os.path.dirname(__file__),
+ 'blueprint-template.d'),
+ 'lib')]:
+ try:
+ for filename2 in os.listdir(src):
+ if filename2.endswith('.sh'):
+ copyfile(os.path.join(src, filename2),
+ os.path.join(self.name,
+ dest,
+ 'blueprint-template.d',
+ filename2))
+ except OSError:
+ pass
+
+ # Write the actual shell code.
for out in self.out:
f.write(out)
+ f.write('\n')
f.close()
- for filename2, content in sorted(self.sources.iteritems()):
- f2 = open(os.path.join(self.name, filename2), 'w')
- f2.write(content)
- f2.close()
- if gzip and 0 != len(self.sources):
+
+ # Bring source tarballs along.
+ for filename2, blob in sorted(self.sources.iteritems()):
+ git.cat_file(blob, os.path.join(self.name, filename2))
+
+ # Possibly gzip the result.
+ if gzip and (0 < len(self.sources) or self.templates):
filename = 'sh-{0}.tar.gz'.format(self.name)
tarball = tarfile.open(filename, 'w:gz')
tarball.add(self.name)
tarball.close()
return filename
+
return filename
View
26 blueprint/git.py
@@ -146,15 +146,23 @@ def content(blob):
return stdout
-def cat_file(blob):
- """
- Return an open file handle to a blob in Git's object store, via the
- git-cat-file(1) command.
- """
- return subprocess.Popen(git_args() + ['cat-file', 'blob', blob],
- close_fds=True,
- preexec_fn=unroot,
- stdout=subprocess.PIPE).stdout
+def cat_file(blob, pathname=None):
+ """
+ If `pathname` is `None`, return an open file handle to the blob in
+ Git's object store, otherwise stream the blob to `pathname`, all via
+ the git-cat-file(1) command.
+ """
+ args = git_args() + ['cat-file', 'blob', blob]
+ if pathname is None:
+ return subprocess.Popen(args,
+ close_fds=True,
+ preexec_fn=unroot,
+ stdout=subprocess.PIPE).stdout
+ else:
+ subprocess.Popen(args,
+ close_fds=True,
+ preexec_fn=unroot,
+ stdout=open(pathname, 'w')).communicate()
def write_tree():
View
1  blueprint/ignore.py
@@ -18,6 +18,7 @@
#
# XXX Update `blueprintignore`(5) if you make changes here.
IGNORE = {'*~': False,
+ '*.blueprint-template.*': False,
'*.dpkg-*': False,
'/etc/.git': False,
'/etc/.pwd.lock': False,
View
5 blueprint/interactive.py
@@ -66,10 +66,7 @@ def source(dirname, filename, gen_content, url):
print('{0} {1}'.format(dirname, url))
elif gen_content is not None:
blob = git.blob(tree, filename)
- p = subprocess.Popen(['cat'],
- close_fds=True,
- stdin=git.cat_file(blob),
- stdout=open(filename, 'w'))
+ git.cat_file(blob, filename)
print('{0} {1}'.format(dirname, filename))
b_chosen = choose()
if b_chosen is None:
View
2  bootstrap.sh
@@ -7,7 +7,6 @@ apt-get -q update
apt-get -q -y install \
build-essential \
git-core \
- m4 \
python python-setuptools \
ruby ruby-dev rubygems \
@@ -16,4 +15,5 @@ gem install fpm
mkdir "$HOME/work"
cd "$HOME/work"
git clone git://github.com/devstructure/blueprint.git
+(cd "blueprint" && git submodule update --init)
git clone git://github.com/rcrowley/ronn.git -b dots
View
4 man/man1/blueprint-create.1
@@ -1,7 +1,7 @@
.\" generated with Ronn/v0.7.3
.\" http://github.com/rtomayko/ronn/tree/0.7.3
.
-.TH "BLUEPRINT\-CREATE" "1" "October 2011" "DevStructure" "Blueprint"
+.TH "BLUEPRINT\-CREATE" "1" "November 2011" "DevStructure" "Blueprint"
.
.SH "NAME"
\fBblueprint\-create\fR \- create a blueprint
@@ -25,7 +25,7 @@ If one of \fB\-\-puppet\fR, \fB\-\-chef\fR, \fB\-\-sh\fR, or \fB\-\-cfn\fR is gi
Debian packages, Ruby gems, Python packages, and PHP PEAR/PECL packages are enumerated in the blueprint\. Debian packages that are listed as \fBessential\fR, \fBimportant\fR, \fBrequired\fR, or \fBstandard\fR and those that are depended upon by \fBubuntu\-minimal\fR, \fBubuntu\-standard\fR, or \fBubuntu\-desktop\fR are ignored\.
.
.P
-The contents of system configuration files in \fB/etc\fR that have been created or modified from their packaged versions will be included in the blueprint\.
+The contents of system configuration files in \fB/etc\fR that have been created or modified from their packaged versions will be included in the blueprint\. If file is found to have a corresponding template (a file with "\fB\.blueprint\-template\.mustache\fR" appended to its pathname) and optionally a corresponding data script (a file with "\fB\.blueprint\-template\.sh\fR" appended to its pathname), this \fBtemplate\fR and \fBdata\fR are included in the blueprint rather than the file\'s literal content\.
.
.P
Anything installed in \fB/usr/local\fR will be archived and included in the blueprint, tagged with the architecture (\fIamd64\fR or \fIi386\fR) of the server\.
View
2  man/man1/blueprint-create.1.ronn
@@ -17,7 +17,7 @@ If one of `--puppet`, `--chef`, `--sh`, or `--cfn` is given, a Puppet module, a
Debian packages, Ruby gems, Python packages, and PHP PEAR/PECL packages are enumerated in the blueprint. Debian packages that are listed as `essential`, `important`, `required`, or `standard` and those that are depended upon by `ubuntu-minimal`, `ubuntu-standard`, or `ubuntu-desktop` are ignored.
-The contents of system configuration files in `/etc` that have been created or modified from their packaged versions will be included in the blueprint.
+The contents of system configuration files in `/etc` that have been created or modified from their packaged versions will be included in the blueprint. If file is found to have a corresponding template (a file with "`.blueprint-template.mustache`" appended to its pathname) and optionally a corresponding data script (a file with "`.blueprint-template.sh`" appended to its pathname), this `template` and `data` are included in the blueprint rather than the file's literal content.
Anything installed in `/usr/local` will be archived and included in the blueprint, tagged with the architecture (_amd64_ or _i386_) of the server.
View
5 man/man1/blueprint-show.1
@@ -1,7 +1,7 @@
.\" generated with Ronn/v0.7.3
.\" http://github.com/rtomayko/ronn/tree/0.7.3
.
-.TH "BLUEPRINT\-SHOW" "1" "October 2011" "DevStructure" "Blueprint"
+.TH "BLUEPRINT\-SHOW" "1" "November 2011" "DevStructure" "Blueprint"
.
.SH "NAME"
\fBblueprint\-show\fR \- generate code from a blueprint
@@ -16,6 +16,9 @@
If none of \fB\-\-puppet\fR, \fB\-\-chef\fR, \fB\-\-sh\fR, or \fB\-\-cfn\fR are given, the raw JSON data structure that describes the blueprint is printed as described in \fBblueprint\fR(5)\.
.
.P
+The POSIX shell code generator is the only one prepared to handle files to be rendered from a template\. All other code generators will print a warning and ignore such files\.
+.
+.P
If \fIname\fR is omitted or \fB\-\fR, a blueprint is read from standard input and treated in the same manner\. See \fBblueprint\fR(5) for the details of the format\.
.
.SH "OPTIONS"
View
2  man/man1/blueprint-show.1.ronn
@@ -11,6 +11,8 @@ blueprint-show(1) -- generate code from a blueprint
If none of `--puppet`, `--chef`, `--sh`, or `--cfn` are given, the raw JSON data structure that describes the blueprint is printed as described in `blueprint`(5).
+The POSIX shell code generator is the only one prepared to handle files to be rendered from a template. All other code generators will print a warning and ignore such files.
+
If _name_ is omitted or `-`, a blueprint is read from standard input and treated in the same manner. See `blueprint`(5) for the details of the format.
## OPTIONS
View
38 man/man1/blueprint-template.1
@@ -0,0 +1,38 @@
+.\" generated with Ronn/v0.7.3
+.\" http://github.com/rtomayko/ronn/tree/0.7.3
+.
+.TH "BLUEPRINT\-TEMPLATE" "1" "November 2011" "DevStructure" "Blueprint"
+.
+.SH "NAME"
+\fBblueprint\-template\fR \-
+.
+.SH "SYNOPSIS"
+\fBblueprint template\fR [\fB\-q\fR] [\fIpathname\fR]
+.
+.SH "DESCRIPTION"
+FIXME
+.
+.SH "OPTIONS"
+.
+.TP
+\fB\-q\fR, \fB\-\-quiet\fR
+Operate quietly\.
+.
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+Show a help message\.
+.
+.SH "FILES"
+.
+.TP
+\fB/etc/blueprint\-template\.d\fR
+FIXME
+.
+.SH "THEME SONG"
+The Flaming Lips \- "The W\.A\.N\.D\. (The Will Always Negates Defeat)"
+.
+.SH "AUTHOR"
+Richard Crowley \fIrichard@devstructure\.com\fR
+.
+.SH "SEE ALSO"
+Part of \fBblueprint\fR(1)\.
View
34 man/man1/blueprint-template.1.ronn
@@ -0,0 +1,34 @@
+blueprint-template(1) --
+============================================================
+
+## SYNOPSIS
+
+`blueprint template` [`-q`] [_pathname_]
+
+## DESCRIPTION
+
+FIXME
+
+## OPTIONS
+
+* `-q`, `--quiet`:
+ Operate quietly.
+* `-h`, `--help`:
+ Show a help message.
+
+## FILES
+
+* `/etc/blueprint-template.d`:
+ FIXME
+
+## THEME SONG
+
+The Flaming Lips - "The W.A.N.D. (The Will Always Negates Defeat)"
+
+## AUTHOR
+
+Richard Crowley <richard@devstructure.com>
+
+## SEE ALSO
+
+Part of `blueprint`(1).
View
25 man/man5/blueprint.5
@@ -1,7 +1,7 @@
.\" generated with Ronn/v0.7.3
.\" http://github.com/rtomayko/ronn/tree/0.7.3
.
-.TH "BLUEPRINT" "5" "October 2011" "DevStructure" "Blueprint"
+.TH "BLUEPRINT" "5" "November 2011" "DevStructure" "Blueprint"
.
.SH "NAME"
\fBblueprint\fR \- blueprint JSON format
@@ -25,6 +25,21 @@
"group": "GROUP",
"mode": "MODE",
"owner": "OWNER",
+ "template": "TEMPLATE"
+ },
+ "PATHNAME": {
+ "dat": "DATA"
+ "encoding": "ENCODING",
+ "group": "GROUP",
+ "mode": "MODE",
+ "owner": "OWNER",
+ "template": "TEMPLATE"
+ },
+ "PATHNAME": {
+ "encoding": "ENCODING",
+ "group": "GROUP",
+ "mode": "MODE",
+ "owner": "OWNER",
"source": "SOURCE"
}
},
@@ -81,7 +96,10 @@ For compatibility with AWS \fBcfn\-init\fR, a URL may appear in the value in pla
Each key in the optional \fBfiles\fR object is the fully\-qualified pathname to a file\. These pathnames should be traversed in alphabetical order\. The associated value contains the \fBowner\fR, owning \fBgroup\fR, \fBmode\fR (a string containing the full 6\-digit octal representation), \fBcontent\fR, and the \fBencoding\fR of content (one of \fIplain\fR or \fIbase64\fR)\. Each file must be placed at its pathname and its metadata must be updated when the blueprint is applied\.
.
.P
-For compatibility with AWS \fBcfn\-init\fR, \fBsource\fR takes precedence over the \fBcontent\fR\. If a file with a \fBsource\fR is encountered, the \fBsource\fR URL should be fetched as the file\'s content\. Blueprint will never generate such objects\.
+The file\'s content may alternately be specified as \fBtemplate\fR and (optionally) \fBdata\fR which contain a \fBmustache\.sh\fR template in the \fBmustache\fR(5) format and POSIX shell code, respectively\. When a blueprint is applied, the template should be given as standard input to \fBmustache\.sh\fR with this data and other default data available in the environment\. The resulting standard output should be taken as the file\'s content\. A copy of \fBmustache\.sh\fR is distributed with Blueprint\.
+.
+.P
+For compatibility with AWS \fBcfn\-init\fR, \fBsource\fR takes precedence over \fBcontent\fR\. If a file with a \fBsource\fR is encountered, the \fBsource\fR URL should be fetched as the file\'s content\. Blueprint will never generate such objects\.
.
.SS "Packages"
Each key within \fBpackages\fR names a package manager\. Each manager contains keys that name packages to be installed by that manager\. Each package name is associated with an array of versions that must be installed\. In most cases, for most managers, this array will have only one element\.
@@ -109,3 +127,6 @@ Richard Crowley \fIrichard@devstructure\.com\fR
.
.SH "SEE ALSO"
\fBblueprint\fR(1)\.
+.
+.P
+\fBmustache\fR(5) at \fIhttp://mustache\.github\.com/mustache\.5\.html\fR and \fBmustache\.sh\fR at \fIhttps://github\.com/rcrowley/mustache\.sh\fR\.
View
21 man/man5/blueprint.5.ronn
@@ -18,6 +18,21 @@ blueprint(5) -- blueprint JSON format
"group": "GROUP",
"mode": "MODE",
"owner": "OWNER",
+ "template": "TEMPLATE"
+ },
+ "PATHNAME": {
+ "dat": "DATA"
+ "encoding": "ENCODING",
+ "group": "GROUP",
+ "mode": "MODE",
+ "owner": "OWNER",
+ "template": "TEMPLATE"
+ },
+ "PATHNAME": {
+ "encoding": "ENCODING",
+ "group": "GROUP",
+ "mode": "MODE",
+ "owner": "OWNER",
"source": "SOURCE"
}
},
@@ -71,7 +86,9 @@ For compatibility with AWS `cfn-init`, a URL may appear in the value in place of
Each key in the optional `files` object is the fully-qualified pathname to a file. These pathnames should be traversed in alphabetical order. The associated value contains the `owner`, owning `group`, `mode` (a string containing the full 6-digit octal representation), `content`, and the `encoding` of content (one of _plain_ or _base64_). Each file must be placed at its pathname and its metadata must be updated when the blueprint is applied.
-For compatibility with AWS `cfn-init`, `source` takes precedence over the `content`. If a file with a `source` is encountered, the `source` URL should be fetched as the file's content. Blueprint will never generate such objects.
+The file's content may alternately be specified as `template` and (optionally) `data` which contain a `mustache.sh` template in the `mustache`(5) format and POSIX shell code, respectively. When a blueprint is applied, the template should be given as standard input to `mustache.sh` with this data and other default data available in the environment. The resulting standard output should be taken as the file's content. A copy of `mustache.sh` is distributed with Blueprint.
+
+For compatibility with AWS `cfn-init`, `source` takes precedence over `content`. If a file with a `source` is encountered, the `source` URL should be fetched as the file's content. Blueprint will never generate such objects.
### Packages
@@ -100,3 +117,5 @@ Richard Crowley <richard@devstructure.com>
## SEE ALSO
`blueprint`(1).
+
+`mustache`(5) at <http://mustache.github.com/mustache.5.html> and `mustache.sh` at <https://github.com/rcrowley/mustache.sh>.
View
5 man/man5/blueprintignore.5
@@ -1,7 +1,7 @@
.\" generated with Ronn/v0.7.3
.\" http://github.com/rtomayko/ronn/tree/0.7.3
.
-.TH "BLUEPRINTIGNORE" "5" "October 2011" "DevStructure" "Blueprint"
+.TH "BLUEPRINTIGNORE" "5" "November 2011" "DevStructure" "Blueprint"
.
.SH "NAME"
\fBblueprintignore\fR \- ignore specific resources when creating blueprints
@@ -47,6 +47,9 @@ The following files and directories are part of the default ignore list\. They c
\fB*~\fR
.
.IP "\(bu" 4
+\fB*\.blueprint\-template\.*\fR
+.
+.IP "\(bu" 4
\fB*\.dpkg\-*\fR
.
.IP "\(bu" 4
View
1  man/man5/blueprintignore.5.ronn
@@ -29,6 +29,7 @@ All APT- or Yum-managed packages considered essential to the system are part of
The following files and directories are part of the default ignore list. They can be negated using `!` just like any other pattern.
* `*~`
+* `*.blueprint-template.*`
* `*.dpkg-*`
* `/etc/.git`
* `/etc/.pwd.lock`
1  mustache.sh
@@ -0,0 +1 @@
+Subproject commit 08b54c496af0448689f3daf5cbc767b2efff52fe
View
2  setup.py.m4 → setup.py.mustache
@@ -1,7 +1,7 @@
from setuptools import setup, find_packages
setup(name='blueprint',
- version='__VERSION__',
+ version='{{VERSION}}',
description='reverse engineer server configuration',
author='Richard Crowley',
author_email='richard@devstructure.com',
Something went wrong with that request. Please try again.