mirror of
https://github.com/ansible/awx.git
synced 2026-01-13 02:50:02 -03:30
Merge remote-tracking branch 'origin/release_3.1.2' into devel
This commit is contained in:
commit
a69dfced74
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 \
|
||||
develop refresh adduser migrate dbchange dbshell runserver celeryd \
|
||||
receiver test test_unit test_coverage coverage_html test_jenkins dev_build \
|
||||
release_build release_clean sdist rpmtar mock-rpm mock-srpm rpm-sign \
|
||||
deb deb-src debian debsign pbuilder reprepro setup_tarball \
|
||||
virtualbox-ovf virtualbox-centos-7 virtualbox-centos-6 \
|
||||
clean-bundle setup_bundle_tarball \
|
||||
receiver test test_unit test_ansible test_coverage coverage_html \
|
||||
test_jenkins dev_build release_build release_clean sdist rpmtar mock-rpm \
|
||||
mock-srpm rpm-sign deb deb-src debian debsign pbuilder \
|
||||
reprepro setup_tarball virtualbox-ovf virtualbox-centos-7 \
|
||||
virtualbox-centos-6 clean-bundle setup_bundle_tarball \
|
||||
ui-docker-machine ui-docker ui-release ui-devel \
|
||||
ui-test ui-deps ui-test-ci ui-test-saucelabs jlaska
|
||||
|
||||
@ -264,8 +264,8 @@ virtualenv_ansible:
|
||||
fi; \
|
||||
if [ ! -d "$(VENV_BASE)/ansible" ]; then \
|
||||
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 -I pip==8.1.2; \
|
||||
$(VENV_BASE)/ansible/bin/pip install $(PIP_OPTIONS) --ignore-installed setuptools==23.0.0 && \
|
||||
$(VENV_BASE)/ansible/bin/pip install $(PIP_OPTIONS) --ignore-installed pip==8.1.2; \
|
||||
fi; \
|
||||
fi
|
||||
|
||||
@ -276,42 +276,32 @@ virtualenv_tower:
|
||||
fi; \
|
||||
if [ ! -d "$(VENV_BASE)/tower" ]; then \
|
||||
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 -I pip==8.1.2; \
|
||||
$(VENV_BASE)/tower/bin/pip install $(PIP_OPTIONS) --ignore-installed setuptools==23.0.0 && \
|
||||
$(VENV_BASE)/tower/bin/pip install $(PIP_OPTIONS) --ignore-installed pip==8.1.2; \
|
||||
fi; \
|
||||
fi
|
||||
|
||||
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 \
|
||||
. $(VENV_BASE)/ansible/bin/activate; \
|
||||
$(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; \
|
||||
$(VENV_BASE)/ansible/bin/pip install pytest; \
|
||||
fi
|
||||
|
||||
# Install third-party requirements needed for Tower's environment.
|
||||
requirements_tower: virtualenv_tower
|
||||
if [ "$(VENV_BASE)" ]; then \
|
||||
. $(VENV_BASE)/tower/bin/activate; \
|
||||
$(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
|
||||
$(VENV_BASE)/tower/bin/pip install $(PIP_OPTIONS) --ignore-installed --no-binary $(SRC_ONLY_PKGS) -r requirements/requirements.txt
|
||||
$(VENV_BASE)/tower/bin/pip uninstall --yes -r requirements/requirements_tower_uninstall.txt
|
||||
|
||||
requirements_tower_dev:
|
||||
if [ "$(VENV_BASE)" ]; then \
|
||||
. $(VENV_BASE)/tower/bin/activate; \
|
||||
$(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
|
||||
$(VENV_BASE)/tower/bin/pip install -r requirements/requirements_dev.txt
|
||||
$(VENV_BASE)/tower/bin/pip uninstall --yes -r requirements/requirements_dev_uninstall.txt
|
||||
|
||||
requirements: requirements_ansible requirements_tower
|
||||
|
||||
requirements_dev: requirements requirements_tower_dev
|
||||
requirements_dev: requirements requirements_tower_dev requirements_ansible_dev
|
||||
|
||||
requirements_test: requirements
|
||||
|
||||
@ -482,7 +472,7 @@ check: flake8 pep8 # pyflakes pylint
|
||||
|
||||
TEST_DIRS ?= awx/main/tests awx/conf/tests awx/sso/tests
|
||||
# Run all API unit tests.
|
||||
test:
|
||||
test: test_ansible
|
||||
@if [ "$(VENV_BASE)" ]; then \
|
||||
. $(VENV_BASE)/tower/bin/activate; \
|
||||
fi; \
|
||||
@ -494,6 +484,12 @@ test_unit:
|
||||
fi; \
|
||||
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.
|
||||
test_coverage:
|
||||
@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)
|
||||
|
||||
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
|
||||
$(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)
|
||||
@ -722,6 +742,8 @@ mock-srpm: rpmtar rpm-build/$(RPM_NVR).src.rpm
|
||||
@echo rpm-build/$(RPM_NVR).src.rpm
|
||||
@echo "#############################################"
|
||||
|
||||
brew-srpm: brewrpmtar mock-srpm
|
||||
|
||||
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 \
|
||||
--define "tower_version $(VERSION)" --define "tower_release $(RELEASE)" $(SCL_DEFINES)
|
||||
|
||||
@ -316,6 +316,8 @@ class OrderByBackend(BaseFilterBackend):
|
||||
else:
|
||||
order_by = (value,)
|
||||
if order_by:
|
||||
order_by = self._strip_sensitive_model_fields(queryset.model, order_by)
|
||||
|
||||
# Special handling of the type field for ordering. In this
|
||||
# case, we're not sorting exactly on the type field, but
|
||||
# given the limited number of views with multiple types,
|
||||
@ -338,3 +340,16 @@ class OrderByBackend(BaseFilterBackend):
|
||||
except FieldError as e:
|
||||
# Return a 400 for invalid field names.
|
||||
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):
|
||||
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)
|
||||
# Permission class should have already validated host_config_key.
|
||||
job_template = self.get_object()
|
||||
@ -2727,14 +2728,14 @@ class JobTemplateCallback(GenericAPIView):
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 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():
|
||||
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.
|
||||
kv = {"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)
|
||||
result = job.signal_start(inventory_sources_already_updated=inventory_sources_already_updated)
|
||||
if not result:
|
||||
data = dict(msg=_('Error starting job!'))
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
@ -3665,7 +3666,7 @@ class AdHocCommandRelaunch(GenericAPIView):
|
||||
data = {}
|
||||
for field in ('job_type', 'inventory_id', 'limit', 'credential_id',
|
||||
'module_name', 'module_args', 'forks', 'verbosity',
|
||||
'become_enabled'):
|
||||
'extra_vars', 'become_enabled'):
|
||||
if field.endswith('_id'):
|
||||
data[field[:-3]] = getattr(obj, field)
|
||||
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',
|
||||
]
|
||||
|
||||
CENSOR_FIELD_WHITELIST = [
|
||||
'msg',
|
||||
'failed',
|
||||
'changed',
|
||||
'results',
|
||||
'start',
|
||||
'end',
|
||||
'delta',
|
||||
'cmd',
|
||||
'_ansible_no_log',
|
||||
'rc',
|
||||
'failed_when_result',
|
||||
'skipped',
|
||||
'skip_reason',
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
super(BaseCallbackModule, self).__init__()
|
||||
self.task_uuids = set()
|
||||
@ -85,6 +69,9 @@ class BaseCallbackModule(CallbackBase):
|
||||
else:
|
||||
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:
|
||||
try:
|
||||
event_context.add_local(event=event, **event_data)
|
||||
@ -132,7 +119,9 @@ class BaseCallbackModule(CallbackBase):
|
||||
task_ctx['task_path'] = task.get_path()
|
||||
except AttributeError:
|
||||
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_ctx['task_args'] = task_args
|
||||
if getattr(task, '_role', None):
|
||||
|
||||
@ -190,7 +190,7 @@ class AdHocCommand(UnifiedJob, JobNotificationMixin):
|
||||
data = {}
|
||||
for field in ('job_type', 'inventory_id', 'limit', 'credential_id',
|
||||
'module_name', 'module_args', 'forks', 'verbosity',
|
||||
'become_enabled'):
|
||||
'extra_vars', 'become_enabled'):
|
||||
data[field] = getattr(self, field)
|
||||
return AdHocCommand.objects.create(**data)
|
||||
|
||||
|
||||
@ -1277,10 +1277,20 @@ class InventoryUpdate(UnifiedJob, InventorySourceOptions, JobNotificationMixin):
|
||||
def get_notification_friendly_name(self):
|
||||
return "Inventory Update"
|
||||
|
||||
def cancel(self):
|
||||
res = super(InventoryUpdate, self).cancel()
|
||||
def _build_job_explanation(self):
|
||||
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:
|
||||
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
|
||||
|
||||
|
||||
|
||||
@ -310,9 +310,13 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour
|
||||
elif self.variables_needed_to_start:
|
||||
variables_needed = True
|
||||
prompting_needed = False
|
||||
for value in self._ask_for_vars_dict().values():
|
||||
if value:
|
||||
prompting_needed = True
|
||||
# The behavior of provisioning callback should mimic
|
||||
# that of job template launch, so prompting_needed should
|
||||
# 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
|
||||
not self.passwords_needed_to_start and
|
||||
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
|
||||
run.
|
||||
'''
|
||||
def cancel(self):
|
||||
res = super(Job, self).cancel()
|
||||
def cancel(self, job_explanation=None):
|
||||
res = super(Job, self).cancel(job_explanation=job_explanation)
|
||||
if self.project_update:
|
||||
self.project_update.cancel()
|
||||
self.project_update.cancel(job_explanation=job_explanation)
|
||||
return res
|
||||
|
||||
|
||||
@ -1139,7 +1143,7 @@ class JobEvent(CreatedModifiedModel):
|
||||
# Save artifact data to parent job (if provided).
|
||||
if artifact_dict:
|
||||
if event_data and isinstance(event_data, dict):
|
||||
# Note: Core has not added support for marking artifacts as
|
||||
# Note: Core has not added support for marking artifacts as
|
||||
# sensitive yet. Going forward, core will not use
|
||||
# _ansible_no_log to denote sensitive set_stats calls.
|
||||
# Instead, they plan to add a flag outside of the traditional
|
||||
|
||||
@ -1025,7 +1025,7 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
||||
if settings.DEBUG:
|
||||
raise
|
||||
|
||||
def cancel(self):
|
||||
def cancel(self, job_explanation=None):
|
||||
if self.can_cancel:
|
||||
if not self.cancel_flag:
|
||||
self.cancel_flag = True
|
||||
@ -1033,6 +1033,9 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
||||
if self.status in ('pending', 'waiting', 'new'):
|
||||
self.status = 'canceled'
|
||||
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.websocket_emit_status("canceled")
|
||||
if settings.BROKER_URL.startswith('amqp://'):
|
||||
|
||||
@ -90,7 +90,7 @@ def celery_startup(conf=None, **kwargs):
|
||||
@worker_process_init.connect
|
||||
def task_set_logger_pre_run(*args, **kwargs):
|
||||
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):
|
||||
@ -1160,6 +1160,7 @@ class RunProjectUpdate(BaseTask):
|
||||
'''
|
||||
env = super(RunProjectUpdate, self).build_env(project_update, **kwargs)
|
||||
env = self.add_ansible_venv(env)
|
||||
env['ANSIBLE_RETRY_FILES_ENABLED'] = str(False)
|
||||
env['ANSIBLE_ASK_PASS'] = str(False)
|
||||
env['ANSIBLE_ASK_SUDO_PASS'] = str(False)
|
||||
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
|
||||
|
||||
|
||||
@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
|
||||
#
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@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", [
|
||||
(None, 403),
|
||||
('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
|
||||
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['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'}]}
|
||||
kwargs = obj._update_unified_job_kwargs(extra_vars={'my_variable':'$encrypted$'})
|
||||
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
|
||||
|
||||
from awx.main.models import (
|
||||
@ -14,3 +15,38 @@ def test_unified_job_workflow_attributes():
|
||||
|
||||
assert job.spawned_by_workflow is True
|
||||
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 cStringIO
|
||||
import json
|
||||
import logging
|
||||
from uuid import uuid4
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf import LazySettings
|
||||
import pytest
|
||||
import requests
|
||||
@ -44,17 +46,27 @@ def http_adapter():
|
||||
return FakeHTTPAdapter()
|
||||
|
||||
|
||||
def test_https_logging_handler_requests_sync_implementation():
|
||||
handler = HTTPSHandler(async=False)
|
||||
assert not isinstance(handler.session, FuturesSession)
|
||||
assert isinstance(handler.session, requests.Session)
|
||||
@pytest.fixture()
|
||||
def connection_error_adapter():
|
||||
class ConnectionErrorAdapter(requests.adapters.HTTPAdapter):
|
||||
|
||||
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():
|
||||
handler = HTTPSHandler(async=True)
|
||||
handler = HTTPSHandler()
|
||||
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())
|
||||
def test_https_logging_handler_defaults(param):
|
||||
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
|
||||
|
||||
|
||||
@pytest.mark.parametrize('message_type, async', [
|
||||
('logstash', False),
|
||||
('logstash', True),
|
||||
('splunk', False),
|
||||
('splunk', True),
|
||||
])
|
||||
def test_https_logging_handler_connection_error(connection_error_adapter,
|
||||
dummy_log_record):
|
||||
handler = HTTPSHandler(host='127.0.0.1', enabled_flag=True,
|
||||
message_type='logstash',
|
||||
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,
|
||||
message_type, async):
|
||||
message_type):
|
||||
handler = HTTPSHandler(host='127.0.0.1', enabled_flag=True,
|
||||
message_type=message_type,
|
||||
enabled_loggers=['awx', 'activity_stream', 'job_events', 'system_tracking'],
|
||||
async=async)
|
||||
enabled_loggers=['awx', 'activity_stream', 'job_events', 'system_tracking'])
|
||||
handler.setFormatter(LogstashFormatter())
|
||||
handler.session.mount('http://', http_adapter)
|
||||
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'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('async', (True, False))
|
||||
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,
|
||||
username='user', password='pass',
|
||||
message_type='logstash',
|
||||
enabled_loggers=['awx', 'activity_stream', 'job_events', 'system_tracking'],
|
||||
async=async)
|
||||
enabled_loggers=['awx', 'activity_stream', 'job_events', 'system_tracking'])
|
||||
handler.setFormatter(LogstashFormatter())
|
||||
handler.session.mount('http://', http_adapter)
|
||||
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")
|
||||
|
||||
|
||||
@pytest.mark.parametrize('async', (True, False))
|
||||
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,
|
||||
password='pass', message_type='splunk',
|
||||
enabled_loggers=['awx', 'activity_stream', 'job_events', 'system_tracking'],
|
||||
async=async)
|
||||
enabled_loggers=['awx', 'activity_stream', 'job_events', 'system_tracking'])
|
||||
handler.setFormatter(LogstashFormatter())
|
||||
handler.session.mount('http://', http_adapter)
|
||||
async_futures = handler.emit(dummy_log_record)
|
||||
|
||||
@ -5,8 +5,10 @@
|
||||
import logging
|
||||
import json
|
||||
import requests
|
||||
from requests.exceptions import RequestException
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from copy import copy
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
# loggly
|
||||
import traceback
|
||||
@ -19,6 +21,8 @@ from awx.main.utils.formatters import LogstashFormatter
|
||||
|
||||
__all__ = ['HTTPSNullHandler', 'BaseHTTPSHandler', 'configure_external_logger']
|
||||
|
||||
logger = logging.getLogger('awx.main.utils.handlers')
|
||||
|
||||
# AWX external logging handler, generally designed to be used
|
||||
# with the accompanying LogstashHandler, derives from python-logstash library
|
||||
# Non-blocking request accomplished by FuturesSession, similar
|
||||
@ -34,6 +38,7 @@ PARAM_NAMES = {
|
||||
'enabled_loggers': 'LOG_AGGREGATOR_LOGGERS',
|
||||
'indv_facts': 'LOG_AGGREGATOR_INDIVIDUAL_FACTS',
|
||||
'enabled_flag': 'LOG_AGGREGATOR_ENABLED',
|
||||
'http_timeout': 'LOG_AGGREGATOR_HTTP_TIMEOUT',
|
||||
}
|
||||
|
||||
|
||||
@ -52,17 +57,41 @@ class HTTPSNullHandler(logging.NullHandler):
|
||||
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):
|
||||
def __init__(self, fqdn=False, **kwargs):
|
||||
super(BaseHTTPSHandler, self).__init__()
|
||||
self.fqdn = fqdn
|
||||
self.async = kwargs.get('async', True)
|
||||
for fd in PARAM_NAMES:
|
||||
setattr(self, fd, kwargs.get(fd, None))
|
||||
if self.async:
|
||||
self.session = FuturesSession()
|
||||
else:
|
||||
self.session = requests.Session()
|
||||
self.session = FuturesSession(executor=VerboseThreadPoolExecutor(
|
||||
max_workers=2 # this is the default used by requests_futures
|
||||
))
|
||||
self.add_auth_information()
|
||||
|
||||
@classmethod
|
||||
@ -135,10 +164,8 @@ class BaseHTTPSHandler(logging.Handler):
|
||||
payload_str = json.dumps(payload_input)
|
||||
else:
|
||||
payload_str = payload_input
|
||||
if self.async:
|
||||
return dict(data=payload_str, background_callback=unused_callback)
|
||||
else:
|
||||
return dict(data=payload_str)
|
||||
return dict(data=payload_str, background_callback=unused_callback,
|
||||
timeout=self.http_timeout)
|
||||
|
||||
def skip_log(self, logger_name):
|
||||
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
|
||||
``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:
|
||||
https://docs.python.org/3/library/concurrent.futures.html#future-objects
|
||||
http://pythonhosted.org/futures/
|
||||
@ -177,17 +200,10 @@ class BaseHTTPSHandler(logging.Handler):
|
||||
for key in facts_dict:
|
||||
fact_payload = copy(payload_data)
|
||||
fact_payload.update(facts_dict[key])
|
||||
if self.async:
|
||||
async_futures.append(self._send(fact_payload))
|
||||
else:
|
||||
self._send(fact_payload)
|
||||
async_futures.append(self._send(fact_payload))
|
||||
return async_futures
|
||||
|
||||
if self.async:
|
||||
return [self._send(payload)]
|
||||
|
||||
self._send(payload)
|
||||
return []
|
||||
return [self._send(payload)]
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except:
|
||||
@ -209,7 +225,7 @@ def add_or_remove_logger(address, 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
|
||||
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
|
||||
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))
|
||||
awx_logger_instance = instance
|
||||
if is_enabled and 'awx' not in settings_module.LOG_AGGREGATOR_LOGGERS:
|
||||
|
||||
@ -25,7 +25,7 @@
|
||||
- name: update project using git and accept hostkey
|
||||
git:
|
||||
dest: "{{project_path|quote}}"
|
||||
repo: "{{scm_url|quote}}"
|
||||
repo: "{{scm_url}}"
|
||||
version: "{{scm_branch|quote}}"
|
||||
force: "{{scm_clean}}"
|
||||
accept_hostkey: "{{scm_accept_hostkey}}"
|
||||
@ -42,7 +42,7 @@
|
||||
- name: update project using git
|
||||
git:
|
||||
dest: "{{project_path|quote}}"
|
||||
repo: "{{scm_url|quote}}"
|
||||
repo: "{{scm_url}}"
|
||||
version: "{{scm_branch|quote}}"
|
||||
force: "{{scm_clean}}"
|
||||
#clone: "{{ scm_full_checkout }}"
|
||||
@ -160,6 +160,11 @@
|
||||
scm_version: "{{scm_version|regex_replace('^.*Revision: ([0-9]+).*$', '\\1')}}"
|
||||
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
|
||||
debug: msg="Repository Version {{ scm_version }}"
|
||||
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
|
||||
# at the native registry location.
|
||||
|
||||
$packages = Get-ChildItem -Path $uninstall_native_path |
|
||||
[PSObject []]$packages = Get-ChildItem -Path $uninstall_native_path |
|
||||
Get-ItemProperty |
|
||||
Select-Object -Property @{Name="name"; Expression={$_."DisplayName"}},
|
||||
@{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
|
||||
# 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 |
|
||||
Select-Object -Property @{Name="name"; Expression={$_."DisplayName"}},
|
||||
@{Name="version"; Expression={$_."DisplayVersion"}},
|
||||
|
||||
@ -867,6 +867,7 @@ INSIGHTS_URL_BASE = "https://access.redhat.com"
|
||||
TOWER_SETTINGS_MANIFEST = {}
|
||||
|
||||
LOG_AGGREGATOR_ENABLED = False
|
||||
LOG_AGGREGATOR_HTTP_TIMEOUT = 5
|
||||
|
||||
# The number of retry attempts for websocket session establishment
|
||||
# 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
|
||||
scope.object = scope.resourceData.data;
|
||||
// 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
|
||||
// array w roles and descriptions for key
|
||||
|
||||
@ -16,7 +16,6 @@ export default
|
||||
roles: '=',
|
||||
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>',
|
||||
link: function(scope, element, attrs, ctrl) {
|
||||
CreateSelect2({
|
||||
|
||||
@ -23,6 +23,7 @@ export default ['$scope', '$rootScope', '$compile', '$location',
|
||||
init();
|
||||
|
||||
function init() {
|
||||
$scope.canEditOrg = true;
|
||||
// Load the list of options for Kind
|
||||
GetChoices({
|
||||
scope: $scope,
|
||||
|
||||
@ -55,7 +55,8 @@ export default
|
||||
dataTitle: i18n._('Organization') + ' ',
|
||||
dataPlacement: 'bottom',
|
||||
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: {
|
||||
label: i18n._('Type'),
|
||||
|
||||
@ -49,7 +49,8 @@ angular.module('InventoryFormDefinition', [])
|
||||
reqExpression: "organizationrequired",
|
||||
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: {
|
||||
label: i18n._('Variables'),
|
||||
|
||||
@ -349,7 +349,11 @@ export default
|
||||
dataPlacement: 'right',
|
||||
dataTitle: i18n._("Host Config Key"),
|
||||
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: {
|
||||
label: i18n._('Labels'),
|
||||
|
||||
@ -42,7 +42,8 @@ export default
|
||||
sourceModel: 'organization',
|
||||
basePath: 'organizations',
|
||||
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,
|
||||
}
|
||||
},
|
||||
|
||||
@ -54,7 +54,8 @@ export default
|
||||
dataContainer: 'body',
|
||||
dataPlacement: 'right',
|
||||
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: {
|
||||
label: i18n._('Labels'),
|
||||
|
||||
@ -39,6 +39,7 @@ function InventoriesAdd($scope, $rootScope, $compile, $location, $log,
|
||||
init();
|
||||
|
||||
function init() {
|
||||
$scope.canEditOrg = true;
|
||||
form.formLabelSize = null;
|
||||
form.formFieldSize = null;
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ function InventoriesEdit($scope, $rootScope, $compile, $location,
|
||||
$log, $stateParams, InventoryForm, Rest, Alert, ProcessErrors,
|
||||
ClearScope, GetBasePath, ParseTypeChange, Wait, ToJSON,
|
||||
ParseVariableString, Prompt, InitiatePlaybookRun,
|
||||
TemplatesService, $state) {
|
||||
TemplatesService, $state, OrgAdminLookup) {
|
||||
|
||||
// Inject dynamic view
|
||||
var defaultUrl = GetBasePath('inventory'),
|
||||
@ -77,6 +77,11 @@ function InventoriesEdit($scope, $rootScope, $compile, $location,
|
||||
field_id: 'inventory_variables'
|
||||
});
|
||||
|
||||
OrgAdminLookup.checkForAdminAccess({organization: data.organization})
|
||||
.then(function(canEditOrg){
|
||||
$scope.canEditOrg = canEditOrg;
|
||||
});
|
||||
|
||||
$scope.inventory_obj = data;
|
||||
$scope.name = data.name;
|
||||
|
||||
@ -132,5 +137,5 @@ export default ['$scope', '$rootScope', '$compile', '$location',
|
||||
'$log', '$stateParams', 'InventoryForm', 'Rest', 'Alert',
|
||||
'ProcessErrors', 'ClearScope', 'GetBasePath', 'ParseTypeChange', 'Wait',
|
||||
'ToJSON', 'ParseVariableString', 'Prompt', 'InitiatePlaybookRun',
|
||||
'TemplatesService', '$state', InventoriesEdit,
|
||||
'TemplatesService', '$state', 'OrgAdminLookup', InventoriesEdit,
|
||||
];
|
||||
|
||||
@ -46,6 +46,9 @@ angular.module('inventory', [
|
||||
data: {
|
||||
activityStream: true,
|
||||
activityStreamTarget: 'inventory'
|
||||
},
|
||||
ncyBreadcrumb: {
|
||||
label: N_('INVENTORIES')
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
function adhocController($q, $scope, $location, $stateParams,
|
||||
$state, CheckPasswords, PromptForPasswords, CreateLaunchDialog, CreateSelect2, adhocForm,
|
||||
GenerateForm, Rest, ProcessErrors, ClearScope, GetBasePath, GetChoices,
|
||||
KindChange, CredentialList, Empty, Wait) {
|
||||
KindChange, CredentialList, ParseTypeChange, Empty, Wait) {
|
||||
|
||||
ClearScope();
|
||||
|
||||
@ -162,6 +162,12 @@ function adhocController($q, $scope, $location, $stateParams,
|
||||
|
||||
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(){
|
||||
$state.go('inventoryManage');
|
||||
};
|
||||
@ -199,6 +205,7 @@ function adhocController($q, $scope, $location, $stateParams,
|
||||
"module_args": "",
|
||||
"forks": 0,
|
||||
"verbosity": 0,
|
||||
"extra_vars": "",
|
||||
"privilege_escalation": ""
|
||||
};
|
||||
|
||||
@ -297,5 +304,5 @@ function adhocController($q, $scope, $location, $stateParams,
|
||||
export default ['$q', '$scope', '$location', '$stateParams',
|
||||
'$state', 'CheckPasswords', 'PromptForPasswords', 'CreateLaunchDialog', 'CreateSelect2',
|
||||
'adhocForm', 'GenerateForm', 'Rest', 'ProcessErrors', 'ClearScope', 'GetBasePath',
|
||||
'GetChoices', 'KindChange', 'CredentialList', 'Empty', 'Wait',
|
||||
'GetChoices', 'KindChange', 'CredentialList', 'ParseTypeChange', 'Empty', 'Wait',
|
||||
adhocController];
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
* @description This form is for executing an adhoc command
|
||||
*/
|
||||
|
||||
export default function() {
|
||||
export default ['i18n', function(i18n) {
|
||||
return {
|
||||
addTitle: 'EXECUTE COMMAND',
|
||||
name: 'adhoc',
|
||||
@ -121,6 +121,23 @@ export default function() {
|
||||
dataPlacement: 'right',
|
||||
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: {
|
||||
reset: {
|
||||
@ -139,4 +156,4 @@ export default function() {
|
||||
|
||||
related: {}
|
||||
};
|
||||
}
|
||||
}];
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<div class="BreadCrumb InventoryManageBreadCrumbs">
|
||||
<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">
|
||||
<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>
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="HostEvent-details--left">
|
||||
<div class="HostEvent-details">
|
||||
<div class="HostEvent-field">
|
||||
<span class="HostEvent-field--label">CREATED</span>
|
||||
<span class="HostEvent-field--content">{{(event.created | longDate) || "No result found"}}</span>
|
||||
|
||||
@ -99,7 +99,7 @@
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.HostEvent .modal-body{
|
||||
max-height: 500px;
|
||||
max-height: 600px;
|
||||
padding: 0px!important;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@ -115,6 +115,7 @@
|
||||
text-transform: uppercase;
|
||||
flex: 0 1 80px;
|
||||
max-width: 80px;
|
||||
min-width: 80px;
|
||||
font-size: 12px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
@ -123,28 +124,10 @@
|
||||
}
|
||||
.HostEvent-field--content{
|
||||
word-wrap: break-word;
|
||||
max-width: 13em;
|
||||
flex: 0 1 13em;
|
||||
}
|
||||
.HostEvent-field--monospaceContent{
|
||||
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 {
|
||||
pointer-events: all!important;
|
||||
}
|
||||
|
||||
@ -213,23 +213,8 @@ job-results-standard-out {
|
||||
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 {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.JobResults .CodeMirror-cursors {
|
||||
display: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.JobResults-downloadTooLarge {
|
||||
|
||||
@ -175,18 +175,12 @@
|
||||
background-color: @btn-bg-hov;
|
||||
color: @btn-txt;
|
||||
}
|
||||
.JobSubmission-revertButton {
|
||||
background-color: @default-bg;
|
||||
color: @default-link;
|
||||
text-transform: uppercase;
|
||||
padding-left:15px;
|
||||
padding-right: 15px;
|
||||
|
||||
.JobSubmission-revertLink {
|
||||
padding-left:10px;
|
||||
font-size: 11px;
|
||||
}
|
||||
.JobSubmission-revertButton:hover{
|
||||
background-color: @default-bg;
|
||||
color: @default-link-hov;
|
||||
}
|
||||
|
||||
.JobSubmission-selectedItem {
|
||||
display: flex;
|
||||
flex: 1 0 auto;
|
||||
|
||||
@ -316,15 +316,6 @@ export default
|
||||
$scope.revertToDefaultInventory = function() {
|
||||
if($scope.has_default_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) {
|
||||
$scope.selected_credential = angular.copy($scope.defaults.credential);
|
||||
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>
|
||||
</div>
|
||||
<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>
|
||||
<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 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>
|
||||
</div>
|
||||
<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>
|
||||
<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 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>
|
||||
|
||||
@ -7,13 +7,17 @@
|
||||
import jobSubCredListController from './job-sub-cred-list.controller';
|
||||
|
||||
export default [ 'templateUrl', 'QuerySet', 'GetBasePath', 'generateList', '$compile', 'CredentialList',
|
||||
function(templateUrl, qs, GetBasePath, GenerateList, $compile, CredentialList) {
|
||||
(templateUrl, qs, GetBasePath, GenerateList, $compile, CredentialList) => {
|
||||
return {
|
||||
scope: {},
|
||||
scope: {
|
||||
selectedCredential: '='
|
||||
},
|
||||
templateUrl: templateUrl('job-submission/lists/credential/job-sub-cred-list'),
|
||||
controller: jobSubCredListController,
|
||||
restrict: 'E',
|
||||
link: function(scope) {
|
||||
link: scope => {
|
||||
let toDestroy = [];
|
||||
|
||||
scope.credential_default_params = {
|
||||
order_by: 'name',
|
||||
page_size: 5,
|
||||
@ -28,11 +32,11 @@ export default [ 'templateUrl', 'QuerySet', 'GetBasePath', 'generateList', '$com
|
||||
|
||||
// Fire off the initial search
|
||||
qs.search(GetBasePath('credentials'), scope.credential_default_params)
|
||||
.then(function(res) {
|
||||
.then(res => {
|
||||
scope.credential_dataset = res.data;
|
||||
scope.credentials = scope.credential_dataset.results;
|
||||
|
||||
var credList = _.cloneDeep(CredentialList);
|
||||
let credList = _.cloneDeep(CredentialList);
|
||||
let html = GenerateList.build({
|
||||
list: credList,
|
||||
input_type: 'radio',
|
||||
@ -43,11 +47,11 @@ export default [ 'templateUrl', 'QuerySet', 'GetBasePath', 'generateList', '$com
|
||||
|
||||
$('#job-submission-credential-lookup').append($compile(html)(scope));
|
||||
|
||||
scope.$watchCollection('credentials', function () {
|
||||
if(scope.selected_credential) {
|
||||
toDestroy.push(scope.$watchCollection('selectedCredential', () => {
|
||||
if(scope.selectedCredential) {
|
||||
// Loop across the inventories and see if one of them should be "checked"
|
||||
scope.credentials.forEach(function(row, i) {
|
||||
if (row.id === scope.selected_credential.id) {
|
||||
scope.credentials.forEach((row, i) => {
|
||||
if (row.id === scope.selectedCredential.id) {
|
||||
scope.credentials[i].checked = 1;
|
||||
}
|
||||
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';
|
||||
|
||||
export default [ 'templateUrl', 'QuerySet', 'GetBasePath', 'generateList', '$compile', 'InventoryList',
|
||||
function(templateUrl, qs, GetBasePath, GenerateList, $compile, InventoryList) {
|
||||
(templateUrl, qs, GetBasePath, GenerateList, $compile, InventoryList) => {
|
||||
return {
|
||||
scope: {},
|
||||
scope: {
|
||||
selectedInventory: '='
|
||||
},
|
||||
templateUrl: templateUrl('job-submission/lists/inventory/job-sub-inv-list'),
|
||||
controller: jobSubInvListController,
|
||||
restrict: 'E',
|
||||
link: function(scope) {
|
||||
link: scope => {
|
||||
let toDestroy = [];
|
||||
|
||||
scope.inventory_default_params = {
|
||||
order_by: 'name',
|
||||
page_size: 5
|
||||
@ -26,11 +30,11 @@ export default [ 'templateUrl', 'QuerySet', 'GetBasePath', 'generateList', '$com
|
||||
|
||||
// Fire off the initial search
|
||||
qs.search(GetBasePath('inventory'), scope.inventory_default_params)
|
||||
.then(function(res) {
|
||||
.then(res => {
|
||||
scope.inventory_dataset = res.data;
|
||||
scope.inventories = scope.inventory_dataset.results;
|
||||
|
||||
var invList = _.cloneDeep(InventoryList);
|
||||
let invList = _.cloneDeep(InventoryList);
|
||||
let html = GenerateList.build({
|
||||
list: invList,
|
||||
input_type: 'radio',
|
||||
@ -41,11 +45,11 @@ export default [ 'templateUrl', 'QuerySet', 'GetBasePath', 'generateList', '$com
|
||||
|
||||
$('#job-submission-inventory-lookup').append($compile(html)(scope));
|
||||
|
||||
scope.$watchCollection('inventories', function () {
|
||||
if(scope.selected_inventory) {
|
||||
toDestroy.push(scope.$watchCollection('selectedInventory', () => {
|
||||
if(scope.selectedInventory) {
|
||||
// Loop across the inventories and see if one of them should be "checked"
|
||||
scope.inventories.forEach(function(row, i) {
|
||||
if (row.id === scope.selected_inventory.id) {
|
||||
scope.inventories.forEach((row, i) => {
|
||||
if (row.id === scope.selectedInventory.id) {
|
||||
scope.inventories[i].checked = 1;
|
||||
}
|
||||
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
|
||||
['$state', '$rootScope', 'Rest', 'GetBasePath', 'ProcessErrors', '$q',
|
||||
['$state', '$rootScope', 'Rest', 'GetBasePath', 'ProcessErrors',
|
||||
'ConfigService',
|
||||
function($state, $rootScope, Rest, GetBasePath, ProcessErrors, $q,
|
||||
function($state, $rootScope, Rest, GetBasePath, ProcessErrors,
|
||||
ConfigService){
|
||||
return {
|
||||
get: function() {
|
||||
@ -29,7 +29,7 @@ export default
|
||||
msg: 'Call to '+ defaultUrl + ' failed. Return status: '+ status});
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
valid: function(license) {
|
||||
if (!license.valid_key){
|
||||
return false;
|
||||
|
||||
@ -19,6 +19,7 @@ export default ['$scope', '$location', '$stateParams', 'GenerateForm',
|
||||
init();
|
||||
|
||||
function init() {
|
||||
$scope.canEditOrg = true;
|
||||
Rest.setUrl(GetBasePath('projects'));
|
||||
Rest.options()
|
||||
.success(function(data) {
|
||||
|
||||
@ -49,7 +49,8 @@ export default ['i18n', 'NotificationsList', function(i18n, NotificationsList) {
|
||||
required: true,
|
||||
dataContainer: 'body',
|
||||
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: {
|
||||
label: i18n._('SCM Type'),
|
||||
@ -134,6 +135,11 @@ export default ['i18n', 'NotificationsList', function(i18n, NotificationsList) {
|
||||
search: {
|
||||
kind: 'scm'
|
||||
},
|
||||
autopopulateLookup: false,
|
||||
awRequiredWhen: {
|
||||
reqExpression: "credRequired",
|
||||
init: false
|
||||
},
|
||||
ngShow: "scm_type && scm_type.value !== 'manual'",
|
||||
sourceModel: 'credential',
|
||||
awLookupType: 'scm_credential',
|
||||
|
||||
@ -95,6 +95,7 @@ export default ['i18n', function(i18n) {
|
||||
awToolTip: "{{ project.scm_schedule_tooltip }}",
|
||||
ngClass: "project.scm_type_class",
|
||||
dataPlacement: 'top',
|
||||
ngShow: "project.summary_fields.user_capabilities.schedule"
|
||||
},
|
||||
edit: {
|
||||
ngClick: "editProject(project.id)",
|
||||
|
||||
@ -493,7 +493,8 @@ function(ConfigurationUtils, i18n, $rootScope) {
|
||||
modelName = attrs.source,
|
||||
lookupType = attrs.awlookuptype,
|
||||
watcher = attrs.awRequiredWhen || undefined,
|
||||
watchBasePath;
|
||||
watchBasePath,
|
||||
awLookupWhen = attrs.awLookupWhen;
|
||||
|
||||
if (attrs.autopopulatelookup !== undefined) {
|
||||
autopopulateLookup = JSON.parse(attrs.autopopulatelookup);
|
||||
@ -501,7 +502,6 @@ function(ConfigurationUtils, i18n, $rootScope) {
|
||||
autopopulateLookup = true;
|
||||
}
|
||||
|
||||
|
||||
// The following block of code is for instances where the
|
||||
// lookup field is reused by varying sub-forms. Example: The groups
|
||||
// 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
|
||||
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;
|
||||
};
|
||||
|
||||
@ -1388,6 +1388,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
|
||||
html += (field.autopopulateLookup !== undefined) ? ` autopopulateLookup=${field.autopopulateLookup} ` : "";
|
||||
html += (field.watchBasePath !== undefined) ? ` watchBasePath=${field.watchBasePath} ` : "";
|
||||
html += `ng-model-options="{ updateOn: 'default blur', debounce: { 'default': 300, 'blur': 0 } }"`;
|
||||
html += (field.awLookupWhen !== undefined) ? this.attr(field, 'awLookupWhen') : "";
|
||||
html += " awlookup >\n";
|
||||
html += "</div>\n";
|
||||
|
||||
|
||||
@ -86,6 +86,9 @@ angular.module('GeneratorHelpers', [systemStatus.name])
|
||||
result += value;
|
||||
result += '"';
|
||||
break;
|
||||
case 'awLookupWhen':
|
||||
result = "ng-attr-awlookup=\"" + value + "\" ";
|
||||
break;
|
||||
default:
|
||||
result = key + "=\"" + value + "\" ";
|
||||
}
|
||||
|
||||
@ -29,13 +29,14 @@ import config from './config/main';
|
||||
import PromptDialog from './prompt-dialog';
|
||||
import directives from './directives';
|
||||
import features from './features/main';
|
||||
import orgAdminLookup from './org-admin-lookup/main';
|
||||
import 'angular-duration-format';
|
||||
|
||||
export default
|
||||
angular.module('shared', [listGenerator.name,
|
||||
formGenerator.name,
|
||||
lookupModal.name,
|
||||
smartSearch.name,
|
||||
smartSearch.name,
|
||||
paginate.name,
|
||||
columnSort.name,
|
||||
filters.name,
|
||||
@ -55,6 +56,7 @@ angular.module('shared', [listGenerator.name,
|
||||
directives.name,
|
||||
filters.name,
|
||||
features.name,
|
||||
orgAdminLookup.name,
|
||||
require('angular-cookies'),
|
||||
'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,
|
||||
ncyBreadcrumb: {
|
||||
[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: {
|
||||
'form': {
|
||||
@ -386,14 +386,15 @@ export default ['$injector', '$stateExtender', '$log', 'i18n', function($injecto
|
||||
|
||||
function buildNotificationState(field) {
|
||||
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({
|
||||
searchPrefix: `${list.iterator}`,
|
||||
name: `${formStateDefinition.name}.${list.iterator}s`,
|
||||
url: `/${list.iterator}s`,
|
||||
ncyBreadcrumb: {
|
||||
parent: `${formStateDefinition.name}`,
|
||||
label: `${field.iterator}s`
|
||||
label: `${breadcrumbLabel}`
|
||||
},
|
||||
params: {
|
||||
[list.iterator + '_search']: {
|
||||
@ -581,14 +582,14 @@ export default ['$injector', '$stateExtender', '$log', 'i18n', function($injecto
|
||||
list = field.include ? $injector.get(field.include) : field,
|
||||
// Added this line specifically for Completed Jobs but should be OK
|
||||
// for all the rest of the related tabs
|
||||
breadcrumbLabel = field.iterator.replace('_', ' '),
|
||||
breadcrumbLabel = (field.iterator.replace('_', ' ') + 's').toUpperCase(),
|
||||
stateConfig = {
|
||||
searchPrefix: `${list.iterator}`,
|
||||
name: `${formStateDefinition.name}.${list.iterator}s`,
|
||||
url: `/${list.iterator}s`,
|
||||
ncyBreadcrumb: {
|
||||
parent: `${formStateDefinition.name}`,
|
||||
label: `${breadcrumbLabel}s`
|
||||
label: `${breadcrumbLabel}`
|
||||
},
|
||||
params: {
|
||||
[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-detailsContent col-lg-9 col-md-9 col-sm-9 col-xs-8">{{ verbosity }}</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>
|
||||
|
||||
@ -149,7 +149,11 @@ export function JobStdoutController ($rootScope, $scope, $state, $stateParams,
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@ -28,6 +28,7 @@ export default ['$scope', '$rootScope', '$stateParams', 'TeamForm', 'GenerateFor
|
||||
init();
|
||||
|
||||
function init() {
|
||||
$scope.canEditOrg = true;
|
||||
// apply form definition's default field values
|
||||
GenerateForm.applyDefaults(form, $scope);
|
||||
|
||||
|
||||
@ -29,6 +29,7 @@
|
||||
generator = GenerateForm;
|
||||
|
||||
function init() {
|
||||
$scope.canEditOrg = true;
|
||||
$scope.parseType = 'yaml';
|
||||
$scope.can_edit = true;
|
||||
// apply form definition's default field values
|
||||
|
||||
@ -8,12 +8,12 @@
|
||||
[ '$scope', '$stateParams', 'WorkflowForm', 'GenerateForm', 'Alert', 'ProcessErrors',
|
||||
'ClearScope', 'GetBasePath', '$q', 'ParseTypeChange', 'Wait', 'Empty',
|
||||
'ToJSON', 'initSurvey', '$state', 'CreateSelect2', 'ParseVariableString',
|
||||
'TemplatesService', 'OrganizationList', 'Rest', 'WorkflowService', 'ToggleNotification',
|
||||
'TemplatesService', 'OrganizationList', 'Rest', 'WorkflowService', 'ToggleNotification', 'OrgAdminLookup',
|
||||
function(
|
||||
$scope, $stateParams, WorkflowForm, GenerateForm, Alert, ProcessErrors,
|
||||
ClearScope, GetBasePath, $q, ParseTypeChange, Wait, Empty,
|
||||
ToJSON, SurveyControllerInit, $state, CreateSelect2, ParseVariableString,
|
||||
TemplatesService, OrganizationList, Rest, WorkflowService, ToggleNotification
|
||||
TemplatesService, OrganizationList, Rest, WorkflowService, ToggleNotification, OrgAdminLookup
|
||||
) {
|
||||
|
||||
ClearScope();
|
||||
@ -145,6 +145,17 @@
|
||||
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');
|
||||
$scope.url = workflowJobTemplateData.url;
|
||||
$scope.survey_enabled = workflowJobTemplateData.survey_enabled;
|
||||
|
||||
@ -347,6 +347,20 @@ export default [ '$state','moment', '$timeout', '$window',
|
||||
if(!d.isStartNode) {
|
||||
let resourceName = (d.unifiedJobTemplate && d.unifiedJobTemplate.name) ? d.unifiedJobTemplate.name : "";
|
||||
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
|
||||
// it properly on the workflow
|
||||
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;
|
||||
}
|
||||
|
||||
.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 {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.WorkflowResults .CodeMirror-cursors {
|
||||
display: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@ -49,3 +49,5 @@ slackclient==1.0.2
|
||||
twilio==5.6.0
|
||||
uWSGI==2.0.14
|
||||
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
|
||||
|
||||
# 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
|
||||
secretstorage==2.3.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
|
||||
|
||||
# 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
|
||||
COPY logstash.conf /
|
||||
RUN touch /logstash.log
|
||||
RUN chown logstash:logstash /logstash.log
|
||||
CMD ["-f", "/logstash.conf"]
|
||||
|
||||
@ -15,5 +15,8 @@ filter {
|
||||
}
|
||||
|
||||
output {
|
||||
stdout { codec => rubydebug }
|
||||
stdout { codec => rubydebug }
|
||||
file {
|
||||
path => "/logstash.log"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
---
|
||||
version: '2'
|
||||
version: '3'
|
||||
services:
|
||||
unit-tests:
|
||||
build:
|
||||
context: ../../../
|
||||
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:
|
||||
SWIG_FEATURES: "-cpperraswarn -includeall -I/usr/include/openssl"
|
||||
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 =
|
||||
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]
|
||||
commands=
|
||||
coverage combine
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user