Initial commit

Change-Id: I680e0343e1d37b185d334c0133ebb155ce7de9be
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..35a854c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+# Compiled files
+*.py[co]
+*.a
+*.o
+*.so
+
+# Other
+.idea
+.*.swp
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..ea2818f
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,13 @@
+Copyright (c) Mirantis Inc.
+
+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.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..4a83bf8
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,35 @@
+DESTDIR=/
+SALTENVDIR=/usr/share/salt-formulas/env
+RECLASSDIR=/usr/share/salt-formulas/reclass
+FORMULANAME=$(shell grep name: metadata.yml|head -1|cut -d : -f 2|grep -Eo '[a-z0-9\-\_]*')
+
+MAKE_PID := $(shell echo $$PPID)
+JOB_FLAG := $(filter -j%, $(subst -j ,-j,$(shell ps T | grep "^\s*$(MAKE_PID).*$(MAKE)")))
+
+ifneq ($(subst -j,,$(JOB_FLAG)),)
+JOBS := $(subst -j,,$(JOB_FLAG))
+else
+JOBS := 1
+endif
+
+all:
+	@echo "make install - Install into DESTDIR"
+	@echo "make test    - Run tests"
+	@echo "make clean   - Cleanup after tests run"
+
+install:
+	# Formula
+	[ -d $(DESTDIR)/$(SALTENVDIR) ] || mkdir -p $(DESTDIR)/$(SALTENVDIR)
+	cp -a $(FORMULANAME) $(DESTDIR)/$(SALTENVDIR)/
+	[ ! -d _modules ] || cp -a _modules $(DESTDIR)/$(SALTENVDIR)/
+	[ ! -d _states ] || cp -a _states $(DESTDIR)/$(SALTENVDIR)/ || true
+	# Metadata
+	[ -d $(DESTDIR)/$(RECLASSDIR)/service/$(FORMULANAME) ] || mkdir -p $(DESTDIR)/$(RECLASSDIR)/service/$(FORMULANAME)
+	cp -a metadata/service/* $(DESTDIR)/$(RECLASSDIR)/service/$(FORMULANAME)
+
+test:
+	[ ! -d tests ] || (cd tests; ./run_tests.sh)
+
+clean:
+	[ ! -d tests/build ] || rm -rf tests/build
+	[ ! -d build ] || rm -rf build
diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000..49d5957
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+0.1
diff --git a/_modules/runtest/__init__.py b/_modules/runtest/__init__.py
new file mode 100644
index 0000000..f95e556
--- /dev/null
+++ b/_modules/runtest/__init__.py
@@ -0,0 +1,141 @@
+# -*- coding: utf-8 -*-
+'''
+'''
+
+import jinja2
+import logging
+import re
+import requests
+
+import pkg_resources
+
+import salt.pillar
+
+from runtest import tempest_sections
+from runtest.utils import requirements
+
+log = logging.getLogger(__name__)
+
+__virtualname__ = 'runtest'
+
+
+def __virtual__():
+    return __virtualname__
+
+
+CFG_TEMPLATE = """
+{%- for section,options in config.items() -%}
+[{{ section }}]
+{%- for opt,val in options.items() %}
+{{ opt }} = {{ val }}
+{%- endfor %}
+
+{% endfor %}
+"""
+
+REQUIREMENTS_LINK='https://raw.githubusercontent.com/openstack/requirements'
+
+def _get_pillar_for_live_minions(timeout=5, gather_job_timeout=15):
+    client = salt.client.get_local_client(__opts__['conf_file'])
+    pillars = client.cmd('*', 'pillar.items', timeout=timeout,
+                         gather_job_timeout=gather_job_timeout)
+
+    return pillars
+
+def generate_tempest_config(dst, *args, **kwargs):
+
+    pillars = _get_pillar_for_live_minions()
+    this_node_pillar = __opts__['pillar']
+    runtest_opts =  this_node_pillar.get(__virtualname__, {}).get('tempest', {})
+    config = {}
+
+    for ts in tempest_sections.SECTIONS:
+        ts_inst = ts(pillars, runtest_opts)
+        config[ts_inst.name] = {}
+        opts = {}
+        for opt in ts_inst.options:
+            val = getattr(ts_inst, opt)
+            if val is None:
+                val = runtest_opts.get(ts_inst.name, {}).get(opt, None)
+
+            if val is not None:
+                opts[opt] = val
+
+        config[ts_inst.name] = opts
+
+    data = jinja2.Environment().from_string(CFG_TEMPLATE).render(config=config)
+    with open(dst, 'w') as cfg_file:
+        cfg_file.write(data)
+
+    return config
+
+def check_global_requirements(openstack_version=None, url=None):
+    """Check if our installed requirements are satisfied with upstream.
+    """
+    if openstack_version is None:
+      openstack_version = __salt__['pillar.get']('runtest:openstack_version')
+    if url is None:
+      url = '%s/%s/global-requirements.txt' % (REQUIREMENTS_LINK, openstack_version)
+
+    requirements_content = requests.get(url).text
+    pkgs = requirements.get_installed_distributions()
+
+    req_deps = {}
+    freq = {}
+    for pkg in pkgs:
+      for line in requirements_content.splitlines():
+        if re.match(r'^%s[ ><!=]' % pkg.project_name, line):
+            req_cond = line.split('#')[0].strip()
+            try:
+                log.info("Checking requirement %(req_name)s, requirement"
+                         " %(req)s".format(req_name=pkg.project_name,
+                                           req=req_cond))
+                pkg_resources.working_set.require(req_cond)
+            except pkg_resources.VersionConflict as ve:
+                log.warning("Requirement %(req_name)s not satisfied. Required "
+                            "version %(req_cond)s. Installed version "
+                            "%(inst_version)s. Exception %(exc)s".format(
+                                req_name=pkg.project_name, req_cond=line,
+                                inst_version=pkg.version, exc=ve))
+                freq[pkg.project_name] = {'installed_version': pkg.version,
+                                          'requirement': line}
+            except Exception as ex: {'error': ex,
+                                     'requirement': line,
+                                     'installed_version': pkg.version}
+    return freq
+
+def check_upper_constraints(openstack_version=None, url=None):
+    """Check if our installed requirements are satisfied with upstream upper constraints.
+    """
+    if openstack_version is None:
+      openstack_version = __salt__['pillar.get']('runtest:openstack_version')
+    if url is None:
+      url = '%s/%s/upper-constraints.txt' % (REQUIREMENTS_LINK, openstack_version)
+
+    constraints_content = requests.get(url).text
+    pkgs = requirements.get_installed_distributions()
+
+    req_deps = {}
+    freq = {}
+    for pkg in pkgs:
+      for line in constraints_content.splitlines():
+        if re.match(r'^%s[ ><!=]' % pkg.project_name, line):
+            req_cond = line.split('#')[0].strip()
+            try:
+                log.info("Checking requirement %(req_name)s, requirement"
+                         " %(req)s".format(req_name=pkg.project_name,
+                                           req=req_cond))
+                pkg_resources.working_set.require(req_cond)
+            except pkg_resources.VersionConflict as ve:
+                log.warning("Requirement %(req_name)s not satisfied. Required "
+                            "version %(req_cond)s. Installed version "
+                            "%(inst_version)s. Exception %(exc)s".format(
+                                req_name=pkg.project_name, req_cond=line,
+                                inst_version=pkg.version, exc=ve))
+                freq[pkg.project_name] = {'installed_version': pkg.version,
+                                          'requirement': line,
+                                          'error': 've'}
+            except Exception as ex: {'error': ex,
+                                     'requirement': line,
+                                     'installed_version': pkg.version}
+    return freq
diff --git a/_modules/runtest/conditions.py b/_modules/runtest/conditions.py
new file mode 100644
index 0000000..05fe24e
--- /dev/null
+++ b/_modules/runtest/conditions.py
@@ -0,0 +1,51 @@
+import jsonpath_rw as jsonpath
+import operator
+
+
+class SimpleCondition(object):
+    op = None
+
+    def check(self, value, expected):
+        return self.op(value, expected)
+
+
+class EqCondition(SimpleCondition):
+    op = operator.eq
+
+OPERATORS = {
+  'eq': EqCondition,
+}
+
+class BaseRule(object):
+
+    def __init__(self, field, op, val, multiple='first'):
+        self.field = field
+        self.op = op
+        self.value = val
+        self.multiple = multiple
+
+    def check(self, pillar):
+        """Check if condition match in the passed pillar.
+
+        :param pillar: pillar data to check for condition in.
+        :return: True if condition match, False otherwise.
+        """
+
+        res = False
+        count = 0
+        for match in jsonpath.parse(self.field).find(pillar):
+            cond_ext = OPERATORS[self.op]()
+            res = cond_ext.check(match.value, self.value)
+            if (self.multiple == 'first' or
+                (self.multiple == 'all' and not res) or
+                (self.multiple == 'any' and res)):
+                    break
+            elif self.multiple == 'multiple' and res:
+                count += 1
+                if count > 1:
+                    return True
+
+        if not res or self.multiple == 'multiple':
+            return False
+
+        return True
diff --git a/_modules/runtest/tempest_sections/__init__.py b/_modules/runtest/tempest_sections/__init__.py
new file mode 100644
index 0000000..bfa5c8f
--- /dev/null
+++ b/_modules/runtest/tempest_sections/__init__.py
@@ -0,0 +1,57 @@
+
+import auth
+
+import baremetal
+import baremetal_feature_enabled
+import compute
+import compute_feature_enabled
+import debug
+import default
+import dns
+import dns_feature_enabled
+import heat_plugin
+import identity
+import identity_feature_enabled
+import image
+import image_feature_enabled
+import network
+import network_feature_enabled
+import object_storage
+import object_storage_feature_enabled
+import orchestration
+import oslo_concurrency
+import scenario
+import service_clients
+import service_available
+import validation
+import volume
+import volume_feature_enabled
+
+SECTIONS = [
+    auth.Auth,
+    baremetal.Baremetal,
+    baremetal_feature_enabled.BaremetalFeatureEnabled,
+    compute.Compute,
+    compute_feature_enabled.ComputeFeatureEnabled,
+    debug.Debug,
+    default.Default,
+    dns.Dns,
+    dns_feature_enabled.DnsFeatureEnabled,
+    heat_plugin.HeatPlugin,
+    identity.Identity,
+    identity_feature_enabled.IdentityFeatureEnabled,
+    image.Image,
+    image_feature_enabled.ImageFeatureEnabled,
+    network.Network,
+    network_feature_enabled.NetworkFeatureEnabled,
+    object_storage.ObjectStorage,
+    object_storage_feature_enabled.ObjectStorageFeatureEnabled,
+    orchestration.Orchestration,
+    oslo_concurrency.OsloConcurrency,
+    scenario.Scenario,
+    service_clients.ServiceClients,
+    service_available.ServiceAvailable,
+    validation.Validation,
+    volume.Volume,
+    volume_feature_enabled.VolumeFeatureEnabled,
+]
diff --git a/_modules/runtest/tempest_sections/auth.py b/_modules/runtest/tempest_sections/auth.py
new file mode 100644
index 0000000..83a5748
--- /dev/null
+++ b/_modules/runtest/tempest_sections/auth.py
@@ -0,0 +1,62 @@
+
+from runtest import conditions
+from runtest.tempest_sections import base_section
+
+class Auth(base_section.BaseSection):
+
+    name = "auth"
+    options = [
+        'admin_domain_name',
+        'admin_password',
+        'admin_project_name',
+        'admin_username',
+        'create_isolated_networks',
+        'default_credentials_domain_name',
+        'tempest_roles',
+        'test_accounts_file',
+        'use_dynamic_credentials',
+    ]
+
+
+    @property
+    def admin_domain_name(self):
+        pass
+
+    @property
+    def admin_password(self):
+        c = conditions.BaseRule('keystone.server.enabled', 'eq', True)
+        return self.get_item_when_condition_match(
+            'keystone.server.admin_password', c)
+
+    @property
+    def admin_project_name(self):
+        c = conditions.BaseRule('keystone.server.enabled', 'eq', True)
+        return self.get_item_when_condition_match(
+            'keystone.server.admin_tenant', c)
+
+    @property
+    def admin_username(self):
+        c = conditions.BaseRule('keystone.server.enabled', 'eq', True)
+        return self.get_item_when_condition_match(
+            'keystone.server.admin_name', c)
+
+    @property
+    def create_isolated_networks(self):
+        pass
+
+    @property
+    def default_credentials_domain_name(self):
+        pass
+
+    @property
+    def tempest_roles(self):
+        pass
+
+    @property
+    def test_accounts_file(self):
+        pass
+
+    @property
+    def use_dynamic_credentials(self):
+        pass
+
diff --git a/_modules/runtest/tempest_sections/baremetal.py b/_modules/runtest/tempest_sections/baremetal.py
new file mode 100644
index 0000000..d93c210
--- /dev/null
+++ b/_modules/runtest/tempest_sections/baremetal.py
@@ -0,0 +1,99 @@
+
+import base_section
+
+class Baremetal(base_section.BaseSection):
+
+    name = "baremetal"
+    options = [
+        'active_timeout',
+        'adjusted_root_disk_size_gb',
+        'association_timeout',
+        'catalog_type',
+        'deploywait_timeout',
+        'driver',
+        'enabled_drivers',
+        'enabled_hardware_types',
+        'endpoint_type',
+        'max_microversion',
+        'min_microversion',
+        'partition_image_ref',
+        'power_timeout',
+        'unprovision_timeout',
+        'use_provision_network',
+        'whole_disk_image_checksum',
+        'whole_disk_image_ref',
+        'whole_disk_image_url',
+    ]
+
+
+    @property
+    def active_timeout(self):
+        pass
+
+    @property
+    def adjusted_root_disk_size_gb(self):
+        pass
+
+    @property
+    def association_timeout(self):
+        pass
+
+    @property
+    def catalog_type(self):
+        pass
+
+    @property
+    def deploywait_timeout(self):
+        pass
+
+    @property
+    def driver(self):
+        pass
+
+    @property
+    def enabled_drivers(self):
+        pass
+
+    @property
+    def enabled_hardware_types(self):
+        pass
+
+    @property
+    def endpoint_type(self):
+        pass
+
+    @property
+    def max_microversion(self):
+        pass
+
+    @property
+    def min_microversion(self):
+        pass
+
+    @property
+    def partition_image_ref(self):
+        pass
+
+    @property
+    def power_timeout(self):
+        pass
+
+    @property
+    def unprovision_timeout(self):
+        pass
+
+    @property
+    def use_provision_network(self):
+        pass
+
+    @property
+    def whole_disk_image_checksum(self):
+        pass
+
+    @property
+    def whole_disk_image_ref(self):
+        pass
+
+    @property
+    def whole_disk_image_url(self):
+        pass
diff --git a/_modules/runtest/tempest_sections/baremetal_feature_enabled.py b/_modules/runtest/tempest_sections/baremetal_feature_enabled.py
new file mode 100644
index 0000000..3f83520
--- /dev/null
+++ b/_modules/runtest/tempest_sections/baremetal_feature_enabled.py
@@ -0,0 +1,14 @@
+
+import base_section
+
+class BaremetalFeatureEnabled(base_section.BaseSection):
+
+    name = "baremetal_feature_enabled"
+    options = [
+        'ipxe_enabled',
+    ]
+
+
+    @property
+    def ipxe_enabled(self):
+        pass
diff --git a/_modules/runtest/tempest_sections/base_section.py b/_modules/runtest/tempest_sections/base_section.py
new file mode 100644
index 0000000..867386f
--- /dev/null
+++ b/_modules/runtest/tempest_sections/base_section.py
@@ -0,0 +1,68 @@
+
+import abc
+import jsonpath_rw as jsonpath
+
+import salt
+
+from runtest import conditions
+
+class BaseSection(object):
+
+    def __init__(self, pillar, runtest_opts):
+        super(BaseSection, self).__init__()
+        self.pillar = pillar
+        self.runtest_opts = runtest_opts
+        self.salt_client = salt.client.get_local_client()
+
+    @abc.abstractproperty
+    def name(self):
+        """"""
+
+    @abc.abstractproperty
+    def options():
+        """"""
+
+    def get_item_when_condition_match(self, item_path, condition):
+        """Get specified item from pillar when condition match.
+        """
+
+        for node, npillar in self.pillar.items():
+            for match in jsonpath.parse(condition.field).find(npillar):
+                if condition.op == 'eq':
+                    if match.value == condition.value:
+                        res = jsonpath.parse(item_path).find(npillar)
+                        if res:
+                            return res[0].value
+
+    def get_nodes_where_condition_match(self, condition):
+        """ Return a list of nodes that have pillar that matches condition.
+        """
+
+        res = []
+        for node, npillar in self.pillar.items():
+            if condition.check(npillar):
+                res.append(node)
+        return res
+
+    def authenticated_openstack_module_call(self, target, module, *args, **kwargs):
+        """Calls specified openstack module from admin keystone user.
+        """
+        auth_profile = {}
+        ks = conditions.BaseRule(field='keystone.server.enabled', op='eq', val=True)
+        auth_profile['connection_password'] = self.get_item_when_condition_match(
+            'keystone.server.admin_password', ks)
+        auth_profile['connection_user'] = self.get_item_when_condition_match(
+            'keystone.server.admin_name', ks)
+        auth_profile['connection_tenant'] = self.get_item_when_condition_match(
+            'keystone.server.admin_tenant', ks)
+        auth_profile['connection_region_name'] = self.get_item_when_condition_match(
+            'keystone.server.region', ks)
+        address = self.get_item_when_condition_match(
+            'keystone.server.bind.public_address', ks)
+        port = self.get_item_when_condition_match('keystone.server.bind.public_port', ks)
+        auth_profile['connection_auth_url'] = "http://{}:{}/v2.0".format(address, port)
+
+        kwargs.update(auth_profile)
+
+        return self.salt_client.cmd(target, 'neutronng.list_networks', timeout=5,
+                                    gather_job_timeout=15, arg=args, kwarg=kwargs)
diff --git a/_modules/runtest/tempest_sections/compute.py b/_modules/runtest/tempest_sections/compute.py
new file mode 100644
index 0000000..4d9ee33
--- /dev/null
+++ b/_modules/runtest/tempest_sections/compute.py
@@ -0,0 +1,94 @@
+
+import base_section
+
+class Compute(base_section.BaseSection):
+
+    name = "compute"
+    options = [
+        'build_interval',
+        'build_timeout',
+        'catalog_type',
+        'endpoint_type',
+        'fixed_network_name',
+        'flavor_ref',
+        'flavor_ref_alt',
+        'hypervisor_type',
+        'image_ref',
+        'image_ref_alt',
+        'max_microversion',
+        'min_compute_nodes',
+        'min_microversion',
+        'ready_wait',
+        'region',
+        'shelved_offload_time',
+        'volume_device_name',
+    ]
+
+
+    @property
+    def build_interval(self):
+        pass
+
+    @property
+    def build_timeout(self):
+        pass
+
+    @property
+    def catalog_type(self):
+        pass
+
+    @property
+    def endpoint_type(self):
+        pass
+
+    @property
+    def fixed_network_name(self):
+        pass
+
+    @property
+    def flavor_ref(self):
+        pass
+
+    @property
+    def flavor_ref_alt(self):
+        pass
+
+    @property
+    def hypervisor_type(self):
+        pass
+
+    @property
+    def image_ref(self):
+        pass
+
+    @property
+    def image_ref_alt(self):
+        pass
+
+    @property
+    def max_microversion(self):
+        pass
+
+    @property
+    def min_compute_nodes(self):
+        pass
+
+    @property
+    def min_microversion(self):
+        pass
+
+    @property
+    def ready_wait(self):
+        pass
+
+    @property
+    def region(self):
+        pass
+
+    @property
+    def shelved_offload_time(self):
+        pass
+
+    @property
+    def volume_device_name(self):
+        pass
diff --git a/_modules/runtest/tempest_sections/compute_feature_enabled.py b/_modules/runtest/tempest_sections/compute_feature_enabled.py
new file mode 100644
index 0000000..6f787f9
--- /dev/null
+++ b/_modules/runtest/tempest_sections/compute_feature_enabled.py
@@ -0,0 +1,156 @@
+
+import base_section
+
+from runtest import conditions
+
+class ComputeFeatureEnabled(base_section.BaseSection):
+
+    name = "compute-feature-enabled"
+    options = [
+        'api_extensions',
+        'attach_encrypted_volume',
+        'block_migrate_cinder_iscsi',
+        'block_migration_for_live_migration',
+        'change_password',
+        'cold_migration',
+        'config_drive',
+        'console_output',
+        'disk_config',
+        'enable_instance_password',
+        'interface_attach',
+        'live_migrate_back_and_forth',
+        'live_migration',
+        'metadata_service',
+        'nova_cert',
+        'pause',
+        'personality',
+        'rdp_console',
+        'rescue',
+        'resize',
+        'scheduler_available_filters',
+        'serial_console',
+        'shelve',
+        'snapshot',
+        'spice_console',
+        'suspend',
+        'swap_volume',
+        'vnc_console',
+    ]
+
+
+    @property
+    def api_extensions(self):
+        pass
+
+    @property
+    def attach_encrypted_volume(self):
+        pass
+
+    @property
+    def block_migrate_cinder_iscsi(self):
+        pass
+
+    @property
+    def block_migration_for_live_migration(self):
+        pass
+
+    @property
+    def change_password(self):
+        pass
+
+    @property
+    def cold_migration(self):
+        pass
+
+    @property
+    def config_drive(self):
+        pass
+
+    @property
+    def console_output(self):
+        pass
+
+    @property
+    def disk_config(self):
+        pass
+
+    @property
+    def enable_instance_password(self):
+        pass
+
+    @property
+    def interface_attach(self):
+        pass
+
+    @property
+    def live_migrate_back_and_forth(self):
+        pass
+
+    @property
+    def live_migration(self):
+        return conditions.BaseRule('*.nova.compute.enabled', 'eq', True,
+                                   multiple='multiple').check(self.pillar)
+
+    @property
+    def metadata_service(self):
+        pass
+
+    @property
+    def nova_cert(self):
+        pass
+
+    @property
+    def pause(self):
+        pass
+
+    @property
+    def personality(self):
+        pass
+
+    @property
+    def rdp_console(self):
+        pass
+
+    @property
+    def rescue(self):
+        pass
+
+    @property
+    def resize(self):
+        # NOTE(vsaienko) allow_resize_to_same_host is hardcoded to True in
+        # nova formula, update when value is configurable.
+        res = conditions.BaseRule('*.nova.compute.enabled', 'eq', True,
+                            multiple='any').check(self.pillar)
+        return res
+
+    @property
+    def scheduler_available_filters(self):
+        pass
+
+    @property
+    def serial_console(self):
+        pass
+
+    @property
+    def shelve(self):
+        pass
+
+    @property
+    def snapshot(self):
+        pass
+
+    @property
+    def spice_console(self):
+        pass
+
+    @property
+    def suspend(self):
+        pass
+
+    @property
+    def swap_volume(self):
+        pass
+
+    @property
+    def vnc_console(self):
+        pass
diff --git a/_modules/runtest/tempest_sections/debug.py b/_modules/runtest/tempest_sections/debug.py
new file mode 100644
index 0000000..21d4154
--- /dev/null
+++ b/_modules/runtest/tempest_sections/debug.py
@@ -0,0 +1,14 @@
+
+import base_section
+
+class Debug(base_section.BaseSection):
+
+    name = "debug"
+    options = [
+        'trace_requests',
+    ]
+
+
+    @property
+    def trace_requests(self):
+        pass
diff --git a/_modules/runtest/tempest_sections/default.py b/_modules/runtest/tempest_sections/default.py
new file mode 100644
index 0000000..0c2761f
--- /dev/null
+++ b/_modules/runtest/tempest_sections/default.py
@@ -0,0 +1,139 @@
+
+import base_section
+
+class Default(base_section.BaseSection):
+
+    name = "DEFAULT"
+    options = [
+        'debug',
+        'log_config_append',
+        'log_date_format',
+        'log_file',
+        'log_dir',
+        'watch_log_file',
+        'use_syslog',
+        'use_journal',
+        'syslog_log_facility',
+        'use_json',
+        'use_stderr',
+        'logging_context_format_string',
+        'logging_default_format_string',
+        'logging_debug_format_suffix',
+        'logging_exception_prefix',
+        'logging_user_identity_format',
+        'default_log_levels',
+        'publish_errors',
+        'instance_format',
+        'instance_uuid_format',
+        'rate_limit_interval',
+        'rate_limit_burst',
+        'rate_limit_except_level',
+        'fatal_deprecations',
+        'resources_prefix',
+        'pause_teardown',
+    ]
+
+
+    @property
+    def debug(self):
+        pass
+
+    @property
+    def log_config_append(self):
+        pass
+
+    @property
+    def log_date_format(self):
+        pass
+
+    @property
+    def log_file(self):
+        pass
+
+    @property
+    def log_dir(self):
+        pass
+
+    @property
+    def watch_log_file(self):
+        pass
+
+    @property
+    def use_syslog(self):
+        pass
+
+    @property
+    def use_journal(self):
+        pass
+
+    @property
+    def syslog_log_facility(self):
+        pass
+
+    @property
+    def use_json(self):
+        pass
+
+    @property
+    def use_stderr(self):
+        pass
+
+    @property
+    def logging_context_format_string(self):
+        pass
+
+    @property
+    def logging_default_format_string(self):
+        pass
+
+    @property
+    def logging_debug_format_suffix(self):
+        pass
+
+    @property
+    def logging_exception_prefix(self):
+        pass
+
+    @property
+    def logging_user_identity_format(self):
+        pass
+
+    @property
+    def default_log_levels(self):
+        pass
+
+    @property
+    def publish_errors(self):
+        pass
+
+    @property
+    def instance_format(self):
+        pass
+
+    @property
+    def instance_uuid_format(self):
+        pass
+
+    @property
+    def rate_limit_interval(self):
+        pass
+
+    @property
+    def rate_limit_burst(self):
+        pass
+
+    @property
+    def rate_limit_except_level(self):
+        pass
+
+    @property
+    def fatal_deprecations(self):
+        pass
+
+    @property
+    def resources_prefix(self):
+        pass
+
+    @property
+    def pause_teardown(self):
+        pass
diff --git a/_modules/runtest/tempest_sections/dns.py b/_modules/runtest/tempest_sections/dns.py
new file mode 100644
index 0000000..9b18b6a
--- /dev/null
+++ b/_modules/runtest/tempest_sections/dns.py
@@ -0,0 +1,44 @@
+
+import base_section
+
+class Dns(base_section.BaseSection):
+
+    name = "dns"
+    options = [
+        'build_interval',
+        'build_timeout',
+        'catalog_type',
+        'endpoint_type',
+        'min_ttl',
+        'nameservers',
+        'query_timeout',
+    ]
+
+
+    @property
+    def build_interval(self):
+        pass
+
+    @property
+    def build_timeout(self):
+        pass
+
+    @property
+    def catalog_type(self):
+        pass
+
+    @property
+    def endpoint_type(self):
+        pass
+
+    @property
+    def min_ttl(self):
+        pass
+
+    @property
+    def nameservers(self):
+        pass
+
+    @property
+    def query_timeout(self):
+        pass
diff --git a/_modules/runtest/tempest_sections/dns_feature_enabled.py b/_modules/runtest/tempest_sections/dns_feature_enabled.py
new file mode 100644
index 0000000..b3fb061
--- /dev/null
+++ b/_modules/runtest/tempest_sections/dns_feature_enabled.py
@@ -0,0 +1,44 @@
+
+import base_section
+
+class DnsFeatureEnabled(base_section.BaseSection):
+
+    name = "dns_feature_enabled"
+    options = [
+        'api_admin',
+        'api_v1',
+        'api_v1_servers',
+        'api_v2',
+        'api_v2_quotas',
+        'api_v2_root_recordsets',
+        'bug_1573141_fixed',
+    ]
+
+
+    @property
+    def api_admin(self):
+        pass
+
+    @property
+    def api_v1(self):
+        pass
+
+    @property
+    def api_v1_servers(self):
+        pass
+
+    @property
+    def api_v2(self):
+        pass
+
+    @property
+    def api_v2_quotas(self):
+        pass
+
+    @property
+    def api_v2_root_recordsets(self):
+        pass
+
+    @property
+    def bug_1573141_fixed(self):
+        pass
diff --git a/_modules/runtest/tempest_sections/heat_plugin.py b/_modules/runtest/tempest_sections/heat_plugin.py
new file mode 100644
index 0000000..0f2e16f
--- /dev/null
+++ b/_modules/runtest/tempest_sections/heat_plugin.py
@@ -0,0 +1,224 @@
+
+import base_section
+
+class HeatPlugin(base_section.BaseSection):
+
+    name = "heat_plugin"
+    options = [
+        'admin_password',
+        'admin_project_name',
+        'admin_username',
+        'auth_url',
+        'auth_version',
+        'boot_config_env',
+        'build_interval',
+        'build_timeout',
+        'ca_file',
+        'catalog_type',
+        'connectivity_timeout',
+        'convergence_engine_enabled',
+        'disable_ssl_certificate_validation',
+        'fixed_network_name',
+        'fixed_subnet_name',
+        'floating_network_name',
+        'heat_config_notify_script',
+        'image_ref',
+        'instance_type',
+        'ip_version_for_ssh',
+        'keypair_name',
+        'minimal_image_ref',
+        'minimal_instance_type',
+        'network_for_ssh',
+        'password',
+        'project_domain_id',
+        'project_domain_name',
+        'project_name',
+        'region',
+        'sighup_config_edit_retries',
+        'sighup_timeout',
+        'skip_functional_test_list',
+        'skip_functional_tests',
+        'skip_scenario_test_list',
+        'skip_scenario_tests',
+        'skip_test_stack_action_list',
+        'ssh_channel_timeout',
+        'ssh_timeout',
+        'tenant_network_mask_bits',
+        'user_domain_id',
+        'user_domain_name',
+        'username',
+        'volume_size',
+    ]
+
+
+    @property
+    def admin_password(self):
+        pass
+
+    @property
+    def admin_project_name(self):
+        pass
+
+    @property
+    def admin_username(self):
+        pass
+
+    @property
+    def auth_url(self):
+        pass
+
+    @property
+    def auth_version(self):
+        pass
+
+    @property
+    def boot_config_env(self):
+        pass
+
+    @property
+    def build_interval(self):
+        pass
+
+    @property
+    def build_timeout(self):
+        pass
+
+    @property
+    def ca_file(self):
+        pass
+
+    @property
+    def catalog_type(self):
+        pass
+
+    @property
+    def connectivity_timeout(self):
+        pass
+
+    @property
+    def convergence_engine_enabled(self):
+        pass
+
+    @property
+    def disable_ssl_certificate_validation(self):
+        pass
+
+    @property
+    def fixed_network_name(self):
+        pass
+
+    @property
+    def fixed_subnet_name(self):
+        pass
+
+    @property
+    def floating_network_name(self):
+        pass
+
+    @property
+    def heat_config_notify_script(self):
+        pass
+
+    @property
+    def image_ref(self):
+        pass
+
+    @property
+    def instance_type(self):
+        pass
+
+    @property
+    def ip_version_for_ssh(self):
+        pass
+
+    @property
+    def keypair_name(self):
+        pass
+
+    @property
+    def minimal_image_ref(self):
+        pass
+
+    @property
+    def minimal_instance_type(self):
+        pass
+
+    @property
+    def network_for_ssh(self):
+        pass
+
+    @property
+    def password(self):
+        pass
+
+    @property
+    def project_domain_id(self):
+        pass
+
+    @property
+    def project_domain_name(self):
+        pass
+
+    @property
+    def project_name(self):
+        pass
+
+    @property
+    def region(self):
+        pass
+
+    @property
+    def sighup_config_edit_retries(self):
+        pass
+
+    @property
+    def sighup_timeout(self):
+        pass
+
+    @property
+    def skip_functional_test_list(self):
+        pass
+
+    @property
+    def skip_functional_tests(self):
+        pass
+
+    @property
+    def skip_scenario_test_list(self):
+        pass
+
+    @property
+    def skip_scenario_tests(self):
+        pass
+
+    @property
+    def skip_test_stack_action_list(self):
+        pass
+
+    @property
+    def ssh_channel_timeout(self):
+        pass
+
+    @property
+    def ssh_timeout(self):
+        pass
+
+    @property
+    def tenant_network_mask_bits(self):
+        pass
+
+    @property
+    def user_domain_id(self):
+        pass
+
+    @property
+    def user_domain_name(self):
+        pass
+
+    @property
+    def username(self):
+        pass
+
+    @property
+    def volume_size(self):
+        pass
diff --git a/_modules/runtest/tempest_sections/identity.py b/_modules/runtest/tempest_sections/identity.py
new file mode 100644
index 0000000..a658a43
--- /dev/null
+++ b/_modules/runtest/tempest_sections/identity.py
@@ -0,0 +1,105 @@
+
+import base_section
+
+from runtest import conditions
+
+class Identity(base_section.BaseSection):
+
+    name = "identity"
+    options = [
+        'admin_domain_scope',
+        'admin_role',
+        'auth_version',
+        'ca_certificates_file',
+        'catalog_type',
+        'default_domain_id',
+        'disable_ssl_certificate_validation',
+        'region',
+        'uri',
+        'uri_v3',
+        'user_lockout_duration',
+        'user_lockout_failure_attempts',
+        'user_unique_last_password_count',
+        'v2_admin_endpoint_type',
+        'v2_public_endpoint_type',
+        'v3_endpoint_type',
+    ]
+
+
+    @property
+    def admin_domain_scope(self):
+        pass
+
+    @property
+    def admin_role(self):
+        pass
+
+    @property
+    def auth_version(self):
+        return 'v3'
+
+    @property
+    def ca_certificates_file(self):
+        c = conditions.BaseRule('keystone.server.enabled', 'eq', True)
+        return self.get_item_when_condition_match(
+            'keystone.server.cacert', c)
+
+    @property
+    def catalog_type(self):
+        pass
+
+    @property
+    def default_domain_id(self):
+        pass
+
+    @property
+    def disable_ssl_certificate_validation(self):
+        pass
+
+    @property
+    def region(self):
+        c = conditions.BaseRule('keystone.server.enabled', 'eq', True)
+        return self.get_item_when_condition_match(
+            'keystone.server.region', c)
+
+    @property
+    def uri(self):
+        c = conditions.BaseRule('keystone.server.enabled', 'eq', True)
+        vip = self.get_item_when_condition_match(
+            '_param.cluster_vip_address', c)
+        port = self.get_item_when_condition_match(
+            'keystone.server.bind.private_port', c)
+        return "http://{}:{}/v2.0".format(vip, port)
+
+    @property
+    def uri_v3(self):
+        c = conditions.BaseRule('keystone.server.enabled', 'eq', True)
+        vip = self.get_item_when_condition_match(
+            '_param.cluster_vip_address', c)
+        port = self.get_item_when_condition_match(
+            'keystone.server.bind.private_port', c)
+        return "http://{}:{}/v3".format(vip, port)
+
+    @property
+    def user_lockout_duration(self):
+        pass
+
+    @property
+    def user_lockout_failure_attempts(self):
+        pass
+
+    @property
+    def user_unique_last_password_count(self):
+        pass
+
+    @property
+    def v2_admin_endpoint_type(self):
+        pass
+
+    @property
+    def v2_public_endpoint_type(self):
+        pass
+
+    @property
+    def v3_endpoint_type(self):
+        pass
diff --git a/_modules/runtest/tempest_sections/identity_feature_enabled.py b/_modules/runtest/tempest_sections/identity_feature_enabled.py
new file mode 100644
index 0000000..166bebd
--- /dev/null
+++ b/_modules/runtest/tempest_sections/identity_feature_enabled.py
@@ -0,0 +1,49 @@
+
+import base_section
+
+class IdentityFeatureEnabled(base_section.BaseSection):
+
+    name = "identity-feature-enabled"
+    options = [
+        'api_extensions',
+        'api_v2',
+        'api_v2_admin',
+        'api_v3',
+        'domain_specific_drivers',
+        'forbid_global_implied_dsr',
+        'security_compliance',
+        'trust',
+    ]
+
+
+    @property
+    def api_extensions(self):
+        pass
+
+    @property
+    def api_v2(self):
+        pass
+
+    @property
+    def api_v2_admin(self):
+        pass
+
+    @property
+    def api_v3(self):
+        pass
+
+    @property
+    def domain_specific_drivers(self):
+        pass
+
+    @property
+    def forbid_global_implied_dsr(self):
+        pass
+
+    @property
+    def security_compliance(self):
+        pass
+
+    @property
+    def trust(self):
+        pass
diff --git a/_modules/runtest/tempest_sections/image.py b/_modules/runtest/tempest_sections/image.py
new file mode 100644
index 0000000..6d9913e
--- /dev/null
+++ b/_modules/runtest/tempest_sections/image.py
@@ -0,0 +1,49 @@
+
+import base_section
+
+class Image(base_section.BaseSection):
+
+    name = "image"
+    options = [
+        'build_interval',
+        'build_timeout',
+        'catalog_type',
+        'container_formats',
+        'disk_formats',
+        'endpoint_type',
+        'http_image',
+        'region',
+    ]
+
+
+    @property
+    def build_interval(self):
+        pass
+
+    @property
+    def build_timeout(self):
+        pass
+
+    @property
+    def catalog_type(self):
+        pass
+
+    @property
+    def container_formats(self):
+        pass
+
+    @property
+    def disk_formats(self):
+        pass
+
+    @property
+    def endpoint_type(self):
+        pass
+
+    @property
+    def http_image(self):
+        pass
+
+    @property
+    def region(self):
+        pass
diff --git a/_modules/runtest/tempest_sections/image_feature_enabled.py b/_modules/runtest/tempest_sections/image_feature_enabled.py
new file mode 100644
index 0000000..3bc05fb
--- /dev/null
+++ b/_modules/runtest/tempest_sections/image_feature_enabled.py
@@ -0,0 +1,36 @@
+
+import base_section
+
+from runtest import conditions
+
+class ImageFeatureEnabled(base_section.BaseSection):
+
+    name = "image-feature-enabled"
+    options = [
+        'api_v1',
+        'api_v2',
+        'deactivate_image',
+    ]
+
+
+    @property
+    def api_v1(self):
+        ge = conditions.BaseRule('*.glance.server.enabled', 'eq', True,
+                                 multiple='any').check(self.pillar)
+        if not ge:
+            return
+        c = conditions.BaseRule('glance.server.enabled', 'eq', True)
+        gv = self.get_item_when_condition_match(
+            'glance.server.version', c)
+        # starting from Ocata glance_v1 is disabled
+        if gv in ['juno', 'kilo', 'liberty', 'mitaka', 'newton']:
+           return True
+        return False
+
+    @property
+    def api_v2(self):
+        return True
+
+    @property
+    def deactivate_image(self):
+        return True
diff --git a/_modules/runtest/tempest_sections/network.py b/_modules/runtest/tempest_sections/network.py
new file mode 100644
index 0000000..1fb61b3
--- /dev/null
+++ b/_modules/runtest/tempest_sections/network.py
@@ -0,0 +1,112 @@
+
+import base_section
+
+from runtest import conditions
+
+class Network(base_section.BaseSection):
+
+    name = "network"
+    options = [
+        'build_interval',
+        'build_timeout',
+        'catalog_type',
+        'default_network',
+        'dns_servers',
+        'endpoint_type',
+        'floating_network_name',
+        'port_vnic_type',
+        'project_network_cidr',
+        'project_network_mask_bits',
+        'project_network_v6_cidr',
+        'project_network_v6_mask_bits',
+        'project_networks_reachable',
+        'public_network_id',
+        'public_router_id',
+        'region',
+        'shared_physical_network',
+    ]
+
+
+    @property
+    def build_interval(self):
+        pass
+
+    @property
+    def build_timeout(self):
+        pass
+
+    @property
+    def catalog_type(self):
+        pass
+
+    @property
+    def default_network(self):
+        pass
+
+    @property
+    def dns_servers(self):
+        pass
+
+    @property
+    def endpoint_type(self):
+        pass
+
+    @property
+    def floating_network_name(self):
+        pass
+
+    @property
+    def port_vnic_type(self):
+        pass
+
+    @property
+    def project_network_cidr(self):
+        pass
+
+    @property
+    def project_network_mask_bits(self):
+        pass
+
+    @property
+    def project_network_v6_cidr(self):
+        pass
+
+    @property
+    def project_network_v6_mask_bits(self):
+        pass
+
+    @property
+    def project_networks_reachable(self):
+        pass
+
+    @property
+    def public_network_id(self):
+        c = conditions.BaseRule(field='keystone.client.enabled', op='eq',
+                                val=True)
+        nodes = self.get_nodes_where_condition_match(c)
+        network_name = self.runtest_opts.get(
+            'convert_to_uuid', {}).get('public_network_id')
+
+        if not network_name:
+          return
+
+        res = self.authenticated_openstack_module_call(
+            nodes[0], 'neutronng.list_netowkrs')[nodes[0]]['networks']
+        networks = [n['id'] for n in res if n['name'] == network_name]
+
+        if len(networks) != 1:
+            raise Exception("Error getting networks: {}".format(networks))
+
+        return networks[0]
+
+    @property
+    def public_router_id(self):
+        pass
+
+    @property
+    def region(self):
+        pass
+
+    @property
+    def shared_physical_network(self):
+        pass
diff --git a/_modules/runtest/tempest_sections/network_feature_enabled.py b/_modules/runtest/tempest_sections/network_feature_enabled.py
new file mode 100644
index 0000000..957c288
--- /dev/null
+++ b/_modules/runtest/tempest_sections/network_feature_enabled.py
@@ -0,0 +1,50 @@
+
+import base_section
+
+from runtest import conditions
+
+class NetworkFeatureEnabled(base_section.BaseSection):
+
+    name = "network-feature-enabled"
+    options = [
+        'api_extensions',
+        'floating_ips',
+        'ipv6',
+        'ipv6_subnet_attributes',
+        'port_admin_state_change',
+        'port_security',
+    ]
+
+
+    @property
+    def api_extensions(self):
+        # We will get this when running
+        # tox -evenv -- tempest verify-config -uro tempest_config_file
+        pass
+
+    @property
+    def floating_ips(self):
+        pass
+
+    @property
+    def ipv6(self):
+        pass
+
+    @property
+    def ipv6_subnet_attributes(self):
+        pass
+
+    @property
+    def port_admin_state_change(self):
+        pass
+
+    @property
+    def port_security(self):
+        c = conditions.BaseRule('neutron.server.enabled', 'eq', True)
+        ext = self.get_item_when_condition_match(
+            'neutron.server.backend.extension', c)
+
+        if 'port_security' in ext:
+            return ext['port_security'].get('enabled', False)
+
+        return True
diff --git a/_modules/runtest/tempest_sections/object_storage.py b/_modules/runtest/tempest_sections/object_storage.py
new file mode 100644
index 0000000..f5fb9f1
--- /dev/null
+++ b/_modules/runtest/tempest_sections/object_storage.py
@@ -0,0 +1,54 @@
+
+import base_section
+
+class ObjectStorage(base_section.BaseSection):
+
+    name = "object-storage"
+    options = [
+        'catalog_type',
+        'cluster_name',
+        'container_sync_interval',
+        'container_sync_timeout',
+        'endpoint_type',
+        'operator_role',
+        'realm_name',
+        'region',
+        'reseller_admin_role',
+    ]
+
+
+    @property
+    def catalog_type(self):
+        pass
+
+    @property
+    def cluster_name(self):
+        pass
+
+    @property
+    def container_sync_interval(self):
+        pass
+
+    @property
+    def container_sync_timeout(self):
+        pass
+
+    @property
+    def endpoint_type(self):
+        pass
+
+    @property
+    def operator_role(self):
+        pass
+
+    @property
+    def realm_name(self):
+        pass
+
+    @property
+    def region(self):
+        pass
+
+    @property
+    def reseller_admin_role(self):
+        pass
diff --git a/_modules/runtest/tempest_sections/object_storage_feature_enabled.py b/_modules/runtest/tempest_sections/object_storage_feature_enabled.py
new file mode 100644
index 0000000..879ea43
--- /dev/null
+++ b/_modules/runtest/tempest_sections/object_storage_feature_enabled.py
@@ -0,0 +1,29 @@
+
+import base_section
+
+class ObjectStorageFeatureEnabled(base_section.BaseSection):
+
+    name = "object-storage-feature-enabled"
+    options = [
+        'container_sync',
+        'discoverability',
+        'discoverable_apis',
+        'object_versioning',
+    ]
+
+
+    @property
+    def container_sync(self):
+        pass
+
+    @property
+    def discoverability(self):
+        pass
+
+    @property
+    def discoverable_apis(self):
+        pass
+
+    @property
+    def object_versioning(self):
+        pass
diff --git a/_modules/runtest/tempest_sections/orchestration.py b/_modules/runtest/tempest_sections/orchestration.py
new file mode 100644
index 0000000..7bd3374
--- /dev/null
+++ b/_modules/runtest/tempest_sections/orchestration.py
@@ -0,0 +1,59 @@
+
+import base_section
+
+class Orchestration(base_section.BaseSection):
+
+    name = "orchestration"
+    options = [
+        'build_interval',
+        'build_timeout',
+        'catalog_type',
+        'endpoint_type',
+        'instance_type',
+        'keypair_name',
+        'max_resources_per_stack',
+        'max_template_size',
+        'region',
+        'stack_owner_role',
+    ]
+
+
+    @property
+    def build_interval(self):
+        pass
+
+    @property
+    def build_timeout(self):
+        pass
+
+    @property
+    def catalog_type(self):
+        pass
+
+    @property
+    def endpoint_type(self):
+        pass
+
+    @property
+    def instance_type(self):
+        pass
+
+    @property
+    def keypair_name(self):
+        pass
+
+    @property
+    def max_resources_per_stack(self):
+        pass
+
+    @property
+    def max_template_size(self):
+        pass
+
+    @property
+    def region(self):
+        pass
+
+    @property
+    def stack_owner_role(self):
+        pass
diff --git a/_modules/runtest/tempest_sections/oslo_concurrency.py b/_modules/runtest/tempest_sections/oslo_concurrency.py
new file mode 100644
index 0000000..c69bd04
--- /dev/null
+++ b/_modules/runtest/tempest_sections/oslo_concurrency.py
@@ -0,0 +1,19 @@
+
+import base_section
+
+class OsloConcurrency(base_section.BaseSection):
+
+    name = "oslo_concurrency"
+    options = [
+        'disable_process_locking',
+        'lock_path',
+    ]
+
+
+    @property
+    def disable_process_locking(self):
+        pass
+
+    @property
+    def lock_path(self):
+        pass
diff --git a/_modules/runtest/tempest_sections/scenario.py b/_modules/runtest/tempest_sections/scenario.py
new file mode 100644
index 0000000..57459a3
--- /dev/null
+++ b/_modules/runtest/tempest_sections/scenario.py
@@ -0,0 +1,54 @@
+
+import base_section
+
+class Scenario(base_section.BaseSection):
+
+    name = "scenario"
+    options = [
+        'aki_img_file',
+        'ami_img_file',
+        'ari_img_file',
+        'dhcp_client',
+        'img_container_format',
+        'img_dir',
+        'img_disk_format',
+        'img_file',
+        'img_properties',
+    ]
+
+
+    @property
+    def aki_img_file(self):
+        pass
+
+    @property
+    def ami_img_file(self):
+        pass
+
+    @property
+    def ari_img_file(self):
+        pass
+
+    @property
+    def dhcp_client(self):
+        pass
+
+    @property
+    def img_container_format(self):
+        pass
+
+    @property
+    def img_dir(self):
+        pass
+
+    @property
+    def img_disk_format(self):
+        pass
+
+    @property
+    def img_file(self):
+        pass
+
+    @property
+    def img_properties(self):
+        pass
diff --git a/_modules/runtest/tempest_sections/service_available.py b/_modules/runtest/tempest_sections/service_available.py
new file mode 100644
index 0000000..e8106d4
--- /dev/null
+++ b/_modules/runtest/tempest_sections/service_available.py
@@ -0,0 +1,78 @@
+
+import base_section
+
+class ServiceAvailable(base_section.BaseSection):
+
+    name = "service_available"
+    options = [
+        'cinder',
+        'designate',
+        'glance',
+        'heat',
+        'ironic',
+        'neutron',
+        'nova',
+        'sahara',
+        'swift',
+    ]
+
+
+
+    def _is_service_enabled(self, service):
+        """Check if service is enabled in specific environment.
+
+        We assume service is enabled when API for this serivce is
+        enabled at least on one node in the cloud.
+
+        :param service:
+        :param pillars:
+        """
+
+        for node, p in self.pillar.items():
+            p_service = p.get(service)
+            if p_service:
+                p_api = (p_service.get('api') or
+                         p_service.get('controller') or
+                         p_service.get('server'))
+
+                if p_api:
+                    if p_api.get('enabled'):
+                        return True
+        return False
+
+    @property
+    def cinder(self):
+        pass
+
+    @property
+    def designate(self):
+        return self._is_service_enabled('designate')
+
+    @property
+    def glance(self):
+        return self._is_service_enabled('glance')
+
+    @property
+    def heat(self):
+        return self._is_service_enabled('heat')
+
+    @property
+    def ironic(self):
+        return self._is_service_enabled('ironic')
+
+    @property
+    def neutron(self):
+        return self._is_service_enabled('neutron')
+
+    @property
+    def nova(self):
+        return self._is_service_enabled('nova')
+
+    @property
+    def sahara(self):
+        return self._is_service_enabled('sahara')
+
+    @property
+    def swift(self):
+        return self._is_service_enabled('swift')
+
diff --git a/_modules/runtest/tempest_sections/service_clients.py b/_modules/runtest/tempest_sections/service_clients.py
new file mode 100644
index 0000000..21878f1
--- /dev/null
+++ b/_modules/runtest/tempest_sections/service_clients.py
@@ -0,0 +1,19 @@
+
+import base_section
+
+class ServiceClients(base_section.BaseSection):
+
+    name = "service-clients"
+    options = [
+        'http_timeout',
+        'proxy_url',
+    ]
+
+
+    @property
+    def http_timeout(self):
+        pass
+
+    @property
+    def proxy_url(self):
+        pass
diff --git a/_modules/runtest/tempest_sections/validation.py b/_modules/runtest/tempest_sections/validation.py
new file mode 100644
index 0000000..9adf570
--- /dev/null
+++ b/_modules/runtest/tempest_sections/validation.py
@@ -0,0 +1,89 @@
+
+import base_section
+
+class Validation(base_section.BaseSection):
+
+    name = "validation"
+    options = [
+        'auth_method',
+        'connect_method',
+        'connect_timeout',
+        'floating_ip_range',
+        'image_ssh_password',
+        'image_ssh_user',
+        'ip_version_for_ssh',
+        'network_for_ssh',
+        'ping_count',
+        'ping_size',
+        'ping_timeout',
+        'run_validation',
+        'security_group',
+        'security_group_rules',
+        'ssh_shell_prologue',
+        'ssh_timeout',
+    ]
+
+
+    @property
+    def auth_method(self):
+        pass
+
+    @property
+    def connect_method(self):
+        pass
+
+    @property
+    def connect_timeout(self):
+        pass
+
+    @property
+    def floating_ip_range(self):
+        pass
+
+    @property
+    def image_ssh_password(self):
+        pass
+
+    @property
+    def image_ssh_user(self):
+        pass
+
+    @property
+    def ip_version_for_ssh(self):
+        pass
+
+    @property
+    def network_for_ssh(self):
+        pass
+
+    @property
+    def ping_count(self):
+        pass
+
+    @property
+    def ping_size(self):
+        pass
+
+    @property
+    def ping_timeout(self):
+        pass
+
+    @property
+    def run_validation(self):
+        pass
+
+    @property
+    def security_group(self):
+        pass
+
+    @property
+    def security_group_rules(self):
+        pass
+
+    @property
+    def ssh_shell_prologue(self):
+        pass
+
+    @property
+    def ssh_timeout(self):
+        pass
diff --git a/_modules/runtest/tempest_sections/volume.py b/_modules/runtest/tempest_sections/volume.py
new file mode 100644
index 0000000..4df5fbb
--- /dev/null
+++ b/_modules/runtest/tempest_sections/volume.py
@@ -0,0 +1,79 @@
+
+import base_section
+
+class Volume(base_section.BaseSection):
+
+    name = "volume"
+    options = [
+        'backend_names',
+        'build_interval',
+        'build_timeout',
+        'catalog_type',
+        'disk_format',
+        'endpoint_type',
+        'manage_snapshot_ref',
+        'manage_volume_ref',
+        'max_microversion',
+        'min_microversion',
+        'region',
+        'storage_protocol',
+        'vendor_name',
+        'volume_size',
+    ]
+
+
+    @property
+    def backend_names(self):
+        pass
+
+    @property
+    def build_interval(self):
+        pass
+
+    @property
+    def build_timeout(self):
+        pass
+
+    @property
+    def catalog_type(self):
+        pass
+
+    @property
+    def disk_format(self):
+        pass
+
+    @property
+    def endpoint_type(self):
+        pass
+
+    @property
+    def manage_snapshot_ref(self):
+        pass
+
+    @property
+    def manage_volume_ref(self):
+        pass
+
+    @property
+    def max_microversion(self):
+        pass
+
+    @property
+    def min_microversion(self):
+        pass
+
+    @property
+    def region(self):
+        pass
+
+    @property
+    def storage_protocol(self):
+        pass
+
+    @property
+    def vendor_name(self):
+        pass
+
+    @property
+    def volume_size(self):
+        pass
diff --git a/_modules/runtest/tempest_sections/volume_feature_enabled.py b/_modules/runtest/tempest_sections/volume_feature_enabled.py
new file mode 100644
index 0000000..22a754c
--- /dev/null
+++ b/_modules/runtest/tempest_sections/volume_feature_enabled.py
@@ -0,0 +1,64 @@
+
+import base_section
+
+class VolumeFeatureEnabled(base_section.BaseSection):
+
+    name = "volume-feature-enabled"
+    options = [
+        'api_extensions',
+        'api_v1',
+        'api_v2',
+        'api_v3',
+        'backup',
+        'clone',
+        'extend_attached_volume',
+        'manage_snapshot',
+        'manage_volume',
+        'multi_backend',
+        'snapshot',
+    ]
+
+
+    @property
+    def api_extensions(self):
+        pass
+
+    @property
+    def api_v1(self):
+        pass
+
+    @property
+    def api_v2(self):
+        pass
+
+    @property
+    def api_v3(self):
+        pass
+
+    @property
+    def backup(self):
+        pass
+
+    @property
+    def clone(self):
+        pass
+
+    @property
+    def extend_attached_volume(self):
+        pass
+
+    @property
+    def manage_snapshot(self):
+        pass
+
+    @property
+    def manage_volume(self):
+        pass
+
+    @property
+    def multi_backend(self):
+        pass
+
+    @property
+    def snapshot(self):
+        pass
diff --git a/_modules/runtest/utils/__init__.py b/_modules/runtest/utils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/_modules/runtest/utils/__init__.py
diff --git a/_modules/runtest/utils/requirements.py b/_modules/runtest/utils/requirements.py
new file mode 100644
index 0000000..b57cf5b
--- /dev/null
+++ b/_modules/runtest/utils/requirements.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+'''
+'''
+
+
+import sys
+import pkg_resources
+
+def get_installed_distributions():
+    """ Returns list of pkg Distribution objects found is sys.path
+    """
+
+    pkgs = []
+    for p in sys.path:
+      for d in pkg_resources.find_distributions(p):
+        pkgs.append(d)
+
+    return pkgs
diff --git a/_states/runtest.py b/_states/runtest.py
new file mode 100644
index 0000000..6700bf7
--- /dev/null
+++ b/_states/runtest.py
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+'''
+Management of runtest resources
+===============================
+:depends:   - jsonpath-rw Python module
+:configuration: See :py:mod:`salt.modules.runtest` for setup instructions.
+'''
+
+import importlib
+import logging
+import os
+from functools import wraps
+LOG = logging.getLogger(__name__)
+
+DEPENDENCIES = ['jsonpath_rw']
+
+def __virtual__():
+    '''
+    Only load if runtest module if dependencies are present
+    '''
+    failures = []
+    for module in DEPENDENCIES:
+        try:
+            importlib.import_module(module)
+        except ImportError:
+            failures.append(module)
+    if failures:
+        log.error("The required modules are not present %s" % ','.join(failures))
+        return False
+    return 'runtest'
+
+def _test_call(method):
+    (resource, functionality) = method.func_name.split('_')
+    if functionality == 'present':
+        functionality = 'updated'
+    else:
+        functionality = 'removed'
+
+    @wraps(method)
+    def check_for_testing(name, *args, **kwargs):
+        if __opts__.get('test', None):
+            return _no_change(name, resource, test=functionality)
+        return method(name, *args, **kwargs)
+    return check_for_testing
+
+@_test_call
+def tempestconf_present(name, regenerate=False):
+    """ Checks that file with config present."""
+
+
+    if os.path.isfile(name) and not regenerate:
+      	return _no_change(name, 'tempest_config')
+
+    cfg = __salt__['runtest.generate_tempest_config'](name)
+    return _created(name, 'tempest_config', cfg)
+
+
+def _created(name, resource, resource_definition):
+    changes_dict = {'name': name,
+                    'changes': resource_definition,
+                    'result': True,
+                    'comment': '{0} {1} created'.format(resource, name)}
+    return changes_dict
+
+def _no_change(name, resource, test=False):
+    changes_dict = {'name': name,
+                    'changes': {},
+                    'result': True}
+    if test:
+        changes_dict['comment'] = \
+            '{0} {1} will be {2}'.format(resource, name, test)
+    else:
+        changes_dict['comment'] = \
+            '{0} {1} is in correct state'.format(resource, name)
+    return changes_dict
diff --git a/metadata.yml b/metadata.yml
new file mode 100644
index 0000000..63db799
--- /dev/null
+++ b/metadata.yml
@@ -0,0 +1,2 @@
+name: "runtest"
+version: "0.1"
diff --git a/metadata/service/tempest.yml b/metadata/service/tempest.yml
new file mode 100644
index 0000000..36ab4ec
--- /dev/null
+++ b/metadata/service/tempest.yml
@@ -0,0 +1,8 @@
+applications:
+ - runtest
+parameters:
+  runtest:
+    enabled: true
+    openstack_version: stable/${_param:openstack_version}
+    tempest:
+      enabled: true
diff --git a/runtest/init.sls b/runtest/init.sls
new file mode 100644
index 0000000..765dc6f
--- /dev/null
+++ b/runtest/init.sls
@@ -0,0 +1,7 @@
+{%- if pillar.runtest is defined %}
+include:
+- runtest.service
+{%- if pillar.runtest.tempest is defined %}
+- runtest.tempest
+{%- endif %}
+{%- endif %}
diff --git a/runtest/map.jinja b/runtest/map.jinja
new file mode 100644
index 0000000..3d1db39
--- /dev/null
+++ b/runtest/map.jinja
@@ -0,0 +1,12 @@
+{% set runtest = salt['grains.filter_by']({
+    'default': {
+        'pkgs':['python-jsonpath-rw']
+    }
+}, grain='os', merge=salt['pillar.get']('runtest'), base='default') %}
+
+{% set tempest = salt['grains.filter_by']({
+    'default': {
+        'cfg_dir': '/root/',
+        'cfg_name': 'tempest.conf',
+    }
+}, grain='os', merge=salt['pillar.get']('runtest', {}).get('tempest', {}), base='default') %}
diff --git a/runtest/service.sls b/runtest/service.sls
new file mode 100644
index 0000000..9b18e23
--- /dev/null
+++ b/runtest/service.sls
@@ -0,0 +1,8 @@
+{% from "runtest/map.jinja" import runtest with context %}
+{%- if runtest.get('enabled', False) -%}
+
+runtest_pkgs:
+  pkg.installed:
+    - names: {{ runtest.pkgs }}
+
+{%- endif -%}
diff --git a/runtest/tempest.sls b/runtest/tempest.sls
new file mode 100644
index 0000000..06024ac
--- /dev/null
+++ b/runtest/tempest.sls
@@ -0,0 +1,16 @@
+{%- from "runtest/map.jinja" import tempest with context %}
+{%- if tempest.get('enabled', False) -%}
+
+tempest_config_dir:
+  file.directory:
+  - name: {{ tempest.cfg_dir }}
+  - makedirs: true
+  - mode: 755
+
+tempest_config_file:
+  runtest.tempestconf_present:
+    - name: {{ tempest.cfg_dir }}/{{ tempest.cfg_name }}
+    - require:
+      - file: tempest_config_dir
+
+{%- endif -%}
diff --git a/tests/pillar/service_single.sls b/tests/pillar/service_single.sls
new file mode 100644
index 0000000..299ecab
--- /dev/null
+++ b/tests/pillar/service_single.sls
@@ -0,0 +1,4 @@
+parameters:
+  runtest:
+    enabled: true
+    openstack_version: stable/${_param:openstack_version}
diff --git a/tests/pillar/tempest_single.sls b/tests/pillar/tempest_single.sls
new file mode 100644
index 0000000..f94de5d
--- /dev/null
+++ b/tests/pillar/tempest_single.sls
@@ -0,0 +1,5 @@
+parameters:
+  runtest:
+    enabled: true
+    tempest:
+      enabled: true
diff --git a/tests/run_tests.sh b/tests/run_tests.sh
new file mode 100755
index 0000000..b5524c4
--- /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}/..
+  - /usr/share/salt-formulas/env
+pillar_roots:
+  base:
+  - ${SALT_PILLAR_DIR}
+  - ${PILLARDIR}
+EOF
+}
+
+fetch_dependency() {
+    dep_name="$(echo $1|cut -d : -f 1)"
+    dep_source="$(echo $1|cut -d : -f 2-)"
+    dep_root="${DEPSDIR}/$(basename $dep_source .git)"
+    dep_metadata="${dep_root}/metadata.yml"
+
+    [ -d /usr/share/salt-formulas/env/${dep_name} ] && log_info "Dependency $dep_name already present in system-wide salt env" && return 0
+    [ -d $dep_root ] && log_info "Dependency $dep_name already fetched" && return 0
+
+    log_info "Fetching dependency $dep_name"
+    [ ! -d ${DEPSDIR} ] && mkdir -p ${DEPSDIR}
+    git clone $dep_source ${DEPSDIR}/$(basename $dep_source .git)
+    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 '%s:%s' % (dep["name"], dep["source"])
+EOF
+}
+
+clean() {
+    log_info "Cleaning up ${BUILDDIR}"
+    [ -d ${BUILDDIR} ] && rm -rf ${BUILDDIR} || exit 0
+}
+
+salt_run() {
+    [ -e ${VEN_DIR}/bin/activate ] && source ${VENV_DIR}/bin/activate
+    salt-call ${SALT_OPTS} $*
+}
+
+prepare() {
+    [ -d ${BUILDDIR} ] && mkdir -p ${BUILDDIR}
+
+    which salt-call || 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