Merge branch 'release_3.1.2' into stable

* release_3.1.2: (149 commits)
  updating changelog for 3.1.2
  Add back SRC_ONLY_PKGS
  Fix ubuntu 14 restart service list pt 2
  Make sure the insight playbook fetch doesn't quote user and pass
  Add requirements/vendor to gitignore
  Conditionally install from local python dependencies in spec file
  Remove requirements/vendor on make clean.
  Update brew-srpm target to generate local requirements files
  Get offline pip installs working
  Add clean-dist target
  Navigate back to the jobDetails state when the user clicks outside the host event modal.
  Don't use jinja quote filter on insights username or password
  fix legacy standard out
  Fixed permissions typo
  add test, restore old behavior in api test
  create _survey_element_validation and use it for updating extra_vars
  Do not set the default if the field was not passed in to kwargs_extra_vars
  Remove log aggregator port required mark.
  Modify according to review feedback.
  Host Event json should be read-only
  ...
This commit is contained in:
Matthew Jones
2017-03-30 22:43:54 -04:00
165 changed files with 1713 additions and 729 deletions

1
.gitignore vendored
View File

@@ -108,6 +108,7 @@ reports
*.results *.results
local/ local/
*.mo *.mo
requirements/vendor
# AWX python libs populated by requirements.txt # AWX python libs populated by requirements.txt
awx/lib/.deps_built awx/lib/.deps_built

122
Makefile
View File

@@ -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
@@ -215,6 +215,7 @@ clean-bundle:
clean-ui: clean-ui:
rm -rf awx/ui/static/ rm -rf awx/ui/static/
rm -rf awx/ui/node_modules/ rm -rf awx/ui/node_modules/
rm -rf awx/ui/coverage/
rm -f $(UI_DEPS_FLAG_FILE) rm -f $(UI_DEPS_FLAG_FILE)
rm -f $(UI_RELEASE_FLAG_FILE) rm -f $(UI_RELEASE_FLAG_FILE)
@@ -224,15 +225,18 @@ clean-tmp:
clean-venv: clean-venv:
rm -rf venv/ rm -rf venv/
clean-dist:
rm -rf dist
# Remove temporary build files, compiled Python files. # Remove temporary build files, compiled Python files.
clean: clean-rpm clean-deb clean-ui clean-tar clean-packer clean-bundle clean: clean-rpm clean-deb clean-ui clean-tar clean-packer clean-bundle clean-dist
rm -rf awx/public rm -rf awx/public
rm -rf awx/lib/site-packages rm -rf awx/lib/site-packages
rm -rf dist/*
rm -rf awx/job_status rm -rf awx/job_status
rm -rf awx/job_output rm -rf awx/job_output
rm -rf reports rm -rf reports
rm -f awx/awx_test.sqlite3 rm -f awx/awx_test.sqlite3
rm -rf requirements/vendor
rm -rf tmp rm -rf tmp
mkdir tmp mkdir tmp
rm -rf build $(NAME)-$(VERSION) *.egg-info rm -rf build $(NAME)-$(VERSION) *.egg-info
@@ -263,8 +267,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
@@ -275,42 +279,40 @@ 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
if [ "$(VENV_BASE)" ]; then \ if [[ "$(PIP_OPTIONS)" == *"--no-index"* ]]; then \
. $(VENV_BASE)/ansible/bin/activate; \ cat requirements/requirements_ansible.txt requirements/requirements_ansible_local.txt | $(VENV_BASE)/ansible/bin/pip install $(PIP_OPTIONS) --ignore-installed -r /dev/stdin ; \
$(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 \ else \
pip install --ignore-installed --no-binary $(SRC_ONLY_PKGS) -r requirements/requirements_ansible.txt ; \ cat requirements/requirements_ansible.txt requirements/requirements_ansible_git.txt | $(VENV_BASE)/ansible/bin/pip install $(PIP_OPTIONS) --no-binary $(SRC_ONLY_PKGS) --ignore-installed -r /dev/stdin ; \
pip uninstall --yes -r requirements/requirements_ansible_uninstall.txt; \ fi
$(VENV_BASE)/ansible/bin/pip uninstall --yes -r requirements/requirements_ansible_uninstall.txt
requirements_ansible_dev:
if [ "$(VENV_BASE)" ]; then \
$(VENV_BASE)/ansible/bin/pip install pytest; \
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 \ if [[ "$(PIP_OPTIONS)" == *"--no-index"* ]]; then \
. $(VENV_BASE)/tower/bin/activate; \ cat requirements/requirements.txt requirements/requirements_local.txt | $(VENV_BASE)/tower/bin/pip install $(PIP_OPTIONS) --ignore-installed -r /dev/stdin ; \
$(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 \ else \
pip install --ignore-installed --no-binary $(SRC_ONLY_PKGS) -r requirements/requirements.txt ; \ cat requirements/requirements.txt requirements/requirements_git.txt | $(VENV_BASE)/tower/bin/pip install $(PIP_OPTIONS) --no-binary $(SRC_ONLY_PKGS) --ignore-installed -r /dev/stdin ; \
pip uninstall --yes -r requirements/requirements_tower_uninstall.txt; \
fi fi
$(VENV_BASE)/tower/bin/pip uninstall --yes -r requirements/requirements_tower_uninstall.txt
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
@@ -481,7 +483,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; \
@@ -493,6 +495,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 \
@@ -608,7 +616,7 @@ ui-test-ci: $(UI_DEPS_FLAG_FILE)
testjs_ci: testjs_ci:
echo "Update UI unittests later" #ui-test-ci echo "Update UI unittests later" #ui-test-ci
jshint: jshint: $(UI_DEPS_FLAG_FILE)
$(NPM_BIN) run --prefix awx/ui jshint $(NPM_BIN) run --prefix awx/ui jshint
ui-test-saucelabs: $(UI_DEPS_FLAG_FILE) ui-test-saucelabs: $(UI_DEPS_FLAG_FILE)
@@ -693,24 +701,68 @@ setup_bundle_tarball: setup-bundle-build setup-bundle-build/$(OFFLINE_TAR_FILE)
rpm-build: rpm-build:
mkdir -p $@ mkdir -p $@
rpm-build/$(SDIST_TAR_FILE): rpm-build dist/$(SDIST_TAR_FILE) rpm-build/$(SDIST_TAR_FILE): rpm-build dist/$(SDIST_TAR_FILE) tar-build/$(SETUP_TAR_FILE)
cp packaging/rpm/$(NAME).spec rpm-build/ cp packaging/rpm/$(NAME).spec rpm-build/
cp packaging/rpm/tower.te rpm-build/ cp packaging/rpm/tower.te rpm-build/
cp packaging/rpm/tower.fc rpm-build/ cp packaging/rpm/tower.fc rpm-build/
cp packaging/rpm/$(NAME).sysconfig rpm-build/ cp packaging/rpm/$(NAME).sysconfig rpm-build/
cp packaging/remove_tower_source.py rpm-build/ cp packaging/remove_tower_source.py rpm-build/
cp packaging/bytecompile.sh rpm-build/ cp packaging/bytecompile.sh rpm-build/
cp tar-build/$(SETUP_TAR_FILE) rpm-build/
if [ "$(OFFICIAL)" != "yes" ] ; then \ if [ "$(OFFICIAL)" != "yes" ] ; then \
(cd dist/ && tar zxf $(SDIST_TAR_FILE)) ; \ (cd dist/ && tar zxf $(SDIST_TAR_FILE)) ; \
(cd dist/ && mv $(NAME)-$(VERSION)-$(BUILD) $(NAME)-$(VERSION)) ; \ (cd dist/ && mv $(NAME)-$(VERSION)-$(BUILD) $(NAME)-$(VERSION)) ; \
(cd dist/ && tar czf ../rpm-build/$(SDIST_TAR_FILE) $(NAME)-$(VERSION)) ; \ (cd dist/ && tar czf ../rpm-build/$(SDIST_TAR_FILE) $(NAME)-$(VERSION)) ; \
ln -sf $(SDIST_TAR_FILE) rpm-build/$(NAME)-$(VERSION).tar.gz ; \ ln -sf $(SDIST_TAR_FILE) rpm-build/$(NAME)-$(VERSION).tar.gz ; \
(cd tar-build/ && tar zxf $(SETUP_TAR_FILE)) ; \
(cd tar-build/ && mv $(NAME)-setup-$(VERSION)-$(BUILD) $(NAME)-setup-$(VERSION)) ; \
(cd tar-build/ && tar czf ../rpm-build/$(SETUP_TAR_FILE) $(NAME)-setup-$(VERSION)) ; \
ln -sf $(SETUP_TAR_FILE) rpm-build/$(NAME)-setup-$(VERSION).tar.gz ; \
else \ else \
cp -a dist/$(SDIST_TAR_FILE) rpm-build/ ; \ cp -a dist/$(SDIST_TAR_FILE) rpm-build/ ; \
fi fi
rpmtar: sdist rpm-build/$(SDIST_TAR_FILE) rpmtar: sdist rpm-build/$(SDIST_TAR_FILE)
brewrpmtar: rpm-build/python-deps.tar.gz requirements/requirements_local.txt requirements/requirements_ansible_local.txt rpmtar
rpm-build/python-deps.tar.gz: requirements/vendor rpm-build
tar czf rpm-build/python-deps.tar.gz requirements/vendor
requirements/vendor:
cat requirements/requirements.txt requirements/requirements_git.txt | pip download \
--no-binary=:all: \
--requirement=/dev/stdin \
--dest=$@ \
--exists-action=i
cat requirements/requirements_ansible.txt requirements/requirements_ansible_git.txt | pip download \
--no-binary=:all: \
--requirement=/dev/stdin \
--dest=$@ \
--exists-action=i
pip download \
--no-binary=:all: \
--requirement=requirements/requirements_setup_requires.txt \
--dest=$@ \
--exists-action=i
requirements/requirements_local.txt:
@echo "This is going to take a while..."
pip download \
--requirement=requirements/requirements_git.txt \
--no-deps \
--exists-action=w \
--dest=requirements/vendor 2>/dev/null | sed -n 's/^\s*Saved\s*//p' > $@
requirements/requirements_ansible_local.txt:
pip download \
--requirement=requirements/requirements_ansible_git.txt \
--no-deps \
--exists-action=w \
--dest=requirements/vendor 2>/dev/null | sed -n 's/^\s*Saved\s*//p' > $@
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)
@@ -721,6 +773,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)
@@ -829,7 +883,7 @@ amazon-ebs:
cd packaging/packer && $(PACKER) build -only $@ $(PACKER_BUILD_OPTS) -var "aws_instance_count=$(AWS_INSTANCE_COUNT)" -var "product_version=$(VERSION)" packer-$(NAME).json cd packaging/packer && $(PACKER) build -only $@ $(PACKER_BUILD_OPTS) -var "aws_instance_count=$(AWS_INSTANCE_COUNT)" -var "product_version=$(VERSION)" packer-$(NAME).json
# Vagrant box using virtualbox provider # Vagrant box using virtualbox provider
vagrant-virtualbox: packaging/packer/ansible-tower-$(VERSION)-virtualbox.box vagrant-virtualbox: packaging/packer/ansible-tower-$(VERSION)-virtualbox.box tar-build/$(SETUP_TAR_FILE)
packaging/packer/ansible-tower-$(VERSION)-virtualbox.box: packaging/packer/output-virtualbox-iso/centos-7.ovf packaging/packer/ansible-tower-$(VERSION)-virtualbox.box: packaging/packer/output-virtualbox-iso/centos-7.ovf
cd packaging/packer && $(PACKER) build -only virtualbox-ovf $(PACKER_BUILD_OPTS) -var "aws_instance_count=$(AWS_INSTANCE_COUNT)" -var "product_version=$(VERSION)" packer-$(NAME).json cd packaging/packer && $(PACKER) build -only virtualbox-ovf $(PACKER_BUILD_OPTS) -var "aws_instance_count=$(AWS_INSTANCE_COUNT)" -var "product_version=$(VERSION)" packer-$(NAME).json
@@ -840,7 +894,7 @@ packaging/packer/output-virtualbox-iso/centos-7.ovf:
virtualbox-iso: packaging/packer/output-virtualbox-iso/centos-7.ovf virtualbox-iso: packaging/packer/output-virtualbox-iso/centos-7.ovf
# Vagrant box using VMware provider # Vagrant box using VMware provider
vagrant-vmware: packaging/packer/ansible-tower-$(VERSION)-vmware.box vagrant-vmware: packaging/packer/ansible-tower-$(VERSION)-vmware.box tar-build/$(SETUP_TAR_FILE)
packaging/packer/output-vmware-iso/centos-7.vmx: packaging/packer/output-vmware-iso/centos-7.vmx:
cd packaging/packer && $(PACKER) build -only vmware-iso packer-centos-7.json cd packaging/packer && $(PACKER) build -only vmware-iso packer-centos-7.json

View File

@@ -5,7 +5,7 @@ import os
import sys import sys
import warnings import warnings
__version__ = '3.1.1' __version__ = '3.1.2'
__all__ = ['__version__'] __all__ = ['__version__']

View File

@@ -3,6 +3,7 @@
# Python # Python
import re import re
import json
# Django # Django
from django.core.exceptions import FieldError, ValidationError from django.core.exceptions import FieldError, ValidationError
@@ -291,7 +292,7 @@ class FieldLookupBackend(BaseFilterBackend):
except (FieldError, FieldDoesNotExist, ValueError, TypeError) as e: except (FieldError, FieldDoesNotExist, ValueError, TypeError) as e:
raise ParseError(e.args[0]) raise ParseError(e.args[0])
except ValidationError as e: except ValidationError as e:
raise ParseError(e.messages) raise ParseError(json.dumps(e.messages, ensure_ascii=False))
class OrderByBackend(BaseFilterBackend): class OrderByBackend(BaseFilterBackend):
@@ -310,6 +311,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,
@@ -332,3 +335,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

View File

@@ -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)
@@ -3503,6 +3504,7 @@ class BaseJobEventsList(SubListAPIView):
parent_model = None # Subclasses must define this attribute. parent_model = None # Subclasses must define this attribute.
relationship = 'job_events' relationship = 'job_events'
view_name = _('Job Events List') view_name = _('Job Events List')
search_fields = ('stdout',)
def finalize_response(self, request, response, *args, **kwargs): def finalize_response(self, request, response, *args, **kwargs):
response['X-UI-Max-Events'] = settings.RECOMMENDED_MAX_EVENTS_DISPLAY_HEADER response['X-UI-Max-Events'] = settings.RECOMMENDED_MAX_EVENTS_DISPLAY_HEADER
@@ -3664,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:

View File

2
awx/lib/tests/pytest.ini Normal file
View File

@@ -0,0 +1,2 @@
[pytest]
addopts = -v

View 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(['-v'])[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)

View File

@@ -30,6 +30,8 @@ from ansible.plugins.callback.default import CallbackModule as DefaultCallbackMo
from .events import event_context from .events import event_context
from .minimal import CallbackModule as MinimalCallbackModule from .minimal import CallbackModule as MinimalCallbackModule
CENSORED = "the output has been hidden due to the fact that 'no_log: true' was specified for this result" # noqa
class BaseCallbackModule(CallbackBase): class BaseCallbackModule(CallbackBase):
''' '''
@@ -55,22 +57,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 +71,13 @@ class BaseCallbackModule(CallbackBase):
else: else:
task = None task = None
if event_data.get('res'):
if event_data['res'].get('_ansible_no_log', False):
event_data['res'] = {'censored': CENSORED}
for i, item in enumerate(event_data['res'].get('results', [])):
if event_data['res']['results'][i].get('_ansible_no_log', False):
event_data['res']['results'][i] = {'censored': CENSORED}
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 +125,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):
@@ -304,6 +299,12 @@ class BaseCallbackModule(CallbackBase):
def v2_runner_on_ok(self, result): def v2_runner_on_ok(self, result):
# FIXME: Display detailed results or not based on verbosity. # FIXME: Display detailed results or not based on verbosity.
# strip environment vars from the job event; it already exists on the
# job and sensitive values are filtered there
if result._task.get_name() == 'setup':
result._result.get('ansible_facts', {}).pop('ansible_env', None)
event_data = dict( event_data = dict(
host=result._host.get_name(), host=result._host.get_name(),
remote_addr=result._host.address, remote_addr=result._host.address,

View File

@@ -242,9 +242,11 @@ register(
field_class=fields.IntegerField, field_class=fields.IntegerField,
allow_null=True, allow_null=True,
label=_('Logging Aggregator Port'), label=_('Logging Aggregator Port'),
help_text=_('Port on Logging Aggregator to send logs to (if required).'), help_text=_('Port on Logging Aggregator to send logs to (if required and not'
' provided in Logging Aggregator).'),
category=_('Logging'), category=_('Logging'),
category_slug='logging', category_slug='logging',
required=False
) )
register( register(
'LOG_AGGREGATOR_TYPE', 'LOG_AGGREGATOR_TYPE',

View File

@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0035_v310_remove_tower_settings'),
]
operations = [
migrations.AlterField(
model_name='project',
name='scm_type',
field=models.CharField(default=b'', choices=[(b'', 'Manual'), (b'git', 'Git'), (b'hg', 'Mercurial'), (b'svn', 'Subversion'), (b'insights', 'Red Hat Insights')], max_length=8, blank=True, help_text='Specifies the source control system used to store the project.', verbose_name='SCM Type'),
),
migrations.AlterField(
model_name='projectupdate',
name='scm_type',
field=models.CharField(default=b'', choices=[(b'', 'Manual'), (b'git', 'Git'), (b'hg', 'Mercurial'), (b'svn', 'Subversion'), (b'insights', 'Red Hat Insights')], max_length=8, blank=True, help_text='Specifies the source control system used to store the project.', verbose_name='SCM Type'),
),
]

View File

@@ -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)

View File

@@ -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

View File

@@ -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
@@ -1139,7 +1143,7 @@ class JobEvent(CreatedModifiedModel):
# Save artifact data to parent job (if provided). # Save artifact data to parent job (if provided).
if artifact_dict: if artifact_dict:
if event_data and isinstance(event_data, 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 # sensitive yet. Going forward, core will not use
# _ansible_no_log to denote sensitive set_stats calls. # _ansible_no_log to denote sensitive set_stats calls.
# Instead, they plan to add a flag outside of the traditional # Instead, they plan to add a flag outside of the traditional

View File

@@ -130,13 +130,18 @@ class SurveyJobTemplateMixin(models.Model):
for survey_element in self.survey_spec.get("spec", []): for survey_element in self.survey_spec.get("spec", []):
default = survey_element.get('default') default = survey_element.get('default')
variable_key = survey_element.get('variable') variable_key = survey_element.get('variable')
if survey_element.get('type') == 'password': if survey_element.get('type') == 'password':
if variable_key in kwargs_extra_vars and default: if variable_key in kwargs_extra_vars and default:
kw_value = kwargs_extra_vars[variable_key] kw_value = kwargs_extra_vars[variable_key]
if kw_value.startswith('$encrypted$') and kw_value != default: if kw_value.startswith('$encrypted$') and kw_value != default:
kwargs_extra_vars[variable_key] = default kwargs_extra_vars[variable_key] = default
if default is not None: if default is not None:
extra_vars[variable_key] = default data = {variable_key: default}
errors = self._survey_element_validation(survey_element, data)
if not errors:
extra_vars[variable_key] = default
# Overwrite job template extra vars with explicit job extra vars # Overwrite job template extra vars with explicit job extra vars
# and add on job extra vars # and add on job extra vars
@@ -144,6 +149,65 @@ class SurveyJobTemplateMixin(models.Model):
kwargs['extra_vars'] = json.dumps(extra_vars) kwargs['extra_vars'] = json.dumps(extra_vars)
return kwargs return kwargs
def _survey_element_validation(self, survey_element, data):
errors = []
if survey_element['variable'] not in data and survey_element['required']:
errors.append("'%s' value missing" % survey_element['variable'])
elif survey_element['type'] in ["textarea", "text", "password"]:
if survey_element['variable'] in data:
if type(data[survey_element['variable']]) not in (str, unicode):
errors.append("Value %s for '%s' expected to be a string." % (data[survey_element['variable']],
survey_element['variable']))
return errors
if 'min' in survey_element and survey_element['min'] not in ["", None] and len(data[survey_element['variable']]) < int(survey_element['min']):
errors.append("'%s' value %s is too small (length is %s must be at least %s)." %
(survey_element['variable'], data[survey_element['variable']], len(data[survey_element['variable']]), survey_element['min']))
if 'max' in survey_element and survey_element['max'] not in ["", None] and len(data[survey_element['variable']]) > int(survey_element['max']):
errors.append("'%s' value %s is too large (must be no more than %s)." %
(survey_element['variable'], data[survey_element['variable']], survey_element['max']))
elif survey_element['type'] == 'integer':
if survey_element['variable'] in data:
if type(data[survey_element['variable']]) != int:
errors.append("Value %s for '%s' expected to be an integer." % (data[survey_element['variable']],
survey_element['variable']))
return errors
if 'min' in survey_element and survey_element['min'] not in ["", None] and survey_element['variable'] in data and \
data[survey_element['variable']] < int(survey_element['min']):
errors.append("'%s' value %s is too small (must be at least %s)." %
(survey_element['variable'], data[survey_element['variable']], survey_element['min']))
if 'max' in survey_element and survey_element['max'] not in ["", None] and survey_element['variable'] in data and \
data[survey_element['variable']] > int(survey_element['max']):
errors.append("'%s' value %s is too large (must be no more than %s)." %
(survey_element['variable'], data[survey_element['variable']], survey_element['max']))
elif survey_element['type'] == 'float':
if survey_element['variable'] in data:
if type(data[survey_element['variable']]) not in (float, int):
errors.append("Value %s for '%s' expected to be a numeric type." % (data[survey_element['variable']],
survey_element['variable']))
return errors
if 'min' in survey_element and survey_element['min'] not in ["", None] and data[survey_element['variable']] < float(survey_element['min']):
errors.append("'%s' value %s is too small (must be at least %s)." %
(survey_element['variable'], data[survey_element['variable']], survey_element['min']))
if 'max' in survey_element and survey_element['max'] not in ["", None] and data[survey_element['variable']] > float(survey_element['max']):
errors.append("'%s' value %s is too large (must be no more than %s)." %
(survey_element['variable'], data[survey_element['variable']], survey_element['max']))
elif survey_element['type'] == 'multiselect':
if survey_element['variable'] in data:
if type(data[survey_element['variable']]) != list:
errors.append("'%s' value is expected to be a list." % survey_element['variable'])
else:
for val in data[survey_element['variable']]:
if val not in survey_element['choices']:
errors.append("Value %s for '%s' expected to be one of %s." % (val, survey_element['variable'],
survey_element['choices']))
elif survey_element['type'] == 'multiplechoice':
if survey_element['variable'] in data:
if data[survey_element['variable']] not in survey_element['choices']:
errors.append("Value %s for '%s' expected to be one of %s." % (data[survey_element['variable']],
survey_element['variable'],
survey_element['choices']))
return errors
def survey_variable_validation(self, data): def survey_variable_validation(self, data):
errors = [] errors = []
if not self.survey_enabled: if not self.survey_enabled:
@@ -153,62 +217,7 @@ class SurveyJobTemplateMixin(models.Model):
if 'description' not in self.survey_spec: if 'description' not in self.survey_spec:
errors.append("'description' missing from survey spec.") errors.append("'description' missing from survey spec.")
for survey_element in self.survey_spec.get("spec", []): for survey_element in self.survey_spec.get("spec", []):
if survey_element['variable'] not in data and \ errors += self._survey_element_validation(survey_element, data)
survey_element['required']:
errors.append("'%s' value missing" % survey_element['variable'])
elif survey_element['type'] in ["textarea", "text", "password"]:
if survey_element['variable'] in data:
if type(data[survey_element['variable']]) not in (str, unicode):
errors.append("Value %s for '%s' expected to be a string." % (data[survey_element['variable']],
survey_element['variable']))
continue
if 'min' in survey_element and survey_element['min'] not in ["", None] and len(data[survey_element['variable']]) < int(survey_element['min']):
errors.append("'%s' value %s is too small (length is %s must be at least %s)." %
(survey_element['variable'], data[survey_element['variable']], len(data[survey_element['variable']]), survey_element['min']))
if 'max' in survey_element and survey_element['max'] not in ["", None] and len(data[survey_element['variable']]) > int(survey_element['max']):
errors.append("'%s' value %s is too large (must be no more than %s)." %
(survey_element['variable'], data[survey_element['variable']], survey_element['max']))
elif survey_element['type'] == 'integer':
if survey_element['variable'] in data:
if type(data[survey_element['variable']]) != int:
errors.append("Value %s for '%s' expected to be an integer." % (data[survey_element['variable']],
survey_element['variable']))
continue
if 'min' in survey_element and survey_element['min'] not in ["", None] and survey_element['variable'] in data and \
data[survey_element['variable']] < int(survey_element['min']):
errors.append("'%s' value %s is too small (must be at least %s)." %
(survey_element['variable'], data[survey_element['variable']], survey_element['min']))
if 'max' in survey_element and survey_element['max'] not in ["", None] and survey_element['variable'] in data and \
data[survey_element['variable']] > int(survey_element['max']):
errors.append("'%s' value %s is too large (must be no more than %s)." %
(survey_element['variable'], data[survey_element['variable']], survey_element['max']))
elif survey_element['type'] == 'float':
if survey_element['variable'] in data:
if type(data[survey_element['variable']]) not in (float, int):
errors.append("Value %s for '%s' expected to be a numeric type." % (data[survey_element['variable']],
survey_element['variable']))
continue
if 'min' in survey_element and survey_element['min'] not in ["", None] and data[survey_element['variable']] < float(survey_element['min']):
errors.append("'%s' value %s is too small (must be at least %s)." %
(survey_element['variable'], data[survey_element['variable']], survey_element['min']))
if 'max' in survey_element and survey_element['max'] not in ["", None] and data[survey_element['variable']] > float(survey_element['max']):
errors.append("'%s' value %s is too large (must be no more than %s)." %
(survey_element['variable'], data[survey_element['variable']], survey_element['max']))
elif survey_element['type'] == 'multiselect':
if survey_element['variable'] in data:
if type(data[survey_element['variable']]) != list:
errors.append("'%s' value is expected to be a list." % survey_element['variable'])
else:
for val in data[survey_element['variable']]:
if val not in survey_element['choices']:
errors.append("Value %s for '%s' expected to be one of %s." % (val, survey_element['variable'],
survey_element['choices']))
elif survey_element['type'] == 'multiplechoice':
if survey_element['variable'] in data:
if data[survey_element['variable']] not in survey_element['choices']:
errors.append("Value %s for '%s' expected to be one of %s." % (data[survey_element['variable']],
survey_element['variable'],
survey_element['choices']))
return errors return errors

View File

@@ -43,6 +43,7 @@ class ProjectOptions(models.Model):
('git', _('Git')), ('git', _('Git')),
('hg', _('Mercurial')), ('hg', _('Mercurial')),
('svn', _('Subversion')), ('svn', _('Subversion')),
('insights', _('Red Hat Insights')),
] ]
class Meta: class Meta:
@@ -120,6 +121,8 @@ class ProjectOptions(models.Model):
return self.scm_type or '' return self.scm_type or ''
def clean_scm_url(self): def clean_scm_url(self):
if self.scm_type == 'insights':
self.scm_url = settings.INSIGHTS_URL_BASE
scm_url = unicode(self.scm_url or '') scm_url = unicode(self.scm_url or '')
if not self.scm_type: if not self.scm_type:
return '' return ''
@@ -141,6 +144,8 @@ class ProjectOptions(models.Model):
if cred.kind != 'scm': if cred.kind != 'scm':
raise ValidationError(_("Credential kind must be 'scm'.")) raise ValidationError(_("Credential kind must be 'scm'."))
try: try:
if self.scm_type == 'insights':
self.scm_url = settings.INSIGHTS_URL_BASE
scm_url = update_scm_url(self.scm_type, self.scm_url, scm_url = update_scm_url(self.scm_type, self.scm_url,
check_special_cases=False) check_special_cases=False)
scm_url_parts = urlparse.urlsplit(scm_url) scm_url_parts = urlparse.urlsplit(scm_url)

View File

@@ -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://'):

View File

@@ -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):
@@ -471,24 +471,24 @@ class BaseTask(Task):
env['PROOT_TMP_DIR'] = settings.AWX_PROOT_BASE_PATH env['PROOT_TMP_DIR'] = settings.AWX_PROOT_BASE_PATH
return env return env
def build_safe_env(self, instance, **kwargs): def build_safe_env(self, env, **kwargs):
''' '''
Build environment dictionary, hiding potentially sensitive information Build environment dictionary, hiding potentially sensitive information
such as passwords or keys. such as passwords or keys.
''' '''
hidden_re = re.compile(r'API|TOKEN|KEY|SECRET|PASS', re.I) hidden_re = re.compile(r'API|TOKEN|KEY|SECRET|PASS', re.I)
urlpass_re = re.compile(r'^.*?://.?:(.*?)@.*?$') urlpass_re = re.compile(r'^.*?://[^:]+:(.*?)@.*?$')
env = self.build_env(instance, **kwargs) safe_env = dict(env)
for k,v in env.items(): for k,v in safe_env.items():
if k in ('REST_API_URL', 'AWS_ACCESS_KEY', 'AWS_ACCESS_KEY_ID'): if k in ('REST_API_URL', 'AWS_ACCESS_KEY', 'AWS_ACCESS_KEY_ID'):
continue continue
elif k.startswith('ANSIBLE_') and not k.startswith('ANSIBLE_NET'): elif k.startswith('ANSIBLE_') and not k.startswith('ANSIBLE_NET'):
continue continue
elif hidden_re.search(k): elif hidden_re.search(k):
env[k] = HIDDEN_PASSWORD safe_env[k] = HIDDEN_PASSWORD
elif type(v) == str and urlpass_re.match(v): elif type(v) == str and urlpass_re.match(v):
env[k] = urlpass_re.sub(HIDDEN_PASSWORD, v) safe_env[k] = urlpass_re.sub(HIDDEN_PASSWORD, v)
return env return safe_env
def args2cmdline(self, *args): def args2cmdline(self, *args):
return ' '.join([pipes.quote(a) for a in args]) return ' '.join([pipes.quote(a) for a in args])
@@ -699,7 +699,7 @@ class BaseTask(Task):
output_replacements = self.build_output_replacements(instance, **kwargs) output_replacements = self.build_output_replacements(instance, **kwargs)
cwd = self.build_cwd(instance, **kwargs) cwd = self.build_cwd(instance, **kwargs)
env = self.build_env(instance, **kwargs) env = self.build_env(instance, **kwargs)
safe_env = self.build_safe_env(instance, **kwargs) safe_env = self.build_safe_env(env, **kwargs)
stdout_handle = self.get_stdout_handle(instance) stdout_handle = self.get_stdout_handle(instance)
if self.should_use_proot(instance, **kwargs): if self.should_use_proot(instance, **kwargs):
if not check_proot_installed(): if not check_proot_installed():
@@ -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.
@@ -1189,6 +1190,9 @@ class RunProjectUpdate(BaseTask):
scm_username = False scm_username = False
elif scm_url_parts.scheme.endswith('ssh'): elif scm_url_parts.scheme.endswith('ssh'):
scm_password = False scm_password = False
elif scm_type == 'insights':
extra_vars['scm_username'] = scm_username
extra_vars['scm_password'] = scm_password
scm_url = update_scm_url(scm_type, scm_url, scm_username, scm_url = update_scm_url(scm_type, scm_url, scm_username,
scm_password, scp_format=True) scm_password, scp_format=True)
else: else:
@@ -1218,6 +1222,7 @@ class RunProjectUpdate(BaseTask):
scm_branch = project_update.scm_branch or {'hg': 'tip'}.get(project_update.scm_type, 'HEAD') scm_branch = project_update.scm_branch or {'hg': 'tip'}.get(project_update.scm_type, 'HEAD')
extra_vars.update({ extra_vars.update({
'project_path': project_update.get_project_path(check_if_exists=False), 'project_path': project_update.get_project_path(check_if_exists=False),
'insights_url': settings.INSIGHTS_URL_BASE,
'scm_type': project_update.scm_type, 'scm_type': project_update.scm_type,
'scm_url': scm_url, 'scm_url': scm_url,
'scm_branch': scm_branch, 'scm_branch': scm_branch,
@@ -1314,10 +1319,10 @@ class RunProjectUpdate(BaseTask):
lines = fd.readlines() lines = fd.readlines()
if lines: if lines:
p.scm_revision = lines[0].strip() p.scm_revision = lines[0].strip()
p.playbook_files = p.playbooks
p.save()
else: else:
logger.error("Could not find scm revision in check") logger.info("Could not find scm revision in check")
p.playbook_files = p.playbooks
p.save()
try: try:
os.remove(self.revision_path) os.remove(self.revision_path)
except Exception, e: except Exception, e:

View File

@@ -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
# #

View File

@@ -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),

View File

@@ -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()

View File

@@ -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

View File

@@ -1,11 +1,13 @@
import os import os
import re import re
import pytest
from pip.operations import freeze from pip.operations import freeze
from django.conf import settings from django.conf import settings
@pytest.mark.skip(reason="This test needs some love")
def test_env_matches_requirements_txt(): def test_env_matches_requirements_txt():
def check_is_in(src, dests): def check_is_in(src, dests):
if src not in dests: if src not in dests:

View File

View 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)

View File

@@ -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

View File

@@ -4,6 +4,7 @@ import json
from awx.main.tasks import RunJob from awx.main.tasks import RunJob
from awx.main.models import ( from awx.main.models import (
Job, Job,
JobTemplate,
WorkflowJobTemplate WorkflowJobTemplate
) )
@@ -78,6 +79,18 @@ def test_job_args_unredacted_passwords(job):
assert extra_vars['secret_key'] == 'my_password' assert extra_vars['secret_key'] == 'my_password'
def test_update_kwargs_survey_invalid_default(survey_spec_factory):
spec = survey_spec_factory('var2')
spec['spec'][0]['required'] = False
spec['spec'][0]['min'] = 3
spec['spec'][0]['default'] = 1
jt = JobTemplate(name="test-jt", survey_spec=spec, survey_enabled=True, extra_vars="var2: 2")
defaulted_extra_vars = jt._update_unified_job_kwargs()
assert 'extra_vars' in defaulted_extra_vars
# Make sure we did not set the invalid default of 1
assert json.loads(defaulted_extra_vars['extra_vars'])['var2'] == 2
class TestWorkflowSurveys: class TestWorkflowSurveys:
def test_update_kwargs_survey_defaults(self, survey_spec_factory): def test_update_kwargs_survey_defaults(self, survey_spec_factory):
"Assure that the survey default over-rides a JT variable" "Assure that the survey default over-rides a JT variable"

View File

@@ -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'])

View File

@@ -71,6 +71,25 @@ def test_run_admin_checks_usage(mocker, current_instances, call_count):
assert 'expire' in mock_sm.call_args_list[0][0][0] assert 'expire' in mock_sm.call_args_list[0][0][0]
@pytest.mark.parametrize("key,value", [
('REST_API_TOKEN', 'SECRET'),
('SECRET_KEY', 'SECRET'),
('RABBITMQ_PASS', 'SECRET'),
('VMWARE_PASSWORD', 'SECRET'),
('API_SECRET', 'SECRET'),
('CALLBACK_CONNECTION', 'amqp://tower:password@localhost:5672/tower'),
])
def test_safe_env_filtering(key, value):
task = tasks.RunJob()
assert task.build_safe_env({key: value})[key] == tasks.HIDDEN_PASSWORD
def test_safe_env_returns_new_copy():
task = tasks.RunJob()
env = {'foo': 'bar'}
assert task.build_safe_env(env) is not env
def test_openstack_client_config_generation(mocker): def test_openstack_client_config_generation(mocker):
update = tasks.RunInventoryUpdate() update = tasks.RunInventoryUpdate()
inventory_update = mocker.Mock(**{ inventory_update = mocker.Mock(**{

View File

@@ -1,7 +1,9 @@
import base64 import base64
import cStringIO
import json import json
import logging import logging
from django.conf import settings
from django.conf import LazySettings from django.conf import LazySettings
import pytest import pytest
import requests import requests
@@ -40,17 +42,27 @@ def ok200_adapter():
return OK200Adapter() return OK200Adapter()
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()
@@ -95,7 +107,17 @@ def test_https_logging_handler_splunk_auth_info():
('http://localhost', None, 'http://localhost'), ('http://localhost', None, 'http://localhost'),
('http://localhost', 80, 'http://localhost'), ('http://localhost', 80, 'http://localhost'),
('http://localhost', 8080, 'http://localhost:8080'), ('http://localhost', 8080, 'http://localhost:8080'),
('https://localhost', 443, 'https://localhost:443') ('https://localhost', 443, 'https://localhost:443'),
('ftp://localhost', 443, 'ftp://localhost:443'),
('https://localhost:550', 443, 'https://localhost:550'),
('https://localhost:yoho/foobar', 443, 'https://localhost:443/foobar'),
('https://localhost:yoho/foobar', None, 'https://localhost:yoho/foobar'),
('http://splunk.server:8088/services/collector/event', 80,
'http://splunk.server:8088/services/collector/event'),
('http://splunk.server/services/collector/event', 80,
'http://splunk.server/services/collector/event'),
('http://splunk.server/services/collector/event', 8088,
'http://splunk.server:8088/services/collector/event'),
]) ])
def test_https_logging_handler_http_host_format(host, port, normalized): def test_https_logging_handler_http_host_format(host, port, normalized):
handler = HTTPSHandler(host=host, port=port) handler = HTTPSHandler(host=host, port=port)
@@ -114,18 +136,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(ok200_adapter, dummy_log_record, def test_https_logging_handler_emit(ok200_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://', ok200_adapter) handler.session.mount('http://', ok200_adapter)
async_futures = handler.emit(dummy_log_record) async_futures = handler.emit(dummy_log_record)
@@ -151,14 +194,12 @@ def test_https_logging_handler_emit(ok200_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(ok200_adapter, def test_https_logging_handler_emit_logstash_with_creds(ok200_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://', ok200_adapter) handler.session.mount('http://', ok200_adapter)
async_futures = handler.emit(dummy_log_record) async_futures = handler.emit(dummy_log_record)
@@ -169,13 +210,11 @@ def test_https_logging_handler_emit_logstash_with_creds(ok200_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(ok200_adapter, def test_https_logging_handler_emit_splunk_with_creds(ok200_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://', ok200_adapter) handler.session.mount('http://', ok200_adapter)
async_futures = handler.emit(dummy_log_record) async_futures = handler.emit(dummy_log_record)

View File

@@ -261,7 +261,7 @@ def update_scm_url(scm_type, url, username=True, password=True,
# git: https://www.kernel.org/pub/software/scm/git/docs/git-clone.html#URLS # git: https://www.kernel.org/pub/software/scm/git/docs/git-clone.html#URLS
# hg: http://www.selenic.com/mercurial/hg.1.html#url-paths # hg: http://www.selenic.com/mercurial/hg.1.html#url-paths
# svn: http://svnbook.red-bean.com/en/1.7/svn-book.html#svn.advanced.reposurls # svn: http://svnbook.red-bean.com/en/1.7/svn-book.html#svn.advanced.reposurls
if scm_type not in ('git', 'hg', 'svn'): if scm_type not in ('git', 'hg', 'svn', 'insights'):
raise ValueError(_('Unsupported SCM type "%s"') % str(scm_type)) raise ValueError(_('Unsupported SCM type "%s"') % str(scm_type))
if not url.strip(): if not url.strip():
return '' return ''
@@ -307,6 +307,7 @@ def update_scm_url(scm_type, url, username=True, password=True,
'git': ('ssh', 'git', 'git+ssh', 'http', 'https', 'ftp', 'ftps', 'file'), 'git': ('ssh', 'git', 'git+ssh', 'http', 'https', 'ftp', 'ftps', 'file'),
'hg': ('http', 'https', 'ssh', 'file'), 'hg': ('http', 'https', 'ssh', 'file'),
'svn': ('http', 'https', 'svn', 'svn+ssh', 'file'), 'svn': ('http', 'https', 'svn', 'svn+ssh', 'file'),
'insights': ('http', 'https')
} }
if parts.scheme not in scm_type_schemes.get(scm_type, ()): if parts.scheme not in scm_type_schemes.get(scm_type, ()):
raise ValueError(_('Unsupported %s URL') % scm_type) raise ValueError(_('Unsupported %s URL') % scm_type)
@@ -342,7 +343,7 @@ def update_scm_url(scm_type, url, username=True, password=True,
#raise ValueError('Password not supported for SSH with Mercurial.') #raise ValueError('Password not supported for SSH with Mercurial.')
netloc_password = '' netloc_password = ''
if netloc_username and parts.scheme != 'file': if netloc_username and parts.scheme != 'file' and scm_type != "insights":
netloc = u':'.join([urllib.quote(x) for x in (netloc_username, netloc_password) if x]) netloc = u':'.join([urllib.quote(x) for x in (netloc_username, netloc_password) if x])
else: else:
netloc = u'' netloc = u''

View File

@@ -13,7 +13,9 @@ class LogstashFormatter(LogstashFormatterVersion1):
ret = super(LogstashFormatter, self).__init__(**kwargs) ret = super(LogstashFormatter, self).__init__(**kwargs)
if settings_module: if settings_module:
self.host_id = settings_module.CLUSTER_HOST_ID self.host_id = settings_module.CLUSTER_HOST_ID
self.tower_uuid = settings_module.LOG_AGGREGATOR_TOWER_UUID if hasattr(settings_module, 'LOG_AGGREGATOR_TOWER_UUID'):
self.tower_uuid = settings_module.LOG_AGGREGATOR_TOWER_UUID
self.message_type = settings_module.LOG_AGGREGATOR_TYPE
return ret return ret
def reformat_data_for_log(self, raw_data, kind=None): def reformat_data_for_log(self, raw_data, kind=None):

View File

@@ -5,6 +5,9 @@
import logging import logging
import json import json
import requests import requests
import time
import urlparse
from concurrent.futures import ThreadPoolExecutor
from copy import copy from copy import copy
# loggly # loggly
@@ -18,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
@@ -33,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',
} }
@@ -47,17 +53,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
@@ -89,10 +119,21 @@ class BaseHTTPSHandler(logging.Handler):
def get_http_host(self): def get_http_host(self):
host = self.host or '' host = self.host or ''
if not host.startswith('http'): # urlparse requires scheme to be provided, default to use http if
host = 'http://%s' % self.host # missing
if self.port != 80 and self.port is not None: if not urlparse.urlsplit(host).scheme:
host = '%s:%s' % (host, str(self.port)) host = 'http://%s' % host
parsed = urlparse.urlsplit(host)
# Insert self.port if its special and port number is either not
# given in host or given as non-numerical
try:
port = parsed.port or self.port
except ValueError:
port = self.port
if port not in (80, None):
new_netloc = '%s:%s' % (parsed.hostname, port)
return urlparse.urlunsplit((parsed.scheme, new_netloc, parsed.path,
parsed.query, parsed.fragment))
return host return host
def get_post_kwargs(self, payload_input): def get_post_kwargs(self, payload_input):
@@ -105,10 +146,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):
@@ -123,10 +162,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/
@@ -147,17 +182,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:
@@ -179,7 +207,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):
@@ -188,7 +216,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:

View File

@@ -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 }}"
@@ -105,6 +105,45 @@
scm_version: "{{ scm_result['after'] }}" scm_version: "{{ scm_result['after'] }}"
when: "'after' in scm_result" when: "'after' in scm_result"
- name: update project using insights
uri:
url: "{{insights_url}}/r/insights/v1/maintenance?ansible=true"
user: "{{scm_username}}"
password: "{{scm_password}}"
force_basic_auth: yes
when: scm_type == 'insights'
register: insights_output
- name: Ensure the project directory is present
file:
dest: "{{project_path|quote}}"
state: directory
when: scm_type == 'insights'
- name: Fetch Insights Playbook With Name
get_url:
url: "{{insights_url}}/r/insights/v3/maintenance/{{item.maintenance_id}}/playbook"
dest: "{{project_path|quote}}/{{item.name}}-{{item.maintenance_id}}.yml"
url_username: "{{scm_username}}"
url_password: "{{scm_password}}"
force_basic_auth: yes
force: yes
when: scm_type == 'insights' and item.name != None
with_items: "{{insights_output.json}}"
failed_when: false
- name: Fetch Insights Playbook
get_url:
url: "{{insights_url}}/r/insights/v3/maintenance/{{item.maintenance_id}}/playbook"
dest: "{{project_path|quote}}/insights-plan-{{item.maintenance_id}}.yml"
url_username: "{{scm_username}}"
url_password: "{{scm_password}}"
force_basic_auth: yes
force: yes
when: scm_type == 'insights' and item.name == None
with_items: "{{insights_output.json}}"
failed_when: false
- name: detect requirements.yml - name: detect requirements.yml
stat: path={{project_path|quote}}/roles/requirements.yml stat: path={{project_path|quote}}/roles/requirements.yml
register: doesRequirementsExist register: doesRequirementsExist
@@ -121,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

View File

@@ -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"}},

View File

@@ -862,9 +862,12 @@ TOWER_ADMIN_ALERTS = True
# Note: This setting may be overridden by database settings. # Note: This setting may be overridden by database settings.
TOWER_URL_BASE = "https://towerhost" TOWER_URL_BASE = "https://towerhost"
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,

View File

@@ -377,9 +377,9 @@ register(
help_text=_('User profile flags updated from group membership (key is user ' help_text=_('User profile flags updated from group membership (key is user '
'attribute name, value is group DN). These are boolean fields ' 'attribute name, value is group DN). These are boolean fields '
'that are matched based on whether the user is a member of the ' 'that are matched based on whether the user is a member of the '
'given group. So far only is_superuser is settable via this ' 'given group. So far only is_superuser and is_system_auditor '
'method. This flag is set both true and false at login time ' 'are settable via this method. This flag is set both true and '
'based on current LDAP settings.'), 'false at login time based on current LDAP settings.'),
category=_('LDAP'), category=_('LDAP'),
category_slug='ldap', category_slug='ldap',
placeholder=collections.OrderedDict([ placeholder=collections.OrderedDict([

View File

@@ -322,7 +322,7 @@ class LDAPUserFlagsField(fields.DictField):
default_error_messages = { default_error_messages = {
'invalid_flag': _('Invalid user flag: "{invalid_flag}".'), 'invalid_flag': _('Invalid user flag: "{invalid_flag}".'),
} }
valid_user_flags = {'is_superuser'} valid_user_flags = {'is_superuser', 'is_system_auditor'}
child = LDAPDNField() child = LDAPDNField()
def to_internal_value(self, data): def to_internal_value(self, data):

View File

@@ -40,7 +40,6 @@
.Form-title{ .Form-title{
flex: 0 1 auto; flex: 0 1 auto;
text-transform: uppercase;
color: @list-header-txt; color: @list-header-txt;
font-size: 14px; font-size: 14px;
font-weight: bold; font-weight: bold;
@@ -50,6 +49,10 @@
margin-bottom: 20px; margin-bottom: 20px;
} }
.Form-title--uppercase {
text-transform: uppercase;
}
.Form-secondaryTitle{ .Form-secondaryTitle{
color: @default-icon; color: @default-icon;
padding-bottom: 20px; padding-bottom: 20px;
@@ -98,8 +101,8 @@
.Form-tabHolder{ .Form-tabHolder{
display: flex; display: flex;
margin-bottom: 20px;
min-height: 30px; min-height: 30px;
flex-wrap:wrap;
} }
.Form-tabs { .Form-tabs {
@@ -115,6 +118,7 @@
height: 30px; height: 30px;
border-radius: 5px; border-radius: 5px;
margin-right: 20px; margin-right: 20px;
margin-bottom: 20px;
padding-left: 10px; padding-left: 10px;
padding-right: 10px; padding-right: 10px;
padding-bottom: 5px; padding-bottom: 5px;
@@ -560,6 +564,8 @@ input[type='radio']:checked:before {
padding-left:15px; padding-left:15px;
padding-right: 15px; padding-right: 15px;
margin-right: 20px; margin-right: 20px;
min-height: 30px;
margin-bottom: 20px;
} }
.Form-primaryButton:hover { .Form-primaryButton:hover {

View File

@@ -147,7 +147,6 @@ table, tbody {
font-size: 14px; font-size: 14px;
font-weight: bold; font-weight: bold;
margin-right: 10px; margin-right: 10px;
text-transform: uppercase;
} }
.List-actionHolder { .List-actionHolder {

View File

@@ -1,47 +1,60 @@
/** @define About */ /** @define About */
@import "./client/src/shared/branding/colors.default.less"; @import "./client/src/shared/branding/colors.default.less";
.About-cowsay--container{
width: 340px; .About-ansibleVersion,
margin: 0 auto; .About-cowsayCode {
font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
} }
.About-cowsay--code{
background-color: @default-bg; .About-cowsayContainer {
padding-left: 30px; width: 340px;
border-style: none; margin: 0 auto;
max-width: 340px;
padding-left: 30px;
} }
.About .modal-header{ .About-cowsayCode {
border: none; background-color: @default-bg;
padding-bottom: 0px; padding-left: 30px;
border-style: none;
max-width: 340px;
padding-left: 30px;
} }
.About .modal-dialog{ .About-modalHeader {
max-width: 500px; border: none;
padding-bottom: 0px;
} }
.About .modal-body{ .About-modalDialog {
padding-top: 0px; max-width: 500px;
} }
.About-brand--redhat{
.About-modalBody {
padding-top: 0px;
padding-bottom: 0px;
}
.About-brandImg {
float: left; float: left;
width: 112px; width: 112px;
padding-top: 13px;
} }
.About-brand--ansible{
max-width: 120px; .About-close {
margin: 0 auto; position: absolute;
top: 15px;
right: 15px;
z-index: 10;
} }
.About-close{
position: absolute; .About-modalFooter {
top: 15px; clear: both;
right: 15px;
z-index: 10;
} }
.About p{
color: @default-interface-txt; .About-footerText {
text-align: right;
color: @default-interface-txt;
margin: 0; margin: 0;
font-size: 12px; font-size: 12px;
padding-top: 10px; padding-top: 10px;
} }
.About-modal--footer {
clear: both; .About-ansibleVersion {
color: @default-data-txt;
} }

View File

@@ -1,27 +1,12 @@
export default export default
['$scope', '$state', 'ConfigService', 'i18n', ['$scope', '$state', 'ConfigService',
function($scope, $state, ConfigService, i18n){ function($scope, $state, ConfigService){
var processVersion = function(version){
// prettify version & calculate padding
// e,g 3.0.0-0.git201602191743/ -> 3.0.0
var split = version.split('-')[0];
var spaces = Math.floor((16-split.length)/2),
paddedStr = "";
for(var i=0; i<=spaces; i++){
paddedStr = paddedStr +" ";
}
paddedStr = paddedStr + split;
for(var j = paddedStr.length; j<16; j++){
paddedStr = paddedStr + " ";
}
return paddedStr;
};
var init = function(){ var init = function(){
ConfigService.getConfig() ConfigService.getConfig()
.then(function(config){ .then(function(config){
$scope.version = config.version.split('-')[0];
$scope.ansible_version = config.ansible_version;
$scope.subscription = config.license_info.subscription_name; $scope.subscription = config.license_info.subscription_name;
$scope.version = processVersion(config.version);
$scope.version_str = i18n._("Version");
$('#about-modal').modal('show'); $('#about-modal').modal('show');
}); });
}; };

View File

@@ -1,19 +1,18 @@
<div class="About modal fade" id="about-modal"> <div class="About modal fade" id="about-modal">
<div class="modal-dialog"> <div class="modal-dialog About-modalDialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header About-modalHeader">
<button data-dismiss="modal" type="button" class="close About-close"> <button data-dismiss="modal" type="button" class="close About-close">
<span class="fa fa-times-circle"></span> <span class="fa fa-times-circle"></span>
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body About-modalBody">
<div class="About-cowsay--container"> <div class="About-cowsayContainer">
<!-- Don't indent this properly, you'll break the cow --> <!-- Don't indent this properly, you'll break the cow -->
<pre class="About-cowsay--code"> <pre class="About-cowsayCode">
________________ _______________
/ Tower {{version_str}} \ < Tower {{version}} >
\<span>{{version}}</span>/ ---------------
----------------
\ ^__^ \ ^__^
\ (oo)\_______ \ (oo)\_______
(__) A )\/\ (__) A )\/\
@@ -21,10 +20,15 @@
|| || || ||
</pre> </pre>
</div> </div>
<div class="About-modal--footer"> <div class="About-modalFooter">
<img class="About-brand--redhat img-responsive" src="/static/assets/tower-logo-login.svg" /> <img class="About-brandImg img-responsive" src="/static/assets/tower-logo-login.svg" />
<p class="text-right">Copyright &copy; 2017 Red Hat, Inc. <br> <p class="About-footerText">
Visit <a href="http://www.ansible.com/" target="_blank">Ansible.com</a> for more information.<br> <span class="About-ansibleVersion">
Ansible {{ ansible_version }}
</span> <br>
Copyright &copy; 2017 Red Hat, Inc. <br>
Visit <a href="http://www.ansible.com/" target="_blank">Ansible.com</a> for more information.<br>
</p>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -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

View File

@@ -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({

View File

@@ -65,7 +65,6 @@
.BreadCrumb-item { .BreadCrumb-item {
display: inline-block; display: inline-block;
color: @default-interface-txt; color: @default-interface-txt;
text-transform: uppercase;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;

View File

@@ -36,19 +36,11 @@
.Form-nav--dropdownContainer { .Form-nav--dropdownContainer {
width: 285px; width: 285px;
margin-top: -52px;
margin-bottom: 22px; margin-bottom: 22px;
margin-left: auto;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
@media (max-width: 900px) {
.Form-nav--dropdownContainer {
margin: 0;
}
}
.Form-nav--dropdown { .Form-nav--dropdown {
width: 60%; width: 60%;
} }

View File

@@ -7,7 +7,7 @@
<div class="tab-pane" id="configuration-panel"> <div class="tab-pane" id="configuration-panel">
<div ng-cloak id="htmlTemplate" class="Panel"> <div ng-cloak id="htmlTemplate" class="Panel">
<div class="Form-header"> <div class="Form-header">
<div class="Form-title" translate>Configure Tower</div> <div class="Form-title" translate>CONFIGURE TOWER</div>
</div> </div>
<div class="row Form-tabRow"> <div class="row Form-tabRow">
<div class="col-lg-12"> <div class="col-lg-12">

View File

@@ -27,7 +27,7 @@
}, },
ncyBreadcrumb: { ncyBreadcrumb: {
parent: 'setup', parent: 'setup',
label: N_("Edit Configuration") label: N_("EDIT CONFIGURATION")
}, },
controller: ConfigurationController, controller: ConfigurationController,
resolve: { resolve: {

View File

@@ -126,6 +126,7 @@ export function CredentialsAdd($scope, $rootScope, $compile, $location, $log,
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,
@@ -288,7 +289,7 @@ CredentialsAdd.$inject = ['$scope', '$rootScope', '$compile', '$location',
export function CredentialsEdit($scope, $rootScope, $compile, $location, $log, export function CredentialsEdit($scope, $rootScope, $compile, $location, $log,
$stateParams, CredentialForm, Rest, Alert, ProcessErrors, ClearScope, Prompt, $stateParams, CredentialForm, Rest, Alert, ProcessErrors, ClearScope, Prompt,
GetBasePath, GetChoices, KindChange, BecomeMethodChange, Empty, OwnerChange, FormSave, Wait, GetBasePath, GetChoices, KindChange, BecomeMethodChange, Empty, OwnerChange, FormSave, Wait,
$state, CreateSelect2, Authorization, i18n) { $state, CreateSelect2, Authorization, i18n, OrgAdminLookup) {
ClearScope(); ClearScope();
@@ -499,6 +500,16 @@ export function CredentialsEdit($scope, $rootScope, $compile, $location, $log,
setAskCheckboxes(); setAskCheckboxes();
if(data.organization) {
OrgAdminLookup.checkForAdminAccess({organization: data.organization})
.then(function(canEditOrg){
$scope.canEditOrg = canEditOrg;
});
}
else {
$scope.canEditOrg = true;
}
$scope.$emit('credentialLoaded'); $scope.$emit('credentialLoaded');
Wait('stop'); Wait('stop');
}) })
@@ -626,5 +637,5 @@ CredentialsEdit.$inject = ['$scope', '$rootScope', '$compile', '$location',
'$log', '$stateParams', 'CredentialForm', 'Rest', 'Alert', '$log', '$stateParams', 'CredentialForm', 'Rest', 'Alert',
'ProcessErrors', 'ClearScope', 'Prompt', 'GetBasePath', 'GetChoices', 'ProcessErrors', 'ClearScope', 'Prompt', 'GetBasePath', 'GetChoices',
'KindChange', 'BecomeMethodChange', 'Empty', 'OwnerChange', 'KindChange', 'BecomeMethodChange', 'Empty', 'OwnerChange',
'FormSave', 'Wait', '$state', 'CreateSelect2', 'Authorization', 'i18n', 'FormSave', 'Wait', '$state', 'CreateSelect2', 'Authorization', 'i18n', 'OrgAdminLookup'
]; ];

View File

@@ -97,7 +97,7 @@ export function ProjectsList($scope, $rootScope, $location, $log, $stateParams,
$scope.reloadList = function(){ $scope.reloadList = function(){
let path = GetBasePath(list.basePath) || GetBasePath(list.name); let path = GetBasePath(list.basePath) || GetBasePath(list.name);
qs.search(path, $stateParams[`${list.iterator}_search`]) qs.search(path, $state.params[`${list.iterator}_search`])
.then(function(searchResponse) { .then(function(searchResponse) {
$scope[`${list.iterator}_dataset`] = searchResponse.data; $scope[`${list.iterator}_dataset`] = searchResponse.data;
$scope[list.name] = $scope[`${list.iterator}_dataset`].results; $scope[list.name] = $scope[`${list.iterator}_dataset`].results;
@@ -314,6 +314,7 @@ export function ProjectsAdd($scope, $rootScope, $compile, $location, $log,
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) {
@@ -348,6 +349,7 @@ export function ProjectsAdd($scope, $rootScope, $compile, $location, $log,
}); });
$scope.scmRequired = false; $scope.scmRequired = false;
$scope.credRequired = false;
master.scm_type = $scope.scm_type; master.scm_type = $scope.scm_type;
}); });
@@ -408,6 +410,7 @@ export function ProjectsAdd($scope, $rootScope, $compile, $location, $log,
if ($scope.scm_type) { if ($scope.scm_type) {
$scope.pathRequired = ($scope.scm_type.value === 'manual') ? true : false; $scope.pathRequired = ($scope.scm_type.value === 'manual') ? true : false;
$scope.scmRequired = ($scope.scm_type.value !== 'manual') ? true : false; $scope.scmRequired = ($scope.scm_type.value !== 'manual') ? true : false;
$scope.credRequired = ($scope.scm_type.value === 'insights') ? true : false;
$scope.scmBranchLabel = ($scope.scm_type.value === 'svn') ? 'Revision #' : 'SCM Branch'; $scope.scmBranchLabel = ($scope.scm_type.value === 'svn') ? 'Revision #' : 'SCM Branch';
} }
@@ -415,6 +418,7 @@ export function ProjectsAdd($scope, $rootScope, $compile, $location, $log,
if ($scope.scm_type.value) { if ($scope.scm_type.value) {
switch ($scope.scm_type.value) { switch ($scope.scm_type.value) {
case 'git': case 'git':
$scope.credentialLabel = "SCM Credential";
$scope.urlPopover = '<p>' + $scope.urlPopover = '<p>' +
i18n._('Example URLs for GIT SCM include:') + i18n._('Example URLs for GIT SCM include:') +
'</p><ul class=\"no-bullets\"><li>https://github.com/ansible/ansible.git</li>' + '</p><ul class=\"no-bullets\"><li>https://github.com/ansible/ansible.git</li>' +
@@ -424,11 +428,13 @@ export function ProjectsAdd($scope, $rootScope, $compile, $location, $log,
'SSH. GIT read only protocol (git://) does not use username or password information.'), '<strong>', '</strong>'); 'SSH. GIT read only protocol (git://) does not use username or password information.'), '<strong>', '</strong>');
break; break;
case 'svn': case 'svn':
$scope.credentialLabel = "SCM Credential";
$scope.urlPopover = '<p>' + i18n._('Example URLs for Subversion SCM include:') + '</p>' + $scope.urlPopover = '<p>' + i18n._('Example URLs for Subversion SCM include:') + '</p>' +
'<ul class=\"no-bullets\"><li>https://github.com/ansible/ansible</li><li>svn://servername.example.com/path</li>' + '<ul class=\"no-bullets\"><li>https://github.com/ansible/ansible</li><li>svn://servername.example.com/path</li>' +
'<li>svn+ssh://servername.example.com/path</li></ul>'; '<li>svn+ssh://servername.example.com/path</li></ul>';
break; break;
case 'hg': case 'hg':
$scope.credentialLabel = "SCM Credential";
$scope.urlPopover = '<p>' + i18n._('Example URLs for Mercurial SCM include:') + '</p>' + $scope.urlPopover = '<p>' + i18n._('Example URLs for Mercurial SCM include:') + '</p>' +
'<ul class=\"no-bullets\"><li>https://bitbucket.org/username/project</li><li>ssh://hg@bitbucket.org/username/project</li>' + '<ul class=\"no-bullets\"><li>https://bitbucket.org/username/project</li><li>ssh://hg@bitbucket.org/username/project</li>' +
'<li>ssh://server.example.com/path</li></ul>' + '<li>ssh://server.example.com/path</li></ul>' +
@@ -436,7 +442,14 @@ export function ProjectsAdd($scope, $rootScope, $compile, $location, $log,
'Do not put the username and key in the URL. ' + 'Do not put the username and key in the URL. ' +
'If using Bitbucket and SSH, do not supply your Bitbucket username.'), '<strong>', '</strong>'); 'If using Bitbucket and SSH, do not supply your Bitbucket username.'), '<strong>', '</strong>');
break; break;
case 'insights':
$scope.pathRequired = false;
$scope.scmRequired = false;
$scope.credRequired = true;
$scope.credentialLabel = "Credential";
break;
default: default:
$scope.credentialLabel = "SCM Credential";
$scope.urlPopover = '<p> ' + i18n._('URL popover text'); $scope.urlPopover = '<p> ' + i18n._('URL popover text');
} }
} }
@@ -455,7 +468,7 @@ ProjectsAdd.$inject = ['$scope', '$rootScope', '$compile', '$location', '$log',
export function ProjectsEdit($scope, $rootScope, $compile, $location, $log, export function ProjectsEdit($scope, $rootScope, $compile, $location, $log,
$stateParams, ProjectsForm, Rest, Alert, ProcessErrors, GenerateForm, $stateParams, ProjectsForm, Rest, Alert, ProcessErrors, GenerateForm,
Prompt, ClearScope, GetBasePath, GetProjectPath, Authorization, Prompt, ClearScope, GetBasePath, GetProjectPath, Authorization,
GetChoices, Empty, DebugForm, Wait, ProjectUpdate, $state, CreateSelect2, ToggleNotification, i18n) { GetChoices, Empty, DebugForm, Wait, ProjectUpdate, $state, CreateSelect2, ToggleNotification, i18n, OrgAdminLookup) {
ClearScope('htmlTemplate'); ClearScope('htmlTemplate');
@@ -509,6 +522,7 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log,
$scope.pathRequired = ($scope.scm_type.value === 'manual') ? true : false; $scope.pathRequired = ($scope.scm_type.value === 'manual') ? true : false;
$scope.scmRequired = ($scope.scm_type.value !== 'manual') ? true : false; $scope.scmRequired = ($scope.scm_type.value !== 'manual') ? true : false;
$scope.credRequired = ($scope.scm_type.value === 'insights') ? true : false;
$scope.scmBranchLabel = ($scope.scm_type.value === 'svn') ? 'Revision #' : 'SCM Branch'; $scope.scmBranchLabel = ($scope.scm_type.value === 'svn') ? 'Revision #' : 'SCM Branch';
Wait('stop'); Wait('stop');
@@ -586,6 +600,11 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log,
$scope.scm_type_class = "btn-disabled"; $scope.scm_type_class = "btn-disabled";
} }
OrgAdminLookup.checkForAdminAccess({organization: data.organization})
.then(function(canEditOrg){
$scope.canEditOrg = canEditOrg;
});
$scope.project_obj = data; $scope.project_obj = data;
$scope.name = data.name; $scope.name = data.name;
$scope.$emit('projectLoaded'); $scope.$emit('projectLoaded');
@@ -692,6 +711,7 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log,
if ($scope.scm_type) { if ($scope.scm_type) {
$scope.pathRequired = ($scope.scm_type.value === 'manual') ? true : false; $scope.pathRequired = ($scope.scm_type.value === 'manual') ? true : false;
$scope.scmRequired = ($scope.scm_type.value !== 'manual') ? true : false; $scope.scmRequired = ($scope.scm_type.value !== 'manual') ? true : false;
$scope.credRequired = ($scope.scm_type.value === 'insights') ? true : false;
$scope.scmBranchLabel = ($scope.scm_type.value === 'svn') ? i18n._('Revision #') : i18n._('SCM Branch'); $scope.scmBranchLabel = ($scope.scm_type.value === 'svn') ? i18n._('Revision #') : i18n._('SCM Branch');
} }
@@ -699,6 +719,7 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log,
if ($scope.scm_type.value) { if ($scope.scm_type.value) {
switch ($scope.scm_type.value) { switch ($scope.scm_type.value) {
case 'git': case 'git':
$scope.credentialLabel = "SCM Credential";
$scope.urlPopover = '<p>' + i18n._('Example URLs for GIT SCM include:') + '</p><ul class=\"no-bullets\"><li>https://github.com/ansible/ansible.git</li>' + $scope.urlPopover = '<p>' + i18n._('Example URLs for GIT SCM include:') + '</p><ul class=\"no-bullets\"><li>https://github.com/ansible/ansible.git</li>' +
'<li>git@github.com:ansible/ansible.git</li><li>git://servername.example.com/ansible.git</li></ul>' + '<li>git@github.com:ansible/ansible.git</li><li>git://servername.example.com/ansible.git</li></ul>' +
'<p>' + i18n.sprintf(i18n._('%sNote:%s When using SSH protocol for GitHub or Bitbucket, enter an SSH key only, ' + '<p>' + i18n.sprintf(i18n._('%sNote:%s When using SSH protocol for GitHub or Bitbucket, enter an SSH key only, ' +
@@ -706,11 +727,13 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log,
'SSH. GIT read only protocol (git://) does not use username or password information.'), '<strong>', '</strong>'); 'SSH. GIT read only protocol (git://) does not use username or password information.'), '<strong>', '</strong>');
break; break;
case 'svn': case 'svn':
$scope.credentialLabel = "SCM Credential";
$scope.urlPopover = '<p>' + i18n._('Example URLs for Subversion SCM include:') + '</p>' + $scope.urlPopover = '<p>' + i18n._('Example URLs for Subversion SCM include:') + '</p>' +
'<ul class=\"no-bullets\"><li>https://github.com/ansible/ansible</li><li>svn://servername.example.com/path</li>' + '<ul class=\"no-bullets\"><li>https://github.com/ansible/ansible</li><li>svn://servername.example.com/path</li>' +
'<li>svn+ssh://servername.example.com/path</li></ul>'; '<li>svn+ssh://servername.example.com/path</li></ul>';
break; break;
case 'hg': case 'hg':
$scope.credentialLabel = "SCM Credential";
$scope.urlPopover = '<p>' + i18n._('Example URLs for Mercurial SCM include:') + '</p>' + $scope.urlPopover = '<p>' + i18n._('Example URLs for Mercurial SCM include:') + '</p>' +
'<ul class=\"no-bullets\"><li>https://bitbucket.org/username/project</li><li>ssh://hg@bitbucket.org/username/project</li>' + '<ul class=\"no-bullets\"><li>https://bitbucket.org/username/project</li><li>ssh://hg@bitbucket.org/username/project</li>' +
'<li>ssh://server.example.com/path</li></ul>' + '<li>ssh://server.example.com/path</li></ul>' +
@@ -718,7 +741,14 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log,
'Do not put the username and key in the URL. ' + 'Do not put the username and key in the URL. ' +
'If using Bitbucket and SSH, do not supply your Bitbucket username.'), '<strong>', '</strong>'); 'If using Bitbucket and SSH, do not supply your Bitbucket username.'), '<strong>', '</strong>');
break; break;
case 'insights':
$scope.pathRequired = false;
$scope.scmRequired = false;
$scope.credRequired = true;
$scope.credentialLabel = "Credential";
break;
default: default:
$scope.credentialLabel = "SCM Credential";
$scope.urlPopover = '<p> ' + i18n._('URL popover text'); $scope.urlPopover = '<p> ' + i18n._('URL popover text');
} }
} }
@@ -742,4 +772,4 @@ export function ProjectsEdit($scope, $rootScope, $compile, $location, $log,
ProjectsEdit.$inject = ['$scope', '$rootScope', '$compile', '$location', '$log', ProjectsEdit.$inject = ['$scope', '$rootScope', '$compile', '$location', '$log',
'$stateParams', 'ProjectsForm', 'Rest', 'Alert', 'ProcessErrors', 'GenerateForm', '$stateParams', 'ProjectsForm', 'Rest', 'Alert', 'ProcessErrors', 'GenerateForm',
'Prompt', 'ClearScope', 'GetBasePath', 'GetProjectPath', 'Authorization', 'GetChoices', 'Empty', 'Prompt', 'ClearScope', 'GetBasePath', 'GetProjectPath', 'Authorization', 'GetChoices', 'Empty',
'DebugForm', 'Wait', 'ProjectUpdate', '$state', 'CreateSelect2', 'ToggleNotification', 'i18n']; 'DebugForm', 'Wait', 'ProjectUpdate', '$state', 'CreateSelect2', 'ToggleNotification', 'i18n', 'OrgAdminLookup'];

View File

@@ -110,6 +110,7 @@ export function TeamsAdd($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);
@@ -154,7 +155,7 @@ TeamsAdd.$inject = ['$scope', '$rootScope', '$stateParams', 'TeamForm', 'Generat
export function TeamsEdit($scope, $rootScope, $stateParams, export function TeamsEdit($scope, $rootScope, $stateParams,
TeamForm, Rest, ProcessErrors, ClearScope, GetBasePath, Wait, $state) { TeamForm, Rest, ProcessErrors, ClearScope, GetBasePath, Wait, $state, OrgAdminLookup) {
ClearScope(); ClearScope();
@@ -172,6 +173,11 @@ export function TeamsEdit($scope, $rootScope, $stateParams,
setScopeFields(data); setScopeFields(data);
$scope.organization_name = data.summary_fields.organization.name; $scope.organization_name = data.summary_fields.organization.name;
OrgAdminLookup.checkForAdminAccess({organization: data.organization})
.then(function(canEditOrg){
$scope.canEditOrg = canEditOrg;
});
$scope.team_obj = data; $scope.team_obj = data;
Wait('stop'); Wait('stop');
}); });
@@ -243,5 +249,5 @@ export function TeamsEdit($scope, $rootScope, $stateParams,
} }
TeamsEdit.$inject = ['$scope', '$rootScope', '$stateParams', 'TeamForm', 'Rest', TeamsEdit.$inject = ['$scope', '$rootScope', '$stateParams', 'TeamForm', 'Rest',
'ProcessErrors', 'ClearScope', 'GetBasePath', 'Wait', '$state' 'ProcessErrors', 'ClearScope', 'GetBasePath', 'Wait', '$state', 'OrgAdminLookup'
]; ];

View File

@@ -10,8 +10,8 @@ export default [ 'i18n', function(i18n){
name: 'hosts', name: 'hosts',
iterator: 'host', iterator: 'host',
selectTitle: i18n._('Add Existing Hosts'), selectTitle: i18n._('Add Existing Hosts'),
editTitle: i18n._('Hosts'), editTitle: i18n._('HOSTS'),
listTitle: i18n._('Hosts'), listTitle: i18n._('HOSTS'),
index: false, index: false,
hover: true, hover: true,
well: true, well: true,

View File

@@ -22,7 +22,7 @@ export default
return { return {
name: 'activity', name: 'activity',
editTitle: i18n._('Activity Detail'), editTitle: i18n._('ACTIVITY DETAIL'),
well: false, well: false,
'class': 'horizontal-narrow', 'class': 'horizontal-narrow',
formFieldSize: 'col-lg-10', formFieldSize: 'col-lg-10',

View File

@@ -15,7 +15,7 @@ export default
.factory('CredentialForm', ['i18n', function(i18n) { .factory('CredentialForm', ['i18n', function(i18n) {
return { return {
addTitle: i18n._('Create Credential'), //Legend in add mode addTitle: i18n._('CREATE CREDENTIAL'), //Legend in add mode
editTitle: '{{ name }}', //Legend in edit mode editTitle: '{{ name }}', //Legend in edit mode
name: 'credential', name: 'credential',
// the top-most node of generated state tree // the top-most node of generated state tree
@@ -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'),

View File

@@ -14,7 +14,7 @@ export default
angular.module('GroupFormDefinition', []) angular.module('GroupFormDefinition', [])
.value('GroupFormObject', { .value('GroupFormObject', {
addTitle: 'Create Group', addTitle: 'CREATE GROUP',
editTitle: '{{ name }}', editTitle: '{{ name }}',
showTitle: true, showTitle: true,
name: 'group', name: 'group',

View File

@@ -14,7 +14,7 @@ export default
angular.module('HostGroupsFormDefinition', []) angular.module('HostGroupsFormDefinition', [])
.value('HostGroupsForm', { .value('HostGroupsForm', {
editTitle: 'Host Groups', editTitle: 'HOST GROUPS',
name: 'host', name: 'host',
well: false, well: false,
formLabelSize: 'col-lg-3', formLabelSize: 'col-lg-3',

View File

@@ -15,7 +15,7 @@ export default
.factory('HostForm', ['i18n', function(i18n) { .factory('HostForm', ['i18n', function(i18n) {
return { return {
addTitle: i18n._('Create Host'), addTitle: i18n._('CREATE HOST'),
editTitle: '{{ host.name }}', editTitle: '{{ host.name }}',
name: 'host', name: 'host',
basePath: 'hosts', basePath: 'hosts',

View File

@@ -15,7 +15,7 @@ angular.module('InventoryFormDefinition', [])
.factory('InventoryForm', ['i18n', function(i18n) { .factory('InventoryForm', ['i18n', function(i18n) {
return { return {
addTitle: i18n._('New Inventory'), addTitle: i18n._('NEW INVENTORY'),
editTitle: '{{ inventory_name }}', editTitle: '{{ inventory_name }}',
name: 'inventory', name: 'inventory',
basePath: 'inventory', basePath: 'inventory',
@@ -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'),

View File

@@ -3,7 +3,7 @@
* *
* All Rights Reserved * All Rights Reserved
*************************************************/ *************************************************/
/** /**
* @ngdoc function * @ngdoc function
* @name forms.function:InventoryStatus * @name forms.function:InventoryStatus
@@ -14,7 +14,7 @@ export default
.value('InventoryStatusForm', { .value('InventoryStatusForm', {
name: 'inventory_update', name: 'inventory_update',
editTitle: 'Inventory Status', editTitle: 'INVENTORY STATUS',
well: false, well: false,
'class': 'horizontal-narrow', 'class': 'horizontal-narrow',

View File

@@ -17,7 +17,7 @@ export default
.factory('JobTemplateFormObject', ['i18n', function(i18n) { .factory('JobTemplateFormObject', ['i18n', function(i18n) {
return { return {
addTitle: i18n._('New Job Template'), addTitle: i18n._('NEW JOB TEMPLATE'),
editTitle: '{{ name }}', editTitle: '{{ name }}',
name: 'job_template', name: 'job_template',
breadcrumbName: i18n._('JOB TEMPLATE'), breadcrumbName: i18n._('JOB TEMPLATE'),
@@ -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'),

View File

@@ -15,7 +15,7 @@ export default
.factory('OrganizationFormObject', ['i18n', function(i18n) { .factory('OrganizationFormObject', ['i18n', function(i18n) {
return { return {
addTitle: i18n._('New Organization'), //Title in add mode addTitle: i18n._('NEW ORGANIZATION'), //Title in add mode
editTitle: '{{ name }}', //Title in edit mode editTitle: '{{ name }}', //Title in edit mode
name: 'organization', //entity or model name in singular form name: 'organization', //entity or model name in singular form
stateTree: 'organizations', stateTree: 'organizations',

View File

@@ -3,7 +3,7 @@
* *
* All Rights Reserved * All Rights Reserved
*************************************************/ *************************************************/
/** /**
* @ngdoc function * @ngdoc function
* @name forms.function:ProjectStatus * @name forms.function:ProjectStatus
@@ -15,7 +15,7 @@ export default
.value('ProjectStatusForm', { .value('ProjectStatusForm', {
name: 'project_update', name: 'project_update',
editTitle: 'SCM Status', editTitle: 'SCM STATUS',
well: false, well: false,
'class': 'horizontal-narrow', 'class': 'horizontal-narrow',

View File

@@ -15,7 +15,7 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
.factory('ProjectsFormObject', ['i18n', function(i18n) { .factory('ProjectsFormObject', ['i18n', function(i18n) {
return { return {
addTitle: i18n._('New Project'), addTitle: i18n._('NEW PROJECT'),
editTitle: '{{ name }}', editTitle: '{{ name }}',
name: 'project', name: 'project',
basePath: 'projects', basePath: 'projects',
@@ -50,7 +50,8 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
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'),
@@ -105,7 +106,7 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
scm_url: { scm_url: {
label: 'SCM URL', label: 'SCM URL',
type: 'text', type: 'text',
ngShow: "scm_type && scm_type.value !== 'manual'", ngShow: "scm_type && scm_type.value !== 'manual' && scm_type.value !== 'insights' ",
awRequiredWhen: { awRequiredWhen: {
reqExpression: "scmRequired", reqExpression: "scmRequired",
init: false init: false
@@ -122,12 +123,12 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
scm_branch: { scm_branch: {
labelBind: "scmBranchLabel", labelBind: "scmBranchLabel",
type: 'text', type: 'text',
ngShow: "scm_type && scm_type.value !== 'manual'", ngShow: "scm_type && scm_type.value !== 'manual' && scm_type.value !== 'insights'",
ngDisabled: '!(project_obj.summary_fields.user_capabilities.edit || canAdd)', ngDisabled: '!(project_obj.summary_fields.user_capabilities.edit || canAdd)',
subForm: 'sourceSubForm', subForm: 'sourceSubForm',
}, },
credential: { credential: {
label: i18n._('SCM Credential'), labelBind: 'credentialLabel',
type: 'lookup', type: 'lookup',
basePath: 'credentials', basePath: 'credentials',
list: 'CredentialList', list: 'CredentialList',
@@ -135,6 +136,11 @@ angular.module('ProjectFormDefinition', ['SchedulesListDefinition'])
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',

View File

@@ -15,7 +15,7 @@ export default
.factory('TeamForm', ['i18n', function(i18n) { .factory('TeamForm', ['i18n', function(i18n) {
return { return {
addTitle: i18n._('New Team'), //Legend in add mode addTitle: i18n._('NEW TEAM'), //Legend in add mode
editTitle: '{{ name }}', //Legend in edit mode editTitle: '{{ name }}', //Legend in edit mode
name: 'team', name: 'team',
// the top-most node of generated state tree // the top-most node of generated state tree
@@ -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,
} }
}, },

View File

@@ -15,7 +15,7 @@ export default
.factory('UserForm', ['i18n', function(i18n) { .factory('UserForm', ['i18n', function(i18n) {
return { return {
addTitle: i18n._('New User'), addTitle: i18n._('NEW USER'),
editTitle: '{{ username }}', editTitle: '{{ username }}',
name: 'user', name: 'user',
// the top-most node of generated state tree // the top-most node of generated state tree
@@ -38,6 +38,17 @@ export default
required: true, required: true,
capitalize: true capitalize: true
}, },
organization: {
label: i18n._('Organization'),
type: 'lookup',
list: 'OrganizationList',
basePath: 'organizations',
sourceModel: 'organization',
sourceField: 'name',
required: true,
excludeMode: 'edit',
ngDisabled: '!(user_obj.summary_fields.user_capabilities.edit || canAdd)'
},
email: { email: {
label: i18n._('Email'), label: i18n._('Email'),
type: 'email', type: 'email',
@@ -55,17 +66,6 @@ export default
autocomplete: false, autocomplete: false,
ngDisabled: '!(user_obj.summary_fields.user_capabilities.edit || canAdd)' ngDisabled: '!(user_obj.summary_fields.user_capabilities.edit || canAdd)'
}, },
organization: {
label: i18n._('Organization'),
type: 'lookup',
list: 'OrganizationList',
basePath: 'organizations',
sourceModel: 'organization',
sourceField: 'name',
required: true,
excludeMode: 'edit',
ngDisabled: '!(user_obj.summary_fields.user_capabilities.edit || canAdd)'
},
password: { password: {
label: i18n._('Password'), label: i18n._('Password'),
type: 'sensitive', type: 'sensitive',

View File

@@ -16,7 +16,7 @@ export default
.factory('WorkflowFormObject', ['i18n', function(i18n) { .factory('WorkflowFormObject', ['i18n', function(i18n) {
return { return {
addTitle: i18n._('New Workflow Job Template'), addTitle: i18n._('NEW WORKFLOW JOB TEMPLATE'),
editTitle: '{{ name }}', editTitle: '{{ name }}',
name: 'workflow_job_template', name: 'workflow_job_template',
breadcrumbName: i18n._('WORKFLOW'), breadcrumbName: i18n._('WORKFLOW'),
@@ -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'),

View File

@@ -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;

View File

@@ -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,
]; ];

View File

@@ -46,6 +46,9 @@ angular.module('inventory', [
data: { data: {
activityStream: true, activityStream: true,
activityStreamTarget: 'inventory' activityStreamTarget: 'inventory'
},
ncyBreadcrumb: {
label: N_('INVENTORIES')
} }
}); });
@@ -97,7 +100,7 @@ angular.module('inventory', [
'@': { '@': {
templateProvider: function(ScheduleList, generateList, ParentObject) { templateProvider: function(ScheduleList, generateList, ParentObject) {
// include name of parent resource in listTitle // include name of parent resource in listTitle
ScheduleList.listTitle = `${ParentObject.name}<div class='List-titleLockup'></div>` + N_('Schedules'); ScheduleList.listTitle = `${ParentObject.name}<div class='List-titleLockup'></div>` + N_('SCHEDULES');
let html = generateList.build({ let html = generateList.build({
list: ScheduleList, list: ScheduleList,
mode: 'edit' mode: 'edit'

View File

@@ -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];

View File

@@ -10,9 +10,9 @@
* @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',
well: true, well: true,
forceListeners: true, forceListeners: true,
@@ -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 />&emsp;\"somevar\": \"somevalue\",<br />&emsp;\"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: {}
}; };
} }];

View File

@@ -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>

View File

@@ -13,7 +13,7 @@
export default ['i18n', function(i18n) { export default ['i18n', function(i18n) {
return { return {
addTitle: i18n._('New Custom Inventory'), addTitle: i18n._('NEW CUSTOM INVENTORY'),
editTitle: '{{ name }}', editTitle: '{{ name }}',
name: 'inventory_script', name: 'inventory_script',
basePath: 'inventory_scripts', basePath: 'inventory_scripts',

View File

@@ -9,7 +9,7 @@
export default ['i18n', function(i18n){ export default ['i18n', function(i18n){
return { return {
name: 'inventory_scripts' , name: 'inventory_scripts' ,
listTitle: i18n._('Inventory Scripts'), listTitle: i18n._('INVENTORY SCRIPTS'),
iterator: 'inventory_script', iterator: 'inventory_script',
index: false, index: false,
hover: false, hover: false,

View File

@@ -57,7 +57,6 @@
font-size: 14px; font-size: 14px;
font-weight: bold; font-weight: bold;
margin-right: 10px; margin-right: 10px;
text-transform: uppercase;
} }
.JobDetail-panelHeaderText:hover{ .JobDetail-panelHeaderText:hover{

View File

@@ -9,12 +9,12 @@
</a> </a>
<span class="HostEvent-title">{{event.host_name}}</span> <span class="HostEvent-title">{{event.host_name}}</span>
<!-- close --> <!-- close -->
<button ui-sref="jobDetail" type="button" class="close"> <button ng-click="closeHostEvent()" type="button" class="close">
<i class="fa fa-times-circle"></i> <i class="fa fa-times-circle"></i>
</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>
@@ -64,7 +64,7 @@
<!-- controls --> <!-- controls -->
<div class="HostEvent-controls"> <div class="HostEvent-controls">
<button ui-sref="jobDetail" class="btn btn-sm btn-default HostEvent-close">Close</button> <button ng-click="closeHostEvent()" class="btn btn-sm btn-default HostEvent-close">Close</button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -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;
} }

View File

@@ -27,7 +27,8 @@
var container = document.getElementById(el); var container = document.getElementById(el);
var editor = CodeMirror.fromTextArea(container, { // jshint ignore:line var editor = CodeMirror.fromTextArea(container, { // jshint ignore:line
lineNumbers: true, lineNumbers: true,
mode: mode mode: mode,
readOnly: true
}); });
editor.setSize("100%", 200); editor.setSize("100%", 200);
editor.getDoc().setValue(data); editor.getDoc().setValue(data);
@@ -44,6 +45,12 @@
return $scope.hostResults.indexOf(result[0]); return $scope.hostResults.indexOf(result[0]);
}; };
$scope.closeHostEvent = function() {
// Unbind the listener so it doesn't fire when we close the modal via navigation
$('#HostEvent').off('hidden.bs.modal');
$state.go('jobDetail');
};
var init = function(){ var init = function(){
hostEvent.event_name = hostEvent.event; hostEvent.event_name = hostEvent.event;
$scope.event = _.cloneDeep(hostEvent); $scope.event = _.cloneDeep(hostEvent);
@@ -97,6 +104,10 @@
} }
} }
$('#HostEvent').modal('show'); $('#HostEvent').modal('show');
$('#HostEvent').on('hidden.bs.modal', function () {
$scope.closeHostEvent();
});
}; };
init(); init();
}]; }];

View File

@@ -161,6 +161,24 @@
flex-direction: column; flex-direction: column;
} }
.JobResults-panelRightTitle{
flex-wrap: wrap;
}
.JobResults-panelRightTitleText{
word-wrap: break-word;
word-break: break-all;
max-width: 100%;
}
.JobResults-badgeAndActionRow{
display:flex;
flex: 1 0 auto;
justify-content: flex-end;
flex-wrap: wrap;
max-width: 100%;
}
.StandardOut-panelHeader { .StandardOut-panelHeader {
flex: initial; flex: initial;
} }
@@ -195,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 {

View File

@@ -39,11 +39,7 @@ function(jobData, jobDataOptions, jobLabels, jobFinished, count, $scope, ParseTy
// used for tag search // used for tag search
$scope.list = { $scope.list = {
basePath: jobData.related.job_events, basePath: jobData.related.job_events,
defaultSearchParams: function(term){ name: 'job_events'
return {
or__stdout__icontains: term,
};
},
}; };
// used for tag search // used for tag search
@@ -455,13 +451,6 @@ function(jobData, jobDataOptions, jobLabels, jobFinished, count, $scope, ParseTy
var getSkeleton = function(url) { var getSkeleton = function(url) {
jobResultsService.getEvents(url) jobResultsService.getEvents(url)
.then(events => { .then(events => {
// old job check: if the job is complete, there is result stdout, and
// there are no job events, it's an old job
if ($scope.jobFinished) {
$scope.showLegacyJobErrorMessage = $scope.job.result_stdout.length &&
!events.results.length;
}
events.results.forEach(event => { events.results.forEach(event => {
if (event.start_line === 0 && event.end_line === 0) { if (event.start_line === 0 && event.end_line === 0) {
$scope.isOld++; $scope.isOld++;

View File

@@ -431,8 +431,8 @@
<div class="Panel JobResults-panelRight"> <div class="Panel JobResults-panelRight">
<!-- RIGHT PANE HEADER --> <!-- RIGHT PANE HEADER -->
<div class="StandardOut-panelHeader"> <div class="StandardOut-panelHeader JobResults-panelRightTitle">
<div class="StandardOut-panelHeaderText"> <div class="StandardOut-panelHeaderText JobResults-panelRightTitleText">
<i class="JobResults-statusResultIcon <i class="JobResults-statusResultIcon
fa icon-job-{{ job_status }}" fa icon-job-{{ job_status }}"
ng-show="stdoutFullScreen" ng-show="stdoutFullScreen"
@@ -443,76 +443,77 @@
</i> </i>
{{ job.name }} {{ job.name }}
</div> </div>
<div class="JobResults-badgeAndActionRow">
<!-- HEADER COUNTS -->
<div class="JobResults-badgeRow">
<!-- PLAYS COUNT -->
<div class="JobResults-badgeTitle">
Plays
</div>
<span class="badge List-titleBadge">
{{ playCount || 0}}
</span>
<!-- HEADER COUNTS --> <!-- TASKS COUNT -->
<div class="JobResults-badgeRow"> <div class="JobResults-badgeTitle">
<!-- PLAYS COUNT --> Tasks
<div class="JobResults-badgeTitle"> </div>
Plays <span class="badge List-titleBadge">
{{ taskCount || 0}}
</span>
<!-- HOSTS COUNT -->
<div class="JobResults-badgeTitle">
Hosts
</div>
<span class="badge List-titleBadge"
ng-if="jobFinished">
{{ hostCount || 0}}
</span>
<span class="badge List-titleBadge"
aw-tool-tip="The host count will update when the job is complete."
data-placement="top"
ng-if="!jobFinished">
<i class="fa fa-ellipsis-h"></i>
</span>
<!-- ELAPSED TIME -->
<div class="JobResults-badgeTitle">
Elapsed
</div>
<span class="badge List-titleBadge">
{{ job.elapsed * 1000 | duration: "hh:mm:ss" }}
</span>
</div> </div>
<span class="badge List-titleBadge">
{{ playCount || 0}}
</span>
<!-- TASKS COUNT --> <!-- HEADER ACTIONS -->
<div class="JobResults-badgeTitle"> <div class="StandardOut-panelHeaderActions">
Tasks
</div>
<span class="badge List-titleBadge">
{{ taskCount || 0}}
</span>
<!-- HOSTS COUNT --> <!-- FULL-SCREEN TOGGLE ACTION -->
<div class="JobResults-badgeTitle">
Hosts
</div>
<span class="badge List-titleBadge"
ng-if="jobFinished">
{{ hostCount || 0}}
</span>
<span class="badge List-titleBadge"
aw-tool-tip="The host count will update when the job is complete."
data-placement="top"
ng-if="!jobFinished">
<i class="fa fa-ellipsis-h"></i>
</span>
<!-- ELAPSED TIME -->
<div class="JobResults-badgeTitle">
Elapsed
</div>
<span class="badge List-titleBadge">
{{ job.elapsed * 1000 | duration: "hh:mm:ss" }}
</span>
</div>
<!-- HEADER ACTIONS -->
<div class="StandardOut-panelHeaderActions">
<!-- FULL-SCREEN TOGGLE ACTION -->
<button class="StandardOut-actionButton"
aw-tool-tip="{{ toggleStdoutFullscreenTooltip }}"
data-tip-watch="toggleStdoutFullscreenTooltip"
data-placement="top"
ng-class="{'StandardOut-actionButton--active': stdoutFullScreen}"
ng-click="toggleStdoutFullscreen()">
<i class="fa fa-arrows-alt"></i>
</button>
<!-- DOWNLOAD ACTION -->
<a ng-show="job.status === 'failed' ||
job.status === 'successful' ||
job.status === 'canceled'"
href="/api/v1/jobs/{{ job.id }}/stdout?format=txt_download">
<button class="StandardOut-actionButton" <button class="StandardOut-actionButton"
aw-tool-tip="{{ standardOutTooltip }}" aw-tool-tip="{{ toggleStdoutFullscreenTooltip }}"
data-tip-watch="standardOutTooltip" data-tip-watch="toggleStdoutFullscreenTooltip"
data-placement="top"> data-placement="top"
<i class="fa fa-download"></i> ng-class="{'StandardOut-actionButton--active': stdoutFullScreen}"
ng-click="toggleStdoutFullscreen()">
<i class="fa fa-arrows-alt"></i>
</button> </button>
</a>
<!-- DOWNLOAD ACTION -->
<a ng-show="job.status === 'failed' ||
job.status === 'successful' ||
job.status === 'canceled'"
href="/api/v1/jobs/{{ job.id }}/stdout?format=txt_download">
<button class="StandardOut-actionButton"
aw-tool-tip="{{ standardOutTooltip }}"
data-tip-watch="standardOutTooltip"
data-placement="top">
<i class="fa fa-download"></i>
</button>
</a>
</div>
</div> </div>
</div> </div>
<host-status-bar></host-status-bar> <host-status-bar></host-status-bar>

View File

@@ -23,19 +23,20 @@
.JobSubmission-header { .JobSubmission-header {
display: flex; display: flex;
flex: 0 0 auto; flex: 0 0 auto;
align-items: center;
} }
.JobSubmission-title { .JobSubmission-title {
align-items: center; align-items: center;
flex: 1 0 auto; flex: 1 0 auto;
display: flex; display: flex;
word-wrap: break-word;
word-break: break-all;
max-width: 98%;
} }
.JobSubmission-titleText { .JobSubmission-titleText {
color: @list-title-txt; color: @list-title-txt;
font-size: 14px; font-size: 14px;
font-weight: bold; font-weight: bold;
margin-right: 10px; margin-right: 10px;
text-transform: uppercase;
} }
.JobSubmission-titleLockup { .JobSubmission-titleLockup {
margin-left: 4px; margin-left: 4px;
@@ -174,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;

View File

@@ -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;
}
});
} }
}; };

View File

@@ -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>

View File

@@ -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()));
} }
}; };
}]; }];

View File

@@ -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()));
} }
}; };
}]; }];

View File

@@ -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() {
@@ -29,7 +29,7 @@ export default
msg: 'Call to '+ defaultUrl + ' failed. Return status: '+ status}); msg: 'Call to '+ defaultUrl + ' failed. Return status: '+ status});
}); });
}, },
valid: function(license) { valid: function(license) {
if (!license.valid_key){ if (!license.valid_key){
return false; return false;

View File

@@ -13,7 +13,7 @@ export default
name: 'jobs', name: 'jobs',
basePath: 'unified_jobs', basePath: 'unified_jobs',
iterator: 'job', iterator: 'job',
editTitle: i18n._('All Jobs'), editTitle: i18n._('ALL JOBS'),
index: false, index: false,
hover: true, hover: true,
well: false, well: false,

View File

@@ -14,7 +14,7 @@ export default
name: 'cloudcredentials', name: 'cloudcredentials',
iterator: 'cloudcredential', iterator: 'cloudcredential',
selectTitle: 'Add Cloud Credentials', selectTitle: 'Add Cloud Credentials',
editTitle: 'Cloud Credentials', editTitle: 'CLOUD CREDENTIALS',
selectInstructions: '<p>Select existing credentials by clicking each credential or checking the related checkbox. When finished, click the blue ' + selectInstructions: '<p>Select existing credentials by clicking each credential or checking the related checkbox. When finished, click the blue ' +
'<em>Select</em> button, located bottom right.</p> <p>Create a brand new credential by clicking the <i class=\"fa fa-plus"></i> button.</p>', '<em>Select</em> button, located bottom right.</p> <p>Create a brand new credential by clicking the <i class=\"fa fa-plus"></i> button.</p>',
index: false, index: false,

View File

@@ -15,7 +15,7 @@ export default
name: 'completed_jobs', name: 'completed_jobs',
basePath: 'api/v1/job_templates/{{$stateParams.job_template_id}}/jobs/?or__status=successful&or__status=failed&or__status=error&or__status=canceled', basePath: 'api/v1/job_templates/{{$stateParams.job_template_id}}/jobs/?or__status=successful&or__status=failed&or__status=error&or__status=canceled',
iterator: 'completed_job', iterator: 'completed_job',
editTitle: i18n._('Completed Jobs'), editTitle: i18n._('COMPLETED JOBS'),
index: false, index: false,
hover: true, hover: true,
well: false, well: false,

View File

@@ -15,8 +15,8 @@ export default
name: 'credentials', name: 'credentials',
iterator: 'credential', iterator: 'credential',
selectTitle: i18n._('Add Credentials'), selectTitle: i18n._('Add Credentials'),
editTitle: i18n._('Credentials'), editTitle: i18n._('CREDENTIALS'),
listTitle: i18n._('Credentials'), listTitle: i18n._('CREDENTIALS'),
selectInstructions: "<p>Select existing credentials by clicking each credential or checking the related checkbox. When " + selectInstructions: "<p>Select existing credentials by clicking each credential or checking the related checkbox. When " +
"finished, click the blue <em>Select</em> button, located bottom right.</p> <p>Create a brand new credential by clicking ", "finished, click the blue <em>Select</em> button, located bottom right.</p> <p>Create a brand new credential by clicking ",
index: false, index: false,

View File

@@ -13,8 +13,8 @@ export default
name: 'inventories', name: 'inventories',
iterator: 'inventory', iterator: 'inventory',
selectTitle: i18n._('Add Inventories'), selectTitle: i18n._('Add Inventories'),
editTitle: i18n._('Inventories'), editTitle: i18n._('INVENTORIES'),
listTitle: i18n._('Inventories'), listTitle: i18n._('INVENTORIES'),
selectInstructions: i18n.sprintf(i18n._("Click on a row to select it, and click Finished when done. Click the %s button to create a new inventory."), "<i class=\"icon-plus\"></i> "), selectInstructions: i18n.sprintf(i18n._("Click on a row to select it, and click Finished when done. Click the %s button to create a new inventory."), "<i class=\"icon-plus\"></i> "),
index: false, index: false,
hover: true, hover: true,

View File

@@ -11,7 +11,7 @@ export default
name: 'groups', name: 'groups',
iterator: 'group', iterator: 'group',
editTitle: '{{ inventory.name }}', editTitle: '{{ inventory.name }}',
listTitle: 'Groups', listTitle: 'GROUPS',
searchSize: 'col-lg-12 col-md-12 col-sm-12 col-xs-12', searchSize: 'col-lg-12 col-md-12 col-sm-12 col-xs-12',
showTitle: false, showTitle: false,
well: true, well: true,

View File

@@ -11,7 +11,7 @@ export default
name: 'hosts', name: 'hosts',
iterator: 'host', iterator: 'host',
editTitle: '{{ selected_group }}', editTitle: '{{ selected_group }}',
listTitle: 'Hosts', listTitle: 'HOSTS',
searchSize: 'col-lg-12 col-md-12 col-sm-12 col-xs-12', searchSize: 'col-lg-12 col-md-12 col-sm-12 col-xs-12',
showTitle: false, showTitle: false,
well: true, well: true,

View File

@@ -12,7 +12,7 @@ export default
name: 'workflow_inventory_sources', name: 'workflow_inventory_sources',
iterator: 'inventory_source', iterator: 'inventory_source',
basePath: 'inventory_sources', basePath: 'inventory_sources',
listTitle: 'Inventory Sources', listTitle: 'INVENTORY SOURCES',
index: false, index: false,
hover: true, hover: true,

View File

@@ -12,7 +12,7 @@ export default
name: 'jobevents', name: 'jobevents',
iterator: 'jobevent', iterator: 'jobevent',
editTitle: i18n._('Job Events'), editTitle: i18n._('JOB EVENTS'),
index: false, index: false,
hover: true, hover: true,
"class": "condensed", "class": "condensed",

View File

@@ -11,7 +11,7 @@ export default
name: 'jobhosts', name: 'jobhosts',
iterator: 'jobhost', iterator: 'jobhost',
editTitle: 'All summaries', editTitle: 'ALL SUMMARIES',
"class": "table-condensed", "class": "table-condensed",
index: false, index: false,
hover: true, hover: true,

View File

@@ -11,7 +11,7 @@ export default
name: 'jobs', name: 'jobs',
iterator: 'job', iterator: 'job',
editTitle: 'Jobs', editTitle: 'JOBS',
'class': 'table-condensed', 'class': 'table-condensed',
index: false, index: false,
hover: true, hover: true,

View File

@@ -15,7 +15,7 @@ export default
selectInstructions: '<p>Select existing organizations by clicking each organization or checking the related checkbox. When finished, ' + selectInstructions: '<p>Select existing organizations by clicking each organization or checking the related checkbox. When finished, ' +
'click the blue <em>Select</em> button, located bottom right.</p><p>Create a new organization by clicking the ' + 'click the blue <em>Select</em> button, located bottom right.</p><p>Create a new organization by clicking the ' +
'<i class=\"fa fa-plus\"></i> button.</p>', '<i class=\"fa fa-plus\"></i> button.</p>',
editTitle: 'Organizations', editTitle: 'ORGANIZATIONS',
hover: true, hover: true,
index: false, index: false,

Some files were not shown because too many files have changed in this diff Show More