diff --git a/.travis.yml b/.travis.yml index e2ccc901fa..60617b9dc1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -135,7 +135,7 @@ script: # as long as PR builds - if [ "${TRAVIS_PULL_REQUEST}" = "false" ] && [ "${IS_NIGHTLY_BUILD}" = "no" ]; then COMMAND_THRESHOLD=$(expr ${COMMAND_THRESHOLD} \* 2); fi; ./scripts/travis/time-command.sh "make ${TASK}" ${COMMAND_THRESHOLD} # Run any additional nightly checks only as part of a nightly (cron) build - - if [ "${IS_NIGHTLY_BUILD}" = "yes" ] && [ "${TASK}" = "ci-checks ci-packs-tests" ]; then make ci-checks-nightly; fi + - if [ "${IS_NIGHTLY_BUILD}" = "yes" ]; then ./scripts/travis/run-nightly-make-task-if-exists.sh "${TASK}"; fi # NOTE: We only generate and submit coverage report for master and version branches # NOTE: We put this here and not after_success so build is marked as failed if this step fails # See https://docs.travis-ci.com/user/customizing-the-build/#breaking-the-build @@ -147,3 +147,13 @@ script: # Alternative: use strict pip pinning, including git-based pip packages before_cache: - if [ ${TRAVIS_PULL_REQUEST} = 'false' ] && [ "${IS_NIGHTLY_BUILD}" = "no" ]; then rm -rf virtualenv/; fi + +# We want to be notified when a master or nightly build fails +notifications: + # Post build failures to '#stackstorm' channel in 'stackstorm' Slack + slack: + rooms: + - secure: "rPA22aDgvNe0/S/2e+cp1rSDdDUPufLXnCbfnRzMPVDSQ2UPdLmEl9IeOoEHZmq92AZtzY8UnQaPFuoM0HAPrYDgKopn4n4KpOo+xUlJ92qdNj5qk3Z1TmQHwUYFvCkMvaR/CpX2liRr/YB3qM+1vFAMsYgmqrBX8vcEqNJQy/M=" + on_pull_requests: false + on_success: change # default: always + on_failure: always # default: always diff --git a/CHANGELOG.rst b/CHANGELOG.rst index adb08ce0b3..4b6e5ec92e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,13 @@ Changelog in development -------------- +Added +~~~~~ + +* Add support for blacklisting / whitelisting hosts to the HTTP runner by adding new + ``url_hosts_blacklist`` and ``url_hosts_whitelist`` runner attribute. (new feature) + #4757 + Changed ~~~~~~~ @@ -13,7 +20,10 @@ Changed Fixed ~~~~~ - +* Fix rbac with execution view where the rbac is unable to verify the pack or uid of the execution + because it was not returned from the action execution db. This would result in an internal server + error when trying to view the results of a single execution. + Contributed by Joshua Meyer (@jdmeyer3) #4758 * Fixed logging middleware to output a ``content_length`` of ``0`` instead of ``Infinity`` when the type of data being returned is not supported. Previously, when the value was set to ``Infinity`` this would result in invalid JSON being output into structured diff --git a/Makefile b/Makefile index b7f4a3780a..bc53018418 100644 --- a/Makefile +++ b/Makefile @@ -25,6 +25,7 @@ COMPONENTS_RUNNERS := $(wildcard contrib/runners/*) COMPONENTS_WITHOUT_ST2TESTS := $(shell ls -a | grep ^st2 | grep -v .egg-info | grep -v st2tests | grep -v st2exporter) COMPONENTS_WITH_RUNNERS := $(COMPONENTS) $(COMPONENTS_RUNNERS) +COMPONENTS_WITH_RUNNERS_WITHOUT_MISTRAL_RUNNER := $(foreach component,$(filter-out contrib/runners/mistral_v2,$(COMPONENTS_WITH_RUNNERS)),$(component)) COMPONENTS_TEST_DIRS := $(wildcard st2*/tests) $(wildcard contrib/runners/*/tests) @@ -42,6 +43,7 @@ space_char := space_char += COMPONENT_PYTHONPATH = $(subst $(space_char),:,$(realpath $(COMPONENTS_WITH_RUNNERS))) COMPONENTS_TEST := $(foreach component,$(filter-out $(COMPONENT_SPECIFIC_TESTS),$(COMPONENTS_WITH_RUNNERS)),$(component)) +COMPONENTS_TEST_WITHOUT_MISTRAL_RUNNER := $(foreach component,$(filter-out $(COMPONENT_SPECIFIC_TESTS),$(COMPONENTS_WITH_RUNNERS_WITHOUT_MISTRAL_RUNNER)),$(component)) COMPONENTS_TEST_COMMA := $(subst $(slash),$(dot),$(subst $(space_char),$(comma),$(COMPONENTS_TEST))) COMPONENTS_TEST_MODULES := $(subst $(slash),$(dot),$(COMPONENTS_TEST_DIRS)) COMPONENTS_TEST_MODULES_COMMA := $(subst $(space_char),$(comma),$(COMPONENTS_TEST_MODULES)) @@ -109,6 +111,8 @@ play: @echo @echo COMPONENTS_WITH_RUNNERS=$(COMPONENTS_WITH_RUNNERS) @echo + @echo COMPONENTS_WITH_RUNNERS_WITHOUT_MISTRAL_RUNNER=$(COMPONENTS_WITH_RUNNERS_WITHOUT_MISTRAL_RUNNER) + @echo @echo COMPONENTS_TEST=$(COMPONENTS_TEST) @echo @echo COMPONENTS_TEST_COMMA=$(COMPONENTS_TEST_COMMA) @@ -119,6 +123,8 @@ play: @echo @echo COMPONENTS_TEST_MODULES_COMMA=$(COMPONENTS_TEST_MODULES_COMMA) @echo + @echo COMPONENTS_TEST_WITHOUT_MISTRAL_RUNNER=$(COMPONENTS_TEST_WITHOUT_MISTRAL_RUNNER) + @echo @echo COMPONENT_PYTHONPATH=$(COMPONENT_PYTHONPATH) @echo @echo TRAVIS_PULL_REQUEST=$(TRAVIS_PULL_REQUEST) @@ -573,7 +579,7 @@ endif @echo @echo "----- Dropping st2-test db -----" @mongo st2-test --eval "db.dropDatabase();" - for component in $(COMPONENTS_TEST); do\ + for component in $(COMPONENTS_TEST_WITHOUT_MISTRAL_RUNNER); do\ echo "==========================================================="; \ echo "Running tests in" $$component; \ echo "-----------------------------------------------------------"; \ @@ -939,6 +945,13 @@ ci-py3-unit: NOSE_WITH_TIMER=$(NOSE_WITH_TIMER) tox -e py36-unit -vv NOSE_WITH_TIMER=$(NOSE_WITH_TIMER) tox -e py36-packs -vv +.PHONY: ci-py3-unit-nightly +ci-py3-unit-nightly: + @echo + @echo "==================== ci-py3-unit ====================" + @echo + NOSE_WITH_TIMER=$(NOSE_WITH_TIMER) tox -e py36-unit-nightly -vv + .PHONY: ci-py3-integration ci-py3-integration: requirements .ci-prepare-integration .ci-py3-integration @@ -977,6 +990,15 @@ ci-py3-integration: requirements .ci-prepare-integration .ci-py3-integration .PHONY: ci-unit ci-unit: .unit-tests-coverage-html +.PHONY: ci-unit-nightly +ci-unit-nightly: + # NOTE: We run mistral runner checks only as part of a nightly build to speed up + # non nightly builds (Mistral will be deprecated in the future) + @echo + @echo "============== ci-unit-nightly ==============" + @echo + nosetests $(NOSE_OPTS) -s -v contrib/runners/mistral_v2/tests/unit + .PHONY: .ci-prepare-integration .ci-prepare-integration: sudo -E ./scripts/travis/prepare-integration.sh diff --git a/contrib/runners/http_runner/http_runner/http_runner.py b/contrib/runners/http_runner/http_runner/http_runner.py index 6915840814..af5edbdf11 100644 --- a/contrib/runners/http_runner/http_runner/http_runner.py +++ b/contrib/runners/http_runner/http_runner/http_runner.py @@ -13,6 +13,7 @@ # limitations under the License. from __future__ import absolute_import + import ast import copy import json @@ -21,6 +22,7 @@ import requests from requests.auth import HTTPBasicAuth from oslo_config import cfg +from six.moves.urllib import parse as urlparse # pylint: disable=import-error from st2common.runners.base import ActionRunner from st2common.runners.base import get_metadata as get_runner_metadata @@ -55,6 +57,8 @@ RUNNER_VERIFY_SSL_CERT = 'verify_ssl_cert' RUNNER_USERNAME = 'username' RUNNER_PASSWORD = 'password' +RUNNER_URL_HOSTS_BLACKLIST = 'url_hosts_blacklist' +RUNNER_URL_HOSTS_WHITELIST = 'url_hosts_whitelist' # Lookup constants for action params ACTION_AUTH = 'auth' @@ -93,10 +97,17 @@ def pre_run(self): self._http_proxy = self.runner_parameters.get(RUNNER_HTTP_PROXY, None) self._https_proxy = self.runner_parameters.get(RUNNER_HTTPS_PROXY, None) self._verify_ssl_cert = self.runner_parameters.get(RUNNER_VERIFY_SSL_CERT, None) + self._url_hosts_blacklist = self.runner_parameters.get(RUNNER_URL_HOSTS_BLACKLIST, []) + self._url_hosts_whitelist = self.runner_parameters.get(RUNNER_URL_HOSTS_WHITELIST, []) def run(self, action_parameters): client = self._get_http_client(action_parameters) + if self._url_hosts_blacklist and self._url_hosts_whitelist: + msg = ('"url_hosts_blacklist" and "url_hosts_whitelist" parameters are mutually ' + 'exclusive. Only one should be provided.') + raise ValueError(msg) + try: result = client.run() except requests.exceptions.Timeout as e: @@ -147,7 +158,9 @@ def _get_http_client(self, action_parameters): headers=headers, cookies=self._cookies, auth=auth, timeout=timeout, allow_redirects=self._allow_redirects, proxies=proxies, files=files, verify=self._verify_ssl_cert, - username=self._username, password=self._password) + username=self._username, password=self._password, + url_hosts_blacklist=self._url_hosts_blacklist, + url_hosts_whitelist=self._url_hosts_whitelist) @staticmethod def _get_result_status(status_code): @@ -158,7 +171,8 @@ def _get_result_status(status_code): class HTTPClient(object): def __init__(self, url=None, method=None, body='', params=None, headers=None, cookies=None, auth=None, timeout=60, allow_redirects=False, proxies=None, - files=None, verify=False, username=None, password=None): + files=None, verify=False, username=None, password=None, + url_hosts_blacklist=None, url_hosts_whitelist=None): if url is None: raise Exception('URL must be specified.') @@ -188,12 +202,30 @@ def __init__(self, url=None, method=None, body='', params=None, headers=None, co self.verify = verify self.username = username self.password = password + self.url_hosts_blacklist = url_hosts_blacklist or [] + self.url_hosts_whitelist = url_hosts_whitelist or [] + + if self.url_hosts_blacklist and self.url_hosts_whitelist: + msg = ('"url_hosts_blacklist" and "url_hosts_whitelist" parameters are mutually ' + 'exclusive. Only one should be provided.') + raise ValueError(msg) def run(self): results = {} resp = None json_content = self._is_json_content() + # Check if the provided URL is blacklisted + is_url_blacklisted = self._is_url_blacklisted(url=self.url) + + if is_url_blacklisted: + raise ValueError('URL "%s" is blacklisted' % (self.url)) + + is_url_whitelisted = self._is_url_whitelisted(url=self.url) + + if not is_url_whitelisted: + raise ValueError('URL "%s" is not whitelisted' % (self.url)) + try: if json_content: # cast params (body) to dict @@ -301,6 +333,49 @@ def _cast_object(self, value): else: return value + def _is_url_blacklisted(self, url): + """ + Verify if the provided URL is blacklisted via url_hosts_blacklist runner parameter. + """ + if not self.url_hosts_blacklist: + # Blacklist is empty + return False + + host = self._get_host_from_url(url=url) + + if host in self.url_hosts_blacklist: + return True + + return False + + def _is_url_whitelisted(self, url): + """ + Verify if the provided URL is whitelisted via url_hosts_whitelist runner parameter. + """ + if not self.url_hosts_whitelist: + return True + + host = self._get_host_from_url(url=url) + + if host in self.url_hosts_whitelist: + return True + + return False + + def _get_host_from_url(self, url): + """ + Return sanitized host (netloc) value from the provided url. + """ + parsed = urlparse.urlparse(url) + + # Remove port and [] + host = parsed.netloc.replace('[', '').replace(']', '') + + if parsed.port is not None: + host = host.replace(':%s' % (parsed.port), '') + + return host + def get_runner(): return HttpRunner(str(uuid.uuid4())) diff --git a/contrib/runners/http_runner/http_runner/runner.yaml b/contrib/runners/http_runner/http_runner/runner.yaml index 85f1e4c1c5..168f68bbb2 100644 --- a/contrib/runners/http_runner/http_runner/runner.yaml +++ b/contrib/runners/http_runner/http_runner/runner.yaml @@ -36,6 +36,22 @@ CA bundle which comes from Mozilla. Verification using a custom CA bundle is not yet supported. Set to False to skip verification. type: boolean + url_hosts_blacklist: + description: Optional list of hosts (network locations) to blacklist (e.g. example.com, + 127.0.0.1, ::1, etc.). If action will try to access that endpoint, an exception will be + thrown and action will be marked as failed. + required: false + type: array + items: + type: string + url_hosts_whitelist: + description: Optional list of hosts (network locations) to whitelist (e.g. example.com, + 127.0.0.1, ::1, etc.). If specified, actions will only be able to hit hosts on this + whitelist. + required: false + type: array + items: + type: string output_key: body output_schema: status_code: diff --git a/contrib/runners/http_runner/tests/unit/test_http_runner.py b/contrib/runners/http_runner/tests/unit/test_http_runner.py index 7f02d987d1..fa12ce27a6 100644 --- a/contrib/runners/http_runner/tests/unit/test_http_runner.py +++ b/contrib/runners/http_runner/tests/unit/test_http_runner.py @@ -15,13 +15,23 @@ from __future__ import absolute_import +import re + import six import mock import unittest2 +from st2common.constants.action import LIVEACTION_STATUS_SUCCEEDED from http_runner.http_runner import HTTPClient +from http_runner.http_runner import HttpRunner + import st2tests.config as tests_config +__all__ = [ + 'HTTPClientTestCase', + 'HTTPRunnerTestCase' +] + if six.PY2: EXPECTED_DATA = '' @@ -33,7 +43,7 @@ class MockResult(object): close = mock.Mock() -class HTTPRunnerTestCase(unittest2.TestCase): +class HTTPClientTestCase(unittest2.TestCase): @classmethod def setUpClass(cls): tests_config.parse_args() @@ -212,3 +222,163 @@ def test_http_unicode_body_data(self, mock_requests): expected_data = body self.assertEqual(call_kwargs['data'], expected_data) + + @mock.patch('http_runner.http_runner.requests') + def test_blacklisted_url_url_hosts_blacklist_runner_parameter(self, mock_requests): + # Black list is empty + self.assertEqual(mock_requests.request.call_count, 0) + + url = 'http://www.example.com' + client = HTTPClient(url=url, method='GET') + client.run() + + self.assertEqual(mock_requests.request.call_count, 1) + + # Blacklist is set + url_hosts_blacklist = [ + 'example.com', + '127.0.0.1', + '::1', + '2001:0db8:85a3:0000:0000:8a2e:0370:7334' + ] + + # Blacklisted urls + urls = [ + 'https://example.com', + 'http://example.com', + 'http://example.com:81', + 'http://example.com:80', + 'http://example.com:9000', + 'http://[::1]:80/', + 'http://[::1]', + 'http://[::1]:9000', + 'http://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]', + 'https://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:8000' + ] + + for url in urls: + expected_msg = r'URL "%s" is blacklisted' % (re.escape(url)) + client = HTTPClient(url=url, method='GET', url_hosts_blacklist=url_hosts_blacklist) + self.assertRaisesRegexp(ValueError, expected_msg, client.run) + + # Non blacklisted URLs + urls = [ + 'https://example2.com', + 'http://example3.com', + 'http://example4.com:81' + ] + + for url in urls: + mock_requests.request.reset_mock() + + self.assertEqual(mock_requests.request.call_count, 0) + + client = HTTPClient(url=url, method='GET', url_hosts_blacklist=url_hosts_blacklist) + client.run() + + self.assertEqual(mock_requests.request.call_count, 1) + + @mock.patch('http_runner.http_runner.requests') + def test_whitelisted_url_url_hosts_whitelist_runner_parameter(self, mock_requests): + # Whitelist is empty + self.assertEqual(mock_requests.request.call_count, 0) + + url = 'http://www.example.com' + client = HTTPClient(url=url, method='GET') + client.run() + + self.assertEqual(mock_requests.request.call_count, 1) + + # Whitelist is set + url_hosts_whitelist = [ + 'example.com', + '127.0.0.1', + '::1', + '2001:0db8:85a3:0000:0000:8a2e:0370:7334' + ] + + # Non whitelisted urls + urls = [ + 'https://www.google.com', + 'https://www.example2.com', + 'http://127.0.0.2' + ] + + for url in urls: + expected_msg = r'URL "%s" is not whitelisted' % (re.escape(url)) + client = HTTPClient(url=url, method='GET', url_hosts_whitelist=url_hosts_whitelist) + self.assertRaisesRegexp(ValueError, expected_msg, client.run) + + # Whitelisted URLS + urls = [ + 'https://example.com', + 'http://example.com', + 'http://example.com:81', + 'http://example.com:80', + 'http://example.com:9000', + 'http://[::1]:80/', + 'http://[::1]', + 'http://[::1]:9000', + 'http://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]', + 'https://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:8000' + ] + + for url in urls: + mock_requests.request.reset_mock() + + self.assertEqual(mock_requests.request.call_count, 0) + + client = HTTPClient(url=url, method='GET', url_hosts_whitelist=url_hosts_whitelist) + client.run() + + self.assertEqual(mock_requests.request.call_count, 1) + + def test_url_host_blacklist_and_url_host_blacklist_params_are_mutually_exclusive(self): + url = 'http://www.example.com' + + expected_msg = (r'"url_hosts_blacklist" and "url_hosts_whitelist" parameters are mutually ' + 'exclusive.') + self.assertRaisesRegexp(ValueError, expected_msg, HTTPClient, url=url, method='GET', + url_hosts_blacklist=[url], url_hosts_whitelist=[url]) + + +class HTTPRunnerTestCase(unittest2.TestCase): + @mock.patch('http_runner.http_runner.requests') + def test_get_success(self, mock_requests): + mock_result = MockResult() + + # Unknown content type, body should be returned raw + mock_result.text = 'foo bar ponies' + mock_result.headers = {'Content-Type': 'text/html'} + mock_result.status_code = 200 + + mock_requests.request.return_value = mock_result + + runner_parameters = { + 'url': 'http://www.example.com', + 'method': 'GET' + } + runner = HttpRunner('id') + runner.runner_parameters = runner_parameters + runner.pre_run() + + status, result, _ = runner.run({}) + self.assertEqual(status, LIVEACTION_STATUS_SUCCEEDED) + self.assertEqual(result['body'], 'foo bar ponies') + self.assertEqual(result['status_code'], 200) + self.assertEqual(result['parsed'], False) + + def test_url_host_blacklist_and_url_host_blacklist_params_are_mutually_exclusive(self): + runner_parameters = { + 'url': 'http://www.example.com', + 'method': 'GET', + 'url_hosts_blacklist': ['http://127.0.0.1'], + 'url_hosts_whitelist': ['http://127.0.0.1'], + } + runner = HttpRunner('id') + runner.runner_parameters = runner_parameters + runner.pre_run() + + expected_msg = (r'"url_hosts_blacklist" and "url_hosts_whitelist" parameters are mutually ' + 'exclusive.') + self.assertRaisesRegexp(ValueError, expected_msg, runner.run, {}) diff --git a/scripts/travis/run-nightly-make-task-if-exists.sh b/scripts/travis/run-nightly-make-task-if-exists.sh new file mode 100755 index 0000000000..35e0ad9f21 --- /dev/null +++ b/scripts/travis/run-nightly-make-task-if-exists.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +# Script which runs a corresponding make nightly tasks if it exists. If a task corresponding +# nightly task doesn't exist, it's ignored. +# +# For example, let's say we have TASK="ci-checks ci-unit ci-pack-tests" and only +# "ci-checks-nightly" make task exists. +# In this scenario, only "ci-check-nightly" tasks would run and other would be ignored. + +TASK=$1 + +if [ ! "${TASK}" ]; then + echo "Missing TASK argument" + echo "Usage: $0 " + exit 2 +fi + +# Note: TASK could contain a list of multiple tasks +TASKS=($TASK) + +EXISTING_TASKS=() +for TASK_NAME in ${TASKS[@]}; do + $(make -n ${TASK_NAME}-nightly &> /dev/null) + + if [ $? -eq 0 ]; then + # Task {TASK}-nightly exists + EXISTING_TASKS+=("$TASK_NAME-nightly") + fi +done + +# Run only tasks which exist +if [ ${#EXISTING_TASKS[@]} -eq 0 ]; then + echo "No existing nightly tasks found..." + exit 0 +fi + +echo "Running the following nightly tasks: ${EXISTING_TASKS[@]}" +exec make ${EXISTING_TASKS[@]} diff --git a/st2api/st2api/controllers/v1/actionexecutions.py b/st2api/st2api/controllers/v1/actionexecutions.py index 5e67417302..1ba3a3031e 100644 --- a/st2api/st2api/controllers/v1/actionexecutions.py +++ b/st2api/st2api/controllers/v1/actionexecutions.py @@ -83,7 +83,14 @@ class ActionExecutionsControllerMixin(BaseRestControllerMixin): mandatory_include_fields_retrieve = [ 'action.parameters', 'runner.runner_parameters', - 'parameters' + 'parameters', + + # Attributes below are mandatory for RBAC installations + 'action.pack', + 'action.uid', + + # Required when rbac.permission_isolation is enabled + 'context' ] # A list of attributes which can be specified using ?exclude_attributes filter diff --git a/st2tests/st2tests/api.py b/st2tests/st2tests/api.py index 404816b964..fc5236d421 100644 --- a/st2tests/st2tests/api.py +++ b/st2tests/st2tests/api.py @@ -180,7 +180,13 @@ class APIControllerWithIncludeAndExcludeFilterTestCase(object): # _get_model_instance method method test_exact_object_count = True + # True if those tests are running with rbac enabled + rbac_enabled = False + def test_get_all_exclude_attributes_and_include_attributes_are_mutually_exclusive(self): + if self.rbac_enabled: + self.use_user(self.users['admin']) + url = self.get_all_path + '?include_attributes=id&exclude_attributes=id' resp = self.app.get(url, expect_errors=True) self.assertEqual(resp.status_int, 400) @@ -189,6 +195,9 @@ def test_get_all_exclude_attributes_and_include_attributes_are_mutually_exclusiv self.assertRegexpMatches(resp.json['faultstring'], expected_msg) def test_get_all_invalid_exclude_and_include_parameter(self): + if self.rbac_enabled: + self.use_user(self.users['admin']) + # 1. Invalid exclude_attributes field url = self.get_all_path + '?exclude_attributes=invalid_field' resp = self.app.get(url, expect_errors=True) @@ -206,6 +215,9 @@ def test_get_all_invalid_exclude_and_include_parameter(self): self.assertRegexpMatches(resp.json['faultstring'], expected_msg) def test_get_all_include_attributes_filter(self): + if self.rbac_enabled: + self.use_user(self.users['admin']) + mandatory_include_fields = self.controller_cls.mandatory_include_fields_response # Create any resources needed by those tests (if not already created inside setUp / @@ -249,6 +261,9 @@ def test_get_all_include_attributes_filter(self): self._delete_mock_models(object_ids) def test_get_all_exclude_attributes_filter(self): + if self.rbac_enabled: + self.use_user(self.users['admin']) + # Create any resources needed by those tests (if not already created inside setUp / # setUpClass) object_ids = self._insert_mock_models() diff --git a/tox.ini b/tox.ini index fb193b3449..3832b40c02 100644 --- a/tox.ini +++ b/tox.ini @@ -46,12 +46,23 @@ commands = nosetests --rednose --immediate -sv contrib/runners/http_runner/tests/unit/ nosetests --rednose --immediate -sv contrib/runners/noop_runner/tests/unit/ nosetests --rednose --immediate -sv contrib/runners/local_runner/tests/unit/ - nosetests --rednose --immediate -sv contrib/runners/mistral_v2/tests/unit/ nosetests --rednose --immediate -sv contrib/runners/orquesta_runner/tests/unit/ nosetests --rednose --immediate -sv contrib/runners/python_runner/tests/unit/ nosetests --rednose --immediate -sv contrib/runners/winrm_runner/tests/unit/ -# Python 3 tasks +[testenv:py36-unit-nightly] +basepython = python3.6 +setenv = PYTHONPATH = {toxinidir}/external:{toxinidir}/st2common:{toxinidir}/st2api:{toxinidir}/st2actions:{toxinidir}/st2exporter:{toxinidir}/st2reactor:{toxinidir}/st2tests:{toxinidir}/contrib/runners/action_chain_runner:{toxinidir}/contrib/runners/local_runner:{toxinidir}/contrib/runners/python_runner:{toxinidir}/contrib/runners/http_runner:{toxinidir}/contrib/runners/noop_runner:{toxinidir}/contrib/runners/announcement_runner:{toxinidir}/contrib/runners/remote_runner:{toxinidir}/contrib/runners/remote_runner:{toxinidir}/contrib/runners/mistral_v2:{toxinidir}/contrib/runners/orquesta_runner:{toxinidir}/contrib/runners/inquirer_runner:{toxinidir}/contrib/runners/http_runner:{toxinidir}/contrib/runners/winrm_runner + VIRTUALENV_DIR = {envdir} +passenv = NOSE_WITH_TIMER TRAVIS +install_command = pip install -U --force-reinstall {opts} {packages} +deps = virtualenv + -r{toxinidir}/requirements.txt + -e{toxinidir}/st2client + -e{toxinidir}/st2common +commands = + nosetests --rednose --immediate -sv contrib/runners/mistral_v2/tests/unit/ + [testenv:py36-packs] basepython = python3.6 setenv = PYTHONPATH = {toxinidir}/external:{toxinidir}/st2common:{toxinidir}/st2api:{toxinidir}/st2actions:{toxinidir}/st2exporter:{toxinidir}/st2reactor:{toxinidir}/st2tests:{toxinidir}/contrib/runners/action_chain_runner:{toxinidir}/contrib/runners/local_runner:{toxinidir}/contrib/runners/python_runner:{toxinidir}/contrib/runners/http_runner:{toxinidir}/contrib/runners/noop_runner:{toxinidir}/contrib/runners/announcement_runner:{toxinidir}/contrib/runners/remote_runner:{toxinidir}/contrib/runners/remote_runner:{toxinidir}/contrib/runners/mistral_v2:{toxinidir}/contrib/runners/orquesta_runner:{toxinidir}/contrib/runners/inquirer_runner:{toxinidir}/contrib/runners/http_runner:{toxinidir}/contrib/runners/winrm_runner