Debianized, add compose and generic containers with upstart.
diff --git a/README.md b/README.md
deleted file mode 100644
index 9225621..0000000
--- a/README.md
+++ /dev/null
@@ -1,23 +0,0 @@
-
-# Docker
-
-Docker is a platform for developers and sysadmins to develop, ship, and run applications. Docker lets you quickly assemble applications from components and eliminates the friction that can come when shipping code. Docker lets you get your code tested and deployed into production as fast as possible.
-
-Docker is supported on the following systems:
-
-* Debian 8.0 Jessie (64-bit)
-* Ubuntu Trusty 14.04 (LTS) (64-bit)
-* Ubuntu Precise 12.04 (LTS) (64-bit)
-* Ubuntu Raring 13.04 and Saucy 13.10 (64 bit)
-
-## Sample pillar
-
-    docker:
-      host:
-        enabled: true
-
-## Read more
-
-* https://docs.docker.com/installation/ubuntulinux/
-* https://github.com/saltstack-formulas/docker-formula
- 
\ No newline at end of file
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..002694c
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,61 @@
+
+======
+Docker
+======
+
+Docker is a platform for developers and sysadmins to develop, ship, and run applications. Docker lets you quickly assemble applications from components and eliminates the friction that can come when shipping code. Docker lets you get your code tested and deployed into production as fast as possible.
+
+Docker is supported on the following systems:
+
+* Debian 8.0 Jessie (64-bit)
+* Ubuntu Trusty 14.04 (LTS) (64-bit)
+* Ubuntu Precise 12.04 (LTS) (64-bit)
+* Ubuntu Raring 13.04 and Saucy 13.10 (64 bit)
+
+Sample pillar
+-------------
+
+.. code-block:: yaml
+
+    docker:
+      host:
+        enabled: true
+
+Containers
+----------
+
+    docker:
+      host:
+        container:
+          registry:
+            image: "registry:2"
+            runoptions:
+              - -e "REGISTRY_STORAGE=inmemory"
+              - -e "GUNICORN_OPTS=[\"--preload\"]"
+              - "--log-driver=syslog"
+              - "-p 5000:5000"
+              - "--rm"
+
+Compose
+-------
+
+.. code-block:: yaml
+
+    docker:
+      compose:
+        container:
+          postgres:
+            restart: always
+            image: postgres:latest
+            volumes_from:
+              - memcached
+            ports:
+              - "5432:5432"
+
+
+Read more
+---------
+
+* https://docs.docker.com/installation/ubuntulinux/
+* https://github.com/saltstack-formulas/docker-formula
+ 
\ No newline at end of file
diff --git a/debian/changelog b/debian/changelog
new file mode 100644
index 0000000..969863f
--- /dev/null
+++ b/debian/changelog
@@ -0,0 +1,5 @@
+salt-formula-docker (0.1) trusty; urgency=medium
+
+  * Initial release
+
+ -- Michael Kuty <michael.kuty@tcpcloud.eu>  Thu, 13 Aug 2015 23:23:41 +0200
diff --git a/debian/compat b/debian/compat
new file mode 100644
index 0000000..ec63514
--- /dev/null
+++ b/debian/compat
@@ -0,0 +1 @@
+9
diff --git a/debian/control b/debian/control
new file mode 100644
index 0000000..bae0d24
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,15 @@
+Source: salt-formula-docker
+Maintainer: Michael Kuty <michael.kuty@tcpcloud.eu>
+Section: admin
+Priority: optional
+Build-Depends: debhelper (>= 9)
+Standards-Version: 3.9.6
+Homepage: http://www.tcpcloud.eu
+Vcs-Browser: https://github.com/tcpcloud/salt-formula-docker
+Vcs-Git: https://github.com/tcpcloud/salt-formula-docker.git
+
+Package: salt-formula-docker
+Architecture: all
+Depends: ${misc:Depends}, salt-master, reclass
+Description: docker salt formula
+ Install and configure docker system.
diff --git a/debian/copyright b/debian/copyright
new file mode 100644
index 0000000..e495fa5
--- /dev/null
+++ b/debian/copyright
@@ -0,0 +1,15 @@
+Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: salt-formula-docker
+Upstream-Contact: 
+Source: https://github.com/tcpcloud/salt-formula-docker
+
+Files: *
+Copyright: 2014-2015 tcp cloud a.s.
+License: Apache-2.0
+  Copyright (C) 2014-2015 tcp cloud a.s.
+  .
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  .
+  On a Debian system you can find a copy of this license in
+  /usr/share/common-licenses/Apache-2.0.
diff --git a/debian/docs b/debian/docs
new file mode 100644
index 0000000..d585829
--- /dev/null
+++ b/debian/docs
@@ -0,0 +1,3 @@
+README.rst
+CHANGELOG.rst
+VERSION
diff --git a/debian/install b/debian/install
new file mode 100644
index 0000000..a242d4b
--- /dev/null
+++ b/debian/install
@@ -0,0 +1,6 @@
+docker/*             /usr/share/salt-formulas/env/docker/
+metadata/service/*      /usr/share/salt-formulas/reclass/service/docker/
+# Not all salt formulas has custom states and modules,
+# uncomment to install them
+# _modules/*         /usr/share/salt-formulas/env/_modules/
+# _states/*         /usr/share/salt-formulas/env/_states/
diff --git a/debian/rules b/debian/rules
new file mode 100755
index 0000000..abde6ef
--- /dev/null
+++ b/debian/rules
@@ -0,0 +1,5 @@
+#!/usr/bin/make -f
+
+%:
+	dh $@
+
diff --git a/debian/source/format b/debian/source/format
new file mode 100644
index 0000000..89ae9db
--- /dev/null
+++ b/debian/source/format
@@ -0,0 +1 @@
+3.0 (native)
diff --git a/doc/source/conf.py b/doc/source/conf.py
new file mode 100644
index 0000000..5e0e635
--- /dev/null
+++ b/doc/source/conf.py
@@ -0,0 +1,73 @@
+# -*- coding: utf-8 -*-
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import sys
+
+sys.path.insert(0, os.path.abspath('../..'))
+# -- General configuration ----------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = [
+    'sphinx.ext.autodoc',
+]
+
+# autodoc generation is a bit aggressive and a nuisance when doing heavy
+# text edit cycles.
+# execute "export SPHINX_DEBUG=1" in your terminal to disable
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'salt-formula-docker'
+copyright = u'2016, tcp cloud a.s.'
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+add_module_names = True
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# -- Options for HTML output --------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  Major themes that come with
+# Sphinx are currently 'default' and 'sphinxdoc'.
+# html_theme_path = ["."]
+# html_theme = '_theme'
+# html_static_path = ['static']
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = '%sdoc' % project
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass
+# [howto/manual]).
+latex_documents = [
+    ('index',
+     '%s.tex' % project,
+     u'%s Documentation' % project,
+     u'OpenStack Foundation', 'manual'),
+]
+
+# Example configuration for intersphinx: refer to the Python standard library.
+# intersphinx_mapping = {'http://docs.python.org/': None}
diff --git a/doc/source/index.rst b/doc/source/index.rst
new file mode 100644
index 0000000..a6210d3
--- /dev/null
+++ b/doc/source/index.rst
@@ -0,0 +1 @@
+.. include:: ../../README.rst
diff --git a/docker/CHANGELOG.rst b/docker/CHANGELOG.rst
new file mode 100644
index 0000000..0e1515c
--- /dev/null
+++ b/docker/CHANGELOG.rst
@@ -0,0 +1,6 @@
+docker formula
+=========================================
+
+0.1 (2015-08-22)
+
+- Initial formula setup
diff --git a/docker/LICENSE b/docker/LICENSE
new file mode 100644
index 0000000..6f2b42f
--- /dev/null
+++ b/docker/LICENSE
@@ -0,0 +1,13 @@
+Copyright (c) 2014-2015 tcp cloud a.s.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
\ No newline at end of file
diff --git a/docker/VERSION b/docker/VERSION
new file mode 100644
index 0000000..49d5957
--- /dev/null
+++ b/docker/VERSION
@@ -0,0 +1 @@
+0.1
diff --git a/docker/compose-ng.sls b/docker/compose-ng.sls
new file mode 100644
index 0000000..d68bea9
--- /dev/null
+++ b/docker/compose-ng.sls
@@ -0,0 +1,83 @@
+{%- from "docker/map.jinja" import compose with context %}
+{%- for name, container in compose.container.items() %}
+  {%- set id = name %}
+  {%- set required_containers = [] %}
+
+{{id}}_image:
+  docker.pulled:
+  {%- set image = container.image.split(':',1) %}
+    - name: {{image[0]}}
+    - tag: {{image[1]}}
+
+
+{{id}} container:
+  {%- if 'dvc' in container and container.dvc %}
+  docker.installed:
+  {%- else %}
+  docker.running:
+  {%- endif %}
+    - name: {{id}}
+    - image: {{container.image}}
+  {%- if 'command' in container %}
+    - command: {{container.command}}
+  {%- endif %}
+  {%- if 'environment' in container and container.environment is iterable %}
+    - environment:
+    {%- for variable, value in container.environment.iteritems() %}
+        - {{variable}}: {{value}}
+    {%- endfor %}
+  {%- endif %}
+  {%- if 'ports' in container and container.ports is iterable %}
+    - ports:
+    {%- for port_mapping in container.ports %}
+      {%- if port_mapping is string %}
+        {%- set mapping = port_mapping.split(':',2) %}
+        {%- if mapping|length < 2 %}
+      - "{{mapping[0]}}"
+        {%- else %}
+      - "{{mapping[-1]}}/tcp":
+            HostPort: "{{mapping[-2]}}"
+            HostIp: "{{mapping[-3]|d('')}}"
+        {%- endif %}
+      {%- elif port_mapping is mapping %}
+      - {{port_mapping}}
+      {%- endif %}
+    {%- endfor %}
+  {%- endif %}
+  {%- if 'volumes' in container %}
+    - volumes:
+    {%- for volume in container.volumes %}
+      - {{volume}}
+    {%- endfor %}
+  {%- endif %}
+  {%- if 'volumes_from' in container %}
+    - volumes_from:
+    {%- for volume in container.volumes_from %}
+      {%- do required_containers.append(volume) %}
+      - {{volume}}
+    {%- endfor %}
+  {%- endif %}
+  {%- if 'links' in container %}
+    - links:
+    {%- for link in container.links %}
+      {%- set name, alias = link.split(':',1) %}
+      {%- do required_containers.append(name) %}
+        {{name}}: {{alias}}
+    {%- endfor %}
+  {%- endif %}
+  {%- if 'restart' in container %}
+    - restart_policy:
+    {%- set policy = container.restart.split(':',1) %}
+        Name: {{policy[0]}}
+    {%- if policy|length > 1 %}
+        MaximumRetryCount: {{policy[1]}}
+    {%- endif %}
+  {%- endif %}
+    - require:
+      - docker: {{id}}_image
+  {%- if required_containers is defined %}
+    {%- for containerid in required_containers %}
+      - docker: {{containerid}}
+    {%- endfor %}
+  {%- endif %}
+{% endfor %}
diff --git a/docker/compose.sls b/docker/compose.sls
new file mode 100644
index 0000000..a35e25a
--- /dev/null
+++ b/docker/compose.sls
@@ -0,0 +1,17 @@
+{% from "docker/map.jinja" import compose with context %}
+
+include:
+- .compose-ng
+
+compose-pip:
+  pkg.installed:
+    - name: python-pip
+  pip.installed:
+    - name: pip
+    - upgrade: True
+
+compose:
+  pip.installed:
+    - name: docker-compose{# == {{ compose.version }}#}
+    - require:
+      - pip: compose-pip
diff --git a/docker/containers.sls b/docker/containers.sls
new file mode 100644
index 0000000..6bcac40
--- /dev/null
+++ b/docker/containers.sls
@@ -0,0 +1,41 @@
+{%- from "docker/map.jinja" import host with context %}
+
+{%- if host.enabled %}
+
+{% for name, container in host.container.items() %}
+docker-image-{{ name }}:
+  cmd.run:
+    - name: docker pull {{ container.image }}
+    - require:
+      - service: docker_service
+
+{# TODO: SysV init script #}
+{%- set init_system = salt["cmd.run"]("ps -p1 | grep -q systemd && echo systemd || echo upstart") %}
+
+docker-container-startup-config-{{ name }}:
+  file.managed:
+{%- if init_system == "systemd" %}
+    - name: /etc/systemd/system/docker-{{ name }}.service
+    - source: salt://docker/files/systemd.conf
+{%- elif init_system == "upstart" %}
+    - name: /etc/init/docker-{{ name }}.conf
+    - source: salt://docker/files/upstart.conf
+{%- endif %}
+    - mode: 700
+    - user: root
+    - template: jinja
+    - defaults:
+        name: {{ name | json }}
+        container: {{ container | json }}
+    - require:
+      - cmd: docker-image-{{ name }}
+
+docker-container-service-{{ name }}:
+  service.running:
+    - name: docker-{{ name }}
+    - enable: True
+    - watch:
+      - file: docker-container-startup-config-{{ name }}
+{% endfor %}
+
+{%- endif %}
diff --git a/docker/files/config b/docker/files/config
new file mode 100644
index 0000000..5bceab7
--- /dev/null
+++ b/docker/files/config
@@ -0,0 +1,19 @@
+{% from "docker/map.jinja" import server with context %}
+
+# Docker Upstart and SysVinit configuration file
+
+# Customize location of Docker binary (especially for development testing).
+#DOCKER="/usr/local/bin/docker"
+
+# Use DOCKER_OPTS to modify the daemon startup options.
+#DOCKER_OPTS="--dns 8.8.8.8 --dns 8.8.4.4"
+
+# If you need Docker to use an HTTP proxy, it can also be specified here.
+#export http_proxy="http://127.0.0.1:3128/"
+
+# This is also a handy place to tweak where Docker's temporary files go.
+#export TMPDIR="/mnt/bigdrive/docker-tmp"
+
+{% for line in server.get("config", []) %}
+{{ line }}
+{% endfor %}
diff --git a/docker/files/systemd.conf b/docker/files/systemd.conf
new file mode 100644
index 0000000..cf1e2de
--- /dev/null
+++ b/docker/files/systemd.conf
@@ -0,0 +1,27 @@
+[Unit]
+Description=Docker container for {{ name }}
+Requires=docker.service
+After=docker.service
+
+{#- Ugly, but covers the cases where variable evaluates to false, is empty, or non-existent #}
+{%- set runoptions = container.get("runoptions") or [] %}
+{%- set stopoptions = container.get("stopoptions") or [] %}
+{%- set cmd = container.get("cmd") or "" %}
+
+{%- if runoptions == "None" %}
+{%- set runoptions = [] %}
+{%- endif %}
+
+{%- if cmd == "None" %}
+{%- set cmd = "" %}
+{%- endif %}
+
+
+[Service]
+Restart=always
+ExecStart=/usr/bin/docker run -d {% for option in runoptions %}{{ option }} {% endfor %} --name={{ name }} {{ container.image }} {{ cmd }}
+ExecStop=/usr/bin/docker stop {{ name }}
+ExecStopPost=/usr/bin/docker rm -f {{ name }}
+
+[Install]
+WantedBy=local.target
diff --git a/docker/files/upstart.conf b/docker/files/upstart.conf
new file mode 100644
index 0000000..6825d43
--- /dev/null
+++ b/docker/files/upstart.conf
@@ -0,0 +1,30 @@
+description "Docker container for {{ name }}"
+start on filesystem and started docker
+stop on runlevel [06]
+respawn
+
+{#- Ugly, but covers the cases where variable evaluates to false, is empty, or non-existent #}
+{%- set runoptions = container.get("runoptions", []) %}
+{%- set stopoptions = container.get("stopoptions", []) %}
+
+{%- if runoptions == "None" %}
+{%- set runoptions = [] %}
+{%- endif %}
+
+script
+  exec docker run \
+  {%- for option in runoptions %}
+    {{ option }} \
+  {%- endfor %}
+    --name={{ name }} \
+    {{ container.image }} {{ container.get("cmd", "") }}
+end script
+
+pre-stop script
+  /usr/bin/docker stop \
+  {%- for option in stopoptions %}
+    {{ option }} \
+  {%- endfor %}
+    {{ name }}
+  /usr/bin/docker rm {{ name }}
+end script
diff --git a/docker/host.sls b/docker/host.sls
index 679927a..2deaf38 100644
--- a/docker/host.sls
+++ b/docker/host.sls
@@ -1,6 +1,9 @@
 {% from "docker/map.jinja" import host with context %}
 {%- if host.enabled %}
 
+include:
+- .containers
+
 docker_packages:
   pkg.latest:
   - pkgs: {{ host.pkgs }}
@@ -28,4 +31,26 @@
   - require:
     - pkg: docker_packages
 
+{% if host.install_docker_py %}
+docker-py-requirements:
+  pkg.installed:
+    - name: python-pip
+  pip.installed:
+    - name: pip
+    - upgrade: True
+
+docker-py:
+  pip.installed:
+    {%- if "pip_version" in host %}
+    - name: docker-py {{ host.pip_version }}
+    {%- else %}
+    - name: docker-py
+    {%- endif %}
+    - require:
+      - pkg: docker_packages
+      - pip: docker-py-requirements
+    - reload_modules: True
+{% endif %}
+
+
 {%- endif %}
\ No newline at end of file
diff --git a/docker/init.sls b/docker/init.sls
index 0d3039d..410a782 100644
--- a/docker/init.sls
+++ b/docker/init.sls
@@ -6,4 +6,7 @@
 {%- if pillar.docker.control is defined %}
 - docker.control
 {%- endif %}
+{%- if pillar.docker.compose is defined %}
+- docker.compose
+{%- endif %}
 {%- endif %}
\ No newline at end of file
diff --git a/docker/map.jinja b/docker/map.jinja
index 5dfc89e..7e4e7a5 100644
--- a/docker/map.jinja
+++ b/docker/map.jinja
@@ -16,4 +16,23 @@
         'pkgs': ['docker-engine'],
         'service': 'docker'
     },
-}, grain='os', merge=salt['pillar.get']('docker:host')) %}
\ No newline at end of file
+}, grain='os', merge=salt['pillar.get']('docker:host')) %}
+
+{% set compose = salt['grains.filter_by']({
+    'CentOS': {
+        'pkgs': ['docker-io'],
+        'service': 'docker'
+    },
+    'Debian': {
+        'pkgs': ['docker.io'],
+        'service': 'docker'
+    },
+    'RedHat': {
+        'pkgs': ['iptables', 'lxc-docker'],
+        'service': 'docker'
+    },
+    'Ubuntu': {
+        'pkgs': ['docker-engine'],
+        'service': 'docker'
+    },
+}, grain='os', merge=salt['pillar.get']('docker:compose')) %}
\ No newline at end of file
diff --git a/docker/meta/sphinx.yml b/docker/meta/sphinx.yml
deleted file mode 100644
index 4243d01..0000000
--- a/docker/meta/sphinx.yml
+++ /dev/null
@@ -1,9 +0,0 @@
-doc:
-  name: Docker
-  description: Docker is a platform for developers and sysadmins to develop, ship, and run applications.
-  role:
-  {%- if pillar.docker.host is defined %}
-    host:
-      name: host
-      param: {}
-  {%- endif %}
diff --git a/metadata/service/compose/container/leonardo.yml b/metadata/service/compose/container/leonardo.yml
new file mode 100644
index 0000000..3e284d1
--- /dev/null
+++ b/metadata/service/compose/container/leonardo.yml
@@ -0,0 +1,14 @@
+classes:
+- service.docker.compose.container.postgresql
+- service.docker.compose.container.memcached
+parameters:
+  docker:
+    compose:
+      container:
+        postgres:
+          restart: always
+          image: leonardocms/demo:latest
+          volumes_from:
+            - memcached
+          ports:
+            - "5432:5432"
diff --git a/metadata/service/compose/container/memcached.yml b/metadata/service/compose/container/memcached.yml
new file mode 100644
index 0000000..229bc9c
--- /dev/null
+++ b/metadata/service/compose/container/memcached.yml
@@ -0,0 +1,8 @@
+parameters:
+  docker:
+    compose:
+      container:
+        memcached:
+          image: memcached:latest
+          ports:
+            - "11211:11211"
\ No newline at end of file
diff --git a/metadata/service/compose/container/postgresql.yml b/metadata/service/compose/container/postgresql.yml
new file mode 100644
index 0000000..3cca4fd
--- /dev/null
+++ b/metadata/service/compose/container/postgresql.yml
@@ -0,0 +1,11 @@
+parameters:
+  docker:
+    compose:
+      container:
+        postgres:
+          restart: always
+          image: postgres:latest
+          volumes_from:
+            - memcached
+          ports:
+            - "5432:5432"
diff --git a/metadata/service/compose/container/registry.yml b/metadata/service/compose/container/registry.yml
new file mode 100644
index 0000000..75eec8a
--- /dev/null
+++ b/metadata/service/compose/container/registry.yml
@@ -0,0 +1,19 @@
+parameters:
+  docker:
+    compose:
+      container:
+        registry:
+          restart: always
+          image: registry:2
+          ports:
+            - 5000:5000
+          environment:
+            REGISTRY_HTTP_TLS_CERTIFICATE: /certs/domain.crt
+            REGISTRY_HTTP_TLS_KEY: /certs/domain.key
+            REGISTRY_AUTH: htpasswd
+            REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
+            REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm
+          volumes:
+            - /path/data:/var/lib/registry
+            - /etc/certs:/certs
+            - /path/auth:/auth
\ No newline at end of file
diff --git a/metadata/service/compose/init.yml b/metadata/service/compose/init.yml
new file mode 100644
index 0000000..8d96859
--- /dev/null
+++ b/metadata/service/compose/init.yml
@@ -0,0 +1,6 @@
+applications:
+- docker
+parameters:
+  docker:
+    compose:
+      enabled: true
\ No newline at end of file
diff --git a/metadata/service/host/container/registry.yml b/metadata/service/host/container/registry.yml
new file mode 100644
index 0000000..c081ed8
--- /dev/null
+++ b/metadata/service/host/container/registry.yml
@@ -0,0 +1,12 @@
+parameters:
+  docker:
+    host:
+      container:
+        registry:
+          image: "registry:2"
+          runoptions:
+            - -e "REGISTRY_STORAGE=inmemory"
+            - -e "GUNICORN_OPTS=[\"--preload\"]"
+            - "--log-driver=syslog"
+            - "-p 5000:5000"
+            - "--rm"
\ No newline at end of file
diff --git a/tests/pillar/compute_cluster.sls b/tests/pillar/compute_cluster.sls
new file mode 100644
index 0000000..6f03b6e
--- /dev/null
+++ b/tests/pillar/compute_cluster.sls
@@ -0,0 +1,49 @@
+nova:
+  compute:
+    version: liberty
+    enabled: true
+    virtualization: kvm
+    heal_instance_info_cache_interval: 60
+    vncproxy_url: openstack:6080
+    bind:
+      vnc_address: 127.0.0.1
+      vnc_port: 6080
+      vnc_name: 0.0.0.0
+    database:
+      engine: mysql
+      host: 127.0.0.1
+      port: 3306
+      name: nova
+      user: nova
+      password: password
+    identity:
+      engine: keystone
+      host: 127.0.0.1
+      port: 35357
+      user: nova
+      password: password
+      tenant: service
+    message_queue:
+      engine: rabbitmq
+      host: 127.0.0.1
+      port: 5672
+      user: openstack
+      password: password
+      virtual_host: '/openstack'
+    image:
+      engine: glance
+      host: 127.0.0.1
+      port: 9292
+    network:
+      engine: neutron
+      host: 127.0.0.1
+      port: 9696
+    cache:
+      engine: memcached
+      members:
+      - host: 127.0.0.1
+        port: 11211
+      - host: 127.0.1.1
+        port: 11211
+      - host: 127.0.2.1
+        port: 11211
diff --git a/tests/pillar/compute_single.sls b/tests/pillar/compute_single.sls
new file mode 100644
index 0000000..036a140
--- /dev/null
+++ b/tests/pillar/compute_single.sls
@@ -0,0 +1,45 @@
+nova:
+  compute:
+    version: liberty
+    enabled: true
+    virtualization: kvm
+    heal_instance_info_cache_interval: 60
+    vncproxy_url: openstack:6080
+    bind:
+      vnc_address: 127.0.0.1
+      vnc_port: 6080
+      vnc_name: 0.0.0.0
+    database:
+      engine: mysql
+      host: 127.0.0.1
+      port: 3306
+      name: nova
+      user: nova
+      password: password
+    identity:
+      engine: keystone
+      host: 127.0.0.1
+      port: 35357
+      user: nova
+      password: password
+      tenant: service
+    message_queue:
+      engine: rabbitmq
+      host: 127.0.0.1
+      port: 5672
+      user: openstack
+      password: password
+      virtual_host: '/openstack'
+    image:
+      engine: glance
+      host: 127.0.0.1
+      port: 9292
+    network:
+      engine: neutron
+      host: 127.0.0.1
+      port: 9696
+    cache:
+      engine: memcached
+      members:
+      - host: 127.0.0.1
+        port: 11211
diff --git a/tests/pillar/control_cluster.sls b/tests/pillar/control_cluster.sls
new file mode 100644
index 0000000..158f565
--- /dev/null
+++ b/tests/pillar/control_cluster.sls
@@ -0,0 +1,48 @@
+nova:
+  controller:
+    enabled: true
+    networking: default
+    version: liberty
+    vncproxy_url: 127.0.0.1
+    security_group: false
+    dhcp_domain: novalocal
+    scheduler_default_filters: "DifferentHostFilter,RetryFilter,AvailabilityZoneFilter,RamFilter,CoreFilter,DiskFilter,ComputeFilter,ComputeCapabilitiesFilter,ImagePropertiesFilter,ServerGroupAntiAffinityFilter,ServerGroupAffinityFilter"
+    cpu_allocation_ratio: 16.0
+    ram_allocation_ratio: 1.5
+    bind:
+      private_address: 127.0.0.1
+      public_address: 127.0.0.1
+      public_name: 127.0.0.1
+      novncproxy_port: 6080
+    database:
+      engine: mysql
+      host: 127.0.0.1
+      port: 3306
+      name: nova
+      user: nova
+      password: password
+    identity:
+      engine: keystone
+      host: 127.0.0.1
+      port: 35357
+      user: nova
+      password: password
+      tenant: service
+    message_queue:
+      engine: rabbitmq
+      host: 127.0.0.1
+      port: 5672
+      user: openstack
+      password: password
+      virtual_host: '/openstack'
+      ha_queues: true
+    glance:
+      host: 
+      port: 9292
+    network:
+      engine: neutron
+      host: 127.0.0.1
+      port: 9696
+      mtu: 1500
+    metadata:
+      password: metadata
diff --git a/tests/pillar/control_single.sls b/tests/pillar/control_single.sls
new file mode 100644
index 0000000..68c77b2
--- /dev/null
+++ b/tests/pillar/control_single.sls
@@ -0,0 +1,52 @@
+nova:
+  controller:
+    enabled: true
+    networking: contrail
+    version: liberty
+    security_group: false
+    vncproxy_url: 127.0.0.1
+    dhcp_domain: novalocal
+    scheduler_default_filters: "DifferentHostFilter,RetryFilter,AvailabilityZoneFilter,RamFilter,CoreFilter,DiskFilter,ComputeFilter,ComputeCapabilitiesFilter,ImagePropertiesFilter,ServerGroupAntiAffinityFilter,ServerGroupAffinityFilter"
+    cpu_allocation_ratio: 16.0
+    ram_allocation_ratio: 1.5
+    bind:
+      private_address: 127.0.0.1
+      public_address: 127.0.0.1
+      public_name: 127.0.0.1
+      novncproxy_port: 6080
+    database:
+      engine: mysql
+      host: localhost
+      port: 3306
+      name: nova
+      user: nova
+      password: password
+    identity:
+      engine: keystone
+      host: 127.0.0.1
+      port: 35357
+      user: nova
+      password: password
+      tenant: service
+    message_queue:
+      engine: rabbitmq
+      host: 127.0.0.1
+      port: 5672
+      user: openstack
+      password: password
+      virtual_host: '/openstack'
+    glance:
+      host: 127.0.0.1
+      port: 9292
+    network:
+      engine: neutron
+      host: 127.0.0.1
+      port: 9696
+      mtu: 1500
+    metadata:
+      password: password
+    cache:
+      engine: memcached
+      members:
+      - host: 127.0.0.1
+        port: 11211
diff --git a/tests/pillar/single.yml b/tests/pillar/single.yml
new file mode 100644
index 0000000..44ebafc
--- /dev/null
+++ b/tests/pillar/single.yml
@@ -0,0 +1,5 @@
+docker:
+  server:
+    enabled: true
+    bind:
+      address: 0.0.0.0
diff --git a/tests/run_tests.sh b/tests/run_tests.sh
new file mode 100755
index 0000000..1f1316f
--- /dev/null
+++ b/tests/run_tests.sh
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+
+set -e
+[ -n "$DEBUG" ] && set -x
+
+CURDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+METADATA=${CURDIR}/../metadata.yml
+FORMULA_NAME=$(cat $METADATA | python -c "import sys,yaml; print yaml.load(sys.stdin)['name']")
+
+## Overrideable parameters
+PILLARDIR=${PILLARDIR:-${CURDIR}/pillar}
+BUILDDIR=${BUILDDIR:-${CURDIR}/build}
+VENV_DIR=${VENV_DIR:-${BUILDDIR}/virtualenv}
+DEPSDIR=${BUILDDIR}/deps
+
+SALT_FILE_DIR=${SALT_FILE_DIR:-${BUILDDIR}/file_root}
+SALT_PILLAR_DIR=${SALT_PILLAR_DIR:-${BUILDDIR}/pillar_root}
+SALT_CONFIG_DIR=${SALT_CONFIG_DIR:-${BUILDDIR}/salt}
+SALT_CACHE_DIR=${SALT_CACHE_DIR:-${SALT_CONFIG_DIR}/cache}
+
+SALT_OPTS="${SALT_OPTS} --retcode-passthrough --local -c ${SALT_CONFIG_DIR}"
+
+if [ "x${SALT_VERSION}" != "x" ]; then
+    PIP_SALT_VERSION="==${SALT_VERSION}"
+fi
+
+## Functions
+log_info() {
+    echo "[INFO] $*"
+}
+
+log_err() {
+    echo "[ERROR] $*" >&2
+}
+
+setup_virtualenv() {
+    log_info "Setting up Python virtualenv"
+    virtualenv $VENV_DIR
+    source ${VENV_DIR}/bin/activate
+    pip install salt${PIP_SALT_VERSION}
+}
+
+setup_pillar() {
+    [ ! -d ${SALT_PILLAR_DIR} ] && mkdir -p ${SALT_PILLAR_DIR}
+    echo "base:" > ${SALT_PILLAR_DIR}/top.sls
+    for pillar in ${PILLARDIR}/*; do
+        state_name=$(basename ${pillar%.sls})
+        echo -e "  ${state_name}:\n    - ${state_name}" >> ${SALT_PILLAR_DIR}/top.sls
+    done
+}
+
+setup_salt() {
+    [ ! -d ${SALT_FILE_DIR} ] && mkdir -p ${SALT_FILE_DIR}
+    [ ! -d ${SALT_CONFIG_DIR} ] && mkdir -p ${SALT_CONFIG_DIR}
+    [ ! -d ${SALT_CACHE_DIR} ] && mkdir -p ${SALT_CACHE_DIR}
+
+    echo "base:" > ${SALT_FILE_DIR}/top.sls
+    for pillar in ${PILLARDIR}/*.sls; do
+        state_name=$(basename ${pillar%.sls})
+        echo -e "  ${state_name}:\n    - ${FORMULA_NAME}" >> ${SALT_FILE_DIR}/top.sls
+    done
+
+    cat << EOF > ${SALT_CONFIG_DIR}/minion
+file_client: local
+cachedir: ${SALT_CACHE_DIR}
+verify_env: False
+
+file_roots:
+  base:
+  - ${SALT_FILE_DIR}
+  - ${CURDIR}/..
+
+pillar_roots:
+  base:
+  - ${SALT_PILLAR_DIR}
+  - ${PILLARDIR}
+EOF
+}
+
+fetch_dependency() {
+    dep_root="${DEPSDIR}/$(basename $1 .git)"
+    dep_metadata="${dep_root}/metadata.yml"
+
+    [ -d $dep_root ] && log_info "Dependency $1 already fetched" && return 0
+
+    log_info "Fetching dependency $1"
+    [ ! -d ${DEPSDIR} ] && mkdir -p ${DEPSDIR}
+    git clone $1 ${DEPSDIR}/$(basename $1 .git)
+
+    dep_name=$(cat $dep_metadata | python -c "import sys,yaml; print yaml.load(sys.stdin)['name']")
+    ln -s ${dep_root}/${dep_name} ${SALT_FILE_DIR}/${dep_name}
+
+    METADATA="${dep_metadata}" install_dependencies
+}
+
+install_dependencies() {
+    grep -E "^dependencies:" ${METADATA} >/dev/null || return 0
+    (python - | while read dep; do fetch_dependency "$dep"; done) << EOF
+import sys,yaml
+for dep in yaml.load(open('${METADATA}', 'ro'))['dependencies']:
+    print dep["source"]
+EOF
+}
+
+clean() {
+    log_info "Cleaning up ${BUILDDIR}"
+    [ -d ${BUILDDIR} ] && rm -rf ${BUILDDIR} || exit 0
+}
+
+salt_run() {
+    source ${VENV_DIR}/bin/activate
+    salt-call ${SALT_OPTS} $*
+}
+
+prepare() {
+    [ -d ${BUILDDIR} ] && mkdir -p ${BUILDDIR}
+
+    setup_virtualenv
+    setup_pillar
+    setup_salt
+    install_dependencies
+}
+
+run() {
+    for pillar in ${PILLARDIR}/*.sls; do
+        state_name=$(basename ${pillar%.sls})
+        salt_run --id=${state_name} state.show_sls ${FORMULA_NAME} || (log_err "Execution of ${FORMULA_NAME}.${state_name} failed"; exit 1)
+    done
+}
+
+_atexit() {
+    RETVAL=$?
+    trap true INT TERM EXIT
+
+    if [ $RETVAL -ne 0 ]; then
+        log_err "Execution failed"
+    else
+        log_info "Execution successful"
+    fi
+    return $RETVAL
+}
+
+## Main
+trap _atexit INT TERM EXIT
+
+case $1 in
+    clean)
+        clean
+        ;;
+    prepare)
+        prepare
+        ;;
+    run)
+        run
+        ;;
+    *)
+        prepare
+        run
+        ;;
+esac