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