mirror of
https://github.com/ansible/awx.git
synced 2026-02-24 22:46:01 -03:30
Merge remote-tracking branch 'origin/release_3.1.2' into devel
This commit is contained in:
82
Makefile
82
Makefile
@@ -176,11 +176,11 @@ UI_RELEASE_FLAG_FILE = awx/ui/.release_built
|
|||||||
|
|
||||||
.PHONY: clean clean-tmp clean-venv rebase push requirements requirements_dev \
|
.PHONY: clean clean-tmp clean-venv rebase push requirements requirements_dev \
|
||||||
develop refresh adduser migrate dbchange dbshell runserver celeryd \
|
develop refresh adduser migrate dbchange dbshell runserver celeryd \
|
||||||
receiver test test_unit test_coverage coverage_html test_jenkins dev_build \
|
receiver test test_unit test_ansible test_coverage coverage_html \
|
||||||
release_build release_clean sdist rpmtar mock-rpm mock-srpm rpm-sign \
|
test_jenkins dev_build release_build release_clean sdist rpmtar mock-rpm \
|
||||||
deb deb-src debian debsign pbuilder reprepro setup_tarball \
|
mock-srpm rpm-sign deb deb-src debian debsign pbuilder \
|
||||||
virtualbox-ovf virtualbox-centos-7 virtualbox-centos-6 \
|
reprepro setup_tarball virtualbox-ovf virtualbox-centos-7 \
|
||||||
clean-bundle setup_bundle_tarball \
|
virtualbox-centos-6 clean-bundle setup_bundle_tarball \
|
||||||
ui-docker-machine ui-docker ui-release ui-devel \
|
ui-docker-machine ui-docker ui-release ui-devel \
|
||||||
ui-test ui-deps ui-test-ci ui-test-saucelabs jlaska
|
ui-test ui-deps ui-test-ci ui-test-saucelabs jlaska
|
||||||
|
|
||||||
@@ -264,8 +264,8 @@ virtualenv_ansible:
|
|||||||
fi; \
|
fi; \
|
||||||
if [ ! -d "$(VENV_BASE)/ansible" ]; then \
|
if [ ! -d "$(VENV_BASE)/ansible" ]; then \
|
||||||
virtualenv --system-site-packages --setuptools $(VENV_BASE)/ansible && \
|
virtualenv --system-site-packages --setuptools $(VENV_BASE)/ansible && \
|
||||||
$(VENV_BASE)/ansible/bin/pip install -I setuptools==23.0.0 && \
|
$(VENV_BASE)/ansible/bin/pip install $(PIP_OPTIONS) --ignore-installed setuptools==23.0.0 && \
|
||||||
$(VENV_BASE)/ansible/bin/pip install -I pip==8.1.2; \
|
$(VENV_BASE)/ansible/bin/pip install $(PIP_OPTIONS) --ignore-installed pip==8.1.2; \
|
||||||
fi; \
|
fi; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -276,42 +276,32 @@ virtualenv_tower:
|
|||||||
fi; \
|
fi; \
|
||||||
if [ ! -d "$(VENV_BASE)/tower" ]; then \
|
if [ ! -d "$(VENV_BASE)/tower" ]; then \
|
||||||
virtualenv --system-site-packages --setuptools $(VENV_BASE)/tower && \
|
virtualenv --system-site-packages --setuptools $(VENV_BASE)/tower && \
|
||||||
$(VENV_BASE)/tower/bin/pip install -I setuptools==23.0.0 && \
|
$(VENV_BASE)/tower/bin/pip install $(PIP_OPTIONS) --ignore-installed setuptools==23.0.0 && \
|
||||||
$(VENV_BASE)/tower/bin/pip install -I pip==8.1.2; \
|
$(VENV_BASE)/tower/bin/pip install $(PIP_OPTIONS) --ignore-installed pip==8.1.2; \
|
||||||
fi; \
|
fi; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
requirements_ansible: virtualenv_ansible
|
requirements_ansible: virtualenv_ansible
|
||||||
|
$(VENV_BASE)/ansible/bin/pip install $(PIP_OPTIONS) --ignore-installed --no-binary $(SRC_ONLY_PKGS) -r requirements/requirements_ansible.txt
|
||||||
|
$(VENV_BASE)/ansible/bin/pip uninstall --yes -r requirements/requirements_ansible_uninstall.txt
|
||||||
|
|
||||||
|
requirements_ansible_dev:
|
||||||
if [ "$(VENV_BASE)" ]; then \
|
if [ "$(VENV_BASE)" ]; then \
|
||||||
. $(VENV_BASE)/ansible/bin/activate; \
|
$(VENV_BASE)/ansible/bin/pip install pytest; \
|
||||||
$(VENV_BASE)/ansible/bin/pip install --ignore-installed --no-binary $(SRC_ONLY_PKGS) -r requirements/requirements_ansible.txt ;\
|
|
||||||
$(VENV_BASE)/ansible/bin/pip uninstall --yes -r requirements/requirements_ansible_uninstall.txt; \
|
|
||||||
else \
|
|
||||||
pip install --ignore-installed --no-binary $(SRC_ONLY_PKGS) -r requirements/requirements_ansible.txt ; \
|
|
||||||
pip uninstall --yes -r requirements/requirements_ansible_uninstall.txt; \
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Install third-party requirements needed for Tower's environment.
|
# Install third-party requirements needed for Tower's environment.
|
||||||
requirements_tower: virtualenv_tower
|
requirements_tower: virtualenv_tower
|
||||||
if [ "$(VENV_BASE)" ]; then \
|
$(VENV_BASE)/tower/bin/pip install $(PIP_OPTIONS) --ignore-installed --no-binary $(SRC_ONLY_PKGS) -r requirements/requirements.txt
|
||||||
. $(VENV_BASE)/tower/bin/activate; \
|
$(VENV_BASE)/tower/bin/pip uninstall --yes -r requirements/requirements_tower_uninstall.txt
|
||||||
$(VENV_BASE)/tower/bin/pip install --ignore-installed --no-binary $(SRC_ONLY_PKGS) -r requirements/requirements.txt ;\
|
|
||||||
$(VENV_BASE)/tower/bin/pip uninstall --yes -r requirements/requirements_tower_uninstall.txt; \
|
|
||||||
else \
|
|
||||||
pip install --ignore-installed --no-binary $(SRC_ONLY_PKGS) -r requirements/requirements.txt ; \
|
|
||||||
pip uninstall --yes -r requirements/requirements_tower_uninstall.txt; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
requirements_tower_dev:
|
requirements_tower_dev:
|
||||||
if [ "$(VENV_BASE)" ]; then \
|
$(VENV_BASE)/tower/bin/pip install -r requirements/requirements_dev.txt
|
||||||
. $(VENV_BASE)/tower/bin/activate; \
|
$(VENV_BASE)/tower/bin/pip uninstall --yes -r requirements/requirements_dev_uninstall.txt
|
||||||
$(VENV_BASE)/tower/bin/pip install -r requirements/requirements_dev.txt; \
|
|
||||||
$(VENV_BASE)/tower/bin/pip uninstall --yes -r requirements/requirements_dev_uninstall.txt; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
requirements: requirements_ansible requirements_tower
|
requirements: requirements_ansible requirements_tower
|
||||||
|
|
||||||
requirements_dev: requirements requirements_tower_dev
|
requirements_dev: requirements requirements_tower_dev requirements_ansible_dev
|
||||||
|
|
||||||
requirements_test: requirements
|
requirements_test: requirements
|
||||||
|
|
||||||
@@ -482,7 +472,7 @@ check: flake8 pep8 # pyflakes pylint
|
|||||||
|
|
||||||
TEST_DIRS ?= awx/main/tests awx/conf/tests awx/sso/tests
|
TEST_DIRS ?= awx/main/tests awx/conf/tests awx/sso/tests
|
||||||
# Run all API unit tests.
|
# Run all API unit tests.
|
||||||
test:
|
test: test_ansible
|
||||||
@if [ "$(VENV_BASE)" ]; then \
|
@if [ "$(VENV_BASE)" ]; then \
|
||||||
. $(VENV_BASE)/tower/bin/activate; \
|
. $(VENV_BASE)/tower/bin/activate; \
|
||||||
fi; \
|
fi; \
|
||||||
@@ -494,6 +484,12 @@ test_unit:
|
|||||||
fi; \
|
fi; \
|
||||||
py.test awx/main/tests/unit awx/conf/tests/unit awx/sso/tests/unit
|
py.test awx/main/tests/unit awx/conf/tests/unit awx/sso/tests/unit
|
||||||
|
|
||||||
|
test_ansible:
|
||||||
|
@if [ "$(VENV_BASE)" ]; then \
|
||||||
|
. $(VENV_BASE)/ansible/bin/activate; \
|
||||||
|
fi; \
|
||||||
|
py.test awx/lib/tests -c awx/lib/tests/pytest.ini
|
||||||
|
|
||||||
# Run all API unit tests with coverage enabled.
|
# Run all API unit tests with coverage enabled.
|
||||||
test_coverage:
|
test_coverage:
|
||||||
@if [ "$(VENV_BASE)" ]; then \
|
@if [ "$(VENV_BASE)" ]; then \
|
||||||
@@ -712,6 +708,30 @@ rpm-build/$(SDIST_TAR_FILE): rpm-build dist/$(SDIST_TAR_FILE)
|
|||||||
|
|
||||||
rpmtar: sdist rpm-build/$(SDIST_TAR_FILE)
|
rpmtar: sdist rpm-build/$(SDIST_TAR_FILE)
|
||||||
|
|
||||||
|
brewrpmtar: rpm-build/python-deps.tar.gz rpmtar
|
||||||
|
|
||||||
|
rpm-build/python-deps.tar.gz: requirements/vendor rpm-build
|
||||||
|
tar czf rpm-build/python-deps.tar.gz requirements/vendor
|
||||||
|
|
||||||
|
requirements/vendor:
|
||||||
|
pip download \
|
||||||
|
--no-binary=:all: \
|
||||||
|
--requirement=requirements/requirements_ansible.txt \
|
||||||
|
--dest=$@ \
|
||||||
|
--exists-action=i
|
||||||
|
|
||||||
|
pip download \
|
||||||
|
--no-binary=:all: \
|
||||||
|
--requirement=requirements/requirements.txt \
|
||||||
|
--dest=$@ \
|
||||||
|
--exists-action=i
|
||||||
|
|
||||||
|
pip download \
|
||||||
|
--no-binary=:all: \
|
||||||
|
--requirement=requirements/requirements_setup_requires.txt \
|
||||||
|
--dest=$@ \
|
||||||
|
--exists-action=i
|
||||||
|
|
||||||
rpm-build/$(RPM_NVR).src.rpm: /etc/mock/$(MOCK_CFG).cfg
|
rpm-build/$(RPM_NVR).src.rpm: /etc/mock/$(MOCK_CFG).cfg
|
||||||
$(MOCK_BIN) -r $(MOCK_CFG) --resultdir rpm-build --buildsrpm --spec rpm-build/$(NAME).spec --sources rpm-build \
|
$(MOCK_BIN) -r $(MOCK_CFG) --resultdir rpm-build --buildsrpm --spec rpm-build/$(NAME).spec --sources rpm-build \
|
||||||
--define "tower_version $(VERSION)" --define "tower_release $(RELEASE)" $(SCL_DEFINES)
|
--define "tower_version $(VERSION)" --define "tower_release $(RELEASE)" $(SCL_DEFINES)
|
||||||
@@ -722,6 +742,8 @@ mock-srpm: rpmtar rpm-build/$(RPM_NVR).src.rpm
|
|||||||
@echo rpm-build/$(RPM_NVR).src.rpm
|
@echo rpm-build/$(RPM_NVR).src.rpm
|
||||||
@echo "#############################################"
|
@echo "#############################################"
|
||||||
|
|
||||||
|
brew-srpm: brewrpmtar mock-srpm
|
||||||
|
|
||||||
rpm-build/$(RPM_NVR).$(RPM_ARCH).rpm: rpm-build/$(RPM_NVR).src.rpm
|
rpm-build/$(RPM_NVR).$(RPM_ARCH).rpm: rpm-build/$(RPM_NVR).src.rpm
|
||||||
$(MOCK_BIN) -r $(MOCK_CFG) --resultdir rpm-build --rebuild rpm-build/$(RPM_NVR).src.rpm \
|
$(MOCK_BIN) -r $(MOCK_CFG) --resultdir rpm-build --rebuild rpm-build/$(RPM_NVR).src.rpm \
|
||||||
--define "tower_version $(VERSION)" --define "tower_release $(RELEASE)" $(SCL_DEFINES)
|
--define "tower_version $(VERSION)" --define "tower_release $(RELEASE)" $(SCL_DEFINES)
|
||||||
|
|||||||
@@ -316,6 +316,8 @@ class OrderByBackend(BaseFilterBackend):
|
|||||||
else:
|
else:
|
||||||
order_by = (value,)
|
order_by = (value,)
|
||||||
if order_by:
|
if order_by:
|
||||||
|
order_by = self._strip_sensitive_model_fields(queryset.model, order_by)
|
||||||
|
|
||||||
# Special handling of the type field for ordering. In this
|
# Special handling of the type field for ordering. In this
|
||||||
# case, we're not sorting exactly on the type field, but
|
# case, we're not sorting exactly on the type field, but
|
||||||
# given the limited number of views with multiple types,
|
# given the limited number of views with multiple types,
|
||||||
@@ -338,3 +340,16 @@ class OrderByBackend(BaseFilterBackend):
|
|||||||
except FieldError as e:
|
except FieldError as e:
|
||||||
# Return a 400 for invalid field names.
|
# Return a 400 for invalid field names.
|
||||||
raise ParseError(*e.args)
|
raise ParseError(*e.args)
|
||||||
|
|
||||||
|
def _strip_sensitive_model_fields(self, model, order_by):
|
||||||
|
for field_name in order_by:
|
||||||
|
# strip off the negation prefix `-` if it exists
|
||||||
|
_field_name = field_name.split('-')[-1]
|
||||||
|
try:
|
||||||
|
# if the field name is encrypted/sensitive, don't sort on it
|
||||||
|
if _field_name in getattr(model, 'PASSWORD_FIELDS', ()) or \
|
||||||
|
getattr(model._meta.get_field(_field_name), '__prevent_search__', False):
|
||||||
|
raise ParseError(_('cannot order by field %s') % _field_name)
|
||||||
|
except FieldDoesNotExist:
|
||||||
|
pass
|
||||||
|
yield field_name
|
||||||
|
|||||||
@@ -2678,7 +2678,8 @@ class JobTemplateCallback(GenericAPIView):
|
|||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
extra_vars = None
|
extra_vars = None
|
||||||
if request.content_type == "application/json":
|
# Be careful here: content_type can look like '<content_type>; charset=blar'
|
||||||
|
if request.content_type.startswith("application/json"):
|
||||||
extra_vars = request.data.get("extra_vars", None)
|
extra_vars = request.data.get("extra_vars", None)
|
||||||
# Permission class should have already validated host_config_key.
|
# Permission class should have already validated host_config_key.
|
||||||
job_template = self.get_object()
|
job_template = self.get_object()
|
||||||
@@ -2727,14 +2728,14 @@ class JobTemplateCallback(GenericAPIView):
|
|||||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
# Everything is fine; actually create the job.
|
# Everything is fine; actually create the job.
|
||||||
|
kv = {"limit": limit, "launch_type": 'callback'}
|
||||||
|
if extra_vars is not None and job_template.ask_variables_on_launch:
|
||||||
|
kv['extra_vars'] = callback_filter_out_ansible_extra_vars(extra_vars)
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
job = job_template.create_job(limit=limit, launch_type='callback')
|
job = job_template.create_job(**kv)
|
||||||
|
|
||||||
# Send a signal to celery that the job should be started.
|
# Send a signal to celery that the job should be started.
|
||||||
kv = {"inventory_sources_already_updated": inventory_sources_already_updated}
|
result = job.signal_start(inventory_sources_already_updated=inventory_sources_already_updated)
|
||||||
if extra_vars is not None:
|
|
||||||
kv['extra_vars'] = callback_filter_out_ansible_extra_vars(extra_vars)
|
|
||||||
result = job.signal_start(**kv)
|
|
||||||
if not result:
|
if not result:
|
||||||
data = dict(msg=_('Error starting job!'))
|
data = dict(msg=_('Error starting job!'))
|
||||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||||
@@ -3665,7 +3666,7 @@ class AdHocCommandRelaunch(GenericAPIView):
|
|||||||
data = {}
|
data = {}
|
||||||
for field in ('job_type', 'inventory_id', 'limit', 'credential_id',
|
for field in ('job_type', 'inventory_id', 'limit', 'credential_id',
|
||||||
'module_name', 'module_args', 'forks', 'verbosity',
|
'module_name', 'module_args', 'forks', 'verbosity',
|
||||||
'become_enabled'):
|
'extra_vars', 'become_enabled'):
|
||||||
if field.endswith('_id'):
|
if field.endswith('_id'):
|
||||||
data[field[:-3]] = getattr(obj, field)
|
data[field[:-3]] = getattr(obj, field)
|
||||||
else:
|
else:
|
||||||
|
|||||||
0
awx/lib/tests/__init__.py
Normal file
0
awx/lib/tests/__init__.py
Normal file
2
awx/lib/tests/pytest.ini
Normal file
2
awx/lib/tests/pytest.ini
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[pytest]
|
||||||
|
addopts = -v
|
||||||
213
awx/lib/tests/test_display_callback.py
Normal file
213
awx/lib/tests/test_display_callback.py
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
from collections import OrderedDict
|
||||||
|
import json
|
||||||
|
import mock
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
# ansible uses `ANSIBLE_CALLBACK_PLUGINS` and `ANSIBLE_STDOUT_CALLBACK` to
|
||||||
|
# discover callback plugins; `ANSIBLE_CALLBACK_PLUGINS` is a list of paths to
|
||||||
|
# search for a plugin implementation (which should be named `CallbackModule`)
|
||||||
|
#
|
||||||
|
# this code modifies the Python path to make our
|
||||||
|
# `awx.lib.tower_display_callback` callback importable (because `awx.lib`
|
||||||
|
# itself is not a package)
|
||||||
|
#
|
||||||
|
# we use the `tower_display_callback` imports below within this file, but
|
||||||
|
# Ansible also uses them when it discovers this file in
|
||||||
|
# `ANSIBLE_CALLBACK_PLUGINS`
|
||||||
|
CALLBACK = os.path.splitext(os.path.basename(__file__))[0]
|
||||||
|
PLUGINS = os.path.dirname(__file__)
|
||||||
|
with mock.patch.dict(os.environ, {'ANSIBLE_STDOUT_CALLBACK': CALLBACK,
|
||||||
|
'ANSIBLE_CALLBACK_PLUGINS': PLUGINS}):
|
||||||
|
from ansible.cli.playbook import PlaybookCLI
|
||||||
|
from ansible.executor.playbook_executor import PlaybookExecutor
|
||||||
|
from ansible.inventory import Inventory
|
||||||
|
from ansible.parsing.dataloader import DataLoader
|
||||||
|
from ansible.vars import VariableManager
|
||||||
|
|
||||||
|
# Add awx/lib to sys.path so we can use the plugin
|
||||||
|
path = os.path.abspath(os.path.join(PLUGINS, '..', '..'))
|
||||||
|
if path not in sys.path:
|
||||||
|
sys.path.insert(0, path)
|
||||||
|
|
||||||
|
from tower_display_callback import TowerDefaultCallbackModule as CallbackModule # noqa
|
||||||
|
from tower_display_callback.events import event_context # noqa
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def cache(request):
|
||||||
|
class Cache(OrderedDict):
|
||||||
|
def set(self, key, value):
|
||||||
|
self[key] = value
|
||||||
|
local_cache = Cache()
|
||||||
|
patch = mock.patch.object(event_context, 'cache', local_cache)
|
||||||
|
patch.start()
|
||||||
|
request.addfinalizer(patch.stop)
|
||||||
|
return local_cache
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def executor(tmpdir_factory, request):
|
||||||
|
playbooks = request.node.callspec.params.get('playbook')
|
||||||
|
playbook_files = []
|
||||||
|
for name, playbook in playbooks.items():
|
||||||
|
filename = str(tmpdir_factory.mktemp('data').join(name))
|
||||||
|
with open(filename, 'w') as f:
|
||||||
|
f.write(playbook)
|
||||||
|
playbook_files.append(filename)
|
||||||
|
|
||||||
|
cli = PlaybookCLI(['', 'playbook.yml'])
|
||||||
|
cli.parse()
|
||||||
|
options = cli.parser.parse_args([])[0]
|
||||||
|
loader = DataLoader()
|
||||||
|
variable_manager = VariableManager()
|
||||||
|
inventory = Inventory(loader=loader, variable_manager=variable_manager,
|
||||||
|
host_list=['localhost'])
|
||||||
|
variable_manager.set_inventory(inventory)
|
||||||
|
|
||||||
|
return PlaybookExecutor(playbooks=playbook_files, inventory=inventory,
|
||||||
|
variable_manager=variable_manager, loader=loader,
|
||||||
|
options=options, passwords={})
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('event', {'playbook_on_start',
|
||||||
|
'playbook_on_play_start',
|
||||||
|
'playbook_on_task_start', 'runner_on_ok',
|
||||||
|
'playbook_on_stats'})
|
||||||
|
@pytest.mark.parametrize('playbook', [
|
||||||
|
{'helloworld.yml': '''
|
||||||
|
- name: Hello World Sample
|
||||||
|
connection: local
|
||||||
|
hosts: all
|
||||||
|
gather_facts: no
|
||||||
|
tasks:
|
||||||
|
- name: Hello Message
|
||||||
|
debug:
|
||||||
|
msg: "Hello World!"
|
||||||
|
'''} # noqa
|
||||||
|
])
|
||||||
|
def test_callback_plugin_receives_events(executor, cache, event, playbook):
|
||||||
|
executor.run()
|
||||||
|
assert len(cache)
|
||||||
|
assert event in [task['event'] for task in cache.values()]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('playbook', [
|
||||||
|
{'no_log_on_ok.yml': '''
|
||||||
|
- name: args should not be logged when task-level no_log is set
|
||||||
|
connection: local
|
||||||
|
hosts: all
|
||||||
|
gather_facts: no
|
||||||
|
tasks:
|
||||||
|
- shell: echo "SENSITIVE"
|
||||||
|
no_log: true
|
||||||
|
'''}, # noqa
|
||||||
|
{'no_log_on_fail.yml': '''
|
||||||
|
- name: failed args should not be logged when task-level no_log is set
|
||||||
|
connection: local
|
||||||
|
hosts: all
|
||||||
|
gather_facts: no
|
||||||
|
tasks:
|
||||||
|
- shell: echo "SENSITIVE"
|
||||||
|
no_log: true
|
||||||
|
failed_when: true
|
||||||
|
ignore_errors: true
|
||||||
|
'''}, # noqa
|
||||||
|
{'no_log_on_skip.yml': '''
|
||||||
|
- name: skipped task args should be suppressed with no_log
|
||||||
|
connection: local
|
||||||
|
hosts: all
|
||||||
|
gather_facts: no
|
||||||
|
tasks:
|
||||||
|
- shell: echo "SENSITIVE"
|
||||||
|
no_log: true
|
||||||
|
when: false
|
||||||
|
'''}, # noqa
|
||||||
|
{'no_log_on_play.yml': '''
|
||||||
|
- name: args should not be logged when play-level no_log set
|
||||||
|
connection: local
|
||||||
|
hosts: all
|
||||||
|
gather_facts: no
|
||||||
|
no_log: true
|
||||||
|
tasks:
|
||||||
|
- shell: echo "SENSITIVE"
|
||||||
|
'''}, # noqa
|
||||||
|
{'async_no_log.yml': '''
|
||||||
|
- name: async task args should suppressed with no_log
|
||||||
|
connection: local
|
||||||
|
hosts: all
|
||||||
|
gather_facts: no
|
||||||
|
no_log: true
|
||||||
|
tasks:
|
||||||
|
- async: 10
|
||||||
|
poll: 1
|
||||||
|
shell: echo "SENSITIVE"
|
||||||
|
no_log: true
|
||||||
|
'''}, # noqa
|
||||||
|
{'with_items.yml': '''
|
||||||
|
- name: with_items tasks should be suppressed with no_log
|
||||||
|
connection: local
|
||||||
|
hosts: all
|
||||||
|
gather_facts: no
|
||||||
|
tasks:
|
||||||
|
- shell: echo {{ item }}
|
||||||
|
no_log: true
|
||||||
|
with_items: [ "SENSITIVE", "SENSITIVE-SKIPPED", "SENSITIVE-FAILED" ]
|
||||||
|
when: item != "SENSITIVE-SKIPPED"
|
||||||
|
failed_when: item == "SENSITIVE-FAILED"
|
||||||
|
ignore_errors: yes
|
||||||
|
'''}, # noqa
|
||||||
|
])
|
||||||
|
def test_callback_plugin_no_log_filters(executor, cache, playbook):
|
||||||
|
executor.run()
|
||||||
|
assert len(cache)
|
||||||
|
assert 'SENSITIVE' not in json.dumps(cache.items())
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('playbook', [
|
||||||
|
{'no_log_on_ok.yml': '''
|
||||||
|
- name: args should not be logged when task-level no_log is set
|
||||||
|
connection: local
|
||||||
|
hosts: all
|
||||||
|
gather_facts: no
|
||||||
|
tasks:
|
||||||
|
- shell: echo "SENSITIVE"
|
||||||
|
- shell: echo "PRIVATE"
|
||||||
|
no_log: true
|
||||||
|
'''}, # noqa
|
||||||
|
])
|
||||||
|
def test_callback_plugin_task_args_leak(executor, cache, playbook):
|
||||||
|
executor.run()
|
||||||
|
events = cache.values()
|
||||||
|
assert events[0]['event'] == 'playbook_on_start'
|
||||||
|
assert events[1]['event'] == 'playbook_on_play_start'
|
||||||
|
|
||||||
|
# task 1
|
||||||
|
assert events[2]['event'] == 'playbook_on_task_start'
|
||||||
|
assert 'SENSITIVE' in events[2]['event_data']['task_args']
|
||||||
|
assert events[3]['event'] == 'runner_on_ok'
|
||||||
|
assert 'SENSITIVE' in events[3]['event_data']['task_args']
|
||||||
|
|
||||||
|
# task 2 no_log=True
|
||||||
|
assert events[4]['event'] == 'playbook_on_task_start'
|
||||||
|
assert events[4]['event_data']['task_args'] == "the output has been hidden due to the fact that 'no_log: true' was specified for this result" # noqa
|
||||||
|
assert events[5]['event'] == 'runner_on_ok'
|
||||||
|
assert events[5]['event_data']['task_args'] == "the output has been hidden due to the fact that 'no_log: true' was specified for this result" # noqa
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('playbook', [
|
||||||
|
{'strip_env_vars.yml': '''
|
||||||
|
- name: sensitive environment variables should be stripped from events
|
||||||
|
connection: local
|
||||||
|
hosts: all
|
||||||
|
tasks:
|
||||||
|
- shell: echo "Hello, World!"
|
||||||
|
'''}, # noqa
|
||||||
|
])
|
||||||
|
def test_callback_plugin_strips_task_environ_variables(executor, cache, playbook):
|
||||||
|
executor.run()
|
||||||
|
assert len(cache)
|
||||||
|
for event in cache.values():
|
||||||
|
assert os.environ['PATH'] not in json.dumps(event)
|
||||||
@@ -55,22 +55,6 @@ class BaseCallbackModule(CallbackBase):
|
|||||||
'playbook_on_no_hosts_remaining',
|
'playbook_on_no_hosts_remaining',
|
||||||
]
|
]
|
||||||
|
|
||||||
CENSOR_FIELD_WHITELIST = [
|
|
||||||
'msg',
|
|
||||||
'failed',
|
|
||||||
'changed',
|
|
||||||
'results',
|
|
||||||
'start',
|
|
||||||
'end',
|
|
||||||
'delta',
|
|
||||||
'cmd',
|
|
||||||
'_ansible_no_log',
|
|
||||||
'rc',
|
|
||||||
'failed_when_result',
|
|
||||||
'skipped',
|
|
||||||
'skip_reason',
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(BaseCallbackModule, self).__init__()
|
super(BaseCallbackModule, self).__init__()
|
||||||
self.task_uuids = set()
|
self.task_uuids = set()
|
||||||
@@ -85,6 +69,9 @@ class BaseCallbackModule(CallbackBase):
|
|||||||
else:
|
else:
|
||||||
task = None
|
task = None
|
||||||
|
|
||||||
|
if event_data.get('res') and event_data['res'].get('_ansible_no_log', False):
|
||||||
|
event_data['res'] = {'censored': "the output has been hidden due to the fact that 'no_log: true' was specified for this result"} # noqa
|
||||||
|
|
||||||
with event_context.display_lock:
|
with event_context.display_lock:
|
||||||
try:
|
try:
|
||||||
event_context.add_local(event=event, **event_data)
|
event_context.add_local(event=event, **event_data)
|
||||||
@@ -132,7 +119,9 @@ class BaseCallbackModule(CallbackBase):
|
|||||||
task_ctx['task_path'] = task.get_path()
|
task_ctx['task_path'] = task.get_path()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
if not task.no_log:
|
if task.no_log:
|
||||||
|
task_ctx['task_args'] = "the output has been hidden due to the fact that 'no_log: true' was specified for this result"
|
||||||
|
else:
|
||||||
task_args = ', '.join(('%s=%s' % a for a in task.args.items()))
|
task_args = ', '.join(('%s=%s' % a for a in task.args.items()))
|
||||||
task_ctx['task_args'] = task_args
|
task_ctx['task_args'] = task_args
|
||||||
if getattr(task, '_role', None):
|
if getattr(task, '_role', None):
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ class AdHocCommand(UnifiedJob, JobNotificationMixin):
|
|||||||
data = {}
|
data = {}
|
||||||
for field in ('job_type', 'inventory_id', 'limit', 'credential_id',
|
for field in ('job_type', 'inventory_id', 'limit', 'credential_id',
|
||||||
'module_name', 'module_args', 'forks', 'verbosity',
|
'module_name', 'module_args', 'forks', 'verbosity',
|
||||||
'become_enabled'):
|
'extra_vars', 'become_enabled'):
|
||||||
data[field] = getattr(self, field)
|
data[field] = getattr(self, field)
|
||||||
return AdHocCommand.objects.create(**data)
|
return AdHocCommand.objects.create(**data)
|
||||||
|
|
||||||
|
|||||||
@@ -1277,10 +1277,20 @@ class InventoryUpdate(UnifiedJob, InventorySourceOptions, JobNotificationMixin):
|
|||||||
def get_notification_friendly_name(self):
|
def get_notification_friendly_name(self):
|
||||||
return "Inventory Update"
|
return "Inventory Update"
|
||||||
|
|
||||||
def cancel(self):
|
def _build_job_explanation(self):
|
||||||
res = super(InventoryUpdate, self).cancel()
|
if not self.job_explanation:
|
||||||
|
return 'Previous Task Canceled: {"job_type": "%s", "job_name": "%s", "job_id": "%s"}' % \
|
||||||
|
(self.model_to_str(), self.name, self.id)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_dependent_jobs(self):
|
||||||
|
return Job.objects.filter(dependent_jobs__in=[self.id])
|
||||||
|
|
||||||
|
def cancel(self, job_explanation=None):
|
||||||
|
|
||||||
|
res = super(InventoryUpdate, self).cancel(job_explanation=job_explanation)
|
||||||
if res:
|
if res:
|
||||||
map(lambda x: x.cancel(), Job.objects.filter(dependent_jobs__in=[self.id]))
|
map(lambda x: x.cancel(job_explanation=self._build_job_explanation()), self.get_dependent_jobs())
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -310,9 +310,13 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour
|
|||||||
elif self.variables_needed_to_start:
|
elif self.variables_needed_to_start:
|
||||||
variables_needed = True
|
variables_needed = True
|
||||||
prompting_needed = False
|
prompting_needed = False
|
||||||
for value in self._ask_for_vars_dict().values():
|
# The behavior of provisioning callback should mimic
|
||||||
if value:
|
# that of job template launch, so prompting_needed should
|
||||||
prompting_needed = True
|
# not block a provisioning callback from creating/launching jobs.
|
||||||
|
if callback_extra_vars is None:
|
||||||
|
for value in self._ask_for_vars_dict().values():
|
||||||
|
if value:
|
||||||
|
prompting_needed = True
|
||||||
return (not prompting_needed and
|
return (not prompting_needed and
|
||||||
not self.passwords_needed_to_start and
|
not self.passwords_needed_to_start and
|
||||||
not variables_needed)
|
not variables_needed)
|
||||||
@@ -633,10 +637,10 @@ class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin):
|
|||||||
Canceling a job also cancels the implicit project update with launch_type
|
Canceling a job also cancels the implicit project update with launch_type
|
||||||
run.
|
run.
|
||||||
'''
|
'''
|
||||||
def cancel(self):
|
def cancel(self, job_explanation=None):
|
||||||
res = super(Job, self).cancel()
|
res = super(Job, self).cancel(job_explanation=job_explanation)
|
||||||
if self.project_update:
|
if self.project_update:
|
||||||
self.project_update.cancel()
|
self.project_update.cancel(job_explanation=job_explanation)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1025,7 +1025,7 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
|||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self, job_explanation=None):
|
||||||
if self.can_cancel:
|
if self.can_cancel:
|
||||||
if not self.cancel_flag:
|
if not self.cancel_flag:
|
||||||
self.cancel_flag = True
|
self.cancel_flag = True
|
||||||
@@ -1033,6 +1033,9 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
|||||||
if self.status in ('pending', 'waiting', 'new'):
|
if self.status in ('pending', 'waiting', 'new'):
|
||||||
self.status = 'canceled'
|
self.status = 'canceled'
|
||||||
cancel_fields.append('status')
|
cancel_fields.append('status')
|
||||||
|
if job_explanation is not None:
|
||||||
|
self.job_explanation = job_explanation
|
||||||
|
cancel_fields.append('job_explanation')
|
||||||
self.save(update_fields=cancel_fields)
|
self.save(update_fields=cancel_fields)
|
||||||
self.websocket_emit_status("canceled")
|
self.websocket_emit_status("canceled")
|
||||||
if settings.BROKER_URL.startswith('amqp://'):
|
if settings.BROKER_URL.startswith('amqp://'):
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ def celery_startup(conf=None, **kwargs):
|
|||||||
@worker_process_init.connect
|
@worker_process_init.connect
|
||||||
def task_set_logger_pre_run(*args, **kwargs):
|
def task_set_logger_pre_run(*args, **kwargs):
|
||||||
cache.close()
|
cache.close()
|
||||||
configure_external_logger(settings, async_flag=False, is_startup=False)
|
configure_external_logger(settings, is_startup=False)
|
||||||
|
|
||||||
|
|
||||||
def _clear_cache_keys(set_of_keys):
|
def _clear_cache_keys(set_of_keys):
|
||||||
@@ -1160,6 +1160,7 @@ class RunProjectUpdate(BaseTask):
|
|||||||
'''
|
'''
|
||||||
env = super(RunProjectUpdate, self).build_env(project_update, **kwargs)
|
env = super(RunProjectUpdate, self).build_env(project_update, **kwargs)
|
||||||
env = self.add_ansible_venv(env)
|
env = self.add_ansible_venv(env)
|
||||||
|
env['ANSIBLE_RETRY_FILES_ENABLED'] = str(False)
|
||||||
env['ANSIBLE_ASK_PASS'] = str(False)
|
env['ANSIBLE_ASK_PASS'] = str(False)
|
||||||
env['ANSIBLE_ASK_SUDO_PASS'] = str(False)
|
env['ANSIBLE_ASK_SUDO_PASS'] = str(False)
|
||||||
env['DISPLAY'] = '' # Prevent stupid password popup when running tests.
|
env['DISPLAY'] = '' # Prevent stupid password popup when running tests.
|
||||||
|
|||||||
@@ -339,6 +339,21 @@ def test_list_created_org_credentials(post, get, organization, org_admin, org_me
|
|||||||
assert response.data['count'] == 0
|
assert response.data['count'] == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('order_by', ('password', '-password', 'password,pk', '-password,pk'))
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_list_cannot_order_by_encrypted_field(post, get, organization, org_admin, order_by):
|
||||||
|
for i, password in enumerate(('abc', 'def', 'xyz')):
|
||||||
|
response = post(reverse('api:credential_list'), {
|
||||||
|
'organization': organization.id,
|
||||||
|
'name': 'C%d' % i,
|
||||||
|
'password': password
|
||||||
|
}, org_admin)
|
||||||
|
|
||||||
|
response = get(reverse('api:credential_list'), org_admin,
|
||||||
|
QUERY_STRING='order_by=%s' % order_by, status=400)
|
||||||
|
assert response.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Openstack Credentials
|
# Openstack Credentials
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -35,6 +35,21 @@ def test_edit_inventory(put, inventory, alice, role_field, expected_status_code)
|
|||||||
put(reverse('api:inventory_detail', args=(inventory.id,)), data, alice, expect=expected_status_code)
|
put(reverse('api:inventory_detail', args=(inventory.id,)), data, alice, expect=expected_status_code)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('order_by', ('script', '-script', 'script,pk', '-script,pk'))
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_list_cannot_order_by_unsearchable_field(get, organization, alice, order_by):
|
||||||
|
for i, script in enumerate(('#!/bin/a', '#!/bin/b', '#!/bin/c')):
|
||||||
|
custom_script = organization.custom_inventory_scripts.create(
|
||||||
|
name="I%d" % i,
|
||||||
|
script=script
|
||||||
|
)
|
||||||
|
custom_script.admin_role.members.add(alice)
|
||||||
|
|
||||||
|
response = get(reverse('api:inventory_script_list'), alice,
|
||||||
|
QUERY_STRING='order_by=%s' % order_by, status=400)
|
||||||
|
assert response.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("role_field,expected_status_code", [
|
@pytest.mark.parametrize("role_field,expected_status_code", [
|
||||||
(None, 403),
|
(None, 403),
|
||||||
('admin_role', 201),
|
('admin_role', 201),
|
||||||
|
|||||||
@@ -344,3 +344,53 @@ def test_job_launch_unprompted_vars_with_survey(mocker, survey_spec_factory, job
|
|||||||
|
|
||||||
# Check that the survey variable is accepted and the job variable isn't
|
# Check that the survey variable is accepted and the job variable isn't
|
||||||
mock_job.signal_start.assert_called_once()
|
mock_job.signal_start.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@pytest.mark.job_runtime_vars
|
||||||
|
def test_callback_accept_prompted_extra_var(mocker, survey_spec_factory, job_template_prompts, post, admin_user, host):
|
||||||
|
job_template = job_template_prompts(True)
|
||||||
|
job_template.host_config_key = "foo"
|
||||||
|
job_template.survey_enabled = True
|
||||||
|
job_template.survey_spec = survey_spec_factory('survey_var')
|
||||||
|
job_template.save()
|
||||||
|
|
||||||
|
with mocker.patch('awx.main.access.BaseAccess.check_license'):
|
||||||
|
mock_job = mocker.MagicMock(spec=Job, id=968, extra_vars={"job_launch_var": 3, "survey_var": 4})
|
||||||
|
with mocker.patch.object(JobTemplate, 'create_unified_job', return_value=mock_job):
|
||||||
|
with mocker.patch('awx.api.serializers.JobSerializer.to_representation', return_value={}):
|
||||||
|
with mocker.patch('awx.api.views.JobTemplateCallback.find_matching_hosts', return_value=[host]):
|
||||||
|
post(
|
||||||
|
reverse('api:job_template_callback', args=[job_template.pk]),
|
||||||
|
dict(extra_vars={"job_launch_var": 3, "survey_var": 4}, host_config_key="foo"),
|
||||||
|
admin_user, expect=201, format='json')
|
||||||
|
assert JobTemplate.create_unified_job.called
|
||||||
|
assert JobTemplate.create_unified_job.call_args == ({'extra_vars': {'survey_var': 4,
|
||||||
|
'job_launch_var': 3},
|
||||||
|
'launch_type': 'callback',
|
||||||
|
'limit': 'single-host'},)
|
||||||
|
|
||||||
|
mock_job.signal_start.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@pytest.mark.job_runtime_vars
|
||||||
|
def test_callback_ignore_unprompted_extra_var(mocker, survey_spec_factory, job_template_prompts, post, admin_user, host):
|
||||||
|
job_template = job_template_prompts(False)
|
||||||
|
job_template.host_config_key = "foo"
|
||||||
|
job_template.save()
|
||||||
|
|
||||||
|
with mocker.patch('awx.main.access.BaseAccess.check_license'):
|
||||||
|
mock_job = mocker.MagicMock(spec=Job, id=968, extra_vars={"job_launch_var": 3, "survey_var": 4})
|
||||||
|
with mocker.patch.object(JobTemplate, 'create_unified_job', return_value=mock_job):
|
||||||
|
with mocker.patch('awx.api.serializers.JobSerializer.to_representation', return_value={}):
|
||||||
|
with mocker.patch('awx.api.views.JobTemplateCallback.find_matching_hosts', return_value=[host]):
|
||||||
|
post(
|
||||||
|
reverse('api:job_template_callback', args=[job_template.pk]),
|
||||||
|
dict(extra_vars={"job_launch_var": 3, "survey_var": 4}, host_config_key="foo"),
|
||||||
|
admin_user, expect=201, format='json')
|
||||||
|
assert JobTemplate.create_unified_job.called
|
||||||
|
assert JobTemplate.create_unified_job.call_args == ({'launch_type': 'callback',
|
||||||
|
'limit': 'single-host'},)
|
||||||
|
|
||||||
|
mock_job.signal_start.assert_called_once()
|
||||||
|
|||||||
@@ -92,3 +92,15 @@ def test_expired_licenses():
|
|||||||
|
|
||||||
assert vdata['compliant'] is False
|
assert vdata['compliant'] is False
|
||||||
assert vdata['grace_period_remaining'] > 0
|
assert vdata['grace_period_remaining'] > 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_cloudforms_license(mocker):
|
||||||
|
with mocker.patch('awx.main.task_engine.TaskEnhancer._check_cloudforms_subscription', return_value=True):
|
||||||
|
task_enhancer = TaskEnhancer()
|
||||||
|
vdata = task_enhancer.validate_enhancements()
|
||||||
|
assert vdata['compliant'] is True
|
||||||
|
assert vdata['subscription_name'] == "Red Hat CloudForms License"
|
||||||
|
assert vdata['available_instances'] == 9999999
|
||||||
|
assert vdata['license_type'] == 'enterprise'
|
||||||
|
assert vdata['features']['ha'] is True
|
||||||
|
|||||||
0
awx/main/tests/unit/models/__init__.py
Normal file
0
awx/main/tests/unit/models/__init__.py
Normal file
38
awx/main/tests/unit/models/test_inventory.py
Normal file
38
awx/main/tests/unit/models/test_inventory.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import pytest
|
||||||
|
import mock
|
||||||
|
from awx.main.models import (
|
||||||
|
UnifiedJob,
|
||||||
|
InventoryUpdate,
|
||||||
|
Job,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def dependent_job(mocker):
|
||||||
|
j = Job(id=3, name='I_am_a_job')
|
||||||
|
j.cancel = mocker.MagicMock(return_value=True)
|
||||||
|
return [j]
|
||||||
|
|
||||||
|
|
||||||
|
def test_cancel(mocker, dependent_job):
|
||||||
|
with mock.patch.object(UnifiedJob, 'cancel', return_value=True) as parent_cancel:
|
||||||
|
iu = InventoryUpdate()
|
||||||
|
|
||||||
|
iu.get_dependent_jobs = mocker.MagicMock(return_value=dependent_job)
|
||||||
|
iu.save = mocker.MagicMock()
|
||||||
|
build_job_explanation_mock = mocker.MagicMock()
|
||||||
|
iu._build_job_explanation = mocker.MagicMock(return_value=build_job_explanation_mock)
|
||||||
|
|
||||||
|
iu.cancel()
|
||||||
|
|
||||||
|
parent_cancel.assert_called_with(job_explanation=None)
|
||||||
|
dependent_job[0].cancel.assert_called_with(job_explanation=build_job_explanation_mock)
|
||||||
|
|
||||||
|
|
||||||
|
def test__build_job_explanation():
|
||||||
|
iu = InventoryUpdate(id=3, name='I_am_an_Inventory_Update')
|
||||||
|
|
||||||
|
job_explanation = iu._build_job_explanation()
|
||||||
|
|
||||||
|
assert job_explanation == 'Previous Task Canceled: {"job_type": "%s", "job_name": "%s", "job_id": "%s"}' % \
|
||||||
|
('inventory_update', 'I_am_an_Inventory_Update', 3)
|
||||||
@@ -115,3 +115,16 @@ def test_job_template_survey_mixin_length(job_template_factory):
|
|||||||
{'type':'password', 'variable':'my_other_variable'}]}
|
{'type':'password', 'variable':'my_other_variable'}]}
|
||||||
kwargs = obj._update_unified_job_kwargs(extra_vars={'my_variable':'$encrypted$'})
|
kwargs = obj._update_unified_job_kwargs(extra_vars={'my_variable':'$encrypted$'})
|
||||||
assert kwargs['extra_vars'] == '{"my_variable": "my_default"}'
|
assert kwargs['extra_vars'] == '{"my_variable": "my_default"}'
|
||||||
|
|
||||||
|
|
||||||
|
def test_job_template_can_start_with_callback_extra_vars_provided(job_template_factory):
|
||||||
|
objects = job_template_factory(
|
||||||
|
'callback_extra_vars_test',
|
||||||
|
organization='org1',
|
||||||
|
inventory='inventory1',
|
||||||
|
credential='cred1',
|
||||||
|
persisted=False,
|
||||||
|
)
|
||||||
|
obj = objects.job_template
|
||||||
|
obj.ask_variables_on_launch = True
|
||||||
|
assert obj.can_start_without_user_input(callback_extra_vars='{"foo": "bar"}') is True
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import pytest
|
||||||
import mock
|
import mock
|
||||||
|
|
||||||
from awx.main.models import (
|
from awx.main.models import (
|
||||||
@@ -14,3 +15,38 @@ def test_unified_job_workflow_attributes():
|
|||||||
|
|
||||||
assert job.spawned_by_workflow is True
|
assert job.spawned_by_workflow is True
|
||||||
assert job.workflow_job_id == 1
|
assert job.workflow_job_id == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def unified_job(mocker):
|
||||||
|
mocker.patch.object(UnifiedJob, 'can_cancel', return_value=True)
|
||||||
|
j = UnifiedJob()
|
||||||
|
j.status = 'pending'
|
||||||
|
j.cancel_flag = None
|
||||||
|
j.save = mocker.MagicMock()
|
||||||
|
j.websocket_emit_status = mocker.MagicMock()
|
||||||
|
return j
|
||||||
|
|
||||||
|
|
||||||
|
def test_cancel(unified_job):
|
||||||
|
|
||||||
|
unified_job.cancel()
|
||||||
|
|
||||||
|
assert unified_job.cancel_flag is True
|
||||||
|
assert unified_job.status == 'canceled'
|
||||||
|
assert unified_job.job_explanation == ''
|
||||||
|
# Note: the websocket emit status check is just reflecting the state of the current code.
|
||||||
|
# Some more thought may want to go into only emitting canceled if/when the job record
|
||||||
|
# status is changed to canceled. Unlike, currently, where it's emitted unconditionally.
|
||||||
|
unified_job.websocket_emit_status.assert_called_with("canceled")
|
||||||
|
unified_job.save.assert_called_with(update_fields=['cancel_flag', 'status'])
|
||||||
|
|
||||||
|
|
||||||
|
def test_cancel_job_explanation(unified_job):
|
||||||
|
job_explanation = 'giggity giggity'
|
||||||
|
|
||||||
|
unified_job.cancel(job_explanation=job_explanation)
|
||||||
|
|
||||||
|
assert unified_job.job_explanation == job_explanation
|
||||||
|
unified_job.save.assert_called_with(update_fields=['cancel_flag', 'status', 'job_explanation'])
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import base64
|
import base64
|
||||||
|
import cStringIO
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.conf import LazySettings
|
from django.conf import LazySettings
|
||||||
import pytest
|
import pytest
|
||||||
import requests
|
import requests
|
||||||
@@ -44,17 +46,27 @@ def http_adapter():
|
|||||||
return FakeHTTPAdapter()
|
return FakeHTTPAdapter()
|
||||||
|
|
||||||
|
|
||||||
def test_https_logging_handler_requests_sync_implementation():
|
@pytest.fixture()
|
||||||
handler = HTTPSHandler(async=False)
|
def connection_error_adapter():
|
||||||
assert not isinstance(handler.session, FuturesSession)
|
class ConnectionErrorAdapter(requests.adapters.HTTPAdapter):
|
||||||
assert isinstance(handler.session, requests.Session)
|
|
||||||
|
def send(self, request, **kwargs):
|
||||||
|
err = requests.packages.urllib3.exceptions.SSLError()
|
||||||
|
raise requests.exceptions.ConnectionError(err, request=request)
|
||||||
|
|
||||||
|
return ConnectionErrorAdapter()
|
||||||
|
|
||||||
|
|
||||||
def test_https_logging_handler_requests_async_implementation():
|
def test_https_logging_handler_requests_async_implementation():
|
||||||
handler = HTTPSHandler(async=True)
|
handler = HTTPSHandler()
|
||||||
assert isinstance(handler.session, FuturesSession)
|
assert isinstance(handler.session, FuturesSession)
|
||||||
|
|
||||||
|
|
||||||
|
def test_https_logging_handler_has_default_http_timeout():
|
||||||
|
handler = HTTPSHandler.from_django_settings(settings)
|
||||||
|
assert handler.http_timeout == 5
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('param', PARAM_NAMES.keys())
|
@pytest.mark.parametrize('param', PARAM_NAMES.keys())
|
||||||
def test_https_logging_handler_defaults(param):
|
def test_https_logging_handler_defaults(param):
|
||||||
handler = HTTPSHandler()
|
handler = HTTPSHandler()
|
||||||
@@ -154,18 +166,39 @@ def test_https_logging_handler_skip_log(params, logger_name, expected):
|
|||||||
assert handler.skip_log(logger_name) is expected
|
assert handler.skip_log(logger_name) is expected
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('message_type, async', [
|
def test_https_logging_handler_connection_error(connection_error_adapter,
|
||||||
('logstash', False),
|
dummy_log_record):
|
||||||
('logstash', True),
|
handler = HTTPSHandler(host='127.0.0.1', enabled_flag=True,
|
||||||
('splunk', False),
|
message_type='logstash',
|
||||||
('splunk', True),
|
enabled_loggers=['awx', 'activity_stream', 'job_events', 'system_tracking'])
|
||||||
])
|
handler.setFormatter(LogstashFormatter())
|
||||||
|
handler.session.mount('http://', connection_error_adapter)
|
||||||
|
|
||||||
|
buff = cStringIO.StringIO()
|
||||||
|
logging.getLogger('awx.main.utils.handlers').addHandler(
|
||||||
|
logging.StreamHandler(buff)
|
||||||
|
)
|
||||||
|
|
||||||
|
async_futures = handler.emit(dummy_log_record)
|
||||||
|
with pytest.raises(requests.exceptions.ConnectionError):
|
||||||
|
[future.result() for future in async_futures]
|
||||||
|
assert 'failed to emit log to external aggregator\nTraceback' in buff.getvalue()
|
||||||
|
|
||||||
|
# we should only log failures *periodically*, so causing *another*
|
||||||
|
# immediate failure shouldn't report a second ConnectionError
|
||||||
|
buff.truncate(0)
|
||||||
|
async_futures = handler.emit(dummy_log_record)
|
||||||
|
with pytest.raises(requests.exceptions.ConnectionError):
|
||||||
|
[future.result() for future in async_futures]
|
||||||
|
assert buff.getvalue() == ''
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('message_type', ['logstash', 'splunk'])
|
||||||
def test_https_logging_handler_emit(http_adapter, dummy_log_record,
|
def test_https_logging_handler_emit(http_adapter, dummy_log_record,
|
||||||
message_type, async):
|
message_type):
|
||||||
handler = HTTPSHandler(host='127.0.0.1', enabled_flag=True,
|
handler = HTTPSHandler(host='127.0.0.1', enabled_flag=True,
|
||||||
message_type=message_type,
|
message_type=message_type,
|
||||||
enabled_loggers=['awx', 'activity_stream', 'job_events', 'system_tracking'],
|
enabled_loggers=['awx', 'activity_stream', 'job_events', 'system_tracking'])
|
||||||
async=async)
|
|
||||||
handler.setFormatter(LogstashFormatter())
|
handler.setFormatter(LogstashFormatter())
|
||||||
handler.session.mount('http://', http_adapter)
|
handler.session.mount('http://', http_adapter)
|
||||||
async_futures = handler.emit(dummy_log_record)
|
async_futures = handler.emit(dummy_log_record)
|
||||||
@@ -191,14 +224,12 @@ def test_https_logging_handler_emit(http_adapter, dummy_log_record,
|
|||||||
assert body['message'] == 'User joe logged in'
|
assert body['message'] == 'User joe logged in'
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('async', (True, False))
|
|
||||||
def test_https_logging_handler_emit_logstash_with_creds(http_adapter,
|
def test_https_logging_handler_emit_logstash_with_creds(http_adapter,
|
||||||
dummy_log_record, async):
|
dummy_log_record):
|
||||||
handler = HTTPSHandler(host='127.0.0.1', enabled_flag=True,
|
handler = HTTPSHandler(host='127.0.0.1', enabled_flag=True,
|
||||||
username='user', password='pass',
|
username='user', password='pass',
|
||||||
message_type='logstash',
|
message_type='logstash',
|
||||||
enabled_loggers=['awx', 'activity_stream', 'job_events', 'system_tracking'],
|
enabled_loggers=['awx', 'activity_stream', 'job_events', 'system_tracking'])
|
||||||
async=async)
|
|
||||||
handler.setFormatter(LogstashFormatter())
|
handler.setFormatter(LogstashFormatter())
|
||||||
handler.session.mount('http://', http_adapter)
|
handler.session.mount('http://', http_adapter)
|
||||||
async_futures = handler.emit(dummy_log_record)
|
async_futures = handler.emit(dummy_log_record)
|
||||||
@@ -209,13 +240,11 @@ def test_https_logging_handler_emit_logstash_with_creds(http_adapter,
|
|||||||
assert request.headers['Authorization'] == 'Basic %s' % base64.b64encode("user:pass")
|
assert request.headers['Authorization'] == 'Basic %s' % base64.b64encode("user:pass")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('async', (True, False))
|
|
||||||
def test_https_logging_handler_emit_splunk_with_creds(http_adapter,
|
def test_https_logging_handler_emit_splunk_with_creds(http_adapter,
|
||||||
dummy_log_record, async):
|
dummy_log_record):
|
||||||
handler = HTTPSHandler(host='127.0.0.1', enabled_flag=True,
|
handler = HTTPSHandler(host='127.0.0.1', enabled_flag=True,
|
||||||
password='pass', message_type='splunk',
|
password='pass', message_type='splunk',
|
||||||
enabled_loggers=['awx', 'activity_stream', 'job_events', 'system_tracking'],
|
enabled_loggers=['awx', 'activity_stream', 'job_events', 'system_tracking'])
|
||||||
async=async)
|
|
||||||
handler.setFormatter(LogstashFormatter())
|
handler.setFormatter(LogstashFormatter())
|
||||||
handler.session.mount('http://', http_adapter)
|
handler.session.mount('http://', http_adapter)
|
||||||
async_futures = handler.emit(dummy_log_record)
|
async_futures = handler.emit(dummy_log_record)
|
||||||
|
|||||||
@@ -5,8 +5,10 @@
|
|||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
import requests
|
import requests
|
||||||
from requests.exceptions import RequestException
|
import time
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from copy import copy
|
from copy import copy
|
||||||
|
from requests.exceptions import RequestException
|
||||||
|
|
||||||
# loggly
|
# loggly
|
||||||
import traceback
|
import traceback
|
||||||
@@ -19,6 +21,8 @@ from awx.main.utils.formatters import LogstashFormatter
|
|||||||
|
|
||||||
__all__ = ['HTTPSNullHandler', 'BaseHTTPSHandler', 'configure_external_logger']
|
__all__ = ['HTTPSNullHandler', 'BaseHTTPSHandler', 'configure_external_logger']
|
||||||
|
|
||||||
|
logger = logging.getLogger('awx.main.utils.handlers')
|
||||||
|
|
||||||
# AWX external logging handler, generally designed to be used
|
# AWX external logging handler, generally designed to be used
|
||||||
# with the accompanying LogstashHandler, derives from python-logstash library
|
# with the accompanying LogstashHandler, derives from python-logstash library
|
||||||
# Non-blocking request accomplished by FuturesSession, similar
|
# Non-blocking request accomplished by FuturesSession, similar
|
||||||
@@ -34,6 +38,7 @@ PARAM_NAMES = {
|
|||||||
'enabled_loggers': 'LOG_AGGREGATOR_LOGGERS',
|
'enabled_loggers': 'LOG_AGGREGATOR_LOGGERS',
|
||||||
'indv_facts': 'LOG_AGGREGATOR_INDIVIDUAL_FACTS',
|
'indv_facts': 'LOG_AGGREGATOR_INDIVIDUAL_FACTS',
|
||||||
'enabled_flag': 'LOG_AGGREGATOR_ENABLED',
|
'enabled_flag': 'LOG_AGGREGATOR_ENABLED',
|
||||||
|
'http_timeout': 'LOG_AGGREGATOR_HTTP_TIMEOUT',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -52,17 +57,41 @@ class HTTPSNullHandler(logging.NullHandler):
|
|||||||
return super(HTTPSNullHandler, self).__init__()
|
return super(HTTPSNullHandler, self).__init__()
|
||||||
|
|
||||||
|
|
||||||
|
class VerboseThreadPoolExecutor(ThreadPoolExecutor):
|
||||||
|
|
||||||
|
last_log_emit = 0
|
||||||
|
|
||||||
|
def submit(self, func, *args, **kwargs):
|
||||||
|
def _wrapped(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
except Exception:
|
||||||
|
# If an exception occurs in a concurrent thread worker (like
|
||||||
|
# a ConnectionError or a read timeout), periodically log
|
||||||
|
# that failure.
|
||||||
|
#
|
||||||
|
# This approach isn't really thread-safe, so we could
|
||||||
|
# potentially log once per thread every 10 seconds, but it
|
||||||
|
# beats logging *every* failed HTTP request in a scenario where
|
||||||
|
# you've typo'd your log aggregator hostname.
|
||||||
|
now = time.time()
|
||||||
|
if now - self.last_log_emit > 10:
|
||||||
|
logger.exception('failed to emit log to external aggregator')
|
||||||
|
self.last_log_emit = now
|
||||||
|
raise
|
||||||
|
return super(VerboseThreadPoolExecutor, self).submit(_wrapped, *args,
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
|
|
||||||
class BaseHTTPSHandler(logging.Handler):
|
class BaseHTTPSHandler(logging.Handler):
|
||||||
def __init__(self, fqdn=False, **kwargs):
|
def __init__(self, fqdn=False, **kwargs):
|
||||||
super(BaseHTTPSHandler, self).__init__()
|
super(BaseHTTPSHandler, self).__init__()
|
||||||
self.fqdn = fqdn
|
self.fqdn = fqdn
|
||||||
self.async = kwargs.get('async', True)
|
|
||||||
for fd in PARAM_NAMES:
|
for fd in PARAM_NAMES:
|
||||||
setattr(self, fd, kwargs.get(fd, None))
|
setattr(self, fd, kwargs.get(fd, None))
|
||||||
if self.async:
|
self.session = FuturesSession(executor=VerboseThreadPoolExecutor(
|
||||||
self.session = FuturesSession()
|
max_workers=2 # this is the default used by requests_futures
|
||||||
else:
|
))
|
||||||
self.session = requests.Session()
|
|
||||||
self.add_auth_information()
|
self.add_auth_information()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -135,10 +164,8 @@ class BaseHTTPSHandler(logging.Handler):
|
|||||||
payload_str = json.dumps(payload_input)
|
payload_str = json.dumps(payload_input)
|
||||||
else:
|
else:
|
||||||
payload_str = payload_input
|
payload_str = payload_input
|
||||||
if self.async:
|
return dict(data=payload_str, background_callback=unused_callback,
|
||||||
return dict(data=payload_str, background_callback=unused_callback)
|
timeout=self.http_timeout)
|
||||||
else:
|
|
||||||
return dict(data=payload_str)
|
|
||||||
|
|
||||||
def skip_log(self, logger_name):
|
def skip_log(self, logger_name):
|
||||||
if self.host == '' or (not self.enabled_flag):
|
if self.host == '' or (not self.enabled_flag):
|
||||||
@@ -153,10 +180,6 @@ class BaseHTTPSHandler(logging.Handler):
|
|||||||
Emit a log record. Returns a list of zero or more
|
Emit a log record. Returns a list of zero or more
|
||||||
``concurrent.futures.Future`` objects.
|
``concurrent.futures.Future`` objects.
|
||||||
|
|
||||||
When ``self.async`` is True, the list will contain one
|
|
||||||
Future object for each HTTP request made. When ``self.async`` is
|
|
||||||
False, the list will be empty.
|
|
||||||
|
|
||||||
See:
|
See:
|
||||||
https://docs.python.org/3/library/concurrent.futures.html#future-objects
|
https://docs.python.org/3/library/concurrent.futures.html#future-objects
|
||||||
http://pythonhosted.org/futures/
|
http://pythonhosted.org/futures/
|
||||||
@@ -177,17 +200,10 @@ class BaseHTTPSHandler(logging.Handler):
|
|||||||
for key in facts_dict:
|
for key in facts_dict:
|
||||||
fact_payload = copy(payload_data)
|
fact_payload = copy(payload_data)
|
||||||
fact_payload.update(facts_dict[key])
|
fact_payload.update(facts_dict[key])
|
||||||
if self.async:
|
async_futures.append(self._send(fact_payload))
|
||||||
async_futures.append(self._send(fact_payload))
|
|
||||||
else:
|
|
||||||
self._send(fact_payload)
|
|
||||||
return async_futures
|
return async_futures
|
||||||
|
|
||||||
if self.async:
|
return [self._send(payload)]
|
||||||
return [self._send(payload)]
|
|
||||||
|
|
||||||
self._send(payload)
|
|
||||||
return []
|
|
||||||
except (KeyboardInterrupt, SystemExit):
|
except (KeyboardInterrupt, SystemExit):
|
||||||
raise
|
raise
|
||||||
except:
|
except:
|
||||||
@@ -209,7 +225,7 @@ def add_or_remove_logger(address, instance):
|
|||||||
specific_logger.handlers.append(instance)
|
specific_logger.handlers.append(instance)
|
||||||
|
|
||||||
|
|
||||||
def configure_external_logger(settings_module, async_flag=True, is_startup=True):
|
def configure_external_logger(settings_module, is_startup=True):
|
||||||
|
|
||||||
is_enabled = settings_module.LOG_AGGREGATOR_ENABLED
|
is_enabled = settings_module.LOG_AGGREGATOR_ENABLED
|
||||||
if is_startup and (not is_enabled):
|
if is_startup and (not is_enabled):
|
||||||
@@ -218,7 +234,7 @@ def configure_external_logger(settings_module, async_flag=True, is_startup=True)
|
|||||||
|
|
||||||
instance = None
|
instance = None
|
||||||
if is_enabled:
|
if is_enabled:
|
||||||
instance = BaseHTTPSHandler.from_django_settings(settings_module, async=async_flag)
|
instance = BaseHTTPSHandler.from_django_settings(settings_module)
|
||||||
instance.setFormatter(LogstashFormatter(settings_module=settings_module))
|
instance.setFormatter(LogstashFormatter(settings_module=settings_module))
|
||||||
awx_logger_instance = instance
|
awx_logger_instance = instance
|
||||||
if is_enabled and 'awx' not in settings_module.LOG_AGGREGATOR_LOGGERS:
|
if is_enabled and 'awx' not in settings_module.LOG_AGGREGATOR_LOGGERS:
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
- name: update project using git and accept hostkey
|
- name: update project using git and accept hostkey
|
||||||
git:
|
git:
|
||||||
dest: "{{project_path|quote}}"
|
dest: "{{project_path|quote}}"
|
||||||
repo: "{{scm_url|quote}}"
|
repo: "{{scm_url}}"
|
||||||
version: "{{scm_branch|quote}}"
|
version: "{{scm_branch|quote}}"
|
||||||
force: "{{scm_clean}}"
|
force: "{{scm_clean}}"
|
||||||
accept_hostkey: "{{scm_accept_hostkey}}"
|
accept_hostkey: "{{scm_accept_hostkey}}"
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
- name: update project using git
|
- name: update project using git
|
||||||
git:
|
git:
|
||||||
dest: "{{project_path|quote}}"
|
dest: "{{project_path|quote}}"
|
||||||
repo: "{{scm_url|quote}}"
|
repo: "{{scm_url}}"
|
||||||
version: "{{scm_branch|quote}}"
|
version: "{{scm_branch|quote}}"
|
||||||
force: "{{scm_clean}}"
|
force: "{{scm_clean}}"
|
||||||
#clone: "{{ scm_full_checkout }}"
|
#clone: "{{ scm_full_checkout }}"
|
||||||
@@ -160,6 +160,11 @@
|
|||||||
scm_version: "{{scm_version|regex_replace('^.*Revision: ([0-9]+).*$', '\\1')}}"
|
scm_version: "{{scm_version|regex_replace('^.*Revision: ([0-9]+).*$', '\\1')}}"
|
||||||
when: scm_type == 'svn'
|
when: scm_type == 'svn'
|
||||||
|
|
||||||
|
- name: parse hg version string properly
|
||||||
|
set_fact:
|
||||||
|
scm_version: "{{scm_version|regex_replace('^([A-Za-z0-9]+).*$', '\\1')}}"
|
||||||
|
when: scm_type == 'hg'
|
||||||
|
|
||||||
- name: Repository Version
|
- name: Repository Version
|
||||||
debug: msg="Repository Version {{ scm_version }}"
|
debug: msg="Repository Version {{ scm_version }}"
|
||||||
when: scm_version is defined
|
when: scm_version is defined
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ if ([System.IntPtr]::Size -eq 4) {
|
|||||||
# This is a 32-bit Windows system, so we only check for 32-bit programs, which will be
|
# This is a 32-bit Windows system, so we only check for 32-bit programs, which will be
|
||||||
# at the native registry location.
|
# at the native registry location.
|
||||||
|
|
||||||
$packages = Get-ChildItem -Path $uninstall_native_path |
|
[PSObject []]$packages = Get-ChildItem -Path $uninstall_native_path |
|
||||||
Get-ItemProperty |
|
Get-ItemProperty |
|
||||||
Select-Object -Property @{Name="name"; Expression={$_."DisplayName"}},
|
Select-Object -Property @{Name="name"; Expression={$_."DisplayName"}},
|
||||||
@{Name="version"; Expression={$_."DisplayVersion"}},
|
@{Name="version"; Expression={$_."DisplayVersion"}},
|
||||||
@@ -38,7 +38,7 @@ if ([System.IntPtr]::Size -eq 4) {
|
|||||||
# This is a 64-bit Windows system, so we check for 64-bit programs in the native
|
# This is a 64-bit Windows system, so we check for 64-bit programs in the native
|
||||||
# registry location, and also for 32-bit programs under Wow6432Node.
|
# registry location, and also for 32-bit programs under Wow6432Node.
|
||||||
|
|
||||||
$packages = Get-ChildItem -Path $uninstall_native_path |
|
[PSObject []]$packages = Get-ChildItem -Path $uninstall_native_path |
|
||||||
Get-ItemProperty |
|
Get-ItemProperty |
|
||||||
Select-Object -Property @{Name="name"; Expression={$_."DisplayName"}},
|
Select-Object -Property @{Name="name"; Expression={$_."DisplayName"}},
|
||||||
@{Name="version"; Expression={$_."DisplayVersion"}},
|
@{Name="version"; Expression={$_."DisplayVersion"}},
|
||||||
|
|||||||
@@ -867,6 +867,7 @@ INSIGHTS_URL_BASE = "https://access.redhat.com"
|
|||||||
TOWER_SETTINGS_MANIFEST = {}
|
TOWER_SETTINGS_MANIFEST = {}
|
||||||
|
|
||||||
LOG_AGGREGATOR_ENABLED = False
|
LOG_AGGREGATOR_ENABLED = False
|
||||||
|
LOG_AGGREGATOR_HTTP_TIMEOUT = 5
|
||||||
|
|
||||||
# The number of retry attempts for websocket session establishment
|
# The number of retry attempts for websocket session establishment
|
||||||
# If you're encountering issues establishing websockets in clustered Tower,
|
# If you're encountering issues establishing websockets in clustered Tower,
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ export default ['$rootScope', '$scope', 'GetBasePath', 'Rest', '$q', 'Wait', 'Pr
|
|||||||
// the object permissions are being added to
|
// the object permissions are being added to
|
||||||
scope.object = scope.resourceData.data;
|
scope.object = scope.resourceData.data;
|
||||||
// array for all possible roles for the object
|
// array for all possible roles for the object
|
||||||
scope.roles = scope.object.summary_fields.object_roles;
|
scope.roles = _.omit(scope.object.summary_fields.object_roles, (key) => {
|
||||||
|
return key.name === 'Read';
|
||||||
|
});
|
||||||
|
|
||||||
// TODO: get working with api
|
// TODO: get working with api
|
||||||
// array w roles and descriptions for key
|
// array w roles and descriptions for key
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ export default
|
|||||||
roles: '=',
|
roles: '=',
|
||||||
model: '='
|
model: '='
|
||||||
},
|
},
|
||||||
// @issue why is the read-only role ommited from this selection?
|
|
||||||
template: '<select ng-cloak class="AddPermissions-selectHide roleSelect2 form-control" ng-model="model" ng-options="role.name for role in roles track by role.id" multiple required></select>',
|
template: '<select ng-cloak class="AddPermissions-selectHide roleSelect2 form-control" ng-model="model" ng-options="role.name for role in roles track by role.id" multiple required></select>',
|
||||||
link: function(scope, element, attrs, ctrl) {
|
link: function(scope, element, attrs, ctrl) {
|
||||||
CreateSelect2({
|
CreateSelect2({
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export default ['$scope', '$rootScope', '$compile', '$location',
|
|||||||
init();
|
init();
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
|
$scope.canEditOrg = true;
|
||||||
// Load the list of options for Kind
|
// Load the list of options for Kind
|
||||||
GetChoices({
|
GetChoices({
|
||||||
scope: $scope,
|
scope: $scope,
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ export default
|
|||||||
dataTitle: i18n._('Organization') + ' ',
|
dataTitle: i18n._('Organization') + ' ',
|
||||||
dataPlacement: 'bottom',
|
dataPlacement: 'bottom',
|
||||||
dataContainer: "body",
|
dataContainer: "body",
|
||||||
ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd)'
|
ngDisabled: '!(credential_obj.summary_fields.user_capabilities.edit || canAdd) || !canEditOrg',
|
||||||
|
awLookupWhen: '(credential_obj.summary_fields.user_capabilities.edit || canAdd) && canEditOrg'
|
||||||
},
|
},
|
||||||
kind: {
|
kind: {
|
||||||
label: i18n._('Type'),
|
label: i18n._('Type'),
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ angular.module('InventoryFormDefinition', [])
|
|||||||
reqExpression: "organizationrequired",
|
reqExpression: "organizationrequired",
|
||||||
init: "true"
|
init: "true"
|
||||||
},
|
},
|
||||||
ngDisabled: '!(inventory_obj.summary_fields.user_capabilities.edit || canAdd)'
|
ngDisabled: '!(inventory_obj.summary_fields.user_capabilities.edit || canAdd) || !canEditOrg',
|
||||||
|
awLookupWhen: '(inventory_obj.summary_fields.user_capabilities.edit || canAdd) && canEditOrg'
|
||||||
},
|
},
|
||||||
variables: {
|
variables: {
|
||||||
label: i18n._('Variables'),
|
label: i18n._('Variables'),
|
||||||
|
|||||||
@@ -349,7 +349,11 @@ export default
|
|||||||
dataPlacement: 'right',
|
dataPlacement: 'right',
|
||||||
dataTitle: i18n._("Host Config Key"),
|
dataTitle: i18n._("Host Config Key"),
|
||||||
dataContainer: "body",
|
dataContainer: "body",
|
||||||
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)'
|
ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)',
|
||||||
|
awRequiredWhen: {
|
||||||
|
reqExpression: 'allow_callbacks',
|
||||||
|
alwaysShowAsterisk: true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
labels: {
|
labels: {
|
||||||
label: i18n._('Labels'),
|
label: i18n._('Labels'),
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ export default
|
|||||||
sourceModel: 'organization',
|
sourceModel: 'organization',
|
||||||
basePath: 'organizations',
|
basePath: 'organizations',
|
||||||
sourceField: 'name',
|
sourceField: 'name',
|
||||||
ngDisabled: '!(team_obj.summary_fields.user_capabilities.edit || canAdd)',
|
ngDisabled: '!(team_obj.summary_fields.user_capabilities.edit || canAdd) || !canEditOrg',
|
||||||
|
awLookupWhen: '(team_obj.summary_fields.user_capabilities.edit || canAdd) && canEditOrg',
|
||||||
required: true,
|
required: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -54,7 +54,8 @@ export default
|
|||||||
dataContainer: 'body',
|
dataContainer: 'body',
|
||||||
dataPlacement: 'right',
|
dataPlacement: 'right',
|
||||||
column: 1,
|
column: 1,
|
||||||
ngDisabled: '!(workflow_job_template_obj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate)'
|
ngDisabled: '!(workflow_job_template_obj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate) || !canEditOrg',
|
||||||
|
awLookupWhen: '(workflow_job_template_obj.summary_fields.user_capabilities.edit || canAddWorkflowJobTemplate) && canEditOrg'
|
||||||
},
|
},
|
||||||
labels: {
|
labels: {
|
||||||
label: i18n._('Labels'),
|
label: i18n._('Labels'),
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ function InventoriesAdd($scope, $rootScope, $compile, $location, $log,
|
|||||||
init();
|
init();
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
|
$scope.canEditOrg = true;
|
||||||
form.formLabelSize = null;
|
form.formLabelSize = null;
|
||||||
form.formFieldSize = null;
|
form.formFieldSize = null;
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ function InventoriesEdit($scope, $rootScope, $compile, $location,
|
|||||||
$log, $stateParams, InventoryForm, Rest, Alert, ProcessErrors,
|
$log, $stateParams, InventoryForm, Rest, Alert, ProcessErrors,
|
||||||
ClearScope, GetBasePath, ParseTypeChange, Wait, ToJSON,
|
ClearScope, GetBasePath, ParseTypeChange, Wait, ToJSON,
|
||||||
ParseVariableString, Prompt, InitiatePlaybookRun,
|
ParseVariableString, Prompt, InitiatePlaybookRun,
|
||||||
TemplatesService, $state) {
|
TemplatesService, $state, OrgAdminLookup) {
|
||||||
|
|
||||||
// Inject dynamic view
|
// Inject dynamic view
|
||||||
var defaultUrl = GetBasePath('inventory'),
|
var defaultUrl = GetBasePath('inventory'),
|
||||||
@@ -77,6 +77,11 @@ function InventoriesEdit($scope, $rootScope, $compile, $location,
|
|||||||
field_id: 'inventory_variables'
|
field_id: 'inventory_variables'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
OrgAdminLookup.checkForAdminAccess({organization: data.organization})
|
||||||
|
.then(function(canEditOrg){
|
||||||
|
$scope.canEditOrg = canEditOrg;
|
||||||
|
});
|
||||||
|
|
||||||
$scope.inventory_obj = data;
|
$scope.inventory_obj = data;
|
||||||
$scope.name = data.name;
|
$scope.name = data.name;
|
||||||
|
|
||||||
@@ -132,5 +137,5 @@ export default ['$scope', '$rootScope', '$compile', '$location',
|
|||||||
'$log', '$stateParams', 'InventoryForm', 'Rest', 'Alert',
|
'$log', '$stateParams', 'InventoryForm', 'Rest', 'Alert',
|
||||||
'ProcessErrors', 'ClearScope', 'GetBasePath', 'ParseTypeChange', 'Wait',
|
'ProcessErrors', 'ClearScope', 'GetBasePath', 'ParseTypeChange', 'Wait',
|
||||||
'ToJSON', 'ParseVariableString', 'Prompt', 'InitiatePlaybookRun',
|
'ToJSON', 'ParseVariableString', 'Prompt', 'InitiatePlaybookRun',
|
||||||
'TemplatesService', '$state', InventoriesEdit,
|
'TemplatesService', '$state', 'OrgAdminLookup', InventoriesEdit,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ angular.module('inventory', [
|
|||||||
data: {
|
data: {
|
||||||
activityStream: true,
|
activityStream: true,
|
||||||
activityStreamTarget: 'inventory'
|
activityStreamTarget: 'inventory'
|
||||||
|
},
|
||||||
|
ncyBreadcrumb: {
|
||||||
|
label: N_('INVENTORIES')
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
function adhocController($q, $scope, $location, $stateParams,
|
function adhocController($q, $scope, $location, $stateParams,
|
||||||
$state, CheckPasswords, PromptForPasswords, CreateLaunchDialog, CreateSelect2, adhocForm,
|
$state, CheckPasswords, PromptForPasswords, CreateLaunchDialog, CreateSelect2, adhocForm,
|
||||||
GenerateForm, Rest, ProcessErrors, ClearScope, GetBasePath, GetChoices,
|
GenerateForm, Rest, ProcessErrors, ClearScope, GetBasePath, GetChoices,
|
||||||
KindChange, CredentialList, Empty, Wait) {
|
KindChange, CredentialList, ParseTypeChange, Empty, Wait) {
|
||||||
|
|
||||||
ClearScope();
|
ClearScope();
|
||||||
|
|
||||||
@@ -162,6 +162,12 @@ function adhocController($q, $scope, $location, $stateParams,
|
|||||||
|
|
||||||
privateFn.initializeForm(id, urls, hostPattern);
|
privateFn.initializeForm(id, urls, hostPattern);
|
||||||
|
|
||||||
|
// init codemirror
|
||||||
|
$scope.extra_vars = '---';
|
||||||
|
$scope.parseType = 'yaml';
|
||||||
|
$scope.envParseType = 'yaml';
|
||||||
|
ParseTypeChange({ scope: $scope, field_id: 'adhoc_extra_vars' , variable: "extra_vars"});
|
||||||
|
|
||||||
$scope.formCancel = function(){
|
$scope.formCancel = function(){
|
||||||
$state.go('inventoryManage');
|
$state.go('inventoryManage');
|
||||||
};
|
};
|
||||||
@@ -199,6 +205,7 @@ function adhocController($q, $scope, $location, $stateParams,
|
|||||||
"module_args": "",
|
"module_args": "",
|
||||||
"forks": 0,
|
"forks": 0,
|
||||||
"verbosity": 0,
|
"verbosity": 0,
|
||||||
|
"extra_vars": "",
|
||||||
"privilege_escalation": ""
|
"privilege_escalation": ""
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -297,5 +304,5 @@ function adhocController($q, $scope, $location, $stateParams,
|
|||||||
export default ['$q', '$scope', '$location', '$stateParams',
|
export default ['$q', '$scope', '$location', '$stateParams',
|
||||||
'$state', 'CheckPasswords', 'PromptForPasswords', 'CreateLaunchDialog', 'CreateSelect2',
|
'$state', 'CheckPasswords', 'PromptForPasswords', 'CreateLaunchDialog', 'CreateSelect2',
|
||||||
'adhocForm', 'GenerateForm', 'Rest', 'ProcessErrors', 'ClearScope', 'GetBasePath',
|
'adhocForm', 'GenerateForm', 'Rest', 'ProcessErrors', 'ClearScope', 'GetBasePath',
|
||||||
'GetChoices', 'KindChange', 'CredentialList', 'Empty', 'Wait',
|
'GetChoices', 'KindChange', 'CredentialList', 'ParseTypeChange', 'Empty', 'Wait',
|
||||||
adhocController];
|
adhocController];
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* @description This form is for executing an adhoc command
|
* @description This form is for executing an adhoc command
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default function() {
|
export default ['i18n', function(i18n) {
|
||||||
return {
|
return {
|
||||||
addTitle: 'EXECUTE COMMAND',
|
addTitle: 'EXECUTE COMMAND',
|
||||||
name: 'adhoc',
|
name: 'adhoc',
|
||||||
@@ -121,6 +121,23 @@ export default function() {
|
|||||||
dataPlacement: 'right',
|
dataPlacement: 'right',
|
||||||
dataContainer: "body"
|
dataContainer: "body"
|
||||||
},
|
},
|
||||||
|
extra_vars: {
|
||||||
|
label: i18n._('Extra Variables'),
|
||||||
|
type: 'textarea',
|
||||||
|
class: 'Form-textAreaLabel Form-formGroup--fullWidth',
|
||||||
|
rows: 6,
|
||||||
|
"default": "---",
|
||||||
|
column: 2,
|
||||||
|
awPopOver: "<p>" + i18n.sprintf(i18n._("Pass extra command line variables. This is the %s or %s command line parameter " +
|
||||||
|
"for %s. Provide key/value pairs using either YAML or JSON."), '<code>-e</code>', '<code>--extra-vars</code>', '<code>ansible</code>') + "</p>" +
|
||||||
|
"JSON:<br />\n" +
|
||||||
|
"<blockquote>{<br /> \"somevar\": \"somevalue\",<br /> \"password\": \"magic\"<br /> }</blockquote>\n" +
|
||||||
|
"YAML:<br />\n" +
|
||||||
|
"<blockquote>---<br />somevar: somevalue<br />password: magic<br /></blockquote>\n",
|
||||||
|
dataTitle: i18n._('Extra Variables'),
|
||||||
|
dataPlacement: 'right',
|
||||||
|
dataContainer: "body"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
buttons: {
|
buttons: {
|
||||||
reset: {
|
reset: {
|
||||||
@@ -139,4 +156,4 @@ export default function() {
|
|||||||
|
|
||||||
related: {}
|
related: {}
|
||||||
};
|
};
|
||||||
}
|
}];
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="BreadCrumb InventoryManageBreadCrumbs">
|
<div class="BreadCrumb InventoryManageBreadCrumbs">
|
||||||
<ol class="BreadCrumb-list">
|
<ol class="BreadCrumb-list">
|
||||||
<li class="BreadCrumb-item"><a ui-sref="inventories">Inventories</a></li>
|
<li class="BreadCrumb-item"><a ui-sref="inventories">INVENTORIES</a></li>
|
||||||
<li class="BreadCrumb-item BreadCrumb-invItem">
|
<li class="BreadCrumb-item BreadCrumb-invItem">
|
||||||
<a href ng-if="currentState !== 'inventoryManage' || groups.length > 0" ng-click="goToInventory()">{{inventory.name}}</a>
|
<a href ng-if="currentState !== 'inventoryManage' || groups.length > 0" ng-click="goToInventory()">{{inventory.name}}</a>
|
||||||
<span ng-if="currentState === 'inventoryManage' && groups.length === 0">{{inventory.name}}</span>
|
<span ng-if="currentState === 'inventoryManage' && groups.length === 0">{{inventory.name}}</span>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="HostEvent-details--left">
|
<div class="HostEvent-details">
|
||||||
<div class="HostEvent-field">
|
<div class="HostEvent-field">
|
||||||
<span class="HostEvent-field--label">CREATED</span>
|
<span class="HostEvent-field--label">CREATED</span>
|
||||||
<span class="HostEvent-field--content">{{(event.created | longDate) || "No result found"}}</span>
|
<span class="HostEvent-field--content">{{(event.created | longDate) || "No result found"}}</span>
|
||||||
|
|||||||
@@ -99,7 +99,7 @@
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
.HostEvent .modal-body{
|
.HostEvent .modal-body{
|
||||||
max-height: 500px;
|
max-height: 600px;
|
||||||
padding: 0px!important;
|
padding: 0px!important;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
@@ -115,6 +115,7 @@
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
flex: 0 1 80px;
|
flex: 0 1 80px;
|
||||||
max-width: 80px;
|
max-width: 80px;
|
||||||
|
min-width: 80px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
@@ -123,28 +124,10 @@
|
|||||||
}
|
}
|
||||||
.HostEvent-field--content{
|
.HostEvent-field--content{
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
max-width: 13em;
|
|
||||||
flex: 0 1 13em;
|
|
||||||
}
|
}
|
||||||
.HostEvent-field--monospaceContent{
|
.HostEvent-field--monospaceContent{
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
.HostEvent-details--left, .HostEvent-details--right{
|
|
||||||
flex: 1 1 47%;
|
|
||||||
}
|
|
||||||
.HostEvent-details--left{
|
|
||||||
margin-right: 40px;
|
|
||||||
}
|
|
||||||
.HostEvent-details--right{
|
|
||||||
.HostEvent-field--label{
|
|
||||||
flex: 0 1 25em;
|
|
||||||
}
|
|
||||||
.HostEvent-field--content{
|
|
||||||
max-width: 15em;
|
|
||||||
flex: 0 1 15em;
|
|
||||||
align-self: flex-end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.HostEvent-button:disabled {
|
.HostEvent-button:disabled {
|
||||||
pointer-events: all!important;
|
pointer-events: all!important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -213,23 +213,8 @@ job-results-standard-out {
|
|||||||
color: @default-icon;
|
color: @default-icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
.JobResults .CodeMirror.cm-s-default,
|
|
||||||
.JobResults .CodeMirror-line {
|
|
||||||
background-color: #f6f6f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.JobResults .CodeMirror-gutter.CodeMirror-lint-markers,
|
|
||||||
.JobResults .CodeMirror-gutter.CodeMirror-linenumbers {
|
|
||||||
background-color: #ebebeb;
|
|
||||||
color: @b7grey;
|
|
||||||
}
|
|
||||||
|
|
||||||
.JobResults .CodeMirror-lines {
|
.JobResults .CodeMirror-lines {
|
||||||
cursor: default;
|
cursor: not-allowed;
|
||||||
}
|
|
||||||
|
|
||||||
.JobResults .CodeMirror-cursors {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.JobResults-downloadTooLarge {
|
.JobResults-downloadTooLarge {
|
||||||
|
|||||||
@@ -175,18 +175,12 @@
|
|||||||
background-color: @btn-bg-hov;
|
background-color: @btn-bg-hov;
|
||||||
color: @btn-txt;
|
color: @btn-txt;
|
||||||
}
|
}
|
||||||
.JobSubmission-revertButton {
|
|
||||||
background-color: @default-bg;
|
.JobSubmission-revertLink {
|
||||||
color: @default-link;
|
padding-left:10px;
|
||||||
text-transform: uppercase;
|
|
||||||
padding-left:15px;
|
|
||||||
padding-right: 15px;
|
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
.JobSubmission-revertButton:hover{
|
|
||||||
background-color: @default-bg;
|
|
||||||
color: @default-link-hov;
|
|
||||||
}
|
|
||||||
.JobSubmission-selectedItem {
|
.JobSubmission-selectedItem {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
|
|||||||
@@ -316,15 +316,6 @@ export default
|
|||||||
$scope.revertToDefaultInventory = function() {
|
$scope.revertToDefaultInventory = function() {
|
||||||
if($scope.has_default_inventory) {
|
if($scope.has_default_inventory) {
|
||||||
$scope.selected_inventory = angular.copy($scope.defaults.inventory);
|
$scope.selected_inventory = angular.copy($scope.defaults.inventory);
|
||||||
|
|
||||||
// Loop across inventories and set update the "checked" attribute for each row
|
|
||||||
$scope.inventories.forEach(function(row, i) {
|
|
||||||
if (row.id === $scope.selected_inventory.id) {
|
|
||||||
$scope.inventories[i].checked = 1;
|
|
||||||
} else {
|
|
||||||
$scope.inventories[i].checked = 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -332,15 +323,6 @@ export default
|
|||||||
if($scope.has_default_credential) {
|
if($scope.has_default_credential) {
|
||||||
$scope.selected_credential = angular.copy($scope.defaults.credential);
|
$scope.selected_credential = angular.copy($scope.defaults.credential);
|
||||||
updateRequiredPasswords();
|
updateRequiredPasswords();
|
||||||
|
|
||||||
// Loop across credentials and set update the "checked" attribute for each row
|
|
||||||
$scope.credentials.forEach(function(row, i) {
|
|
||||||
if (row.id === $scope.selected_credential.id) {
|
|
||||||
$scope.credentials[i].checked = 1;
|
|
||||||
} else {
|
|
||||||
$scope.credentials[i].checked = 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -26,10 +26,10 @@
|
|||||||
<span class="JobSubmission-selectedItemNone" ng-show="!selected_inventory">None selected</span>
|
<span class="JobSubmission-selectedItemNone" ng-show="!selected_inventory">None selected</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="JobSubmission-selectedItemRevert" ng-if="ask_inventory_on_launch && has_default_inventory">
|
<div class="JobSubmission-selectedItemRevert" ng-if="ask_inventory_on_launch && has_default_inventory">
|
||||||
<button class="btn btn-xs JobSubmission-revertButton" ng-hide="selected_inventory.id === defaults.inventory.id" ng-click="revertToDefaultInventory()">REVERT TO DEFAULT</button>
|
<a class="Form-labelAction JobSubmission-revertLink" href="" ng-hide="selected_inventory.id === defaults.inventory.id" ng-click="revertToDefaultInventory()">REVERT</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<job-sub-inv-list ng-if="includeInventoryList"></job-sub-inv-list>
|
<job-sub-inv-list ng-if="includeInventoryList" selected-inventory="$parent.selected_inventory"></job-sub-inv-list>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ng-if="ask_credential_on_launch || password_needed" ng-show="step === 'credential'" class="JobSubmission-form">
|
<div ng-if="ask_credential_on_launch || password_needed" ng-show="step === 'credential'" class="JobSubmission-form">
|
||||||
@@ -41,10 +41,10 @@
|
|||||||
<span class="JobSubmission-selectedItemNone" ng-show="!selected_credential">None selected</span>
|
<span class="JobSubmission-selectedItemNone" ng-show="!selected_credential">None selected</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="JobSubmission-selectedItemRevert" ng-if="ask_credential_on_launch && has_default_credential">
|
<div class="JobSubmission-selectedItemRevert" ng-if="ask_credential_on_launch && has_default_credential">
|
||||||
<button class="btn btn-xs JobSubmission-revertButton" ng-hide="selected_credential.id === defaults.credential.id" ng-click="revertToDefaultCredential()">REVERT TO DEFAULT</button>
|
<a class="Form-labelAction JobSubmission-revertLink" href="" ng-hide="selected_credential.id === defaults.credential.id" ng-click="revertToDefaultCredential()">REVERT</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<job-sub-cred-list ng-if="includeCredentialList"></job-sub-cred-list>
|
<job-sub-cred-list ng-if="includeCredentialList" selected-credential="$parent.selected_credential"></job-sub-cred-list>
|
||||||
<div ng-show="ssh_password_required || ssh_key_unlock_required || become_password_required || vault_password_required">
|
<div ng-show="ssh_password_required || ssh_key_unlock_required || become_password_required || vault_password_required">
|
||||||
<div class="JobSubmission-instructions">Launching this job requires the passwords listed below. Enter and confirm each password before continuing.</div>
|
<div class="JobSubmission-instructions">Launching this job requires the passwords listed below. Enter and confirm each password before continuing.</div>
|
||||||
<form name="forms.credentialpasswords" autocomplete="off" novalidate>
|
<form name="forms.credentialpasswords" autocomplete="off" novalidate>
|
||||||
|
|||||||
@@ -7,13 +7,17 @@
|
|||||||
import jobSubCredListController from './job-sub-cred-list.controller';
|
import jobSubCredListController from './job-sub-cred-list.controller';
|
||||||
|
|
||||||
export default [ 'templateUrl', 'QuerySet', 'GetBasePath', 'generateList', '$compile', 'CredentialList',
|
export default [ 'templateUrl', 'QuerySet', 'GetBasePath', 'generateList', '$compile', 'CredentialList',
|
||||||
function(templateUrl, qs, GetBasePath, GenerateList, $compile, CredentialList) {
|
(templateUrl, qs, GetBasePath, GenerateList, $compile, CredentialList) => {
|
||||||
return {
|
return {
|
||||||
scope: {},
|
scope: {
|
||||||
|
selectedCredential: '='
|
||||||
|
},
|
||||||
templateUrl: templateUrl('job-submission/lists/credential/job-sub-cred-list'),
|
templateUrl: templateUrl('job-submission/lists/credential/job-sub-cred-list'),
|
||||||
controller: jobSubCredListController,
|
controller: jobSubCredListController,
|
||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
link: function(scope) {
|
link: scope => {
|
||||||
|
let toDestroy = [];
|
||||||
|
|
||||||
scope.credential_default_params = {
|
scope.credential_default_params = {
|
||||||
order_by: 'name',
|
order_by: 'name',
|
||||||
page_size: 5,
|
page_size: 5,
|
||||||
@@ -28,11 +32,11 @@ export default [ 'templateUrl', 'QuerySet', 'GetBasePath', 'generateList', '$com
|
|||||||
|
|
||||||
// Fire off the initial search
|
// Fire off the initial search
|
||||||
qs.search(GetBasePath('credentials'), scope.credential_default_params)
|
qs.search(GetBasePath('credentials'), scope.credential_default_params)
|
||||||
.then(function(res) {
|
.then(res => {
|
||||||
scope.credential_dataset = res.data;
|
scope.credential_dataset = res.data;
|
||||||
scope.credentials = scope.credential_dataset.results;
|
scope.credentials = scope.credential_dataset.results;
|
||||||
|
|
||||||
var credList = _.cloneDeep(CredentialList);
|
let credList = _.cloneDeep(CredentialList);
|
||||||
let html = GenerateList.build({
|
let html = GenerateList.build({
|
||||||
list: credList,
|
list: credList,
|
||||||
input_type: 'radio',
|
input_type: 'radio',
|
||||||
@@ -43,11 +47,11 @@ export default [ 'templateUrl', 'QuerySet', 'GetBasePath', 'generateList', '$com
|
|||||||
|
|
||||||
$('#job-submission-credential-lookup').append($compile(html)(scope));
|
$('#job-submission-credential-lookup').append($compile(html)(scope));
|
||||||
|
|
||||||
scope.$watchCollection('credentials', function () {
|
toDestroy.push(scope.$watchCollection('selectedCredential', () => {
|
||||||
if(scope.selected_credential) {
|
if(scope.selectedCredential) {
|
||||||
// Loop across the inventories and see if one of them should be "checked"
|
// Loop across the inventories and see if one of them should be "checked"
|
||||||
scope.credentials.forEach(function(row, i) {
|
scope.credentials.forEach((row, i) => {
|
||||||
if (row.id === scope.selected_credential.id) {
|
if (row.id === scope.selectedCredential.id) {
|
||||||
scope.credentials[i].checked = 1;
|
scope.credentials[i].checked = 1;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -55,9 +59,10 @@ export default [ 'templateUrl', 'QuerySet', 'GetBasePath', 'generateList', '$com
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
scope.$on('$destroy', () => toDestroy.forEach(watcher => watcher()));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}];
|
}];
|
||||||
|
|||||||
@@ -7,13 +7,17 @@
|
|||||||
import jobSubInvListController from './job-sub-inv-list.controller';
|
import jobSubInvListController from './job-sub-inv-list.controller';
|
||||||
|
|
||||||
export default [ 'templateUrl', 'QuerySet', 'GetBasePath', 'generateList', '$compile', 'InventoryList',
|
export default [ 'templateUrl', 'QuerySet', 'GetBasePath', 'generateList', '$compile', 'InventoryList',
|
||||||
function(templateUrl, qs, GetBasePath, GenerateList, $compile, InventoryList) {
|
(templateUrl, qs, GetBasePath, GenerateList, $compile, InventoryList) => {
|
||||||
return {
|
return {
|
||||||
scope: {},
|
scope: {
|
||||||
|
selectedInventory: '='
|
||||||
|
},
|
||||||
templateUrl: templateUrl('job-submission/lists/inventory/job-sub-inv-list'),
|
templateUrl: templateUrl('job-submission/lists/inventory/job-sub-inv-list'),
|
||||||
controller: jobSubInvListController,
|
controller: jobSubInvListController,
|
||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
link: function(scope) {
|
link: scope => {
|
||||||
|
let toDestroy = [];
|
||||||
|
|
||||||
scope.inventory_default_params = {
|
scope.inventory_default_params = {
|
||||||
order_by: 'name',
|
order_by: 'name',
|
||||||
page_size: 5
|
page_size: 5
|
||||||
@@ -26,11 +30,11 @@ export default [ 'templateUrl', 'QuerySet', 'GetBasePath', 'generateList', '$com
|
|||||||
|
|
||||||
// Fire off the initial search
|
// Fire off the initial search
|
||||||
qs.search(GetBasePath('inventory'), scope.inventory_default_params)
|
qs.search(GetBasePath('inventory'), scope.inventory_default_params)
|
||||||
.then(function(res) {
|
.then(res => {
|
||||||
scope.inventory_dataset = res.data;
|
scope.inventory_dataset = res.data;
|
||||||
scope.inventories = scope.inventory_dataset.results;
|
scope.inventories = scope.inventory_dataset.results;
|
||||||
|
|
||||||
var invList = _.cloneDeep(InventoryList);
|
let invList = _.cloneDeep(InventoryList);
|
||||||
let html = GenerateList.build({
|
let html = GenerateList.build({
|
||||||
list: invList,
|
list: invList,
|
||||||
input_type: 'radio',
|
input_type: 'radio',
|
||||||
@@ -41,11 +45,11 @@ export default [ 'templateUrl', 'QuerySet', 'GetBasePath', 'generateList', '$com
|
|||||||
|
|
||||||
$('#job-submission-inventory-lookup').append($compile(html)(scope));
|
$('#job-submission-inventory-lookup').append($compile(html)(scope));
|
||||||
|
|
||||||
scope.$watchCollection('inventories', function () {
|
toDestroy.push(scope.$watchCollection('selectedInventory', () => {
|
||||||
if(scope.selected_inventory) {
|
if(scope.selectedInventory) {
|
||||||
// Loop across the inventories and see if one of them should be "checked"
|
// Loop across the inventories and see if one of them should be "checked"
|
||||||
scope.inventories.forEach(function(row, i) {
|
scope.inventories.forEach((row, i) => {
|
||||||
if (row.id === scope.selected_inventory.id) {
|
if (row.id === scope.selectedInventory.id) {
|
||||||
scope.inventories[i].checked = 1;
|
scope.inventories[i].checked = 1;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -53,8 +57,10 @@ export default [ 'templateUrl', 'QuerySet', 'GetBasePath', 'generateList', '$com
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
scope.$on('$destroy', () => toDestroy.forEach(watcher => watcher()));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}];
|
}];
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
*************************************************/
|
*************************************************/
|
||||||
|
|
||||||
export default
|
export default
|
||||||
['$state', '$rootScope', 'Rest', 'GetBasePath', 'ProcessErrors', '$q',
|
['$state', '$rootScope', 'Rest', 'GetBasePath', 'ProcessErrors',
|
||||||
'ConfigService',
|
'ConfigService',
|
||||||
function($state, $rootScope, Rest, GetBasePath, ProcessErrors, $q,
|
function($state, $rootScope, Rest, GetBasePath, ProcessErrors,
|
||||||
ConfigService){
|
ConfigService){
|
||||||
return {
|
return {
|
||||||
get: function() {
|
get: function() {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export default ['$scope', '$location', '$stateParams', 'GenerateForm',
|
|||||||
init();
|
init();
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
|
$scope.canEditOrg = true;
|
||||||
Rest.setUrl(GetBasePath('projects'));
|
Rest.setUrl(GetBasePath('projects'));
|
||||||
Rest.options()
|
Rest.options()
|
||||||
.success(function(data) {
|
.success(function(data) {
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ export default ['i18n', 'NotificationsList', function(i18n, NotificationsList) {
|
|||||||
required: true,
|
required: true,
|
||||||
dataContainer: 'body',
|
dataContainer: 'body',
|
||||||
dataPlacement: 'right',
|
dataPlacement: 'right',
|
||||||
ngDisabled: '!(project_obj.summary_fields.user_capabilities.edit || canAdd)'
|
ngDisabled: '!(project_obj.summary_fields.user_capabilities.edit || canAdd) || !canEditOrg',
|
||||||
|
awLookupWhen: '(project_obj.summary_fields.user_capabilities.edit || canAdd) && canEditOrg'
|
||||||
},
|
},
|
||||||
scm_type: {
|
scm_type: {
|
||||||
label: i18n._('SCM Type'),
|
label: i18n._('SCM Type'),
|
||||||
@@ -134,6 +135,11 @@ export default ['i18n', 'NotificationsList', function(i18n, NotificationsList) {
|
|||||||
search: {
|
search: {
|
||||||
kind: 'scm'
|
kind: 'scm'
|
||||||
},
|
},
|
||||||
|
autopopulateLookup: false,
|
||||||
|
awRequiredWhen: {
|
||||||
|
reqExpression: "credRequired",
|
||||||
|
init: false
|
||||||
|
},
|
||||||
ngShow: "scm_type && scm_type.value !== 'manual'",
|
ngShow: "scm_type && scm_type.value !== 'manual'",
|
||||||
sourceModel: 'credential',
|
sourceModel: 'credential',
|
||||||
awLookupType: 'scm_credential',
|
awLookupType: 'scm_credential',
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ export default ['i18n', function(i18n) {
|
|||||||
awToolTip: "{{ project.scm_schedule_tooltip }}",
|
awToolTip: "{{ project.scm_schedule_tooltip }}",
|
||||||
ngClass: "project.scm_type_class",
|
ngClass: "project.scm_type_class",
|
||||||
dataPlacement: 'top',
|
dataPlacement: 'top',
|
||||||
|
ngShow: "project.summary_fields.user_capabilities.schedule"
|
||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
ngClick: "editProject(project.id)",
|
ngClick: "editProject(project.id)",
|
||||||
|
|||||||
@@ -493,7 +493,8 @@ function(ConfigurationUtils, i18n, $rootScope) {
|
|||||||
modelName = attrs.source,
|
modelName = attrs.source,
|
||||||
lookupType = attrs.awlookuptype,
|
lookupType = attrs.awlookuptype,
|
||||||
watcher = attrs.awRequiredWhen || undefined,
|
watcher = attrs.awRequiredWhen || undefined,
|
||||||
watchBasePath;
|
watchBasePath,
|
||||||
|
awLookupWhen = attrs.awLookupWhen;
|
||||||
|
|
||||||
if (attrs.autopopulatelookup !== undefined) {
|
if (attrs.autopopulatelookup !== undefined) {
|
||||||
autopopulateLookup = JSON.parse(attrs.autopopulatelookup);
|
autopopulateLookup = JSON.parse(attrs.autopopulatelookup);
|
||||||
@@ -501,7 +502,6 @@ function(ConfigurationUtils, i18n, $rootScope) {
|
|||||||
autopopulateLookup = true;
|
autopopulateLookup = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// The following block of code is for instances where the
|
// The following block of code is for instances where the
|
||||||
// lookup field is reused by varying sub-forms. Example: The groups
|
// lookup field is reused by varying sub-forms. Example: The groups
|
||||||
// form will change it's credential lookup based on the
|
// form will change it's credential lookup based on the
|
||||||
@@ -602,7 +602,12 @@ function(ConfigurationUtils, i18n, $rootScope) {
|
|||||||
// form.$pending will contain object reference to any ngModelControllers with outstanding requests
|
// form.$pending will contain object reference to any ngModelControllers with outstanding requests
|
||||||
fieldCtrl.$asyncValidators.validResource = function(modelValue, viewValue) {
|
fieldCtrl.$asyncValidators.validResource = function(modelValue, viewValue) {
|
||||||
|
|
||||||
applyValidationStrategy(viewValue, fieldCtrl);
|
if(awLookupWhen === undefined || (awLookupWhen !== undefined && Boolean(scope.$eval(awLookupWhen)) === true)) {
|
||||||
|
applyValidationStrategy(viewValue, fieldCtrl);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
defer.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
return defer.promise;
|
return defer.promise;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1388,6 +1388,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
|||||||
html += (field.autopopulateLookup !== undefined) ? ` autopopulateLookup=${field.autopopulateLookup} ` : "";
|
html += (field.autopopulateLookup !== undefined) ? ` autopopulateLookup=${field.autopopulateLookup} ` : "";
|
||||||
html += (field.watchBasePath !== undefined) ? ` watchBasePath=${field.watchBasePath} ` : "";
|
html += (field.watchBasePath !== undefined) ? ` watchBasePath=${field.watchBasePath} ` : "";
|
||||||
html += `ng-model-options="{ updateOn: 'default blur', debounce: { 'default': 300, 'blur': 0 } }"`;
|
html += `ng-model-options="{ updateOn: 'default blur', debounce: { 'default': 300, 'blur': 0 } }"`;
|
||||||
|
html += (field.awLookupWhen !== undefined) ? this.attr(field, 'awLookupWhen') : "";
|
||||||
html += " awlookup >\n";
|
html += " awlookup >\n";
|
||||||
html += "</div>\n";
|
html += "</div>\n";
|
||||||
|
|
||||||
|
|||||||
@@ -86,6 +86,9 @@ angular.module('GeneratorHelpers', [systemStatus.name])
|
|||||||
result += value;
|
result += value;
|
||||||
result += '"';
|
result += '"';
|
||||||
break;
|
break;
|
||||||
|
case 'awLookupWhen':
|
||||||
|
result = "ng-attr-awlookup=\"" + value + "\" ";
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
result = key + "=\"" + value + "\" ";
|
result = key + "=\"" + value + "\" ";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,13 +29,14 @@ import config from './config/main';
|
|||||||
import PromptDialog from './prompt-dialog';
|
import PromptDialog from './prompt-dialog';
|
||||||
import directives from './directives';
|
import directives from './directives';
|
||||||
import features from './features/main';
|
import features from './features/main';
|
||||||
|
import orgAdminLookup from './org-admin-lookup/main';
|
||||||
import 'angular-duration-format';
|
import 'angular-duration-format';
|
||||||
|
|
||||||
export default
|
export default
|
||||||
angular.module('shared', [listGenerator.name,
|
angular.module('shared', [listGenerator.name,
|
||||||
formGenerator.name,
|
formGenerator.name,
|
||||||
lookupModal.name,
|
lookupModal.name,
|
||||||
smartSearch.name,
|
smartSearch.name,
|
||||||
paginate.name,
|
paginate.name,
|
||||||
columnSort.name,
|
columnSort.name,
|
||||||
filters.name,
|
filters.name,
|
||||||
@@ -55,6 +56,7 @@ angular.module('shared', [listGenerator.name,
|
|||||||
directives.name,
|
directives.name,
|
||||||
filters.name,
|
filters.name,
|
||||||
features.name,
|
features.name,
|
||||||
|
orgAdminLookup.name,
|
||||||
require('angular-cookies'),
|
require('angular-cookies'),
|
||||||
'angular-duration-format'
|
'angular-duration-format'
|
||||||
])
|
])
|
||||||
|
|||||||
11
awx/ui/client/src/shared/org-admin-lookup/main.js
Normal file
11
awx/ui/client/src/shared/org-admin-lookup/main.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/*************************************************
|
||||||
|
* Copyright (c) 2017 Ansible, Inc.
|
||||||
|
*
|
||||||
|
* All Rights Reserved
|
||||||
|
*************************************************/
|
||||||
|
|
||||||
|
import OrgAdminLookupFactory from './org-admin-lookup.factory';
|
||||||
|
|
||||||
|
export default
|
||||||
|
angular.module('orgAdminLookup', [])
|
||||||
|
.service('OrgAdminLookup', OrgAdminLookupFactory);
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/*************************************************
|
||||||
|
* Copyright (c) 2017 Ansible, Inc.
|
||||||
|
*
|
||||||
|
* All Rights Reserved
|
||||||
|
*************************************************/
|
||||||
|
|
||||||
|
export default
|
||||||
|
['Rest', 'Authorization', 'GetBasePath', '$rootScope', '$q',
|
||||||
|
function(Rest, Authorization, GetBasePath, $rootScope, $q){
|
||||||
|
return {
|
||||||
|
checkForAdminAccess: function(params) {
|
||||||
|
// params.organization - id of the organization in question
|
||||||
|
var deferred = $q.defer();
|
||||||
|
if(Authorization.getUserInfo('is_superuser') !== true) {
|
||||||
|
Rest.setUrl(GetBasePath('users') + $rootScope.current_user.id + '/admin_of_organizations');
|
||||||
|
Rest.get({ params: { id: params.organization } })
|
||||||
|
.success(function(data) {
|
||||||
|
if(data.count && data.count > 0) {
|
||||||
|
deferred.resolve(true);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
deferred.resolve(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
deferred.resolve(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
@@ -150,7 +150,7 @@ export default ['$injector', '$stateExtender', '$log', 'i18n', function($injecto
|
|||||||
url: url,
|
url: url,
|
||||||
ncyBreadcrumb: {
|
ncyBreadcrumb: {
|
||||||
[params.parent ? 'parent' : null]: `${params.parent}`,
|
[params.parent ? 'parent' : null]: `${params.parent}`,
|
||||||
label: i18n.sprintf(i18n._("CREATE %s"), i18n._(`${form.breadcrumbName || form.name}`))
|
label: i18n.sprintf(i18n._("CREATE %s"), i18n._(`${form.breadcrumbName || form.name.toUpperCase()}`))
|
||||||
},
|
},
|
||||||
views: {
|
views: {
|
||||||
'form': {
|
'form': {
|
||||||
@@ -386,14 +386,15 @@ export default ['$injector', '$stateExtender', '$log', 'i18n', function($injecto
|
|||||||
|
|
||||||
function buildNotificationState(field) {
|
function buildNotificationState(field) {
|
||||||
let state,
|
let state,
|
||||||
list = field.include ? $injector.get(field.include) : field;
|
list = field.include ? $injector.get(field.include) : field,
|
||||||
|
breadcrumbLabel = (field.iterator.replace('_', ' ') + 's').toUpperCase();
|
||||||
state = $stateExtender.buildDefinition({
|
state = $stateExtender.buildDefinition({
|
||||||
searchPrefix: `${list.iterator}`,
|
searchPrefix: `${list.iterator}`,
|
||||||
name: `${formStateDefinition.name}.${list.iterator}s`,
|
name: `${formStateDefinition.name}.${list.iterator}s`,
|
||||||
url: `/${list.iterator}s`,
|
url: `/${list.iterator}s`,
|
||||||
ncyBreadcrumb: {
|
ncyBreadcrumb: {
|
||||||
parent: `${formStateDefinition.name}`,
|
parent: `${formStateDefinition.name}`,
|
||||||
label: `${field.iterator}s`
|
label: `${breadcrumbLabel}`
|
||||||
},
|
},
|
||||||
params: {
|
params: {
|
||||||
[list.iterator + '_search']: {
|
[list.iterator + '_search']: {
|
||||||
@@ -581,14 +582,14 @@ export default ['$injector', '$stateExtender', '$log', 'i18n', function($injecto
|
|||||||
list = field.include ? $injector.get(field.include) : field,
|
list = field.include ? $injector.get(field.include) : field,
|
||||||
// Added this line specifically for Completed Jobs but should be OK
|
// Added this line specifically for Completed Jobs but should be OK
|
||||||
// for all the rest of the related tabs
|
// for all the rest of the related tabs
|
||||||
breadcrumbLabel = field.iterator.replace('_', ' '),
|
breadcrumbLabel = (field.iterator.replace('_', ' ') + 's').toUpperCase(),
|
||||||
stateConfig = {
|
stateConfig = {
|
||||||
searchPrefix: `${list.iterator}`,
|
searchPrefix: `${list.iterator}`,
|
||||||
name: `${formStateDefinition.name}.${list.iterator}s`,
|
name: `${formStateDefinition.name}.${list.iterator}s`,
|
||||||
url: `/${list.iterator}s`,
|
url: `/${list.iterator}s`,
|
||||||
ncyBreadcrumb: {
|
ncyBreadcrumb: {
|
||||||
parent: `${formStateDefinition.name}`,
|
parent: `${formStateDefinition.name}`,
|
||||||
label: `${breadcrumbLabel}s`
|
label: `${breadcrumbLabel}`
|
||||||
},
|
},
|
||||||
params: {
|
params: {
|
||||||
[list.iterator + '_search']: {
|
[list.iterator + '_search']: {
|
||||||
|
|||||||
@@ -99,6 +99,26 @@
|
|||||||
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4" translate>Verbosity</div>
|
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4" translate>Verbosity</div>
|
||||||
<div class="StandardOut-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">{{ verbosity }}</div>
|
<div class="StandardOut-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">{{ verbosity }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="StandardOut-detailsRow" ng-show="job.extra_vars">
|
||||||
|
<div class="StandardOut-detailsLabel col-lg-3 col-md-3 col-sm-3 col-xs-4" translate>
|
||||||
|
Extra Variables
|
||||||
|
<i class="StandardOut-extraVarsHelp fa fa-question-circle"
|
||||||
|
aw-tool-tip="Read only view of extra variables added to the ad-hoc command."
|
||||||
|
data-placement="top">
|
||||||
|
</i>
|
||||||
|
</div>
|
||||||
|
<div class="StandardOut-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">
|
||||||
|
<textarea
|
||||||
|
rows="6"
|
||||||
|
ng-model="extra_vars"
|
||||||
|
name="variables"
|
||||||
|
class="StandardOut-extraVars"
|
||||||
|
id="pre-formatted-variables">
|
||||||
|
</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -149,7 +149,11 @@ export function JobStdoutController ($rootScope, $scope, $state, $stateParams,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (data.extra_vars) {
|
if (data.extra_vars) {
|
||||||
ParseTypeChange({ scope: $scope, field_id: 'pre-formatted-variables' });
|
ParseTypeChange({
|
||||||
|
scope: $scope,
|
||||||
|
field_id: 'pre-formatted-variables',
|
||||||
|
readOnly: true
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($scope.job.type === 'inventory_update' && !$scope.inv_manage_group_link) {
|
if ($scope.job.type === 'inventory_update' && !$scope.inv_manage_group_link) {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export default ['$scope', '$rootScope', '$stateParams', 'TeamForm', 'GenerateFor
|
|||||||
init();
|
init();
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
|
$scope.canEditOrg = true;
|
||||||
// apply form definition's default field values
|
// apply form definition's default field values
|
||||||
GenerateForm.applyDefaults(form, $scope);
|
GenerateForm.applyDefaults(form, $scope);
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
generator = GenerateForm;
|
generator = GenerateForm;
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
|
$scope.canEditOrg = true;
|
||||||
$scope.parseType = 'yaml';
|
$scope.parseType = 'yaml';
|
||||||
$scope.can_edit = true;
|
$scope.can_edit = true;
|
||||||
// apply form definition's default field values
|
// apply form definition's default field values
|
||||||
|
|||||||
@@ -8,12 +8,12 @@
|
|||||||
[ '$scope', '$stateParams', 'WorkflowForm', 'GenerateForm', 'Alert', 'ProcessErrors',
|
[ '$scope', '$stateParams', 'WorkflowForm', 'GenerateForm', 'Alert', 'ProcessErrors',
|
||||||
'ClearScope', 'GetBasePath', '$q', 'ParseTypeChange', 'Wait', 'Empty',
|
'ClearScope', 'GetBasePath', '$q', 'ParseTypeChange', 'Wait', 'Empty',
|
||||||
'ToJSON', 'initSurvey', '$state', 'CreateSelect2', 'ParseVariableString',
|
'ToJSON', 'initSurvey', '$state', 'CreateSelect2', 'ParseVariableString',
|
||||||
'TemplatesService', 'OrganizationList', 'Rest', 'WorkflowService', 'ToggleNotification',
|
'TemplatesService', 'OrganizationList', 'Rest', 'WorkflowService', 'ToggleNotification', 'OrgAdminLookup',
|
||||||
function(
|
function(
|
||||||
$scope, $stateParams, WorkflowForm, GenerateForm, Alert, ProcessErrors,
|
$scope, $stateParams, WorkflowForm, GenerateForm, Alert, ProcessErrors,
|
||||||
ClearScope, GetBasePath, $q, ParseTypeChange, Wait, Empty,
|
ClearScope, GetBasePath, $q, ParseTypeChange, Wait, Empty,
|
||||||
ToJSON, SurveyControllerInit, $state, CreateSelect2, ParseVariableString,
|
ToJSON, SurveyControllerInit, $state, CreateSelect2, ParseVariableString,
|
||||||
TemplatesService, OrganizationList, Rest, WorkflowService, ToggleNotification
|
TemplatesService, OrganizationList, Rest, WorkflowService, ToggleNotification, OrgAdminLookup
|
||||||
) {
|
) {
|
||||||
|
|
||||||
ClearScope();
|
ClearScope();
|
||||||
@@ -145,6 +145,17 @@
|
|||||||
workflowJobTemplateData.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField];
|
workflowJobTemplateData.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(workflowJobTemplateData.organization) {
|
||||||
|
OrgAdminLookup.checkForAdminAccess({organization: workflowJobTemplateData.organization})
|
||||||
|
.then(function(canEditOrg){
|
||||||
|
$scope.canEditOrg = canEditOrg;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$scope.canEditOrg = true;
|
||||||
|
}
|
||||||
|
|
||||||
Wait('stop');
|
Wait('stop');
|
||||||
$scope.url = workflowJobTemplateData.url;
|
$scope.url = workflowJobTemplateData.url;
|
||||||
$scope.survey_enabled = workflowJobTemplateData.survey_enabled;
|
$scope.survey_enabled = workflowJobTemplateData.survey_enabled;
|
||||||
|
|||||||
@@ -347,6 +347,20 @@ export default [ '$state','moment', '$timeout', '$window',
|
|||||||
if(!d.isStartNode) {
|
if(!d.isStartNode) {
|
||||||
let resourceName = (d.unifiedJobTemplate && d.unifiedJobTemplate.name) ? d.unifiedJobTemplate.name : "";
|
let resourceName = (d.unifiedJobTemplate && d.unifiedJobTemplate.name) ? d.unifiedJobTemplate.name : "";
|
||||||
if(resourceName && resourceName.length > maxNodeTextLength) {
|
if(resourceName && resourceName.length > maxNodeTextLength) {
|
||||||
|
// When the graph is initially rendered all the links come after the nodes (when you look at the dom).
|
||||||
|
// SVG components are painted in order of appearance. There is no concept of z-index, only the order.
|
||||||
|
// As such, we need to move the nodes after the links so that when the tooltip renders it shows up on top
|
||||||
|
// of the links and not underneath them. I tried rendering the links before the nodes but that lead to
|
||||||
|
// some weird link animation that I didn't care to try to fix.
|
||||||
|
svgGroup.selectAll("g.node").each(function() {
|
||||||
|
this.parentNode.appendChild(this);
|
||||||
|
});
|
||||||
|
// After the nodes have been properly placed after the links, we need to make sure that the node that
|
||||||
|
// the user is hovering over is at the very end of the list. This way the tooltip will appear on top
|
||||||
|
// of all other nodes.
|
||||||
|
svgGroup.selectAll("g.node").sort(function (a) {
|
||||||
|
return (a.id !== d.id) ? -1 : 1;
|
||||||
|
});
|
||||||
// Render the tooltip quickly in the dom and then remove. This lets us know how big the tooltip is so that we can place
|
// Render the tooltip quickly in the dom and then remove. This lets us know how big the tooltip is so that we can place
|
||||||
// it properly on the workflow
|
// it properly on the workflow
|
||||||
let tooltipDimensionChecker = $("<div style='visibility:hidden;font-size:12px;position:absolute;' class='WorkflowChart-tooltipContents'><span>" + resourceName + "</span></div>");
|
let tooltipDimensionChecker = $("<div style='visibility:hidden;font-size:12px;position:absolute;' class='WorkflowChart-tooltipContents'><span>" + resourceName + "</span></div>");
|
||||||
|
|||||||
@@ -146,21 +146,6 @@
|
|||||||
color: @default-icon;
|
color: @default-icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
.WorkflowResults .CodeMirror.cm-s-default,
|
|
||||||
.WorkflowResults .CodeMirror-line {
|
|
||||||
background-color: #f6f6f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.WorkflowResults .CodeMirror-gutter.CodeMirror-lint-markers,
|
|
||||||
.WorkflowResults .CodeMirror-gutter.CodeMirror-linenumbers {
|
|
||||||
background-color: #ebebeb;
|
|
||||||
color: @b7grey;
|
|
||||||
}
|
|
||||||
|
|
||||||
.WorkflowResults .CodeMirror-lines {
|
.WorkflowResults .CodeMirror-lines {
|
||||||
cursor: default;
|
cursor: not-allowed;
|
||||||
}
|
|
||||||
|
|
||||||
.WorkflowResults .CodeMirror-cursors {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,3 +49,5 @@ slackclient==1.0.2
|
|||||||
twilio==5.6.0
|
twilio==5.6.0
|
||||||
uWSGI==2.0.14
|
uWSGI==2.0.14
|
||||||
xmltodict==0.10.2
|
xmltodict==0.10.2
|
||||||
|
pip==8.1.2
|
||||||
|
setuptools==23.0.0
|
||||||
|
|||||||
@@ -197,4 +197,5 @@ xmltodict==0.10.2
|
|||||||
zope.interface==4.3.3 # via twisted
|
zope.interface==4.3.3 # via twisted
|
||||||
|
|
||||||
# The following packages are considered to be unsafe in a requirements file:
|
# The following packages are considered to be unsafe in a requirements file:
|
||||||
# setuptools # via cryptography, django-polymorphic, python-ldap, zope.interface
|
pip==8.1.2
|
||||||
|
setuptools==23.0.0
|
||||||
|
|||||||
@@ -11,3 +11,5 @@ pyvmomi==6.5
|
|||||||
pywinrm[kerberos]==0.2.2
|
pywinrm[kerberos]==0.2.2
|
||||||
secretstorage==2.3.1
|
secretstorage==2.3.1
|
||||||
shade==1.13.1
|
shade==1.13.1
|
||||||
|
setuptools==23.0.0
|
||||||
|
pip==8.1.2
|
||||||
|
|||||||
@@ -128,4 +128,5 @@ wrapt==1.10.8 # via debtcollector, positional
|
|||||||
xmltodict==0.10.2 # via pywinrm
|
xmltodict==0.10.2 # via pywinrm
|
||||||
|
|
||||||
# The following packages are considered to be unsafe in a requirements file:
|
# The following packages are considered to be unsafe in a requirements file:
|
||||||
# setuptools # via cryptography
|
pip==8.1.2
|
||||||
|
setuptools==23.0.0
|
||||||
|
|||||||
5
requirements/requirements_setup_requires.txt
Normal file
5
requirements/requirements_setup_requires.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
pbr>=1.8
|
||||||
|
setuptools_scm>=1.15.0
|
||||||
|
vcversioner>=2.16.0.0
|
||||||
|
pytest-runner
|
||||||
|
isort
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
FROM logstash:5-alpine
|
FROM logstash:5-alpine
|
||||||
COPY logstash.conf /
|
COPY logstash.conf /
|
||||||
|
RUN touch /logstash.log
|
||||||
|
RUN chown logstash:logstash /logstash.log
|
||||||
CMD ["-f", "/logstash.conf"]
|
CMD ["-f", "/logstash.conf"]
|
||||||
|
|||||||
@@ -15,5 +15,8 @@ filter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
output {
|
output {
|
||||||
stdout { codec => rubydebug }
|
stdout { codec => rubydebug }
|
||||||
|
file {
|
||||||
|
path => "/logstash.log"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
---
|
---
|
||||||
version: '2'
|
version: '3'
|
||||||
services:
|
services:
|
||||||
unit-tests:
|
unit-tests:
|
||||||
build:
|
build:
|
||||||
context: ../../../
|
context: ../../../
|
||||||
dockerfile: tools/docker-compose/unit-tests/Dockerfile
|
dockerfile: tools/docker-compose/unit-tests/Dockerfile
|
||||||
image: gcr.io/ansible-tower-engineering/unit-test-runner:${GIT_BRANCH}
|
image: gcr.io/ansible-tower-engineering/unit-test-runner:${GIT_BRANCH:-latest}
|
||||||
environment:
|
environment:
|
||||||
SWIG_FEATURES: "-cpperraswarn -includeall -I/usr/include/openssl"
|
SWIG_FEATURES: "-cpperraswarn -includeall -I/usr/include/openssl"
|
||||||
TEST_DIRS: awx/main/tests/functional awx/main/tests/unit awx/conf/tests awx/sso/tests
|
TEST_DIRS: awx/main/tests/functional awx/main/tests/unit awx/conf/tests awx/sso/tests
|
||||||
|
|||||||
8
tox.ini
8
tox.ini
@@ -56,6 +56,14 @@ deps =
|
|||||||
commands =
|
commands =
|
||||||
make UI_TEST_MODE=CI test-ui
|
make UI_TEST_MODE=CI test-ui
|
||||||
|
|
||||||
|
[testenv:ansible]
|
||||||
|
deps =
|
||||||
|
ansible
|
||||||
|
pytest
|
||||||
|
-r{toxinidir}/requirements/requirements_ansible.txt
|
||||||
|
commands =
|
||||||
|
{envdir}/bin/py.test awx/lib/tests/ -c awx/lib/tests/pytest.ini {posargs}
|
||||||
|
|
||||||
[testenv:coveralls]
|
[testenv:coveralls]
|
||||||
commands=
|
commands=
|
||||||
coverage combine
|
coverage combine
|
||||||
|
|||||||
Reference in New Issue
Block a user